##// END OF EJS Templates
Cache threads
neko259 -
r324:c419aa11 default
parent child Browse files
Show More
@@ -1,403 +1,421 b''
1 import os
1 import os
2 from random import random
2 from random import random
3 import time
3 import time
4 import math
4 import math
5 from django.core.cache import cache
5 from django.core.cache import cache
6
6
7 from django.db import models
7 from django.db import models
8 from django.db.models import Count
8 from django.db.models import Count
9 from django.http import Http404
9 from django.http import Http404
10 from django.utils import timezone
10 from django.utils import timezone
11 from markupfield.fields import MarkupField
11 from markupfield.fields import MarkupField
12 from boards import settings as board_settings
12 from boards import settings as board_settings
13
13
14 from neboard import settings
14 from neboard import settings
15 import thumbs
15 import thumbs
16
16
17 import re
17 import re
18
18
19 IMAGE_THUMB_SIZE = (200, 150)
19 IMAGE_THUMB_SIZE = (200, 150)
20
20
21 TITLE_MAX_LENGTH = 50
21 TITLE_MAX_LENGTH = 50
22
22
23 DEFAULT_MARKUP_TYPE = 'markdown'
23 DEFAULT_MARKUP_TYPE = 'markdown'
24
24
25 NO_PARENT = -1
25 NO_PARENT = -1
26 NO_IP = '0.0.0.0'
26 NO_IP = '0.0.0.0'
27 UNKNOWN_UA = ''
27 UNKNOWN_UA = ''
28 ALL_PAGES = -1
28 ALL_PAGES = -1
29 OPENING_POST_POPULARITY_WEIGHT = 2
29 OPENING_POST_POPULARITY_WEIGHT = 2
30 IMAGES_DIRECTORY = 'images/'
30 IMAGES_DIRECTORY = 'images/'
31 FILE_EXTENSION_DELIMITER = '.'
31 FILE_EXTENSION_DELIMITER = '.'
32
32
33 RANK_ADMIN = 0
33 RANK_ADMIN = 0
34 RANK_MODERATOR = 10
34 RANK_MODERATOR = 10
35 RANK_USER = 100
35 RANK_USER = 100
36
36
37 SETTING_MODERATE = "moderate"
37 SETTING_MODERATE = "moderate"
38
38
39 REGEX_REPLY = re.compile('>>(\d+)')
39 REGEX_REPLY = re.compile('>>(\d+)')
40
40
41
41
42 class PostManager(models.Manager):
42 class PostManager(models.Manager):
43
43
44 def create_post(self, title, text, image=None, thread=None,
44 def create_post(self, title, text, image=None, thread=None,
45 ip=NO_IP, tags=None, user=None):
45 ip=NO_IP, tags=None, user=None):
46 post = self.create(title=title,
46 post = self.create(title=title,
47 text=text,
47 text=text,
48 pub_time=timezone.now(),
48 pub_time=timezone.now(),
49 thread=thread,
49 thread=thread,
50 image=image,
50 image=image,
51 poster_ip=ip,
51 poster_ip=ip,
52 poster_user_agent=UNKNOWN_UA,
52 poster_user_agent=UNKNOWN_UA,
53 last_edit_time=timezone.now(),
53 last_edit_time=timezone.now(),
54 bump_time=timezone.now(),
54 bump_time=timezone.now(),
55 user=user)
55 user=user)
56
56
57 if tags:
57 if tags:
58 map(post.tags.add, tags)
58 map(post.tags.add, tags)
59 for tag in tags:
59 for tag in tags:
60 tag.threads.add(post)
60 tag.threads.add(post)
61
61
62 if thread:
62 if thread:
63 thread.replies.add(post)
63 thread.replies.add(post)
64 thread.bump()
64 thread.bump()
65 thread.last_edit_time = timezone.now()
65 thread.last_edit_time = timezone.now()
66 thread.save()
66 thread.save()
67
68 cache_key = thread.get_cache_key
69 cache.delete(cache_key)
70
67 else:
71 else:
68 self._delete_old_threads()
72 self._delete_old_threads()
69
73
70 self.connect_replies(post)
74 self.connect_replies(post)
71
75
72 return post
76 return post
73
77
74 def delete_post(self, post):
78 def delete_post(self, post):
75 if post.replies.count() > 0:
79 if post.replies.count() > 0:
76 map(self.delete_post, post.replies.all())
80 map(self.delete_post, post.replies.all())
77
81
78 # Update thread's last edit time (used as cache key)
82 # Update thread's last edit time (used as cache key)
79 thread = post.thread
83 thread = post.thread
80 if thread:
84 if thread:
81 thread.last_edit_time = timezone.now()
85 thread.last_edit_time = timezone.now()
82 thread.save()
86 thread.save()
83
87
88 cache_key = thread.get_cache_key
89 cache.delete(cache_key)
90
84 post.delete()
91 post.delete()
85
92
86 def delete_posts_by_ip(self, ip):
93 def delete_posts_by_ip(self, ip):
87 posts = self.filter(poster_ip=ip)
94 posts = self.filter(poster_ip=ip)
88 map(self.delete_post, posts)
95 map(self.delete_post, posts)
89
96
90 def get_threads(self, tag=None, page=ALL_PAGES,
97 def get_threads(self, tag=None, page=ALL_PAGES,
91 order_by='-bump_time'):
98 order_by='-bump_time'):
92 if tag:
99 if tag:
93 threads = tag.threads
100 threads = tag.threads
94
101
95 if threads.count() == 0:
102 if threads.count() == 0:
96 raise Http404
103 raise Http404
97 else:
104 else:
98 threads = self.filter(thread=None)
105 threads = self.filter(thread=None)
99
106
100 threads = threads.order_by(order_by)
107 threads = threads.order_by(order_by)
101
108
102 if page != ALL_PAGES:
109 if page != ALL_PAGES:
103 thread_count = threads.count()
110 thread_count = threads.count()
104
111
105 if page < self._get_page_count(thread_count):
112 if page < self._get_page_count(thread_count):
106 start_thread = page * settings.THREADS_PER_PAGE
113 start_thread = page * settings.THREADS_PER_PAGE
107 end_thread = min(start_thread + settings.THREADS_PER_PAGE,
114 end_thread = min(start_thread + settings.THREADS_PER_PAGE,
108 thread_count)
115 thread_count)
109 threads = threads[start_thread:end_thread]
116 threads = threads[start_thread:end_thread]
110
117
111 return threads
118 return threads
112
119
113 def get_thread(self, opening_post_id):
120 def get_thread(self, opening_post_id):
114 try:
121 try:
115 opening_post = self.get(id=opening_post_id, thread=None)
122 opening_post = self.get(id=opening_post_id, thread=None)
116 except Post.DoesNotExist:
123 except Post.DoesNotExist:
117 raise Http404
124 raise Http404
118
125
126 cache_key = opening_post.get_cache_key()
127 thread = cache.get(cache_key)
128 if thread:
129 return thread
130
119 if opening_post.replies:
131 if opening_post.replies:
120 thread = [opening_post]
132 thread = [opening_post]
121 thread.extend(opening_post.replies.all().order_by('pub_time'))
133 thread.extend(opening_post.replies.all().order_by('pub_time'))
122
134
135 cache.set(cache_key, thread, board_settings.CACHE_TIMEOUT)
136
123 return thread
137 return thread
124
138
125 def exists(self, post_id):
139 def exists(self, post_id):
126 posts = self.filter(id=post_id)
140 posts = self.filter(id=post_id)
127
141
128 return posts.count() > 0
142 return posts.count() > 0
129
143
130 def get_thread_page_count(self, tag=None):
144 def get_thread_page_count(self, tag=None):
131 if tag:
145 if tag:
132 threads = self.filter(thread=None, tags=tag)
146 threads = self.filter(thread=None, tags=tag)
133 else:
147 else:
134 threads = self.filter(thread=None)
148 threads = self.filter(thread=None)
135
149
136 return self._get_page_count(threads.count())
150 return self._get_page_count(threads.count())
137
151
138 def _delete_old_threads(self):
152 def _delete_old_threads(self):
139 """
153 """
140 Preserves maximum thread count. If there are too many threads,
154 Preserves maximum thread count. If there are too many threads,
141 delete the old ones.
155 delete the old ones.
142 """
156 """
143
157
144 # TODO Move old threads to the archive instead of deleting them.
158 # TODO Move old threads to the archive instead of deleting them.
145 # Maybe make some 'old' field in the model to indicate the thread
159 # Maybe make some 'old' field in the model to indicate the thread
146 # must not be shown and be able for replying.
160 # must not be shown and be able for replying.
147
161
148 threads = self.get_threads()
162 threads = self.get_threads()
149 thread_count = threads.count()
163 thread_count = threads.count()
150
164
151 if thread_count > settings.MAX_THREAD_COUNT:
165 if thread_count > settings.MAX_THREAD_COUNT:
152 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
166 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
153 old_threads = threads[thread_count - num_threads_to_delete:]
167 old_threads = threads[thread_count - num_threads_to_delete:]
154
168
155 map(self.delete_post, old_threads)
169 map(self.delete_post, old_threads)
156
170
157 def connect_replies(self, post):
171 def connect_replies(self, post):
158 """Connect replies to a post to show them as a refmap"""
172 """Connect replies to a post to show them as a refmap"""
159
173
160 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
174 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
161 id = reply_number.group(1)
175 id = reply_number.group(1)
162 ref_post = self.filter(id=id)
176 ref_post = self.filter(id=id)
163 if ref_post.count() > 0:
177 if ref_post.count() > 0:
164 ref_post[0].referenced_posts.add(post)
178 ref_post[0].referenced_posts.add(post)
165
179
166 def _get_page_count(self, thread_count):
180 def _get_page_count(self, thread_count):
167 return int(math.ceil(thread_count / float(settings.THREADS_PER_PAGE)))
181 return int(math.ceil(thread_count / float(settings.THREADS_PER_PAGE)))
168
182
169
183
170 class TagManager(models.Manager):
184 class TagManager(models.Manager):
171
185
172 def get_not_empty_tags(self):
186 def get_not_empty_tags(self):
173 tags = self.annotate(Count('threads')) \
187 tags = self.annotate(Count('threads')) \
174 .filter(threads__count__gt=0).order_by('name')
188 .filter(threads__count__gt=0).order_by('name')
175
189
176 return tags
190 return tags
177
191
178
192
179 class Tag(models.Model):
193 class Tag(models.Model):
180 """
194 """
181 A tag is a text node assigned to the post. The tag serves as a board
195 A tag is a text node assigned to the post. The tag serves as a board
182 section. There can be multiple tags for each message
196 section. There can be multiple tags for each message
183 """
197 """
184
198
185 objects = TagManager()
199 objects = TagManager()
186
200
187 name = models.CharField(max_length=100)
201 name = models.CharField(max_length=100)
188 threads = models.ManyToManyField('Post', null=True,
202 threads = models.ManyToManyField('Post', null=True,
189 blank=True, related_name='tag+')
203 blank=True, related_name='tag+')
190 linked = models.ForeignKey('Tag', null=True, blank=True)
204 linked = models.ForeignKey('Tag', null=True, blank=True)
191
205
192 def __unicode__(self):
206 def __unicode__(self):
193 return self.name
207 return self.name
194
208
195 def is_empty(self):
209 def is_empty(self):
196 return self.get_post_count() == 0
210 return self.get_post_count() == 0
197
211
198 def get_post_count(self):
212 def get_post_count(self):
199 return self.threads.count()
213 return self.threads.count()
200
214
201 def get_popularity(self):
215 def get_popularity(self):
202 posts_with_tag = Post.objects.get_threads(tag=self)
216 posts_with_tag = Post.objects.get_threads(tag=self)
203 reply_count = 0
217 reply_count = 0
204 for post in posts_with_tag:
218 for post in posts_with_tag:
205 reply_count += post.get_reply_count()
219 reply_count += post.get_reply_count()
206 reply_count += OPENING_POST_POPULARITY_WEIGHT
220 reply_count += OPENING_POST_POPULARITY_WEIGHT
207
221
208 return reply_count
222 return reply_count
209
223
210 def get_linked_tags(self):
224 def get_linked_tags(self):
211 tag_list = []
225 tag_list = []
212 self.get_linked_tags_list(tag_list)
226 self.get_linked_tags_list(tag_list)
213
227
214 return tag_list
228 return tag_list
215
229
216 def get_linked_tags_list(self, tag_list=[]):
230 def get_linked_tags_list(self, tag_list=[]):
217 """
231 """
218 Returns the list of tags linked to current. The list can be got
232 Returns the list of tags linked to current. The list can be got
219 through returned value or tag_list parameter
233 through returned value or tag_list parameter
220 """
234 """
221
235
222 linked_tag = self.linked
236 linked_tag = self.linked
223
237
224 if linked_tag and not (linked_tag in tag_list):
238 if linked_tag and not (linked_tag in tag_list):
225 tag_list.append(linked_tag)
239 tag_list.append(linked_tag)
226
240
227 linked_tag.get_linked_tags_list(tag_list)
241 linked_tag.get_linked_tags_list(tag_list)
228
242
229
243
230 class Post(models.Model):
244 class Post(models.Model):
231 """A post is a message."""
245 """A post is a message."""
232
246
233 objects = PostManager()
247 objects = PostManager()
234
248
235 def _update_image_filename(self, filename):
249 def _update_image_filename(self, filename):
236 """Get unique image filename"""
250 """Get unique image filename"""
237
251
238 path = IMAGES_DIRECTORY
252 path = IMAGES_DIRECTORY
239 new_name = str(int(time.mktime(time.gmtime())))
253 new_name = str(int(time.mktime(time.gmtime())))
240 new_name += str(int(random() * 1000))
254 new_name += str(int(random() * 1000))
241 new_name += FILE_EXTENSION_DELIMITER
255 new_name += FILE_EXTENSION_DELIMITER
242 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
256 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
243
257
244 return os.path.join(path, new_name)
258 return os.path.join(path, new_name)
245
259
246 title = models.CharField(max_length=TITLE_MAX_LENGTH)
260 title = models.CharField(max_length=TITLE_MAX_LENGTH)
247 pub_time = models.DateTimeField()
261 pub_time = models.DateTimeField()
248 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
262 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
249 escape_html=False)
263 escape_html=False)
250
264
251 image_width = models.IntegerField(default=0)
265 image_width = models.IntegerField(default=0)
252 image_height = models.IntegerField(default=0)
266 image_height = models.IntegerField(default=0)
253
267
254 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
268 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
255 blank=True, sizes=(IMAGE_THUMB_SIZE,),
269 blank=True, sizes=(IMAGE_THUMB_SIZE,),
256 width_field='image_width',
270 width_field='image_width',
257 height_field='image_height')
271 height_field='image_height')
258
272
259 poster_ip = models.GenericIPAddressField()
273 poster_ip = models.GenericIPAddressField()
260 poster_user_agent = models.TextField()
274 poster_user_agent = models.TextField()
261
275
262 thread = models.ForeignKey('Post', null=True, default=None)
276 thread = models.ForeignKey('Post', null=True, default=None)
263 tags = models.ManyToManyField(Tag)
277 tags = models.ManyToManyField(Tag)
264 last_edit_time = models.DateTimeField()
278 last_edit_time = models.DateTimeField()
265 bump_time = models.DateTimeField()
279 bump_time = models.DateTimeField()
266 user = models.ForeignKey('User', null=True, default=None)
280 user = models.ForeignKey('User', null=True, default=None)
267
281
268 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
282 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
269 blank=True, related_name='re+')
283 blank=True, related_name='re+')
270 referenced_posts = models.ManyToManyField('Post', symmetrical=False, null=True,
284 referenced_posts = models.ManyToManyField('Post', symmetrical=False, null=True,
271 blank=True, related_name='rfp+')
285 blank=True, related_name='rfp+')
272
286
273 def __unicode__(self):
287 def __unicode__(self):
274 return '#' + str(self.id) + ' ' + self.title + ' (' + \
288 return '#' + str(self.id) + ' ' + self.title + ' (' + \
275 self.text.raw[:50] + ')'
289 self.text.raw[:50] + ')'
276
290
277 def get_title(self):
291 def get_title(self):
278 title = self.title
292 title = self.title
279 if len(title) == 0:
293 if len(title) == 0:
280 title = self.text.raw[:20]
294 title = self.text.raw[:20]
281
295
282 return title
296 return title
283
297
284 def get_reply_count(self):
298 def get_reply_count(self):
285 return self.replies.count()
299 return self.replies.count()
286
300
287 def get_images_count(self):
301 def get_images_count(self):
288 images_count = 1 if self.image else 0
302 images_count = 1 if self.image else 0
289 images_count += self.replies.filter(image_width__gt=0).count()
303 images_count += self.replies.filter(image_width__gt=0).count()
290
304
291 return images_count
305 return images_count
292
306
293 def can_bump(self):
307 def can_bump(self):
294 """Check if the thread can be bumped by replying"""
308 """Check if the thread can be bumped by replying"""
295
309
296 post_count = self.get_reply_count() + 1
310 post_count = self.get_reply_count() + 1
297
311
298 return post_count <= settings.MAX_POSTS_PER_THREAD
312 return post_count <= settings.MAX_POSTS_PER_THREAD
299
313
300 def bump(self):
314 def bump(self):
301 """Bump (move to up) thread"""
315 """Bump (move to up) thread"""
302
316
303 if self.can_bump():
317 if self.can_bump():
304 self.bump_time = timezone.now()
318 self.bump_time = timezone.now()
305
319
320
306 def get_last_replies(self):
321 def get_last_replies(self):
307 if settings.LAST_REPLIES_COUNT > 0:
322 if settings.LAST_REPLIES_COUNT > 0:
308 reply_count = self.get_reply_count()
323 reply_count = self.get_reply_count()
309
324
310 if reply_count > 0:
325 if reply_count > 0:
311 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
326 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
312 reply_count)
327 reply_count)
313 last_replies = self.replies.all().order_by('pub_time')[reply_count -
328 last_replies = self.replies.all().order_by('pub_time')[reply_count -
314 reply_count_to_show:]
329 reply_count_to_show:]
315
330
316 return last_replies
331 return last_replies
317
332
318 def get_tags(self):
333 def get_tags(self):
319 """Get a sorted tag list"""
334 """Get a sorted tag list"""
320
335
321 return self.tags.order_by('name')
336 return self.tags.order_by('name')
322
337
338 def get_cache_key(self):
339 return str(self.id) + str(self.last_edit_time)
340
323
341
324 class User(models.Model):
342 class User(models.Model):
325
343
326 user_id = models.CharField(max_length=50)
344 user_id = models.CharField(max_length=50)
327 rank = models.IntegerField()
345 rank = models.IntegerField()
328
346
329 registration_time = models.DateTimeField()
347 registration_time = models.DateTimeField()
330
348
331 fav_tags = models.ManyToManyField(Tag, null=True, blank=True)
349 fav_tags = models.ManyToManyField(Tag, null=True, blank=True)
332 fav_threads = models.ManyToManyField(Post, related_name='+', null=True,
350 fav_threads = models.ManyToManyField(Post, related_name='+', null=True,
333 blank=True)
351 blank=True)
334
352
335 def save_setting(self, name, value):
353 def save_setting(self, name, value):
336 setting, created = Setting.objects.get_or_create(name=name, user=self)
354 setting, created = Setting.objects.get_or_create(name=name, user=self)
337 setting.value = str(value)
355 setting.value = str(value)
338 setting.save()
356 setting.save()
339
357
340 return setting
358 return setting
341
359
342 def get_setting(self, name):
360 def get_setting(self, name):
343 if Setting.objects.filter(name=name, user=self).exists():
361 if Setting.objects.filter(name=name, user=self).exists():
344 setting = Setting.objects.get(name=name, user=self)
362 setting = Setting.objects.get(name=name, user=self)
345 setting_value = setting.value
363 setting_value = setting.value
346 else:
364 else:
347 setting_value = None
365 setting_value = None
348
366
349 return setting_value
367 return setting_value
350
368
351 def is_moderator(self):
369 def is_moderator(self):
352 return RANK_MODERATOR >= self.rank
370 return RANK_MODERATOR >= self.rank
353
371
354 def get_sorted_fav_tags(self):
372 def get_sorted_fav_tags(self):
355 cache_key = self._get_tag_cache_key()
373 cache_key = self._get_tag_cache_key()
356 fav_tags = cache.get(cache_key)
374 fav_tags = cache.get(cache_key)
357 if fav_tags:
375 if fav_tags:
358 return fav_tags
376 return fav_tags
359
377
360 tags = self.fav_tags.annotate(Count('threads'))\
378 tags = self.fav_tags.annotate(Count('threads'))\
361 .filter(threads__count__gt=0).order_by('name')
379 .filter(threads__count__gt=0).order_by('name')
362
380
363 if tags:
381 if tags:
364 cache.set(cache_key, tags, board_settings.CACHE_TIMEOUT)
382 cache.set(cache_key, tags, board_settings.CACHE_TIMEOUT)
365
383
366 return tags
384 return tags
367
385
368 def get_post_count(self):
386 def get_post_count(self):
369 return Post.objects.filter(user=self).count()
387 return Post.objects.filter(user=self).count()
370
388
371 def __unicode__(self):
389 def __unicode__(self):
372 return self.user_id + '(' + str(self.rank) + ')'
390 return self.user_id + '(' + str(self.rank) + ')'
373
391
374 def get_last_access_time(self):
392 def get_last_access_time(self):
375 posts = Post.objects.filter(user=self)
393 posts = Post.objects.filter(user=self)
376 if posts.count() > 0:
394 if posts.count() > 0:
377 return posts.latest('pub_time').pub_time
395 return posts.latest('pub_time').pub_time
378
396
379 def add_tag(self, tag):
397 def add_tag(self, tag):
380 self.fav_tags.add(tag)
398 self.fav_tags.add(tag)
381 cache.delete(self._get_tag_cache_key())
399 cache.delete(self._get_tag_cache_key())
382
400
383 def remove_tag(self, tag):
401 def remove_tag(self, tag):
384 self.fav_tags.remove(tag)
402 self.fav_tags.remove(tag)
385 cache.delete(self._get_tag_cache_key())
403 cache.delete(self._get_tag_cache_key())
386
404
387 def _get_tag_cache_key(self):
405 def _get_tag_cache_key(self):
388 return self.user_id + '_tags'
406 return self.user_id + '_tags'
389
407
390
408
391 class Setting(models.Model):
409 class Setting(models.Model):
392
410
393 name = models.CharField(max_length=50)
411 name = models.CharField(max_length=50)
394 value = models.CharField(max_length=50)
412 value = models.CharField(max_length=50)
395 user = models.ForeignKey(User)
413 user = models.ForeignKey(User)
396
414
397
415
398 class Ban(models.Model):
416 class Ban(models.Model):
399
417
400 ip = models.GenericIPAddressField()
418 ip = models.GenericIPAddressField()
401
419
402 def __unicode__(self):
420 def __unicode__(self):
403 return self.ip
421 return self.ip
General Comments 0
You need to be logged in to leave comments. Login now