##// END OF EJS Templates
Added cache for thread opening post
neko259 -
r593:0e05549a default
parent child Browse files
Show More
@@ -1,444 +1,451 b''
1 1 from datetime import datetime, timedelta, date
2 2 from datetime import time as dtime
3 3 import os
4 4 from random import random
5 5 import time
6 6 import math
7 7 import re
8 8 import hashlib
9 9
10 10 from django.core.cache import cache
11 11 from django.core.paginator import Paginator
12 12 from django.core.urlresolvers import reverse
13 13
14 14 from django.db import models, transaction
15 15 from django.http import Http404
16 16 from django.utils import timezone
17 17 from markupfield.fields import MarkupField
18 18
19 19 from neboard import settings
20 20 from boards import thumbs
21 21
22 22 MAX_TITLE_LENGTH = 50
23 23
24 24 APP_LABEL_BOARDS = 'boards'
25 25
26 26 CACHE_KEY_PPD = 'ppd'
27 27 CACHE_KEY_POST_URL = 'post_url'
28 CACHE_KEY_OPENING_POST = 'opening_post'
28 29
29 30 POSTS_PER_DAY_RANGE = range(7)
30 31
31 32 BAN_REASON_AUTO = 'Auto'
32 33
33 34 IMAGE_THUMB_SIZE = (200, 150)
34 35
35 36 TITLE_MAX_LENGTH = 50
36 37
37 38 DEFAULT_MARKUP_TYPE = 'markdown'
38 39
39 40 NO_PARENT = -1
40 41 NO_IP = '0.0.0.0'
41 42 UNKNOWN_UA = ''
42 43 ALL_PAGES = -1
43 44 IMAGES_DIRECTORY = 'images/'
44 45 FILE_EXTENSION_DELIMITER = '.'
45 46
46 47 SETTING_MODERATE = "moderate"
47 48
48 49 REGEX_REPLY = re.compile('>>(\d+)')
49 50
50 51
51 52 class PostManager(models.Manager):
52 53
53 54 def create_post(self, title, text, image=None, thread=None,
54 55 ip=NO_IP, tags=None, user=None):
55 56 """
56 57 Create new post
57 58 """
58 59
59 60 posting_time = timezone.now()
60 61 if not thread:
61 62 thread = Thread.objects.create(bump_time=posting_time,
62 63 last_edit_time=posting_time)
63 64 else:
64 65 thread.bump()
65 66 thread.last_edit_time = posting_time
66 67 thread.save()
67 68
68 69 post = self.create(title=title,
69 70 text=text,
70 71 pub_time=posting_time,
71 72 thread_new=thread,
72 73 image=image,
73 74 poster_ip=ip,
74 75 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
75 76 # last!
76 77 last_edit_time=posting_time,
77 78 user=user)
78 79
79 80 thread.replies.add(post)
80 81 if tags:
81 82 linked_tags = []
82 83 for tag in tags:
83 84 tag_linked_tags = tag.get_linked_tags()
84 85 if len(tag_linked_tags) > 0:
85 86 linked_tags.extend(tag_linked_tags)
86 87
87 88 tags.extend(linked_tags)
88 89 map(thread.add_tag, tags)
89 90
90 91 self._delete_old_threads()
91 92 self.connect_replies(post)
92 93
93 94 return post
94 95
95 96 def delete_post(self, post):
96 97 """
97 98 Delete post and update or delete its thread
98 99 """
99 100
100 101 thread = post.thread_new
101 102
102 103 if post.is_opening():
103 104 thread.delete_with_posts()
104 105 else:
105 106 thread.last_edit_time = timezone.now()
106 107 thread.save()
107 108
108 109 post.delete()
109 110
110 111 def delete_posts_by_ip(self, ip):
111 112 """
112 113 Delete all posts of the author with same IP
113 114 """
114 115
115 116 posts = self.filter(poster_ip=ip)
116 117 map(self.delete_post, posts)
117 118
118 119 # TODO This method may not be needed any more, because django's paginator
119 120 # is used
120 121 def get_threads(self, tag=None, page=ALL_PAGES,
121 122 order_by='-bump_time', archived=False):
122 123 if tag:
123 124 threads = tag.threads
124 125
125 126 if not threads.exists():
126 127 raise Http404
127 128 else:
128 129 threads = Thread.objects.all()
129 130
130 131 threads = threads.filter(archived=archived).order_by(order_by)
131 132
132 133 if page != ALL_PAGES:
133 134 threads = Paginator(threads, settings.THREADS_PER_PAGE).page(
134 135 page).object_list
135 136
136 137 return threads
137 138
138 139 # TODO Move this method to thread manager
139 140 def _delete_old_threads(self):
140 141 """
141 142 Preserves maximum thread count. If there are too many threads,
142 143 archive the old ones.
143 144 """
144 145
145 146 threads = self.get_threads()
146 147 thread_count = threads.count()
147 148
148 149 if thread_count > settings.MAX_THREAD_COUNT:
149 150 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
150 151 old_threads = threads[thread_count - num_threads_to_delete:]
151 152
152 153 for thread in old_threads:
153 154 thread.archived = True
154 155 thread.last_edit_time = timezone.now()
155 156 thread.save()
156 157
157 158 def connect_replies(self, post):
158 159 """
159 160 Connect replies to a post to show them as a reflink map
160 161 """
161 162
162 163 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
163 164 post_id = reply_number.group(1)
164 165 ref_post = self.filter(id=post_id)
165 166 if ref_post.count() > 0:
166 167 referenced_post = ref_post[0]
167 168 referenced_post.referenced_posts.add(post)
168 169 referenced_post.last_edit_time = post.pub_time
169 170 referenced_post.save()
170 171
171 172 referenced_thread = referenced_post.thread_new
172 173 referenced_thread.last_edit_time = post.pub_time
173 174 referenced_thread.save()
174 175
175 176 def get_posts_per_day(self):
176 177 """
177 178 Get average count of posts per day for the last 7 days
178 179 """
179 180
180 181 today = date.today()
181 182 ppd = cache.get(CACHE_KEY_PPD + str(today))
182 183 if ppd:
183 184 return ppd
184 185
185 186 posts_per_days = []
186 187 for i in POSTS_PER_DAY_RANGE:
187 188 day_end = today - timedelta(i + 1)
188 189 day_start = today - timedelta(i + 2)
189 190
190 191 day_time_start = timezone.make_aware(datetime.combine(day_start,
191 192 dtime()), timezone.get_current_timezone())
192 193 day_time_end = timezone.make_aware(datetime.combine(day_end,
193 194 dtime()), timezone.get_current_timezone())
194 195
195 196 posts_per_days.append(float(self.filter(
196 197 pub_time__lte=day_time_end,
197 198 pub_time__gte=day_time_start).count()))
198 199
199 200 ppd = (sum(posts_per_day for posts_per_day in posts_per_days) /
200 201 len(posts_per_days))
201 202 cache.set(CACHE_KEY_PPD + str(today), ppd)
202 203 return ppd
203 204
204 205
205 206 class Post(models.Model):
206 207 """A post is a message."""
207 208
208 209 objects = PostManager()
209 210
210 211 class Meta:
211 212 app_label = APP_LABEL_BOARDS
212 213
213 214 # TODO Save original file name to some field
214 215 def _update_image_filename(self, filename):
215 216 """Get unique image filename"""
216 217
217 218 path = IMAGES_DIRECTORY
218 219 new_name = str(int(time.mktime(time.gmtime())))
219 220 new_name += str(int(random() * 1000))
220 221 new_name += FILE_EXTENSION_DELIMITER
221 222 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
222 223
223 224 return os.path.join(path, new_name)
224 225
225 226 title = models.CharField(max_length=TITLE_MAX_LENGTH)
226 227 pub_time = models.DateTimeField()
227 228 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
228 229 escape_html=False)
229 230
230 231 image_width = models.IntegerField(default=0)
231 232 image_height = models.IntegerField(default=0)
232 233
233 234 image_pre_width = models.IntegerField(default=0)
234 235 image_pre_height = models.IntegerField(default=0)
235 236
236 237 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
237 238 blank=True, sizes=(IMAGE_THUMB_SIZE,),
238 239 width_field='image_width',
239 240 height_field='image_height',
240 241 preview_width_field='image_pre_width',
241 242 preview_height_field='image_pre_height')
242 243 image_hash = models.CharField(max_length=36)
243 244
244 245 poster_ip = models.GenericIPAddressField()
245 246 poster_user_agent = models.TextField()
246 247
247 248 thread = models.ForeignKey('Post', null=True, default=None)
248 249 thread_new = models.ForeignKey('Thread', null=True, default=None)
249 250 last_edit_time = models.DateTimeField()
250 251 user = models.ForeignKey('User', null=True, default=None)
251 252
252 253 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
253 254 null=True,
254 255 blank=True, related_name='rfp+')
255 256
256 257 def __unicode__(self):
257 258 return '#' + str(self.id) + ' ' + self.title + ' (' + \
258 259 self.text.raw[:50] + ')'
259 260
260 261 def get_title(self):
261 262 title = self.title
262 263 if len(title) == 0:
263 264 title = self.text.rendered
264 265
265 266 return title
266 267
267 268 def get_sorted_referenced_posts(self):
268 269 return self.referenced_posts.order_by('id')
269 270
270 271 def is_referenced(self):
271 272 return self.referenced_posts.all().exists()
272 273
273 274 def is_opening(self):
274 return self.thread_new.get_replies()[0] == self
275 return self.thread_new.get_opening_post() == self
275 276
276 277 def save(self, *args, **kwargs):
277 278 """
278 279 Save the model and compute the image hash
279 280 """
280 281
281 282 if not self.pk and self.image:
282 283 md5 = hashlib.md5()
283 284 for chunk in self.image.chunks():
284 285 md5.update(chunk)
285 286 self.image_hash = md5.hexdigest()
286 287 super(Post, self).save(*args, **kwargs)
287 288
288 289 @transaction.atomic
289 290 def add_tag(self, tag):
290 291 edit_time = timezone.now()
291 292
292 293 thread = self.thread_new
293 294 thread.add_tag(tag)
294 295 self.last_edit_time = edit_time
295 296 self.save()
296 297
297 298 thread.last_edit_time = edit_time
298 299 thread.save()
299 300
300 301 @transaction.atomic
301 302 def remove_tag(self, tag):
302 303 edit_time = timezone.now()
303 304
304 305 thread = self.thread_new
305 306 thread.remove_tag(tag)
306 307 self.last_edit_time = edit_time
307 308 self.save()
308 309
309 310 thread.last_edit_time = edit_time
310 311 thread.save()
311 312
312 313 def get_url(self):
313 314 """
314 315 Get full url to this post
315 316 """
316 317
317 318 cache_key = CACHE_KEY_POST_URL + str(self.id)
318 319 link = cache.get(cache_key)
319 320
320 321 if not link:
321 322 opening_post = self.thread_new.get_opening_post()
322 323 if self != opening_post:
323 324 link = reverse('thread',
324 325 kwargs={'post_id': opening_post.id}) + '#' + str(
325 326 self.id)
326 327 else:
327 328 link = reverse('thread', kwargs={'post_id': self.id})
328 329
329 330 cache.set(cache_key, link)
330 331
331 332 return link
332 333
333 334
334 335 class Thread(models.Model):
335 336
336 337 class Meta:
337 338 app_label = APP_LABEL_BOARDS
338 339
339 340 tags = models.ManyToManyField('Tag')
340 341 bump_time = models.DateTimeField()
341 342 last_edit_time = models.DateTimeField()
342 343 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
343 344 blank=True, related_name='tre+')
344 345 archived = models.BooleanField(default=False)
345 346
346 347 def get_tags(self):
347 348 """
348 349 Get a sorted tag list
349 350 """
350 351
351 352 return self.tags.order_by('name')
352 353
353 354 def bump(self):
354 355 """
355 356 Bump (move to up) thread
356 357 """
357 358
358 359 if self.can_bump():
359 360 self.bump_time = timezone.now()
360 361
361 362 def get_reply_count(self):
362 363 return self.replies.count()
363 364
364 365 def get_images_count(self):
365 366 return self.replies.filter(image_width__gt=0).count()
366 367
367 368 def can_bump(self):
368 369 """
369 370 Check if the thread can be bumped by replying
370 371 """
371 372
372 373 if self.archived:
373 374 return False
374 375
375 376 post_count = self.get_reply_count()
376 377
377 378 return post_count < settings.MAX_POSTS_PER_THREAD
378 379
379 380 def delete_with_posts(self):
380 381 """
381 382 Completely delete thread and all its posts
382 383 """
383 384
384 385 if self.replies.count() > 0:
385 386 self.replies.all().delete()
386 387
387 388 self.delete()
388 389
389 390 def get_last_replies(self):
390 391 """
391 392 Get last replies, not including opening post
392 393 """
393 394
394 395 if settings.LAST_REPLIES_COUNT > 0:
395 396 reply_count = self.get_reply_count()
396 397
397 398 if reply_count > 0:
398 399 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
399 400 reply_count - 1)
400 401 last_replies = self.replies.all().order_by('pub_time')[
401 402 reply_count - reply_count_to_show:]
402 403
403 404 return last_replies
404 405
405 406 def get_skipped_replies_count(self):
406 407 last_replies = self.get_last_replies()
407 408 return self.get_reply_count() - len(last_replies) - 1
408 409
409 410 def get_replies(self):
410 411 """
411 412 Get sorted thread posts
412 413 """
413 414
414 415 return self.replies.all().order_by('pub_time')
415 416
416 417 def add_tag(self, tag):
417 418 """
418 419 Connect thread to a tag and tag to a thread
419 420 """
420 421
421 422 self.tags.add(tag)
422 423 tag.threads.add(self)
423 424
424 425 def remove_tag(self, tag):
425 426 self.tags.remove(tag)
426 427 tag.threads.remove(self)
427 428
428 429 def get_opening_post(self):
429 430 """
430 431 Get first post of the thread
431 432 """
432 433
433 return self.get_replies()[0]
434 cache_key = CACHE_KEY_OPENING_POST + str(self.id)
435 opening_post = cache.get(cache_key)
436 if not opening_post:
437 opening_post = self.get_replies()[0]
438 cache.set(cache_key, opening_post)
439
440 return opening_post
434 441
435 442 def __unicode__(self):
436 443 return str(self.id)
437 444
438 445 def get_pub_time(self):
439 446 """
440 447 Thread does not have its own pub time, so we need to get it from
441 448 the opening post
442 449 """
443 450
444 451 return self.get_opening_post().pub_time
General Comments 0
You need to be logged in to leave comments. Login now