##// END OF EJS Templates
Recognize svg mimetype
neko259 -
r1373:a110319d default
parent child Browse files
Show More
@@ -1,404 +1,405 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 import pytz
5 import pytz
6
6
7 from django import forms
7 from django import forms
8 from django.core.files.uploadedfile import SimpleUploadedFile
8 from django.core.files.uploadedfile import SimpleUploadedFile
9 from django.core.exceptions import ObjectDoesNotExist
9 from django.core.exceptions import ObjectDoesNotExist
10 from django.forms.util import ErrorList
10 from django.forms.util import ErrorList
11 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
11 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
12
12
13 from boards.mdx_neboard import formatters
13 from boards.mdx_neboard import formatters
14 from boards.models.attachment.downloaders import Downloader
14 from boards.models.attachment.downloaders import Downloader
15 from boards.models.post import TITLE_MAX_LENGTH
15 from boards.models.post import TITLE_MAX_LENGTH
16 from boards.models import Tag, Post
16 from boards.models import Tag, Post
17 from boards.utils import validate_file_size, get_file_mimetype, \
17 from boards.utils import validate_file_size, get_file_mimetype, \
18 FILE_EXTENSION_DELIMITER
18 FILE_EXTENSION_DELIMITER
19 from neboard import settings
19 from neboard import settings
20 import boards.settings as board_settings
20 import boards.settings as board_settings
21 import neboard
21 import neboard
22
22
23 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
23 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
24
24
25 VETERAN_POSTING_DELAY = 5
25 VETERAN_POSTING_DELAY = 5
26
26
27 ATTRIBUTE_PLACEHOLDER = 'placeholder'
27 ATTRIBUTE_PLACEHOLDER = 'placeholder'
28 ATTRIBUTE_ROWS = 'rows'
28 ATTRIBUTE_ROWS = 'rows'
29
29
30 LAST_POST_TIME = 'last_post_time'
30 LAST_POST_TIME = 'last_post_time'
31 LAST_LOGIN_TIME = 'last_login_time'
31 LAST_LOGIN_TIME = 'last_login_time'
32 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
32 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
33 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
33 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
34
34
35 LABEL_TITLE = _('Title')
35 LABEL_TITLE = _('Title')
36 LABEL_TEXT = _('Text')
36 LABEL_TEXT = _('Text')
37 LABEL_TAG = _('Tag')
37 LABEL_TAG = _('Tag')
38 LABEL_SEARCH = _('Search')
38 LABEL_SEARCH = _('Search')
39
39
40 ERROR_SPEED = 'Please wait %(delay)d second before sending message'
40 ERROR_SPEED = 'Please wait %(delay)d second before sending message'
41 ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message'
41 ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message'
42
42
43 TAG_MAX_LENGTH = 20
43 TAG_MAX_LENGTH = 20
44
44
45 TEXTAREA_ROWS = 4
45 TEXTAREA_ROWS = 4
46
46
47 TRIPCODE_DELIM = '#'
47 TRIPCODE_DELIM = '#'
48
48
49 # TODO Maybe this may be converted into the database table?
49 # TODO Maybe this may be converted into the database table?
50 MIMETYPE_EXTENSIONS = {
50 MIMETYPE_EXTENSIONS = {
51 'image/jpeg': 'jpeg',
51 'image/jpeg': 'jpeg',
52 'image/png': 'png',
52 'image/png': 'png',
53 'image/gif': 'gif',
53 'image/gif': 'gif',
54 'video/webm': 'webm',
54 'video/webm': 'webm',
55 'application/pdf': 'pdf',
55 'application/pdf': 'pdf',
56 'x-diff': 'diff',
56 'x-diff': 'diff',
57 'image/svg+xml': 'svg',
57 }
58 }
58
59
59
60
60 def get_timezones():
61 def get_timezones():
61 timezones = []
62 timezones = []
62 for tz in pytz.common_timezones:
63 for tz in pytz.common_timezones:
63 timezones.append((tz, tz),)
64 timezones.append((tz, tz),)
64 return timezones
65 return timezones
65
66
66
67
67 class FormatPanel(forms.Textarea):
68 class FormatPanel(forms.Textarea):
68 """
69 """
69 Panel for text formatting. Consists of buttons to add different tags to the
70 Panel for text formatting. Consists of buttons to add different tags to the
70 form text area.
71 form text area.
71 """
72 """
72
73
73 def render(self, name, value, attrs=None):
74 def render(self, name, value, attrs=None):
74 output = '<div id="mark-panel">'
75 output = '<div id="mark-panel">'
75 for formatter in formatters:
76 for formatter in formatters:
76 output += '<span class="mark_btn"' + \
77 output += '<span class="mark_btn"' + \
77 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
78 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
78 '\', \'' + formatter.format_right + '\')">' + \
79 '\', \'' + formatter.format_right + '\')">' + \
79 formatter.preview_left + formatter.name + \
80 formatter.preview_left + formatter.name + \
80 formatter.preview_right + '</span>'
81 formatter.preview_right + '</span>'
81
82
82 output += '</div>'
83 output += '</div>'
83 output += super(FormatPanel, self).render(name, value, attrs=None)
84 output += super(FormatPanel, self).render(name, value, attrs=None)
84
85
85 return output
86 return output
86
87
87
88
88 class PlainErrorList(ErrorList):
89 class PlainErrorList(ErrorList):
89 def __unicode__(self):
90 def __unicode__(self):
90 return self.as_text()
91 return self.as_text()
91
92
92 def as_text(self):
93 def as_text(self):
93 return ''.join(['(!) %s ' % e for e in self])
94 return ''.join(['(!) %s ' % e for e in self])
94
95
95
96
96 class NeboardForm(forms.Form):
97 class NeboardForm(forms.Form):
97 """
98 """
98 Form with neboard-specific formatting.
99 Form with neboard-specific formatting.
99 """
100 """
100
101
101 def as_div(self):
102 def as_div(self):
102 """
103 """
103 Returns this form rendered as HTML <as_div>s.
104 Returns this form rendered as HTML <as_div>s.
104 """
105 """
105
106
106 return self._html_output(
107 return self._html_output(
107 # TODO Do not show hidden rows in the list here
108 # TODO Do not show hidden rows in the list here
108 normal_row='<div class="form-row">'
109 normal_row='<div class="form-row">'
109 '<div class="form-label">'
110 '<div class="form-label">'
110 '%(label)s'
111 '%(label)s'
111 '</div>'
112 '</div>'
112 '<div class="form-input">'
113 '<div class="form-input">'
113 '%(field)s'
114 '%(field)s'
114 '</div>'
115 '</div>'
115 '</div>'
116 '</div>'
116 '<div class="form-row">'
117 '<div class="form-row">'
117 '%(help_text)s'
118 '%(help_text)s'
118 '</div>',
119 '</div>',
119 error_row='<div class="form-row">'
120 error_row='<div class="form-row">'
120 '<div class="form-label"></div>'
121 '<div class="form-label"></div>'
121 '<div class="form-errors">%s</div>'
122 '<div class="form-errors">%s</div>'
122 '</div>',
123 '</div>',
123 row_ender='</div>',
124 row_ender='</div>',
124 help_text_html='%s',
125 help_text_html='%s',
125 errors_on_separate_row=True)
126 errors_on_separate_row=True)
126
127
127 def as_json_errors(self):
128 def as_json_errors(self):
128 errors = []
129 errors = []
129
130
130 for name, field in list(self.fields.items()):
131 for name, field in list(self.fields.items()):
131 if self[name].errors:
132 if self[name].errors:
132 errors.append({
133 errors.append({
133 'field': name,
134 'field': name,
134 'errors': self[name].errors.as_text(),
135 'errors': self[name].errors.as_text(),
135 })
136 })
136
137
137 return errors
138 return errors
138
139
139
140
140 class PostForm(NeboardForm):
141 class PostForm(NeboardForm):
141
142
142 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
143 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
143 label=LABEL_TITLE,
144 label=LABEL_TITLE,
144 widget=forms.TextInput(
145 widget=forms.TextInput(
145 attrs={ATTRIBUTE_PLACEHOLDER:
146 attrs={ATTRIBUTE_PLACEHOLDER:
146 'test#tripcode'}))
147 'test#tripcode'}))
147 text = forms.CharField(
148 text = forms.CharField(
148 widget=FormatPanel(attrs={
149 widget=FormatPanel(attrs={
149 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
150 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
150 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
151 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
151 }),
152 }),
152 required=False, label=LABEL_TEXT)
153 required=False, label=LABEL_TEXT)
153 file = forms.FileField(required=False, label=_('File'),
154 file = forms.FileField(required=False, label=_('File'),
154 widget=forms.ClearableFileInput(
155 widget=forms.ClearableFileInput(
155 attrs={'accept': 'file/*'}))
156 attrs={'accept': 'file/*'}))
156 file_url = forms.CharField(required=False, label=_('File URL'),
157 file_url = forms.CharField(required=False, label=_('File URL'),
157 widget=forms.TextInput(
158 widget=forms.TextInput(
158 attrs={ATTRIBUTE_PLACEHOLDER:
159 attrs={ATTRIBUTE_PLACEHOLDER:
159 'http://example.com/image.png'}))
160 'http://example.com/image.png'}))
160
161
161 # This field is for spam prevention only
162 # This field is for spam prevention only
162 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
163 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
163 widget=forms.TextInput(attrs={
164 widget=forms.TextInput(attrs={
164 'class': 'form-email'}))
165 'class': 'form-email'}))
165 threads = forms.CharField(required=False, label=_('Additional threads'),
166 threads = forms.CharField(required=False, label=_('Additional threads'),
166 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
167 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
167 '123 456 789'}))
168 '123 456 789'}))
168
169
169 session = None
170 session = None
170 need_to_ban = False
171 need_to_ban = False
171
172
172 def _update_file_extension(self, file):
173 def _update_file_extension(self, file):
173 if file:
174 if file:
174 mimetype = get_file_mimetype(file)
175 mimetype = get_file_mimetype(file)
175 extension = MIMETYPE_EXTENSIONS.get(mimetype)
176 extension = MIMETYPE_EXTENSIONS.get(mimetype)
176 if extension:
177 if extension:
177 filename = file.name.split(FILE_EXTENSION_DELIMITER, 1)[0]
178 filename = file.name.split(FILE_EXTENSION_DELIMITER, 1)[0]
178 new_filename = filename + FILE_EXTENSION_DELIMITER + extension
179 new_filename = filename + FILE_EXTENSION_DELIMITER + extension
179
180
180 file.name = new_filename
181 file.name = new_filename
181 else:
182 else:
182 logger = logging.getLogger('boards.forms.extension')
183 logger = logging.getLogger('boards.forms.extension')
183
184
184 logger.info('Unrecognized file mimetype: {}'.format(mimetype))
185 logger.info('Unrecognized file mimetype: {}'.format(mimetype))
185
186
186 def clean_title(self):
187 def clean_title(self):
187 title = self.cleaned_data['title']
188 title = self.cleaned_data['title']
188 if title:
189 if title:
189 if len(title) > TITLE_MAX_LENGTH:
190 if len(title) > TITLE_MAX_LENGTH:
190 raise forms.ValidationError(_('Title must have less than %s '
191 raise forms.ValidationError(_('Title must have less than %s '
191 'characters') %
192 'characters') %
192 str(TITLE_MAX_LENGTH))
193 str(TITLE_MAX_LENGTH))
193 return title
194 return title
194
195
195 def clean_text(self):
196 def clean_text(self):
196 text = self.cleaned_data['text'].strip()
197 text = self.cleaned_data['text'].strip()
197 if text:
198 if text:
198 max_length = board_settings.get_int('Forms', 'MaxTextLength')
199 max_length = board_settings.get_int('Forms', 'MaxTextLength')
199 if len(text) > max_length:
200 if len(text) > max_length:
200 raise forms.ValidationError(_('Text must have less than %s '
201 raise forms.ValidationError(_('Text must have less than %s '
201 'characters') % str(max_length))
202 'characters') % str(max_length))
202 return text
203 return text
203
204
204 def clean_file(self):
205 def clean_file(self):
205 file = self.cleaned_data['file']
206 file = self.cleaned_data['file']
206
207
207 if file:
208 if file:
208 validate_file_size(file.size)
209 validate_file_size(file.size)
209 self._update_file_extension(file)
210 self._update_file_extension(file)
210
211
211 return file
212 return file
212
213
213 def clean_file_url(self):
214 def clean_file_url(self):
214 url = self.cleaned_data['file_url']
215 url = self.cleaned_data['file_url']
215
216
216 file = None
217 file = None
217 if url:
218 if url:
218 file = self._get_file_from_url(url)
219 file = self._get_file_from_url(url)
219
220
220 if not file:
221 if not file:
221 raise forms.ValidationError(_('Invalid URL'))
222 raise forms.ValidationError(_('Invalid URL'))
222 else:
223 else:
223 validate_file_size(file.size)
224 validate_file_size(file.size)
224 self._update_file_extension(file)
225 self._update_file_extension(file)
225
226
226 return file
227 return file
227
228
228 def clean_threads(self):
229 def clean_threads(self):
229 threads_str = self.cleaned_data['threads']
230 threads_str = self.cleaned_data['threads']
230
231
231 if len(threads_str) > 0:
232 if len(threads_str) > 0:
232 threads_id_list = threads_str.split(' ')
233 threads_id_list = threads_str.split(' ')
233
234
234 threads = list()
235 threads = list()
235
236
236 for thread_id in threads_id_list:
237 for thread_id in threads_id_list:
237 try:
238 try:
238 thread = Post.objects.get(id=int(thread_id))
239 thread = Post.objects.get(id=int(thread_id))
239 if not thread.is_opening() or thread.get_thread().archived:
240 if not thread.is_opening() or thread.get_thread().archived:
240 raise ObjectDoesNotExist()
241 raise ObjectDoesNotExist()
241 threads.append(thread)
242 threads.append(thread)
242 except (ObjectDoesNotExist, ValueError):
243 except (ObjectDoesNotExist, ValueError):
243 raise forms.ValidationError(_('Invalid additional thread list'))
244 raise forms.ValidationError(_('Invalid additional thread list'))
244
245
245 return threads
246 return threads
246
247
247 def clean(self):
248 def clean(self):
248 cleaned_data = super(PostForm, self).clean()
249 cleaned_data = super(PostForm, self).clean()
249
250
250 if cleaned_data['email']:
251 if cleaned_data['email']:
251 self.need_to_ban = True
252 self.need_to_ban = True
252 raise forms.ValidationError('A human cannot enter a hidden field')
253 raise forms.ValidationError('A human cannot enter a hidden field')
253
254
254 if not self.errors:
255 if not self.errors:
255 self._clean_text_file()
256 self._clean_text_file()
256
257
257 if not self.errors and self.session:
258 if not self.errors and self.session:
258 self._validate_posting_speed()
259 self._validate_posting_speed()
259
260
260 return cleaned_data
261 return cleaned_data
261
262
262 def get_file(self):
263 def get_file(self):
263 """
264 """
264 Gets file from form or URL.
265 Gets file from form or URL.
265 """
266 """
266
267
267 file = self.cleaned_data['file']
268 file = self.cleaned_data['file']
268 return file or self.cleaned_data['file_url']
269 return file or self.cleaned_data['file_url']
269
270
270 def get_tripcode(self):
271 def get_tripcode(self):
271 title = self.cleaned_data['title']
272 title = self.cleaned_data['title']
272 if title is not None and TRIPCODE_DELIM in title:
273 if title is not None and TRIPCODE_DELIM in title:
273 code = title.split(TRIPCODE_DELIM, maxsplit=1)[1] + neboard.settings.SECRET_KEY
274 code = title.split(TRIPCODE_DELIM, maxsplit=1)[1] + neboard.settings.SECRET_KEY
274 tripcode = hashlib.md5(code.encode()).hexdigest()
275 tripcode = hashlib.md5(code.encode()).hexdigest()
275 else:
276 else:
276 tripcode = ''
277 tripcode = ''
277 return tripcode
278 return tripcode
278
279
279 def get_title(self):
280 def get_title(self):
280 title = self.cleaned_data['title']
281 title = self.cleaned_data['title']
281 if title is not None and TRIPCODE_DELIM in title:
282 if title is not None and TRIPCODE_DELIM in title:
282 return title.split(TRIPCODE_DELIM, maxsplit=1)[0]
283 return title.split(TRIPCODE_DELIM, maxsplit=1)[0]
283 else:
284 else:
284 return title
285 return title
285
286
286 def _clean_text_file(self):
287 def _clean_text_file(self):
287 text = self.cleaned_data.get('text')
288 text = self.cleaned_data.get('text')
288 file = self.get_file()
289 file = self.get_file()
289
290
290 if (not text) and (not file):
291 if (not text) and (not file):
291 error_message = _('Either text or file must be entered.')
292 error_message = _('Either text or file must be entered.')
292 self._errors['text'] = self.error_class([error_message])
293 self._errors['text'] = self.error_class([error_message])
293
294
294 def _validate_posting_speed(self):
295 def _validate_posting_speed(self):
295 can_post = True
296 can_post = True
296
297
297 posting_delay = settings.POSTING_DELAY
298 posting_delay = settings.POSTING_DELAY
298
299
299 if board_settings.get_bool('Forms', 'LimitPostingSpeed'):
300 if board_settings.get_bool('Forms', 'LimitPostingSpeed'):
300 now = time.time()
301 now = time.time()
301
302
302 current_delay = 0
303 current_delay = 0
303
304
304 if LAST_POST_TIME not in self.session:
305 if LAST_POST_TIME not in self.session:
305 self.session[LAST_POST_TIME] = now
306 self.session[LAST_POST_TIME] = now
306
307
307 need_delay = True
308 need_delay = True
308 else:
309 else:
309 last_post_time = self.session.get(LAST_POST_TIME)
310 last_post_time = self.session.get(LAST_POST_TIME)
310 current_delay = int(now - last_post_time)
311 current_delay = int(now - last_post_time)
311
312
312 need_delay = current_delay < posting_delay
313 need_delay = current_delay < posting_delay
313
314
314 if need_delay:
315 if need_delay:
315 delay = posting_delay - current_delay
316 delay = posting_delay - current_delay
316 error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL,
317 error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL,
317 delay) % {'delay': delay}
318 delay) % {'delay': delay}
318 self._errors['text'] = self.error_class([error_message])
319 self._errors['text'] = self.error_class([error_message])
319
320
320 can_post = False
321 can_post = False
321
322
322 if can_post:
323 if can_post:
323 self.session[LAST_POST_TIME] = now
324 self.session[LAST_POST_TIME] = now
324
325
325 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
326 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
326 """
327 """
327 Gets an file file from URL.
328 Gets an file file from URL.
328 """
329 """
329
330
330 img_temp = None
331 img_temp = None
331
332
332 try:
333 try:
333 for downloader in Downloader.__subclasses__():
334 for downloader in Downloader.__subclasses__():
334 if downloader.handles(url):
335 if downloader.handles(url):
335 return downloader.download(url)
336 return downloader.download(url)
336 # If nobody of the specific downloaders handles this, use generic
337 # If nobody of the specific downloaders handles this, use generic
337 # one
338 # one
338 return Downloader.download(url)
339 return Downloader.download(url)
339 except forms.ValidationError as e:
340 except forms.ValidationError as e:
340 raise e
341 raise e
341 except Exception as e:
342 except Exception as e:
342 # Just return no file
343 # Just return no file
343 pass
344 pass
344
345
345
346
346 class ThreadForm(PostForm):
347 class ThreadForm(PostForm):
347
348
348 tags = forms.CharField(
349 tags = forms.CharField(
349 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
350 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
350 max_length=100, label=_('Tags'), required=True)
351 max_length=100, label=_('Tags'), required=True)
351
352
352 def clean_tags(self):
353 def clean_tags(self):
353 tags = self.cleaned_data['tags'].strip()
354 tags = self.cleaned_data['tags'].strip()
354
355
355 if not tags or not REGEX_TAGS.match(tags):
356 if not tags or not REGEX_TAGS.match(tags):
356 raise forms.ValidationError(
357 raise forms.ValidationError(
357 _('Inappropriate characters in tags.'))
358 _('Inappropriate characters in tags.'))
358
359
359 required_tag_exists = False
360 required_tag_exists = False
360 tag_set = set()
361 tag_set = set()
361 for tag_string in tags.split():
362 for tag_string in tags.split():
362 tag, created = Tag.objects.get_or_create(name=tag_string.strip().lower())
363 tag, created = Tag.objects.get_or_create(name=tag_string.strip().lower())
363 tag_set.add(tag)
364 tag_set.add(tag)
364
365
365 # If this is a new tag, don't check for its parents because nobody
366 # If this is a new tag, don't check for its parents because nobody
366 # added them yet
367 # added them yet
367 if not created:
368 if not created:
368 tag_set |= set(tag.get_all_parents())
369 tag_set |= set(tag.get_all_parents())
369
370
370 for tag in tag_set:
371 for tag in tag_set:
371 if tag.required:
372 if tag.required:
372 required_tag_exists = True
373 required_tag_exists = True
373 break
374 break
374
375
375 if not required_tag_exists:
376 if not required_tag_exists:
376 raise forms.ValidationError(
377 raise forms.ValidationError(
377 _('Need at least one section.'))
378 _('Need at least one section.'))
378
379
379 return tag_set
380 return tag_set
380
381
381 def clean(self):
382 def clean(self):
382 cleaned_data = super(ThreadForm, self).clean()
383 cleaned_data = super(ThreadForm, self).clean()
383
384
384 return cleaned_data
385 return cleaned_data
385
386
386
387
387 class SettingsForm(NeboardForm):
388 class SettingsForm(NeboardForm):
388
389
389 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
390 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
390 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('Image view mode'))
391 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('Image view mode'))
391 username = forms.CharField(label=_('User name'), required=False)
392 username = forms.CharField(label=_('User name'), required=False)
392 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
393 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
393
394
394 def clean_username(self):
395 def clean_username(self):
395 username = self.cleaned_data['username']
396 username = self.cleaned_data['username']
396
397
397 if username and not REGEX_TAGS.match(username):
398 if username and not REGEX_TAGS.match(username):
398 raise forms.ValidationError(_('Inappropriate characters.'))
399 raise forms.ValidationError(_('Inappropriate characters.'))
399
400
400 return username
401 return username
401
402
402
403
403 class SearchForm(NeboardForm):
404 class SearchForm(NeboardForm):
404 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
405 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
General Comments 0
You need to be logged in to leave comments. Login now