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