##// END OF EJS Templates
Speed up getting post URL and made it work when OP is just created
neko259 -
r1443:e96568cb default
parent child Browse files
Show More
@@ -1,372 +1,372 b''
1 import logging
1 import logging
2 import re
2 import re
3 import uuid
3 import uuid
4
4
5 from django.core.exceptions import ObjectDoesNotExist
5 from django.core.exceptions import ObjectDoesNotExist
6 from django.core.urlresolvers import reverse
6 from django.core.urlresolvers import reverse
7 from django.db import models
7 from django.db import models
8 from django.db.models import TextField, QuerySet
8 from django.db.models import TextField, QuerySet
9 from django.template.defaultfilters import striptags, truncatewords
9 from django.template.defaultfilters import striptags, truncatewords
10 from django.template.loader import render_to_string
10 from django.template.loader import render_to_string
11 from django.utils import timezone
11 from django.utils import timezone
12
12
13 from boards import settings
13 from boards import settings
14 from boards.abstracts.tripcode import Tripcode
14 from boards.abstracts.tripcode import Tripcode
15 from boards.mdx_neboard import Parser
15 from boards.mdx_neboard import Parser
16 from boards.models import PostImage, Attachment
16 from boards.models import PostImage, Attachment
17 from boards.models.base import Viewable
17 from boards.models.base import Viewable
18 from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
18 from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
19 from boards.models.post.manager import PostManager
19 from boards.models.post.manager import PostManager
20 from boards.models.user import Notification
20 from boards.models.user import Notification
21
21
22 CSS_CLS_HIDDEN_POST = 'hidden_post'
22 CSS_CLS_HIDDEN_POST = 'hidden_post'
23 CSS_CLS_DEAD_POST = 'dead_post'
23 CSS_CLS_DEAD_POST = 'dead_post'
24 CSS_CLS_ARCHIVE_POST = 'archive_post'
24 CSS_CLS_ARCHIVE_POST = 'archive_post'
25 CSS_CLS_POST = 'post'
25 CSS_CLS_POST = 'post'
26 CSS_CLS_MONOCHROME = 'monochrome'
26 CSS_CLS_MONOCHROME = 'monochrome'
27
27
28 TITLE_MAX_WORDS = 10
28 TITLE_MAX_WORDS = 10
29
29
30 APP_LABEL_BOARDS = 'boards'
30 APP_LABEL_BOARDS = 'boards'
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 = 200
36 TITLE_MAX_LENGTH = 200
37
37
38 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
38 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
39 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
39 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
40
40
41 PARAMETER_TRUNCATED = 'truncated'
41 PARAMETER_TRUNCATED = 'truncated'
42 PARAMETER_TAG = 'tag'
42 PARAMETER_TAG = 'tag'
43 PARAMETER_OFFSET = 'offset'
43 PARAMETER_OFFSET = 'offset'
44 PARAMETER_DIFF_TYPE = 'type'
44 PARAMETER_DIFF_TYPE = 'type'
45 PARAMETER_CSS_CLASS = 'css_class'
45 PARAMETER_CSS_CLASS = 'css_class'
46 PARAMETER_THREAD = 'thread'
46 PARAMETER_THREAD = 'thread'
47 PARAMETER_IS_OPENING = 'is_opening'
47 PARAMETER_IS_OPENING = 'is_opening'
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 'need_open_link',
57 'need_open_link',
58 'truncated',
58 'truncated',
59 'mode_tree',
59 'mode_tree',
60 'perms',
60 'perms',
61 )
61 )
62
62
63
63
64 class Post(models.Model, Viewable):
64 class Post(models.Model, Viewable):
65 """A post is a message."""
65 """A post is a message."""
66
66
67 objects = PostManager()
67 objects = PostManager()
68
68
69 class Meta:
69 class Meta:
70 app_label = APP_LABEL_BOARDS
70 app_label = APP_LABEL_BOARDS
71 ordering = ('id',)
71 ordering = ('id',)
72
72
73 title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
73 title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
74 pub_time = models.DateTimeField()
74 pub_time = models.DateTimeField()
75 text = TextField(blank=True, null=True)
75 text = TextField(blank=True, null=True)
76 _text_rendered = TextField(blank=True, null=True, editable=False)
76 _text_rendered = TextField(blank=True, null=True, editable=False)
77
77
78 images = models.ManyToManyField(PostImage, null=True, blank=True,
78 images = models.ManyToManyField(PostImage, null=True, blank=True,
79 related_name='post_images', db_index=True)
79 related_name='post_images', db_index=True)
80 attachments = models.ManyToManyField(Attachment, null=True, blank=True,
80 attachments = models.ManyToManyField(Attachment, null=True, blank=True,
81 related_name='attachment_posts')
81 related_name='attachment_posts')
82
82
83 poster_ip = models.GenericIPAddressField()
83 poster_ip = models.GenericIPAddressField()
84
84
85 # TODO This field can be removed cause UID is used for update now
85 # TODO This field can be removed cause UID is used for update now
86 last_edit_time = models.DateTimeField()
86 last_edit_time = models.DateTimeField()
87
87
88 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
88 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
89 null=True,
89 null=True,
90 blank=True, related_name='refposts',
90 blank=True, related_name='refposts',
91 db_index=True)
91 db_index=True)
92 refmap = models.TextField(null=True, blank=True)
92 refmap = models.TextField(null=True, blank=True)
93 threads = models.ManyToManyField('Thread', db_index=True,
93 threads = models.ManyToManyField('Thread', db_index=True,
94 related_name='multi_replies')
94 related_name='multi_replies')
95 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
95 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
96
96
97 url = models.TextField()
97 url = models.TextField()
98 uid = models.TextField(db_index=True)
98 uid = models.TextField(db_index=True)
99
99
100 tripcode = models.CharField(max_length=50, blank=True, default='')
100 tripcode = models.CharField(max_length=50, blank=True, default='')
101 opening = models.BooleanField(db_index=True)
101 opening = models.BooleanField(db_index=True)
102 hidden = models.BooleanField(default=False)
102 hidden = models.BooleanField(default=False)
103
103
104 def __str__(self):
104 def __str__(self):
105 return 'P#{}/{}'.format(self.id, self.get_title())
105 return 'P#{}/{}'.format(self.id, self.get_title())
106
106
107 def get_referenced_posts(self):
107 def get_referenced_posts(self):
108 threads = self.get_threads().all()
108 threads = self.get_threads().all()
109 return self.referenced_posts.filter(threads__in=threads)\
109 return self.referenced_posts.filter(threads__in=threads)\
110 .order_by('pub_time').distinct().all()
110 .order_by('pub_time').distinct().all()
111
111
112 def get_title(self) -> str:
112 def get_title(self) -> str:
113 return self.title
113 return self.title
114
114
115 def get_title_or_text(self):
115 def get_title_or_text(self):
116 title = self.get_title()
116 title = self.get_title()
117 if not title:
117 if not title:
118 title = truncatewords(striptags(self.get_text()), TITLE_MAX_WORDS)
118 title = truncatewords(striptags(self.get_text()), TITLE_MAX_WORDS)
119
119
120 return title
120 return title
121
121
122 def build_refmap(self) -> None:
122 def build_refmap(self) -> None:
123 """
123 """
124 Builds a replies map string from replies list. This is a cache to stop
124 Builds a replies map string from replies list. This is a cache to stop
125 the server from recalculating the map on every post show.
125 the server from recalculating the map on every post show.
126 """
126 """
127
127
128 post_urls = [refpost.get_link_view()
128 post_urls = [refpost.get_link_view()
129 for refpost in self.referenced_posts.all()]
129 for refpost in self.referenced_posts.all()]
130
130
131 self.refmap = ', '.join(post_urls)
131 self.refmap = ', '.join(post_urls)
132
132
133 def is_referenced(self) -> bool:
133 def is_referenced(self) -> bool:
134 return self.refmap and len(self.refmap) > 0
134 return self.refmap and len(self.refmap) > 0
135
135
136 def is_opening(self) -> bool:
136 def is_opening(self) -> bool:
137 """
137 """
138 Checks if this is an opening post or just a reply.
138 Checks if this is an opening post or just a reply.
139 """
139 """
140
140
141 return self.opening
141 return self.opening
142
142
143 def get_absolute_url(self, thread=None):
143 def get_absolute_url(self, thread=None):
144 url = None
144 url = None
145
145
146 if thread is None:
146 if thread is None:
147 thread = self.get_thread()
147 thread = self.get_thread()
148
148
149 # Url is cached only for the "main" thread. When getting url
149 # Url is cached only for the "main" thread. When getting url
150 # for other threads, do it manually.
150 # for other threads, do it manually.
151 if self.url:
151 if self.url:
152 url = self.url
152 url = self.url
153
153
154 if url is None:
154 if url is None:
155 opening_id = thread.get_opening_post_id()
155 opening = self.is_opening()
156 opening_id = self.id if opening else thread.get_opening_post_id()
156 url = reverse('thread', kwargs={'post_id': opening_id})
157 url = reverse('thread', kwargs={'post_id': opening_id})
157 if self.id != opening_id:
158 if not opening:
158 url += '#' + str(self.id)
159 url += '#' + str(self.id)
159
160
160 return url
161 return url
161
162
162 def get_thread(self):
163 def get_thread(self):
163 return self.thread
164 return self.thread
164
165
165 def get_threads(self) -> QuerySet:
166 def get_threads(self) -> QuerySet:
166 """
167 """
167 Gets post's thread.
168 Gets post's thread.
168 """
169 """
169
170
170 return self.threads
171 return self.threads
171
172
172 def get_view(self, *args, **kwargs) -> str:
173 def get_view(self, *args, **kwargs) -> str:
173 """
174 """
174 Renders post's HTML view. Some of the post params can be passed over
175 Renders post's HTML view. Some of the post params can be passed over
175 kwargs for the means of caching (if we view the thread, some params
176 kwargs for the means of caching (if we view the thread, some params
176 are same for every post and don't need to be computed over and over.
177 are same for every post and don't need to be computed over and over.
177 """
178 """
178
179
179 thread = self.get_thread()
180 thread = self.get_thread()
180
181
181 css_classes = [CSS_CLS_POST]
182 css_classes = [CSS_CLS_POST]
182 if thread.is_archived():
183 if thread.is_archived():
183 css_classes.append(CSS_CLS_ARCHIVE_POST)
184 css_classes.append(CSS_CLS_ARCHIVE_POST)
184 elif not thread.can_bump():
185 elif not thread.can_bump():
185 css_classes.append(CSS_CLS_DEAD_POST)
186 css_classes.append(CSS_CLS_DEAD_POST)
186 if self.is_hidden():
187 if self.is_hidden():
187 css_classes.append(CSS_CLS_HIDDEN_POST)
188 css_classes.append(CSS_CLS_HIDDEN_POST)
188 if thread.is_monochrome():
189 if thread.is_monochrome():
189 css_classes.append(CSS_CLS_MONOCHROME)
190 css_classes.append(CSS_CLS_MONOCHROME)
190
191
191 params = dict()
192 params = dict()
192 for param in POST_VIEW_PARAMS:
193 for param in POST_VIEW_PARAMS:
193 if param in kwargs:
194 if param in kwargs:
194 params[param] = kwargs[param]
195 params[param] = kwargs[param]
195
196
196 params.update({
197 params.update({
197 PARAMETER_POST: self,
198 PARAMETER_POST: self,
198 PARAMETER_IS_OPENING: self.is_opening(),
199 PARAMETER_IS_OPENING: self.is_opening(),
199 PARAMETER_THREAD: thread,
200 PARAMETER_THREAD: thread,
200 PARAMETER_CSS_CLASS: ' '.join(css_classes),
201 PARAMETER_CSS_CLASS: ' '.join(css_classes),
201 })
202 })
202
203
203 return render_to_string('boards/post.html', params)
204 return render_to_string('boards/post.html', params)
204
205
205 def get_search_view(self, *args, **kwargs):
206 def get_search_view(self, *args, **kwargs):
206 return self.get_view(need_op_data=True, *args, **kwargs)
207 return self.get_view(need_op_data=True, *args, **kwargs)
207
208
208 def get_first_image(self) -> PostImage:
209 def get_first_image(self) -> PostImage:
209 return self.images.earliest('id')
210 return self.images.earliest('id')
210
211
211 def delete(self, using=None):
212 def delete(self, using=None):
212 """
213 """
213 Deletes all post images and the post itself.
214 Deletes all post images and the post itself.
214 """
215 """
215
216
216 for image in self.images.all():
217 for image in self.images.all():
217 image_refs_count = image.post_images.count()
218 image_refs_count = image.post_images.count()
218 if image_refs_count == 1:
219 if image_refs_count == 1:
219 image.delete()
220 image.delete()
220
221
221 for attachment in self.attachments.all():
222 for attachment in self.attachments.all():
222 attachment_refs_count = attachment.attachment_posts.count()
223 attachment_refs_count = attachment.attachment_posts.count()
223 if attachment_refs_count == 1:
224 if attachment_refs_count == 1:
224 attachment.delete()
225 attachment.delete()
225
226
226 thread = self.get_thread()
227 thread = self.get_thread()
227 thread.last_edit_time = timezone.now()
228 thread.last_edit_time = timezone.now()
228 thread.save()
229 thread.save()
229
230
230 super(Post, self).delete(using)
231 super(Post, self).delete(using)
231
232
232 logging.getLogger('boards.post.delete').info(
233 logging.getLogger('boards.post.delete').info(
233 'Deleted post {}'.format(self))
234 'Deleted post {}'.format(self))
234
235
235 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
236 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
236 include_last_update=False) -> str:
237 include_last_update=False) -> str:
237 """
238 """
238 Gets post HTML or JSON data that can be rendered on a page or used by
239 Gets post HTML or JSON data that can be rendered on a page or used by
239 API.
240 API.
240 """
241 """
241
242
242 return get_exporter(format_type).export(self, request,
243 return get_exporter(format_type).export(self, request,
243 include_last_update)
244 include_last_update)
244
245
245 def notify_clients(self, recursive=True):
246 def notify_clients(self, recursive=True):
246 """
247 """
247 Sends post HTML data to the thread web socket.
248 Sends post HTML data to the thread web socket.
248 """
249 """
249
250
250 if not settings.get_bool('External', 'WebsocketsEnabled'):
251 if not settings.get_bool('External', 'WebsocketsEnabled'):
251 return
252 return
252
253
253 thread_ids = list()
254 thread_ids = list()
254 for thread in self.get_threads().all():
255 for thread in self.get_threads().all():
255 thread_ids.append(thread.id)
256 thread_ids.append(thread.id)
256
257
257 thread.notify_clients()
258 thread.notify_clients()
258
259
259 if recursive:
260 if recursive:
260 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
261 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
261 post_id = reply_number.group(1)
262 post_id = reply_number.group(1)
262
263
263 try:
264 try:
264 ref_post = Post.objects.get(id=post_id)
265 ref_post = Post.objects.get(id=post_id)
265
266
266 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
267 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
267 # If post is in this thread, its thread was already notified.
268 # If post is in this thread, its thread was already notified.
268 # Otherwise, notify its thread separately.
269 # Otherwise, notify its thread separately.
269 ref_post.notify_clients(recursive=False)
270 ref_post.notify_clients(recursive=False)
270 except ObjectDoesNotExist:
271 except ObjectDoesNotExist:
271 pass
272 pass
272
273
273 def build_url(self):
274 def build_url(self):
274 self.url = self.get_absolute_url()
275 self.url = self.get_absolute_url()
275 self.save(update_fields=['url'])
276 self.save(update_fields=['url'])
276
277
277 def save(self, force_insert=False, force_update=False, using=None,
278 def save(self, force_insert=False, force_update=False, using=None,
278 update_fields=None):
279 update_fields=None):
279 new_post = self.id is None
280 new_post = self.id is None
280
281
281 self._text_rendered = Parser().parse(self.get_raw_text())
282 self._text_rendered = Parser().parse(self.get_raw_text())
282
283
283 self.uid = str(uuid.uuid4())
284 self.uid = str(uuid.uuid4())
284 if update_fields is not None and 'uid' not in update_fields:
285 if update_fields is not None and 'uid' not in update_fields:
285 update_fields += ['uid']
286 update_fields += ['uid']
286
287
287 if not new_post:
288 if not new_post:
288 for thread in self.get_threads().all():
289 for thread in self.get_threads().all():
289 thread.last_edit_time = self.last_edit_time
290 thread.last_edit_time = self.last_edit_time
290
291
291 thread.save(update_fields=['last_edit_time', 'status'])
292 thread.save(update_fields=['last_edit_time', 'status'])
292
293
293 super().save(force_insert, force_update, using, update_fields)
294 super().save(force_insert, force_update, using, update_fields)
294
295
295 # Post save triggers
296 if self.url is None:
296 if new_post:
297 self.build_url()
297 self.build_url()
298
298
299 self._connect_replies()
299 self._connect_replies()
300 self._connect_notifications()
300 self._connect_notifications()
301
301
302 def get_text(self) -> str:
302 def get_text(self) -> str:
303 return self._text_rendered
303 return self._text_rendered
304
304
305 def get_raw_text(self) -> str:
305 def get_raw_text(self) -> str:
306 return self.text
306 return self.text
307
307
308 def get_absolute_id(self) -> str:
308 def get_absolute_id(self) -> str:
309 """
309 """
310 If the post has many threads, shows its main thread OP id in the post
310 If the post has many threads, shows its main thread OP id in the post
311 ID.
311 ID.
312 """
312 """
313
313
314 if self.get_threads().count() > 1:
314 if self.get_threads().count() > 1:
315 return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id)
315 return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id)
316 else:
316 else:
317 return str(self.id)
317 return str(self.id)
318
318
319 def _connect_notifications(self):
319 def _connect_notifications(self):
320 for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()):
320 for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()):
321 user_name = reply_number.group(1).lower()
321 user_name = reply_number.group(1).lower()
322 Notification.objects.get_or_create(name=user_name, post=self)
322 Notification.objects.get_or_create(name=user_name, post=self)
323
323
324 def _connect_replies(self):
324 def _connect_replies(self):
325 """
325 """
326 Connects replies to a post to show them as a reflink map
326 Connects replies to a post to show them as a reflink map
327 """
327 """
328
328
329 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
329 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
330 post_id = reply_number.group(1)
330 post_id = reply_number.group(1)
331
331
332 try:
332 try:
333 referenced_post = Post.objects.get(id=post_id)
333 referenced_post = Post.objects.get(id=post_id)
334
334
335 referenced_post.referenced_posts.add(self)
335 referenced_post.referenced_posts.add(self)
336 referenced_post.last_edit_time = self.pub_time
336 referenced_post.last_edit_time = self.pub_time
337 referenced_post.build_refmap()
337 referenced_post.build_refmap()
338 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
338 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
339 except ObjectDoesNotExist:
339 except ObjectDoesNotExist:
340 pass
340 pass
341
341
342 def connect_threads(self, opening_posts):
342 def connect_threads(self, opening_posts):
343 for opening_post in opening_posts:
343 for opening_post in opening_posts:
344 threads = opening_post.get_threads().all()
344 threads = opening_post.get_threads().all()
345 for thread in threads:
345 for thread in threads:
346 if thread.can_bump():
346 if thread.can_bump():
347 thread.update_bump_status()
347 thread.update_bump_status()
348
348
349 thread.last_edit_time = self.last_edit_time
349 thread.last_edit_time = self.last_edit_time
350 thread.save(update_fields=['last_edit_time', 'status'])
350 thread.save(update_fields=['last_edit_time', 'status'])
351 self.threads.add(opening_post.get_thread())
351 self.threads.add(opening_post.get_thread())
352
352
353 def get_tripcode(self):
353 def get_tripcode(self):
354 if self.tripcode:
354 if self.tripcode:
355 return Tripcode(self.tripcode)
355 return Tripcode(self.tripcode)
356
356
357 def get_link_view(self):
357 def get_link_view(self):
358 """
358 """
359 Gets view of a reflink to the post.
359 Gets view of a reflink to the post.
360 """
360 """
361 result = '<a href="{}">&gt;&gt;{}</a>'.format(self.get_absolute_url(),
361 result = '<a href="{}">&gt;&gt;{}</a>'.format(self.get_absolute_url(),
362 self.id)
362 self.id)
363 if self.is_opening():
363 if self.is_opening():
364 result = '<b>{}</b>'.format(result)
364 result = '<b>{}</b>'.format(result)
365
365
366 return result
366 return result
367
367
368 def is_hidden(self) -> bool:
368 def is_hidden(self) -> bool:
369 return self.hidden
369 return self.hidden
370
370
371 def set_hidden(self, hidden):
371 def set_hidden(self, hidden):
372 self.hidden = hidden
372 self.hidden = hidden
@@ -1,144 +1,145 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 hashlib
4 import hashlib
5 from random import random
5 from random import random
6 import time
6 import time
7 import hmac
7 import hmac
8
8
9 from django.core.cache import cache
9 from django.core.cache import cache
10 from django.db.models import Model
10 from django.db.models import Model
11 from django import forms
11 from django import forms
12 from django.template.defaultfilters import filesizeformat
12 from django.template.defaultfilters import filesizeformat
13 from django.utils import timezone
13 from django.utils import timezone
14 from django.utils.translation import ugettext_lazy as _
14 from django.utils.translation import ugettext_lazy as _
15 import magic
15 import magic
16 from portage import os
16 from portage import os
17
17
18 import boards
18 import boards
19 from boards.settings import get_bool
19 from boards.settings import get_bool
20 from neboard import settings
20 from neboard import settings
21
21
22 CACHE_KEY_DELIMITER = '_'
22 CACHE_KEY_DELIMITER = '_'
23
23
24 HTTP_FORWARDED = 'HTTP_X_FORWARDED_FOR'
24 HTTP_FORWARDED = 'HTTP_X_FORWARDED_FOR'
25 META_REMOTE_ADDR = 'REMOTE_ADDR'
25 META_REMOTE_ADDR = 'REMOTE_ADDR'
26
26
27 SETTING_MESSAGES = 'Messages'
27 SETTING_MESSAGES = 'Messages'
28 SETTING_ANON_MODE = 'AnonymousMode'
28 SETTING_ANON_MODE = 'AnonymousMode'
29
29
30 ANON_IP = '127.0.0.1'
30 ANON_IP = '127.0.0.1'
31
31
32 UPLOAD_DIRS ={
32 UPLOAD_DIRS ={
33 'PostImage': 'images/',
33 'PostImage': 'images/',
34 'Attachment': 'files/',
34 'Attachment': 'files/',
35 }
35 }
36 FILE_EXTENSION_DELIMITER = '.'
36 FILE_EXTENSION_DELIMITER = '.'
37
37
38
38
39 def is_anonymous_mode():
39 def is_anonymous_mode():
40 return get_bool(SETTING_MESSAGES, SETTING_ANON_MODE)
40 return get_bool(SETTING_MESSAGES, SETTING_ANON_MODE)
41
41
42
42
43 def get_client_ip(request):
43 def get_client_ip(request):
44 if is_anonymous_mode():
44 if is_anonymous_mode():
45 ip = ANON_IP
45 ip = ANON_IP
46 else:
46 else:
47 x_forwarded_for = request.META.get(HTTP_FORWARDED)
47 x_forwarded_for = request.META.get(HTTP_FORWARDED)
48 if x_forwarded_for:
48 if x_forwarded_for:
49 ip = x_forwarded_for.split(',')[-1].strip()
49 ip = x_forwarded_for.split(',')[-1].strip()
50 else:
50 else:
51 ip = request.META.get(META_REMOTE_ADDR)
51 ip = request.META.get(META_REMOTE_ADDR)
52 return ip
52 return ip
53
53
54
54
55 # TODO The output format is not epoch because it includes microseconds
55 # TODO The output format is not epoch because it includes microseconds
56 def datetime_to_epoch(datetime):
56 def datetime_to_epoch(datetime):
57 return int(time.mktime(timezone.localtime(
57 return int(time.mktime(timezone.localtime(
58 datetime,timezone.get_current_timezone()).timetuple())
58 datetime,timezone.get_current_timezone()).timetuple())
59 * 1000000 + datetime.microsecond)
59 * 1000000 + datetime.microsecond)
60
60
61
61
62 def get_websocket_token(user_id='', timestamp=''):
62 def get_websocket_token(user_id='', timestamp=''):
63 """
63 """
64 Create token to validate information provided by new connection.
64 Create token to validate information provided by new connection.
65 """
65 """
66
66
67 sign = hmac.new(settings.CENTRIFUGE_PROJECT_SECRET.encode())
67 sign = hmac.new(settings.CENTRIFUGE_PROJECT_SECRET.encode())
68 sign.update(settings.CENTRIFUGE_PROJECT_ID.encode())
68 sign.update(settings.CENTRIFUGE_PROJECT_ID.encode())
69 sign.update(user_id.encode())
69 sign.update(user_id.encode())
70 sign.update(timestamp.encode())
70 sign.update(timestamp.encode())
71 token = sign.hexdigest()
71 token = sign.hexdigest()
72
72
73 return token
73 return token
74
74
75
75
76 # TODO Test this carefully
76 # TODO Test this carefully
77 def cached_result(key_method=None):
77 def cached_result(key_method=None):
78 """
78 """
79 Caches method result in the Django's cache system, persisted by object name,
79 Caches method result in the Django's cache system, persisted by object name,
80 object name, model id if object is a Django model, args and kwargs if any.
80 object name, model id if object is a Django model, args and kwargs if any.
81 """
81 """
82 def _cached_result(function):
82 def _cached_result(function):
83 def inner_func(obj, *args, **kwargs):
83 def inner_func(obj, *args, **kwargs):
84 cache_key_params = [obj.__class__.__name__, function.__name__]
84 cache_key_params = [obj.__class__.__name__, function.__name__]
85
85
86 cache_key_params += args
86 cache_key_params += args
87 for key, value in kwargs:
87 for key, value in kwargs:
88 cache_key_params.append(key + ':' + value)
88 cache_key_params.append(key + ':' + value)
89
89
90 if isinstance(obj, Model):
90 if isinstance(obj, Model):
91 cache_key_params.append(str(obj.id))
91 cache_key_params.append(str(obj.id))
92
92
93 if key_method is not None:
93 if key_method is not None:
94 cache_key_params += [str(arg) for arg in key_method(obj)]
94 cache_key_params += [str(arg) for arg in key_method(obj)]
95
95
96 cache_key = CACHE_KEY_DELIMITER.join(cache_key_params)
96 cache_key = CACHE_KEY_DELIMITER.join(cache_key_params)
97
97
98 persisted_result = cache.get(cache_key)
98 persisted_result = cache.get(cache_key)
99 if persisted_result is not None:
99 if persisted_result is not None:
100 result = persisted_result
100 result = persisted_result
101 else:
101 else:
102 result = function(obj, *args, **kwargs)
102 result = function(obj, *args, **kwargs)
103 cache.set(cache_key, result)
103 if result is not None:
104 cache.set(cache_key, result)
104
105
105 return result
106 return result
106
107
107 return inner_func
108 return inner_func
108 return _cached_result
109 return _cached_result
109
110
110
111
111 def get_file_hash(file) -> str:
112 def get_file_hash(file) -> str:
112 md5 = hashlib.md5()
113 md5 = hashlib.md5()
113 for chunk in file.chunks():
114 for chunk in file.chunks():
114 md5.update(chunk)
115 md5.update(chunk)
115 return md5.hexdigest()
116 return md5.hexdigest()
116
117
117
118
118 def validate_file_size(size: int):
119 def validate_file_size(size: int):
119 max_size = boards.settings.get_int('Forms', 'MaxFileSize')
120 max_size = boards.settings.get_int('Forms', 'MaxFileSize')
120 if size > max_size:
121 if size > max_size:
121 raise forms.ValidationError(
122 raise forms.ValidationError(
122 _('File must be less than %s but is %s.')
123 _('File must be less than %s but is %s.')
123 % (filesizeformat(max_size), filesizeformat(size)))
124 % (filesizeformat(max_size), filesizeformat(size)))
124
125
125
126
126 def get_extension(filename):
127 def get_extension(filename):
127 return filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
128 return filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
128
129
129
130
130 def get_upload_filename(model_instance, old_filename):
131 def get_upload_filename(model_instance, old_filename):
131 # TODO Use something other than random number in file name
132 # TODO Use something other than random number in file name
132 extension = get_extension(old_filename)
133 extension = get_extension(old_filename)
133 new_name = '{}{}.{}'.format(
134 new_name = '{}{}.{}'.format(
134 str(int(time.mktime(time.gmtime()))),
135 str(int(time.mktime(time.gmtime()))),
135 str(int(random() * 1000)),
136 str(int(random() * 1000)),
136 extension)
137 extension)
137
138
138 directory = UPLOAD_DIRS[type(model_instance).__name__]
139 directory = UPLOAD_DIRS[type(model_instance).__name__]
139
140
140 return os.path.join(directory, new_name)
141 return os.path.join(directory, new_name)
141
142
142
143
143 def get_file_mimetype(file) -> str:
144 def get_file_mimetype(file) -> str:
144 return magic.from_buffer(file.chunks().__next__(), mime=True).decode()
145 return magic.from_buffer(file.chunks().__next__(), mime=True).decode()
General Comments 0
You need to be logged in to leave comments. Login now