##// END OF EJS Templates
Disallow threads with space-only tags
neko259 -
r679:d10ed955 default
parent child Browse files
Show More
@@ -1,352 +1,351 b''
1 import re
1 import re
2 import time
2 import time
3 import hashlib
3 import hashlib
4
4
5 from captcha.fields import CaptchaField
5 from captcha.fields import CaptchaField
6 from django import forms
6 from django import forms
7 from django.forms.util import ErrorList
7 from django.forms.util import ErrorList
8 from django.utils.translation import ugettext_lazy as _
8 from django.utils.translation import ugettext_lazy as _
9
9
10 from boards.mdx_neboard import formatters
10 from boards.mdx_neboard import formatters
11 from boards.models.post import TITLE_MAX_LENGTH
11 from boards.models.post import TITLE_MAX_LENGTH
12 from boards.models import User, Post
12 from boards.models import User, Post
13 from neboard import settings
13 from neboard import settings
14 from boards import utils
14 from boards import utils
15 import boards.settings as board_settings
15 import boards.settings as board_settings
16
16
17 VETERAN_POSTING_DELAY = 5
17 VETERAN_POSTING_DELAY = 5
18
18
19 ATTRIBUTE_PLACEHOLDER = 'placeholder'
19 ATTRIBUTE_PLACEHOLDER = 'placeholder'
20
20
21 LAST_POST_TIME = 'last_post_time'
21 LAST_POST_TIME = 'last_post_time'
22 LAST_LOGIN_TIME = 'last_login_time'
22 LAST_LOGIN_TIME = 'last_login_time'
23 TEXT_PLACEHOLDER = _('''Type message here. You can reply to message >>123 like
23 TEXT_PLACEHOLDER = _('''Type message here. You can reply to message >>123 like
24 this. 2 new lines are required to start new paragraph.''')
24 this. 2 new lines are required to start new paragraph.''')
25 TAGS_PLACEHOLDER = _('tag1 several_words_tag')
25 TAGS_PLACEHOLDER = _('tag1 several_words_tag')
26
26
27 ERROR_IMAGE_DUPLICATE = _('Such image was already posted')
27 ERROR_IMAGE_DUPLICATE = _('Such image was already posted')
28
28
29 LABEL_TITLE = _('Title')
29 LABEL_TITLE = _('Title')
30 LABEL_TEXT = _('Text')
30 LABEL_TEXT = _('Text')
31 LABEL_TAG = _('Tag')
31 LABEL_TAG = _('Tag')
32
32
33 TAG_MAX_LENGTH = 20
33 TAG_MAX_LENGTH = 20
34
34
35 REGEX_TAG = ur'^[\w\d]+$'
35 REGEX_TAG = ur'^[\w\d]+$'
36
36
37
37
38 class FormatPanel(forms.Textarea):
38 class FormatPanel(forms.Textarea):
39 def render(self, name, value, attrs=None):
39 def render(self, name, value, attrs=None):
40 output = '<div id="mark-panel">'
40 output = '<div id="mark-panel">'
41 for formatter in formatters:
41 for formatter in formatters:
42 output += u'<span class="mark_btn"' + \
42 output += u'<span class="mark_btn"' + \
43 u' onClick="addMarkToMsg(\'' + formatter.format_left + \
43 u' onClick="addMarkToMsg(\'' + formatter.format_left + \
44 '\', \'' + formatter.format_right + '\')">' + \
44 '\', \'' + formatter.format_right + '\')">' + \
45 formatter.preview_left + formatter.name + \
45 formatter.preview_left + formatter.name + \
46 formatter.preview_right + u'</span>'
46 formatter.preview_right + u'</span>'
47
47
48 output += '</div>'
48 output += '</div>'
49 output += super(FormatPanel, self).render(name, value, attrs=None)
49 output += super(FormatPanel, self).render(name, value, attrs=None)
50
50
51 return output
51 return output
52
52
53
53
54 class PlainErrorList(ErrorList):
54 class PlainErrorList(ErrorList):
55 def __unicode__(self):
55 def __unicode__(self):
56 return self.as_text()
56 return self.as_text()
57
57
58 def as_text(self):
58 def as_text(self):
59 return ''.join([u'(!) %s ' % e for e in self])
59 return ''.join([u'(!) %s ' % e for e in self])
60
60
61
61
62 class NeboardForm(forms.Form):
62 class NeboardForm(forms.Form):
63
63
64 def as_div(self):
64 def as_div(self):
65 """
65 """
66 Returns this form rendered as HTML <as_div>s.
66 Returns this form rendered as HTML <as_div>s.
67 """
67 """
68
68
69 return self._html_output(
69 return self._html_output(
70 # TODO Do not show hidden rows in the list here
70 # TODO Do not show hidden rows in the list here
71 normal_row='<div class="form-row">'
71 normal_row='<div class="form-row">'
72 '<div class="form-label">'
72 '<div class="form-label">'
73 '%(label)s'
73 '%(label)s'
74 '</div>'
74 '</div>'
75 '<div class="form-input">'
75 '<div class="form-input">'
76 '%(field)s'
76 '%(field)s'
77 '</div>'
77 '</div>'
78 '%(help_text)s'
78 '%(help_text)s'
79 '</div>',
79 '</div>',
80 error_row='<div class="form-row">'
80 error_row='<div class="form-row">'
81 '<div class="form-label"></div>'
81 '<div class="form-label"></div>'
82 '<div class="form-errors">%s</div>'
82 '<div class="form-errors">%s</div>'
83 '</div>',
83 '</div>',
84 row_ender='</div>',
84 row_ender='</div>',
85 help_text_html='%s',
85 help_text_html='%s',
86 errors_on_separate_row=True)
86 errors_on_separate_row=True)
87
87
88 def as_json_errors(self):
88 def as_json_errors(self):
89 errors = []
89 errors = []
90
90
91 for name, field in self.fields.items():
91 for name, field in self.fields.items():
92 if self[name].errors:
92 if self[name].errors:
93 errors.append({
93 errors.append({
94 'field': name,
94 'field': name,
95 'errors': self[name].errors.as_text(),
95 'errors': self[name].errors.as_text(),
96 })
96 })
97
97
98 return errors
98 return errors
99
99
100
100
101 class PostForm(NeboardForm):
101 class PostForm(NeboardForm):
102
102
103 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
103 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
104 label=LABEL_TITLE)
104 label=LABEL_TITLE)
105 text = forms.CharField(
105 text = forms.CharField(
106 widget=FormatPanel(attrs={ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER}),
106 widget=FormatPanel(attrs={ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER}),
107 required=False, label=LABEL_TEXT)
107 required=False, label=LABEL_TEXT)
108 image = forms.ImageField(required=False, label=_('Image'),
108 image = forms.ImageField(required=False, label=_('Image'),
109 widget=forms.ClearableFileInput(attrs={'accept': 'image/*'}))
109 widget=forms.ClearableFileInput(attrs={'accept': 'image/*'}))
110
110
111 # This field is for spam prevention only
111 # This field is for spam prevention only
112 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
112 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
113 widget=forms.TextInput(attrs={
113 widget=forms.TextInput(attrs={
114 'class': 'form-email'}))
114 'class': 'form-email'}))
115
115
116 session = None
116 session = None
117 need_to_ban = False
117 need_to_ban = False
118
118
119 def clean_title(self):
119 def clean_title(self):
120 title = self.cleaned_data['title']
120 title = self.cleaned_data['title']
121 if title:
121 if title:
122 if len(title) > TITLE_MAX_LENGTH:
122 if len(title) > TITLE_MAX_LENGTH:
123 raise forms.ValidationError(_('Title must have less than %s '
123 raise forms.ValidationError(_('Title must have less than %s '
124 'characters') %
124 'characters') %
125 str(TITLE_MAX_LENGTH))
125 str(TITLE_MAX_LENGTH))
126 return title
126 return title
127
127
128 def clean_text(self):
128 def clean_text(self):
129 text = self.cleaned_data['text'].strip()
129 text = self.cleaned_data['text'].strip()
130 if text:
130 if text:
131 if len(text) > board_settings.MAX_TEXT_LENGTH:
131 if len(text) > board_settings.MAX_TEXT_LENGTH:
132 raise forms.ValidationError(_('Text must have less than %s '
132 raise forms.ValidationError(_('Text must have less than %s '
133 'characters') %
133 'characters') %
134 str(board_settings
134 str(board_settings
135 .MAX_TEXT_LENGTH))
135 .MAX_TEXT_LENGTH))
136 return text
136 return text
137
137
138 def clean_image(self):
138 def clean_image(self):
139 image = self.cleaned_data['image']
139 image = self.cleaned_data['image']
140 if image:
140 if image:
141 if image._size > board_settings.MAX_IMAGE_SIZE:
141 if image._size > board_settings.MAX_IMAGE_SIZE:
142 raise forms.ValidationError(
142 raise forms.ValidationError(
143 _('Image must be less than %s bytes')
143 _('Image must be less than %s bytes')
144 % str(board_settings.MAX_IMAGE_SIZE))
144 % str(board_settings.MAX_IMAGE_SIZE))
145
145
146 md5 = hashlib.md5()
146 md5 = hashlib.md5()
147 for chunk in image.chunks():
147 for chunk in image.chunks():
148 md5.update(chunk)
148 md5.update(chunk)
149 image_hash = md5.hexdigest()
149 image_hash = md5.hexdigest()
150 if Post.objects.filter(image_hash=image_hash).exists():
150 if Post.objects.filter(image_hash=image_hash).exists():
151 raise forms.ValidationError(ERROR_IMAGE_DUPLICATE)
151 raise forms.ValidationError(ERROR_IMAGE_DUPLICATE)
152
152
153 return image
153 return image
154
154
155 def clean(self):
155 def clean(self):
156 cleaned_data = super(PostForm, self).clean()
156 cleaned_data = super(PostForm, self).clean()
157
157
158 if not self.session:
158 if not self.session:
159 raise forms.ValidationError('Humans have sessions')
159 raise forms.ValidationError('Humans have sessions')
160
160
161 if cleaned_data['email']:
161 if cleaned_data['email']:
162 self.need_to_ban = True
162 self.need_to_ban = True
163 raise forms.ValidationError('A human cannot enter a hidden field')
163 raise forms.ValidationError('A human cannot enter a hidden field')
164
164
165 if not self.errors:
165 if not self.errors:
166 self._clean_text_image()
166 self._clean_text_image()
167
167
168 if not self.errors and self.session:
168 if not self.errors and self.session:
169 self._validate_posting_speed()
169 self._validate_posting_speed()
170
170
171 return cleaned_data
171 return cleaned_data
172
172
173 def _clean_text_image(self):
173 def _clean_text_image(self):
174 text = self.cleaned_data.get('text')
174 text = self.cleaned_data.get('text')
175 image = self.cleaned_data.get('image')
175 image = self.cleaned_data.get('image')
176
176
177 if (not text) and (not image):
177 if (not text) and (not image):
178 error_message = _('Either text or image must be entered.')
178 error_message = _('Either text or image must be entered.')
179 self._errors['text'] = self.error_class([error_message])
179 self._errors['text'] = self.error_class([error_message])
180
180
181 def _validate_posting_speed(self):
181 def _validate_posting_speed(self):
182 can_post = True
182 can_post = True
183
183
184 # TODO Remove this, it's only for test
184 # TODO Remove this, it's only for test
185 if not 'user_id' in self.session:
185 if not 'user_id' in self.session:
186 return
186 return
187
187
188 user = User.objects.get(id=self.session['user_id'])
188 user = User.objects.get(id=self.session['user_id'])
189 if user.is_veteran():
189 if user.is_veteran():
190 posting_delay = VETERAN_POSTING_DELAY
190 posting_delay = VETERAN_POSTING_DELAY
191 else:
191 else:
192 posting_delay = settings.POSTING_DELAY
192 posting_delay = settings.POSTING_DELAY
193
193
194 if LAST_POST_TIME in self.session:
194 if LAST_POST_TIME in self.session:
195 now = time.time()
195 now = time.time()
196 last_post_time = self.session[LAST_POST_TIME]
196 last_post_time = self.session[LAST_POST_TIME]
197
197
198 current_delay = int(now - last_post_time)
198 current_delay = int(now - last_post_time)
199
199
200 if current_delay < posting_delay:
200 if current_delay < posting_delay:
201 error_message = _('Wait %s seconds after last posting') % str(
201 error_message = _('Wait %s seconds after last posting') % str(
202 posting_delay - current_delay)
202 posting_delay - current_delay)
203 self._errors['text'] = self.error_class([error_message])
203 self._errors['text'] = self.error_class([error_message])
204
204
205 can_post = False
205 can_post = False
206
206
207 if can_post:
207 if can_post:
208 self.session[LAST_POST_TIME] = time.time()
208 self.session[LAST_POST_TIME] = time.time()
209
209
210
210
211 class ThreadForm(PostForm):
211 class ThreadForm(PostForm):
212
212
213 regex_tags = re.compile(ur'^[\w\s\d]+$', re.UNICODE)
213 regex_tags = re.compile(ur'^[\w\s\d]+$', re.UNICODE)
214
214
215 tags = forms.CharField(
215 tags = forms.CharField(
216 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
216 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
217 max_length=100, label=_('Tags'))
217 max_length=100, label=_('Tags'), required=True)
218
218
219 def clean_tags(self):
219 def clean_tags(self):
220 tags = self.cleaned_data['tags'].strip()
220 tags = self.cleaned_data['tags'].strip()
221
221
222 if tags:
222 if not tags or not self.regex_tags.match(tags):
223 if not self.regex_tags.match(tags):
223 raise forms.ValidationError(
224 raise forms.ValidationError(
224 _('Inappropriate characters in tags.'))
225 _('Inappropriate characters in tags.'))
226
225
227 return tags
226 return tags
228
227
229 def clean(self):
228 def clean(self):
230 cleaned_data = super(ThreadForm, self).clean()
229 cleaned_data = super(ThreadForm, self).clean()
231
230
232 return cleaned_data
231 return cleaned_data
233
232
234
233
235 class PostCaptchaForm(PostForm):
234 class PostCaptchaForm(PostForm):
236 captcha = CaptchaField()
235 captcha = CaptchaField()
237
236
238 def __init__(self, *args, **kwargs):
237 def __init__(self, *args, **kwargs):
239 self.request = kwargs['request']
238 self.request = kwargs['request']
240 del kwargs['request']
239 del kwargs['request']
241
240
242 super(PostCaptchaForm, self).__init__(*args, **kwargs)
241 super(PostCaptchaForm, self).__init__(*args, **kwargs)
243
242
244 def clean(self):
243 def clean(self):
245 cleaned_data = super(PostCaptchaForm, self).clean()
244 cleaned_data = super(PostCaptchaForm, self).clean()
246
245
247 success = self.is_valid()
246 success = self.is_valid()
248 utils.update_captcha_access(self.request, success)
247 utils.update_captcha_access(self.request, success)
249
248
250 if success:
249 if success:
251 return cleaned_data
250 return cleaned_data
252 else:
251 else:
253 raise forms.ValidationError(_("Captcha validation failed"))
252 raise forms.ValidationError(_("Captcha validation failed"))
254
253
255
254
256 class ThreadCaptchaForm(ThreadForm):
255 class ThreadCaptchaForm(ThreadForm):
257 captcha = CaptchaField()
256 captcha = CaptchaField()
258
257
259 def __init__(self, *args, **kwargs):
258 def __init__(self, *args, **kwargs):
260 self.request = kwargs['request']
259 self.request = kwargs['request']
261 del kwargs['request']
260 del kwargs['request']
262
261
263 super(ThreadCaptchaForm, self).__init__(*args, **kwargs)
262 super(ThreadCaptchaForm, self).__init__(*args, **kwargs)
264
263
265 def clean(self):
264 def clean(self):
266 cleaned_data = super(ThreadCaptchaForm, self).clean()
265 cleaned_data = super(ThreadCaptchaForm, self).clean()
267
266
268 success = self.is_valid()
267 success = self.is_valid()
269 utils.update_captcha_access(self.request, success)
268 utils.update_captcha_access(self.request, success)
270
269
271 if success:
270 if success:
272 return cleaned_data
271 return cleaned_data
273 else:
272 else:
274 raise forms.ValidationError(_("Captcha validation failed"))
273 raise forms.ValidationError(_("Captcha validation failed"))
275
274
276
275
277 class SettingsForm(NeboardForm):
276 class SettingsForm(NeboardForm):
278
277
279 theme = forms.ChoiceField(choices=settings.THEMES,
278 theme = forms.ChoiceField(choices=settings.THEMES,
280 label=_('Theme'))
279 label=_('Theme'))
281
280
282
281
283 class ModeratorSettingsForm(SettingsForm):
282 class ModeratorSettingsForm(SettingsForm):
284
283
285 moderate = forms.BooleanField(required=False, label=_('Enable moderation '
284 moderate = forms.BooleanField(required=False, label=_('Enable moderation '
286 'panel'))
285 'panel'))
287
286
288
287
289 class LoginForm(NeboardForm):
288 class LoginForm(NeboardForm):
290
289
291 user_id = forms.CharField()
290 user_id = forms.CharField()
292
291
293 session = None
292 session = None
294
293
295 def clean_user_id(self):
294 def clean_user_id(self):
296 user_id = self.cleaned_data['user_id']
295 user_id = self.cleaned_data['user_id']
297 if user_id:
296 if user_id:
298 users = User.objects.filter(user_id=user_id)
297 users = User.objects.filter(user_id=user_id)
299 if len(users) == 0:
298 if len(users) == 0:
300 raise forms.ValidationError(_('No such user found'))
299 raise forms.ValidationError(_('No such user found'))
301
300
302 return user_id
301 return user_id
303
302
304 def _validate_login_speed(self):
303 def _validate_login_speed(self):
305 can_post = True
304 can_post = True
306
305
307 if LAST_LOGIN_TIME in self.session:
306 if LAST_LOGIN_TIME in self.session:
308 now = time.time()
307 now = time.time()
309 last_login_time = self.session[LAST_LOGIN_TIME]
308 last_login_time = self.session[LAST_LOGIN_TIME]
310
309
311 current_delay = int(now - last_login_time)
310 current_delay = int(now - last_login_time)
312
311
313 if current_delay < board_settings.LOGIN_TIMEOUT:
312 if current_delay < board_settings.LOGIN_TIMEOUT:
314 error_message = _('Wait %s minutes after last login') % str(
313 error_message = _('Wait %s minutes after last login') % str(
315 (board_settings.LOGIN_TIMEOUT - current_delay) / 60)
314 (board_settings.LOGIN_TIMEOUT - current_delay) / 60)
316 self._errors['user_id'] = self.error_class([error_message])
315 self._errors['user_id'] = self.error_class([error_message])
317
316
318 can_post = False
317 can_post = False
319
318
320 if can_post:
319 if can_post:
321 self.session[LAST_LOGIN_TIME] = time.time()
320 self.session[LAST_LOGIN_TIME] = time.time()
322
321
323 def clean(self):
322 def clean(self):
324 if not self.session:
323 if not self.session:
325 raise forms.ValidationError('Humans have sessions')
324 raise forms.ValidationError('Humans have sessions')
326
325
327 self._validate_login_speed()
326 self._validate_login_speed()
328
327
329 cleaned_data = super(LoginForm, self).clean()
328 cleaned_data = super(LoginForm, self).clean()
330
329
331 return cleaned_data
330 return cleaned_data
332
331
333
332
334 class AddTagForm(NeboardForm):
333 class AddTagForm(NeboardForm):
335
334
336 tag = forms.CharField(max_length=TAG_MAX_LENGTH, label=LABEL_TAG)
335 tag = forms.CharField(max_length=TAG_MAX_LENGTH, label=LABEL_TAG)
337 method = forms.CharField(widget=forms.HiddenInput(), initial='add_tag')
336 method = forms.CharField(widget=forms.HiddenInput(), initial='add_tag')
338
337
339 def clean_tag(self):
338 def clean_tag(self):
340 tag = self.cleaned_data['tag']
339 tag = self.cleaned_data['tag']
341
340
342 regex_tag = re.compile(REGEX_TAG, re.UNICODE)
341 regex_tag = re.compile(REGEX_TAG, re.UNICODE)
343 if not regex_tag.match(tag):
342 if not regex_tag.match(tag):
344 raise forms.ValidationError(_('Inappropriate characters in tags.'))
343 raise forms.ValidationError(_('Inappropriate characters in tags.'))
345
344
346 return tag
345 return tag
347
346
348 def clean(self):
347 def clean(self):
349 cleaned_data = super(AddTagForm, self).clean()
348 cleaned_data = super(AddTagForm, self).clean()
350
349
351 return cleaned_data
350 return cleaned_data
352
351
General Comments 0
You need to be logged in to leave comments. Login now