##// END OF EJS Templates
Merged with default branch
neko259 -
r805:40a08ce9 merge decentral
parent child Browse files
Show More
@@ -1,143 +1,146 b''
1 1 from django.shortcuts import get_object_or_404
2 2 from boards.models import Tag
3 3
4 4 __author__ = 'neko259'
5 5
6 6 SESSION_SETTING = 'setting'
7 7
8 8 PERMISSION_MODERATE = 'moderator'
9 9
10 10 SETTING_THEME = 'theme'
11 11 SETTING_FAVORITE_TAGS = 'favorite_tags'
12 12 SETTING_HIDDEN_TAGS = 'hidden_tags'
13 13 SETTING_PERMISSIONS = 'permissions'
14 14
15 15 DEFAULT_THEME = 'md'
16 16
17 17
18 18 def get_settings_manager(request):
19 19 """
20 20 Get settings manager based on the request object. Currently only
21 21 session-based manager is supported. In the future, cookie-based or
22 22 database-based managers could be implemented.
23 23 """
24 24 return SessionSettingsManager(request.session)
25 25
26 26
27 27 class SettingsManager:
28 28 """
29 29 Base settings manager class. get_setting and set_setting methods should
30 30 be overriden.
31 31 """
32 32 def __init__(self):
33 33 pass
34 34
35 35 def get_theme(self):
36 36 theme = self.get_setting(SETTING_THEME)
37 37 if not theme:
38 38 theme = DEFAULT_THEME
39 39 self.set_setting(SETTING_THEME, theme)
40 40
41 41 return theme
42 42
43 43 def set_theme(self, theme):
44 44 self.set_setting(SETTING_THEME, theme)
45 45
46 46 def has_permission(self, permission):
47 47 permissions = self.get_setting(SETTING_PERMISSIONS)
48 48 if permissions:
49 49 return permission in permissions
50 50 else:
51 51 return False
52 52
53 53 def get_setting(self, setting):
54 54 pass
55 55
56 56 def set_setting(self, setting, value):
57 57 pass
58 58
59 59 def add_permission(self, permission):
60 60 permissions = self.get_setting(SETTING_PERMISSIONS)
61 61 if not permissions:
62 62 permissions = [permission]
63 63 else:
64 64 permissions.append(permission)
65 65 self.set_setting(SETTING_PERMISSIONS, permissions)
66 66
67 67 def del_permission(self, permission):
68 68 permissions = self.get_setting(SETTING_PERMISSIONS)
69 69 if not permissions:
70 70 permissions = []
71 71 else:
72 72 permissions.remove(permission)
73 73 self.set_setting(SETTING_PERMISSIONS, permissions)
74 74
75 75 def get_fav_tags(self):
76 76 tag_names = self.get_setting(SETTING_FAVORITE_TAGS)
77 77 tags = []
78 78 if tag_names:
79 79 for tag_name in tag_names:
80 80 tag = get_object_or_404(Tag, name=tag_name)
81 81 tags.append(tag)
82
83 82 return tags
84 83
85 84 def add_fav_tag(self, tag):
86 85 tags = self.get_setting(SETTING_FAVORITE_TAGS)
87 86 if not tags:
88 87 tags = [tag.name]
89 88 else:
90 89 if not tag.name in tags:
91 90 tags.append(tag.name)
91
92 tags.sort()
92 93 self.set_setting(SETTING_FAVORITE_TAGS, tags)
93 94
94 95 def del_fav_tag(self, tag):
95 96 tags = self.get_setting(SETTING_FAVORITE_TAGS)
96 97 if tag.name in tags:
97 98 tags.remove(tag.name)
98 99 self.set_setting(SETTING_FAVORITE_TAGS, tags)
99 100
100 101 def get_hidden_tags(self):
101 102 tag_names = self.get_setting(SETTING_HIDDEN_TAGS)
102 103 tags = []
103 104 if tag_names:
104 105 for tag_name in tag_names:
105 106 tag = get_object_or_404(Tag, name=tag_name)
106 107 tags.append(tag)
107 108
108 109 return tags
109 110
110 111 def add_hidden_tag(self, tag):
111 112 tags = self.get_setting(SETTING_HIDDEN_TAGS)
112 113 if not tags:
113 114 tags = [tag.name]
114 115 else:
115 116 if not tag.name in tags:
116 117 tags.append(tag.name)
118
119 tags.sort()
117 120 self.set_setting(SETTING_HIDDEN_TAGS, tags)
118 121
119 122 def del_hidden_tag(self, tag):
120 123 tags = self.get_setting(SETTING_HIDDEN_TAGS)
121 124 if tag.name in tags:
122 125 tags.remove(tag.name)
123 126 self.set_setting(SETTING_HIDDEN_TAGS, tags)
124 127
125 128
126 129 class SessionSettingsManager(SettingsManager):
127 130 """
128 131 Session-based settings manager. All settings are saved to the user's
129 132 session.
130 133 """
131 134 def __init__(self, session):
132 135 SettingsManager.__init__(self)
133 136 self.session = session
134 137
135 138 def get_setting(self, setting):
136 139 if setting in self.session:
137 140 return self.session[setting]
138 141 else:
139 142 return None
140 143
141 144 def set_setting(self, setting, value):
142 145 self.session[setting] = value
143 146
@@ -1,38 +1,43 b''
1 1 from django.contrib import admin
2 2 from boards.models import Post, Tag, Ban, Thread, KeyPair
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 14 list_display = ('name',)
15 15
16 16 class ThreadAdmin(admin.ModelAdmin):
17 17
18 18 def title(self, obj):
19 19 return obj.get_opening_post().title
20 20
21 21 def reply_count(self, obj):
22 22 return obj.get_reply_count()
23 23
24 24 list_display = ('id', 'title', 'reply_count', 'archived')
25 25 list_filter = ('bump_time', 'archived')
26 26 search_fields = ('id', 'title')
27 27
28 28
29 29 class KeyPairAdmin(admin.ModelAdmin):
30 30 list_display = ('public_key', 'primary')
31 31 list_filter = ('primary',)
32 32 search_fields = ('public_key',)
33 33
34 class BanAdmin(admin.ModelAdmin):
35 list_display = ('ip', 'can_read')
36 list_filter = ('can_read',)
37 search_fields = ('ip',)
38
34 39 admin.site.register(Post, PostAdmin)
35 40 admin.site.register(Tag, TagAdmin)
36 admin.site.register(Ban)
41 admin.site.register(Ban, BanAdmin)
37 42 admin.site.register(Thread, ThreadAdmin)
38 43 admin.site.register(KeyPair, KeyPairAdmin)
@@ -1,178 +1,178 b''
1 1 # coding=utf-8
2 2
3 3 import re
4 4 import bbcode
5 5
6 6 import boards
7 7
8 8
9 9 __author__ = 'neko259'
10 10
11 11
12 REFLINK_PATTERN = re.compile(r'\d+')
12 REFLINK_PATTERN = re.compile(r'^\d+$')
13 13 MULTI_NEWLINES_PATTERN = re.compile(r'(\r?\n){2,}')
14 14 ONE_NEWLINE = '\n'
15 15
16 16
17 17 class TextFormatter():
18 18 """
19 19 An interface for formatter that can be used in the text format panel
20 20 """
21 21
22 22 def __init__(self):
23 23 pass
24 24
25 25 name = ''
26 26
27 27 # Left and right tags for the button preview
28 28 preview_left = ''
29 29 preview_right = ''
30 30
31 31 # Left and right characters for the textarea input
32 32 format_left = ''
33 33 format_right = ''
34 34
35 35
36 36 class AutolinkPattern():
37 37 def handleMatch(self, m):
38 38 link_element = etree.Element('a')
39 39 href = m.group(2)
40 40 link_element.set('href', href)
41 41 link_element.text = href
42 42
43 43 return link_element
44 44
45 45
46 46 class QuotePattern(TextFormatter):
47 47 name = 'q'
48 48 preview_left = '<span class="multiquote">'
49 49 preview_right = '</span>'
50 50
51 51 format_left = '[quote]'
52 52 format_right = '[/quote]'
53 53
54 54
55 55 class SpoilerPattern(TextFormatter):
56 56 name = 'spoiler'
57 57 preview_left = '<span class="spoiler">'
58 58 preview_right = '</span>'
59 59
60 60 format_left = '[spoiler]'
61 61 format_right = '[/spoiler]'
62 62
63 63 def handleMatch(self, m):
64 64 quote_element = etree.Element('span')
65 65 quote_element.set('class', 'spoiler')
66 66 quote_element.text = m.group(2)
67 67
68 68 return quote_element
69 69
70 70
71 71 class CommentPattern(TextFormatter):
72 72 name = ''
73 73 preview_left = '<span class="comment">// '
74 74 preview_right = '</span>'
75 75
76 76 format_left = '[comment]'
77 77 format_right = '[/comment]'
78 78
79 79
80 80 # TODO Use <s> tag here
81 81 class StrikeThroughPattern(TextFormatter):
82 82 name = 's'
83 83 preview_left = '<span class="strikethrough">'
84 84 preview_right = '</span>'
85 85
86 86 format_left = '[s]'
87 87 format_right = '[/s]'
88 88
89 89
90 90 class ItalicPattern(TextFormatter):
91 91 name = 'i'
92 92 preview_left = '<i>'
93 93 preview_right = '</i>'
94 94
95 95 format_left = '[i]'
96 96 format_right = '[/i]'
97 97
98 98
99 99 class BoldPattern(TextFormatter):
100 100 name = 'b'
101 101 preview_left = '<b>'
102 102 preview_right = '</b>'
103 103
104 104 format_left = '[b]'
105 105 format_right = '[/b]'
106 106
107 107
108 108 class CodePattern(TextFormatter):
109 109 name = 'code'
110 110 preview_left = '<code>'
111 111 preview_right = '</code>'
112 112
113 113 format_left = '[code]'
114 114 format_right = '[/code]'
115 115
116 116
117 117 def render_reflink(tag_name, value, options, parent, context):
118 118 if not REFLINK_PATTERN.match(value):
119 119 return u'>>%s' % value
120 120
121 121 post_id = int(value)
122 122
123 123 posts = boards.models.Post.objects.filter(id=post_id)
124 124 if posts.exists():
125 125 post = posts[0]
126 126
127 return u'<a href=%s>&gt;&gt;%s</a>' % (post.get_url(), post_id)
127 return u'<a href="%s">&gt;&gt;%s</a>' % (post.get_url(), post_id)
128 128 else:
129 129 return u'>>%s' % value
130 130
131 131
132 132 def render_quote(tag_name, value, options, parent, context):
133 133 source = u''
134 134 if 'source' in options:
135 135 source = options['source']
136 136
137 137 result = u''
138 138 if source:
139 139 result = u'<div class="multiquote"><div class="quote-header">%s</div><div class="quote-text">%s</div></div>' % (source, value)
140 140 else:
141 141 result = u'<div class="multiquote"><div class="quote-text">%s</div></div>' % value
142 142
143 143 return result
144 144
145 145
146 146 def preparse_text(text):
147 147 """
148 148 Performs manual parsing before the bbcode parser is used.
149 149 """
150 150
151 151 return MULTI_NEWLINES_PATTERN.sub(ONE_NEWLINE, text)
152 152
153 153
154 154 def bbcode_extended(markup):
155 155 parser = bbcode.Parser()
156 156 parser.add_formatter('post', render_reflink, strip=True)
157 157 parser.add_formatter('quote', render_quote, strip=True)
158 158 parser.add_simple_formatter('comment',
159 159 u'<span class="comment">//%(value)s</span>')
160 160 parser.add_simple_formatter('spoiler',
161 161 u'<span class="spoiler">%(value)s</span>')
162 162 parser.add_simple_formatter('s',
163 163 u'<span class="strikethrough">%(value)s</span>')
164 164 parser.add_simple_formatter('code',
165 165 u'<pre><code>%(value)s</pre></code>')
166 166
167 167 text = preparse_text(markup)
168 168 return parser.format(text)
169 169
170 170 formatters = [
171 171 QuotePattern,
172 172 SpoilerPattern,
173 173 ItalicPattern,
174 174 BoldPattern,
175 175 CommentPattern,
176 176 StrikeThroughPattern,
177 177 CodePattern,
178 178 ]
@@ -1,460 +1,467 b''
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 br {
110 110 margin-bottom: .5em;
111 111 }
112 112
113 113 .post-form-w {
114 114 background: #333344;
115 115 border-top: solid 1px #888;
116 116 border-bottom: solid 1px #888;
117 117 color: #fff;
118 118 padding: 10px;
119 119 margin-bottom: 5px;
120 120 margin-top: 5px;
121 121 }
122 122
123 123 .form-row {
124 124 width: 100%;
125 125 }
126 126
127 127 .form-label {
128 128 padding: .25em 1ex .25em 0;
129 129 vertical-align: top;
130 130 }
131 131
132 132 .form-input {
133 133 padding: .25em 0;
134 134 }
135 135
136 136 .form-errors {
137 137 font-weight: bolder;
138 138 vertical-align: middle;
139 139 }
140 140
141 141 .post-form input:not([name="image"]), .post-form textarea {
142 142 background: #333;
143 143 color: #fff;
144 144 border: solid 1px;
145 145 padding: 0;
146 146 font: medium sans-serif;
147 147 width: 100%;
148 148 }
149 149
150 150 .form-submit {
151 151 display: table;
152 152 margin-bottom: 1ex;
153 153 margin-top: 1ex;
154 154 }
155 155
156 156 .form-title {
157 157 font-weight: bold;
158 158 font-size: 2ex;
159 159 margin-bottom: 0.5ex;
160 160 }
161 161
162 162 .post-form input[type="submit"], input[type="submit"] {
163 163 background: #222;
164 164 border: solid 2px #fff;
165 165 color: #fff;
166 166 padding: 0.5ex;
167 167 }
168 168
169 169 input[type="submit"]:hover {
170 170 background: #060;
171 171 }
172 172
173 173 blockquote {
174 174 border-left: solid 2px;
175 175 padding-left: 5px;
176 176 color: #B1FB17;
177 177 margin: 0;
178 178 }
179 179
180 180 .post > .image {
181 181 float: left;
182 182 margin: 0 1ex .5ex 0;
183 183 min-width: 1px;
184 184 text-align: center;
185 185 display: table-row;
186 186 }
187 187
188 188 .post > .metadata {
189 189 clear: left;
190 190 }
191 191
192 192 .get {
193 193 font-weight: bold;
194 194 color: #d55;
195 195 }
196 196
197 197 * {
198 198 text-decoration: none;
199 199 }
200 200
201 201 .dead_post {
202 202 background-color: #442222;
203 203 }
204 204
205 205 .archive_post {
206 206 background-color: #000;
207 207 }
208 208
209 209 .mark_btn {
210 210 border: 1px solid;
211 211 min-width: 2ex;
212 212 padding: 2px 2ex;
213 213 }
214 214
215 215 .mark_btn:hover {
216 216 background: #555;
217 217 }
218 218
219 219 .quote {
220 220 color: #92cf38;
221 221 font-style: italic;
222 222 }
223 223
224 224 .multiquote {
225 225 padding: 3px;
226 226 display: inline-block;
227 227 background: #222;
228 228 border-style: solid;
229 229 border-width: 1px 1px 1px 4px;
230 230 font-size: 0.9em;
231 231 }
232 232
233 233 .spoiler {
234 234 background: white;
235 235 color: white;
236 236 }
237 237
238 238 .spoiler:hover {
239 239 color: black;
240 240 }
241 241
242 242 .comment {
243 243 color: #eb2;
244 244 }
245 245
246 246 a:hover {
247 247 text-decoration: underline;
248 248 }
249 249
250 250 .last-replies {
251 251 margin-left: 3ex;
252 252 margin-right: 3ex;
253 border-left: solid 1px #777;
254 border-right: solid 1px #777;
253 255 }
254 256
255 257 .thread {
256 258 margin-bottom: 3ex;
257 259 margin-top: 1ex;
258 260 }
259 261
260 262 .post:target {
261 263 border: solid 2px white;
262 264 }
263 265
264 266 pre{
265 267 white-space:pre-wrap
266 268 }
267 269
268 270 li {
269 271 list-style-position: inside;
270 272 }
271 273
272 274 .fancybox-skin {
273 275 position: relative;
274 276 background-color: #fff;
275 277 color: #ddd;
276 278 text-shadow: none;
277 279 }
278 280
279 281 .fancybox-image {
280 282 border: 1px solid black;
281 283 }
282 284
283 285 .image-mode-tab {
284 286 background: #444;
285 287 color: #eee;
286 288 margin-top: 5px;
287 289 padding: 5px;
288 290 border-top: 1px solid #888;
289 291 border-bottom: 1px solid #888;
290 292 }
291 293
292 294 .image-mode-tab > label {
293 295 margin: 0 1ex;
294 296 }
295 297
296 298 .image-mode-tab > label > input {
297 299 margin-right: .5ex;
298 300 }
299 301
300 302 #posts-table {
301 303 margin-top: 5px;
302 304 margin-bottom: 5px;
303 305 }
304 306
305 307 .tag_info > h2 {
306 308 margin: 0;
307 309 }
308 310
309 311 .post-info {
310 312 color: #ddd;
311 313 margin-bottom: 1ex;
312 314 }
313 315
314 316 .moderator_info {
315 317 color: #e99d41;
316 318 float: right;
317 319 font-weight: bold;
318 320 }
319 321
320 322 .refmap {
321 323 font-size: 0.9em;
322 324 color: #ccc;
323 325 margin-top: 1em;
324 326 }
325 327
326 328 .fav {
327 329 color: yellow;
328 330 }
329 331
330 332 .not_fav {
331 333 color: #ccc;
332 334 }
333 335
334 336 .role {
335 337 text-decoration: underline;
336 338 }
337 339
338 340 .form-email {
339 341 display: none;
340 342 }
341 343
342 344 .footer {
343 345 margin: 5px;
344 346 }
345 347
346 348 .bar-value {
347 349 background: rgba(50, 55, 164, 0.45);
348 350 font-size: 0.9em;
349 351 height: 1.5em;
350 352 }
351 353
352 354 .bar-bg {
353 355 position: relative;
354 356 border-top: solid 1px #888;
355 357 border-bottom: solid 1px #888;
356 358 margin-top: 5px;
357 359 overflow: hidden;
358 360 }
359 361
360 362 .bar-text {
361 363 padding: 2px;
362 364 position: absolute;
363 365 left: 0;
364 366 top: 0;
365 367 }
366 368
367 369 .page_link {
368 370 background: #444;
369 371 border-top: solid 1px #888;
370 372 border-bottom: solid 1px #888;
371 373 padding: 5px;
372 374 color: #eee;
373 375 font-size: 2ex;
374 376 }
375 377
376 378 .skipped_replies {
377 margin: 5px;
379 padding: 5px;
380 margin-left: 3ex;
381 margin-right: 3ex;
382 border-left: solid 1px #888;
383 border-right: solid 1px #888;
384 background: #000;
378 385 }
379 386
380 387 .current_page {
381 388 border: solid 1px #afdcec;
382 389 padding: 2px;
383 390 }
384 391
385 392 .current_mode {
386 393 font-weight: bold;
387 394 }
388 395
389 396 .gallery_image {
390 397 border: solid 1px;
391 398 padding: 0.5ex;
392 399 margin: 0.5ex;
393 400 text-align: center;
394 401 }
395 402
396 403 code {
397 404 border: dashed 1px #ccc;
398 405 background: #111;
399 406 padding: 2px;
400 407 font-size: 1.2em;
401 408 display: inline-block;
402 409 }
403 410
404 411 pre {
405 412 overflow: auto;
406 413 }
407 414
408 415 .img-full {
409 416 background: #222;
410 417 border: solid 1px white;
411 418 }
412 419
413 420 .tag_item {
414 421 display: inline-block;
415 422 border: 1px dashed #666;
416 423 margin: 0.2ex;
417 424 padding: 0.1ex;
418 425 }
419 426
420 427 #id_models li {
421 428 list-style: none;
422 429 }
423 430
424 431 #id_q {
425 432 margin-left: 1ex;
426 433 }
427 434
428 435 ul {
429 436 padding-left: 0px;
430 437 }
431 438
432 439 .quote-header {
433 440 border-bottom: 2px solid #ddd;
434 441 margin-bottom: 1ex;
435 442 padding-bottom: .5ex;
436 443 color: #ddd;
437 444 font-size: 1.2em;
438 445 }
439 446
440 447 /* Post */
441 448 .post > .message, .post > .image {
442 449 padding-left: 1em;
443 450 }
444 451
445 452 /* Reflink preview */
446 453 .post_preview {
447 454 border-left: 1px solid #777;
448 455 border-right: 1px solid #777;
449 456 }
450 457
451 458 /* Code highlighter */
452 459 .hljs {
453 460 color: #fff;
454 461 background: #000;
455 462 display: inline-block;
456 463 }
457 464
458 465 .hljs, .hljs-subst, .hljs-tag .hljs-title, .lisp .hljs-title, .clojure .hljs-built_in, .nginx .hljs-title {
459 466 color: #fff;
460 467 }
@@ -1,68 +1,66 b''
1 1 {% extends "boards/base.html" %}
2 2
3 3 {% load i18n %}
4 4 {% load cache %}
5 5 {% load static from staticfiles %}
6 6 {% load board %}
7 7
8 8 {% block head %}
9 9 <title>{{ thread.get_opening_post.get_title|striptags|truncatewords:10 }}
10 10 - {{ site_name }}</title>
11 11 {% endblock %}
12 12
13 13 {% block content %}
14 14 {% spaceless %}
15 15 {% get_current_language as LANGUAGE_CODE %}
16 16
17 <script src="{% static 'js/thread.js' %}"></script>
18
19 17 {% cache 600 thread_gallery_view thread.id thread.last_edit_time LANGUAGE_CODE request.get_host %}
20 18 <div class="image-mode-tab">
21 19 <a href="{% url 'thread' thread.get_opening_post.id %}">{% trans 'Normal mode' %}</a>,
22 20 <a class="current_mode" href="{% url 'thread_mode' thread.get_opening_post.id 'gallery' %}">{% trans 'Gallery mode' %}</a>
23 21 </div>
24 22
25 23 <div id="posts-table">
26 24 {% for post in posts %}
27 25 <div class="gallery_image">
28 26 {% with post.get_first_image as image %}
29 27 <div>
30 28 <a
31 29 class="thumb"
32 30 href="{{ image.image.url }}"><img
33 31 src="{{ image.image.url_200x150 }}"
34 32 alt="{{ post.id }}"
35 33 width="{{ image.pre_width }}"
36 34 height="{{ image.pre_height }}"
37 35 data-width="{{ image.width }}"
38 36 data-height="{{ image.height }}"/>
39 37 </a>
40 38 </div>
41 39 <div class="gallery_image_metadata">
42 40 {{ image.width }}x{{ image.height }}
43 41 {% image_actions image.image.url request.get_host %}
44 42 </div>
45 43 {% endwith %}
46 44 </div>
47 45 {% endfor %}
48 46 </div>
49 47 {% endcache %}
50 48
51 49 {% endspaceless %}
52 50 {% endblock %}
53 51
54 52 {% block metapanel %}
55 53
56 54 {% get_current_language as LANGUAGE_CODE %}
57 55
58 56 <span class="metapanel" data-last-update="{{ last_update }}">
59 57 {% cache 600 thread_meta thread.last_edit_time moderator LANGUAGE_CODE %}
60 58 <span id="reply-count">{{ thread.get_reply_count }}</span>/{{ max_replies }}
61 59 {% trans 'messages' %},
62 60 <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}.
63 61 {% trans 'Last update: ' %}{{ thread.last_edit_time }}
64 62 [<a href="rss/">RSS</a>]
65 63 {% endcache %}
66 64 </span>
67 65
68 66 {% endblock %}
@@ -1,288 +1,336 b''
1 1 # coding=utf-8
2 2 import time
3 3 import logging
4 import simplejson
4 5 from django.core.paginator import Paginator
5 6
6 7 from django.test import TestCase
7 8 from django.test.client import Client
8 9 from django.core.urlresolvers import reverse, NoReverseMatch
9 10 from boards.abstracts.settingsmanager import get_settings_manager
10 11
11 12 from boards.models import Post, Tag, Thread, KeyPair
12 13 from boards import urls
13 14 from boards import settings
15 from boards.views.api import api_get_threaddiff
16 from boards.utils import datetime_to_epoch
14 17 import neboard
15 18
16 19 TEST_TAG = 'test_tag'
17 20
18 21 PAGE_404 = 'boards/404.html'
19 22
20 23 TEST_TEXT = 'test text'
21 24
22 25 NEW_THREAD_PAGE = '/'
23 26 THREAD_PAGE_ONE = '/thread/1/'
24 27 THREAD_PAGE = '/thread/'
25 28 TAG_PAGE = '/tag/'
26 29 HTTP_CODE_REDIRECT = 302
27 30 HTTP_CODE_OK = 200
28 31 HTTP_CODE_NOT_FOUND = 404
29 32
30 33 logger = logging.getLogger(__name__)
31 34
32 35
33 36 class PostTests(TestCase):
34 37
35 38 def _create_post(self):
36 39 tag = Tag.objects.create(name=TEST_TAG)
37 40 return Post.objects.create_post(title='title', text='text',
38 41 tags=[tag])
39 42
40 43 def test_post_add(self):
41 44 """Test adding post"""
42 45
43 46 post = self._create_post()
44 47
45 48 self.assertIsNotNone(post, 'No post was created.')
46 49 self.assertEqual(TEST_TAG, post.get_thread().tags.all()[0].name,
47 50 'No tags were added to the post.')
48 51
49 52 def test_delete_post(self):
50 53 """Test post deletion"""
51 54
52 55 post = self._create_post()
53 56 post_id = post.id
54 57
55 58 Post.objects.delete_post(post)
56 59
57 60 self.assertFalse(Post.objects.filter(id=post_id).exists())
58 61
59 62 def test_delete_thread(self):
60 63 """Test thread deletion"""
61 64
62 65 opening_post = self._create_post()
63 66 thread = opening_post.get_thread()
64 67 reply = Post.objects.create_post("", "", thread=thread)
65 68
66 69 thread.delete()
67 70
68 71 self.assertFalse(Post.objects.filter(id=reply.id).exists())
69 72
70 73 def test_post_to_thread(self):
71 74 """Test adding post to a thread"""
72 75
73 76 op = self._create_post()
74 77 post = Post.objects.create_post("", "", thread=op.get_thread())
75 78
76 79 self.assertIsNotNone(post, 'Reply to thread wasn\'t created')
77 80 self.assertEqual(op.get_thread().last_edit_time, post.pub_time,
78 81 'Post\'s create time doesn\'t match thread last edit'
79 82 ' time')
80 83
81 84 def test_delete_posts_by_ip(self):
82 85 """Test deleting posts with the given ip"""
83 86
84 87 post = self._create_post()
85 88 post_id = post.id
86 89
87 90 Post.objects.delete_posts_by_ip('0.0.0.0')
88 91
89 92 self.assertFalse(Post.objects.filter(id=post_id).exists())
90 93
91 94 def test_get_thread(self):
92 95 """Test getting all posts of a thread"""
93 96
94 97 opening_post = self._create_post()
95 98
96 99 for i in range(0, 2):
97 100 Post.objects.create_post('title', 'text',
98 101 thread=opening_post.get_thread())
99 102
100 103 thread = opening_post.get_thread()
101 104
102 105 self.assertEqual(3, thread.replies.count())
103 106
104 107 def test_create_post_with_tag(self):
105 108 """Test adding tag to post"""
106 109
107 110 tag = Tag.objects.create(name='test_tag')
108 111 post = Post.objects.create_post(title='title', text='text', tags=[tag])
109 112
110 113 thread = post.get_thread()
111 114 self.assertIsNotNone(post, 'Post not created')
112 115 self.assertTrue(tag in thread.tags.all(), 'Tag not added to thread')
113 116 self.assertTrue(thread in tag.threads.all(), 'Thread not added to tag')
114 117
115 118 def test_thread_max_count(self):
116 119 """Test deletion of old posts when the max thread count is reached"""
117 120
118 121 for i in range(settings.MAX_THREAD_COUNT + 1):
119 122 self._create_post()
120 123
121 124 self.assertEqual(settings.MAX_THREAD_COUNT,
122 125 len(Thread.objects.filter(archived=False)))
123 126
124 127 def test_pages(self):
125 128 """Test that the thread list is properly split into pages"""
126 129
127 130 for i in range(settings.MAX_THREAD_COUNT):
128 131 self._create_post()
129 132
130 133 all_threads = Thread.objects.filter(archived=False)
131 134
132 135 paginator = Paginator(Thread.objects.filter(archived=False),
133 136 settings.THREADS_PER_PAGE)
134 137 posts_in_second_page = paginator.page(2).object_list
135 138 first_post = posts_in_second_page[0]
136 139
137 140 self.assertEqual(all_threads[settings.THREADS_PER_PAGE].id,
138 141 first_post.id)
139 142
140 143
141 144 class PagesTest(TestCase):
142 145
143 146 def test_404(self):
144 147 """Test receiving error 404 when opening a non-existent page"""
145 148
146 149 tag_name = u'test_tag'
147 150 tag = Tag.objects.create(name=tag_name)
148 151 client = Client()
149 152
150 153 Post.objects.create_post('title', TEST_TEXT, tags=[tag])
151 154
152 155 existing_post_id = Post.objects.all()[0].id
153 156 response_existing = client.get(THREAD_PAGE + str(existing_post_id) +
154 157 '/')
155 158 self.assertEqual(HTTP_CODE_OK, response_existing.status_code,
156 159 u'Cannot open existing thread')
157 160
158 161 response_not_existing = client.get(THREAD_PAGE + str(
159 162 existing_post_id + 1) + '/')
160 163 self.assertEqual(PAGE_404, response_not_existing.templates[0].name,
161 164 u'Not existing thread is opened')
162 165
163 166 response_existing = client.get(TAG_PAGE + tag_name + '/')
164 167 self.assertEqual(HTTP_CODE_OK,
165 168 response_existing.status_code,
166 169 u'Cannot open existing tag')
167 170
168 171 response_not_existing = client.get(TAG_PAGE + u'not_tag' + '/')
169 172 self.assertEqual(PAGE_404,
170 173 response_not_existing.templates[0].name,
171 174 u'Not existing tag is opened')
172 175
173 176 reply_id = Post.objects.create_post('', TEST_TEXT,
174 177 thread=Post.objects.all()[0]
175 178 .get_thread())
176 179 response_not_existing = client.get(THREAD_PAGE + str(
177 180 reply_id) + '/')
178 181 self.assertEqual(PAGE_404,
179 182 response_not_existing.templates[0].name,
180 183 u'Reply is opened as a thread')
181 184
182 185
183 186 class FormTest(TestCase):
184 187 def test_post_validation(self):
185 188 client = Client()
186 189
187 190 valid_tags = u'tag1 tag_2 Ρ‚Π΅Π³_3'
188 191 invalid_tags = u'$%_356 ---'
189 192
190 193 response = client.post(NEW_THREAD_PAGE, {'title': 'test title',
191 194 'text': TEST_TEXT,
192 195 'tags': valid_tags})
193 196 self.assertEqual(response.status_code, HTTP_CODE_REDIRECT,
194 197 msg='Posting new message failed: got code ' +
195 198 str(response.status_code))
196 199
197 200 self.assertEqual(1, Post.objects.count(),
198 201 msg='No posts were created')
199 202
200 203 client.post(NEW_THREAD_PAGE, {'text': TEST_TEXT,
201 204 'tags': invalid_tags})
202 205 self.assertEqual(1, Post.objects.count(), msg='The validation passed '
203 206 'where it should fail')
204 207
205 208 # Change posting delay so we don't have to wait for 30 seconds or more
206 209 old_posting_delay = neboard.settings.POSTING_DELAY
207 210 # Wait fot the posting delay or we won't be able to post
208 211 settings.POSTING_DELAY = 1
209 212 time.sleep(neboard.settings.POSTING_DELAY + 1)
210 213 response = client.post(THREAD_PAGE_ONE, {'text': TEST_TEXT,
211 214 'tags': valid_tags})
212 215 self.assertEqual(HTTP_CODE_REDIRECT, response.status_code,
213 216 msg=u'Posting new message failed: got code ' +
214 217 str(response.status_code))
215 218 # Restore posting delay
216 219 settings.POSTING_DELAY = old_posting_delay
217 220
218 221 self.assertEqual(2, Post.objects.count(),
219 222 msg=u'No posts were created')
220 223
221 224
222 225 class ViewTest(TestCase):
223 226
224 227 def test_all_views(self):
225 228 """
226 229 Try opening all views defined in ulrs.py that don't need additional
227 230 parameters
228 231 """
229 232
230 233 client = Client()
231 234 for url in urls.urlpatterns:
232 235 try:
233 236 view_name = url.name
234 237 logger.debug('Testing view %s' % view_name)
235 238
236 239 try:
237 240 response = client.get(reverse(view_name))
238 241
239 242 self.assertEqual(HTTP_CODE_OK, response.status_code,
240 243 '%s view not opened' % view_name)
241 244 except NoReverseMatch:
242 245 # This view just needs additional arguments
243 246 pass
244 247 except Exception as e:
245 248 self.fail('Got exception %s at %s view' % (e, view_name))
246 249 except AttributeError:
247 250 # This is normal, some views do not have names
248 251 pass
249 252
250 253
251 254 class AbstractTest(TestCase):
252 255 def test_settings_manager(self):
253 256 request = MockRequest()
254 257 settings_manager = get_settings_manager(request)
255 258
256 259 settings_manager.set_setting('test_setting', 'test_value')
257 260 self.assertEqual('test_value', settings_manager.get_setting(
258 261 'test_setting'), u'Setting update failed.')
259 262
260 263
261 264 class MockRequest:
262 265 def __init__(self):
263 266 self.session = dict()
267 self.GET = dict()
268 self.POST = dict()
264 269
265 270
266 271 class KeyTest(TestCase):
267 272 def test_create_key(self):
268 273 key = KeyPair.objects.generate_key('ecdsa')
269 274
270 275 self.assertIsNotNone(key, 'The key was not created.')
271 276
272 277 def test_validation(self):
273 278 key = KeyPair.objects.generate_key(key_type='ecdsa')
274 279 message = 'msg'
275 280 signature = key.sign(message)
276 281 valid = KeyPair.objects.verify(key.public_key, message, signature,
277 282 key_type='ecdsa')
278 283
279 284 self.assertTrue(valid, 'Message verification failed.')
280 285
281 286 def test_primary_constraint(self):
282 287 KeyPair.objects.generate_key(key_type='ecdsa', primary=True)
283 288
284 289 try:
285 290 KeyPair.objects.generate_key(key_type='ecdsa', primary=True)
286 291 self.fail('Exception should be thrown indicating there can be only one primary key.')
287 292 except Exception:
288 293 pass
294
295
296 class ApiTest(TestCase):
297 def test_thread_diff(self):
298 tag = Tag.objects.create(name=TEST_TAG)
299 opening_post = Post.objects.create_post(title='title', text='text',
300 tags=[tag])
301
302 last_edit_time = datetime_to_epoch(opening_post.last_edit_time)
303
304 # Check the exact timestamp post was added
305 empty_response = api_get_threaddiff(MockRequest(),
306 str(opening_post.thread_new.id),
307 str(last_edit_time))
308 diff = simplejson.loads(empty_response.content)
309 self.assertEqual(0, len(diff['added']),
310 'There must be no added posts in the diff.')
311 self.assertEqual(0, len(diff['updated']),
312 'There must be no updated posts in the diff.')
313
314 reply = Post.objects.create_post(title='',
315 text='[post]%d[/post]\ntext' % opening_post.id,
316 thread=opening_post.thread_new)
317
318 # Check the timestamp before post was added
319 response = api_get_threaddiff(MockRequest(),
320 str(opening_post.thread_new.id),
321 str(last_edit_time))
322 diff = simplejson.loads(response.content)
323 self.assertEqual(1, len(diff['added']),
324 'There must be 1 added posts in the diff.')
325 self.assertEqual(1, len(diff['updated']),
326 'There must be 1 updated posts in the diff.')
327
328 empty_response = api_get_threaddiff(MockRequest(),
329 str(opening_post.thread_new.id),
330 str(datetime_to_epoch(reply.last_edit_time)))
331 diff = simplejson.loads(empty_response.content)
332 self.assertEqual(0, len(diff['added']),
333 'There must be no added posts in the diff.')
334 self.assertEqual(0, len(diff['updated']),
335 'There must be no updated posts in the diff.')
336
@@ -1,245 +1,248 b''
1 1 from datetime import datetime
2 2 import json
3 3 import logging
4 4 from django.db import transaction
5 5 from django.http import HttpResponse
6 6 from django.shortcuts import get_object_or_404, render
7 7 from django.template import RequestContext
8 8 from django.utils import timezone
9 9 from django.core import serializers
10 10 from django.template.loader import render_to_string
11 11
12 12 from boards.forms import PostForm, PlainErrorList
13 13 from boards.models import Post, Thread, Tag
14 14 from boards.utils import datetime_to_epoch
15 15 from boards.views.thread import ThreadView
16 16
17 17 __author__ = 'neko259'
18 18
19 19 PARAMETER_TRUNCATED = 'truncated'
20 20 PARAMETER_TAG = 'tag'
21 21 PARAMETER_OFFSET = 'offset'
22 22 PARAMETER_DIFF_TYPE = 'type'
23 23
24 24 DIFF_TYPE_HTML = 'html'
25 25 DIFF_TYPE_JSON = 'json'
26 26
27 27 STATUS_OK = 'ok'
28 28 STATUS_ERROR = 'error'
29 29
30 30 logger = logging.getLogger(__name__)
31 31
32 32
33 33 @transaction.atomic
34 34 def api_get_threaddiff(request, thread_id, last_update_time):
35 35 """
36 36 Gets posts that were changed or added since time
37 37 """
38 38
39 39 thread = get_object_or_404(Post, id=thread_id).get_thread()
40 40
41 filter_time = datetime.fromtimestamp(float(last_update_time) / 1000000,
41 # Add 1 to ensure we don't load the same post over and over
42 last_update_timestamp = float(last_update_time) + 1
43
44 filter_time = datetime.fromtimestamp(last_update_timestamp / 1000000,
42 45 timezone.get_current_timezone())
43 46
44 47 json_data = {
45 48 'added': [],
46 49 'updated': [],
47 50 'last_update': None,
48 51 }
49 52 added_posts = Post.objects.filter(thread_new=thread,
50 53 pub_time__gt=filter_time) \
51 54 .order_by('pub_time')
52 55 updated_posts = Post.objects.filter(thread_new=thread,
53 56 pub_time__lte=filter_time,
54 57 last_edit_time__gt=filter_time)
55 58
56 59 diff_type = DIFF_TYPE_HTML
57 60 if PARAMETER_DIFF_TYPE in request.GET:
58 61 diff_type = request.GET[PARAMETER_DIFF_TYPE]
59 62
60 63 for post in added_posts:
61 64 json_data['added'].append(_get_post_data(post.id, diff_type, request))
62 65 for post in updated_posts:
63 66 json_data['updated'].append(_get_post_data(post.id, diff_type, request))
64 67 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
65 68
66 69 return HttpResponse(content=json.dumps(json_data))
67 70
68 71
69 72 def api_add_post(request, opening_post_id):
70 73 """
71 74 Adds a post and return the JSON response for it
72 75 """
73 76
74 77 opening_post = get_object_or_404(Post, id=opening_post_id)
75 78
76 79 logger.info('Adding post via api...')
77 80
78 81 status = STATUS_OK
79 82 errors = []
80 83
81 84 if request.method == 'POST':
82 85 form = PostForm(request.POST, request.FILES, error_class=PlainErrorList)
83 86 form.session = request.session
84 87
85 88 if form.need_to_ban:
86 89 # Ban user because he is suspected to be a bot
87 90 # _ban_current_user(request)
88 91 status = STATUS_ERROR
89 92 if form.is_valid():
90 93 post = ThreadView().new_post(request, form, opening_post,
91 94 html_response=False)
92 95 if not post:
93 96 status = STATUS_ERROR
94 97 else:
95 98 logger.info('Added post #%d via api.' % post.id)
96 99 else:
97 100 status = STATUS_ERROR
98 101 errors = form.as_json_errors()
99 102
100 103 response = {
101 104 'status': status,
102 105 'errors': errors,
103 106 }
104 107
105 108 return HttpResponse(content=json.dumps(response))
106 109
107 110
108 111 def get_post(request, post_id):
109 112 """
110 113 Gets the html of a post. Used for popups. Post can be truncated if used
111 114 in threads list with 'truncated' get parameter.
112 115 """
113 116
114 117 logger.info('Getting post #%s' % post_id)
115 118
116 119 post = get_object_or_404(Post, id=post_id)
117 120
118 121 context = RequestContext(request)
119 122 context['post'] = post
120 123 if PARAMETER_TRUNCATED in request.GET:
121 124 context[PARAMETER_TRUNCATED] = True
122 125
123 126 return render(request, 'boards/api_post.html', context)
124 127
125 128
126 129 # TODO Test this
127 130 def api_get_threads(request, count):
128 131 """
129 132 Gets the JSON thread opening posts list.
130 133 Parameters that can be used for filtering:
131 134 tag, offset (from which thread to get results)
132 135 """
133 136
134 137 if PARAMETER_TAG in request.GET:
135 138 tag_name = request.GET[PARAMETER_TAG]
136 139 if tag_name is not None:
137 140 tag = get_object_or_404(Tag, name=tag_name)
138 141 threads = tag.threads.filter(archived=False)
139 142 else:
140 143 threads = Thread.objects.filter(archived=False)
141 144
142 145 if PARAMETER_OFFSET in request.GET:
143 146 offset = request.GET[PARAMETER_OFFSET]
144 147 offset = int(offset) if offset is not None else 0
145 148 else:
146 149 offset = 0
147 150
148 151 threads = threads.order_by('-bump_time')
149 152 threads = threads[offset:offset + int(count)]
150 153
151 154 opening_posts = []
152 155 for thread in threads:
153 156 opening_post = thread.get_opening_post()
154 157
155 158 # TODO Add tags, replies and images count
156 159 opening_posts.append(_get_post_data(opening_post.id,
157 160 include_last_update=True))
158 161
159 162 return HttpResponse(content=json.dumps(opening_posts))
160 163
161 164
162 165 # TODO Test this
163 166 def api_get_tags(request):
164 167 """
165 168 Gets all tags or user tags.
166 169 """
167 170
168 171 # TODO Get favorite tags for the given user ID
169 172
170 173 tags = Tag.objects.get_not_empty_tags()
171 174 tag_names = []
172 175 for tag in tags:
173 176 tag_names.append(tag.name)
174 177
175 178 return HttpResponse(content=json.dumps(tag_names))
176 179
177 180
178 181 # TODO The result can be cached by the thread last update time
179 182 # TODO Test this
180 183 def api_get_thread_posts(request, opening_post_id):
181 184 """
182 185 Gets the JSON array of thread posts
183 186 """
184 187
185 188 opening_post = get_object_or_404(Post, id=opening_post_id)
186 189 thread = opening_post.get_thread()
187 190 posts = thread.get_replies()
188 191
189 192 json_data = {
190 193 'posts': [],
191 194 'last_update': None,
192 195 }
193 196 json_post_list = []
194 197
195 198 for post in posts:
196 199 json_post_list.append(_get_post_data(post.id))
197 200 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
198 201 json_data['posts'] = json_post_list
199 202
200 203 return HttpResponse(content=json.dumps(json_data))
201 204
202 205
203 206 def api_get_post(request, post_id):
204 207 """
205 208 Gets the JSON of a post. This can be
206 209 used as and API for external clients.
207 210 """
208 211
209 212 post = get_object_or_404(Post, id=post_id)
210 213
211 214 json = serializers.serialize("json", [post], fields=(
212 215 "pub_time", "_text_rendered", "title", "text", "image",
213 216 "image_width", "image_height", "replies", "tags"
214 217 ))
215 218
216 219 return HttpResponse(content=json)
217 220
218 221
219 222 # TODO Add pub time and replies
220 223 def _get_post_data(post_id, format_type=DIFF_TYPE_JSON, request=None,
221 224 include_last_update=False):
222 225 if format_type == DIFF_TYPE_HTML:
223 226 post = get_object_or_404(Post, id=post_id)
224 227
225 228 context = RequestContext(request)
226 229 context['post'] = post
227 230 if PARAMETER_TRUNCATED in request.GET:
228 231 context[PARAMETER_TRUNCATED] = True
229 232
230 233 return render_to_string('boards/api_post.html', context)
231 234 elif format_type == DIFF_TYPE_JSON:
232 235 post = get_object_or_404(Post, id=post_id)
233 236 post_json = {
234 237 'id': post.id,
235 238 'title': post.title,
236 239 'text': post.text.rendered,
237 240 }
238 241 if post.images.exists():
239 242 post_image = post.get_first_image()
240 243 post_json['image'] = post_image.image.url
241 244 post_json['image_preview'] = post_image.image.url_200x150
242 245 if include_last_update:
243 246 post_json['bump_time'] = datetime_to_epoch(
244 247 post.thread_new.bump_time)
245 248 return post_json
General Comments 0
You need to be logged in to leave comments. Login now