##// END OF EJS Templates
Rebuild refmap after post from it was deleted
neko259 -
r1619:9c0a2b69 default
parent child Browse files
Show More
@@ -1,394 +1,399 b''
1 import uuid
1 import uuid
2
2
3 import re
3 import re
4 from boards import settings
4 from boards import settings
5 from boards.abstracts.tripcode import Tripcode
5 from boards.abstracts.tripcode import Tripcode
6 from boards.models import Attachment, KeyPair, GlobalId
6 from boards.models import Attachment, KeyPair, GlobalId
7 from boards.models.attachment import FILE_TYPES_IMAGE
7 from boards.models.attachment import FILE_TYPES_IMAGE
8 from boards.models.base import Viewable
8 from boards.models.base import Viewable
9 from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
9 from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
10 from boards.models.post.manager import PostManager
10 from boards.models.post.manager import PostManager
11 from boards.utils import datetime_to_epoch
11 from boards.utils import datetime_to_epoch
12 from django.core.exceptions import ObjectDoesNotExist
12 from django.core.exceptions import ObjectDoesNotExist
13 from django.core.urlresolvers import reverse
13 from django.core.urlresolvers import reverse
14 from django.db import models
14 from django.db import models
15 from django.db.models import TextField, QuerySet, F
15 from django.db.models import TextField, QuerySet, F
16 from django.template.defaultfilters import truncatewords, striptags
16 from django.template.defaultfilters import truncatewords, striptags
17 from django.template.loader import render_to_string
17 from django.template.loader import render_to_string
18
18
19 CSS_CLS_HIDDEN_POST = 'hidden_post'
19 CSS_CLS_HIDDEN_POST = 'hidden_post'
20 CSS_CLS_DEAD_POST = 'dead_post'
20 CSS_CLS_DEAD_POST = 'dead_post'
21 CSS_CLS_ARCHIVE_POST = 'archive_post'
21 CSS_CLS_ARCHIVE_POST = 'archive_post'
22 CSS_CLS_POST = 'post'
22 CSS_CLS_POST = 'post'
23 CSS_CLS_MONOCHROME = 'monochrome'
23 CSS_CLS_MONOCHROME = 'monochrome'
24
24
25 TITLE_MAX_WORDS = 10
25 TITLE_MAX_WORDS = 10
26
26
27 APP_LABEL_BOARDS = 'boards'
27 APP_LABEL_BOARDS = 'boards'
28
28
29 BAN_REASON_AUTO = 'Auto'
29 BAN_REASON_AUTO = 'Auto'
30
30
31 TITLE_MAX_LENGTH = 200
31 TITLE_MAX_LENGTH = 200
32
32
33 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
33 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
34 REGEX_GLOBAL_REPLY = re.compile(r'\[post\](\w+)::([^:]+)::(\d+)\[/post\]')
34 REGEX_GLOBAL_REPLY = re.compile(r'\[post\](\w+)::([^:]+)::(\d+)\[/post\]')
35 REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?')
35 REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?')
36 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
36 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
37
37
38 PARAMETER_TRUNCATED = 'truncated'
38 PARAMETER_TRUNCATED = 'truncated'
39 PARAMETER_TAG = 'tag'
39 PARAMETER_TAG = 'tag'
40 PARAMETER_OFFSET = 'offset'
40 PARAMETER_OFFSET = 'offset'
41 PARAMETER_DIFF_TYPE = 'type'
41 PARAMETER_DIFF_TYPE = 'type'
42 PARAMETER_CSS_CLASS = 'css_class'
42 PARAMETER_CSS_CLASS = 'css_class'
43 PARAMETER_THREAD = 'thread'
43 PARAMETER_THREAD = 'thread'
44 PARAMETER_IS_OPENING = 'is_opening'
44 PARAMETER_IS_OPENING = 'is_opening'
45 PARAMETER_POST = 'post'
45 PARAMETER_POST = 'post'
46 PARAMETER_OP_ID = 'opening_post_id'
46 PARAMETER_OP_ID = 'opening_post_id'
47 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
47 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
48 PARAMETER_REPLY_LINK = 'reply_link'
48 PARAMETER_REPLY_LINK = 'reply_link'
49 PARAMETER_NEED_OP_DATA = 'need_op_data'
49 PARAMETER_NEED_OP_DATA = 'need_op_data'
50
50
51 POST_VIEW_PARAMS = (
51 POST_VIEW_PARAMS = (
52 'need_op_data',
52 'need_op_data',
53 'reply_link',
53 'reply_link',
54 'need_open_link',
54 'need_open_link',
55 'truncated',
55 'truncated',
56 'mode_tree',
56 'mode_tree',
57 'perms',
57 'perms',
58 'tree_depth',
58 'tree_depth',
59 )
59 )
60
60
61
61
62 class Post(models.Model, Viewable):
62 class Post(models.Model, Viewable):
63 """A post is a message."""
63 """A post is a message."""
64
64
65 objects = PostManager()
65 objects = PostManager()
66
66
67 class Meta:
67 class Meta:
68 app_label = APP_LABEL_BOARDS
68 app_label = APP_LABEL_BOARDS
69 ordering = ('id',)
69 ordering = ('id',)
70
70
71 title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
71 title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
72 pub_time = models.DateTimeField()
72 pub_time = models.DateTimeField()
73 text = TextField(blank=True, null=True)
73 text = TextField(blank=True, null=True)
74 _text_rendered = TextField(blank=True, null=True, editable=False)
74 _text_rendered = TextField(blank=True, null=True, editable=False)
75
75
76 attachments = models.ManyToManyField(Attachment, null=True, blank=True,
76 attachments = models.ManyToManyField(Attachment, null=True, blank=True,
77 related_name='attachment_posts')
77 related_name='attachment_posts')
78
78
79 poster_ip = models.GenericIPAddressField()
79 poster_ip = models.GenericIPAddressField()
80
80
81 # Used for cache and threads updating
81 # Used for cache and threads updating
82 last_edit_time = models.DateTimeField()
82 last_edit_time = models.DateTimeField()
83
83
84 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
84 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
85 null=True,
85 null=True,
86 blank=True, related_name='refposts',
86 blank=True, related_name='refposts',
87 db_index=True)
87 db_index=True)
88 refmap = models.TextField(null=True, blank=True)
88 refmap = models.TextField(null=True, blank=True)
89 threads = models.ManyToManyField('Thread', db_index=True,
89 threads = models.ManyToManyField('Thread', db_index=True,
90 related_name='multi_replies')
90 related_name='multi_replies')
91 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
91 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
92
92
93 url = models.TextField()
93 url = models.TextField()
94 uid = models.TextField(db_index=True)
94 uid = models.TextField(db_index=True)
95
95
96 # Global ID with author key. If the message was downloaded from another
96 # Global ID with author key. If the message was downloaded from another
97 # server, this indicates the server.
97 # server, this indicates the server.
98 global_id = models.OneToOneField(GlobalId, null=True, blank=True,
98 global_id = models.OneToOneField(GlobalId, null=True, blank=True,
99 on_delete=models.CASCADE)
99 on_delete=models.CASCADE)
100
100
101 tripcode = models.CharField(max_length=50, blank=True, default='')
101 tripcode = models.CharField(max_length=50, blank=True, default='')
102 opening = models.BooleanField(db_index=True)
102 opening = models.BooleanField(db_index=True)
103 hidden = models.BooleanField(default=False)
103 hidden = models.BooleanField(default=False)
104 version = models.IntegerField(default=1)
104 version = models.IntegerField(default=1)
105
105
106 def __str__(self):
106 def __str__(self):
107 return 'P#{}/{}'.format(self.id, self.get_title())
107 return 'P#{}/{}'.format(self.id, self.get_title())
108
108
109 def get_title(self) -> str:
109 def get_title(self) -> str:
110 return self.title
110 return self.title
111
111
112 def get_title_or_text(self):
112 def get_title_or_text(self):
113 title = self.get_title()
113 title = self.get_title()
114 if not title:
114 if not title:
115 title = truncatewords(striptags(self.get_text()), TITLE_MAX_WORDS)
115 title = truncatewords(striptags(self.get_text()), TITLE_MAX_WORDS)
116
116
117 return title
117 return title
118
118
119 def build_refmap(self) -> None:
119 def build_refmap(self, excluded_ids=None) -> None:
120 """
120 """
121 Builds a replies map string from replies list. This is a cache to stop
121 Builds a replies map string from replies list. This is a cache to stop
122 the server from recalculating the map on every post show.
122 the server from recalculating the map on every post show.
123 """
123 """
124
124
125 post_urls = [refpost.get_link_view()
125 replies = self.referenced_posts
126 for refpost in self.referenced_posts.all()]
126 if excluded_ids is not None:
127 replies = replies.exclude(id__in=excluded_ids)
128 else:
129 replies = replies.all()
130
131 post_urls = [refpost.get_link_view() for refpost in replies]
127
132
128 self.refmap = ', '.join(post_urls)
133 self.refmap = ', '.join(post_urls)
129
134
130 def is_referenced(self) -> bool:
135 def is_referenced(self) -> bool:
131 return self.refmap and len(self.refmap) > 0
136 return self.refmap and len(self.refmap) > 0
132
137
133 def is_opening(self) -> bool:
138 def is_opening(self) -> bool:
134 """
139 """
135 Checks if this is an opening post or just a reply.
140 Checks if this is an opening post or just a reply.
136 """
141 """
137
142
138 return self.opening
143 return self.opening
139
144
140 def get_absolute_url(self, thread=None):
145 def get_absolute_url(self, thread=None):
141 url = None
146 url = None
142
147
143 if thread is None:
148 if thread is None:
144 thread = self.get_thread()
149 thread = self.get_thread()
145
150
146 # Url is cached only for the "main" thread. When getting url
151 # Url is cached only for the "main" thread. When getting url
147 # for other threads, do it manually.
152 # for other threads, do it manually.
148 if self.url:
153 if self.url:
149 url = self.url
154 url = self.url
150
155
151 if url is None:
156 if url is None:
152 opening = self.is_opening()
157 opening = self.is_opening()
153 opening_id = self.id if opening else thread.get_opening_post_id()
158 opening_id = self.id if opening else thread.get_opening_post_id()
154 url = reverse('thread', kwargs={'post_id': opening_id})
159 url = reverse('thread', kwargs={'post_id': opening_id})
155 if not opening:
160 if not opening:
156 url += '#' + str(self.id)
161 url += '#' + str(self.id)
157
162
158 return url
163 return url
159
164
160 def get_thread(self):
165 def get_thread(self):
161 return self.thread
166 return self.thread
162
167
163 def get_thread_id(self):
168 def get_thread_id(self):
164 return self.thread_id
169 return self.thread_id
165
170
166 def get_threads(self) -> QuerySet:
171 def get_threads(self) -> QuerySet:
167 """
172 """
168 Gets post's thread.
173 Gets post's thread.
169 """
174 """
170
175
171 return self.threads
176 return self.threads
172
177
173 def _get_cache_key(self):
178 def _get_cache_key(self):
174 return [datetime_to_epoch(self.last_edit_time)]
179 return [datetime_to_epoch(self.last_edit_time)]
175
180
176 def get_view(self, *args, **kwargs) -> str:
181 def get_view(self, *args, **kwargs) -> str:
177 """
182 """
178 Renders post's HTML view. Some of the post params can be passed over
183 Renders post's HTML view. Some of the post params can be passed over
179 kwargs for the means of caching (if we view the thread, some params
184 kwargs for the means of caching (if we view the thread, some params
180 are same for every post and don't need to be computed over and over.
185 are same for every post and don't need to be computed over and over.
181 """
186 """
182
187
183 thread = self.get_thread()
188 thread = self.get_thread()
184
189
185 css_classes = [CSS_CLS_POST]
190 css_classes = [CSS_CLS_POST]
186 if thread.is_archived():
191 if thread.is_archived():
187 css_classes.append(CSS_CLS_ARCHIVE_POST)
192 css_classes.append(CSS_CLS_ARCHIVE_POST)
188 elif not thread.can_bump():
193 elif not thread.can_bump():
189 css_classes.append(CSS_CLS_DEAD_POST)
194 css_classes.append(CSS_CLS_DEAD_POST)
190 if self.is_hidden():
195 if self.is_hidden():
191 css_classes.append(CSS_CLS_HIDDEN_POST)
196 css_classes.append(CSS_CLS_HIDDEN_POST)
192 if thread.is_monochrome():
197 if thread.is_monochrome():
193 css_classes.append(CSS_CLS_MONOCHROME)
198 css_classes.append(CSS_CLS_MONOCHROME)
194
199
195 params = dict()
200 params = dict()
196 for param in POST_VIEW_PARAMS:
201 for param in POST_VIEW_PARAMS:
197 if param in kwargs:
202 if param in kwargs:
198 params[param] = kwargs[param]
203 params[param] = kwargs[param]
199
204
200 params.update({
205 params.update({
201 PARAMETER_POST: self,
206 PARAMETER_POST: self,
202 PARAMETER_IS_OPENING: self.is_opening(),
207 PARAMETER_IS_OPENING: self.is_opening(),
203 PARAMETER_THREAD: thread,
208 PARAMETER_THREAD: thread,
204 PARAMETER_CSS_CLASS: ' '.join(css_classes),
209 PARAMETER_CSS_CLASS: ' '.join(css_classes),
205 })
210 })
206
211
207 return render_to_string('boards/post.html', params)
212 return render_to_string('boards/post.html', params)
208
213
209 def get_search_view(self, *args, **kwargs):
214 def get_search_view(self, *args, **kwargs):
210 return self.get_view(need_op_data=True, *args, **kwargs)
215 return self.get_view(need_op_data=True, *args, **kwargs)
211
216
212 def get_first_image(self) -> Attachment:
217 def get_first_image(self) -> Attachment:
213 return self.attachments.filter(mimetype__in=FILE_TYPES_IMAGE).earliest('id')
218 return self.attachments.filter(mimetype__in=FILE_TYPES_IMAGE).earliest('id')
214
219
215 def set_global_id(self, key_pair=None):
220 def set_global_id(self, key_pair=None):
216 """
221 """
217 Sets global id based on the given key pair. If no key pair is given,
222 Sets global id based on the given key pair. If no key pair is given,
218 default one is used.
223 default one is used.
219 """
224 """
220
225
221 if key_pair:
226 if key_pair:
222 key = key_pair
227 key = key_pair
223 else:
228 else:
224 try:
229 try:
225 key = KeyPair.objects.get(primary=True)
230 key = KeyPair.objects.get(primary=True)
226 except KeyPair.DoesNotExist:
231 except KeyPair.DoesNotExist:
227 # Do not update the global id because there is no key defined
232 # Do not update the global id because there is no key defined
228 return
233 return
229 global_id = GlobalId(key_type=key.key_type,
234 global_id = GlobalId(key_type=key.key_type,
230 key=key.public_key,
235 key=key.public_key,
231 local_id=self.id)
236 local_id=self.id)
232 global_id.save()
237 global_id.save()
233
238
234 self.global_id = global_id
239 self.global_id = global_id
235
240
236 self.save(update_fields=['global_id'])
241 self.save(update_fields=['global_id'])
237
242
238 def get_pub_time_str(self):
243 def get_pub_time_str(self):
239 return str(self.pub_time)
244 return str(self.pub_time)
240
245
241 def get_replied_ids(self):
246 def get_replied_ids(self):
242 """
247 """
243 Gets ID list of the posts that this post replies.
248 Gets ID list of the posts that this post replies.
244 """
249 """
245
250
246 raw_text = self.get_raw_text()
251 raw_text = self.get_raw_text()
247
252
248 local_replied = REGEX_REPLY.findall(raw_text)
253 local_replied = REGEX_REPLY.findall(raw_text)
249 global_replied = []
254 global_replied = []
250 for match in REGEX_GLOBAL_REPLY.findall(raw_text):
255 for match in REGEX_GLOBAL_REPLY.findall(raw_text):
251 key_type = match[0]
256 key_type = match[0]
252 key = match[1]
257 key = match[1]
253 local_id = match[2]
258 local_id = match[2]
254
259
255 try:
260 try:
256 global_id = GlobalId.objects.get(key_type=key_type,
261 global_id = GlobalId.objects.get(key_type=key_type,
257 key=key, local_id=local_id)
262 key=key, local_id=local_id)
258 for post in Post.objects.filter(global_id=global_id).only('id'):
263 for post in Post.objects.filter(global_id=global_id).only('id'):
259 global_replied.append(post.id)
264 global_replied.append(post.id)
260 except GlobalId.DoesNotExist:
265 except GlobalId.DoesNotExist:
261 pass
266 pass
262 return local_replied + global_replied
267 return local_replied + global_replied
263
268
264 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
269 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
265 include_last_update=False) -> str:
270 include_last_update=False) -> str:
266 """
271 """
267 Gets post HTML or JSON data that can be rendered on a page or used by
272 Gets post HTML or JSON data that can be rendered on a page or used by
268 API.
273 API.
269 """
274 """
270
275
271 return get_exporter(format_type).export(self, request,
276 return get_exporter(format_type).export(self, request,
272 include_last_update)
277 include_last_update)
273
278
274 def notify_clients(self, recursive=True):
279 def notify_clients(self, recursive=True):
275 """
280 """
276 Sends post HTML data to the thread web socket.
281 Sends post HTML data to the thread web socket.
277 """
282 """
278
283
279 if not settings.get_bool('External', 'WebsocketsEnabled'):
284 if not settings.get_bool('External', 'WebsocketsEnabled'):
280 return
285 return
281
286
282 thread_ids = list()
287 thread_ids = list()
283 for thread in self.get_threads().all():
288 for thread in self.get_threads().all():
284 thread_ids.append(thread.id)
289 thread_ids.append(thread.id)
285
290
286 thread.notify_clients()
291 thread.notify_clients()
287
292
288 if recursive:
293 if recursive:
289 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
294 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
290 post_id = reply_number.group(1)
295 post_id = reply_number.group(1)
291
296
292 try:
297 try:
293 ref_post = Post.objects.get(id=post_id)
298 ref_post = Post.objects.get(id=post_id)
294
299
295 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
300 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
296 # If post is in this thread, its thread was already notified.
301 # If post is in this thread, its thread was already notified.
297 # Otherwise, notify its thread separately.
302 # Otherwise, notify its thread separately.
298 ref_post.notify_clients(recursive=False)
303 ref_post.notify_clients(recursive=False)
299 except ObjectDoesNotExist:
304 except ObjectDoesNotExist:
300 pass
305 pass
301
306
302 def build_url(self):
307 def build_url(self):
303 self.url = self.get_absolute_url()
308 self.url = self.get_absolute_url()
304 self.save(update_fields=['url'])
309 self.save(update_fields=['url'])
305
310
306 def save(self, force_insert=False, force_update=False, using=None,
311 def save(self, force_insert=False, force_update=False, using=None,
307 update_fields=None):
312 update_fields=None):
308 new_post = self.id is None
313 new_post = self.id is None
309
314
310 self.uid = str(uuid.uuid4())
315 self.uid = str(uuid.uuid4())
311 if update_fields is not None and 'uid' not in update_fields:
316 if update_fields is not None and 'uid' not in update_fields:
312 update_fields += ['uid']
317 update_fields += ['uid']
313
318
314 if not new_post:
319 if not new_post:
315 for thread in self.get_threads().all():
320 for thread in self.get_threads().all():
316 thread.last_edit_time = self.last_edit_time
321 thread.last_edit_time = self.last_edit_time
317
322
318 thread.save(update_fields=['last_edit_time', 'status'])
323 thread.save(update_fields=['last_edit_time', 'status'])
319
324
320 super().save(force_insert, force_update, using, update_fields)
325 super().save(force_insert, force_update, using, update_fields)
321
326
322 if self.url is None:
327 if self.url is None:
323 self.build_url()
328 self.build_url()
324
329
325 def get_text(self) -> str:
330 def get_text(self) -> str:
326 return self._text_rendered
331 return self._text_rendered
327
332
328 def get_raw_text(self) -> str:
333 def get_raw_text(self) -> str:
329 return self.text
334 return self.text
330
335
331 def get_sync_text(self) -> str:
336 def get_sync_text(self) -> str:
332 """
337 """
333 Returns text applicable for sync. It has absolute post reflinks.
338 Returns text applicable for sync. It has absolute post reflinks.
334 """
339 """
335
340
336 replacements = dict()
341 replacements = dict()
337 for post_id in REGEX_REPLY.findall(self.get_raw_text()):
342 for post_id in REGEX_REPLY.findall(self.get_raw_text()):
338 try:
343 try:
339 absolute_post_id = str(Post.objects.get(id=post_id).global_id)
344 absolute_post_id = str(Post.objects.get(id=post_id).global_id)
340 replacements[post_id] = absolute_post_id
345 replacements[post_id] = absolute_post_id
341 except Post.DoesNotExist:
346 except Post.DoesNotExist:
342 pass
347 pass
343
348
344 text = self.get_raw_text() or ''
349 text = self.get_raw_text() or ''
345 for key in replacements:
350 for key in replacements:
346 text = text.replace('[post]{}[/post]'.format(key),
351 text = text.replace('[post]{}[/post]'.format(key),
347 '[post]{}[/post]'.format(replacements[key]))
352 '[post]{}[/post]'.format(replacements[key]))
348 text = text.replace('\r\n', '\n').replace('\r', '\n')
353 text = text.replace('\r\n', '\n').replace('\r', '\n')
349
354
350 return text
355 return text
351
356
352 def connect_threads(self, opening_posts):
357 def connect_threads(self, opening_posts):
353 for opening_post in opening_posts:
358 for opening_post in opening_posts:
354 threads = opening_post.get_threads().all()
359 threads = opening_post.get_threads().all()
355 for thread in threads:
360 for thread in threads:
356 if thread.can_bump():
361 if thread.can_bump():
357 thread.update_bump_status()
362 thread.update_bump_status()
358
363
359 thread.last_edit_time = self.last_edit_time
364 thread.last_edit_time = self.last_edit_time
360 thread.save(update_fields=['last_edit_time', 'status'])
365 thread.save(update_fields=['last_edit_time', 'status'])
361 self.threads.add(opening_post.get_thread())
366 self.threads.add(opening_post.get_thread())
362
367
363 def get_tripcode(self):
368 def get_tripcode(self):
364 if self.tripcode:
369 if self.tripcode:
365 return Tripcode(self.tripcode)
370 return Tripcode(self.tripcode)
366
371
367 def get_link_view(self):
372 def get_link_view(self):
368 """
373 """
369 Gets view of a reflink to the post.
374 Gets view of a reflink to the post.
370 """
375 """
371 result = '<a href="{}">&gt;&gt;{}</a>'.format(self.get_absolute_url(),
376 result = '<a href="{}">&gt;&gt;{}</a>'.format(self.get_absolute_url(),
372 self.id)
377 self.id)
373 if self.is_opening():
378 if self.is_opening():
374 result = '<b>{}</b>'.format(result)
379 result = '<b>{}</b>'.format(result)
375
380
376 return result
381 return result
377
382
378 def is_hidden(self) -> bool:
383 def is_hidden(self) -> bool:
379 return self.hidden
384 return self.hidden
380
385
381 def set_hidden(self, hidden):
386 def set_hidden(self, hidden):
382 self.hidden = hidden
387 self.hidden = hidden
383
388
384 def increment_version(self):
389 def increment_version(self):
385 self.version = F('version') + 1
390 self.version = F('version') + 1
386
391
387 def clear_cache(self):
392 def clear_cache(self):
388 """
393 """
389 Clears sync data (content cache, signatures etc).
394 Clears sync data (content cache, signatures etc).
390 """
395 """
391 global_id = self.global_id
396 global_id = self.global_id
392 if global_id is not None and global_id.is_local()\
397 if global_id is not None and global_id.is_local()\
393 and global_id.content is not None:
398 and global_id.content is not None:
394 global_id.clear_cache()
399 global_id.clear_cache()
@@ -1,108 +1,115 b''
1 import re
1 import re
2 from boards import thumbs
2 from boards import thumbs
3 from boards.mdx_neboard import get_parser
3 from boards.mdx_neboard import get_parser
4
4
5 from boards.models import Post, GlobalId, Attachment
5 from boards.models import Post, GlobalId, Attachment
6 from boards.models.attachment.viewers import FILE_TYPES_IMAGE
6 from boards.models.attachment.viewers import FILE_TYPES_IMAGE
7 from boards.models.post import REGEX_NOTIFICATION, REGEX_REPLY,\
7 from boards.models.post import REGEX_NOTIFICATION, REGEX_REPLY,\
8 REGEX_GLOBAL_REPLY
8 REGEX_GLOBAL_REPLY
9 from boards.models.post.manager import post_import_deps
9 from boards.models.post.manager import post_import_deps
10 from boards.models.user import Notification
10 from boards.models.user import Notification
11 from django.db.models.signals import post_save, pre_save, pre_delete, \
11 from django.db.models.signals import post_save, pre_save, pre_delete, \
12 post_delete
12 post_delete
13 from django.dispatch import receiver
13 from django.dispatch import receiver
14 from django.utils import timezone
14 from django.utils import timezone
15
15
16
16
17 THUMB_SIZES = ((200, 150),)
17 THUMB_SIZES = ((200, 150),)
18
18
19
19
20 @receiver(post_save, sender=Post)
20 @receiver(post_save, sender=Post)
21 def connect_replies(instance, **kwargs):
21 def connect_replies(instance, **kwargs):
22 for reply_number in re.finditer(REGEX_REPLY, instance.get_raw_text()):
22 for reply_number in re.finditer(REGEX_REPLY, instance.get_raw_text()):
23 post_id = reply_number.group(1)
23 post_id = reply_number.group(1)
24
24
25 try:
25 try:
26 referenced_post = Post.objects.get(id=post_id)
26 referenced_post = Post.objects.get(id=post_id)
27
27
28 if not referenced_post.referenced_posts.filter(
28 if not referenced_post.referenced_posts.filter(
29 id=instance.id).exists():
29 id=instance.id).exists():
30 referenced_post.referenced_posts.add(instance)
30 referenced_post.referenced_posts.add(instance)
31 referenced_post.last_edit_time = instance.pub_time
31 referenced_post.last_edit_time = instance.pub_time
32 referenced_post.build_refmap()
32 referenced_post.build_refmap()
33 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
33 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
34 except Post.DoesNotExist:
34 except Post.DoesNotExist:
35 pass
35 pass
36
36
37
37
38 @receiver(post_save, sender=Post)
38 @receiver(post_save, sender=Post)
39 @receiver(post_import_deps, sender=Post)
39 @receiver(post_import_deps, sender=Post)
40 def connect_global_replies(instance, **kwargs):
40 def connect_global_replies(instance, **kwargs):
41 for reply_number in re.finditer(REGEX_GLOBAL_REPLY, instance.get_raw_text()):
41 for reply_number in re.finditer(REGEX_GLOBAL_REPLY, instance.get_raw_text()):
42 key_type = reply_number.group(1)
42 key_type = reply_number.group(1)
43 key = reply_number.group(2)
43 key = reply_number.group(2)
44 local_id = reply_number.group(3)
44 local_id = reply_number.group(3)
45
45
46 try:
46 try:
47 global_id = GlobalId.objects.get(key_type=key_type, key=key,
47 global_id = GlobalId.objects.get(key_type=key_type, key=key,
48 local_id=local_id)
48 local_id=local_id)
49 referenced_post = Post.objects.get(global_id=global_id)
49 referenced_post = Post.objects.get(global_id=global_id)
50 referenced_post.referenced_posts.add(instance)
50 referenced_post.referenced_posts.add(instance)
51 referenced_post.last_edit_time = instance.pub_time
51 referenced_post.last_edit_time = instance.pub_time
52 referenced_post.build_refmap()
52 referenced_post.build_refmap()
53 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
53 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
54 except (GlobalId.DoesNotExist, Post.DoesNotExist):
54 except (GlobalId.DoesNotExist, Post.DoesNotExist):
55 pass
55 pass
56
56
57
57
58 @receiver(post_save, sender=Post)
58 @receiver(post_save, sender=Post)
59 def connect_notifications(instance, **kwargs):
59 def connect_notifications(instance, **kwargs):
60 for reply_number in re.finditer(REGEX_NOTIFICATION, instance.get_raw_text()):
60 for reply_number in re.finditer(REGEX_NOTIFICATION, instance.get_raw_text()):
61 user_name = reply_number.group(1).lower()
61 user_name = reply_number.group(1).lower()
62 Notification.objects.get_or_create(name=user_name, post=instance)
62 Notification.objects.get_or_create(name=user_name, post=instance)
63
63
64
64
65 @receiver(pre_save, sender=Post)
65 @receiver(pre_save, sender=Post)
66 @receiver(post_import_deps, sender=Post)
66 @receiver(post_import_deps, sender=Post)
67 def parse_text(instance, **kwargs):
67 def parse_text(instance, **kwargs):
68 instance._text_rendered = get_parser().parse(instance.get_raw_text())
68 instance._text_rendered = get_parser().parse(instance.get_raw_text())
69
69
70
70
71 @receiver(pre_delete, sender=Post)
71 @receiver(pre_delete, sender=Post)
72 def delete_attachments(instance, **kwargs):
72 def delete_attachments(instance, **kwargs):
73 for attachment in instance.attachments.all():
73 for attachment in instance.attachments.all():
74 attachment_refs_count = attachment.attachment_posts.count()
74 attachment_refs_count = attachment.attachment_posts.count()
75 if attachment_refs_count == 1:
75 if attachment_refs_count == 1:
76 attachment.delete()
76 attachment.delete()
77
77
78
78
79 @receiver(post_delete, sender=Post)
79 @receiver(post_delete, sender=Post)
80 def update_thread_on_delete(instance, **kwargs):
80 def update_thread_on_delete(instance, **kwargs):
81 thread = instance.get_thread()
81 thread = instance.get_thread()
82 thread.last_edit_time = timezone.now()
82 thread.last_edit_time = timezone.now()
83 thread.save()
83 thread.save()
84
84
85
85
86 @receiver(post_delete, sender=Post)
86 @receiver(post_delete, sender=Post)
87 def delete_global_id(instance, **kwargs):
87 def delete_global_id(instance, **kwargs):
88 if instance.global_id and instance.global_id.id:
88 if instance.global_id and instance.global_id.id:
89 instance.global_id.delete()
89 instance.global_id.delete()
90
90
91
91
92 @receiver(post_save, sender=Attachment)
92 @receiver(post_save, sender=Attachment)
93 def generate_thumb(instance, **kwargs):
93 def generate_thumb(instance, **kwargs):
94 if instance.mimetype in FILE_TYPES_IMAGE:
94 if instance.mimetype in FILE_TYPES_IMAGE:
95 for size in THUMB_SIZES:
95 for size in THUMB_SIZES:
96 (w, h) = size
96 (w, h) = size
97 split = instance.file.name.rsplit('.', 1)
97 split = instance.file.name.rsplit('.', 1)
98 thumb_name = '%s.%sx%s.%s' % (split[0], w, h, split[1])
98 thumb_name = '%s.%sx%s.%s' % (split[0], w, h, split[1])
99
99
100 if not instance.file.storage.exists(thumb_name):
100 if not instance.file.storage.exists(thumb_name):
101 # you can use another thumbnailing function if you like
101 # you can use another thumbnailing function if you like
102 thumb_content = thumbs.generate_thumb(instance.file, size, split[1])
102 thumb_content = thumbs.generate_thumb(instance.file, size, split[1])
103
103
104 thumb_name_ = instance.file.storage.save(thumb_name, thumb_content)
104 thumb_name_ = instance.file.storage.save(thumb_name, thumb_content)
105
105
106 if not thumb_name == thumb_name_:
106 if not thumb_name == thumb_name_:
107 raise ValueError(
107 raise ValueError(
108 'There is already a file named %s' % thumb_name_)
108 'There is already a file named %s' % thumb_name_)
109
110
111 @receiver(pre_delete, sender=Post)
112 def rebuild_refmap(instance, **kwargs):
113 for referenced_post in instance.refposts.all():
114 referenced_post.build_refmap(excluded_ids=[instance.id])
115 referenced_post.save(update_fields=['refmap'])
General Comments 0
You need to be logged in to leave comments. Login now