##// END OF EJS Templates
Added post admin page with tags edit capability
neko259 -
r566:f1e34d4b 1.7-dev
parent child Browse files
Show More
@@ -0,0 +1,38 b''
1 {% extends "boards/base.html" %}
2
3 {% load i18n %}
4 {% load cache %}
5 {% load static from staticfiles %}
6 {% load board %}
7
8 {% block head %}
9 <title>#{{ post.id }} - {{ site_name }}</title>
10 {% endblock %}
11
12 {% block content %}
13 {% spaceless %}
14
15 {% post_view post moderator=moderator %}
16
17 {% if post.is_opening %}
18 <div class="post">
19 {% trans 'Tags:' %}
20 {% for tag in post.thread_new.get_tags %}
21 <a class="tag" href={% url 'tag' tag.name %}>#{{ tag.name }}</a>
22 <a href="?method=delete_tag&tag={{ tag.name }}">[X]</a>
23 {% if not forloop.last %},{% endif %}
24 {% endfor %}
25 <div class="post-form-w">
26 <form id="form" enctype="multipart/form-data"
27 method="post">{% csrf_token %}
28 {{ tag_form.as_div }}
29 <div class="form-submit">
30 <input type="submit" value="{% trans "Add tag" %}"/>
31 </div>
32 </form>
33 </div>
34 </div>
35 {% endif %}
36
37 {% endspaceless %}
38 {% endblock %}
@@ -0,0 +1,57 b''
1 from django.shortcuts import render, get_object_or_404, redirect
2
3 from boards.views.base import BaseBoardView
4 from boards.views.mixins import DispatcherMixin
5 from boards.models.post import Post
6 from boards.models.tag import Tag
7 from boards.forms import AddTagForm, PlainErrorList
8
9 class PostAdminView(BaseBoardView, DispatcherMixin):
10
11 def get(self, request, post_id, form=None):
12 user = self._get_user(request)
13 if not user.is_moderator:
14 redirect('index')
15
16 post = get_object_or_404(Post, id=post_id)
17
18 if not form:
19 dispatch_result = self.dispatch_method(request, post)
20 if dispatch_result:
21 return dispatch_result
22 form = AddTagForm()
23
24 context = self.get_context_data(request=request)
25
26 context['post'] = post
27
28 context['tag_form'] = form
29
30 return render(request, 'boards/post_admin.html', context)
31
32 def post(self, request, post_id):
33 user = self._get_user(request)
34 if not user.is_moderator:
35 redirect('index')
36
37 post = get_object_or_404(Post, id=post_id)
38 return self.dispatch_method(request, post)
39
40 def delete_tag(self, request, post):
41 tag_name = request.GET['tag']
42 tag = get_object_or_404(Tag, name=tag_name)
43
44 post.remove_tag(tag)
45
46 return redirect('post_admin', post.id)
47
48 def add_tag(self, request, post):
49 form = AddTagForm(request.POST, error_class=PlainErrorList)
50 if form.is_valid():
51 tag_name = form.cleaned_data['tag']
52 tag, created = Tag.objects.get_or_create(name=tag_name)
53
54 post.add_tag(tag)
55 return redirect('post_admin', post.id)
56 else:
57 return self.get(request, post.id, form)
@@ -1,313 +1,339 b''
1 import re
1 import re
2 import time
2 import time
3 import hashlib
3 import hashlib
4
4
5 from captcha.fields import CaptchaField
5 from captcha.fields import CaptchaField
6 from django import forms
6 from django import forms
7 from django.forms.util import ErrorList
7 from django.forms.util import ErrorList
8 from django.utils.translation import ugettext_lazy as _
8 from django.utils.translation import ugettext_lazy as _
9
9
10 from boards.mdx_neboard import formatters
10 from boards.mdx_neboard import formatters
11 from boards.models.post import TITLE_MAX_LENGTH
11 from boards.models.post import TITLE_MAX_LENGTH
12 from boards.models import User, Post
12 from boards.models import User, Post
13 from neboard import settings
13 from neboard import settings
14 from boards import utils
14 from boards import utils
15 import boards.settings as board_settings
15 import boards.settings as board_settings
16
16
17 ATTRIBUTE_PLACEHOLDER = 'placeholder'
17 ATTRIBUTE_PLACEHOLDER = 'placeholder'
18
18
19 LAST_POST_TIME = 'last_post_time'
19 LAST_POST_TIME = 'last_post_time'
20 LAST_LOGIN_TIME = 'last_login_time'
20 LAST_LOGIN_TIME = 'last_login_time'
21 TEXT_PLACEHOLDER = _('''Type message here. You can reply to message >>123 like
21 TEXT_PLACEHOLDER = _('''Type message here. You can reply to message >>123 like
22 this. 2 new lines are required to start new paragraph.''')
22 this. 2 new lines are required to start new paragraph.''')
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')
30
31 TAG_MAX_LENGTH = 20
32
33 REGEX_TAG = ur'^[\w\d]+$'
29
34
30
35
31 class FormatPanel(forms.Textarea):
36 class FormatPanel(forms.Textarea):
32 def render(self, name, value, attrs=None):
37 def render(self, name, value, attrs=None):
33 output = '<div id="mark-panel">'
38 output = '<div id="mark-panel">'
34 for formatter in formatters:
39 for formatter in formatters:
35 output += u'<span class="mark_btn"' + \
40 output += u'<span class="mark_btn"' + \
36 u' onClick="addMarkToMsg(\'' + formatter.format_left + \
41 u' onClick="addMarkToMsg(\'' + formatter.format_left + \
37 '\', \'' + formatter.format_right + '\')">' + \
42 '\', \'' + formatter.format_right + '\')">' + \
38 formatter.preview_left + formatter.name + \
43 formatter.preview_left + formatter.name + \
39 formatter.preview_right + u'</span>'
44 formatter.preview_right + u'</span>'
40
45
41 output += '</div>'
46 output += '</div>'
42 output += super(FormatPanel, self).render(name, value, attrs=None)
47 output += super(FormatPanel, self).render(name, value, attrs=None)
43
48
44 return output
49 return output
45
50
46
51
47 class PlainErrorList(ErrorList):
52 class PlainErrorList(ErrorList):
48 def __unicode__(self):
53 def __unicode__(self):
49 return self.as_text()
54 return self.as_text()
50
55
51 def as_text(self):
56 def as_text(self):
52 return ''.join([u'(!) %s ' % e for e in self])
57 return ''.join([u'(!) %s ' % e for e in self])
53
58
54
59
55 class NeboardForm(forms.Form):
60 class NeboardForm(forms.Form):
56
61
57 def as_div(self):
62 def as_div(self):
58 """
63 """
59 Returns this form rendered as HTML <as_div>s.
64 Returns this form rendered as HTML <as_div>s.
60 """
65 """
61
66
62 return self._html_output(
67 return self._html_output(
63 # TODO Do not show hidden rows in the list here
68 # TODO Do not show hidden rows in the list here
64 normal_row='<div class="form-row">'
69 normal_row='<div class="form-row">'
65 '<div class="form-label">'
70 '<div class="form-label">'
66 '%(label)s'
71 '%(label)s'
67 '</div>'
72 '</div>'
68 '<div class="form-input">'
73 '<div class="form-input">'
69 '%(field)s'
74 '%(field)s'
70 '</div>'
75 '</div>'
71 '%(help_text)s'
76 '%(help_text)s'
72 '</div>',
77 '</div>',
73 error_row='<div class="form-row">'
78 error_row='<div class="form-row">'
74 '<div class="form-label"></div>'
79 '<div class="form-label"></div>'
75 '<div class="form-errors">%s</div>'
80 '<div class="form-errors">%s</div>'
76 '</div>',
81 '</div>',
77 row_ender='</div>',
82 row_ender='</div>',
78 help_text_html='%s',
83 help_text_html='%s',
79 errors_on_separate_row=True)
84 errors_on_separate_row=True)
80
85
81 def as_json_errors(self):
86 def as_json_errors(self):
82 errors = []
87 errors = []
83
88
84 for name, field in self.fields.items():
89 for name, field in self.fields.items():
85 if self[name].errors:
90 if self[name].errors:
86 errors.append({
91 errors.append({
87 'field': name,
92 'field': name,
88 'errors': self[name].errors.as_text(),
93 'errors': self[name].errors.as_text(),
89 })
94 })
90
95
91 return errors
96 return errors
92
97
93
98
94 class PostForm(NeboardForm):
99 class PostForm(NeboardForm):
95
100
96 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
101 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
97 label=LABEL_TITLE)
102 label=LABEL_TITLE)
98 text = forms.CharField(
103 text = forms.CharField(
99 widget=FormatPanel(attrs={ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER}),
104 widget=FormatPanel(attrs={ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER}),
100 required=False, label=LABEL_TEXT)
105 required=False, label=LABEL_TEXT)
101 image = forms.ImageField(required=False, label=_('Image'))
106 image = forms.ImageField(required=False, label=_('Image'))
102
107
103 # This field is for spam prevention only
108 # This field is for spam prevention only
104 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
109 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
105 widget=forms.TextInput(attrs={
110 widget=forms.TextInput(attrs={
106 'class': 'form-email'}))
111 'class': 'form-email'}))
107
112
108 session = None
113 session = None
109 need_to_ban = False
114 need_to_ban = False
110
115
111 def clean_title(self):
116 def clean_title(self):
112 title = self.cleaned_data['title']
117 title = self.cleaned_data['title']
113 if title:
118 if title:
114 if len(title) > TITLE_MAX_LENGTH:
119 if len(title) > TITLE_MAX_LENGTH:
115 raise forms.ValidationError(_('Title must have less than %s '
120 raise forms.ValidationError(_('Title must have less than %s '
116 'characters') %
121 'characters') %
117 str(TITLE_MAX_LENGTH))
122 str(TITLE_MAX_LENGTH))
118 return title
123 return title
119
124
120 def clean_text(self):
125 def clean_text(self):
121 text = self.cleaned_data['text']
126 text = self.cleaned_data['text']
122 if text:
127 if text:
123 if len(text) > board_settings.MAX_TEXT_LENGTH:
128 if len(text) > board_settings.MAX_TEXT_LENGTH:
124 raise forms.ValidationError(_('Text must have less than %s '
129 raise forms.ValidationError(_('Text must have less than %s '
125 'characters') %
130 'characters') %
126 str(board_settings
131 str(board_settings
127 .MAX_TEXT_LENGTH))
132 .MAX_TEXT_LENGTH))
128 return text
133 return text
129
134
130 def clean_image(self):
135 def clean_image(self):
131 image = self.cleaned_data['image']
136 image = self.cleaned_data['image']
132 if image:
137 if image:
133 if image._size > board_settings.MAX_IMAGE_SIZE:
138 if image._size > board_settings.MAX_IMAGE_SIZE:
134 raise forms.ValidationError(
139 raise forms.ValidationError(
135 _('Image must be less than %s bytes')
140 _('Image must be less than %s bytes')
136 % str(board_settings.MAX_IMAGE_SIZE))
141 % str(board_settings.MAX_IMAGE_SIZE))
137
142
138 md5 = hashlib.md5()
143 md5 = hashlib.md5()
139 for chunk in image.chunks():
144 for chunk in image.chunks():
140 md5.update(chunk)
145 md5.update(chunk)
141 image_hash = md5.hexdigest()
146 image_hash = md5.hexdigest()
142 if Post.objects.filter(image_hash=image_hash).exists():
147 if Post.objects.filter(image_hash=image_hash).exists():
143 raise forms.ValidationError(ERROR_IMAGE_DUPLICATE)
148 raise forms.ValidationError(ERROR_IMAGE_DUPLICATE)
144
149
145 return image
150 return image
146
151
147 def clean(self):
152 def clean(self):
148 cleaned_data = super(PostForm, self).clean()
153 cleaned_data = super(PostForm, self).clean()
149
154
150 if not self.session:
155 if not self.session:
151 raise forms.ValidationError('Humans have sessions')
156 raise forms.ValidationError('Humans have sessions')
152
157
153 if cleaned_data['email']:
158 if cleaned_data['email']:
154 self.need_to_ban = True
159 self.need_to_ban = True
155 raise forms.ValidationError('A human cannot enter a hidden field')
160 raise forms.ValidationError('A human cannot enter a hidden field')
156
161
157 if not self.errors:
162 if not self.errors:
158 self._clean_text_image()
163 self._clean_text_image()
159
164
160 if not self.errors and self.session:
165 if not self.errors and self.session:
161 self._validate_posting_speed()
166 self._validate_posting_speed()
162
167
163 return cleaned_data
168 return cleaned_data
164
169
165 def _clean_text_image(self):
170 def _clean_text_image(self):
166 text = self.cleaned_data.get('text')
171 text = self.cleaned_data.get('text')
167 image = self.cleaned_data.get('image')
172 image = self.cleaned_data.get('image')
168
173
169 if (not text) and (not image):
174 if (not text) and (not image):
170 error_message = _('Either text or image must be entered.')
175 error_message = _('Either text or image must be entered.')
171 self._errors['text'] = self.error_class([error_message])
176 self._errors['text'] = self.error_class([error_message])
172
177
173 def _validate_posting_speed(self):
178 def _validate_posting_speed(self):
174 can_post = True
179 can_post = True
175
180
176 if LAST_POST_TIME in self.session:
181 if LAST_POST_TIME in self.session:
177 now = time.time()
182 now = time.time()
178 last_post_time = self.session[LAST_POST_TIME]
183 last_post_time = self.session[LAST_POST_TIME]
179
184
180 current_delay = int(now - last_post_time)
185 current_delay = int(now - last_post_time)
181
186
182 if current_delay < settings.POSTING_DELAY:
187 if current_delay < settings.POSTING_DELAY:
183 error_message = _('Wait %s seconds after last posting') % str(
188 error_message = _('Wait %s seconds after last posting') % str(
184 settings.POSTING_DELAY - current_delay)
189 settings.POSTING_DELAY - current_delay)
185 self._errors['text'] = self.error_class([error_message])
190 self._errors['text'] = self.error_class([error_message])
186
191
187 can_post = False
192 can_post = False
188
193
189 if can_post:
194 if can_post:
190 self.session[LAST_POST_TIME] = time.time()
195 self.session[LAST_POST_TIME] = time.time()
191
196
192
197
193 class ThreadForm(PostForm):
198 class ThreadForm(PostForm):
194
199
195 regex_tags = re.compile(ur'^[\w\s\d]+$', re.UNICODE)
200 regex_tags = re.compile(ur'^[\w\s\d]+$', re.UNICODE)
196
201
197 tags = forms.CharField(
202 tags = forms.CharField(
198 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
203 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
199 max_length=100, label=_('Tags'))
204 max_length=100, label=_('Tags'))
200
205
201 def clean_tags(self):
206 def clean_tags(self):
202 tags = self.cleaned_data['tags']
207 tags = self.cleaned_data['tags']
203
208
204 if tags:
209 if tags:
205 if not self.regex_tags.match(tags):
210 if not self.regex_tags.match(tags):
206 raise forms.ValidationError(
211 raise forms.ValidationError(
207 _('Inappropriate characters in tags.'))
212 _('Inappropriate characters in tags.'))
208
213
209 return tags
214 return tags
210
215
211 def clean(self):
216 def clean(self):
212 cleaned_data = super(ThreadForm, self).clean()
217 cleaned_data = super(ThreadForm, self).clean()
213
218
214 return cleaned_data
219 return cleaned_data
215
220
216
221
217 class PostCaptchaForm(PostForm):
222 class PostCaptchaForm(PostForm):
218 captcha = CaptchaField()
223 captcha = CaptchaField()
219
224
220 def __init__(self, *args, **kwargs):
225 def __init__(self, *args, **kwargs):
221 self.request = kwargs['request']
226 self.request = kwargs['request']
222 del kwargs['request']
227 del kwargs['request']
223
228
224 super(PostCaptchaForm, self).__init__(*args, **kwargs)
229 super(PostCaptchaForm, self).__init__(*args, **kwargs)
225
230
226 def clean(self):
231 def clean(self):
227 cleaned_data = super(PostCaptchaForm, self).clean()
232 cleaned_data = super(PostCaptchaForm, self).clean()
228
233
229 success = self.is_valid()
234 success = self.is_valid()
230 utils.update_captcha_access(self.request, success)
235 utils.update_captcha_access(self.request, success)
231
236
232 if success:
237 if success:
233 return cleaned_data
238 return cleaned_data
234 else:
239 else:
235 raise forms.ValidationError(_("Captcha validation failed"))
240 raise forms.ValidationError(_("Captcha validation failed"))
236
241
237
242
238 class ThreadCaptchaForm(ThreadForm):
243 class ThreadCaptchaForm(ThreadForm):
239 captcha = CaptchaField()
244 captcha = CaptchaField()
240
245
241 def __init__(self, *args, **kwargs):
246 def __init__(self, *args, **kwargs):
242 self.request = kwargs['request']
247 self.request = kwargs['request']
243 del kwargs['request']
248 del kwargs['request']
244
249
245 super(ThreadCaptchaForm, self).__init__(*args, **kwargs)
250 super(ThreadCaptchaForm, self).__init__(*args, **kwargs)
246
251
247 def clean(self):
252 def clean(self):
248 cleaned_data = super(ThreadCaptchaForm, self).clean()
253 cleaned_data = super(ThreadCaptchaForm, self).clean()
249
254
250 success = self.is_valid()
255 success = self.is_valid()
251 utils.update_captcha_access(self.request, success)
256 utils.update_captcha_access(self.request, success)
252
257
253 if success:
258 if success:
254 return cleaned_data
259 return cleaned_data
255 else:
260 else:
256 raise forms.ValidationError(_("Captcha validation failed"))
261 raise forms.ValidationError(_("Captcha validation failed"))
257
262
258
263
259 class SettingsForm(NeboardForm):
264 class SettingsForm(NeboardForm):
260
265
261 theme = forms.ChoiceField(choices=settings.THEMES,
266 theme = forms.ChoiceField(choices=settings.THEMES,
262 label=_('Theme'))
267 label=_('Theme'))
263
268
264
269
265 class ModeratorSettingsForm(SettingsForm):
270 class ModeratorSettingsForm(SettingsForm):
266
271
267 moderate = forms.BooleanField(required=False, label=_('Enable moderation '
272 moderate = forms.BooleanField(required=False, label=_('Enable moderation '
268 'panel'))
273 'panel'))
269
274
270
275
271 class LoginForm(NeboardForm):
276 class LoginForm(NeboardForm):
272
277
273 user_id = forms.CharField()
278 user_id = forms.CharField()
274
279
275 session = None
280 session = None
276
281
277 def clean_user_id(self):
282 def clean_user_id(self):
278 user_id = self.cleaned_data['user_id']
283 user_id = self.cleaned_data['user_id']
279 if user_id:
284 if user_id:
280 users = User.objects.filter(user_id=user_id)
285 users = User.objects.filter(user_id=user_id)
281 if len(users) == 0:
286 if len(users) == 0:
282 raise forms.ValidationError(_('No such user found'))
287 raise forms.ValidationError(_('No such user found'))
283
288
284 return user_id
289 return user_id
285
290
286 def _validate_login_speed(self):
291 def _validate_login_speed(self):
287 can_post = True
292 can_post = True
288
293
289 if LAST_LOGIN_TIME in self.session:
294 if LAST_LOGIN_TIME in self.session:
290 now = time.time()
295 now = time.time()
291 last_login_time = self.session[LAST_LOGIN_TIME]
296 last_login_time = self.session[LAST_LOGIN_TIME]
292
297
293 current_delay = int(now - last_login_time)
298 current_delay = int(now - last_login_time)
294
299
295 if current_delay < board_settings.LOGIN_TIMEOUT:
300 if current_delay < board_settings.LOGIN_TIMEOUT:
296 error_message = _('Wait %s minutes after last login') % str(
301 error_message = _('Wait %s minutes after last login') % str(
297 (board_settings.LOGIN_TIMEOUT - current_delay) / 60)
302 (board_settings.LOGIN_TIMEOUT - current_delay) / 60)
298 self._errors['user_id'] = self.error_class([error_message])
303 self._errors['user_id'] = self.error_class([error_message])
299
304
300 can_post = False
305 can_post = False
301
306
302 if can_post:
307 if can_post:
303 self.session[LAST_LOGIN_TIME] = time.time()
308 self.session[LAST_LOGIN_TIME] = time.time()
304
309
305 def clean(self):
310 def clean(self):
306 if not self.session:
311 if not self.session:
307 raise forms.ValidationError('Humans have sessions')
312 raise forms.ValidationError('Humans have sessions')
308
313
309 self._validate_login_speed()
314 self._validate_login_speed()
310
315
311 cleaned_data = super(LoginForm, self).clean()
316 cleaned_data = super(LoginForm, self).clean()
312
317
313 return cleaned_data
318 return cleaned_data
319
320
321 class AddTagForm(NeboardForm):
322
323 tag = forms.CharField(max_length=TAG_MAX_LENGTH, label=LABEL_TAG)
324 method = forms.CharField(widget=forms.HiddenInput(), initial='add_tag')
325
326 def clean_tag(self):
327 tag = self.cleaned_data['tag']
328
329 regex_tag = re.compile(REGEX_TAG, re.UNICODE)
330 if not regex_tag.match(tag):
331 raise forms.ValidationError(_('Inappropriate characters in tags.'))
332
333 return tag
334
335 def clean(self):
336 cleaned_data = super(AddTagForm, self).clean()
337
338 return cleaned_data
339
1 NO CONTENT: modified file, binary diff hidden
NO CONTENT: modified file, binary diff hidden
@@ -1,359 +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-01-15 10:46+0200\n"
10 "POT-Creation-Date: 2014-01-22 13:07+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:5
21 #: authors.py:5
22 msgid "author"
22 msgid "author"
23 msgstr "Π°Π²Ρ‚ΠΎΡ€"
23 msgstr "Π°Π²Ρ‚ΠΎΡ€"
24
24
25 #: authors.py:6
25 #: authors.py:6
26 msgid "developer"
26 msgid "developer"
27 msgstr "Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ"
27 msgstr "Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ"
28
28
29 #: authors.py:7
29 #: authors.py:7
30 msgid "javascript developer"
30 msgid "javascript developer"
31 msgstr "Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ javascript"
31 msgstr "Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ javascript"
32
32
33 #: authors.py:8
33 #: authors.py:8
34 msgid "designer"
34 msgid "designer"
35 msgstr "Π΄ΠΈΠ·Π°ΠΉΠ½Π΅Ρ€"
35 msgstr "Π΄ΠΈΠ·Π°ΠΉΠ½Π΅Ρ€"
36
36
37 #: forms.py:21
37 #: forms.py:21
38 msgid ""
38 msgid ""
39 "Type message here. You can reply to message >>123 like\n"
39 "Type message here. You can reply to message >>123 like\n"
40 " this. 2 new lines are required to start new paragraph."
40 " this. 2 new lines are required to start new paragraph."
41 msgstr ""
41 msgstr ""
42 "Π’Π²Π΅Π΄ΠΈΡ‚Π΅ сообщСниС здСсь. Π’Ρ‹ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ ΠΎΡ‚Π²Π΅Ρ‚ΠΈΡ‚ΡŒ Π½Π° сообщСниС >>123 Π²ΠΎΡ‚ Ρ‚Π°ΠΊ. 2 "
42 "Π’Π²Π΅Π΄ΠΈΡ‚Π΅ сообщСниС здСсь. Π’Ρ‹ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ ΠΎΡ‚Π²Π΅Ρ‚ΠΈΡ‚ΡŒ Π½Π° сообщСниС >>123 Π²ΠΎΡ‚ Ρ‚Π°ΠΊ. 2 "
43 "пСрСноса строки ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹ для создания Π½ΠΎΠ²ΠΎΠ³ΠΎ Π°Π±Π·Π°Ρ†Π°."
43 "пСрСноса строки ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹ для создания Π½ΠΎΠ²ΠΎΠ³ΠΎ Π°Π±Π·Π°Ρ†Π°."
44
44
45 #: forms.py:23
45 #: forms.py:23
46 msgid "tag1 several_words_tag"
46 msgid "tag1 several_words_tag"
47 msgstr "Ρ‚Π΅Π³1 Ρ‚Π΅Π³_ΠΈΠ·_Π½Π΅ΡΠΊΠΎΠ»ΡŒΠΊΠΈΡ…_слов"
47 msgstr "Ρ‚Π΅Π³1 Ρ‚Π΅Π³_ΠΈΠ·_Π½Π΅ΡΠΊΠΎΠ»ΡŒΠΊΠΈΡ…_слов"
48
48
49 #: forms.py:25
49 #: forms.py:25
50 msgid "Such image was already posted"
50 msgid "Such image was already posted"
51 msgstr "Π’Π°ΠΊΠΎΠ΅ ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ ΡƒΠΆΠ΅ Π±Ρ‹Π»ΠΎ Π·Π°Π³Ρ€ΡƒΠΆΠ΅Π½ΠΎ"
51 msgstr "Π’Π°ΠΊΠΎΠ΅ ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ ΡƒΠΆΠ΅ Π±Ρ‹Π»ΠΎ Π·Π°Π³Ρ€ΡƒΠΆΠ΅Π½ΠΎ"
52
52
53 #: forms.py:27
53 #: forms.py:27
54 msgid "Title"
54 msgid "Title"
55 msgstr "Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ"
55 msgstr "Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ"
56
56
57 #: forms.py:28
57 #: forms.py:28
58 msgid "Text"
58 msgid "Text"
59 msgstr "ВСкст"
59 msgstr "ВСкст"
60
60
61 #: forms.py:89
61 #: forms.py:29
62 msgid "Tag"
63 msgstr "Π’Π΅Π³"
64
65 #: forms.py:106
62 msgid "Image"
66 msgid "Image"
63 msgstr "Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅"
67 msgstr "Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅"
64
68
65 #: forms.py:92
69 #: forms.py:109
66 msgid "e-mail"
70 msgid "e-mail"
67 msgstr ""
71 msgstr ""
68
72
69 #: forms.py:103
73 #: forms.py:120
70 #, python-format
74 #, python-format
71 msgid "Title must have less than %s characters"
75 msgid "Title must have less than %s characters"
72 msgstr "Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ Π΄ΠΎΠ»ΠΆΠ΅Π½ ΠΈΠΌΠ΅Ρ‚ΡŒ мСньшС %s символов"
76 msgstr "Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ Π΄ΠΎΠ»ΠΆΠ΅Π½ ΠΈΠΌΠ΅Ρ‚ΡŒ мСньшС %s символов"
73
77
74 #: forms.py:112
78 #: forms.py:129
75 #, python-format
79 #, python-format
76 msgid "Text must have less than %s characters"
80 msgid "Text must have less than %s characters"
77 msgstr "ВСкст Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±Ρ‹Ρ‚ΡŒ ΠΊΠΎΡ€ΠΎΡ‡Π΅ %s символов"
81 msgstr "ВСкст Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±Ρ‹Ρ‚ΡŒ ΠΊΠΎΡ€ΠΎΡ‡Π΅ %s символов"
78
82
79 #: forms.py:123
83 #: forms.py:140
80 #, python-format
84 #, python-format
81 msgid "Image must be less than %s bytes"
85 msgid "Image must be less than %s bytes"
82 msgstr "Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ Π΄ΠΎΠ»ΠΆΠ½ΠΎ Π±Ρ‹Ρ‚ΡŒ ΠΌΠ΅Π½Π΅Π΅ %s Π±Π°ΠΉΡ‚"
86 msgstr "Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ Π΄ΠΎΠ»ΠΆΠ½ΠΎ Π±Ρ‹Ρ‚ΡŒ ΠΌΠ΅Π½Π΅Π΅ %s Π±Π°ΠΉΡ‚"
83
87
84 #: forms.py:158
88 #: forms.py:175
85 msgid "Either text or image must be entered."
89 msgid "Either text or image must be entered."
86 msgstr "ВСкст ΠΈΠ»ΠΈ ΠΊΠ°Ρ€Ρ‚ΠΈΠ½ΠΊΠ° Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ Π²Π²Π΅Π΄Π΅Π½Ρ‹."
90 msgstr "ВСкст ΠΈΠ»ΠΈ ΠΊΠ°Ρ€Ρ‚ΠΈΠ½ΠΊΠ° Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ Π²Π²Π΅Π΄Π΅Π½Ρ‹."
87
91
88 #: forms.py:171
92 #: forms.py:188
89 #, python-format
93 #, python-format
90 msgid "Wait %s seconds after last posting"
94 msgid "Wait %s seconds after last posting"
91 msgstr "ΠŸΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡ‚Π΅ %s сСкунд послС послСднСго постинга"
95 msgstr "ΠŸΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡ‚Π΅ %s сСкунд послС послСднСго постинга"
92
96
93 #: forms.py:187 templates/boards/tags.html:6 templates/boards/rss/post.html:10
97 #: forms.py:204 templates/boards/tags.html:6 templates/boards/rss/post.html:10
94 msgid "Tags"
98 msgid "Tags"
95 msgstr "Π’Π΅Π³ΠΈ"
99 msgstr "Π’Π΅Π³ΠΈ"
96
100
97 #: forms.py:195
101 #: forms.py:212 forms.py:331
98 msgid "Inappropriate characters in tags."
102 msgid "Inappropriate characters in tags."
99 msgstr "НСдопустимыС символы Π² Ρ‚Π΅Π³Π°Ρ…."
103 msgstr "НСдопустимыС символы Π² Ρ‚Π΅Π³Π°Ρ…."
100
104
101 #: forms.py:223 forms.py:244
105 #: forms.py:240 forms.py:261
102 msgid "Captcha validation failed"
106 msgid "Captcha validation failed"
103 msgstr "ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° ΠΊΠ°ΠΏΡ‡ΠΈ ΠΏΡ€ΠΎΠ²Π°Π»Π΅Π½Π°"
107 msgstr "ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° ΠΊΠ°ΠΏΡ‡ΠΈ ΠΏΡ€ΠΎΠ²Π°Π»Π΅Π½Π°"
104
108
105 #: forms.py:250
109 #: forms.py:267
106 msgid "Theme"
110 msgid "Theme"
107 msgstr "Π’Π΅ΠΌΠ°"
111 msgstr "Π’Π΅ΠΌΠ°"
108
112
109 #: forms.py:255
113 #: forms.py:272
110 msgid "Enable moderation panel"
114 msgid "Enable moderation panel"
111 msgstr "Π’ΠΊΠ»ΡŽΡ‡ΠΈΡ‚ΡŒ панСль ΠΌΠΎΠ΄Π΅Ρ€Π°Ρ†ΠΈΠΈ"
115 msgstr "Π’ΠΊΠ»ΡŽΡ‡ΠΈΡ‚ΡŒ панСль ΠΌΠΎΠ΄Π΅Ρ€Π°Ρ†ΠΈΠΈ"
112
116
113 #: forms.py:270
117 #: forms.py:287
114 msgid "No such user found"
118 msgid "No such user found"
115 msgstr "Π”Π°Π½Π½Ρ‹ΠΉ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½"
119 msgstr "Π”Π°Π½Π½Ρ‹ΠΉ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½"
116
120
117 #: forms.py:284
121 #: forms.py:301
118 #, python-format
122 #, python-format
119 msgid "Wait %s minutes after last login"
123 msgid "Wait %s minutes after last login"
120 msgstr "ΠŸΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡ‚Π΅ %s ΠΌΠΈΠ½ΡƒΡ‚ послС послСднСго Π²Ρ…ΠΎΠ΄Π°"
124 msgstr "ΠŸΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡ‚Π΅ %s ΠΌΠΈΠ½ΡƒΡ‚ послС послСднСго Π²Ρ…ΠΎΠ΄Π°"
121
125
122 #: templates/boards/404.html:6
126 #: templates/boards/404.html:6
123 msgid "Not found"
127 msgid "Not found"
124 msgstr "НС найдСно"
128 msgstr "НС найдСно"
125
129
126 #: templates/boards/404.html:12
130 #: templates/boards/404.html:12
127 msgid "This page does not exist"
131 msgid "This page does not exist"
128 msgstr "Π­Ρ‚ΠΎΠΉ страницы Π½Π΅ сущСствуСт"
132 msgstr "Π­Ρ‚ΠΎΠΉ страницы Π½Π΅ сущСствуСт"
129
133
130 #: templates/boards/archive.html:9 templates/boards/base.html:51
131 msgid "Archive"
132 msgstr "Архив"
133
134 #: templates/boards/archive.html:39 templates/boards/posting_general.html:64
135 msgid "Previous page"
136 msgstr "ΠŸΡ€Π΅Π΄Ρ‹Π΄ΡƒΡ‰Π°Ρ страница"
137
138 #: templates/boards/archive.html:68
139 msgid "Open"
140 msgstr "ΠžΡ‚ΠΊΡ€Ρ‹Ρ‚ΡŒ"
141
142 #: templates/boards/archive.html:74 templates/boards/post.html:37
143 #: templates/boards/posting_general.html:103 templates/boards/thread.html:69
144 msgid "Delete"
145 msgstr "Π£Π΄Π°Π»ΠΈΡ‚ΡŒ"
146
147 #: templates/boards/archive.html:78 templates/boards/post.html:40
148 #: templates/boards/posting_general.html:107 templates/boards/thread.html:72
149 msgid "Ban IP"
150 msgstr "Π—Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ IP"
151
152 #: templates/boards/archive.html:87 templates/boards/post.html:53
153 #: templates/boards/posting_general.html:116
154 #: templates/boards/posting_general.html:180 templates/boards/thread.html:81
155 msgid "Replies"
156 msgstr "ΠžΡ‚Π²Π΅Ρ‚Ρ‹"
157
158 #: templates/boards/archive.html:96 templates/boards/posting_general.html:125
159 #: templates/boards/thread.html:138 templates/boards/thread_gallery.html:58
160 msgid "images"
161 msgstr "ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ"
162
163 #: templates/boards/archive.html:97 templates/boards/thread.html:137
164 #: templates/boards/thread_gallery.html:57
165 msgid "replies"
166 msgstr "ΠΎΡ‚Π²Π΅Ρ‚ΠΎΠ²"
167
168 #: templates/boards/archive.html:116 templates/boards/posting_general.html:203
169 msgid "Next page"
170 msgstr "Π‘Π»Π΅Π΄ΡƒΡŽΡ‰Π°Ρ страница"
171
172 #: templates/boards/archive.html:121 templates/boards/posting_general.html:208
173 msgid "No threads exist. Create the first one!"
174 msgstr "НСт Ρ‚Π΅ΠΌ. Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ ΠΏΠ΅Ρ€Π²ΡƒΡŽ!"
175
176 #: templates/boards/archive.html:130 templates/boards/posting_general.html:235
177 msgid "Pages:"
178 msgstr "Π‘Ρ‚Ρ€Π°Π½ΠΈΡ†Ρ‹: "
179
180 #: templates/boards/authors.html:6 templates/boards/authors.html.py:12
134 #: templates/boards/authors.html:6 templates/boards/authors.html.py:12
181 msgid "Authors"
135 msgid "Authors"
182 msgstr "Авторы"
136 msgstr "Авторы"
183
137
184 #: templates/boards/authors.html:25
138 #: templates/boards/authors.html:26
185 msgid "Distributed under the"
139 msgid "Distributed under the"
186 msgstr "РаспространяСтся ΠΏΠΎΠ΄"
140 msgstr "РаспространяСтся ΠΏΠΎΠ΄"
187
141
188 #: templates/boards/authors.html:27
142 #: templates/boards/authors.html:28
189 msgid "license"
143 msgid "license"
190 msgstr "Π»ΠΈΡ†Π΅Π½Π·ΠΈΠ΅ΠΉ"
144 msgstr "Π»ΠΈΡ†Π΅Π½Π·ΠΈΠ΅ΠΉ"
191
145
192 #: templates/boards/authors.html:29
146 #: templates/boards/authors.html:30
193 msgid "Repository"
147 msgid "Repository"
194 msgstr "Π Π΅ΠΏΠΎΠ·ΠΈΡ‚ΠΎΡ€ΠΈΠΉ"
148 msgstr "Π Π΅ΠΏΠΎΠ·ΠΈΡ‚ΠΎΡ€ΠΈΠΉ"
195
149
196 #: templates/boards/base.html:14
150 #: templates/boards/base.html:14
197 msgid "Feed"
151 msgid "Feed"
198 msgstr "Π›Π΅Π½Ρ‚Π°"
152 msgstr "Π›Π΅Π½Ρ‚Π°"
199
153
200 #: templates/boards/base.html:31
154 #: templates/boards/base.html:31
201 msgid "All threads"
155 msgid "All threads"
202 msgstr "ВсС Ρ‚Π΅ΠΌΡ‹"
156 msgstr "ВсС Ρ‚Π΅ΠΌΡ‹"
203
157
204 #: templates/boards/base.html:36
158 #: templates/boards/base.html:36
205 msgid "Tag management"
159 msgid "Tag management"
206 msgstr "Π£ΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠ΅ Ρ‚Π΅Π³Π°ΠΌΠΈ"
160 msgstr "Π£ΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠ΅ Ρ‚Π΅Π³Π°ΠΌΠΈ"
207
161
208 #: templates/boards/base.html:38
162 #: templates/boards/base.html:38 templates/boards/settings.html:7
209 msgid "Settings"
163 msgid "Settings"
210 msgstr "Настройки"
164 msgstr "Настройки"
211
165
212 #: templates/boards/base.html:50 templates/boards/login.html:6
166 #: templates/boards/base.html:50 templates/boards/login.html:6
213 #: templates/boards/login.html.py:21
167 #: templates/boards/login.html.py:21
214 msgid "Login"
168 msgid "Login"
215 msgstr "Π’Ρ…ΠΎΠ΄"
169 msgstr "Π’Ρ…ΠΎΠ΄"
216
170
171 #: templates/boards/base.html:51
172 msgid "Archive"
173 msgstr "Архив"
174
217 #: templates/boards/base.html:53
175 #: templates/boards/base.html:53
218 #, python-format
176 #, python-format
219 msgid "Speed: %(ppd)s posts per day"
177 msgid "Speed: %(ppd)s posts per day"
220 msgstr "Π‘ΠΊΠΎΡ€ΠΎΡΡ‚ΡŒ: %(ppd)s сообщСний Π² дСнь"
178 msgstr "Π‘ΠΊΠΎΡ€ΠΎΡΡ‚ΡŒ: %(ppd)s сообщСний Π² дСнь"
221
179
222 #: templates/boards/base.html:55
180 #: templates/boards/base.html:55
223 msgid "Up"
181 msgid "Up"
224 msgstr "Π’Π²Π΅Ρ€Ρ…"
182 msgstr "Π’Π²Π΅Ρ€Ρ…"
225
183
226 #: templates/boards/login.html:15
184 #: templates/boards/login.html:15
227 msgid "User ID"
185 msgid "User ID"
228 msgstr "ID ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ"
186 msgstr "ID ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ"
229
187
230 #: templates/boards/login.html:24
188 #: templates/boards/login.html:24
231 msgid "Insert your user id above"
189 msgid "Insert your user id above"
232 msgstr "Π’ΡΡ‚Π°Π²ΡŒΡ‚Π΅ свой ID ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ Π²Ρ‹ΡˆΠ΅"
190 msgstr "Π’ΡΡ‚Π°Π²ΡŒΡ‚Π΅ свой ID ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ Π²Ρ‹ΡˆΠ΅"
233
191
234 #: templates/boards/posting_general.html:97
192 #: templates/boards/post.html:47
193 msgid "Open"
194 msgstr "ΠžΡ‚ΠΊΡ€Ρ‹Ρ‚ΡŒ"
195
196 #: templates/boards/post.html:49
235 msgid "Reply"
197 msgid "Reply"
236 msgstr "ΠžΡ‚Π²Π΅Ρ‚"
198 msgstr "ΠžΡ‚Π²Π΅Ρ‚"
237
199
238 #: templates/boards/posting_general.html:142
200 #: templates/boards/post.html:56
201 msgid "Edit"
202 msgstr "Π˜Π·ΠΌΠ΅Π½ΠΈΡ‚ΡŒ"
203
204 #: templates/boards/post.html:58
205 msgid "Delete"
206 msgstr "Π£Π΄Π°Π»ΠΈΡ‚ΡŒ"
207
208 #: templates/boards/post.html:61
209 msgid "Ban IP"
210 msgstr "Π—Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ IP"
211
212 #: templates/boards/post.html:74
213 msgid "Replies"
214 msgstr "ΠžΡ‚Π²Π΅Ρ‚Ρ‹"
215
216 #: templates/boards/post.html:85 templates/boards/thread.html:74
217 #: templates/boards/thread_gallery.html:59
218 msgid "images"
219 msgstr "ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ"
220
221 #: templates/boards/post_admin.html:19
222 msgid "Tags:"
223 msgstr "Π’Π΅Π³ΠΈ:"
224
225 #: templates/boards/post_admin.html:30
226 msgid "Add tag"
227 msgstr "Π”ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ Ρ‚Π΅Π³"
228
229 #: templates/boards/posting_general.html:64
230 msgid "Previous page"
231 msgstr "ΠŸΡ€Π΅Π΄Ρ‹Π΄ΡƒΡ‰Π°Ρ страница"
232
233 #: templates/boards/posting_general.html:77
239 #, python-format
234 #, python-format
240 msgid "Skipped %(count)s replies. Open thread to see all replies."
235 msgid "Skipped %(count)s replies. Open thread to see all replies."
241 msgstr "ΠŸΡ€ΠΎΠΏΡƒΡ‰Π΅Π½ΠΎ %(count)s ΠΎΡ‚Π²Π΅Ρ‚ΠΎΠ². ΠžΡ‚ΠΊΡ€ΠΎΠΉΡ‚Π΅ Ρ‚Ρ€Π΅Π΄, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΡƒΠ²ΠΈΠ΄Π΅Ρ‚ΡŒ всС ΠΎΡ‚Π²Π΅Ρ‚Ρ‹."
236 msgstr "ΠŸΡ€ΠΎΠΏΡƒΡ‰Π΅Π½ΠΎ %(count)s ΠΎΡ‚Π²Π΅Ρ‚ΠΎΠ². ΠžΡ‚ΠΊΡ€ΠΎΠΉΡ‚Π΅ Ρ‚Ρ€Π΅Π΄, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΡƒΠ²ΠΈΠ΄Π΅Ρ‚ΡŒ всС ΠΎΡ‚Π²Π΅Ρ‚Ρ‹."
242
237
243 #: templates/boards/posting_general.html:214
238 #: templates/boards/posting_general.html:100
239 msgid "Next page"
240 msgstr "Π‘Π»Π΅Π΄ΡƒΡŽΡ‰Π°Ρ страница"
241
242 #: templates/boards/posting_general.html:105
243 msgid "No threads exist. Create the first one!"
244 msgstr "НСт Ρ‚Π΅ΠΌ. Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ ΠΏΠ΅Ρ€Π²ΡƒΡŽ!"
245
246 #: templates/boards/posting_general.html:111
244 msgid "Create new thread"
247 msgid "Create new thread"
245 msgstr "Π‘ΠΎΠ·Π΄Π°Ρ‚ΡŒ Π½ΠΎΠ²ΡƒΡŽ Ρ‚Π΅ΠΌΡƒ"
248 msgstr "Π‘ΠΎΠ·Π΄Π°Ρ‚ΡŒ Π½ΠΎΠ²ΡƒΡŽ Ρ‚Π΅ΠΌΡƒ"
246
249
247 #: templates/boards/posting_general.html:218 templates/boards/thread.html:115
250 #: templates/boards/posting_general.html:115 templates/boards/thread.html:50
248 msgid "Post"
251 msgid "Post"
249 msgstr "ΠžΡ‚ΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ"
252 msgstr "ΠžΡ‚ΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ"
250
253
251 #: templates/boards/posting_general.html:222
254 #: templates/boards/posting_general.html:119
252 msgid "Tags must be delimited by spaces. Text or image is required."
255 msgid "Tags must be delimited by spaces. Text or image is required."
253 msgstr ""
256 msgstr ""
254 "Π’Π΅Π³ΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ Ρ€Π°Π·Π΄Π΅Π»Π΅Π½Ρ‹ ΠΏΡ€ΠΎΠ±Π΅Π»Π°ΠΌΠΈ. ВСкст ΠΈΠ»ΠΈ ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹."
257 "Π’Π΅Π³ΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ Ρ€Π°Π·Π΄Π΅Π»Π΅Π½Ρ‹ ΠΏΡ€ΠΎΠ±Π΅Π»Π°ΠΌΠΈ. ВСкст ΠΈΠ»ΠΈ ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹."
255
258
256 #: templates/boards/posting_general.html:225 templates/boards/thread.html:119
259 #: templates/boards/posting_general.html:122 templates/boards/thread.html:54
257 msgid "Text syntax"
260 msgid "Text syntax"
258 msgstr "Бинтаксис тСкста"
261 msgstr "Бинтаксис тСкста"
259
262
263 #: templates/boards/posting_general.html:132
264 msgid "Pages:"
265 msgstr "Π‘Ρ‚Ρ€Π°Π½ΠΈΡ†Ρ‹: "
266
260 #: templates/boards/settings.html:14
267 #: templates/boards/settings.html:14
261 msgid "User:"
268 msgid "User:"
262 msgstr "ΠŸΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ:"
269 msgstr "ΠŸΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ:"
263
270
264 #: templates/boards/settings.html:16
271 #: templates/boards/settings.html:16
265 msgid "You are moderator."
272 msgid "You are moderator."
266 msgstr "Π’Ρ‹ ΠΌΠΎΠ΄Π΅Ρ€Π°Ρ‚ΠΎΡ€."
273 msgstr "Π’Ρ‹ ΠΌΠΎΠ΄Π΅Ρ€Π°Ρ‚ΠΎΡ€."
267
274
268 #: templates/boards/settings.html:19
275 #: templates/boards/settings.html:19
269 msgid "Posts:"
276 msgid "Posts:"
270 msgstr "Π‘ΠΎΠΎΠ±Ρ‰Π΅Π½ΠΈΠΉ:"
277 msgstr "Π‘ΠΎΠΎΠ±Ρ‰Π΅Π½ΠΈΠΉ:"
271
278
272 #: templates/boards/settings.html:20
279 #: templates/boards/settings.html:20
273 msgid "First access:"
280 msgid "First access:"
274 msgstr "ΠŸΠ΅Ρ€Π²Ρ‹ΠΉ доступ:"
281 msgstr "ΠŸΠ΅Ρ€Π²Ρ‹ΠΉ доступ:"
275
282
276 #: templates/boards/settings.html:22
283 #: templates/boards/settings.html:22
277 msgid "Last access:"
284 msgid "Last access:"
278 msgstr "ПослСдний доступ: "
285 msgstr "ПослСдний доступ: "
279
286
280 #: templates/boards/settings.html:31
287 #: templates/boards/settings.html:31
281 msgid "Save"
288 msgid "Save"
282 msgstr "Π‘ΠΎΡ…Ρ€Π°Π½ΠΈΡ‚ΡŒ"
289 msgstr "Π‘ΠΎΡ…Ρ€Π°Π½ΠΈΡ‚ΡŒ"
283
290
284 #: templates/boards/tags.html:27
291 #: templates/boards/tags.html:27
285 msgid "No tags found."
292 msgid "No tags found."
286 msgstr "Π’Π΅Π³ΠΈ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½Ρ‹."
293 msgstr "Π’Π΅Π³ΠΈ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½Ρ‹."
287
294
288 #: templates/boards/thread.html:19 templates/boards/thread_gallery.html:20
295 #: templates/boards/thread.html:20 templates/boards/thread_gallery.html:21
289 msgid "Normal mode"
296 msgid "Normal mode"
290 msgstr "ΠΠΎΡ€ΠΌΠ°Π»ΡŒΠ½Ρ‹ΠΉ Ρ€Π΅ΠΆΠΈΠΌ"
297 msgstr "ΠΠΎΡ€ΠΌΠ°Π»ΡŒΠ½Ρ‹ΠΉ Ρ€Π΅ΠΆΠΈΠΌ"
291
298
292 #: templates/boards/thread.html:20 templates/boards/thread_gallery.html:21
299 #: templates/boards/thread.html:21 templates/boards/thread_gallery.html:22
293 msgid "Gallery mode"
300 msgid "Gallery mode"
294 msgstr "Π Π΅ΠΆΠΈΠΌ Π³Π°Π»Π΅Ρ€Π΅ΠΈ"
301 msgstr "Π Π΅ΠΆΠΈΠΌ Π³Π°Π»Π΅Ρ€Π΅ΠΈ"
295
302
296 #: templates/boards/thread.html:28
303 #: templates/boards/thread.html:29
297 msgid "posts to bumplimit"
304 msgid "posts to bumplimit"
298 msgstr "сообщСний Π΄ΠΎ Π±Π°ΠΌΠΏΠ»ΠΈΠΌΠΈΡ‚Π°"
305 msgstr "сообщСний Π΄ΠΎ Π±Π°ΠΌΠΏΠ»ΠΈΠΌΠΈΡ‚Π°"
299
306
300 #: templates/boards/thread.html:109
307 #: templates/boards/thread.html:44
301 msgid "Reply to thread"
308 msgid "Reply to thread"
302 msgstr "ΠžΡ‚Π²Π΅Ρ‚ΠΈΡ‚ΡŒ Π² Ρ‚Π΅ΠΌΡƒ"
309 msgstr "ΠžΡ‚Π²Π΅Ρ‚ΠΈΡ‚ΡŒ Π² Ρ‚Π΅ΠΌΡƒ"
303
310
304 #: templates/boards/thread.html:139 templates/boards/thread_gallery.html:59
311 #: templates/boards/thread.html:73 templates/boards/thread_gallery.html:58
312 msgid "replies"
313 msgstr "ΠΎΡ‚Π²Π΅Ρ‚ΠΎΠ²"
314
315 #: templates/boards/thread.html:75 templates/boards/thread_gallery.html:60
305 msgid "Last update: "
316 msgid "Last update: "
306 msgstr "ПослСднСС обновлСниС: "
317 msgstr "ПослСднСС обновлСниС: "
307
318
308 #: templates/boards/rss/post.html:5
319 #: templates/boards/rss/post.html:5
309 msgid "Post image"
320 msgid "Post image"
310 msgstr "Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ сообщСния"
321 msgstr "Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ сообщСния"
311
322
312 #: templates/boards/staticpages/banned.html:6
323 #: templates/boards/staticpages/banned.html:6
313 msgid "Banned"
324 msgid "Banned"
314 msgstr "Π—Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Π½"
325 msgstr "Π—Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Π½"
315
326
316 #: templates/boards/staticpages/banned.html:11
327 #: templates/boards/staticpages/banned.html:11
317 msgid "Your IP address has been banned. Contact the administrator"
328 msgid "Your IP address has been banned. Contact the administrator"
318 msgstr "Π’Π°Ρˆ IP адрСс Π±Ρ‹Π» Π·Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Π½. Π‘Π²ΡΠΆΠΈΡ‚Π΅ΡΡŒ с администратором"
329 msgstr "Π’Π°Ρˆ IP адрСс Π±Ρ‹Π» Π·Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Π½. Π‘Π²ΡΠΆΠΈΡ‚Π΅ΡΡŒ с администратором"
319
330
320 #: templates/boards/staticpages/help.html:6
331 #: templates/boards/staticpages/help.html:6
321 #: templates/boards/staticpages/help.html:10
332 #: templates/boards/staticpages/help.html:10
322 msgid "Syntax"
333 msgid "Syntax"
323 msgstr "Бинтаксис"
334 msgstr "Бинтаксис"
324
335
325 #: templates/boards/staticpages/help.html:11
336 #: templates/boards/staticpages/help.html:11
326 msgid "2 line breaks for a new line."
337 msgid "2 line breaks for a new line."
327 msgstr "2 ΠΏΠ΅Ρ€Π΅Π²ΠΎΠ΄Π° строки ΡΠΎΠ·Π΄Π°ΡŽΡ‚ Π½ΠΎΠ²Ρ‹ΠΉ Π°Π±Π·Π°Ρ†."
338 msgstr "2 ΠΏΠ΅Ρ€Π΅Π²ΠΎΠ΄Π° строки ΡΠΎΠ·Π΄Π°ΡŽΡ‚ Π½ΠΎΠ²Ρ‹ΠΉ Π°Π±Π·Π°Ρ†."
328
339
329 #: templates/boards/staticpages/help.html:12
340 #: templates/boards/staticpages/help.html:12
330 msgid "Italic text"
341 msgid "Italic text"
331 msgstr "ΠšΡƒΡ€ΡΠΈΠ²Π½Ρ‹ΠΉ тСкст"
342 msgstr "ΠšΡƒΡ€ΡΠΈΠ²Π½Ρ‹ΠΉ тСкст"
332
343
333 #: templates/boards/staticpages/help.html:13
344 #: templates/boards/staticpages/help.html:13
334 msgid "Bold text"
345 msgid "Bold text"
335 msgstr "ΠŸΠΎΠ»ΡƒΠΆΠΈΡ€Π½Ρ‹ΠΉ тСкст"
346 msgstr "ΠŸΠΎΠ»ΡƒΠΆΠΈΡ€Π½Ρ‹ΠΉ тСкст"
336
347
337 #: templates/boards/staticpages/help.html:14
348 #: templates/boards/staticpages/help.html:14
338 msgid "Spoiler"
349 msgid "Spoiler"
339 msgstr "Π‘ΠΏΠΎΠΉΠ»Π΅Ρ€"
350 msgstr "Π‘ΠΏΠΎΠΉΠ»Π΅Ρ€"
340
351
341 #: templates/boards/staticpages/help.html:15
352 #: templates/boards/staticpages/help.html:15
342 msgid "Link to a post"
353 msgid "Link to a post"
343 msgstr "Бсылка Π½Π° сообщСниС"
354 msgstr "Бсылка Π½Π° сообщСниС"
344
355
345 #: templates/boards/staticpages/help.html:16
356 #: templates/boards/staticpages/help.html:16
346 msgid "Strikethrough text"
357 msgid "Strikethrough text"
347 msgstr "Π—Π°Ρ‡Π΅Ρ€ΠΊΠ½ΡƒΡ‚Ρ‹ΠΉ тСкст"
358 msgstr "Π—Π°Ρ‡Π΅Ρ€ΠΊΠ½ΡƒΡ‚Ρ‹ΠΉ тСкст"
348
359
349 #: templates/boards/staticpages/help.html:17
360 #: templates/boards/staticpages/help.html:17
350 msgid "You need to new line before:"
361 msgid "You need to new line before:"
351 msgstr "ΠŸΠ΅Ρ€Π΅Π΄ этими Ρ‚Π΅Π³Π°ΠΌΠΈ Π½ΡƒΠΆΠ½Π° новая строка:"
362 msgstr "ΠŸΠ΅Ρ€Π΅Π΄ этими Ρ‚Π΅Π³Π°ΠΌΠΈ Π½ΡƒΠΆΠ½Π° новая строка:"
352
363
353 #: templates/boards/staticpages/help.html:18
364 #: templates/boards/staticpages/help.html:18
354 msgid "Comment"
365 msgid "Comment"
355 msgstr "ΠšΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΉ"
366 msgstr "ΠšΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΉ"
356
367
357 #: templates/boards/staticpages/help.html:19
368 #: templates/boards/staticpages/help.html:19
358 msgid "Quote"
369 msgid "Quote"
359 msgstr "Π¦ΠΈΡ‚Π°Ρ‚Π°"
370 msgstr "Π¦ΠΈΡ‚Π°Ρ‚Π°"
@@ -1,393 +1,417 b''
1 from datetime import datetime, timedelta
1 from datetime import datetime, timedelta
2 from datetime import time as dtime
2 from datetime import time as dtime
3 import os
3 import os
4 from random import random
4 from random import random
5 import time
5 import time
6 import math
6 import math
7 import re
7 import re
8 import hashlib
8 import hashlib
9
9
10 from django.core.cache import cache
10 from django.core.cache import cache
11 from django.core.paginator import Paginator
11 from django.core.paginator import Paginator
12
12
13 from django.db import models
13 from django.db import models, transaction
14 from django.http import Http404
14 from django.http import Http404
15 from django.utils import timezone
15 from django.utils import timezone
16 from markupfield.fields import MarkupField
16 from markupfield.fields import MarkupField
17
17
18 from neboard import settings
18 from neboard import settings
19 from boards import thumbs
19 from boards import thumbs
20
20
21 MAX_TITLE_LENGTH = 50
21 MAX_TITLE_LENGTH = 50
22
22
23 APP_LABEL_BOARDS = 'boards'
23 APP_LABEL_BOARDS = 'boards'
24
24
25 CACHE_KEY_PPD = 'ppd'
25 CACHE_KEY_PPD = 'ppd'
26
26
27 POSTS_PER_DAY_RANGE = range(7)
27 POSTS_PER_DAY_RANGE = range(7)
28
28
29 BAN_REASON_AUTO = 'Auto'
29 BAN_REASON_AUTO = 'Auto'
30
30
31 IMAGE_THUMB_SIZE = (200, 150)
31 IMAGE_THUMB_SIZE = (200, 150)
32
32
33 TITLE_MAX_LENGTH = 50
33 TITLE_MAX_LENGTH = 50
34
34
35 DEFAULT_MARKUP_TYPE = 'markdown'
35 DEFAULT_MARKUP_TYPE = 'markdown'
36
36
37 NO_PARENT = -1
37 NO_PARENT = -1
38 NO_IP = '0.0.0.0'
38 NO_IP = '0.0.0.0'
39 UNKNOWN_UA = ''
39 UNKNOWN_UA = ''
40 ALL_PAGES = -1
40 ALL_PAGES = -1
41 IMAGES_DIRECTORY = 'images/'
41 IMAGES_DIRECTORY = 'images/'
42 FILE_EXTENSION_DELIMITER = '.'
42 FILE_EXTENSION_DELIMITER = '.'
43
43
44 SETTING_MODERATE = "moderate"
44 SETTING_MODERATE = "moderate"
45
45
46 REGEX_REPLY = re.compile('>>(\d+)')
46 REGEX_REPLY = re.compile('>>(\d+)')
47
47
48
48
49 class PostManager(models.Manager):
49 class PostManager(models.Manager):
50
50
51 def create_post(self, title, text, image=None, thread=None,
51 def create_post(self, title, text, image=None, thread=None,
52 ip=NO_IP, tags=None, user=None):
52 ip=NO_IP, tags=None, user=None):
53 """
53 """
54 Create new post
54 Create new post
55 """
55 """
56
56
57 posting_time = timezone.now()
57 posting_time = timezone.now()
58 if not thread:
58 if not thread:
59 thread = Thread.objects.create(bump_time=posting_time,
59 thread = Thread.objects.create(bump_time=posting_time,
60 last_edit_time=posting_time)
60 last_edit_time=posting_time)
61 else:
61 else:
62 thread.bump()
62 thread.bump()
63 thread.last_edit_time = posting_time
63 thread.last_edit_time = posting_time
64 thread.save()
64 thread.save()
65
65
66 post = self.create(title=title,
66 post = self.create(title=title,
67 text=text,
67 text=text,
68 pub_time=posting_time,
68 pub_time=posting_time,
69 thread_new=thread,
69 thread_new=thread,
70 image=image,
70 image=image,
71 poster_ip=ip,
71 poster_ip=ip,
72 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
72 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
73 # last!
73 # last!
74 last_edit_time=posting_time,
74 last_edit_time=posting_time,
75 user=user)
75 user=user)
76
76
77 thread.replies.add(post)
77 thread.replies.add(post)
78 if tags:
78 if tags:
79 linked_tags = []
79 linked_tags = []
80 for tag in tags:
80 for tag in tags:
81 tag_linked_tags = tag.get_linked_tags()
81 tag_linked_tags = tag.get_linked_tags()
82 if len(tag_linked_tags) > 0:
82 if len(tag_linked_tags) > 0:
83 linked_tags.extend(tag_linked_tags)
83 linked_tags.extend(tag_linked_tags)
84
84
85 tags.extend(linked_tags)
85 tags.extend(linked_tags)
86 map(thread.add_tag, tags)
86 map(thread.add_tag, tags)
87
87
88 self._delete_old_threads()
88 self._delete_old_threads()
89 self.connect_replies(post)
89 self.connect_replies(post)
90
90
91 return post
91 return post
92
92
93 def delete_post(self, post):
93 def delete_post(self, post):
94 """
94 """
95 Delete post and update or delete its thread
95 Delete post and update or delete its thread
96 """
96 """
97
97
98 thread = post.thread_new
98 thread = post.thread_new
99
99
100 if post.is_opening():
100 if post.is_opening():
101 thread.delete_with_posts()
101 thread.delete_with_posts()
102 else:
102 else:
103 thread.last_edit_time = timezone.now()
103 thread.last_edit_time = timezone.now()
104 thread.save()
104 thread.save()
105
105
106 post.delete()
106 post.delete()
107
107
108 def delete_posts_by_ip(self, ip):
108 def delete_posts_by_ip(self, ip):
109 """
109 """
110 Delete all posts of the author with same IP
110 Delete all posts of the author with same IP
111 """
111 """
112
112
113 posts = self.filter(poster_ip=ip)
113 posts = self.filter(poster_ip=ip)
114 map(self.delete_post, posts)
114 map(self.delete_post, posts)
115
115
116 # TODO This method may not be needed any more, because django's paginator
116 # TODO This method may not be needed any more, because django's paginator
117 # is used
117 # is used
118 def get_threads(self, tag=None, page=ALL_PAGES,
118 def get_threads(self, tag=None, page=ALL_PAGES,
119 order_by='-bump_time', archived=False):
119 order_by='-bump_time', archived=False):
120 if tag:
120 if tag:
121 threads = tag.threads
121 threads = tag.threads
122
122
123 if not threads.exists():
123 if not threads.exists():
124 raise Http404
124 raise Http404
125 else:
125 else:
126 threads = Thread.objects.all()
126 threads = Thread.objects.all()
127
127
128 threads = threads.filter(archived=archived).order_by(order_by)
128 threads = threads.filter(archived=archived).order_by(order_by)
129
129
130 if page != ALL_PAGES:
130 if page != ALL_PAGES:
131 threads = Paginator(threads, settings.THREADS_PER_PAGE).page(
131 threads = Paginator(threads, settings.THREADS_PER_PAGE).page(
132 page).object_list
132 page).object_list
133
133
134 return threads
134 return threads
135
135
136 # TODO Move this method to thread manager
136 # TODO Move this method to thread manager
137 def _delete_old_threads(self):
137 def _delete_old_threads(self):
138 """
138 """
139 Preserves maximum thread count. If there are too many threads,
139 Preserves maximum thread count. If there are too many threads,
140 archive the old ones.
140 archive the old ones.
141 """
141 """
142
142
143 threads = self.get_threads()
143 threads = self.get_threads()
144 thread_count = threads.count()
144 thread_count = threads.count()
145
145
146 if thread_count > settings.MAX_THREAD_COUNT:
146 if thread_count > settings.MAX_THREAD_COUNT:
147 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
147 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
148 old_threads = threads[thread_count - num_threads_to_delete:]
148 old_threads = threads[thread_count - num_threads_to_delete:]
149
149
150 for thread in old_threads:
150 for thread in old_threads:
151 thread.archived = True
151 thread.archived = True
152 thread.last_edit_time = timezone.now()
152 thread.last_edit_time = timezone.now()
153 thread.save()
153 thread.save()
154
154
155 def connect_replies(self, post):
155 def connect_replies(self, post):
156 """
156 """
157 Connect replies to a post to show them as a reflink map
157 Connect replies to a post to show them as a reflink map
158 """
158 """
159
159
160 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
160 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
161 post_id = reply_number.group(1)
161 post_id = reply_number.group(1)
162 ref_post = self.filter(id=post_id)
162 ref_post = self.filter(id=post_id)
163 if ref_post.count() > 0:
163 if ref_post.count() > 0:
164 referenced_post = ref_post[0]
164 referenced_post = ref_post[0]
165 referenced_post.referenced_posts.add(post)
165 referenced_post.referenced_posts.add(post)
166 referenced_post.last_edit_time = post.pub_time
166 referenced_post.last_edit_time = post.pub_time
167 referenced_post.save()
167 referenced_post.save()
168
168
169 referenced_thread = referenced_post.thread_new
169 referenced_thread = referenced_post.thread_new
170 referenced_thread.last_edit_time = post.pub_time
170 referenced_thread.last_edit_time = post.pub_time
171 referenced_thread.save()
171 referenced_thread.save()
172
172
173 def get_posts_per_day(self):
173 def get_posts_per_day(self):
174 """
174 """
175 Get average count of posts per day for the last 7 days
175 Get average count of posts per day for the last 7 days
176 """
176 """
177
177
178 today = datetime.now().date()
178 today = datetime.now().date()
179 ppd = cache.get(CACHE_KEY_PPD + str(today))
179 ppd = cache.get(CACHE_KEY_PPD + str(today))
180 if ppd:
180 if ppd:
181 return ppd
181 return ppd
182
182
183 posts_per_days = []
183 posts_per_days = []
184 for i in POSTS_PER_DAY_RANGE:
184 for i in POSTS_PER_DAY_RANGE:
185 day_end = today - timedelta(i + 1)
185 day_end = today - timedelta(i + 1)
186 day_start = today - timedelta(i + 2)
186 day_start = today - timedelta(i + 2)
187
187
188 day_time_start = timezone.make_aware(datetime.combine(day_start,
188 day_time_start = timezone.make_aware(datetime.combine(day_start,
189 dtime()), timezone.get_current_timezone())
189 dtime()), timezone.get_current_timezone())
190 day_time_end = timezone.make_aware(datetime.combine(day_end,
190 day_time_end = timezone.make_aware(datetime.combine(day_end,
191 dtime()), timezone.get_current_timezone())
191 dtime()), timezone.get_current_timezone())
192
192
193 posts_per_days.append(float(self.filter(
193 posts_per_days.append(float(self.filter(
194 pub_time__lte=day_time_end,
194 pub_time__lte=day_time_end,
195 pub_time__gte=day_time_start).count()))
195 pub_time__gte=day_time_start).count()))
196
196
197 ppd = (sum(posts_per_day for posts_per_day in posts_per_days) /
197 ppd = (sum(posts_per_day for posts_per_day in posts_per_days) /
198 len(posts_per_days))
198 len(posts_per_days))
199 cache.set(CACHE_KEY_PPD, ppd)
199 cache.set(CACHE_KEY_PPD, ppd)
200 return ppd
200 return ppd
201
201
202
202
203 class Post(models.Model):
203 class Post(models.Model):
204 """A post is a message."""
204 """A post is a message."""
205
205
206 objects = PostManager()
206 objects = PostManager()
207
207
208 class Meta:
208 class Meta:
209 app_label = APP_LABEL_BOARDS
209 app_label = APP_LABEL_BOARDS
210
210
211 # TODO Save original file name to some field
211 # TODO Save original file name to some field
212 def _update_image_filename(self, filename):
212 def _update_image_filename(self, filename):
213 """Get unique image filename"""
213 """Get unique image filename"""
214
214
215 path = IMAGES_DIRECTORY
215 path = IMAGES_DIRECTORY
216 new_name = str(int(time.mktime(time.gmtime())))
216 new_name = str(int(time.mktime(time.gmtime())))
217 new_name += str(int(random() * 1000))
217 new_name += str(int(random() * 1000))
218 new_name += FILE_EXTENSION_DELIMITER
218 new_name += FILE_EXTENSION_DELIMITER
219 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
219 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
220
220
221 return os.path.join(path, new_name)
221 return os.path.join(path, new_name)
222
222
223 title = models.CharField(max_length=TITLE_MAX_LENGTH)
223 title = models.CharField(max_length=TITLE_MAX_LENGTH)
224 pub_time = models.DateTimeField()
224 pub_time = models.DateTimeField()
225 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
225 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
226 escape_html=False)
226 escape_html=False)
227
227
228 image_width = models.IntegerField(default=0)
228 image_width = models.IntegerField(default=0)
229 image_height = models.IntegerField(default=0)
229 image_height = models.IntegerField(default=0)
230
230
231 image_pre_width = models.IntegerField(default=0)
231 image_pre_width = models.IntegerField(default=0)
232 image_pre_height = models.IntegerField(default=0)
232 image_pre_height = models.IntegerField(default=0)
233
233
234 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
234 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
235 blank=True, sizes=(IMAGE_THUMB_SIZE,),
235 blank=True, sizes=(IMAGE_THUMB_SIZE,),
236 width_field='image_width',
236 width_field='image_width',
237 height_field='image_height',
237 height_field='image_height',
238 preview_width_field='image_pre_width',
238 preview_width_field='image_pre_width',
239 preview_height_field='image_pre_height')
239 preview_height_field='image_pre_height')
240 image_hash = models.CharField(max_length=36)
240 image_hash = models.CharField(max_length=36)
241
241
242 poster_ip = models.GenericIPAddressField()
242 poster_ip = models.GenericIPAddressField()
243 poster_user_agent = models.TextField()
243 poster_user_agent = models.TextField()
244
244
245 thread = models.ForeignKey('Post', null=True, default=None)
245 thread = models.ForeignKey('Post', null=True, default=None)
246 thread_new = models.ForeignKey('Thread', null=True, default=None)
246 thread_new = models.ForeignKey('Thread', null=True, default=None)
247 last_edit_time = models.DateTimeField()
247 last_edit_time = models.DateTimeField()
248 user = models.ForeignKey('User', null=True, default=None)
248 user = models.ForeignKey('User', null=True, default=None)
249
249
250 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
250 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
251 null=True,
251 null=True,
252 blank=True, related_name='rfp+')
252 blank=True, related_name='rfp+')
253
253
254 def __unicode__(self):
254 def __unicode__(self):
255 return '#' + str(self.id) + ' ' + self.title + ' (' + \
255 return '#' + str(self.id) + ' ' + self.title + ' (' + \
256 self.text.raw[:50] + ')'
256 self.text.raw[:50] + ')'
257
257
258 def get_title(self):
258 def get_title(self):
259 title = self.title
259 title = self.title
260 if len(title) == 0:
260 if len(title) == 0:
261 title = self.text.rendered
261 title = self.text.rendered
262
262
263 return title
263 return title
264
264
265 def get_sorted_referenced_posts(self):
265 def get_sorted_referenced_posts(self):
266 return self.referenced_posts.order_by('id')
266 return self.referenced_posts.order_by('id')
267
267
268 def is_referenced(self):
268 def is_referenced(self):
269 return self.referenced_posts.all().exists()
269 return self.referenced_posts.all().exists()
270
270
271 def is_opening(self):
271 def is_opening(self):
272 return self.thread_new.get_replies()[0] == self
272 return self.thread_new.get_replies()[0] == self
273
273
274 def save(self, *args, **kwargs):
274 def save(self, *args, **kwargs):
275 """
275 """
276 Save the model and compute the image hash
276 Save the model and compute the image hash
277 """
277 """
278
278
279 if not self.pk and self.image:
279 if not self.pk and self.image:
280 md5 = hashlib.md5()
280 md5 = hashlib.md5()
281 for chunk in self.image.chunks():
281 for chunk in self.image.chunks():
282 md5.update(chunk)
282 md5.update(chunk)
283 self.image_hash = md5.hexdigest()
283 self.image_hash = md5.hexdigest()
284 super(Post, self).save(*args, **kwargs)
284 super(Post, self).save(*args, **kwargs)
285
285
286 @transaction.atomic
287 def add_tag(self, tag):
288 edit_time = timezone.now()
289
290 thread = self.thread_new
291 thread.add_tag(tag)
292 self.last_edit_time = edit_time
293 self.save()
294
295 thread.last_edit_time = edit_time
296 thread.save()
297
298 @transaction.atomic
299 def remove_tag(self, tag):
300 edit_time = timezone.now()
301
302 thread = self.thread_new
303 thread.tags.remove(tag)
304 self.last_edit_time = edit_time
305 self.save()
306
307 thread.last_edit_time = edit_time
308 thread.save()
309
286
310
287 class Thread(models.Model):
311 class Thread(models.Model):
288
312
289 class Meta:
313 class Meta:
290 app_label = APP_LABEL_BOARDS
314 app_label = APP_LABEL_BOARDS
291
315
292 tags = models.ManyToManyField('Tag')
316 tags = models.ManyToManyField('Tag')
293 bump_time = models.DateTimeField()
317 bump_time = models.DateTimeField()
294 last_edit_time = models.DateTimeField()
318 last_edit_time = models.DateTimeField()
295 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
319 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
296 blank=True, related_name='tre+')
320 blank=True, related_name='tre+')
297 archived = models.BooleanField(default=False)
321 archived = models.BooleanField(default=False)
298
322
299 def get_tags(self):
323 def get_tags(self):
300 """
324 """
301 Get a sorted tag list
325 Get a sorted tag list
302 """
326 """
303
327
304 return self.tags.order_by('name')
328 return self.tags.order_by('name')
305
329
306 def bump(self):
330 def bump(self):
307 """
331 """
308 Bump (move to up) thread
332 Bump (move to up) thread
309 """
333 """
310
334
311 if self.can_bump():
335 if self.can_bump():
312 self.bump_time = timezone.now()
336 self.bump_time = timezone.now()
313
337
314 def get_reply_count(self):
338 def get_reply_count(self):
315 return self.replies.count()
339 return self.replies.count()
316
340
317 def get_images_count(self):
341 def get_images_count(self):
318 return self.replies.filter(image_width__gt=0).count()
342 return self.replies.filter(image_width__gt=0).count()
319
343
320 def can_bump(self):
344 def can_bump(self):
321 """
345 """
322 Check if the thread can be bumped by replying
346 Check if the thread can be bumped by replying
323 """
347 """
324
348
325 if self.archived:
349 if self.archived:
326 return False
350 return False
327
351
328 post_count = self.get_reply_count()
352 post_count = self.get_reply_count()
329
353
330 return post_count < settings.MAX_POSTS_PER_THREAD
354 return post_count < settings.MAX_POSTS_PER_THREAD
331
355
332 def delete_with_posts(self):
356 def delete_with_posts(self):
333 """
357 """
334 Completely delete thread and all its posts
358 Completely delete thread and all its posts
335 """
359 """
336
360
337 if self.replies.count() > 0:
361 if self.replies.count() > 0:
338 self.replies.all().delete()
362 self.replies.all().delete()
339
363
340 self.delete()
364 self.delete()
341
365
342 def get_last_replies(self):
366 def get_last_replies(self):
343 """
367 """
344 Get last replies, not including opening post
368 Get last replies, not including opening post
345 """
369 """
346
370
347 if settings.LAST_REPLIES_COUNT > 0:
371 if settings.LAST_REPLIES_COUNT > 0:
348 reply_count = self.get_reply_count()
372 reply_count = self.get_reply_count()
349
373
350 if reply_count > 0:
374 if reply_count > 0:
351 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
375 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
352 reply_count - 1)
376 reply_count - 1)
353 last_replies = self.replies.all().order_by('pub_time')[
377 last_replies = self.replies.all().order_by('pub_time')[
354 reply_count - reply_count_to_show:]
378 reply_count - reply_count_to_show:]
355
379
356 return last_replies
380 return last_replies
357
381
358 def get_skipped_replies_count(self):
382 def get_skipped_replies_count(self):
359 last_replies = self.get_last_replies()
383 last_replies = self.get_last_replies()
360 return self.get_reply_count() - len(last_replies) - 1
384 return self.get_reply_count() - len(last_replies) - 1
361
385
362 def get_replies(self):
386 def get_replies(self):
363 """
387 """
364 Get sorted thread posts
388 Get sorted thread posts
365 """
389 """
366
390
367 return self.replies.all().order_by('pub_time')
391 return self.replies.all().order_by('pub_time')
368
392
369 def add_tag(self, tag):
393 def add_tag(self, tag):
370 """
394 """
371 Connect thread to a tag and tag to a thread
395 Connect thread to a tag and tag to a thread
372 """
396 """
373
397
374 self.tags.add(tag)
398 self.tags.add(tag)
375 tag.threads.add(self)
399 tag.threads.add(self)
376
400
377 def get_opening_post(self):
401 def get_opening_post(self):
378 """
402 """
379 Get first post of the thread
403 Get first post of the thread
380 """
404 """
381
405
382 return self.get_replies()[0]
406 return self.get_replies()[0]
383
407
384 def __unicode__(self):
408 def __unicode__(self):
385 return str(self.id)
409 return str(self.id)
386
410
387 def get_pub_time(self):
411 def get_pub_time(self):
388 """
412 """
389 Thread does not have its own pub time, so we need to get it from
413 Thread does not have its own pub time, so we need to get it from
390 the opening post
414 the opening post
391 """
415 """
392
416
393 return self.get_opening_post().pub_time
417 return self.get_opening_post().pub_time
@@ -1,96 +1,98 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 {% cache 600 post post.id post.thread_new.last_edit_time truncated moderator LANGUAGE_CODE need_open_link %}
7 {% cache 600 post post.id post.thread_new.last_edit_time truncated moderator LANGUAGE_CODE need_open_link %}
8 {% spaceless %}
8 {% spaceless %}
9 {% with thread=post.thread_new %}
9 {% with thread=post.thread_new %}
10 {% if thread.archived %}
10 {% if thread.archived %}
11 <div class="post archive_post" id="{{ post.id }}">
11 <div class="post archive_post" id="{{ post.id }}">
12 {% elif thread.can_bump %}
12 {% elif thread.can_bump %}
13 <div class="post" id="{{ post.id }}">
13 <div class="post" id="{{ post.id }}">
14 {% else %}
14 {% else %}
15 <div class="post dead_post" id="{{ post.id }}">
15 <div class="post dead_post" id="{{ post.id }}">
16 {% endif %}
16 {% endif %}
17
17
18 {% if post.image %}
18 {% if post.image %}
19 <div class="image">
19 <div class="image">
20 <a
20 <a
21 class="thumb"
21 class="thumb"
22 href="{{ post.image.url }}"><img
22 href="{{ post.image.url }}"><img
23 src="{{ post.image.url_200x150 }}"
23 src="{{ post.image.url_200x150 }}"
24 alt="{{ post.id }}"
24 alt="{{ post.id }}"
25 width="{{ post.image_pre_width }}"
25 width="{{ post.image_pre_width }}"
26 height="{{ post.image_pre_height }}"
26 height="{{ post.image_pre_height }}"
27 data-width="{{ post.image_width }}"
27 data-width="{{ post.image_width }}"
28 data-height="{{ post.image_height }}"/>
28 data-height="{{ post.image_height }}"/>
29 </a>
29 </a>
30 </div>
30 </div>
31 {% endif %}
31 {% endif %}
32 <div class="message">
32 <div class="message">
33 <div class="post-info">
33 <div class="post-info">
34 <span class="title">{{ post.title }}</span>
34 <span class="title">{{ post.title }}</span>
35 <a class="post_id" href="{% post_url post.id %}">
35 <a class="post_id" href="{% post_url post.id %}">
36 ({{ post.id }}) </a>
36 ({{ post.id }}) </a>
37 [<span class="pub_time">{{ post.pub_time }}</span>]
37 [<span class="pub_time">{{ post.pub_time }}</span>]
38 {% if thread.archived %}
38 {% if thread.archived %}
39 β€” [{{ thread.bump_time }}]
39 β€” [{{ thread.bump_time }}]
40 {% endif %}
40 {% endif %}
41 {% if not truncated and not thread.archived%}
41 {% if not truncated and not thread.archived%}
42 [<a href="#" onclick="javascript:addQuickReply('{{ post.id }}')
42 [<a href="#" onclick="javascript:addQuickReply('{{ post.id }}')
43 ; return false;">&gt;&gt;</a>]
43 ; return false;">&gt;&gt;</a>]
44 {% endif %}
44 {% endif %}
45 {% if post.is_opening and need_open_link %}
45 {% if post.is_opening and need_open_link %}
46 {% if post.thread_new.archived %}
46 {% if post.thread_new.archived %}
47 [<a class="link" href="{% url 'thread' post.id %}">{% trans "Open" %}</a>]
47 [<a class="link" href="{% url 'thread' post.id %}">{% trans "Open" %}</a>]
48 {% else %}
48 {% else %}
49 [<a class="link" href="{% url 'thread' post.id %}#form">{% trans "Reply" %}</a>]
49 [<a class="link" href="{% url 'thread' post.id %}#form">{% trans "Reply" %}</a>]
50 {% endif %}
50 {% endif %}
51 {% endif %}
51 {% endif %}
52
52
53 {% if moderator %}
53 {% if moderator %}
54 <span class="moderator_info">
54 <span class="moderator_info">
55 [<a href="{% url 'post_admin' post_id=post.id %}"
56 >{% trans 'Edit' %}</a>]
55 [<a href="{% url 'delete' post_id=post.id %}"
57 [<a href="{% url 'delete' post_id=post.id %}"
56 >{% trans 'Delete' %}</a>]
58 >{% trans 'Delete' %}</a>]
57 ({{ post.poster_ip }})
59 ({{ post.poster_ip }})
58 [<a href="{% url 'ban' post_id=post.id %}?next={{ request.path }}"
60 [<a href="{% url 'ban' post_id=post.id %}?next={{ request.path }}"
59 >{% trans 'Ban IP' %}</a>]
61 >{% trans 'Ban IP' %}</a>]
60 </span>
62 </span>
61 {% endif %}
63 {% endif %}
62 </div>
64 </div>
63 {% autoescape off %}
65 {% autoescape off %}
64 {% if truncated %}
66 {% if truncated %}
65 {{ post.text.rendered|truncatewords_html:50 }}
67 {{ post.text.rendered|truncatewords_html:50 }}
66 {% else %}
68 {% else %}
67 {{ post.text.rendered }}
69 {{ post.text.rendered }}
68 {% endif %}
70 {% endif %}
69 {% endautoescape %}
71 {% endautoescape %}
70 {% if post.is_referenced %}
72 {% if post.is_referenced %}
71 <div class="refmap">
73 <div class="refmap">
72 {% trans "Replies" %}:
74 {% trans "Replies" %}:
73 {% for ref_post in post.get_sorted_referenced_posts %}
75 {% for ref_post in post.get_sorted_referenced_posts %}
74 <a href="{% post_url ref_post.id %}">&gt;&gt;{{ ref_post.id }}</a
76 <a href="{% post_url ref_post.id %}">&gt;&gt;{{ ref_post.id }}</a
75 >{% if not forloop.last %},{% endif %}
77 >{% if not forloop.last %},{% endif %}
76 {% endfor %}
78 {% endfor %}
77 </div>
79 </div>
78 {% endif %}
80 {% endif %}
79 </div>
81 </div>
80 {% if post.is_opening and thread.tags.exists %}
82 {% if post.is_opening and thread.tags.exists %}
81 <div class="metadata">
83 <div class="metadata">
82 {% if post.is_opening and need_open_link %}
84 {% if post.is_opening and need_open_link %}
83 {{ thread.get_images_count }} {% trans 'images' %}.
85 {{ thread.get_images_count }} {% trans 'images' %}.
84 {% endif %}
86 {% endif %}
85 <span class="tags">
87 <span class="tags">
86 {% for tag in thread.get_tags %}
88 {% for tag in thread.get_tags %}
87 <a class="tag" href="{% url 'tag' tag.name %}">
89 <a class="tag" href="{% url 'tag' tag.name %}">
88 #{{ tag.name }}</a>{% if not forloop.last %},{% endif %}
90 #{{ tag.name }}</a>{% if not forloop.last %},{% endif %}
89 {% endfor %}
91 {% endfor %}
90 </span>
92 </span>
91 </div>
93 </div>
92 {% endif %}
94 {% endif %}
93 </div>
95 </div>
94 {% endwith %}
96 {% endwith %}
95 {% endspaceless %}
97 {% endspaceless %}
96 {% endcache %}
98 {% endcache %}
@@ -1,76 +1,83 b''
1 from django.conf.urls import patterns, url, include
1 from django.conf.urls import patterns, url, include
2 from boards import views
2 from boards import views
3 from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed
3 from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed
4 from boards.views import api, tag_threads, all_threads, archived_threads, \
4 from boards.views import api, tag_threads, all_threads, archived_threads, \
5 login, settings, all_tags
5 login, settings, all_tags
6 from boards.views.authors import AuthorsView
6 from boards.views.authors import AuthorsView
7 from boards.views.delete_post import DeletePostView
7 from boards.views.delete_post import DeletePostView
8 from boards.views.ban import BanUserView
8 from boards.views.ban import BanUserView
9 from boards.views.static import StaticPageView
9 from boards.views.static import StaticPageView
10 from boards.views.post_admin import PostAdminView
10
11
11 js_info_dict = {
12 js_info_dict = {
12 'packages': ('boards',),
13 'packages': ('boards',),
13 }
14 }
14
15
15 urlpatterns = patterns('',
16 urlpatterns = patterns('',
16
17
17 # /boards/
18 # /boards/
18 url(r'^$', all_threads.AllThreadsView.as_view(), name='index'),
19 url(r'^$', all_threads.AllThreadsView.as_view(), name='index'),
19 # /boards/page/
20 # /boards/page/
20 url(r'^page/(?P<page>\w+)/$', all_threads.AllThreadsView.as_view(),
21 url(r'^page/(?P<page>\w+)/$', all_threads.AllThreadsView.as_view(),
21 name='index'),
22 name='index'),
22
23
23 url(r'^archive/$', archived_threads.ArchiveView.as_view(), name='archive'),
24 url(r'^archive/$', archived_threads.ArchiveView.as_view(), name='archive'),
24 url(r'^archive/page/(?P<page>\w+)/$',
25 url(r'^archive/page/(?P<page>\w+)/$',
25 archived_threads.ArchiveView.as_view(), name='archive'),
26 archived_threads.ArchiveView.as_view(), name='archive'),
26
27
27 # login page
28 # login page
28 url(r'^login/$', login.LoginView.as_view(), name='login'),
29 url(r'^login/$', login.LoginView.as_view(), name='login'),
29
30
30 # /boards/tag/tag_name/
31 # /boards/tag/tag_name/
31 url(r'^tag/(?P<tag_name>\w+)/$', tag_threads.TagView.as_view(),
32 url(r'^tag/(?P<tag_name>\w+)/$', tag_threads.TagView.as_view(),
32 name='tag'),
33 name='tag'),
33 # /boards/tag/tag_id/page/
34 # /boards/tag/tag_id/page/
34 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/$',
35 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/$',
35 tag_threads.TagView.as_view(), name='tag'),
36 tag_threads.TagView.as_view(), name='tag'),
36
37
37 # /boards/thread/
38 # /boards/thread/
38 url(r'^thread/(?P<post_id>\w+)/$', views.thread.ThreadView.as_view(),
39 url(r'^thread/(?P<post_id>\w+)/$', views.thread.ThreadView.as_view(),
39 name='thread'),
40 name='thread'),
40 url(r'^thread/(?P<post_id>\w+)/(?P<mode>\w+)/$', views.thread.ThreadView
41 url(r'^thread/(?P<post_id>\w+)/(?P<mode>\w+)/$', views.thread.ThreadView
41 .as_view(), name='thread_mode'),
42 .as_view(), name='thread_mode'),
42
43
44 # /boards/post_admin/
45 url(r'^post_admin/(?P<post_id>\w+)/$', PostAdminView.as_view(),
46 name='post_admin'),
47
43 url(r'^settings/$', settings.SettingsView.as_view(), name='settings'),
48 url(r'^settings/$', settings.SettingsView.as_view(), name='settings'),
44 url(r'^tags/$', all_tags.AllTagsView.as_view(), name='tags'),
49 url(r'^tags/$', all_tags.AllTagsView.as_view(), name='tags'),
45 url(r'^captcha/', include('captcha.urls')),
50 url(r'^captcha/', include('captcha.urls')),
46 url(r'^authors/$', AuthorsView.as_view(), name='authors'),
51 url(r'^authors/$', AuthorsView.as_view(), name='authors'),
47 url(r'^delete/(?P<post_id>\w+)/$', DeletePostView.as_view(),
52 url(r'^delete/(?P<post_id>\w+)/$', DeletePostView.as_view(),
48 name='delete'),
53 name='delete'),
49 url(r'^ban/(?P<post_id>\w+)/$', BanUserView.as_view(), name='ban'),
54 url(r'^ban/(?P<post_id>\w+)/$', BanUserView.as_view(), name='ban'),
50
55
51 url(r'^banned/$', views.banned.BannedView.as_view(), name='banned'),
56 url(r'^banned/$', views.banned.BannedView.as_view(), name='banned'),
52 url(r'^staticpage/(?P<name>\w+)/$', StaticPageView.as_view(),
57 url(r'^staticpage/(?P<name>\w+)/$', StaticPageView.as_view(),
53 name='staticpage'),
58 name='staticpage'),
54
59
55 # RSS feeds
60 # RSS feeds
56 url(r'^rss/$', AllThreadsFeed()),
61 url(r'^rss/$', AllThreadsFeed()),
57 url(r'^page/(?P<page>\w+)/rss/$', AllThreadsFeed()),
62 url(r'^page/(?P<page>\w+)/rss/$', AllThreadsFeed()),
58 url(r'^tag/(?P<tag_name>\w+)/rss/$', TagThreadsFeed()),
63 url(r'^tag/(?P<tag_name>\w+)/rss/$', TagThreadsFeed()),
59 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/rss/$', TagThreadsFeed()),
64 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/rss/$', TagThreadsFeed()),
60 url(r'^thread/(?P<post_id>\w+)/rss/$', ThreadPostsFeed()),
65 url(r'^thread/(?P<post_id>\w+)/rss/$', ThreadPostsFeed()),
61
66
62 # i18n
67 # i18n
63 url(r'^jsi18n/$', 'boards.views.cached_js_catalog', js_info_dict, name='js_info_dict'),
68 url(r'^jsi18n/$', 'boards.views.cached_js_catalog', js_info_dict,
69 name='js_info_dict'),
64
70
65 # API
71 # API
66 url(r'^api/post/(?P<post_id>\w+)/$', api.get_post, name="get_post"),
72 url(r'^api/post/(?P<post_id>\w+)/$', api.get_post, name="get_post"),
67 url(r'^api/diff_thread/(?P<thread_id>\w+)/(?P<last_update_time>\w+)/$',
73 url(r'^api/diff_thread/(?P<thread_id>\w+)/(?P<last_update_time>\w+)/$',
68 api.api_get_threaddiff, name="get_thread_diff"),
74 api.api_get_threaddiff, name="get_thread_diff"),
69 url(r'^api/threads/(?P<count>\w+)/$', api.api_get_threads,
75 url(r'^api/threads/(?P<count>\w+)/$', api.api_get_threads,
70 name='get_threads'),
76 name='get_threads'),
71 url(r'^api/tags/$', api.api_get_tags, name='get_tags'),
77 url(r'^api/tags/$', api.api_get_tags, name='get_tags'),
72 url(r'^api/thread/(?P<opening_post_id>\w+)/$', api.api_get_thread_posts,
78 url(r'^api/thread/(?P<opening_post_id>\w+)/$', api.api_get_thread_posts,
73 name='get_thread'),
79 name='get_thread'),
74 url(r'^api/add_post/(?P<opening_post_id>\w+)/$', api.api_add_post, name='add_post'),
80 url(r'^api/add_post/(?P<opening_post_id>\w+)/$', api.api_add_post,
81 name='add_post'),
75
82
76 )
83 )
@@ -1,33 +1,39 b''
1 PARAMETER_METHOD = 'method'
1 PARAMETER_METHOD = 'method'
2
2
3 from django.shortcuts import redirect
3 from django.shortcuts import redirect
4 from django.http import HttpResponseRedirect
4 from django.http import HttpResponseRedirect
5
5
6
6
7 class RedirectNextMixin:
7 class RedirectNextMixin:
8
8
9 def redirect_to_next(self, request):
9 def redirect_to_next(self, request):
10 """
10 """
11 If a 'next' parameter was specified, redirect to the next page. This
11 If a 'next' parameter was specified, redirect to the next page. This
12 is used when the user is required to return to some page after the
12 is used when the user is required to return to some page after the
13 current view has finished its work.
13 current view has finished its work.
14 """
14 """
15
15
16 if 'next' in request.GET:
16 if 'next' in request.GET:
17 next_page = request.GET['next']
17 next_page = request.GET['next']
18 return HttpResponseRedirect(next_page)
18 return HttpResponseRedirect(next_page)
19 else:
19 else:
20 return redirect('index')
20 return redirect('index')
21
21
22
22
23 class DispatcherMixin:
23 class DispatcherMixin:
24 """
24 """
25 This class contains a dispather method that can run a method specified by
25 This class contains a dispather method that can run a method specified by
26 'method' request parameter.
26 'method' request parameter.
27 """
27 """
28
28
29 def dispatch_method(self, request):
29 def dispatch_method(self, *args, **kwargs):
30 request = args[0]
31
32 method_name = None
30 if PARAMETER_METHOD in request.GET:
33 if PARAMETER_METHOD in request.GET:
31 method_name = request.GET[PARAMETER_METHOD]
34 method_name = request.GET[PARAMETER_METHOD]
32 return getattr(self, method_name)(request)
35 elif PARAMETER_METHOD in request.POST:
36 method_name = request.POST[PARAMETER_METHOD]
33
37
38 if method_name:
39 return getattr(self, method_name)(*args, **kwargs)
General Comments 0
You need to be logged in to leave comments. Login now