##// END OF EJS Templates
Added background to tripcodes, which is inverted tripcode color
neko259 -
r1294:e6412fea default
parent child Browse files
Show More
@@ -1,447 +1,457 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 import settings
14 from boards import settings
15 from boards.mdx_neboard import Parser
15 from boards.mdx_neboard import Parser
16 from boards.models import PostImage, Attachment
16 from boards.models import PostImage, Attachment
17 from boards.models.base import Viewable
17 from boards.models.base import Viewable
18 from boards import utils
18 from boards import utils
19 from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
19 from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
20 from boards.models.user import Notification, Ban
20 from boards.models.user import Notification, Ban
21 import boards.models.thread
21 import boards.models.thread
22
22
23
23
24 APP_LABEL_BOARDS = 'boards'
24 APP_LABEL_BOARDS = 'boards'
25
25
26 POSTS_PER_DAY_RANGE = 7
26 POSTS_PER_DAY_RANGE = 7
27
27
28 BAN_REASON_AUTO = 'Auto'
28 BAN_REASON_AUTO = 'Auto'
29
29
30 IMAGE_THUMB_SIZE = (200, 150)
30 IMAGE_THUMB_SIZE = (200, 150)
31
31
32 TITLE_MAX_LENGTH = 200
32 TITLE_MAX_LENGTH = 200
33
33
34 # TODO This should be removed
34 # TODO This should be removed
35 NO_IP = '0.0.0.0'
35 NO_IP = '0.0.0.0'
36
36
37 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
37 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
38 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
38 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
39
39
40 PARAMETER_TRUNCATED = 'truncated'
40 PARAMETER_TRUNCATED = 'truncated'
41 PARAMETER_TAG = 'tag'
41 PARAMETER_TAG = 'tag'
42 PARAMETER_OFFSET = 'offset'
42 PARAMETER_OFFSET = 'offset'
43 PARAMETER_DIFF_TYPE = 'type'
43 PARAMETER_DIFF_TYPE = 'type'
44 PARAMETER_CSS_CLASS = 'css_class'
44 PARAMETER_CSS_CLASS = 'css_class'
45 PARAMETER_THREAD = 'thread'
45 PARAMETER_THREAD = 'thread'
46 PARAMETER_IS_OPENING = 'is_opening'
46 PARAMETER_IS_OPENING = 'is_opening'
47 PARAMETER_MODERATOR = 'moderator'
47 PARAMETER_MODERATOR = 'moderator'
48 PARAMETER_POST = 'post'
48 PARAMETER_POST = 'post'
49 PARAMETER_OP_ID = 'opening_post_id'
49 PARAMETER_OP_ID = 'opening_post_id'
50 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
50 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
51 PARAMETER_REPLY_LINK = 'reply_link'
51 PARAMETER_REPLY_LINK = 'reply_link'
52 PARAMETER_NEED_OP_DATA = 'need_op_data'
52 PARAMETER_NEED_OP_DATA = 'need_op_data'
53
53
54 POST_VIEW_PARAMS = (
54 POST_VIEW_PARAMS = (
55 'need_op_data',
55 'need_op_data',
56 'reply_link',
56 'reply_link',
57 'moderator',
57 'moderator',
58 'need_open_link',
58 'need_open_link',
59 'truncated',
59 'truncated',
60 'mode_tree',
60 'mode_tree',
61 )
61 )
62
62
63 REFMAP_STR = '<a href="{}">&gt;&gt;{}</a>'
63 REFMAP_STR = '<a href="{}">&gt;&gt;{}</a>'
64
64
65 IMAGE_TYPES = (
65 IMAGE_TYPES = (
66 'jpeg',
66 'jpeg',
67 'jpg',
67 'jpg',
68 'png',
68 'png',
69 'bmp',
69 'bmp',
70 'gif',
70 'gif',
71 )
71 )
72
72
73
73
74 class PostManager(models.Manager):
74 class PostManager(models.Manager):
75 @transaction.atomic
75 @transaction.atomic
76 def create_post(self, title: str, text: str, file=None, thread=None,
76 def create_post(self, title: str, text: str, file=None, thread=None,
77 ip=NO_IP, tags: list=None, opening_posts: list=None, tripcode=None):
77 ip=NO_IP, tags: list=None, opening_posts: list=None, tripcode=None):
78 """
78 """
79 Creates new post
79 Creates new post
80 """
80 """
81
81
82 is_banned = Ban.objects.filter(ip=ip).exists()
82 is_banned = Ban.objects.filter(ip=ip).exists()
83
83
84 # TODO Raise specific exception and catch it in the views
84 # TODO Raise specific exception and catch it in the views
85 if is_banned:
85 if is_banned:
86 raise Exception("This user is banned")
86 raise Exception("This user is banned")
87
87
88 if not tags:
88 if not tags:
89 tags = []
89 tags = []
90 if not opening_posts:
90 if not opening_posts:
91 opening_posts = []
91 opening_posts = []
92
92
93 posting_time = timezone.now()
93 posting_time = timezone.now()
94 new_thread = False
94 new_thread = False
95 if not thread:
95 if not thread:
96 thread = boards.models.thread.Thread.objects.create(
96 thread = boards.models.thread.Thread.objects.create(
97 bump_time=posting_time, last_edit_time=posting_time)
97 bump_time=posting_time, last_edit_time=posting_time)
98 list(map(thread.tags.add, tags))
98 list(map(thread.tags.add, tags))
99 boards.models.thread.Thread.objects.process_oldest_threads()
99 boards.models.thread.Thread.objects.process_oldest_threads()
100 new_thread = True
100 new_thread = True
101
101
102 pre_text = Parser().preparse(text)
102 pre_text = Parser().preparse(text)
103
103
104 post = self.create(title=title,
104 post = self.create(title=title,
105 text=pre_text,
105 text=pre_text,
106 pub_time=posting_time,
106 pub_time=posting_time,
107 poster_ip=ip,
107 poster_ip=ip,
108 thread=thread,
108 thread=thread,
109 last_edit_time=posting_time,
109 last_edit_time=posting_time,
110 tripcode=tripcode)
110 tripcode=tripcode)
111 post.threads.add(thread)
111 post.threads.add(thread)
112
112
113 logger = logging.getLogger('boards.post.create')
113 logger = logging.getLogger('boards.post.create')
114
114
115 logger.info('Created post {} by {}'.format(post, post.poster_ip))
115 logger.info('Created post {} by {}'.format(post, post.poster_ip))
116
116
117 # TODO Move this to other place
117 # TODO Move this to other place
118 if file:
118 if file:
119 file_type = file.name.split('.')[-1].lower()
119 file_type = file.name.split('.')[-1].lower()
120 if file_type in IMAGE_TYPES:
120 if file_type in IMAGE_TYPES:
121 post.images.add(PostImage.objects.create_with_hash(file))
121 post.images.add(PostImage.objects.create_with_hash(file))
122 else:
122 else:
123 post.attachments.add(Attachment.objects.create_with_hash(file))
123 post.attachments.add(Attachment.objects.create_with_hash(file))
124
124
125 post.build_url()
125 post.build_url()
126 post.connect_replies()
126 post.connect_replies()
127 post.connect_threads(opening_posts)
127 post.connect_threads(opening_posts)
128 post.connect_notifications()
128 post.connect_notifications()
129
129
130 # Thread needs to be bumped only when the post is already created
130 # Thread needs to be bumped only when the post is already created
131 if not new_thread:
131 if not new_thread:
132 thread.last_edit_time = posting_time
132 thread.last_edit_time = posting_time
133 thread.bump()
133 thread.bump()
134 thread.save()
134 thread.save()
135
135
136 return post
136 return post
137
137
138 def delete_posts_by_ip(self, ip):
138 def delete_posts_by_ip(self, ip):
139 """
139 """
140 Deletes all posts of the author with same IP
140 Deletes all posts of the author with same IP
141 """
141 """
142
142
143 posts = self.filter(poster_ip=ip)
143 posts = self.filter(poster_ip=ip)
144 for post in posts:
144 for post in posts:
145 post.delete()
145 post.delete()
146
146
147 @utils.cached_result()
147 @utils.cached_result()
148 def get_posts_per_day(self) -> float:
148 def get_posts_per_day(self) -> float:
149 """
149 """
150 Gets average count of posts per day for the last 7 days
150 Gets average count of posts per day for the last 7 days
151 """
151 """
152
152
153 day_end = date.today()
153 day_end = date.today()
154 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
154 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
155
155
156 day_time_start = timezone.make_aware(datetime.combine(
156 day_time_start = timezone.make_aware(datetime.combine(
157 day_start, dtime()), timezone.get_current_timezone())
157 day_start, dtime()), timezone.get_current_timezone())
158 day_time_end = timezone.make_aware(datetime.combine(
158 day_time_end = timezone.make_aware(datetime.combine(
159 day_end, dtime()), timezone.get_current_timezone())
159 day_end, dtime()), timezone.get_current_timezone())
160
160
161 posts_per_period = float(self.filter(
161 posts_per_period = float(self.filter(
162 pub_time__lte=day_time_end,
162 pub_time__lte=day_time_end,
163 pub_time__gte=day_time_start).count())
163 pub_time__gte=day_time_start).count())
164
164
165 ppd = posts_per_period / POSTS_PER_DAY_RANGE
165 ppd = posts_per_period / POSTS_PER_DAY_RANGE
166
166
167 return ppd
167 return ppd
168
168
169
169
170 class Post(models.Model, Viewable):
170 class Post(models.Model, Viewable):
171 """A post is a message."""
171 """A post is a message."""
172
172
173 objects = PostManager()
173 objects = PostManager()
174
174
175 class Meta:
175 class Meta:
176 app_label = APP_LABEL_BOARDS
176 app_label = APP_LABEL_BOARDS
177 ordering = ('id',)
177 ordering = ('id',)
178
178
179 title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
179 title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
180 pub_time = models.DateTimeField()
180 pub_time = models.DateTimeField()
181 text = TextField(blank=True, null=True)
181 text = TextField(blank=True, null=True)
182 _text_rendered = TextField(blank=True, null=True, editable=False)
182 _text_rendered = TextField(blank=True, null=True, editable=False)
183
183
184 images = models.ManyToManyField(PostImage, null=True, blank=True,
184 images = models.ManyToManyField(PostImage, null=True, blank=True,
185 related_name='post_images', db_index=True)
185 related_name='post_images', db_index=True)
186 attachments = models.ManyToManyField(Attachment, null=True, blank=True,
186 attachments = models.ManyToManyField(Attachment, null=True, blank=True,
187 related_name='attachment_posts')
187 related_name='attachment_posts')
188
188
189 poster_ip = models.GenericIPAddressField()
189 poster_ip = models.GenericIPAddressField()
190
190
191 # TODO This field can be removed cause UID is used for update now
191 # TODO This field can be removed cause UID is used for update now
192 last_edit_time = models.DateTimeField()
192 last_edit_time = models.DateTimeField()
193
193
194 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
194 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
195 null=True,
195 null=True,
196 blank=True, related_name='refposts',
196 blank=True, related_name='refposts',
197 db_index=True)
197 db_index=True)
198 refmap = models.TextField(null=True, blank=True)
198 refmap = models.TextField(null=True, blank=True)
199 threads = models.ManyToManyField('Thread', db_index=True)
199 threads = models.ManyToManyField('Thread', db_index=True)
200 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
200 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
201
201
202 url = models.TextField()
202 url = models.TextField()
203 uid = models.TextField(db_index=True)
203 uid = models.TextField(db_index=True)
204
204
205 tripcode = models.CharField(max_length=50, null=True)
205 tripcode = models.CharField(max_length=50, null=True)
206
206
207 def __str__(self):
207 def __str__(self):
208 return 'P#{}/{}'.format(self.id, self.title)
208 return 'P#{}/{}'.format(self.id, self.title)
209
209
210 def get_referenced_posts(self):
210 def get_referenced_posts(self):
211 threads = self.get_threads().all()
211 threads = self.get_threads().all()
212 return self.referenced_posts.filter(threads__in=threads)\
212 return self.referenced_posts.filter(threads__in=threads)\
213 .order_by('pub_time').distinct().all()
213 .order_by('pub_time').distinct().all()
214
214
215 def get_title(self) -> str:
215 def get_title(self) -> str:
216 """
216 """
217 Gets original post title or part of its text.
217 Gets original post title or part of its text.
218 """
218 """
219
219
220 title = self.title
220 title = self.title
221 if not title:
221 if not title:
222 title = self.get_text()
222 title = self.get_text()
223
223
224 return title
224 return title
225
225
226 def build_refmap(self) -> None:
226 def build_refmap(self) -> None:
227 """
227 """
228 Builds a replies map string from replies list. This is a cache to stop
228 Builds a replies map string from replies list. This is a cache to stop
229 the server from recalculating the map on every post show.
229 the server from recalculating the map on every post show.
230 """
230 """
231
231
232 post_urls = [REFMAP_STR.format(refpost.get_absolute_url(), refpost.id)
232 post_urls = [REFMAP_STR.format(refpost.get_absolute_url(), refpost.id)
233 for refpost in self.referenced_posts.all()]
233 for refpost in self.referenced_posts.all()]
234
234
235 self.refmap = ', '.join(post_urls)
235 self.refmap = ', '.join(post_urls)
236
236
237 def is_referenced(self) -> bool:
237 def is_referenced(self) -> bool:
238 return self.refmap and len(self.refmap) > 0
238 return self.refmap and len(self.refmap) > 0
239
239
240 def is_opening(self) -> bool:
240 def is_opening(self) -> bool:
241 """
241 """
242 Checks if this is an opening post or just a reply.
242 Checks if this is an opening post or just a reply.
243 """
243 """
244
244
245 return self.get_thread().get_opening_post_id() == self.id
245 return self.get_thread().get_opening_post_id() == self.id
246
246
247 def get_absolute_url(self):
247 def get_absolute_url(self):
248 if self.url:
248 if self.url:
249 return self.url
249 return self.url
250 else:
250 else:
251 opening_id = self.get_thread().get_opening_post_id()
251 opening_id = self.get_thread().get_opening_post_id()
252 post_url = reverse('thread', kwargs={'post_id': opening_id})
252 post_url = reverse('thread', kwargs={'post_id': opening_id})
253 if self.id != opening_id:
253 if self.id != opening_id:
254 post_url += '#' + str(self.id)
254 post_url += '#' + str(self.id)
255 return post_url
255 return post_url
256
256
257
257
258 def get_thread(self):
258 def get_thread(self):
259 return self.thread
259 return self.thread
260
260
261 def get_threads(self) -> QuerySet:
261 def get_threads(self) -> QuerySet:
262 """
262 """
263 Gets post's thread.
263 Gets post's thread.
264 """
264 """
265
265
266 return self.threads
266 return self.threads
267
267
268 def get_view(self, *args, **kwargs) -> str:
268 def get_view(self, *args, **kwargs) -> str:
269 """
269 """
270 Renders post's HTML view. Some of the post params can be passed over
270 Renders post's HTML view. Some of the post params can be passed over
271 kwargs for the means of caching (if we view the thread, some params
271 kwargs for the means of caching (if we view the thread, some params
272 are same for every post and don't need to be computed over and over.
272 are same for every post and don't need to be computed over and over.
273 """
273 """
274
274
275 thread = self.get_thread()
275 thread = self.get_thread()
276 is_opening = kwargs.get(PARAMETER_IS_OPENING, self.is_opening())
276 is_opening = kwargs.get(PARAMETER_IS_OPENING, self.is_opening())
277
277
278 if is_opening:
278 if is_opening:
279 opening_post_id = self.id
279 opening_post_id = self.id
280 else:
280 else:
281 opening_post_id = thread.get_opening_post_id()
281 opening_post_id = thread.get_opening_post_id()
282
282
283 css_class = 'post'
283 css_class = 'post'
284 if thread.archived:
284 if thread.archived:
285 css_class += ' archive_post'
285 css_class += ' archive_post'
286 elif not thread.can_bump():
286 elif not thread.can_bump():
287 css_class += ' dead_post'
287 css_class += ' dead_post'
288
288
289 params = dict()
289 params = dict()
290 for param in POST_VIEW_PARAMS:
290 for param in POST_VIEW_PARAMS:
291 if param in kwargs:
291 if param in kwargs:
292 params[param] = kwargs[param]
292 params[param] = kwargs[param]
293
293
294 params.update({
294 params.update({
295 PARAMETER_POST: self,
295 PARAMETER_POST: self,
296 PARAMETER_IS_OPENING: is_opening,
296 PARAMETER_IS_OPENING: is_opening,
297 PARAMETER_THREAD: thread,
297 PARAMETER_THREAD: thread,
298 PARAMETER_CSS_CLASS: css_class,
298 PARAMETER_CSS_CLASS: css_class,
299 PARAMETER_OP_ID: opening_post_id,
299 PARAMETER_OP_ID: opening_post_id,
300 })
300 })
301
301
302 return render_to_string('boards/post.html', params)
302 return render_to_string('boards/post.html', params)
303
303
304 def get_search_view(self, *args, **kwargs):
304 def get_search_view(self, *args, **kwargs):
305 return self.get_view(need_op_data=True, *args, **kwargs)
305 return self.get_view(need_op_data=True, *args, **kwargs)
306
306
307 def get_first_image(self) -> PostImage:
307 def get_first_image(self) -> PostImage:
308 return self.images.earliest('id')
308 return self.images.earliest('id')
309
309
310 def delete(self, using=None):
310 def delete(self, using=None):
311 """
311 """
312 Deletes all post images and the post itself.
312 Deletes all post images and the post itself.
313 """
313 """
314
314
315 for image in self.images.all():
315 for image in self.images.all():
316 image_refs_count = image.post_images.count()
316 image_refs_count = image.post_images.count()
317 if image_refs_count == 1:
317 if image_refs_count == 1:
318 image.delete()
318 image.delete()
319
319
320 for attachment in self.attachments.all():
320 for attachment in self.attachments.all():
321 attachment_refs_count = attachment.attachment_posts.count()
321 attachment_refs_count = attachment.attachment_posts.count()
322 if attachment_refs_count == 1:
322 if attachment_refs_count == 1:
323 attachment.delete()
323 attachment.delete()
324
324
325 thread = self.get_thread()
325 thread = self.get_thread()
326 thread.last_edit_time = timezone.now()
326 thread.last_edit_time = timezone.now()
327 thread.save()
327 thread.save()
328
328
329 super(Post, self).delete(using)
329 super(Post, self).delete(using)
330
330
331 logging.getLogger('boards.post.delete').info(
331 logging.getLogger('boards.post.delete').info(
332 'Deleted post {}'.format(self))
332 'Deleted post {}'.format(self))
333
333
334 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
334 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
335 include_last_update=False) -> str:
335 include_last_update=False) -> str:
336 """
336 """
337 Gets post HTML or JSON data that can be rendered on a page or used by
337 Gets post HTML or JSON data that can be rendered on a page or used by
338 API.
338 API.
339 """
339 """
340
340
341 return get_exporter(format_type).export(self, request,
341 return get_exporter(format_type).export(self, request,
342 include_last_update)
342 include_last_update)
343
343
344 def notify_clients(self, recursive=True):
344 def notify_clients(self, recursive=True):
345 """
345 """
346 Sends post HTML data to the thread web socket.
346 Sends post HTML data to the thread web socket.
347 """
347 """
348
348
349 if not settings.get_bool('External', 'WebsocketsEnabled'):
349 if not settings.get_bool('External', 'WebsocketsEnabled'):
350 return
350 return
351
351
352 thread_ids = list()
352 thread_ids = list()
353 for thread in self.get_threads().all():
353 for thread in self.get_threads().all():
354 thread_ids.append(thread.id)
354 thread_ids.append(thread.id)
355
355
356 thread.notify_clients()
356 thread.notify_clients()
357
357
358 if recursive:
358 if recursive:
359 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
359 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
360 post_id = reply_number.group(1)
360 post_id = reply_number.group(1)
361
361
362 try:
362 try:
363 ref_post = Post.objects.get(id=post_id)
363 ref_post = Post.objects.get(id=post_id)
364
364
365 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
365 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
366 # If post is in this thread, its thread was already notified.
366 # If post is in this thread, its thread was already notified.
367 # Otherwise, notify its thread separately.
367 # Otherwise, notify its thread separately.
368 ref_post.notify_clients(recursive=False)
368 ref_post.notify_clients(recursive=False)
369 except ObjectDoesNotExist:
369 except ObjectDoesNotExist:
370 pass
370 pass
371
371
372 def build_url(self):
372 def build_url(self):
373 self.url = self.get_absolute_url()
373 self.url = self.get_absolute_url()
374 self.save(update_fields=['url'])
374 self.save(update_fields=['url'])
375
375
376 def save(self, force_insert=False, force_update=False, using=None,
376 def save(self, force_insert=False, force_update=False, using=None,
377 update_fields=None):
377 update_fields=None):
378 self._text_rendered = Parser().parse(self.get_raw_text())
378 self._text_rendered = Parser().parse(self.get_raw_text())
379
379
380 self.uid = str(uuid.uuid4())
380 self.uid = str(uuid.uuid4())
381 if update_fields is not None and 'uid' not in update_fields:
381 if update_fields is not None and 'uid' not in update_fields:
382 update_fields += ['uid']
382 update_fields += ['uid']
383
383
384 if self.id:
384 if self.id:
385 for thread in self.get_threads().all():
385 for thread in self.get_threads().all():
386 thread.last_edit_time = self.last_edit_time
386 thread.last_edit_time = self.last_edit_time
387
387
388 thread.save(update_fields=['last_edit_time', 'bumpable'])
388 thread.save(update_fields=['last_edit_time', 'bumpable'])
389
389
390 super().save(force_insert, force_update, using, update_fields)
390 super().save(force_insert, force_update, using, update_fields)
391
391
392 def get_text(self) -> str:
392 def get_text(self) -> str:
393 return self._text_rendered
393 return self._text_rendered
394
394
395 def get_raw_text(self) -> str:
395 def get_raw_text(self) -> str:
396 return self.text
396 return self.text
397
397
398 def get_absolute_id(self) -> str:
398 def get_absolute_id(self) -> str:
399 """
399 """
400 If the post has many threads, shows its main thread OP id in the post
400 If the post has many threads, shows its main thread OP id in the post
401 ID.
401 ID.
402 """
402 """
403
403
404 if self.get_threads().count() > 1:
404 if self.get_threads().count() > 1:
405 return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id)
405 return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id)
406 else:
406 else:
407 return str(self.id)
407 return str(self.id)
408
408
409 def connect_notifications(self):
409 def connect_notifications(self):
410 for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()):
410 for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()):
411 user_name = reply_number.group(1).lower()
411 user_name = reply_number.group(1).lower()
412 Notification.objects.get_or_create(name=user_name, post=self)
412 Notification.objects.get_or_create(name=user_name, post=self)
413
413
414 def connect_replies(self):
414 def connect_replies(self):
415 """
415 """
416 Connects replies to a post to show them as a reflink map
416 Connects replies to a post to show them as a reflink map
417 """
417 """
418
418
419 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
419 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
420 post_id = reply_number.group(1)
420 post_id = reply_number.group(1)
421
421
422 try:
422 try:
423 referenced_post = Post.objects.get(id=post_id)
423 referenced_post = Post.objects.get(id=post_id)
424
424
425 referenced_post.referenced_posts.add(self)
425 referenced_post.referenced_posts.add(self)
426 referenced_post.last_edit_time = self.pub_time
426 referenced_post.last_edit_time = self.pub_time
427 referenced_post.build_refmap()
427 referenced_post.build_refmap()
428 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
428 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
429 except ObjectDoesNotExist:
429 except ObjectDoesNotExist:
430 pass
430 pass
431
431
432 def connect_threads(self, opening_posts):
432 def connect_threads(self, opening_posts):
433 for opening_post in opening_posts:
433 for opening_post in opening_posts:
434 threads = opening_post.get_threads().all()
434 threads = opening_post.get_threads().all()
435 for thread in threads:
435 for thread in threads:
436 if thread.can_bump():
436 if thread.can_bump():
437 thread.update_bump_status()
437 thread.update_bump_status()
438
438
439 thread.last_edit_time = self.last_edit_time
439 thread.last_edit_time = self.last_edit_time
440 thread.save(update_fields=['last_edit_time', 'bumpable'])
440 thread.save(update_fields=['last_edit_time', 'bumpable'])
441 self.threads.add(opening_post.get_thread())
441 self.threads.add(opening_post.get_thread())
442
442
443 def get_tripcode_color(self):
443 def get_tripcode_color(self):
444 return self.tripcode[:6]
444 return self.tripcode[:6]
445
445
446 def get_tripcode_background(self):
447 code = self.get_tripcode_color()
448 result = ''
449
450 for i in range(0, len(code), 2):
451 p = code[i:i+2]
452 result += hex(255 - int(p, 16))[2:]
453
454 return result
455
446 def get_short_tripcode(self):
456 def get_short_tripcode(self):
447 return self.tripcode[:10] No newline at end of file
457 return self.tripcode[:10]
@@ -1,113 +1,113 b''
1 {% load i18n %}
1 {% load i18n %}
2 {% load board %}
2 {% load board %}
3
3
4 {% get_current_language as LANGUAGE_CODE %}
4 {% get_current_language as LANGUAGE_CODE %}
5
5
6 <div class="{{ css_class }}" id="{{ post.id }}" data-uid="{{ post.uid }}">
6 <div class="{{ css_class }}" id="{{ post.id }}" data-uid="{{ post.uid }}">
7 <div class="post-info">
7 <div class="post-info">
8 <a class="post_id" href="{{ post.get_absolute_url }}">#{{ post.get_absolute_id }}</a>
8 <a class="post_id" href="{{ post.get_absolute_url }}">#{{ post.get_absolute_id }}</a>
9 <span class="title">{{ post.title }}</span>
9 <span class="title">{{ post.title }}</span>
10 <span class="pub_time"><time datetime="{{ post.pub_time|date:'c' }}">{{ post.pub_time }}</time></span>
10 <span class="pub_time"><time datetime="{{ post.pub_time|date:'c' }}">{{ post.pub_time }}</time></span>
11 {% if post.tripcode %}
11 {% if post.tripcode %}
12 <span style="color: #{{post.get_tripcode_color}}">{{ post.get_short_tripcode }}</span>
12 <span style="color: #{{ post.get_tripcode_color }}; background: #{{ post.get_tripcode_background }}">{{ post.get_short_tripcode }}</span>
13 {% endif %}
13 {% endif %}
14 {% comment %}
14 {% comment %}
15 Thread death time needs to be shown only if the thread is alredy archived
15 Thread death time needs to be shown only if the thread is alredy archived
16 and this is an opening post (thread death time) or a post for popup
16 and this is an opening post (thread death time) or a post for popup
17 (we don't see OP here so we show the death time in the post itself).
17 (we don't see OP here so we show the death time in the post itself).
18 {% endcomment %}
18 {% endcomment %}
19 {% if thread.archived %}
19 {% if thread.archived %}
20 {% if is_opening %}
20 {% if is_opening %}
21 β€” <time datetime="{{ thread.bump_time|date:'c' }}">{{ thread.bump_time }}</time>
21 β€” <time datetime="{{ thread.bump_time|date:'c' }}">{{ thread.bump_time }}</time>
22 {% endif %}
22 {% endif %}
23 {% endif %}
23 {% endif %}
24 {% if is_opening %}
24 {% if is_opening %}
25 {% if need_open_link %}
25 {% if need_open_link %}
26 {% if thread.archived %}
26 {% if thread.archived %}
27 <a class="link" href="{% url 'thread' post.id %}">{% trans "Open" %}</a>
27 <a class="link" href="{% url 'thread' post.id %}">{% trans "Open" %}</a>
28 {% else %}
28 {% else %}
29 <a class="link" href="{% url 'thread' post.id %}#form">{% trans "Reply" %}</a>
29 <a class="link" href="{% url 'thread' post.id %}#form">{% trans "Reply" %}</a>
30 {% endif %}
30 {% endif %}
31 {% endif %}
31 {% endif %}
32 {% else %}
32 {% else %}
33 {% if need_op_data %}
33 {% if need_op_data %}
34 {% with thread.get_opening_post as op %}
34 {% with thread.get_opening_post as op %}
35 {% trans " in " %}<a href="{{ op.get_absolute_url }}">&gt;&gt;{{ op.id }}</a> <span class="title">{{ op.get_title|striptags|truncatewords:5 }}</span>
35 {% trans " in " %}<a href="{{ op.get_absolute_url }}">&gt;&gt;{{ op.id }}</a> <span class="title">{{ op.get_title|striptags|truncatewords:5 }}</span>
36 {% endwith %}
36 {% endwith %}
37 {% endif %}
37 {% endif %}
38 {% endif %}
38 {% endif %}
39 {% if reply_link and not thread.archived %}
39 {% if reply_link and not thread.archived %}
40 <a href="#form" onclick="addQuickReply('{{ post.id }}'); return false;">{% trans 'Reply' %}</a>
40 <a href="#form" onclick="addQuickReply('{{ post.id }}'); return false;">{% trans 'Reply' %}</a>
41 {% endif %}
41 {% endif %}
42
42
43 {% if moderator %}
43 {% if moderator %}
44 <span class="moderator_info">
44 <span class="moderator_info">
45 | <a href="{% url 'admin:boards_post_change' post.id %}">{% trans 'Edit' %}</a>
45 | <a href="{% url 'admin:boards_post_change' post.id %}">{% trans 'Edit' %}</a>
46 {% if is_opening %}
46 {% if is_opening %}
47 | <a href="{% url 'admin:boards_thread_change' thread.id %}">{% trans 'Edit thread' %}</a>
47 | <a href="{% url 'admin:boards_thread_change' thread.id %}">{% trans 'Edit thread' %}</a>
48 {% endif %}
48 {% endif %}
49 </span>
49 </span>
50 {% endif %}
50 {% endif %}
51 </div>
51 </div>
52 {% comment %}
52 {% comment %}
53 Post images. Currently only 1 image can be posted and shown, but post model
53 Post images. Currently only 1 image can be posted and shown, but post model
54 supports multiple.
54 supports multiple.
55 {% endcomment %}
55 {% endcomment %}
56 {% if post.images.exists %}
56 {% if post.images.exists %}
57 {% with post.images.first as image %}
57 {% with post.images.first as image %}
58 {% autoescape off %}
58 {% autoescape off %}
59 {{ image.get_view }}
59 {{ image.get_view }}
60 {% endautoescape %}
60 {% endautoescape %}
61 {% endwith %}
61 {% endwith %}
62 {% endif %}
62 {% endif %}
63 {% if post.attachments.exists %}
63 {% if post.attachments.exists %}
64 {% with post.attachments.first as file %}
64 {% with post.attachments.first as file %}
65 {% autoescape off %}
65 {% autoescape off %}
66 {{ file.get_view }}
66 {{ file.get_view }}
67 {% endautoescape %}
67 {% endautoescape %}
68 {% endwith %}
68 {% endwith %}
69 {% endif %}
69 {% endif %}
70 {% comment %}
70 {% comment %}
71 Post message (text)
71 Post message (text)
72 {% endcomment %}
72 {% endcomment %}
73 <div class="message">
73 <div class="message">
74 {% autoescape off %}
74 {% autoescape off %}
75 {% if truncated %}
75 {% if truncated %}
76 {{ post.get_text|truncatewords_html:50 }}
76 {{ post.get_text|truncatewords_html:50 }}
77 {% else %}
77 {% else %}
78 {{ post.get_text }}
78 {{ post.get_text }}
79 {% endif %}
79 {% endif %}
80 {% endautoescape %}
80 {% endautoescape %}
81 </div>
81 </div>
82 {% if post.is_referenced %}
82 {% if post.is_referenced %}
83 {% if mode_tree %}
83 {% if mode_tree %}
84 <div class="tree_reply">
84 <div class="tree_reply">
85 {% for refpost in post.get_referenced_posts %}
85 {% for refpost in post.get_referenced_posts %}
86 {% post_view refpost mode_tree=True %}
86 {% post_view refpost mode_tree=True %}
87 {% endfor %}
87 {% endfor %}
88 </div>
88 </div>
89 {% else %}
89 {% else %}
90 <div class="refmap">
90 <div class="refmap">
91 {% autoescape off %}
91 {% autoescape off %}
92 {% trans "Replies" %}: {{ post.refmap }}
92 {% trans "Replies" %}: {{ post.refmap }}
93 {% endautoescape %}
93 {% endautoescape %}
94 </div>
94 </div>
95 {% endif %}
95 {% endif %}
96 {% endif %}
96 {% endif %}
97 {% comment %}
97 {% comment %}
98 Thread metadata: counters, tags etc
98 Thread metadata: counters, tags etc
99 {% endcomment %}
99 {% endcomment %}
100 {% if is_opening %}
100 {% if is_opening %}
101 <div class="metadata">
101 <div class="metadata">
102 {% if is_opening and need_open_link %}
102 {% if is_opening and need_open_link %}
103 {{ thread.get_reply_count }} {% trans 'messages' %},
103 {{ thread.get_reply_count }} {% trans 'messages' %},
104 {{ thread.get_images_count }} {% trans 'images' %}.
104 {{ thread.get_images_count }} {% trans 'images' %}.
105 {% endif %}
105 {% endif %}
106 <span class="tags">
106 <span class="tags">
107 {% autoescape off %}
107 {% autoescape off %}
108 {{ thread.get_tag_url_list }}
108 {{ thread.get_tag_url_list }}
109 {% endautoescape %}
109 {% endautoescape %}
110 </span>
110 </span>
111 </div>
111 </div>
112 {% endif %}
112 {% endif %}
113 </div>
113 </div>
General Comments 0
You need to be logged in to leave comments. Login now