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