##// END OF EJS Templates
Added file and url multi value field that will simplify adding many files to one post in future
neko259 -
r1752:4ca1286e default
parent child Browse files
Show More
@@ -0,0 +1,42 b''
1 from django import forms
2 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
3
4
5 ATTRIBUTE_PLACEHOLDER = 'placeholder'
6
7
8 class UrlFileWidget(forms.MultiWidget):
9 def __init__(self, *args, **kwargs):
10 widgets = (
11 forms.ClearableFileInput(attrs={'accept': 'file/*'}),
12 forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
13 'http://example.com/image.png'}),
14 )
15 super().__init__(widgets, *args, **kwargs)
16
17 def decompress(self, value):
18 return [None, None]
19
20
21 class UrlFileField(forms.MultiValueField):
22 widget = UrlFileWidget
23
24 def __init__(self, *args, **kwargs):
25 fields = (
26 forms.FileField(required=False, label=_('File'),
27 widget=forms.ClearableFileInput(
28 attrs={'accept': 'file/*'})),
29 forms.CharField(required=False, label=_('File URL'),
30 widget=forms.TextInput(
31 attrs={ATTRIBUTE_PLACEHOLDER:
32 'http://example.com/image.png'})),
33 )
34
35 super().__init__(
36 fields=fields,
37 require_all_fields=False, *args, **kwargs)
38
39 def compress(self, data_list):
40 if data_list and len(data_list) >= 2:
41 return data_list[0] or data_list[1]
42
@@ -1,478 +1,478 b''
1 import hashlib
1 import hashlib
2 import re
2 import re
3 import time
3 import time
4 import logging
4 import logging
5
5
6 import pytz
6 import pytz
7
7
8 from django import forms
8 from django import forms
9 from django.core.files.uploadedfile import SimpleUploadedFile
9 from django.core.files.uploadedfile import SimpleUploadedFile
10 from django.core.exceptions import ObjectDoesNotExist
10 from django.core.exceptions import ObjectDoesNotExist
11 from django.core.files.uploadedfile import UploadedFile
11 from django.forms.utils import ErrorList
12 from django.forms.utils import ErrorList
12 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
13 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
13 from django.utils import timezone
14 from django.utils import timezone
14
15
15 from boards.abstracts.settingsmanager import get_settings_manager
16 from boards.abstracts.settingsmanager import get_settings_manager
16 from boards.abstracts.attachment_alias import get_image_by_alias
17 from boards.abstracts.attachment_alias import get_image_by_alias
17 from boards.mdx_neboard import formatters
18 from boards.mdx_neboard import formatters
18 from boards.models.attachment.downloaders import download
19 from boards.models.attachment.downloaders import download
19 from boards.models.post import TITLE_MAX_LENGTH
20 from boards.models.post import TITLE_MAX_LENGTH
20 from boards.models import Tag, Post
21 from boards.models import Tag, Post
21 from boards.utils import validate_file_size, get_file_mimetype, \
22 from boards.utils import validate_file_size, get_file_mimetype, \
22 FILE_EXTENSION_DELIMITER
23 FILE_EXTENSION_DELIMITER
24 from boards.abstracts.fields import UrlFileField
23 from neboard import settings
25 from neboard import settings
24 import boards.settings as board_settings
26 import boards.settings as board_settings
25 import neboard
27 import neboard
26
28
27 POW_HASH_LENGTH = 16
29 POW_HASH_LENGTH = 16
28 POW_LIFE_MINUTES = 5
30 POW_LIFE_MINUTES = 5
29
31
30 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
32 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
31 REGEX_USERNAMES = re.compile(r'^[\w\s\d,]+$', re.UNICODE)
33 REGEX_USERNAMES = re.compile(r'^[\w\s\d,]+$', re.UNICODE)
32 REGEX_URL = re.compile(r'^(http|https|ftp|magnet):\/\/', re.UNICODE)
34 REGEX_URL = re.compile(r'^(http|https|ftp|magnet):\/\/', re.UNICODE)
33
35
34 VETERAN_POSTING_DELAY = 5
36 VETERAN_POSTING_DELAY = 5
35
37
36 ATTRIBUTE_PLACEHOLDER = 'placeholder'
38 ATTRIBUTE_PLACEHOLDER = 'placeholder'
37 ATTRIBUTE_ROWS = 'rows'
39 ATTRIBUTE_ROWS = 'rows'
38
40
39 LAST_POST_TIME = 'last_post_time'
41 LAST_POST_TIME = 'last_post_time'
40 LAST_LOGIN_TIME = 'last_login_time'
42 LAST_LOGIN_TIME = 'last_login_time'
41 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
43 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
42 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
44 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
43
45
44 LABEL_TITLE = _('Title')
46 LABEL_TITLE = _('Title')
45 LABEL_TEXT = _('Text')
47 LABEL_TEXT = _('Text')
46 LABEL_TAG = _('Tag')
48 LABEL_TAG = _('Tag')
47 LABEL_SEARCH = _('Search')
49 LABEL_SEARCH = _('Search')
48
50
49 ERROR_SPEED = 'Please wait %(delay)d second before sending message'
51 ERROR_SPEED = 'Please wait %(delay)d second before sending message'
50 ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message'
52 ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message'
51
53
52 TAG_MAX_LENGTH = 20
54 TAG_MAX_LENGTH = 20
53
55
54 TEXTAREA_ROWS = 4
56 TEXTAREA_ROWS = 4
55
57
56 TRIPCODE_DELIM = '#'
58 TRIPCODE_DELIM = '#'
57
59
58 # TODO Maybe this may be converted into the database table?
60 # TODO Maybe this may be converted into the database table?
59 MIMETYPE_EXTENSIONS = {
61 MIMETYPE_EXTENSIONS = {
60 'image/jpeg': 'jpeg',
62 'image/jpeg': 'jpeg',
61 'image/png': 'png',
63 'image/png': 'png',
62 'image/gif': 'gif',
64 'image/gif': 'gif',
63 'video/webm': 'webm',
65 'video/webm': 'webm',
64 'application/pdf': 'pdf',
66 'application/pdf': 'pdf',
65 'x-diff': 'diff',
67 'x-diff': 'diff',
66 'image/svg+xml': 'svg',
68 'image/svg+xml': 'svg',
67 'application/x-shockwave-flash': 'swf',
69 'application/x-shockwave-flash': 'swf',
68 'image/x-ms-bmp': 'bmp',
70 'image/x-ms-bmp': 'bmp',
69 'image/bmp': 'bmp',
71 'image/bmp': 'bmp',
70 }
72 }
71
73
72
74
73 logger = logging.getLogger('boards.forms')
75 logger = logging.getLogger('boards.forms')
74
76
75
77
76 def get_timezones():
78 def get_timezones():
77 timezones = []
79 timezones = []
78 for tz in pytz.common_timezones:
80 for tz in pytz.common_timezones:
79 timezones.append((tz, tz),)
81 timezones.append((tz, tz),)
80 return timezones
82 return timezones
81
83
82
84
83 class FormatPanel(forms.Textarea):
85 class FormatPanel(forms.Textarea):
84 """
86 """
85 Panel for text formatting. Consists of buttons to add different tags to the
87 Panel for text formatting. Consists of buttons to add different tags to the
86 form text area.
88 form text area.
87 """
89 """
88
90
89 def render(self, name, value, attrs=None):
91 def render(self, name, value, attrs=None):
90 output = '<div id="mark-panel">'
92 output = '<div id="mark-panel">'
91 for formatter in formatters:
93 for formatter in formatters:
92 output += '<span class="mark_btn"' + \
94 output += '<span class="mark_btn"' + \
93 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
95 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
94 '\', \'' + formatter.format_right + '\')">' + \
96 '\', \'' + formatter.format_right + '\')">' + \
95 formatter.preview_left + formatter.name + \
97 formatter.preview_left + formatter.name + \
96 formatter.preview_right + '</span>'
98 formatter.preview_right + '</span>'
97
99
98 output += '</div>'
100 output += '</div>'
99 output += super(FormatPanel, self).render(name, value, attrs=attrs)
101 output += super(FormatPanel, self).render(name, value, attrs=attrs)
100
102
101 return output
103 return output
102
104
103
105
104 class PlainErrorList(ErrorList):
106 class PlainErrorList(ErrorList):
105 def __unicode__(self):
107 def __unicode__(self):
106 return self.as_text()
108 return self.as_text()
107
109
108 def as_text(self):
110 def as_text(self):
109 return ''.join(['(!) %s ' % e for e in self])
111 return ''.join(['(!) %s ' % e for e in self])
110
112
111
113
112 class NeboardForm(forms.Form):
114 class NeboardForm(forms.Form):
113 """
115 """
114 Form with neboard-specific formatting.
116 Form with neboard-specific formatting.
115 """
117 """
116 required_css_class = 'required-field'
118 required_css_class = 'required-field'
117
119
118 def as_div(self):
120 def as_div(self):
119 """
121 """
120 Returns this form rendered as HTML <as_div>s.
122 Returns this form rendered as HTML <as_div>s.
121 """
123 """
122
124
123 return self._html_output(
125 return self._html_output(
124 # TODO Do not show hidden rows in the list here
126 # TODO Do not show hidden rows in the list here
125 normal_row='<div class="form-row">'
127 normal_row='<div class="form-row">'
126 '<div class="form-label">'
128 '<div class="form-label">'
127 '%(label)s'
129 '%(label)s'
128 '</div>'
130 '</div>'
129 '<div class="form-input">'
131 '<div class="form-input">'
130 '%(field)s'
132 '%(field)s'
131 '</div>'
133 '</div>'
132 '</div>'
134 '</div>'
133 '<div class="form-row">'
135 '<div class="form-row">'
134 '%(help_text)s'
136 '%(help_text)s'
135 '</div>',
137 '</div>',
136 error_row='<div class="form-row">'
138 error_row='<div class="form-row">'
137 '<div class="form-label"></div>'
139 '<div class="form-label"></div>'
138 '<div class="form-errors">%s</div>'
140 '<div class="form-errors">%s</div>'
139 '</div>',
141 '</div>',
140 row_ender='</div>',
142 row_ender='</div>',
141 help_text_html='%s',
143 help_text_html='%s',
142 errors_on_separate_row=True)
144 errors_on_separate_row=True)
143
145
144 def as_json_errors(self):
146 def as_json_errors(self):
145 errors = []
147 errors = []
146
148
147 for name, field in list(self.fields.items()):
149 for name, field in list(self.fields.items()):
148 if self[name].errors:
150 if self[name].errors:
149 errors.append({
151 errors.append({
150 'field': name,
152 'field': name,
151 'errors': self[name].errors.as_text(),
153 'errors': self[name].errors.as_text(),
152 })
154 })
153
155
154 return errors
156 return errors
155
157
156
158
157 class PostForm(NeboardForm):
159 class PostForm(NeboardForm):
158
160
159 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
161 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
160 label=LABEL_TITLE,
162 label=LABEL_TITLE,
161 widget=forms.TextInput(
163 widget=forms.TextInput(
162 attrs={ATTRIBUTE_PLACEHOLDER:
164 attrs={ATTRIBUTE_PLACEHOLDER:
163 'test#tripcode'}))
165 'test#tripcode'}))
164 text = forms.CharField(
166 text = forms.CharField(
165 widget=FormatPanel(attrs={
167 widget=FormatPanel(attrs={
166 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
168 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
167 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
169 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
168 }),
170 }),
169 required=False, label=LABEL_TEXT)
171 required=False, label=LABEL_TEXT)
170 file = forms.FileField(required=False, label=_('File'),
172 file = UrlFileField(required=False, label=_('File'))
171 widget=forms.ClearableFileInput(
172 attrs={'accept': 'file/*'}))
173 file_url = forms.CharField(required=False, label=_('File URL'),
174 widget=forms.TextInput(
175 attrs={ATTRIBUTE_PLACEHOLDER:
176 'http://example.com/image.png'}))
177
173
178 # This field is for spam prevention only
174 # This field is for spam prevention only
179 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
175 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
180 widget=forms.TextInput(attrs={
176 widget=forms.TextInput(attrs={
181 'class': 'form-email'}))
177 'class': 'form-email'}))
182 subscribe = forms.BooleanField(required=False, label=_('Subscribe to thread'))
178 subscribe = forms.BooleanField(required=False, label=_('Subscribe to thread'))
183
179
184 guess = forms.CharField(widget=forms.HiddenInput(), required=False)
180 guess = forms.CharField(widget=forms.HiddenInput(), required=False)
185 timestamp = forms.CharField(widget=forms.HiddenInput(), required=False)
181 timestamp = forms.CharField(widget=forms.HiddenInput(), required=False)
186 iteration = forms.CharField(widget=forms.HiddenInput(), required=False)
182 iteration = forms.CharField(widget=forms.HiddenInput(), required=False)
187
183
188 session = None
184 session = None
189 need_to_ban = False
185 need_to_ban = False
190 image = None
186 image = None
191
187
192 def _update_file_extension(self, file):
188 def _update_file_extension(self, file):
193 if file:
189 if file:
194 mimetype = get_file_mimetype(file)
190 mimetype = get_file_mimetype(file)
195 extension = MIMETYPE_EXTENSIONS.get(mimetype)
191 extension = MIMETYPE_EXTENSIONS.get(mimetype)
196 if extension:
192 if extension:
197 filename = file.name.split(FILE_EXTENSION_DELIMITER, 1)[0]
193 filename = file.name.split(FILE_EXTENSION_DELIMITER, 1)[0]
198 new_filename = filename + FILE_EXTENSION_DELIMITER + extension
194 new_filename = filename + FILE_EXTENSION_DELIMITER + extension
199
195
200 file.name = new_filename
196 file.name = new_filename
201 else:
197 else:
202 logger = logging.getLogger('boards.forms.extension')
198 logger = logging.getLogger('boards.forms.extension')
203
199
204 logger.info('Unrecognized file mimetype: {}'.format(mimetype))
200 logger.info('Unrecognized file mimetype: {}'.format(mimetype))
205
201
206 def clean_title(self):
202 def clean_title(self):
207 title = self.cleaned_data['title']
203 title = self.cleaned_data['title']
208 if title:
204 if title:
209 if len(title) > TITLE_MAX_LENGTH:
205 if len(title) > TITLE_MAX_LENGTH:
210 raise forms.ValidationError(_('Title must have less than %s '
206 raise forms.ValidationError(_('Title must have less than %s '
211 'characters') %
207 'characters') %
212 str(TITLE_MAX_LENGTH))
208 str(TITLE_MAX_LENGTH))
213 return title
209 return title
214
210
215 def clean_text(self):
211 def clean_text(self):
216 text = self.cleaned_data['text'].strip()
212 text = self.cleaned_data['text'].strip()
217 if text:
213 if text:
218 max_length = board_settings.get_int('Forms', 'MaxTextLength')
214 max_length = board_settings.get_int('Forms', 'MaxTextLength')
219 if len(text) > max_length:
215 if len(text) > max_length:
220 raise forms.ValidationError(_('Text must have less than %s '
216 raise forms.ValidationError(_('Text must have less than %s '
221 'characters') % str(max_length))
217 'characters') % str(max_length))
222 return text
218 return text
223
219
224 def clean_file(self):
220 def clean_file(self):
225 file = self.cleaned_data['file']
221 file = self.cleaned_data['file']
226
222
227 if file:
223 if isinstance(file, UploadedFile):
228 validate_file_size(file.size)
224 file = self._clean_file_file(file)
225 else:
226 file = self._clean_file_url(file)
227
228 return file
229
230 def _clean_file_file(self, file):
231 validate_file_size(file.size)
229 self._update_file_extension(file)
232 self._update_file_extension(file)
230
233
231 return file
234 return file
232
235
233 def clean_file_url(self):
234 url = self.cleaned_data['file_url']
235
236
237 def _clean_file_url(self, url):
236 file = None
238 file = None
237
239
238 if url:
240 if url:
239 try:
241 try:
240 file = get_image_by_alias(url, self.session)
242 file = get_image_by_alias(url, self.session)
241 self.image = file
243 self.image = file
242
244
243 if file is not None:
245 if file is not None:
244 return
246 return
245
247
246 if file is None:
248 if file is None:
247 file = self._get_file_from_url(url)
249 file = self._get_file_from_url(url)
248 if not file:
250 if not file:
249 raise forms.ValidationError(_('Invalid URL'))
251 raise forms.ValidationError(_('Invalid URL'))
250 else:
252 else:
251 validate_file_size(file.size)
253 validate_file_size(file.size)
252 self._update_file_extension(file)
254 self._update_file_extension(file)
253 except forms.ValidationError as e:
255 except forms.ValidationError as e:
254 # Assume we will get the plain URL instead of a file and save it
256 # Assume we will get the plain URL instead of a file and save it
255 if REGEX_URL.match(url):
257 if REGEX_URL.match(url):
256 logger.info('Error in forms: {}'.format(e))
258 logger.info('Error in forms: {}'.format(e))
257 return url
259 return url
258 else:
260 else:
259 raise e
261 raise e
260
262
261 return file
263 return file
262
264
263 def clean(self):
265 def clean(self):
264 cleaned_data = super(PostForm, self).clean()
266 cleaned_data = super(PostForm, self).clean()
265
267
266 if cleaned_data['email']:
268 if cleaned_data['email']:
267 if board_settings.get_bool('Forms', 'Autoban'):
269 if board_settings.get_bool('Forms', 'Autoban'):
268 self.need_to_ban = True
270 self.need_to_ban = True
269 raise forms.ValidationError('A human cannot enter a hidden field')
271 raise forms.ValidationError('A human cannot enter a hidden field')
270
272
271 if not self.errors:
273 if not self.errors:
272 self._clean_text_file()
274 self._clean_text_file()
273
275
274 limit_speed = board_settings.get_bool('Forms', 'LimitPostingSpeed')
276 limit_speed = board_settings.get_bool('Forms', 'LimitPostingSpeed')
275 limit_first = board_settings.get_bool('Forms', 'LimitFirstPosting')
277 limit_first = board_settings.get_bool('Forms', 'LimitFirstPosting')
276
278
277 settings_manager = get_settings_manager(self)
279 settings_manager = get_settings_manager(self)
278 if not self.errors and limit_speed or (limit_first and not settings_manager.get_setting('confirmed_user')):
280 if not self.errors and limit_speed or (limit_first and not settings_manager.get_setting('confirmed_user')):
279 pow_difficulty = board_settings.get_int('Forms', 'PowDifficulty')
281 pow_difficulty = board_settings.get_int('Forms', 'PowDifficulty')
280 if pow_difficulty > 0:
282 if pow_difficulty > 0:
281 # PoW-based
283 # PoW-based
282 if cleaned_data['timestamp'] \
284 if cleaned_data['timestamp'] \
283 and cleaned_data['iteration'] and cleaned_data['guess'] \
285 and cleaned_data['iteration'] and cleaned_data['guess'] \
284 and not settings_manager.get_setting('confirmed_user'):
286 and not settings_manager.get_setting('confirmed_user'):
285 self._validate_hash(cleaned_data['timestamp'], cleaned_data['iteration'], cleaned_data['guess'], cleaned_data['text'])
287 self._validate_hash(cleaned_data['timestamp'], cleaned_data['iteration'], cleaned_data['guess'], cleaned_data['text'])
286 else:
288 else:
287 # Time-based
289 # Time-based
288 self._validate_posting_speed()
290 self._validate_posting_speed()
289 settings_manager.set_setting('confirmed_user', True)
291 settings_manager.set_setting('confirmed_user', True)
290
292
291 return cleaned_data
293 return cleaned_data
292
294
293 def get_file(self):
295 def get_file(self):
294 """
296 """
295 Gets file from form or URL.
297 Gets file from form or URL.
296 """
298 """
297
299
298 file = self.cleaned_data['file']
300 file = self.cleaned_data['file']
299 if type(self.cleaned_data['file_url']) is not str:
301 if isinstance(file, UploadedFile):
300 file_url = self.cleaned_data['file_url']
302 return file
301 else:
302 file_url = None
303 return file or file_url
304
303
305 def get_file_url(self):
304 def get_file_url(self):
306 if not self.get_file():
305 file = self.cleaned_data['file']
307 return self.cleaned_data['file_url']
306 if type(file) == str:
307 return file
308
308
309 def get_tripcode(self):
309 def get_tripcode(self):
310 title = self.cleaned_data['title']
310 title = self.cleaned_data['title']
311 if title is not None and TRIPCODE_DELIM in title:
311 if title is not None and TRIPCODE_DELIM in title:
312 code = title.split(TRIPCODE_DELIM, maxsplit=1)[1] + neboard.settings.SECRET_KEY
312 code = title.split(TRIPCODE_DELIM, maxsplit=1)[1] + neboard.settings.SECRET_KEY
313 tripcode = hashlib.md5(code.encode()).hexdigest()
313 tripcode = hashlib.md5(code.encode()).hexdigest()
314 else:
314 else:
315 tripcode = ''
315 tripcode = ''
316 return tripcode
316 return tripcode
317
317
318 def get_title(self):
318 def get_title(self):
319 title = self.cleaned_data['title']
319 title = self.cleaned_data['title']
320 if title is not None and TRIPCODE_DELIM in title:
320 if title is not None and TRIPCODE_DELIM in title:
321 return title.split(TRIPCODE_DELIM, maxsplit=1)[0]
321 return title.split(TRIPCODE_DELIM, maxsplit=1)[0]
322 else:
322 else:
323 return title
323 return title
324
324
325 def get_images(self):
325 def get_images(self):
326 if self.image:
326 if self.image:
327 return [self.image]
327 return [self.image]
328 else:
328 else:
329 return []
329 return []
330
330
331 def is_subscribe(self):
331 def is_subscribe(self):
332 return self.cleaned_data['subscribe']
332 return self.cleaned_data['subscribe']
333
333
334 def _clean_text_file(self):
334 def _clean_text_file(self):
335 text = self.cleaned_data.get('text')
335 text = self.cleaned_data.get('text')
336 file = self.get_file()
336 file = self.get_file()
337 file_url = self.get_file_url()
337 file_url = self.get_file_url()
338 images = self.get_images()
338 images = self.get_images()
339
339
340 if (not text) and (not file) and (not file_url) and len(images) == 0:
340 if (not text) and (not file) and (not file_url) and len(images) == 0:
341 error_message = _('Either text or file must be entered.')
341 error_message = _('Either text or file must be entered.')
342 self._errors['text'] = self.error_class([error_message])
342 self._errors['text'] = self.error_class([error_message])
343
343
344 def _validate_posting_speed(self):
344 def _validate_posting_speed(self):
345 can_post = True
345 can_post = True
346
346
347 posting_delay = board_settings.get_int('Forms', 'PostingDelay')
347 posting_delay = board_settings.get_int('Forms', 'PostingDelay')
348
348
349 if board_settings.get_bool('Forms', 'LimitPostingSpeed'):
349 if board_settings.get_bool('Forms', 'LimitPostingSpeed'):
350 now = time.time()
350 now = time.time()
351
351
352 current_delay = 0
352 current_delay = 0
353
353
354 if LAST_POST_TIME not in self.session:
354 if LAST_POST_TIME not in self.session:
355 self.session[LAST_POST_TIME] = now
355 self.session[LAST_POST_TIME] = now
356
356
357 need_delay = True
357 need_delay = True
358 else:
358 else:
359 last_post_time = self.session.get(LAST_POST_TIME)
359 last_post_time = self.session.get(LAST_POST_TIME)
360 current_delay = int(now - last_post_time)
360 current_delay = int(now - last_post_time)
361
361
362 need_delay = current_delay < posting_delay
362 need_delay = current_delay < posting_delay
363
363
364 if need_delay:
364 if need_delay:
365 delay = posting_delay - current_delay
365 delay = posting_delay - current_delay
366 error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL,
366 error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL,
367 delay) % {'delay': delay}
367 delay) % {'delay': delay}
368 self._errors['text'] = self.error_class([error_message])
368 self._errors['text'] = self.error_class([error_message])
369
369
370 can_post = False
370 can_post = False
371
371
372 if can_post:
372 if can_post:
373 self.session[LAST_POST_TIME] = now
373 self.session[LAST_POST_TIME] = now
374
374
375 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
375 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
376 """
376 """
377 Gets an file file from URL.
377 Gets an file file from URL.
378 """
378 """
379
379
380 try:
380 try:
381 return download(url)
381 return download(url)
382 except forms.ValidationError as e:
382 except forms.ValidationError as e:
383 raise e
383 raise e
384 except Exception as e:
384 except Exception as e:
385 raise forms.ValidationError(e)
385 raise forms.ValidationError(e)
386
386
387 def _validate_hash(self, timestamp: str, iteration: str, guess: str, message: str):
387 def _validate_hash(self, timestamp: str, iteration: str, guess: str, message: str):
388 post_time = timezone.datetime.fromtimestamp(
388 post_time = timezone.datetime.fromtimestamp(
389 int(timestamp[:-3]), tz=timezone.get_current_timezone())
389 int(timestamp[:-3]), tz=timezone.get_current_timezone())
390
390
391 payload = timestamp + message.replace('\r\n', '\n')
391 payload = timestamp + message.replace('\r\n', '\n')
392 difficulty = board_settings.get_int('Forms', 'PowDifficulty')
392 difficulty = board_settings.get_int('Forms', 'PowDifficulty')
393 target = str(int(2 ** (POW_HASH_LENGTH * 3) / difficulty))
393 target = str(int(2 ** (POW_HASH_LENGTH * 3) / difficulty))
394 if len(target) < POW_HASH_LENGTH:
394 if len(target) < POW_HASH_LENGTH:
395 target = '0' * (POW_HASH_LENGTH - len(target)) + target
395 target = '0' * (POW_HASH_LENGTH - len(target)) + target
396
396
397 computed_guess = hashlib.sha256((payload + iteration).encode())\
397 computed_guess = hashlib.sha256((payload + iteration).encode())\
398 .hexdigest()[0:POW_HASH_LENGTH]
398 .hexdigest()[0:POW_HASH_LENGTH]
399 if guess != computed_guess or guess > target:
399 if guess != computed_guess or guess > target:
400 self._errors['text'] = self.error_class(
400 self._errors['text'] = self.error_class(
401 [_('Invalid PoW.')])
401 [_('Invalid PoW.')])
402
402
403
403
404
404
405 class ThreadForm(PostForm):
405 class ThreadForm(PostForm):
406
406
407 tags = forms.CharField(
407 tags = forms.CharField(
408 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
408 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
409 max_length=100, label=_('Tags'), required=True)
409 max_length=100, label=_('Tags'), required=True)
410 monochrome = forms.BooleanField(label=_('Monochrome'), required=False)
410 monochrome = forms.BooleanField(label=_('Monochrome'), required=False)
411
411
412 def clean_tags(self):
412 def clean_tags(self):
413 tags = self.cleaned_data['tags'].strip()
413 tags = self.cleaned_data['tags'].strip()
414
414
415 if not tags or not REGEX_TAGS.match(tags):
415 if not tags or not REGEX_TAGS.match(tags):
416 raise forms.ValidationError(
416 raise forms.ValidationError(
417 _('Inappropriate characters in tags.'))
417 _('Inappropriate characters in tags.'))
418
418
419 default_tag_name = board_settings.get('Forms', 'DefaultTag')\
419 default_tag_name = board_settings.get('Forms', 'DefaultTag')\
420 .strip().lower()
420 .strip().lower()
421
421
422 required_tag_exists = False
422 required_tag_exists = False
423 tag_set = set()
423 tag_set = set()
424 for tag_string in tags.split():
424 for tag_string in tags.split():
425 if tag_string.strip().lower() == default_tag_name:
425 if tag_string.strip().lower() == default_tag_name:
426 required_tag_exists = True
426 required_tag_exists = True
427 tag, created = Tag.objects.get_or_create(
427 tag, created = Tag.objects.get_or_create(
428 name=tag_string.strip().lower(), required=True)
428 name=tag_string.strip().lower(), required=True)
429 else:
429 else:
430 tag, created = Tag.objects.get_or_create(
430 tag, created = Tag.objects.get_or_create(
431 name=tag_string.strip().lower())
431 name=tag_string.strip().lower())
432 tag_set.add(tag)
432 tag_set.add(tag)
433
433
434 # If this is a new tag, don't check for its parents because nobody
434 # If this is a new tag, don't check for its parents because nobody
435 # added them yet
435 # added them yet
436 if not created:
436 if not created:
437 tag_set |= set(tag.get_all_parents())
437 tag_set |= set(tag.get_all_parents())
438
438
439 for tag in tag_set:
439 for tag in tag_set:
440 if tag.required:
440 if tag.required:
441 required_tag_exists = True
441 required_tag_exists = True
442 break
442 break
443
443
444 # Use default tag if no section exists
444 # Use default tag if no section exists
445 if not required_tag_exists:
445 if not required_tag_exists:
446 default_tag, created = Tag.objects.get_or_create(
446 default_tag, created = Tag.objects.get_or_create(
447 name=default_tag_name, required=True)
447 name=default_tag_name, required=True)
448 tag_set.add(default_tag)
448 tag_set.add(default_tag)
449
449
450 return tag_set
450 return tag_set
451
451
452 def clean(self):
452 def clean(self):
453 cleaned_data = super(ThreadForm, self).clean()
453 cleaned_data = super(ThreadForm, self).clean()
454
454
455 return cleaned_data
455 return cleaned_data
456
456
457 def is_monochrome(self):
457 def is_monochrome(self):
458 return self.cleaned_data['monochrome']
458 return self.cleaned_data['monochrome']
459
459
460
460
461 class SettingsForm(NeboardForm):
461 class SettingsForm(NeboardForm):
462
462
463 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
463 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
464 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('Image view mode'))
464 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('Image view mode'))
465 username = forms.CharField(label=_('User name'), required=False)
465 username = forms.CharField(label=_('User name'), required=False)
466 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
466 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
467
467
468 def clean_username(self):
468 def clean_username(self):
469 username = self.cleaned_data['username']
469 username = self.cleaned_data['username']
470
470
471 if username and not REGEX_USERNAMES.match(username):
471 if username and not REGEX_USERNAMES.match(username):
472 raise forms.ValidationError(_('Inappropriate characters.'))
472 raise forms.ValidationError(_('Inappropriate characters.'))
473
473
474 return username
474 return username
475
475
476
476
477 class SearchForm(NeboardForm):
477 class SearchForm(NeboardForm):
478 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
478 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
@@ -1,208 +1,207 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.get_text|safe }}</div>
34 <div>{{ banner.get_text|safe }}</div>
35 <div>{% trans 'Details' %}: <a href="{{ banner.post.get_absolute_url }}">>>{{ banner.post.id }}</a></div>
35 <div>{% trans 'Details' %}: <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.get_first_image %}
43 {% with image=random_image_post.get_first_image %}
44 <a href="{{ random_image_post.get_absolute_url }}"><img
44 <a href="{{ random_image_post.get_absolute_url }}"><img
45 src="{{ image.get_thumb_url }}"
45 src="{{ image.get_thumb_url }}"
46 width="{{ image.get_preview_size.0 }}"
46 width="{{ image.get_preview_size.0 }}"
47 height="{{ image.get_preview_size.1 }}"
47 height="{{ image.get_preview_size.1 }}"
48 alt="{{ random_image_post.id }}"/></a>
48 alt="{{ random_image_post.id }}"/></a>
49 {% endwith %}
49 {% endwith %}
50 </div>
50 </div>
51 {% endif %}
51 {% endif %}
52 <div class="tag-text-data">
52 <div class="tag-text-data">
53 <h2>
53 <h2>
54 /{{ tag.get_view|safe }}/
54 /{{ tag.get_view|safe }}/
55 </h2>
55 </h2>
56 {% if perms.change_tag %}
56 {% if perms.change_tag %}
57 <div class="moderator_info"><a href="{% url 'admin:boards_tag_change' tag.id %}">{% trans 'Edit tag' %}</a></div>
57 <div class="moderator_info"><a href="{% url 'admin:boards_tag_change' tag.id %}">{% trans 'Edit tag' %}</a></div>
58 {% endif %}
58 {% endif %}
59 <p>
59 <p>
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_favorite %}
61 {% if is_favorite %}
62 <button name="method" value="unsubscribe" class="fav">β˜… {% trans "Remove from favorites" %}</button>
62 <button name="method" value="unsubscribe" class="fav">β˜… {% trans "Remove from favorites" %}</button>
63 {% else %}
63 {% else %}
64 <button name="method" value="subscribe" class="not_fav">β˜… {% trans "Add to favorites" %}</button>
64 <button name="method" value="subscribe" class="not_fav">β˜… {% trans "Add to favorites" %}</button>
65 {% endif %}
65 {% endif %}
66 </form>
66 </form>
67 <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form">
67 <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form">
68 {% if is_hidden %}
68 {% if is_hidden %}
69 <button name="method" value="unhide" class="fav">{% trans "Show" %}</button>
69 <button name="method" value="unhide" class="fav">{% trans "Show" %}</button>
70 {% else %}
70 {% else %}
71 <button name="method" value="hide" class="not_fav">{% trans "Hide" %}</button>
71 <button name="method" value="hide" class="not_fav">{% trans "Hide" %}</button>
72 {% endif %}
72 {% endif %}
73 </form>
73 </form>
74 <a href="{% url 'tag_gallery' tag.name %}">{% trans 'Gallery' %}</a>
74 <a href="{% url 'tag_gallery' tag.name %}">{% trans 'Gallery' %}</a>
75 </p>
75 </p>
76 {% if tag.get_description %}
76 {% if tag.get_description %}
77 <p>{{ tag.get_description|safe }}</p>
77 <p>{{ tag.get_description|safe }}</p>
78 {% endif %}
78 {% endif %}
79 <p>
79 <p>
80 {% with active_count=tag.get_active_thread_count bumplimit_count=tag.get_bumplimit_thread_count archived_count=tag.get_archived_thread_count %}
80 {% with active_count=tag.get_active_thread_count bumplimit_count=tag.get_bumplimit_thread_count archived_count=tag.get_archived_thread_count %}
81 {% if active_count %}
81 {% if active_count %}
82 ● {{ active_count }}&ensp;
82 ● {{ active_count }}&ensp;
83 {% endif %}
83 {% endif %}
84 {% if bumplimit_count %}
84 {% if bumplimit_count %}
85 ◍ {{ bumplimit_count }}&ensp;
85 ◍ {{ bumplimit_count }}&ensp;
86 {% endif %}
86 {% endif %}
87 {% if archived_count %}
87 {% if archived_count %}
88 β—‹ {{ archived_count }}&ensp;
88 β—‹ {{ archived_count }}&ensp;
89 {% endif %}
89 {% endif %}
90 {% endwith %}
90 {% endwith %}
91 β™₯ {{ tag.get_post_count }}
91 β™₯ {{ tag.get_post_count }}
92 </p>
92 </p>
93 {% if tag.get_all_parents %}
93 {% if tag.get_all_parents %}
94 <p>
94 <p>
95 {% for parent in tag.get_all_parents %}
95 {% for parent in tag.get_all_parents %}
96 {{ parent.get_view|safe }} &gt;
96 {{ parent.get_view|safe }} &gt;
97 {% endfor %}
97 {% endfor %}
98 {{ tag.get_view|safe }}
98 {{ tag.get_view|safe }}
99 </p>
99 </p>
100 {% endif %}
100 {% endif %}
101 {% if tag.get_children.all %}
101 {% if tag.get_children.all %}
102 <p>
102 <p>
103 {% trans "Subsections: " %}
103 {% trans "Subsections: " %}
104 {% for child in tag.get_children.all %}
104 {% for child in tag.get_children.all %}
105 {{ child.get_view|safe }}{% if not forloop.last%}, {% endif %}
105 {{ child.get_view|safe }}{% if not forloop.last%}, {% endif %}
106 {% endfor %}
106 {% endfor %}
107 </p>
107 </p>
108 {% endif %}
108 {% endif %}
109 </div>
109 </div>
110 </div>
110 </div>
111 {% endif %}
111 {% endif %}
112
112
113 {% if threads %}
113 {% if threads %}
114 {% if prev_page_link %}
114 {% if prev_page_link %}
115 <div class="page_link">
115 <div class="page_link">
116 <a href="{{ prev_page_link }}">&lt;&lt; {% trans "Previous page" %} &lt;&lt;</a>
116 <a href="{{ prev_page_link }}">&lt;&lt; {% trans "Previous page" %} &lt;&lt;</a>
117 </div>
117 </div>
118 {% endif %}
118 {% endif %}
119
119
120 {% for thread in threads %}
120 {% for thread in threads %}
121 <div class="thread">
121 <div class="thread">
122 {% post_view thread.get_opening_post thread=thread truncated=True need_open_link=True %}
122 {% post_view thread.get_opening_post thread=thread truncated=True need_open_link=True %}
123 {% if not thread.archived %}
123 {% if not thread.archived %}
124 {% with last_replies=thread.get_last_replies %}
124 {% with last_replies=thread.get_last_replies %}
125 {% if last_replies %}
125 {% if last_replies %}
126 {% with skipped_replies_count=thread.get_skipped_replies_count %}
126 {% with skipped_replies_count=thread.get_skipped_replies_count %}
127 {% if skipped_replies_count %}
127 {% if skipped_replies_count %}
128 <div class="skipped_replies">
128 <div class="skipped_replies">
129 <a href="{% url 'thread' thread.get_opening_post_id %}">
129 <a href="{% url 'thread' thread.get_opening_post_id %}">
130 {% 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 %}
130 {% 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 %}
131 </a>
131 </a>
132 </div>
132 </div>
133 {% endif %}
133 {% endif %}
134 {% endwith %}
134 {% endwith %}
135 <div class="last-replies">
135 <div class="last-replies">
136 {% for post in last_replies %}
136 {% for post in last_replies %}
137 {% post_view post truncated=True %}
137 {% post_view post truncated=True %}
138 {% endfor %}
138 {% endfor %}
139 </div>
139 </div>
140 {% endif %}
140 {% endif %}
141 {% endwith %}
141 {% endwith %}
142 {% endif %}
142 {% endif %}
143 </div>
143 </div>
144 {% endfor %}
144 {% endfor %}
145
145
146 {% if next_page_link %}
146 {% if next_page_link %}
147 <div class="page_link">
147 <div class="page_link">
148 <a href="{{ next_page_link }}">&gt;&gt; {% trans "Next page" %} &gt;&gt;</a>
148 <a href="{{ next_page_link }}">&gt;&gt; {% trans "Next page" %} &gt;&gt;</a>
149 </div>
149 </div>
150 {% endif %}
150 {% endif %}
151 {% else %}
151 {% else %}
152 <div class="post">
152 <div class="post">
153 {% trans 'No threads exist. Create the first one!' %}</div>
153 {% trans 'No threads exist. Create the first one!' %}</div>
154 {% endif %}
154 {% endif %}
155
155
156 <div class="post-form-w">
156 <div class="post-form-w">
157 <script src="{% static 'js/panel.js' %}"></script>
157 <script src="{% static 'js/panel.js' %}"></script>
158 <div class="post-form" data-hasher="{% static 'js/3party/sha256.js' %}"
158 <div class="post-form" data-hasher="{% static 'js/3party/sha256.js' %}"
159 data-pow-script="{% static 'js/proof_of_work.js' %}">
159 data-pow-script="{% static 'js/proof_of_work.js' %}">
160 <div class="form-title">{% trans "Create new thread" %}</div>
160 <div class="form-title">{% trans "Create new thread" %}</div>
161 <div class="swappable-form-full">
161 <div class="swappable-form-full">
162 <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %}
162 <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %}
163 {{ form.as_div }}
163 {{ form.as_div }}
164 <div class="form-submit">
164 <div class="form-submit">
165 <input type="submit" value="{% trans "Post" %}"/>
165 <input type="submit" value="{% trans "Post" %}"/>
166 <button id="preview-button" type="button" onclick="return false;">{% trans 'Preview' %}</button>
166 <button id="preview-button" type="button" onclick="return false;">{% trans 'Preview' %}</button>
167 <button id="file-source-button" type="button" onclick="return false;">{% trans 'Change file source' %}</button>
168 </div>
167 </div>
169 </form>
168 </form>
170 </div>
169 </div>
171 <div>
170 <div>
172 {% trans 'Tags must be delimited by spaces. Text or image is required.' %}
171 {% trans 'Tags must be delimited by spaces. Text or image is required.' %}
173 {% with size=max_file_size|filesizeformat %}
172 {% with size=max_file_size|filesizeformat %}
174 {% blocktrans %}Max file size is {{ size }}.{% endblocktrans %}
173 {% blocktrans %}Max file size is {{ size }}.{% endblocktrans %}
175 {% endwith %}
174 {% endwith %}
176 </div>
175 </div>
177 <div id="preview-text"></div>
176 <div id="preview-text"></div>
178 <div><a href="{% url "staticpage" name="help" %}">{% trans 'Text syntax' %}</a></div>
177 <div><a href="{% url "staticpage" name="help" %}">{% trans 'Text syntax' %}</a></div>
179 </div>
178 </div>
180 </div>
179 </div>
181
180
182 <script src="{% static 'js/form.js' %}"></script>
181 <script src="{% static 'js/form.js' %}"></script>
183 <script src="{% static 'js/3party/jquery.blockUI.js' %}"></script>
182 <script src="{% static 'js/3party/jquery.blockUI.js' %}"></script>
184 <script src="{% static 'js/thread_create.js' %}"></script>
183 <script src="{% static 'js/thread_create.js' %}"></script>
185
184
186 {% endblock %}
185 {% endblock %}
187
186
188 {% block metapanel %}
187 {% block metapanel %}
189
188
190 <span class="metapanel">
189 <span class="metapanel">
191 {% trans "Pages:" %}
190 {% trans "Pages:" %}
192 [
191 [
193 {% with dividers=paginator.get_dividers %}
192 {% with dividers=paginator.get_dividers %}
194 {% for page in paginator.get_divided_range %}
193 {% for page in paginator.get_divided_range %}
195 {% if page in dividers %}
194 {% if page in dividers %}
196 …,
195 …,
197 {% endif %}
196 {% endif %}
198 <a
197 <a
199 {% ifequal page current_page.number %}
198 {% ifequal page current_page.number %}
200 class="current_page"
199 class="current_page"
201 {% endifequal %}
200 {% endifequal %}
202 href="{% page_url paginator page %}">{{ page }}</a>{% if not forloop.last %},{% endif %}
201 href="{% page_url paginator page %}">{{ page }}</a>{% if not forloop.last %},{% endif %}
203 {% endfor %}
202 {% endfor %}
204 {% endwith %}
203 {% endwith %}
205 ]
204 ]
206 </span>
205 </span>
207
206
208 {% endblock %}
207 {% endblock %}
@@ -1,81 +1,80 b''
1 {% extends "boards/thread.html" %}
1 {% extends "boards/thread.html" %}
2
2
3 {% load i18n %}
3 {% load i18n %}
4 {% load static from staticfiles %}
4 {% load static from staticfiles %}
5 {% load board %}
5 {% load board %}
6 {% load tz %}
6 {% load tz %}
7
7
8 {% block thread_content %}
8 {% block thread_content %}
9 {% get_current_language as LANGUAGE_CODE %}
9 {% get_current_language as LANGUAGE_CODE %}
10 {% get_current_timezone as TIME_ZONE %}
10 {% get_current_timezone as TIME_ZONE %}
11
11
12 <div id="quote-button">{% trans 'Quote' %}</div>
12 <div id="quote-button">{% trans 'Quote' %}</div>
13
13
14 <div class="tag_info">
14 <div class="tag_info">
15 <h2>
15 <h2>
16 <form action="{% url 'thread' opening_post.id %}" method="post" class="post-button-form">
16 <form action="{% url 'thread' opening_post.id %}" method="post" class="post-button-form">
17 {% csrf_token %}
17 {% csrf_token %}
18 {% if is_favorite %}
18 {% if is_favorite %}
19 <button name="method" value="unsubscribe" class="fav">β˜…</button>
19 <button name="method" value="unsubscribe" class="fav">β˜…</button>
20 {% else %}
20 {% else %}
21 <button name="method" value="subscribe" class="not_fav">β˜…</button>
21 <button name="method" value="subscribe" class="not_fav">β˜…</button>
22 {% endif %}
22 {% endif %}
23 </form>
23 </form>
24 {{ opening_post.get_title_or_text }}
24 {{ opening_post.get_title_or_text }}
25 </h2>
25 </h2>
26 </div>
26 </div>
27
27
28 {% if bumpable and thread.has_post_limit %}
28 {% if bumpable and thread.has_post_limit %}
29 <div class="bar-bg">
29 <div class="bar-bg">
30 <div class="bar-value" style="width:{{ bumplimit_progress }}%" id="bumplimit_progress">
30 <div class="bar-value" style="width:{{ bumplimit_progress }}%" id="bumplimit_progress">
31 </div>
31 </div>
32 <div class="bar-text">
32 <div class="bar-text">
33 <span id="left_to_limit">{{ posts_left }}</span> {% trans 'posts to bumplimit' %}
33 <span id="left_to_limit">{{ posts_left }}</span> {% trans 'posts to bumplimit' %}
34 </div>
34 </div>
35 </div>
35 </div>
36 {% endif %}
36 {% endif %}
37
37
38 <div class="thread">
38 <div class="thread">
39 {% for post in thread.get_viewable_replies %}
39 {% for post in thread.get_viewable_replies %}
40 {% post_view post reply_link=True thread=thread %}
40 {% post_view post reply_link=True thread=thread %}
41 {% endfor %}
41 {% endfor %}
42 </div>
42 </div>
43
43
44 {% if not thread.is_archived %}
44 {% if not thread.is_archived %}
45 <div class="post-form-w">
45 <div class="post-form-w">
46 <script src="{% static 'js/panel.js' %}"></script>
46 <script src="{% static 'js/panel.js' %}"></script>
47 <div class="form-title">{% trans "Reply to thread" %} #{{ opening_post.id }}<span class="reply-to-message"> {% trans "to message " %} #<span id="reply-to-message-id"></span></span></div>
47 <div class="form-title">{% trans "Reply to thread" %} #{{ opening_post.id }}<span class="reply-to-message"> {% trans "to message " %} #<span id="reply-to-message-id"></span></span></div>
48 <div class="post-form" id="compact-form" data-hasher="{% static 'js/3party/sha256.js' %}"
48 <div class="post-form" id="compact-form" data-hasher="{% static 'js/3party/sha256.js' %}"
49 data-pow-script="{% static 'js/proof_of_work.js' %}">
49 data-pow-script="{% static 'js/proof_of_work.js' %}">
50 <div class="swappable-form-full">
50 <div class="swappable-form-full">
51 <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %}
51 <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %}
52 <div class="compact-form-text"></div>
52 <div class="compact-form-text"></div>
53 {{ form.as_div }}
53 {{ form.as_div }}
54 <div class="form-submit">
54 <div class="form-submit">
55 <input type="submit" value="{% trans "Post" %}"/>
55 <input type="submit" value="{% trans "Post" %}"/>
56 <button id="preview-button" type="button" onclick="return false;">{% trans 'Preview' %}</button>
56 <button id="preview-button" type="button" onclick="return false;">{% trans 'Preview' %}</button>
57 <button id="file-source-button" type="button" onclick="return false;">{% trans 'Change file source' %}</button>
58 </div>
57 </div>
59 </form>
58 </form>
60 </div>
59 </div>
61 <div id="preview-text"></div>
60 <div id="preview-text"></div>
62 <div>
61 <div>
63 {% with size=max_file_size|filesizeformat %}
62 {% with size=max_file_size|filesizeformat %}
64 {% blocktrans %}Max file size is {{ size }}.{% endblocktrans %}
63 {% blocktrans %}Max file size is {{ size }}.{% endblocktrans %}
65 {% endwith %}
64 {% endwith %}
66 </div>
65 </div>
67 <div><a href="{% url "staticpage" name="help" %}">
66 <div><a href="{% url "staticpage" name="help" %}">
68 {% trans 'Text syntax' %}</a></div>
67 {% trans 'Text syntax' %}</a></div>
69 <div><a id="form-close-button" href="#" onClick="resetFormPosition(); return false;">{% trans 'Close form' %}</a></div>
68 <div><a id="form-close-button" href="#" onClick="resetFormPosition(); return false;">{% trans 'Close form' %}</a></div>
70 </div>
69 </div>
71 </div>
70 </div>
72
71
73 <script src="{% static 'js/form.js' %}"></script>
72 <script src="{% static 'js/form.js' %}"></script>
74 <script src="{% static 'js/jquery.form.min.js' %}"></script>
73 <script src="{% static 'js/jquery.form.min.js' %}"></script>
75 <script src="{% static 'js/3party/jquery.blockUI.js' %}"></script>
74 <script src="{% static 'js/3party/jquery.blockUI.js' %}"></script>
76 <script src="{% static 'js/thread.js' %}"></script>
75 <script src="{% static 'js/thread.js' %}"></script>
77 <script src="{% static 'js/thread_update.js' %}"></script>
76 <script src="{% static 'js/thread_update.js' %}"></script>
78 {% endif %}
77 {% endif %}
79
78
80 <script src="{% static 'js/3party/centrifuge.js' %}"></script>
79 <script src="{% static 'js/3party/centrifuge.js' %}"></script>
81 {% endblock %}
80 {% endblock %}
General Comments 0
You need to be logged in to leave comments. Login now