##// END OF EJS Templates
Show thread, post and tag names in admin with python3
neko259 -
r875:dc78f601 default
parent child Browse files
Show More
@@ -1,446 +1,445 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 from adjacent import Client
3 from adjacent import Client
4 import logging
4 import logging
5 import re
5 import re
6
6
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.shortcuts import get_object_or_404
10 from django.shortcuts import get_object_or_404
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 from markupfield.fields import MarkupField
14 from markupfield.fields import MarkupField
15 from boards import settings
15 from boards import settings
16
16
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_CHANNEL_THREAD = "thread:"
22 WS_CHANNEL_THREAD = "thread:"
23
23
24 APP_LABEL_BOARDS = 'boards'
24 APP_LABEL_BOARDS = 'boards'
25
25
26 CACHE_KEY_PPD = 'ppd'
26 CACHE_KEY_PPD = 'ppd'
27 CACHE_KEY_POST_URL = 'post_url'
27 CACHE_KEY_POST_URL = 'post_url'
28
28
29 POSTS_PER_DAY_RANGE = 7
29 POSTS_PER_DAY_RANGE = 7
30
30
31 BAN_REASON_AUTO = 'Auto'
31 BAN_REASON_AUTO = 'Auto'
32
32
33 IMAGE_THUMB_SIZE = (200, 150)
33 IMAGE_THUMB_SIZE = (200, 150)
34
34
35 TITLE_MAX_LENGTH = 200
35 TITLE_MAX_LENGTH = 200
36
36
37 DEFAULT_MARKUP_TYPE = 'bbcode'
37 DEFAULT_MARKUP_TYPE = 'bbcode'
38
38
39 # TODO This should be removed
39 # TODO This should be removed
40 NO_IP = '0.0.0.0'
40 NO_IP = '0.0.0.0'
41
41
42 # TODO Real user agent should be saved instead of this
42 # TODO Real user agent should be saved instead of this
43 UNKNOWN_UA = ''
43 UNKNOWN_UA = ''
44
44
45 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
45 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
46
46
47 PARAMETER_TRUNCATED = 'truncated'
47 PARAMETER_TRUNCATED = 'truncated'
48 PARAMETER_TAG = 'tag'
48 PARAMETER_TAG = 'tag'
49 PARAMETER_OFFSET = 'offset'
49 PARAMETER_OFFSET = 'offset'
50 PARAMETER_DIFF_TYPE = 'type'
50 PARAMETER_DIFF_TYPE = 'type'
51
51
52 DIFF_TYPE_HTML = 'html'
52 DIFF_TYPE_HTML = 'html'
53 DIFF_TYPE_JSON = 'json'
53 DIFF_TYPE_JSON = 'json'
54
54
55 PREPARSE_PATTERNS = {
55 PREPARSE_PATTERNS = {
56 r'>>(\d+)': r'[post]\1[/post]',
56 r'>>(\d+)': r'[post]\1[/post]',
57 r'>(.+)': r'[quote]\1[/quote]'
57 r'>(.+)': r'[quote]\1[/quote]'
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 thread.bump()
77 thread.bump()
78 thread.last_edit_time = posting_time
78 thread.last_edit_time = posting_time
79 if thread.can_bump() and (
79 if thread.can_bump() and (
80 thread.get_reply_count() >= settings.MAX_POSTS_PER_THREAD):
80 thread.get_reply_count() >= settings.MAX_POSTS_PER_THREAD):
81 thread.bumpable = False
81 thread.bumpable = False
82 thread.save()
82 thread.save()
83 new_thread = False
83 new_thread = False
84
84
85 pre_text = self._preparse_text(text)
85 pre_text = self._preparse_text(text)
86
86
87 post = self.create(title=title,
87 post = self.create(title=title,
88 text=pre_text,
88 text=pre_text,
89 pub_time=posting_time,
89 pub_time=posting_time,
90 thread_new=thread,
90 thread_new=thread,
91 poster_ip=ip,
91 poster_ip=ip,
92 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
92 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
93 # last!
93 # last!
94 last_edit_time=posting_time)
94 last_edit_time=posting_time)
95
95
96 logger = logging.getLogger('boards.post.create')
96 logger = logging.getLogger('boards.post.create')
97
97
98 logger.info('Created post #%d with title "%s"' % (post.id,
98 logger.info('Created post #%d with title "%s"' % (post.id,
99 post.title))
99 post.title))
100
100
101 if image:
101 if image:
102 post_image = PostImage.objects.create(image=image)
102 post_image = PostImage.objects.create(image=image)
103 post.images.add(post_image)
103 post.images.add(post_image)
104 logger.info('Created image #%d for post #%d' % (post_image.id,
104 logger.info('Created image #%d for post #%d' % (post_image.id,
105 post.id))
105 post.id))
106
106
107 thread.replies.add(post)
107 thread.replies.add(post)
108 list(map(thread.add_tag, tags))
108 list(map(thread.add_tag, tags))
109
109
110 if new_thread:
110 if new_thread:
111 Thread.objects.process_oldest_threads()
111 Thread.objects.process_oldest_threads()
112 self.connect_replies(post)
112 self.connect_replies(post)
113
113
114
114
115 return post
115 return post
116
116
117 def delete_post(self, post):
117 def delete_post(self, post):
118 """
118 """
119 Deletes post and update or delete its thread
119 Deletes post and update or delete its thread
120 """
120 """
121
121
122 post_id = post.id
122 post_id = post.id
123
123
124 thread = post.get_thread()
124 thread = post.get_thread()
125
125
126 if post.is_opening():
126 if post.is_opening():
127 thread.delete()
127 thread.delete()
128 else:
128 else:
129 thread.last_edit_time = timezone.now()
129 thread.last_edit_time = timezone.now()
130 thread.save()
130 thread.save()
131
131
132 post.delete()
132 post.delete()
133
133
134 logging.getLogger('boards.post.delete').info('Deleted post #%d (%s)' % (post_id, post.get_title()))
134 logging.getLogger('boards.post.delete').info('Deleted post #%d (%s)' % (post_id, post.get_title()))
135
135
136 def delete_posts_by_ip(self, ip):
136 def delete_posts_by_ip(self, ip):
137 """
137 """
138 Deletes all posts of the author with same IP
138 Deletes all posts of the author with same IP
139 """
139 """
140
140
141 posts = self.filter(poster_ip=ip)
141 posts = self.filter(poster_ip=ip)
142 for post in posts:
142 for post in posts:
143 self.delete_post(post)
143 self.delete_post(post)
144
144
145 def connect_replies(self, post):
145 def connect_replies(self, post):
146 """
146 """
147 Connects replies to a post to show them as a reflink map
147 Connects replies to a post to show them as a reflink map
148 """
148 """
149
149
150 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
150 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
151 post_id = reply_number.group(1)
151 post_id = reply_number.group(1)
152 ref_post = self.filter(id=post_id)
152 ref_post = self.filter(id=post_id)
153 if ref_post.count() > 0:
153 if ref_post.count() > 0:
154 referenced_post = ref_post[0]
154 referenced_post = ref_post[0]
155 referenced_post.referenced_posts.add(post)
155 referenced_post.referenced_posts.add(post)
156 referenced_post.last_edit_time = post.pub_time
156 referenced_post.last_edit_time = post.pub_time
157 referenced_post.build_refmap()
157 referenced_post.build_refmap()
158 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
158 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
159
159
160 referenced_thread = referenced_post.get_thread()
160 referenced_thread = referenced_post.get_thread()
161 referenced_thread.last_edit_time = post.pub_time
161 referenced_thread.last_edit_time = post.pub_time
162 referenced_thread.save(update_fields=['last_edit_time'])
162 referenced_thread.save(update_fields=['last_edit_time'])
163
163
164 def get_posts_per_day(self):
164 def get_posts_per_day(self):
165 """
165 """
166 Gets average count of posts per day for the last 7 days
166 Gets average count of posts per day for the last 7 days
167 """
167 """
168
168
169 day_end = date.today()
169 day_end = date.today()
170 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
170 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
171
171
172 cache_key = CACHE_KEY_PPD + str(day_end)
172 cache_key = CACHE_KEY_PPD + str(day_end)
173 ppd = cache.get(cache_key)
173 ppd = cache.get(cache_key)
174 if ppd:
174 if ppd:
175 return ppd
175 return ppd
176
176
177 day_time_start = timezone.make_aware(datetime.combine(
177 day_time_start = timezone.make_aware(datetime.combine(
178 day_start, dtime()), timezone.get_current_timezone())
178 day_start, dtime()), timezone.get_current_timezone())
179 day_time_end = timezone.make_aware(datetime.combine(
179 day_time_end = timezone.make_aware(datetime.combine(
180 day_end, dtime()), timezone.get_current_timezone())
180 day_end, dtime()), timezone.get_current_timezone())
181
181
182 posts_per_period = float(self.filter(
182 posts_per_period = float(self.filter(
183 pub_time__lte=day_time_end,
183 pub_time__lte=day_time_end,
184 pub_time__gte=day_time_start).count())
184 pub_time__gte=day_time_start).count())
185
185
186 ppd = posts_per_period / POSTS_PER_DAY_RANGE
186 ppd = posts_per_period / POSTS_PER_DAY_RANGE
187
187
188 cache.set(cache_key, ppd)
188 cache.set(cache_key, ppd)
189 return ppd
189 return ppd
190
190
191 def _preparse_text(self, text):
191 def _preparse_text(self, text):
192 """
192 """
193 Preparses text to change patterns like '>>' to a proper bbcode
193 Preparses text to change patterns like '>>' to a proper bbcode
194 tags.
194 tags.
195 """
195 """
196
196
197 for key, value in PREPARSE_PATTERNS.items():
197 for key, value in PREPARSE_PATTERNS.items():
198 text = re.sub(key, value, text)
198 text = re.sub(key, value, text)
199
199
200 return text
200 return text
201
201
202
202
203 class Post(models.Model, Viewable):
203 class Post(models.Model, Viewable):
204 """A post is a message."""
204 """A post is a message."""
205
205
206 objects = PostManager()
206 objects = PostManager()
207
207
208 class Meta:
208 class Meta:
209 app_label = APP_LABEL_BOARDS
209 app_label = APP_LABEL_BOARDS
210 ordering = ('id',)
210 ordering = ('id',)
211
211
212 title = models.CharField(max_length=TITLE_MAX_LENGTH)
212 title = models.CharField(max_length=TITLE_MAX_LENGTH)
213 pub_time = models.DateTimeField()
213 pub_time = models.DateTimeField()
214 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
214 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
215 escape_html=False)
215 escape_html=False)
216
216
217 images = models.ManyToManyField(PostImage, null=True, blank=True,
217 images = models.ManyToManyField(PostImage, null=True, blank=True,
218 related_name='ip+', db_index=True)
218 related_name='ip+', db_index=True)
219
219
220 poster_ip = models.GenericIPAddressField()
220 poster_ip = models.GenericIPAddressField()
221 poster_user_agent = models.TextField()
221 poster_user_agent = models.TextField()
222
222
223 thread_new = models.ForeignKey('Thread', null=True, default=None,
223 thread_new = models.ForeignKey('Thread', null=True, default=None,
224 db_index=True)
224 db_index=True)
225 last_edit_time = models.DateTimeField()
225 last_edit_time = models.DateTimeField()
226
226
227 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
227 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
228 null=True,
228 null=True,
229 blank=True, related_name='rfp+',
229 blank=True, related_name='rfp+',
230 db_index=True)
230 db_index=True)
231 refmap = models.TextField(null=True, blank=True)
231 refmap = models.TextField(null=True, blank=True)
232
232
233 def __unicode__(self):
233 def __str__(self):
234 return '#' + str(self.id) + ' ' + self.title + ' (' + \
234 return '#{} {} ({})'.format(self.id, self.title, self.text.raw[:50])
235 self.text.raw[:50] + ')'
236
235
237 def get_title(self):
236 def get_title(self):
238 """
237 """
239 Gets original post title or part of its text.
238 Gets original post title or part of its text.
240 """
239 """
241
240
242 title = self.title
241 title = self.title
243 if not title:
242 if not title:
244 title = self.text.rendered
243 title = self.text.rendered
245
244
246 return title
245 return title
247
246
248 def build_refmap(self):
247 def build_refmap(self):
249 """
248 """
250 Builds a replies map string from replies list. This is a cache to stop
249 Builds a replies map string from replies list. This is a cache to stop
251 the server from recalculating the map on every post show.
250 the server from recalculating the map on every post show.
252 """
251 """
253 map_string = ''
252 map_string = ''
254
253
255 first = True
254 first = True
256 for refpost in self.referenced_posts.all():
255 for refpost in self.referenced_posts.all():
257 if not first:
256 if not first:
258 map_string += ', '
257 map_string += ', '
259 map_string += '<a href="%s">&gt;&gt;%s</a>' % (refpost.get_url(),
258 map_string += '<a href="%s">&gt;&gt;%s</a>' % (refpost.get_url(),
260 refpost.id)
259 refpost.id)
261 first = False
260 first = False
262
261
263 self.refmap = map_string
262 self.refmap = map_string
264
263
265 def get_sorted_referenced_posts(self):
264 def get_sorted_referenced_posts(self):
266 return self.refmap
265 return self.refmap
267
266
268 def is_referenced(self):
267 def is_referenced(self):
269 if not self.refmap:
268 if not self.refmap:
270 return False
269 return False
271 else:
270 else:
272 return len(self.refmap) > 0
271 return len(self.refmap) > 0
273
272
274 def is_opening(self):
273 def is_opening(self):
275 """
274 """
276 Checks if this is an opening post or just a reply.
275 Checks if this is an opening post or just a reply.
277 """
276 """
278
277
279 return self.get_thread().get_opening_post_id() == self.id
278 return self.get_thread().get_opening_post_id() == self.id
280
279
281 @transaction.atomic
280 @transaction.atomic
282 def add_tag(self, tag):
281 def add_tag(self, tag):
283 edit_time = timezone.now()
282 edit_time = timezone.now()
284
283
285 thread = self.get_thread()
284 thread = self.get_thread()
286 thread.add_tag(tag)
285 thread.add_tag(tag)
287 self.last_edit_time = edit_time
286 self.last_edit_time = edit_time
288 self.save(update_fields=['last_edit_time'])
287 self.save(update_fields=['last_edit_time'])
289
288
290 thread.last_edit_time = edit_time
289 thread.last_edit_time = edit_time
291 thread.save(update_fields=['last_edit_time'])
290 thread.save(update_fields=['last_edit_time'])
292
291
293 @transaction.atomic
292 @transaction.atomic
294 def remove_tag(self, tag):
293 def remove_tag(self, tag):
295 edit_time = timezone.now()
294 edit_time = timezone.now()
296
295
297 thread = self.get_thread()
296 thread = self.get_thread()
298 thread.remove_tag(tag)
297 thread.remove_tag(tag)
299 self.last_edit_time = edit_time
298 self.last_edit_time = edit_time
300 self.save(update_fields=['last_edit_time'])
299 self.save(update_fields=['last_edit_time'])
301
300
302 thread.last_edit_time = edit_time
301 thread.last_edit_time = edit_time
303 thread.save(update_fields=['last_edit_time'])
302 thread.save(update_fields=['last_edit_time'])
304
303
305 def get_url(self, thread=None):
304 def get_url(self, thread=None):
306 """
305 """
307 Gets full url to the post.
306 Gets full url to the post.
308 """
307 """
309
308
310 cache_key = CACHE_KEY_POST_URL + str(self.id)
309 cache_key = CACHE_KEY_POST_URL + str(self.id)
311 link = cache.get(cache_key)
310 link = cache.get(cache_key)
312
311
313 if not link:
312 if not link:
314 if not thread:
313 if not thread:
315 thread = self.get_thread()
314 thread = self.get_thread()
316
315
317 opening_id = thread.get_opening_post_id()
316 opening_id = thread.get_opening_post_id()
318
317
319 if self.id != opening_id:
318 if self.id != opening_id:
320 link = reverse('thread', kwargs={
319 link = reverse('thread', kwargs={
321 'post_id': opening_id}) + '#' + str(self.id)
320 'post_id': opening_id}) + '#' + str(self.id)
322 else:
321 else:
323 link = reverse('thread', kwargs={'post_id': self.id})
322 link = reverse('thread', kwargs={'post_id': self.id})
324
323
325 cache.set(cache_key, link)
324 cache.set(cache_key, link)
326
325
327 return link
326 return link
328
327
329 def get_thread(self):
328 def get_thread(self):
330 """
329 """
331 Gets post's thread.
330 Gets post's thread.
332 """
331 """
333
332
334 return self.thread_new
333 return self.thread_new
335
334
336 def get_referenced_posts(self):
335 def get_referenced_posts(self):
337 return self.referenced_posts.only('id', 'thread_new')
336 return self.referenced_posts.only('id', 'thread_new')
338
337
339 def get_text(self):
338 def get_text(self):
340 return self.text
339 return self.text
341
340
342 def get_view(self, moderator=False, need_open_link=False,
341 def get_view(self, moderator=False, need_open_link=False,
343 truncated=False, *args, **kwargs):
342 truncated=False, *args, **kwargs):
344 if 'is_opening' in kwargs:
343 if 'is_opening' in kwargs:
345 is_opening = kwargs['is_opening']
344 is_opening = kwargs['is_opening']
346 else:
345 else:
347 is_opening = self.is_opening()
346 is_opening = self.is_opening()
348
347
349 if 'thread' in kwargs:
348 if 'thread' in kwargs:
350 thread = kwargs['thread']
349 thread = kwargs['thread']
351 else:
350 else:
352 thread = self.get_thread()
351 thread = self.get_thread()
353
352
354 if 'can_bump' in kwargs:
353 if 'can_bump' in kwargs:
355 can_bump = kwargs['can_bump']
354 can_bump = kwargs['can_bump']
356 else:
355 else:
357 can_bump = thread.can_bump()
356 can_bump = thread.can_bump()
358
357
359 if is_opening:
358 if is_opening:
360 opening_post_id = self.id
359 opening_post_id = self.id
361 else:
360 else:
362 opening_post_id = thread.get_opening_post_id()
361 opening_post_id = thread.get_opening_post_id()
363
362
364 return render_to_string('boards/post.html', {
363 return render_to_string('boards/post.html', {
365 'post': self,
364 'post': self,
366 'moderator': moderator,
365 'moderator': moderator,
367 'is_opening': is_opening,
366 'is_opening': is_opening,
368 'thread': thread,
367 'thread': thread,
369 'bumpable': can_bump,
368 'bumpable': can_bump,
370 'need_open_link': need_open_link,
369 'need_open_link': need_open_link,
371 'truncated': truncated,
370 'truncated': truncated,
372 'opening_post_id': opening_post_id,
371 'opening_post_id': opening_post_id,
373 })
372 })
374
373
375 def get_first_image(self):
374 def get_first_image(self):
376 return self.images.earliest('id')
375 return self.images.earliest('id')
377
376
378 def delete(self, using=None):
377 def delete(self, using=None):
379 """
378 """
380 Deletes all post images and the post itself.
379 Deletes all post images and the post itself.
381 """
380 """
382
381
383 self.images.all().delete()
382 self.images.all().delete()
384
383
385 super(Post, self).delete(using)
384 super(Post, self).delete(using)
386
385
387 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
386 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
388 include_last_update=False):
387 include_last_update=False):
389 """
388 """
390 Gets post HTML or JSON data that can be rendered on a page or used by
389 Gets post HTML or JSON data that can be rendered on a page or used by
391 API.
390 API.
392 """
391 """
393
392
394 if format_type == DIFF_TYPE_HTML:
393 if format_type == DIFF_TYPE_HTML:
395 context = RequestContext(request)
394 context = RequestContext(request)
396 context['post'] = self
395 context['post'] = self
397 if PARAMETER_TRUNCATED in request.GET:
396 if PARAMETER_TRUNCATED in request.GET:
398 context[PARAMETER_TRUNCATED] = True
397 context[PARAMETER_TRUNCATED] = True
399
398
400 # TODO Use dict here
399 # TODO Use dict here
401 return render_to_string('boards/api_post.html',
400 return render_to_string('boards/api_post.html',
402 context_instance=context)
401 context_instance=context)
403 elif format_type == DIFF_TYPE_JSON:
402 elif format_type == DIFF_TYPE_JSON:
404 post_json = {
403 post_json = {
405 'id': self.id,
404 'id': self.id,
406 'title': self.title,
405 'title': self.title,
407 'text': self.text.rendered,
406 'text': self.text.rendered,
408 }
407 }
409 if self.images.exists():
408 if self.images.exists():
410 post_image = self.get_first_image()
409 post_image = self.get_first_image()
411 post_json['image'] = post_image.image.url
410 post_json['image'] = post_image.image.url
412 post_json['image_preview'] = post_image.image.url_200x150
411 post_json['image_preview'] = post_image.image.url_200x150
413 if include_last_update:
412 if include_last_update:
414 post_json['bump_time'] = datetime_to_epoch(
413 post_json['bump_time'] = datetime_to_epoch(
415 self.thread_new.bump_time)
414 self.thread_new.bump_time)
416 return post_json
415 return post_json
417
416
418 def send_to_websocket(self, request, recursive=True):
417 def send_to_websocket(self, request, recursive=True):
419 """
418 """
420 Sends post HTML data to the thread web socket.
419 Sends post HTML data to the thread web socket.
421 """
420 """
422
421
423 if not settings.WEBSOCKETS_ENABLED:
422 if not settings.WEBSOCKETS_ENABLED:
424 return
423 return
425
424
426 client = Client()
425 client = Client()
427
426
428 channel_name = WS_CHANNEL_THREAD + str(self.get_thread().get_opening_post_id())
427 channel_name = WS_CHANNEL_THREAD + str(self.get_thread().get_opening_post_id())
429 client.publish(channel_name, {
428 client.publish(channel_name, {
430 'html': self.get_post_data(
429 'html': self.get_post_data(
431 format_type=DIFF_TYPE_HTML,
430 format_type=DIFF_TYPE_HTML,
432 request=request),
431 request=request),
433 'diff_type': 'added' if recursive else 'updated',
432 'diff_type': 'added' if recursive else 'updated',
434 })
433 })
435 client.send()
434 client.send()
436
435
437 logger = logging.getLogger('boards.post.websocket')
436 logger = logging.getLogger('boards.post.websocket')
438
437
439 logger.info('Sent post #{} to channel {}'.format(self.id, channel_name))
438 logger.info('Sent post #{} to channel {}'.format(self.id, channel_name))
440
439
441 if recursive:
440 if recursive:
442 for reply_number in re.finditer(REGEX_REPLY, self.text.raw):
441 for reply_number in re.finditer(REGEX_REPLY, self.text.raw):
443 post_id = reply_number.group(1)
442 post_id = reply_number.group(1)
444 ref_post = Post.objects.filter(id=post_id)[0]
443 ref_post = Post.objects.filter(id=post_id)[0]
445
444
446 ref_post.send_to_websocket(request, recursive=False)
445 ref_post.send_to_websocket(request, recursive=False)
@@ -1,78 +1,78 b''
1 from django.template.loader import render_to_string
1 from django.template.loader import render_to_string
2 from django.db import models
2 from django.db import models
3 from django.db.models import Count, Sum
3 from django.db.models import Count, Sum
4 from django.core.urlresolvers import reverse
4 from django.core.urlresolvers import reverse
5
5
6 from boards.models import Thread
6 from boards.models import Thread
7 from boards.models.base import Viewable
7 from boards.models.base import Viewable
8
8
9
9
10 __author__ = 'neko259'
10 __author__ = 'neko259'
11
11
12
12
13 class TagManager(models.Manager):
13 class TagManager(models.Manager):
14
14
15 def get_not_empty_tags(self):
15 def get_not_empty_tags(self):
16 """
16 """
17 Gets tags that have non-archived threads.
17 Gets tags that have non-archived threads.
18 """
18 """
19
19
20 tags = self.annotate(Count('threads')) \
20 tags = self.annotate(Count('threads')) \
21 .filter(threads__count__gt=0).order_by('name')
21 .filter(threads__count__gt=0).order_by('name')
22
22
23 return tags
23 return tags
24
24
25
25
26 class Tag(models.Model, Viewable):
26 class Tag(models.Model, Viewable):
27 """
27 """
28 A tag is a text node assigned to the thread. The tag serves as a board
28 A tag is a text node assigned to the thread. The tag serves as a board
29 section. There can be multiple tags for each thread
29 section. There can be multiple tags for each thread
30 """
30 """
31
31
32 objects = TagManager()
32 objects = TagManager()
33
33
34 class Meta:
34 class Meta:
35 app_label = 'boards'
35 app_label = 'boards'
36 ordering = ('name',)
36 ordering = ('name',)
37
37
38 name = models.CharField(max_length=100, db_index=True)
38 name = models.CharField(max_length=100, db_index=True)
39 threads = models.ManyToManyField(Thread, null=True,
39 threads = models.ManyToManyField(Thread, null=True,
40 blank=True, related_name='tag+')
40 blank=True, related_name='tag+')
41
41
42 def __unicode__(self):
42 def __str__(self):
43 return self.name
43 return self.name
44
44
45 def is_empty(self):
45 def is_empty(self):
46 """
46 """
47 Checks if the tag has some threads.
47 Checks if the tag has some threads.
48 """
48 """
49
49
50 return self.get_thread_count() == 0
50 return self.get_thread_count() == 0
51
51
52 def get_thread_count(self):
52 def get_thread_count(self):
53 return self.threads.count()
53 return self.threads.count()
54
54
55 def get_post_count(self, archived=False):
55 def get_post_count(self, archived=False):
56 """
56 """
57 Gets posts count for the tag's threads.
57 Gets posts count for the tag's threads.
58 """
58 """
59
59
60 posts_count = 0
60 posts_count = 0
61
61
62 threads = self.threads.filter(archived=archived)
62 threads = self.threads.filter(archived=archived)
63 if threads.exists():
63 if threads.exists():
64 posts_count = threads.annotate(posts_count=Count('replies')) \
64 posts_count = threads.annotate(posts_count=Count('replies')) \
65 .aggregate(posts_sum=Sum('posts_count'))['posts_sum']
65 .aggregate(posts_sum=Sum('posts_count'))['posts_sum']
66
66
67 if not posts_count:
67 if not posts_count:
68 posts_count = 0
68 posts_count = 0
69
69
70 return posts_count
70 return posts_count
71
71
72 def get_url(self):
72 def get_url(self):
73 return reverse('tag', kwargs={'tag_name': self.name})
73 return reverse('tag', kwargs={'tag_name': self.name})
74
74
75 def get_view(self, *args, **kwargs):
75 def get_view(self, *args, **kwargs):
76 return render_to_string('boards/tag.html', {
76 return render_to_string('boards/tag.html', {
77 'tag': self,
77 'tag': self,
78 })
78 })
@@ -1,185 +1,188 b''
1 import logging
1 import logging
2 from django.db.models import Count
2 from django.db.models import Count
3 from django.utils import timezone
3 from django.utils import timezone
4 from django.core.cache import cache
4 from django.core.cache import cache
5 from django.db import models
5 from django.db import models
6 from boards import settings
6 from boards import settings
7
7
8 __author__ = 'neko259'
8 __author__ = 'neko259'
9
9
10
10
11 logger = logging.getLogger(__name__)
11 logger = logging.getLogger(__name__)
12
12
13
13
14 CACHE_KEY_OPENING_POST = 'opening_post_id'
14 CACHE_KEY_OPENING_POST = 'opening_post_id'
15
15
16
16
17 class ThreadManager(models.Manager):
17 class ThreadManager(models.Manager):
18 def process_oldest_threads(self):
18 def process_oldest_threads(self):
19 """
19 """
20 Preserves maximum thread count. If there are too many threads,
20 Preserves maximum thread count. If there are too many threads,
21 archive or delete the old ones.
21 archive or delete the old ones.
22 """
22 """
23
23
24 threads = Thread.objects.filter(archived=False).order_by('-bump_time')
24 threads = Thread.objects.filter(archived=False).order_by('-bump_time')
25 thread_count = threads.count()
25 thread_count = threads.count()
26
26
27 if thread_count > settings.MAX_THREAD_COUNT:
27 if thread_count > settings.MAX_THREAD_COUNT:
28 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
28 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
29 old_threads = threads[thread_count - num_threads_to_delete:]
29 old_threads = threads[thread_count - num_threads_to_delete:]
30
30
31 for thread in old_threads:
31 for thread in old_threads:
32 if settings.ARCHIVE_THREADS:
32 if settings.ARCHIVE_THREADS:
33 self._archive_thread(thread)
33 self._archive_thread(thread)
34 else:
34 else:
35 thread.delete()
35 thread.delete()
36
36
37 logger.info('Processed %d old threads' % num_threads_to_delete)
37 logger.info('Processed %d old threads' % num_threads_to_delete)
38
38
39 def _archive_thread(self, thread):
39 def _archive_thread(self, thread):
40 thread.archived = True
40 thread.archived = True
41 thread.bumpable = False
41 thread.bumpable = False
42 thread.last_edit_time = timezone.now()
42 thread.last_edit_time = timezone.now()
43 thread.save(update_fields=['archived', 'last_edit_time', 'bumpable'])
43 thread.save(update_fields=['archived', 'last_edit_time', 'bumpable'])
44
44
45
45
46 class Thread(models.Model):
46 class Thread(models.Model):
47 objects = ThreadManager()
47 objects = ThreadManager()
48
48
49 class Meta:
49 class Meta:
50 app_label = 'boards'
50 app_label = 'boards'
51
51
52 tags = models.ManyToManyField('Tag')
52 tags = models.ManyToManyField('Tag')
53 bump_time = models.DateTimeField()
53 bump_time = models.DateTimeField()
54 last_edit_time = models.DateTimeField()
54 last_edit_time = models.DateTimeField()
55 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
55 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
56 blank=True, related_name='tre+')
56 blank=True, related_name='tre+')
57 archived = models.BooleanField(default=False)
57 archived = models.BooleanField(default=False)
58 bumpable = models.BooleanField(default=True)
58 bumpable = models.BooleanField(default=True)
59
59
60 def get_tags(self):
60 def get_tags(self):
61 """
61 """
62 Gets a sorted tag list.
62 Gets a sorted tag list.
63 """
63 """
64
64
65 return self.tags.order_by('name')
65 return self.tags.order_by('name')
66
66
67 def bump(self):
67 def bump(self):
68 """
68 """
69 Bumps (moves to up) thread if possible.
69 Bumps (moves to up) thread if possible.
70 """
70 """
71
71
72 if self.can_bump():
72 if self.can_bump():
73 self.bump_time = timezone.now()
73 self.bump_time = timezone.now()
74
74
75 logger.info('Bumped thread %d' % self.id)
75 logger.info('Bumped thread %d' % self.id)
76
76
77 def get_reply_count(self):
77 def get_reply_count(self):
78 return self.replies.count()
78 return self.replies.count()
79
79
80 def get_images_count(self):
80 def get_images_count(self):
81 # TODO Use sum
81 # TODO Use sum
82 total_count = 0
82 total_count = 0
83 for post_with_image in self.replies.annotate(images_count=Count(
83 for post_with_image in self.replies.annotate(images_count=Count(
84 'images')):
84 'images')):
85 total_count += post_with_image.images_count
85 total_count += post_with_image.images_count
86 return total_count
86 return total_count
87
87
88 def can_bump(self):
88 def can_bump(self):
89 """
89 """
90 Checks if the thread can be bumped by replying to it.
90 Checks if the thread can be bumped by replying to it.
91 """
91 """
92
92
93 return self.bumpable
93 return self.bumpable
94
94
95 def get_last_replies(self):
95 def get_last_replies(self):
96 """
96 """
97 Gets several last replies, not including opening post
97 Gets several last replies, not including opening post
98 """
98 """
99
99
100 if settings.LAST_REPLIES_COUNT > 0:
100 if settings.LAST_REPLIES_COUNT > 0:
101 reply_count = self.get_reply_count()
101 reply_count = self.get_reply_count()
102
102
103 if reply_count > 0:
103 if reply_count > 0:
104 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
104 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
105 reply_count - 1)
105 reply_count - 1)
106 replies = self.get_replies()
106 replies = self.get_replies()
107 last_replies = replies[reply_count - reply_count_to_show:]
107 last_replies = replies[reply_count - reply_count_to_show:]
108
108
109 return last_replies
109 return last_replies
110
110
111 def get_skipped_replies_count(self):
111 def get_skipped_replies_count(self):
112 """
112 """
113 Gets number of posts between opening post and last replies.
113 Gets number of posts between opening post and last replies.
114 """
114 """
115 reply_count = self.get_reply_count()
115 reply_count = self.get_reply_count()
116 last_replies_count = min(settings.LAST_REPLIES_COUNT,
116 last_replies_count = min(settings.LAST_REPLIES_COUNT,
117 reply_count - 1)
117 reply_count - 1)
118 return reply_count - last_replies_count - 1
118 return reply_count - last_replies_count - 1
119
119
120 def get_replies(self, view_fields_only=False):
120 def get_replies(self, view_fields_only=False):
121 """
121 """
122 Gets sorted thread posts
122 Gets sorted thread posts
123 """
123 """
124
124
125 query = self.replies.order_by('pub_time').prefetch_related('images')
125 query = self.replies.order_by('pub_time').prefetch_related('images')
126 if view_fields_only:
126 if view_fields_only:
127 query = query.defer('poster_user_agent', 'text_markup_type')
127 query = query.defer('poster_user_agent', 'text_markup_type')
128 return query.all()
128 return query.all()
129
129
130 def get_replies_with_images(self, view_fields_only=False):
130 def get_replies_with_images(self, view_fields_only=False):
131 return self.get_replies(view_fields_only).annotate(images_count=Count(
131 return self.get_replies(view_fields_only).annotate(images_count=Count(
132 'images')).filter(images_count__gt=0)
132 'images')).filter(images_count__gt=0)
133
133
134 def add_tag(self, tag):
134 def add_tag(self, tag):
135 """
135 """
136 Connects thread to a tag and tag to a thread
136 Connects thread to a tag and tag to a thread
137 """
137 """
138
138
139 self.tags.add(tag)
139 self.tags.add(tag)
140 tag.threads.add(self)
140 tag.threads.add(self)
141
141
142 def remove_tag(self, tag):
142 def remove_tag(self, tag):
143 self.tags.remove(tag)
143 self.tags.remove(tag)
144 tag.threads.remove(self)
144 tag.threads.remove(self)
145
145
146 def get_opening_post(self, only_id=False):
146 def get_opening_post(self, only_id=False):
147 """
147 """
148 Gets the first post of the thread
148 Gets the first post of the thread
149 """
149 """
150
150
151 query = self.replies.order_by('pub_time')
151 query = self.replies.order_by('pub_time')
152 if only_id:
152 if only_id:
153 query = query.only('id')
153 query = query.only('id')
154 opening_post = query.first()
154 opening_post = query.first()
155
155
156 return opening_post
156 return opening_post
157
157
158 def get_opening_post_id(self):
158 def get_opening_post_id(self):
159 """
159 """
160 Gets ID of the first thread post.
160 Gets ID of the first thread post.
161 """
161 """
162
162
163 cache_key = CACHE_KEY_OPENING_POST + str(self.id)
163 cache_key = CACHE_KEY_OPENING_POST + str(self.id)
164 opening_post_id = cache.get(cache_key)
164 opening_post_id = cache.get(cache_key)
165 if not opening_post_id:
165 if not opening_post_id:
166 opening_post_id = self.get_opening_post(only_id=True).id
166 opening_post_id = self.get_opening_post(only_id=True).id
167 cache.set(cache_key, opening_post_id)
167 cache.set(cache_key, opening_post_id)
168
168
169 return opening_post_id
169 return opening_post_id
170
170
171 def __unicode__(self):
171 def __unicode__(self):
172 return str(self.id)
172 return str(self.id)
173
173
174 def get_pub_time(self):
174 def get_pub_time(self):
175 """
175 """
176 Gets opening post's pub time because thread does not have its own one.
176 Gets opening post's pub time because thread does not have its own one.
177 """
177 """
178
178
179 return self.get_opening_post().pub_time
179 return self.get_opening_post().pub_time
180
180
181 def delete(self, using=None):
181 def delete(self, using=None):
182 if self.replies.exists():
182 if self.replies.exists():
183 self.replies.all().delete()
183 self.replies.all().delete()
184
184
185 super(Thread, self).delete(using)
185 super(Thread, self).delete(using)
186
187 def __str__(self):
188 return '{}/#{}'.format(self.id, self.get_opening_post_id())
General Comments 0
You need to be logged in to leave comments. Login now