##// END OF EJS Templates
Added settings to limit posting speed. Added message when the form data is sent and response not yet received
neko259 -
r725:ce7b9618 default
parent child Browse files
Show More
@@ -1,356 +1,357 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, PostImage
12 from boards.models import User, PostImage
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 LABEL_SEARCH = _('Search')
32 LABEL_SEARCH = _('Search')
33
33
34 TAG_MAX_LENGTH = 20
34 TAG_MAX_LENGTH = 20
35
35
36 REGEX_TAG = ur'^[\w\d]+$'
36 REGEX_TAG = ur'^[\w\d]+$'
37
37
38
38
39 class FormatPanel(forms.Textarea):
39 class FormatPanel(forms.Textarea):
40 def render(self, name, value, attrs=None):
40 def render(self, name, value, attrs=None):
41 output = '<div id="mark-panel">'
41 output = '<div id="mark-panel">'
42 for formatter in formatters:
42 for formatter in formatters:
43 output += u'<span class="mark_btn"' + \
43 output += u'<span class="mark_btn"' + \
44 u' onClick="addMarkToMsg(\'' + formatter.format_left + \
44 u' onClick="addMarkToMsg(\'' + formatter.format_left + \
45 '\', \'' + formatter.format_right + '\')">' + \
45 '\', \'' + formatter.format_right + '\')">' + \
46 formatter.preview_left + formatter.name + \
46 formatter.preview_left + formatter.name + \
47 formatter.preview_right + u'</span>'
47 formatter.preview_right + u'</span>'
48
48
49 output += '</div>'
49 output += '</div>'
50 output += super(FormatPanel, self).render(name, value, attrs=None)
50 output += super(FormatPanel, self).render(name, value, attrs=None)
51
51
52 return output
52 return output
53
53
54
54
55 class PlainErrorList(ErrorList):
55 class PlainErrorList(ErrorList):
56 def __unicode__(self):
56 def __unicode__(self):
57 return self.as_text()
57 return self.as_text()
58
58
59 def as_text(self):
59 def as_text(self):
60 return ''.join([u'(!) %s ' % e for e in self])
60 return ''.join([u'(!) %s ' % e for e in self])
61
61
62
62
63 class NeboardForm(forms.Form):
63 class NeboardForm(forms.Form):
64
64
65 def as_div(self):
65 def as_div(self):
66 """
66 """
67 Returns this form rendered as HTML <as_div>s.
67 Returns this form rendered as HTML <as_div>s.
68 """
68 """
69
69
70 return self._html_output(
70 return self._html_output(
71 # TODO Do not show hidden rows in the list here
71 # TODO Do not show hidden rows in the list here
72 normal_row='<div class="form-row"><div class="form-label">'
72 normal_row='<div class="form-row"><div class="form-label">'
73 '%(label)s'
73 '%(label)s'
74 '</div></div>'
74 '</div></div>'
75 '<div class="form-row"><div class="form-input">'
75 '<div class="form-row"><div class="form-input">'
76 '%(field)s'
76 '%(field)s'
77 '</div></div>'
77 '</div></div>'
78 '<div class="form-row">'
78 '<div class="form-row">'
79 '%(help_text)s'
79 '%(help_text)s'
80 '</div>',
80 '</div>',
81 error_row='<div class="form-row">'
81 error_row='<div class="form-row">'
82 '<div class="form-label"></div>'
82 '<div class="form-label"></div>'
83 '<div class="form-errors">%s</div>'
83 '<div class="form-errors">%s</div>'
84 '</div>',
84 '</div>',
85 row_ender='</div>',
85 row_ender='</div>',
86 help_text_html='%s',
86 help_text_html='%s',
87 errors_on_separate_row=True)
87 errors_on_separate_row=True)
88
88
89 def as_json_errors(self):
89 def as_json_errors(self):
90 errors = []
90 errors = []
91
91
92 for name, field in self.fields.items():
92 for name, field in self.fields.items():
93 if self[name].errors:
93 if self[name].errors:
94 errors.append({
94 errors.append({
95 'field': name,
95 'field': name,
96 'errors': self[name].errors.as_text(),
96 'errors': self[name].errors.as_text(),
97 })
97 })
98
98
99 return errors
99 return errors
100
100
101
101
102 class PostForm(NeboardForm):
102 class PostForm(NeboardForm):
103
103
104 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
104 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
105 label=LABEL_TITLE)
105 label=LABEL_TITLE)
106 text = forms.CharField(
106 text = forms.CharField(
107 widget=FormatPanel(attrs={ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER}),
107 widget=FormatPanel(attrs={ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER}),
108 required=False, label=LABEL_TEXT)
108 required=False, label=LABEL_TEXT)
109 image = forms.ImageField(required=False, label=_('Image'),
109 image = forms.ImageField(required=False, label=_('Image'),
110 widget=forms.ClearableFileInput(
110 widget=forms.ClearableFileInput(
111 attrs={'accept': 'image/*'}))
111 attrs={'accept': 'image/*'}))
112
112
113 # This field is for spam prevention only
113 # This field is for spam prevention only
114 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
114 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
115 widget=forms.TextInput(attrs={
115 widget=forms.TextInput(attrs={
116 'class': 'form-email'}))
116 'class': 'form-email'}))
117
117
118 session = None
118 session = None
119 need_to_ban = False
119 need_to_ban = False
120
120
121 def clean_title(self):
121 def clean_title(self):
122 title = self.cleaned_data['title']
122 title = self.cleaned_data['title']
123 if title:
123 if title:
124 if len(title) > TITLE_MAX_LENGTH:
124 if len(title) > TITLE_MAX_LENGTH:
125 raise forms.ValidationError(_('Title must have less than %s '
125 raise forms.ValidationError(_('Title must have less than %s '
126 'characters') %
126 'characters') %
127 str(TITLE_MAX_LENGTH))
127 str(TITLE_MAX_LENGTH))
128 return title
128 return title
129
129
130 def clean_text(self):
130 def clean_text(self):
131 text = self.cleaned_data['text'].strip()
131 text = self.cleaned_data['text'].strip()
132 if text:
132 if text:
133 if len(text) > board_settings.MAX_TEXT_LENGTH:
133 if len(text) > board_settings.MAX_TEXT_LENGTH:
134 raise forms.ValidationError(_('Text must have less than %s '
134 raise forms.ValidationError(_('Text must have less than %s '
135 'characters') %
135 'characters') %
136 str(board_settings
136 str(board_settings
137 .MAX_TEXT_LENGTH))
137 .MAX_TEXT_LENGTH))
138 return text
138 return text
139
139
140 def clean_image(self):
140 def clean_image(self):
141 image = self.cleaned_data['image']
141 image = self.cleaned_data['image']
142 if image:
142 if image:
143 if image.size > board_settings.MAX_IMAGE_SIZE:
143 if image.size > board_settings.MAX_IMAGE_SIZE:
144 raise forms.ValidationError(
144 raise forms.ValidationError(
145 _('Image must be less than %s bytes')
145 _('Image must be less than %s bytes')
146 % str(board_settings.MAX_IMAGE_SIZE))
146 % str(board_settings.MAX_IMAGE_SIZE))
147
147
148 md5 = hashlib.md5()
148 md5 = hashlib.md5()
149 for chunk in image.chunks():
149 for chunk in image.chunks():
150 md5.update(chunk)
150 md5.update(chunk)
151 image_hash = md5.hexdigest()
151 image_hash = md5.hexdigest()
152 if PostImage.objects.filter(hash=image_hash).exists():
152 if PostImage.objects.filter(hash=image_hash).exists():
153 raise forms.ValidationError(ERROR_IMAGE_DUPLICATE)
153 raise forms.ValidationError(ERROR_IMAGE_DUPLICATE)
154
154
155 return image
155 return image
156
156
157 def clean(self):
157 def clean(self):
158 cleaned_data = super(PostForm, self).clean()
158 cleaned_data = super(PostForm, self).clean()
159
159
160 if not self.session:
160 if not self.session:
161 raise forms.ValidationError('Humans have sessions')
161 raise forms.ValidationError('Humans have sessions')
162
162
163 if cleaned_data['email']:
163 if cleaned_data['email']:
164 self.need_to_ban = True
164 self.need_to_ban = True
165 raise forms.ValidationError('A human cannot enter a hidden field')
165 raise forms.ValidationError('A human cannot enter a hidden field')
166
166
167 if not self.errors:
167 if not self.errors:
168 self._clean_text_image()
168 self._clean_text_image()
169
169
170 if not self.errors and self.session:
170 if not self.errors and self.session:
171 self._validate_posting_speed()
171 self._validate_posting_speed()
172
172
173 return cleaned_data
173 return cleaned_data
174
174
175 def _clean_text_image(self):
175 def _clean_text_image(self):
176 text = self.cleaned_data.get('text')
176 text = self.cleaned_data.get('text')
177 image = self.cleaned_data.get('image')
177 image = self.cleaned_data.get('image')
178
178
179 if (not text) and (not image):
179 if (not text) and (not image):
180 error_message = _('Either text or image must be entered.')
180 error_message = _('Either text or image must be entered.')
181 self._errors['text'] = self.error_class([error_message])
181 self._errors['text'] = self.error_class([error_message])
182
182
183 def _validate_posting_speed(self):
183 def _validate_posting_speed(self):
184 can_post = True
184 can_post = True
185
185
186 # TODO Remove this, it's only for test
186 # TODO Remove this, it's only for test
187 if not 'user_id' in self.session:
187 if not 'user_id' in self.session:
188 return
188 return
189
189
190 user = User.objects.get(id=self.session['user_id'])
190 user = User.objects.get(id=self.session['user_id'])
191 if user.is_veteran():
191 if user.is_veteran():
192 posting_delay = VETERAN_POSTING_DELAY
192 posting_delay = VETERAN_POSTING_DELAY
193 else:
193 else:
194 posting_delay = settings.POSTING_DELAY
194 posting_delay = settings.POSTING_DELAY
195
195
196 if LAST_POST_TIME in self.session:
196 if board_settings.LIMIT_POSTING_SPEED and LAST_POST_TIME in \
197 self.session:
197 now = time.time()
198 now = time.time()
198 last_post_time = self.session[LAST_POST_TIME]
199 last_post_time = self.session[LAST_POST_TIME]
199
200
200 current_delay = int(now - last_post_time)
201 current_delay = int(now - last_post_time)
201
202
202 if current_delay < posting_delay:
203 if current_delay < posting_delay:
203 error_message = _('Wait %s seconds after last posting') % str(
204 error_message = _('Wait %s seconds after last posting') % str(
204 posting_delay - current_delay)
205 posting_delay - current_delay)
205 self._errors['text'] = self.error_class([error_message])
206 self._errors['text'] = self.error_class([error_message])
206
207
207 can_post = False
208 can_post = False
208
209
209 if can_post:
210 if can_post:
210 self.session[LAST_POST_TIME] = time.time()
211 self.session[LAST_POST_TIME] = time.time()
211
212
212
213
213 class ThreadForm(PostForm):
214 class ThreadForm(PostForm):
214
215
215 regex_tags = re.compile(ur'^[\w\s\d]+$', re.UNICODE)
216 regex_tags = re.compile(ur'^[\w\s\d]+$', re.UNICODE)
216
217
217 tags = forms.CharField(
218 tags = forms.CharField(
218 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
219 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
219 max_length=100, label=_('Tags'), required=True)
220 max_length=100, label=_('Tags'), required=True)
220
221
221 def clean_tags(self):
222 def clean_tags(self):
222 tags = self.cleaned_data['tags'].strip()
223 tags = self.cleaned_data['tags'].strip()
223
224
224 if not tags or not self.regex_tags.match(tags):
225 if not tags or not self.regex_tags.match(tags):
225 raise forms.ValidationError(
226 raise forms.ValidationError(
226 _('Inappropriate characters in tags.'))
227 _('Inappropriate characters in tags.'))
227
228
228 return tags
229 return tags
229
230
230 def clean(self):
231 def clean(self):
231 cleaned_data = super(ThreadForm, self).clean()
232 cleaned_data = super(ThreadForm, self).clean()
232
233
233 return cleaned_data
234 return cleaned_data
234
235
235
236
236 class PostCaptchaForm(PostForm):
237 class PostCaptchaForm(PostForm):
237 captcha = CaptchaField()
238 captcha = CaptchaField()
238
239
239 def __init__(self, *args, **kwargs):
240 def __init__(self, *args, **kwargs):
240 self.request = kwargs['request']
241 self.request = kwargs['request']
241 del kwargs['request']
242 del kwargs['request']
242
243
243 super(PostCaptchaForm, self).__init__(*args, **kwargs)
244 super(PostCaptchaForm, self).__init__(*args, **kwargs)
244
245
245 def clean(self):
246 def clean(self):
246 cleaned_data = super(PostCaptchaForm, self).clean()
247 cleaned_data = super(PostCaptchaForm, self).clean()
247
248
248 success = self.is_valid()
249 success = self.is_valid()
249 utils.update_captcha_access(self.request, success)
250 utils.update_captcha_access(self.request, success)
250
251
251 if success:
252 if success:
252 return cleaned_data
253 return cleaned_data
253 else:
254 else:
254 raise forms.ValidationError(_("Captcha validation failed"))
255 raise forms.ValidationError(_("Captcha validation failed"))
255
256
256
257
257 class ThreadCaptchaForm(ThreadForm):
258 class ThreadCaptchaForm(ThreadForm):
258 captcha = CaptchaField()
259 captcha = CaptchaField()
259
260
260 def __init__(self, *args, **kwargs):
261 def __init__(self, *args, **kwargs):
261 self.request = kwargs['request']
262 self.request = kwargs['request']
262 del kwargs['request']
263 del kwargs['request']
263
264
264 super(ThreadCaptchaForm, self).__init__(*args, **kwargs)
265 super(ThreadCaptchaForm, self).__init__(*args, **kwargs)
265
266
266 def clean(self):
267 def clean(self):
267 cleaned_data = super(ThreadCaptchaForm, self).clean()
268 cleaned_data = super(ThreadCaptchaForm, self).clean()
268
269
269 success = self.is_valid()
270 success = self.is_valid()
270 utils.update_captcha_access(self.request, success)
271 utils.update_captcha_access(self.request, success)
271
272
272 if success:
273 if success:
273 return cleaned_data
274 return cleaned_data
274 else:
275 else:
275 raise forms.ValidationError(_("Captcha validation failed"))
276 raise forms.ValidationError(_("Captcha validation failed"))
276
277
277
278
278 class SettingsForm(NeboardForm):
279 class SettingsForm(NeboardForm):
279
280
280 theme = forms.ChoiceField(choices=settings.THEMES,
281 theme = forms.ChoiceField(choices=settings.THEMES,
281 label=_('Theme'))
282 label=_('Theme'))
282
283
283
284
284 class ModeratorSettingsForm(SettingsForm):
285 class ModeratorSettingsForm(SettingsForm):
285
286
286 moderate = forms.BooleanField(required=False, label=_('Enable moderation '
287 moderate = forms.BooleanField(required=False, label=_('Enable moderation '
287 'panel'))
288 'panel'))
288
289
289
290
290 class LoginForm(NeboardForm):
291 class LoginForm(NeboardForm):
291
292
292 user_id = forms.CharField()
293 user_id = forms.CharField()
293
294
294 session = None
295 session = None
295
296
296 def clean_user_id(self):
297 def clean_user_id(self):
297 user_id = self.cleaned_data['user_id']
298 user_id = self.cleaned_data['user_id']
298 if user_id:
299 if user_id:
299 users = User.objects.filter(user_id=user_id)
300 users = User.objects.filter(user_id=user_id)
300 if len(users) == 0:
301 if len(users) == 0:
301 raise forms.ValidationError(_('No such user found'))
302 raise forms.ValidationError(_('No such user found'))
302
303
303 return user_id
304 return user_id
304
305
305 def _validate_login_speed(self):
306 def _validate_login_speed(self):
306 can_post = True
307 can_post = True
307
308
308 if LAST_LOGIN_TIME in self.session:
309 if LAST_LOGIN_TIME in self.session:
309 now = time.time()
310 now = time.time()
310 last_login_time = self.session[LAST_LOGIN_TIME]
311 last_login_time = self.session[LAST_LOGIN_TIME]
311
312
312 current_delay = int(now - last_login_time)
313 current_delay = int(now - last_login_time)
313
314
314 if current_delay < board_settings.LOGIN_TIMEOUT:
315 if current_delay < board_settings.LOGIN_TIMEOUT:
315 error_message = _('Wait %s minutes after last login') % str(
316 error_message = _('Wait %s minutes after last login') % str(
316 (board_settings.LOGIN_TIMEOUT - current_delay) / 60)
317 (board_settings.LOGIN_TIMEOUT - current_delay) / 60)
317 self._errors['user_id'] = self.error_class([error_message])
318 self._errors['user_id'] = self.error_class([error_message])
318
319
319 can_post = False
320 can_post = False
320
321
321 if can_post:
322 if can_post:
322 self.session[LAST_LOGIN_TIME] = time.time()
323 self.session[LAST_LOGIN_TIME] = time.time()
323
324
324 def clean(self):
325 def clean(self):
325 if not self.session:
326 if not self.session:
326 raise forms.ValidationError('Humans have sessions')
327 raise forms.ValidationError('Humans have sessions')
327
328
328 self._validate_login_speed()
329 self._validate_login_speed()
329
330
330 cleaned_data = super(LoginForm, self).clean()
331 cleaned_data = super(LoginForm, self).clean()
331
332
332 return cleaned_data
333 return cleaned_data
333
334
334
335
335 class AddTagForm(NeboardForm):
336 class AddTagForm(NeboardForm):
336
337
337 tag = forms.CharField(max_length=TAG_MAX_LENGTH, label=LABEL_TAG)
338 tag = forms.CharField(max_length=TAG_MAX_LENGTH, label=LABEL_TAG)
338 method = forms.CharField(widget=forms.HiddenInput(), initial='add_tag')
339 method = forms.CharField(widget=forms.HiddenInput(), initial='add_tag')
339
340
340 def clean_tag(self):
341 def clean_tag(self):
341 tag = self.cleaned_data['tag']
342 tag = self.cleaned_data['tag']
342
343
343 regex_tag = re.compile(REGEX_TAG, re.UNICODE)
344 regex_tag = re.compile(REGEX_TAG, re.UNICODE)
344 if not regex_tag.match(tag):
345 if not regex_tag.match(tag):
345 raise forms.ValidationError(_('Inappropriate characters in tags.'))
346 raise forms.ValidationError(_('Inappropriate characters in tags.'))
346
347
347 return tag
348 return tag
348
349
349 def clean(self):
350 def clean(self):
350 cleaned_data = super(AddTagForm, self).clean()
351 cleaned_data = super(AddTagForm, self).clean()
351
352
352 return cleaned_data
353 return cleaned_data
353
354
354
355
355 class SearchForm(NeboardForm):
356 class SearchForm(NeboardForm):
356 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False) No newline at end of file
357 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
1 NO CONTENT: modified file, binary diff hidden
NO CONTENT: modified file, binary diff hidden
@@ -1,43 +1,32 b''
1 # SOME DESCRIPTIVE TITLE.
1 # SOME DESCRIPTIVE TITLE.
2 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
2 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3 # This file is distributed under the same license as the PACKAGE package.
3 # This file is distributed under the same license as the PACKAGE package.
4 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
4 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
5 #
5 #
6 #, fuzzy
6 #, fuzzy
7 msgid ""
7 msgid ""
8 msgstr ""
8 msgstr ""
9 "Project-Id-Version: PACKAGE VERSION\n"
9 "Project-Id-Version: PACKAGE VERSION\n"
10 "Report-Msgid-Bugs-To: \n"
10 "Report-Msgid-Bugs-To: \n"
11 "POT-Creation-Date: 2013-12-21 21:45+0200\n"
11 "POT-Creation-Date: 2014-07-02 13:26+0300\n"
12 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
12 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
13 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
13 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
14 "Language-Team: LANGUAGE <LL@li.org>\n"
14 "Language-Team: LANGUAGE <LL@li.org>\n"
15 "Language: \n"
15 "Language: \n"
16 "MIME-Version: 1.0\n"
16 "MIME-Version: 1.0\n"
17 "Content-Type: text/plain; charset=UTF-8\n"
17 "Content-Type: text/plain; charset=UTF-8\n"
18 "Content-Transfer-Encoding: 8bit\n"
18 "Content-Transfer-Encoding: 8bit\n"
19 "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
19 "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
20 "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
20 "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
21
21
22 #: static/js/refpopup.js:57
22 #: static/js/refpopup.js:58
23 msgid "Loading..."
23 msgid "Loading..."
24 msgstr "Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ°..."
24 msgstr "Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ°..."
25
25
26 #: static/js/refpopup.js:76
26 #: static/js/refpopup.js:77
27 msgid "Post not found"
27 msgid "Post not found"
28 msgstr "Π‘ΠΎΠΎΠ±Ρ‰Π΅Π½ΠΈΠ΅ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½ΠΎ"
28 msgstr "Π‘ΠΎΠΎΠ±Ρ‰Π΅Π½ΠΈΠ΅ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½ΠΎ"
29
29
30 #: static/js/thread.js:32
30 #: static/js/thread_update.js:279
31 msgid "Normal"
31 msgid "Sending message..."
32 msgstr "ΠΠΎΡ€ΠΌΠ°Π»ΡŒΠ½Ρ‹ΠΉ"
32 msgstr "ΠžΡ‚ΠΏΡ€Π°Π²ΠΊΠ° сообщСния..." No newline at end of file
33
34 #: static/js/thread.js:33
35 msgid "Gallery"
36 msgstr "ГалСрСя"
37
38 #: static/js/thread_update.js:177
39 msgid "[new posts]"
40 msgstr "[Π½ΠΎΠ²Ρ‹Π΅ посты]"
41
42 #~ msgid "Replies"
43 #~ msgstr "ΠžΡ‚Π²Π΅Ρ‚Ρ‹"
@@ -1,18 +1,20 b''
1 VERSION = '1.8.1 Kara'
1 VERSION = '1.8.1 Kara'
2 SITE_NAME = 'Neboard'
2 SITE_NAME = 'Neboard'
3
3
4 CACHE_TIMEOUT = 600 # Timeout for caching, if cache is used
4 CACHE_TIMEOUT = 600 # Timeout for caching, if cache is used
5 LOGIN_TIMEOUT = 3600 # Timeout between login tries
5 LOGIN_TIMEOUT = 3600 # Timeout between login tries
6 MAX_TEXT_LENGTH = 30000 # Max post length in characters
6 MAX_TEXT_LENGTH = 30000 # Max post length in characters
7 MAX_IMAGE_SIZE = 8 * 1024 * 1024 # Max image size
7 MAX_IMAGE_SIZE = 8 * 1024 * 1024 # Max image size
8
8
9 # Thread bumplimit
9 # Thread bumplimit
10 MAX_POSTS_PER_THREAD = 10
10 MAX_POSTS_PER_THREAD = 10
11 # Old posts will be archived or deleted if this value is reached
11 # Old posts will be archived or deleted if this value is reached
12 MAX_THREAD_COUNT = 5
12 MAX_THREAD_COUNT = 5
13 THREADS_PER_PAGE = 3
13 THREADS_PER_PAGE = 3
14 DEFAULT_THEME = 'md'
14 DEFAULT_THEME = 'md'
15 LAST_REPLIES_COUNT = 3
15 LAST_REPLIES_COUNT = 3
16
16
17 # Enable archiving threads instead of deletion when the thread limit is reached
17 # Enable archiving threads instead of deletion when the thread limit is reached
18 ARCHIVE_THREADS = True No newline at end of file
18 ARCHIVE_THREADS = True
19 # Limit posting speed
20 LIMIT_POSTING_SPEED = False No newline at end of file
@@ -1,272 +1,288 b''
1 /*
1 /*
2 @licstart The following is the entire license notice for the
2 @licstart The following is the entire license notice for the
3 JavaScript code in this page.
3 JavaScript code in this page.
4
4
5
5
6 Copyright (C) 2013 neko259
6 Copyright (C) 2013 neko259
7
7
8 The JavaScript code in this page is free software: you can
8 The JavaScript code in this page is free software: you can
9 redistribute it and/or modify it under the terms of the GNU
9 redistribute it and/or modify it under the terms of the GNU
10 General Public License (GNU GPL) as published by the Free Software
10 General Public License (GNU GPL) as published by the Free Software
11 Foundation, either version 3 of the License, or (at your option)
11 Foundation, either version 3 of the License, or (at your option)
12 any later version. The code is distributed WITHOUT ANY WARRANTY;
12 any later version. The code is distributed WITHOUT ANY WARRANTY;
13 without even the implied warranty of MERCHANTABILITY or FITNESS
13 without even the implied warranty of MERCHANTABILITY or FITNESS
14 FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
14 FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
15
15
16 As additional permission under GNU GPL version 3 section 7, you
16 As additional permission under GNU GPL version 3 section 7, you
17 may distribute non-source (e.g., minimized or compacted) forms of
17 may distribute non-source (e.g., minimized or compacted) forms of
18 that code without the copy of the GNU GPL normally required by
18 that code without the copy of the GNU GPL normally required by
19 section 4, provided you include this license notice and a URL
19 section 4, provided you include this license notice and a URL
20 through which recipients can access the Corresponding Source.
20 through which recipients can access the Corresponding Source.
21
21
22 @licend The above is the entire license notice
22 @licend The above is the entire license notice
23 for the JavaScript code in this page.
23 for the JavaScript code in this page.
24 */
24 */
25
25
26 var THREAD_UPDATE_DELAY = 10000;
26 var THREAD_UPDATE_DELAY = 10000;
27
27
28 var loading = false;
28 var loading = false;
29 var lastUpdateTime = null;
29 var lastUpdateTime = null;
30 var unreadPosts = 0
30 var unreadPosts = 0;
31
31
32 function blink(node) {
32 function blink(node) {
33 var blinkCount = 2;
33 var blinkCount = 2;
34
34
35 var nodeToAnimate = node;
35 var nodeToAnimate = node;
36 for (var i = 0; i < blinkCount; i++) {
36 for (var i = 0; i < blinkCount; i++) {
37 nodeToAnimate = nodeToAnimate.fadeTo('fast', 0.5).fadeTo('fast', 1.0);
37 nodeToAnimate = nodeToAnimate.fadeTo('fast', 0.5).fadeTo('fast', 1.0);
38 }
38 }
39 }
39 }
40
40
41 function updateThread() {
41 function updateThread() {
42 if (loading) {
42 if (loading) {
43 return;
43 return;
44 }
44 }
45
45
46 loading = true;
46 loading = true;
47
47
48 var threadPosts = $('div.thread').children('.post');
48 var threadPosts = $('div.thread').children('.post');
49
49
50 var lastPost = threadPosts.last();
50 var lastPost = threadPosts.last();
51 var threadId = threadPosts.first().attr('id');
51 var threadId = threadPosts.first().attr('id');
52
52
53 var diffUrl = '/api/diff_thread/' + threadId + '/' + lastUpdateTime + '/';
53 var diffUrl = '/api/diff_thread/' + threadId + '/' + lastUpdateTime + '/';
54 $.getJSON(diffUrl)
54 $.getJSON(diffUrl)
55 .success(function(data) {
55 .success(function(data) {
56 var bottom = isPageBottom();
56 var bottom = isPageBottom();
57
57
58 var lastUpdate = '';
58 var lastUpdate = '';
59
59
60 var addedPosts = data.added;
60 var addedPosts = data.added;
61 for (var i = 0; i < addedPosts.length; i++) {
61 for (var i = 0; i < addedPosts.length; i++) {
62 var postText = addedPosts[i];
62 var postText = addedPosts[i];
63
63
64 var post = $(postText);
64 var post = $(postText);
65
65
66 if (lastUpdate === '') {
66 if (lastUpdate === '') {
67 lastUpdate = post.find('.pub_time').text();
67 lastUpdate = post.find('.pub_time').text();
68 }
68 }
69
69
70 post.appendTo(lastPost.parent());
70 post.appendTo(lastPost.parent());
71 processNewPost(post);
71 processNewPost(post);
72
72
73 lastPost = post;
73 lastPost = post;
74 blink(post);
74 blink(post);
75 }
75 }
76
76
77 var updatedPosts = data.updated;
77 var updatedPosts = data.updated;
78 for (var i = 0; i < updatedPosts.length; i++) {
78 for (var i = 0; i < updatedPosts.length; i++) {
79 var postText = updatedPosts[i];
79 var postText = updatedPosts[i];
80
80
81 var post = $(postText);
81 var post = $(postText);
82
82
83 if (lastUpdate === '') {
83 if (lastUpdate === '') {
84 lastUpdate = post.find('.pub_time').text();
84 lastUpdate = post.find('.pub_time').text();
85 }
85 }
86
86
87 var postId = post.attr('id');
87 var postId = post.attr('id');
88
88
89 var oldPost = $('div.thread').children('.post[id=' + postId + ']');
89 var oldPost = $('div.thread').children('.post[id=' + postId + ']');
90
90
91 oldPost.replaceWith(post);
91 oldPost.replaceWith(post);
92 processNewPost(post);
92 processNewPost(post);
93
93
94 blink(post);
94 blink(post);
95 }
95 }
96
96
97 // TODO Process deleted posts
97 // TODO Process deleted posts
98
98
99 lastUpdateTime = data.last_update;
99 lastUpdateTime = data.last_update;
100 loading = false;
100 loading = false;
101
101
102 if (bottom) {
102 if (bottom) {
103 scrollToBottom();
103 scrollToBottom();
104 }
104 }
105
105
106 var hasPostChanges = (updatedPosts.length > 0)
106 var hasPostChanges = (updatedPosts.length > 0)
107 || (addedPosts.length > 0);
107 || (addedPosts.length > 0);
108 if (hasPostChanges) {
108 if (hasPostChanges) {
109 updateMetadataPanel(lastUpdate);
109 updateMetadataPanel(lastUpdate);
110 }
110 }
111
111
112 updateBumplimitProgress(data.added.length);
112 updateBumplimitProgress(data.added.length);
113
113
114 if (data.added.length + data.updated.length > 0) {
114 if (data.added.length + data.updated.length > 0) {
115 showNewPostsTitle(data.added.length);
115 showNewPostsTitle(data.added.length);
116 }
116 }
117 })
117 })
118 .error(function(data) {
118 .error(function(data) {
119 // TODO Show error message that server is unavailable?
119 // TODO Show error message that server is unavailable?
120
120
121 loading = false;
121 loading = false;
122 });
122 });
123 }
123 }
124
124
125 function isPageBottom() {
125 function isPageBottom() {
126 var scroll = $(window).scrollTop() / ($(document).height()
126 var scroll = $(window).scrollTop() / ($(document).height()
127 - $(window).height())
127 - $(window).height())
128
128
129 return scroll == 1
129 return scroll == 1
130 }
130 }
131
131
132 function initAutoupdate() {
132 function initAutoupdate() {
133 loading = false;
133 loading = false;
134
134
135 lastUpdateTime = $('.metapanel').attr('data-last-update');
135 lastUpdateTime = $('.metapanel').attr('data-last-update');
136
136
137 setInterval(updateThread, THREAD_UPDATE_DELAY);
137 setInterval(updateThread, THREAD_UPDATE_DELAY);
138 }
138 }
139
139
140 function getReplyCount() {
140 function getReplyCount() {
141 return $('.thread').children('.post').length
141 return $('.thread').children('.post').length
142 }
142 }
143
143
144 function getImageCount() {
144 function getImageCount() {
145 return $('.thread').find('img').length
145 return $('.thread').find('img').length
146 }
146 }
147
147
148 function updateMetadataPanel(lastUpdate) {
148 function updateMetadataPanel(lastUpdate) {
149 var replyCountField = $('#reply-count');
149 var replyCountField = $('#reply-count');
150 var imageCountField = $('#image-count');
150 var imageCountField = $('#image-count');
151
151
152 replyCountField.text(getReplyCount());
152 replyCountField.text(getReplyCount());
153 imageCountField.text(getImageCount());
153 imageCountField.text(getImageCount());
154
154
155 if (lastUpdate !== '') {
155 if (lastUpdate !== '') {
156 var lastUpdateField = $('#last-update');
156 var lastUpdateField = $('#last-update');
157 lastUpdateField.text(lastUpdate);
157 lastUpdateField.text(lastUpdate);
158 blink(lastUpdateField);
158 blink(lastUpdateField);
159 }
159 }
160
160
161 blink(replyCountField);
161 blink(replyCountField);
162 blink(imageCountField);
162 blink(imageCountField);
163 }
163 }
164
164
165 /**
165 /**
166 * Update bumplimit progress bar
166 * Update bumplimit progress bar
167 */
167 */
168 function updateBumplimitProgress(postDelta) {
168 function updateBumplimitProgress(postDelta) {
169 var progressBar = $('#bumplimit_progress');
169 var progressBar = $('#bumplimit_progress');
170 if (progressBar) {
170 if (progressBar) {
171 var postsToLimitElement = $('#left_to_limit');
171 var postsToLimitElement = $('#left_to_limit');
172
172
173 var oldPostsToLimit = parseInt(postsToLimitElement.text());
173 var oldPostsToLimit = parseInt(postsToLimitElement.text());
174 var postCount = getReplyCount();
174 var postCount = getReplyCount();
175 var bumplimit = postCount - postDelta + oldPostsToLimit;
175 var bumplimit = postCount - postDelta + oldPostsToLimit;
176
176
177 var newPostsToLimit = bumplimit - postCount;
177 var newPostsToLimit = bumplimit - postCount;
178 if (newPostsToLimit <= 0) {
178 if (newPostsToLimit <= 0) {
179 $('.bar-bg').remove();
179 $('.bar-bg').remove();
180 $('.thread').children('.post').addClass('dead_post');
180 $('.thread').children('.post').addClass('dead_post');
181 } else {
181 } else {
182 postsToLimitElement.text(newPostsToLimit);
182 postsToLimitElement.text(newPostsToLimit);
183 progressBar.width((100 - postCount / bumplimit * 100.0) + '%');
183 progressBar.width((100 - postCount / bumplimit * 100.0) + '%');
184 }
184 }
185 }
185 }
186 }
186 }
187
187
188 var documentOriginalTitle = '';
188 var documentOriginalTitle = '';
189 /**
189 /**
190 * Show 'new posts' text in the title if the document is not visible to a user
190 * Show 'new posts' text in the title if the document is not visible to a user
191 */
191 */
192 function showNewPostsTitle(newPostCount) {
192 function showNewPostsTitle(newPostCount) {
193 if (document.hidden) {
193 if (document.hidden) {
194 if (documentOriginalTitle === '') {
194 if (documentOriginalTitle === '') {
195 documentOriginalTitle = document.title;
195 documentOriginalTitle = document.title;
196 }
196 }
197 unreadPosts = unreadPosts + newPostCount;
197 unreadPosts = unreadPosts + newPostCount;
198 document.title = '[' + unreadPosts + '] ' + documentOriginalTitle;
198 document.title = '[' + unreadPosts + '] ' + documentOriginalTitle;
199
199
200 document.addEventListener('visibilitychange', function() {
200 document.addEventListener('visibilitychange', function() {
201 if (documentOriginalTitle !== '') {
201 if (documentOriginalTitle !== '') {
202 document.title = documentOriginalTitle;
202 document.title = documentOriginalTitle;
203 documentOriginalTitle = '';
203 documentOriginalTitle = '';
204 unreadPosts = 0;
204 unreadPosts = 0;
205 }
205 }
206
206
207 document.removeEventListener('visibilitychange', null);
207 document.removeEventListener('visibilitychange', null);
208 });
208 });
209 }
209 }
210 }
210 }
211
211
212 /**
212 /**
213 * Clear all entered values in the form fields
213 * Clear all entered values in the form fields
214 */
214 */
215 function resetForm(form) {
215 function resetForm(form) {
216 form.find('input:text, input:password, input:file, select, textarea').val('');
216 form.find('input:text, input:password, input:file, select, textarea').val('');
217 form.find('input:radio, input:checkbox')
217 form.find('input:radio, input:checkbox')
218 .removeAttr('checked').removeAttr('selected');
218 .removeAttr('checked').removeAttr('selected');
219 $('.file_wrap').find('.file-thumb').remove();
219 $('.file_wrap').find('.file-thumb').remove();
220 }
220 }
221
221
222 /**
222 /**
223 * When the form is posted, this method will be run as a callback
223 * When the form is posted, this method will be run as a callback
224 */
224 */
225 function updateOnPost(response, statusText, xhr, form) {
225 function updateOnPost(response, statusText, xhr, form) {
226 var json = $.parseJSON(response);
226 var json = $.parseJSON(response);
227 var status = json.status;
227 var status = json.status;
228
228
229 form.children('.form-errors').remove();
229 showAsErrors(form, '');
230
230
231 if (status === 'ok') {
231 if (status === 'ok') {
232 resetForm(form);
232 resetForm(form);
233 updateThread();
233 updateThread();
234 } else {
234 } else {
235 var errors = json.errors;
235 var errors = json.errors;
236 for (var i = 0; i < errors.length; i++) {
236 for (var i = 0; i < errors.length; i++) {
237 var fieldErrors = errors[i];
237 var fieldErrors = errors[i];
238
238
239 var error = fieldErrors.errors;
239 var error = fieldErrors.errors;
240
240
241 var errorList = $('<div class="form-errors">' + error
241 showAsErrors(form, error);
242 + '<div>');
243 errorList.appendTo(form);
244 }
242 }
245 }
243 }
246 }
244 }
247
245
248 /**
246 /**
247 * Show text in the errors row of the form.
248 * @param form
249 * @param text
250 */
251 function showAsErrors(form, text) {
252 form.children('.form-errors').remove();
253
254 if (text.length > 0) {
255 var errorList = $('<div class="form-errors">' + text
256 + '<div>');
257 errorList.appendTo(form);
258 }
259 }
260
261 /**
249 * Run js methods that are usually run on the document, on the new post
262 * Run js methods that are usually run on the document, on the new post
250 */
263 */
251 function processNewPost(post) {
264 function processNewPost(post) {
252 addRefLinkPreview(post[0]);
265 addRefLinkPreview(post[0]);
253 highlightCode(post);
266 highlightCode(post);
254 }
267 }
255
268
256 $(document).ready(function(){
269 $(document).ready(function(){
257 initAutoupdate();
270 initAutoupdate();
258
271
259 // Post form data over AJAX
272 // Post form data over AJAX
260 var threadId = $('div.thread').children('.post').first().attr('id');;
273 var threadId = $('div.thread').children('.post').first().attr('id');
261
274
262 var form = $('#form');
275 var form = $('#form');
263
276
264 var options = {
277 var options = {
278 beforeSubmit: function(arr, $form, options) {
279 showAsErrors($('form'), gettext('Sending message...'));
280 },
265 success: updateOnPost,
281 success: updateOnPost,
266 url: '/api/add_post/' + threadId + '/'
282 url: '/api/add_post/' + threadId + '/'
267 };
283 };
268
284
269 form.ajaxForm(options);
285 form.ajaxForm(options);
270
286
271 resetForm(form);
287 resetForm(form);
272 });
288 });
General Comments 0
You need to be logged in to leave comments. Login now