##// END OF EJS Templates
Fixed thread bumping
neko259 -
r1221:a2cae5ee default
parent child Browse files
Show More
@@ -1,421 +1,419 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 from django.db.models import TextField
10 from django.db.models import TextField, QuerySet
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.post.export import get_exporter, DIFF_TYPE_JSON
20 20 from boards.models.user import Notification, Ban
21 21 import boards.models.thread
22 22
23 23
24 24 APP_LABEL_BOARDS = 'boards'
25 25
26 26 POSTS_PER_DAY_RANGE = 7
27 27
28 28 BAN_REASON_AUTO = 'Auto'
29 29
30 30 IMAGE_THUMB_SIZE = (200, 150)
31 31
32 32 TITLE_MAX_LENGTH = 200
33 33
34 34 # TODO This should be removed
35 35 NO_IP = '0.0.0.0'
36 36
37 37 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
38 38 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
39 39
40 40 PARAMETER_TRUNCATED = 'truncated'
41 41 PARAMETER_TAG = 'tag'
42 42 PARAMETER_OFFSET = 'offset'
43 43 PARAMETER_DIFF_TYPE = 'type'
44 44 PARAMETER_CSS_CLASS = 'css_class'
45 45 PARAMETER_THREAD = 'thread'
46 46 PARAMETER_IS_OPENING = 'is_opening'
47 47 PARAMETER_MODERATOR = 'moderator'
48 48 PARAMETER_POST = 'post'
49 49 PARAMETER_OP_ID = 'opening_post_id'
50 50 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
51 51 PARAMETER_REPLY_LINK = 'reply_link'
52 52 PARAMETER_NEED_OP_DATA = 'need_op_data'
53 53
54 54 POST_VIEW_PARAMS = (
55 55 'need_op_data',
56 56 'reply_link',
57 57 'moderator',
58 58 'need_open_link',
59 59 'truncated',
60 60 'mode_tree',
61 61 )
62 62
63 63 REFMAP_STR = '<a href="{}">&gt;&gt;{}</a>'
64 64
65 65
66 66 class PostManager(models.Manager):
67 67 @transaction.atomic
68 68 def create_post(self, title: str, text: str, image=None, thread=None,
69 ip=NO_IP, tags: list=None, threads: list=None):
69 ip=NO_IP, tags: list=None, opening_posts: list=None):
70 70 """
71 71 Creates new post
72 72 """
73 73
74 74 is_banned = Ban.objects.filter(ip=ip).exists()
75 75
76 76 # TODO Raise specific exception and catch it in the views
77 77 if is_banned:
78 78 raise Exception("This user is banned")
79 79
80 80 if not tags:
81 81 tags = []
82 if not threads:
83 threads = []
82 if not opening_posts:
83 opening_posts = []
84 84
85 85 posting_time = timezone.now()
86 86 if not thread:
87 87 thread = boards.models.thread.Thread.objects.create(
88 88 bump_time=posting_time, last_edit_time=posting_time)
89 89 list(map(thread.tags.add, tags))
90 90 new_thread = True
91 91 else:
92 92 new_thread = False
93 93
94 94 pre_text = Parser().preparse(text)
95 95
96 96 post = self.create(title=title,
97 97 text=pre_text,
98 98 pub_time=posting_time,
99 99 poster_ip=ip,
100 100 thread=thread,
101 101 last_edit_time=posting_time)
102 102 post.threads.add(thread)
103 103
104 104 logger = logging.getLogger('boards.post.create')
105 105
106 106 logger.info('Created post {} by {}'.format(post, post.poster_ip))
107 107
108 108 if image:
109 109 post.images.add(PostImage.objects.create_with_hash(image))
110 110
111 111 if new_thread:
112 112 boards.models.thread.Thread.objects.process_oldest_threads()
113 113 else:
114 114 thread.last_edit_time = posting_time
115 115 thread.bump()
116 116 thread.save()
117 117
118 118 post.build_url()
119 119 post.connect_replies()
120 post.connect_threads(threads)
120 post.connect_threads(opening_posts)
121 121 post.connect_notifications()
122 122
123 123 return post
124 124
125 125 def delete_posts_by_ip(self, ip):
126 126 """
127 127 Deletes all posts of the author with same IP
128 128 """
129 129
130 130 posts = self.filter(poster_ip=ip)
131 131 for post in posts:
132 132 post.delete()
133 133
134 134 @utils.cached_result()
135 135 def get_posts_per_day(self) -> float:
136 136 """
137 137 Gets average count of posts per day for the last 7 days
138 138 """
139 139
140 140 day_end = date.today()
141 141 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
142 142
143 143 day_time_start = timezone.make_aware(datetime.combine(
144 144 day_start, dtime()), timezone.get_current_timezone())
145 145 day_time_end = timezone.make_aware(datetime.combine(
146 146 day_end, dtime()), timezone.get_current_timezone())
147 147
148 148 posts_per_period = float(self.filter(
149 149 pub_time__lte=day_time_end,
150 150 pub_time__gte=day_time_start).count())
151 151
152 152 ppd = posts_per_period / POSTS_PER_DAY_RANGE
153 153
154 154 return ppd
155 155
156 156
157 157 class Post(models.Model, Viewable):
158 158 """A post is a message."""
159 159
160 160 objects = PostManager()
161 161
162 162 class Meta:
163 163 app_label = APP_LABEL_BOARDS
164 164 ordering = ('id',)
165 165
166 166 title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
167 167 pub_time = models.DateTimeField()
168 168 text = TextField(blank=True, null=True)
169 169 _text_rendered = TextField(blank=True, null=True, editable=False)
170 170
171 171 images = models.ManyToManyField(PostImage, null=True, blank=True,
172 172 related_name='ip+', db_index=True)
173 173
174 174 poster_ip = models.GenericIPAddressField()
175 175
176 176 # TODO This field can be removed cause UID is used for update now
177 177 last_edit_time = models.DateTimeField()
178 178
179 179 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
180 180 null=True,
181 181 blank=True, related_name='refposts',
182 182 db_index=True)
183 183 refmap = models.TextField(null=True, blank=True)
184 184 threads = models.ManyToManyField('Thread', db_index=True)
185 185 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
186 186
187 187 url = models.TextField()
188 188 uid = models.TextField(db_index=True)
189 189
190 190 def __str__(self):
191 191 return 'P#{}/{}'.format(self.id, self.title)
192 192
193 193 def get_referenced_posts(self):
194 194 threads = self.get_threads().all()
195 195 return self.referenced_posts.filter(threads__in=threads)\
196 196 .order_by('pub_time').distinct().all()
197 197
198 198 def get_title(self) -> str:
199 199 """
200 200 Gets original post title or part of its text.
201 201 """
202 202
203 203 title = self.title
204 204 if not title:
205 205 title = self.get_text()
206 206
207 207 return title
208 208
209 209 def build_refmap(self) -> None:
210 210 """
211 211 Builds a replies map string from replies list. This is a cache to stop
212 212 the server from recalculating the map on every post show.
213 213 """
214 214
215 215 post_urls = [REFMAP_STR.format(refpost.get_absolute_url(), refpost.id)
216 216 for refpost in self.referenced_posts.all()]
217 217
218 218 self.refmap = ', '.join(post_urls)
219 219
220 220 def is_referenced(self) -> bool:
221 221 return self.refmap and len(self.refmap) > 0
222 222
223 223 def is_opening(self) -> bool:
224 224 """
225 225 Checks if this is an opening post or just a reply.
226 226 """
227 227
228 228 return self.get_thread().get_opening_post_id() == self.id
229 229
230 230 def get_absolute_url(self):
231 231 if self.url:
232 232 return self.url
233 233 else:
234 234 opening_id = self.get_thread().get_opening_post_id()
235 235 post_url = reverse('thread', kwargs={'post_id': opening_id})
236 236 if self.id != opening_id:
237 237 post_url += '#' + str(self.id)
238 238 return post_url
239 239
240 240
241 241 def get_thread(self):
242 242 return self.thread
243 243
244 def get_threads(self) -> list:
244 def get_threads(self) -> QuerySet:
245 245 """
246 246 Gets post's thread.
247 247 """
248 248
249 249 return self.threads
250 250
251 251 def get_view(self, *args, **kwargs) -> str:
252 252 """
253 253 Renders post's HTML view. Some of the post params can be passed over
254 254 kwargs for the means of caching (if we view the thread, some params
255 255 are same for every post and don't need to be computed over and over.
256 256 """
257 257
258 258 thread = self.get_thread()
259 259 is_opening = kwargs.get(PARAMETER_IS_OPENING, self.is_opening())
260 260
261 261 if is_opening:
262 262 opening_post_id = self.id
263 263 else:
264 264 opening_post_id = thread.get_opening_post_id()
265 265
266 266 css_class = 'post'
267 267 if thread.archived:
268 268 css_class += ' archive_post'
269 269 elif not thread.can_bump():
270 270 css_class += ' dead_post'
271 271
272 272 params = dict()
273 273 for param in POST_VIEW_PARAMS:
274 274 if param in kwargs:
275 275 params[param] = kwargs[param]
276 276
277 277 params.update({
278 278 PARAMETER_POST: self,
279 279 PARAMETER_IS_OPENING: is_opening,
280 280 PARAMETER_THREAD: thread,
281 281 PARAMETER_CSS_CLASS: css_class,
282 282 PARAMETER_OP_ID: opening_post_id,
283 283 })
284 284
285 285 return render_to_string('boards/post.html', params)
286 286
287 287 def get_search_view(self, *args, **kwargs):
288 288 return self.get_view(need_op_data=True, *args, **kwargs)
289 289
290 290 def get_first_image(self) -> PostImage:
291 291 return self.images.earliest('id')
292 292
293 293 def delete(self, using=None):
294 294 """
295 295 Deletes all post images and the post itself.
296 296 """
297 297
298 298 for image in self.images.all():
299 299 image_refs_count = Post.objects.filter(images__in=[image]).count()
300 300 if image_refs_count == 1:
301 301 image.delete()
302 302
303 303 thread = self.get_thread()
304 304 thread.last_edit_time = timezone.now()
305 305 thread.save()
306 306
307 307 super(Post, self).delete(using)
308 308
309 309 logging.getLogger('boards.post.delete').info(
310 310 'Deleted post {}'.format(self))
311 311
312 312 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
313 313 include_last_update=False) -> str:
314 314 """
315 315 Gets post HTML or JSON data that can be rendered on a page or used by
316 316 API.
317 317 """
318 318
319 319 return get_exporter(format_type).export(self, request,
320 320 include_last_update)
321 321
322 322 def notify_clients(self, recursive=True):
323 323 """
324 324 Sends post HTML data to the thread web socket.
325 325 """
326 326
327 327 if not settings.get_bool('External', 'WebsocketsEnabled'):
328 328 return
329 329
330 330 thread_ids = list()
331 331 for thread in self.get_threads().all():
332 332 thread_ids.append(thread.id)
333 333
334 334 thread.notify_clients()
335 335
336 336 if recursive:
337 337 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
338 338 post_id = reply_number.group(1)
339 339
340 340 try:
341 341 ref_post = Post.objects.get(id=post_id)
342 342
343 343 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
344 344 # If post is in this thread, its thread was already notified.
345 345 # Otherwise, notify its thread separately.
346 346 ref_post.notify_clients(recursive=False)
347 347 except ObjectDoesNotExist:
348 348 pass
349 349
350 350 def build_url(self):
351 351 self.url = self.get_absolute_url()
352 352 self.save(update_fields=['url'])
353 353
354 354 def save(self, force_insert=False, force_update=False, using=None,
355 355 update_fields=None):
356 356 self._text_rendered = Parser().parse(self.get_raw_text())
357 357
358 358 self.uid = str(uuid.uuid4())
359 359 if update_fields is not None and 'uid' not in update_fields:
360 360 update_fields += ['uid']
361 361
362 362 if self.id:
363 363 for thread in self.get_threads().all():
364 if thread.can_bump():
365 thread.update_bump_status(exclude_posts=[self])
366 364 thread.last_edit_time = self.last_edit_time
367 365
368 366 thread.save(update_fields=['last_edit_time', 'bumpable'])
369 367
370 368 super().save(force_insert, force_update, using, update_fields)
371 369
372 370 def get_text(self) -> str:
373 371 return self._text_rendered
374 372
375 373 def get_raw_text(self) -> str:
376 374 return self.text
377 375
378 376 def get_absolute_id(self) -> str:
379 377 """
380 378 If the post has many threads, shows its main thread OP id in the post
381 379 ID.
382 380 """
383 381
384 382 if self.get_threads().count() > 1:
385 383 return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id)
386 384 else:
387 385 return str(self.id)
388 386
389 387 def connect_notifications(self):
390 388 for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()):
391 389 user_name = reply_number.group(1).lower()
392 390 Notification.objects.get_or_create(name=user_name, post=self)
393 391
394 392 def connect_replies(self):
395 393 """
396 394 Connects replies to a post to show them as a reflink map
397 395 """
398 396
399 397 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
400 398 post_id = reply_number.group(1)
401 399
402 400 try:
403 401 referenced_post = Post.objects.get(id=post_id)
404 402
405 403 referenced_post.referenced_posts.add(self)
406 404 referenced_post.last_edit_time = self.pub_time
407 405 referenced_post.build_refmap()
408 406 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
409 407 except ObjectDoesNotExist:
410 408 pass
411 409
412 410 def connect_threads(self, opening_posts):
413 411 for opening_post in opening_posts:
414 412 threads = opening_post.get_threads().all()
415 413 for thread in threads:
416 414 if thread.can_bump():
417 415 thread.update_bump_status()
418 416
419 417 thread.last_edit_time = self.last_edit_time
420 418 thread.save(update_fields=['last_edit_time', 'bumpable'])
421 419 self.threads.add(opening_post.get_thread())
@@ -1,235 +1,237 b''
1 1 import logging
2 2 from adjacent import Client
3 3
4 4 from django.db.models import Count, Sum, QuerySet
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 37 max_thread_count = settings.get_int('Messages', 'MaxThreadCount')
38 38 if thread_count > max_thread_count:
39 39 num_threads_to_delete = thread_count - max_thread_count
40 40 old_threads = threads[thread_count - num_threads_to_delete:]
41 41
42 42 for thread in old_threads:
43 43 if settings.get_bool('Storage', 'ArchiveThreads'):
44 44 self._archive_thread(thread)
45 45 else:
46 46 thread.delete()
47 47
48 48 logger.info('Processed %d old threads' % num_threads_to_delete)
49 49
50 50 def _archive_thread(self, thread):
51 51 thread.archived = True
52 52 thread.bumpable = False
53 53 thread.last_edit_time = timezone.now()
54 54 thread.update_posts_time()
55 55 thread.save(update_fields=['archived', 'last_edit_time', 'bumpable'])
56 56
57 57
58 58 def get_thread_max_posts():
59 59 return settings.get_int('Messages', 'MaxPostsPerThread')
60 60
61 61
62 62 class Thread(models.Model):
63 63 objects = ThreadManager()
64 64
65 65 class Meta:
66 66 app_label = 'boards'
67 67
68 68 tags = models.ManyToManyField('Tag')
69 69 bump_time = models.DateTimeField(db_index=True)
70 70 last_edit_time = models.DateTimeField()
71 71 archived = models.BooleanField(default=False)
72 72 bumpable = models.BooleanField(default=True)
73 73 max_posts = models.IntegerField(default=get_thread_max_posts)
74 74
75 75 def get_tags(self) -> QuerySet:
76 76 """
77 77 Gets a sorted tag list.
78 78 """
79 79
80 80 return self.tags.order_by('name')
81 81
82 82 def bump(self):
83 83 """
84 84 Bumps (moves to up) thread if possible.
85 85 """
86 86
87 87 if self.can_bump():
88 88 self.bump_time = self.last_edit_time
89 89
90 90 self.update_bump_status()
91 91
92 92 logger.info('Bumped thread %d' % self.id)
93 93
94 94 def has_post_limit(self) -> bool:
95 95 return self.max_posts > 0
96 96
97 97 def update_bump_status(self, exclude_posts=None):
98 98 if self.has_post_limit() and self.get_reply_count() >= self.max_posts:
99 99 self.bumpable = False
100 100 self.update_posts_time(exclude_posts=exclude_posts)
101 101
102 102 def _get_cache_key(self):
103 103 return [datetime_to_epoch(self.last_edit_time)]
104 104
105 105 @cached_result(key_method=_get_cache_key)
106 106 def get_reply_count(self) -> int:
107 107 return self.get_replies().count()
108 108
109 109 @cached_result(key_method=_get_cache_key)
110 110 def get_images_count(self) -> int:
111 111 return self.get_replies().annotate(images_count=Count(
112 112 'images')).aggregate(Sum('images_count'))['images_count__sum']
113 113
114 114 def can_bump(self) -> bool:
115 115 """
116 116 Checks if the thread can be bumped by replying to it.
117 117 """
118 118
119 119 return self.bumpable and not self.archived
120 120
121 121 def get_last_replies(self) -> QuerySet:
122 122 """
123 123 Gets several last replies, not including opening post
124 124 """
125 125
126 126 last_replies_count = settings.get_int('View', 'LastRepliesCount')
127 127
128 128 if last_replies_count > 0:
129 129 reply_count = self.get_reply_count()
130 130
131 131 if reply_count > 0:
132 132 reply_count_to_show = min(last_replies_count,
133 133 reply_count - 1)
134 134 replies = self.get_replies()
135 135 last_replies = replies[reply_count - reply_count_to_show:]
136 136
137 137 return last_replies
138 138
139 139 def get_skipped_replies_count(self) -> int:
140 140 """
141 141 Gets number of posts between opening post and last replies.
142 142 """
143 143 reply_count = self.get_reply_count()
144 144 last_replies_count = min(settings.get_int('View', 'LastRepliesCount'),
145 145 reply_count - 1)
146 146 return reply_count - last_replies_count - 1
147 147
148 148 def get_replies(self, view_fields_only=False) -> QuerySet:
149 149 """
150 150 Gets sorted thread posts
151 151 """
152 152
153 153 query = Post.objects.filter(threads__in=[self])
154 154 query = query.order_by('pub_time').prefetch_related('images', 'thread', 'threads')
155 155 if view_fields_only:
156 156 query = query.defer('poster_ip')
157 157 return query.all()
158 158
159 159 def get_top_level_replies(self) -> QuerySet:
160 160 return self.get_replies().exclude(refposts__threads__in=[self])
161 161
162 162 def get_replies_with_images(self, view_fields_only=False) -> QuerySet:
163 163 """
164 164 Gets replies that have at least one image attached
165 165 """
166 166
167 167 return self.get_replies(view_fields_only).annotate(images_count=Count(
168 168 'images')).filter(images_count__gt=0)
169 169
170 170 def get_opening_post(self, only_id=False) -> Post:
171 171 """
172 172 Gets the first post of the thread
173 173 """
174 174
175 175 query = self.get_replies().order_by('pub_time')
176 176 if only_id:
177 177 query = query.only('id')
178 178 opening_post = query.first()
179 179
180 180 return opening_post
181 181
182 182 @cached_result()
183 183 def get_opening_post_id(self) -> int:
184 184 """
185 185 Gets ID of the first thread post.
186 186 """
187 187
188 188 return self.get_opening_post(only_id=True).id
189 189
190 190 def get_pub_time(self):
191 191 """
192 192 Gets opening post's pub time because thread does not have its own one.
193 193 """
194 194
195 195 return self.get_opening_post().pub_time
196 196
197 197 def delete(self, using=None):
198 198 """
199 199 Deletes thread with all replies.
200 200 """
201 201
202 202 for reply in self.get_replies().all():
203 203 reply.delete()
204 204
205 205 super(Thread, self).delete(using)
206 206
207 207 def __str__(self):
208 208 return 'T#{}/{}'.format(self.id, self.get_opening_post_id())
209 209
210 210 def get_tag_url_list(self) -> list:
211 211 return boards.models.Tag.objects.get_tag_url_list(self.get_tags())
212 212
213 213 def update_posts_time(self, exclude_posts=None):
214 last_edit_time = self.last_edit_time
215
214 216 for post in self.post_set.all():
215 217 if exclude_posts is None or post not in exclude_posts:
216 218 # Manual update is required because uids are generated on save
217 post.last_edit_time = self.last_edit_time
219 post.last_edit_time = last_edit_time
218 220 post.save(update_fields=['last_edit_time'])
219 221
220 post.threads.update(last_edit_time=self.last_edit_time)
222 post.get_threads().update(last_edit_time=last_edit_time)
221 223
222 224 def notify_clients(self):
223 225 if not settings.get_bool('External', 'WebsocketsEnabled'):
224 226 return
225 227
226 228 client = Client()
227 229
228 230 channel_name = WS_CHANNEL_THREAD + str(self.get_opening_post_id())
229 231 client.publish(channel_name, {
230 232 WS_NOTIFICATION_TYPE: WS_NOTIFICATION_TYPE_NEW_POST,
231 233 })
232 234 client.send()
233 235
234 236 def get_absolute_url(self):
235 237 return self.get_opening_post().get_absolute_url()
@@ -1,388 +1,391 b''
1 1 /*
2 2 @licstart The following is the entire license notice for the
3 3 JavaScript code in this page.
4 4
5 5
6 6 Copyright (C) 2013-2014 neko259
7 7
8 8 The JavaScript code in this page is free software: you can
9 9 redistribute it and/or modify it under the terms of the GNU
10 10 General Public License (GNU GPL) as published by the Free Software
11 11 Foundation, either version 3 of the License, or (at your option)
12 12 any later version. The code is distributed WITHOUT ANY WARRANTY;
13 13 without even the implied warranty of MERCHANTABILITY or FITNESS
14 14 FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
15 15
16 16 As additional permission under GNU GPL version 3 section 7, you
17 17 may distribute non-source (e.g., minimized or compacted) forms of
18 18 that code without the copy of the GNU GPL normally required by
19 19 section 4, provided you include this license notice and a URL
20 20 through which recipients can access the Corresponding Source.
21 21
22 22 @licend The above is the entire license notice
23 23 for the JavaScript code in this page.
24 24 */
25 25
26 26 var CLASS_POST = '.post'
27 27
28 28 var POST_ADDED = 0;
29 29 var POST_UPDATED = 1;
30 30
31 31 var JS_AUTOUPDATE_PERIOD = 20000;
32 32
33 33 var wsUser = '';
34 34
35 35 var unreadPosts = 0;
36 36 var documentOriginalTitle = '';
37 37
38 38 // Thread ID does not change, can be stored one time
39 39 var threadId = $('div.thread').children(CLASS_POST).first().attr('id');
40 40
41 41 /**
42 42 * Connect to websocket server and subscribe to thread updates. On any update we
43 43 * request a thread diff.
44 44 *
45 45 * @returns {boolean} true if connected, false otherwise
46 46 */
47 47 function connectWebsocket() {
48 48 var metapanel = $('.metapanel')[0];
49 49
50 50 var wsHost = metapanel.getAttribute('data-ws-host');
51 51 var wsPort = metapanel.getAttribute('data-ws-port');
52 52
53 53 if (wsHost.length > 0 && wsPort.length > 0) {
54 54 var centrifuge = new Centrifuge({
55 55 "url": 'ws://' + wsHost + ':' + wsPort + "/connection/websocket",
56 56 "project": metapanel.getAttribute('data-ws-project'),
57 57 "user": wsUser,
58 58 "timestamp": metapanel.getAttribute('data-ws-token-time'),
59 59 "token": metapanel.getAttribute('data-ws-token'),
60 60 "debug": false
61 61 });
62 62
63 63 centrifuge.on('error', function(error_message) {
64 64 console.log("Error connecting to websocket server.");
65 65 console.log(error_message);
66 66 console.log("Using javascript update instead.");
67 67
68 68 // If websockets don't work, enable JS update instead
69 69 enableJsUpdate()
70 70 });
71 71
72 72 centrifuge.on('connect', function() {
73 73 var channelName = 'thread:' + threadId;
74 74 centrifuge.subscribe(channelName, function(message) {
75 75 getThreadDiff();
76 76 });
77 77
78 78 // For the case we closed the browser and missed some updates
79 79 getThreadDiff();
80 80 $('#autoupdate').hide();
81 81 });
82 82
83 83 centrifuge.connect();
84 84
85 85 return true;
86 86 } else {
87 87 return false;
88 88 }
89 89 }
90 90
91 91 /**
92 92 * Get diff of the posts from the current thread timestamp.
93 93 * This is required if the browser was closed and some post updates were
94 94 * missed.
95 95 */
96 96 function getThreadDiff() {
97 97 var lastUpdateTime = $('.metapanel').attr('data-last-update');
98 98 var lastPostId = $('.post').last().attr('id');
99 99
100 100 var uids = '';
101 101 var posts = $('.post');
102 102 for (var i = 0; i < posts.length; i++) {
103 103 uids += posts[i].getAttribute('data-uid') + ' ';
104 104 }
105 105
106 106 var data = {
107 107 uids: uids,
108 108 thread: threadId
109 109 }
110 110
111 111 var diffUrl = '/api/diff_thread/';
112 112
113 113 $.post(diffUrl,
114 114 data,
115 115 function(data) {
116 116 var updatedPosts = data.updated;
117 117 var addedPostCount = 0;
118 118
119 119 for (var i = 0; i < updatedPosts.length; i++) {
120 120 var postText = updatedPosts[i];
121 121 var post = $(postText);
122 122
123 123 if (updatePost(post) == POST_ADDED) {
124 124 addedPostCount++;
125 125 }
126 126 }
127 127
128 128 var hasMetaUpdates = updatedPosts.length > 0;
129 129 if (hasMetaUpdates) {
130 130 updateMetadataPanel();
131 131 }
132 132
133 133 if (addedPostCount > 0) {
134 134 updateBumplimitProgress(addedPostCount);
135 135 }
136 136
137 137 if (updatedPosts.length > 0) {
138 138 showNewPostsTitle(addedPostCount);
139 139 }
140 140
141 141 // TODO Process removed posts if any
142 142 $('.metapanel').attr('data-last-update', data.last_update);
143 143 },
144 144 'json'
145 145 )
146 146 }
147 147
148 148 /**
149 149 * Add or update the post on html page.
150 150 */
151 151 function updatePost(postHtml) {
152 152 // This needs to be set on start because the page is scrolled after posts
153 153 // are added or updated
154 154 var bottom = isPageBottom();
155 155
156 156 var post = $(postHtml);
157 157
158 158 var threadBlock = $('div.thread');
159 159
160 160 var postId = post.attr('id');
161 161
162 162 // If the post already exists, replace it. Otherwise add as a new one.
163 163 var existingPosts = threadBlock.children('.post[id=' + postId + ']');
164 164
165 165 var type;
166 166
167 167 if (existingPosts.size() > 0) {
168 168 existingPosts.replaceWith(post);
169 169
170 170 type = POST_UPDATED;
171 171 } else {
172 172 post.appendTo(threadBlock);
173 173
174 174 if (bottom) {
175 175 scrollToBottom();
176 176 }
177 177
178 178 type = POST_ADDED;
179 179 }
180 180
181 181 processNewPost(post);
182 182
183 183 return type;
184 184 }
185 185
186 186 /**
187 187 * Initiate a blinking animation on a node to show it was updated.
188 188 */
189 189 function blink(node) {
190 190 var blinkCount = 2;
191 191
192 192 var nodeToAnimate = node;
193 193 for (var i = 0; i < blinkCount; i++) {
194 194 nodeToAnimate = nodeToAnimate.fadeTo('fast', 0.5).fadeTo('fast', 1.0);
195 195 }
196 196 }
197 197
198 198 function isPageBottom() {
199 199 var scroll = $(window).scrollTop() / ($(document).height()
200 200 - $(window).height());
201 201
202 202 return scroll == 1
203 203 }
204 204
205 205 function enableJsUpdate() {
206 206 setInterval(getThreadDiff, JS_AUTOUPDATE_PERIOD);
207 207 return true;
208 208 }
209 209
210 210 function initAutoupdate() {
211 211 if (location.protocol === 'https:') {
212 212 return enableJsUpdate();
213 213 } else {
214 214 if (connectWebsocket()) {
215 215 return true;
216 216 } else {
217 217 return enableJsUpdate();
218 218 }
219 219 }
220 220 }
221 221
222 222 function getReplyCount() {
223 223 return $('.thread').children(CLASS_POST).length
224 224 }
225 225
226 226 function getImageCount() {
227 227 return $('.thread').find('img').length
228 228 }
229 229
230 230 /**
231 231 * Update post count, images count and last update time in the metadata
232 232 * panel.
233 233 */
234 234 function updateMetadataPanel() {
235 235 var replyCountField = $('#reply-count');
236 236 var imageCountField = $('#image-count');
237 237
238 238 replyCountField.text(getReplyCount());
239 239 imageCountField.text(getImageCount());
240 240
241 241 var lastUpdate = $('.post:last').children('.post-info').first()
242 242 .children('.pub_time').first().html();
243 243 if (lastUpdate !== '') {
244 244 var lastUpdateField = $('#last-update');
245 245 lastUpdateField.html(lastUpdate);
246 246 blink(lastUpdateField);
247 247 }
248 248
249 249 blink(replyCountField);
250 250 blink(imageCountField);
251 251 }
252 252
253 253 /**
254 254 * Update bumplimit progress bar
255 255 */
256 256 function updateBumplimitProgress(postDelta) {
257 257 var progressBar = $('#bumplimit_progress');
258 258 if (progressBar) {
259 259 var postsToLimitElement = $('#left_to_limit');
260 260
261 261 var oldPostsToLimit = parseInt(postsToLimitElement.text());
262 262 var postCount = getReplyCount();
263 263 var bumplimit = postCount - postDelta + oldPostsToLimit;
264 264
265 265 var newPostsToLimit = bumplimit - postCount;
266 266 if (newPostsToLimit <= 0) {
267 267 $('.bar-bg').remove();
268 268 } else {
269 269 postsToLimitElement.text(newPostsToLimit);
270 270 progressBar.width((100 - postCount / bumplimit * 100.0) + '%');
271 271 }
272 272 }
273 273 }
274 274
275 275 /**
276 276 * Show 'new posts' text in the title if the document is not visible to a user
277 277 */
278 278 function showNewPostsTitle(newPostCount) {
279 279 if (document.hidden) {
280 280 if (documentOriginalTitle === '') {
281 281 documentOriginalTitle = document.title;
282 282 }
283 283 unreadPosts = unreadPosts + newPostCount;
284 284
285 285 var newTitle = '* ';
286 286 if (unreadPosts > 0) {
287 287 newTitle += '[' + unreadPosts + '] ';
288 288 }
289 289 newTitle += documentOriginalTitle;
290 290
291 291 document.title = newTitle;
292 292
293 293 document.addEventListener('visibilitychange', function() {
294 294 if (documentOriginalTitle !== '') {
295 295 document.title = documentOriginalTitle;
296 296 documentOriginalTitle = '';
297 297 unreadPosts = 0;
298 298 }
299 299
300 300 document.removeEventListener('visibilitychange', null);
301 301 });
302 302 }
303 303 }
304 304
305 305 /**
306 306 * Clear all entered values in the form fields
307 307 */
308 308 function resetForm(form) {
309 309 form.find('input:text, input:password, input:file, select, textarea').val('');
310 310 form.find('input:radio, input:checkbox')
311 311 .removeAttr('checked').removeAttr('selected');
312 312 $('.file_wrap').find('.file-thumb').remove();
313 313 $('#preview-text').hide();
314 314 }
315 315
316 316 /**
317 317 * When the form is posted, this method will be run as a callback
318 318 */
319 319 function updateOnPost(response, statusText, xhr, form) {
320 320 var json = $.parseJSON(response);
321 321 var status = json.status;
322 322
323 323 showAsErrors(form, '');
324 324
325 325 if (status === 'ok') {
326 326 resetFormPosition();
327 327 resetForm(form);
328 328 getThreadDiff();
329 329 scrollToBottom();
330 330 } else {
331 331 var errors = json.errors;
332 332 for (var i = 0; i < errors.length; i++) {
333 333 var fieldErrors = errors[i];
334 334
335 335 var error = fieldErrors.errors;
336 336
337 337 showAsErrors(form, error);
338 338 }
339 339 }
340 340 }
341 341
342 342 /**
343 343 * Show text in the errors row of the form.
344 344 * @param form
345 345 * @param text
346 346 */
347 347 function showAsErrors(form, text) {
348 348 form.children('.form-errors').remove();
349 349
350 350 if (text.length > 0) {
351 351 var errorList = $('<div class="form-errors">' + text + '<div>');
352 352 errorList.appendTo(form);
353 353 }
354 354 }
355 355
356 356 /**
357 357 * Run js methods that are usually run on the document, on the new post
358 358 */
359 359 function processNewPost(post) {
360 360 addRefLinkPreview(post[0]);
361 361 highlightCode(post);
362 362 blink(post);
363 363 }
364 364
365 365 $(document).ready(function(){
366 366 if (initAutoupdate()) {
367 367 // Post form data over AJAX
368 368 var threadId = $('div.thread').children('.post').first().attr('id');
369 369
370 370 var form = $('#form');
371 371
372 372 if (form.length > 0) {
373 373 var options = {
374 374 beforeSubmit: function(arr, $form, options) {
375 375 showAsErrors($('form'), gettext('Sending message...'));
376 376 },
377 377 success: updateOnPost,
378 error: function() {
379 showAsErrors($('form'), gettext('Server error!'));
380 },
378 381 url: '/api/add_post/' + threadId + '/'
379 382 };
380 383
381 384 form.ajaxForm(options);
382 385
383 386 resetForm(form);
384 387 }
385 388 }
386 389
387 390 $('#autoupdate').click(getThreadDiff);
388 391 });
@@ -1,135 +1,178 b''
1 1 from django.core.paginator import Paginator
2 2 from django.test import TestCase
3
3 4 from boards import settings
4 5 from boards.models import Tag, Post, Thread
5 6
6 7
7 8 class PostTests(TestCase):
8 9
9 10 def _create_post(self):
10 11 tag, created = Tag.objects.get_or_create(name='test_tag')
11 12 return Post.objects.create_post(title='title', text='text',
12 13 tags=[tag])
13 14
14 15 def test_post_add(self):
15 16 """Test adding post"""
16 17
17 18 post = self._create_post()
18 19
19 20 self.assertIsNotNone(post, 'No post was created.')
20 21 self.assertEqual('test_tag', post.get_thread().tags.all()[0].name,
21 22 'No tags were added to the post.')
22 23
23 24 def test_delete_post(self):
24 25 """Test post deletion"""
25 26
26 27 post = self._create_post()
27 28 post_id = post.id
28 29
29 30 post.delete()
30 31
31 32 self.assertFalse(Post.objects.filter(id=post_id).exists())
32 33
33 34 def test_delete_thread(self):
34 35 """Test thread deletion"""
35 36
36 37 opening_post = self._create_post()
37 38 thread = opening_post.get_thread()
38 39 reply = Post.objects.create_post("", "", thread=thread)
39 40
40 41 thread.delete()
41 42
42 43 self.assertFalse(Post.objects.filter(id=reply.id).exists(),
43 44 'Reply was not deleted with the thread.')
44 45 self.assertFalse(Post.objects.filter(id=opening_post.id).exists(),
45 46 'Opening post was not deleted with the thread.')
46 47
47 48 def test_post_to_thread(self):
48 49 """Test adding post to a thread"""
49 50
50 51 op = self._create_post()
51 52 post = Post.objects.create_post("", "", thread=op.get_thread())
52 53
53 54 self.assertIsNotNone(post, 'Reply to thread wasn\'t created')
54 55 self.assertEqual(op.get_thread().last_edit_time, post.pub_time,
55 56 'Post\'s create time doesn\'t match thread last edit'
56 57 ' time')
57 58
58 59 def test_delete_posts_by_ip(self):
59 60 """Test deleting posts with the given ip"""
60 61
61 62 post = self._create_post()
62 63 post_id = post.id
63 64
64 65 Post.objects.delete_posts_by_ip('0.0.0.0')
65 66
66 67 self.assertFalse(Post.objects.filter(id=post_id).exists())
67 68
68 69 def test_get_thread(self):
69 70 """Test getting all posts of a thread"""
70 71
71 72 opening_post = self._create_post()
72 73
73 74 for i in range(2):
74 75 Post.objects.create_post('title', 'text',
75 76 thread=opening_post.get_thread())
76 77
77 78 thread = opening_post.get_thread()
78 79
79 80 self.assertEqual(3, thread.get_replies().count())
80 81
81 82 def test_create_post_with_tag(self):
82 83 """Test adding tag to post"""
83 84
84 85 tag = Tag.objects.create(name='test_tag')
85 86 post = Post.objects.create_post(title='title', text='text', tags=[tag])
86 87
87 88 thread = post.get_thread()
88 89 self.assertIsNotNone(post, 'Post not created')
89 90 self.assertTrue(tag in thread.tags.all(), 'Tag not added to thread')
90 91
91 92 def test_thread_max_count(self):
92 93 """Test deletion of old posts when the max thread count is reached"""
93 94
94 95 for i in range(settings.get_int('Messages', 'MaxThreadCount') + 1):
95 96 self._create_post()
96 97
97 98 self.assertEqual(settings.get_int('Messages', 'MaxThreadCount'),
98 99 len(Thread.objects.filter(archived=False)))
99 100
100 101 def test_pages(self):
101 102 """Test that the thread list is properly split into pages"""
102 103
103 104 for i in range(settings.get_int('Messages', 'MaxThreadCount')):
104 105 self._create_post()
105 106
106 107 all_threads = Thread.objects.filter(archived=False)
107 108
108 109 paginator = Paginator(Thread.objects.filter(archived=False),
109 110 settings.get_int('View', 'ThreadsPerPage'))
110 111 posts_in_second_page = paginator.page(2).object_list
111 112 first_post = posts_in_second_page[0]
112 113
113 114 self.assertEqual(all_threads[settings.get_int('View', 'ThreadsPerPage')].id,
114 115 first_post.id)
115 116
116 117 def test_thread_replies(self):
117 118 """
118 119 Tests that the replies can be queried from a thread in all possible
119 120 ways.
120 121 """
121 122
122 123 tag = Tag.objects.create(name='test_tag')
123 124 opening_post = Post.objects.create_post(title='title', text='text',
124 125 tags=[tag])
125 126 thread = opening_post.get_thread()
126 127
127 reply1 = Post.objects.create_post(title='title', text='text', thread=thread)
128 reply2 = Post.objects.create_post(title='title', text='text', thread=thread)
128 Post.objects.create_post(title='title', text='text', thread=thread)
129 Post.objects.create_post(title='title', text='text', thread=thread)
129 130
130 131 replies = thread.get_replies()
131 132 self.assertTrue(len(replies) > 0, 'No replies found for thread.')
132 133
133 134 replies = thread.get_replies(view_fields_only=True)
134 135 self.assertTrue(len(replies) > 0,
135 136 'No replies found for thread with view fields only.')
137
138 def test_bumplimit(self):
139 """
140 Tests that the thread bumpable status is changed and post uids and
141 last update times are updated across all post threads.
142 """
143
144 op1 = Post.objects.create_post(title='title', text='text')
145 op2 = Post.objects.create_post(title='title', text='text')
146
147 thread1 = op1.get_thread()
148 thread1.max_posts = 5
149 thread1.save()
150
151 uid_1 = op1.uid
152 uid_2 = op2.uid
153
154 # Create multi reply
155 Post.objects.create_post(
156 title='title', text='text', thread=thread1,
157 opening_posts=[op1, op2])
158 thread_update_time_2 = op2.get_thread().last_edit_time
159 for i in range(6):
160 Post.objects.create_post(title='title', text='text',
161 thread=thread1)
162
163 self.assertFalse(op1.get_thread().can_bump(),
164 'Thread is bumpable when it should not be.')
165 self.assertTrue(op2.get_thread().can_bump(),
166 'Thread is not bumpable when it should be.')
167 self.assertNotEqual(
168 uid_1, Post.objects.get(id=op1.id).uid,
169 'UID of the first OP should be changed but it is not.')
170 self.assertEqual(
171 uid_2, Post.objects.get(id=op2.id).uid,
172 'UID of the first OP should not be changed but it is.')
173
174 self.assertNotEqual(
175 thread_update_time_2,
176 Thread.objects.get(id=op2.get_thread().id).last_edit_time,
177 'Thread last update time should change when the other thread '
178 'changes status.')
@@ -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/all_threads.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, form: ThreadForm=None):
47 47 page = request.GET.get('page', DEFAULT_PAGE)
48 48
49 49 params = self.get_context_data(request=request)
50 50
51 51 if not form:
52 52 form = ThreadForm(error_class=PlainErrorList)
53 53
54 54 self.settings_manager = get_settings_manager(request)
55 55 paginator = get_paginator(self.get_threads(),
56 56 settings.get_int('View', 'ThreadsPerPage'))
57 57 paginator.current_page = int(page)
58 58
59 59 try:
60 60 threads = paginator.page(page).object_list
61 61 except EmptyPage:
62 62 raise Http404()
63 63
64 64 params[PARAMETER_THREADS] = threads
65 65 params[CONTEXT_FORM] = form
66 66 params[PARAMETER_BANNERS] = Banner.objects.order_by('-id').all()
67 67
68 68 self.get_page_context(paginator, params, page)
69 69
70 70 return render(request, TEMPLATE, params)
71 71
72 72 def post(self, request):
73 73 form = ThreadForm(request.POST, request.FILES,
74 74 error_class=PlainErrorList)
75 75 form.session = request.session
76 76
77 77 if form.is_valid():
78 78 return self.create_thread(request, form)
79 79 if form.need_to_ban:
80 80 # Ban user because he is suspected to be a bot
81 81 self._ban_current_user(request)
82 82
83 83 return self.get(request, form)
84 84
85 85 def get_page_context(self, paginator, params, page):
86 86 """
87 87 Get pagination context variables
88 88 """
89 89
90 90 params[PARAMETER_PAGINATOR] = paginator
91 91 current_page = paginator.page(int(page))
92 92 params[PARAMETER_CURRENT_PAGE] = current_page
93 93 if current_page.has_previous():
94 94 params[PARAMETER_PREV_LINK] = self.get_previous_page_link(
95 95 current_page)
96 96 if current_page.has_next():
97 97 params[PARAMETER_NEXT_LINK] = self.get_next_page_link(current_page)
98 98
99 99 def get_previous_page_link(self, current_page):
100 100 return reverse('index') + '?page=' \
101 101 + str(current_page.previous_page_number())
102 102
103 103 def get_next_page_link(self, current_page):
104 104 return reverse('index') + '?page=' \
105 105 + str(current_page.next_page_number())
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 ip=ip, tags=tags, threads=threads)
154 ip=ip, tags=tags, opening_posts=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_absolute_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,138 +1,138 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 CONTEXT_MODE = 'mode'
25 25 CONTEXT_OP = 'opening_post'
26 26
27 27 FORM_TITLE = 'title'
28 28 FORM_TEXT = 'text'
29 29 FORM_IMAGE = 'image'
30 30 FORM_THREADS = 'threads'
31 31
32 32
33 33 class ThreadView(BaseBoardView, PostMixin, FormMixin):
34 34
35 35 def get(self, request, post_id, form: PostForm=None):
36 36 try:
37 37 opening_post = Post.objects.get(id=post_id)
38 38 except ObjectDoesNotExist:
39 39 raise Http404
40 40
41 41 # If this is not OP, don't show it as it is
42 42 if not opening_post.is_opening():
43 43 return redirect(opening_post.get_thread().get_opening_post()
44 44 .get_absolute_url())
45 45
46 46 if not form:
47 47 form = PostForm(error_class=PlainErrorList)
48 48
49 49 thread_to_show = opening_post.get_thread()
50 50
51 51 params = dict()
52 52
53 53 params[CONTEXT_FORM] = form
54 54 params[CONTEXT_LASTUPDATE] = str(thread_to_show.last_edit_time)
55 55 params[CONTEXT_THREAD] = thread_to_show
56 56 params[CONTEXT_MODE] = self.get_mode()
57 57 params[CONTEXT_OP] = opening_post
58 58
59 59 if settings.get_bool('External', 'WebsocketsEnabled'):
60 60 token_time = format(timezone.now(), u'U')
61 61
62 62 params[CONTEXT_WS_TIME] = token_time
63 63 params[CONTEXT_WS_TOKEN] = utils.get_websocket_token(
64 64 timestamp=token_time)
65 65 params[CONTEXT_WS_PROJECT] = neboard.settings.CENTRIFUGE_PROJECT_ID
66 66 params[CONTEXT_WS_HOST] = request.get_host().split(':')[0]
67 67 params[CONTEXT_WS_PORT] = neboard.settings.CENTRIFUGE_PORT
68 68
69 69 params.update(self.get_data(thread_to_show))
70 70
71 71 return render(request, self.get_template(), params)
72 72
73 73 def post(self, request, post_id):
74 74 opening_post = get_object_or_404(Post, id=post_id)
75 75
76 76 # If this is not OP, don't show it as it is
77 77 if not opening_post.is_opening():
78 78 raise Http404
79 79
80 80 if not opening_post.get_thread().archived:
81 81 form = PostForm(request.POST, request.FILES,
82 82 error_class=PlainErrorList)
83 83 form.session = request.session
84 84
85 85 if form.is_valid():
86 86 return self.new_post(request, form, opening_post)
87 87 if form.need_to_ban:
88 88 # Ban user because he is suspected to be a bot
89 89 self._ban_current_user(request)
90 90
91 91 return self.get(request, post_id, form)
92 92
93 93 def new_post(self, request, form: PostForm, opening_post: Post=None,
94 94 html_response=True):
95 95 """
96 96 Adds a new post (in thread or as a reply).
97 97 """
98 98
99 99 ip = utils.get_client_ip(request)
100 100
101 101 data = form.cleaned_data
102 102
103 103 title = data[FORM_TITLE]
104 104 text = data[FORM_TEXT]
105 105 image = form.get_image()
106 106 threads = data[FORM_THREADS]
107 107
108 108 text = self._remove_invalid_links(text)
109 109
110 110 post_thread = opening_post.get_thread()
111 111
112 112 post = Post.objects.create_post(title=title, text=text, image=image,
113 113 thread=post_thread, ip=ip,
114 threads=threads)
114 opening_posts=threads)
115 115 post.notify_clients()
116 116
117 117 if html_response:
118 118 if opening_post:
119 119 return redirect(post.get_absolute_url())
120 120 else:
121 121 return post
122 122
123 123 def get_data(self, thread) -> dict:
124 124 """
125 125 Returns context params for the view.
126 126 """
127 127
128 128 return dict()
129 129
130 130 def get_template(self) -> str:
131 131 """
132 132 Gets template to show the thread mode on.
133 133 """
134 134
135 135 pass
136 136
137 137 def get_mode(self) -> str:
138 138 pass
General Comments 0
You need to be logged in to leave comments. Login now