##// END OF EJS Templates
Tags localization
neko259 -
r1860:2c43e9bb default
parent child Browse files
Show More
@@ -0,0 +1,20 b''
1 # -*- coding: utf-8 -*-
2 # Generated by Django 1.10.5 on 2017-02-24 19:40
3 from __future__ import unicode_literals
4
5 from django.db import migrations, models
6
7
8 class Migration(migrations.Migration):
9
10 dependencies = [
11 ('boards', '0056_auto_20170123_1620'),
12 ]
13
14 operations = [
15 migrations.AddField(
16 model_name='tag',
17 name='aliases',
18 field=models.TextField(blank=True),
19 ),
20 ]
@@ -1,524 +1,529 b''
1 1 import hashlib
2 2 import logging
3 3 import re
4 4 import time
5 5 import traceback
6 6
7 7 import pytz
8 8
9 9 from PIL import Image
10 10
11 11 from django import forms
12 12 from django.core.files.uploadedfile import SimpleUploadedFile, UploadedFile
13 13 from django.forms.utils import ErrorList
14 14 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
15 15 from django.core.files.images import get_image_dimensions
16 16
17 17 import boards.settings as board_settings
18 18 import neboard
19 19 from boards import utils
20 20 from boards.abstracts.attachment_alias import get_image_by_alias
21 21 from boards.abstracts.settingsmanager import get_settings_manager
22 22 from boards.forms.fields import UrlFileField
23 23 from boards.mdx_neboard import formatters
24 24 from boards.models import Attachment
25 25 from boards.models import Tag
26 26 from boards.models.attachment.downloaders import download, REGEX_MAGNET
27 27 from boards.models.post import TITLE_MAX_LENGTH
28 28 from boards.utils import validate_file_size, get_file_mimetype, \
29 29 FILE_EXTENSION_DELIMITER
30 30 from boards.models.attachment.viewers import FILE_TYPES_IMAGE
31 31 from neboard import settings
32 32
33 33 SECTION_FORMS = 'Forms'
34 34
35 35 POW_HASH_LENGTH = 16
36 36 POW_LIFE_MINUTES = 5
37 37
38 38 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
39 39 REGEX_USERNAMES = re.compile(r'^[\w\s\d,]+$', re.UNICODE)
40 40 REGEX_URL = re.compile(r'^(http|https|ftp):\/\/', re.UNICODE)
41 41
42 42 VETERAN_POSTING_DELAY = 5
43 43
44 44 ATTRIBUTE_PLACEHOLDER = 'placeholder'
45 45 ATTRIBUTE_ROWS = 'rows'
46 46
47 47 LAST_POST_TIME = 'last_post_time'
48 48 LAST_LOGIN_TIME = 'last_login_time'
49 49 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
50 50 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
51 51
52 52 LABEL_TITLE = _('Title')
53 53 LABEL_TEXT = _('Text')
54 54 LABEL_TAG = _('Tag')
55 55 LABEL_SEARCH = _('Search')
56 56 LABEL_FILE = _('File')
57 57 LABEL_DUPLICATES = _('Check for duplicates')
58 58
59 59 ERROR_SPEED = 'Please wait %(delay)d second before sending message'
60 60 ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message'
61 61 ERROR_MANY_FILES = 'You can post no more than %(files)d file.'
62 62 ERROR_MANY_FILES_PLURAL = 'You can post no more than %(files)d files.'
63 63 ERROR_DUPLICATES = 'Some files are already present on the board.'
64 64
65 65 TAG_MAX_LENGTH = 20
66 66
67 67 TEXTAREA_ROWS = 4
68 68
69 69 TRIPCODE_DELIM = '#'
70 70
71 71 # TODO Maybe this may be converted into the database table?
72 72 MIMETYPE_EXTENSIONS = {
73 73 'image/jpeg': 'jpeg',
74 74 'image/png': 'png',
75 75 'image/gif': 'gif',
76 76 'video/webm': 'webm',
77 77 'application/pdf': 'pdf',
78 78 'x-diff': 'diff',
79 79 'image/svg+xml': 'svg',
80 80 'application/x-shockwave-flash': 'swf',
81 81 'image/x-ms-bmp': 'bmp',
82 82 'image/bmp': 'bmp',
83 83 }
84 84
85 85
86 86 logger = logging.getLogger('boards.forms')
87 87
88 88
89 89 def get_timezones():
90 90 timezones = []
91 91 for tz in pytz.common_timezones:
92 92 timezones.append((tz, tz),)
93 93 return timezones
94 94
95 95
96 96 class FormatPanel(forms.Textarea):
97 97 """
98 98 Panel for text formatting. Consists of buttons to add different tags to the
99 99 form text area.
100 100 """
101 101
102 102 def render(self, name, value, attrs=None):
103 103 output = '<div id="mark-panel">'
104 104 for formatter in formatters:
105 105 output += '<span class="mark_btn"' + \
106 106 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
107 107 '\', \'' + formatter.format_right + '\')">' + \
108 108 formatter.preview_left + formatter.name + \
109 109 formatter.preview_right + '</span>'
110 110
111 111 output += '</div>'
112 112 output += super(FormatPanel, self).render(name, value, attrs=attrs)
113 113
114 114 return output
115 115
116 116
117 117 class PlainErrorList(ErrorList):
118 118 def __unicode__(self):
119 119 return self.as_text()
120 120
121 121 def as_text(self):
122 122 return ''.join(['(!) %s ' % e for e in self])
123 123
124 124
125 125 class NeboardForm(forms.Form):
126 126 """
127 127 Form with neboard-specific formatting.
128 128 """
129 129 required_css_class = 'required-field'
130 130
131 131 def as_div(self):
132 132 """
133 133 Returns this form rendered as HTML <as_div>s.
134 134 """
135 135
136 136 return self._html_output(
137 137 # TODO Do not show hidden rows in the list here
138 138 normal_row='<div class="form-row">'
139 139 '<div class="form-label">'
140 140 '%(label)s'
141 141 '</div>'
142 142 '<div class="form-input">'
143 143 '%(field)s'
144 144 '</div>'
145 145 '</div>'
146 146 '<div class="form-row">'
147 147 '%(help_text)s'
148 148 '</div>',
149 149 error_row='<div class="form-row">'
150 150 '<div class="form-label"></div>'
151 151 '<div class="form-errors">%s</div>'
152 152 '</div>',
153 153 row_ender='</div>',
154 154 help_text_html='%s',
155 155 errors_on_separate_row=True)
156 156
157 157 def as_json_errors(self):
158 158 errors = []
159 159
160 160 for name, field in list(self.fields.items()):
161 161 if self[name].errors:
162 162 errors.append({
163 163 'field': name,
164 164 'errors': self[name].errors.as_text(),
165 165 })
166 166
167 167 return errors
168 168
169 169
170 170 class PostForm(NeboardForm):
171 171
172 172 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
173 173 label=LABEL_TITLE,
174 174 widget=forms.TextInput(
175 175 attrs={ATTRIBUTE_PLACEHOLDER: 'title#tripcode'}))
176 176 text = forms.CharField(
177 177 widget=FormatPanel(attrs={
178 178 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
179 179 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
180 180 }),
181 181 required=False, label=LABEL_TEXT)
182 182 file = UrlFileField(required=False, label=LABEL_FILE)
183 183
184 184 # This field is for spam prevention only
185 185 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
186 186 widget=forms.TextInput(attrs={
187 187 'class': 'form-email'}))
188 188 subscribe = forms.BooleanField(required=False, label=_('Subscribe to thread'))
189 189 check_duplicates = forms.BooleanField(required=False, label=LABEL_DUPLICATES)
190 190
191 191 guess = forms.CharField(widget=forms.HiddenInput(), required=False)
192 192 timestamp = forms.CharField(widget=forms.HiddenInput(), required=False)
193 193 iteration = forms.CharField(widget=forms.HiddenInput(), required=False)
194 194
195 195 session = None
196 196 need_to_ban = False
197 197 image = None
198 198
199 199 def clean_title(self):
200 200 title = self.cleaned_data['title']
201 201 if title:
202 202 if len(title) > TITLE_MAX_LENGTH:
203 203 raise forms.ValidationError(_('Title must have less than %s '
204 204 'characters') %
205 205 str(TITLE_MAX_LENGTH))
206 206 return title
207 207
208 208 def clean_text(self):
209 209 text = self.cleaned_data['text'].strip()
210 210 if text:
211 211 max_length = board_settings.get_int(SECTION_FORMS, 'MaxTextLength')
212 212 if len(text) > max_length:
213 213 raise forms.ValidationError(_('Text must have less than %s '
214 214 'characters') % str(max_length))
215 215 return text
216 216
217 217 def clean_file(self):
218 218 return self._clean_files(self.cleaned_data['file'])
219 219
220 220 def clean(self):
221 221 cleaned_data = super(PostForm, self).clean()
222 222
223 223 if cleaned_data['email']:
224 224 if board_settings.get_bool(SECTION_FORMS, 'Autoban'):
225 225 self.need_to_ban = True
226 226 raise forms.ValidationError('A human cannot enter a hidden field')
227 227
228 228 if not self.errors:
229 229 self._clean_text_file()
230 230
231 231 limit_speed = board_settings.get_bool(SECTION_FORMS, 'LimitPostingSpeed')
232 232 limit_first = board_settings.get_bool(SECTION_FORMS, 'LimitFirstPosting')
233 233
234 234 settings_manager = get_settings_manager(self)
235 235 if not self.errors and limit_speed or (limit_first and not settings_manager.get_setting('confirmed_user')):
236 236 pow_difficulty = board_settings.get_int(SECTION_FORMS, 'PowDifficulty')
237 237 if pow_difficulty > 0:
238 238 # PoW-based
239 239 if cleaned_data['timestamp'] \
240 240 and cleaned_data['iteration'] and cleaned_data['guess'] \
241 241 and not settings_manager.get_setting('confirmed_user'):
242 242 self._validate_hash(cleaned_data['timestamp'], cleaned_data['iteration'], cleaned_data['guess'], cleaned_data['text'])
243 243 else:
244 244 # Time-based
245 245 self._validate_posting_speed()
246 246 settings_manager.set_setting('confirmed_user', True)
247 247 if self.cleaned_data['check_duplicates']:
248 248 self._check_file_duplicates(self.get_files())
249 249
250 250 return cleaned_data
251 251
252 252 def get_files(self):
253 253 """
254 254 Gets file from form or URL.
255 255 """
256 256
257 257 files = []
258 258 for file in self.cleaned_data['file']:
259 259 if isinstance(file, UploadedFile):
260 260 files.append(file)
261 261
262 262 return files
263 263
264 264 def get_file_urls(self):
265 265 files = []
266 266 for file in self.cleaned_data['file']:
267 267 if type(file) == str:
268 268 files.append(file)
269 269
270 270 return files
271 271
272 272 def get_tripcode(self):
273 273 title = self.cleaned_data['title']
274 274 if title is not None and TRIPCODE_DELIM in title:
275 275 code = title.split(TRIPCODE_DELIM, maxsplit=1)[1] + neboard.settings.SECRET_KEY
276 276 tripcode = hashlib.md5(code.encode()).hexdigest()
277 277 else:
278 278 tripcode = ''
279 279 return tripcode
280 280
281 281 def get_title(self):
282 282 title = self.cleaned_data['title']
283 283 if title is not None and TRIPCODE_DELIM in title:
284 284 return title.split(TRIPCODE_DELIM, maxsplit=1)[0]
285 285 else:
286 286 return title
287 287
288 288 def get_images(self):
289 289 if self.image:
290 290 return [self.image]
291 291 else:
292 292 return []
293 293
294 294 def is_subscribe(self):
295 295 return self.cleaned_data['subscribe']
296 296
297 297 def _update_file_extension(self, file):
298 298 if file:
299 299 mimetype = get_file_mimetype(file)
300 300 extension = MIMETYPE_EXTENSIONS.get(mimetype)
301 301 if extension:
302 302 filename = file.name.split(FILE_EXTENSION_DELIMITER, 1)[0]
303 303 new_filename = filename + FILE_EXTENSION_DELIMITER + extension
304 304
305 305 file.name = new_filename
306 306 else:
307 307 logger.info('Unrecognized file mimetype: {}'.format(mimetype))
308 308
309 309 def _clean_files(self, inputs):
310 310 files = []
311 311
312 312 max_file_count = board_settings.get_int(SECTION_FORMS, 'MaxFileCount')
313 313 if len(inputs) > max_file_count:
314 314 raise forms.ValidationError(
315 315 ungettext_lazy(ERROR_MANY_FILES, ERROR_MANY_FILES,
316 316 max_file_count) % {'files': max_file_count})
317 317 for file_input in inputs:
318 318 if isinstance(file_input, UploadedFile):
319 319 files.append(self._clean_file_file(file_input))
320 320 else:
321 321 files.append(self._clean_file_url(file_input))
322 322
323 323 for file in files:
324 324 self._validate_image_dimensions(file)
325 325
326 326 return files
327 327
328 328 def _validate_image_dimensions(self, file):
329 329 if isinstance(file, UploadedFile):
330 330 mimetype = get_file_mimetype(file)
331 331 if mimetype.split('/')[-1] in FILE_TYPES_IMAGE:
332 332 Image.warnings.simplefilter('error', Image.DecompressionBombWarning)
333 333 try:
334 334 print(get_image_dimensions(file))
335 335 except Exception:
336 336 raise forms.ValidationError('Possible decompression bomb or large image.')
337 337
338 338 def _clean_file_file(self, file):
339 339 validate_file_size(file.size)
340 340 self._update_file_extension(file)
341 341
342 342 return file
343 343
344 344 def _clean_file_url(self, url):
345 345 file = None
346 346
347 347 if url:
348 348 try:
349 349 file = get_image_by_alias(url, self.session)
350 350 self.image = file
351 351
352 352 if file is not None:
353 353 return
354 354
355 355 if file is None:
356 356 file = self._get_file_from_url(url)
357 357 if not file:
358 358 raise forms.ValidationError(_('Invalid URL'))
359 359 else:
360 360 validate_file_size(file.size)
361 361 self._update_file_extension(file)
362 362 except forms.ValidationError as e:
363 363 # Assume we will get the plain URL instead of a file and save it
364 364 if REGEX_URL.match(url) or REGEX_MAGNET.match(url):
365 365 logger.info('Error in forms: {}'.format(e))
366 366 return url
367 367 else:
368 368 raise e
369 369
370 370 return file
371 371
372 372 def _clean_text_file(self):
373 373 text = self.cleaned_data.get('text')
374 374 file = self.get_files()
375 375 file_url = self.get_file_urls()
376 376 images = self.get_images()
377 377
378 378 if (not text) and (not file) and (not file_url) and len(images) == 0:
379 379 error_message = _('Either text or file must be entered.')
380 380 self._add_general_error(error_message)
381 381
382 382 def _validate_posting_speed(self):
383 383 can_post = True
384 384
385 385 posting_delay = board_settings.get_int(SECTION_FORMS, 'PostingDelay')
386 386
387 387 if board_settings.get_bool(SECTION_FORMS, 'LimitPostingSpeed'):
388 388 now = time.time()
389 389
390 390 current_delay = 0
391 391
392 392 if LAST_POST_TIME not in self.session:
393 393 self.session[LAST_POST_TIME] = now
394 394
395 395 need_delay = True
396 396 else:
397 397 last_post_time = self.session.get(LAST_POST_TIME)
398 398 current_delay = int(now - last_post_time)
399 399
400 400 need_delay = current_delay < posting_delay
401 401
402 402 if need_delay:
403 403 delay = posting_delay - current_delay
404 404 error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL,
405 405 delay) % {'delay': delay}
406 406 self._add_general_error(error_message)
407 407
408 408 can_post = False
409 409
410 410 if can_post:
411 411 self.session[LAST_POST_TIME] = now
412 412
413 413 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
414 414 """
415 415 Gets an file file from URL.
416 416 """
417 417
418 418 try:
419 419 return download(url)
420 420 except forms.ValidationError as e:
421 421 raise e
422 422 except Exception as e:
423 423 raise forms.ValidationError(e)
424 424
425 425 def _validate_hash(self, timestamp: str, iteration: str, guess: str, message: str):
426 426 payload = timestamp + message.replace('\r\n', '\n')
427 427 difficulty = board_settings.get_int(SECTION_FORMS, 'PowDifficulty')
428 428 target = str(int(2 ** (POW_HASH_LENGTH * 3) / difficulty))
429 429 if len(target) < POW_HASH_LENGTH:
430 430 target = '0' * (POW_HASH_LENGTH - len(target)) + target
431 431
432 432 computed_guess = hashlib.sha256((payload + iteration).encode())\
433 433 .hexdigest()[0:POW_HASH_LENGTH]
434 434 if guess != computed_guess or guess > target:
435 435 self._add_general_error(_('Invalid PoW.'))
436 436
437 437 def _check_file_duplicates(self, files):
438 438 for file in files:
439 439 file_hash = utils.get_file_hash(file)
440 440 if Attachment.objects.get_existing_duplicate(file_hash, file):
441 441 self._add_general_error(_(ERROR_DUPLICATES))
442 442
443 443 def _add_general_error(self, message):
444 444 self.add_error('text', forms.ValidationError(message))
445 445
446 446
447 447 class ThreadForm(PostForm):
448 448
449 449 tags = forms.CharField(
450 450 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
451 451 max_length=100, label=_('Tags'), required=True)
452 452 monochrome = forms.BooleanField(label=_('Monochrome'), required=False)
453 453
454 454 def clean_tags(self):
455 455 tags = self.cleaned_data['tags'].strip()
456 456
457 457 if not tags or not REGEX_TAGS.match(tags):
458 458 raise forms.ValidationError(
459 459 _('Inappropriate characters in tags.'))
460 460
461 461 default_tag_name = board_settings.get(SECTION_FORMS, 'DefaultTag')\
462 462 .strip().lower()
463 463
464 464 required_tag_exists = False
465 465 tag_set = set()
466 466 for tag_string in tags.split():
467 if tag_string.strip().lower() == default_tag_name:
467 tag_name = tag_string.strip().lower()
468 if tag_name == default_tag_name:
468 469 required_tag_exists = True
469 470 tag, created = Tag.objects.get_or_create(
470 name=tag_string.strip().lower(), required=True)
471 name=tag_name, required=True)
472 else:
473 tag = Tag.objects.get_by_alias(tag_name)
474 if tag:
475 created = False
471 476 else:
472 477 tag, created = Tag.objects.get_or_create(
473 name=tag_string.strip().lower())
478 name=tag_name)
474 479 tag_set.add(tag)
475 480
476 481 # If this is a new tag, don't check for its parents because nobody
477 482 # added them yet
478 483 if not created:
479 484 tag_set |= set(tag.get_all_parents())
480 485
481 486 for tag in tag_set:
482 487 if tag.required:
483 488 required_tag_exists = True
484 489 break
485 490
486 491 # Use default tag if no section exists
487 492 if not required_tag_exists:
488 493 default_tag, created = Tag.objects.get_or_create(
489 494 name=default_tag_name, required=True)
490 495 tag_set.add(default_tag)
491 496
492 497 return tag_set
493 498
494 499 def clean(self):
495 500 cleaned_data = super(ThreadForm, self).clean()
496 501
497 502 return cleaned_data
498 503
499 504 def is_monochrome(self):
500 505 return self.cleaned_data['monochrome']
501 506
502 507
503 508 class SettingsForm(NeboardForm):
504 509
505 510 theme = forms.ChoiceField(
506 511 choices=board_settings.get_list_dict('View', 'Themes'),
507 512 label=_('Theme'))
508 513 image_viewer = forms.ChoiceField(
509 514 choices=board_settings.get_list_dict('View', 'ImageViewers'),
510 515 label=_('Image view mode'))
511 516 username = forms.CharField(label=_('User name'), required=False)
512 517 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
513 518
514 519 def clean_username(self):
515 520 username = self.cleaned_data['username']
516 521
517 522 if username and not REGEX_USERNAMES.match(username):
518 523 raise forms.ValidationError(_('Inappropriate characters.'))
519 524
520 525 return username
521 526
522 527
523 528 class SearchForm(NeboardForm):
524 529 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
@@ -1,145 +1,162 b''
1 1 import hashlib
2 import re
3
2 4 from boards.models.attachment import FILE_TYPES_IMAGE
3 5 from django.template.loader import render_to_string
4 6 from django.db import models
5 7 from django.db.models import Count
6 8 from django.core.urlresolvers import reverse
9 from django.utils.translation import get_language
7 10
8 11 from boards.models import Attachment
9 12 from boards.models.base import Viewable
10 13 from boards.models.thread import STATUS_ACTIVE, STATUS_BUMPLIMIT, STATUS_ARCHIVE
11 14 from boards.utils import cached_result
12 15 import boards
13 16
14 17 __author__ = 'neko259'
15 18
16 19
17 20 RELATED_TAGS_COUNT = 5
18 21
22 REGEX_TAG_ALIAS = r'{}:(\w+),'
23
19 24
20 25 class TagManager(models.Manager):
21
22 26 def get_not_empty_tags(self):
23 27 """
24 28 Gets tags that have non-archived threads.
25 29 """
26 30
27 31 return self.annotate(num_threads=Count('thread_tags')).filter(num_threads__gt=0)\
28 32 .order_by('name')
29 33
30 34 def get_tag_url_list(self, tags: list) -> str:
31 35 """
32 36 Gets a comma-separated list of tag links.
33 37 """
34 38
35 39 return ', '.join([tag.get_view() for tag in tags])
36 40
41 def get_by_alias(self, alias):
42 return self.filter(aliases__contains=":{},".format(alias)).first()
43
37 44
38 45 class Tag(models.Model, Viewable):
39 46 """
40 47 A tag is a text node assigned to the thread. The tag serves as a board
41 48 section. There can be multiple tags for each thread
42 49 """
43 50
44 51 objects = TagManager()
45 52
46 53 class Meta:
47 54 app_label = 'boards'
48 55 ordering = ('name',)
49 56
50 57 name = models.CharField(max_length=100, db_index=True, unique=True)
51 58 required = models.BooleanField(default=False, db_index=True)
52 59 description = models.TextField(blank=True)
53 60
54 61 parent = models.ForeignKey('Tag', null=True, blank=True,
55 62 related_name='children')
63 aliases = models.TextField(blank=True)
56 64
57 65 def __str__(self):
58 66 return self.name
59 67
60 68 def is_empty(self) -> bool:
61 69 """
62 70 Checks if the tag has some threads.
63 71 """
64 72
65 73 return self.get_thread_count() == 0
66 74
67 75 def get_thread_count(self, status=None) -> int:
68 76 threads = self.get_threads()
69 77 if status is not None:
70 78 threads = threads.filter(status=status)
71 79 return threads.count()
72 80
73 81 def get_active_thread_count(self) -> int:
74 82 return self.get_thread_count(status=STATUS_ACTIVE)
75 83
76 84 def get_bumplimit_thread_count(self) -> int:
77 85 return self.get_thread_count(status=STATUS_BUMPLIMIT)
78 86
79 87 def get_archived_thread_count(self) -> int:
80 88 return self.get_thread_count(status=STATUS_ARCHIVE)
81 89
82 90 def get_absolute_url(self):
83 91 return reverse('tag', kwargs={'tag_name': self.name})
84 92
85 93 def get_threads(self):
86 94 return self.thread_tags.order_by('-bump_time')
87 95
88 96 def is_required(self):
89 97 return self.required
90 98
91 99 def get_view(self):
100 locale = get_language()
101
102 if self.aliases:
103 match = re.search(REGEX_TAG_ALIAS.format(locale), self.aliases)
104 if match:
105 localized_tag_name = match.group(1)
106 else:
107 localized_tag_name = self.name
108
92 109 link = '<a class="tag" href="{}">{}</a>'.format(
93 self.get_absolute_url(), self.name)
110 self.get_absolute_url(), localized_tag_name)
94 111 if self.is_required():
95 112 link = '<b>{}</b>'.format(link)
96 113 return link
97 114
98 115 @cached_result()
99 116 def get_post_count(self):
100 117 return self.get_threads().aggregate(num_posts=Count('replies'))['num_posts']
101 118
102 119 def get_description(self):
103 120 return self.description
104 121
105 122 def get_random_image_post(self, status=[STATUS_ACTIVE, STATUS_BUMPLIMIT]):
106 123 posts = boards.models.Post.objects.filter(attachments__mimetype__in=FILE_TYPES_IMAGE)\
107 124 .annotate(images_count=Count(
108 125 'attachments')).filter(images_count__gt=0, thread__tags__in=[self])
109 126 if status is not None:
110 127 posts = posts.filter(thread__status__in=status)
111 128 return posts.order_by('?').first()
112 129
113 130 def get_first_letter(self):
114 131 return self.name and self.name[0] or ''
115 132
116 133 def get_related_tags(self):
117 134 return set(Tag.objects.filter(thread_tags__in=self.get_threads()).exclude(
118 135 id=self.id).order_by('?')[:RELATED_TAGS_COUNT])
119 136
120 137 @cached_result()
121 138 def get_color(self):
122 139 """
123 140 Gets color hashed from the tag name.
124 141 """
125 142 return hashlib.md5(self.name.encode()).hexdigest()[:6]
126 143
127 144 def get_parent(self):
128 145 return self.parent
129 146
130 147 def get_all_parents(self):
131 148 parents = list()
132 149 parent = self.get_parent()
133 150 if parent and parent not in parents:
134 151 parents.insert(0, parent)
135 152 parents = parent.get_all_parents() + parents
136 153
137 154 return parents
138 155
139 156 def get_children(self):
140 157 return self.children
141 158
142 159 def get_images(self):
143 160 return Attachment.objects.filter(
144 161 attachment_posts__thread__tags__in=[self]).filter(
145 162 mimetype__in=FILE_TYPES_IMAGE).order_by('-attachment_posts__pub_time')
General Comments 0
You need to be logged in to leave comments. Login now