##// END OF EJS Templates
Fixed sending posts to websockets. Cleaned up new post view code
neko259 -
r916:2aafa436 default
parent child Browse files
Show More
@@ -1,438 +1,438 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 WS_NOTIFICATION_TYPE_NEW_POST = 'new_post'
22 WS_NOTIFICATION_TYPE_NEW_POST = 'new_post'
23 WS_NOTIFICATION_TYPE = 'notification_type'
23 WS_NOTIFICATION_TYPE = 'notification_type'
24
24
25 WS_CHANNEL_THREAD = "thread:"
25 WS_CHANNEL_THREAD = "thread:"
26
26
27 APP_LABEL_BOARDS = 'boards'
27 APP_LABEL_BOARDS = 'boards'
28
28
29 CACHE_KEY_PPD = 'ppd'
29 CACHE_KEY_PPD = 'ppd'
30 CACHE_KEY_POST_URL = 'post_url'
30 CACHE_KEY_POST_URL = 'post_url'
31
31
32 POSTS_PER_DAY_RANGE = 7
32 POSTS_PER_DAY_RANGE = 7
33
33
34 BAN_REASON_AUTO = 'Auto'
34 BAN_REASON_AUTO = 'Auto'
35
35
36 IMAGE_THUMB_SIZE = (200, 150)
36 IMAGE_THUMB_SIZE = (200, 150)
37
37
38 TITLE_MAX_LENGTH = 200
38 TITLE_MAX_LENGTH = 200
39
39
40 # TODO This should be removed
40 # TODO This should be removed
41 NO_IP = '0.0.0.0'
41 NO_IP = '0.0.0.0'
42
42
43 # TODO Real user agent should be saved instead of this
43 # TODO Real user agent should be saved instead of this
44 UNKNOWN_UA = ''
44 UNKNOWN_UA = ''
45
45
46 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
46 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
47
47
48 PARAMETER_TRUNCATED = 'truncated'
48 PARAMETER_TRUNCATED = 'truncated'
49 PARAMETER_TAG = 'tag'
49 PARAMETER_TAG = 'tag'
50 PARAMETER_OFFSET = 'offset'
50 PARAMETER_OFFSET = 'offset'
51 PARAMETER_DIFF_TYPE = 'type'
51 PARAMETER_DIFF_TYPE = 'type'
52
52
53 DIFF_TYPE_HTML = 'html'
53 DIFF_TYPE_HTML = 'html'
54 DIFF_TYPE_JSON = 'json'
54 DIFF_TYPE_JSON = 'json'
55
55
56 PREPARSE_PATTERNS = {
56 PREPARSE_PATTERNS = {
57 r'>>(\d+)': r'[post]\1[/post]', # Reflink ">>123"
57 r'>>(\d+)': r'[post]\1[/post]', # Reflink ">>123"
58 r'^>(.+)': r'[quote]\1[/quote]', # Quote ">text"
58 r'^>(.+)': r'[quote]\1[/quote]', # Quote ">text"
59 r'^//(.+)': r'[comment]\1[/comment]', # Comment "//text"
59 r'^//(.+)': r'[comment]\1[/comment]', # Comment "//text"
60 }
60 }
61
61
62
62
63 class PostManager(models.Manager):
63 class PostManager(models.Manager):
64 @transaction.atomic
64 @transaction.atomic
65 def create_post(self, title: str, text: str, image=None, thread=None,
65 def create_post(self, title: str, text: str, image=None, thread=None,
66 ip=NO_IP, tags: list=None):
66 ip=NO_IP, tags: list=None):
67 """
67 """
68 Creates new post
68 Creates new post
69 """
69 """
70
70
71 if not tags:
71 if not tags:
72 tags = []
72 tags = []
73
73
74 posting_time = timezone.now()
74 posting_time = timezone.now()
75 if not thread:
75 if not thread:
76 thread = Thread.objects.create(bump_time=posting_time,
76 thread = Thread.objects.create(bump_time=posting_time,
77 last_edit_time=posting_time)
77 last_edit_time=posting_time)
78 new_thread = True
78 new_thread = True
79 else:
79 else:
80 new_thread = False
80 new_thread = False
81
81
82 pre_text = self._preparse_text(text)
82 pre_text = self._preparse_text(text)
83
83
84 post = self.create(title=title,
84 post = self.create(title=title,
85 text=pre_text,
85 text=pre_text,
86 pub_time=posting_time,
86 pub_time=posting_time,
87 thread_new=thread,
87 thread_new=thread,
88 poster_ip=ip,
88 poster_ip=ip,
89 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
89 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
90 # last!
90 # last!
91 last_edit_time=posting_time)
91 last_edit_time=posting_time)
92
92
93 logger = logging.getLogger('boards.post.create')
93 logger = logging.getLogger('boards.post.create')
94
94
95 logger.info('Created post {} by {}'.format(
95 logger.info('Created post {} by {}'.format(
96 post, post.poster_ip))
96 post, post.poster_ip))
97
97
98 if image:
98 if image:
99 post_image = PostImage.objects.create(image=image)
99 post_image = PostImage.objects.create(image=image)
100 post.images.add(post_image)
100 post.images.add(post_image)
101 logger.info('Created image #{} for post #{}'.format(
101 logger.info('Created image #{} for post #{}'.format(
102 post_image.id, post.id))
102 post_image.id, post.id))
103
103
104 thread.replies.add(post)
104 thread.replies.add(post)
105 list(map(thread.add_tag, tags))
105 list(map(thread.add_tag, tags))
106
106
107 if new_thread:
107 if new_thread:
108 Thread.objects.process_oldest_threads()
108 Thread.objects.process_oldest_threads()
109 else:
109 else:
110 thread.bump()
110 thread.bump()
111 thread.last_edit_time = posting_time
111 thread.last_edit_time = posting_time
112 thread.save()
112 thread.save()
113
113
114 self.connect_replies(post)
114 self.connect_replies(post)
115
115
116 return post
116 return post
117
117
118 def delete_posts_by_ip(self, ip):
118 def delete_posts_by_ip(self, ip):
119 """
119 """
120 Deletes all posts of the author with same IP
120 Deletes all posts of the author with same IP
121 """
121 """
122
122
123 posts = self.filter(poster_ip=ip)
123 posts = self.filter(poster_ip=ip)
124 for post in posts:
124 for post in posts:
125 post.delete()
125 post.delete()
126
126
127 def connect_replies(self, post):
127 def connect_replies(self, post):
128 """
128 """
129 Connects replies to a post to show them as a reflink map
129 Connects replies to a post to show them as a reflink map
130 """
130 """
131
131
132 for reply_number in re.finditer(REGEX_REPLY, post.get_raw_text()):
132 for reply_number in re.finditer(REGEX_REPLY, post.get_raw_text()):
133 post_id = reply_number.group(1)
133 post_id = reply_number.group(1)
134 ref_post = self.filter(id=post_id)
134 ref_post = self.filter(id=post_id)
135 if ref_post.count() > 0:
135 if ref_post.count() > 0:
136 referenced_post = ref_post[0]
136 referenced_post = ref_post[0]
137 referenced_post.referenced_posts.add(post)
137 referenced_post.referenced_posts.add(post)
138 referenced_post.last_edit_time = post.pub_time
138 referenced_post.last_edit_time = post.pub_time
139 referenced_post.build_refmap()
139 referenced_post.build_refmap()
140 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
140 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
141
141
142 referenced_thread = referenced_post.get_thread()
142 referenced_thread = referenced_post.get_thread()
143 referenced_thread.last_edit_time = post.pub_time
143 referenced_thread.last_edit_time = post.pub_time
144 referenced_thread.save(update_fields=['last_edit_time'])
144 referenced_thread.save(update_fields=['last_edit_time'])
145
145
146 def get_posts_per_day(self):
146 def get_posts_per_day(self):
147 """
147 """
148 Gets average count of posts per day for the last 7 days
148 Gets average count of posts per day for the last 7 days
149 """
149 """
150
150
151 day_end = date.today()
151 day_end = date.today()
152 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
152 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
153
153
154 cache_key = CACHE_KEY_PPD + str(day_end)
154 cache_key = CACHE_KEY_PPD + str(day_end)
155 ppd = cache.get(cache_key)
155 ppd = cache.get(cache_key)
156 if ppd:
156 if ppd:
157 return ppd
157 return ppd
158
158
159 day_time_start = timezone.make_aware(datetime.combine(
159 day_time_start = timezone.make_aware(datetime.combine(
160 day_start, dtime()), timezone.get_current_timezone())
160 day_start, dtime()), timezone.get_current_timezone())
161 day_time_end = timezone.make_aware(datetime.combine(
161 day_time_end = timezone.make_aware(datetime.combine(
162 day_end, dtime()), timezone.get_current_timezone())
162 day_end, dtime()), timezone.get_current_timezone())
163
163
164 posts_per_period = float(self.filter(
164 posts_per_period = float(self.filter(
165 pub_time__lte=day_time_end,
165 pub_time__lte=day_time_end,
166 pub_time__gte=day_time_start).count())
166 pub_time__gte=day_time_start).count())
167
167
168 ppd = posts_per_period / POSTS_PER_DAY_RANGE
168 ppd = posts_per_period / POSTS_PER_DAY_RANGE
169
169
170 cache.set(cache_key, ppd)
170 cache.set(cache_key, ppd)
171 return ppd
171 return ppd
172
172
173 def _preparse_text(self, text):
173 def _preparse_text(self, text):
174 """
174 """
175 Preparses text to change patterns like '>>' to a proper bbcode
175 Preparses text to change patterns like '>>' to a proper bbcode
176 tags.
176 tags.
177 """
177 """
178
178
179 for key, value in PREPARSE_PATTERNS.items():
179 for key, value in PREPARSE_PATTERNS.items():
180 text = re.sub(key, value, text, flags=re.MULTILINE)
180 text = re.sub(key, value, text, flags=re.MULTILINE)
181
181
182 return text
182 return text
183
183
184
184
185 class Post(models.Model, Viewable):
185 class Post(models.Model, Viewable):
186 """A post is a message."""
186 """A post is a message."""
187
187
188 objects = PostManager()
188 objects = PostManager()
189
189
190 class Meta:
190 class Meta:
191 app_label = APP_LABEL_BOARDS
191 app_label = APP_LABEL_BOARDS
192 ordering = ('id',)
192 ordering = ('id',)
193
193
194 title = models.CharField(max_length=TITLE_MAX_LENGTH)
194 title = models.CharField(max_length=TITLE_MAX_LENGTH)
195 pub_time = models.DateTimeField()
195 pub_time = models.DateTimeField()
196 text = TextField(blank=True, null=True)
196 text = TextField(blank=True, null=True)
197 _text_rendered = TextField(blank=True, null=True, editable=False)
197 _text_rendered = TextField(blank=True, null=True, editable=False)
198
198
199 images = models.ManyToManyField(PostImage, null=True, blank=True,
199 images = models.ManyToManyField(PostImage, null=True, blank=True,
200 related_name='ip+', db_index=True)
200 related_name='ip+', db_index=True)
201
201
202 poster_ip = models.GenericIPAddressField()
202 poster_ip = models.GenericIPAddressField()
203 poster_user_agent = models.TextField()
203 poster_user_agent = models.TextField()
204
204
205 thread_new = models.ForeignKey('Thread', null=True, default=None,
205 thread_new = models.ForeignKey('Thread', null=True, default=None,
206 db_index=True)
206 db_index=True)
207 last_edit_time = models.DateTimeField()
207 last_edit_time = models.DateTimeField()
208
208
209 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
209 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
210 null=True,
210 null=True,
211 blank=True, related_name='rfp+',
211 blank=True, related_name='rfp+',
212 db_index=True)
212 db_index=True)
213 refmap = models.TextField(null=True, blank=True)
213 refmap = models.TextField(null=True, blank=True)
214
214
215 def __str__(self):
215 def __str__(self):
216 return 'P#{}/{}'.format(self.id, self.title)
216 return 'P#{}/{}'.format(self.id, self.title)
217
217
218 def get_title(self) -> str:
218 def get_title(self) -> str:
219 """
219 """
220 Gets original post title or part of its text.
220 Gets original post title or part of its text.
221 """
221 """
222
222
223 title = self.title
223 title = self.title
224 if not title:
224 if not title:
225 title = self.get_text()
225 title = self.get_text()
226
226
227 return title
227 return title
228
228
229 def build_refmap(self) -> None:
229 def build_refmap(self) -> None:
230 """
230 """
231 Builds a replies map string from replies list. This is a cache to stop
231 Builds a replies map string from replies list. This is a cache to stop
232 the server from recalculating the map on every post show.
232 the server from recalculating the map on every post show.
233 """
233 """
234 map_string = ''
234 map_string = ''
235
235
236 first = True
236 first = True
237 for refpost in self.referenced_posts.all():
237 for refpost in self.referenced_posts.all():
238 if not first:
238 if not first:
239 map_string += ', '
239 map_string += ', '
240 map_string += '<a href="%s">&gt;&gt;%s</a>' % (refpost.get_url(),
240 map_string += '<a href="%s">&gt;&gt;%s</a>' % (refpost.get_url(),
241 refpost.id)
241 refpost.id)
242 first = False
242 first = False
243
243
244 self.refmap = map_string
244 self.refmap = map_string
245
245
246 def get_sorted_referenced_posts(self):
246 def get_sorted_referenced_posts(self):
247 return self.refmap
247 return self.refmap
248
248
249 def is_referenced(self) -> bool:
249 def is_referenced(self) -> bool:
250 if not self.refmap:
250 if not self.refmap:
251 return False
251 return False
252 else:
252 else:
253 return len(self.refmap) > 0
253 return len(self.refmap) > 0
254
254
255 def is_opening(self) -> bool:
255 def is_opening(self) -> bool:
256 """
256 """
257 Checks if this is an opening post or just a reply.
257 Checks if this is an opening post or just a reply.
258 """
258 """
259
259
260 return self.get_thread().get_opening_post_id() == self.id
260 return self.get_thread().get_opening_post_id() == self.id
261
261
262 @transaction.atomic
262 @transaction.atomic
263 def add_tag(self, tag):
263 def add_tag(self, tag):
264 edit_time = timezone.now()
264 edit_time = timezone.now()
265
265
266 thread = self.get_thread()
266 thread = self.get_thread()
267 thread.add_tag(tag)
267 thread.add_tag(tag)
268 self.last_edit_time = edit_time
268 self.last_edit_time = edit_time
269 self.save(update_fields=['last_edit_time'])
269 self.save(update_fields=['last_edit_time'])
270
270
271 thread.last_edit_time = edit_time
271 thread.last_edit_time = edit_time
272 thread.save(update_fields=['last_edit_time'])
272 thread.save(update_fields=['last_edit_time'])
273
273
274 def get_url(self, thread=None):
274 def get_url(self, thread=None):
275 """
275 """
276 Gets full url to the post.
276 Gets full url to the post.
277 """
277 """
278
278
279 cache_key = CACHE_KEY_POST_URL + str(self.id)
279 cache_key = CACHE_KEY_POST_URL + str(self.id)
280 link = cache.get(cache_key)
280 link = cache.get(cache_key)
281
281
282 if not link:
282 if not link:
283 if not thread:
283 if not thread:
284 thread = self.get_thread()
284 thread = self.get_thread()
285
285
286 opening_id = thread.get_opening_post_id()
286 opening_id = thread.get_opening_post_id()
287
287
288 if self.id != opening_id:
288 if self.id != opening_id:
289 link = reverse('thread', kwargs={
289 link = reverse('thread', kwargs={
290 'post_id': opening_id}) + '#' + str(self.id)
290 'post_id': opening_id}) + '#' + str(self.id)
291 else:
291 else:
292 link = reverse('thread', kwargs={'post_id': self.id})
292 link = reverse('thread', kwargs={'post_id': self.id})
293
293
294 cache.set(cache_key, link)
294 cache.set(cache_key, link)
295
295
296 return link
296 return link
297
297
298 def get_thread(self) -> Thread:
298 def get_thread(self) -> Thread:
299 """
299 """
300 Gets post's thread.
300 Gets post's thread.
301 """
301 """
302
302
303 return self.thread_new
303 return self.thread_new
304
304
305 def get_referenced_posts(self):
305 def get_referenced_posts(self):
306 return self.referenced_posts.only('id', 'thread_new')
306 return self.referenced_posts.only('id', 'thread_new')
307
307
308 def get_view(self, moderator=False, need_open_link=False,
308 def get_view(self, moderator=False, need_open_link=False,
309 truncated=False, *args, **kwargs):
309 truncated=False, *args, **kwargs):
310 if 'is_opening' in kwargs:
310 if 'is_opening' in kwargs:
311 is_opening = kwargs['is_opening']
311 is_opening = kwargs['is_opening']
312 else:
312 else:
313 is_opening = self.is_opening()
313 is_opening = self.is_opening()
314
314
315 if 'thread' in kwargs:
315 if 'thread' in kwargs:
316 thread = kwargs['thread']
316 thread = kwargs['thread']
317 else:
317 else:
318 thread = self.get_thread()
318 thread = self.get_thread()
319
319
320 if 'can_bump' in kwargs:
320 if 'can_bump' in kwargs:
321 can_bump = kwargs['can_bump']
321 can_bump = kwargs['can_bump']
322 else:
322 else:
323 can_bump = thread.can_bump()
323 can_bump = thread.can_bump()
324
324
325 if is_opening:
325 if is_opening:
326 opening_post_id = self.id
326 opening_post_id = self.id
327 else:
327 else:
328 opening_post_id = thread.get_opening_post_id()
328 opening_post_id = thread.get_opening_post_id()
329
329
330 return render_to_string('boards/post.html', {
330 return render_to_string('boards/post.html', {
331 'post': self,
331 'post': self,
332 'moderator': moderator,
332 'moderator': moderator,
333 'is_opening': is_opening,
333 'is_opening': is_opening,
334 'thread': thread,
334 'thread': thread,
335 'bumpable': can_bump,
335 'bumpable': can_bump,
336 'need_open_link': need_open_link,
336 'need_open_link': need_open_link,
337 'truncated': truncated,
337 'truncated': truncated,
338 'opening_post_id': opening_post_id,
338 'opening_post_id': opening_post_id,
339 })
339 })
340
340
341 def get_first_image(self) -> PostImage:
341 def get_first_image(self) -> PostImage:
342 return self.images.earliest('id')
342 return self.images.earliest('id')
343
343
344 def delete(self, using=None):
344 def delete(self, using=None):
345 """
345 """
346 Deletes all post images and the post itself. If the post is opening,
346 Deletes all post images and the post itself. If the post is opening,
347 thread with all posts is deleted.
347 thread with all posts is deleted.
348 """
348 """
349
349
350 self.images.all().delete()
350 self.images.all().delete()
351
351
352 if self.is_opening():
352 if self.is_opening():
353 self.get_thread().delete()
353 self.get_thread().delete()
354 else:
354 else:
355 thread = self.get_thread()
355 thread = self.get_thread()
356 thread.last_edit_time = timezone.now()
356 thread.last_edit_time = timezone.now()
357 thread.save()
357 thread.save()
358
358
359 super(Post, self).delete(using)
359 super(Post, self).delete(using)
360
360
361 logging.getLogger('boards.post.delete').info(
361 logging.getLogger('boards.post.delete').info(
362 'Deleted post {}'.format(self))
362 'Deleted post {}'.format(self))
363
363
364 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
364 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
365 include_last_update=False):
365 include_last_update=False):
366 """
366 """
367 Gets post HTML or JSON data that can be rendered on a page or used by
367 Gets post HTML or JSON data that can be rendered on a page or used by
368 API.
368 API.
369 """
369 """
370
370
371 if format_type == DIFF_TYPE_HTML:
371 if format_type == DIFF_TYPE_HTML:
372 context = RequestContext(request)
372 context = RequestContext(request)
373 context['post'] = self
373 context['post'] = self
374 if PARAMETER_TRUNCATED in request.GET:
374 if PARAMETER_TRUNCATED in request.GET:
375 context[PARAMETER_TRUNCATED] = True
375 context[PARAMETER_TRUNCATED] = True
376
376
377 # TODO Use dict here
377 # TODO Use dict here
378 return render_to_string('boards/api_post.html',
378 return render_to_string('boards/api_post.html',
379 context_instance=context)
379 context_instance=context)
380 elif format_type == DIFF_TYPE_JSON:
380 elif format_type == DIFF_TYPE_JSON:
381 post_json = {
381 post_json = {
382 'id': self.id,
382 'id': self.id,
383 'title': self.title,
383 'title': self.title,
384 'text': self._text_rendered,
384 'text': self._text_rendered,
385 }
385 }
386 if self.images.exists():
386 if self.images.exists():
387 post_image = self.get_first_image()
387 post_image = self.get_first_image()
388 post_json['image'] = post_image.image.url
388 post_json['image'] = post_image.image.url
389 post_json['image_preview'] = post_image.image.url_200x150
389 post_json['image_preview'] = post_image.image.url_200x150
390 if include_last_update:
390 if include_last_update:
391 post_json['bump_time'] = datetime_to_epoch(
391 post_json['bump_time'] = datetime_to_epoch(
392 self.thread_new.bump_time)
392 self.thread_new.bump_time)
393 return post_json
393 return post_json
394
394
395 def send_to_websocket(self, request, recursive=True):
395 def send_to_websocket(self, request, recursive=True):
396 """
396 """
397 Sends post HTML data to the thread web socket.
397 Sends post HTML data to the thread web socket.
398 """
398 """
399
399
400 if not settings.WEBSOCKETS_ENABLED:
400 if not settings.WEBSOCKETS_ENABLED:
401 return
401 return
402
402
403 client = Client()
403 client = Client()
404
404
405 thread = self.get_thread()
405 thread = self.get_thread()
406 thread_id = thread.id
406 thread_id = thread.id
407 channel_name = WS_CHANNEL_THREAD + str(thread.get_opening_post_id())
407 channel_name = WS_CHANNEL_THREAD + str(thread.get_opening_post_id())
408 client.publish(channel_name, {
408 client.publish(channel_name, {
409 WS_NOTIFICATION_TYPE: WS_NOTIFICATION_TYPE_NEW_POST,
409 WS_NOTIFICATION_TYPE: WS_NOTIFICATION_TYPE_NEW_POST,
410 })
410 })
411 client.send()
411 client.send()
412
412
413 logger = logging.getLogger('boards.post.websocket')
413 logger = logging.getLogger('boards.post.websocket')
414
414
415 logger.info('Sent notification from post #{} to channel {}'.format(
415 logger.info('Sent notification from post #{} to channel {}'.format(
416 self.id, channel_name))
416 self.id, channel_name))
417
417
418 if recursive:
418 if recursive:
419 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
419 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
420 post_id = reply_number.group(1)
420 post_id = reply_number.group(1)
421 ref_post = Post.objects.filter(id=post_id)[0]
421 ref_post = Post.objects.filter(id=post_id)[0]
422
422
423 # If post is in this thread, its thread was already notified.
423 # If post is in this thread, its thread was already notified.
424 # Otherwise, notify its thread separately.
424 # Otherwise, notify its thread separately.
425 if ref_post.thread_id != thread_id:
425 if ref_post.thread_new_id != thread_id:
426 ref_post.send_to_websocket(request, recursive=False)
426 ref_post.send_to_websocket(request, recursive=False)
427
427
428 def save(self, force_insert=False, force_update=False, using=None,
428 def save(self, force_insert=False, force_update=False, using=None,
429 update_fields=None):
429 update_fields=None):
430 self._text_rendered = bbcode_extended(self.get_raw_text())
430 self._text_rendered = bbcode_extended(self.get_raw_text())
431
431
432 super().save(force_insert, force_update, using, update_fields)
432 super().save(force_insert, force_update, using, update_fields)
433
433
434 def get_text(self) -> str:
434 def get_text(self) -> str:
435 return self._text_rendered
435 return self._text_rendered
436
436
437 def get_raw_text(self) -> str:
437 def get_raw_text(self) -> str:
438 return self.text
438 return self.text
@@ -1,149 +1,143 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 def new_post(self, request, form, opening_post=None, html_response=True):
116 def new_post(self, request, form, opening_post=None, html_response=True):
117 """Add a new post (in thread or as a reply)."""
117 """Add a new post (in thread or as a reply)."""
118
118
119 ip = utils.get_client_ip(request)
119 ip = utils.get_client_ip(request)
120
120
121 data = form.cleaned_data
121 data = form.cleaned_data
122
122
123 title = data[FORM_TITLE]
123 title = data[FORM_TITLE]
124 text = data[FORM_TEXT]
124 text = data[FORM_TEXT]
125 image = data.get(FORM_IMAGE)
125
126
126 text = self._remove_invalid_links(text)
127 text = self._remove_invalid_links(text)
127
128
128 if FORM_IMAGE in list(data.keys()):
129 image = data[FORM_IMAGE]
130 else:
131 image = None
132
133 tags = []
134
135 post_thread = opening_post.get_thread()
129 post_thread = opening_post.get_thread()
136
130
137 post = Post.objects.create_post(title=title, text=text, image=image,
131 post = Post.objects.create_post(title=title, text=text, image=image,
138 thread=post_thread, ip=ip, tags=tags)
132 thread=post_thread, ip=ip)
139 post.send_to_websocket(request)
133 post.send_to_websocket(request)
140
134
141 thread_to_show = (opening_post.id if opening_post else post.id)
135 thread_to_show = (opening_post.id if opening_post else post.id)
142
136
143 if html_response:
137 if html_response:
144 if opening_post:
138 if opening_post:
145 return redirect(
139 return redirect(
146 reverse('thread', kwargs={'post_id': thread_to_show})
140 reverse('thread', kwargs={'post_id': thread_to_show})
147 + '#' + str(post.id))
141 + '#' + str(post.id))
148 else:
142 else:
149 return post
143 return post
General Comments 0
You need to be logged in to leave comments. Login now