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