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