##// END OF EJS Templates
Trigger post update on a new post instead of sending the post itself. This fixed issues with posts sent in a fixed locale instead of a different locale for each client
neko259 -
r895:ffaaf497 default
parent child Browse files
Show More
@@ -1,445 +1,442 b''
1 from datetime import datetime, timedelta, date
1 from datetime import datetime, timedelta, date
2 from datetime import time as dtime
2 from datetime import time as dtime
3 import logging
3 import logging
4 import re
4 import re
5
5
6 from adjacent import Client
6 from adjacent import Client
7 from django.core.cache import cache
7 from django.core.cache import cache
8 from django.core.urlresolvers import reverse
8 from django.core.urlresolvers import reverse
9 from django.db import models, transaction
9 from django.db import models, transaction
10 from django.db.models import TextField
10 from django.db.models import TextField
11 from django.template import RequestContext
11 from django.template import RequestContext
12 from django.template.loader import render_to_string
12 from django.template.loader import render_to_string
13 from django.utils import timezone
13 from django.utils import timezone
14
14
15 from boards import settings
15 from boards import settings
16 from boards.mdx_neboard import bbcode_extended
16 from boards.mdx_neboard import bbcode_extended
17 from boards.models import PostImage
17 from boards.models import PostImage
18 from boards.models.base import Viewable
18 from boards.models.base import Viewable
19 from boards.models.thread import Thread
19 from boards.models.thread import Thread
20 from boards.utils import datetime_to_epoch
20 from boards.utils import datetime_to_epoch
21
21
22
22
23 WS_CHANNEL_THREAD = "thread:"
23 WS_CHANNEL_THREAD = "thread:"
24
24
25 APP_LABEL_BOARDS = 'boards'
25 APP_LABEL_BOARDS = 'boards'
26
26
27 CACHE_KEY_PPD = 'ppd'
27 CACHE_KEY_PPD = 'ppd'
28 CACHE_KEY_POST_URL = 'post_url'
28 CACHE_KEY_POST_URL = 'post_url'
29
29
30 POSTS_PER_DAY_RANGE = 7
30 POSTS_PER_DAY_RANGE = 7
31
31
32 BAN_REASON_AUTO = 'Auto'
32 BAN_REASON_AUTO = 'Auto'
33
33
34 IMAGE_THUMB_SIZE = (200, 150)
34 IMAGE_THUMB_SIZE = (200, 150)
35
35
36 TITLE_MAX_LENGTH = 200
36 TITLE_MAX_LENGTH = 200
37
37
38 # TODO This should be removed
38 # TODO This should be removed
39 NO_IP = '0.0.0.0'
39 NO_IP = '0.0.0.0'
40
40
41 # TODO Real user agent should be saved instead of this
41 # TODO Real user agent should be saved instead of this
42 UNKNOWN_UA = ''
42 UNKNOWN_UA = ''
43
43
44 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
44 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
45
45
46 PARAMETER_TRUNCATED = 'truncated'
46 PARAMETER_TRUNCATED = 'truncated'
47 PARAMETER_TAG = 'tag'
47 PARAMETER_TAG = 'tag'
48 PARAMETER_OFFSET = 'offset'
48 PARAMETER_OFFSET = 'offset'
49 PARAMETER_DIFF_TYPE = 'type'
49 PARAMETER_DIFF_TYPE = 'type'
50
50
51 DIFF_TYPE_HTML = 'html'
51 DIFF_TYPE_HTML = 'html'
52 DIFF_TYPE_JSON = 'json'
52 DIFF_TYPE_JSON = 'json'
53
53
54 PREPARSE_PATTERNS = {
54 PREPARSE_PATTERNS = {
55 r'>>(\d+)': r'[post]\1[/post]', # Reflink ">>123"
55 r'>>(\d+)': r'[post]\1[/post]', # Reflink ">>123"
56 r'^>(.+)': r'[quote]\1[/quote]', # Quote ">text"
56 r'^>(.+)': r'[quote]\1[/quote]', # Quote ">text"
57 r'^//(.+)': r'[comment]\1[/comment]', # Comment "//text"
57 r'^//(.+)': r'[comment]\1[/comment]', # Comment "//text"
58 }
58 }
59
59
60
60
61 class PostManager(models.Manager):
61 class PostManager(models.Manager):
62 def create_post(self, title, text, image=None, thread=None, ip=NO_IP,
62 def create_post(self, title, text, image=None, thread=None, ip=NO_IP,
63 tags=None):
63 tags=None):
64 """
64 """
65 Creates new post
65 Creates new post
66 """
66 """
67
67
68 if not tags:
68 if not tags:
69 tags = []
69 tags = []
70
70
71 posting_time = timezone.now()
71 posting_time = timezone.now()
72 if not thread:
72 if not thread:
73 thread = Thread.objects.create(bump_time=posting_time,
73 thread = Thread.objects.create(bump_time=posting_time,
74 last_edit_time=posting_time)
74 last_edit_time=posting_time)
75 new_thread = True
75 new_thread = True
76 else:
76 else:
77 new_thread = False
77 new_thread = False
78
78
79 pre_text = self._preparse_text(text)
79 pre_text = self._preparse_text(text)
80
80
81 post = self.create(title=title,
81 post = self.create(title=title,
82 text=pre_text,
82 text=pre_text,
83 pub_time=posting_time,
83 pub_time=posting_time,
84 thread_new=thread,
84 thread_new=thread,
85 poster_ip=ip,
85 poster_ip=ip,
86 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
86 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
87 # last!
87 # last!
88 last_edit_time=posting_time)
88 last_edit_time=posting_time)
89
89
90 logger = logging.getLogger('boards.post.create')
90 logger = logging.getLogger('boards.post.create')
91
91
92 logger.info('Created post {} by {}'.format(
92 logger.info('Created post {} by {}'.format(
93 post, post.poster_ip))
93 post, post.poster_ip))
94
94
95 if image:
95 if image:
96 post_image = PostImage.objects.create(image=image)
96 post_image = PostImage.objects.create(image=image)
97 post.images.add(post_image)
97 post.images.add(post_image)
98 logger.info('Created image #{} for post #{}'.format(
98 logger.info('Created image #{} for post #{}'.format(
99 post_image.id, post.id))
99 post_image.id, post.id))
100
100
101 thread.replies.add(post)
101 thread.replies.add(post)
102 list(map(thread.add_tag, tags))
102 list(map(thread.add_tag, tags))
103
103
104 if new_thread:
104 if new_thread:
105 Thread.objects.process_oldest_threads()
105 Thread.objects.process_oldest_threads()
106 else:
106 else:
107 thread.bump()
107 thread.bump()
108 thread.last_edit_time = posting_time
108 thread.last_edit_time = posting_time
109 thread.save()
109 thread.save()
110
110
111 self.connect_replies(post)
111 self.connect_replies(post)
112
112
113 return post
113 return post
114
114
115 def delete_posts_by_ip(self, ip):
115 def delete_posts_by_ip(self, ip):
116 """
116 """
117 Deletes all posts of the author with same IP
117 Deletes all posts of the author with same IP
118 """
118 """
119
119
120 posts = self.filter(poster_ip=ip)
120 posts = self.filter(poster_ip=ip)
121 for post in posts:
121 for post in posts:
122 post.delete()
122 post.delete()
123
123
124 def connect_replies(self, post):
124 def connect_replies(self, post):
125 """
125 """
126 Connects replies to a post to show them as a reflink map
126 Connects replies to a post to show them as a reflink map
127 """
127 """
128
128
129 for reply_number in re.finditer(REGEX_REPLY, post.get_raw_text()):
129 for reply_number in re.finditer(REGEX_REPLY, post.get_raw_text()):
130 post_id = reply_number.group(1)
130 post_id = reply_number.group(1)
131 ref_post = self.filter(id=post_id)
131 ref_post = self.filter(id=post_id)
132 if ref_post.count() > 0:
132 if ref_post.count() > 0:
133 referenced_post = ref_post[0]
133 referenced_post = ref_post[0]
134 referenced_post.referenced_posts.add(post)
134 referenced_post.referenced_posts.add(post)
135 referenced_post.last_edit_time = post.pub_time
135 referenced_post.last_edit_time = post.pub_time
136 referenced_post.build_refmap()
136 referenced_post.build_refmap()
137 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
137 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
138
138
139 referenced_thread = referenced_post.get_thread()
139 referenced_thread = referenced_post.get_thread()
140 referenced_thread.last_edit_time = post.pub_time
140 referenced_thread.last_edit_time = post.pub_time
141 referenced_thread.save(update_fields=['last_edit_time'])
141 referenced_thread.save(update_fields=['last_edit_time'])
142
142
143 def get_posts_per_day(self):
143 def get_posts_per_day(self):
144 """
144 """
145 Gets average count of posts per day for the last 7 days
145 Gets average count of posts per day for the last 7 days
146 """
146 """
147
147
148 day_end = date.today()
148 day_end = date.today()
149 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
149 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
150
150
151 cache_key = CACHE_KEY_PPD + str(day_end)
151 cache_key = CACHE_KEY_PPD + str(day_end)
152 ppd = cache.get(cache_key)
152 ppd = cache.get(cache_key)
153 if ppd:
153 if ppd:
154 return ppd
154 return ppd
155
155
156 day_time_start = timezone.make_aware(datetime.combine(
156 day_time_start = timezone.make_aware(datetime.combine(
157 day_start, dtime()), timezone.get_current_timezone())
157 day_start, dtime()), timezone.get_current_timezone())
158 day_time_end = timezone.make_aware(datetime.combine(
158 day_time_end = timezone.make_aware(datetime.combine(
159 day_end, dtime()), timezone.get_current_timezone())
159 day_end, dtime()), timezone.get_current_timezone())
160
160
161 posts_per_period = float(self.filter(
161 posts_per_period = float(self.filter(
162 pub_time__lte=day_time_end,
162 pub_time__lte=day_time_end,
163 pub_time__gte=day_time_start).count())
163 pub_time__gte=day_time_start).count())
164
164
165 ppd = posts_per_period / POSTS_PER_DAY_RANGE
165 ppd = posts_per_period / POSTS_PER_DAY_RANGE
166
166
167 cache.set(cache_key, ppd)
167 cache.set(cache_key, ppd)
168 return ppd
168 return ppd
169
169
170 def _preparse_text(self, text):
170 def _preparse_text(self, text):
171 """
171 """
172 Preparses text to change patterns like '>>' to a proper bbcode
172 Preparses text to change patterns like '>>' to a proper bbcode
173 tags.
173 tags.
174 """
174 """
175
175
176 for key, value in PREPARSE_PATTERNS.items():
176 for key, value in PREPARSE_PATTERNS.items():
177 text = re.sub(key, value, text, flags=re.MULTILINE)
177 text = re.sub(key, value, text, flags=re.MULTILINE)
178
178
179 return text
179 return text
180
180
181
181
182 class Post(models.Model, Viewable):
182 class Post(models.Model, Viewable):
183 """A post is a message."""
183 """A post is a message."""
184
184
185 objects = PostManager()
185 objects = PostManager()
186
186
187 class Meta:
187 class Meta:
188 app_label = APP_LABEL_BOARDS
188 app_label = APP_LABEL_BOARDS
189 ordering = ('id',)
189 ordering = ('id',)
190
190
191 title = models.CharField(max_length=TITLE_MAX_LENGTH)
191 title = models.CharField(max_length=TITLE_MAX_LENGTH)
192 pub_time = models.DateTimeField()
192 pub_time = models.DateTimeField()
193 text = TextField(blank=True, null=True)
193 text = TextField(blank=True, null=True)
194 _text_rendered = TextField(blank=True, null=True, editable=False)
194 _text_rendered = TextField(blank=True, null=True, editable=False)
195
195
196 images = models.ManyToManyField(PostImage, null=True, blank=True,
196 images = models.ManyToManyField(PostImage, null=True, blank=True,
197 related_name='ip+', db_index=True)
197 related_name='ip+', db_index=True)
198
198
199 poster_ip = models.GenericIPAddressField()
199 poster_ip = models.GenericIPAddressField()
200 poster_user_agent = models.TextField()
200 poster_user_agent = models.TextField()
201
201
202 thread_new = models.ForeignKey('Thread', null=True, default=None,
202 thread_new = models.ForeignKey('Thread', null=True, default=None,
203 db_index=True)
203 db_index=True)
204 last_edit_time = models.DateTimeField()
204 last_edit_time = models.DateTimeField()
205
205
206 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
206 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
207 null=True,
207 null=True,
208 blank=True, related_name='rfp+',
208 blank=True, related_name='rfp+',
209 db_index=True)
209 db_index=True)
210 refmap = models.TextField(null=True, blank=True)
210 refmap = models.TextField(null=True, blank=True)
211
211
212 def __str__(self):
212 def __str__(self):
213 return 'P#{}/{}'.format(self.id, self.title)
213 return 'P#{}/{}'.format(self.id, self.title)
214
214
215 def get_title(self):
215 def get_title(self):
216 """
216 """
217 Gets original post title or part of its text.
217 Gets original post title or part of its text.
218 """
218 """
219
219
220 title = self.title
220 title = self.title
221 if not title:
221 if not title:
222 title = self.get_text()
222 title = self.get_text()
223
223
224 return title
224 return title
225
225
226 def build_refmap(self):
226 def build_refmap(self):
227 """
227 """
228 Builds a replies map string from replies list. This is a cache to stop
228 Builds a replies map string from replies list. This is a cache to stop
229 the server from recalculating the map on every post show.
229 the server from recalculating the map on every post show.
230 """
230 """
231 map_string = ''
231 map_string = ''
232
232
233 first = True
233 first = True
234 for refpost in self.referenced_posts.all():
234 for refpost in self.referenced_posts.all():
235 if not first:
235 if not first:
236 map_string += ', '
236 map_string += ', '
237 map_string += '<a href="%s">&gt;&gt;%s</a>' % (refpost.get_url(),
237 map_string += '<a href="%s">&gt;&gt;%s</a>' % (refpost.get_url(),
238 refpost.id)
238 refpost.id)
239 first = False
239 first = False
240
240
241 self.refmap = map_string
241 self.refmap = map_string
242
242
243 def get_sorted_referenced_posts(self):
243 def get_sorted_referenced_posts(self):
244 return self.refmap
244 return self.refmap
245
245
246 def is_referenced(self):
246 def is_referenced(self):
247 if not self.refmap:
247 if not self.refmap:
248 return False
248 return False
249 else:
249 else:
250 return len(self.refmap) > 0
250 return len(self.refmap) > 0
251
251
252 def is_opening(self):
252 def is_opening(self):
253 """
253 """
254 Checks if this is an opening post or just a reply.
254 Checks if this is an opening post or just a reply.
255 """
255 """
256
256
257 return self.get_thread().get_opening_post_id() == self.id
257 return self.get_thread().get_opening_post_id() == self.id
258
258
259 @transaction.atomic
259 @transaction.atomic
260 def add_tag(self, tag):
260 def add_tag(self, tag):
261 edit_time = timezone.now()
261 edit_time = timezone.now()
262
262
263 thread = self.get_thread()
263 thread = self.get_thread()
264 thread.add_tag(tag)
264 thread.add_tag(tag)
265 self.last_edit_time = edit_time
265 self.last_edit_time = edit_time
266 self.save(update_fields=['last_edit_time'])
266 self.save(update_fields=['last_edit_time'])
267
267
268 thread.last_edit_time = edit_time
268 thread.last_edit_time = edit_time
269 thread.save(update_fields=['last_edit_time'])
269 thread.save(update_fields=['last_edit_time'])
270
270
271 @transaction.atomic
271 @transaction.atomic
272 def remove_tag(self, tag):
272 def remove_tag(self, tag):
273 edit_time = timezone.now()
273 edit_time = timezone.now()
274
274
275 thread = self.get_thread()
275 thread = self.get_thread()
276 thread.remove_tag(tag)
276 thread.remove_tag(tag)
277 self.last_edit_time = edit_time
277 self.last_edit_time = edit_time
278 self.save(update_fields=['last_edit_time'])
278 self.save(update_fields=['last_edit_time'])
279
279
280 thread.last_edit_time = edit_time
280 thread.last_edit_time = edit_time
281 thread.save(update_fields=['last_edit_time'])
281 thread.save(update_fields=['last_edit_time'])
282
282
283 def get_url(self, thread=None):
283 def get_url(self, thread=None):
284 """
284 """
285 Gets full url to the post.
285 Gets full url to the post.
286 """
286 """
287
287
288 cache_key = CACHE_KEY_POST_URL + str(self.id)
288 cache_key = CACHE_KEY_POST_URL + str(self.id)
289 link = cache.get(cache_key)
289 link = cache.get(cache_key)
290
290
291 if not link:
291 if not link:
292 if not thread:
292 if not thread:
293 thread = self.get_thread()
293 thread = self.get_thread()
294
294
295 opening_id = thread.get_opening_post_id()
295 opening_id = thread.get_opening_post_id()
296
296
297 if self.id != opening_id:
297 if self.id != opening_id:
298 link = reverse('thread', kwargs={
298 link = reverse('thread', kwargs={
299 'post_id': opening_id}) + '#' + str(self.id)
299 'post_id': opening_id}) + '#' + str(self.id)
300 else:
300 else:
301 link = reverse('thread', kwargs={'post_id': self.id})
301 link = reverse('thread', kwargs={'post_id': self.id})
302
302
303 cache.set(cache_key, link)
303 cache.set(cache_key, link)
304
304
305 return link
305 return link
306
306
307 def get_thread(self):
307 def get_thread(self):
308 """
308 """
309 Gets post's thread.
309 Gets post's thread.
310 """
310 """
311
311
312 return self.thread_new
312 return self.thread_new
313
313
314 def get_referenced_posts(self):
314 def get_referenced_posts(self):
315 return self.referenced_posts.only('id', 'thread_new')
315 return self.referenced_posts.only('id', 'thread_new')
316
316
317 def get_view(self, moderator=False, need_open_link=False,
317 def get_view(self, moderator=False, need_open_link=False,
318 truncated=False, *args, **kwargs):
318 truncated=False, *args, **kwargs):
319 if 'is_opening' in kwargs:
319 if 'is_opening' in kwargs:
320 is_opening = kwargs['is_opening']
320 is_opening = kwargs['is_opening']
321 else:
321 else:
322 is_opening = self.is_opening()
322 is_opening = self.is_opening()
323
323
324 if 'thread' in kwargs:
324 if 'thread' in kwargs:
325 thread = kwargs['thread']
325 thread = kwargs['thread']
326 else:
326 else:
327 thread = self.get_thread()
327 thread = self.get_thread()
328
328
329 if 'can_bump' in kwargs:
329 if 'can_bump' in kwargs:
330 can_bump = kwargs['can_bump']
330 can_bump = kwargs['can_bump']
331 else:
331 else:
332 can_bump = thread.can_bump()
332 can_bump = thread.can_bump()
333
333
334 if is_opening:
334 if is_opening:
335 opening_post_id = self.id
335 opening_post_id = self.id
336 else:
336 else:
337 opening_post_id = thread.get_opening_post_id()
337 opening_post_id = thread.get_opening_post_id()
338
338
339 return render_to_string('boards/post.html', {
339 return render_to_string('boards/post.html', {
340 'post': self,
340 'post': self,
341 'moderator': moderator,
341 'moderator': moderator,
342 'is_opening': is_opening,
342 'is_opening': is_opening,
343 'thread': thread,
343 'thread': thread,
344 'bumpable': can_bump,
344 'bumpable': can_bump,
345 'need_open_link': need_open_link,
345 'need_open_link': need_open_link,
346 'truncated': truncated,
346 'truncated': truncated,
347 'opening_post_id': opening_post_id,
347 'opening_post_id': opening_post_id,
348 })
348 })
349
349
350 def get_first_image(self):
350 def get_first_image(self):
351 return self.images.earliest('id')
351 return self.images.earliest('id')
352
352
353 def delete(self, using=None):
353 def delete(self, using=None):
354 """
354 """
355 Deletes all post images and the post itself. If the post is opening,
355 Deletes all post images and the post itself. If the post is opening,
356 thread with all posts is deleted.
356 thread with all posts is deleted.
357 """
357 """
358
358
359 self.images.all().delete()
359 self.images.all().delete()
360
360
361 if self.is_opening():
361 if self.is_opening():
362 self.get_thread().delete()
362 self.get_thread().delete()
363 else:
363 else:
364 thread = self.get_thread()
364 thread = self.get_thread()
365 thread.last_edit_time = timezone.now()
365 thread.last_edit_time = timezone.now()
366 thread.save()
366 thread.save()
367
367
368 super(Post, self).delete(using)
368 super(Post, self).delete(using)
369
369
370 logging.getLogger('boards.post.delete').info(
370 logging.getLogger('boards.post.delete').info(
371 'Deleted post {}'.format(self))
371 'Deleted post {}'.format(self))
372
372
373 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
373 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
374 include_last_update=False):
374 include_last_update=False):
375 """
375 """
376 Gets post HTML or JSON data that can be rendered on a page or used by
376 Gets post HTML or JSON data that can be rendered on a page or used by
377 API.
377 API.
378 """
378 """
379
379
380 if format_type == DIFF_TYPE_HTML:
380 if format_type == DIFF_TYPE_HTML:
381 context = RequestContext(request)
381 context = RequestContext(request)
382 context['post'] = self
382 context['post'] = self
383 if PARAMETER_TRUNCATED in request.GET:
383 if PARAMETER_TRUNCATED in request.GET:
384 context[PARAMETER_TRUNCATED] = True
384 context[PARAMETER_TRUNCATED] = True
385
385
386 # TODO Use dict here
386 # TODO Use dict here
387 return render_to_string('boards/api_post.html',
387 return render_to_string('boards/api_post.html',
388 context_instance=context)
388 context_instance=context)
389 elif format_type == DIFF_TYPE_JSON:
389 elif format_type == DIFF_TYPE_JSON:
390 post_json = {
390 post_json = {
391 'id': self.id,
391 'id': self.id,
392 'title': self.title,
392 'title': self.title,
393 'text': self._text_rendered,
393 'text': self._text_rendered,
394 }
394 }
395 if self.images.exists():
395 if self.images.exists():
396 post_image = self.get_first_image()
396 post_image = self.get_first_image()
397 post_json['image'] = post_image.image.url
397 post_json['image'] = post_image.image.url
398 post_json['image_preview'] = post_image.image.url_200x150
398 post_json['image_preview'] = post_image.image.url_200x150
399 if include_last_update:
399 if include_last_update:
400 post_json['bump_time'] = datetime_to_epoch(
400 post_json['bump_time'] = datetime_to_epoch(
401 self.thread_new.bump_time)
401 self.thread_new.bump_time)
402 return post_json
402 return post_json
403
403
404 def send_to_websocket(self, request, recursive=True):
404 def send_to_websocket(self, request, recursive=True):
405 """
405 """
406 Sends post HTML data to the thread web socket.
406 Sends post HTML data to the thread web socket.
407 """
407 """
408
408
409 if not settings.WEBSOCKETS_ENABLED:
409 if not settings.WEBSOCKETS_ENABLED:
410 return
410 return
411
411
412 client = Client()
412 client = Client()
413
413
414 channel_name = WS_CHANNEL_THREAD + str(self.get_thread()
414 channel_name = WS_CHANNEL_THREAD + str(self.get_thread()
415 .get_opening_post_id())
415 .get_opening_post_id())
416 client.publish(channel_name, {
416 client.publish(channel_name, {
417 'html': self.get_post_data(
417 'notification_type': 'new_post',
418 format_type=DIFF_TYPE_HTML,
419 request=request),
420 'diff_type': 'added' if recursive else 'updated',
421 })
418 })
422 client.send()
419 client.send()
423
420
424 logger = logging.getLogger('boards.post.websocket')
421 logger = logging.getLogger('boards.post.websocket')
425
422
426 logger.info('Sent post #{} to channel {}'.format(self.id, channel_name))
423 logger.info('Sent post #{} to channel {}'.format(self.id, channel_name))
427
424
428 if recursive:
425 if recursive:
429 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
426 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
430 post_id = reply_number.group(1)
427 post_id = reply_number.group(1)
431 ref_post = Post.objects.filter(id=post_id)[0]
428 ref_post = Post.objects.filter(id=post_id)[0]
432
429
433 ref_post.send_to_websocket(request, recursive=False)
430 ref_post.send_to_websocket(request, recursive=False)
434
431
435 def save(self, force_insert=False, force_update=False, using=None,
432 def save(self, force_insert=False, force_update=False, using=None,
436 update_fields=None):
433 update_fields=None):
437 self._text_rendered = bbcode_extended(self.get_raw_text())
434 self._text_rendered = bbcode_extended(self.get_raw_text())
438
435
439 super().save(force_insert, force_update, using, update_fields)
436 super().save(force_insert, force_update, using, update_fields)
440
437
441 def get_text(self):
438 def get_text(self):
442 return self._text_rendered
439 return self._text_rendered
443
440
444 def get_raw_text(self):
441 def get_raw_text(self):
445 return self.text
442 return self.text
@@ -1,333 +1,335 b''
1 /*
1 /*
2 @licstart The following is the entire license notice for the
2 @licstart The following is the entire license notice for the
3 JavaScript code in this page.
3 JavaScript code in this page.
4
4
5
5
6 Copyright (C) 2013 neko259
6 Copyright (C) 2013-2014 neko259
7
7
8 The JavaScript code in this page is free software: you can
8 The JavaScript code in this page is free software: you can
9 redistribute it and/or modify it under the terms of the GNU
9 redistribute it and/or modify it under the terms of the GNU
10 General Public License (GNU GPL) as published by the Free Software
10 General Public License (GNU GPL) as published by the Free Software
11 Foundation, either version 3 of the License, or (at your option)
11 Foundation, either version 3 of the License, or (at your option)
12 any later version. The code is distributed WITHOUT ANY WARRANTY;
12 any later version. The code is distributed WITHOUT ANY WARRANTY;
13 without even the implied warranty of MERCHANTABILITY or FITNESS
13 without even the implied warranty of MERCHANTABILITY or FITNESS
14 FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
14 FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
15
15
16 As additional permission under GNU GPL version 3 section 7, you
16 As additional permission under GNU GPL version 3 section 7, you
17 may distribute non-source (e.g., minimized or compacted) forms of
17 may distribute non-source (e.g., minimized or compacted) forms of
18 that code without the copy of the GNU GPL normally required by
18 that code without the copy of the GNU GPL normally required by
19 section 4, provided you include this license notice and a URL
19 section 4, provided you include this license notice and a URL
20 through which recipients can access the Corresponding Source.
20 through which recipients can access the Corresponding Source.
21
21
22 @licend The above is the entire license notice
22 @licend The above is the entire license notice
23 for the JavaScript code in this page.
23 for the JavaScript code in this page.
24 */
24 */
25
25
26 var wsUrl = 'ws://localhost:9090/connection/websocket';
27 var wsUser = '';
26 var wsUser = '';
28
27
29 var loading = false;
28 var loading = false;
30 var unreadPosts = 0;
29 var unreadPosts = 0;
31 var documentOriginalTitle = '';
30 var documentOriginalTitle = '';
32
31
33 // Thread ID does not change, can be stored one time
32 // Thread ID does not change, can be stored one time
34 var threadId = $('div.thread').children('.post').first().attr('id');
33 var threadId = $('div.thread').children('.post').first().attr('id');
35
34
35 /**
36 * Connect to websocket server and subscribe to thread updates. On any update we
37 * request a thread diff.
38 *
39 * @returns {boolean} true if connected, false otherwise
40 */
36 function connectWebsocket() {
41 function connectWebsocket() {
37 var metapanel = $('.metapanel')[0];
42 var metapanel = $('.metapanel')[0];
38
43
39 var wsHost = metapanel.getAttribute('data-ws-host');
44 var wsHost = metapanel.getAttribute('data-ws-host');
40 var wsPort = metapanel.getAttribute('data-ws-port');
45 var wsPort = metapanel.getAttribute('data-ws-port');
41
46
42 if (wsHost.length > 0 && wsPort.length > 0)
47 if (wsHost.length > 0 && wsPort.length > 0)
43 var centrifuge = new Centrifuge({
48 var centrifuge = new Centrifuge({
44 "url": 'ws://' + wsHost + ':' + wsPort + "/connection/websocket",
49 "url": 'ws://' + wsHost + ':' + wsPort + "/connection/websocket",
45 "project": metapanel.getAttribute('data-ws-project'),
50 "project": metapanel.getAttribute('data-ws-project'),
46 "user": wsUser,
51 "user": wsUser,
47 "timestamp": metapanel.getAttribute('data-last-update'),
52 "timestamp": metapanel.getAttribute('data-last-update'),
48 "token": metapanel.getAttribute('data-ws-token'),
53 "token": metapanel.getAttribute('data-ws-token'),
49 "debug": false
54 "debug": false
50 });
55 });
51
56
52 centrifuge.on('error', function(error_message) {
57 centrifuge.on('error', function(error_message) {
53 console.log("Error connecting to websocket server.");
58 console.log("Error connecting to websocket server.");
54 return false;
59 return false;
55 });
60 });
56
61
57 centrifuge.on('connect', function() {
62 centrifuge.on('connect', function() {
58 var channelName = 'thread:' + threadId;
63 var channelName = 'thread:' + threadId;
59 centrifuge.subscribe(channelName, function(message) {
64 centrifuge.subscribe(channelName, function(message) {
60 var postHtml = message.data['html'];
65 getThreadDiff();
61 var isAdded = (message.data['diff_type'] === 'added');
62
63 if (postHtml) {
64 updatePost(postHtml, isAdded);
65 }
66 });
66 });
67
67
68 // For the case we closed the browser and missed some updates
68 // For the case we closed the browser and missed some updates
69 getThreadDiff();
69 getThreadDiff();
70 $('#autoupdate').text('[+]');
70 $('#autoupdate').text('[+]');
71 });
71 });
72
72
73 centrifuge.connect();
73 centrifuge.connect();
74
74
75 return true;
75 return true;
76 }
76 }
77
77
78 /**
78 /**
79 * Get diff of the posts from the current thread timestamp.
79 * Get diff of the posts from the current thread timestamp.
80 * This is required if the browser was closed and some post updates were
80 * This is required if the browser was closed and some post updates were
81 * missed.
81 * missed.
82 */
82 */
83 function getThreadDiff() {
83 function getThreadDiff() {
84 var lastUpdateTime = $('.metapanel').attr('data-last-update');
84 var lastUpdateTime = $('.metapanel').attr('data-last-update');
85
85
86 var diffUrl = '/api/diff_thread/' + threadId + '/' + lastUpdateTime + '/';
86 var diffUrl = '/api/diff_thread/' + threadId + '/' + lastUpdateTime + '/';
87
87
88 $.getJSON(diffUrl)
88 $.getJSON(diffUrl)
89 .success(function(data) {
89 .success(function(data) {
90 var bottom = isPageBottom();
91 var addedPosts = data.added;
90 var addedPosts = data.added;
92
91
93 for (var i = 0; i < addedPosts.length; i++) {
92 for (var i = 0; i < addedPosts.length; i++) {
94 var postText = addedPosts[i];
93 var postText = addedPosts[i];
95 var post = $(postText);
94 var post = $(postText);
96
95
97 updatePost(post, true)
96 updatePost(post)
98
97
99 lastPost = post;
98 lastPost = post;
100 }
99 }
101
100
102 var updatedPosts = data.updated;
101 var updatedPosts = data.updated;
103
102
104 for (var i = 0; i < updatedPosts.length; i++) {
103 for (var i = 0; i < updatedPosts.length; i++) {
105 var postText = updatedPosts[i];
104 var postText = updatedPosts[i];
106 var post = $(postText);
105 var post = $(postText);
107
106
108 updatePost(post, false)
107 updatePost(post)
109 }
108 }
110
109
111 // TODO Process removed posts if any
110 // TODO Process removed posts if any
111 $('.metapanel').attr('data-last-update', data.last_update);
112 })
112 })
113 }
113 }
114
114
115 /**
115 /**
116 * Add or update the post on thml page.
116 * Add or update the post on html page.
117 */
117 */
118 function updatePost(postHtml, isAdded) {
118 function updatePost(postHtml) {
119 // This needs to be set on start because the page is scrolled after posts
119 // This needs to be set on start because the page is scrolled after posts
120 // are added or updated
120 // are added or updated
121 var bottom = isPageBottom();
121 var bottom = isPageBottom();
122
122
123 var post = $(postHtml);
123 var post = $(postHtml);
124
124
125 var threadPosts = $('div.thread').children('.post');
125 var threadBlock = $('div.thread');
126
126
127 var lastUpdate = '';
127 var lastUpdate = '';
128
128
129 if (isAdded) {
129 var postId = post.attr('id');
130
131 // If the post already exists, replace it. Otherwise add as a new one.
132 var existingPosts = threadBlock.children('.post[id=' + postId + ']');
133
134 if (existingPosts.size() > 0) {
135 existingPosts.replaceWith(post);
136 } else {
137 var threadPosts = threadBlock.children('.post');
130 var lastPost = threadPosts.last();
138 var lastPost = threadPosts.last();
131
139
132 post.appendTo(lastPost.parent());
140 post.appendTo(lastPost.parent());
133
141
134 updateBumplimitProgress(1);
142 updateBumplimitProgress(1);
135 showNewPostsTitle(1);
143 showNewPostsTitle(1);
136
144
137 lastUpdate = post.children('.post-info').first()
145 lastUpdate = post.children('.post-info').first()
138 .children('.pub_time').first().text();
146 .children('.pub_time').first().text();
139
147
140 if (bottom) {
148 if (bottom) {
141 scrollToBottom();
149 scrollToBottom();
142 }
150 }
143 } else {
144 var postId = post.attr('id');
145
146 var oldPost = $('div.thread').children('.post[id=' + postId + ']');
147
148 oldPost.replaceWith(post);
149 }
151 }
150
152
151 processNewPost(post);
153 processNewPost(post);
152 updateMetadataPanel(lastUpdate)
154 updateMetadataPanel(lastUpdate)
153 }
155 }
154
156
155 /**
157 /**
156 * Initiate a blinking animation on a node to show it was updated.
158 * Initiate a blinking animation on a node to show it was updated.
157 */
159 */
158 function blink(node) {
160 function blink(node) {
159 var blinkCount = 2;
161 var blinkCount = 2;
160
162
161 var nodeToAnimate = node;
163 var nodeToAnimate = node;
162 for (var i = 0; i < blinkCount; i++) {
164 for (var i = 0; i < blinkCount; i++) {
163 nodeToAnimate = nodeToAnimate.fadeTo('fast', 0.5).fadeTo('fast', 1.0);
165 nodeToAnimate = nodeToAnimate.fadeTo('fast', 0.5).fadeTo('fast', 1.0);
164 }
166 }
165 }
167 }
166
168
167 function isPageBottom() {
169 function isPageBottom() {
168 var scroll = $(window).scrollTop() / ($(document).height()
170 var scroll = $(window).scrollTop() / ($(document).height()
169 - $(window).height());
171 - $(window).height());
170
172
171 return scroll == 1
173 return scroll == 1
172 }
174 }
173
175
174 function initAutoupdate() {
176 function initAutoupdate() {
175 return connectWebsocket();
177 return connectWebsocket();
176 }
178 }
177
179
178 function getReplyCount() {
180 function getReplyCount() {
179 return $('.thread').children('.post').length
181 return $('.thread').children('.post').length
180 }
182 }
181
183
182 function getImageCount() {
184 function getImageCount() {
183 return $('.thread').find('img').length
185 return $('.thread').find('img').length
184 }
186 }
185
187
186 /**
188 /**
187 * Update post count, images count and last update time in the metadata
189 * Update post count, images count and last update time in the metadata
188 * panel.
190 * panel.
189 */
191 */
190 function updateMetadataPanel(lastUpdate) {
192 function updateMetadataPanel(lastUpdate) {
191 var replyCountField = $('#reply-count');
193 var replyCountField = $('#reply-count');
192 var imageCountField = $('#image-count');
194 var imageCountField = $('#image-count');
193
195
194 replyCountField.text(getReplyCount());
196 replyCountField.text(getReplyCount());
195 imageCountField.text(getImageCount());
197 imageCountField.text(getImageCount());
196
198
197 if (lastUpdate !== '') {
199 if (lastUpdate !== '') {
198 var lastUpdateField = $('#last-update');
200 var lastUpdateField = $('#last-update');
199 lastUpdateField.text(lastUpdate);
201 lastUpdateField.text(lastUpdate);
200 blink(lastUpdateField);
202 blink(lastUpdateField);
201 }
203 }
202
204
203 blink(replyCountField);
205 blink(replyCountField);
204 blink(imageCountField);
206 blink(imageCountField);
205 }
207 }
206
208
207 /**
209 /**
208 * Update bumplimit progress bar
210 * Update bumplimit progress bar
209 */
211 */
210 function updateBumplimitProgress(postDelta) {
212 function updateBumplimitProgress(postDelta) {
211 var progressBar = $('#bumplimit_progress');
213 var progressBar = $('#bumplimit_progress');
212 if (progressBar) {
214 if (progressBar) {
213 var postsToLimitElement = $('#left_to_limit');
215 var postsToLimitElement = $('#left_to_limit');
214
216
215 var oldPostsToLimit = parseInt(postsToLimitElement.text());
217 var oldPostsToLimit = parseInt(postsToLimitElement.text());
216 var postCount = getReplyCount();
218 var postCount = getReplyCount();
217 var bumplimit = postCount - postDelta + oldPostsToLimit;
219 var bumplimit = postCount - postDelta + oldPostsToLimit;
218
220
219 var newPostsToLimit = bumplimit - postCount;
221 var newPostsToLimit = bumplimit - postCount;
220 if (newPostsToLimit <= 0) {
222 if (newPostsToLimit <= 0) {
221 $('.bar-bg').remove();
223 $('.bar-bg').remove();
222 $('.thread').children('.post').addClass('dead_post');
224 $('.thread').children('.post').addClass('dead_post');
223 } else {
225 } else {
224 postsToLimitElement.text(newPostsToLimit);
226 postsToLimitElement.text(newPostsToLimit);
225 progressBar.width((100 - postCount / bumplimit * 100.0) + '%');
227 progressBar.width((100 - postCount / bumplimit * 100.0) + '%');
226 }
228 }
227 }
229 }
228 }
230 }
229
231
230 /**
232 /**
231 * Show 'new posts' text in the title if the document is not visible to a user
233 * Show 'new posts' text in the title if the document is not visible to a user
232 */
234 */
233 function showNewPostsTitle(newPostCount) {
235 function showNewPostsTitle(newPostCount) {
234 if (document.hidden) {
236 if (document.hidden) {
235 if (documentOriginalTitle === '') {
237 if (documentOriginalTitle === '') {
236 documentOriginalTitle = document.title;
238 documentOriginalTitle = document.title;
237 }
239 }
238 unreadPosts = unreadPosts + newPostCount;
240 unreadPosts = unreadPosts + newPostCount;
239 document.title = '[' + unreadPosts + '] ' + documentOriginalTitle;
241 document.title = '[' + unreadPosts + '] ' + documentOriginalTitle;
240
242
241 document.addEventListener('visibilitychange', function() {
243 document.addEventListener('visibilitychange', function() {
242 if (documentOriginalTitle !== '') {
244 if (documentOriginalTitle !== '') {
243 document.title = documentOriginalTitle;
245 document.title = documentOriginalTitle;
244 documentOriginalTitle = '';
246 documentOriginalTitle = '';
245 unreadPosts = 0;
247 unreadPosts = 0;
246 }
248 }
247
249
248 document.removeEventListener('visibilitychange', null);
250 document.removeEventListener('visibilitychange', null);
249 });
251 });
250 }
252 }
251 }
253 }
252
254
253 /**
255 /**
254 * Clear all entered values in the form fields
256 * Clear all entered values in the form fields
255 */
257 */
256 function resetForm(form) {
258 function resetForm(form) {
257 form.find('input:text, input:password, input:file, select, textarea').val('');
259 form.find('input:text, input:password, input:file, select, textarea').val('');
258 form.find('input:radio, input:checkbox')
260 form.find('input:radio, input:checkbox')
259 .removeAttr('checked').removeAttr('selected');
261 .removeAttr('checked').removeAttr('selected');
260 $('.file_wrap').find('.file-thumb').remove();
262 $('.file_wrap').find('.file-thumb').remove();
261 }
263 }
262
264
263 /**
265 /**
264 * When the form is posted, this method will be run as a callback
266 * When the form is posted, this method will be run as a callback
265 */
267 */
266 function updateOnPost(response, statusText, xhr, form) {
268 function updateOnPost(response, statusText, xhr, form) {
267 var json = $.parseJSON(response);
269 var json = $.parseJSON(response);
268 var status = json.status;
270 var status = json.status;
269
271
270 showAsErrors(form, '');
272 showAsErrors(form, '');
271
273
272 if (status === 'ok') {
274 if (status === 'ok') {
273 resetForm(form);
275 resetForm(form);
274 } else {
276 } else {
275 var errors = json.errors;
277 var errors = json.errors;
276 for (var i = 0; i < errors.length; i++) {
278 for (var i = 0; i < errors.length; i++) {
277 var fieldErrors = errors[i];
279 var fieldErrors = errors[i];
278
280
279 var error = fieldErrors.errors;
281 var error = fieldErrors.errors;
280
282
281 showAsErrors(form, error);
283 showAsErrors(form, error);
282 }
284 }
283 }
285 }
284
286
285 scrollToBottom();
287 scrollToBottom();
286 }
288 }
287
289
288 /**
290 /**
289 * Show text in the errors row of the form.
291 * Show text in the errors row of the form.
290 * @param form
292 * @param form
291 * @param text
293 * @param text
292 */
294 */
293 function showAsErrors(form, text) {
295 function showAsErrors(form, text) {
294 form.children('.form-errors').remove();
296 form.children('.form-errors').remove();
295
297
296 if (text.length > 0) {
298 if (text.length > 0) {
297 var errorList = $('<div class="form-errors">' + text
299 var errorList = $('<div class="form-errors">' + text
298 + '<div>');
300 + '<div>');
299 errorList.appendTo(form);
301 errorList.appendTo(form);
300 }
302 }
301 }
303 }
302
304
303 /**
305 /**
304 * Run js methods that are usually run on the document, on the new post
306 * Run js methods that are usually run on the document, on the new post
305 */
307 */
306 function processNewPost(post) {
308 function processNewPost(post) {
307 addRefLinkPreview(post[0]);
309 addRefLinkPreview(post[0]);
308 highlightCode(post);
310 highlightCode(post);
309 blink(post);
311 blink(post);
310 }
312 }
311
313
312 $(document).ready(function(){
314 $(document).ready(function(){
313 if ('WebSocket' in window) {
315 if ('WebSocket' in window) {
314 if (initAutoupdate()) {
316 if (initAutoupdate()) {
315 // Post form data over AJAX
317 // Post form data over AJAX
316 var threadId = $('div.thread').children('.post').first().attr('id');
318 var threadId = $('div.thread').children('.post').first().attr('id');
317
319
318 var form = $('#form');
320 var form = $('#form');
319
321
320 var options = {
322 var options = {
321 beforeSubmit: function(arr, $form, options) {
323 beforeSubmit: function(arr, $form, options) {
322 showAsErrors($('form'), gettext('Sending message...'));
324 showAsErrors($('form'), gettext('Sending message...'));
323 },
325 },
324 success: updateOnPost,
326 success: updateOnPost,
325 url: '/api/add_post/' + threadId + '/'
327 url: '/api/add_post/' + threadId + '/'
326 };
328 };
327
329
328 form.ajaxForm(options);
330 form.ajaxForm(options);
329
331
330 resetForm(form);
332 resetForm(form);
331 }
333 }
332 }
334 }
333 });
335 });
@@ -1,157 +1,157 b''
1 from django.core.urlresolvers import reverse
1 from django.core.urlresolvers import reverse
2 from django.db import transaction
2 from django.db import transaction
3 from django.http import Http404
3 from django.http import Http404
4 from django.shortcuts import get_object_or_404, render, redirect
4 from django.shortcuts import get_object_or_404, render, redirect
5 from django.views.generic.edit import FormMixin
5 from django.views.generic.edit import FormMixin
6
6
7 from boards import utils, settings
7 from boards import utils, settings
8 from boards.forms import PostForm, PlainErrorList
8 from boards.forms import PostForm, PlainErrorList
9 from boards.models import Post, Ban
9 from boards.models import Post, Ban
10 from boards.views.banned import BannedView
10 from boards.views.banned import BannedView
11 from boards.views.base import BaseBoardView, CONTEXT_FORM
11 from boards.views.base import BaseBoardView, CONTEXT_FORM
12 from boards.views.posting_mixin import PostMixin
12 from boards.views.posting_mixin import PostMixin
13 import neboard
13 import neboard
14
14
15 TEMPLATE_GALLERY = 'boards/thread_gallery.html'
15 TEMPLATE_GALLERY = 'boards/thread_gallery.html'
16 TEMPLATE_NORMAL = 'boards/thread.html'
16 TEMPLATE_NORMAL = 'boards/thread.html'
17
17
18 CONTEXT_POSTS = 'posts'
18 CONTEXT_POSTS = 'posts'
19 CONTEXT_OP = 'opening_post'
19 CONTEXT_OP = 'opening_post'
20 CONTEXT_BUMPLIMIT_PRG = 'bumplimit_progress'
20 CONTEXT_BUMPLIMIT_PRG = 'bumplimit_progress'
21 CONTEXT_POSTS_LEFT = 'posts_left'
21 CONTEXT_POSTS_LEFT = 'posts_left'
22 CONTEXT_LASTUPDATE = "last_update"
22 CONTEXT_LASTUPDATE = "last_update"
23 CONTEXT_MAX_REPLIES = 'max_replies'
23 CONTEXT_MAX_REPLIES = 'max_replies'
24 CONTEXT_THREAD = 'thread'
24 CONTEXT_THREAD = 'thread'
25 CONTEXT_BUMPABLE = 'bumpable'
25 CONTEXT_BUMPABLE = 'bumpable'
26 CONTEXT_WS_TOKEN = 'ws_token'
26 CONTEXT_WS_TOKEN = 'ws_token'
27 CONTEXT_WS_PROJECT = 'ws_project'
27 CONTEXT_WS_PROJECT = 'ws_project'
28 CONTEXT_WS_HOST = 'ws_host'
28 CONTEXT_WS_HOST = 'ws_host'
29 CONTEXT_WS_PORT = 'ws_port'
29 CONTEXT_WS_PORT = 'ws_port'
30
30
31 FORM_TITLE = 'title'
31 FORM_TITLE = 'title'
32 FORM_TEXT = 'text'
32 FORM_TEXT = 'text'
33 FORM_IMAGE = 'image'
33 FORM_IMAGE = 'image'
34
34
35 MODE_GALLERY = 'gallery'
35 MODE_GALLERY = 'gallery'
36 MODE_NORMAL = 'normal'
36 MODE_NORMAL = 'normal'
37
37
38
38
39 class ThreadView(BaseBoardView, PostMixin, FormMixin):
39 class ThreadView(BaseBoardView, PostMixin, FormMixin):
40
40
41 def get(self, request, post_id, mode=MODE_NORMAL, form=None):
41 def get(self, request, post_id, mode=MODE_NORMAL, form=None):
42 try:
42 try:
43 opening_post = Post.objects.filter(id=post_id).only('thread_new')[0]
43 opening_post = Post.objects.filter(id=post_id).only('thread_new')[0]
44 except IndexError:
44 except IndexError:
45 raise Http404
45 raise Http404
46
46
47 # If this is not OP, don't show it as it is
47 # If this is not OP, don't show it as it is
48 if not opening_post or not opening_post.is_opening():
48 if not opening_post or not opening_post.is_opening():
49 raise Http404
49 raise Http404
50
50
51 if not form:
51 if not form:
52 form = PostForm(error_class=PlainErrorList)
52 form = PostForm(error_class=PlainErrorList)
53
53
54 thread_to_show = opening_post.get_thread()
54 thread_to_show = opening_post.get_thread()
55
55
56 context = self.get_context_data(request=request)
56 context = self.get_context_data(request=request)
57
57
58 context[CONTEXT_FORM] = form
58 context[CONTEXT_FORM] = form
59 context[CONTEXT_LASTUPDATE] = str(utils.datetime_to_epoch(
59 context[CONTEXT_LASTUPDATE] = str(utils.datetime_to_epoch(
60 thread_to_show.last_edit_time))
60 thread_to_show.last_edit_time))
61 context[CONTEXT_THREAD] = thread_to_show
61 context[CONTEXT_THREAD] = thread_to_show
62 context[CONTEXT_MAX_REPLIES] = settings.MAX_POSTS_PER_THREAD
62 context[CONTEXT_MAX_REPLIES] = settings.MAX_POSTS_PER_THREAD
63
63
64 if settings.WEBSOCKETS_ENABLED:
64 if settings.WEBSOCKETS_ENABLED:
65 context[CONTEXT_WS_TOKEN] = utils.get_websocket_token(
65 context[CONTEXT_WS_TOKEN] = utils.get_websocket_token(
66 timestamp=context[CONTEXT_LASTUPDATE])
66 timestamp=context[CONTEXT_LASTUPDATE])
67 context[CONTEXT_WS_PROJECT] = neboard.settings.CENTRIFUGE_PROJECT_ID
67 context[CONTEXT_WS_PROJECT] = neboard.settings.CENTRIFUGE_PROJECT_ID
68 context[CONTEXT_WS_HOST] = request.get_host().split(':')[0]
68 context[CONTEXT_WS_HOST] = request.get_host().split(':')[0]
69 context[CONTEXT_WS_PORT] = neboard.settings.CENTRIFUGE_PORT
69 context[CONTEXT_WS_PORT] = neboard.settings.CENTRIFUGE_PORT
70
70
71 # TODO Move this to subclasses: NormalThreadView, GalleryThreadView etc
71 # TODO Move this to subclasses: NormalThreadView, GalleryThreadView etc
72 if MODE_NORMAL == mode:
72 if MODE_NORMAL == mode:
73 bumpable = thread_to_show.can_bump()
73 bumpable = thread_to_show.can_bump()
74 context[CONTEXT_BUMPABLE] = bumpable
74 context[CONTEXT_BUMPABLE] = bumpable
75 if bumpable:
75 if bumpable:
76 left_posts = settings.MAX_POSTS_PER_THREAD \
76 left_posts = settings.MAX_POSTS_PER_THREAD \
77 - thread_to_show.get_reply_count()
77 - thread_to_show.get_reply_count()
78 context[CONTEXT_POSTS_LEFT] = left_posts
78 context[CONTEXT_POSTS_LEFT] = left_posts
79 context[CONTEXT_BUMPLIMIT_PRG] = str(
79 context[CONTEXT_BUMPLIMIT_PRG] = str(
80 float(left_posts) / settings.MAX_POSTS_PER_THREAD * 100)
80 float(left_posts) / settings.MAX_POSTS_PER_THREAD * 100)
81
81
82 context[CONTEXT_OP] = opening_post
82 context[CONTEXT_OP] = opening_post
83
83
84 document = TEMPLATE_NORMAL
84 document = TEMPLATE_NORMAL
85 elif MODE_GALLERY == mode:
85 elif MODE_GALLERY == mode:
86 context[CONTEXT_POSTS] = thread_to_show.get_replies_with_images(
86 context[CONTEXT_POSTS] = thread_to_show.get_replies_with_images(
87 view_fields_only=True)
87 view_fields_only=True)
88
88
89 document = TEMPLATE_GALLERY
89 document = TEMPLATE_GALLERY
90 else:
90 else:
91 raise Http404
91 raise Http404
92
92
93 # TODO Use dict here
93 # TODO Use dict here
94 return render(request, document, context_instance=context)
94 return render(request, document, context_instance=context)
95
95
96 def post(self, request, post_id, mode=MODE_NORMAL):
96 def post(self, request, post_id, mode=MODE_NORMAL):
97 opening_post = get_object_or_404(Post, id=post_id)
97 opening_post = get_object_or_404(Post, id=post_id)
98
98
99 # If this is not OP, don't show it as it is
99 # If this is not OP, don't show it as it is
100 if not opening_post.is_opening():
100 if not opening_post.is_opening():
101 raise Http404
101 raise Http404
102
102
103 if not opening_post.get_thread().archived:
103 if not opening_post.get_thread().archived:
104 form = PostForm(request.POST, request.FILES,
104 form = PostForm(request.POST, request.FILES,
105 error_class=PlainErrorList)
105 error_class=PlainErrorList)
106 form.session = request.session
106 form.session = request.session
107
107
108 if form.is_valid():
108 if form.is_valid():
109 return self.new_post(request, form, opening_post)
109 return self.new_post(request, form, opening_post)
110 if form.need_to_ban:
110 if form.need_to_ban:
111 # Ban user because he is suspected to be a bot
111 # Ban user because he is suspected to be a bot
112 self._ban_current_user(request)
112 self._ban_current_user(request)
113
113
114 return self.get(request, post_id, mode, form)
114 return self.get(request, post_id, mode, form)
115
115
116 @transaction.atomic
116 @transaction.atomic
117 def new_post(self, request, form, opening_post=None, html_response=True):
117 def new_post(self, request, form, opening_post=None, html_response=True):
118 """Add a new post (in thread or as a reply)."""
118 """Add a new post (in thread or as a reply)."""
119
119
120 ip = utils.get_client_ip(request)
120 ip = utils.get_client_ip(request)
121 is_banned = Ban.objects.filter(ip=ip).exists()
121 is_banned = Ban.objects.filter(ip=ip).exists()
122
122
123 if is_banned:
123 if is_banned:
124 if html_response:
124 if html_response:
125 return redirect(BannedView().as_view())
125 return redirect(BannedView().as_view())
126 else:
126 else:
127 return None
127 return None
128
128
129 data = form.cleaned_data
129 data = form.cleaned_data
130
130
131 title = data[FORM_TITLE]
131 title = data[FORM_TITLE]
132 text = data[FORM_TEXT]
132 text = data[FORM_TEXT]
133
133
134 text = self._remove_invalid_links(text)
134 text = self._remove_invalid_links(text)
135
135
136 if FORM_IMAGE in list(data.keys()):
136 if FORM_IMAGE in list(data.keys()):
137 image = data[FORM_IMAGE]
137 image = data[FORM_IMAGE]
138 else:
138 else:
139 image = None
139 image = None
140
140
141 tags = []
141 tags = []
142
142
143 post_thread = opening_post.get_thread()
143 post_thread = opening_post.get_thread()
144
144
145 post = Post.objects.create_post(title=title, text=text, image=image,
145 post = Post.objects.create_post(title=title, text=text, image=image,
146 thread=post_thread, ip=ip, tags=tags)
146 thread=post_thread, ip=ip, tags=tags)
147 post.send_to_websocket(request)
147 post.send_to_websocket(request, recursive=False)
148
148
149 thread_to_show = (opening_post.id if opening_post else post.id)
149 thread_to_show = (opening_post.id if opening_post else post.id)
150
150
151 if html_response:
151 if html_response:
152 if opening_post:
152 if opening_post:
153 return redirect(
153 return redirect(
154 reverse('thread', kwargs={'post_id': thread_to_show})
154 reverse('thread', kwargs={'post_id': thread_to_show})
155 + '#' + str(post.id))
155 + '#' + str(post.id))
156 else:
156 else:
157 return post
157 return post
General Comments 0
You need to be logged in to leave comments. Login now