##// END OF EJS Templates
Actually delete the opening post when deleting the thread
neko259 -
r884:e3087995 default
parent child Browse files
Show More
@@ -1,447 +1,447 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]',
55 r'>>(\d+)': r'[post]\1[/post]',
56 r'>(.+)': r'[quote]\1[/quote]'
56 r'>(.+)': r'[quote]\1[/quote]'
57 }
57 }
58
58
59
59
60 class PostManager(models.Manager):
60 class PostManager(models.Manager):
61 def create_post(self, title, text, image=None, thread=None, ip=NO_IP,
61 def create_post(self, title, text, image=None, thread=None, ip=NO_IP,
62 tags=None):
62 tags=None):
63 """
63 """
64 Creates new post
64 Creates new post
65 """
65 """
66
66
67 if not tags:
67 if not tags:
68 tags = []
68 tags = []
69
69
70 posting_time = timezone.now()
70 posting_time = timezone.now()
71 if not thread:
71 if not thread:
72 thread = Thread.objects.create(bump_time=posting_time,
72 thread = Thread.objects.create(bump_time=posting_time,
73 last_edit_time=posting_time)
73 last_edit_time=posting_time)
74 new_thread = True
74 new_thread = True
75 else:
75 else:
76 thread.bump()
76 thread.bump()
77 thread.last_edit_time = posting_time
77 thread.last_edit_time = posting_time
78 if thread.can_bump() and (
78 if thread.can_bump() and (
79 thread.get_reply_count() >= settings.MAX_POSTS_PER_THREAD):
79 thread.get_reply_count() >= settings.MAX_POSTS_PER_THREAD):
80 thread.bumpable = False
80 thread.bumpable = False
81 thread.save()
81 thread.save()
82 new_thread = False
82 new_thread = False
83
83
84 pre_text = self._preparse_text(text)
84 pre_text = self._preparse_text(text)
85
85
86 post = self.create(title=title,
86 post = self.create(title=title,
87 text=pre_text,
87 text=pre_text,
88 pub_time=posting_time,
88 pub_time=posting_time,
89 thread_new=thread,
89 thread_new=thread,
90 poster_ip=ip,
90 poster_ip=ip,
91 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
91 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
92 # last!
92 # last!
93 last_edit_time=posting_time)
93 last_edit_time=posting_time)
94
94
95 logger = logging.getLogger('boards.post.create')
95 logger = logging.getLogger('boards.post.create')
96
96
97 logger.info('Created post #{} with title "{}" by {}'.format(
97 logger.info('Created post #{} with title "{}" by {}'.format(
98 post.id, post.title, post.poster_ip))
98 post.id, post.title, post.poster_ip))
99
99
100 if image:
100 if image:
101 post_image = PostImage.objects.create(image=image)
101 post_image = PostImage.objects.create(image=image)
102 post.images.add(post_image)
102 post.images.add(post_image)
103 logger.info('Created image #{} for post #{}'.format(
103 logger.info('Created image #{} for post #{}'.format(
104 post_image.id, post.id))
104 post_image.id, post.id))
105
105
106 thread.replies.add(post)
106 thread.replies.add(post)
107 list(map(thread.add_tag, tags))
107 list(map(thread.add_tag, tags))
108
108
109 if new_thread:
109 if new_thread:
110 Thread.objects.process_oldest_threads()
110 Thread.objects.process_oldest_threads()
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)
177 text = re.sub(key, value, text)
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_text(self):
317 def get_text(self):
318 return self.text
318 return self.text
319
319
320 def get_view(self, moderator=False, need_open_link=False,
320 def get_view(self, moderator=False, need_open_link=False,
321 truncated=False, *args, **kwargs):
321 truncated=False, *args, **kwargs):
322 if 'is_opening' in kwargs:
322 if 'is_opening' in kwargs:
323 is_opening = kwargs['is_opening']
323 is_opening = kwargs['is_opening']
324 else:
324 else:
325 is_opening = self.is_opening()
325 is_opening = self.is_opening()
326
326
327 if 'thread' in kwargs:
327 if 'thread' in kwargs:
328 thread = kwargs['thread']
328 thread = kwargs['thread']
329 else:
329 else:
330 thread = self.get_thread()
330 thread = self.get_thread()
331
331
332 if 'can_bump' in kwargs:
332 if 'can_bump' in kwargs:
333 can_bump = kwargs['can_bump']
333 can_bump = kwargs['can_bump']
334 else:
334 else:
335 can_bump = thread.can_bump()
335 can_bump = thread.can_bump()
336
336
337 if is_opening:
337 if is_opening:
338 opening_post_id = self.id
338 opening_post_id = self.id
339 else:
339 else:
340 opening_post_id = thread.get_opening_post_id()
340 opening_post_id = thread.get_opening_post_id()
341
341
342 return render_to_string('boards/post.html', {
342 return render_to_string('boards/post.html', {
343 'post': self,
343 'post': self,
344 'moderator': moderator,
344 'moderator': moderator,
345 'is_opening': is_opening,
345 'is_opening': is_opening,
346 'thread': thread,
346 'thread': thread,
347 'bumpable': can_bump,
347 'bumpable': can_bump,
348 'need_open_link': need_open_link,
348 'need_open_link': need_open_link,
349 'truncated': truncated,
349 'truncated': truncated,
350 'opening_post_id': opening_post_id,
350 'opening_post_id': opening_post_id,
351 })
351 })
352
352
353 def get_first_image(self):
353 def get_first_image(self):
354 return self.images.earliest('id')
354 return self.images.earliest('id')
355
355
356 def delete(self, using=None):
356 def delete(self, using=None):
357 """
357 """
358 Deletes all post images and the post itself. If the post is opening,
358 Deletes all post images and the post itself. If the post is opening,
359 thread with all posts is deleted.
359 thread with all posts is deleted.
360 """
360 """
361
361
362 self.images.all().delete()
362 self.images.all().delete()
363
363
364 if self.is_opening():
364 if self.is_opening():
365 self.get_thread().delete()
365 self.get_thread().delete()
366 else:
366 else:
367 thread = self.get_thread()
367 thread = self.get_thread()
368 thread.last_edit_time = timezone.now()
368 thread.last_edit_time = timezone.now()
369 thread.save()
369 thread.save()
370
370
371 super(Post, self).delete(using)
371 super(Post, self).delete(using)
372
372
373 logging.getLogger('boards.post.delete').info(
373 logging.getLogger('boards.post.delete').info(
374 'Deleted post P#{}/{}'.format(self.id, self.get_title()))
374 'Deleted post P#{}/{}'.format(self.id, self.get_title()))
375
375
376 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
376 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
377 include_last_update=False):
377 include_last_update=False):
378 """
378 """
379 Gets post HTML or JSON data that can be rendered on a page or used by
379 Gets post HTML or JSON data that can be rendered on a page or used by
380 API.
380 API.
381 """
381 """
382
382
383 if format_type == DIFF_TYPE_HTML:
383 if format_type == DIFF_TYPE_HTML:
384 context = RequestContext(request)
384 context = RequestContext(request)
385 context['post'] = self
385 context['post'] = self
386 if PARAMETER_TRUNCATED in request.GET:
386 if PARAMETER_TRUNCATED in request.GET:
387 context[PARAMETER_TRUNCATED] = True
387 context[PARAMETER_TRUNCATED] = True
388
388
389 # TODO Use dict here
389 # TODO Use dict here
390 return render_to_string('boards/api_post.html',
390 return render_to_string('boards/api_post.html',
391 context_instance=context)
391 context_instance=context)
392 elif format_type == DIFF_TYPE_JSON:
392 elif format_type == DIFF_TYPE_JSON:
393 post_json = {
393 post_json = {
394 'id': self.id,
394 'id': self.id,
395 'title': self.title,
395 'title': self.title,
396 'text': self.text.rendered,
396 'text': self.text.rendered,
397 }
397 }
398 if self.images.exists():
398 if self.images.exists():
399 post_image = self.get_first_image()
399 post_image = self.get_first_image()
400 post_json['image'] = post_image.image.url
400 post_json['image'] = post_image.image.url
401 post_json['image_preview'] = post_image.image.url_200x150
401 post_json['image_preview'] = post_image.image.url_200x150
402 if include_last_update:
402 if include_last_update:
403 post_json['bump_time'] = datetime_to_epoch(
403 post_json['bump_time'] = datetime_to_epoch(
404 self.thread_new.bump_time)
404 self.thread_new.bump_time)
405 return post_json
405 return post_json
406
406
407 def send_to_websocket(self, request, recursive=True):
407 def send_to_websocket(self, request, recursive=True):
408 """
408 """
409 Sends post HTML data to the thread web socket.
409 Sends post HTML data to the thread web socket.
410 """
410 """
411
411
412 if not settings.WEBSOCKETS_ENABLED:
412 if not settings.WEBSOCKETS_ENABLED:
413 return
413 return
414
414
415 client = Client()
415 client = Client()
416
416
417 channel_name = WS_CHANNEL_THREAD + str(self.get_thread().get_opening_post_id())
417 channel_name = WS_CHANNEL_THREAD + str(self.get_thread().get_opening_post_id())
418 client.publish(channel_name, {
418 client.publish(channel_name, {
419 'html': self.get_post_data(
419 'html': self.get_post_data(
420 format_type=DIFF_TYPE_HTML,
420 format_type=DIFF_TYPE_HTML,
421 request=request),
421 request=request),
422 'diff_type': 'added' if recursive else 'updated',
422 'diff_type': 'added' if recursive else 'updated',
423 })
423 })
424 client.send()
424 client.send()
425
425
426 logger = logging.getLogger('boards.post.websocket')
426 logger = logging.getLogger('boards.post.websocket')
427
427
428 logger.info('Sent post #{} to channel {}'.format(self.id, channel_name))
428 logger.info('Sent post #{} to channel {}'.format(self.id, channel_name))
429
429
430 if recursive:
430 if recursive:
431 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
431 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
432 post_id = reply_number.group(1)
432 post_id = reply_number.group(1)
433 ref_post = Post.objects.filter(id=post_id)[0]
433 ref_post = Post.objects.filter(id=post_id)[0]
434
434
435 ref_post.send_to_websocket(request, recursive=False)
435 ref_post.send_to_websocket(request, recursive=False)
436
436
437 def save(self, force_insert=False, force_update=False, using=None,
437 def save(self, force_insert=False, force_update=False, using=None,
438 update_fields=None):
438 update_fields=None):
439 self._text_rendered = bbcode_extended(self.get_raw_text())
439 self._text_rendered = bbcode_extended(self.get_raw_text())
440
440
441 super().save(force_insert, force_update, using, update_fields)
441 super().save(force_insert, force_update, using, update_fields)
442
442
443 def get_text(self):
443 def get_text(self):
444 return self._text_rendered
444 return self._text_rendered
445
445
446 def get_raw_text(self):
446 def get_raw_text(self):
447 return self.text
447 return self.text
@@ -1,112 +1,115 b''
1 from django.core.paginator import Paginator
1 from django.core.paginator import Paginator
2 from django.test import TestCase
2 from django.test import TestCase
3 from boards import settings
3 from boards import settings
4 from boards.models import Tag, Post, Thread
4 from boards.models import Tag, Post, Thread
5
5
6
6
7 class PostTests(TestCase):
7 class PostTests(TestCase):
8
8
9 def _create_post(self):
9 def _create_post(self):
10 tag = Tag.objects.create(name='test_tag')
10 tag = Tag.objects.create(name='test_tag')
11 return Post.objects.create_post(title='title', text='text',
11 return Post.objects.create_post(title='title', text='text',
12 tags=[tag])
12 tags=[tag])
13
13
14 def test_post_add(self):
14 def test_post_add(self):
15 """Test adding post"""
15 """Test adding post"""
16
16
17 post = self._create_post()
17 post = self._create_post()
18
18
19 self.assertIsNotNone(post, 'No post was created.')
19 self.assertIsNotNone(post, 'No post was created.')
20 self.assertEqual('test_tag', post.get_thread().tags.all()[0].name,
20 self.assertEqual('test_tag', post.get_thread().tags.all()[0].name,
21 'No tags were added to the post.')
21 'No tags were added to the post.')
22
22
23 def test_delete_post(self):
23 def test_delete_post(self):
24 """Test post deletion"""
24 """Test post deletion"""
25
25
26 post = self._create_post()
26 post = self._create_post()
27 post_id = post.id
27 post_id = post.id
28
28
29 post.delete()
29 post.delete()
30
30
31 self.assertFalse(Post.objects.filter(id=post_id).exists())
31 self.assertFalse(Post.objects.filter(id=post_id).exists())
32
32
33 def test_delete_thread(self):
33 def test_delete_thread(self):
34 """Test thread deletion"""
34 """Test thread deletion"""
35
35
36 opening_post = self._create_post()
36 opening_post = self._create_post()
37 thread = opening_post.get_thread()
37 thread = opening_post.get_thread()
38 reply = Post.objects.create_post("", "", thread=thread)
38 reply = Post.objects.create_post("", "", thread=thread)
39
39
40 thread.delete()
40 opening_post.delete()
41
41
42 self.assertFalse(Post.objects.filter(id=reply.id).exists())
42 self.assertFalse(Post.objects.filter(id=reply.id).exists(),
43 'Reply was not deleted with the thread.')
44 self.assertFalse(Post.objects.filter(id=opening_post.id).exists(),
45 'Opening post was not deleted with the thread.')
43
46
44 def test_post_to_thread(self):
47 def test_post_to_thread(self):
45 """Test adding post to a thread"""
48 """Test adding post to a thread"""
46
49
47 op = self._create_post()
50 op = self._create_post()
48 post = Post.objects.create_post("", "", thread=op.get_thread())
51 post = Post.objects.create_post("", "", thread=op.get_thread())
49
52
50 self.assertIsNotNone(post, 'Reply to thread wasn\'t created')
53 self.assertIsNotNone(post, 'Reply to thread wasn\'t created')
51 self.assertEqual(op.get_thread().last_edit_time, post.pub_time,
54 self.assertEqual(op.get_thread().last_edit_time, post.pub_time,
52 'Post\'s create time doesn\'t match thread last edit'
55 'Post\'s create time doesn\'t match thread last edit'
53 ' time')
56 ' time')
54
57
55 def test_delete_posts_by_ip(self):
58 def test_delete_posts_by_ip(self):
56 """Test deleting posts with the given ip"""
59 """Test deleting posts with the given ip"""
57
60
58 post = self._create_post()
61 post = self._create_post()
59 post_id = post.id
62 post_id = post.id
60
63
61 Post.objects.delete_posts_by_ip('0.0.0.0')
64 Post.objects.delete_posts_by_ip('0.0.0.0')
62
65
63 self.assertFalse(Post.objects.filter(id=post_id).exists())
66 self.assertFalse(Post.objects.filter(id=post_id).exists())
64
67
65 def test_get_thread(self):
68 def test_get_thread(self):
66 """Test getting all posts of a thread"""
69 """Test getting all posts of a thread"""
67
70
68 opening_post = self._create_post()
71 opening_post = self._create_post()
69
72
70 for i in range(2):
73 for i in range(2):
71 Post.objects.create_post('title', 'text',
74 Post.objects.create_post('title', 'text',
72 thread=opening_post.get_thread())
75 thread=opening_post.get_thread())
73
76
74 thread = opening_post.get_thread()
77 thread = opening_post.get_thread()
75
78
76 self.assertEqual(3, thread.replies.count())
79 self.assertEqual(3, thread.replies.count())
77
80
78 def test_create_post_with_tag(self):
81 def test_create_post_with_tag(self):
79 """Test adding tag to post"""
82 """Test adding tag to post"""
80
83
81 tag = Tag.objects.create(name='test_tag')
84 tag = Tag.objects.create(name='test_tag')
82 post = Post.objects.create_post(title='title', text='text', tags=[tag])
85 post = Post.objects.create_post(title='title', text='text', tags=[tag])
83
86
84 thread = post.get_thread()
87 thread = post.get_thread()
85 self.assertIsNotNone(post, 'Post not created')
88 self.assertIsNotNone(post, 'Post not created')
86 self.assertTrue(tag in thread.tags.all(), 'Tag not added to thread')
89 self.assertTrue(tag in thread.tags.all(), 'Tag not added to thread')
87 self.assertTrue(thread in tag.threads.all(), 'Thread not added to tag')
90 self.assertTrue(thread in tag.threads.all(), 'Thread not added to tag')
88
91
89 def test_thread_max_count(self):
92 def test_thread_max_count(self):
90 """Test deletion of old posts when the max thread count is reached"""
93 """Test deletion of old posts when the max thread count is reached"""
91
94
92 for i in range(settings.MAX_THREAD_COUNT + 1):
95 for i in range(settings.MAX_THREAD_COUNT + 1):
93 self._create_post()
96 self._create_post()
94
97
95 self.assertEqual(settings.MAX_THREAD_COUNT,
98 self.assertEqual(settings.MAX_THREAD_COUNT,
96 len(Thread.objects.filter(archived=False)))
99 len(Thread.objects.filter(archived=False)))
97
100
98 def test_pages(self):
101 def test_pages(self):
99 """Test that the thread list is properly split into pages"""
102 """Test that the thread list is properly split into pages"""
100
103
101 for i in range(settings.MAX_THREAD_COUNT):
104 for i in range(settings.MAX_THREAD_COUNT):
102 self._create_post()
105 self._create_post()
103
106
104 all_threads = Thread.objects.filter(archived=False)
107 all_threads = Thread.objects.filter(archived=False)
105
108
106 paginator = Paginator(Thread.objects.filter(archived=False),
109 paginator = Paginator(Thread.objects.filter(archived=False),
107 settings.THREADS_PER_PAGE)
110 settings.THREADS_PER_PAGE)
108 posts_in_second_page = paginator.page(2).object_list
111 posts_in_second_page = paginator.page(2).object_list
109 first_post = posts_in_second_page[0]
112 first_post = posts_in_second_page[0]
110
113
111 self.assertEqual(all_threads[settings.THREADS_PER_PAGE].id,
114 self.assertEqual(all_threads[settings.THREADS_PER_PAGE].id,
112 first_post.id) No newline at end of file
115 first_post.id)
General Comments 0
You need to be logged in to leave comments. Login now