##// END OF EJS Templates
Added required tags. At least one such tag is needed to create a thread. All...
neko259 -
r922:d17fdea9 default
parent child Browse files
Show More
@@ -0,0 +1,20 b''
1 # -*- coding: utf-8 -*-
2 from __future__ import unicode_literals
3
4 from django.db import models, migrations
5
6
7 class Migration(migrations.Migration):
8
9 dependencies = [
10 ('boards', '0003_remove_tag_threads'),
11 ]
12
13 operations = [
14 migrations.AddField(
15 model_name='tag',
16 name='required',
17 field=models.BooleanField(default=False),
18 preserve_default=True,
19 ),
20 ]
@@ -1,294 +1,305 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, Tag
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 posting_delay = settings.POSTING_DELAY
184 posting_delay = settings.POSTING_DELAY
185
185
186 if board_settings.LIMIT_POSTING_SPEED and LAST_POST_TIME in \
186 if board_settings.LIMIT_POSTING_SPEED and LAST_POST_TIME in \
187 self.session:
187 self.session:
188 now = time.time()
188 now = time.time()
189 last_post_time = self.session[LAST_POST_TIME]
189 last_post_time = self.session[LAST_POST_TIME]
190
190
191 current_delay = int(now - last_post_time)
191 current_delay = int(now - last_post_time)
192
192
193 if current_delay < posting_delay:
193 if current_delay < posting_delay:
194 error_message = _('Wait %s seconds after last posting') % str(
194 error_message = _('Wait %s seconds after last posting') % str(
195 posting_delay - current_delay)
195 posting_delay - current_delay)
196 self._errors['text'] = self.error_class([error_message])
196 self._errors['text'] = self.error_class([error_message])
197
197
198 can_post = False
198 can_post = False
199
199
200 if can_post:
200 if can_post:
201 self.session[LAST_POST_TIME] = time.time()
201 self.session[LAST_POST_TIME] = time.time()
202
202
203
203
204 class ThreadForm(PostForm):
204 class ThreadForm(PostForm):
205
205
206 regex_tags = re.compile(r'^[\w\s\d]+$', re.UNICODE)
206 regex_tags = re.compile(r'^[\w\s\d]+$', re.UNICODE)
207
207
208 tags = forms.CharField(
208 tags = forms.CharField(
209 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
209 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
210 max_length=100, label=_('Tags'), required=True)
210 max_length=100, label=_('Tags'), required=True)
211
211
212 def clean_tags(self):
212 def clean_tags(self):
213 tags = self.cleaned_data['tags'].strip()
213 tags = self.cleaned_data['tags'].strip()
214
214
215 if not tags or not self.regex_tags.match(tags):
215 if not tags or not self.regex_tags.match(tags):
216 raise forms.ValidationError(
216 raise forms.ValidationError(
217 _('Inappropriate characters in tags.'))
217 _('Inappropriate characters in tags.'))
218
218
219 tag_models = []
220 required_tag_exists = False
221 for tag in tags.split():
222 tag_model = Tag.objects.filter(name=tag.strip().lower(),
223 required=True)
224 if tag_model.exists():
225 required_tag_exists = True
226
227 if not required_tag_exists:
228 raise forms.ValidationError(_('Need at least 1 required tag.'))
229
219 return tags
230 return tags
220
231
221 def clean(self):
232 def clean(self):
222 cleaned_data = super(ThreadForm, self).clean()
233 cleaned_data = super(ThreadForm, self).clean()
223
234
224 return cleaned_data
235 return cleaned_data
225
236
226
237
227 class SettingsForm(NeboardForm):
238 class SettingsForm(NeboardForm):
228
239
229 theme = forms.ChoiceField(choices=settings.THEMES,
240 theme = forms.ChoiceField(choices=settings.THEMES,
230 label=_('Theme'))
241 label=_('Theme'))
231
242
232
243
233 class AddTagForm(NeboardForm):
244 class AddTagForm(NeboardForm):
234
245
235 tag = forms.CharField(max_length=TAG_MAX_LENGTH, label=LABEL_TAG)
246 tag = forms.CharField(max_length=TAG_MAX_LENGTH, label=LABEL_TAG)
236 method = forms.CharField(widget=forms.HiddenInput(), initial='add_tag')
247 method = forms.CharField(widget=forms.HiddenInput(), initial='add_tag')
237
248
238 def clean_tag(self):
249 def clean_tag(self):
239 tag = self.cleaned_data['tag']
250 tag = self.cleaned_data['tag']
240
251
241 regex_tag = re.compile(REGEX_TAG, re.UNICODE)
252 regex_tag = re.compile(REGEX_TAG, re.UNICODE)
242 if not regex_tag.match(tag):
253 if not regex_tag.match(tag):
243 raise forms.ValidationError(_('Inappropriate characters in tags.'))
254 raise forms.ValidationError(_('Inappropriate characters in tags.'))
244
255
245 return tag
256 return tag
246
257
247 def clean(self):
258 def clean(self):
248 cleaned_data = super(AddTagForm, self).clean()
259 cleaned_data = super(AddTagForm, self).clean()
249
260
250 return cleaned_data
261 return cleaned_data
251
262
252
263
253 class SearchForm(NeboardForm):
264 class SearchForm(NeboardForm):
254 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
265 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
255
266
256
267
257 class LoginForm(NeboardForm):
268 class LoginForm(NeboardForm):
258
269
259 password = forms.CharField()
270 password = forms.CharField()
260
271
261 session = None
272 session = None
262
273
263 def clean_password(self):
274 def clean_password(self):
264 password = self.cleaned_data['password']
275 password = self.cleaned_data['password']
265 if board_settings.MASTER_PASSWORD != password:
276 if board_settings.MASTER_PASSWORD != password:
266 raise forms.ValidationError(_('Invalid master password'))
277 raise forms.ValidationError(_('Invalid master password'))
267
278
268 return password
279 return password
269
280
270 def _validate_login_speed(self):
281 def _validate_login_speed(self):
271 can_post = True
282 can_post = True
272
283
273 if LAST_LOGIN_TIME in self.session:
284 if LAST_LOGIN_TIME in self.session:
274 now = time.time()
285 now = time.time()
275 last_login_time = self.session[LAST_LOGIN_TIME]
286 last_login_time = self.session[LAST_LOGIN_TIME]
276
287
277 current_delay = int(now - last_login_time)
288 current_delay = int(now - last_login_time)
278
289
279 if current_delay < board_settings.LOGIN_TIMEOUT:
290 if current_delay < board_settings.LOGIN_TIMEOUT:
280 error_message = _('Wait %s minutes after last login') % str(
291 error_message = _('Wait %s minutes after last login') % str(
281 (board_settings.LOGIN_TIMEOUT - current_delay) / 60)
292 (board_settings.LOGIN_TIMEOUT - current_delay) / 60)
282 self._errors['password'] = self.error_class([error_message])
293 self._errors['password'] = self.error_class([error_message])
283
294
284 can_post = False
295 can_post = False
285
296
286 if can_post:
297 if can_post:
287 self.session[LAST_LOGIN_TIME] = time.time()
298 self.session[LAST_LOGIN_TIME] = time.time()
288
299
289 def clean(self):
300 def clean(self):
290 self._validate_login_speed()
301 self._validate_login_speed()
291
302
292 cleaned_data = super(LoginForm, self).clean()
303 cleaned_data = super(LoginForm, self).clean()
293
304
294 return cleaned_data
305 return cleaned_data
1 NO CONTENT: modified file, binary diff hidden
NO CONTENT: modified file, binary diff hidden
@@ -1,367 +1,370 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 msgid ""
6 msgid ""
7 msgstr ""
7 msgstr ""
8 "Project-Id-Version: PACKAGE VERSION\n"
8 "Project-Id-Version: PACKAGE VERSION\n"
9 "Report-Msgid-Bugs-To: \n"
9 "Report-Msgid-Bugs-To: \n"
10 "POT-Creation-Date: 2014-11-18 16:31+0200\n"
10 "POT-Creation-Date: 2015-01-08 16:36+0200\n"
11 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
11 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
12 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
12 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
13 "Language-Team: LANGUAGE <LL@li.org>\n"
13 "Language-Team: LANGUAGE <LL@li.org>\n"
14 "Language: ru\n"
14 "Language: ru\n"
15 "MIME-Version: 1.0\n"
15 "MIME-Version: 1.0\n"
16 "Content-Type: text/plain; charset=UTF-8\n"
16 "Content-Type: text/plain; charset=UTF-8\n"
17 "Content-Transfer-Encoding: 8bit\n"
17 "Content-Transfer-Encoding: 8bit\n"
18 "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
18 "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
19 "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
19 "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
20
20
21 #: authors.py:9
21 #: authors.py:9
22 msgid "author"
22 msgid "author"
23 msgstr "автор"
23 msgstr "автор"
24
24
25 #: authors.py:10
25 #: authors.py:10
26 msgid "developer"
26 msgid "developer"
27 msgstr "разработчик"
27 msgstr "разработчик"
28
28
29 #: authors.py:11
29 #: authors.py:11
30 msgid "javascript developer"
30 msgid "javascript developer"
31 msgstr "разработчик javascript"
31 msgstr "разработчик javascript"
32
32
33 #: authors.py:12
33 #: authors.py:12
34 msgid "designer"
34 msgid "designer"
35 msgstr "дизайнер"
35 msgstr "дизайнер"
36
36
37 #: forms.py:22
37 #: forms.py:22
38 msgid "Type message here. Use formatting panel for more advanced usage."
38 msgid "Type message here. Use formatting panel for more advanced usage."
39 msgstr ""
39 msgstr ""
40 "Вводите сообщение сюда. Используйте панель для более сложного форматирования."
40 "Вводите сообщение сюда. Используйте панель для более сложного форматирования."
41
41
42 #: forms.py:23
42 #: forms.py:23
43 msgid "tag1 several_words_tag"
43 msgid "tag1 several_words_tag"
44 msgstr "тег1 тег_из_нескольких_слов"
44 msgstr "метка1 метка_из_нескольких_слов"
45
45
46 #: forms.py:25
46 #: forms.py:25
47 msgid "Such image was already posted"
47 msgid "Such image was already posted"
48 msgstr "Такое изображение уже было загружено"
48 msgstr "Такое изображение уже было загружено"
49
49
50 #: forms.py:27
50 #: forms.py:27
51 msgid "Title"
51 msgid "Title"
52 msgstr "Заголовок"
52 msgstr "Заголовок"
53
53
54 #: forms.py:28
54 #: forms.py:28
55 msgid "Text"
55 msgid "Text"
56 msgstr "Текст"
56 msgstr "Текст"
57
57
58 #: forms.py:29
58 #: forms.py:29
59 msgid "Tag"
59 msgid "Tag"
60 msgstr "Тег"
60 msgstr "Метка"
61
61
62 #: forms.py:30 templates/boards/base.html:40 templates/search/search.html:9
62 #: forms.py:30 templates/boards/base.html:38 templates/search/search.html:9
63 #: templates/search/search.html.py:13
63 #: templates/search/search.html.py:13
64 msgid "Search"
64 msgid "Search"
65 msgstr "Поиск"
65 msgstr "Поиск"
66
66
67 #: forms.py:107
67 #: forms.py:107
68 msgid "Image"
68 msgid "Image"
69 msgstr "Изображение"
69 msgstr "Изображение"
70
70
71 #: forms.py:112
71 #: forms.py:112
72 msgid "e-mail"
72 msgid "e-mail"
73 msgstr ""
73 msgstr ""
74
74
75 #: forms.py:123
75 #: forms.py:123
76 #, python-format
76 #, python-format
77 msgid "Title must have less than %s characters"
77 msgid "Title must have less than %s characters"
78 msgstr "Заголовок должен иметь меньше %s символов"
78 msgstr "Заголовок должен иметь меньше %s символов"
79
79
80 #: forms.py:132
80 #: forms.py:132
81 #, python-format
81 #, python-format
82 msgid "Text must have less than %s characters"
82 msgid "Text must have less than %s characters"
83 msgstr "Текст должен быть короче %s символов"
83 msgstr "Текст должен быть короче %s символов"
84
84
85 #: forms.py:143
85 #: forms.py:143
86 #, python-format
86 #, python-format
87 msgid "Image must be less than %s bytes"
87 msgid "Image must be less than %s bytes"
88 msgstr "Изображение должно быть менее %s байт"
88 msgstr "Изображение должно быть менее %s байт"
89
89
90 #: forms.py:178
90 #: forms.py:178
91 msgid "Either text or image must be entered."
91 msgid "Either text or image must be entered."
92 msgstr "Текст или картинка должны быть введены."
92 msgstr "Текст или картинка должны быть введены."
93
93
94 #: forms.py:194
94 #: forms.py:194
95 #, python-format
95 #, python-format
96 msgid "Wait %s seconds after last posting"
96 msgid "Wait %s seconds after last posting"
97 msgstr "Подождите %s секунд после последнего постинга"
97 msgstr "Подождите %s секунд после последнего постинга"
98
98
99 #: forms.py:210 templates/boards/rss/post.html:10 templates/boards/tags.html:7
99 #: forms.py:210 templates/boards/rss/post.html:10 templates/boards/tags.html:7
100 msgid "Tags"
100 msgid "Tags"
101 msgstr "Теги"
101 msgstr "Метки"
102
102
103 #: forms.py:217 forms.py:243
103 #: forms.py:217 forms.py:254
104 msgid "Inappropriate characters in tags."
104 msgid "Inappropriate characters in tags."
105 msgstr "Недопустимые символы в тегах."
105 msgstr "Недопустимые символы в метках."
106
106
107 #: forms.py:230
107 #: forms.py:228
108 msgid "Need at least 1 required tag."
109 msgstr "Нужна хотя бы 1 обязательная метка."
110
111 #: forms.py:241
108 msgid "Theme"
112 msgid "Theme"
109 msgstr "Тема"
113 msgstr "Тема"
110
114
111 #: forms.py:266
115 #: forms.py:277
112 msgid "Invalid master password"
116 msgid "Invalid master password"
113 msgstr "Неверный мастер-пароль"
117 msgstr "Неверный мастер-пароль"
114
118
115 #: forms.py:280
119 #: forms.py:291
116 #, python-format
120 #, python-format
117 msgid "Wait %s minutes after last login"
121 msgid "Wait %s minutes after last login"
118 msgstr "Подождите %s минут после последнего входа"
122 msgstr "Подождите %s минут после последнего входа"
119
123
120 #: templates/boards/404.html:6
124 #: templates/boards/404.html:6
121 msgid "Not found"
125 msgid "Not found"
122 msgstr "Не найдено"
126 msgstr "Не найдено"
123
127
124 #: templates/boards/404.html:12
128 #: templates/boards/404.html:12
125 msgid "This page does not exist"
129 msgid "This page does not exist"
126 msgstr "Этой страницы не существует"
130 msgstr "Этой страницы не существует"
127
131
128 #: templates/boards/authors.html:6 templates/boards/authors.html.py:12
132 #: templates/boards/authors.html:6 templates/boards/authors.html.py:12
129 msgid "Authors"
133 msgid "Authors"
130 msgstr "Авторы"
134 msgstr "Авторы"
131
135
132 #: templates/boards/authors.html:26
136 #: templates/boards/authors.html:26
133 msgid "Distributed under the"
137 msgid "Distributed under the"
134 msgstr "Распространяется под"
138 msgstr "Распространяется под"
135
139
136 #: templates/boards/authors.html:28
140 #: templates/boards/authors.html:28
137 msgid "license"
141 msgid "license"
138 msgstr "лицензией"
142 msgstr "лицензией"
139
143
140 #: templates/boards/authors.html:30
144 #: templates/boards/authors.html:30
141 msgid "Repository"
145 msgid "Repository"
142 msgstr "Репозиторий"
146 msgstr "Репозиторий"
143
147
144 #: templates/boards/base.html:16
148 #: templates/boards/base.html:13
145 msgid "Feed"
149 msgid "Feed"
146 msgstr "Лента"
150 msgstr "Лента"
147
151
148 #: templates/boards/base.html:33
152 #: templates/boards/base.html:30
149 msgid "All threads"
153 msgid "All threads"
150 msgstr "Все темы"
154 msgstr "Все темы"
151
155
152 #: templates/boards/base.html:38
156 #: templates/boards/base.html:36
153 msgid "Tag management"
157 msgid "Tag management"
154 msgstr "Управление тегами"
158 msgstr "Управление метками"
155
159
156 #: templates/boards/base.html:41 templates/boards/settings.html:7
160 #: templates/boards/base.html:39 templates/boards/settings.html:7
157 msgid "Settings"
161 msgid "Settings"
158 msgstr "Настройки"
162 msgstr "Настройки"
159
163
160 #: templates/boards/base.html:56
164 #: templates/boards/base.html:52
161 msgid "Admin"
165 msgid "Admin"
162 msgstr ""
166 msgstr ""
163
167
164 #: templates/boards/base.html:58
168 #: templates/boards/base.html:54
165 #, python-format
169 #, python-format
166 msgid "Speed: %(ppd)s posts per day"
170 msgid "Speed: %(ppd)s posts per day"
167 msgstr "Скорость: %(ppd)s сообщений в день"
171 msgstr "Скорость: %(ppd)s сообщений в день"
168
172
169 #: templates/boards/base.html:60
173 #: templates/boards/base.html:56
170 msgid "Up"
174 msgid "Up"
171 msgstr "Вверх"
175 msgstr "Вверх"
172
176
173 #: templates/boards/login.html:6 templates/boards/login.html.py:16
177 #: templates/boards/login.html:6 templates/boards/login.html.py:16
174 msgid "Login"
178 msgid "Login"
175 msgstr "Вход"
179 msgstr "Вход"
176
180
177 #: templates/boards/login.html:19
181 #: templates/boards/login.html:19
178 msgid "Insert your user id above"
182 msgid "Insert your user id above"
179 msgstr "Вставьте свой ID пользователя выше"
183 msgstr "Вставьте свой ID пользователя выше"
180
184
181 #: templates/boards/post.html:21 templates/boards/staticpages/help.html:17
185 #: templates/boards/post.html:19 templates/boards/staticpages/help.html:17
182 msgid "Quote"
186 msgid "Quote"
183 msgstr "Цитата"
187 msgstr "Цитата"
184
188
185 #: templates/boards/post.html:31
189 #: templates/boards/post.html:27
186 msgid "Open"
190 msgid "Open"
187 msgstr "Открыть"
191 msgstr "Открыть"
188
192
189 #: templates/boards/post.html:33
193 #: templates/boards/post.html:29
190 msgid "Reply"
194 msgid "Reply"
191 msgstr "Ответ"
195 msgstr "Ответ"
192
196
193 #: templates/boards/post.html:40
197 #: templates/boards/post.html:36
194 msgid "Edit"
198 msgid "Edit"
195 msgstr "Изменить"
199 msgstr "Изменить"
196
200
197 #: templates/boards/post.html:42
201 #: templates/boards/post.html:39
198 msgid "Edit thread"
202 msgid "Edit thread"
199 msgstr "Изменить тему"
203 msgstr "Изменить тему"
200
204
201 #: templates/boards/post.html:73
205 #: templates/boards/post.html:71
202 msgid "Replies"
206 msgid "Replies"
203 msgstr "Ответы"
207 msgstr "Ответы"
204
208
205 #: templates/boards/post.html:83 templates/boards/thread.html:100
209 #: templates/boards/post.html:79 templates/boards/thread.html:89
206 #: templates/boards/thread_gallery.html:59
210 #: templates/boards/thread_gallery.html:59
207 msgid "messages"
211 msgid "messages"
208 msgstr "сообщений"
212 msgstr "сообщений"
209
213
210 #: templates/boards/post.html:84 templates/boards/thread.html:101
214 #: templates/boards/post.html:80 templates/boards/thread.html:90
211 #: templates/boards/thread_gallery.html:60
215 #: templates/boards/thread_gallery.html:60
212 msgid "images"
216 msgid "images"
213 msgstr "изображений"
217 msgstr "изображений"
214
218
215 #: templates/boards/post_admin.html:19
219 #: templates/boards/post_admin.html:19
216 msgid "Tags:"
220 msgid "Tags:"
217 msgstr "Теги:"
221 msgstr "Метки:"
218
222
219 #: templates/boards/post_admin.html:30
223 #: templates/boards/post_admin.html:30
220 msgid "Add tag"
224 msgid "Add tag"
221 msgstr "Добавить тег"
225 msgstr "Добавить метку"
222
226
223 #: templates/boards/posting_general.html:56
227 #: templates/boards/posting_general.html:56
224 msgid "Show tag"
228 msgid "Show tag"
225 msgstr "Показывать тег"
229 msgstr "Показывать метку"
226
230
227 #: templates/boards/posting_general.html:60
231 #: templates/boards/posting_general.html:60
228 msgid "Hide tag"
232 msgid "Hide tag"
229 msgstr "Скрывать тег"
233 msgstr "Скрывать метку"
230
234
231 #: templates/boards/posting_general.html:79 templates/search/search.html:22
235 #: templates/boards/posting_general.html:66
236 msgid "Edit tag"
237 msgstr "Изменить метку"
238
239 #: templates/boards/posting_general.html:82 templates/search/search.html:22
232 msgid "Previous page"
240 msgid "Previous page"
233 msgstr "Предыдущая страница"
241 msgstr "Предыдущая страница"
234
242
235 #: templates/boards/posting_general.html:94
243 #: templates/boards/posting_general.html:97
236 #, python-format
244 #, python-format
237 msgid "Skipped %(count)s replies. Open thread to see all replies."
245 msgid "Skipped %(count)s replies. Open thread to see all replies."
238 msgstr "Пропущено %(count)s ответов. Откройте тред, чтобы увидеть все ответы."
246 msgstr "Пропущено %(count)s ответов. Откройте тред, чтобы увидеть все ответы."
239
247
240 #: templates/boards/posting_general.html:121 templates/search/search.html:33
248 #: templates/boards/posting_general.html:124 templates/search/search.html:33
241 msgid "Next page"
249 msgid "Next page"
242 msgstr "Следующая страница"
250 msgstr "Следующая страница"
243
251
244 #: templates/boards/posting_general.html:126
252 #: templates/boards/posting_general.html:129
245 msgid "No threads exist. Create the first one!"
253 msgid "No threads exist. Create the first one!"
246 msgstr "Нет тем. Создайте первую!"
254 msgstr "Нет тем. Создайте первую!"
247
255
248 #: templates/boards/posting_general.html:132
256 #: templates/boards/posting_general.html:135
249 msgid "Create new thread"
257 msgid "Create new thread"
250 msgstr "Создать новую тему"
258 msgstr "Создать новую тему"
251
259
252 #: templates/boards/posting_general.html:137 templates/boards/preview.html:16
260 #: templates/boards/posting_general.html:140 templates/boards/preview.html:16
253 #: templates/boards/thread.html:59
261 #: templates/boards/thread.html:54
254 msgid "Post"
262 msgid "Post"
255 msgstr "Отправить"
263 msgstr "Отправить"
256
264
257 #: templates/boards/posting_general.html:142
265 #: templates/boards/posting_general.html:145
258 msgid "Tags must be delimited by spaces. Text or image is required."
266 msgid "Tags must be delimited by spaces. Text or image is required."
259 msgstr ""
267 msgstr ""
260 "Теги должны быть разделены пробелами. Текст или изображение обязательны."
268 "Метки должны быть разделены пробелами. Текст или изображение обязательны."
261
269
262 #: templates/boards/posting_general.html:145 templates/boards/thread.html:67
270 #: templates/boards/posting_general.html:148 templates/boards/thread.html:62
263 msgid "Text syntax"
271 msgid "Text syntax"
264 msgstr "Синтаксис текста"
272 msgstr "Синтаксис текста"
265
273
266 #: templates/boards/posting_general.html:157
274 #: templates/boards/posting_general.html:160
267 msgid "Pages:"
275 msgid "Pages:"
268 msgstr "Страницы: "
276 msgstr "Страницы: "
269
277
270 #: templates/boards/preview.html:6 templates/boards/staticpages/help.html:19
278 #: templates/boards/preview.html:6 templates/boards/staticpages/help.html:19
271 msgid "Preview"
279 msgid "Preview"
272 msgstr "Предпросмотр"
280 msgstr "Предпросмотр"
273
281
274 #: templates/boards/rss/post.html:5
282 #: templates/boards/rss/post.html:5
275 msgid "Post image"
283 msgid "Post image"
276 msgstr "Изображение сообщения"
284 msgstr "Изображение сообщения"
277
285
278 #: templates/boards/settings.html:15
286 #: templates/boards/settings.html:15
279 msgid "You are moderator."
287 msgid "You are moderator."
280 msgstr "Вы модератор."
288 msgstr "Вы модератор."
281
289
282 #: templates/boards/settings.html:19
290 #: templates/boards/settings.html:19
283 msgid "Hidden tags:"
291 msgid "Hidden tags:"
284 msgstr "Скрытые теги:"
292 msgstr "Скрытые метки:"
285
293
286 #: templates/boards/settings.html:26
294 #: templates/boards/settings.html:26
287 msgid "No hidden tags."
295 msgid "No hidden tags."
288 msgstr "Нет скрытых тегов."
296 msgstr "Нет скрытых меток."
289
297
290 #: templates/boards/settings.html:35
298 #: templates/boards/settings.html:35
291 msgid "Save"
299 msgid "Save"
292 msgstr "Сохранить"
300 msgstr "Сохранить"
293
301
294 #: templates/boards/staticpages/banned.html:6
302 #: templates/boards/staticpages/banned.html:6
295 msgid "Banned"
303 msgid "Banned"
296 msgstr "Заблокирован"
304 msgstr "Заблокирован"
297
305
298 #: templates/boards/staticpages/banned.html:11
306 #: templates/boards/staticpages/banned.html:11
299 msgid "Your IP address has been banned. Contact the administrator"
307 msgid "Your IP address has been banned. Contact the administrator"
300 msgstr "Ваш IP адрес был заблокирован. Свяжитесь с администратором"
308 msgstr "Ваш IP адрес был заблокирован. Свяжитесь с администратором"
301
309
302 #: templates/boards/staticpages/help.html:6
310 #: templates/boards/staticpages/help.html:6
303 #: templates/boards/staticpages/help.html:10
311 #: templates/boards/staticpages/help.html:10
304 msgid "Syntax"
312 msgid "Syntax"
305 msgstr "Синтаксис"
313 msgstr "Синтаксис"
306
314
307 #: templates/boards/staticpages/help.html:11
315 #: templates/boards/staticpages/help.html:11
308 msgid "Italic text"
316 msgid "Italic text"
309 msgstr "Курсивный текст"
317 msgstr "Курсивный текст"
310
318
311 #: templates/boards/staticpages/help.html:12
319 #: templates/boards/staticpages/help.html:12
312 msgid "Bold text"
320 msgid "Bold text"
313 msgstr "Полужирный текст"
321 msgstr "Полужирный текст"
314
322
315 #: templates/boards/staticpages/help.html:13
323 #: templates/boards/staticpages/help.html:13
316 msgid "Spoiler"
324 msgid "Spoiler"
317 msgstr "Спойлер"
325 msgstr "Спойлер"
318
326
319 #: templates/boards/staticpages/help.html:14
327 #: templates/boards/staticpages/help.html:14
320 msgid "Link to a post"
328 msgid "Link to a post"
321 msgstr "Ссылка на сообщение"
329 msgstr "Ссылка на сообщение"
322
330
323 #: templates/boards/staticpages/help.html:15
331 #: templates/boards/staticpages/help.html:15
324 msgid "Strikethrough text"
332 msgid "Strikethrough text"
325 msgstr "Зачеркнутый текст"
333 msgstr "Зачеркнутый текст"
326
334
327 #: templates/boards/staticpages/help.html:16
335 #: templates/boards/staticpages/help.html:16
328 msgid "Comment"
336 msgid "Comment"
329 msgstr "Комментарий"
337 msgstr "Комментарий"
330
338
331 #: templates/boards/staticpages/help.html:19
339 #: templates/boards/staticpages/help.html:19
332 msgid "You can try pasting the text and previewing the result here:"
340 msgid "You can try pasting the text and previewing the result here:"
333 msgstr "Вы можете попробовать вставить текст и проверить результат здесь:"
341 msgstr "Вы можете попробовать вставить текст и проверить результат здесь:"
334
342
335 #: templates/boards/tags.html:22
343 #: templates/boards/tags.html:23
336 msgid "No tags found."
344 msgid "No tags found."
337 msgstr "Теги не найдены."
345 msgstr "Метки не найдены."
338
346
339 #: templates/boards/thread.html:21 templates/boards/thread_gallery.html:19
347 #: templates/boards/thread.html:19 templates/boards/thread_gallery.html:19
340 msgid "Normal mode"
348 msgid "Normal mode"
341 msgstr "Нормальный режим"
349 msgstr "Нормальный режим"
342
350
343 #: templates/boards/thread.html:22 templates/boards/thread_gallery.html:20
351 #: templates/boards/thread.html:20 templates/boards/thread_gallery.html:20
344 msgid "Gallery mode"
352 msgid "Gallery mode"
345 msgstr "Режим галереи"
353 msgstr "Режим галереи"
346
354
347 #: templates/boards/thread.html:30
355 #: templates/boards/thread.html:28
348 msgid "posts to bumplimit"
356 msgid "posts to bumplimit"
349 msgstr "сообщений до бамплимита"
357 msgstr "сообщений до бамплимита"
350
358
351 #: templates/boards/thread.html:51
359 #: templates/boards/thread.html:46
352 msgid "Reply to thread"
360 msgid "Reply to thread"
353 msgstr "Ответить в тему"
361 msgstr "Ответить в тему"
354
362
355 #: templates/boards/thread.html:64
363 #: templates/boards/thread.html:59
356 msgid "Switch mode"
364 msgid "Switch mode"
357 msgstr "Переключить режим"
365 msgstr "Переключить режим"
358
366
359 #: templates/boards/thread.html:102 templates/boards/thread_gallery.html:61
367 #: templates/boards/thread.html:91 templates/boards/thread_gallery.html:61
360 msgid "Last update: "
368 msgid "Last update: "
361 msgstr "Последнее обновление: "
369 msgstr "Последнее обновление: "
362
370
363 #~ msgid "Delete"
364 #~ msgstr "Удалить"
365
366 #~ msgid "Ban IP"
367 #~ msgstr "Заблокировать IP"
@@ -1,10 +1,18 b''
1 __author__ = 'neko259'
1 __author__ = 'neko259'
2
2
3
3
4 class Viewable():
4 class Viewable():
5 def __init__(self):
5 def __init__(self):
6 pass
6 pass
7
7
8 def get_view(self, *args, **kwargs):
8 def get_view(self, *args, **kwargs):
9 """Get an HTML view for a model"""
9 """
10 pass No newline at end of file
10 Gets an HTML view for a model
11 """
12 pass
13
14 def get_search_view(self, *args, **kwargs):
15 """
16 Gets an HTML view for search.
17 """
18 pass
@@ -1,438 +1,441 b''
1 from datetime import datetime, timedelta, date
1 from datetime import datetime, timedelta, date
2 from datetime import time as dtime
2 from datetime import time as dtime
3 import logging
3 import logging
4 import re
4 import re
5
5
6 from adjacent import Client
6 from adjacent import Client
7 from django.core.cache import cache
7 from django.core.cache import cache
8 from django.core.urlresolvers import reverse
8 from django.core.urlresolvers import reverse
9 from django.db import models, transaction
9 from django.db import models, transaction
10 from django.db.models import TextField
10 from django.db.models import TextField
11 from django.template.loader import render_to_string
11 from django.template.loader import render_to_string
12 from django.utils import timezone
12 from django.utils import timezone
13
13
14 from boards import settings
14 from boards import settings
15 from boards.mdx_neboard import bbcode_extended
15 from boards.mdx_neboard import bbcode_extended
16 from boards.models import PostImage
16 from boards.models import PostImage
17 from boards.models.base import Viewable
17 from boards.models.base import Viewable
18 from boards.models.thread import Thread
18 from boards.models.thread import Thread
19 from boards.utils import datetime_to_epoch
19 from boards.utils import datetime_to_epoch
20
20
21
21
22 WS_NOTIFICATION_TYPE_NEW_POST = 'new_post'
22 WS_NOTIFICATION_TYPE_NEW_POST = 'new_post'
23 WS_NOTIFICATION_TYPE = 'notification_type'
23 WS_NOTIFICATION_TYPE = 'notification_type'
24
24
25 WS_CHANNEL_THREAD = "thread:"
25 WS_CHANNEL_THREAD = "thread:"
26
26
27 APP_LABEL_BOARDS = 'boards'
27 APP_LABEL_BOARDS = 'boards'
28
28
29 CACHE_KEY_PPD = 'ppd'
29 CACHE_KEY_PPD = 'ppd'
30 CACHE_KEY_POST_URL = 'post_url'
30 CACHE_KEY_POST_URL = 'post_url'
31
31
32 POSTS_PER_DAY_RANGE = 7
32 POSTS_PER_DAY_RANGE = 7
33
33
34 BAN_REASON_AUTO = 'Auto'
34 BAN_REASON_AUTO = 'Auto'
35
35
36 IMAGE_THUMB_SIZE = (200, 150)
36 IMAGE_THUMB_SIZE = (200, 150)
37
37
38 TITLE_MAX_LENGTH = 200
38 TITLE_MAX_LENGTH = 200
39
39
40 # TODO This should be removed
40 # TODO This should be removed
41 NO_IP = '0.0.0.0'
41 NO_IP = '0.0.0.0'
42
42
43 # TODO Real user agent should be saved instead of this
43 # TODO Real user agent should be saved instead of this
44 UNKNOWN_UA = ''
44 UNKNOWN_UA = ''
45
45
46 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
46 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
47
47
48 PARAMETER_TRUNCATED = 'truncated'
48 PARAMETER_TRUNCATED = 'truncated'
49 PARAMETER_TAG = 'tag'
49 PARAMETER_TAG = 'tag'
50 PARAMETER_OFFSET = 'offset'
50 PARAMETER_OFFSET = 'offset'
51 PARAMETER_DIFF_TYPE = 'type'
51 PARAMETER_DIFF_TYPE = 'type'
52 PARAMETER_BUMPABLE = 'bumpable'
52 PARAMETER_BUMPABLE = 'bumpable'
53 PARAMETER_THREAD = 'thread'
53 PARAMETER_THREAD = 'thread'
54 PARAMETER_IS_OPENING = 'is_opening'
54 PARAMETER_IS_OPENING = 'is_opening'
55 PARAMETER_MODERATOR = 'moderator'
55 PARAMETER_MODERATOR = 'moderator'
56 PARAMETER_POST = 'post'
56 PARAMETER_POST = 'post'
57 PARAMETER_OP_ID = 'opening_post_id'
57 PARAMETER_OP_ID = 'opening_post_id'
58 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
58 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
59
59
60 DIFF_TYPE_HTML = 'html'
60 DIFF_TYPE_HTML = 'html'
61 DIFF_TYPE_JSON = 'json'
61 DIFF_TYPE_JSON = 'json'
62
62
63 PREPARSE_PATTERNS = {
63 PREPARSE_PATTERNS = {
64 r'>>(\d+)': r'[post]\1[/post]', # Reflink ">>123"
64 r'>>(\d+)': r'[post]\1[/post]', # Reflink ">>123"
65 r'^>(.+)': r'[quote]\1[/quote]', # Quote ">text"
65 r'^>(.+)': r'[quote]\1[/quote]', # Quote ">text"
66 r'^//(.+)': r'[comment]\1[/comment]', # Comment "//text"
66 r'^//(.+)': r'[comment]\1[/comment]', # Comment "//text"
67 }
67 }
68
68
69
69
70 class PostManager(models.Manager):
70 class PostManager(models.Manager):
71 @transaction.atomic
71 @transaction.atomic
72 def create_post(self, title: str, text: str, image=None, thread=None,
72 def create_post(self, title: str, text: str, image=None, thread=None,
73 ip=NO_IP, tags: list=None):
73 ip=NO_IP, tags: list=None):
74 """
74 """
75 Creates new post
75 Creates new post
76 """
76 """
77
77
78 if not tags:
78 if not tags:
79 tags = []
79 tags = []
80
80
81 posting_time = timezone.now()
81 posting_time = timezone.now()
82 if not thread:
82 if not thread:
83 thread = Thread.objects.create(bump_time=posting_time,
83 thread = Thread.objects.create(bump_time=posting_time,
84 last_edit_time=posting_time)
84 last_edit_time=posting_time)
85 new_thread = True
85 new_thread = True
86 else:
86 else:
87 new_thread = False
87 new_thread = False
88
88
89 pre_text = self._preparse_text(text)
89 pre_text = self._preparse_text(text)
90
90
91 post = self.create(title=title,
91 post = self.create(title=title,
92 text=pre_text,
92 text=pre_text,
93 pub_time=posting_time,
93 pub_time=posting_time,
94 thread_new=thread,
94 thread_new=thread,
95 poster_ip=ip,
95 poster_ip=ip,
96 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
96 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
97 # last!
97 # last!
98 last_edit_time=posting_time)
98 last_edit_time=posting_time)
99
99
100 logger = logging.getLogger('boards.post.create')
100 logger = logging.getLogger('boards.post.create')
101
101
102 logger.info('Created post {} by {}'.format(
102 logger.info('Created post {} by {}'.format(
103 post, post.poster_ip))
103 post, post.poster_ip))
104
104
105 if image:
105 if image:
106 post_image = PostImage.objects.create(image=image)
106 post_image = PostImage.objects.create(image=image)
107 post.images.add(post_image)
107 post.images.add(post_image)
108 logger.info('Created image #{} for post #{}'.format(
108 logger.info('Created image #{} for post #{}'.format(
109 post_image.id, post.id))
109 post_image.id, post.id))
110
110
111 thread.replies.add(post)
111 thread.replies.add(post)
112 list(map(thread.add_tag, tags))
112 list(map(thread.add_tag, tags))
113
113
114 if new_thread:
114 if new_thread:
115 Thread.objects.process_oldest_threads()
115 Thread.objects.process_oldest_threads()
116 else:
116 else:
117 thread.bump()
117 thread.bump()
118 thread.last_edit_time = posting_time
118 thread.last_edit_time = posting_time
119 thread.save()
119 thread.save()
120
120
121 self.connect_replies(post)
121 self.connect_replies(post)
122
122
123 return post
123 return post
124
124
125 def delete_posts_by_ip(self, ip):
125 def delete_posts_by_ip(self, ip):
126 """
126 """
127 Deletes all posts of the author with same IP
127 Deletes all posts of the author with same IP
128 """
128 """
129
129
130 posts = self.filter(poster_ip=ip)
130 posts = self.filter(poster_ip=ip)
131 for post in posts:
131 for post in posts:
132 post.delete()
132 post.delete()
133
133
134 def connect_replies(self, post):
134 def connect_replies(self, post):
135 """
135 """
136 Connects replies to a post to show them as a reflink map
136 Connects replies to a post to show them as a reflink map
137 """
137 """
138
138
139 for reply_number in re.finditer(REGEX_REPLY, post.get_raw_text()):
139 for reply_number in re.finditer(REGEX_REPLY, post.get_raw_text()):
140 post_id = reply_number.group(1)
140 post_id = reply_number.group(1)
141 ref_post = self.filter(id=post_id)
141 ref_post = self.filter(id=post_id)
142 if ref_post.count() > 0:
142 if ref_post.count() > 0:
143 referenced_post = ref_post[0]
143 referenced_post = ref_post[0]
144 referenced_post.referenced_posts.add(post)
144 referenced_post.referenced_posts.add(post)
145 referenced_post.last_edit_time = post.pub_time
145 referenced_post.last_edit_time = post.pub_time
146 referenced_post.build_refmap()
146 referenced_post.build_refmap()
147 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
147 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
148
148
149 referenced_thread = referenced_post.get_thread()
149 referenced_thread = referenced_post.get_thread()
150 referenced_thread.last_edit_time = post.pub_time
150 referenced_thread.last_edit_time = post.pub_time
151 referenced_thread.save(update_fields=['last_edit_time'])
151 referenced_thread.save(update_fields=['last_edit_time'])
152
152
153 def get_posts_per_day(self):
153 def get_posts_per_day(self):
154 """
154 """
155 Gets average count of posts per day for the last 7 days
155 Gets average count of posts per day for the last 7 days
156 """
156 """
157
157
158 day_end = date.today()
158 day_end = date.today()
159 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
159 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
160
160
161 cache_key = CACHE_KEY_PPD + str(day_end)
161 cache_key = CACHE_KEY_PPD + str(day_end)
162 ppd = cache.get(cache_key)
162 ppd = cache.get(cache_key)
163 if ppd:
163 if ppd:
164 return ppd
164 return ppd
165
165
166 day_time_start = timezone.make_aware(datetime.combine(
166 day_time_start = timezone.make_aware(datetime.combine(
167 day_start, dtime()), timezone.get_current_timezone())
167 day_start, dtime()), timezone.get_current_timezone())
168 day_time_end = timezone.make_aware(datetime.combine(
168 day_time_end = timezone.make_aware(datetime.combine(
169 day_end, dtime()), timezone.get_current_timezone())
169 day_end, dtime()), timezone.get_current_timezone())
170
170
171 posts_per_period = float(self.filter(
171 posts_per_period = float(self.filter(
172 pub_time__lte=day_time_end,
172 pub_time__lte=day_time_end,
173 pub_time__gte=day_time_start).count())
173 pub_time__gte=day_time_start).count())
174
174
175 ppd = posts_per_period / POSTS_PER_DAY_RANGE
175 ppd = posts_per_period / POSTS_PER_DAY_RANGE
176
176
177 cache.set(cache_key, ppd)
177 cache.set(cache_key, ppd)
178 return ppd
178 return ppd
179
179
180 def _preparse_text(self, text):
180 def _preparse_text(self, text):
181 """
181 """
182 Preparses text to change patterns like '>>' to a proper bbcode
182 Preparses text to change patterns like '>>' to a proper bbcode
183 tags.
183 tags.
184 """
184 """
185
185
186 for key, value in PREPARSE_PATTERNS.items():
186 for key, value in PREPARSE_PATTERNS.items():
187 text = re.sub(key, value, text, flags=re.MULTILINE)
187 text = re.sub(key, value, text, flags=re.MULTILINE)
188
188
189 return text
189 return text
190
190
191
191
192 class Post(models.Model, Viewable):
192 class Post(models.Model, Viewable):
193 """A post is a message."""
193 """A post is a message."""
194
194
195 objects = PostManager()
195 objects = PostManager()
196
196
197 class Meta:
197 class Meta:
198 app_label = APP_LABEL_BOARDS
198 app_label = APP_LABEL_BOARDS
199 ordering = ('id',)
199 ordering = ('id',)
200
200
201 title = models.CharField(max_length=TITLE_MAX_LENGTH)
201 title = models.CharField(max_length=TITLE_MAX_LENGTH)
202 pub_time = models.DateTimeField()
202 pub_time = models.DateTimeField()
203 text = TextField(blank=True, null=True)
203 text = TextField(blank=True, null=True)
204 _text_rendered = TextField(blank=True, null=True, editable=False)
204 _text_rendered = TextField(blank=True, null=True, editable=False)
205
205
206 images = models.ManyToManyField(PostImage, null=True, blank=True,
206 images = models.ManyToManyField(PostImage, null=True, blank=True,
207 related_name='ip+', db_index=True)
207 related_name='ip+', db_index=True)
208
208
209 poster_ip = models.GenericIPAddressField()
209 poster_ip = models.GenericIPAddressField()
210 poster_user_agent = models.TextField()
210 poster_user_agent = models.TextField()
211
211
212 thread_new = models.ForeignKey('Thread', null=True, default=None,
212 thread_new = models.ForeignKey('Thread', null=True, default=None,
213 db_index=True)
213 db_index=True)
214 last_edit_time = models.DateTimeField()
214 last_edit_time = models.DateTimeField()
215
215
216 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
216 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
217 null=True,
217 null=True,
218 blank=True, related_name='rfp+',
218 blank=True, related_name='rfp+',
219 db_index=True)
219 db_index=True)
220 refmap = models.TextField(null=True, blank=True)
220 refmap = models.TextField(null=True, blank=True)
221
221
222 def __str__(self):
222 def __str__(self):
223 return 'P#{}/{}'.format(self.id, self.title)
223 return 'P#{}/{}'.format(self.id, self.title)
224
224
225 def get_title(self) -> str:
225 def get_title(self) -> str:
226 """
226 """
227 Gets original post title or part of its text.
227 Gets original post title or part of its text.
228 """
228 """
229
229
230 title = self.title
230 title = self.title
231 if not title:
231 if not title:
232 title = self.get_text()
232 title = self.get_text()
233
233
234 return title
234 return title
235
235
236 def build_refmap(self) -> None:
236 def build_refmap(self) -> None:
237 """
237 """
238 Builds a replies map string from replies list. This is a cache to stop
238 Builds a replies map string from replies list. This is a cache to stop
239 the server from recalculating the map on every post show.
239 the server from recalculating the map on every post show.
240 """
240 """
241 map_string = ''
241 map_string = ''
242
242
243 first = True
243 first = True
244 for refpost in self.referenced_posts.all():
244 for refpost in self.referenced_posts.all():
245 if not first:
245 if not first:
246 map_string += ', '
246 map_string += ', '
247 map_string += '<a href="%s">&gt;&gt;%s</a>' % (refpost.get_url(),
247 map_string += '<a href="%s">&gt;&gt;%s</a>' % (refpost.get_url(),
248 refpost.id)
248 refpost.id)
249 first = False
249 first = False
250
250
251 self.refmap = map_string
251 self.refmap = map_string
252
252
253 def get_sorted_referenced_posts(self):
253 def get_sorted_referenced_posts(self):
254 return self.refmap
254 return self.refmap
255
255
256 def is_referenced(self) -> bool:
256 def is_referenced(self) -> bool:
257 if not self.refmap:
257 if not self.refmap:
258 return False
258 return False
259 else:
259 else:
260 return len(self.refmap) > 0
260 return len(self.refmap) > 0
261
261
262 def is_opening(self) -> bool:
262 def is_opening(self) -> bool:
263 """
263 """
264 Checks if this is an opening post or just a reply.
264 Checks if this is an opening post or just a reply.
265 """
265 """
266
266
267 return self.get_thread().get_opening_post_id() == self.id
267 return self.get_thread().get_opening_post_id() == self.id
268
268
269 @transaction.atomic
269 @transaction.atomic
270 def add_tag(self, tag):
270 def add_tag(self, tag):
271 edit_time = timezone.now()
271 edit_time = timezone.now()
272
272
273 thread = self.get_thread()
273 thread = self.get_thread()
274 thread.add_tag(tag)
274 thread.add_tag(tag)
275 self.last_edit_time = edit_time
275 self.last_edit_time = edit_time
276 self.save(update_fields=['last_edit_time'])
276 self.save(update_fields=['last_edit_time'])
277
277
278 thread.last_edit_time = edit_time
278 thread.last_edit_time = edit_time
279 thread.save(update_fields=['last_edit_time'])
279 thread.save(update_fields=['last_edit_time'])
280
280
281 def get_url(self, thread=None):
281 def get_url(self, thread=None):
282 """
282 """
283 Gets full url to the post.
283 Gets full url to the post.
284 """
284 """
285
285
286 cache_key = CACHE_KEY_POST_URL + str(self.id)
286 cache_key = CACHE_KEY_POST_URL + str(self.id)
287 link = cache.get(cache_key)
287 link = cache.get(cache_key)
288
288
289 if not link:
289 if not link:
290 if not thread:
290 if not thread:
291 thread = self.get_thread()
291 thread = self.get_thread()
292
292
293 opening_id = thread.get_opening_post_id()
293 opening_id = thread.get_opening_post_id()
294
294
295 if self.id != opening_id:
295 if self.id != opening_id:
296 link = reverse('thread', kwargs={
296 link = reverse('thread', kwargs={
297 'post_id': opening_id}) + '#' + str(self.id)
297 'post_id': opening_id}) + '#' + str(self.id)
298 else:
298 else:
299 link = reverse('thread', kwargs={'post_id': self.id})
299 link = reverse('thread', kwargs={'post_id': self.id})
300
300
301 cache.set(cache_key, link)
301 cache.set(cache_key, link)
302
302
303 return link
303 return link
304
304
305 def get_thread(self) -> Thread:
305 def get_thread(self) -> Thread:
306 """
306 """
307 Gets post's thread.
307 Gets post's thread.
308 """
308 """
309
309
310 return self.thread_new
310 return self.thread_new
311
311
312 def get_referenced_posts(self):
312 def get_referenced_posts(self):
313 return self.referenced_posts.only('id', 'thread_new')
313 return self.referenced_posts.only('id', 'thread_new')
314
314
315 def get_view(self, moderator=False, need_open_link=False,
315 def get_view(self, moderator=False, need_open_link=False,
316 truncated=False, *args, **kwargs):
316 truncated=False, *args, **kwargs):
317 """
317 """
318 Renders post's HTML view. Some of the post params can be passed over
318 Renders post's HTML view. Some of the post params can be passed over
319 kwargs for the means of caching (if we view the thread, some params
319 kwargs for the means of caching (if we view the thread, some params
320 are same for every post and don't need to be computed over and over.
320 are same for every post and don't need to be computed over and over.
321 """
321 """
322
322
323 is_opening = kwargs.get(PARAMETER_IS_OPENING, self.is_opening())
323 is_opening = kwargs.get(PARAMETER_IS_OPENING, self.is_opening())
324 thread = kwargs.get(PARAMETER_THREAD, self.get_thread())
324 thread = kwargs.get(PARAMETER_THREAD, self.get_thread())
325 can_bump = kwargs.get(PARAMETER_BUMPABLE, thread.can_bump())
325 can_bump = kwargs.get(PARAMETER_BUMPABLE, thread.can_bump())
326
326
327 if is_opening:
327 if is_opening:
328 opening_post_id = self.id
328 opening_post_id = self.id
329 else:
329 else:
330 opening_post_id = thread.get_opening_post_id()
330 opening_post_id = thread.get_opening_post_id()
331
331
332 return render_to_string('boards/post.html', {
332 return render_to_string('boards/post.html', {
333 PARAMETER_POST: self,
333 PARAMETER_POST: self,
334 PARAMETER_MODERATOR: moderator,
334 PARAMETER_MODERATOR: moderator,
335 PARAMETER_IS_OPENING: is_opening,
335 PARAMETER_IS_OPENING: is_opening,
336 PARAMETER_THREAD: thread,
336 PARAMETER_THREAD: thread,
337 PARAMETER_BUMPABLE: can_bump,
337 PARAMETER_BUMPABLE: can_bump,
338 PARAMETER_NEED_OPEN_LINK: need_open_link,
338 PARAMETER_NEED_OPEN_LINK: need_open_link,
339 PARAMETER_TRUNCATED: truncated,
339 PARAMETER_TRUNCATED: truncated,
340 PARAMETER_OP_ID: opening_post_id,
340 PARAMETER_OP_ID: opening_post_id,
341 })
341 })
342
342
343 def get_search_view(self, *args, **kwargs):
344 return self.get_view(args, kwargs)
345
343 def get_first_image(self) -> PostImage:
346 def get_first_image(self) -> PostImage:
344 return self.images.earliest('id')
347 return self.images.earliest('id')
345
348
346 def delete(self, using=None):
349 def delete(self, using=None):
347 """
350 """
348 Deletes all post images and the post itself. If the post is opening,
351 Deletes all post images and the post itself. If the post is opening,
349 thread with all posts is deleted.
352 thread with all posts is deleted.
350 """
353 """
351
354
352 self.images.all().delete()
355 self.images.all().delete()
353
356
354 if self.is_opening():
357 if self.is_opening():
355 self.get_thread().delete()
358 self.get_thread().delete()
356 else:
359 else:
357 thread = self.get_thread()
360 thread = self.get_thread()
358 thread.last_edit_time = timezone.now()
361 thread.last_edit_time = timezone.now()
359 thread.save()
362 thread.save()
360
363
361 super(Post, self).delete(using)
364 super(Post, self).delete(using)
362
365
363 logging.getLogger('boards.post.delete').info(
366 logging.getLogger('boards.post.delete').info(
364 'Deleted post {}'.format(self))
367 'Deleted post {}'.format(self))
365
368
366 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
369 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
367 include_last_update=False):
370 include_last_update=False):
368 """
371 """
369 Gets post HTML or JSON data that can be rendered on a page or used by
372 Gets post HTML or JSON data that can be rendered on a page or used by
370 API.
373 API.
371 """
374 """
372
375
373 if format_type == DIFF_TYPE_HTML:
376 if format_type == DIFF_TYPE_HTML:
374 params = dict()
377 params = dict()
375 params['post'] = self
378 params['post'] = self
376 if PARAMETER_TRUNCATED in request.GET:
379 if PARAMETER_TRUNCATED in request.GET:
377 params[PARAMETER_TRUNCATED] = True
380 params[PARAMETER_TRUNCATED] = True
378
381
379 return render_to_string('boards/api_post.html', params)
382 return render_to_string('boards/api_post.html', params)
380 elif format_type == DIFF_TYPE_JSON:
383 elif format_type == DIFF_TYPE_JSON:
381 post_json = {
384 post_json = {
382 'id': self.id,
385 'id': self.id,
383 'title': self.title,
386 'title': self.title,
384 'text': self._text_rendered,
387 'text': self._text_rendered,
385 }
388 }
386 if self.images.exists():
389 if self.images.exists():
387 post_image = self.get_first_image()
390 post_image = self.get_first_image()
388 post_json['image'] = post_image.image.url
391 post_json['image'] = post_image.image.url
389 post_json['image_preview'] = post_image.image.url_200x150
392 post_json['image_preview'] = post_image.image.url_200x150
390 if include_last_update:
393 if include_last_update:
391 post_json['bump_time'] = datetime_to_epoch(
394 post_json['bump_time'] = datetime_to_epoch(
392 self.thread_new.bump_time)
395 self.thread_new.bump_time)
393 return post_json
396 return post_json
394
397
395 def send_to_websocket(self, request, recursive=True):
398 def send_to_websocket(self, request, recursive=True):
396 """
399 """
397 Sends post HTML data to the thread web socket.
400 Sends post HTML data to the thread web socket.
398 """
401 """
399
402
400 if not settings.WEBSOCKETS_ENABLED:
403 if not settings.WEBSOCKETS_ENABLED:
401 return
404 return
402
405
403 client = Client()
406 client = Client()
404
407
405 thread = self.get_thread()
408 thread = self.get_thread()
406 thread_id = thread.id
409 thread_id = thread.id
407 channel_name = WS_CHANNEL_THREAD + str(thread.get_opening_post_id())
410 channel_name = WS_CHANNEL_THREAD + str(thread.get_opening_post_id())
408 client.publish(channel_name, {
411 client.publish(channel_name, {
409 WS_NOTIFICATION_TYPE: WS_NOTIFICATION_TYPE_NEW_POST,
412 WS_NOTIFICATION_TYPE: WS_NOTIFICATION_TYPE_NEW_POST,
410 })
413 })
411 client.send()
414 client.send()
412
415
413 logger = logging.getLogger('boards.post.websocket')
416 logger = logging.getLogger('boards.post.websocket')
414
417
415 logger.info('Sent notification from post #{} to channel {}'.format(
418 logger.info('Sent notification from post #{} to channel {}'.format(
416 self.id, channel_name))
419 self.id, channel_name))
417
420
418 if recursive:
421 if recursive:
419 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
422 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
420 post_id = reply_number.group(1)
423 post_id = reply_number.group(1)
421 ref_post = Post.objects.filter(id=post_id)[0]
424 ref_post = Post.objects.filter(id=post_id)[0]
422
425
423 # If post is in this thread, its thread was already notified.
426 # If post is in this thread, its thread was already notified.
424 # Otherwise, notify its thread separately.
427 # Otherwise, notify its thread separately.
425 if ref_post.thread_new_id != thread_id:
428 if ref_post.thread_new_id != thread_id:
426 ref_post.send_to_websocket(request, recursive=False)
429 ref_post.send_to_websocket(request, recursive=False)
427
430
428 def save(self, force_insert=False, force_update=False, using=None,
431 def save(self, force_insert=False, force_update=False, using=None,
429 update_fields=None):
432 update_fields=None):
430 self._text_rendered = bbcode_extended(self.get_raw_text())
433 self._text_rendered = bbcode_extended(self.get_raw_text())
431
434
432 super().save(force_insert, force_update, using, update_fields)
435 super().save(force_insert, force_update, using, update_fields)
433
436
434 def get_text(self) -> str:
437 def get_text(self) -> str:
435 return self._text_rendered
438 return self._text_rendered
436
439
437 def get_raw_text(self) -> str:
440 def get_raw_text(self) -> str:
438 return self.text
441 return self.text
@@ -1,82 +1,94 b''
1 from django.template.loader import render_to_string
1 from django.template.loader import render_to_string
2 from django.db import models
2 from django.db import models
3 from django.db.models import Count, Sum
3 from django.db.models import Count, Sum
4 from django.core.urlresolvers import reverse
4 from django.core.urlresolvers import reverse
5
5
6 from boards.models import Thread
6 from boards.models import Thread
7 from boards.models.base import Viewable
7 from boards.models.base import Viewable
8
8
9
9
10 __author__ = 'neko259'
10 __author__ = 'neko259'
11
11
12
12
13 class TagManager(models.Manager):
13 class TagManager(models.Manager):
14
14
15 def get_not_empty_tags(self):
15 def get_not_empty_tags(self):
16 """
16 """
17 Gets tags that have non-archived threads.
17 Gets tags that have non-archived threads.
18 """
18 """
19
19
20 not_empty_tags = list()
20 not_empty_tags = list()
21 tags = self.order_by('name')
21 tags = self.order_by('-required', 'name')
22 for tag in tags:
22 for tag in tags:
23 if tag.get_thread_count() > 0:
23 if tag.get_thread_count() > 0:
24 not_empty_tags.append(tag)
24 not_empty_tags.append(tag)
25
25
26 return not_empty_tags
26 return not_empty_tags
27
27
28
28
29 class Tag(models.Model, Viewable):
29 class Tag(models.Model, Viewable):
30 """
30 """
31 A tag is a text node assigned to the thread. The tag serves as a board
31 A tag is a text node assigned to the thread. The tag serves as a board
32 section. There can be multiple tags for each thread
32 section. There can be multiple tags for each thread
33 """
33 """
34
34
35 objects = TagManager()
35 objects = TagManager()
36
36
37 class Meta:
37 class Meta:
38 app_label = 'boards'
38 app_label = 'boards'
39 ordering = ('name',)
39 ordering = ('name',)
40
40
41 name = models.CharField(max_length=100, db_index=True)
41 name = models.CharField(max_length=100, db_index=True)
42 required = models.BooleanField(default=False)
42
43
43 def __str__(self):
44 def __str__(self):
44 return self.name
45 return self.name
45
46
46 def is_empty(self) -> bool:
47 def is_empty(self) -> bool:
47 """
48 """
48 Checks if the tag has some threads.
49 Checks if the tag has some threads.
49 """
50 """
50
51
51 return self.get_thread_count() == 0
52 return self.get_thread_count() == 0
52
53
53 def get_thread_count(self) -> int:
54 def get_thread_count(self) -> int:
54 return self.get_threads().count()
55 return self.get_threads().count()
55
56
56 def get_post_count(self, archived=False):
57 def get_post_count(self, archived=False):
57 """
58 """
58 Gets posts count for the tag's threads.
59 Gets posts count for the tag's threads.
59 """
60 """
60
61
61 posts_count = 0
62 posts_count = 0
62
63
63 threads = self.get_threads().filter(archived=archived)
64 threads = self.get_threads().filter(archived=archived)
64 if threads.exists():
65 if threads.exists():
65 posts_count = threads.annotate(posts_count=Count('replies')) \
66 posts_count = threads.annotate(posts_count=Count('replies')) \
66 .aggregate(posts_sum=Sum('posts_count'))['posts_sum']
67 .aggregate(posts_sum=Sum('posts_count'))['posts_sum']
67
68
68 if not posts_count:
69 if not posts_count:
69 posts_count = 0
70 posts_count = 0
70
71
71 return posts_count
72 return posts_count
72
73
73 def get_url(self):
74 def get_url(self):
74 return reverse('tag', kwargs={'tag_name': self.name})
75 return reverse('tag', kwargs={'tag_name': self.name})
75
76
76 def get_view(self, *args, **kwargs):
77 def get_threads(self):
78 return Thread.objects.filter(tags__in=[self]).order_by('-bump_time')
79
80 def is_required(self):
81 return self.required
82
83 def get_view(self):
84 #prefix = '##' if self.is_required() else '#'
85 link = '<a class="tag" href="{}">{}</a>'.format(
86 self.get_url(), self.name)
87 if self.is_required():
88 link = '<b>{}</b>'.format(link)
89 return link
90
91 def get_search_view(self, *args, **kwargs):
77 return render_to_string('boards/tag.html', {
92 return render_to_string('boards/tag.html', {
78 'tag': self,
93 'tag': self,
79 })
94 })
80
81 def get_threads(self):
82 return Thread.objects.filter(tags__in=[self]).order_by('-bump_time')
@@ -1,335 +1,333 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-2014 neko259
6 Copyright (C) 2013-2014 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 wsUser = '';
26 var wsUser = '';
27
27
28 var loading = false;
28 var loading = false;
29 var unreadPosts = 0;
29 var unreadPosts = 0;
30 var documentOriginalTitle = '';
30 var documentOriginalTitle = '';
31
31
32 // Thread ID does not change, can be stored one time
32 // Thread ID does not change, can be stored one time
33 var threadId = $('div.thread').children('.post').first().attr('id');
33 var threadId = $('div.thread').children('.post').first().attr('id');
34
34
35 /**
35 /**
36 * Connect to websocket server and subscribe to thread updates. On any update we
36 * Connect to websocket server and subscribe to thread updates. On any update we
37 * request a thread diff.
37 * request a thread diff.
38 *
38 *
39 * @returns {boolean} true if connected, false otherwise
39 * @returns {boolean} true if connected, false otherwise
40 */
40 */
41 function connectWebsocket() {
41 function connectWebsocket() {
42 var metapanel = $('.metapanel')[0];
42 var metapanel = $('.metapanel')[0];
43
43
44 var wsHost = metapanel.getAttribute('data-ws-host');
44 var wsHost = metapanel.getAttribute('data-ws-host');
45 var wsPort = metapanel.getAttribute('data-ws-port');
45 var wsPort = metapanel.getAttribute('data-ws-port');
46
46
47 if (wsHost.length > 0 && wsPort.length > 0)
47 if (wsHost.length > 0 && wsPort.length > 0)
48 var centrifuge = new Centrifuge({
48 var centrifuge = new Centrifuge({
49 "url": 'ws://' + wsHost + ':' + wsPort + "/connection/websocket",
49 "url": 'ws://' + wsHost + ':' + wsPort + "/connection/websocket",
50 "project": metapanel.getAttribute('data-ws-project'),
50 "project": metapanel.getAttribute('data-ws-project'),
51 "user": wsUser,
51 "user": wsUser,
52 "timestamp": metapanel.getAttribute('data-last-update'),
52 "timestamp": metapanel.getAttribute('data-last-update'),
53 "token": metapanel.getAttribute('data-ws-token'),
53 "token": metapanel.getAttribute('data-ws-token'),
54 "debug": false
54 "debug": false
55 });
55 });
56
56
57 centrifuge.on('error', function(error_message) {
57 centrifuge.on('error', function(error_message) {
58 console.log("Error connecting to websocket server.");
58 console.log("Error connecting to websocket server.");
59 return false;
59 return false;
60 });
60 });
61
61
62 centrifuge.on('connect', function() {
62 centrifuge.on('connect', function() {
63 var channelName = 'thread:' + threadId;
63 var channelName = 'thread:' + threadId;
64 centrifuge.subscribe(channelName, function(message) {
64 centrifuge.subscribe(channelName, function(message) {
65 getThreadDiff();
65 getThreadDiff();
66 });
66 });
67
67
68 // For the case we closed the browser and missed some updates
68 // For the case we closed the browser and missed some updates
69 getThreadDiff();
69 getThreadDiff();
70 $('#autoupdate').text('[+]');
70 $('#autoupdate').text('[+]');
71 });
71 });
72
72
73 centrifuge.connect();
73 centrifuge.connect();
74
74
75 return true;
75 return true;
76 }
76 }
77
77
78 /**
78 /**
79 * Get diff of the posts from the current thread timestamp.
79 * Get diff of the posts from the current thread timestamp.
80 * This is required if the browser was closed and some post updates were
80 * This is required if the browser was closed and some post updates were
81 * missed.
81 * missed.
82 */
82 */
83 function getThreadDiff() {
83 function getThreadDiff() {
84 var lastUpdateTime = $('.metapanel').attr('data-last-update');
84 var lastUpdateTime = $('.metapanel').attr('data-last-update');
85
85
86 var diffUrl = '/api/diff_thread/' + threadId + '/' + lastUpdateTime + '/';
86 var diffUrl = '/api/diff_thread/' + threadId + '/' + lastUpdateTime + '/';
87
87
88 $.getJSON(diffUrl)
88 $.getJSON(diffUrl)
89 .success(function(data) {
89 .success(function(data) {
90 var addedPosts = data.added;
90 var addedPosts = data.added;
91
91
92 for (var i = 0; i < addedPosts.length; i++) {
92 for (var i = 0; i < addedPosts.length; i++) {
93 var postText = addedPosts[i];
93 var postText = addedPosts[i];
94 var post = $(postText);
94 var post = $(postText);
95
95
96 updatePost(post)
96 updatePost(post)
97
97
98 lastPost = post;
98 lastPost = post;
99 }
99 }
100
100
101 var updatedPosts = data.updated;
101 var updatedPosts = data.updated;
102
102
103 for (var i = 0; i < updatedPosts.length; i++) {
103 for (var i = 0; i < updatedPosts.length; i++) {
104 var postText = updatedPosts[i];
104 var postText = updatedPosts[i];
105 var post = $(postText);
105 var post = $(postText);
106
106
107 updatePost(post)
107 updatePost(post)
108 }
108 }
109
109
110 // TODO Process removed posts if any
110 // TODO Process removed posts if any
111 $('.metapanel').attr('data-last-update', data.last_update);
111 $('.metapanel').attr('data-last-update', data.last_update);
112 })
112 })
113 }
113 }
114
114
115 /**
115 /**
116 * Add or update the post on html page.
116 * Add or update the post on html page.
117 */
117 */
118 function updatePost(postHtml) {
118 function updatePost(postHtml) {
119 // This needs to be set on start because the page is scrolled after posts
119 // This needs to be set on start because the page is scrolled after posts
120 // are added or updated
120 // are added or updated
121 var bottom = isPageBottom();
121 var bottom = isPageBottom();
122
122
123 var post = $(postHtml);
123 var post = $(postHtml);
124
124
125 var threadBlock = $('div.thread');
125 var threadBlock = $('div.thread');
126
126
127 var lastUpdate = '';
127 var lastUpdate = '';
128
128
129 var postId = post.attr('id');
129 var postId = post.attr('id');
130
130
131 // If the post already exists, replace it. Otherwise add as a new one.
131 // If the post already exists, replace it. Otherwise add as a new one.
132 var existingPosts = threadBlock.children('.post[id=' + postId + ']');
132 var existingPosts = threadBlock.children('.post[id=' + postId + ']');
133
133
134 if (existingPosts.size() > 0) {
134 if (existingPosts.size() > 0) {
135 existingPosts.replaceWith(post);
135 existingPosts.replaceWith(post);
136 } else {
136 } else {
137 var threadPosts = threadBlock.children('.post');
137 var threadPosts = threadBlock.children('.post');
138 var lastPost = threadPosts.last();
138 var lastPost = threadPosts.last();
139
139
140 post.appendTo(lastPost.parent());
140 post.appendTo(lastPost.parent());
141
141
142 updateBumplimitProgress(1);
142 updateBumplimitProgress(1);
143 showNewPostsTitle(1);
143 showNewPostsTitle(1);
144
144
145 lastUpdate = post.children('.post-info').first()
145 lastUpdate = post.children('.post-info').first()
146 .children('.pub_time').first().text();
146 .children('.pub_time').first().text();
147
147
148 if (bottom) {
148 if (bottom) {
149 scrollToBottom();
149 scrollToBottom();
150 }
150 }
151 }
151 }
152
152
153 processNewPost(post);
153 processNewPost(post);
154 updateMetadataPanel(lastUpdate)
154 updateMetadataPanel(lastUpdate)
155 }
155 }
156
156
157 /**
157 /**
158 * Initiate a blinking animation on a node to show it was updated.
158 * Initiate a blinking animation on a node to show it was updated.
159 */
159 */
160 function blink(node) {
160 function blink(node) {
161 var blinkCount = 2;
161 var blinkCount = 2;
162
162
163 var nodeToAnimate = node;
163 var nodeToAnimate = node;
164 for (var i = 0; i < blinkCount; i++) {
164 for (var i = 0; i < blinkCount; i++) {
165 nodeToAnimate = nodeToAnimate.fadeTo('fast', 0.5).fadeTo('fast', 1.0);
165 nodeToAnimate = nodeToAnimate.fadeTo('fast', 0.5).fadeTo('fast', 1.0);
166 }
166 }
167 }
167 }
168
168
169 function isPageBottom() {
169 function isPageBottom() {
170 var scroll = $(window).scrollTop() / ($(document).height()
170 var scroll = $(window).scrollTop() / ($(document).height()
171 - $(window).height());
171 - $(window).height());
172
172
173 return scroll == 1
173 return scroll == 1
174 }
174 }
175
175
176 function initAutoupdate() {
176 function initAutoupdate() {
177 return connectWebsocket();
177 return connectWebsocket();
178 }
178 }
179
179
180 function getReplyCount() {
180 function getReplyCount() {
181 return $('.thread').children('.post').length
181 return $('.thread').children('.post').length
182 }
182 }
183
183
184 function getImageCount() {
184 function getImageCount() {
185 return $('.thread').find('img').length
185 return $('.thread').find('img').length
186 }
186 }
187
187
188 /**
188 /**
189 * Update post count, images count and last update time in the metadata
189 * Update post count, images count and last update time in the metadata
190 * panel.
190 * panel.
191 */
191 */
192 function updateMetadataPanel(lastUpdate) {
192 function updateMetadataPanel(lastUpdate) {
193 var replyCountField = $('#reply-count');
193 var replyCountField = $('#reply-count');
194 var imageCountField = $('#image-count');
194 var imageCountField = $('#image-count');
195
195
196 replyCountField.text(getReplyCount());
196 replyCountField.text(getReplyCount());
197 imageCountField.text(getImageCount());
197 imageCountField.text(getImageCount());
198
198
199 if (lastUpdate !== '') {
199 if (lastUpdate !== '') {
200 var lastUpdateField = $('#last-update');
200 var lastUpdateField = $('#last-update');
201 lastUpdateField.text(lastUpdate);
201 lastUpdateField.text(lastUpdate);
202 blink(lastUpdateField);
202 blink(lastUpdateField);
203 }
203 }
204
204
205 blink(replyCountField);
205 blink(replyCountField);
206 blink(imageCountField);
206 blink(imageCountField);
207 }
207 }
208
208
209 /**
209 /**
210 * Update bumplimit progress bar
210 * Update bumplimit progress bar
211 */
211 */
212 function updateBumplimitProgress(postDelta) {
212 function updateBumplimitProgress(postDelta) {
213 var progressBar = $('#bumplimit_progress');
213 var progressBar = $('#bumplimit_progress');
214 if (progressBar) {
214 if (progressBar) {
215 var postsToLimitElement = $('#left_to_limit');
215 var postsToLimitElement = $('#left_to_limit');
216
216
217 var oldPostsToLimit = parseInt(postsToLimitElement.text());
217 var oldPostsToLimit = parseInt(postsToLimitElement.text());
218 var postCount = getReplyCount();
218 var postCount = getReplyCount();
219 var bumplimit = postCount - postDelta + oldPostsToLimit;
219 var bumplimit = postCount - postDelta + oldPostsToLimit;
220
220
221 var newPostsToLimit = bumplimit - postCount;
221 var newPostsToLimit = bumplimit - postCount;
222 if (newPostsToLimit <= 0) {
222 if (newPostsToLimit <= 0) {
223 $('.bar-bg').remove();
223 $('.bar-bg').remove();
224 $('.thread').children('.post').addClass('dead_post');
224 $('.thread').children('.post').addClass('dead_post');
225 } else {
225 } else {
226 postsToLimitElement.text(newPostsToLimit);
226 postsToLimitElement.text(newPostsToLimit);
227 progressBar.width((100 - postCount / bumplimit * 100.0) + '%');
227 progressBar.width((100 - postCount / bumplimit * 100.0) + '%');
228 }
228 }
229 }
229 }
230 }
230 }
231
231
232 /**
232 /**
233 * Show 'new posts' text in the title if the document is not visible to a user
233 * Show 'new posts' text in the title if the document is not visible to a user
234 */
234 */
235 function showNewPostsTitle(newPostCount) {
235 function showNewPostsTitle(newPostCount) {
236 if (document.hidden) {
236 if (document.hidden) {
237 if (documentOriginalTitle === '') {
237 if (documentOriginalTitle === '') {
238 documentOriginalTitle = document.title;
238 documentOriginalTitle = document.title;
239 }
239 }
240 unreadPosts = unreadPosts + newPostCount;
240 unreadPosts = unreadPosts + newPostCount;
241 document.title = '[' + unreadPosts + '] ' + documentOriginalTitle;
241 document.title = '[' + unreadPosts + '] ' + documentOriginalTitle;
242
242
243 document.addEventListener('visibilitychange', function() {
243 document.addEventListener('visibilitychange', function() {
244 if (documentOriginalTitle !== '') {
244 if (documentOriginalTitle !== '') {
245 document.title = documentOriginalTitle;
245 document.title = documentOriginalTitle;
246 documentOriginalTitle = '';
246 documentOriginalTitle = '';
247 unreadPosts = 0;
247 unreadPosts = 0;
248 }
248 }
249
249
250 document.removeEventListener('visibilitychange', null);
250 document.removeEventListener('visibilitychange', null);
251 });
251 });
252 }
252 }
253 }
253 }
254
254
255 /**
255 /**
256 * Clear all entered values in the form fields
256 * Clear all entered values in the form fields
257 */
257 */
258 function resetForm(form) {
258 function resetForm(form) {
259 form.find('input:text, input:password, input:file, select, textarea').val('');
259 form.find('input:text, input:password, input:file, select, textarea').val('');
260 form.find('input:radio, input:checkbox')
260 form.find('input:radio, input:checkbox')
261 .removeAttr('checked').removeAttr('selected');
261 .removeAttr('checked').removeAttr('selected');
262 $('.file_wrap').find('.file-thumb').remove();
262 $('.file_wrap').find('.file-thumb').remove();
263 }
263 }
264
264
265 /**
265 /**
266 * When the form is posted, this method will be run as a callback
266 * When the form is posted, this method will be run as a callback
267 */
267 */
268 function updateOnPost(response, statusText, xhr, form) {
268 function updateOnPost(response, statusText, xhr, form) {
269 var json = $.parseJSON(response);
269 var json = $.parseJSON(response);
270 var status = json.status;
270 var status = json.status;
271
271
272 showAsErrors(form, '');
272 showAsErrors(form, '');
273
273
274 if (status === 'ok') {
274 if (status === 'ok') {
275 resetForm(form);
275 resetForm(form);
276 } else {
276 } else {
277 var errors = json.errors;
277 var errors = json.errors;
278 for (var i = 0; i < errors.length; i++) {
278 for (var i = 0; i < errors.length; i++) {
279 var fieldErrors = errors[i];
279 var fieldErrors = errors[i];
280
280
281 var error = fieldErrors.errors;
281 var error = fieldErrors.errors;
282
282
283 showAsErrors(form, error);
283 showAsErrors(form, error);
284 }
284 }
285 }
285 }
286
286
287 scrollToBottom();
287 scrollToBottom();
288 }
288 }
289
289
290 /**
290 /**
291 * Show text in the errors row of the form.
291 * Show text in the errors row of the form.
292 * @param form
292 * @param form
293 * @param text
293 * @param text
294 */
294 */
295 function showAsErrors(form, text) {
295 function showAsErrors(form, text) {
296 form.children('.form-errors').remove();
296 form.children('.form-errors').remove();
297
297
298 if (text.length > 0) {
298 if (text.length > 0) {
299 var errorList = $('<div class="form-errors">' + text
299 var errorList = $('<div class="form-errors">' + text
300 + '<div>');
300 + '<div>');
301 errorList.appendTo(form);
301 errorList.appendTo(form);
302 }
302 }
303 }
303 }
304
304
305 /**
305 /**
306 * Run js methods that are usually run on the document, on the new post
306 * Run js methods that are usually run on the document, on the new post
307 */
307 */
308 function processNewPost(post) {
308 function processNewPost(post) {
309 addRefLinkPreview(post[0]);
309 addRefLinkPreview(post[0]);
310 highlightCode(post);
310 highlightCode(post);
311 blink(post);
311 blink(post);
312 }
312 }
313
313
314 $(document).ready(function(){
314 $(document).ready(function(){
315 if ('WebSocket' in window) {
316 if (initAutoupdate()) {
315 if (initAutoupdate()) {
317 // Post form data over AJAX
316 // Post form data over AJAX
318 var threadId = $('div.thread').children('.post').first().attr('id');
317 var threadId = $('div.thread').children('.post').first().attr('id');
319
318
320 var form = $('#form');
319 var form = $('#form');
321
320
322 var options = {
321 var options = {
323 beforeSubmit: function(arr, $form, options) {
322 beforeSubmit: function(arr, $form, options) {
324 showAsErrors($('form'), gettext('Sending message...'));
323 showAsErrors($('form'), gettext('Sending message...'));
325 },
324 },
326 success: updateOnPost,
325 success: getThreadDiff,
327 url: '/api/add_post/' + threadId + '/'
326 url: '/api/add_post/' + threadId + '/'
328 };
327 };
329
328
330 form.ajaxForm(options);
329 form.ajaxForm(options);
331
330
332 resetForm(form);
331 resetForm(form);
333 }
332 }
334 }
335 });
333 });
@@ -1,59 +1,60 b''
1 {% load staticfiles %}
1 {% load staticfiles %}
2 {% load i18n %}
2 {% load i18n %}
3 {% load l10n %}
3 {% load l10n %}
4 {% load static from staticfiles %}
4 {% load static from staticfiles %}
5
5
6 <!DOCTYPE html>
6 <!DOCTYPE html>
7 <html>
7 <html>
8 <head>
8 <head>
9 <link rel="stylesheet" type="text/css" href="{% static 'css/base.css' %}" media="all"/>
9 <link rel="stylesheet" type="text/css" href="{% static 'css/base.css' %}" media="all"/>
10 <link rel="stylesheet" type="text/css" href="{% static 'css/3party/highlight.css' %}" media="all"/>
10 <link rel="stylesheet" type="text/css" href="{% static 'css/3party/highlight.css' %}" media="all"/>
11 <link rel="stylesheet" type="text/css" href="{% static theme_css %}" media="all"/>
11 <link rel="stylesheet" type="text/css" href="{% static theme_css %}" media="all"/>
12
12
13 <link rel="alternate" type="application/rss+xml" href="rss/" title="{% trans 'Feed' %}"/>
13 <link rel="alternate" type="application/rss+xml" href="rss/" title="{% trans 'Feed' %}"/>
14
14
15 <link rel="icon" type="image/png"
15 <link rel="icon" type="image/png"
16 href="{% static 'favicon.png' %}">
16 href="{% static 'favicon.png' %}">
17
17
18 <meta name="viewport" content="width=device-width, initial-scale=1"/>
18 <meta name="viewport" content="width=device-width, initial-scale=1"/>
19 <meta charset="utf-8"/>
19 <meta charset="utf-8"/>
20
20
21 {% block head %}{% endblock %}
21 {% block head %}{% endblock %}
22 </head>
22 </head>
23 <body>
23 <body>
24 <script src="{% static 'js/jquery-2.0.1.min.js' %}"></script>
24 <script src="{% static 'js/jquery-2.0.1.min.js' %}"></script>
25 <script src="{% static 'js/jquery-ui-1.10.3.custom.min.js' %}"></script>
25 <script src="{% static 'js/jquery-ui-1.10.3.custom.min.js' %}"></script>
26 <script src="{% static 'js/jquery.mousewheel.js' %}"></script>
26 <script src="{% static 'js/jquery.mousewheel.js' %}"></script>
27 <script src="{% url 'js_info_dict' %}"></script>
27 <script src="{% url 'js_info_dict' %}"></script>
28
28
29 <div class="navigation_panel header">
29 <div class="navigation_panel header">
30 <a class="link" href="{% url 'index' %}">{% trans "All threads" %}</a>
30 <a class="link" href="{% url 'index' %}">{% trans "All threads" %}</a>
31 {% for tag in tags %}
31 {% for tag in tags %}
32 <a class="tag" href="{% url 'tag' tag_name=tag.name %}"
32 {% autoescape off %}
33 >#{{ tag.name }}</a>,
33 {{ tag.get_view }}{% if not forloop.last %},{% endif %}
34 {% endautoescape %}
34 {% endfor %}
35 {% endfor %}
35 <a href="{% url 'tags' %}" title="{% trans 'Tag management' %}"
36 <a href="{% url 'tags' %}" title="{% trans 'Tag management' %}"
36 >[...]</a>,
37 >[...]</a>,
37 <a href="{% url 'search' %}" title="{% trans 'Search' %}">[S]</a>
38 <a href="{% url 'search' %}" title="{% trans 'Search' %}">[S]</a>
38 <a class="link" href="{% url 'settings' %}">{% trans 'Settings' %}</a>
39 <a class="link" href="{% url 'settings' %}">{% trans 'Settings' %}</a>
39 </div>
40 </div>
40
41
41 {% block content %}{% endblock %}
42 {% block content %}{% endblock %}
42
43
43 <script src="{% static 'js/3party/highlight.min.js' %}"></script>
44 <script src="{% static 'js/3party/highlight.min.js' %}"></script>
44 <script src="{% static 'js/popup.js' %}"></script>
45 <script src="{% static 'js/popup.js' %}"></script>
45 <script src="{% static 'js/image.js' %}"></script>
46 <script src="{% static 'js/image.js' %}"></script>
46 <script src="{% static 'js/refpopup.js' %}"></script>
47 <script src="{% static 'js/refpopup.js' %}"></script>
47 <script src="{% static 'js/main.js' %}"></script>
48 <script src="{% static 'js/main.js' %}"></script>
48
49
49 <div class="navigation_panel footer">
50 <div class="navigation_panel footer">
50 {% block metapanel %}{% endblock %}
51 {% block metapanel %}{% endblock %}
51 [<a href="{% url 'admin:index' %}">{% trans 'Admin' %}</a>]
52 [<a href="{% url 'admin:index' %}">{% trans 'Admin' %}</a>]
52 {% with ppd=posts_per_day|floatformat:2 %}
53 {% with ppd=posts_per_day|floatformat:2 %}
53 {% blocktrans %}Speed: {{ ppd }} posts per day{% endblocktrans %}
54 {% blocktrans %}Speed: {{ ppd }} posts per day{% endblocktrans %}
54 {% endwith %}
55 {% endwith %}
55 <a class="link" href="#top">{% trans 'Up' %}</a>
56 <a class="link" href="#top">{% trans 'Up' %}</a>
56 </div>
57 </div>
57
58
58 </body>
59 </body>
59 </html>
60 </html>
@@ -1,90 +1,91 b''
1 {% load i18n %}
1 {% load i18n %}
2 {% load board %}
2 {% load board %}
3 {% load cache %}
3 {% load cache %}
4
4
5 {% get_current_language as LANGUAGE_CODE %}
5 {% get_current_language as LANGUAGE_CODE %}
6
6
7 {% if thread.archived %}
7 {% if thread.archived %}
8 <div class="post archive_post" id="{{ post.id }}">
8 <div class="post archive_post" id="{{ post.id }}">
9 {% elif bumpable %}
9 {% elif bumpable %}
10 <div class="post" id="{{ post.id }}">
10 <div class="post" id="{{ post.id }}">
11 {% else %}
11 {% else %}
12 <div class="post dead_post" id="{{ post.id }}">
12 <div class="post dead_post" id="{{ post.id }}">
13 {% endif %}
13 {% endif %}
14
14
15 <div class="post-info">
15 <div class="post-info">
16 <a class="post_id" href="{% post_object_url post thread=thread %}"
16 <a class="post_id" href="{% post_object_url post thread=thread %}"
17 {% if not truncated and not thread.archived %}
17 {% if not truncated and not thread.archived %}
18 onclick="javascript:addQuickReply('{{ post.id }}'); return false;"
18 onclick="javascript:addQuickReply('{{ post.id }}'); return false;"
19 title="{% trans 'Quote' %}" {% endif %}>({{ post.id }})</a>
19 title="{% trans 'Quote' %}" {% endif %}>({{ post.id }})</a>
20 <span class="title">{{ post.title }}</span>
20 <span class="title">{{ post.title }}</span>
21 <span class="pub_time">{{ post.pub_time }}</span>
21 <span class="pub_time">{{ post.pub_time }}</span>
22 {% if thread.archived %}
22 {% if thread.archived %}
23 — {{ thread.bump_time }}
23 — {{ thread.bump_time }}
24 {% endif %}
24 {% endif %}
25 {% if is_opening and need_open_link %}
25 {% if is_opening and need_open_link %}
26 {% if thread.archived %}
26 {% if thread.archived %}
27 [<a class="link" href="{% url 'thread' post.id %}">{% trans "Open" %}</a>]
27 [<a class="link" href="{% url 'thread' post.id %}">{% trans "Open" %}</a>]
28 {% else %}
28 {% else %}
29 [<a class="link" href="{% url 'thread' post.id %}#form">{% trans "Reply" %}</a>]
29 [<a class="link" href="{% url 'thread' post.id %}#form">{% trans "Reply" %}</a>]
30 {% endif %}
30 {% endif %}
31 {% endif %}
31 {% endif %}
32
32
33 {% if moderator %}
33 {% if moderator %}
34 <span class="moderator_info">
34 <span class="moderator_info">
35 [<a href="{% url 'admin:boards_post_change' post.id %}"
35 [<a href="{% url 'admin:boards_post_change' post.id %}"
36 >{% trans 'Edit' %}</a>]
36 >{% trans 'Edit' %}</a>]
37 {% if is_opening %}
37 {% if is_opening %}
38 [<a href="{% url 'admin:boards_thread_change' thread.id %}"
38 [<a href="{% url 'admin:boards_thread_change' thread.id %}"
39 >{% trans 'Edit thread' %}</a>]
39 >{% trans 'Edit thread' %}</a>]
40 {% endif %}
40 {% endif %}
41 </span>
41 </span>
42 {% endif %}
42 {% endif %}
43 </div>
43 </div>
44 {% if post.images.exists %}
44 {% if post.images.exists %}
45 {% with post.images.all.0 as image %}
45 {% with post.images.all.0 as image %}
46 <div class="image">
46 <div class="image">
47 <a
47 <a
48 class="thumb"
48 class="thumb"
49 href="{{ image.image.url }}"><img
49 href="{{ image.image.url }}"><img
50 src="{{ image.image.url_200x150 }}"
50 src="{{ image.image.url_200x150 }}"
51 alt="{{ post.id }}"
51 alt="{{ post.id }}"
52 width="{{ image.pre_width }}"
52 width="{{ image.pre_width }}"
53 height="{{ image.pre_height }}"
53 height="{{ image.pre_height }}"
54 data-width="{{ image.width }}"
54 data-width="{{ image.width }}"
55 data-height="{{ image.height }}"/>
55 data-height="{{ image.height }}"/>
56 </a>
56 </a>
57 </div>
57 </div>
58 {% endwith %}
58 {% endwith %}
59 {% endif %}
59 {% endif %}
60 <div class="message">
60 <div class="message">
61 {% autoescape off %}
61 {% autoescape off %}
62 {% if truncated %}
62 {% if truncated %}
63 {{ post.get_text|truncatewords_html:50 }}
63 {{ post.get_text|truncatewords_html:50 }}
64 {% else %}
64 {% else %}
65 {{ post.get_text }}
65 {{ post.get_text }}
66 {% endif %}
66 {% endif %}
67 {% endautoescape %}
67 {% endautoescape %}
68 {% if post.is_referenced %}
68 {% if post.is_referenced %}
69 <div class="refmap">
69 <div class="refmap">
70 {% autoescape off %}
70 {% autoescape off %}
71 {% trans "Replies" %}: {{ post.refmap }}
71 {% trans "Replies" %}: {{ post.refmap }}
72 {% endautoescape %}
72 {% endautoescape %}
73 </div>
73 </div>
74 {% endif %}
74 {% endif %}
75 </div>
75 </div>
76 {% if is_opening %}
76 {% if is_opening %}
77 <div class="metadata">
77 <div class="metadata">
78 {% if is_opening and need_open_link %}
78 {% if is_opening and need_open_link %}
79 {{ thread.get_reply_count }} {% trans 'messages' %},
79 {{ thread.get_reply_count }} {% trans 'messages' %},
80 {{ thread.get_images_count }} {% trans 'images' %}.
80 {{ thread.get_images_count }} {% trans 'images' %}.
81 {% endif %}
81 {% endif %}
82 <span class="tags">
82 <span class="tags">
83 {% for tag in thread.get_tags %}
83 {% for tag in thread.get_tags %}
84 <a class="tag" href="{% url 'tag' tag.name %}">
84 {% autoescape off %}
85 #{{ tag.name }}</a>{% if not forloop.last %},{% endif %}
85 {{ tag.get_view }}{% if not forloop.last %},{% endif %}
86 {% endautoescape %}
86 {% endfor %}
87 {% endfor %}
87 </span>
88 </span>
88 </div>
89 </div>
89 {% endif %}
90 {% endif %}
90 </div>
91 </div>
@@ -1,197 +1,200 b''
1 {% extends "boards/base.html" %}
1 {% extends "boards/base.html" %}
2
2
3 {% load i18n %}
3 {% load i18n %}
4 {% load cache %}
4 {% load cache %}
5 {% load board %}
5 {% load board %}
6 {% load static %}
6 {% load static %}
7
7
8 {% block head %}
8 {% block head %}
9 {% if tag %}
9 {% if tag %}
10 <title>{{ tag.name }} - {{ site_name }}</title>
10 <title>{{ tag.name }} - {{ site_name }}</title>
11 {% else %}
11 {% else %}
12 <title>{{ site_name }}</title>
12 <title>{{ site_name }}</title>
13 {% endif %}
13 {% endif %}
14
14
15 {% if current_page.has_previous %}
15 {% if current_page.has_previous %}
16 <link rel="prev" href="
16 <link rel="prev" href="
17 {% if tag %}
17 {% if tag %}
18 {% url "tag" tag_name=tag.name page=current_page.previous_page_number %}
18 {% url "tag" tag_name=tag.name page=current_page.previous_page_number %}
19 {% elif archived %}
19 {% elif archived %}
20 {% url "archive" page=current_page.previous_page_number %}
20 {% url "archive" page=current_page.previous_page_number %}
21 {% else %}
21 {% else %}
22 {% url "index" page=current_page.previous_page_number %}
22 {% url "index" page=current_page.previous_page_number %}
23 {% endif %}
23 {% endif %}
24 " />
24 " />
25 {% endif %}
25 {% endif %}
26 {% if current_page.has_next %}
26 {% if current_page.has_next %}
27 <link rel="next" href="
27 <link rel="next" href="
28 {% if tag %}
28 {% if tag %}
29 {% url "tag" tag_name=tag.name page=current_page.next_page_number %}
29 {% url "tag" tag_name=tag.name page=current_page.next_page_number %}
30 {% elif archived %}
30 {% elif archived %}
31 {% url "archive" page=current_page.next_page_number %}
31 {% url "archive" page=current_page.next_page_number %}
32 {% else %}
32 {% else %}
33 {% url "index" page=current_page.next_page_number %}
33 {% url "index" page=current_page.next_page_number %}
34 {% endif %}
34 {% endif %}
35 " />
35 " />
36 {% endif %}
36 {% endif %}
37
37
38 {% endblock %}
38 {% endblock %}
39
39
40 {% block content %}
40 {% block content %}
41
41
42 {% get_current_language as LANGUAGE_CODE %}
42 {% get_current_language as LANGUAGE_CODE %}
43
43
44 {% if tag %}
44 {% if tag %}
45 <div class="tag_info">
45 <div class="tag_info">
46 <h2>
46 <h2>
47 {% if tag in fav_tags %}
47 {% if tag in fav_tags %}
48 <a href="{% url 'tag' tag.name %}?method=unsubscribe&next={{ request.path }}"
48 <a href="{% url 'tag' tag.name %}?method=unsubscribe&next={{ request.path }}"
49 class="fav" rel="nofollow"></a>
49 class="fav" rel="nofollow"></a>
50 {% else %}
50 {% else %}
51 <a href="{% url 'tag' tag.name %}?method=subscribe&next={{ request.path }}"
51 <a href="{% url 'tag' tag.name %}?method=subscribe&next={{ request.path }}"
52 class="not_fav" rel="nofollow"></a>
52 class="not_fav" rel="nofollow"></a>
53 {% endif %}
53 {% endif %}
54 {% if tag in hidden_tags %}
54 {% if tag in hidden_tags %}
55 <a href="{% url 'tag' tag.name %}?method=unhide&next={{ request.path }}"
55 <a href="{% url 'tag' tag.name %}?method=unhide&next={{ request.path }}"
56 title="{% trans 'Show tag' %}"
56 title="{% trans 'Show tag' %}"
57 class="fav" rel="nofollow">H</a>
57 class="fav" rel="nofollow">H</a>
58 {% else %}
58 {% else %}
59 <a href="{% url 'tag' tag.name %}?method=hide&next={{ request.path }}"
59 <a href="{% url 'tag' tag.name %}?method=hide&next={{ request.path }}"
60 title="{% trans 'Hide tag' %}"
60 title="{% trans 'Hide tag' %}"
61 class="not_fav" rel="nofollow">H</a>
61 class="not_fav" rel="nofollow">H</a>
62 {% endif %}
62 {% endif %}
63 #{{ tag.name }}
63 {% autoescape off %}
64 {{ tag.get_view }}
65 {% endautoescape %}
66 [<a href="{% url 'admin:boards_tag_change' tag.id %}"$>{% trans 'Edit tag' %}</a>]
64 </h2>
67 </h2>
65 </div>
68 </div>
66 {% endif %}
69 {% endif %}
67
70
68 {% if threads %}
71 {% if threads %}
69 {% if current_page.has_previous %}
72 {% if current_page.has_previous %}
70 <div class="page_link">
73 <div class="page_link">
71 <a href="
74 <a href="
72 {% if tag %}
75 {% if tag %}
73 {% url "tag" tag_name=tag.name page=current_page.previous_page_number %}
76 {% url "tag" tag_name=tag.name page=current_page.previous_page_number %}
74 {% elif archived %}
77 {% elif archived %}
75 {% url "archive" page=current_page.previous_page_number %}
78 {% url "archive" page=current_page.previous_page_number %}
76 {% else %}
79 {% else %}
77 {% url "index" page=current_page.previous_page_number %}
80 {% url "index" page=current_page.previous_page_number %}
78 {% endif %}
81 {% endif %}
79 ">{% trans "Previous page" %}</a>
82 ">{% trans "Previous page" %}</a>
80 </div>
83 </div>
81 {% endif %}
84 {% endif %}
82
85
83 {% for thread in threads %}
86 {% for thread in threads %}
84 {% cache 600 thread_short thread.id thread.last_edit_time moderator LANGUAGE_CODE %}
87 {% cache 600 thread_short thread.id thread.last_edit_time moderator LANGUAGE_CODE %}
85 <div class="thread">
88 <div class="thread">
86 {% with can_bump=thread.can_bump %}
89 {% with can_bump=thread.can_bump %}
87 {% post_view thread.get_opening_post moderator is_opening=True thread=thread can_bump=can_bump truncated=True need_open_link=True %}
90 {% post_view thread.get_opening_post moderator is_opening=True thread=thread can_bump=can_bump truncated=True need_open_link=True %}
88 {% if not thread.archived %}
91 {% if not thread.archived %}
89 {% with last_replies=thread.get_last_replies %}
92 {% with last_replies=thread.get_last_replies %}
90 {% if last_replies %}
93 {% if last_replies %}
91 {% if thread.get_skipped_replies_count %}
94 {% if thread.get_skipped_replies_count %}
92 <div class="skipped_replies">
95 <div class="skipped_replies">
93 <a href="{% url 'thread' thread.get_opening_post.id %}">
96 <a href="{% url 'thread' thread.get_opening_post.id %}">
94 {% blocktrans with count=thread.get_skipped_replies_count %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %}
97 {% blocktrans with count=thread.get_skipped_replies_count %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %}
95 </a>
98 </a>
96 </div>
99 </div>
97 {% endif %}
100 {% endif %}
98 <div class="last-replies">
101 <div class="last-replies">
99 {% for post in last_replies %}
102 {% for post in last_replies %}
100 {% post_view post moderator=moderator is_opening=False thread=thread can_bump=can_bump truncated=True %}
103 {% post_view post moderator=moderator is_opening=False thread=thread can_bump=can_bump truncated=True %}
101 {% endfor %}
104 {% endfor %}
102 </div>
105 </div>
103 {% endif %}
106 {% endif %}
104 {% endwith %}
107 {% endwith %}
105 {% endif %}
108 {% endif %}
106 {% endwith %}
109 {% endwith %}
107 </div>
110 </div>
108 {% endcache %}
111 {% endcache %}
109 {% endfor %}
112 {% endfor %}
110
113
111 {% if current_page.has_next %}
114 {% if current_page.has_next %}
112 <div class="page_link">
115 <div class="page_link">
113 <a href="
116 <a href="
114 {% if tag %}
117 {% if tag %}
115 {% url "tag" tag_name=tag.name page=current_page.next_page_number %}
118 {% url "tag" tag_name=tag.name page=current_page.next_page_number %}
116 {% elif archived %}
119 {% elif archived %}
117 {% url "archive" page=current_page.next_page_number %}
120 {% url "archive" page=current_page.next_page_number %}
118 {% else %}
121 {% else %}
119 {% url "index" page=current_page.next_page_number %}
122 {% url "index" page=current_page.next_page_number %}
120 {% endif %}
123 {% endif %}
121 ">{% trans "Next page" %}</a>
124 ">{% trans "Next page" %}</a>
122 </div>
125 </div>
123 {% endif %}
126 {% endif %}
124 {% else %}
127 {% else %}
125 <div class="post">
128 <div class="post">
126 {% trans 'No threads exist. Create the first one!' %}</div>
129 {% trans 'No threads exist. Create the first one!' %}</div>
127 {% endif %}
130 {% endif %}
128
131
129 <div class="post-form-w">
132 <div class="post-form-w">
130 <script src="{% static 'js/panel.js' %}"></script>
133 <script src="{% static 'js/panel.js' %}"></script>
131 <div class="post-form">
134 <div class="post-form">
132 <div class="form-title">{% trans "Create new thread" %}</div>
135 <div class="form-title">{% trans "Create new thread" %}</div>
133 <div class="swappable-form-full">
136 <div class="swappable-form-full">
134 <form enctype="multipart/form-data" method="post">{% csrf_token %}
137 <form enctype="multipart/form-data" method="post">{% csrf_token %}
135 {{ form.as_div }}
138 {{ form.as_div }}
136 <div class="form-submit">
139 <div class="form-submit">
137 <input type="submit" value="{% trans "Post" %}"/>
140 <input type="submit" value="{% trans "Post" %}"/>
138 </div>
141 </div>
139 </form>
142 </form>
140 </div>
143 </div>
141 <div>
144 <div>
142 {% trans 'Tags must be delimited by spaces. Text or image is required.' %}
145 {% trans 'Tags must be delimited by spaces. Text or image is required.' %}
143 </div>
146 </div>
144 <div><a href="{% url "staticpage" name="help" %}">
147 <div><a href="{% url "staticpage" name="help" %}">
145 {% trans 'Text syntax' %}</a></div>
148 {% trans 'Text syntax' %}</a></div>
146 </div>
149 </div>
147 </div>
150 </div>
148
151
149 <script src="{% static 'js/form.js' %}"></script>
152 <script src="{% static 'js/form.js' %}"></script>
150
153
151 {% endblock %}
154 {% endblock %}
152
155
153 {% block metapanel %}
156 {% block metapanel %}
154
157
155 <span class="metapanel">
158 <span class="metapanel">
156 <b><a href="{% url "authors" %}">{{ site_name }}</a> {{ version }}</b>
159 <b><a href="{% url "authors" %}">{{ site_name }}</a> {{ version }}</b>
157 {% trans "Pages:" %}
160 {% trans "Pages:" %}
158 <a href="
161 <a href="
159 {% if tag %}
162 {% if tag %}
160 {% url "tag" tag_name=tag.name page=paginator.page_range|first %}
163 {% url "tag" tag_name=tag.name page=paginator.page_range|first %}
161 {% elif archived %}
164 {% elif archived %}
162 {% url "archive" page=paginator.page_range|first %}
165 {% url "archive" page=paginator.page_range|first %}
163 {% else %}
166 {% else %}
164 {% url "index" page=paginator.page_range|first %}
167 {% url "index" page=paginator.page_range|first %}
165 {% endif %}
168 {% endif %}
166 ">&lt;&lt;</a>
169 ">&lt;&lt;</a>
167 [
170 [
168 {% for page in paginator.center_range %}
171 {% for page in paginator.center_range %}
169 <a
172 <a
170 {% ifequal page current_page.number %}
173 {% ifequal page current_page.number %}
171 class="current_page"
174 class="current_page"
172 {% endifequal %}
175 {% endifequal %}
173 href="
176 href="
174 {% if tag %}
177 {% if tag %}
175 {% url "tag" tag_name=tag.name page=page %}
178 {% url "tag" tag_name=tag.name page=page %}
176 {% elif archived %}
179 {% elif archived %}
177 {% url "archive" page=page %}
180 {% url "archive" page=page %}
178 {% else %}
181 {% else %}
179 {% url "index" page=page %}
182 {% url "index" page=page %}
180 {% endif %}
183 {% endif %}
181 ">{{ page }}</a>
184 ">{{ page }}</a>
182 {% if not forloop.last %},{% endif %}
185 {% if not forloop.last %},{% endif %}
183 {% endfor %}
186 {% endfor %}
184 ]
187 ]
185 <a href="
188 <a href="
186 {% if tag %}
189 {% if tag %}
187 {% url "tag" tag_name=tag.name page=paginator.page_range|last %}
190 {% url "tag" tag_name=tag.name page=paginator.page_range|last %}
188 {% elif archived %}
191 {% elif archived %}
189 {% url "archive" page=paginator.page_range|last %}
192 {% url "archive" page=paginator.page_range|last %}
190 {% else %}
193 {% else %}
191 {% url "index" page=paginator.page_range|last %}
194 {% url "index" page=paginator.page_range|last %}
192 {% endif %}
195 {% endif %}
193 ">&gt;&gt;</a>
196 ">&gt;&gt;</a>
194 [<a href="rss/">RSS</a>]
197 [<a href="rss/">RSS</a>]
195 </span>
198 </span>
196
199
197 {% endblock %}
200 {% endblock %}
@@ -1,3 +1,5 b''
1 <div class="post">
1 <div class="post">
2 <a class="tag" href="{% url 'tag' tag_name=tag.name %}">#{{ tag.name }}</a>
2 {% autoescape off %}
3 </div> No newline at end of file
3 {{ tag.get_view }}
4 {% endautoescape %}
5 </div>
@@ -1,27 +1,28 b''
1 {% extends "boards/base.html" %}
1 {% extends "boards/base.html" %}
2
2
3 {% load i18n %}
3 {% load i18n %}
4 {% load cache %}
4 {% load cache %}
5
5
6 {% block head %}
6 {% block head %}
7 <title>Neboard - {% trans "Tags" %}</title>
7 <title>Neboard - {% trans "Tags" %}</title>
8 {% endblock %}
8 {% endblock %}
9
9
10 {% block content %}
10 {% block content %}
11
11
12 {% cache 600 all_tags_list %}
12 {% cache 600 all_tags_list %}
13 <div class="post">
13 <div class="post">
14 {% if all_tags %}
14 {% if all_tags %}
15 {% for tag in all_tags %}
15 {% for tag in all_tags %}
16 <div class="tag_item">
16 <div class="tag_item">
17 <a class="tag" href="{% url 'tag' tag.name %}">
17 {% autoescape off %}
18 #{{ tag.name }}</a>
18 {{ tag.get_view }}
19 {% endautoescape %}
19 </div>
20 </div>
20 {% endfor %}
21 {% endfor %}
21 {% else %}
22 {% else %}
22 {% trans 'No tags found.' %}
23 {% trans 'No tags found.' %}
23 {% endif %}
24 {% endif %}
24 </div>
25 </div>
25 {% endcache %}
26 {% endcache %}
26
27
27 {% endblock %}
28 {% endblock %}
@@ -1,38 +1,38 b''
1 {% extends 'boards/base.html' %}
1 {% extends 'boards/base.html' %}
2
2
3 {% load board %}
3 {% load board %}
4 {% load i18n %}
4 {% load i18n %}
5
5
6 {% block content %}
6 {% block content %}
7 <div class="post-form-w">
7 <div class="post-form-w">
8 <div class="post-form">
8 <div class="post-form">
9 <h3>{% trans 'Search' %}</h3>
9 <h3>{% trans 'Search' %}</h3>
10 <form method="get" action="">
10 <form method="get" action="">
11 {{ form.as_div }}
11 {{ form.as_div }}
12 <div class="form-submit">
12 <div class="form-submit">
13 <input type="submit" value="{% trans 'Search' %}">
13 <input type="submit" value="{% trans 'Search' %}">
14 </div>
14 </div>
15 </form>
15 </form>
16 </div>
16 </div>
17 </div>
17 </div>
18
18
19 {% if page %}
19 {% if page %}
20 {% if page.has_previous %}
20 {% if page.has_previous %}
21 <div class="page_link">
21 <div class="page_link">
22 <a href="?query={{ query }}&amp;page={{ page.previous_page_number }}">{% trans "Previous page" %}
22 <a href="?query={{ query }}&amp;page={{ page.previous_page_number }}">{% trans "Previous page" %}
23 </a>
23 </a>
24 </div>
24 </div>
25 {% endif %}
25 {% endif %}
26
26
27 {% for result in page.object_list %}
27 {% for result in page.object_list %}
28 {{ result.object.get_view }}
28 {{ result.object.get_search_view }}
29 {% endfor %}
29 {% endfor %}
30
30
31 {% if page.has_next %}
31 {% if page.has_next %}
32 <div class="page_link">
32 <div class="page_link">
33 <a href="?query={{ query }}&amp;page={{ page.next_page_number }}">{% trans "Next page" %}
33 <a href="?query={{ query }}&amp;page={{ page.next_page_number }}">{% trans "Next page" %}
34 </a>
34 </a>
35 </div>
35 </div>
36 {% endif %}
36 {% endif %}
37 {% endif %}
37 {% endif %}
38 {% endblock %} No newline at end of file
38 {% endblock %}
@@ -1,55 +1,56 b''
1 from django.test import TestCase, Client
1 from django.test import TestCase, Client
2 import time
2 import time
3 from boards import settings
3 from boards import settings
4 from boards.models import Post
4 from boards.models import Post, Tag
5 import neboard
5 import neboard
6
6
7
7
8 TEST_TAG = 'test_tag'
8 TEST_TAG = 'test_tag'
9
9
10 PAGE_404 = 'boards/404.html'
10 PAGE_404 = 'boards/404.html'
11
11
12 TEST_TEXT = 'test text'
12 TEST_TEXT = 'test text'
13
13
14 NEW_THREAD_PAGE = '/'
14 NEW_THREAD_PAGE = '/'
15 THREAD_PAGE_ONE = '/thread/1/'
15 THREAD_PAGE_ONE = '/thread/1/'
16 HTTP_CODE_REDIRECT = 302
16 HTTP_CODE_REDIRECT = 302
17
17
18
18
19 class FormTest(TestCase):
19 class FormTest(TestCase):
20 def test_post_validation(self):
20 def test_post_validation(self):
21 client = Client()
21 client = Client()
22
22
23 valid_tags = 'tag1 tag_2 тег_3'
23 valid_tags = 'tag1 tag_2 тег_3'
24 invalid_tags = '$%_356 ---'
24 invalid_tags = '$%_356 ---'
25 Tag.objects.create(name='tag1', required=True)
25
26
26 response = client.post(NEW_THREAD_PAGE, {'title': 'test title',
27 response = client.post(NEW_THREAD_PAGE, {'title': 'test title',
27 'text': TEST_TEXT,
28 'text': TEST_TEXT,
28 'tags': valid_tags})
29 'tags': valid_tags})
29 self.assertEqual(response.status_code, HTTP_CODE_REDIRECT,
30 self.assertEqual(response.status_code, HTTP_CODE_REDIRECT,
30 msg='Posting new message failed: got code ' +
31 msg='Posting new message failed: got code ' +
31 str(response.status_code))
32 str(response.status_code))
32
33
33 self.assertEqual(1, Post.objects.count(),
34 self.assertEqual(1, Post.objects.count(),
34 msg='No posts were created')
35 msg='No posts were created')
35
36
36 client.post(NEW_THREAD_PAGE, {'text': TEST_TEXT,
37 client.post(NEW_THREAD_PAGE, {'text': TEST_TEXT,
37 'tags': invalid_tags})
38 'tags': invalid_tags})
38 self.assertEqual(1, Post.objects.count(), msg='The validation passed '
39 self.assertEqual(1, Post.objects.count(), msg='The validation passed '
39 'where it should fail')
40 'where it should fail')
40
41
41 # Change posting delay so we don't have to wait for 30 seconds or more
42 # Change posting delay so we don't have to wait for 30 seconds or more
42 old_posting_delay = neboard.settings.POSTING_DELAY
43 old_posting_delay = neboard.settings.POSTING_DELAY
43 # Wait fot the posting delay or we won't be able to post
44 # Wait fot the posting delay or we won't be able to post
44 settings.POSTING_DELAY = 1
45 settings.POSTING_DELAY = 1
45 time.sleep(neboard.settings.POSTING_DELAY + 1)
46 time.sleep(neboard.settings.POSTING_DELAY + 1)
46 response = client.post(THREAD_PAGE_ONE, {'text': TEST_TEXT,
47 response = client.post(THREAD_PAGE_ONE, {'text': TEST_TEXT,
47 'tags': valid_tags})
48 'tags': valid_tags})
48 self.assertEqual(HTTP_CODE_REDIRECT, response.status_code,
49 self.assertEqual(HTTP_CODE_REDIRECT, response.status_code,
49 msg='Posting new message failed: got code ' +
50 msg='Posting new message failed: got code ' +
50 str(response.status_code))
51 str(response.status_code))
51 # Restore posting delay
52 # Restore posting delay
52 settings.POSTING_DELAY = old_posting_delay
53 settings.POSTING_DELAY = old_posting_delay
53
54
54 self.assertEqual(2, Post.objects.count(),
55 self.assertEqual(2, Post.objects.count(),
55 msg='No posts were created')
56 msg='No posts were created')
@@ -1,10 +1,10 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python3
2 import os
2 import os
3 import sys
3 import sys
4
4
5 if __name__ == "__main__":
5 if __name__ == "__main__":
6 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "neboard.settings")
6 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "neboard.settings")
7
7
8 from django.core.management import execute_from_command_line
8 from django.core.management import execute_from_command_line
9
9
10 execute_from_command_line(sys.argv)
10 execute_from_command_line(sys.argv)
1 NO CONTENT: file was removed
NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now