##// END OF EJS Templates
Show all tag parents at the tag page
neko259 -
r1361:1af05638 default
parent child Browse files
Show More
@@ -1,372 +1,371 b''
1 import hashlib
1 import hashlib
2 import re
2 import re
3 import time
3 import time
4
4
5 import pytz
5 import pytz
6 from django import forms
6 from django import forms
7 from django.core.files.uploadedfile import SimpleUploadedFile
7 from django.core.files.uploadedfile import SimpleUploadedFile
8 from django.core.exceptions import ObjectDoesNotExist
8 from django.core.exceptions import ObjectDoesNotExist
9 from django.forms.util import ErrorList
9 from django.forms.util import ErrorList
10 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
10 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
11
11
12 from boards.mdx_neboard import formatters
12 from boards.mdx_neboard import formatters
13 from boards.models.attachment.downloaders import Downloader
13 from boards.models.attachment.downloaders import Downloader
14 from boards.models.post import TITLE_MAX_LENGTH
14 from boards.models.post import TITLE_MAX_LENGTH
15 from boards.models import Tag, Post
15 from boards.models import Tag, Post
16 from boards.utils import validate_file_size
16 from boards.utils import validate_file_size
17 from neboard import settings
17 from neboard import settings
18 import boards.settings as board_settings
18 import boards.settings as board_settings
19 import neboard
19 import neboard
20
20
21 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
21 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
22
22
23 VETERAN_POSTING_DELAY = 5
23 VETERAN_POSTING_DELAY = 5
24
24
25 ATTRIBUTE_PLACEHOLDER = 'placeholder'
25 ATTRIBUTE_PLACEHOLDER = 'placeholder'
26 ATTRIBUTE_ROWS = 'rows'
26 ATTRIBUTE_ROWS = 'rows'
27
27
28 LAST_POST_TIME = 'last_post_time'
28 LAST_POST_TIME = 'last_post_time'
29 LAST_LOGIN_TIME = 'last_login_time'
29 LAST_LOGIN_TIME = 'last_login_time'
30 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
30 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
31 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
31 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
32
32
33 LABEL_TITLE = _('Title')
33 LABEL_TITLE = _('Title')
34 LABEL_TEXT = _('Text')
34 LABEL_TEXT = _('Text')
35 LABEL_TAG = _('Tag')
35 LABEL_TAG = _('Tag')
36 LABEL_SEARCH = _('Search')
36 LABEL_SEARCH = _('Search')
37
37
38 ERROR_SPEED = 'Please wait %(delay)d second before sending message'
38 ERROR_SPEED = 'Please wait %(delay)d second before sending message'
39 ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message'
39 ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message'
40
40
41 TAG_MAX_LENGTH = 20
41 TAG_MAX_LENGTH = 20
42
42
43 TEXTAREA_ROWS = 4
43 TEXTAREA_ROWS = 4
44
44
45
45
46 def get_timezones():
46 def get_timezones():
47 timezones = []
47 timezones = []
48 for tz in pytz.common_timezones:
48 for tz in pytz.common_timezones:
49 timezones.append((tz, tz),)
49 timezones.append((tz, tz),)
50 return timezones
50 return timezones
51
51
52
52
53 class FormatPanel(forms.Textarea):
53 class FormatPanel(forms.Textarea):
54 """
54 """
55 Panel for text formatting. Consists of buttons to add different tags to the
55 Panel for text formatting. Consists of buttons to add different tags to the
56 form text area.
56 form text area.
57 """
57 """
58
58
59 def render(self, name, value, attrs=None):
59 def render(self, name, value, attrs=None):
60 output = '<div id="mark-panel">'
60 output = '<div id="mark-panel">'
61 for formatter in formatters:
61 for formatter in formatters:
62 output += '<span class="mark_btn"' + \
62 output += '<span class="mark_btn"' + \
63 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
63 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
64 '\', \'' + formatter.format_right + '\')">' + \
64 '\', \'' + formatter.format_right + '\')">' + \
65 formatter.preview_left + formatter.name + \
65 formatter.preview_left + formatter.name + \
66 formatter.preview_right + '</span>'
66 formatter.preview_right + '</span>'
67
67
68 output += '</div>'
68 output += '</div>'
69 output += super(FormatPanel, self).render(name, value, attrs=None)
69 output += super(FormatPanel, self).render(name, value, attrs=None)
70
70
71 return output
71 return output
72
72
73
73
74 class PlainErrorList(ErrorList):
74 class PlainErrorList(ErrorList):
75 def __unicode__(self):
75 def __unicode__(self):
76 return self.as_text()
76 return self.as_text()
77
77
78 def as_text(self):
78 def as_text(self):
79 return ''.join(['(!) %s ' % e for e in self])
79 return ''.join(['(!) %s ' % e for e in self])
80
80
81
81
82 class NeboardForm(forms.Form):
82 class NeboardForm(forms.Form):
83 """
83 """
84 Form with neboard-specific formatting.
84 Form with neboard-specific formatting.
85 """
85 """
86
86
87 def as_div(self):
87 def as_div(self):
88 """
88 """
89 Returns this form rendered as HTML <as_div>s.
89 Returns this form rendered as HTML <as_div>s.
90 """
90 """
91
91
92 return self._html_output(
92 return self._html_output(
93 # TODO Do not show hidden rows in the list here
93 # TODO Do not show hidden rows in the list here
94 normal_row='<div class="form-row">'
94 normal_row='<div class="form-row">'
95 '<div class="form-label">'
95 '<div class="form-label">'
96 '%(label)s'
96 '%(label)s'
97 '</div>'
97 '</div>'
98 '<div class="form-input">'
98 '<div class="form-input">'
99 '%(field)s'
99 '%(field)s'
100 '</div>'
100 '</div>'
101 '</div>'
101 '</div>'
102 '<div class="form-row">'
102 '<div class="form-row">'
103 '%(help_text)s'
103 '%(help_text)s'
104 '</div>',
104 '</div>',
105 error_row='<div class="form-row">'
105 error_row='<div class="form-row">'
106 '<div class="form-label"></div>'
106 '<div class="form-label"></div>'
107 '<div class="form-errors">%s</div>'
107 '<div class="form-errors">%s</div>'
108 '</div>',
108 '</div>',
109 row_ender='</div>',
109 row_ender='</div>',
110 help_text_html='%s',
110 help_text_html='%s',
111 errors_on_separate_row=True)
111 errors_on_separate_row=True)
112
112
113 def as_json_errors(self):
113 def as_json_errors(self):
114 errors = []
114 errors = []
115
115
116 for name, field in list(self.fields.items()):
116 for name, field in list(self.fields.items()):
117 if self[name].errors:
117 if self[name].errors:
118 errors.append({
118 errors.append({
119 'field': name,
119 'field': name,
120 'errors': self[name].errors.as_text(),
120 'errors': self[name].errors.as_text(),
121 })
121 })
122
122
123 return errors
123 return errors
124
124
125
125
126 class PostForm(NeboardForm):
126 class PostForm(NeboardForm):
127
127
128 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
128 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
129 label=LABEL_TITLE,
129 label=LABEL_TITLE,
130 widget=forms.TextInput(
130 widget=forms.TextInput(
131 attrs={ATTRIBUTE_PLACEHOLDER:
131 attrs={ATTRIBUTE_PLACEHOLDER:
132 'test#tripcode'}))
132 'test#tripcode'}))
133 text = forms.CharField(
133 text = forms.CharField(
134 widget=FormatPanel(attrs={
134 widget=FormatPanel(attrs={
135 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
135 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
136 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
136 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
137 }),
137 }),
138 required=False, label=LABEL_TEXT)
138 required=False, label=LABEL_TEXT)
139 file = forms.FileField(required=False, label=_('File'),
139 file = forms.FileField(required=False, label=_('File'),
140 widget=forms.ClearableFileInput(
140 widget=forms.ClearableFileInput(
141 attrs={'accept': 'file/*'}))
141 attrs={'accept': 'file/*'}))
142 file_url = forms.CharField(required=False, label=_('File URL'),
142 file_url = forms.CharField(required=False, label=_('File URL'),
143 widget=forms.TextInput(
143 widget=forms.TextInput(
144 attrs={ATTRIBUTE_PLACEHOLDER:
144 attrs={ATTRIBUTE_PLACEHOLDER:
145 'http://example.com/image.png'}))
145 'http://example.com/image.png'}))
146
146
147 # This field is for spam prevention only
147 # This field is for spam prevention only
148 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
148 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
149 widget=forms.TextInput(attrs={
149 widget=forms.TextInput(attrs={
150 'class': 'form-email'}))
150 'class': 'form-email'}))
151 threads = forms.CharField(required=False, label=_('Additional threads'),
151 threads = forms.CharField(required=False, label=_('Additional threads'),
152 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
152 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
153 '123 456 789'}))
153 '123 456 789'}))
154
154
155 session = None
155 session = None
156 need_to_ban = False
156 need_to_ban = False
157
157
158 def clean_title(self):
158 def clean_title(self):
159 title = self.cleaned_data['title']
159 title = self.cleaned_data['title']
160 if title:
160 if title:
161 if len(title) > TITLE_MAX_LENGTH:
161 if len(title) > TITLE_MAX_LENGTH:
162 raise forms.ValidationError(_('Title must have less than %s '
162 raise forms.ValidationError(_('Title must have less than %s '
163 'characters') %
163 'characters') %
164 str(TITLE_MAX_LENGTH))
164 str(TITLE_MAX_LENGTH))
165 return title
165 return title
166
166
167 def clean_text(self):
167 def clean_text(self):
168 text = self.cleaned_data['text'].strip()
168 text = self.cleaned_data['text'].strip()
169 if text:
169 if text:
170 max_length = board_settings.get_int('Forms', 'MaxTextLength')
170 max_length = board_settings.get_int('Forms', 'MaxTextLength')
171 if len(text) > max_length:
171 if len(text) > max_length:
172 raise forms.ValidationError(_('Text must have less than %s '
172 raise forms.ValidationError(_('Text must have less than %s '
173 'characters') % str(max_length))
173 'characters') % str(max_length))
174 return text
174 return text
175
175
176 def clean_file(self):
176 def clean_file(self):
177 file = self.cleaned_data['file']
177 file = self.cleaned_data['file']
178
178
179 if file:
179 if file:
180 validate_file_size(file.size)
180 validate_file_size(file.size)
181
181
182 return file
182 return file
183
183
184 def clean_file_url(self):
184 def clean_file_url(self):
185 url = self.cleaned_data['file_url']
185 url = self.cleaned_data['file_url']
186
186
187 file = None
187 file = None
188 if url:
188 if url:
189 file = self._get_file_from_url(url)
189 file = self._get_file_from_url(url)
190
190
191 if not file:
191 if not file:
192 raise forms.ValidationError(_('Invalid URL'))
192 raise forms.ValidationError(_('Invalid URL'))
193 else:
193 else:
194 validate_file_size(file.size)
194 validate_file_size(file.size)
195
195
196 return file
196 return file
197
197
198 def clean_threads(self):
198 def clean_threads(self):
199 threads_str = self.cleaned_data['threads']
199 threads_str = self.cleaned_data['threads']
200
200
201 if len(threads_str) > 0:
201 if len(threads_str) > 0:
202 threads_id_list = threads_str.split(' ')
202 threads_id_list = threads_str.split(' ')
203
203
204 threads = list()
204 threads = list()
205
205
206 for thread_id in threads_id_list:
206 for thread_id in threads_id_list:
207 try:
207 try:
208 thread = Post.objects.get(id=int(thread_id))
208 thread = Post.objects.get(id=int(thread_id))
209 if not thread.is_opening() or thread.get_thread().archived:
209 if not thread.is_opening() or thread.get_thread().archived:
210 raise ObjectDoesNotExist()
210 raise ObjectDoesNotExist()
211 threads.append(thread)
211 threads.append(thread)
212 except (ObjectDoesNotExist, ValueError):
212 except (ObjectDoesNotExist, ValueError):
213 raise forms.ValidationError(_('Invalid additional thread list'))
213 raise forms.ValidationError(_('Invalid additional thread list'))
214
214
215 return threads
215 return threads
216
216
217 def clean(self):
217 def clean(self):
218 cleaned_data = super(PostForm, self).clean()
218 cleaned_data = super(PostForm, self).clean()
219
219
220 if cleaned_data['email']:
220 if cleaned_data['email']:
221 self.need_to_ban = True
221 self.need_to_ban = True
222 raise forms.ValidationError('A human cannot enter a hidden field')
222 raise forms.ValidationError('A human cannot enter a hidden field')
223
223
224 if not self.errors:
224 if not self.errors:
225 self._clean_text_file()
225 self._clean_text_file()
226
226
227 if not self.errors and self.session:
227 if not self.errors and self.session:
228 self._validate_posting_speed()
228 self._validate_posting_speed()
229
229
230 return cleaned_data
230 return cleaned_data
231
231
232 def get_file(self):
232 def get_file(self):
233 """
233 """
234 Gets file from form or URL.
234 Gets file from form or URL.
235 """
235 """
236
236
237 file = self.cleaned_data['file']
237 file = self.cleaned_data['file']
238 return file or self.cleaned_data['file_url']
238 return file or self.cleaned_data['file_url']
239
239
240 def get_tripcode(self):
240 def get_tripcode(self):
241 title = self.cleaned_data['title']
241 title = self.cleaned_data['title']
242 if title is not None and '#' in title:
242 if title is not None and '#' in title:
243 code = title.split('#', maxsplit=1)[1] + neboard.settings.SECRET_KEY
243 code = title.split('#', maxsplit=1)[1] + neboard.settings.SECRET_KEY
244 return hashlib.md5(code.encode()).hexdigest()
244 return hashlib.md5(code.encode()).hexdigest()
245
245
246 def get_title(self):
246 def get_title(self):
247 title = self.cleaned_data['title']
247 title = self.cleaned_data['title']
248 if title is not None and '#' in title:
248 if title is not None and '#' in title:
249 return title.split('#', maxsplit=1)[0]
249 return title.split('#', maxsplit=1)[0]
250 else:
250 else:
251 return title
251 return title
252
252
253 def _clean_text_file(self):
253 def _clean_text_file(self):
254 text = self.cleaned_data.get('text')
254 text = self.cleaned_data.get('text')
255 file = self.get_file()
255 file = self.get_file()
256
256
257 if (not text) and (not file):
257 if (not text) and (not file):
258 error_message = _('Either text or file must be entered.')
258 error_message = _('Either text or file must be entered.')
259 self._errors['text'] = self.error_class([error_message])
259 self._errors['text'] = self.error_class([error_message])
260
260
261 def _validate_posting_speed(self):
261 def _validate_posting_speed(self):
262 can_post = True
262 can_post = True
263
263
264 posting_delay = settings.POSTING_DELAY
264 posting_delay = settings.POSTING_DELAY
265
265
266 if board_settings.get_bool('Forms', 'LimitPostingSpeed'):
266 if board_settings.get_bool('Forms', 'LimitPostingSpeed'):
267 now = time.time()
267 now = time.time()
268
268
269 current_delay = 0
269 current_delay = 0
270
270
271 if LAST_POST_TIME not in self.session:
271 if LAST_POST_TIME not in self.session:
272 self.session[LAST_POST_TIME] = now
272 self.session[LAST_POST_TIME] = now
273
273
274 need_delay = True
274 need_delay = True
275 else:
275 else:
276 last_post_time = self.session.get(LAST_POST_TIME)
276 last_post_time = self.session.get(LAST_POST_TIME)
277 current_delay = int(now - last_post_time)
277 current_delay = int(now - last_post_time)
278
278
279 need_delay = current_delay < posting_delay
279 need_delay = current_delay < posting_delay
280
280
281 if need_delay:
281 if need_delay:
282 delay = posting_delay - current_delay
282 delay = posting_delay - current_delay
283 error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL,
283 error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL,
284 delay) % {'delay': delay}
284 delay) % {'delay': delay}
285 self._errors['text'] = self.error_class([error_message])
285 self._errors['text'] = self.error_class([error_message])
286
286
287 can_post = False
287 can_post = False
288
288
289 if can_post:
289 if can_post:
290 self.session[LAST_POST_TIME] = now
290 self.session[LAST_POST_TIME] = now
291
291
292 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
292 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
293 """
293 """
294 Gets an file file from URL.
294 Gets an file file from URL.
295 """
295 """
296
296
297 img_temp = None
297 img_temp = None
298
298
299 try:
299 try:
300 for downloader in Downloader.__subclasses__():
300 for downloader in Downloader.__subclasses__():
301 if downloader.handles(url):
301 if downloader.handles(url):
302 return downloader.download(url)
302 return downloader.download(url)
303 # If nobody of the specific downloaders handles this, use generic
303 # If nobody of the specific downloaders handles this, use generic
304 # one
304 # one
305 return Downloader.download(url)
305 return Downloader.download(url)
306 except forms.ValidationError as e:
306 except forms.ValidationError as e:
307 raise e
307 raise e
308 except Exception as e:
308 except Exception as e:
309 # Just return no file
309 # Just return no file
310 pass
310 pass
311
311
312
312
313 class ThreadForm(PostForm):
313 class ThreadForm(PostForm):
314
314
315 tags = forms.CharField(
315 tags = forms.CharField(
316 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
316 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
317 max_length=100, label=_('Tags'), required=True)
317 max_length=100, label=_('Tags'), required=True)
318
318
319 def clean_tags(self):
319 def clean_tags(self):
320 tags = self.cleaned_data['tags'].strip()
320 tags = self.cleaned_data['tags'].strip()
321
321
322 if not tags or not REGEX_TAGS.match(tags):
322 if not tags or not REGEX_TAGS.match(tags):
323 raise forms.ValidationError(
323 raise forms.ValidationError(
324 _('Inappropriate characters in tags.'))
324 _('Inappropriate characters in tags.'))
325
325
326 required_tag_exists = False
326 required_tag_exists = False
327 tag_set = set()
327 tag_set = set()
328 for tag_string in tags.split():
328 for tag_string in tags.split():
329 tag, created = Tag.objects.get_or_create(name=tag_string.strip().lower())
329 tag, created = Tag.objects.get_or_create(name=tag_string.strip().lower())
330 tag_set.add(tag)
330 tag_set.add(tag)
331
331
332 # If this is a new tag, don't check for its parents because nobody
332 # If this is a new tag, don't check for its parents because nobody
333 # added them yet
333 # added them yet
334 if not created:
334 if not created:
335 tag_set |= tag.get_all_parents()
335 tag_set |= set(tag.get_all_parents())
336
336
337 for tag in tag_set:
337 for tag in tag_set:
338 if tag.required:
338 if tag.required:
339 required_tag_exists = True
339 required_tag_exists = True
340 break
340 break
341
341
342 if not required_tag_exists:
342 if not required_tag_exists:
343 all_tags = Tag.objects.filter(required=True)
344 raise forms.ValidationError(
343 raise forms.ValidationError(
345 _('Need at least one section.'))
344 _('Need at least one section.'))
346
345
347 return tag_set
346 return tag_set
348
347
349 def clean(self):
348 def clean(self):
350 cleaned_data = super(ThreadForm, self).clean()
349 cleaned_data = super(ThreadForm, self).clean()
351
350
352 return cleaned_data
351 return cleaned_data
353
352
354
353
355 class SettingsForm(NeboardForm):
354 class SettingsForm(NeboardForm):
356
355
357 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
356 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
358 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('Image view mode'))
357 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('Image view mode'))
359 username = forms.CharField(label=_('User name'), required=False)
358 username = forms.CharField(label=_('User name'), required=False)
360 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
359 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
361
360
362 def clean_username(self):
361 def clean_username(self):
363 username = self.cleaned_data['username']
362 username = self.cleaned_data['username']
364
363
365 if username and not REGEX_TAGS.match(username):
364 if username and not REGEX_TAGS.match(username):
366 raise forms.ValidationError(_('Inappropriate characters.'))
365 raise forms.ValidationError(_('Inappropriate characters.'))
367
366
368 return username
367 return username
369
368
370
369
371 class SearchForm(NeboardForm):
370 class SearchForm(NeboardForm):
372 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
371 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
@@ -1,134 +1,134 b''
1 import hashlib
1 import hashlib
2 from django.template.loader import render_to_string
2 from django.template.loader import render_to_string
3 from django.db import models
3 from django.db import models
4 from django.db.models import Count
4 from django.db.models import Count
5 from django.core.urlresolvers import reverse
5 from django.core.urlresolvers import reverse
6
6
7 from boards.models.base import Viewable
7 from boards.models.base import Viewable
8 from boards.utils import cached_result
8 from boards.utils import cached_result
9 import boards
9 import boards
10
10
11 __author__ = 'neko259'
11 __author__ = 'neko259'
12
12
13
13
14 RELATED_TAGS_COUNT = 5
14 RELATED_TAGS_COUNT = 5
15
15
16
16
17 class TagManager(models.Manager):
17 class TagManager(models.Manager):
18
18
19 def get_not_empty_tags(self):
19 def get_not_empty_tags(self):
20 """
20 """
21 Gets tags that have non-archived threads.
21 Gets tags that have non-archived threads.
22 """
22 """
23
23
24 return self.annotate(num_threads=Count('thread_tags')).filter(num_threads__gt=0)\
24 return self.annotate(num_threads=Count('thread_tags')).filter(num_threads__gt=0)\
25 .order_by('-required', 'name')
25 .order_by('-required', 'name')
26
26
27 def get_tag_url_list(self, tags: list) -> str:
27 def get_tag_url_list(self, tags: list) -> str:
28 """
28 """
29 Gets a comma-separated list of tag links.
29 Gets a comma-separated list of tag links.
30 """
30 """
31
31
32 return ', '.join([tag.get_view() for tag in tags])
32 return ', '.join([tag.get_view() for tag in tags])
33
33
34
34
35 class Tag(models.Model, Viewable):
35 class Tag(models.Model, Viewable):
36 """
36 """
37 A tag is a text node assigned to the thread. The tag serves as a board
37 A tag is a text node assigned to the thread. The tag serves as a board
38 section. There can be multiple tags for each thread
38 section. There can be multiple tags for each thread
39 """
39 """
40
40
41 objects = TagManager()
41 objects = TagManager()
42
42
43 class Meta:
43 class Meta:
44 app_label = 'boards'
44 app_label = 'boards'
45 ordering = ('name',)
45 ordering = ('name',)
46
46
47 name = models.CharField(max_length=100, db_index=True, unique=True)
47 name = models.CharField(max_length=100, db_index=True, unique=True)
48 required = models.BooleanField(default=False, db_index=True)
48 required = models.BooleanField(default=False, db_index=True)
49 description = models.TextField(blank=True)
49 description = models.TextField(blank=True)
50
50
51 parent = models.ForeignKey('Tag', null=True, related_name='children')
51 parent = models.ForeignKey('Tag', null=True, related_name='children')
52
52
53 def __str__(self):
53 def __str__(self):
54 return self.name
54 return self.name
55
55
56 def is_empty(self) -> bool:
56 def is_empty(self) -> bool:
57 """
57 """
58 Checks if the tag has some threads.
58 Checks if the tag has some threads.
59 """
59 """
60
60
61 return self.get_thread_count() == 0
61 return self.get_thread_count() == 0
62
62
63 def get_thread_count(self, archived=None) -> int:
63 def get_thread_count(self, archived=None) -> int:
64 threads = self.get_threads()
64 threads = self.get_threads()
65 if archived is not None:
65 if archived is not None:
66 threads = threads.filter(archived=archived)
66 threads = threads.filter(archived=archived)
67 return threads.count()
67 return threads.count()
68
68
69 def get_active_thread_count(self) -> int:
69 def get_active_thread_count(self) -> int:
70 return self.get_thread_count(archived=False)
70 return self.get_thread_count(archived=False)
71
71
72 def get_absolute_url(self):
72 def get_absolute_url(self):
73 return reverse('tag', kwargs={'tag_name': self.name})
73 return reverse('tag', kwargs={'tag_name': self.name})
74
74
75 def get_threads(self):
75 def get_threads(self):
76 return self.thread_tags.order_by('-bump_time')
76 return self.thread_tags.order_by('-bump_time')
77
77
78 def is_required(self):
78 def is_required(self):
79 return self.required
79 return self.required
80
80
81 def get_view(self):
81 def get_view(self):
82 link = '<a class="tag" href="{}">{}</a>'.format(
82 link = '<a class="tag" href="{}">{}</a>'.format(
83 self.get_absolute_url(), self.name)
83 self.get_absolute_url(), self.name)
84 if self.is_required():
84 if self.is_required():
85 link = '<b>{}</b>'.format(link)
85 link = '<b>{}</b>'.format(link)
86 return link
86 return link
87
87
88 def get_search_view(self, *args, **kwargs):
88 def get_search_view(self, *args, **kwargs):
89 return render_to_string('boards/tag.html', {
89 return render_to_string('boards/tag.html', {
90 'tag': self,
90 'tag': self,
91 })
91 })
92
92
93 @cached_result()
93 @cached_result()
94 def get_post_count(self):
94 def get_post_count(self):
95 return self.get_threads().aggregate(num_posts=Count('multi_replies'))['num_posts']
95 return self.get_threads().aggregate(num_posts=Count('multi_replies'))['num_posts']
96
96
97 def get_description(self):
97 def get_description(self):
98 return self.description
98 return self.description
99
99
100 def get_random_image_post(self, archived=False):
100 def get_random_image_post(self, archived=False):
101 posts = boards.models.Post.objects.annotate(images_count=Count(
101 posts = boards.models.Post.objects.annotate(images_count=Count(
102 'images')).filter(images_count__gt=0, threads__tags__in=[self])
102 'images')).filter(images_count__gt=0, threads__tags__in=[self])
103 if archived is not None:
103 if archived is not None:
104 posts = posts.filter(thread__archived=archived)
104 posts = posts.filter(thread__archived=archived)
105 return posts.order_by('?').first()
105 return posts.order_by('?').first()
106
106
107 def get_first_letter(self):
107 def get_first_letter(self):
108 return self.name and self.name[0] or ''
108 return self.name and self.name[0] or ''
109
109
110 def get_related_tags(self):
110 def get_related_tags(self):
111 return set(Tag.objects.filter(thread_tags__in=self.get_threads()).exclude(
111 return set(Tag.objects.filter(thread_tags__in=self.get_threads()).exclude(
112 id=self.id).order_by('?')[:RELATED_TAGS_COUNT])
112 id=self.id).order_by('?')[:RELATED_TAGS_COUNT])
113
113
114 @cached_result()
114 @cached_result()
115 def get_color(self):
115 def get_color(self):
116 """
116 """
117 Gets color hashed from the tag name.
117 Gets color hashed from the tag name.
118 """
118 """
119 return hashlib.md5(self.name.encode()).hexdigest()[:6]
119 return hashlib.md5(self.name.encode()).hexdigest()[:6]
120
120
121 def get_parent(self):
121 def get_parent(self):
122 return self.parent
122 return self.parent
123
123
124 def get_all_parents(self):
124 def get_all_parents(self):
125 parents = set()
125 parents = list()
126 parent = self.get_parent()
126 parent = self.get_parent()
127 if parent and parent not in parents:
127 if parent and parent not in parents:
128 parents.add(parent)
128 parents.insert(0, parent)
129 parents |= parent.get_all_parents()
129 parents = parent.get_all_parents() + parents
130
130
131 return parents
131 return parents
132
132
133 def get_children(self):
133 def get_children(self):
134 return self.children
134 return self.children
@@ -1,187 +1,187 b''
1 {% extends "boards/base.html" %}
1 {% extends "boards/base.html" %}
2
2
3 {% load i18n %}
3 {% load i18n %}
4 {% load board %}
4 {% load board %}
5 {% load static %}
5 {% load static %}
6 {% load tz %}
6 {% load tz %}
7
7
8 {% block head %}
8 {% block head %}
9 <meta name="robots" content="noindex">
9 <meta name="robots" content="noindex">
10
10
11 {% if tag %}
11 {% if tag %}
12 <title>{{ tag.name }} - {{ site_name }}</title>
12 <title>{{ tag.name }} - {{ site_name }}</title>
13 {% else %}
13 {% else %}
14 <title>{{ site_name }}</title>
14 <title>{{ site_name }}</title>
15 {% endif %}
15 {% endif %}
16
16
17 {% if prev_page_link %}
17 {% if prev_page_link %}
18 <link rel="prev" href="{{ prev_page_link }}" />
18 <link rel="prev" href="{{ prev_page_link }}" />
19 {% endif %}
19 {% endif %}
20 {% if next_page_link %}
20 {% if next_page_link %}
21 <link rel="next" href="{{ next_page_link }}" />
21 <link rel="next" href="{{ next_page_link }}" />
22 {% endif %}
22 {% endif %}
23
23
24 {% endblock %}
24 {% endblock %}
25
25
26 {% block content %}
26 {% block content %}
27
27
28 {% get_current_language as LANGUAGE_CODE %}
28 {% get_current_language as LANGUAGE_CODE %}
29 {% get_current_timezone as TIME_ZONE %}
29 {% get_current_timezone as TIME_ZONE %}
30
30
31 {% for banner in banners %}
31 {% for banner in banners %}
32 <div class="post">
32 <div class="post">
33 <div class="title">{{ banner.title }}</div>
33 <div class="title">{{ banner.title }}</div>
34 <div>{{ banner.text }}</div>
34 <div>{{ banner.text }}</div>
35 <div>{% trans 'Related message' %}: <a href="{{ banner.post.get_absolute_url }}">>>{{ banner.post.id }}</a></div>
35 <div>{% trans 'Related message' %}: <a href="{{ banner.post.get_absolute_url }}">>>{{ banner.post.id }}</a></div>
36 </div>
36 </div>
37 {% endfor %}
37 {% endfor %}
38
38
39 {% if tag %}
39 {% if tag %}
40 <div class="tag_info" style="border-bottom: solid .5ex #{{ tag.get_color }}">
40 <div class="tag_info" style="border-bottom: solid .5ex #{{ tag.get_color }}">
41 {% if random_image_post %}
41 {% if random_image_post %}
42 <div class="tag-image">
42 <div class="tag-image">
43 {% with image=random_image_post.images.first %}
43 {% with image=random_image_post.images.first %}
44 <a href="{{ random_image_post.get_absolute_url }}"><img
44 <a href="{{ random_image_post.get_absolute_url }}"><img
45 src="{{ image.image.url_200x150 }}"
45 src="{{ image.image.url_200x150 }}"
46 width="{{ image.pre_width }}"
46 width="{{ image.pre_width }}"
47 height="{{ image.pre_height }}"/></a>
47 height="{{ image.pre_height }}"/></a>
48 {% endwith %}
48 {% endwith %}
49 </div>
49 </div>
50 {% endif %}
50 {% endif %}
51 <div class="tag-text-data">
51 <div class="tag-text-data">
52 <h2>
52 <h2>
53 <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form">
53 <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form">
54 {% if is_favorite %}
54 {% if is_favorite %}
55 <button name="method" value="unsubscribe" class="fav"></button>
55 <button name="method" value="unsubscribe" class="fav"></button>
56 {% else %}
56 {% else %}
57 <button name="method" value="subscribe" class="not_fav"></button>
57 <button name="method" value="subscribe" class="not_fav"></button>
58 {% endif %}
58 {% endif %}
59 </form>
59 </form>
60 <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form">
60 <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form">
61 {% if is_hidden %}
61 {% if is_hidden %}
62 <button name="method" value="unhide" class="fav">H</button>
62 <button name="method" value="unhide" class="fav">H</button>
63 {% else %}
63 {% else %}
64 <button name="method" value="hide" class="not_fav">H</button>
64 <button name="method" value="hide" class="not_fav">H</button>
65 {% endif %}
65 {% endif %}
66 </form>
66 </form>
67 {{ tag.get_view|safe }}
67 {{ tag.get_view|safe }}
68 {% if moderator %}
68 {% if moderator %}
69 <span class="moderator_info">| <a href="{% url 'admin:boards_tag_change' tag.id %}">{% trans 'Edit tag' %}</a></span>
69 <span class="moderator_info">| <a href="{% url 'admin:boards_tag_change' tag.id %}">{% trans 'Edit tag' %}</a></span>
70 {% endif %}
70 {% endif %}
71 </h2>
71 </h2>
72 {% if tag.get_description %}
72 {% if tag.get_description %}
73 <p>{{ tag.get_description|safe }}</p>
73 <p>{{ tag.get_description|safe }}</p>
74 {% endif %}
74 {% endif %}
75 <p>{% blocktrans with active_thread_count=tag.get_active_thread_count thread_count=tag.get_thread_count post_count=tag.get_post_count %}This tag has {{ thread_count }} threads ({{ active_thread_count}} active) and {{ post_count }} posts.{% endblocktrans %}</p>
75 <p>{% blocktrans with active_thread_count=tag.get_active_thread_count thread_count=tag.get_thread_count post_count=tag.get_post_count %}This tag has {{ thread_count }} threads ({{ active_thread_count}} active) and {{ post_count }} posts.{% endblocktrans %}</p>
76 {% if tag.get_parent %}
76 {% if tag.get_all_parents %}
77 <p>
77 <p>
78 {% if tag.get_parent %}
78 {% for parent in tag.get_all_parents %}
79 {{ tag.get_parent.get_view|safe }} /
79 {{ parent.get_view|safe }} &gt;
80 {% endif %}
80 {% endfor %}
81 {{ tag.get_view|safe }}
81 {{ tag.get_view|safe }}
82 </p>
82 </p>
83 {% endif %}
83 {% endif %}
84 </div>
84 </div>
85 </div>
85 </div>
86 {% endif %}
86 {% endif %}
87
87
88 {% if threads %}
88 {% if threads %}
89 {% if prev_page_link %}
89 {% if prev_page_link %}
90 <div class="page_link">
90 <div class="page_link">
91 <a href="{{ prev_page_link }}">{% trans "Previous page" %}</a>
91 <a href="{{ prev_page_link }}">{% trans "Previous page" %}</a>
92 </div>
92 </div>
93 {% endif %}
93 {% endif %}
94
94
95 {% for thread in threads %}
95 {% for thread in threads %}
96 <div class="thread">
96 <div class="thread">
97 {% post_view thread.get_opening_post moderator=moderator thread=thread truncated=True need_open_link=True %}
97 {% post_view thread.get_opening_post moderator=moderator thread=thread truncated=True need_open_link=True %}
98 {% if not thread.archived %}
98 {% if not thread.archived %}
99 {% with last_replies=thread.get_last_replies %}
99 {% with last_replies=thread.get_last_replies %}
100 {% if last_replies %}
100 {% if last_replies %}
101 {% with skipped_replies_count=thread.get_skipped_replies_count %}
101 {% with skipped_replies_count=thread.get_skipped_replies_count %}
102 {% if skipped_replies_count %}
102 {% if skipped_replies_count %}
103 <div class="skipped_replies">
103 <div class="skipped_replies">
104 <a href="{% url 'thread' thread.get_opening_post_id %}">
104 <a href="{% url 'thread' thread.get_opening_post_id %}">
105 {% blocktrans count count=skipped_replies_count %}Skipped {{ count }} reply. Open thread to see all replies.{% plural %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %}
105 {% blocktrans count count=skipped_replies_count %}Skipped {{ count }} reply. Open thread to see all replies.{% plural %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %}
106 </a>
106 </a>
107 </div>
107 </div>
108 {% endif %}
108 {% endif %}
109 {% endwith %}
109 {% endwith %}
110 <div class="last-replies">
110 <div class="last-replies">
111 {% for post in last_replies %}
111 {% for post in last_replies %}
112 {% post_view post moderator=moderator truncated=True %}
112 {% post_view post moderator=moderator truncated=True %}
113 {% endfor %}
113 {% endfor %}
114 </div>
114 </div>
115 {% endif %}
115 {% endif %}
116 {% endwith %}
116 {% endwith %}
117 {% endif %}
117 {% endif %}
118 </div>
118 </div>
119 {% endfor %}
119 {% endfor %}
120
120
121 {% if next_page_link %}
121 {% if next_page_link %}
122 <div class="page_link">
122 <div class="page_link">
123 <a href="{{ next_page_link }}">{% trans "Next page" %}</a>
123 <a href="{{ next_page_link }}">{% trans "Next page" %}</a>
124 </div>
124 </div>
125 {% endif %}
125 {% endif %}
126 {% else %}
126 {% else %}
127 <div class="post">
127 <div class="post">
128 {% trans 'No threads exist. Create the first one!' %}</div>
128 {% trans 'No threads exist. Create the first one!' %}</div>
129 {% endif %}
129 {% endif %}
130
130
131 <div class="post-form-w">
131 <div class="post-form-w">
132 <script src="{% static 'js/panel.js' %}"></script>
132 <script src="{% static 'js/panel.js' %}"></script>
133 <div class="post-form">
133 <div class="post-form">
134 <div class="form-title">{% trans "Create new thread" %}</div>
134 <div class="form-title">{% trans "Create new thread" %}</div>
135 <div class="swappable-form-full">
135 <div class="swappable-form-full">
136 <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %}
136 <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %}
137 {{ form.as_div }}
137 {{ form.as_div }}
138 <div class="form-submit">
138 <div class="form-submit">
139 <input type="submit" value="{% trans "Post" %}"/>
139 <input type="submit" value="{% trans "Post" %}"/>
140 <button id="preview-button" onclick="return false;">{% trans 'Preview' %}</button>
140 <button id="preview-button" onclick="return false;">{% trans 'Preview' %}</button>
141 </div>
141 </div>
142 </form>
142 </form>
143 </div>
143 </div>
144 <div>
144 <div>
145 {% trans 'Tags must be delimited by spaces. Text or image is required.' %}
145 {% trans 'Tags must be delimited by spaces. Text or image is required.' %}
146 </div>
146 </div>
147 <div id="preview-text"></div>
147 <div id="preview-text"></div>
148 <div><a href="{% url "staticpage" name="help" %}">{% trans 'Text syntax' %}</a></div>
148 <div><a href="{% url "staticpage" name="help" %}">{% trans 'Text syntax' %}</a></div>
149 <div><a href="{% url "tags" "required" %}">{% trans 'Tags' %}</a></div>
149 <div><a href="{% url "tags" "required" %}">{% trans 'Tags' %}</a></div>
150 </div>
150 </div>
151 </div>
151 </div>
152
152
153 <script src="{% static 'js/form.js' %}"></script>
153 <script src="{% static 'js/form.js' %}"></script>
154 <script src="{% static 'js/thread_create.js' %}"></script>
154 <script src="{% static 'js/thread_create.js' %}"></script>
155
155
156 {% endblock %}
156 {% endblock %}
157
157
158 {% block metapanel %}
158 {% block metapanel %}
159
159
160 <span class="metapanel">
160 <span class="metapanel">
161 <b><a href="{% url "authors" %}">{{ site_name }}</a> {{ version }}</b>
161 <b><a href="{% url "authors" %}">{{ site_name }}</a> {{ version }}</b>
162 {% trans "Pages:" %}
162 {% trans "Pages:" %}
163 [
163 [
164 {% with dividers=paginator.get_dividers %}
164 {% with dividers=paginator.get_dividers %}
165 {% for page in paginator.get_divided_range %}
165 {% for page in paginator.get_divided_range %}
166 {% if page in dividers %}
166 {% if page in dividers %}
167 …,
167 …,
168 {% endif %}
168 {% endif %}
169 <a
169 <a
170 {% ifequal page current_page.number %}
170 {% ifequal page current_page.number %}
171 class="current_page"
171 class="current_page"
172 {% endifequal %}
172 {% endifequal %}
173 href="
173 href="
174 {% if tag %}
174 {% if tag %}
175 {% url "tag" tag_name=tag.name %}?page={{ page }}
175 {% url "tag" tag_name=tag.name %}?page={{ page }}
176 {% else %}
176 {% else %}
177 {% url "index" %}?page={{ page }}
177 {% url "index" %}?page={{ page }}
178 {% endif %}
178 {% endif %}
179 ">{{ page }}</a>
179 ">{{ page }}</a>
180 {% if not forloop.last %},{% endif %}
180 {% if not forloop.last %},{% endif %}
181 {% endfor %}
181 {% endfor %}
182 {% endwith %}
182 {% endwith %}
183 ]
183 ]
184 [<a href="rss/">RSS</a>]
184 [<a href="rss/">RSS</a>]
185 </span>
185 </span>
186
186
187 {% endblock %}
187 {% endblock %}
General Comments 0
You need to be logged in to leave comments. Login now