##// END OF EJS Templates
Merged with default
neko259 -
r1231:db37f36a merge decentral
parent child Browse files
Show More
@@ -1,527 +1,522 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.mdx_neboard import Parser
14 from boards.mdx_neboard import Parser
15 from boards.models import KeyPair, GlobalId
15 from boards.models import KeyPair, GlobalId
16 from boards import settings
16 from boards import settings
17 from boards.models import PostImage
17 from boards.models import PostImage
18 from boards.models.base import Viewable
18 from boards.models.base import Viewable
19 from boards import utils
19 from boards import utils
20 from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
20 from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
21 from boards.models.user import Notification, Ban
21 from boards.models.user import Notification, Ban
22 import boards.models.thread
22 import boards.models.thread
23
23
24 WS_NOTIFICATION_TYPE_NEW_POST = 'new_post'
24 WS_NOTIFICATION_TYPE_NEW_POST = 'new_post'
25 WS_NOTIFICATION_TYPE = 'notification_type'
25 WS_NOTIFICATION_TYPE = 'notification_type'
26
26
27 WS_CHANNEL_THREAD = "thread:"
27 WS_CHANNEL_THREAD = "thread:"
28
28
29 APP_LABEL_BOARDS = 'boards'
29 APP_LABEL_BOARDS = 'boards'
30
30
31 POSTS_PER_DAY_RANGE = 7
31 POSTS_PER_DAY_RANGE = 7
32
32
33 BAN_REASON_AUTO = 'Auto'
33 BAN_REASON_AUTO = 'Auto'
34
34
35 IMAGE_THUMB_SIZE = (200, 150)
35 IMAGE_THUMB_SIZE = (200, 150)
36
36
37 TITLE_MAX_LENGTH = 200
37 TITLE_MAX_LENGTH = 200
38
38
39 NO_IP = '0.0.0.0'
39 NO_IP = '0.0.0.0'
40
40
41 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
41 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
42 REGEX_GLOBAL_REPLY = re.compile(r'\[post\](\w+)::([^:]+)::(\d+)\[/post\]')
42 REGEX_GLOBAL_REPLY = re.compile(r'\[post\](\w+)::([^:]+)::(\d+)\[/post\]')
43 REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?')
43 REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?')
44 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
44 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
45
45
46 PARAMETER_TRUNCATED = 'truncated'
46 PARAMETER_TRUNCATED = 'truncated'
47 PARAMETER_TAG = 'tag'
47 PARAMETER_TAG = 'tag'
48 PARAMETER_OFFSET = 'offset'
48 PARAMETER_OFFSET = 'offset'
49 PARAMETER_DIFF_TYPE = 'type'
49 PARAMETER_DIFF_TYPE = 'type'
50 PARAMETER_CSS_CLASS = 'css_class'
50 PARAMETER_CSS_CLASS = 'css_class'
51 PARAMETER_THREAD = 'thread'
51 PARAMETER_THREAD = 'thread'
52 PARAMETER_IS_OPENING = 'is_opening'
52 PARAMETER_IS_OPENING = 'is_opening'
53 PARAMETER_MODERATOR = 'moderator'
53 PARAMETER_MODERATOR = 'moderator'
54 PARAMETER_POST = 'post'
54 PARAMETER_POST = 'post'
55 PARAMETER_OP_ID = 'opening_post_id'
55 PARAMETER_OP_ID = 'opening_post_id'
56 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
56 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
57 PARAMETER_REPLY_LINK = 'reply_link'
57 PARAMETER_REPLY_LINK = 'reply_link'
58 PARAMETER_NEED_OP_DATA = 'need_op_data'
58 PARAMETER_NEED_OP_DATA = 'need_op_data'
59
59
60 POST_VIEW_PARAMS = (
60 POST_VIEW_PARAMS = (
61 'need_op_data',
61 'need_op_data',
62 'reply_link',
62 'reply_link',
63 'moderator',
63 'moderator',
64 'need_open_link',
64 'need_open_link',
65 'truncated',
65 'truncated',
66 'mode_tree',
66 'mode_tree',
67 )
67 )
68
68
69 REFMAP_STR = '<a href="{}">&gt;&gt;{}</a>'
69 REFMAP_STR = '<a href="{}">&gt;&gt;{}</a>'
70
70
71
71
72 class PostManager(models.Manager):
72 class PostManager(models.Manager):
73 @transaction.atomic
73 @transaction.atomic
74 def create_post(self, title: str, text: str, image=None, thread=None,
74 def create_post(self, title: str, text: str, image=None, thread=None,
75 ip=NO_IP, tags: list=None, opening_posts: list=None):
75 ip=NO_IP, tags: list=None, opening_posts: list=None):
76 """
76 """
77 Creates new post
77 Creates new post
78 """
78 """
79
79
80 is_banned = Ban.objects.filter(ip=ip).exists()
80 is_banned = Ban.objects.filter(ip=ip).exists()
81
81
82 # TODO Raise specific exception and catch it in the views
82 # TODO Raise specific exception and catch it in the views
83 if is_banned:
83 if is_banned:
84 raise Exception("This user is banned")
84 raise Exception("This user is banned")
85
85
86 if not tags:
86 if not tags:
87 tags = []
87 tags = []
88 if not opening_posts:
88 if not opening_posts:
89 opening_posts = []
89 opening_posts = []
90
90
91 posting_time = timezone.now()
91 posting_time = timezone.now()
92 if not thread:
92 if not thread:
93 thread = boards.models.thread.Thread.objects.create(
93 thread = boards.models.thread.Thread.objects.create(
94 bump_time=posting_time, last_edit_time=posting_time)
94 bump_time=posting_time, last_edit_time=posting_time)
95 list(map(thread.tags.add, tags))
95 list(map(thread.tags.add, tags))
96 new_thread = True
96 boards.models.thread.Thread.objects.process_oldest_threads()
97 else:
97 else:
98 new_thread = False
98 thread.last_edit_time = posting_time
99 thread.bump()
100 thread.save()
99
101
100 pre_text = Parser().preparse(text)
102 pre_text = Parser().preparse(text)
101
103
102 post = self.create(title=title,
104 post = self.create(title=title,
103 text=pre_text,
105 text=pre_text,
104 pub_time=posting_time,
106 pub_time=posting_time,
105 poster_ip=ip,
107 poster_ip=ip,
106 thread=thread,
108 thread=thread,
107 last_edit_time=posting_time)
109 last_edit_time=posting_time)
108 post.threads.add(thread)
110 post.threads.add(thread)
109
111
110 post.set_global_id()
112 post.set_global_id()
111
113
112 logger = logging.getLogger('boards.post.create')
114 logger = logging.getLogger('boards.post.create')
113
115
114 logger.info('Created post {} by {}'.format(post, post.poster_ip))
116 logger.info('Created post {} by {}'.format(post, post.poster_ip))
115
117
116 if image:
118 if image:
117 post.images.add(PostImage.objects.create_with_hash(image))
119 post.images.add(PostImage.objects.create_with_hash(image))
118
120
119 if new_thread:
120 boards.models.thread.Thread.objects.process_oldest_threads()
121 else:
122 thread.last_edit_time = posting_time
123 thread.bump()
124 thread.save()
125
126 post.build_url()
121 post.build_url()
127 post.connect_replies()
122 post.connect_replies()
128 post.connect_threads(opening_posts)
123 post.connect_threads(opening_posts)
129 post.connect_notifications()
124 post.connect_notifications()
130
125
131 return post
126 return post
132
127
133 @transaction.atomic
128 @transaction.atomic
134 def import_post(self, title: str, text:str, pub_time: str,
129 def import_post(self, title: str, text:str, pub_time: str,
135 opening_post=None):
130 opening_post=None):
136 if opening_post is None:
131 if opening_post is None:
137 thread = boards.models.thread.Thread.objects.create(
132 thread = boards.models.thread.Thread.objects.create(
138 bump_time=pub_time, last_edit_time=pub_time)
133 bump_time=pub_time, last_edit_time=pub_time)
139 # list(map(thread.tags.add, tags))
134 # list(map(thread.tags.add, tags))
140 new_thread = True
135 new_thread = True
141 else:
136 else:
142 thread = opening_post.get_thread()
137 thread = opening_post.get_thread()
143 new_thread = False
138 new_thread = False
144
139
145 post = Post.objects.create(title=title, text=text,
140 post = Post.objects.create(title=title, text=text,
146 pub_time=pub_time,
141 pub_time=pub_time,
147 poster_ip=NO_IP,
142 poster_ip=NO_IP,
148 last_edit_time=pub_time,
143 last_edit_time=pub_time,
149 thread_id=thread.id)
144 thread_id=thread.id)
150
145
151 post.build_url()
146 post.build_url()
152 post.connect_replies()
147 post.connect_replies()
153 post.connect_notifications()
148 post.connect_notifications()
154
149
155 return post
150 return post
156
151
157 def delete_posts_by_ip(self, ip):
152 def delete_posts_by_ip(self, ip):
158 """
153 """
159 Deletes all posts of the author with same IP
154 Deletes all posts of the author with same IP
160 """
155 """
161
156
162 posts = self.filter(poster_ip=ip)
157 posts = self.filter(poster_ip=ip)
163 for post in posts:
158 for post in posts:
164 post.delete()
159 post.delete()
165
160
166 @utils.cached_result()
161 @utils.cached_result()
167 def get_posts_per_day(self) -> float:
162 def get_posts_per_day(self) -> float:
168 """
163 """
169 Gets average count of posts per day for the last 7 days
164 Gets average count of posts per day for the last 7 days
170 """
165 """
171
166
172 day_end = date.today()
167 day_end = date.today()
173 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
168 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
174
169
175 day_time_start = timezone.make_aware(datetime.combine(
170 day_time_start = timezone.make_aware(datetime.combine(
176 day_start, dtime()), timezone.get_current_timezone())
171 day_start, dtime()), timezone.get_current_timezone())
177 day_time_end = timezone.make_aware(datetime.combine(
172 day_time_end = timezone.make_aware(datetime.combine(
178 day_end, dtime()), timezone.get_current_timezone())
173 day_end, dtime()), timezone.get_current_timezone())
179
174
180 posts_per_period = float(self.filter(
175 posts_per_period = float(self.filter(
181 pub_time__lte=day_time_end,
176 pub_time__lte=day_time_end,
182 pub_time__gte=day_time_start).count())
177 pub_time__gte=day_time_start).count())
183
178
184 ppd = posts_per_period / POSTS_PER_DAY_RANGE
179 ppd = posts_per_period / POSTS_PER_DAY_RANGE
185
180
186 return ppd
181 return ppd
187
182
188
183
189 class Post(models.Model, Viewable):
184 class Post(models.Model, Viewable):
190 """A post is a message."""
185 """A post is a message."""
191
186
192 objects = PostManager()
187 objects = PostManager()
193
188
194 class Meta:
189 class Meta:
195 app_label = APP_LABEL_BOARDS
190 app_label = APP_LABEL_BOARDS
196 ordering = ('id',)
191 ordering = ('id',)
197
192
198 title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
193 title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
199 pub_time = models.DateTimeField()
194 pub_time = models.DateTimeField()
200 text = TextField(blank=True, null=True)
195 text = TextField(blank=True, null=True)
201 _text_rendered = TextField(blank=True, null=True, editable=False)
196 _text_rendered = TextField(blank=True, null=True, editable=False)
202
197
203 images = models.ManyToManyField(PostImage, null=True, blank=True,
198 images = models.ManyToManyField(PostImage, null=True, blank=True,
204 related_name='ip+', db_index=True)
199 related_name='ip+', db_index=True)
205
200
206 poster_ip = models.GenericIPAddressField()
201 poster_ip = models.GenericIPAddressField()
207
202
208 # TODO This field can be removed cause UID is used for update now
203 # TODO This field can be removed cause UID is used for update now
209 last_edit_time = models.DateTimeField()
204 last_edit_time = models.DateTimeField()
210
205
211 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
206 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
212 null=True,
207 null=True,
213 blank=True, related_name='refposts',
208 blank=True, related_name='refposts',
214 db_index=True)
209 db_index=True)
215 refmap = models.TextField(null=True, blank=True)
210 refmap = models.TextField(null=True, blank=True)
216 threads = models.ManyToManyField('Thread', db_index=True)
211 threads = models.ManyToManyField('Thread', db_index=True)
217 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
212 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
218
213
219 url = models.TextField()
214 url = models.TextField()
220 uid = models.TextField(db_index=True)
215 uid = models.TextField(db_index=True)
221
216
222 # Global ID with author key. If the message was downloaded from another
217 # Global ID with author key. If the message was downloaded from another
223 # server, this indicates the server.
218 # server, this indicates the server.
224 global_id = models.OneToOneField('GlobalId', null=True, blank=True)
219 global_id = models.OneToOneField('GlobalId', null=True, blank=True)
225
220
226 # One post can be signed by many nodes that give their trust to it
221 # One post can be signed by many nodes that give their trust to it
227 signature = models.ManyToManyField('Signature', null=True, blank=True)
222 signature = models.ManyToManyField('Signature', null=True, blank=True)
228
223
229 def __str__(self):
224 def __str__(self):
230 return 'P#{}/{}'.format(self.id, self.title)
225 return 'P#{}/{}'.format(self.id, self.title)
231
226
232 def get_referenced_posts(self):
227 def get_referenced_posts(self):
233 threads = self.get_threads().all()
228 threads = self.get_threads().all()
234 return self.referenced_posts.filter(threads__in=threads)\
229 return self.referenced_posts.filter(threads__in=threads)\
235 .order_by('pub_time').distinct().all()
230 .order_by('pub_time').distinct().all()
236
231
237 def get_title(self) -> str:
232 def get_title(self) -> str:
238 """
233 """
239 Gets original post title or part of its text.
234 Gets original post title or part of its text.
240 """
235 """
241
236
242 title = self.title
237 title = self.title
243 if not title:
238 if not title:
244 title = self.get_text()
239 title = self.get_text()
245
240
246 return title
241 return title
247
242
248 def build_refmap(self) -> None:
243 def build_refmap(self) -> None:
249 """
244 """
250 Builds a replies map string from replies list. This is a cache to stop
245 Builds a replies map string from replies list. This is a cache to stop
251 the server from recalculating the map on every post show.
246 the server from recalculating the map on every post show.
252 """
247 """
253
248
254 post_urls = [REFMAP_STR.format(refpost.get_absolute_url(), refpost.id)
249 post_urls = [REFMAP_STR.format(refpost.get_absolute_url(), refpost.id)
255 for refpost in self.referenced_posts.all()]
250 for refpost in self.referenced_posts.all()]
256
251
257 self.refmap = ', '.join(post_urls)
252 self.refmap = ', '.join(post_urls)
258
253
259 def is_referenced(self) -> bool:
254 def is_referenced(self) -> bool:
260 return self.refmap and len(self.refmap) > 0
255 return self.refmap and len(self.refmap) > 0
261
256
262 def is_opening(self) -> bool:
257 def is_opening(self) -> bool:
263 """
258 """
264 Checks if this is an opening post or just a reply.
259 Checks if this is an opening post or just a reply.
265 """
260 """
266
261
267 return self.get_thread().get_opening_post_id() == self.id
262 return self.get_thread().get_opening_post_id() == self.id
268
263
269 def get_absolute_url(self):
264 def get_absolute_url(self):
270 if self.url:
265 if self.url:
271 return self.url
266 return self.url
272 else:
267 else:
273 opening_id = self.get_thread().get_opening_post_id()
268 opening_id = self.get_thread().get_opening_post_id()
274 post_url = reverse('thread', kwargs={'post_id': opening_id})
269 post_url = reverse('thread', kwargs={'post_id': opening_id})
275 if self.id != opening_id:
270 if self.id != opening_id:
276 post_url += '#' + str(self.id)
271 post_url += '#' + str(self.id)
277 return post_url
272 return post_url
278
273
279 def get_thread(self):
274 def get_thread(self):
280 return self.thread
275 return self.thread
281
276
282 def get_threads(self) -> QuerySet:
277 def get_threads(self) -> QuerySet:
283 """
278 """
284 Gets post's thread.
279 Gets post's thread.
285 """
280 """
286
281
287 return self.threads
282 return self.threads
288
283
289 def get_view(self, *args, **kwargs) -> str:
284 def get_view(self, *args, **kwargs) -> str:
290 """
285 """
291 Renders post's HTML view. Some of the post params can be passed over
286 Renders post's HTML view. Some of the post params can be passed over
292 kwargs for the means of caching (if we view the thread, some params
287 kwargs for the means of caching (if we view the thread, some params
293 are same for every post and don't need to be computed over and over.
288 are same for every post and don't need to be computed over and over.
294 """
289 """
295
290
296 thread = self.get_thread()
291 thread = self.get_thread()
297 is_opening = kwargs.get(PARAMETER_IS_OPENING, self.is_opening())
292 is_opening = kwargs.get(PARAMETER_IS_OPENING, self.is_opening())
298
293
299 if is_opening:
294 if is_opening:
300 opening_post_id = self.id
295 opening_post_id = self.id
301 else:
296 else:
302 opening_post_id = thread.get_opening_post_id()
297 opening_post_id = thread.get_opening_post_id()
303
298
304 css_class = 'post'
299 css_class = 'post'
305 if thread.archived:
300 if thread.archived:
306 css_class += ' archive_post'
301 css_class += ' archive_post'
307 elif not thread.can_bump():
302 elif not thread.can_bump():
308 css_class += ' dead_post'
303 css_class += ' dead_post'
309
304
310 params = dict()
305 params = dict()
311 for param in POST_VIEW_PARAMS:
306 for param in POST_VIEW_PARAMS:
312 if param in kwargs:
307 if param in kwargs:
313 params[param] = kwargs[param]
308 params[param] = kwargs[param]
314
309
315 params.update({
310 params.update({
316 PARAMETER_POST: self,
311 PARAMETER_POST: self,
317 PARAMETER_IS_OPENING: is_opening,
312 PARAMETER_IS_OPENING: is_opening,
318 PARAMETER_THREAD: thread,
313 PARAMETER_THREAD: thread,
319 PARAMETER_CSS_CLASS: css_class,
314 PARAMETER_CSS_CLASS: css_class,
320 PARAMETER_OP_ID: opening_post_id,
315 PARAMETER_OP_ID: opening_post_id,
321 })
316 })
322
317
323 return render_to_string('boards/post.html', params)
318 return render_to_string('boards/post.html', params)
324
319
325 def get_search_view(self, *args, **kwargs):
320 def get_search_view(self, *args, **kwargs):
326 return self.get_view(need_op_data=True, *args, **kwargs)
321 return self.get_view(need_op_data=True, *args, **kwargs)
327
322
328 def get_first_image(self) -> PostImage:
323 def get_first_image(self) -> PostImage:
329 return self.images.earliest('id')
324 return self.images.earliest('id')
330
325
331 def delete(self, using=None):
326 def delete(self, using=None):
332 """
327 """
333 Deletes all post images and the post itself.
328 Deletes all post images and the post itself.
334 """
329 """
335
330
336 for image in self.images.all():
331 for image in self.images.all():
337 image_refs_count = Post.objects.filter(images__in=[image]).count()
332 image_refs_count = Post.objects.filter(images__in=[image]).count()
338 if image_refs_count == 1:
333 if image_refs_count == 1:
339 image.delete()
334 image.delete()
340
335
341 self.signature.all().delete()
336 self.signature.all().delete()
342 if self.global_id:
337 if self.global_id:
343 self.global_id.delete()
338 self.global_id.delete()
344
339
345 thread = self.get_thread()
340 thread = self.get_thread()
346 thread.last_edit_time = timezone.now()
341 thread.last_edit_time = timezone.now()
347 thread.save()
342 thread.save()
348
343
349 super(Post, self).delete(using)
344 super(Post, self).delete(using)
350
345
351 logging.getLogger('boards.post.delete').info(
346 logging.getLogger('boards.post.delete').info(
352 'Deleted post {}'.format(self))
347 'Deleted post {}'.format(self))
353
348
354 def set_global_id(self, key_pair=None):
349 def set_global_id(self, key_pair=None):
355 """
350 """
356 Sets global id based on the given key pair. If no key pair is given,
351 Sets global id based on the given key pair. If no key pair is given,
357 default one is used.
352 default one is used.
358 """
353 """
359
354
360 if key_pair:
355 if key_pair:
361 key = key_pair
356 key = key_pair
362 else:
357 else:
363 try:
358 try:
364 key = KeyPair.objects.get(primary=True)
359 key = KeyPair.objects.get(primary=True)
365 except KeyPair.DoesNotExist:
360 except KeyPair.DoesNotExist:
366 # Do not update the global id because there is no key defined
361 # Do not update the global id because there is no key defined
367 return
362 return
368 global_id = GlobalId(key_type=key.key_type,
363 global_id = GlobalId(key_type=key.key_type,
369 key=key.public_key,
364 key=key.public_key,
370 local_id=self.id)
365 local_id=self.id)
371 global_id.save()
366 global_id.save()
372
367
373 self.global_id = global_id
368 self.global_id = global_id
374
369
375 self.save(update_fields=['global_id'])
370 self.save(update_fields=['global_id'])
376
371
377 def get_pub_time_str(self):
372 def get_pub_time_str(self):
378 return str(self.pub_time)
373 return str(self.pub_time)
379
374
380 def get_replied_ids(self):
375 def get_replied_ids(self):
381 """
376 """
382 Gets ID list of the posts that this post replies.
377 Gets ID list of the posts that this post replies.
383 """
378 """
384
379
385 raw_text = self.get_raw_text()
380 raw_text = self.get_raw_text()
386
381
387 local_replied = REGEX_REPLY.findall(raw_text)
382 local_replied = REGEX_REPLY.findall(raw_text)
388 global_replied = []
383 global_replied = []
389 for match in REGEX_GLOBAL_REPLY.findall(raw_text):
384 for match in REGEX_GLOBAL_REPLY.findall(raw_text):
390 key_type = match[0]
385 key_type = match[0]
391 key = match[1]
386 key = match[1]
392 local_id = match[2]
387 local_id = match[2]
393
388
394 try:
389 try:
395 global_id = GlobalId.objects.get(key_type=key_type,
390 global_id = GlobalId.objects.get(key_type=key_type,
396 key=key, local_id=local_id)
391 key=key, local_id=local_id)
397 for post in Post.objects.filter(global_id=global_id).only('id'):
392 for post in Post.objects.filter(global_id=global_id).only('id'):
398 global_replied.append(post.id)
393 global_replied.append(post.id)
399 except GlobalId.DoesNotExist:
394 except GlobalId.DoesNotExist:
400 pass
395 pass
401 return local_replied + global_replied
396 return local_replied + global_replied
402
397
403 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
398 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
404 include_last_update=False) -> str:
399 include_last_update=False) -> str:
405 """
400 """
406 Gets post HTML or JSON data that can be rendered on a page or used by
401 Gets post HTML or JSON data that can be rendered on a page or used by
407 API.
402 API.
408 """
403 """
409
404
410 return get_exporter(format_type).export(self, request,
405 return get_exporter(format_type).export(self, request,
411 include_last_update)
406 include_last_update)
412
407
413 def notify_clients(self, recursive=True):
408 def notify_clients(self, recursive=True):
414 """
409 """
415 Sends post HTML data to the thread web socket.
410 Sends post HTML data to the thread web socket.
416 """
411 """
417
412
418 if not settings.get_bool('External', 'WebsocketsEnabled'):
413 if not settings.get_bool('External', 'WebsocketsEnabled'):
419 return
414 return
420
415
421 thread_ids = list()
416 thread_ids = list()
422 for thread in self.get_threads().all():
417 for thread in self.get_threads().all():
423 thread_ids.append(thread.id)
418 thread_ids.append(thread.id)
424
419
425 thread.notify_clients()
420 thread.notify_clients()
426
421
427 if recursive:
422 if recursive:
428 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
423 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
429 post_id = reply_number.group(1)
424 post_id = reply_number.group(1)
430
425
431 try:
426 try:
432 ref_post = Post.objects.get(id=post_id)
427 ref_post = Post.objects.get(id=post_id)
433
428
434 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
429 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
435 # If post is in this thread, its thread was already notified.
430 # If post is in this thread, its thread was already notified.
436 # Otherwise, notify its thread separately.
431 # Otherwise, notify its thread separately.
437 ref_post.notify_clients(recursive=False)
432 ref_post.notify_clients(recursive=False)
438 except ObjectDoesNotExist:
433 except ObjectDoesNotExist:
439 pass
434 pass
440
435
441 def build_url(self):
436 def build_url(self):
442 self.url = self.get_absolute_url()
437 self.url = self.get_absolute_url()
443 self.save(update_fields=['url'])
438 self.save(update_fields=['url'])
444
439
445 def save(self, force_insert=False, force_update=False, using=None,
440 def save(self, force_insert=False, force_update=False, using=None,
446 update_fields=None):
441 update_fields=None):
447 self._text_rendered = Parser().parse(self.get_raw_text())
442 self._text_rendered = Parser().parse(self.get_raw_text())
448
443
449 self.uid = str(uuid.uuid4())
444 self.uid = str(uuid.uuid4())
450 if update_fields is not None and 'uid' not in update_fields:
445 if update_fields is not None and 'uid' not in update_fields:
451 update_fields += ['uid']
446 update_fields += ['uid']
452
447
453 if self.id:
448 if self.id:
454 for thread in self.get_threads().all():
449 for thread in self.get_threads().all():
455 thread.last_edit_time = self.last_edit_time
450 thread.last_edit_time = self.last_edit_time
456
451
457 thread.save(update_fields=['last_edit_time'])
452 thread.save(update_fields=['last_edit_time'])
458
453
459 super().save(force_insert, force_update, using, update_fields)
454 super().save(force_insert, force_update, using, update_fields)
460
455
461 def get_text(self) -> str:
456 def get_text(self) -> str:
462 return self._text_rendered
457 return self._text_rendered
463
458
464 def get_raw_text(self) -> str:
459 def get_raw_text(self) -> str:
465 return self.text
460 return self.text
466
461
467 def get_sync_text(self) -> str:
462 def get_sync_text(self) -> str:
468 """
463 """
469 Returns text applicable for sync. It has absolute post reflinks.
464 Returns text applicable for sync. It has absolute post reflinks.
470 """
465 """
471
466
472 replacements = dict()
467 replacements = dict()
473 for post_id in REGEX_REPLY.findall(self.get_raw_text()):
468 for post_id in REGEX_REPLY.findall(self.get_raw_text()):
474 absolute_post_id = str(Post.objects.get(id=post_id).global_id)
469 absolute_post_id = str(Post.objects.get(id=post_id).global_id)
475 replacements[post_id] = absolute_post_id
470 replacements[post_id] = absolute_post_id
476
471
477 text = self.get_raw_text()
472 text = self.get_raw_text()
478 for key in replacements:
473 for key in replacements:
479 text = text.replace('[post]{}[/post]'.format(key),
474 text = text.replace('[post]{}[/post]'.format(key),
480 '[post]{}[/post]'.format(replacements[key]))
475 '[post]{}[/post]'.format(replacements[key]))
481
476
482 return text
477 return text
483
478
484 def get_absolute_id(self) -> str:
479 def get_absolute_id(self) -> str:
485 """
480 """
486 If the post has many threads, shows its main thread OP id in the post
481 If the post has many threads, shows its main thread OP id in the post
487 ID.
482 ID.
488 """
483 """
489
484
490 if self.get_threads().count() > 1:
485 if self.get_threads().count() > 1:
491 return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id)
486 return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id)
492 else:
487 else:
493 return str(self.id)
488 return str(self.id)
494
489
495 def connect_notifications(self):
490 def connect_notifications(self):
496 for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()):
491 for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()):
497 user_name = reply_number.group(1).lower()
492 user_name = reply_number.group(1).lower()
498 Notification.objects.get_or_create(name=user_name, post=self)
493 Notification.objects.get_or_create(name=user_name, post=self)
499
494
500 def connect_replies(self):
495 def connect_replies(self):
501 """
496 """
502 Connects replies to a post to show them as a reflink map
497 Connects replies to a post to show them as a reflink map
503 """
498 """
504
499
505 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
500 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
506 post_id = reply_number.group(1)
501 post_id = reply_number.group(1)
507
502
508 try:
503 try:
509 referenced_post = Post.objects.get(id=post_id)
504 referenced_post = Post.objects.get(id=post_id)
510
505
511 referenced_post.referenced_posts.add(self)
506 referenced_post.referenced_posts.add(self)
512 referenced_post.last_edit_time = self.pub_time
507 referenced_post.last_edit_time = self.pub_time
513 referenced_post.build_refmap()
508 referenced_post.build_refmap()
514 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
509 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
515 except ObjectDoesNotExist:
510 except ObjectDoesNotExist:
516 pass
511 pass
517
512
518 def connect_threads(self, opening_posts):
513 def connect_threads(self, opening_posts):
519 for opening_post in opening_posts:
514 for opening_post in opening_posts:
520 threads = opening_post.get_threads().all()
515 threads = opening_post.get_threads().all()
521 for thread in threads:
516 for thread in threads:
522 if thread.can_bump():
517 if thread.can_bump():
523 thread.update_bump_status()
518 thread.update_bump_status()
524
519
525 thread.last_edit_time = self.last_edit_time
520 thread.last_edit_time = self.last_edit_time
526 thread.save(update_fields=['last_edit_time', 'bumpable'])
521 thread.save(update_fields=['last_edit_time', 'bumpable'])
527 self.threads.add(opening_post.get_thread())
522 self.threads.add(opening_post.get_thread())
General Comments 0
You need to be logged in to leave comments. Login now