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