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