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