##// END OF EJS Templates
Fixed limiting posting speed
neko259 -
r832:e351426f default
parent child Browse files
Show More
@@ -1,298 +1,294 b''
1 import re
1 import re
2 import time
2 import time
3 import hashlib
3 import hashlib
4
4
5 from django import forms
5 from django import forms
6 from django.forms.util import ErrorList
6 from django.forms.util import ErrorList
7 from django.utils.translation import ugettext_lazy as _
7 from django.utils.translation import ugettext_lazy as _
8
8
9 from boards.mdx_neboard import formatters
9 from boards.mdx_neboard import formatters
10 from boards.models.post import TITLE_MAX_LENGTH
10 from boards.models.post import TITLE_MAX_LENGTH
11 from boards.models import PostImage
11 from boards.models import PostImage
12 from neboard import settings
12 from neboard import settings
13 from boards import utils
13 from boards import utils
14 import boards.settings as board_settings
14 import boards.settings as board_settings
15
15
16 VETERAN_POSTING_DELAY = 5
16 VETERAN_POSTING_DELAY = 5
17
17
18 ATTRIBUTE_PLACEHOLDER = 'placeholder'
18 ATTRIBUTE_PLACEHOLDER = 'placeholder'
19
19
20 LAST_POST_TIME = 'last_post_time'
20 LAST_POST_TIME = 'last_post_time'
21 LAST_LOGIN_TIME = 'last_login_time'
21 LAST_LOGIN_TIME = 'last_login_time'
22 TEXT_PLACEHOLDER = _('''Type message here. Use formatting panel for more advanced usage.''')
22 TEXT_PLACEHOLDER = _('''Type message here. Use formatting panel for more advanced usage.''')
23 TAGS_PLACEHOLDER = _('tag1 several_words_tag')
23 TAGS_PLACEHOLDER = _('tag1 several_words_tag')
24
24
25 ERROR_IMAGE_DUPLICATE = _('Such image was already posted')
25 ERROR_IMAGE_DUPLICATE = _('Such image was already posted')
26
26
27 LABEL_TITLE = _('Title')
27 LABEL_TITLE = _('Title')
28 LABEL_TEXT = _('Text')
28 LABEL_TEXT = _('Text')
29 LABEL_TAG = _('Tag')
29 LABEL_TAG = _('Tag')
30 LABEL_SEARCH = _('Search')
30 LABEL_SEARCH = _('Search')
31
31
32 TAG_MAX_LENGTH = 20
32 TAG_MAX_LENGTH = 20
33
33
34 REGEX_TAG = r'^[\w\d]+$'
34 REGEX_TAG = r'^[\w\d]+$'
35
35
36
36
37 class FormatPanel(forms.Textarea):
37 class FormatPanel(forms.Textarea):
38 def render(self, name, value, attrs=None):
38 def render(self, name, value, attrs=None):
39 output = '<div id="mark-panel">'
39 output = '<div id="mark-panel">'
40 for formatter in formatters:
40 for formatter in formatters:
41 output += '<span class="mark_btn"' + \
41 output += '<span class="mark_btn"' + \
42 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
42 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
43 '\', \'' + formatter.format_right + '\')">' + \
43 '\', \'' + formatter.format_right + '\')">' + \
44 formatter.preview_left + formatter.name + \
44 formatter.preview_left + formatter.name + \
45 formatter.preview_right + '</span>'
45 formatter.preview_right + '</span>'
46
46
47 output += '</div>'
47 output += '</div>'
48 output += super(FormatPanel, self).render(name, value, attrs=None)
48 output += super(FormatPanel, self).render(name, value, attrs=None)
49
49
50 return output
50 return output
51
51
52
52
53 class PlainErrorList(ErrorList):
53 class PlainErrorList(ErrorList):
54 def __unicode__(self):
54 def __unicode__(self):
55 return self.as_text()
55 return self.as_text()
56
56
57 def as_text(self):
57 def as_text(self):
58 return ''.join(['(!) %s ' % e for e in self])
58 return ''.join(['(!) %s ' % e for e in self])
59
59
60
60
61 class NeboardForm(forms.Form):
61 class NeboardForm(forms.Form):
62
62
63 def as_div(self):
63 def as_div(self):
64 """
64 """
65 Returns this form rendered as HTML <as_div>s.
65 Returns this form rendered as HTML <as_div>s.
66 """
66 """
67
67
68 return self._html_output(
68 return self._html_output(
69 # TODO Do not show hidden rows in the list here
69 # TODO Do not show hidden rows in the list here
70 normal_row='<div class="form-row"><div class="form-label">'
70 normal_row='<div class="form-row"><div class="form-label">'
71 '%(label)s'
71 '%(label)s'
72 '</div></div>'
72 '</div></div>'
73 '<div class="form-row"><div class="form-input">'
73 '<div class="form-row"><div class="form-input">'
74 '%(field)s'
74 '%(field)s'
75 '</div></div>'
75 '</div></div>'
76 '<div class="form-row">'
76 '<div class="form-row">'
77 '%(help_text)s'
77 '%(help_text)s'
78 '</div>',
78 '</div>',
79 error_row='<div class="form-row">'
79 error_row='<div class="form-row">'
80 '<div class="form-label"></div>'
80 '<div class="form-label"></div>'
81 '<div class="form-errors">%s</div>'
81 '<div class="form-errors">%s</div>'
82 '</div>',
82 '</div>',
83 row_ender='</div>',
83 row_ender='</div>',
84 help_text_html='%s',
84 help_text_html='%s',
85 errors_on_separate_row=True)
85 errors_on_separate_row=True)
86
86
87 def as_json_errors(self):
87 def as_json_errors(self):
88 errors = []
88 errors = []
89
89
90 for name, field in list(self.fields.items()):
90 for name, field in list(self.fields.items()):
91 if self[name].errors:
91 if self[name].errors:
92 errors.append({
92 errors.append({
93 'field': name,
93 'field': name,
94 'errors': self[name].errors.as_text(),
94 'errors': self[name].errors.as_text(),
95 })
95 })
96
96
97 return errors
97 return errors
98
98
99
99
100 class PostForm(NeboardForm):
100 class PostForm(NeboardForm):
101
101
102 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
102 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
103 label=LABEL_TITLE)
103 label=LABEL_TITLE)
104 text = forms.CharField(
104 text = forms.CharField(
105 widget=FormatPanel(attrs={ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER}),
105 widget=FormatPanel(attrs={ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER}),
106 required=False, label=LABEL_TEXT)
106 required=False, label=LABEL_TEXT)
107 image = forms.ImageField(required=False, label=_('Image'),
107 image = forms.ImageField(required=False, label=_('Image'),
108 widget=forms.ClearableFileInput(
108 widget=forms.ClearableFileInput(
109 attrs={'accept': 'image/*'}))
109 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 PostImage.objects.filter(hash=image_hash).exists():
150 if PostImage.objects.filter(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
185 if not 'user_id' in self.session:
186 return
187
188 posting_delay = settings.POSTING_DELAY
184 posting_delay = settings.POSTING_DELAY
189
185
190 if board_settings.LIMIT_POSTING_SPEED and LAST_POST_TIME in \
186 if board_settings.LIMIT_POSTING_SPEED and LAST_POST_TIME in \
191 self.session:
187 self.session:
192 now = time.time()
188 now = time.time()
193 last_post_time = self.session[LAST_POST_TIME]
189 last_post_time = self.session[LAST_POST_TIME]
194
190
195 current_delay = int(now - last_post_time)
191 current_delay = int(now - last_post_time)
196
192
197 if current_delay < posting_delay:
193 if current_delay < posting_delay:
198 error_message = _('Wait %s seconds after last posting') % str(
194 error_message = _('Wait %s seconds after last posting') % str(
199 posting_delay - current_delay)
195 posting_delay - current_delay)
200 self._errors['text'] = self.error_class([error_message])
196 self._errors['text'] = self.error_class([error_message])
201
197
202 can_post = False
198 can_post = False
203
199
204 if can_post:
200 if can_post:
205 self.session[LAST_POST_TIME] = time.time()
201 self.session[LAST_POST_TIME] = time.time()
206
202
207
203
208 class ThreadForm(PostForm):
204 class ThreadForm(PostForm):
209
205
210 regex_tags = re.compile(r'^[\w\s\d]+$', re.UNICODE)
206 regex_tags = re.compile(r'^[\w\s\d]+$', re.UNICODE)
211
207
212 tags = forms.CharField(
208 tags = forms.CharField(
213 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
209 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
214 max_length=100, label=_('Tags'), required=True)
210 max_length=100, label=_('Tags'), required=True)
215
211
216 def clean_tags(self):
212 def clean_tags(self):
217 tags = self.cleaned_data['tags'].strip()
213 tags = self.cleaned_data['tags'].strip()
218
214
219 if not tags or not self.regex_tags.match(tags):
215 if not tags or not self.regex_tags.match(tags):
220 raise forms.ValidationError(
216 raise forms.ValidationError(
221 _('Inappropriate characters in tags.'))
217 _('Inappropriate characters in tags.'))
222
218
223 return tags
219 return tags
224
220
225 def clean(self):
221 def clean(self):
226 cleaned_data = super(ThreadForm, self).clean()
222 cleaned_data = super(ThreadForm, self).clean()
227
223
228 return cleaned_data
224 return cleaned_data
229
225
230
226
231 class SettingsForm(NeboardForm):
227 class SettingsForm(NeboardForm):
232
228
233 theme = forms.ChoiceField(choices=settings.THEMES,
229 theme = forms.ChoiceField(choices=settings.THEMES,
234 label=_('Theme'))
230 label=_('Theme'))
235
231
236
232
237 class AddTagForm(NeboardForm):
233 class AddTagForm(NeboardForm):
238
234
239 tag = forms.CharField(max_length=TAG_MAX_LENGTH, label=LABEL_TAG)
235 tag = forms.CharField(max_length=TAG_MAX_LENGTH, label=LABEL_TAG)
240 method = forms.CharField(widget=forms.HiddenInput(), initial='add_tag')
236 method = forms.CharField(widget=forms.HiddenInput(), initial='add_tag')
241
237
242 def clean_tag(self):
238 def clean_tag(self):
243 tag = self.cleaned_data['tag']
239 tag = self.cleaned_data['tag']
244
240
245 regex_tag = re.compile(REGEX_TAG, re.UNICODE)
241 regex_tag = re.compile(REGEX_TAG, re.UNICODE)
246 if not regex_tag.match(tag):
242 if not regex_tag.match(tag):
247 raise forms.ValidationError(_('Inappropriate characters in tags.'))
243 raise forms.ValidationError(_('Inappropriate characters in tags.'))
248
244
249 return tag
245 return tag
250
246
251 def clean(self):
247 def clean(self):
252 cleaned_data = super(AddTagForm, self).clean()
248 cleaned_data = super(AddTagForm, self).clean()
253
249
254 return cleaned_data
250 return cleaned_data
255
251
256
252
257 class SearchForm(NeboardForm):
253 class SearchForm(NeboardForm):
258 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
254 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
259
255
260
256
261 class LoginForm(NeboardForm):
257 class LoginForm(NeboardForm):
262
258
263 password = forms.CharField()
259 password = forms.CharField()
264
260
265 session = None
261 session = None
266
262
267 def clean_password(self):
263 def clean_password(self):
268 password = self.cleaned_data['password']
264 password = self.cleaned_data['password']
269 if board_settings.MASTER_PASSWORD != password:
265 if board_settings.MASTER_PASSWORD != password:
270 raise forms.ValidationError(_('Invalid master password'))
266 raise forms.ValidationError(_('Invalid master password'))
271
267
272 return password
268 return password
273
269
274 def _validate_login_speed(self):
270 def _validate_login_speed(self):
275 can_post = True
271 can_post = True
276
272
277 if LAST_LOGIN_TIME in self.session:
273 if LAST_LOGIN_TIME in self.session:
278 now = time.time()
274 now = time.time()
279 last_login_time = self.session[LAST_LOGIN_TIME]
275 last_login_time = self.session[LAST_LOGIN_TIME]
280
276
281 current_delay = int(now - last_login_time)
277 current_delay = int(now - last_login_time)
282
278
283 if current_delay < board_settings.LOGIN_TIMEOUT:
279 if current_delay < board_settings.LOGIN_TIMEOUT:
284 error_message = _('Wait %s minutes after last login') % str(
280 error_message = _('Wait %s minutes after last login') % str(
285 (board_settings.LOGIN_TIMEOUT - current_delay) / 60)
281 (board_settings.LOGIN_TIMEOUT - current_delay) / 60)
286 self._errors['password'] = self.error_class([error_message])
282 self._errors['password'] = self.error_class([error_message])
287
283
288 can_post = False
284 can_post = False
289
285
290 if can_post:
286 if can_post:
291 self.session[LAST_LOGIN_TIME] = time.time()
287 self.session[LAST_LOGIN_TIME] = time.time()
292
288
293 def clean(self):
289 def clean(self):
294 self._validate_login_speed()
290 self._validate_login_speed()
295
291
296 cleaned_data = super(LoginForm, self).clean()
292 cleaned_data = super(LoginForm, self).clean()
297
293
298 return cleaned_data
294 return cleaned_data
General Comments 0
You need to be logged in to leave comments. Login now