##// END OF EJS Templates
Fixed resetting cache when the thread is bumped
neko259 -
r1240:92ddbd4a default
parent child Browse files
Show More
@@ -1,414 +1,418 b''
1 1 from datetime import datetime, timedelta, date
2 2 from datetime import time as dtime
3 3 import logging
4 4 import re
5 5 import uuid
6 6
7 7 from django.core.exceptions import ObjectDoesNotExist
8 8 from django.core.urlresolvers import reverse
9 9 from django.db import models, transaction
10 10 from django.db.models import TextField, QuerySet
11 11 from django.template.loader import render_to_string
12 12 from django.utils import timezone
13 13
14 14 from boards import settings
15 15 from boards.mdx_neboard import Parser
16 16 from boards.models import PostImage
17 17 from boards.models.base import Viewable
18 18 from boards import utils
19 19 from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
20 20 from boards.models.user import Notification, Ban
21 21 import boards.models.thread
22 22
23 23
24 24 APP_LABEL_BOARDS = 'boards'
25 25
26 26 POSTS_PER_DAY_RANGE = 7
27 27
28 28 BAN_REASON_AUTO = 'Auto'
29 29
30 30 IMAGE_THUMB_SIZE = (200, 150)
31 31
32 32 TITLE_MAX_LENGTH = 200
33 33
34 34 # TODO This should be removed
35 35 NO_IP = '0.0.0.0'
36 36
37 37 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
38 38 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
39 39
40 40 PARAMETER_TRUNCATED = 'truncated'
41 41 PARAMETER_TAG = 'tag'
42 42 PARAMETER_OFFSET = 'offset'
43 43 PARAMETER_DIFF_TYPE = 'type'
44 44 PARAMETER_CSS_CLASS = 'css_class'
45 45 PARAMETER_THREAD = 'thread'
46 46 PARAMETER_IS_OPENING = 'is_opening'
47 47 PARAMETER_MODERATOR = 'moderator'
48 48 PARAMETER_POST = 'post'
49 49 PARAMETER_OP_ID = 'opening_post_id'
50 50 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
51 51 PARAMETER_REPLY_LINK = 'reply_link'
52 52 PARAMETER_NEED_OP_DATA = 'need_op_data'
53 53
54 54 POST_VIEW_PARAMS = (
55 55 'need_op_data',
56 56 'reply_link',
57 57 'moderator',
58 58 'need_open_link',
59 59 'truncated',
60 60 'mode_tree',
61 61 )
62 62
63 63 REFMAP_STR = '<a href="{}">&gt;&gt;{}</a>'
64 64
65 65
66 66 class PostManager(models.Manager):
67 67 @transaction.atomic
68 68 def create_post(self, title: str, text: str, image=None, thread=None,
69 69 ip=NO_IP, tags: list=None, opening_posts: list=None):
70 70 """
71 71 Creates new post
72 72 """
73 73
74 74 is_banned = Ban.objects.filter(ip=ip).exists()
75 75
76 76 # TODO Raise specific exception and catch it in the views
77 77 if is_banned:
78 78 raise Exception("This user is banned")
79 79
80 80 if not tags:
81 81 tags = []
82 82 if not opening_posts:
83 83 opening_posts = []
84 84
85 85 posting_time = timezone.now()
86 new_thread = False
86 87 if not thread:
87 88 thread = boards.models.thread.Thread.objects.create(
88 89 bump_time=posting_time, last_edit_time=posting_time)
89 90 list(map(thread.tags.add, tags))
90 91 boards.models.thread.Thread.objects.process_oldest_threads()
91 else:
92 thread.last_edit_time = posting_time
93 thread.bump()
94 thread.save()
92 new_thread = True
95 93
96 94 pre_text = Parser().preparse(text)
97 95
98 96 post = self.create(title=title,
99 97 text=pre_text,
100 98 pub_time=posting_time,
101 99 poster_ip=ip,
102 100 thread=thread,
103 101 last_edit_time=posting_time)
104 102 post.threads.add(thread)
105 103
106 104 logger = logging.getLogger('boards.post.create')
107 105
108 106 logger.info('Created post {} by {}'.format(post, post.poster_ip))
109 107
110 108 if image:
111 109 post.images.add(PostImage.objects.create_with_hash(image))
112 110
113 111 post.build_url()
114 112 post.connect_replies()
115 113 post.connect_threads(opening_posts)
116 114 post.connect_notifications()
117 115
116 # Thread needs to be bumped only when the post is already created
117 if not new_thread:
118 thread.last_edit_time = posting_time
119 thread.bump()
120 thread.save()
121
118 122 return post
119 123
120 124 def delete_posts_by_ip(self, ip):
121 125 """
122 126 Deletes all posts of the author with same IP
123 127 """
124 128
125 129 posts = self.filter(poster_ip=ip)
126 130 for post in posts:
127 131 post.delete()
128 132
129 133 @utils.cached_result()
130 134 def get_posts_per_day(self) -> float:
131 135 """
132 136 Gets average count of posts per day for the last 7 days
133 137 """
134 138
135 139 day_end = date.today()
136 140 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
137 141
138 142 day_time_start = timezone.make_aware(datetime.combine(
139 143 day_start, dtime()), timezone.get_current_timezone())
140 144 day_time_end = timezone.make_aware(datetime.combine(
141 145 day_end, dtime()), timezone.get_current_timezone())
142 146
143 147 posts_per_period = float(self.filter(
144 148 pub_time__lte=day_time_end,
145 149 pub_time__gte=day_time_start).count())
146 150
147 151 ppd = posts_per_period / POSTS_PER_DAY_RANGE
148 152
149 153 return ppd
150 154
151 155
152 156 class Post(models.Model, Viewable):
153 157 """A post is a message."""
154 158
155 159 objects = PostManager()
156 160
157 161 class Meta:
158 162 app_label = APP_LABEL_BOARDS
159 163 ordering = ('id',)
160 164
161 165 title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
162 166 pub_time = models.DateTimeField()
163 167 text = TextField(blank=True, null=True)
164 168 _text_rendered = TextField(blank=True, null=True, editable=False)
165 169
166 170 images = models.ManyToManyField(PostImage, null=True, blank=True,
167 171 related_name='ip+', db_index=True)
168 172
169 173 poster_ip = models.GenericIPAddressField()
170 174
171 175 # TODO This field can be removed cause UID is used for update now
172 176 last_edit_time = models.DateTimeField()
173 177
174 178 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
175 179 null=True,
176 180 blank=True, related_name='refposts',
177 181 db_index=True)
178 182 refmap = models.TextField(null=True, blank=True)
179 183 threads = models.ManyToManyField('Thread', db_index=True)
180 184 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
181 185
182 186 url = models.TextField()
183 187 uid = models.TextField(db_index=True)
184 188
185 189 def __str__(self):
186 190 return 'P#{}/{}'.format(self.id, self.title)
187 191
188 192 def get_referenced_posts(self):
189 193 threads = self.get_threads().all()
190 194 return self.referenced_posts.filter(threads__in=threads)\
191 195 .order_by('pub_time').distinct().all()
192 196
193 197 def get_title(self) -> str:
194 198 """
195 199 Gets original post title or part of its text.
196 200 """
197 201
198 202 title = self.title
199 203 if not title:
200 204 title = self.get_text()
201 205
202 206 return title
203 207
204 208 def build_refmap(self) -> None:
205 209 """
206 210 Builds a replies map string from replies list. This is a cache to stop
207 211 the server from recalculating the map on every post show.
208 212 """
209 213
210 214 post_urls = [REFMAP_STR.format(refpost.get_absolute_url(), refpost.id)
211 215 for refpost in self.referenced_posts.all()]
212 216
213 217 self.refmap = ', '.join(post_urls)
214 218
215 219 def is_referenced(self) -> bool:
216 220 return self.refmap and len(self.refmap) > 0
217 221
218 222 def is_opening(self) -> bool:
219 223 """
220 224 Checks if this is an opening post or just a reply.
221 225 """
222 226
223 227 return self.get_thread().get_opening_post_id() == self.id
224 228
225 229 def get_absolute_url(self):
226 230 if self.url:
227 231 return self.url
228 232 else:
229 233 opening_id = self.get_thread().get_opening_post_id()
230 234 post_url = reverse('thread', kwargs={'post_id': opening_id})
231 235 if self.id != opening_id:
232 236 post_url += '#' + str(self.id)
233 237 return post_url
234 238
235 239
236 240 def get_thread(self):
237 241 return self.thread
238 242
239 243 def get_threads(self) -> QuerySet:
240 244 """
241 245 Gets post's thread.
242 246 """
243 247
244 248 return self.threads
245 249
246 250 def get_view(self, *args, **kwargs) -> str:
247 251 """
248 252 Renders post's HTML view. Some of the post params can be passed over
249 253 kwargs for the means of caching (if we view the thread, some params
250 254 are same for every post and don't need to be computed over and over.
251 255 """
252 256
253 257 thread = self.get_thread()
254 258 is_opening = kwargs.get(PARAMETER_IS_OPENING, self.is_opening())
255 259
256 260 if is_opening:
257 261 opening_post_id = self.id
258 262 else:
259 263 opening_post_id = thread.get_opening_post_id()
260 264
261 265 css_class = 'post'
262 266 if thread.archived:
263 267 css_class += ' archive_post'
264 268 elif not thread.can_bump():
265 269 css_class += ' dead_post'
266 270
267 271 params = dict()
268 272 for param in POST_VIEW_PARAMS:
269 273 if param in kwargs:
270 274 params[param] = kwargs[param]
271 275
272 276 params.update({
273 277 PARAMETER_POST: self,
274 278 PARAMETER_IS_OPENING: is_opening,
275 279 PARAMETER_THREAD: thread,
276 280 PARAMETER_CSS_CLASS: css_class,
277 281 PARAMETER_OP_ID: opening_post_id,
278 282 })
279 283
280 284 return render_to_string('boards/post.html', params)
281 285
282 286 def get_search_view(self, *args, **kwargs):
283 287 return self.get_view(need_op_data=True, *args, **kwargs)
284 288
285 289 def get_first_image(self) -> PostImage:
286 290 return self.images.earliest('id')
287 291
288 292 def delete(self, using=None):
289 293 """
290 294 Deletes all post images and the post itself.
291 295 """
292 296
293 297 for image in self.images.all():
294 298 image_refs_count = Post.objects.filter(images__in=[image]).count()
295 299 if image_refs_count == 1:
296 300 image.delete()
297 301
298 302 thread = self.get_thread()
299 303 thread.last_edit_time = timezone.now()
300 304 thread.save()
301 305
302 306 super(Post, self).delete(using)
303 307
304 308 logging.getLogger('boards.post.delete').info(
305 309 'Deleted post {}'.format(self))
306 310
307 311 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
308 312 include_last_update=False) -> str:
309 313 """
310 314 Gets post HTML or JSON data that can be rendered on a page or used by
311 315 API.
312 316 """
313 317
314 318 return get_exporter(format_type).export(self, request,
315 319 include_last_update)
316 320
317 321 def notify_clients(self, recursive=True):
318 322 """
319 323 Sends post HTML data to the thread web socket.
320 324 """
321 325
322 326 if not settings.get_bool('External', 'WebsocketsEnabled'):
323 327 return
324 328
325 329 thread_ids = list()
326 330 for thread in self.get_threads().all():
327 331 thread_ids.append(thread.id)
328 332
329 333 thread.notify_clients()
330 334
331 335 if recursive:
332 336 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
333 337 post_id = reply_number.group(1)
334 338
335 339 try:
336 340 ref_post = Post.objects.get(id=post_id)
337 341
338 342 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
339 343 # If post is in this thread, its thread was already notified.
340 344 # Otherwise, notify its thread separately.
341 345 ref_post.notify_clients(recursive=False)
342 346 except ObjectDoesNotExist:
343 347 pass
344 348
345 349 def build_url(self):
346 350 self.url = self.get_absolute_url()
347 351 self.save(update_fields=['url'])
348 352
349 353 def save(self, force_insert=False, force_update=False, using=None,
350 354 update_fields=None):
351 355 self._text_rendered = Parser().parse(self.get_raw_text())
352 356
353 357 self.uid = str(uuid.uuid4())
354 358 if update_fields is not None and 'uid' not in update_fields:
355 359 update_fields += ['uid']
356 360
357 361 if self.id:
358 362 for thread in self.get_threads().all():
359 363 thread.last_edit_time = self.last_edit_time
360 364
361 365 thread.save(update_fields=['last_edit_time', 'bumpable'])
362 366
363 367 super().save(force_insert, force_update, using, update_fields)
364 368
365 369 def get_text(self) -> str:
366 370 return self._text_rendered
367 371
368 372 def get_raw_text(self) -> str:
369 373 return self.text
370 374
371 375 def get_absolute_id(self) -> str:
372 376 """
373 377 If the post has many threads, shows its main thread OP id in the post
374 378 ID.
375 379 """
376 380
377 381 if self.get_threads().count() > 1:
378 382 return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id)
379 383 else:
380 384 return str(self.id)
381 385
382 386 def connect_notifications(self):
383 387 for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()):
384 388 user_name = reply_number.group(1).lower()
385 389 Notification.objects.get_or_create(name=user_name, post=self)
386 390
387 391 def connect_replies(self):
388 392 """
389 393 Connects replies to a post to show them as a reflink map
390 394 """
391 395
392 396 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
393 397 post_id = reply_number.group(1)
394 398
395 399 try:
396 400 referenced_post = Post.objects.get(id=post_id)
397 401
398 402 referenced_post.referenced_posts.add(self)
399 403 referenced_post.last_edit_time = self.pub_time
400 404 referenced_post.build_refmap()
401 405 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
402 406 except ObjectDoesNotExist:
403 407 pass
404 408
405 409 def connect_threads(self, opening_posts):
406 410 for opening_post in opening_posts:
407 411 threads = opening_post.get_threads().all()
408 412 for thread in threads:
409 413 if thread.can_bump():
410 414 thread.update_bump_status()
411 415
412 416 thread.last_edit_time = self.last_edit_time
413 417 thread.save(update_fields=['last_edit_time', 'bumpable'])
414 418 self.threads.add(opening_post.get_thread())
@@ -1,85 +1,84 b''
1 1 """
2 2 This module contains helper functions and helper classes.
3 3 """
4 4 import time
5 5 import hmac
6 import functools
7 6
8 7 from django.core.cache import cache
9 8 from django.db.models import Model
10 9
11 10 from django.utils import timezone
12 11
13 12 from neboard import settings
14 13
15 14
16 15 CACHE_KEY_DELIMITER = '_'
17 16 PERMISSION_MODERATE = 'moderation'
18 17
19 18 def get_client_ip(request):
20 19 x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
21 20 if x_forwarded_for:
22 21 ip = x_forwarded_for.split(',')[-1].strip()
23 22 else:
24 23 ip = request.META.get('REMOTE_ADDR')
25 24 return ip
26 25
27 26
28 27 # TODO The output format is not epoch because it includes microseconds
29 28 def datetime_to_epoch(datetime):
30 29 return int(time.mktime(timezone.localtime(
31 30 datetime,timezone.get_current_timezone()).timetuple())
32 31 * 1000000 + datetime.microsecond)
33 32
34 33
35 34 def get_websocket_token(user_id='', timestamp=''):
36 35 """
37 36 Create token to validate information provided by new connection.
38 37 """
39 38
40 39 sign = hmac.new(settings.CENTRIFUGE_PROJECT_SECRET.encode())
41 40 sign.update(settings.CENTRIFUGE_PROJECT_ID.encode())
42 41 sign.update(user_id.encode())
43 42 sign.update(timestamp.encode())
44 43 token = sign.hexdigest()
45 44
46 45 return token
47 46
48 47
49 48 def cached_result(key_method=None):
50 49 """
51 50 Caches method result in the Django's cache system, persisted by object name,
52 51 object name and model id if object is a Django model.
53 52 """
54 53 def _cached_result(function):
55 54 def inner_func(obj, *args, **kwargs):
56 55 # TODO Include method arguments to the cache key
57 56 cache_key_params = [obj.__class__.__name__, function.__name__]
58 57 if isinstance(obj, Model):
59 58 cache_key_params.append(str(obj.id))
60 59
61 60 if key_method is not None:
62 61 cache_key_params += [str(arg) for arg in key_method(obj)]
63 62
64 63 cache_key = CACHE_KEY_DELIMITER.join(cache_key_params)
65 64
66 65 persisted_result = cache.get(cache_key)
67 66 if persisted_result is not None:
68 67 result = persisted_result
69 68 else:
70 69 result = function(obj, *args, **kwargs)
71 70 cache.set(cache_key, result)
72 71
73 72 return result
74 73
75 74 return inner_func
76 75 return _cached_result
77 76
78 77
79 78 def is_moderator(request):
80 79 try:
81 80 moderate = request.user.has_perm(PERMISSION_MODERATE)
82 81 except AttributeError:
83 82 moderate = False
84 83
85 84 return moderate No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now