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