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