Show More
@@ -1,33 +1,31 | |||
|
1 | 1 | from django.contrib import admin |
|
2 | 2 | from boards.models import Post, Tag, Ban, Thread |
|
3 | 3 | |
|
4 | 4 | |
|
5 | 5 | class PostAdmin(admin.ModelAdmin): |
|
6 | 6 | |
|
7 | 7 | list_display = ('id', 'title', 'text') |
|
8 | 8 | list_filter = ('pub_time', 'thread_new') |
|
9 | 9 | search_fields = ('id', 'title', 'text') |
|
10 | 10 | |
|
11 | 11 | |
|
12 | 12 | class TagAdmin(admin.ModelAdmin): |
|
13 | 13 | |
|
14 |
list_display = ('name', |
|
|
15 | list_filter = ('linked',) | |
|
16 | ||
|
14 | list_display = ('name',) | |
|
17 | 15 | |
|
18 | 16 | class ThreadAdmin(admin.ModelAdmin): |
|
19 | 17 | |
|
20 | 18 | def title(self, obj): |
|
21 | 19 | return obj.get_opening_post().title |
|
22 | 20 | |
|
23 | 21 | def reply_count(self, obj): |
|
24 | 22 | return obj.get_reply_count() |
|
25 | 23 | |
|
26 | 24 | list_display = ('id', 'title', 'reply_count', 'archived') |
|
27 | 25 | list_filter = ('bump_time', 'archived') |
|
28 | 26 | search_fields = ('id', 'title') |
|
29 | 27 | |
|
30 | 28 | admin.site.register(Post, PostAdmin) |
|
31 | 29 | admin.site.register(Tag, TagAdmin) |
|
32 | 30 | admin.site.register(Ban) |
|
33 | 31 | admin.site.register(Thread, ThreadAdmin) |
@@ -1,342 +1,341 | |||
|
1 | 1 | import re |
|
2 | 2 | import time |
|
3 | 3 | import hashlib |
|
4 | 4 | |
|
5 | 5 | from captcha.fields import CaptchaField |
|
6 | 6 | from django import forms |
|
7 | 7 | from django.forms.util import ErrorList |
|
8 | 8 | from django.utils.translation import ugettext_lazy as _ |
|
9 | 9 | |
|
10 | 10 | from boards.mdx_neboard import formatters |
|
11 | 11 | from boards.models.post import TITLE_MAX_LENGTH |
|
12 | 12 | from boards.models import PostImage |
|
13 | 13 | from neboard import settings |
|
14 | 14 | from boards import utils |
|
15 | 15 | import boards.settings as board_settings |
|
16 | 16 | |
|
17 | 17 | VETERAN_POSTING_DELAY = 5 |
|
18 | 18 | |
|
19 | 19 | ATTRIBUTE_PLACEHOLDER = 'placeholder' |
|
20 | 20 | |
|
21 | 21 | LAST_POST_TIME = 'last_post_time' |
|
22 | 22 | LAST_LOGIN_TIME = 'last_login_time' |
|
23 |
TEXT_PLACEHOLDER = _('''Type message here. |
|
|
24 | this. 2 new lines are required to start new paragraph.''') | |
|
23 | TEXT_PLACEHOLDER = _('''Type message here. Use formatting panel for more advanced usage.''') | |
|
25 | 24 | TAGS_PLACEHOLDER = _('tag1 several_words_tag') |
|
26 | 25 | |
|
27 | 26 | ERROR_IMAGE_DUPLICATE = _('Such image was already posted') |
|
28 | 27 | |
|
29 | 28 | LABEL_TITLE = _('Title') |
|
30 | 29 | LABEL_TEXT = _('Text') |
|
31 | 30 | LABEL_TAG = _('Tag') |
|
32 | 31 | LABEL_SEARCH = _('Search') |
|
33 | 32 | |
|
34 | 33 | TAG_MAX_LENGTH = 20 |
|
35 | 34 | |
|
36 | 35 | REGEX_TAG = ur'^[\w\d]+$' |
|
37 | 36 | |
|
38 | 37 | |
|
39 | 38 | class FormatPanel(forms.Textarea): |
|
40 | 39 | def render(self, name, value, attrs=None): |
|
41 | 40 | output = '<div id="mark-panel">' |
|
42 | 41 | for formatter in formatters: |
|
43 | 42 | output += u'<span class="mark_btn"' + \ |
|
44 | 43 | u' onClick="addMarkToMsg(\'' + formatter.format_left + \ |
|
45 | 44 | '\', \'' + formatter.format_right + '\')">' + \ |
|
46 | 45 | formatter.preview_left + formatter.name + \ |
|
47 | 46 | formatter.preview_right + u'</span>' |
|
48 | 47 | |
|
49 | 48 | output += '</div>' |
|
50 | 49 | output += super(FormatPanel, self).render(name, value, attrs=None) |
|
51 | 50 | |
|
52 | 51 | return output |
|
53 | 52 | |
|
54 | 53 | |
|
55 | 54 | class PlainErrorList(ErrorList): |
|
56 | 55 | def __unicode__(self): |
|
57 | 56 | return self.as_text() |
|
58 | 57 | |
|
59 | 58 | def as_text(self): |
|
60 | 59 | return ''.join([u'(!) %s ' % e for e in self]) |
|
61 | 60 | |
|
62 | 61 | |
|
63 | 62 | class NeboardForm(forms.Form): |
|
64 | 63 | |
|
65 | 64 | def as_div(self): |
|
66 | 65 | """ |
|
67 | 66 | Returns this form rendered as HTML <as_div>s. |
|
68 | 67 | """ |
|
69 | 68 | |
|
70 | 69 | return self._html_output( |
|
71 | 70 | # TODO Do not show hidden rows in the list here |
|
72 | 71 | normal_row='<div class="form-row"><div class="form-label">' |
|
73 | 72 | '%(label)s' |
|
74 | 73 | '</div></div>' |
|
75 | 74 | '<div class="form-row"><div class="form-input">' |
|
76 | 75 | '%(field)s' |
|
77 | 76 | '</div></div>' |
|
78 | 77 | '<div class="form-row">' |
|
79 | 78 | '%(help_text)s' |
|
80 | 79 | '</div>', |
|
81 | 80 | error_row='<div class="form-row">' |
|
82 | 81 | '<div class="form-label"></div>' |
|
83 | 82 | '<div class="form-errors">%s</div>' |
|
84 | 83 | '</div>', |
|
85 | 84 | row_ender='</div>', |
|
86 | 85 | help_text_html='%s', |
|
87 | 86 | errors_on_separate_row=True) |
|
88 | 87 | |
|
89 | 88 | def as_json_errors(self): |
|
90 | 89 | errors = [] |
|
91 | 90 | |
|
92 | 91 | for name, field in self.fields.items(): |
|
93 | 92 | if self[name].errors: |
|
94 | 93 | errors.append({ |
|
95 | 94 | 'field': name, |
|
96 | 95 | 'errors': self[name].errors.as_text(), |
|
97 | 96 | }) |
|
98 | 97 | |
|
99 | 98 | return errors |
|
100 | 99 | |
|
101 | 100 | |
|
102 | 101 | class PostForm(NeboardForm): |
|
103 | 102 | |
|
104 | 103 | title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False, |
|
105 | 104 | label=LABEL_TITLE) |
|
106 | 105 | text = forms.CharField( |
|
107 | 106 | widget=FormatPanel(attrs={ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER}), |
|
108 | 107 | required=False, label=LABEL_TEXT) |
|
109 | 108 | image = forms.ImageField(required=False, label=_('Image'), |
|
110 | 109 | widget=forms.ClearableFileInput( |
|
111 | 110 | attrs={'accept': 'image/*'})) |
|
112 | 111 | |
|
113 | 112 | # This field is for spam prevention only |
|
114 | 113 | email = forms.CharField(max_length=100, required=False, label=_('e-mail'), |
|
115 | 114 | widget=forms.TextInput(attrs={ |
|
116 | 115 | 'class': 'form-email'})) |
|
117 | 116 | |
|
118 | 117 | session = None |
|
119 | 118 | need_to_ban = False |
|
120 | 119 | |
|
121 | 120 | def clean_title(self): |
|
122 | 121 | title = self.cleaned_data['title'] |
|
123 | 122 | if title: |
|
124 | 123 | if len(title) > TITLE_MAX_LENGTH: |
|
125 | 124 | raise forms.ValidationError(_('Title must have less than %s ' |
|
126 | 125 | 'characters') % |
|
127 | 126 | str(TITLE_MAX_LENGTH)) |
|
128 | 127 | return title |
|
129 | 128 | |
|
130 | 129 | def clean_text(self): |
|
131 | 130 | text = self.cleaned_data['text'].strip() |
|
132 | 131 | if text: |
|
133 | 132 | if len(text) > board_settings.MAX_TEXT_LENGTH: |
|
134 | 133 | raise forms.ValidationError(_('Text must have less than %s ' |
|
135 | 134 | 'characters') % |
|
136 | 135 | str(board_settings |
|
137 | 136 | .MAX_TEXT_LENGTH)) |
|
138 | 137 | return text |
|
139 | 138 | |
|
140 | 139 | def clean_image(self): |
|
141 | 140 | image = self.cleaned_data['image'] |
|
142 | 141 | if image: |
|
143 | 142 | if image.size > board_settings.MAX_IMAGE_SIZE: |
|
144 | 143 | raise forms.ValidationError( |
|
145 | 144 | _('Image must be less than %s bytes') |
|
146 | 145 | % str(board_settings.MAX_IMAGE_SIZE)) |
|
147 | 146 | |
|
148 | 147 | md5 = hashlib.md5() |
|
149 | 148 | for chunk in image.chunks(): |
|
150 | 149 | md5.update(chunk) |
|
151 | 150 | image_hash = md5.hexdigest() |
|
152 | 151 | if PostImage.objects.filter(hash=image_hash).exists(): |
|
153 | 152 | raise forms.ValidationError(ERROR_IMAGE_DUPLICATE) |
|
154 | 153 | |
|
155 | 154 | return image |
|
156 | 155 | |
|
157 | 156 | def clean(self): |
|
158 | 157 | cleaned_data = super(PostForm, self).clean() |
|
159 | 158 | |
|
160 | 159 | if not self.session: |
|
161 | 160 | raise forms.ValidationError('Humans have sessions') |
|
162 | 161 | |
|
163 | 162 | if cleaned_data['email']: |
|
164 | 163 | self.need_to_ban = True |
|
165 | 164 | raise forms.ValidationError('A human cannot enter a hidden field') |
|
166 | 165 | |
|
167 | 166 | if not self.errors: |
|
168 | 167 | self._clean_text_image() |
|
169 | 168 | |
|
170 | 169 | if not self.errors and self.session: |
|
171 | 170 | self._validate_posting_speed() |
|
172 | 171 | |
|
173 | 172 | return cleaned_data |
|
174 | 173 | |
|
175 | 174 | def _clean_text_image(self): |
|
176 | 175 | text = self.cleaned_data.get('text') |
|
177 | 176 | image = self.cleaned_data.get('image') |
|
178 | 177 | |
|
179 | 178 | if (not text) and (not image): |
|
180 | 179 | error_message = _('Either text or image must be entered.') |
|
181 | 180 | self._errors['text'] = self.error_class([error_message]) |
|
182 | 181 | |
|
183 | 182 | def _validate_posting_speed(self): |
|
184 | 183 | can_post = True |
|
185 | 184 | |
|
186 | 185 | # TODO Remove this, it's only for test |
|
187 | 186 | if not 'user_id' in self.session: |
|
188 | 187 | return |
|
189 | 188 | |
|
190 | 189 | posting_delay = settings.POSTING_DELAY |
|
191 | 190 | |
|
192 | 191 | if board_settings.LIMIT_POSTING_SPEED and LAST_POST_TIME in \ |
|
193 | 192 | self.session: |
|
194 | 193 | now = time.time() |
|
195 | 194 | last_post_time = self.session[LAST_POST_TIME] |
|
196 | 195 | |
|
197 | 196 | current_delay = int(now - last_post_time) |
|
198 | 197 | |
|
199 | 198 | if current_delay < posting_delay: |
|
200 | 199 | error_message = _('Wait %s seconds after last posting') % str( |
|
201 | 200 | posting_delay - current_delay) |
|
202 | 201 | self._errors['text'] = self.error_class([error_message]) |
|
203 | 202 | |
|
204 | 203 | can_post = False |
|
205 | 204 | |
|
206 | 205 | if can_post: |
|
207 | 206 | self.session[LAST_POST_TIME] = time.time() |
|
208 | 207 | |
|
209 | 208 | |
|
210 | 209 | class ThreadForm(PostForm): |
|
211 | 210 | |
|
212 | 211 | regex_tags = re.compile(ur'^[\w\s\d]+$', re.UNICODE) |
|
213 | 212 | |
|
214 | 213 | tags = forms.CharField( |
|
215 | 214 | widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}), |
|
216 | 215 | max_length=100, label=_('Tags'), required=True) |
|
217 | 216 | |
|
218 | 217 | def clean_tags(self): |
|
219 | 218 | tags = self.cleaned_data['tags'].strip() |
|
220 | 219 | |
|
221 | 220 | if not tags or not self.regex_tags.match(tags): |
|
222 | 221 | raise forms.ValidationError( |
|
223 | 222 | _('Inappropriate characters in tags.')) |
|
224 | 223 | |
|
225 | 224 | return tags |
|
226 | 225 | |
|
227 | 226 | def clean(self): |
|
228 | 227 | cleaned_data = super(ThreadForm, self).clean() |
|
229 | 228 | |
|
230 | 229 | return cleaned_data |
|
231 | 230 | |
|
232 | 231 | |
|
233 | 232 | class PostCaptchaForm(PostForm): |
|
234 | 233 | captcha = CaptchaField() |
|
235 | 234 | |
|
236 | 235 | def __init__(self, *args, **kwargs): |
|
237 | 236 | self.request = kwargs['request'] |
|
238 | 237 | del kwargs['request'] |
|
239 | 238 | |
|
240 | 239 | super(PostCaptchaForm, self).__init__(*args, **kwargs) |
|
241 | 240 | |
|
242 | 241 | def clean(self): |
|
243 | 242 | cleaned_data = super(PostCaptchaForm, self).clean() |
|
244 | 243 | |
|
245 | 244 | success = self.is_valid() |
|
246 | 245 | utils.update_captcha_access(self.request, success) |
|
247 | 246 | |
|
248 | 247 | if success: |
|
249 | 248 | return cleaned_data |
|
250 | 249 | else: |
|
251 | 250 | raise forms.ValidationError(_("Captcha validation failed")) |
|
252 | 251 | |
|
253 | 252 | |
|
254 | 253 | class ThreadCaptchaForm(ThreadForm): |
|
255 | 254 | captcha = CaptchaField() |
|
256 | 255 | |
|
257 | 256 | def __init__(self, *args, **kwargs): |
|
258 | 257 | self.request = kwargs['request'] |
|
259 | 258 | del kwargs['request'] |
|
260 | 259 | |
|
261 | 260 | super(ThreadCaptchaForm, self).__init__(*args, **kwargs) |
|
262 | 261 | |
|
263 | 262 | def clean(self): |
|
264 | 263 | cleaned_data = super(ThreadCaptchaForm, self).clean() |
|
265 | 264 | |
|
266 | 265 | success = self.is_valid() |
|
267 | 266 | utils.update_captcha_access(self.request, success) |
|
268 | 267 | |
|
269 | 268 | if success: |
|
270 | 269 | return cleaned_data |
|
271 | 270 | else: |
|
272 | 271 | raise forms.ValidationError(_("Captcha validation failed")) |
|
273 | 272 | |
|
274 | 273 | |
|
275 | 274 | class SettingsForm(NeboardForm): |
|
276 | 275 | |
|
277 | 276 | theme = forms.ChoiceField(choices=settings.THEMES, |
|
278 | 277 | label=_('Theme')) |
|
279 | 278 | |
|
280 | 279 | |
|
281 | 280 | class AddTagForm(NeboardForm): |
|
282 | 281 | |
|
283 | 282 | tag = forms.CharField(max_length=TAG_MAX_LENGTH, label=LABEL_TAG) |
|
284 | 283 | method = forms.CharField(widget=forms.HiddenInput(), initial='add_tag') |
|
285 | 284 | |
|
286 | 285 | def clean_tag(self): |
|
287 | 286 | tag = self.cleaned_data['tag'] |
|
288 | 287 | |
|
289 | 288 | regex_tag = re.compile(REGEX_TAG, re.UNICODE) |
|
290 | 289 | if not regex_tag.match(tag): |
|
291 | 290 | raise forms.ValidationError(_('Inappropriate characters in tags.')) |
|
292 | 291 | |
|
293 | 292 | return tag |
|
294 | 293 | |
|
295 | 294 | def clean(self): |
|
296 | 295 | cleaned_data = super(AddTagForm, self).clean() |
|
297 | 296 | |
|
298 | 297 | return cleaned_data |
|
299 | 298 | |
|
300 | 299 | |
|
301 | 300 | class SearchForm(NeboardForm): |
|
302 | 301 | query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False) |
|
303 | 302 | |
|
304 | 303 | |
|
305 | 304 | class LoginForm(NeboardForm): |
|
306 | 305 | |
|
307 | 306 | password = forms.CharField() |
|
308 | 307 | |
|
309 | 308 | session = None |
|
310 | 309 | |
|
311 | 310 | def clean_password(self): |
|
312 | 311 | password = self.cleaned_data['password'] |
|
313 | 312 | if board_settings.MASTER_PASSWORD != password: |
|
314 | 313 | raise forms.ValidationError(_('Invalid master password')) |
|
315 | 314 | |
|
316 | 315 | return password |
|
317 | 316 | |
|
318 | 317 | def _validate_login_speed(self): |
|
319 | 318 | can_post = True |
|
320 | 319 | |
|
321 | 320 | if LAST_LOGIN_TIME in self.session: |
|
322 | 321 | now = time.time() |
|
323 | 322 | last_login_time = self.session[LAST_LOGIN_TIME] |
|
324 | 323 | |
|
325 | 324 | current_delay = int(now - last_login_time) |
|
326 | 325 | |
|
327 | 326 | if current_delay < board_settings.LOGIN_TIMEOUT: |
|
328 | 327 | error_message = _('Wait %s minutes after last login') % str( |
|
329 | 328 | (board_settings.LOGIN_TIMEOUT - current_delay) / 60) |
|
330 | 329 | self._errors['password'] = self.error_class([error_message]) |
|
331 | 330 | |
|
332 | 331 | can_post = False |
|
333 | 332 | |
|
334 | 333 | if can_post: |
|
335 | 334 | self.session[LAST_LOGIN_TIME] = time.time() |
|
336 | 335 | |
|
337 | 336 | def clean(self): |
|
338 | 337 | self._validate_login_speed() |
|
339 | 338 | |
|
340 | 339 | cleaned_data = super(LoginForm, self).clean() |
|
341 | 340 | |
|
342 | 341 | return cleaned_data |
|
1 | NO CONTENT: modified file, binary diff hidden |
@@ -1,372 +1,360 | |||
|
1 | 1 | # SOME DESCRIPTIVE TITLE. |
|
2 | 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER |
|
3 | 3 | # This file is distributed under the same license as the PACKAGE package. |
|
4 | 4 | # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. |
|
5 | 5 | # |
|
6 | 6 | msgid "" |
|
7 | 7 | msgstr "" |
|
8 | 8 | "Project-Id-Version: PACKAGE VERSION\n" |
|
9 | 9 | "Report-Msgid-Bugs-To: \n" |
|
10 |
"POT-Creation-Date: 2014-07-0 |
|
|
10 | "POT-Creation-Date: 2014-07-08 18:07+0300\n" | |
|
11 | 11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" |
|
12 | 12 | "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" |
|
13 | 13 | "Language-Team: LANGUAGE <LL@li.org>\n" |
|
14 | 14 | "Language: ru\n" |
|
15 | 15 | "MIME-Version: 1.0\n" |
|
16 | 16 | "Content-Type: text/plain; charset=UTF-8\n" |
|
17 | 17 | "Content-Transfer-Encoding: 8bit\n" |
|
18 | 18 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" |
|
19 | 19 | "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" |
|
20 | 20 | |
|
21 | 21 | #: authors.py:9 |
|
22 | 22 | msgid "author" |
|
23 | 23 | msgstr "Π°Π²ΡΠΎΡ" |
|
24 | 24 | |
|
25 | 25 | #: authors.py:10 |
|
26 | 26 | msgid "developer" |
|
27 | 27 | msgstr "ΡΠ°Π·ΡΠ°Π±ΠΎΡΡΠΈΠΊ" |
|
28 | 28 | |
|
29 | 29 | #: authors.py:11 |
|
30 | 30 | msgid "javascript developer" |
|
31 | 31 | msgstr "ΡΠ°Π·ΡΠ°Π±ΠΎΡΡΠΈΠΊ javascript" |
|
32 | 32 | |
|
33 | 33 | #: authors.py:12 |
|
34 | 34 | msgid "designer" |
|
35 | 35 | msgstr "Π΄ΠΈΠ·Π°ΠΉΠ½Π΅Ρ" |
|
36 | 36 | |
|
37 | 37 | #: forms.py:23 |
|
38 | msgid "" | |
|
39 | "Type message here. You can reply to message >>123 like\n" | |
|
40 | " this. 2 new lines are required to start new paragraph." | |
|
41 | msgstr "" | |
|
42 | "ΠΠ²Π΅Π΄ΠΈΡΠ΅ ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΠ΅ Π·Π΄Π΅ΡΡ. ΠΡ ΠΌΠΎΠΆΠ΅ΡΠ΅ ΠΎΡΠ²Π΅ΡΠΈΡΡ Π½Π° ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΠ΅ >>123 Π²ΠΎΡ ΡΠ°ΠΊ. 2 " | |
|
43 | "ΠΏΠ΅ΡΠ΅Π½ΠΎΡΠ° ΡΡΡΠΎΠΊΠΈ ΠΎΠ±ΡΠ·Π°ΡΠ΅Π»ΡΠ½Ρ Π΄Π»Ρ ΡΠΎΠ·Π΄Π°Π½ΠΈΡ Π½ΠΎΠ²ΠΎΠ³ΠΎ Π°Π±Π·Π°ΡΠ°." | |
|
38 | msgid "Type message here. Use formatting panel for more advanced usage." | |
|
39 | msgstr "ΠΠ²ΠΎΠ΄ΠΈΡΠ΅ ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΠ΅ ΡΡΠ΄Π°. ΠΡΠΏΠΎΠ»ΡΠ·ΡΠΉΡΠ΅ ΠΏΠ°Π½Π΅Π»Ρ Π΄Π»Ρ Π±ΠΎΠ»Π΅Π΅ ΡΠ»ΠΎΠΆΠ½ΠΎΠ³ΠΎ ΡΠΎΡΠΌΠ°ΡΠΈΡΠΎΠ²Π°Π½ΠΈΡ." | |
|
44 | 40 | |
|
45 |
#: forms.py:2 |
|
|
41 | #: forms.py:24 | |
|
46 | 42 | msgid "tag1 several_words_tag" |
|
47 | 43 | msgstr "ΡΠ΅Π³1 ΡΠ΅Π³_ΠΈΠ·_Π½Π΅ΡΠΊΠΎΠ»ΡΠΊΠΈΡ _ΡΠ»ΠΎΠ²" |
|
48 | 44 | |
|
49 |
#: forms.py:2 |
|
|
45 | #: forms.py:26 | |
|
50 | 46 | msgid "Such image was already posted" |
|
51 | 47 | msgstr "Π’Π°ΠΊΠΎΠ΅ ΠΈΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΠ΅ ΡΠΆΠ΅ Π±ΡΠ»ΠΎ Π·Π°Π³ΡΡΠΆΠ΅Π½ΠΎ" |
|
52 | 48 | |
|
53 |
#: forms.py:2 |
|
|
49 | #: forms.py:28 | |
|
54 | 50 | msgid "Title" |
|
55 | 51 | msgstr "ΠΠ°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ" |
|
56 | 52 | |
|
57 |
#: forms.py: |
|
|
53 | #: forms.py:29 | |
|
58 | 54 | msgid "Text" |
|
59 | 55 | msgstr "Π’Π΅ΠΊΡΡ" |
|
60 | 56 | |
|
61 |
#: forms.py:3 |
|
|
57 | #: forms.py:30 | |
|
62 | 58 | msgid "Tag" |
|
63 | 59 | msgstr "Π’Π΅Π³" |
|
64 | 60 | |
|
65 |
#: forms.py:3 |
|
|
61 | #: forms.py:31 templates/boards/base.html:54 templates/search/search.html:9 | |
|
66 | 62 | #: templates/search/search.html.py:13 |
|
67 | 63 | msgid "Search" |
|
68 | 64 | msgstr "ΠΠΎΠΈΡΠΊ" |
|
69 | 65 | |
|
70 |
#: forms.py:10 |
|
|
66 | #: forms.py:108 | |
|
71 | 67 | msgid "Image" |
|
72 | 68 | msgstr "ΠΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΠ΅" |
|
73 | 69 | |
|
74 |
#: forms.py:11 |
|
|
70 | #: forms.py:113 | |
|
75 | 71 | msgid "e-mail" |
|
76 | 72 | msgstr "" |
|
77 | 73 | |
|
78 |
#: forms.py:12 |
|
|
74 | #: forms.py:124 | |
|
79 | 75 | #, python-format |
|
80 | 76 | msgid "Title must have less than %s characters" |
|
81 | 77 | msgstr "ΠΠ°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ Π΄ΠΎΠ»ΠΆΠ΅Π½ ΠΈΠΌΠ΅ΡΡ ΠΌΠ΅Π½ΡΡΠ΅ %s ΡΠΈΠΌΠ²ΠΎΠ»ΠΎΠ²" |
|
82 | 78 | |
|
83 |
#: forms.py:13 |
|
|
79 | #: forms.py:133 | |
|
84 | 80 | #, python-format |
|
85 | 81 | msgid "Text must have less than %s characters" |
|
86 | 82 | msgstr "Π’Π΅ΠΊΡΡ Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±ΡΡΡ ΠΊΠΎΡΠΎΡΠ΅ %s ΡΠΈΠΌΠ²ΠΎΠ»ΠΎΠ²" |
|
87 | 83 | |
|
88 |
#: forms.py:14 |
|
|
84 | #: forms.py:144 | |
|
89 | 85 | #, python-format |
|
90 | 86 | msgid "Image must be less than %s bytes" |
|
91 | 87 | msgstr "ΠΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΠ΅ Π΄ΠΎΠ»ΠΆΠ½ΠΎ Π±ΡΡΡ ΠΌΠ΅Π½Π΅Π΅ %s Π±Π°ΠΉΡ" |
|
92 | 88 | |
|
93 |
#: forms.py:1 |
|
|
89 | #: forms.py:179 | |
|
94 | 90 | msgid "Either text or image must be entered." |
|
95 | 91 | msgstr "Π’Π΅ΠΊΡΡ ΠΈΠ»ΠΈ ΠΊΠ°ΡΡΠΈΠ½ΠΊΠ° Π΄ΠΎΠ»ΠΆΠ½Ρ Π±ΡΡΡ Π²Π²Π΅Π΄Π΅Π½Ρ." |
|
96 | 92 | |
|
97 |
#: forms.py: |
|
|
93 | #: forms.py:199 | |
|
98 | 94 | #, python-format |
|
99 | 95 | msgid "Wait %s seconds after last posting" |
|
100 | 96 | msgstr "ΠΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡΠ΅ %s ΡΠ΅ΠΊΡΠ½Π΄ ΠΏΠΎΡΠ»Π΅ ΠΏΠΎΡΠ»Π΅Π΄Π½Π΅Π³ΠΎ ΠΏΠΎΡΡΠΈΠ½Π³Π°" |
|
101 | 97 | |
|
102 |
#: forms.py:21 |
|
|
98 | #: forms.py:215 templates/boards/tags.html:7 templates/boards/rss/post.html:10 | |
|
103 | 99 | msgid "Tags" |
|
104 | 100 | msgstr "Π’Π΅Π³ΠΈ" |
|
105 | 101 | |
|
106 |
#: forms.py:22 |
|
|
102 | #: forms.py:222 forms.py:290 | |
|
107 | 103 | msgid "Inappropriate characters in tags." |
|
108 | 104 | msgstr "ΠΠ΅Π΄ΠΎΠΏΡΡΡΠΈΠΌΡΠ΅ ΡΠΈΠΌΠ²ΠΎΠ»Ρ Π² ΡΠ΅Π³Π°Ρ ." |
|
109 | 105 | |
|
110 |
#: forms.py:25 |
|
|
106 | #: forms.py:250 forms.py:271 | |
|
111 | 107 | msgid "Captcha validation failed" |
|
112 | 108 | msgstr "ΠΡΠΎΠ²Π΅ΡΠΊΠ° ΠΊΠ°ΠΏΡΠΈ ΠΏΡΠΎΠ²Π°Π»Π΅Π½Π°" |
|
113 | 109 | |
|
114 |
#: forms.py:27 |
|
|
110 | #: forms.py:277 | |
|
115 | 111 | msgid "Theme" |
|
116 | 112 | msgstr "Π’Π΅ΠΌΠ°" |
|
117 | 113 | |
|
118 |
#: forms.py:31 |
|
|
114 | #: forms.py:313 | |
|
119 | 115 | msgid "Invalid master password" |
|
120 | 116 | msgstr "ΠΠ΅Π²Π΅ΡΠ½ΡΠΉ ΠΌΠ°ΡΡΠ΅Ρ-ΠΏΠ°ΡΠΎΠ»Ρ" |
|
121 | 117 | |
|
122 |
#: forms.py:32 |
|
|
118 | #: forms.py:327 | |
|
123 | 119 | #, python-format |
|
124 | 120 | msgid "Wait %s minutes after last login" |
|
125 | 121 | msgstr "ΠΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡΠ΅ %s ΠΌΠΈΠ½ΡΡ ΠΏΠΎΡΠ»Π΅ ΠΏΠΎΡΠ»Π΅Π΄Π½Π΅Π³ΠΎ Π²Ρ ΠΎΠ΄Π°" |
|
126 | 122 | |
|
127 | 123 | #: templates/boards/404.html:6 |
|
128 | 124 | msgid "Not found" |
|
129 | 125 | msgstr "ΠΠ΅ Π½Π°ΠΉΠ΄Π΅Π½ΠΎ" |
|
130 | 126 | |
|
131 | 127 | #: templates/boards/404.html:12 |
|
132 | 128 | msgid "This page does not exist" |
|
133 | 129 | msgstr "ΠΡΠΎΠΉ ΡΡΡΠ°Π½ΠΈΡΡ Π½Π΅ ΡΡΡΠ΅ΡΡΠ²ΡΠ΅Ρ" |
|
134 | 130 | |
|
135 | 131 | #: templates/boards/authors.html:6 templates/boards/authors.html.py:12 |
|
136 | 132 | msgid "Authors" |
|
137 | 133 | msgstr "ΠΠ²ΡΠΎΡΡ" |
|
138 | 134 | |
|
139 | 135 | #: templates/boards/authors.html:26 |
|
140 | 136 | msgid "Distributed under the" |
|
141 | 137 | msgstr "Π Π°ΡΠΏΡΠΎΡΡΡΠ°Π½ΡΠ΅ΡΡΡ ΠΏΠΎΠ΄" |
|
142 | 138 | |
|
143 | 139 | #: templates/boards/authors.html:28 |
|
144 | 140 | msgid "license" |
|
145 | 141 | msgstr "Π»ΠΈΡΠ΅Π½Π·ΠΈΠ΅ΠΉ" |
|
146 | 142 | |
|
147 | 143 | #: templates/boards/authors.html:30 |
|
148 | 144 | msgid "Repository" |
|
149 | 145 | msgstr "Π Π΅ΠΏΠΎΠ·ΠΈΡΠΎΡΠΈΠΉ" |
|
150 | 146 | |
|
151 | 147 | #: templates/boards/base.html:12 |
|
152 | 148 | msgid "Feed" |
|
153 | 149 | msgstr "ΠΠ΅Π½ΡΠ°" |
|
154 | 150 | |
|
155 | 151 | #: templates/boards/base.html:29 |
|
156 | 152 | msgid "All threads" |
|
157 | 153 | msgstr "ΠΡΠ΅ ΡΠ΅ΠΌΡ" |
|
158 | 154 | |
|
159 | 155 | #: templates/boards/base.html:34 |
|
160 | 156 | msgid "Tag management" |
|
161 | 157 | msgstr "Π£ΠΏΡΠ°Π²Π»Π΅Π½ΠΈΠ΅ ΡΠ΅Π³Π°ΠΌΠΈ" |
|
162 | 158 | |
|
163 | 159 | #: templates/boards/base.html:36 templates/boards/settings.html:7 |
|
164 | 160 | msgid "Settings" |
|
165 | 161 | msgstr "ΠΠ°ΡΡΡΠΎΠΉΠΊΠΈ" |
|
166 | 162 | |
|
167 | 163 | #: templates/boards/base.html:50 |
|
168 | 164 | msgid "Logout" |
|
169 | 165 | msgstr "ΠΡΡ ΠΎΠ΄" |
|
170 | 166 | |
|
171 | 167 | #: templates/boards/base.html:52 templates/boards/login.html:6 |
|
172 | 168 | #: templates/boards/login.html.py:16 |
|
173 | 169 | msgid "Login" |
|
174 | 170 | msgstr "ΠΡ ΠΎΠ΄" |
|
175 | 171 | |
|
176 | 172 | #: templates/boards/base.html:56 |
|
177 | 173 | #, python-format |
|
178 | 174 | msgid "Speed: %(ppd)s posts per day" |
|
179 | 175 | msgstr "Π‘ΠΊΠΎΡΠΎΡΡΡ: %(ppd)s ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΠΉ Π² Π΄Π΅Π½Ρ" |
|
180 | 176 | |
|
181 | 177 | #: templates/boards/base.html:58 |
|
182 | 178 | msgid "Up" |
|
183 | 179 | msgstr "ΠΠ²Π΅ΡΡ " |
|
184 | 180 | |
|
185 | 181 | #: templates/boards/login.html:19 |
|
186 | 182 | msgid "Insert your user id above" |
|
187 | 183 | msgstr "ΠΡΡΠ°Π²ΡΡΠ΅ ΡΠ²ΠΎΠΉ ID ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Ρ Π²ΡΡΠ΅" |
|
188 | 184 | |
|
189 |
#: templates/boards/post.html:21 templates/boards/staticpages/help.html:1 |
|
|
185 | #: templates/boards/post.html:21 templates/boards/staticpages/help.html:17 | |
|
190 | 186 | msgid "Quote" |
|
191 | 187 | msgstr "Π¦ΠΈΡΠ°ΡΠ°" |
|
192 | 188 | |
|
193 | 189 | #: templates/boards/post.html:31 |
|
194 | 190 | msgid "Open" |
|
195 | 191 | msgstr "ΠΡΠΊΡΡΡΡ" |
|
196 | 192 | |
|
197 | 193 | #: templates/boards/post.html:33 |
|
198 | 194 | msgid "Reply" |
|
199 | 195 | msgstr "ΠΡΠ²Π΅Ρ" |
|
200 | 196 | |
|
201 | 197 | #: templates/boards/post.html:40 |
|
202 | 198 | msgid "Edit" |
|
203 | 199 | msgstr "ΠΠ·ΠΌΠ΅Π½ΠΈΡΡ" |
|
204 | 200 | |
|
205 | 201 | #: templates/boards/post.html:42 |
|
206 | 202 | msgid "Delete" |
|
207 | 203 | msgstr "Π£Π΄Π°Π»ΠΈΡΡ" |
|
208 | 204 | |
|
209 | 205 | #: templates/boards/post.html:45 |
|
210 | 206 | msgid "Ban IP" |
|
211 | 207 | msgstr "ΠΠ°Π±Π»ΠΎΠΊΠΈΡΠΎΠ²Π°ΡΡ IP" |
|
212 | 208 | |
|
213 | 209 | #: templates/boards/post.html:76 |
|
214 | 210 | msgid "Replies" |
|
215 | 211 | msgstr "ΠΡΠ²Π΅ΡΡ" |
|
216 | 212 | |
|
217 | 213 | #: templates/boards/post.html:86 templates/boards/thread.html:88 |
|
218 | 214 | #: templates/boards/thread_gallery.html:61 |
|
219 | 215 | msgid "replies" |
|
220 | 216 | msgstr "ΠΎΡΠ²Π΅ΡΠΎΠ²" |
|
221 | 217 | |
|
222 | 218 | #: templates/boards/post.html:87 templates/boards/thread.html:89 |
|
223 | 219 | #: templates/boards/thread_gallery.html:62 |
|
224 | 220 | msgid "images" |
|
225 | 221 | msgstr "ΠΈΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΠΉ" |
|
226 | 222 | |
|
227 | 223 | #: templates/boards/post_admin.html:19 |
|
228 | 224 | msgid "Tags:" |
|
229 | 225 | msgstr "Π’Π΅Π³ΠΈ:" |
|
230 | 226 | |
|
231 | 227 | #: templates/boards/post_admin.html:30 |
|
232 | 228 | msgid "Add tag" |
|
233 | 229 | msgstr "ΠΠΎΠ±Π°Π²ΠΈΡΡ ΡΠ΅Π³" |
|
234 | 230 | |
|
235 | 231 | #: templates/boards/posting_general.html:56 |
|
236 | 232 | msgid "Show tag" |
|
237 | 233 | msgstr "ΠΠΎΠΊΠ°Π·ΡΠ²Π°ΡΡ ΡΠ΅Π³" |
|
238 | 234 | |
|
239 | 235 | #: templates/boards/posting_general.html:60 |
|
240 | 236 | msgid "Hide tag" |
|
241 | 237 | msgstr "Π‘ΠΊΡΡΠ²Π°ΡΡ ΡΠ΅Π³" |
|
242 | 238 | |
|
243 | 239 | #: templates/boards/posting_general.html:79 templates/search/search.html:22 |
|
244 | 240 | msgid "Previous page" |
|
245 | 241 | msgstr "ΠΡΠ΅Π΄ΡΠ΄ΡΡΠ°Ρ ΡΡΡΠ°Π½ΠΈΡΠ°" |
|
246 | 242 | |
|
247 | 243 | #: templates/boards/posting_general.html:94 |
|
248 | 244 | #, python-format |
|
249 | 245 | msgid "Skipped %(count)s replies. Open thread to see all replies." |
|
250 | 246 | msgstr "ΠΡΠΎΠΏΡΡΠ΅Π½ΠΎ %(count)s ΠΎΡΠ²Π΅ΡΠΎΠ². ΠΡΠΊΡΠΎΠΉΡΠ΅ ΡΡΠ΅Π΄, ΡΡΠΎΠ±Ρ ΡΠ²ΠΈΠ΄Π΅ΡΡ Π²ΡΠ΅ ΠΎΡΠ²Π΅ΡΡ." |
|
251 | 247 | |
|
252 | 248 | #: templates/boards/posting_general.html:121 templates/search/search.html:33 |
|
253 | 249 | msgid "Next page" |
|
254 | 250 | msgstr "Π‘Π»Π΅Π΄ΡΡΡΠ°Ρ ΡΡΡΠ°Π½ΠΈΡΠ°" |
|
255 | 251 | |
|
256 | 252 | #: templates/boards/posting_general.html:126 |
|
257 | 253 | msgid "No threads exist. Create the first one!" |
|
258 | 254 | msgstr "ΠΠ΅Ρ ΡΠ΅ΠΌ. Π‘ΠΎΠ·Π΄Π°ΠΉΡΠ΅ ΠΏΠ΅ΡΠ²ΡΡ!" |
|
259 | 255 | |
|
260 | 256 | #: templates/boards/posting_general.html:132 |
|
261 | 257 | msgid "Create new thread" |
|
262 | 258 | msgstr "Π‘ΠΎΠ·Π΄Π°ΡΡ Π½ΠΎΠ²ΡΡ ΡΠ΅ΠΌΡ" |
|
263 | 259 | |
|
264 | 260 | #: templates/boards/posting_general.html:137 templates/boards/thread.html:58 |
|
265 | 261 | msgid "Post" |
|
266 | 262 | msgstr "ΠΡΠΏΡΠ°Π²ΠΈΡΡ" |
|
267 | 263 | |
|
268 | 264 | #: templates/boards/posting_general.html:142 |
|
269 | 265 | msgid "Tags must be delimited by spaces. Text or image is required." |
|
270 | 266 | msgstr "" |
|
271 | 267 | "Π’Π΅Π³ΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ Π±ΡΡΡ ΡΠ°Π·Π΄Π΅Π»Π΅Π½Ρ ΠΏΡΠΎΠ±Π΅Π»Π°ΠΌΠΈ. Π’Π΅ΠΊΡΡ ΠΈΠ»ΠΈ ΠΈΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΠ΅ ΠΎΠ±ΡΠ·Π°ΡΠ΅Π»ΡΠ½Ρ." |
|
272 | 268 | |
|
273 | 269 | #: templates/boards/posting_general.html:145 templates/boards/thread.html:66 |
|
274 | 270 | msgid "Text syntax" |
|
275 | 271 | msgstr "Π‘ΠΈΠ½ΡΠ°ΠΊΡΠΈΡ ΡΠ΅ΠΊΡΡΠ°" |
|
276 | 272 | |
|
277 | 273 | #: templates/boards/posting_general.html:157 |
|
278 | 274 | msgid "Pages:" |
|
279 | 275 | msgstr "Π‘ΡΡΠ°Π½ΠΈΡΡ: " |
|
280 | 276 | |
|
281 | 277 | #: templates/boards/settings.html:15 |
|
282 | 278 | msgid "You are moderator." |
|
283 | 279 | msgstr "ΠΡ ΠΌΠΎΠ΄Π΅ΡΠ°ΡΠΎΡ." |
|
284 | 280 | |
|
285 | 281 | #: templates/boards/settings.html:19 |
|
286 | 282 | msgid "Hidden tags:" |
|
287 | 283 | msgstr "Π‘ΠΊΡΡΡΡΠ΅ ΡΠ΅Π³ΠΈ:" |
|
288 | 284 | |
|
289 | 285 | #: templates/boards/settings.html:26 |
|
290 | 286 | msgid "No hidden tags." |
|
291 | 287 | msgstr "ΠΠ΅Ρ ΡΠΊΡΡΡΡΡ ΡΠ΅Π³ΠΎΠ²." |
|
292 | 288 | |
|
293 | 289 | #: templates/boards/settings.html:35 |
|
294 | 290 | msgid "Save" |
|
295 | 291 | msgstr "Π‘ΠΎΡ ΡΠ°Π½ΠΈΡΡ" |
|
296 | 292 | |
|
297 | 293 | #: templates/boards/tags.html:22 |
|
298 | 294 | msgid "No tags found." |
|
299 | 295 | msgstr "Π’Π΅Π³ΠΈ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½Ρ." |
|
300 | 296 | |
|
301 | 297 | #: templates/boards/thread.html:20 templates/boards/thread_gallery.html:21 |
|
302 | 298 | msgid "Normal mode" |
|
303 | 299 | msgstr "ΠΠΎΡΠΌΠ°Π»ΡΠ½ΡΠΉ ΡΠ΅ΠΆΠΈΠΌ" |
|
304 | 300 | |
|
305 | 301 | #: templates/boards/thread.html:21 templates/boards/thread_gallery.html:22 |
|
306 | 302 | msgid "Gallery mode" |
|
307 | 303 | msgstr "Π Π΅ΠΆΠΈΠΌ Π³Π°Π»Π΅ΡΠ΅ΠΈ" |
|
308 | 304 | |
|
309 | 305 | #: templates/boards/thread.html:29 |
|
310 | 306 | msgid "posts to bumplimit" |
|
311 | 307 | msgstr "ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΠΉ Π΄ΠΎ Π±Π°ΠΌΠΏΠ»ΠΈΠΌΠΈΡΠ°" |
|
312 | 308 | |
|
313 | 309 | #: templates/boards/thread.html:50 |
|
314 | 310 | msgid "Reply to thread" |
|
315 | 311 | msgstr "ΠΡΠ²Π΅ΡΠΈΡΡ Π² ΡΠ΅ΠΌΡ" |
|
316 | 312 | |
|
317 | 313 | #: templates/boards/thread.html:63 |
|
318 | 314 | msgid "Switch mode" |
|
319 | 315 | msgstr "ΠΠ΅ΡΠ΅ΠΊΠ»ΡΡΠΈΡΡ ΡΠ΅ΠΆΠΈΠΌ" |
|
320 | 316 | |
|
321 | 317 | #: templates/boards/thread.html:90 templates/boards/thread_gallery.html:63 |
|
322 | 318 | msgid "Last update: " |
|
323 | 319 | msgstr "ΠΠΎΡΠ»Π΅Π΄Π½Π΅Π΅ ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅: " |
|
324 | 320 | |
|
325 | 321 | #: templates/boards/rss/post.html:5 |
|
326 | 322 | msgid "Post image" |
|
327 | 323 | msgstr "ΠΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΠ΅ ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΡ" |
|
328 | 324 | |
|
329 | 325 | #: templates/boards/staticpages/banned.html:6 |
|
330 | 326 | msgid "Banned" |
|
331 | 327 | msgstr "ΠΠ°Π±Π»ΠΎΠΊΠΈΡΠΎΠ²Π°Π½" |
|
332 | 328 | |
|
333 | 329 | #: templates/boards/staticpages/banned.html:11 |
|
334 | 330 | msgid "Your IP address has been banned. Contact the administrator" |
|
335 | 331 | msgstr "ΠΠ°Ρ IP Π°Π΄ΡΠ΅Ρ Π±ΡΠ» Π·Π°Π±Π»ΠΎΠΊΠΈΡΠΎΠ²Π°Π½. Π‘Π²ΡΠΆΠΈΡΠ΅ΡΡ Ρ Π°Π΄ΠΌΠΈΠ½ΠΈΡΡΡΠ°ΡΠΎΡΠΎΠΌ" |
|
336 | 332 | |
|
337 | 333 | #: templates/boards/staticpages/help.html:6 |
|
338 | 334 | #: templates/boards/staticpages/help.html:10 |
|
339 | 335 | msgid "Syntax" |
|
340 | 336 | msgstr "Π‘ΠΈΠ½ΡΠ°ΠΊΡΠΈΡ" |
|
341 | 337 | |
|
342 | 338 | #: templates/boards/staticpages/help.html:11 |
|
343 | msgid "2 line breaks for a new line." | |
|
344 | msgstr "2 ΠΏΠ΅ΡΠ΅Π²ΠΎΠ΄Π° ΡΡΡΠΎΠΊΠΈ ΡΠΎΠ·Π΄Π°ΡΡ Π½ΠΎΠ²ΡΠΉ Π°Π±Π·Π°Ρ." | |
|
345 | ||
|
346 | #: templates/boards/staticpages/help.html:12 | |
|
347 | 339 | msgid "Italic text" |
|
348 | 340 | msgstr "ΠΡΡΡΠΈΠ²Π½ΡΠΉ ΡΠ΅ΠΊΡΡ" |
|
349 | 341 | |
|
350 |
#: templates/boards/staticpages/help.html:1 |
|
|
342 | #: templates/boards/staticpages/help.html:12 | |
|
351 | 343 | msgid "Bold text" |
|
352 | 344 | msgstr "ΠΠΎΠ»ΡΠΆΠΈΡΠ½ΡΠΉ ΡΠ΅ΠΊΡΡ" |
|
353 | 345 | |
|
354 |
#: templates/boards/staticpages/help.html:1 |
|
|
346 | #: templates/boards/staticpages/help.html:13 | |
|
355 | 347 | msgid "Spoiler" |
|
356 | 348 | msgstr "Π‘ΠΏΠΎΠΉΠ»Π΅Ρ" |
|
357 | 349 | |
|
358 |
#: templates/boards/staticpages/help.html:1 |
|
|
350 | #: templates/boards/staticpages/help.html:14 | |
|
359 | 351 | msgid "Link to a post" |
|
360 | 352 | msgstr "Π‘ΡΡΠ»ΠΊΠ° Π½Π° ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΠ΅" |
|
361 | 353 | |
|
362 |
#: templates/boards/staticpages/help.html:1 |
|
|
354 | #: templates/boards/staticpages/help.html:15 | |
|
363 | 355 | msgid "Strikethrough text" |
|
364 | 356 | msgstr "ΠΠ°ΡΠ΅ΡΠΊΠ½ΡΡΡΠΉ ΡΠ΅ΠΊΡΡ" |
|
365 | 357 | |
|
366 |
#: templates/boards/staticpages/help.html:1 |
|
|
367 | msgid "You need to new line before:" | |
|
368 | msgstr "ΠΠ΅ΡΠ΅Π΄ ΡΡΠΈΠΌΠΈ ΡΠ΅Π³Π°ΠΌΠΈ Π½ΡΠΆΠ½Π° Π½ΠΎΠ²Π°Ρ ΡΡΡΠΎΠΊΠ°:" | |
|
369 | ||
|
370 | #: templates/boards/staticpages/help.html:18 | |
|
358 | #: templates/boards/staticpages/help.html:16 | |
|
371 | 359 | msgid "Comment" |
|
372 | 360 | msgstr "ΠΠΎΠΌΠΌΠ΅Π½ΡΠ°ΡΠΈΠΉ" |
@@ -1,349 +1,346 | |||
|
1 | 1 | from datetime import datetime, timedelta, date |
|
2 | 2 | from datetime import time as dtime |
|
3 | 3 | import logging |
|
4 | 4 | import re |
|
5 | 5 | |
|
6 | 6 | from django.core.cache import cache |
|
7 | 7 | from django.core.urlresolvers import reverse |
|
8 | 8 | from django.db import models, transaction |
|
9 | 9 | from django.template.loader import render_to_string |
|
10 | 10 | from django.utils import timezone |
|
11 | 11 | from markupfield.fields import MarkupField |
|
12 | 12 | |
|
13 | 13 | from boards.models import PostImage |
|
14 | 14 | from boards.models.base import Viewable |
|
15 | 15 | from boards.models.thread import Thread |
|
16 | 16 | |
|
17 | 17 | |
|
18 | 18 | APP_LABEL_BOARDS = 'boards' |
|
19 | 19 | |
|
20 | 20 | CACHE_KEY_PPD = 'ppd' |
|
21 | 21 | CACHE_KEY_POST_URL = 'post_url' |
|
22 | 22 | |
|
23 | 23 | POSTS_PER_DAY_RANGE = range(7) |
|
24 | 24 | |
|
25 | 25 | BAN_REASON_AUTO = 'Auto' |
|
26 | 26 | |
|
27 | 27 | IMAGE_THUMB_SIZE = (200, 150) |
|
28 | 28 | |
|
29 | 29 | TITLE_MAX_LENGTH = 200 |
|
30 | 30 | |
|
31 | 31 | DEFAULT_MARKUP_TYPE = 'bbcode' |
|
32 | 32 | |
|
33 | 33 | # TODO This should be removed |
|
34 | 34 | NO_IP = '0.0.0.0' |
|
35 | 35 | |
|
36 | 36 | # TODO Real user agent should be saved instead of this |
|
37 | 37 | UNKNOWN_UA = '' |
|
38 | 38 | |
|
39 | 39 | SETTING_MODERATE = "moderate" |
|
40 | 40 | |
|
41 |
REGEX_REPLY = re.compile(' |
|
|
41 | REGEX_REPLY = re.compile(r'>>(\d+)') | |
|
42 | 42 | |
|
43 | 43 | logger = logging.getLogger(__name__) |
|
44 | 44 | |
|
45 | 45 | |
|
46 | 46 | class PostManager(models.Manager): |
|
47 | ||
|
48 | 47 | def create_post(self, title, text, image=None, thread=None, ip=NO_IP, |
|
49 | 48 | tags=None): |
|
50 | 49 | """ |
|
51 | 50 | Creates new post |
|
52 | 51 | """ |
|
53 | 52 | |
|
54 | 53 | posting_time = timezone.now() |
|
55 | 54 | if not thread: |
|
56 | 55 | thread = Thread.objects.create(bump_time=posting_time, |
|
57 | 56 | last_edit_time=posting_time) |
|
58 | 57 | new_thread = True |
|
59 | 58 | else: |
|
60 | 59 | thread.bump() |
|
61 | 60 | thread.last_edit_time = posting_time |
|
62 | 61 | thread.save() |
|
63 | 62 | new_thread = False |
|
64 | 63 | |
|
65 | 64 | post = self.create(title=title, |
|
66 | 65 | text=text, |
|
67 | 66 | pub_time=posting_time, |
|
68 | 67 | thread_new=thread, |
|
69 | 68 | poster_ip=ip, |
|
70 | 69 | poster_user_agent=UNKNOWN_UA, # TODO Get UA at |
|
71 | 70 | # last! |
|
72 | 71 | last_edit_time=posting_time) |
|
73 | 72 | |
|
74 | 73 | if image: |
|
75 | 74 | post_image = PostImage.objects.create(image=image) |
|
76 | 75 | post.images.add(post_image) |
|
77 | 76 | logger.info('Created image #%d for post #%d' % (post_image.id, |
|
78 | 77 | post.id)) |
|
79 | 78 | |
|
80 | 79 | thread.replies.add(post) |
|
81 | 80 | if tags: |
|
82 | linked_tags = [] | |
|
83 | for tag in tags: | |
|
84 | tag_linked_tags = tag.get_linked_tags() | |
|
85 | if len(tag_linked_tags) > 0: | |
|
86 | linked_tags.extend(tag_linked_tags) | |
|
87 | ||
|
88 | tags.extend(linked_tags) | |
|
89 | 81 | map(thread.add_tag, tags) |
|
90 | 82 | |
|
91 | 83 | if new_thread: |
|
92 | 84 | Thread.objects.process_oldest_threads() |
|
93 | 85 | self.connect_replies(post) |
|
94 | 86 | |
|
95 | 87 | logger.info('Created post #%d' % post.id) |
|
96 | 88 | |
|
97 | 89 | return post |
|
98 | 90 | |
|
99 | 91 | def delete_post(self, post): |
|
100 | 92 | """ |
|
101 | 93 | Deletes post and update or delete its thread |
|
102 | 94 | """ |
|
103 | 95 | |
|
104 | 96 | post_id = post.id |
|
105 | 97 | |
|
106 | 98 | thread = post.get_thread() |
|
107 | 99 | |
|
108 | 100 | if post.is_opening(): |
|
109 | 101 | thread.delete() |
|
110 | 102 | else: |
|
111 | 103 | thread.last_edit_time = timezone.now() |
|
112 | 104 | thread.save() |
|
113 | 105 | |
|
114 | 106 | post.delete() |
|
115 | 107 | |
|
116 | 108 | logger.info('Deleted post #%d' % post_id) |
|
117 | 109 | |
|
118 | 110 | def delete_posts_by_ip(self, ip): |
|
119 | 111 | """ |
|
120 | 112 | Deletes all posts of the author with same IP |
|
121 | 113 | """ |
|
122 | 114 | |
|
123 | 115 | posts = self.filter(poster_ip=ip) |
|
124 | 116 | map(self.delete_post, posts) |
|
125 | 117 | |
|
126 | 118 | def connect_replies(self, post): |
|
127 | 119 | """ |
|
128 | 120 | Connects replies to a post to show them as a reflink map |
|
129 | 121 | """ |
|
130 | 122 | |
|
131 |
for reply_number in re.finditer(REGEX_REPLY, post.text.r |
|
|
123 | for reply_number in re.finditer(REGEX_REPLY, post.text.rendered): | |
|
132 | 124 | post_id = reply_number.group(1) |
|
133 | 125 | ref_post = self.filter(id=post_id) |
|
134 | 126 | if ref_post.count() > 0: |
|
135 | 127 | referenced_post = ref_post[0] |
|
136 | 128 | referenced_post.referenced_posts.add(post) |
|
137 | 129 | referenced_post.last_edit_time = post.pub_time |
|
138 | 130 | referenced_post.build_refmap() |
|
139 | 131 | referenced_post.save(update_fields=['refmap', 'last_edit_time']) |
|
140 | 132 | |
|
141 | 133 | referenced_thread = referenced_post.get_thread() |
|
142 | 134 | referenced_thread.last_edit_time = post.pub_time |
|
143 | 135 | referenced_thread.save(update_fields=['last_edit_time']) |
|
144 | 136 | |
|
145 | 137 | def get_posts_per_day(self): |
|
146 | 138 | """ |
|
147 | 139 | Gets average count of posts per day for the last 7 days |
|
148 | 140 | """ |
|
149 | 141 | |
|
150 | 142 | today = date.today() |
|
151 | 143 | ppd = cache.get(CACHE_KEY_PPD + str(today)) |
|
152 | 144 | if ppd: |
|
153 | 145 | return ppd |
|
154 | 146 | |
|
155 | 147 | posts_per_days = [] |
|
156 | 148 | for i in POSTS_PER_DAY_RANGE: |
|
157 | 149 | day_end = today - timedelta(i + 1) |
|
158 | 150 | day_start = today - timedelta(i + 2) |
|
159 | 151 | |
|
160 | 152 | day_time_start = timezone.make_aware(datetime.combine( |
|
161 | 153 | day_start, dtime()), timezone.get_current_timezone()) |
|
162 | 154 | day_time_end = timezone.make_aware(datetime.combine( |
|
163 | 155 | day_end, dtime()), timezone.get_current_timezone()) |
|
164 | 156 | |
|
165 | 157 | posts_per_days.append(float(self.filter( |
|
166 | 158 | pub_time__lte=day_time_end, |
|
167 | 159 | pub_time__gte=day_time_start).count())) |
|
168 | 160 | |
|
169 | 161 | ppd = (sum(posts_per_day for posts_per_day in posts_per_days) / |
|
170 | 162 | len(posts_per_days)) |
|
171 | 163 | cache.set(CACHE_KEY_PPD + str(today), ppd) |
|
172 | 164 | return ppd |
|
173 | 165 | |
|
174 | 166 | |
|
175 | 167 | class Post(models.Model, Viewable): |
|
176 | 168 | """A post is a message.""" |
|
177 | 169 | |
|
178 | 170 | objects = PostManager() |
|
179 | 171 | |
|
180 | 172 | class Meta: |
|
181 | 173 | app_label = APP_LABEL_BOARDS |
|
182 | 174 | ordering = ('id',) |
|
183 | 175 | |
|
184 | 176 | title = models.CharField(max_length=TITLE_MAX_LENGTH) |
|
185 | 177 | pub_time = models.DateTimeField() |
|
186 | 178 | text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE, |
|
187 | 179 | escape_html=False) |
|
188 | 180 | |
|
189 | 181 | images = models.ManyToManyField(PostImage, null=True, blank=True, |
|
190 | 182 | related_name='ip+', db_index=True) |
|
191 | 183 | |
|
192 | 184 | poster_ip = models.GenericIPAddressField() |
|
193 | 185 | poster_user_agent = models.TextField() |
|
194 | 186 | |
|
195 | 187 | thread_new = models.ForeignKey('Thread', null=True, default=None, |
|
196 | 188 | db_index=True) |
|
197 | 189 | last_edit_time = models.DateTimeField() |
|
198 | 190 | |
|
199 | 191 | referenced_posts = models.ManyToManyField('Post', symmetrical=False, |
|
200 | 192 | null=True, |
|
201 | 193 | blank=True, related_name='rfp+', |
|
202 | 194 | db_index=True) |
|
203 | 195 | refmap = models.TextField(null=True, blank=True) |
|
204 | 196 | |
|
205 | 197 | def __unicode__(self): |
|
206 | 198 | return '#' + str(self.id) + ' ' + self.title + ' (' + \ |
|
207 | 199 | self.text.raw[:50] + ')' |
|
208 | 200 | |
|
209 | 201 | def get_title(self): |
|
210 | 202 | """ |
|
211 | 203 | Gets original post title or part of its text. |
|
212 | 204 | """ |
|
213 | 205 | |
|
214 | 206 | title = self.title |
|
215 | 207 | if not title: |
|
216 | 208 | title = self.text.rendered |
|
217 | 209 | |
|
218 | 210 | return title |
|
219 | 211 | |
|
220 | 212 | def build_refmap(self): |
|
213 | """ | |
|
214 | Builds a replies map string from replies list. This is a cache to stop | |
|
215 | the server from recalculating the map on every post show. | |
|
216 | """ | |
|
221 | 217 | map_string = '' |
|
222 | 218 | |
|
223 | 219 | first = True |
|
224 | 220 | for refpost in self.referenced_posts.all(): |
|
225 | 221 | if not first: |
|
226 | 222 | map_string += ', ' |
|
227 |
map_string += '<a href="%s">>>%s</a>' % (refpost.get_url(), |
|
|
223 | map_string += '<a href="%s">>>%s</a>' % (refpost.get_url(), | |
|
224 | refpost.id) | |
|
228 | 225 | first = False |
|
229 | 226 | |
|
230 | 227 | self.refmap = map_string |
|
231 | 228 | |
|
232 | 229 | def get_sorted_referenced_posts(self): |
|
233 | 230 | return self.refmap |
|
234 | 231 | |
|
235 | 232 | def is_referenced(self): |
|
236 | 233 | return len(self.refmap) > 0 |
|
237 | 234 | |
|
238 | 235 | def is_opening(self): |
|
239 | 236 | """ |
|
240 | 237 | Checks if this is an opening post or just a reply. |
|
241 | 238 | """ |
|
242 | 239 | |
|
243 | 240 | return self.get_thread().get_opening_post_id() == self.id |
|
244 | 241 | |
|
245 | 242 | @transaction.atomic |
|
246 | 243 | def add_tag(self, tag): |
|
247 | 244 | edit_time = timezone.now() |
|
248 | 245 | |
|
249 | 246 | thread = self.get_thread() |
|
250 | 247 | thread.add_tag(tag) |
|
251 | 248 | self.last_edit_time = edit_time |
|
252 | self.save() | |
|
249 | self.save(update_fields=['last_edit_time']) | |
|
253 | 250 | |
|
254 | 251 | thread.last_edit_time = edit_time |
|
255 | thread.save() | |
|
252 | thread.save(update_fields=['last_edit_time']) | |
|
256 | 253 | |
|
257 | 254 | @transaction.atomic |
|
258 | 255 | def remove_tag(self, tag): |
|
259 | 256 | edit_time = timezone.now() |
|
260 | 257 | |
|
261 | 258 | thread = self.get_thread() |
|
262 | 259 | thread.remove_tag(tag) |
|
263 | 260 | self.last_edit_time = edit_time |
|
264 | self.save() | |
|
261 | self.save(update_fields=['last_edit_time']) | |
|
265 | 262 | |
|
266 | 263 | thread.last_edit_time = edit_time |
|
267 | thread.save() | |
|
264 | thread.save(update_fields=['last_edit_time']) | |
|
268 | 265 | |
|
269 | 266 | def get_url(self, thread=None): |
|
270 | 267 | """ |
|
271 | 268 | Gets full url to the post. |
|
272 | 269 | """ |
|
273 | 270 | |
|
274 | 271 | cache_key = CACHE_KEY_POST_URL + str(self.id) |
|
275 | 272 | link = cache.get(cache_key) |
|
276 | 273 | |
|
277 | 274 | if not link: |
|
278 | 275 | if not thread: |
|
279 | 276 | thread = self.get_thread() |
|
280 | 277 | |
|
281 | 278 | opening_id = thread.get_opening_post_id() |
|
282 | 279 | |
|
283 | 280 | if self.id != opening_id: |
|
284 | 281 | link = reverse('thread', kwargs={ |
|
285 | 282 | 'post_id': opening_id}) + '#' + str(self.id) |
|
286 | 283 | else: |
|
287 | 284 | link = reverse('thread', kwargs={'post_id': self.id}) |
|
288 | 285 | |
|
289 | 286 | cache.set(cache_key, link) |
|
290 | 287 | |
|
291 | 288 | return link |
|
292 | 289 | |
|
293 | 290 | def get_thread(self): |
|
294 | 291 | """ |
|
295 | 292 | Gets post's thread. |
|
296 | 293 | """ |
|
297 | 294 | |
|
298 | 295 | return self.thread_new |
|
299 | 296 | |
|
300 | 297 | def get_referenced_posts(self): |
|
301 | 298 | return self.referenced_posts.only('id', 'thread_new') |
|
302 | 299 | |
|
303 | 300 | def get_text(self): |
|
304 | 301 | return self.text |
|
305 | 302 | |
|
306 | 303 | def get_view(self, moderator=False, need_open_link=False, |
|
307 | 304 | truncated=False, *args, **kwargs): |
|
308 | 305 | if 'is_opening' in kwargs: |
|
309 | 306 | is_opening = kwargs['is_opening'] |
|
310 | 307 | else: |
|
311 | 308 | is_opening = self.is_opening() |
|
312 | 309 | |
|
313 | 310 | if 'thread' in kwargs: |
|
314 | 311 | thread = kwargs['thread'] |
|
315 | 312 | else: |
|
316 | 313 | thread = self.get_thread() |
|
317 | 314 | |
|
318 | 315 | if 'can_bump' in kwargs: |
|
319 | 316 | can_bump = kwargs['can_bump'] |
|
320 | 317 | else: |
|
321 | 318 | can_bump = thread.can_bump() |
|
322 | 319 | |
|
323 | 320 | if is_opening: |
|
324 | 321 | opening_post_id = self.id |
|
325 | 322 | else: |
|
326 | 323 | opening_post_id = thread.get_opening_post_id() |
|
327 | 324 | |
|
328 | 325 | return render_to_string('boards/post.html', { |
|
329 | 326 | 'post': self, |
|
330 | 327 | 'moderator': moderator, |
|
331 | 328 | 'is_opening': is_opening, |
|
332 | 329 | 'thread': thread, |
|
333 | 330 | 'bumpable': can_bump, |
|
334 | 331 | 'need_open_link': need_open_link, |
|
335 | 332 | 'truncated': truncated, |
|
336 | 333 | 'opening_post_id': opening_post_id, |
|
337 | 334 | }) |
|
338 | 335 | |
|
339 | 336 | def get_first_image(self): |
|
340 | 337 | return self.images.earliest('id') |
|
341 | 338 | |
|
342 | 339 | def delete(self, using=None): |
|
343 | 340 | """ |
|
344 | Delete all post images and the post itself. | |
|
341 | Deletes all post images and the post itself. | |
|
345 | 342 | """ |
|
346 | 343 | |
|
347 | 344 | self.images.all().delete() |
|
348 | 345 | |
|
349 | 346 | super(Post, self).delete(using) |
@@ -1,104 +1,78 | |||
|
1 | 1 | from django.template.loader import render_to_string |
|
2 | 2 | from django.db import models |
|
3 | 3 | from django.db.models import Count, Sum |
|
4 | 4 | from django.core.urlresolvers import reverse |
|
5 | 5 | |
|
6 | 6 | from boards.models import Thread |
|
7 | 7 | from boards.models.base import Viewable |
|
8 | 8 | |
|
9 | 9 | |
|
10 | 10 | __author__ = 'neko259' |
|
11 | 11 | |
|
12 | 12 | |
|
13 | 13 | class TagManager(models.Manager): |
|
14 | 14 | |
|
15 | 15 | def get_not_empty_tags(self): |
|
16 | 16 | """ |
|
17 | 17 | Gets tags that have non-archived threads. |
|
18 | 18 | """ |
|
19 | 19 | |
|
20 | 20 | tags = self.annotate(Count('threads')) \ |
|
21 | 21 | .filter(threads__count__gt=0).order_by('name') |
|
22 | 22 | |
|
23 | 23 | return tags |
|
24 | 24 | |
|
25 | 25 | |
|
26 | 26 | class Tag(models.Model, Viewable): |
|
27 | 27 | """ |
|
28 | 28 | A tag is a text node assigned to the thread. The tag serves as a board |
|
29 | 29 | section. There can be multiple tags for each thread |
|
30 | 30 | """ |
|
31 | 31 | |
|
32 | 32 | objects = TagManager() |
|
33 | 33 | |
|
34 | 34 | class Meta: |
|
35 | 35 | app_label = 'boards' |
|
36 | 36 | ordering = ('name',) |
|
37 | 37 | |
|
38 | 38 | name = models.CharField(max_length=100, db_index=True) |
|
39 | 39 | threads = models.ManyToManyField(Thread, null=True, |
|
40 | 40 | blank=True, related_name='tag+') |
|
41 | linked = models.ForeignKey('Tag', null=True, blank=True) | |
|
42 | 41 | |
|
43 | 42 | def __unicode__(self): |
|
44 | 43 | return self.name |
|
45 | 44 | |
|
46 | 45 | def is_empty(self): |
|
47 | 46 | """ |
|
48 | 47 | Checks if the tag has some threads. |
|
49 | 48 | """ |
|
50 | 49 | |
|
51 | 50 | return self.get_thread_count() == 0 |
|
52 | 51 | |
|
53 | 52 | def get_thread_count(self): |
|
54 | 53 | return self.threads.count() |
|
55 | 54 | |
|
56 | def get_linked_tags(self): | |
|
57 | """ | |
|
58 | Gets tags linked to the current one. | |
|
59 | """ | |
|
60 | ||
|
61 | tag_list = [] | |
|
62 | self.get_linked_tags_list(tag_list) | |
|
63 | ||
|
64 | return tag_list | |
|
65 | ||
|
66 | def get_linked_tags_list(self, tag_list=None): | |
|
67 | """ | |
|
68 | Returns the list of tags linked to current. The list can be got | |
|
69 | through returned value or tag_list parameter | |
|
70 | """ | |
|
71 | if not tag_list: | |
|
72 | tag_list = [] | |
|
73 | ||
|
74 | linked_tag = self.linked | |
|
75 | ||
|
76 | if linked_tag and not (linked_tag in tag_list): | |
|
77 | tag_list.append(linked_tag) | |
|
78 | ||
|
79 | linked_tag.get_linked_tags_list(tag_list) | |
|
80 | ||
|
81 | 55 | def get_post_count(self, archived=False): |
|
82 | 56 | """ |
|
83 | 57 | Gets posts count for the tag's threads. |
|
84 | 58 | """ |
|
85 | 59 | |
|
86 | 60 | posts_count = 0 |
|
87 | 61 | |
|
88 | 62 | threads = self.threads.filter(archived=archived) |
|
89 | 63 | if threads.exists(): |
|
90 | 64 | posts_count = threads.annotate(posts_count=Count('replies')) \ |
|
91 | 65 | .aggregate(posts_sum=Sum('posts_count'))['posts_sum'] |
|
92 | 66 | |
|
93 | 67 | if not posts_count: |
|
94 | 68 | posts_count = 0 |
|
95 | 69 | |
|
96 | 70 | return posts_count |
|
97 | 71 | |
|
98 | 72 | def get_url(self): |
|
99 | 73 | return reverse('tag', kwargs={'tag_name': self.name}) |
|
100 | 74 | |
|
101 | 75 | def get_view(self, *args, **kwargs): |
|
102 | 76 | return render_to_string('boards/tag.html', { |
|
103 | 77 | 'tag': self, |
|
104 | 78 | }) |
@@ -1,446 +1,448 | |||
|
1 | 1 | html { |
|
2 | 2 | background: #555; |
|
3 | 3 | color: #ffffff; |
|
4 | 4 | } |
|
5 | 5 | |
|
6 | 6 | body { |
|
7 | 7 | margin: 0; |
|
8 | 8 | } |
|
9 | 9 | |
|
10 | 10 | #admin_panel { |
|
11 | 11 | background: #FF0000; |
|
12 | 12 | color: #00FF00 |
|
13 | 13 | } |
|
14 | 14 | |
|
15 | 15 | .input_field_error { |
|
16 | 16 | color: #FF0000; |
|
17 | 17 | } |
|
18 | 18 | |
|
19 | 19 | .title { |
|
20 | 20 | font-weight: bold; |
|
21 | 21 | color: #ffcc00; |
|
22 | 22 | } |
|
23 | 23 | |
|
24 | 24 | .link, a { |
|
25 | 25 | color: #afdcec; |
|
26 | 26 | } |
|
27 | 27 | |
|
28 | 28 | .block { |
|
29 | 29 | display: inline-block; |
|
30 | 30 | vertical-align: top; |
|
31 | 31 | } |
|
32 | 32 | |
|
33 | 33 | .tag { |
|
34 | 34 | color: #FFD37D; |
|
35 | 35 | } |
|
36 | 36 | |
|
37 | 37 | .post_id { |
|
38 | 38 | color: #fff380; |
|
39 | 39 | } |
|
40 | 40 | |
|
41 | 41 | .post, .dead_post, .archive_post, #posts-table { |
|
42 | 42 | background: #333; |
|
43 | 43 | padding: 10px; |
|
44 | 44 | clear: left; |
|
45 | 45 | word-wrap: break-word; |
|
46 | 46 | border-top: 1px solid #777; |
|
47 | 47 | border-bottom: 1px solid #777; |
|
48 | 48 | } |
|
49 | 49 | |
|
50 | 50 | .post + .post { |
|
51 | 51 | border-top: none; |
|
52 | 52 | } |
|
53 | 53 | |
|
54 | 54 | .dead_post + .dead_post { |
|
55 | 55 | border-top: none; |
|
56 | 56 | } |
|
57 | 57 | |
|
58 | 58 | .archive_post + .archive_post { |
|
59 | 59 | border-top: none; |
|
60 | 60 | } |
|
61 | 61 | |
|
62 | 62 | .metadata { |
|
63 | 63 | padding-top: 5px; |
|
64 | 64 | margin-top: 10px; |
|
65 | 65 | border-top: solid 1px #666; |
|
66 | 66 | color: #ddd; |
|
67 | 67 | } |
|
68 | 68 | |
|
69 | 69 | .navigation_panel, .tag_info { |
|
70 | 70 | background: #444; |
|
71 | 71 | margin-bottom: 5px; |
|
72 | 72 | margin-top: 5px; |
|
73 | 73 | padding: 10px; |
|
74 | 74 | border-bottom: solid 1px #888; |
|
75 | 75 | border-top: solid 1px #888; |
|
76 | 76 | color: #eee; |
|
77 | 77 | } |
|
78 | 78 | |
|
79 | 79 | .navigation_panel .link { |
|
80 | 80 | border-right: 1px solid #fff; |
|
81 | 81 | font-weight: bold; |
|
82 | 82 | margin-right: 1ex; |
|
83 | 83 | padding-right: 1ex; |
|
84 | 84 | } |
|
85 | 85 | .navigation_panel .link:last-child { |
|
86 | 86 | border-left: 1px solid #fff; |
|
87 | 87 | border-right: none; |
|
88 | 88 | float: right; |
|
89 | 89 | margin-left: 1ex; |
|
90 | 90 | margin-right: 0; |
|
91 | 91 | padding-left: 1ex; |
|
92 | 92 | padding-right: 0; |
|
93 | 93 | } |
|
94 | 94 | |
|
95 | 95 | .navigation_panel::after, .post::after { |
|
96 | 96 | clear: both; |
|
97 | 97 | content: "."; |
|
98 | 98 | display: block; |
|
99 | 99 | height: 0; |
|
100 | 100 | line-height: 0; |
|
101 | 101 | visibility: hidden; |
|
102 | 102 | } |
|
103 | 103 | |
|
104 | 104 | p { |
|
105 | 105 | margin-top: .5em; |
|
106 | 106 | margin-bottom: .5em; |
|
107 | 107 | } |
|
108 | 108 | |
|
109 | 109 | .post-form-w { |
|
110 | 110 | background: #333344; |
|
111 | 111 | border-top: solid 1px #888; |
|
112 | 112 | border-bottom: solid 1px #888; |
|
113 | 113 | color: #fff; |
|
114 | 114 | padding: 10px; |
|
115 | 115 | margin-bottom: 5px; |
|
116 | 116 | margin-top: 5px; |
|
117 | 117 | } |
|
118 | 118 | |
|
119 | 119 | .form-row { |
|
120 | 120 | width: 100%; |
|
121 | 121 | } |
|
122 | 122 | |
|
123 | 123 | .form-label { |
|
124 | 124 | padding: .25em 1ex .25em 0; |
|
125 | 125 | vertical-align: top; |
|
126 | 126 | } |
|
127 | 127 | |
|
128 | 128 | .form-input { |
|
129 | 129 | padding: .25em 0; |
|
130 | 130 | } |
|
131 | 131 | |
|
132 | 132 | .form-errors { |
|
133 | 133 | font-weight: bolder; |
|
134 | 134 | vertical-align: middle; |
|
135 | 135 | padding: 3px; |
|
136 | 136 | background: repeating-linear-gradient( |
|
137 | 137 | -45deg, |
|
138 | 138 | #330, |
|
139 | 139 | #330 10px, |
|
140 | 140 | #111 10px, |
|
141 | 141 | #111 20px |
|
142 | 142 | ); |
|
143 | 143 | } |
|
144 | 144 | |
|
145 | 145 | .post-form input:not([name="image"]), .post-form textarea { |
|
146 | 146 | background: #333; |
|
147 | 147 | color: #fff; |
|
148 | 148 | border: solid 1px; |
|
149 | 149 | padding: 0; |
|
150 | 150 | font: medium sans-serif; |
|
151 | 151 | width: 100%; |
|
152 | 152 | } |
|
153 | 153 | |
|
154 | 154 | .form-submit { |
|
155 | 155 | display: table; |
|
156 | 156 | margin-bottom: 1ex; |
|
157 | 157 | margin-top: 1ex; |
|
158 | 158 | } |
|
159 | 159 | |
|
160 | 160 | .form-title { |
|
161 | 161 | font-weight: bold; |
|
162 | 162 | font-size: 2ex; |
|
163 | 163 | margin-bottom: 0.5ex; |
|
164 | 164 | } |
|
165 | 165 | |
|
166 | 166 | .post-form input[type="submit"], input[type="submit"] { |
|
167 | 167 | background: #222; |
|
168 | 168 | border: solid 2px #fff; |
|
169 | 169 | color: #fff; |
|
170 | 170 | padding: 0.5ex; |
|
171 | 171 | } |
|
172 | 172 | |
|
173 | 173 | input[type="submit"]:hover { |
|
174 | 174 | background: #060; |
|
175 | 175 | } |
|
176 | 176 | |
|
177 | 177 | blockquote { |
|
178 | 178 | border-left: solid 2px; |
|
179 | 179 | padding-left: 5px; |
|
180 | 180 | color: #B1FB17; |
|
181 | 181 | margin: 0; |
|
182 | 182 | } |
|
183 | 183 | |
|
184 | 184 | .post > .image { |
|
185 | 185 | float: left; |
|
186 | 186 | margin: 0 1ex .5ex 0; |
|
187 | 187 | min-width: 1px; |
|
188 | 188 | text-align: center; |
|
189 | 189 | display: table-row; |
|
190 | 190 | } |
|
191 | 191 | |
|
192 | 192 | .post > .metadata { |
|
193 | 193 | clear: left; |
|
194 | 194 | } |
|
195 | 195 | |
|
196 | 196 | .get { |
|
197 | 197 | font-weight: bold; |
|
198 | 198 | color: #d55; |
|
199 | 199 | } |
|
200 | 200 | |
|
201 | 201 | * { |
|
202 | 202 | text-decoration: none; |
|
203 | 203 | } |
|
204 | 204 | |
|
205 | 205 | .dead_post { |
|
206 | 206 | background-color: #442222; |
|
207 | 207 | } |
|
208 | 208 | |
|
209 | 209 | .archive_post { |
|
210 | 210 | background-color: #000; |
|
211 | 211 | } |
|
212 | 212 | |
|
213 | 213 | .mark_btn { |
|
214 | 214 | border: 1px solid; |
|
215 | 215 | min-width: 2ex; |
|
216 | 216 | padding: 2px 2ex; |
|
217 | 217 | } |
|
218 | 218 | |
|
219 | 219 | .mark_btn:hover { |
|
220 | 220 | background: #555; |
|
221 | 221 | } |
|
222 | 222 | |
|
223 | 223 | .quote { |
|
224 | 224 | color: #92cf38; |
|
225 | 225 | font-style: italic; |
|
226 | 226 | } |
|
227 | 227 | |
|
228 | 228 | .multiquote { |
|
229 | color: #92cf38; | |
|
230 | font-style: italic; | |
|
231 | border-left: solid 3px #00aa00; | |
|
232 | padding-left: 3px; | |
|
229 | border-left: solid 4px #ccc; | |
|
230 | padding: 3px; | |
|
233 | 231 | display: inline-block; |
|
232 | background: #222; | |
|
233 | border-right: solid 1px #ccc; | |
|
234 | border-top: solid 1px #ccc; | |
|
235 | border-bottom: solid 1px #ccc; | |
|
234 | 236 | } |
|
235 | 237 | |
|
236 | 238 | .spoiler { |
|
237 | 239 | background: white; |
|
238 | 240 | color: white; |
|
239 | 241 | } |
|
240 | 242 | |
|
241 | 243 | .spoiler:hover { |
|
242 | 244 | color: black; |
|
243 | 245 | } |
|
244 | 246 | |
|
245 | 247 | .comment { |
|
246 | 248 | color: #eb2; |
|
247 | 249 | } |
|
248 | 250 | |
|
249 | 251 | a:hover { |
|
250 | 252 | text-decoration: underline; |
|
251 | 253 | } |
|
252 | 254 | |
|
253 | 255 | .last-replies { |
|
254 | 256 | margin-left: 3ex; |
|
255 | 257 | margin-right: 3ex; |
|
256 | 258 | } |
|
257 | 259 | |
|
258 | 260 | .thread { |
|
259 | 261 | margin-bottom: 3ex; |
|
260 | 262 | margin-top: 1ex; |
|
261 | 263 | } |
|
262 | 264 | |
|
263 | 265 | .post:target { |
|
264 | 266 | border: solid 2px white; |
|
265 | 267 | } |
|
266 | 268 | |
|
267 | 269 | pre{ |
|
268 | 270 | white-space:pre-wrap |
|
269 | 271 | } |
|
270 | 272 | |
|
271 | 273 | li { |
|
272 | 274 | list-style-position: inside; |
|
273 | 275 | } |
|
274 | 276 | |
|
275 | 277 | .fancybox-skin { |
|
276 | 278 | position: relative; |
|
277 | 279 | background-color: #fff; |
|
278 | 280 | color: #ddd; |
|
279 | 281 | text-shadow: none; |
|
280 | 282 | } |
|
281 | 283 | |
|
282 | 284 | .fancybox-image { |
|
283 | 285 | border: 1px solid black; |
|
284 | 286 | } |
|
285 | 287 | |
|
286 | 288 | .image-mode-tab { |
|
287 | 289 | background: #444; |
|
288 | 290 | color: #eee; |
|
289 | 291 | margin-top: 5px; |
|
290 | 292 | padding: 5px; |
|
291 | 293 | border-top: 1px solid #888; |
|
292 | 294 | border-bottom: 1px solid #888; |
|
293 | 295 | } |
|
294 | 296 | |
|
295 | 297 | .image-mode-tab > label { |
|
296 | 298 | margin: 0 1ex; |
|
297 | 299 | } |
|
298 | 300 | |
|
299 | 301 | .image-mode-tab > label > input { |
|
300 | 302 | margin-right: .5ex; |
|
301 | 303 | } |
|
302 | 304 | |
|
303 | 305 | #posts-table { |
|
304 | 306 | margin-top: 5px; |
|
305 | 307 | margin-bottom: 5px; |
|
306 | 308 | } |
|
307 | 309 | |
|
308 | 310 | .tag_info > h2 { |
|
309 | 311 | margin: 0; |
|
310 | 312 | } |
|
311 | 313 | |
|
312 | 314 | .post-info { |
|
313 | 315 | color: #ddd; |
|
314 | 316 | margin-bottom: 1ex; |
|
315 | 317 | } |
|
316 | 318 | |
|
317 | 319 | .moderator_info { |
|
318 | 320 | color: #e99d41; |
|
319 | 321 | float: right; |
|
320 | 322 | font-weight: bold; |
|
321 | 323 | } |
|
322 | 324 | |
|
323 | 325 | .refmap { |
|
324 | 326 | font-size: 0.9em; |
|
325 | 327 | color: #ccc; |
|
326 | 328 | margin-top: 1em; |
|
327 | 329 | } |
|
328 | 330 | |
|
329 | 331 | .fav { |
|
330 | 332 | color: yellow; |
|
331 | 333 | } |
|
332 | 334 | |
|
333 | 335 | .not_fav { |
|
334 | 336 | color: #ccc; |
|
335 | 337 | } |
|
336 | 338 | |
|
337 | 339 | .role { |
|
338 | 340 | text-decoration: underline; |
|
339 | 341 | } |
|
340 | 342 | |
|
341 | 343 | .form-email { |
|
342 | 344 | display: none; |
|
343 | 345 | } |
|
344 | 346 | |
|
345 | 347 | .footer { |
|
346 | 348 | margin: 5px; |
|
347 | 349 | } |
|
348 | 350 | |
|
349 | 351 | .bar-value { |
|
350 | 352 | background: rgba(50, 55, 164, 0.45); |
|
351 | 353 | font-size: 0.9em; |
|
352 | 354 | height: 1.5em; |
|
353 | 355 | } |
|
354 | 356 | |
|
355 | 357 | .bar-bg { |
|
356 | 358 | position: relative; |
|
357 | 359 | border-top: solid 1px #888; |
|
358 | 360 | border-bottom: solid 1px #888; |
|
359 | 361 | margin-top: 5px; |
|
360 | 362 | overflow: hidden; |
|
361 | 363 | } |
|
362 | 364 | |
|
363 | 365 | .bar-text { |
|
364 | 366 | padding: 2px; |
|
365 | 367 | position: absolute; |
|
366 | 368 | left: 0; |
|
367 | 369 | top: 0; |
|
368 | 370 | } |
|
369 | 371 | |
|
370 | 372 | .page_link { |
|
371 | 373 | background: #444; |
|
372 | 374 | border-top: solid 1px #888; |
|
373 | 375 | border-bottom: solid 1px #888; |
|
374 | 376 | padding: 5px; |
|
375 | 377 | color: #eee; |
|
376 | 378 | font-size: 2ex; |
|
377 | 379 | } |
|
378 | 380 | |
|
379 | 381 | .skipped_replies { |
|
380 | 382 | margin: 5px; |
|
381 | 383 | } |
|
382 | 384 | |
|
383 | 385 | .current_page { |
|
384 | 386 | border: solid 1px #afdcec; |
|
385 | 387 | padding: 2px; |
|
386 | 388 | } |
|
387 | 389 | |
|
388 | 390 | .current_mode { |
|
389 | 391 | font-weight: bold; |
|
390 | 392 | } |
|
391 | 393 | |
|
392 | 394 | .gallery_image { |
|
393 | 395 | border: solid 1px; |
|
394 | 396 | padding: 0.5ex; |
|
395 | 397 | margin: 0.5ex; |
|
396 | 398 | text-align: center; |
|
397 | 399 | } |
|
398 | 400 | |
|
399 | 401 | code { |
|
400 | 402 | border: dashed 1px #ccc; |
|
401 | 403 | background: #111; |
|
402 | 404 | padding: 2px; |
|
403 | 405 | font-size: 1.2em; |
|
404 | 406 | display: inline-block; |
|
405 | 407 | } |
|
406 | 408 | |
|
407 | 409 | pre { |
|
408 | 410 | overflow: auto; |
|
409 | 411 | } |
|
410 | 412 | |
|
411 | 413 | .img-full { |
|
412 | 414 | background: #222; |
|
413 | 415 | border: solid 1px white; |
|
414 | 416 | } |
|
415 | 417 | |
|
416 | 418 | .tag_item { |
|
417 | 419 | display: inline-block; |
|
418 | 420 | border: 1px dashed #666; |
|
419 | 421 | margin: 0.2ex; |
|
420 | 422 | padding: 0.1ex; |
|
421 | 423 | } |
|
422 | 424 | |
|
423 | 425 | #id_models li { |
|
424 | 426 | list-style: none; |
|
425 | 427 | } |
|
426 | 428 | |
|
427 | 429 | #id_q { |
|
428 | 430 | margin-left: 1ex; |
|
429 | 431 | } |
|
430 | 432 | |
|
431 | 433 | ul { |
|
432 | 434 | padding-left: 0px; |
|
433 | 435 | } |
|
434 | 436 | |
|
435 | 437 | /* Reflink preview */ |
|
436 | 438 | .post_preview { |
|
437 | 439 | border-left: 1px solid #777; |
|
438 | 440 | border-right: 1px solid #777; |
|
439 | 441 | } |
|
440 | 442 | |
|
441 | 443 | /* Code highlighter */ |
|
442 | 444 | .hljs { |
|
443 | 445 | color: #fff; |
|
444 | 446 | background: #000; |
|
445 | 447 | display: inline-block; |
|
446 | 448 | } |
@@ -1,275 +1,270 | |||
|
1 | 1 | # coding=utf-8 |
|
2 | 2 | import time |
|
3 | 3 | import logging |
|
4 | 4 | from django.core.paginator import Paginator |
|
5 | 5 | |
|
6 | 6 | from django.test import TestCase |
|
7 | 7 | from django.test.client import Client |
|
8 | 8 | from django.core.urlresolvers import reverse, NoReverseMatch |
|
9 | 9 | from boards.abstracts.settingsmanager import get_settings_manager |
|
10 | 10 | |
|
11 | 11 | from boards.models import Post, Tag, Thread |
|
12 | 12 | from boards import urls |
|
13 | 13 | from boards import settings |
|
14 | 14 | import neboard |
|
15 | 15 | |
|
16 | TEST_TAG = 'test_tag' | |
|
17 | ||
|
16 | 18 | PAGE_404 = 'boards/404.html' |
|
17 | 19 | |
|
18 | 20 | TEST_TEXT = 'test text' |
|
19 | 21 | |
|
20 | 22 | NEW_THREAD_PAGE = '/' |
|
21 | 23 | THREAD_PAGE_ONE = '/thread/1/' |
|
22 | 24 | THREAD_PAGE = '/thread/' |
|
23 | 25 | TAG_PAGE = '/tag/' |
|
24 | 26 | HTTP_CODE_REDIRECT = 302 |
|
25 | 27 | HTTP_CODE_OK = 200 |
|
26 | 28 | HTTP_CODE_NOT_FOUND = 404 |
|
27 | 29 | |
|
28 | 30 | logger = logging.getLogger(__name__) |
|
29 | 31 | |
|
30 | 32 | |
|
31 | 33 | class PostTests(TestCase): |
|
32 | 34 | |
|
33 | 35 | def _create_post(self): |
|
34 | return Post.objects.create_post(title='title', text='text') | |
|
36 | tag = Tag.objects.create(name=TEST_TAG) | |
|
37 | return Post.objects.create_post(title='title', text='text', | |
|
38 | tags=[tag]) | |
|
35 | 39 | |
|
36 | 40 | def test_post_add(self): |
|
37 | 41 | """Test adding post""" |
|
38 | 42 | |
|
39 | 43 | post = self._create_post() |
|
40 | 44 | |
|
41 | self.assertIsNotNone(post, 'No post was created') | |
|
45 | self.assertIsNotNone(post, 'No post was created.') | |
|
46 | self.assertEqual(TEST_TAG, post.get_thread().tags.all()[0].name, | |
|
47 | 'No tags were added to the post.') | |
|
42 | 48 | |
|
43 | 49 | def test_delete_post(self): |
|
44 | 50 | """Test post deletion""" |
|
45 | 51 | |
|
46 | 52 | post = self._create_post() |
|
47 | 53 | post_id = post.id |
|
48 | 54 | |
|
49 | 55 | Post.objects.delete_post(post) |
|
50 | 56 | |
|
51 | 57 | self.assertFalse(Post.objects.filter(id=post_id).exists()) |
|
52 | 58 | |
|
53 | 59 | def test_delete_thread(self): |
|
54 | 60 | """Test thread deletion""" |
|
55 | 61 | |
|
56 | 62 | opening_post = self._create_post() |
|
57 | 63 | thread = opening_post.get_thread() |
|
58 | 64 | reply = Post.objects.create_post("", "", thread=thread) |
|
59 | 65 | |
|
60 | 66 | thread.delete() |
|
61 | 67 | |
|
62 | 68 | self.assertFalse(Post.objects.filter(id=reply.id).exists()) |
|
63 | 69 | |
|
64 | 70 | def test_post_to_thread(self): |
|
65 | 71 | """Test adding post to a thread""" |
|
66 | 72 | |
|
67 | 73 | op = self._create_post() |
|
68 | 74 | post = Post.objects.create_post("", "", thread=op.get_thread()) |
|
69 | 75 | |
|
70 | 76 | self.assertIsNotNone(post, 'Reply to thread wasn\'t created') |
|
71 | 77 | self.assertEqual(op.get_thread().last_edit_time, post.pub_time, |
|
72 | 78 | 'Post\'s create time doesn\'t match thread last edit' |
|
73 | 79 | ' time') |
|
74 | 80 | |
|
75 | 81 | def test_delete_posts_by_ip(self): |
|
76 | 82 | """Test deleting posts with the given ip""" |
|
77 | 83 | |
|
78 | 84 | post = self._create_post() |
|
79 | 85 | post_id = post.id |
|
80 | 86 | |
|
81 | 87 | Post.objects.delete_posts_by_ip('0.0.0.0') |
|
82 | 88 | |
|
83 | 89 | self.assertFalse(Post.objects.filter(id=post_id).exists()) |
|
84 | 90 | |
|
85 | 91 | def test_get_thread(self): |
|
86 | 92 | """Test getting all posts of a thread""" |
|
87 | 93 | |
|
88 | 94 | opening_post = self._create_post() |
|
89 | 95 | |
|
90 | 96 | for i in range(0, 2): |
|
91 | 97 | Post.objects.create_post('title', 'text', |
|
92 | 98 | thread=opening_post.get_thread()) |
|
93 | 99 | |
|
94 | 100 | thread = opening_post.get_thread() |
|
95 | 101 | |
|
96 | 102 | self.assertEqual(3, thread.replies.count()) |
|
97 | 103 | |
|
98 | 104 | def test_create_post_with_tag(self): |
|
99 | 105 | """Test adding tag to post""" |
|
100 | 106 | |
|
101 | 107 | tag = Tag.objects.create(name='test_tag') |
|
102 | 108 | post = Post.objects.create_post(title='title', text='text', tags=[tag]) |
|
103 | 109 | |
|
104 | 110 | thread = post.get_thread() |
|
105 | 111 | self.assertIsNotNone(post, 'Post not created') |
|
106 | 112 | self.assertTrue(tag in thread.tags.all(), 'Tag not added to thread') |
|
107 | 113 | self.assertTrue(thread in tag.threads.all(), 'Thread not added to tag') |
|
108 | 114 | |
|
109 | 115 | def test_thread_max_count(self): |
|
110 | 116 | """Test deletion of old posts when the max thread count is reached""" |
|
111 | 117 | |
|
112 | 118 | for i in range(settings.MAX_THREAD_COUNT + 1): |
|
113 | 119 | self._create_post() |
|
114 | 120 | |
|
115 | 121 | self.assertEqual(settings.MAX_THREAD_COUNT, |
|
116 | 122 | len(Thread.objects.filter(archived=False))) |
|
117 | 123 | |
|
118 | 124 | def test_pages(self): |
|
119 | 125 | """Test that the thread list is properly split into pages""" |
|
120 | 126 | |
|
121 | 127 | for i in range(settings.MAX_THREAD_COUNT): |
|
122 | 128 | self._create_post() |
|
123 | 129 | |
|
124 | 130 | all_threads = Thread.objects.filter(archived=False) |
|
125 | 131 | |
|
126 | 132 | paginator = Paginator(Thread.objects.filter(archived=False), |
|
127 | 133 | settings.THREADS_PER_PAGE) |
|
128 | 134 | posts_in_second_page = paginator.page(2).object_list |
|
129 | 135 | first_post = posts_in_second_page[0] |
|
130 | 136 | |
|
131 | 137 | self.assertEqual(all_threads[settings.THREADS_PER_PAGE].id, |
|
132 | 138 | first_post.id) |
|
133 | 139 | |
|
134 | def test_linked_tag(self): | |
|
135 | """Test adding a linked tag""" | |
|
136 | ||
|
137 | linked_tag = Tag.objects.create(name=u'tag1') | |
|
138 | tag = Tag.objects.create(name=u'tag2', linked=linked_tag) | |
|
139 | ||
|
140 | post = Post.objects.create_post("", "", tags=[tag]) | |
|
141 | ||
|
142 | self.assertTrue(linked_tag in post.get_thread().tags.all(), | |
|
143 | 'Linked tag was not added') | |
|
144 | ||
|
145 | 140 | |
|
146 | 141 | class PagesTest(TestCase): |
|
147 | 142 | |
|
148 | 143 | def test_404(self): |
|
149 | 144 | """Test receiving error 404 when opening a non-existent page""" |
|
150 | 145 | |
|
151 | 146 | tag_name = u'test_tag' |
|
152 | 147 | tag = Tag.objects.create(name=tag_name) |
|
153 | 148 | client = Client() |
|
154 | 149 | |
|
155 | 150 | Post.objects.create_post('title', TEST_TEXT, tags=[tag]) |
|
156 | 151 | |
|
157 | 152 | existing_post_id = Post.objects.all()[0].id |
|
158 | 153 | response_existing = client.get(THREAD_PAGE + str(existing_post_id) + |
|
159 | 154 | '/') |
|
160 | 155 | self.assertEqual(HTTP_CODE_OK, response_existing.status_code, |
|
161 | 156 | u'Cannot open existing thread') |
|
162 | 157 | |
|
163 | 158 | response_not_existing = client.get(THREAD_PAGE + str( |
|
164 | 159 | existing_post_id + 1) + '/') |
|
165 | 160 | self.assertEqual(PAGE_404, response_not_existing.templates[0].name, |
|
166 | 161 | u'Not existing thread is opened') |
|
167 | 162 | |
|
168 | 163 | response_existing = client.get(TAG_PAGE + tag_name + '/') |
|
169 | 164 | self.assertEqual(HTTP_CODE_OK, |
|
170 | 165 | response_existing.status_code, |
|
171 | 166 | u'Cannot open existing tag') |
|
172 | 167 | |
|
173 | 168 | response_not_existing = client.get(TAG_PAGE + u'not_tag' + '/') |
|
174 | 169 | self.assertEqual(PAGE_404, |
|
175 | 170 | response_not_existing.templates[0].name, |
|
176 | 171 | u'Not existing tag is opened') |
|
177 | 172 | |
|
178 | 173 | reply_id = Post.objects.create_post('', TEST_TEXT, |
|
179 | 174 | thread=Post.objects.all()[0] |
|
180 | 175 | .get_thread()) |
|
181 | 176 | response_not_existing = client.get(THREAD_PAGE + str( |
|
182 | 177 | reply_id) + '/') |
|
183 | 178 | self.assertEqual(PAGE_404, |
|
184 | 179 | response_not_existing.templates[0].name, |
|
185 | 180 | u'Reply is opened as a thread') |
|
186 | 181 | |
|
187 | 182 | |
|
188 | 183 | class FormTest(TestCase): |
|
189 | 184 | def test_post_validation(self): |
|
190 | 185 | # Disable captcha for the test |
|
191 | 186 | captcha_enabled = neboard.settings.ENABLE_CAPTCHA |
|
192 | 187 | neboard.settings.ENABLE_CAPTCHA = False |
|
193 | 188 | |
|
194 | 189 | client = Client() |
|
195 | 190 | |
|
196 | 191 | valid_tags = u'tag1 tag_2 ΡΠ΅Π³_3' |
|
197 | 192 | invalid_tags = u'$%_356 ---' |
|
198 | 193 | |
|
199 | 194 | response = client.post(NEW_THREAD_PAGE, {'title': 'test title', |
|
200 | 195 | 'text': TEST_TEXT, |
|
201 | 196 | 'tags': valid_tags}) |
|
202 | 197 | self.assertEqual(response.status_code, HTTP_CODE_REDIRECT, |
|
203 | 198 | msg='Posting new message failed: got code ' + |
|
204 | 199 | str(response.status_code)) |
|
205 | 200 | |
|
206 | 201 | self.assertEqual(1, Post.objects.count(), |
|
207 | 202 | msg='No posts were created') |
|
208 | 203 | |
|
209 | 204 | client.post(NEW_THREAD_PAGE, {'text': TEST_TEXT, |
|
210 | 205 | 'tags': invalid_tags}) |
|
211 | 206 | self.assertEqual(1, Post.objects.count(), msg='The validation passed ' |
|
212 | 207 | 'where it should fail') |
|
213 | 208 | |
|
214 | 209 | # Change posting delay so we don't have to wait for 30 seconds or more |
|
215 | 210 | old_posting_delay = neboard.settings.POSTING_DELAY |
|
216 | 211 | # Wait fot the posting delay or we won't be able to post |
|
217 | 212 | settings.POSTING_DELAY = 1 |
|
218 | 213 | time.sleep(neboard.settings.POSTING_DELAY + 1) |
|
219 | 214 | response = client.post(THREAD_PAGE_ONE, {'text': TEST_TEXT, |
|
220 | 215 | 'tags': valid_tags}) |
|
221 | 216 | self.assertEqual(HTTP_CODE_REDIRECT, response.status_code, |
|
222 | 217 | msg=u'Posting new message failed: got code ' + |
|
223 | 218 | str(response.status_code)) |
|
224 | 219 | # Restore posting delay |
|
225 | 220 | settings.POSTING_DELAY = old_posting_delay |
|
226 | 221 | |
|
227 | 222 | self.assertEqual(2, Post.objects.count(), |
|
228 | 223 | msg=u'No posts were created') |
|
229 | 224 | |
|
230 | 225 | # Restore captcha setting |
|
231 | 226 | settings.ENABLE_CAPTCHA = captcha_enabled |
|
232 | 227 | |
|
233 | 228 | |
|
234 | 229 | class ViewTest(TestCase): |
|
235 | 230 | |
|
236 | 231 | def test_all_views(self): |
|
237 | 232 | """ |
|
238 | 233 | Try opening all views defined in ulrs.py that don't need additional |
|
239 | 234 | parameters |
|
240 | 235 | """ |
|
241 | 236 | |
|
242 | 237 | client = Client() |
|
243 | 238 | for url in urls.urlpatterns: |
|
244 | 239 | try: |
|
245 | 240 | view_name = url.name |
|
246 | 241 | logger.debug('Testing view %s' % view_name) |
|
247 | 242 | |
|
248 | 243 | try: |
|
249 | 244 | response = client.get(reverse(view_name)) |
|
250 | 245 | |
|
251 | 246 | self.assertEqual(HTTP_CODE_OK, response.status_code, |
|
252 | 247 | '%s view not opened' % view_name) |
|
253 | 248 | except NoReverseMatch: |
|
254 | 249 | # This view just needs additional arguments |
|
255 | 250 | pass |
|
256 | 251 | except Exception, e: |
|
257 | 252 | self.fail('Got exception %s at %s view' % (e, view_name)) |
|
258 | 253 | except AttributeError: |
|
259 | 254 | # This is normal, some views do not have names |
|
260 | 255 | pass |
|
261 | 256 | |
|
262 | 257 | |
|
263 | 258 | class AbstractTest(TestCase): |
|
264 | 259 | def test_settings_manager(self): |
|
265 | 260 | request = MockRequest() |
|
266 | 261 | settings_manager = get_settings_manager(request) |
|
267 | 262 | |
|
268 | 263 | settings_manager.set_setting('test_setting', 'test_value') |
|
269 | 264 | self.assertEqual('test_value', settings_manager.get_setting( |
|
270 | 265 | 'test_setting'), u'Setting update failed.') |
|
271 | 266 | |
|
272 | 267 | |
|
273 | 268 | class MockRequest: |
|
274 | 269 | def __init__(self): |
|
275 | self.session = dict() No newline at end of file | |
|
270 | self.session = dict() |
@@ -1,37 +1,42 | |||
|
1 | 1 | # 1.5 Aker # |
|
2 | 2 | * Saving image previews size. No space will be shown below images in some |
|
3 | 3 | styles. |
|
4 | 4 | * Showing notification in page title when new posts are loaded into the open |
|
5 | 5 | thread. |
|
6 | 6 | * Thread moderation fixes |
|
7 | 7 | * Added new gallery with search links and image metadata |
|
8 | 8 | |
|
9 | 9 | # 1.6 Amon # |
|
10 | 10 | * Deleted threads are moved to archive instead of permanent delete |
|
11 | 11 | * User management fixes and optimizations |
|
12 | 12 | * Markdown fixes |
|
13 | 13 | * Pagination changes. Pages counter now starts from 1 instead of 0 |
|
14 | 14 | * Added API for viewing threads and posts |
|
15 | 15 | * New tag popularity algorithm |
|
16 | 16 | * Tags list page changes. Now tags list is more like a tag cloud |
|
17 | 17 | |
|
18 | 18 | # 1.7 Anubis |
|
19 | 19 | * [ADMIN] Added admin page for post editing, capable of adding and removing tags |
|
20 | 20 | * [CODE] Post view unification |
|
21 | 21 | * Post caching instead of thread caching |
|
22 | 22 | * Simplified tag list page |
|
23 | 23 | * [API] Added api for thread update in json |
|
24 | 24 | * Image duplicate check |
|
25 | 25 | * Posting over ajax (no page reload now) |
|
26 | 26 | * Update last update time with thread update |
|
27 | 27 | * Added z-index to the images to move the dragged image to front |
|
28 | 28 | * [CODE] Major view refactoring. Now almost all views are class-based |
|
29 | 29 | |
|
30 | 30 | # 1.8 Kara |
|
31 | 31 | * [CODE] Removed thread update logging |
|
32 | 32 | * [CODE] Refactored compact form. Now it uses the same one form and moves |
|
33 | 33 | elements instead of swapping them |
|
34 | 34 | * [CODE] Moved image to a separate model. This will allow to add multiple |
|
35 | 35 | images to a post |
|
36 | 36 | * Added search over posts and tags |
|
37 | 37 | * [ADMIN] Command to remove empty users |
|
38 | ||
|
39 | # 2.0 D'Anna | |
|
40 | * Removed users. Now settings are stored in sessions | |
|
41 | * Changed markdown to bbcode | |
|
42 | * Removed linked tags |
General Comments 0
You need to be logged in to leave comments.
Login now