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