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