##// END OF EJS Templates
Implemented ini settings parser
neko259 -
r1153:bbf2916c default
parent child Browse files
Show More
@@ -0,0 +1,33 b''
1 [Version]
2 Version = 2.7.0 Chani
3 SiteName = Neboard
4
5 [Cache]
6 # Timeout for caching, if cache is used
7 CacheTimeout = 600
8
9 [Forms]
10 # Max post length in characters
11 MaxTextLength = 30000
12 MaxImageSize = 8000000
13 LimitPostingSpeed = false
14
15 [Messages]
16 # Thread bumplimit
17 MaxPostsPerThread = 10
18 # Old posts will be archived or deleted if this value is reached
19 MaxThreadCount = 5
20
21 [View]
22 DefaultTheme = md
23 DefaultImageViewer = simple
24 LastRepliesCount = 3
25 ThreadsPerPage = 3
26
27 [Storage]
28 # Enable archiving threads instead of deletion when the thread limit is reached
29 ArchiveThreads = true
30
31 [External]
32 # Thread update
33 WebsocketsEnabled = false
@@ -1,62 +1,63 b''
1 1 from boards.abstracts.settingsmanager import get_settings_manager, \
2 2 SETTING_USERNAME, SETTING_LAST_NOTIFICATION_ID, SETTING_IMAGE_VIEWER
3 3 from boards.models.user import Notification
4 4
5 5 __author__ = 'neko259'
6 6
7 7 from boards import settings, utils
8 8 from boards.models import Post, Tag
9 9
10 10 CONTEXT_SITE_NAME = 'site_name'
11 11 CONTEXT_VERSION = 'version'
12 12 CONTEXT_MODERATOR = 'moderator'
13 13 CONTEXT_THEME_CSS = 'theme_css'
14 14 CONTEXT_THEME = 'theme'
15 15 CONTEXT_PPD = 'posts_per_day'
16 16 CONTEXT_TAGS = 'tags'
17 17 CONTEXT_USER = 'user'
18 18 CONTEXT_NEW_NOTIFICATIONS_COUNT = 'new_notifications_count'
19 19 CONTEXT_USERNAME = 'username'
20 20 CONTEXT_TAGS_STR = 'tags_str'
21 21 CONTEXT_IMAGE_VIEWER = 'image_viewer'
22 22
23 23
24 24 def get_notifications(context, request):
25 25 settings_manager = get_settings_manager(request)
26 26 username = settings_manager.get_setting(SETTING_USERNAME)
27 27 new_notifications_count = 0
28 28 if username is not None and len(username) > 0:
29 29 last_notification_id = settings_manager.get_setting(
30 30 SETTING_LAST_NOTIFICATION_ID)
31 31
32 32 new_notifications_count = Notification.objects.get_notification_posts(
33 33 username=username, last=last_notification_id).count()
34 34 context[CONTEXT_NEW_NOTIFICATIONS_COUNT] = new_notifications_count
35 35 context[CONTEXT_USERNAME] = username
36 36
37 37
38 38 def user_and_ui_processor(request):
39 39 context = dict()
40 40
41 41 context[CONTEXT_PPD] = float(Post.objects.get_posts_per_day())
42 42
43 43 settings_manager = get_settings_manager(request)
44 44 fav_tags = settings_manager.get_fav_tags()
45 45 context[CONTEXT_TAGS] = fav_tags
46 46 context[CONTEXT_TAGS_STR] = Tag.objects.get_tag_url_list(fav_tags)
47 47 theme = settings_manager.get_theme()
48 48 context[CONTEXT_THEME] = theme
49 49 context[CONTEXT_THEME_CSS] = 'css/' + theme + '/base_page.css'
50 50
51 51 # This shows the moderator panel
52 52 context[CONTEXT_MODERATOR] = utils.is_moderator(request)
53 53
54 context[CONTEXT_VERSION] = settings.VERSION
55 context[CONTEXT_SITE_NAME] = settings.SITE_NAME
54 context[CONTEXT_VERSION] = settings.get('Version', 'Version')
55 context[CONTEXT_SITE_NAME] = settings.get('Version', 'SiteName')
56 56
57 57 context[CONTEXT_IMAGE_VIEWER] = settings_manager.get_setting(
58 SETTING_IMAGE_VIEWER, default=settings.DEFAULT_IMAGE_VIEWER)
58 SETTING_IMAGE_VIEWER,
59 default=settings.get('View', 'DefaultImageViewer'))
59 60
60 61 get_notifications(context, request)
61 62
62 63 return context
@@ -1,384 +1,384 b''
1 1 import re
2 2 import time
3 3 import pytz
4 4
5 5 from django import forms
6 6 from django.core.files.uploadedfile import SimpleUploadedFile
7 7 from django.core.exceptions import ObjectDoesNotExist
8 8 from django.forms.util import ErrorList
9 9 from django.utils.translation import ugettext_lazy as _
10 10 import requests
11 11
12 12 from boards.mdx_neboard import formatters
13 13 from boards.models.post import TITLE_MAX_LENGTH
14 14 from boards.models import Tag, Post
15 15 from neboard import settings
16 16 import boards.settings as board_settings
17 17
18 18
19 19 CONTENT_TYPE_IMAGE = (
20 20 'image/jpeg',
21 21 'image/png',
22 22 'image/gif',
23 23 'image/bmp',
24 24 )
25 25
26 26 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
27 27
28 28 VETERAN_POSTING_DELAY = 5
29 29
30 30 ATTRIBUTE_PLACEHOLDER = 'placeholder'
31 31 ATTRIBUTE_ROWS = 'rows'
32 32
33 33 LAST_POST_TIME = 'last_post_time'
34 34 LAST_LOGIN_TIME = 'last_login_time'
35 35 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
36 36 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
37 37
38 38 LABEL_TITLE = _('Title')
39 39 LABEL_TEXT = _('Text')
40 40 LABEL_TAG = _('Tag')
41 41 LABEL_SEARCH = _('Search')
42 42
43 43 ERROR_SPEED = _('Please wait %s seconds before sending message')
44 44
45 45 TAG_MAX_LENGTH = 20
46 46
47 47 IMAGE_DOWNLOAD_CHUNK_BYTES = 100000
48 48
49 49 HTTP_RESULT_OK = 200
50 50
51 51 TEXTAREA_ROWS = 4
52 52
53 53
54 54 def get_timezones():
55 55 timezones = []
56 56 for tz in pytz.common_timezones:
57 57 timezones.append((tz, tz),)
58 58 return timezones
59 59
60 60
61 61 class FormatPanel(forms.Textarea):
62 62 """
63 63 Panel for text formatting. Consists of buttons to add different tags to the
64 64 form text area.
65 65 """
66 66
67 67 def render(self, name, value, attrs=None):
68 68 output = '<div id="mark-panel">'
69 69 for formatter in formatters:
70 70 output += '<span class="mark_btn"' + \
71 71 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
72 72 '\', \'' + formatter.format_right + '\')">' + \
73 73 formatter.preview_left + formatter.name + \
74 74 formatter.preview_right + '</span>'
75 75
76 76 output += '</div>'
77 77 output += super(FormatPanel, self).render(name, value, attrs=None)
78 78
79 79 return output
80 80
81 81
82 82 class PlainErrorList(ErrorList):
83 83 def __unicode__(self):
84 84 return self.as_text()
85 85
86 86 def as_text(self):
87 87 return ''.join(['(!) %s ' % e for e in self])
88 88
89 89
90 90 class NeboardForm(forms.Form):
91 91 """
92 92 Form with neboard-specific formatting.
93 93 """
94 94
95 95 def as_div(self):
96 96 """
97 97 Returns this form rendered as HTML <as_div>s.
98 98 """
99 99
100 100 return self._html_output(
101 101 # TODO Do not show hidden rows in the list here
102 102 normal_row='<div class="form-row">'
103 103 '<div class="form-label">'
104 104 '%(label)s'
105 105 '</div>'
106 106 '<div class="form-input">'
107 107 '%(field)s'
108 108 '</div>'
109 109 '</div>'
110 110 '<div class="form-row">'
111 111 '%(help_text)s'
112 112 '</div>',
113 113 error_row='<div class="form-row">'
114 114 '<div class="form-label"></div>'
115 115 '<div class="form-errors">%s</div>'
116 116 '</div>',
117 117 row_ender='</div>',
118 118 help_text_html='%s',
119 119 errors_on_separate_row=True)
120 120
121 121 def as_json_errors(self):
122 122 errors = []
123 123
124 124 for name, field in list(self.fields.items()):
125 125 if self[name].errors:
126 126 errors.append({
127 127 'field': name,
128 128 'errors': self[name].errors.as_text(),
129 129 })
130 130
131 131 return errors
132 132
133 133
134 134 class PostForm(NeboardForm):
135 135
136 136 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
137 137 label=LABEL_TITLE)
138 138 text = forms.CharField(
139 139 widget=FormatPanel(attrs={
140 140 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
141 141 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
142 142 }),
143 143 required=False, label=LABEL_TEXT)
144 144 image = forms.ImageField(required=False, label=_('Image'),
145 145 widget=forms.ClearableFileInput(
146 146 attrs={'accept': 'image/*'}))
147 147 image_url = forms.CharField(required=False, label=_('Image URL'),
148 148 widget=forms.TextInput(
149 149 attrs={ATTRIBUTE_PLACEHOLDER:
150 150 'http://example.com/image.png'}))
151 151
152 152 # This field is for spam prevention only
153 153 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
154 154 widget=forms.TextInput(attrs={
155 155 'class': 'form-email'}))
156 156 threads = forms.CharField(required=False, label=_('Additional threads'),
157 157 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
158 158 '123 456 789'}))
159 159
160 160 session = None
161 161 need_to_ban = False
162 162
163 163 def clean_title(self):
164 164 title = self.cleaned_data['title']
165 165 if title:
166 166 if len(title) > TITLE_MAX_LENGTH:
167 167 raise forms.ValidationError(_('Title must have less than %s '
168 168 'characters') %
169 169 str(TITLE_MAX_LENGTH))
170 170 return title
171 171
172 172 def clean_text(self):
173 173 text = self.cleaned_data['text'].strip()
174 174 if text:
175 if len(text) > board_settings.MAX_TEXT_LENGTH:
175 max_length = board_settings.get_int('Forms', 'MaxTextLength')
176 if len(text) > max_length:
176 177 raise forms.ValidationError(_('Text must have less than %s '
177 'characters') %
178 str(board_settings
179 .MAX_TEXT_LENGTH))
178 'characters') % str(max_length))
180 179 return text
181 180
182 181 def clean_image(self):
183 182 image = self.cleaned_data['image']
184 183
185 184 if image:
186 185 self.validate_image_size(image.size)
187 186
188 187 return image
189 188
190 189 def clean_image_url(self):
191 190 url = self.cleaned_data['image_url']
192 191
193 192 image = None
194 193 if url:
195 194 image = self._get_image_from_url(url)
196 195
197 196 if not image:
198 197 raise forms.ValidationError(_('Invalid URL'))
199 198 else:
200 199 self.validate_image_size(image.size)
201 200
202 201 return image
203 202
204 203 def clean_threads(self):
205 204 threads_str = self.cleaned_data['threads']
206 205
207 206 if len(threads_str) > 0:
208 207 threads_id_list = threads_str.split(' ')
209 208
210 209 threads = list()
211 210
212 211 for thread_id in threads_id_list:
213 212 try:
214 213 thread = Post.objects.get(id=int(thread_id))
215 214 if not thread.is_opening() or thread.get_thread().archived:
216 215 raise ObjectDoesNotExist()
217 216 threads.append(thread)
218 217 except (ObjectDoesNotExist, ValueError):
219 218 raise forms.ValidationError(_('Invalid additional thread list'))
220 219
221 220 return threads
222 221
223 222 def clean(self):
224 223 cleaned_data = super(PostForm, self).clean()
225 224
226 225 if cleaned_data['email']:
227 226 self.need_to_ban = True
228 227 raise forms.ValidationError('A human cannot enter a hidden field')
229 228
230 229 if not self.errors:
231 230 self._clean_text_image()
232 231
233 232 if not self.errors and self.session:
234 233 self._validate_posting_speed()
235 234
236 235 return cleaned_data
237 236
238 237 def get_image(self):
239 238 """
240 239 Gets image from file or URL.
241 240 """
242 241
243 242 image = self.cleaned_data['image']
244 243 return image if image else self.cleaned_data['image_url']
245 244
246 245 def _clean_text_image(self):
247 246 text = self.cleaned_data.get('text')
248 247 image = self.get_image()
249 248
250 249 if (not text) and (not image):
251 250 error_message = _('Either text or image must be entered.')
252 251 self._errors['text'] = self.error_class([error_message])
253 252
254 253 def _validate_posting_speed(self):
255 254 can_post = True
256 255
257 256 posting_delay = settings.POSTING_DELAY
258 257
259 if board_settings.LIMIT_POSTING_SPEED:
258 if board_settings.get_bool('Forms', 'LimitPostingSpeed'):
260 259 now = time.time()
261 260
262 261 current_delay = 0
263 262 need_delay = False
264 263
265 264 if not LAST_POST_TIME in self.session:
266 265 self.session[LAST_POST_TIME] = now
267 266
268 267 need_delay = True
269 268 else:
270 269 last_post_time = self.session.get(LAST_POST_TIME)
271 270 current_delay = int(now - last_post_time)
272 271
273 272 need_delay = current_delay < posting_delay
274 273
275 274 if need_delay:
276 275 error_message = ERROR_SPEED % str(posting_delay
277 276 - current_delay)
278 277 self._errors['text'] = self.error_class([error_message])
279 278
280 279 can_post = False
281 280
282 281 if can_post:
283 282 self.session[LAST_POST_TIME] = now
284 283
285 284 def validate_image_size(self, size: int):
286 if size > board_settings.MAX_IMAGE_SIZE:
285 max_size = board_settings.get_int('Forms', 'MaxImageSize')
286 if size > max_size:
287 287 raise forms.ValidationError(
288 288 _('Image must be less than %s bytes')
289 % str(board_settings.MAX_IMAGE_SIZE))
289 % str(max_size))
290 290
291 291 def _get_image_from_url(self, url: str) -> SimpleUploadedFile:
292 292 """
293 293 Gets an image file from URL.
294 294 """
295 295
296 296 img_temp = None
297 297
298 298 try:
299 299 # Verify content headers
300 300 response_head = requests.head(url, verify=False)
301 301 content_type = response_head.headers['content-type'].split(';')[0]
302 302 if content_type in CONTENT_TYPE_IMAGE:
303 303 length_header = response_head.headers.get('content-length')
304 304 if length_header:
305 305 length = int(length_header)
306 306 self.validate_image_size(length)
307 307 # Get the actual content into memory
308 308 response = requests.get(url, verify=False, stream=True)
309 309
310 310 # Download image, stop if the size exceeds limit
311 311 size = 0
312 312 content = b''
313 313 for chunk in response.iter_content(IMAGE_DOWNLOAD_CHUNK_BYTES):
314 314 size += len(chunk)
315 315 self.validate_image_size(size)
316 316 content += chunk
317 317
318 318 if response.status_code == HTTP_RESULT_OK and content:
319 319 # Set a dummy file name that will be replaced
320 320 # anyway, just keep the valid extension
321 321 filename = 'image.' + content_type.split('/')[1]
322 322 img_temp = SimpleUploadedFile(filename, content,
323 323 content_type)
324 324 except Exception:
325 325 # Just return no image
326 326 pass
327 327
328 328 return img_temp
329 329
330 330
331 331 class ThreadForm(PostForm):
332 332
333 333 tags = forms.CharField(
334 334 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
335 335 max_length=100, label=_('Tags'), required=True)
336 336
337 337 def clean_tags(self):
338 338 tags = self.cleaned_data['tags'].strip()
339 339
340 340 if not tags or not REGEX_TAGS.match(tags):
341 341 raise forms.ValidationError(
342 342 _('Inappropriate characters in tags.'))
343 343
344 344 required_tag_exists = False
345 345 for tag in tags.split():
346 346 try:
347 347 Tag.objects.get(name=tag.strip().lower(), required=True)
348 348 required_tag_exists = True
349 349 break
350 350 except ObjectDoesNotExist:
351 351 pass
352 352
353 353 if not required_tag_exists:
354 354 all_tags = Tag.objects.filter(required=True)
355 355 raise forms.ValidationError(
356 356 _('Need at least one of the tags: ')
357 357 + ', '.join([tag.name for tag in all_tags]))
358 358
359 359 return tags
360 360
361 361 def clean(self):
362 362 cleaned_data = super(ThreadForm, self).clean()
363 363
364 364 return cleaned_data
365 365
366 366
367 367 class SettingsForm(NeboardForm):
368 368
369 369 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
370 370 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('Image view mode'))
371 371 username = forms.CharField(label=_('User name'), required=False)
372 372 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
373 373
374 374 def clean_username(self):
375 375 username = self.cleaned_data['username']
376 376
377 377 if username and not REGEX_TAGS.match(username):
378 378 raise forms.ValidationError(_('Inappropriate characters.'))
379 379
380 380 return username
381 381
382 382
383 383 class SearchForm(NeboardForm):
384 384 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
@@ -1,438 +1,438 b''
1 1 from datetime import datetime, timedelta, date
2 2 from datetime import time as dtime
3 3 import logging
4 4 import re
5 5 import uuid
6 6
7 7 from django.core.exceptions import ObjectDoesNotExist
8 8 from django.core.urlresolvers import reverse
9 9 from django.db import models, transaction
10 10 from django.db.models import TextField
11 11 from django.template.loader import render_to_string
12 12 from django.utils import timezone
13 13
14 14 from boards import settings
15 15 from boards.mdx_neboard import Parser
16 16 from boards.models import PostImage
17 17 from boards.models.base import Viewable
18 18 from boards import utils
19 19 from boards.models.user import Notification, Ban
20 20 import boards.models.thread
21 21
22 22
23 23 APP_LABEL_BOARDS = 'boards'
24 24
25 25 POSTS_PER_DAY_RANGE = 7
26 26
27 27 BAN_REASON_AUTO = 'Auto'
28 28
29 29 IMAGE_THUMB_SIZE = (200, 150)
30 30
31 31 TITLE_MAX_LENGTH = 200
32 32
33 33 # TODO This should be removed
34 34 NO_IP = '0.0.0.0'
35 35
36 36 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
37 37 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
38 38
39 39 PARAMETER_TRUNCATED = 'truncated'
40 40 PARAMETER_TAG = 'tag'
41 41 PARAMETER_OFFSET = 'offset'
42 42 PARAMETER_DIFF_TYPE = 'type'
43 43 PARAMETER_CSS_CLASS = 'css_class'
44 44 PARAMETER_THREAD = 'thread'
45 45 PARAMETER_IS_OPENING = 'is_opening'
46 46 PARAMETER_MODERATOR = 'moderator'
47 47 PARAMETER_POST = 'post'
48 48 PARAMETER_OP_ID = 'opening_post_id'
49 49 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
50 50 PARAMETER_REPLY_LINK = 'reply_link'
51 51
52 52 DIFF_TYPE_HTML = 'html'
53 53 DIFF_TYPE_JSON = 'json'
54 54
55 55 REFMAP_STR = '<a href="{}">&gt;&gt;{}</a>'
56 56
57 57
58 58 class PostManager(models.Manager):
59 59 @transaction.atomic
60 60 def create_post(self, title: str, text: str, image=None, thread=None,
61 61 ip=NO_IP, tags: list=None, threads: list=None):
62 62 """
63 63 Creates new post
64 64 """
65 65
66 66 is_banned = Ban.objects.filter(ip=ip).exists()
67 67
68 68 # TODO Raise specific exception and catch it in the views
69 69 if is_banned:
70 70 raise Exception("This user is banned")
71 71
72 72 if not tags:
73 73 tags = []
74 74 if not threads:
75 75 threads = []
76 76
77 77 posting_time = timezone.now()
78 78 if not thread:
79 79 thread = boards.models.thread.Thread.objects.create(
80 80 bump_time=posting_time, last_edit_time=posting_time)
81 81 new_thread = True
82 82 else:
83 83 new_thread = False
84 84
85 85 pre_text = Parser().preparse(text)
86 86
87 87 post = self.create(title=title,
88 88 text=pre_text,
89 89 pub_time=posting_time,
90 90 poster_ip=ip,
91 91 thread=thread,
92 92 last_edit_time=posting_time)
93 93 post.threads.add(thread)
94 94
95 95 logger = logging.getLogger('boards.post.create')
96 96
97 97 logger.info('Created post {} by {}'.format(post, post.poster_ip))
98 98
99 99 if image:
100 100 post.images.add(PostImage.objects.create_with_hash(image))
101 101
102 102 list(map(thread.add_tag, tags))
103 103
104 104 if new_thread:
105 105 boards.models.thread.Thread.objects.process_oldest_threads()
106 106 else:
107 107 thread.last_edit_time = posting_time
108 108 thread.bump()
109 109 thread.save()
110 110
111 111 post.connect_replies()
112 112 post.connect_threads(threads)
113 113 post.connect_notifications()
114 114
115 115 post.build_url()
116 116
117 117 return post
118 118
119 119 def delete_posts_by_ip(self, ip):
120 120 """
121 121 Deletes all posts of the author with same IP
122 122 """
123 123
124 124 posts = self.filter(poster_ip=ip)
125 125 for post in posts:
126 126 post.delete()
127 127
128 128 @utils.cached_result()
129 129 def get_posts_per_day(self) -> float:
130 130 """
131 131 Gets average count of posts per day for the last 7 days
132 132 """
133 133
134 134 day_end = date.today()
135 135 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
136 136
137 137 day_time_start = timezone.make_aware(datetime.combine(
138 138 day_start, dtime()), timezone.get_current_timezone())
139 139 day_time_end = timezone.make_aware(datetime.combine(
140 140 day_end, dtime()), timezone.get_current_timezone())
141 141
142 142 posts_per_period = float(self.filter(
143 143 pub_time__lte=day_time_end,
144 144 pub_time__gte=day_time_start).count())
145 145
146 146 ppd = posts_per_period / POSTS_PER_DAY_RANGE
147 147
148 148 return ppd
149 149
150 150
151 151 class Post(models.Model, Viewable):
152 152 """A post is a message."""
153 153
154 154 objects = PostManager()
155 155
156 156 class Meta:
157 157 app_label = APP_LABEL_BOARDS
158 158 ordering = ('id',)
159 159
160 160 title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
161 161 pub_time = models.DateTimeField()
162 162 text = TextField(blank=True, null=True)
163 163 _text_rendered = TextField(blank=True, null=True, editable=False)
164 164
165 165 images = models.ManyToManyField(PostImage, null=True, blank=True,
166 166 related_name='ip+', db_index=True)
167 167
168 168 poster_ip = models.GenericIPAddressField()
169 169
170 170 # TODO This field can be removed cause UID is used for update now
171 171 last_edit_time = models.DateTimeField()
172 172
173 173 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
174 174 null=True,
175 175 blank=True, related_name='rfp+',
176 176 db_index=True)
177 177 refmap = models.TextField(null=True, blank=True)
178 178 threads = models.ManyToManyField('Thread', db_index=True)
179 179 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
180 180
181 181 url = models.TextField()
182 182 uid = models.TextField(db_index=True)
183 183
184 184 def __str__(self):
185 185 return 'P#{}/{}'.format(self.id, self.title)
186 186
187 187 def get_title(self) -> str:
188 188 """
189 189 Gets original post title or part of its text.
190 190 """
191 191
192 192 title = self.title
193 193 if not title:
194 194 title = self.get_text()
195 195
196 196 return title
197 197
198 198 def build_refmap(self) -> None:
199 199 """
200 200 Builds a replies map string from replies list. This is a cache to stop
201 201 the server from recalculating the map on every post show.
202 202 """
203 203
204 204 post_urls = [REFMAP_STR.format(refpost.get_url(), refpost.id)
205 205 for refpost in self.referenced_posts.all()]
206 206
207 207 self.refmap = ', '.join(post_urls)
208 208
209 209 def is_referenced(self) -> bool:
210 210 return self.refmap and len(self.refmap) > 0
211 211
212 212 def is_opening(self) -> bool:
213 213 """
214 214 Checks if this is an opening post or just a reply.
215 215 """
216 216
217 217 return self.get_thread().get_opening_post_id() == self.id
218 218
219 219 # TODO Remove this and use get_absolute_url method
220 220 def get_url(self):
221 221 return self.url
222 222
223 223 def get_absolute_url(self):
224 224 return self.url
225 225
226 226 def get_thread(self):
227 227 return self.thread
228 228
229 229 def get_threads(self) -> list:
230 230 """
231 231 Gets post's thread.
232 232 """
233 233
234 234 return self.threads
235 235
236 236 def get_view(self, moderator=False, need_open_link=False,
237 237 truncated=False, reply_link=False, *args, **kwargs) -> str:
238 238 """
239 239 Renders post's HTML view. Some of the post params can be passed over
240 240 kwargs for the means of caching (if we view the thread, some params
241 241 are same for every post and don't need to be computed over and over.
242 242 """
243 243
244 244 thread = self.get_thread()
245 245 is_opening = kwargs.get(PARAMETER_IS_OPENING, self.is_opening())
246 246
247 247 if is_opening:
248 248 opening_post_id = self.id
249 249 else:
250 250 opening_post_id = thread.get_opening_post_id()
251 251
252 252 css_class = 'post'
253 253 if thread.archived:
254 254 css_class += ' archive_post'
255 255 elif not thread.can_bump():
256 256 css_class += ' dead_post'
257 257
258 258 return render_to_string('boards/post.html', {
259 259 PARAMETER_POST: self,
260 260 PARAMETER_MODERATOR: moderator,
261 261 PARAMETER_IS_OPENING: is_opening,
262 262 PARAMETER_THREAD: thread,
263 263 PARAMETER_CSS_CLASS: css_class,
264 264 PARAMETER_NEED_OPEN_LINK: need_open_link,
265 265 PARAMETER_TRUNCATED: truncated,
266 266 PARAMETER_OP_ID: opening_post_id,
267 267 PARAMETER_REPLY_LINK: reply_link,
268 268 })
269 269
270 270 def get_search_view(self, *args, **kwargs):
271 271 return self.get_view(args, kwargs)
272 272
273 273 def get_first_image(self) -> PostImage:
274 274 return self.images.earliest('id')
275 275
276 276 def delete(self, using=None):
277 277 """
278 278 Deletes all post images and the post itself.
279 279 """
280 280
281 281 for image in self.images.all():
282 282 image_refs_count = Post.objects.filter(images__in=[image]).count()
283 283 if image_refs_count == 1:
284 284 image.delete()
285 285
286 286 thread = self.get_thread()
287 287 thread.last_edit_time = timezone.now()
288 288 thread.save()
289 289
290 290 super(Post, self).delete(using)
291 291
292 292 logging.getLogger('boards.post.delete').info(
293 293 'Deleted post {}'.format(self))
294 294
295 295 # TODO Implement this with OOP, e.g. use the factory and HtmlPostData class
296 296 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
297 297 include_last_update=False) -> str:
298 298 """
299 299 Gets post HTML or JSON data that can be rendered on a page or used by
300 300 API.
301 301 """
302 302
303 303 if format_type == DIFF_TYPE_HTML:
304 304 if request is not None and PARAMETER_TRUNCATED in request.GET:
305 305 truncated = True
306 306 reply_link = False
307 307 else:
308 308 truncated = False
309 309 reply_link = True
310 310
311 311 return self.get_view(truncated=truncated, reply_link=reply_link,
312 312 moderator=utils.is_moderator(request))
313 313 elif format_type == DIFF_TYPE_JSON:
314 314 post_json = {
315 315 'id': self.id,
316 316 'title': self.title,
317 317 'text': self._text_rendered,
318 318 }
319 319 if self.images.exists():
320 320 post_image = self.get_first_image()
321 321 post_json['image'] = post_image.image.url
322 322 post_json['image_preview'] = post_image.image.url_200x150
323 323 if include_last_update:
324 324 post_json['bump_time'] = utils.datetime_to_epoch(
325 325 self.get_thread().bump_time)
326 326 return post_json
327 327
328 328 def notify_clients(self, recursive=True):
329 329 """
330 330 Sends post HTML data to the thread web socket.
331 331 """
332 332
333 if not settings.WEBSOCKETS_ENABLED:
333 if not settings.get_bool('External', 'WebsocketsEnabled'):
334 334 return
335 335
336 336 thread_ids = list()
337 337 for thread in self.get_threads().all():
338 338 thread_ids.append(thread.id)
339 339
340 340 thread.notify_clients()
341 341
342 342 if recursive:
343 343 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
344 344 post_id = reply_number.group(1)
345 345
346 346 try:
347 347 ref_post = Post.objects.get(id=post_id)
348 348
349 349 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
350 350 # If post is in this thread, its thread was already notified.
351 351 # Otherwise, notify its thread separately.
352 352 ref_post.notify_clients(recursive=False)
353 353 except ObjectDoesNotExist:
354 354 pass
355 355
356 356 def build_url(self):
357 357 thread = self.get_thread()
358 358 opening_id = thread.get_opening_post_id()
359 359 post_url = reverse('thread', kwargs={'post_id': opening_id})
360 360 if self.id != opening_id:
361 361 post_url += '#' + str(self.id)
362 362 self.url = post_url
363 363 self.save(update_fields=['url'])
364 364
365 365 def save(self, force_insert=False, force_update=False, using=None,
366 366 update_fields=None):
367 367 self._text_rendered = Parser().parse(self.get_raw_text())
368 368
369 369 self.uid = str(uuid.uuid4())
370 370 if update_fields is not None and 'uid' not in update_fields:
371 371 update_fields += ['uid']
372 372
373 373 if self.id:
374 374 for thread in self.get_threads().all():
375 375 if thread.can_bump():
376 376 thread.update_bump_status(exclude_posts=[self])
377 377 thread.last_edit_time = self.last_edit_time
378 378
379 379 thread.save(update_fields=['last_edit_time', 'bumpable'])
380 380
381 381 super().save(force_insert, force_update, using, update_fields)
382 382
383 383 def get_text(self) -> str:
384 384 return self._text_rendered
385 385
386 386 def get_raw_text(self) -> str:
387 387 return self.text
388 388
389 389 def get_absolute_id(self) -> str:
390 390 """
391 391 If the post has many threads, shows its main thread OP id in the post
392 392 ID.
393 393 """
394 394
395 395 if self.get_threads().count() > 1:
396 396 return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id)
397 397 else:
398 398 return str(self.id)
399 399
400 400 def connect_notifications(self):
401 401 for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()):
402 402 user_name = reply_number.group(1).lower()
403 403 Notification.objects.get_or_create(name=user_name, post=self)
404 404
405 405 def connect_replies(self):
406 406 """
407 407 Connects replies to a post to show them as a reflink map
408 408 """
409 409
410 410 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
411 411 post_id = reply_number.group(1)
412 412
413 413 try:
414 414 referenced_post = Post.objects.get(id=post_id)
415 415
416 416 referenced_post.referenced_posts.add(self)
417 417 referenced_post.last_edit_time = self.pub_time
418 418 referenced_post.build_refmap()
419 419 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
420 420 except ObjectDoesNotExist:
421 421 pass
422 422
423 423 def connect_threads(self, opening_posts):
424 424 """
425 425 If the referenced post is an OP in another thread,
426 426 make this post multi-thread.
427 427 """
428 428
429 429 for opening_post in opening_posts:
430 430 threads = opening_post.get_threads().all()
431 431 for thread in threads:
432 432 if thread.can_bump():
433 433 thread.update_bump_status()
434 434
435 435 thread.last_edit_time = self.last_edit_time
436 436 thread.save(update_fields=['last_edit_time', 'bumpable'])
437 437
438 438 self.threads.add(thread)
@@ -1,237 +1,240 b''
1 1 import logging
2 2 from adjacent import Client
3 3
4 4 from django.db.models import Count, Sum
5 5 from django.utils import timezone
6 6 from django.db import models
7 7
8 8 from boards import settings
9 9 import boards
10 10 from boards.utils import cached_result, datetime_to_epoch
11 11 from boards.models.post import Post
12 12 from boards.models.tag import Tag
13 13
14 14
15 15 __author__ = 'neko259'
16 16
17 17
18 18 logger = logging.getLogger(__name__)
19 19
20 20
21 21 WS_NOTIFICATION_TYPE_NEW_POST = 'new_post'
22 22 WS_NOTIFICATION_TYPE = 'notification_type'
23 23
24 24 WS_CHANNEL_THREAD = "thread:"
25 25
26 26
27 27 class ThreadManager(models.Manager):
28 28 def process_oldest_threads(self):
29 29 """
30 30 Preserves maximum thread count. If there are too many threads,
31 31 archive or delete the old ones.
32 32 """
33 33
34 34 threads = Thread.objects.filter(archived=False).order_by('-bump_time')
35 35 thread_count = threads.count()
36 36
37 if thread_count > settings.MAX_THREAD_COUNT:
38 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
37 max_thread_count = settings.get_int('Messages', 'MaxThreadCount')
38 if thread_count > max_thread_count:
39 num_threads_to_delete = thread_count - max_thread_count
39 40 old_threads = threads[thread_count - num_threads_to_delete:]
40 41
41 42 for thread in old_threads:
42 if settings.ARCHIVE_THREADS:
43 if settings.get_bool('Storage', 'ArchiveThreads'):
43 44 self._archive_thread(thread)
44 45 else:
45 46 thread.delete()
46 47
47 48 logger.info('Processed %d old threads' % num_threads_to_delete)
48 49
49 50 def _archive_thread(self, thread):
50 51 thread.archived = True
51 52 thread.bumpable = False
52 53 thread.last_edit_time = timezone.now()
53 54 thread.update_posts_time()
54 55 thread.save(update_fields=['archived', 'last_edit_time', 'bumpable'])
55 56
56 57
57 58 def get_thread_max_posts():
58 return settings.MAX_POSTS_PER_THREAD
59 return settings.get_int('Messages', 'MaxPostsPerThread')
59 60
60 61
61 62 class Thread(models.Model):
62 63 objects = ThreadManager()
63 64
64 65 class Meta:
65 66 app_label = 'boards'
66 67
67 68 tags = models.ManyToManyField('Tag')
68 69 bump_time = models.DateTimeField(db_index=True)
69 70 last_edit_time = models.DateTimeField()
70 71 archived = models.BooleanField(default=False)
71 72 bumpable = models.BooleanField(default=True)
72 73 max_posts = models.IntegerField(default=get_thread_max_posts)
73 74
74 75 def get_tags(self) -> list:
75 76 """
76 77 Gets a sorted tag list.
77 78 """
78 79
79 80 return self.tags.order_by('name')
80 81
81 82 def bump(self):
82 83 """
83 84 Bumps (moves to up) thread if possible.
84 85 """
85 86
86 87 if self.can_bump():
87 88 self.bump_time = self.last_edit_time
88 89
89 90 self.update_bump_status()
90 91
91 92 logger.info('Bumped thread %d' % self.id)
92 93
93 94 def has_post_limit(self) -> bool:
94 95 return self.max_posts > 0
95 96
96 97 def update_bump_status(self, exclude_posts=None):
97 98 if self.has_post_limit() and self.get_reply_count() >= self.max_posts:
98 99 self.bumpable = False
99 100 self.update_posts_time(exclude_posts=exclude_posts)
100 101
101 102 def _get_cache_key(self):
102 103 return [datetime_to_epoch(self.last_edit_time)]
103 104
104 105 @cached_result(key_method=_get_cache_key)
105 106 def get_reply_count(self) -> int:
106 107 return self.get_replies().count()
107 108
108 109 @cached_result(key_method=_get_cache_key)
109 110 def get_images_count(self) -> int:
110 111 return self.get_replies().annotate(images_count=Count(
111 112 'images')).aggregate(Sum('images_count'))['images_count__sum']
112 113
113 114 def can_bump(self) -> bool:
114 115 """
115 116 Checks if the thread can be bumped by replying to it.
116 117 """
117 118
118 119 return self.bumpable and not self.archived
119 120
120 121 def get_last_replies(self) -> list:
121 122 """
122 123 Gets several last replies, not including opening post
123 124 """
124 125
125 if settings.LAST_REPLIES_COUNT > 0:
126 last_replies_count = settings.get_int('View', 'LastRepliesCount')
127
128 if last_replies_count > 0:
126 129 reply_count = self.get_reply_count()
127 130
128 131 if reply_count > 0:
129 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
132 reply_count_to_show = min(last_replies_count,
130 133 reply_count - 1)
131 134 replies = self.get_replies()
132 135 last_replies = replies[reply_count - reply_count_to_show:]
133 136
134 137 return last_replies
135 138
136 139 def get_skipped_replies_count(self) -> int:
137 140 """
138 141 Gets number of posts between opening post and last replies.
139 142 """
140 143 reply_count = self.get_reply_count()
141 last_replies_count = min(settings.LAST_REPLIES_COUNT,
144 last_replies_count = min(settings.get_int('View', 'LastRepliesCount'),
142 145 reply_count - 1)
143 146 return reply_count - last_replies_count - 1
144 147
145 148 def get_replies(self, view_fields_only=False) -> list:
146 149 """
147 150 Gets sorted thread posts
148 151 """
149 152
150 153 query = Post.objects.filter(threads__in=[self])
151 154 query = query.order_by('pub_time').prefetch_related('images', 'thread', 'threads')
152 155 if view_fields_only:
153 156 query = query.defer('poster_ip')
154 157 return query.all()
155 158
156 159 def get_replies_with_images(self, view_fields_only=False) -> list:
157 160 """
158 161 Gets replies that have at least one image attached
159 162 """
160 163
161 164 return self.get_replies(view_fields_only).annotate(images_count=Count(
162 165 'images')).filter(images_count__gt=0)
163 166
164 167 # TODO Do we still need this?
165 168 def add_tag(self, tag: Tag):
166 169 """
167 170 Connects thread to a tag and tag to a thread
168 171 """
169 172
170 173 self.tags.add(tag)
171 174
172 175 def get_opening_post(self, only_id=False) -> Post:
173 176 """
174 177 Gets the first post of the thread
175 178 """
176 179
177 180 query = self.get_replies().order_by('pub_time')
178 181 if only_id:
179 182 query = query.only('id')
180 183 opening_post = query.first()
181 184
182 185 return opening_post
183 186
184 187 @cached_result()
185 188 def get_opening_post_id(self) -> int:
186 189 """
187 190 Gets ID of the first thread post.
188 191 """
189 192
190 193 return self.get_opening_post(only_id=True).id
191 194
192 195 def get_pub_time(self):
193 196 """
194 197 Gets opening post's pub time because thread does not have its own one.
195 198 """
196 199
197 200 return self.get_opening_post().pub_time
198 201
199 202 def delete(self, using=None):
200 203 """
201 204 Deletes thread with all replies.
202 205 """
203 206
204 207 for reply in self.get_replies().all():
205 208 reply.delete()
206 209
207 210 super(Thread, self).delete(using)
208 211
209 212 def __str__(self):
210 213 return 'T#{}/{}'.format(self.id, self.get_opening_post_id())
211 214
212 215 def get_tag_url_list(self) -> list:
213 216 return boards.models.Tag.objects.get_tag_url_list(self.get_tags())
214 217
215 218 def update_posts_time(self, exclude_posts=None):
216 219 for post in self.post_set.all():
217 220 if exclude_posts is not None and post not in exclude_posts:
218 221 # Manual update is required because uids are generated on save
219 222 post.last_edit_time = self.last_edit_time
220 223 post.save(update_fields=['last_edit_time'])
221 224
222 225 post.threads.update(last_edit_time=self.last_edit_time)
223 226
224 227 def notify_clients(self):
225 if not settings.WEBSOCKETS_ENABLED:
228 if not settings.get_bool('External', 'WebsocketsEnabled'):
226 229 return
227 230
228 231 client = Client()
229 232
230 233 channel_name = WS_CHANNEL_THREAD + str(self.get_opening_post_id())
231 234 client.publish(channel_name, {
232 235 WS_NOTIFICATION_TYPE: WS_NOTIFICATION_TYPE_NEW_POST,
233 236 })
234 237 client.send()
235 238
236 239 def get_absolute_url(self):
237 240 return self.get_opening_post().get_absolute_url()
@@ -1,80 +1,80 b''
1 1 from django.contrib.syndication.views import Feed
2 2 from django.core.urlresolvers import reverse
3 3 from django.shortcuts import get_object_or_404
4 4 from boards.models import Post, Tag, Thread
5 5 from boards import settings
6 6
7 7 __author__ = 'neko259'
8 8
9 9
10 10 # TODO Make tests for all of these
11 11 class AllThreadsFeed(Feed):
12 12
13 title = settings.SITE_NAME + ' - All threads'
13 title = settings.get('Version', 'SiteName') + ' - All threads'
14 14 link = '/'
15 15 description_template = 'boards/rss/post.html'
16 16
17 17 def items(self):
18 18 return Thread.objects.filter(archived=False).order_by('-id')
19 19
20 20 def item_title(self, item):
21 21 return item.get_opening_post().title
22 22
23 23 def item_link(self, item):
24 24 return reverse('thread', args={item.get_opening_post_id()})
25 25
26 26 def item_pubdate(self, item):
27 27 return item.get_pub_time()
28 28
29 29
30 30 class TagThreadsFeed(Feed):
31 31
32 32 link = '/'
33 33 description_template = 'boards/rss/post.html'
34 34
35 35 def items(self, obj):
36 36 return obj.threads.filter(archived=False).order_by('-id')
37 37
38 38 def get_object(self, request, tag_name):
39 39 return get_object_or_404(Tag, name=tag_name)
40 40
41 41 def item_title(self, item):
42 42 return item.get_opening_post().title
43 43
44 44 def item_link(self, item):
45 45 return reverse('thread', args={item.get_opening_post_id()})
46 46
47 47 def item_pubdate(self, item):
48 48 return item.get_pub_time()
49 49
50 50 def title(self, obj):
51 51 return obj.name
52 52
53 53
54 54 class ThreadPostsFeed(Feed):
55 55
56 56 link = '/'
57 57 description_template = 'boards/rss/post.html'
58 58
59 59 def items(self, obj):
60 60 return obj.get_thread().get_replies()
61 61
62 62 def get_object(self, request, post_id):
63 63 return get_object_or_404(Post, id=post_id)
64 64
65 65 def item_title(self, item):
66 66 return item.title
67 67
68 68 def item_link(self, item):
69 69 if not item.is_opening():
70 70 return reverse('thread', args={
71 71 item.get_thread().get_opening_post_id()
72 72 }) + "#" + str(item.id)
73 73 else:
74 74 return reverse('thread', args={item.id})
75 75
76 76 def item_pubdate(self, item):
77 77 return item.pub_time
78 78
79 79 def title(self, obj):
80 80 return obj.title
@@ -1,2 +1,18 b''
1 from boards.default_settings import *
1 import configparser
2
3
4 config = configparser.ConfigParser()
5 config.read('boards/config/default_settings.ini')
6 config.read('boards/config/settings.ini')
7
2 8
9 def get(section, name):
10 return config[section][name]
11
12
13 def get_int(section, name):
14 return int(get(section, name))
15
16
17 def get_bool(section, name):
18 return get(section, name) == 'true'
@@ -1,169 +1,169 b''
1 1 from django.core.urlresolvers import reverse
2 2 from django.core.files import File
3 3 from django.core.files.temp import NamedTemporaryFile
4 4 from django.core.paginator import EmptyPage
5 5 from django.db import transaction
6 6 from django.http import Http404
7 7 from django.shortcuts import render, redirect
8 8 import requests
9 9
10 10 from boards import utils, settings
11 11 from boards.abstracts.paginator import get_paginator
12 12 from boards.abstracts.settingsmanager import get_settings_manager
13 13 from boards.forms import ThreadForm, PlainErrorList
14 14 from boards.models import Post, Thread, Ban, Tag, PostImage, Banner
15 15 from boards.views.banned import BannedView
16 16 from boards.views.base import BaseBoardView, CONTEXT_FORM
17 17 from boards.views.posting_mixin import PostMixin
18 18
19 19
20 20 FORM_TAGS = 'tags'
21 21 FORM_TEXT = 'text'
22 22 FORM_TITLE = 'title'
23 23 FORM_IMAGE = 'image'
24 24 FORM_THREADS = 'threads'
25 25
26 26 TAG_DELIMITER = ' '
27 27
28 28 PARAMETER_CURRENT_PAGE = 'current_page'
29 29 PARAMETER_PAGINATOR = 'paginator'
30 30 PARAMETER_THREADS = 'threads'
31 31 PARAMETER_BANNERS = 'banners'
32 32
33 33 PARAMETER_PREV_LINK = 'prev_page_link'
34 34 PARAMETER_NEXT_LINK = 'next_page_link'
35 35
36 36 TEMPLATE = 'boards/posting_general.html'
37 37 DEFAULT_PAGE = 1
38 38
39 39
40 40 class AllThreadsView(PostMixin, BaseBoardView):
41 41
42 42 def __init__(self):
43 43 self.settings_manager = None
44 44 super(AllThreadsView, self).__init__()
45 45
46 46 def get(self, request, page=DEFAULT_PAGE, form: ThreadForm=None):
47 47 params = self.get_context_data(request=request)
48 48
49 49 if not form:
50 50 form = ThreadForm(error_class=PlainErrorList)
51 51
52 52 self.settings_manager = get_settings_manager(request)
53 53 paginator = get_paginator(self.get_threads(),
54 settings.THREADS_PER_PAGE)
54 settings.get_int('View', 'ThreadsPerPage'))
55 55 paginator.current_page = int(page)
56 56
57 57 try:
58 58 threads = paginator.page(page).object_list
59 59 except EmptyPage:
60 60 raise Http404()
61 61
62 62 params[PARAMETER_THREADS] = threads
63 63 params[CONTEXT_FORM] = form
64 64 params[PARAMETER_BANNERS] = Banner.objects.order_by('-id').all()
65 65
66 66 self.get_page_context(paginator, params, page)
67 67
68 68 return render(request, TEMPLATE, params)
69 69
70 70 def post(self, request, page=DEFAULT_PAGE):
71 71 form = ThreadForm(request.POST, request.FILES,
72 72 error_class=PlainErrorList)
73 73 form.session = request.session
74 74
75 75 if form.is_valid():
76 76 return self.create_thread(request, form)
77 77 if form.need_to_ban:
78 78 # Ban user because he is suspected to be a bot
79 79 self._ban_current_user(request)
80 80
81 81 return self.get(request, page, form)
82 82
83 83 def get_page_context(self, paginator, params, page):
84 84 """
85 85 Get pagination context variables
86 86 """
87 87
88 88 params[PARAMETER_PAGINATOR] = paginator
89 89 current_page = paginator.page(int(page))
90 90 params[PARAMETER_CURRENT_PAGE] = current_page
91 91 if current_page.has_previous():
92 92 params[PARAMETER_PREV_LINK] = self.get_previous_page_link(
93 93 current_page)
94 94 if current_page.has_next():
95 95 params[PARAMETER_NEXT_LINK] = self.get_next_page_link(current_page)
96 96
97 97 def get_previous_page_link(self, current_page):
98 98 return reverse('index', kwargs={
99 99 'page': current_page.previous_page_number(),
100 100 })
101 101
102 102 def get_next_page_link(self, current_page):
103 103 return reverse('index', kwargs={
104 104 'page': current_page.next_page_number(),
105 105 })
106 106
107 107 @staticmethod
108 108 def parse_tags_string(tag_strings):
109 109 """
110 110 Parses tag list string and returns tag object list.
111 111 """
112 112
113 113 tags = []
114 114
115 115 if tag_strings:
116 116 tag_strings = tag_strings.split(TAG_DELIMITER)
117 117 for tag_name in tag_strings:
118 118 tag_name = tag_name.strip().lower()
119 119 if len(tag_name) > 0:
120 120 tag, created = Tag.objects.get_or_create(name=tag_name)
121 121 tags.append(tag)
122 122
123 123 return tags
124 124
125 125 @transaction.atomic
126 126 def create_thread(self, request, form: ThreadForm, html_response=True):
127 127 """
128 128 Creates a new thread with an opening post.
129 129 """
130 130
131 131 ip = utils.get_client_ip(request)
132 132 is_banned = Ban.objects.filter(ip=ip).exists()
133 133
134 134 if is_banned:
135 135 if html_response:
136 136 return redirect(BannedView().as_view())
137 137 else:
138 138 return
139 139
140 140 data = form.cleaned_data
141 141
142 142 title = data[FORM_TITLE]
143 143 text = data[FORM_TEXT]
144 144 image = form.get_image()
145 145 threads = data[FORM_THREADS]
146 146
147 147 text = self._remove_invalid_links(text)
148 148
149 149 tag_strings = data[FORM_TAGS]
150 150
151 151 tags = self.parse_tags_string(tag_strings)
152 152
153 153 post = Post.objects.create_post(title=title, text=text, image=image,
154 154 ip=ip, tags=tags, threads=threads)
155 155
156 156 # This is required to update the threads to which posts we have replied
157 157 # when creating this one
158 158 post.notify_clients()
159 159
160 160 if html_response:
161 161 return redirect(post.get_url())
162 162
163 163 def get_threads(self):
164 164 """
165 165 Gets list of threads that will be shown on a page.
166 166 """
167 167
168 168 return Thread.objects.order_by('-bump_time')\
169 169 .exclude(tags__in=self.settings_manager.get_hidden_tags())
@@ -1,76 +1,77 b''
1 1 from django.db import transaction
2 2 from django.shortcuts import render, redirect
3 3 from django.utils import timezone
4 4
5 5 from boards.abstracts.settingsmanager import get_settings_manager, \
6 6 SETTING_USERNAME, SETTING_LAST_NOTIFICATION_ID, SETTING_IMAGE_VIEWER
7 7 from boards.middlewares import SESSION_TIMEZONE
8 8 from boards.views.base import BaseBoardView, CONTEXT_FORM
9 9 from boards.forms import SettingsForm, PlainErrorList
10 10 from boards import settings
11 11
12 12
13 13 FORM_THEME = 'theme'
14 14 FORM_USERNAME = 'username'
15 15 FORM_TIMEZONE = 'timezone'
16 16 FORM_IMAGE_VIEWER = 'image_viewer'
17 17
18 18 CONTEXT_HIDDEN_TAGS = 'hidden_tags'
19 19
20 20 TEMPLATE = 'boards/settings.html'
21 21
22 22
23 23 class SettingsView(BaseBoardView):
24 24
25 25 def get(self, request):
26 26 params = dict()
27 27 settings_manager = get_settings_manager(request)
28 28
29 29 selected_theme = settings_manager.get_theme()
30 30
31 31 form = SettingsForm(
32 32 initial={
33 33 FORM_THEME: selected_theme,
34 34 FORM_IMAGE_VIEWER: settings_manager.get_setting(
35 SETTING_IMAGE_VIEWER, default=settings.DEFAULT_IMAGE_VIEWER),
35 SETTING_IMAGE_VIEWER,
36 default=settings.get('View', 'DefaultImageViewer')),
36 37 FORM_USERNAME: settings_manager.get_setting(SETTING_USERNAME),
37 38 FORM_TIMEZONE: request.session.get(
38 39 SESSION_TIMEZONE, timezone.get_current_timezone()),
39 40 },
40 41 error_class=PlainErrorList)
41 42
42 43 params[CONTEXT_FORM] = form
43 44 params[CONTEXT_HIDDEN_TAGS] = settings_manager.get_hidden_tags()
44 45
45 46 return render(request, TEMPLATE, params)
46 47
47 48 def post(self, request):
48 49 settings_manager = get_settings_manager(request)
49 50
50 51 with transaction.atomic():
51 52 form = SettingsForm(request.POST, error_class=PlainErrorList)
52 53
53 54 if form.is_valid():
54 55 selected_theme = form.cleaned_data[FORM_THEME]
55 56 username = form.cleaned_data[FORM_USERNAME].lower()
56 57
57 58 settings_manager.set_theme(selected_theme)
58 59 settings_manager.set_setting(SETTING_IMAGE_VIEWER,
59 60 form.cleaned_data[FORM_IMAGE_VIEWER])
60 61
61 62 old_username = settings_manager.get_setting(SETTING_USERNAME)
62 63 if username != old_username:
63 64 settings_manager.set_setting(SETTING_USERNAME, username)
64 65 settings_manager.set_setting(SETTING_LAST_NOTIFICATION_ID, None)
65 66
66 67 request.session[SESSION_TIMEZONE] = form.cleaned_data[FORM_TIMEZONE]
67 68
68 69 return redirect('settings')
69 70 else:
70 71 params = dict()
71 72
72 73 params[CONTEXT_FORM] = form
73 74 params[CONTEXT_HIDDEN_TAGS] = settings_manager.get_hidden_tags()
74 75
75 76 return render(request, TEMPLATE, params)
76 77
@@ -1,130 +1,130 b''
1 1 from django.core.exceptions import ObjectDoesNotExist
2 2 from django.http import Http404
3 3 from django.shortcuts import get_object_or_404, render, redirect
4 4 from django.views.generic.edit import FormMixin
5 5 from django.utils import timezone
6 6 from django.utils.dateformat import format
7 7
8 8 from boards import utils, settings
9 9 from boards.forms import PostForm, PlainErrorList
10 10 from boards.models import Post
11 11 from boards.views.base import BaseBoardView, CONTEXT_FORM
12 12 from boards.views.posting_mixin import PostMixin
13 13
14 14 import neboard
15 15
16 16
17 17 CONTEXT_LASTUPDATE = "last_update"
18 18 CONTEXT_THREAD = 'thread'
19 19 CONTEXT_WS_TOKEN = 'ws_token'
20 20 CONTEXT_WS_PROJECT = 'ws_project'
21 21 CONTEXT_WS_HOST = 'ws_host'
22 22 CONTEXT_WS_PORT = 'ws_port'
23 23 CONTEXT_WS_TIME = 'ws_token_time'
24 24
25 25 FORM_TITLE = 'title'
26 26 FORM_TEXT = 'text'
27 27 FORM_IMAGE = 'image'
28 28 FORM_THREADS = 'threads'
29 29
30 30
31 31 class ThreadView(BaseBoardView, PostMixin, FormMixin):
32 32
33 33 def get(self, request, post_id, form: PostForm=None):
34 34 try:
35 35 opening_post = Post.objects.get(id=post_id)
36 36 except ObjectDoesNotExist:
37 37 raise Http404
38 38
39 39 # If this is not OP, don't show it as it is
40 40 if not opening_post.is_opening():
41 41 return redirect(opening_post.get_thread().get_opening_post().get_url())
42 42
43 43 if not form:
44 44 form = PostForm(error_class=PlainErrorList)
45 45
46 46 thread_to_show = opening_post.get_thread()
47 47
48 48 params = dict()
49 49
50 50 params[CONTEXT_FORM] = form
51 51 params[CONTEXT_LASTUPDATE] = str(thread_to_show.last_edit_time)
52 52 params[CONTEXT_THREAD] = thread_to_show
53 53
54 if settings.WEBSOCKETS_ENABLED:
54 if settings.get_bool('External', 'WebsocketsEnabled'):
55 55 token_time = format(timezone.now(), u'U')
56 56
57 57 params[CONTEXT_WS_TIME] = token_time
58 58 params[CONTEXT_WS_TOKEN] = utils.get_websocket_token(
59 59 timestamp=token_time)
60 60 params[CONTEXT_WS_PROJECT] = neboard.settings.CENTRIFUGE_PROJECT_ID
61 61 params[CONTEXT_WS_HOST] = request.get_host().split(':')[0]
62 62 params[CONTEXT_WS_PORT] = neboard.settings.CENTRIFUGE_PORT
63 63
64 64 params.update(self.get_data(thread_to_show))
65 65
66 66 return render(request, self.get_template(), params)
67 67
68 68 def post(self, request, post_id):
69 69 opening_post = get_object_or_404(Post, id=post_id)
70 70
71 71 # If this is not OP, don't show it as it is
72 72 if not opening_post.is_opening():
73 73 raise Http404
74 74
75 75 if not opening_post.get_thread().archived:
76 76 form = PostForm(request.POST, request.FILES,
77 77 error_class=PlainErrorList)
78 78 form.session = request.session
79 79
80 80 if form.is_valid():
81 81 return self.new_post(request, form, opening_post)
82 82 if form.need_to_ban:
83 83 # Ban user because he is suspected to be a bot
84 84 self._ban_current_user(request)
85 85
86 86 return self.get(request, post_id, form)
87 87
88 88 def new_post(self, request, form: PostForm, opening_post: Post=None,
89 89 html_response=True):
90 90 """
91 91 Adds a new post (in thread or as a reply).
92 92 """
93 93
94 94 ip = utils.get_client_ip(request)
95 95
96 96 data = form.cleaned_data
97 97
98 98 title = data[FORM_TITLE]
99 99 text = data[FORM_TEXT]
100 100 image = form.get_image()
101 101 threads = data[FORM_THREADS]
102 102
103 103 text = self._remove_invalid_links(text)
104 104
105 105 post_thread = opening_post.get_thread()
106 106
107 107 post = Post.objects.create_post(title=title, text=text, image=image,
108 108 thread=post_thread, ip=ip,
109 109 threads=threads)
110 110 post.notify_clients()
111 111
112 112 if html_response:
113 113 if opening_post:
114 114 return redirect(post.get_url())
115 115 else:
116 116 return post
117 117
118 118 def get_data(self, thread):
119 119 """
120 120 Returns context params for the view.
121 121 """
122 122
123 123 pass
124 124
125 125 def get_template(self):
126 126 """
127 127 Gets template to show the thread mode on.
128 128 """
129 129
130 130 pass
1 NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now