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