##// END OF EJS Templates
Adapt to django-2.0
neko259 -
r1986:0b41439a default
parent child Browse files
Show More
@@ -1,193 +1,193 b''
1 from boards.abstracts.sticker_factory import StickerFactory
1 from django.contrib import admin
2 from django.urls import reverse
3 from django.utils.translation import ugettext_lazy as _
4
5 from boards.models import Post, Tag, Ban, Thread, Banner, Attachment, \
6 KeyPair, GlobalId, TagAlias, STATUS_ACTIVE
2 from boards.models.attachment import FILE_TYPES_IMAGE, AttachmentSticker, \
7 from boards.models.attachment import FILE_TYPES_IMAGE, AttachmentSticker, \
3 StickerPack
8 StickerPack
4 from django.contrib import admin
5 from django.utils.translation import ugettext_lazy as _
6 from django.core.urlresolvers import reverse
7 from boards.models import Post, Tag, Ban, Thread, Banner, Attachment, \
8 KeyPair, GlobalId, TagAlias, STATUS_ACTIVE
9 from boards.models.source import ThreadSource
9 from boards.models.source import ThreadSource
10
10
11
11
12 @admin.register(Post)
12 @admin.register(Post)
13 class PostAdmin(admin.ModelAdmin):
13 class PostAdmin(admin.ModelAdmin):
14
14
15 list_display = ('id', 'title', 'text', 'poster_ip', 'linked_images',
15 list_display = ('id', 'title', 'text', 'poster_ip', 'linked_images',
16 'foreign', 'tags')
16 'foreign', 'tags')
17 list_filter = ('pub_time',)
17 list_filter = ('pub_time',)
18 search_fields = ('id', 'title', 'text', 'poster_ip')
18 search_fields = ('id', 'title', 'text', 'poster_ip')
19 exclude = ('referenced_posts', 'refmap', 'images', 'global_id')
19 exclude = ('referenced_posts', 'refmap', 'images', 'global_id')
20 readonly_fields = ('poster_ip', 'thread', 'linked_images',
20 readonly_fields = ('poster_ip', 'thread', 'linked_images',
21 'attachments', 'uid', 'url', 'pub_time', 'opening', 'linked_global_id',
21 'attachments', 'uid', 'url', 'pub_time', 'opening', 'linked_global_id',
22 'foreign', 'tags')
22 'foreign', 'tags')
23
23
24 def ban_poster(self, request, queryset):
24 def ban_poster(self, request, queryset):
25 bans = 0
25 bans = 0
26 for post in queryset:
26 for post in queryset:
27 poster_ip = post.poster_ip
27 poster_ip = post.poster_ip
28 ban, created = Ban.objects.get_or_create(ip=poster_ip)
28 ban, created = Ban.objects.get_or_create(ip=poster_ip)
29 if created:
29 if created:
30 bans += 1
30 bans += 1
31 self.message_user(request, _('{} posters were banned').format(bans))
31 self.message_user(request, _('{} posters were banned').format(bans))
32
32
33 def ban_latter_with_delete(self, request, queryset):
33 def ban_latter_with_delete(self, request, queryset):
34 bans = 0
34 bans = 0
35 hidden = 0
35 hidden = 0
36 for post in queryset:
36 for post in queryset:
37 poster_ip = post.poster_ip
37 poster_ip = post.poster_ip
38 ban, created = Ban.objects.get_or_create(ip=poster_ip)
38 ban, created = Ban.objects.get_or_create(ip=poster_ip)
39 if created:
39 if created:
40 bans += 1
40 bans += 1
41 posts = Post.objects.filter(poster_ip=poster_ip, id__gte=post.id)
41 posts = Post.objects.filter(poster_ip=poster_ip, id__gte=post.id)
42 hidden += posts.count()
42 hidden += posts.count()
43 posts.delete()
43 posts.delete()
44 self.message_user(request, _('{} posters were banned, {} messages were removed.').format(bans, hidden))
44 self.message_user(request, _('{} posters were banned, {} messages were removed.').format(bans, hidden))
45 ban_latter_with_delete.short_description = _('Ban user and delete posts starting from this one and later')
45 ban_latter_with_delete.short_description = _('Ban user and delete posts starting from this one and later')
46
46
47 def linked_images(self, obj: Post):
47 def linked_images(self, obj: Post):
48 images = obj.attachments.filter(mimetype__in=FILE_TYPES_IMAGE)
48 images = obj.attachments.filter(mimetype__in=FILE_TYPES_IMAGE)
49 image_urls = ['<a href="{}"><img src="{}" /></a>'.format(
49 image_urls = ['<a href="{}"><img src="{}" /></a>'.format(
50 reverse('admin:%s_%s_change' % (image._meta.app_label,
50 reverse('admin:%s_%s_change' % (image._meta.app_label,
51 image._meta.model_name),
51 image._meta.model_name),
52 args=[image.id]), image.get_thumb_url()) for image in images]
52 args=[image.id]), image.get_thumb_url()) for image in images]
53 return ', '.join(image_urls)
53 return ', '.join(image_urls)
54 linked_images.allow_tags = True
54 linked_images.allow_tags = True
55
55
56 def linked_global_id(self, obj: Post):
56 def linked_global_id(self, obj: Post):
57 global_id = obj.global_id
57 global_id = obj.global_id
58 if global_id is not None:
58 if global_id is not None:
59 return '<a href="{}">{}</a>'.format(
59 return '<a href="{}">{}</a>'.format(
60 reverse('admin:%s_%s_change' % (global_id._meta.app_label,
60 reverse('admin:%s_%s_change' % (global_id._meta.app_label,
61 global_id._meta.model_name),
61 global_id._meta.model_name),
62 args=[global_id.id]), str(global_id))
62 args=[global_id.id]), str(global_id))
63 linked_global_id.allow_tags = True
63 linked_global_id.allow_tags = True
64
64
65 def tags(self, obj: Post):
65 def tags(self, obj: Post):
66 return ', '.join([tag.get_name() for tag in obj.get_tags()])
66 return ', '.join([tag.get_name() for tag in obj.get_tags()])
67
67
68 def save_model(self, request, obj, form, change):
68 def save_model(self, request, obj, form, change):
69 obj.save()
69 obj.save()
70 obj.clear_cache()
70 obj.clear_cache()
71
71
72 def foreign(self, obj: Post):
72 def foreign(self, obj: Post):
73 return obj is not None and obj.global_id is not None and\
73 return obj is not None and obj.global_id is not None and\
74 not obj.global_id.is_local()
74 not obj.global_id.is_local()
75
75
76 actions = ['ban_poster', 'ban_latter_with_delete']
76 actions = ['ban_poster', 'ban_latter_with_delete']
77
77
78
78
79 @admin.register(Tag)
79 @admin.register(Tag)
80 class TagAdmin(admin.ModelAdmin):
80 class TagAdmin(admin.ModelAdmin):
81 def thread_count(self, obj: Tag) -> int:
81 def thread_count(self, obj: Tag) -> int:
82 return obj.get_thread_count()
82 return obj.get_thread_count()
83
83
84 def display_children(self, obj: Tag):
84 def display_children(self, obj: Tag):
85 return ', '.join([str(child) for child in obj.get_children().all()])
85 return ', '.join([str(child) for child in obj.get_children().all()])
86
86
87 def name(self, obj: Tag):
87 def name(self, obj: Tag):
88 return obj.get_name()
88 return obj.get_name()
89
89
90 def save_model(self, request, obj, form, change):
90 def save_model(self, request, obj, form, change):
91 super().save_model(request, obj, form, change)
91 super().save_model(request, obj, form, change)
92 for thread in obj.get_threads().all():
92 for thread in obj.get_threads().all():
93 thread.refresh_tags()
93 thread.refresh_tags()
94
94
95 list_display = ('name', 'thread_count', 'display_children')
95 list_display = ('name', 'thread_count', 'display_children')
96 search_fields = ('id',)
96 search_fields = ('id',)
97 readonly_fields = ('name',)
97 readonly_fields = ('name',)
98
98
99
99
100 @admin.register(TagAlias)
100 @admin.register(TagAlias)
101 class TagAliasAdmin(admin.ModelAdmin):
101 class TagAliasAdmin(admin.ModelAdmin):
102 list_display = ('locale', 'name', 'parent')
102 list_display = ('locale', 'name', 'parent')
103 list_filter = ('locale',)
103 list_filter = ('locale',)
104 search_fields = ('name',)
104 search_fields = ('name',)
105
105
106
106
107 @admin.register(Thread)
107 @admin.register(Thread)
108 class ThreadAdmin(admin.ModelAdmin):
108 class ThreadAdmin(admin.ModelAdmin):
109
109
110 def title(self, obj: Thread) -> str:
110 def title(self, obj: Thread) -> str:
111 return obj.get_opening_post().get_title()
111 return obj.get_opening_post().get_title()
112
112
113 def reply_count(self, obj: Thread) -> int:
113 def reply_count(self, obj: Thread) -> int:
114 return obj.get_reply_count()
114 return obj.get_reply_count()
115
115
116 def ip(self, obj: Thread):
116 def ip(self, obj: Thread):
117 return obj.get_opening_post().poster_ip
117 return obj.get_opening_post().poster_ip
118
118
119 def display_tags(self, obj: Thread):
119 def display_tags(self, obj: Thread):
120 return ', '.join([str(tag) for tag in obj.get_tags().all()])
120 return ', '.join([str(tag) for tag in obj.get_tags().all()])
121
121
122 def op(self, obj: Thread):
122 def op(self, obj: Thread):
123 return obj.get_opening_post_id()
123 return obj.get_opening_post_id()
124
124
125 # Save parent tags when editing tags
125 # Save parent tags when editing tags
126 def save_related(self, request, form, formsets, change):
126 def save_related(self, request, form, formsets, change):
127 super().save_related(request, form, formsets, change)
127 super().save_related(request, form, formsets, change)
128 form.instance.refresh_tags()
128 form.instance.refresh_tags()
129
129
130 def save_model(self, request, obj, form, change):
130 def save_model(self, request, obj, form, change):
131 op = obj.get_opening_post()
131 op = obj.get_opening_post()
132 obj.save()
132 obj.save()
133 op.clear_cache()
133 op.clear_cache()
134
134
135 list_display = ('id', 'op', 'title', 'reply_count', 'status', 'ip',
135 list_display = ('id', 'op', 'title', 'reply_count', 'status', 'ip',
136 'display_tags')
136 'display_tags')
137 list_filter = ('bump_time', 'status')
137 list_filter = ('bump_time', 'status')
138 search_fields = ('id', 'title')
138 search_fields = ('id', 'title')
139 filter_horizontal = ('tags',)
139 filter_horizontal = ('tags',)
140
140
141
141
142 @admin.register(KeyPair)
142 @admin.register(KeyPair)
143 class KeyPairAdmin(admin.ModelAdmin):
143 class KeyPairAdmin(admin.ModelAdmin):
144 list_display = ('public_key', 'primary')
144 list_display = ('public_key', 'primary')
145 list_filter = ('primary',)
145 list_filter = ('primary',)
146 search_fields = ('public_key',)
146 search_fields = ('public_key',)
147
147
148
148
149 @admin.register(Ban)
149 @admin.register(Ban)
150 class BanAdmin(admin.ModelAdmin):
150 class BanAdmin(admin.ModelAdmin):
151 list_display = ('ip', 'can_read')
151 list_display = ('ip', 'can_read')
152 list_filter = ('can_read',)
152 list_filter = ('can_read',)
153 search_fields = ('ip',)
153 search_fields = ('ip',)
154
154
155
155
156 @admin.register(Banner)
156 @admin.register(Banner)
157 class BannerAdmin(admin.ModelAdmin):
157 class BannerAdmin(admin.ModelAdmin):
158 list_display = ('title', 'text')
158 list_display = ('title', 'text')
159
159
160
160
161 @admin.register(Attachment)
161 @admin.register(Attachment)
162 class AttachmentAdmin(admin.ModelAdmin):
162 class AttachmentAdmin(admin.ModelAdmin):
163 list_display = ('__str__', 'mimetype', 'file', 'url')
163 list_display = ('__str__', 'mimetype', 'file', 'url')
164
164
165
165
166 @admin.register(AttachmentSticker)
166 @admin.register(AttachmentSticker)
167 class AttachmentStickerAdmin(admin.ModelAdmin):
167 class AttachmentStickerAdmin(admin.ModelAdmin):
168 search_fields = ('name',)
168 search_fields = ('name',)
169
169
170
170
171 @admin.register(StickerPack)
171 @admin.register(StickerPack)
172 class StickerPackAdmin(admin.ModelAdmin):
172 class StickerPackAdmin(admin.ModelAdmin):
173 search_fields = ('name',)
173 search_fields = ('name',)
174
174
175
175
176 @admin.register(GlobalId)
176 @admin.register(GlobalId)
177 class GlobalIdAdmin(admin.ModelAdmin):
177 class GlobalIdAdmin(admin.ModelAdmin):
178 def is_linked(self, obj):
178 def is_linked(self, obj):
179 return Post.objects.filter(global_id=obj).exists()
179 return Post.objects.filter(global_id=obj).exists()
180
180
181 list_display = ('__str__', 'is_linked',)
181 list_display = ('__str__', 'is_linked',)
182 readonly_fields = ('content',)
182 readonly_fields = ('content',)
183
183
184
184
185 @admin.register(ThreadSource)
185 @admin.register(ThreadSource)
186 class ThreadSourceAdmin(admin.ModelAdmin):
186 class ThreadSourceAdmin(admin.ModelAdmin):
187 search_fields = ('name', 'source')
187 search_fields = ('name', 'source')
188
188
189 def formfield_for_foreignkey(self, db_field, request, **kwargs):
189 def formfield_for_foreignkey(self, db_field, request, **kwargs):
190 if db_field.name == 'thread':
190 if db_field.name == 'thread':
191 kwargs['queryset'] = Thread.objects.filter(status=STATUS_ACTIVE)
191 kwargs['queryset'] = Thread.objects.filter(status=STATUS_ACTIVE)
192 return super().formfield_for_foreignkey(db_field, request, **kwargs)
192 return super().formfield_for_foreignkey(db_field, request, **kwargs)
193
193
@@ -1,288 +1,289 b''
1 # coding=utf-8
1 # coding=utf-8
2 from xml import etree
2
3
3 import re
4 import re
4 import random
5 import random
5 import bbcode
6 import bbcode
6
7
7 from urllib.parse import unquote
8 from urllib.parse import unquote
8
9
9 from django.core.exceptions import ObjectDoesNotExist
10 from django.core.exceptions import ObjectDoesNotExist
10 from django.core.urlresolvers import reverse
11 from django.urls import reverse
11
12
12 import boards
13 import boards
13 from boards import settings
14 from boards import settings
14 from neboard.settings import ALLOWED_HOSTS
15 from neboard.settings import ALLOWED_HOSTS
15
16
16
17
17 __author__ = 'neko259'
18 __author__ = 'neko259'
18
19
19
20
20 REFLINK_PATTERN = re.compile(r'^\d+$')
21 REFLINK_PATTERN = re.compile(r'^\d+$')
21 GLOBAL_REFLINK_PATTERN = re.compile(r'(\w+)::([^:]+)::(\d+)')
22 GLOBAL_REFLINK_PATTERN = re.compile(r'(\w+)::([^:]+)::(\d+)')
22 MULTI_NEWLINES_PATTERN = re.compile(r'(\r?\n){2,}')
23 MULTI_NEWLINES_PATTERN = re.compile(r'(\r?\n){2,}')
23 ONE_NEWLINE = '\n'
24 ONE_NEWLINE = '\n'
24 REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?')
25 REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?')
25 LINE_BREAK_HTML = '<div class="br"></div>'
26 LINE_BREAK_HTML = '<div class="br"></div>'
26 SPOILER_SPACE = '&nbsp;'
27 SPOILER_SPACE = '&nbsp;'
27
28
28 MAX_SPOILER_MULTIPLIER = 2
29 MAX_SPOILER_MULTIPLIER = 2
29 MAX_SPOILER_SPACE_COUNT = 20
30 MAX_SPOILER_SPACE_COUNT = 20
30
31
31
32
32 class TextFormatter():
33 class TextFormatter():
33 """
34 """
34 An interface for formatter that can be used in the text format panel
35 An interface for formatter that can be used in the text format panel
35 """
36 """
36
37
37 def __init__(self):
38 def __init__(self):
38 pass
39 pass
39
40
40 name = ''
41 name = ''
41
42
42 # Left and right tags for the button preview
43 # Left and right tags for the button preview
43 preview_left = ''
44 preview_left = ''
44 preview_right = ''
45 preview_right = ''
45
46
46 # Left and right characters for the textarea input
47 # Left and right characters for the textarea input
47 format_left = ''
48 format_left = ''
48 format_right = ''
49 format_right = ''
49
50
50
51
51 class AutolinkPattern():
52 class AutolinkPattern():
52 def handleMatch(self, m):
53 def handleMatch(self, m):
53 link_element = etree.Element('a')
54 link_element = etree.Element('a')
54 href = m.group(2)
55 href = m.group(2)
55 link_element.set('href', href)
56 link_element.set('href', href)
56 link_element.text = href
57 link_element.text = href
57
58
58 return link_element
59 return link_element
59
60
60
61
61 class QuotePattern(TextFormatter):
62 class QuotePattern(TextFormatter):
62 name = '>q'
63 name = '>q'
63 preview_left = '<span class="quote">'
64 preview_left = '<span class="quote">'
64 preview_right = '</span>'
65 preview_right = '</span>'
65
66
66 format_left = '[quote]'
67 format_left = '[quote]'
67 format_right = '[/quote]'
68 format_right = '[/quote]'
68
69
69
70
70 class SpoilerPattern(TextFormatter):
71 class SpoilerPattern(TextFormatter):
71 name = 'spoiler'
72 name = 'spoiler'
72 preview_left = '<span class="spoiler">'
73 preview_left = '<span class="spoiler">'
73 preview_right = '</span>'
74 preview_right = '</span>'
74
75
75 format_left = '[spoiler]'
76 format_left = '[spoiler]'
76 format_right = '[/spoiler]'
77 format_right = '[/spoiler]'
77
78
78
79
79 class CommentPattern(TextFormatter):
80 class CommentPattern(TextFormatter):
80 name = ''
81 name = ''
81 preview_left = '<span class="comment">// '
82 preview_left = '<span class="comment">// '
82 preview_right = '</span>'
83 preview_right = '</span>'
83
84
84 format_left = '[comment]'
85 format_left = '[comment]'
85 format_right = '[/comment]'
86 format_right = '[/comment]'
86
87
87
88
88 # TODO Use <s> tag here
89 # TODO Use <s> tag here
89 class StrikeThroughPattern(TextFormatter):
90 class StrikeThroughPattern(TextFormatter):
90 name = 's'
91 name = 's'
91 preview_left = '<span class="strikethrough">'
92 preview_left = '<span class="strikethrough">'
92 preview_right = '</span>'
93 preview_right = '</span>'
93
94
94 format_left = '[s]'
95 format_left = '[s]'
95 format_right = '[/s]'
96 format_right = '[/s]'
96
97
97
98
98 class ItalicPattern(TextFormatter):
99 class ItalicPattern(TextFormatter):
99 name = 'i'
100 name = 'i'
100 preview_left = '<i>'
101 preview_left = '<i>'
101 preview_right = '</i>'
102 preview_right = '</i>'
102
103
103 format_left = '[i]'
104 format_left = '[i]'
104 format_right = '[/i]'
105 format_right = '[/i]'
105
106
106
107
107 class BoldPattern(TextFormatter):
108 class BoldPattern(TextFormatter):
108 name = 'b'
109 name = 'b'
109 preview_left = '<b>'
110 preview_left = '<b>'
110 preview_right = '</b>'
111 preview_right = '</b>'
111
112
112 format_left = '[b]'
113 format_left = '[b]'
113 format_right = '[/b]'
114 format_right = '[/b]'
114
115
115
116
116 class CodePattern(TextFormatter):
117 class CodePattern(TextFormatter):
117 name = 'code'
118 name = 'code'
118 preview_left = '<code>'
119 preview_left = '<code>'
119 preview_right = '</code>'
120 preview_right = '</code>'
120
121
121 format_left = '[code]'
122 format_left = '[code]'
122 format_right = '[/code]'
123 format_right = '[/code]'
123
124
124
125
125 class HintPattern(TextFormatter):
126 class HintPattern(TextFormatter):
126 name = 'hint'
127 name = 'hint'
127 preview_left = '<span class="hint">'
128 preview_left = '<span class="hint">'
128 preview_right = '</span>'
129 preview_right = '</span>'
129
130
130 format_left = '[hint]'
131 format_left = '[hint]'
131 format_right = '[/hint]'
132 format_right = '[/hint]'
132
133
133
134
134 def render_reflink(tag_name, value, options, parent, context):
135 def render_reflink(tag_name, value, options, parent, context):
135 result = '>>%s' % value
136 result = '>>%s' % value
136
137
137 post = None
138 post = None
138 if REFLINK_PATTERN.match(value):
139 if REFLINK_PATTERN.match(value):
139 post_id = int(value)
140 post_id = int(value)
140
141
141 try:
142 try:
142 post = boards.models.Post.objects.get(id=post_id)
143 post = boards.models.Post.objects.get(id=post_id)
143
144
144 except ObjectDoesNotExist:
145 except ObjectDoesNotExist:
145 pass
146 pass
146 elif GLOBAL_REFLINK_PATTERN.match(value):
147 elif GLOBAL_REFLINK_PATTERN.match(value):
147 match = GLOBAL_REFLINK_PATTERN.search(value)
148 match = GLOBAL_REFLINK_PATTERN.search(value)
148 try:
149 try:
149 global_id = boards.models.GlobalId.objects.get(
150 global_id = boards.models.GlobalId.objects.get(
150 key_type=match.group(1), key=match.group(2),
151 key_type=match.group(1), key=match.group(2),
151 local_id=match.group(3))
152 local_id=match.group(3))
152 post = global_id.post
153 post = global_id.post
153 except ObjectDoesNotExist:
154 except ObjectDoesNotExist:
154 pass
155 pass
155
156
156 if post is not None:
157 if post is not None:
157 result = post.get_link_view()
158 result = post.get_link_view()
158
159
159 return result
160 return result
160
161
161
162
162 def render_quote(tag_name, value, options, parent, context):
163 def render_quote(tag_name, value, options, parent, context):
163 source = ''
164 source = ''
164 if 'source' in options:
165 if 'source' in options:
165 source = options['source']
166 source = options['source']
166 elif 'quote' in options:
167 elif 'quote' in options:
167 source = options['quote']
168 source = options['quote']
168
169
169 if source:
170 if source:
170 result = '<div class="multiquote"><div class="quote-header">%s</div><div class="quote-text">%s</div></div>' % (source, value)
171 result = '<div class="multiquote"><div class="quote-header">%s</div><div class="quote-text">%s</div></div>' % (source, value)
171 else:
172 else:
172 # Insert a ">" at the start of every line
173 # Insert a ">" at the start of every line
173 result = '<span class="quote">&gt;{}</span>'.format(
174 result = '<span class="quote">&gt;{}</span>'.format(
174 value.replace(LINE_BREAK_HTML,
175 value.replace(LINE_BREAK_HTML,
175 '{}&gt;'.format(LINE_BREAK_HTML)))
176 '{}&gt;'.format(LINE_BREAK_HTML)))
176
177
177 return result
178 return result
178
179
179
180
180 def render_hint(tag_name, value, options, parent, context):
181 def render_hint(tag_name, value, options, parent, context):
181 if 'hint' in options:
182 if 'hint' in options:
182 hint = options['hint']
183 hint = options['hint']
183 result = '<span class="hint" title="{}">{}</span>'.format(hint, value)
184 result = '<span class="hint" title="{}">{}</span>'.format(hint, value)
184 else:
185 else:
185 result = value
186 result = value
186 return result
187 return result
187
188
188
189
189 def render_notification(tag_name, value, options, parent, content):
190 def render_notification(tag_name, value, options, parent, content):
190 username = value.lower()
191 username = value.lower()
191
192
192 return '<a href="{}" class="user-cast">@{}</a>'.format(
193 return '<a href="{}" class="user-cast">@{}</a>'.format(
193 reverse('notifications', kwargs={'username': username}), username)
194 reverse('notifications', kwargs={'username': username}), username)
194
195
195
196
196 def render_tag(tag_name, value, options, parent, context):
197 def render_tag(tag_name, value, options, parent, context):
197 tag_name = value.lower()
198 tag_name = value.lower()
198
199
199 try:
200 try:
200 url = boards.models.Tag.objects.get(name=tag_name).get_view()
201 url = boards.models.Tag.objects.get(name=tag_name).get_view()
201 except ObjectDoesNotExist:
202 except ObjectDoesNotExist:
202 url = tag_name
203 url = tag_name
203
204
204 return url
205 return url
205
206
206
207
207 def render_spoiler(tag_name, value, options, parent, context):
208 def render_spoiler(tag_name, value, options, parent, context):
208 if settings.get_bool('Forms', 'AdditionalSpoilerSpaces'):
209 if settings.get_bool('Forms', 'AdditionalSpoilerSpaces'):
209 text_len = len(value)
210 text_len = len(value)
210 space_count = min(random.randint(0, text_len * MAX_SPOILER_MULTIPLIER),
211 space_count = min(random.randint(0, text_len * MAX_SPOILER_MULTIPLIER),
211 MAX_SPOILER_SPACE_COUNT)
212 MAX_SPOILER_SPACE_COUNT)
212 side_spaces = SPOILER_SPACE * (space_count // 2)
213 side_spaces = SPOILER_SPACE * (space_count // 2)
213 else:
214 else:
214 side_spaces = ''
215 side_spaces = ''
215 return '<span class="spoiler">{}{}{}</span>'.format(side_spaces,
216 return '<span class="spoiler">{}{}{}</span>'.format(side_spaces,
216 value, side_spaces)
217 value, side_spaces)
217
218
218
219
219 formatters = [
220 formatters = [
220 QuotePattern,
221 QuotePattern,
221 SpoilerPattern,
222 SpoilerPattern,
222 ItalicPattern,
223 ItalicPattern,
223 BoldPattern,
224 BoldPattern,
224 CommentPattern,
225 CommentPattern,
225 StrikeThroughPattern,
226 StrikeThroughPattern,
226 CodePattern,
227 CodePattern,
227 HintPattern,
228 HintPattern,
228 ]
229 ]
229
230
230
231
231 PREPARSE_PATTERNS = {
232 PREPARSE_PATTERNS = {
232 r'(?<!>)>>(\d+)': r'[post]\1[/post]', # Reflink ">>123"
233 r'(?<!>)>>(\d+)': r'[post]\1[/post]', # Reflink ">>123"
233 r'^>([^>].+)': r'[quote]\1[/quote]', # Quote ">text"
234 r'^>([^>].+)': r'[quote]\1[/quote]', # Quote ">text"
234 r'^//\s?(.+)': r'[comment]\1[/comment]', # Comment "//text"
235 r'^//\s?(.+)': r'[comment]\1[/comment]', # Comment "//text"
235 r'\B@(\w+)': r'[user]\1[/user]', # User notification "@user"
236 r'\B@(\w+)': r'[user]\1[/user]', # User notification "@user"
236 }
237 }
237
238
238 for hostname in ALLOWED_HOSTS:
239 for hostname in ALLOWED_HOSTS:
239 if hostname != '*':
240 if hostname != '*':
240 PREPARSE_PATTERNS[r'https?://{}/thread/\d+/#(\d+)/?'.format(hostname)] = r'[post]\1[/post]'
241 PREPARSE_PATTERNS[r'https?://{}/thread/\d+/#(\d+)/?'.format(hostname)] = r'[post]\1[/post]'
241 PREPARSE_PATTERNS[r'https?://{}/thread/(\d+)/?'.format(hostname)] = r'[post]\1[/post]'
242 PREPARSE_PATTERNS[r'https?://{}/thread/(\d+)/?'.format(hostname)] = r'[post]\1[/post]'
242
243
243
244
244 class Parser:
245 class Parser:
245 def __init__(self):
246 def __init__(self):
246 # The newline hack is added because br's margin does not work in all
247 # The newline hack is added because br's margin does not work in all
247 # browsers except firefox, when the div's does.
248 # browsers except firefox, when the div's does.
248 self.parser = bbcode.Parser(newline=LINE_BREAK_HTML)
249 self.parser = bbcode.Parser(newline=LINE_BREAK_HTML)
249
250
250 self.parser.add_formatter('post', render_reflink, strip=True)
251 self.parser.add_formatter('post', render_reflink, strip=True)
251 self.parser.add_formatter('quote', render_quote, strip=True)
252 self.parser.add_formatter('quote', render_quote, strip=True)
252 self.parser.add_formatter('hint', render_hint, strip=True)
253 self.parser.add_formatter('hint', render_hint, strip=True)
253 self.parser.add_formatter('user', render_notification, strip=True)
254 self.parser.add_formatter('user', render_notification, strip=True)
254 self.parser.add_formatter('tag', render_tag, strip=True)
255 self.parser.add_formatter('tag', render_tag, strip=True)
255 self.parser.add_formatter('spoiler', render_spoiler, strip=True)
256 self.parser.add_formatter('spoiler', render_spoiler, strip=True)
256 self.parser.add_simple_formatter(
257 self.parser.add_simple_formatter(
257 'comment', '<span class="comment">// %(value)s</span>', strip=True)
258 'comment', '<span class="comment">// %(value)s</span>', strip=True)
258 self.parser.add_simple_formatter(
259 self.parser.add_simple_formatter(
259 's', '<span class="strikethrough">%(value)s</span>')
260 's', '<span class="strikethrough">%(value)s</span>')
260 self.parser.add_simple_formatter('code',
261 self.parser.add_simple_formatter('code',
261 '<pre><code>%(value)s</pre></code>',
262 '<pre><code>%(value)s</pre></code>',
262 render_embedded=False,
263 render_embedded=False,
263 escape_html=True,
264 escape_html=True,
264 replace_links=False,
265 replace_links=False,
265 replace_cosmetic=False)
266 replace_cosmetic=False)
266
267
267 def preparse(self, text):
268 def preparse(self, text):
268 """
269 """
269 Performs manual parsing before the bbcode parser is used.
270 Performs manual parsing before the bbcode parser is used.
270 Preparsed text is saved as raw and the text before preparsing is lost.
271 Preparsed text is saved as raw and the text before preparsing is lost.
271 """
272 """
272 new_text = MULTI_NEWLINES_PATTERN.sub(ONE_NEWLINE, text)
273 new_text = MULTI_NEWLINES_PATTERN.sub(ONE_NEWLINE, text)
273
274
274 for key, value in PREPARSE_PATTERNS.items():
275 for key, value in PREPARSE_PATTERNS.items():
275 new_text = re.sub(key, value, new_text, flags=re.MULTILINE)
276 new_text = re.sub(key, value, new_text, flags=re.MULTILINE)
276
277
277 for link in REGEX_URL.findall(text):
278 for link in REGEX_URL.findall(text):
278 new_text = new_text.replace(link, unquote(link))
279 new_text = new_text.replace(link, unquote(link))
279
280
280 return new_text
281 return new_text
281
282
282 def parse(self, text):
283 def parse(self, text):
283 return self.parser.format(text)
284 return self.parser.format(text)
284
285
285
286
286 parser = Parser()
287 parser = Parser()
287 def get_parser():
288 def get_parser():
288 return parser
289 return parser
@@ -1,44 +1,54 b''
1 import pytz
1 import pytz
2
2
3 from django.shortcuts import redirect
3 from django.shortcuts import redirect
4 from django.utils import timezone
4 from django.utils import timezone
5
5
6 from boards import utils
6 from boards import utils
7 from boards.models import Ban
7 from boards.models import Ban
8
8
9 SESSION_TIMEZONE = 'django_timezone'
9 SESSION_TIMEZONE = 'django_timezone'
10
10
11 RESPONSE_CONTENT_TYPE = 'Content-Type'
11 RESPONSE_CONTENT_TYPE = 'Content-Type'
12
12
13 TYPE_HTML = 'text/html'
13 TYPE_HTML = 'text/html'
14
14
15
15
16 class BanMiddleware:
16 class BanMiddleware:
17 """
17 """
18 This is run before showing the thread. Banned users don't need to see
18 This is run before showing the thread. Banned users don't need to see
19 anything
19 anything
20 """
20 """
21
21
22 def __init__(self):
22 def __init__(self, get_response):
23 pass
23 self.get_response = get_response
24
24
25 def process_view(self, request, view_func, view_args, view_kwargs):
25 def __call__(self, request):
26 response = self.get_response(request)
26
27
27 if request.path != '/banned/':
28 if request.path != '/banned/':
28 ip = utils.get_client_ip(request)
29 ip = utils.get_client_ip(request)
29 bans = Ban.objects.filter(ip=ip)
30 bans = Ban.objects.filter(ip=ip)
30
31
31 if bans.exists():
32 if bans.exists():
32 ban = bans[0]
33 ban = bans[0]
33 if not ban.can_read:
34 if not ban.can_read:
34 return redirect('banned')
35 return redirect('banned')
35
36
37 return response
38
36
39
37 class TimezoneMiddleware(object):
40 class TimezoneMiddleware(object):
38 def process_request(self, request):
41 def __init__(self, get_response):
42 self.get_response = get_response
43
44 def __call__(self, request):
45 response = self.get_response(request)
46
39 tzname = request.session.get(SESSION_TIMEZONE)
47 tzname = request.session.get(SESSION_TIMEZONE)
40 if tzname:
48 if tzname:
41 timezone.activate(pytz.timezone(tzname))
49 timezone.activate(pytz.timezone(tzname))
42 else:
50 else:
43 timezone.deactivate()
51 timezone.deactivate()
44
52
53 return response
54
@@ -1,112 +1,112 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 from __future__ import unicode_literals
2 from __future__ import unicode_literals
3
3
4 from django.db import models, migrations
4 from django.db import models, migrations
5 import boards.models.base
5 import boards.models.base
6 import boards.thumbs
6 import boards.thumbs
7
7
8
8
9 class Migration(migrations.Migration):
9 class Migration(migrations.Migration):
10
10
11 dependencies = [
11 dependencies = [
12 ]
12 ]
13
13
14 operations = [
14 operations = [
15 migrations.CreateModel(
15 migrations.CreateModel(
16 name='Ban',
16 name='Ban',
17 fields=[
17 fields=[
18 ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')),
18 ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')),
19 ('ip', models.GenericIPAddressField()),
19 ('ip', models.GenericIPAddressField()),
20 ('reason', models.CharField(max_length=200, default='Auto')),
20 ('reason', models.CharField(max_length=200, default='Auto')),
21 ('can_read', models.BooleanField(default=True)),
21 ('can_read', models.BooleanField(default=True)),
22 ],
22 ],
23 options={
23 options={
24 },
24 },
25 bases=(models.Model,),
25 bases=(models.Model,),
26 ),
26 ),
27 migrations.CreateModel(
27 migrations.CreateModel(
28 name='Post',
28 name='Post',
29 fields=[
29 fields=[
30 ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')),
30 ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')),
31 ('title', models.CharField(max_length=200)),
31 ('title', models.CharField(max_length=200)),
32 ('pub_time', models.DateTimeField()),
32 ('pub_time', models.DateTimeField()),
33 ('text', models.TextField(null=True, blank=True)),
33 ('text', models.TextField(null=True, blank=True)),
34 ('text_markup_type', models.CharField(choices=[('', '--'), ('bbcode', 'bbcode')], max_length=30, default='bbcode')),
34 ('text_markup_type', models.CharField(choices=[('', '--'), ('bbcode', 'bbcode')], max_length=30, default='bbcode')),
35 ('poster_ip', models.GenericIPAddressField()),
35 ('poster_ip', models.GenericIPAddressField()),
36 ('_text_rendered', models.TextField(editable=False)),
36 ('_text_rendered', models.TextField(editable=False)),
37 ('poster_user_agent', models.TextField()),
37 ('poster_user_agent', models.TextField()),
38 ('last_edit_time', models.DateTimeField()),
38 ('last_edit_time', models.DateTimeField()),
39 ('refmap', models.TextField(null=True, blank=True)),
39 ('refmap', models.TextField(null=True, blank=True)),
40 ],
40 ],
41 options={
41 options={
42 'ordering': ('id',),
42 'ordering': ('id',),
43 },
43 },
44 bases=(models.Model, boards.models.base.Viewable),
44 bases=(models.Model, boards.models.base.Viewable),
45 ),
45 ),
46 migrations.CreateModel(
46 migrations.CreateModel(
47 name='PostImage',
47 name='PostImage',
48 fields=[
48 fields=[
49 ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')),
49 ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')),
50 ('width', models.IntegerField(default=0)),
50 ('width', models.IntegerField(default=0)),
51 ('height', models.IntegerField(default=0)),
51 ('height', models.IntegerField(default=0)),
52 ('pre_width', models.IntegerField(default=0)),
52 ('pre_width', models.IntegerField(default=0)),
53 ('pre_height', models.IntegerField(default=0)),
53 ('pre_height', models.IntegerField(default=0)),
54 ('image', boards.thumbs.ImageWithThumbsField(height_field='height', width_field='width', blank=True)),
54 ('image', boards.thumbs.ImageWithThumbsField(height_field='height', width_field='width', blank=True)),
55 ('hash', models.CharField(max_length=36)),
55 ('hash', models.CharField(max_length=36)),
56 ],
56 ],
57 options={
57 options={
58 'ordering': ('id',),
58 'ordering': ('id',),
59 },
59 },
60 bases=(models.Model,),
60 bases=(models.Model,),
61 ),
61 ),
62 migrations.CreateModel(
62 migrations.CreateModel(
63 name='Tag',
63 name='Tag',
64 fields=[
64 fields=[
65 ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')),
65 ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')),
66 ('name', models.CharField(db_index=True, max_length=100)),
66 ('name', models.CharField(db_index=True, max_length=100)),
67 ],
67 ],
68 options={
68 options={
69 'ordering': ('name',),
69 'ordering': ('name',),
70 },
70 },
71 bases=(models.Model, boards.models.base.Viewable),
71 bases=(models.Model, boards.models.base.Viewable),
72 ),
72 ),
73 migrations.CreateModel(
73 migrations.CreateModel(
74 name='Thread',
74 name='Thread',
75 fields=[
75 fields=[
76 ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')),
76 ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')),
77 ('bump_time', models.DateTimeField()),
77 ('bump_time', models.DateTimeField()),
78 ('last_edit_time', models.DateTimeField()),
78 ('last_edit_time', models.DateTimeField()),
79 ('archived', models.BooleanField(default=False)),
79 ('archived', models.BooleanField(default=False)),
80 ('bumpable', models.BooleanField(default=True)),
80 ('bumpable', models.BooleanField(default=True)),
81 ('replies', models.ManyToManyField(null=True, related_name='tre+', to='boards.Post', blank=True)),
81 ('replies', models.ManyToManyField(null=True, related_name='tre+', to='boards.Post', blank=True)),
82 ('tags', models.ManyToManyField(to='boards.Tag')),
82 ('tags', models.ManyToManyField(to='boards.Tag')),
83 ],
83 ],
84 options={
84 options={
85 },
85 },
86 bases=(models.Model,),
86 bases=(models.Model,),
87 ),
87 ),
88 migrations.AddField(
88 migrations.AddField(
89 model_name='tag',
89 model_name='tag',
90 name='threads',
90 name='threads',
91 field=models.ManyToManyField(null=True, related_name='tag+', to='boards.Thread', blank=True),
91 field=models.ManyToManyField(null=True, related_name='tag+', to='boards.Thread', blank=True),
92 preserve_default=True,
92 preserve_default=True,
93 ),
93 ),
94 migrations.AddField(
94 migrations.AddField(
95 model_name='post',
95 model_name='post',
96 name='images',
96 name='images',
97 field=models.ManyToManyField(null=True, db_index=True, related_name='ip+', to='boards.PostImage', blank=True),
97 field=models.ManyToManyField(null=True, db_index=True, related_name='ip+', to='boards.PostImage', blank=True),
98 preserve_default=True,
98 preserve_default=True,
99 ),
99 ),
100 migrations.AddField(
100 migrations.AddField(
101 model_name='post',
101 model_name='post',
102 name='referenced_posts',
102 name='referenced_posts',
103 field=models.ManyToManyField(null=True, db_index=True, related_name='rfp+', to='boards.Post', blank=True),
103 field=models.ManyToManyField(null=True, db_index=True, related_name='rfp+', to='boards.Post', blank=True),
104 preserve_default=True,
104 preserve_default=True,
105 ),
105 ),
106 migrations.AddField(
106 migrations.AddField(
107 model_name='post',
107 model_name='post',
108 name='thread_new',
108 name='thread_new',
109 field=models.ForeignKey(null=True, default=None, to='boards.Thread'),
109 field=models.ForeignKey(on_delete=models.CASCADE, null=True, default=None, to='boards.Thread'),
110 preserve_default=True,
110 preserve_default=True,
111 ),
111 ),
112 ]
112 ]
@@ -1,28 +1,28 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 from __future__ import unicode_literals
2 from __future__ import unicode_literals
3
3
4 from django.db import models, migrations
4 from django.db import models, migrations
5
5
6
6
7 class Migration(migrations.Migration):
7 class Migration(migrations.Migration):
8
8
9 def first_thread_to_thread(apps, schema_editor):
9 def first_thread_to_thread(apps, schema_editor):
10 Post = apps.get_model('boards', 'Post')
10 Post = apps.get_model('boards', 'Post')
11 for post in Post.objects.all():
11 for post in Post.objects.all():
12 post.thread = post.threads.first()
12 post.thread = post.threads.first()
13 post.save(update_fields=['thread'])
13 post.save(update_fields=['thread'])
14
14
15
15
16 dependencies = [
16 dependencies = [
17 ('boards', '0008_auto_20150205_1304'),
17 ('boards', '0008_auto_20150205_1304'),
18 ]
18 ]
19
19
20 operations = [
20 operations = [
21 migrations.AddField(
21 migrations.AddField(
22 model_name='post',
22 model_name='post',
23 name='thread',
23 name='thread',
24 field=models.ForeignKey(related_name='pt+', to='boards.Thread', default=None, null=True),
24 field=models.ForeignKey(on_delete=models.CASCADE, related_name='pt+', to='boards.Thread', default=None, null=True),
25 preserve_default=False,
25 preserve_default=False,
26 ),
26 ),
27 migrations.RunPython(first_thread_to_thread),
27 migrations.RunPython(first_thread_to_thread),
28 ]
28 ]
@@ -1,44 +1,44 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 from __future__ import unicode_literals
2 from __future__ import unicode_literals
3
3
4 from django.db import models, migrations
4 from django.db import models, migrations
5
5
6
6
7 class Migration(migrations.Migration):
7 class Migration(migrations.Migration):
8
8
9 def clean_duplicate_tags(apps, schema_editor):
9 def clean_duplicate_tags(apps, schema_editor):
10 Tag = apps.get_model('boards', 'Tag')
10 Tag = apps.get_model('boards', 'Tag')
11 for tag in Tag.objects.all():
11 for tag in Tag.objects.all():
12 tags_with_same_name = Tag.objects.filter(name=tag.name).all()
12 tags_with_same_name = Tag.objects.filter(name=tag.name).all()
13 if len(tags_with_same_name) > 1:
13 if len(tags_with_same_name) > 1:
14 for tag_duplicate in tags_with_same_name[1:]:
14 for tag_duplicate in tags_with_same_name[1:]:
15 threads = tag_duplicate.thread_set.all()
15 threads = tag_duplicate.thread_set.all()
16 for thread in threads:
16 for thread in threads:
17 thread.tags.add(tag)
17 thread.tags.add(tag)
18 tag_duplicate.delete()
18 tag_duplicate.delete()
19
19
20 dependencies = [
20 dependencies = [
21 ('boards', '0009_post_thread'),
21 ('boards', '0009_post_thread'),
22 ]
22 ]
23
23
24 operations = [
24 operations = [
25 migrations.AlterField(
25 migrations.AlterField(
26 model_name='post',
26 model_name='post',
27 name='thread',
27 name='thread',
28 field=models.ForeignKey(to='boards.Thread', related_name='pt+'),
28 field=models.ForeignKey(on_delete=models.CASCADE, to='boards.Thread', related_name='pt+'),
29 preserve_default=True,
29 preserve_default=True,
30 ),
30 ),
31 migrations.RunPython(clean_duplicate_tags),
31 migrations.RunPython(clean_duplicate_tags),
32 migrations.AlterField(
32 migrations.AlterField(
33 model_name='tag',
33 model_name='tag',
34 name='name',
34 name='name',
35 field=models.CharField(db_index=True, unique=True, max_length=100),
35 field=models.CharField(db_index=True, unique=True, max_length=100),
36 preserve_default=True,
36 preserve_default=True,
37 ),
37 ),
38 migrations.AlterField(
38 migrations.AlterField(
39 model_name='tag',
39 model_name='tag',
40 name='required',
40 name='required',
41 field=models.BooleanField(db_index=True, default=False),
41 field=models.BooleanField(db_index=True, default=False),
42 preserve_default=True,
42 preserve_default=True,
43 ),
43 ),
44 ]
44 ]
@@ -1,25 +1,25 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 from __future__ import unicode_literals
2 from __future__ import unicode_literals
3
3
4 from django.db import models, migrations
4 from django.db import models, migrations
5
5
6
6
7 class Migration(migrations.Migration):
7 class Migration(migrations.Migration):
8
8
9 dependencies = [
9 dependencies = [
10 ('boards', '0010_auto_20150208_1451'),
10 ('boards', '0010_auto_20150208_1451'),
11 ]
11 ]
12
12
13 operations = [
13 operations = [
14 migrations.CreateModel(
14 migrations.CreateModel(
15 name='Notification',
15 name='Notification',
16 fields=[
16 fields=[
17 ('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)),
17 ('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)),
18 ('name', models.TextField()),
18 ('name', models.TextField()),
19 ('post', models.ForeignKey(to='boards.Post')),
19 ('post', models.ForeignKey(on_delete=models.CASCADE, to='boards.Post')),
20 ],
20 ],
21 options={
21 options={
22 },
22 },
23 bases=(models.Model,),
23 bases=(models.Model,),
24 ),
24 ),
25 ]
25 ]
@@ -1,33 +1,33 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 from __future__ import unicode_literals
2 from __future__ import unicode_literals
3 from django.core.urlresolvers import reverse
3 from django.urls import reverse
4
4
5 from django.db import models, migrations
5 from django.db import models, migrations
6
6
7
7
8 class Migration(migrations.Migration):
8 class Migration(migrations.Migration):
9
9
10 def update_urls(apps, schema_editor):
10 def update_urls(apps, schema_editor):
11 Post = apps.get_model('boards', 'Post')
11 Post = apps.get_model('boards', 'Post')
12 for post in Post.objects.all():
12 for post in Post.objects.all():
13 thread = post.thread
13 thread = post.thread
14 opening_id = Post.objects.filter(threads__in=[thread]).order_by('pub_time').first().id
14 opening_id = Post.objects.filter(threads__in=[thread]).order_by('pub_time').first().id
15 post_url = reverse('thread', kwargs={'post_id': opening_id})
15 post_url = reverse('thread', kwargs={'post_id': opening_id})
16 if post.id != opening_id:
16 if post.id != opening_id:
17 post_url += '#' + str(post.id)
17 post_url += '#' + str(post.id)
18 post.url = post_url
18 post.url = post_url
19 post.save(update_fields=['url'])
19 post.save(update_fields=['url'])
20
20
21 dependencies = [
21 dependencies = [
22 ('boards', '0013_auto_20150408_1210'),
22 ('boards', '0013_auto_20150408_1210'),
23 ]
23 ]
24
24
25 operations = [
25 operations = [
26 migrations.AddField(
26 migrations.AddField(
27 model_name='post',
27 model_name='post',
28 name='url',
28 name='url',
29 field=models.TextField(default=''),
29 field=models.TextField(default=''),
30 preserve_default=False,
30 preserve_default=False,
31 ),
31 ),
32 migrations.RunPython(update_urls),
32 migrations.RunPython(update_urls),
33 ]
33 ]
@@ -1,23 +1,23 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 from __future__ import unicode_literals
2 from __future__ import unicode_literals
3
3
4 from django.db import models, migrations
4 from django.db import models, migrations
5
5
6
6
7 class Migration(migrations.Migration):
7 class Migration(migrations.Migration):
8
8
9 dependencies = [
9 dependencies = [
10 ('boards', '0017_auto_20150503_1847'),
10 ('boards', '0017_auto_20150503_1847'),
11 ]
11 ]
12
12
13 operations = [
13 operations = [
14 migrations.CreateModel(
14 migrations.CreateModel(
15 name='Banner',
15 name='Banner',
16 fields=[
16 fields=[
17 ('id', models.AutoField(serialize=False, primary_key=True, verbose_name='ID', auto_created=True)),
17 ('id', models.AutoField(serialize=False, primary_key=True, verbose_name='ID', auto_created=True)),
18 ('title', models.TextField()),
18 ('title', models.TextField()),
19 ('text', models.TextField()),
19 ('text', models.TextField()),
20 ('post', models.ForeignKey(to='boards.Post')),
20 ('post', models.ForeignKey(on_delete=models.CASCADE, to='boards.Post')),
21 ],
21 ],
22 ),
22 ),
23 ]
23 ]
@@ -1,48 +1,48 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 from __future__ import unicode_literals
2 from __future__ import unicode_literals
3
3
4 from django.db import models, migrations
4 from django.db import models, migrations
5
5
6
6
7 class Migration(migrations.Migration):
7 class Migration(migrations.Migration):
8
8
9 dependencies = [
9 dependencies = [
10 ('boards', '0025_auto_20150825_2049'),
10 ('boards', '0025_auto_20150825_2049'),
11 ]
11 ]
12
12
13 operations = [
13 operations = [
14 migrations.CreateModel(
14 migrations.CreateModel(
15 name='GlobalId',
15 name='GlobalId',
16 fields=[
16 fields=[
17 ('id', models.AutoField(serialize=False, verbose_name='ID', primary_key=True, auto_created=True)),
17 ('id', models.AutoField(serialize=False, verbose_name='ID', primary_key=True, auto_created=True)),
18 ('key', models.TextField()),
18 ('key', models.TextField()),
19 ('key_type', models.TextField()),
19 ('key_type', models.TextField()),
20 ('local_id', models.IntegerField()),
20 ('local_id', models.IntegerField()),
21 ],
21 ],
22 ),
22 ),
23 migrations.CreateModel(
23 migrations.CreateModel(
24 name='KeyPair',
24 name='KeyPair',
25 fields=[
25 fields=[
26 ('id', models.AutoField(serialize=False, verbose_name='ID', primary_key=True, auto_created=True)),
26 ('id', models.AutoField(serialize=False, verbose_name='ID', primary_key=True, auto_created=True)),
27 ('public_key', models.TextField()),
27 ('public_key', models.TextField()),
28 ('private_key', models.TextField()),
28 ('private_key', models.TextField()),
29 ('key_type', models.TextField()),
29 ('key_type', models.TextField()),
30 ('primary', models.BooleanField(default=False)),
30 ('primary', models.BooleanField(default=False)),
31 ],
31 ],
32 ),
32 ),
33 migrations.CreateModel(
33 migrations.CreateModel(
34 name='Signature',
34 name='Signature',
35 fields=[
35 fields=[
36 ('id', models.AutoField(serialize=False, verbose_name='ID', primary_key=True, auto_created=True)),
36 ('id', models.AutoField(serialize=False, verbose_name='ID', primary_key=True, auto_created=True)),
37 ('key_type', models.TextField()),
37 ('key_type', models.TextField()),
38 ('key', models.TextField()),
38 ('key', models.TextField()),
39 ('signature', models.TextField()),
39 ('signature', models.TextField()),
40 ('global_id', models.ForeignKey(to='boards.GlobalId')),
40 ('global_id', models.ForeignKey(on_delete=models.CASCADE, to='boards.GlobalId')),
41 ],
41 ],
42 ),
42 ),
43 migrations.AddField(
43 migrations.AddField(
44 model_name='post',
44 model_name='post',
45 name='global_id',
45 name='global_id',
46 field=models.OneToOneField(to='boards.GlobalId', null=True, blank=True),
46 field=models.OneToOneField(on_delete=models.CASCADE, to='boards.GlobalId', null=True, blank=True),
47 ),
47 ),
48 ]
48 ]
@@ -1,19 +1,19 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 from __future__ import unicode_literals
2 from __future__ import unicode_literals
3
3
4 from django.db import models, migrations
4 from django.db import models, migrations
5
5
6
6
7 class Migration(migrations.Migration):
7 class Migration(migrations.Migration):
8
8
9 dependencies = [
9 dependencies = [
10 ('boards', '0028_auto_20150928_2211'),
10 ('boards', '0028_auto_20150928_2211'),
11 ]
11 ]
12
12
13 operations = [
13 operations = [
14 migrations.AddField(
14 migrations.AddField(
15 model_name='tag',
15 model_name='tag',
16 name='parent',
16 name='parent',
17 field=models.ForeignKey(to='boards.Tag', null=True),
17 field=models.ForeignKey(on_delete=models.CASCADE, to='boards.Tag', null=True),
18 ),
18 ),
19 ]
19 ]
@@ -1,19 +1,19 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 from __future__ import unicode_literals
2 from __future__ import unicode_literals
3
3
4 from django.db import models, migrations
4 from django.db import models, migrations
5
5
6
6
7 class Migration(migrations.Migration):
7 class Migration(migrations.Migration):
8
8
9 dependencies = [
9 dependencies = [
10 ('boards', '0029_tag_parent'),
10 ('boards', '0029_tag_parent'),
11 ]
11 ]
12
12
13 operations = [
13 operations = [
14 migrations.AlterField(
14 migrations.AlterField(
15 model_name='tag',
15 model_name='tag',
16 name='parent',
16 name='parent',
17 field=models.ForeignKey(related_name='children', null=True, to='boards.Tag'),
17 field=models.ForeignKey(on_delete=models.CASCADE, related_name='children', null=True, to='boards.Tag'),
18 ),
18 ),
19 ]
19 ]
@@ -1,19 +1,19 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 from __future__ import unicode_literals
2 from __future__ import unicode_literals
3
3
4 from django.db import migrations, models
4 from django.db import migrations, models
5
5
6
6
7 class Migration(migrations.Migration):
7 class Migration(migrations.Migration):
8
8
9 dependencies = [
9 dependencies = [
10 ('boards', '0032_auto_20151014_2222'),
10 ('boards', '0032_auto_20151014_2222'),
11 ]
11 ]
12
12
13 operations = [
13 operations = [
14 migrations.AlterField(
14 migrations.AlterField(
15 model_name='tag',
15 model_name='tag',
16 name='parent',
16 name='parent',
17 field=models.ForeignKey(blank=True, related_name='children', to='boards.Tag', null=True),
17 field=models.ForeignKey(on_delete=models.CASCADE, blank=True, related_name='children', to='boards.Tag', null=True),
18 ),
18 ),
19 ]
19 ]
@@ -1,185 +1,185 b''
1 from itertools import zip_longest
1 from itertools import zip_longest
2
2
3 import boards
3 import boards
4 from boards.models import STATUS_ARCHIVE
4 from boards.models import STATUS_ARCHIVE
5 from django.core.files.images import get_image_dimensions
5 from django.core.files.images import get_image_dimensions
6 from django.db import models
6 from django.db import models
7
7
8 from boards import utils
8 from boards import utils
9 from boards.models.attachment.viewers import get_viewers, AbstractViewer, \
9 from boards.models.attachment.viewers import get_viewers, AbstractViewer, \
10 FILE_TYPES_IMAGE
10 FILE_TYPES_IMAGE
11 from boards.utils import get_upload_filename, get_extension, cached_result, \
11 from boards.utils import get_upload_filename, get_extension, cached_result, \
12 get_file_mimetype
12 get_file_mimetype
13
13
14
14
15 class AttachmentManager(models.Manager):
15 class AttachmentManager(models.Manager):
16 def create_with_hash(self, file):
16 def create_with_hash(self, file):
17 file_hash = utils.get_file_hash(file)
17 file_hash = utils.get_file_hash(file)
18 attachment = self.get_existing_duplicate(file_hash, file)
18 attachment = self.get_existing_duplicate(file_hash, file)
19 if not attachment:
19 if not attachment:
20 file_type = get_file_mimetype(file)
20 file_type = get_file_mimetype(file)
21 attachment = self.create(file=file, mimetype=file_type,
21 attachment = self.create(file=file, mimetype=file_type,
22 hash=file_hash)
22 hash=file_hash)
23
23
24 return attachment
24 return attachment
25
25
26 def create_from_url(self, url):
26 def create_from_url(self, url):
27 existing = self.filter(url=url)
27 existing = self.filter(url=url)
28 if len(existing) > 0:
28 if len(existing) > 0:
29 attachment = existing[0]
29 attachment = existing[0]
30 else:
30 else:
31 attachment = self.create(url=url)
31 attachment = self.create(url=url)
32 return attachment
32 return attachment
33
33
34 def get_random_images(self, count, tags=None):
34 def get_random_images(self, count, tags=None):
35 images = self.filter(mimetype__in=FILE_TYPES_IMAGE).exclude(
35 images = self.filter(mimetype__in=FILE_TYPES_IMAGE).exclude(
36 attachment_posts__thread__status=STATUS_ARCHIVE)
36 attachment_posts__thread__status=STATUS_ARCHIVE)
37 if tags is not None:
37 if tags is not None:
38 images = images.filter(attachment_posts__threads__tags__in=tags)
38 images = images.filter(attachment_posts__threads__tags__in=tags)
39 return images.order_by('?')[:count]
39 return images.order_by('?')[:count]
40
40
41 def get_existing_duplicate(self, file_hash, file):
41 def get_existing_duplicate(self, file_hash, file):
42 """
42 """
43 Gets an attachment with the same file if one exists.
43 Gets an attachment with the same file if one exists.
44 """
44 """
45 existing = self.filter(hash=file_hash)
45 existing = self.filter(hash=file_hash)
46 attachment = None
46 attachment = None
47 for existing_attachment in existing:
47 for existing_attachment in existing:
48 existing_file = existing_attachment.file
48 existing_file = existing_attachment.file
49
49
50 file_chunks = file.chunks()
50 file_chunks = file.chunks()
51 existing_file_chunks = existing_file.chunks()
51 existing_file_chunks = existing_file.chunks()
52
52
53 if self._compare_chunks(file_chunks, existing_file_chunks):
53 if self._compare_chunks(file_chunks, existing_file_chunks):
54 attachment = existing_attachment
54 attachment = existing_attachment
55 return attachment
55 return attachment
56
56
57 def get_by_alias(self, name):
57 def get_by_alias(self, name):
58 pack_name, sticker_name = name.split('/')
58 pack_name, sticker_name = name.split('/')
59 try:
59 try:
60 return AttachmentSticker.objects.get(name=sticker_name, stickerpack__name=pack_name).attachment
60 return AttachmentSticker.objects.get(name=sticker_name, stickerpack__name=pack_name).attachment
61 except AttachmentSticker.DoesNotExist:
61 except AttachmentSticker.DoesNotExist:
62 return None
62 return None
63
63
64 def _compare_chunks(self, chunks1, chunks2):
64 def _compare_chunks(self, chunks1, chunks2):
65 """
65 """
66 Compares 2 chunks of different sizes (e.g. first chunk array contains
66 Compares 2 chunks of different sizes (e.g. first chunk array contains
67 all data in 1 chunk, and other one -- in a multiple of smaller ones.
67 all data in 1 chunk, and other one -- in a multiple of smaller ones.
68 """
68 """
69 equal = True
69 equal = True
70
70
71 position1 = 0
71 position1 = 0
72 position2 = 0
72 position2 = 0
73 chunk1 = None
73 chunk1 = None
74 chunk2 = None
74 chunk2 = None
75 chunk1ended = False
75 chunk1ended = False
76 chunk2ended = False
76 chunk2ended = False
77 while True:
77 while True:
78 if not chunk1 or len(chunk1) <= position1:
78 if not chunk1 or len(chunk1) <= position1:
79 try:
79 try:
80 chunk1 = chunks1.__next__()
80 chunk1 = chunks1.__next__()
81 position1 = 0
81 position1 = 0
82 except StopIteration:
82 except StopIteration:
83 chunk1ended = True
83 chunk1ended = True
84 if not chunk2 or len(chunk2) <= position2:
84 if not chunk2 or len(chunk2) <= position2:
85 try:
85 try:
86 chunk2 = chunks2.__next__()
86 chunk2 = chunks2.__next__()
87 position2 = 0
87 position2 = 0
88 except StopIteration:
88 except StopIteration:
89 chunk2ended = True
89 chunk2ended = True
90
90
91 if chunk1ended and chunk2ended:
91 if chunk1ended and chunk2ended:
92 # Same size chunksm checked for equality previously
92 # Same size chunksm checked for equality previously
93 break
93 break
94 elif chunk1ended or chunk2ended:
94 elif chunk1ended or chunk2ended:
95 # Different size chunks, not equal
95 # Different size chunks, not equal
96 equal = False
96 equal = False
97 break
97 break
98 elif chunk1[position1] != chunk2[position2]:
98 elif chunk1[position1] != chunk2[position2]:
99 # Different bytes, not equal
99 # Different bytes, not equal
100 equal = False
100 equal = False
101 break
101 break
102 else:
102 else:
103 position1 += 1
103 position1 += 1
104 position2 += 1
104 position2 += 1
105 return equal
105 return equal
106
106
107
107
108 class Attachment(models.Model):
108 class Attachment(models.Model):
109 objects = AttachmentManager()
109 objects = AttachmentManager()
110
110
111 class Meta:
111 class Meta:
112 app_label = 'boards'
112 app_label = 'boards'
113 ordering = ('id',)
113 ordering = ('id',)
114
114
115 file = models.FileField(upload_to=get_upload_filename, null=True)
115 file = models.FileField(upload_to=get_upload_filename, null=True)
116 mimetype = models.CharField(max_length=200, null=True)
116 mimetype = models.CharField(max_length=200, null=True)
117 hash = models.CharField(max_length=36, null=True)
117 hash = models.CharField(max_length=36, null=True)
118 url = models.TextField(blank=True, default='')
118 url = models.TextField(blank=True, default='')
119
119
120 def get_view(self):
120 def get_view(self):
121 file_viewer = None
121 file_viewer = None
122 for viewer in get_viewers():
122 for viewer in get_viewers():
123 if viewer.supports(self.mimetype):
123 if viewer.supports(self.mimetype):
124 file_viewer = viewer
124 file_viewer = viewer
125 break
125 break
126 if file_viewer is None:
126 if file_viewer is None:
127 file_viewer = AbstractViewer
127 file_viewer = AbstractViewer
128
128
129 return file_viewer(self.file, self.mimetype, self.id, self.url).get_view()
129 return file_viewer(self.file, self.mimetype, self.id, self.url).get_view()
130
130
131 def __str__(self):
131 def __str__(self):
132 return self.url or self.file.url
132 return self.url or self.file.url
133
133
134 def get_random_associated_post(self):
134 def get_random_associated_post(self):
135 posts = boards.models.Post.objects.filter(attachments__in=[self])
135 posts = boards.models.Post.objects.filter(attachments__in=[self])
136 return posts.order_by('?').first()
136 return posts.order_by('?').first()
137
137
138 @cached_result()
138 @cached_result()
139 def get_size(self):
139 def get_size(self):
140 if self.file:
140 if self.file:
141 if self.mimetype in FILE_TYPES_IMAGE:
141 if self.mimetype in FILE_TYPES_IMAGE:
142 return get_image_dimensions(self.file)
142 return get_image_dimensions(self.file)
143 else:
143 else:
144 return 200, 150
144 return 200, 150
145
145
146 def get_thumb_url(self):
146 def get_thumb_url(self):
147 split = self.file.url.rsplit('.', 1)
147 split = self.file.url.rsplit('.', 1)
148 w, h = 200, 150
148 w, h = 200, 150
149 return '%s.%sx%s.%s' % (split[0], w, h, split[1])
149 return '%s.%sx%s.%s' % (split[0], w, h, split[1])
150
150
151 @cached_result()
151 @cached_result()
152 def get_preview_size(self):
152 def get_preview_size(self):
153 size = 200, 150
153 size = 200, 150
154 if self.mimetype in FILE_TYPES_IMAGE:
154 if self.mimetype in FILE_TYPES_IMAGE:
155 preview_path = self.file.path.replace('.', '.200x150.')
155 preview_path = self.file.path.replace('.', '.200x150.')
156 try:
156 try:
157 size = get_image_dimensions(preview_path)
157 size = get_image_dimensions(preview_path)
158 except Exception:
158 except Exception:
159 pass
159 pass
160
160
161 return size
161 return size
162
162
163 def is_internal(self):
163 def is_internal(self):
164 return self.url is None or len(self.url) == 0
164 return self.url is None or len(self.url) == 0
165
165
166
166
167 class StickerPack(models.Model):
167 class StickerPack(models.Model):
168 name = models.TextField(unique=True)
168 name = models.TextField(unique=True)
169 tripcode = models.TextField(blank=True)
169 tripcode = models.TextField(blank=True)
170
170
171 def __str__(self):
171 def __str__(self):
172 return self.name
172 return self.name
173
173
174
174
175 class AttachmentSticker(models.Model):
175 class AttachmentSticker(models.Model):
176 attachment = models.ForeignKey('Attachment')
176 attachment = models.ForeignKey('Attachment', on_delete=models.CASCADE)
177 name = models.TextField(unique=True)
177 name = models.TextField(unique=True)
178 stickerpack = models.ForeignKey('StickerPack')
178 stickerpack = models.ForeignKey('StickerPack', on_delete=models.CASCADE)
179
179
180 def __str__(self):
180 def __str__(self):
181 # Local stickers do not have a sticker pack
181 # Local stickers do not have a sticker pack
182 if hasattr(self, 'stickerpack'):
182 if hasattr(self, 'stickerpack'):
183 return '{}/{}'.format(str(self.stickerpack), self.name)
183 return '{}/{}'.format(str(self.stickerpack), self.name)
184 else:
184 else:
185 return self.name
185 return self.name
@@ -1,244 +1,237 b''
1 import re
2
3 from PIL import Image
4
5 from django.contrib.staticfiles import finders
1 from django.contrib.staticfiles import finders
6 from django.contrib.staticfiles.templatetags.staticfiles import static
2 from django.contrib.staticfiles.templatetags.staticfiles import static
7 from django.core.files.images import get_image_dimensions
3 from django.core.files.images import get_image_dimensions
8 from django.template.defaultfilters import filesizeformat
4 from django.template.defaultfilters import filesizeformat
9 from django.core.urlresolvers import reverse
10 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
11
5
6 from boards import settings
12 from boards.utils import get_domain, cached_result, get_extension
7 from boards.utils import get_domain, cached_result, get_extension
13 from boards import settings
14
15
8
16 FILE_STUB_IMAGE = 'images/file.png'
9 FILE_STUB_IMAGE = 'images/file.png'
17 FILE_STUB_URL = 'url'
10 FILE_STUB_URL = 'url'
18 FILE_FILEFORMAT = 'images/fileformats/{}.png'
11 FILE_FILEFORMAT = 'images/fileformats/{}.png'
19
12
20
13
21 FILE_TYPES_VIDEO = (
14 FILE_TYPES_VIDEO = (
22 'video/webm',
15 'video/webm',
23 'video/mp4',
16 'video/mp4',
24 'video/mpeg',
17 'video/mpeg',
25 'video/ogv',
18 'video/ogv',
26 )
19 )
27 FILE_TYPE_SVG = 'image/svg+xml'
20 FILE_TYPE_SVG = 'image/svg+xml'
28 FILE_TYPES_AUDIO = (
21 FILE_TYPES_AUDIO = (
29 'audio/ogg',
22 'audio/ogg',
30 'audio/mpeg',
23 'audio/mpeg',
31 'audio/opus',
24 'audio/opus',
32 'audio/x-flac',
25 'audio/x-flac',
33 'audio/mpeg',
26 'audio/mpeg',
34 )
27 )
35 FILE_TYPES_IMAGE = (
28 FILE_TYPES_IMAGE = (
36 'image/jpeg',
29 'image/jpeg',
37 'image/jpg',
30 'image/jpg',
38 'image/png',
31 'image/png',
39 'image/bmp',
32 'image/bmp',
40 'image/gif',
33 'image/gif',
41 )
34 )
42
35
43 PLAIN_FILE_FORMATS = {
36 PLAIN_FILE_FORMATS = {
44 'zip': 'archive',
37 'zip': 'archive',
45 'tar': 'archive',
38 'tar': 'archive',
46 'gz': 'archive',
39 'gz': 'archive',
47 'mid' : 'midi',
40 'mid' : 'midi',
48 }
41 }
49
42
50 URL_PROTOCOLS = {
43 URL_PROTOCOLS = {
51 'magnet': 'magnet',
44 'magnet': 'magnet',
52 }
45 }
53
46
54 CSS_CLASS_IMAGE = 'image'
47 CSS_CLASS_IMAGE = 'image'
55 CSS_CLASS_THUMB = 'thumb'
48 CSS_CLASS_THUMB = 'thumb'
56
49
57 ABSTRACT_VIEW = '<div class="image">'\
50 ABSTRACT_VIEW = '<div class="image">'\
58 '{}'\
51 '{}'\
59 '<div class="image-metadata"><a href="{}" download >{}, {}</a>'\
52 '<div class="image-metadata"><a href="{}" download >{}, {}</a>'\
60 ' <a class="file-menu" href="#" data-type="{}" data-search-url="{}" data-filename="{}" data-id="{}">&#8942; </a></div>'\
53 ' <a class="file-menu" href="#" data-type="{}" data-search-url="{}" data-filename="{}" data-id="{}">&#8942; </a></div>'\
61 '</div>'
54 '</div>'
62 URL_VIEW = '<div class="image">' \
55 URL_VIEW = '<div class="image">' \
63 '{}' \
56 '{}' \
64 '<div class="image-metadata">{}</div>' \
57 '<div class="image-metadata">{}</div>' \
65 '</div>'
58 '</div>'
66 ABSTRACT_FORMAT_VIEW = '<a href="{}">'\
59 ABSTRACT_FORMAT_VIEW = '<a href="{}">'\
67 '<img class="url-image" src="{}" width="{}" height="{}"/>'\
60 '<img class="url-image" src="{}" width="{}" height="{}"/>'\
68 '</a>'
61 '</a>'
69 VIDEO_FORMAT_VIEW = '<video width="200" height="150" controls src="{}"></video>'
62 VIDEO_FORMAT_VIEW = '<video width="200" height="150" controls src="{}"></video>'
70 AUDIO_FORMAT_VIEW = '<audio controls src="{}"></audio>'
63 AUDIO_FORMAT_VIEW = '<audio controls src="{}"></audio>'
71 IMAGE_FORMAT_VIEW = '<a class="{}" href="{full}">' \
64 IMAGE_FORMAT_VIEW = '<a class="{}" href="{full}">' \
72 '<img class="post-image-preview"' \
65 '<img class="post-image-preview"' \
73 ' src="{}"' \
66 ' src="{}"' \
74 ' alt="{}"' \
67 ' alt="{}"' \
75 ' width="{}"' \
68 ' width="{}"' \
76 ' height="{}"' \
69 ' height="{}"' \
77 ' data-width="{}"' \
70 ' data-width="{}"' \
78 ' data-height="{}" />' \
71 ' data-height="{}" />' \
79 '</a>'
72 '</a>'
80 SVG_FORMAT_VIEW = '<a class="thumb" href="{}">'\
73 SVG_FORMAT_VIEW = '<a class="thumb" href="{}">'\
81 '<img class="post-image-preview" width="200" height="150" src="{}" />'\
74 '<img class="post-image-preview" width="200" height="150" src="{}" />'\
82 '</a>'
75 '</a>'
83 URL_FORMAT_VIEW = '<a href="{}">' \
76 URL_FORMAT_VIEW = '<a href="{}">' \
84 '<img class="url-image" src="{}" width="{}" height="{}"/>' \
77 '<img class="url-image" src="{}" width="{}" height="{}"/>' \
85 '</a>'
78 '</a>'
86
79
87
80
88 def get_viewers():
81 def get_viewers():
89 return AbstractViewer.__subclasses__()
82 return AbstractViewer.__subclasses__()
90
83
91
84
92 def get_static_dimensions(filename):
85 def get_static_dimensions(filename):
93 file_path = finders.find(filename)
86 file_path = finders.find(filename)
94 return get_image_dimensions(file_path)
87 return get_image_dimensions(file_path)
95
88
96
89
97 # TODO Move this to utils
90 # TODO Move this to utils
98 def file_exists(filename):
91 def file_exists(filename):
99 return finders.find(filename) is not None
92 return finders.find(filename) is not None
100
93
101
94
102 class AbstractViewer:
95 class AbstractViewer:
103 def __init__(self, file, file_type, id, url):
96 def __init__(self, file, file_type, id, url):
104 self.file = file
97 self.file = file
105 self.file_type = file_type
98 self.file_type = file_type
106 self.id = id
99 self.id = id
107 self.url = url
100 self.url = url
108 self.extension = get_extension(self.file.name).lower()
101 self.extension = get_extension(self.file.name).lower()
109
102
110 @staticmethod
103 @staticmethod
111 def supports(file_type):
104 def supports(file_type):
112 return True
105 return True
113
106
114 def get_view(self):
107 def get_view(self):
115 search_host = settings.get('External', 'ImageSearchHost')
108 search_host = settings.get('External', 'ImageSearchHost')
116 if search_host:
109 if search_host:
117 if search_host.endswith('/'):
110 if search_host.endswith('/'):
118 search_host = search_host[:-1]
111 search_host = search_host[:-1]
119 search_url = search_host + self.file.url
112 search_url = search_host + self.file.url
120 else:
113 else:
121 search_url = ''
114 search_url = ''
122
115
123 return ABSTRACT_VIEW.format(self.get_format_view(), self.file.url,
116 return ABSTRACT_VIEW.format(self.get_format_view(), self.file.url,
124 self.file_type, filesizeformat(self.file.size),
117 self.file_type, filesizeformat(self.file.size),
125 self.file_type, search_url, self.file.name, self.id)
118 self.file_type, search_url, self.file.name, self.id)
126
119
127 def get_format_view(self):
120 def get_format_view(self):
128 image_name = PLAIN_FILE_FORMATS.get(self.extension, self.extension)
121 image_name = PLAIN_FILE_FORMATS.get(self.extension, self.extension)
129 file_name = FILE_FILEFORMAT.format(image_name)
122 file_name = FILE_FILEFORMAT.format(image_name)
130
123
131 if file_exists(file_name):
124 if file_exists(file_name):
132 image = file_name
125 image = file_name
133 else:
126 else:
134 image = FILE_STUB_IMAGE
127 image = FILE_STUB_IMAGE
135
128
136 w, h = get_static_dimensions(image)
129 w, h = get_static_dimensions(image)
137
130
138 return ABSTRACT_FORMAT_VIEW.format(self.file.url, static(image), w, h)
131 return ABSTRACT_FORMAT_VIEW.format(self.file.url, static(image), w, h)
139
132
140
133
141 class VideoViewer(AbstractViewer):
134 class VideoViewer(AbstractViewer):
142 @staticmethod
135 @staticmethod
143 def supports(file_type):
136 def supports(file_type):
144 return file_type in FILE_TYPES_VIDEO
137 return file_type in FILE_TYPES_VIDEO
145
138
146 def get_format_view(self):
139 def get_format_view(self):
147 return VIDEO_FORMAT_VIEW.format(self.file.url)
140 return VIDEO_FORMAT_VIEW.format(self.file.url)
148
141
149
142
150 class AudioViewer(AbstractViewer):
143 class AudioViewer(AbstractViewer):
151 @staticmethod
144 @staticmethod
152 def supports(file_type):
145 def supports(file_type):
153 return file_type in FILE_TYPES_AUDIO
146 return file_type in FILE_TYPES_AUDIO
154
147
155 def get_format_view(self):
148 def get_format_view(self):
156 return AUDIO_FORMAT_VIEW.format(self.file.url)
149 return AUDIO_FORMAT_VIEW.format(self.file.url)
157
150
158
151
159 class SvgViewer(AbstractViewer):
152 class SvgViewer(AbstractViewer):
160 @staticmethod
153 @staticmethod
161 def supports(file_type):
154 def supports(file_type):
162 return file_type == FILE_TYPE_SVG
155 return file_type == FILE_TYPE_SVG
163
156
164 def get_format_view(self):
157 def get_format_view(self):
165 return SVG_FORMAT_VIEW.format(self.file.url, self.file.url)
158 return SVG_FORMAT_VIEW.format(self.file.url, self.file.url)
166
159
167
160
168 class ImageViewer(AbstractViewer):
161 class ImageViewer(AbstractViewer):
169 @staticmethod
162 @staticmethod
170 def supports(file_type):
163 def supports(file_type):
171 return file_type in FILE_TYPES_IMAGE
164 return file_type in FILE_TYPES_IMAGE
172
165
173 def get_format_view(self):
166 def get_format_view(self):
174 metadata = '{}, {}'.format(self.file.name.split('.')[-1],
167 metadata = '{}, {}'.format(self.file.name.split('.')[-1],
175 filesizeformat(self.file.size))
168 filesizeformat(self.file.size))
176
169
177 try:
170 try:
178 width, height = get_image_dimensions(self.file.path)
171 width, height = get_image_dimensions(self.file.path)
179 except Exception:
172 except Exception:
180 # If the image is a decompression bomb, treat it as just a regular
173 # If the image is a decompression bomb, treat it as just a regular
181 # file
174 # file
182 return super().get_format_view()
175 return super().get_format_view()
183
176
184 preview_path = self.file.path.replace('.', '.200x150.')
177 preview_path = self.file.path.replace('.', '.200x150.')
185 try:
178 try:
186 pre_width, pre_height = get_image_dimensions(preview_path)
179 pre_width, pre_height = get_image_dimensions(preview_path)
187 except Exception:
180 except Exception:
188 return super().get_format_view()
181 return super().get_format_view()
189
182
190 split = self.file.url.rsplit('.', 1)
183 split = self.file.url.rsplit('.', 1)
191 w, h = 200, 150
184 w, h = 200, 150
192 thumb_url = '%s.%sx%s.%s' % (split[0], w, h, split[1])
185 thumb_url = '%s.%sx%s.%s' % (split[0], w, h, split[1])
193
186
194 return IMAGE_FORMAT_VIEW.format(CSS_CLASS_THUMB,
187 return IMAGE_FORMAT_VIEW.format(CSS_CLASS_THUMB,
195 thumb_url,
188 thumb_url,
196 self.id,
189 self.id,
197 str(pre_width),
190 str(pre_width),
198 str(pre_height), str(width), str(height),
191 str(pre_height), str(width), str(height),
199 full=self.file.url, image_meta=metadata)
192 full=self.file.url, image_meta=metadata)
200
193
201
194
202 class UrlViewer(AbstractViewer):
195 class UrlViewer(AbstractViewer):
203 @staticmethod
196 @staticmethod
204 def supports(file_type):
197 def supports(file_type):
205 return file_type is None
198 return file_type is None
206
199
207 def get_view(self):
200 def get_view(self):
208 return URL_VIEW.format(self.get_format_view(), get_domain(self.url))
201 return URL_VIEW.format(self.get_format_view(), get_domain(self.url))
209
202
210 def get_format_view(self):
203 def get_format_view(self):
211 protocol = self.url.split(':')[0]
204 protocol = self.url.split(':')[0]
212
205
213 domain = get_domain(self.url)
206 domain = get_domain(self.url)
214
207
215 if protocol in URL_PROTOCOLS:
208 if protocol in URL_PROTOCOLS:
216 url_image_name = URL_PROTOCOLS.get(protocol)
209 url_image_name = URL_PROTOCOLS.get(protocol)
217 elif domain:
210 elif domain:
218 url_image_name = self._find_image_for_domains(domain) or FILE_STUB_URL
211 url_image_name = self._find_image_for_domains(domain) or FILE_STUB_URL
219 else:
212 else:
220 url_image_name = FILE_STUB_URL
213 url_image_name = FILE_STUB_URL
221
214
222 image_path = 'images/{}.png'.format(url_image_name)
215 image_path = 'images/{}.png'.format(url_image_name)
223 image = static(image_path)
216 image = static(image_path)
224 w, h = get_static_dimensions(image_path)
217 w, h = get_static_dimensions(image_path)
225
218
226 return URL_FORMAT_VIEW.format(self.url, image, w, h)
219 return URL_FORMAT_VIEW.format(self.url, image, w, h)
227
220
228 @cached_result()
221 @cached_result()
229 def _find_image_for_domains(self, domain):
222 def _find_image_for_domains(self, domain):
230 """
223 """
231 Searches for the domain image for every domain level except top.
224 Searches for the domain image for every domain level except top.
232 E.g. for l3.example.co.uk it will search for l3.example.co.uk, then
225 E.g. for l3.example.co.uk it will search for l3.example.co.uk, then
233 example.co.uk, then co.uk
226 example.co.uk, then co.uk
234 """
227 """
235 levels = domain.split('.')
228 levels = domain.split('.')
236 while len(levels) > 1:
229 while len(levels) > 1:
237 domain = '.'.join(levels)
230 domain = '.'.join(levels)
238
231
239 filename = 'images/domains/{}.png'.format(domain)
232 filename = 'images/domains/{}.png'.format(domain)
240 if file_exists(filename):
233 if file_exists(filename):
241 return 'domains/' + domain
234 return 'domains/' + domain
242 else:
235 else:
243 del levels[0]
236 del levels[0]
244
237
@@ -1,13 +1,13 b''
1 from django.db import models
1 from django.db import models
2
2
3
3
4 class Banner(models.Model):
4 class Banner(models.Model):
5 title = models.TextField()
5 title = models.TextField()
6 text = models.TextField(blank=True, null=True)
6 text = models.TextField(blank=True, null=True)
7 post = models.ForeignKey('Post')
7 post = models.ForeignKey('Post', on_delete=models.CASCADE)
8
8
9 def __str__(self):
9 def __str__(self):
10 return self.title
10 return self.title
11
11
12 def get_text(self) -> str:
12 def get_text(self) -> str:
13 return self.text or self.post.get_text()
13 return self.text or self.post.get_text()
@@ -1,360 +1,360 b''
1 import uuid
1 import uuid
2
2 import hashlib
3 import hashlib
3 import re
4 import re
5 from django.db import models
6 from django.db.models import TextField
7 from django.template.defaultfilters import truncatewords, striptags
8 from django.template.loader import render_to_string
9 from django.urls import reverse
4
10
5 from boards import settings
6 from boards.abstracts.tripcode import Tripcode
11 from boards.abstracts.tripcode import Tripcode
7 from boards.models import Attachment, KeyPair, GlobalId
12 from boards.models import Attachment, KeyPair, GlobalId
8 from boards.models.attachment import FILE_TYPES_IMAGE
13 from boards.models.attachment import FILE_TYPES_IMAGE
9 from boards.models.base import Viewable
14 from boards.models.base import Viewable
10 from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
15 from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
11 from boards.models.post.manager import PostManager, NO_IP
16 from boards.models.post.manager import PostManager, NO_IP
12 from boards.utils import datetime_to_epoch
17 from boards.utils import datetime_to_epoch
13 from django.core.exceptions import ObjectDoesNotExist
14 from django.core.urlresolvers import reverse
15 from django.db import models
16 from django.db.models import TextField, QuerySet, F
17 from django.template.defaultfilters import truncatewords, striptags
18 from django.template.loader import render_to_string
19
18
20 CSS_CLS_HIDDEN_POST = 'hidden_post'
19 CSS_CLS_HIDDEN_POST = 'hidden_post'
21 CSS_CLS_DEAD_POST = 'dead_post'
20 CSS_CLS_DEAD_POST = 'dead_post'
22 CSS_CLS_ARCHIVE_POST = 'archive_post'
21 CSS_CLS_ARCHIVE_POST = 'archive_post'
23 CSS_CLS_POST = 'post'
22 CSS_CLS_POST = 'post'
24 CSS_CLS_MONOCHROME = 'monochrome'
23 CSS_CLS_MONOCHROME = 'monochrome'
25
24
26 TITLE_MAX_WORDS = 10
25 TITLE_MAX_WORDS = 10
27
26
28 APP_LABEL_BOARDS = 'boards'
27 APP_LABEL_BOARDS = 'boards'
29
28
30 BAN_REASON_AUTO = 'Auto'
29 BAN_REASON_AUTO = 'Auto'
31
30
32 TITLE_MAX_LENGTH = 200
31 TITLE_MAX_LENGTH = 200
33
32
34 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
33 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
35 REGEX_GLOBAL_REPLY = re.compile(r'\[post\](\w+)::([^:]+)::(\d+)\[/post\]')
34 REGEX_GLOBAL_REPLY = re.compile(r'\[post\](\w+)::([^:]+)::(\d+)\[/post\]')
36 REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?')
35 REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?')
37 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
36 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
38
37
39 PARAMETER_TRUNCATED = 'truncated'
38 PARAMETER_TRUNCATED = 'truncated'
40 PARAMETER_TAG = 'tag'
39 PARAMETER_TAG = 'tag'
41 PARAMETER_OFFSET = 'offset'
40 PARAMETER_OFFSET = 'offset'
42 PARAMETER_DIFF_TYPE = 'type'
41 PARAMETER_DIFF_TYPE = 'type'
43 PARAMETER_CSS_CLASS = 'css_class'
42 PARAMETER_CSS_CLASS = 'css_class'
44 PARAMETER_THREAD = 'thread'
43 PARAMETER_THREAD = 'thread'
45 PARAMETER_IS_OPENING = 'is_opening'
44 PARAMETER_IS_OPENING = 'is_opening'
46 PARAMETER_POST = 'post'
45 PARAMETER_POST = 'post'
47 PARAMETER_OP_ID = 'opening_post_id'
46 PARAMETER_OP_ID = 'opening_post_id'
48 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
47 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
49 PARAMETER_REPLY_LINK = 'reply_link'
48 PARAMETER_REPLY_LINK = 'reply_link'
50 PARAMETER_NEED_OP_DATA = 'need_op_data'
49 PARAMETER_NEED_OP_DATA = 'need_op_data'
51
50
52 POST_VIEW_PARAMS = (
51 POST_VIEW_PARAMS = (
53 'need_op_data',
52 'need_op_data',
54 'reply_link',
53 'reply_link',
55 'need_open_link',
54 'need_open_link',
56 'truncated',
55 'truncated',
57 'mode_tree',
56 'mode_tree',
58 'perms',
57 'perms',
59 'tree_depth',
58 'tree_depth',
60 )
59 )
61
60
62
61
63 class Post(models.Model, Viewable):
62 class Post(models.Model, Viewable):
64 """A post is a message."""
63 """A post is a message."""
65
64
66 objects = PostManager()
65 objects = PostManager()
67
66
68 class Meta:
67 class Meta:
69 app_label = APP_LABEL_BOARDS
68 app_label = APP_LABEL_BOARDS
70 ordering = ('id',)
69 ordering = ('id',)
71
70
72 title = models.CharField(max_length=TITLE_MAX_LENGTH, blank=True, default='')
71 title = models.CharField(max_length=TITLE_MAX_LENGTH, blank=True, default='')
73 pub_time = models.DateTimeField(db_index=True)
72 pub_time = models.DateTimeField(db_index=True)
74 text = TextField(blank=True, default='')
73 text = TextField(blank=True, default='')
75 _text_rendered = TextField(blank=True, null=True, editable=False)
74 _text_rendered = TextField(blank=True, null=True, editable=False)
76
75
77 attachments = models.ManyToManyField(Attachment, null=True, blank=True,
76 attachments = models.ManyToManyField(Attachment, null=True, blank=True,
78 related_name='attachment_posts')
77 related_name='attachment_posts')
79
78
80 poster_ip = models.GenericIPAddressField()
79 poster_ip = models.GenericIPAddressField()
81
80
82 # Used for cache and threads updating
81 # Used for cache and threads updating
83 last_edit_time = models.DateTimeField()
82 last_edit_time = models.DateTimeField()
84
83
85 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
84 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
86 null=True,
85 null=True,
87 blank=True, related_name='refposts',
86 blank=True, related_name='refposts',
88 db_index=True)
87 db_index=True)
89 refmap = models.TextField(null=True, blank=True)
88 refmap = models.TextField(null=True, blank=True)
90 thread = models.ForeignKey('Thread', db_index=True, related_name='replies')
89 thread = models.ForeignKey('Thread', on_delete=models.CASCADE,
90 db_index=True, related_name='replies')
91
91
92 url = models.TextField()
92 url = models.TextField()
93 uid = models.TextField()
93 uid = models.TextField()
94
94
95 # Global ID with author key. If the message was downloaded from another
95 # Global ID with author key. If the message was downloaded from another
96 # server, this indicates the server.
96 # server, this indicates the server.
97 global_id = models.OneToOneField(GlobalId, null=True, blank=True,
97 global_id = models.OneToOneField(GlobalId, null=True, blank=True,
98 on_delete=models.CASCADE)
98 on_delete=models.CASCADE)
99
99
100 tripcode = models.CharField(max_length=50, blank=True, default='')
100 tripcode = models.CharField(max_length=50, blank=True, default='')
101 opening = models.BooleanField(db_index=True)
101 opening = models.BooleanField(db_index=True)
102 hidden = models.BooleanField(default=False)
102 hidden = models.BooleanField(default=False)
103
103
104 def __str__(self):
104 def __str__(self):
105 return 'P#{}/{}'.format(self.id, self.get_title())
105 return 'P#{}/{}'.format(self.id, self.get_title())
106
106
107 def get_title(self) -> str:
107 def get_title(self) -> str:
108 return self.title
108 return self.title
109
109
110 def get_title_or_text(self):
110 def get_title_or_text(self):
111 title = self.get_title()
111 title = self.get_title()
112 if not title:
112 if not title:
113 title = truncatewords(striptags(self.get_text()), TITLE_MAX_WORDS)
113 title = truncatewords(striptags(self.get_text()), TITLE_MAX_WORDS)
114
114
115 return title
115 return title
116
116
117 def build_refmap(self, excluded_ids=None) -> None:
117 def build_refmap(self, excluded_ids=None) -> None:
118 """
118 """
119 Builds a replies map string from replies list. This is a cache to stop
119 Builds a replies map string from replies list. This is a cache to stop
120 the server from recalculating the map on every post show.
120 the server from recalculating the map on every post show.
121 """
121 """
122
122
123 replies = self.referenced_posts
123 replies = self.referenced_posts
124 if excluded_ids is not None:
124 if excluded_ids is not None:
125 replies = replies.exclude(id__in=excluded_ids)
125 replies = replies.exclude(id__in=excluded_ids)
126 else:
126 else:
127 replies = replies.all()
127 replies = replies.all()
128
128
129 post_urls = [refpost.get_link_view() for refpost in replies]
129 post_urls = [refpost.get_link_view() for refpost in replies]
130
130
131 self.refmap = ', '.join(post_urls)
131 self.refmap = ', '.join(post_urls)
132
132
133 def is_referenced(self) -> bool:
133 def is_referenced(self) -> bool:
134 return self.refmap and len(self.refmap) > 0
134 return self.refmap and len(self.refmap) > 0
135
135
136 def is_opening(self) -> bool:
136 def is_opening(self) -> bool:
137 """
137 """
138 Checks if this is an opening post or just a reply.
138 Checks if this is an opening post or just a reply.
139 """
139 """
140
140
141 return self.opening
141 return self.opening
142
142
143 def get_absolute_url(self, thread=None):
143 def get_absolute_url(self, thread=None):
144 # Url is cached only for the "main" thread. When getting url
144 # Url is cached only for the "main" thread. When getting url
145 # for other threads, do it manually.
145 # for other threads, do it manually.
146 return self.url
146 return self.url
147
147
148 def get_thread(self):
148 def get_thread(self):
149 return self.thread
149 return self.thread
150
150
151 def get_thread_id(self):
151 def get_thread_id(self):
152 return self.thread_id
152 return self.thread_id
153
153
154 def _get_cache_key(self):
154 def _get_cache_key(self):
155 return [datetime_to_epoch(self.last_edit_time)]
155 return [datetime_to_epoch(self.last_edit_time)]
156
156
157 def get_view_params(self, *args, **kwargs):
157 def get_view_params(self, *args, **kwargs):
158 """
158 """
159 Gets the parameters required for viewing the post based on the arguments
159 Gets the parameters required for viewing the post based on the arguments
160 given and the post itself.
160 given and the post itself.
161 """
161 """
162 thread = kwargs.get('thread') or self.get_thread()
162 thread = kwargs.get('thread') or self.get_thread()
163
163
164 css_classes = [CSS_CLS_POST]
164 css_classes = [CSS_CLS_POST]
165 if thread.is_archived():
165 if thread.is_archived():
166 css_classes.append(CSS_CLS_ARCHIVE_POST)
166 css_classes.append(CSS_CLS_ARCHIVE_POST)
167 elif not thread.can_bump():
167 elif not thread.can_bump():
168 css_classes.append(CSS_CLS_DEAD_POST)
168 css_classes.append(CSS_CLS_DEAD_POST)
169 if self.is_hidden():
169 if self.is_hidden():
170 css_classes.append(CSS_CLS_HIDDEN_POST)
170 css_classes.append(CSS_CLS_HIDDEN_POST)
171 if thread.is_monochrome():
171 if thread.is_monochrome():
172 css_classes.append(CSS_CLS_MONOCHROME)
172 css_classes.append(CSS_CLS_MONOCHROME)
173
173
174 params = dict()
174 params = dict()
175 for param in POST_VIEW_PARAMS:
175 for param in POST_VIEW_PARAMS:
176 if param in kwargs:
176 if param in kwargs:
177 params[param] = kwargs[param]
177 params[param] = kwargs[param]
178
178
179 params.update({
179 params.update({
180 PARAMETER_POST: self,
180 PARAMETER_POST: self,
181 PARAMETER_IS_OPENING: self.is_opening(),
181 PARAMETER_IS_OPENING: self.is_opening(),
182 PARAMETER_THREAD: thread,
182 PARAMETER_THREAD: thread,
183 PARAMETER_CSS_CLASS: ' '.join(css_classes),
183 PARAMETER_CSS_CLASS: ' '.join(css_classes),
184 })
184 })
185
185
186 return params
186 return params
187
187
188 def get_view(self, *args, **kwargs) -> str:
188 def get_view(self, *args, **kwargs) -> str:
189 """
189 """
190 Renders post's HTML view. Some of the post params can be passed over
190 Renders post's HTML view. Some of the post params can be passed over
191 kwargs for the means of caching (if we view the thread, some params
191 kwargs for the means of caching (if we view the thread, some params
192 are same for every post and don't need to be computed over and over.
192 are same for every post and don't need to be computed over and over.
193 """
193 """
194 params = self.get_view_params(*args, **kwargs)
194 params = self.get_view_params(*args, **kwargs)
195
195
196 return render_to_string('boards/post.html', params)
196 return render_to_string('boards/post.html', params)
197
197
198 def get_images(self) -> Attachment:
198 def get_images(self) -> Attachment:
199 return self.attachments.filter(mimetype__in=FILE_TYPES_IMAGE)
199 return self.attachments.filter(mimetype__in=FILE_TYPES_IMAGE)
200
200
201 def get_first_image(self) -> Attachment:
201 def get_first_image(self) -> Attachment:
202 try:
202 try:
203 return self.get_images().earliest('-id')
203 return self.get_images().earliest('-id')
204 except Attachment.DoesNotExist:
204 except Attachment.DoesNotExist:
205 return None
205 return None
206
206
207 def set_global_id(self, key_pair=None):
207 def set_global_id(self, key_pair=None):
208 """
208 """
209 Sets global id based on the given key pair. If no key pair is given,
209 Sets global id based on the given key pair. If no key pair is given,
210 default one is used.
210 default one is used.
211 """
211 """
212
212
213 if key_pair:
213 if key_pair:
214 key = key_pair
214 key = key_pair
215 else:
215 else:
216 try:
216 try:
217 key = KeyPair.objects.get(primary=True)
217 key = KeyPair.objects.get(primary=True)
218 except KeyPair.DoesNotExist:
218 except KeyPair.DoesNotExist:
219 # Do not update the global id because there is no key defined
219 # Do not update the global id because there is no key defined
220 return
220 return
221 global_id = GlobalId(key_type=key.key_type,
221 global_id = GlobalId(key_type=key.key_type,
222 key=key.public_key,
222 key=key.public_key,
223 local_id=self.id)
223 local_id=self.id)
224 global_id.save()
224 global_id.save()
225
225
226 self.global_id = global_id
226 self.global_id = global_id
227
227
228 self.save(update_fields=['global_id'])
228 self.save(update_fields=['global_id'])
229
229
230 def get_pub_time_str(self):
230 def get_pub_time_str(self):
231 return str(self.pub_time)
231 return str(self.pub_time)
232
232
233 def get_replied_ids(self):
233 def get_replied_ids(self):
234 """
234 """
235 Gets ID list of the posts that this post replies.
235 Gets ID list of the posts that this post replies.
236 """
236 """
237
237
238 raw_text = self.get_raw_text()
238 raw_text = self.get_raw_text()
239
239
240 local_replied = REGEX_REPLY.findall(raw_text)
240 local_replied = REGEX_REPLY.findall(raw_text)
241 global_replied = []
241 global_replied = []
242 for match in REGEX_GLOBAL_REPLY.findall(raw_text):
242 for match in REGEX_GLOBAL_REPLY.findall(raw_text):
243 key_type = match[0]
243 key_type = match[0]
244 key = match[1]
244 key = match[1]
245 local_id = match[2]
245 local_id = match[2]
246
246
247 try:
247 try:
248 global_id = GlobalId.objects.get(key_type=key_type,
248 global_id = GlobalId.objects.get(key_type=key_type,
249 key=key, local_id=local_id)
249 key=key, local_id=local_id)
250 for post in Post.objects.filter(global_id=global_id).only('id'):
250 for post in Post.objects.filter(global_id=global_id).only('id'):
251 global_replied.append(post.id)
251 global_replied.append(post.id)
252 except GlobalId.DoesNotExist:
252 except GlobalId.DoesNotExist:
253 pass
253 pass
254 return local_replied + global_replied
254 return local_replied + global_replied
255
255
256 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
256 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
257 include_last_update=False) -> str:
257 include_last_update=False) -> str:
258 """
258 """
259 Gets post HTML or JSON data that can be rendered on a page or used by
259 Gets post HTML or JSON data that can be rendered on a page or used by
260 API.
260 API.
261 """
261 """
262
262
263 return get_exporter(format_type).export(self, request,
263 return get_exporter(format_type).export(self, request,
264 include_last_update)
264 include_last_update)
265
265
266 def _build_url(self):
266 def _build_url(self):
267 opening = self.is_opening()
267 opening = self.is_opening()
268 opening_id = self.id if opening else self.get_thread().get_opening_post_id()
268 opening_id = self.id if opening else self.get_thread().get_opening_post_id()
269 url = reverse('thread', kwargs={'post_id': opening_id})
269 url = reverse('thread', kwargs={'post_id': opening_id})
270 if not opening:
270 if not opening:
271 url += '#' + str(self.id)
271 url += '#' + str(self.id)
272
272
273 return url
273 return url
274
274
275 def save(self, force_insert=False, force_update=False, using=None,
275 def save(self, force_insert=False, force_update=False, using=None,
276 update_fields=None):
276 update_fields=None):
277 new_post = self.id is None
277 new_post = self.id is None
278
278
279 self.uid = str(uuid.uuid4())
279 self.uid = str(uuid.uuid4())
280 if update_fields is not None and 'uid' not in update_fields:
280 if update_fields is not None and 'uid' not in update_fields:
281 update_fields += ['uid']
281 update_fields += ['uid']
282
282
283 if not new_post:
283 if not new_post:
284 thread = self.get_thread()
284 thread = self.get_thread()
285 if thread:
285 if thread:
286 thread.last_edit_time = self.last_edit_time
286 thread.last_edit_time = self.last_edit_time
287 thread.save(update_fields=['last_edit_time', 'status'])
287 thread.save(update_fields=['last_edit_time', 'status'])
288
288
289 super().save(force_insert, force_update, using, update_fields)
289 super().save(force_insert, force_update, using, update_fields)
290
290
291 if new_post:
291 if new_post:
292 self.url = self._build_url()
292 self.url = self._build_url()
293 super().save(update_fields=['url'])
293 super().save(update_fields=['url'])
294
294
295 def get_text(self) -> str:
295 def get_text(self) -> str:
296 return self._text_rendered
296 return self._text_rendered
297
297
298 def get_raw_text(self) -> str:
298 def get_raw_text(self) -> str:
299 return self.text
299 return self.text
300
300
301 def get_sync_text(self) -> str:
301 def get_sync_text(self) -> str:
302 """
302 """
303 Returns text applicable for sync. It has absolute post reflinks.
303 Returns text applicable for sync. It has absolute post reflinks.
304 """
304 """
305
305
306 replacements = dict()
306 replacements = dict()
307 for post_id in REGEX_REPLY.findall(self.get_raw_text()):
307 for post_id in REGEX_REPLY.findall(self.get_raw_text()):
308 try:
308 try:
309 absolute_post_id = str(Post.objects.get(id=post_id).global_id)
309 absolute_post_id = str(Post.objects.get(id=post_id).global_id)
310 replacements[post_id] = absolute_post_id
310 replacements[post_id] = absolute_post_id
311 except Post.DoesNotExist:
311 except Post.DoesNotExist:
312 pass
312 pass
313
313
314 text = self.get_raw_text() or ''
314 text = self.get_raw_text() or ''
315 for key in replacements:
315 for key in replacements:
316 text = text.replace('[post]{}[/post]'.format(key),
316 text = text.replace('[post]{}[/post]'.format(key),
317 '[post]{}[/post]'.format(replacements[key]))
317 '[post]{}[/post]'.format(replacements[key]))
318 text = text.replace('\r\n', '\n').replace('\r', '\n')
318 text = text.replace('\r\n', '\n').replace('\r', '\n')
319
319
320 return text
320 return text
321
321
322 def get_tripcode(self):
322 def get_tripcode(self):
323 if self.tripcode:
323 if self.tripcode:
324 return Tripcode(self.tripcode)
324 return Tripcode(self.tripcode)
325
325
326 def get_link_view(self):
326 def get_link_view(self):
327 """
327 """
328 Gets view of a reflink to the post.
328 Gets view of a reflink to the post.
329 """
329 """
330 result = '<a href="{}">&gt;&gt;{}</a>'.format(self.get_absolute_url(),
330 result = '<a href="{}">&gt;&gt;{}</a>'.format(self.get_absolute_url(),
331 self.id)
331 self.id)
332 if self.is_opening():
332 if self.is_opening():
333 result = '<b>{}</b>'.format(result)
333 result = '<b>{}</b>'.format(result)
334
334
335 return result
335 return result
336
336
337 def is_hidden(self) -> bool:
337 def is_hidden(self) -> bool:
338 return self.hidden
338 return self.hidden
339
339
340 def set_hidden(self, hidden):
340 def set_hidden(self, hidden):
341 self.hidden = hidden
341 self.hidden = hidden
342
342
343 def clear_cache(self):
343 def clear_cache(self):
344 """
344 """
345 Clears sync data (content cache, signatures etc).
345 Clears sync data (content cache, signatures etc).
346 """
346 """
347 global_id = self.global_id
347 global_id = self.global_id
348 if global_id is not None and global_id.is_local()\
348 if global_id is not None and global_id.is_local()\
349 and global_id.content is not None:
349 and global_id.content is not None:
350 global_id.clear_cache()
350 global_id.clear_cache()
351
351
352 def get_tags(self):
352 def get_tags(self):
353 return self.get_thread().get_tags()
353 return self.get_thread().get_tags()
354
354
355 def get_ip_color(self):
355 def get_ip_color(self):
356 return hashlib.md5(self.poster_ip.encode()).hexdigest()[:6]
356 return hashlib.md5(self.poster_ip.encode()).hexdigest()[:6]
357
357
358 def has_ip(self):
358 def has_ip(self):
359 return self.poster_ip != NO_IP
359 return self.poster_ip != NO_IP
360
360
@@ -1,125 +1,125 b''
1 import xml.etree.ElementTree as et
1 import xml.etree.ElementTree as et
2
2
3 from django.db import models
3 from django.db import models
4
4
5 from boards.models import KeyPair
5 from boards.models import KeyPair
6
6
7 TAG_MODEL = 'model'
7 TAG_MODEL = 'model'
8 TAG_REQUEST = 'request'
8 TAG_REQUEST = 'request'
9 TAG_ID = 'id'
9 TAG_ID = 'id'
10
10
11 TYPE_GET = 'get'
11 TYPE_GET = 'get'
12 TYPE_LIST = 'list'
12 TYPE_LIST = 'list'
13
13
14 ATTR_VERSION = 'version'
14 ATTR_VERSION = 'version'
15 ATTR_TYPE = 'type'
15 ATTR_TYPE = 'type'
16 ATTR_NAME = 'name'
16 ATTR_NAME = 'name'
17
17
18 ATTR_KEY = 'key'
18 ATTR_KEY = 'key'
19 ATTR_KEY_TYPE = 'type'
19 ATTR_KEY_TYPE = 'type'
20 ATTR_LOCAL_ID = 'local-id'
20 ATTR_LOCAL_ID = 'local-id'
21
21
22
22
23 class GlobalIdManager(models.Manager):
23 class GlobalIdManager(models.Manager):
24 def global_id_exists(self, global_id):
24 def global_id_exists(self, global_id):
25 """
25 """
26 Checks if the same global id already exists in the system.
26 Checks if the same global id already exists in the system.
27 """
27 """
28
28
29 return self.filter(key=global_id.key,
29 return self.filter(key=global_id.key,
30 key_type=global_id.key_type,
30 key_type=global_id.key_type,
31 local_id=global_id.local_id).exists()
31 local_id=global_id.local_id).exists()
32
32
33
33
34 class GlobalId(models.Model):
34 class GlobalId(models.Model):
35 """
35 """
36 Global model ID and cache.
36 Global model ID and cache.
37 Key, key type and local ID make a single global identificator of the model.
37 Key, key type and local ID make a single global identificator of the model.
38 Content is an XML cache of the model that can be passed along between nodes
38 Content is an XML cache of the model that can be passed along between nodes
39 without manual serialization each time.
39 without manual serialization each time.
40 """
40 """
41 class Meta:
41 class Meta:
42 app_label = 'boards'
42 app_label = 'boards'
43
43
44 objects = GlobalIdManager()
44 objects = GlobalIdManager()
45
45
46 def __init__(self, *args, **kwargs):
46 def __init__(self, *args, **kwargs):
47 models.Model.__init__(self, *args, **kwargs)
47 models.Model.__init__(self, *args, **kwargs)
48
48
49 if 'key' in kwargs and 'key_type' in kwargs and 'local_id' in kwargs:
49 if 'key' in kwargs and 'key_type' in kwargs and 'local_id' in kwargs:
50 self.key = kwargs['key']
50 self.key = kwargs['key']
51 self.key_type = kwargs['key_type']
51 self.key_type = kwargs['key_type']
52 self.local_id = kwargs['local_id']
52 self.local_id = kwargs['local_id']
53
53
54 key = models.TextField()
54 key = models.TextField()
55 key_type = models.TextField()
55 key_type = models.TextField()
56 local_id = models.IntegerField()
56 local_id = models.IntegerField()
57 content = models.TextField(blank=True, null=True)
57 content = models.TextField(blank=True, null=True)
58
58
59 def __str__(self):
59 def __str__(self):
60 return '%s::%s::%d' % (self.key_type, self.key, self.local_id)
60 return '%s::%s::%d' % (self.key_type, self.key, self.local_id)
61
61
62 def to_xml_element(self, element: et.Element):
62 def to_xml_element(self, element: et.Element):
63 """
63 """
64 Exports global id to an XML element.
64 Exports global id to an XML element.
65 """
65 """
66
66
67 element.set(ATTR_KEY, self.key)
67 element.set(ATTR_KEY, self.key)
68 element.set(ATTR_KEY_TYPE, self.key_type)
68 element.set(ATTR_KEY_TYPE, self.key_type)
69 element.set(ATTR_LOCAL_ID, str(self.local_id))
69 element.set(ATTR_LOCAL_ID, str(self.local_id))
70
70
71 @staticmethod
71 @staticmethod
72 def from_xml_element(element: et.Element):
72 def from_xml_element(element: et.Element):
73 """
73 """
74 Parses XML id tag and gets global id from it.
74 Parses XML id tag and gets global id from it.
75
75
76 Arguments:
76 Arguments:
77 element -- the XML 'id' element
77 element -- the XML 'id' element
78
78
79 Returns:
79 Returns:
80 global_id -- id itself
80 global_id -- id itself
81 exists -- True if the global id was taken from database, False if it
81 exists -- True if the global id was taken from database, False if it
82 did not exist and was created.
82 did not exist and was created.
83 """
83 """
84
84
85 try:
85 try:
86 return GlobalId.objects.get(key=element.get(ATTR_KEY),
86 return GlobalId.objects.get(key=element.get(ATTR_KEY),
87 key_type=element.get(ATTR_KEY_TYPE),
87 key_type=element.get(ATTR_KEY_TYPE),
88 local_id=int(element.get(
88 local_id=int(element.get(
89 ATTR_LOCAL_ID))), True
89 ATTR_LOCAL_ID))), True
90 except GlobalId.DoesNotExist:
90 except GlobalId.DoesNotExist:
91 return GlobalId(key=element.get(ATTR_KEY),
91 return GlobalId(key=element.get(ATTR_KEY),
92 key_type=element.get(ATTR_KEY_TYPE),
92 key_type=element.get(ATTR_KEY_TYPE),
93 local_id=int(element.get(ATTR_LOCAL_ID))), False
93 local_id=int(element.get(ATTR_LOCAL_ID))), False
94
94
95 def is_local(self):
95 def is_local(self):
96 """Checks fo the ID is local model's"""
96 """Checks fo the ID is local model's"""
97 return KeyPair.objects.filter(
97 return KeyPair.objects.filter(
98 key_type=self.key_type, public_key=self.key).exists()
98 key_type=self.key_type, public_key=self.key).exists()
99
99
100 def clear_cache(self):
100 def clear_cache(self):
101 """
101 """
102 Removes content cache and signatures.
102 Removes content cache and signatures.
103 """
103 """
104 self.content = None
104 self.content = None
105 self.save()
105 self.save()
106 self.signature_set.all().delete()
106 self.signature_set.all().delete()
107
107
108
108
109 class Signature(models.Model):
109 class Signature(models.Model):
110 class Meta:
110 class Meta:
111 app_label = 'boards'
111 app_label = 'boards'
112
112
113 def __init__(self, *args, **kwargs):
113 def __init__(self, *args, **kwargs):
114 models.Model.__init__(self, *args, **kwargs)
114 models.Model.__init__(self, *args, **kwargs)
115
115
116 if 'key' in kwargs and 'key_type' in kwargs and 'signature' in kwargs:
116 if 'key' in kwargs and 'key_type' in kwargs and 'signature' in kwargs:
117 self.key_type = kwargs['key_type']
117 self.key_type = kwargs['key_type']
118 self.key = kwargs['key']
118 self.key = kwargs['key']
119 self.signature = kwargs['signature']
119 self.signature = kwargs['signature']
120
120
121 key_type = models.TextField()
121 key_type = models.TextField()
122 key = models.TextField()
122 key = models.TextField()
123 signature = models.TextField()
123 signature = models.TextField()
124
124
125 global_id = models.ForeignKey('GlobalId')
125 global_id = models.ForeignKey('GlobalId', on_delete=models.CASCADE)
@@ -1,84 +1,84 b''
1 import feedparser
1 import feedparser
2 import logging
2 import logging
3 import calendar
3 import calendar
4
4
5 from time import mktime
5 from time import mktime
6 from datetime import datetime
6 from datetime import datetime
7
7
8 from django.db import models, transaction
8 from django.db import models, transaction
9 from django.utils.dateparse import parse_datetime
9 from django.utils.dateparse import parse_datetime
10 from django.utils.timezone import utc
10 from django.utils.timezone import utc
11 from django.utils import timezone
11 from django.utils import timezone
12 from django.utils.html import strip_tags
12 from django.utils.html import strip_tags
13
13
14 from boards.models import Post
14 from boards.models import Post
15 from boards.models.post import TITLE_MAX_LENGTH
15 from boards.models.post import TITLE_MAX_LENGTH
16 from boards.utils import get_tripcode_from_text
16 from boards.utils import get_tripcode_from_text
17 from boards import settings
17 from boards import settings
18
18
19
19
20 SOURCE_TYPE_MAX_LENGTH = 100
20 SOURCE_TYPE_MAX_LENGTH = 100
21 SOURCE_TYPE_RSS = 'RSS'
21 SOURCE_TYPE_RSS = 'RSS'
22 TYPE_CHOICES = (
22 TYPE_CHOICES = (
23 (SOURCE_TYPE_RSS, SOURCE_TYPE_RSS),
23 (SOURCE_TYPE_RSS, SOURCE_TYPE_RSS),
24 )
24 )
25
25
26
26
27 class ThreadSource(models.Model):
27 class ThreadSource(models.Model):
28 class Meta:
28 class Meta:
29 app_label = 'boards'
29 app_label = 'boards'
30
30
31 name = models.TextField()
31 name = models.TextField()
32 thread = models.ForeignKey('Thread')
32 thread = models.ForeignKey('Thread', on_delete=models.CASCADE)
33 timestamp = models.DateTimeField()
33 timestamp = models.DateTimeField()
34 source = models.TextField()
34 source = models.TextField()
35 source_type = models.CharField(max_length=SOURCE_TYPE_MAX_LENGTH,
35 source_type = models.CharField(max_length=SOURCE_TYPE_MAX_LENGTH,
36 choices=TYPE_CHOICES)
36 choices=TYPE_CHOICES)
37
37
38 def __str__(self):
38 def __str__(self):
39 return self.name
39 return self.name
40
40
41 @transaction.atomic
41 @transaction.atomic
42 def fetch_latest_posts(self):
42 def fetch_latest_posts(self):
43 """Creates new posts with the info fetched since the timestamp."""
43 """Creates new posts with the info fetched since the timestamp."""
44 logger = logging.getLogger('boards.source')
44 logger = logging.getLogger('boards.source')
45
45
46 if self.thread.is_archived():
46 if self.thread.is_archived():
47 logger.error('The thread {} is archived, please try another one'.format(self.thread))
47 logger.error('The thread {} is archived, please try another one'.format(self.thread))
48 else:
48 else:
49 tripcode = get_tripcode_from_text(
49 tripcode = get_tripcode_from_text(
50 settings.get('External', 'SourceFetcherTripcode'))
50 settings.get('External', 'SourceFetcherTripcode'))
51 start_timestamp = self.timestamp
51 start_timestamp = self.timestamp
52 last_timestamp = start_timestamp
52 last_timestamp = start_timestamp
53 logger.info('Start timestamp is {}'.format(start_timestamp))
53 logger.info('Start timestamp is {}'.format(start_timestamp))
54 if self.thread.is_bumplimit():
54 if self.thread.is_bumplimit():
55 logger.warn('The thread {} has reached its bumplimit, please create a new one'.format(self.thread))
55 logger.warn('The thread {} has reached its bumplimit, please create a new one'.format(self.thread))
56 if self.source_type == SOURCE_TYPE_RSS:
56 if self.source_type == SOURCE_TYPE_RSS:
57 feed = feedparser.parse(self.source)
57 feed = feedparser.parse(self.source)
58 items = sorted(feed.entries, key=lambda entry: entry.published_parsed)
58 items = sorted(feed.entries, key=lambda entry: entry.published_parsed)
59 for item in items:
59 for item in items:
60 title = self.strip_title(item.title, TITLE_MAX_LENGTH)
60 title = self.strip_title(item.title, TITLE_MAX_LENGTH)
61 timestamp = datetime.fromtimestamp(calendar.timegm(item.published_parsed), tz=utc)
61 timestamp = datetime.fromtimestamp(calendar.timegm(item.published_parsed), tz=utc)
62 if not timestamp:
62 if not timestamp:
63 logger.error('Invalid timestamp {} for {}'.format(item.published, title))
63 logger.error('Invalid timestamp {} for {}'.format(item.published, title))
64 else:
64 else:
65 if timestamp > last_timestamp:
65 if timestamp > last_timestamp:
66 last_timestamp = timestamp
66 last_timestamp = timestamp
67 if timestamp > start_timestamp:
67 if timestamp > start_timestamp:
68 Post.objects.create_post(title=title, text=self.parse_text(item.description),
68 Post.objects.create_post(title=title, text=self.parse_text(item.description),
69 thread=self.thread, file_urls=[item.link], tripcode=tripcode)
69 thread=self.thread, file_urls=[item.link], tripcode=tripcode)
70 logger.info('Fetched item {} from {} into thread {}'.format(
70 logger.info('Fetched item {} from {} into thread {}'.format(
71 title, self.name, self.thread))
71 title, self.name, self.thread))
72 logger.info('New timestamp is {}'.format(last_timestamp))
72 logger.info('New timestamp is {}'.format(last_timestamp))
73 self.timestamp = last_timestamp
73 self.timestamp = last_timestamp
74 self.save(update_fields=['timestamp'])
74 self.save(update_fields=['timestamp'])
75
75
76 def parse_text(self, text):
76 def parse_text(self, text):
77 return strip_tags(text)
77 return strip_tags(text)
78
78
79 def strip_title(self, title, max_length):
79 def strip_title(self, title, max_length):
80 result = title
80 result = title
81 if len(title) > max_length:
81 if len(title) > max_length:
82 result = title[:max_length - 1] + '…'
82 result = title[:max_length - 1] + '…'
83 return result
83 return result
84
84
@@ -1,212 +1,212 b''
1 import hashlib
1 import hashlib
2 from django.core.urlresolvers import reverse
2 from django.urls import reverse
3 from django.db import models
3 from django.db import models
4 from django.db.models import Count, Q
4 from django.db.models import Count, Q
5 from django.utils.translation import get_language
5 from django.utils.translation import get_language
6
6
7 import boards
7 import boards
8 from boards.models import Attachment
8 from boards.models import Attachment
9 from boards.models.attachment import FILE_TYPES_IMAGE
9 from boards.models.attachment import FILE_TYPES_IMAGE
10 from boards.models.base import Viewable
10 from boards.models.base import Viewable
11 from boards.models.thread import STATUS_ACTIVE, STATUS_BUMPLIMIT, STATUS_ARCHIVE
11 from boards.models.thread import STATUS_ACTIVE, STATUS_BUMPLIMIT, STATUS_ARCHIVE
12 from boards.utils import cached_result
12 from boards.utils import cached_result
13
13
14 __author__ = 'neko259'
14 __author__ = 'neko259'
15
15
16
16
17 RELATED_TAGS_COUNT = 5
17 RELATED_TAGS_COUNT = 5
18 DEFAULT_LOCALE = 'default'
18 DEFAULT_LOCALE = 'default'
19
19
20
20
21 class TagAliasManager(models.Manager):
21 class TagAliasManager(models.Manager):
22 def filter_localized(self, *args, **kwargs):
22 def filter_localized(self, *args, **kwargs):
23 locale = get_language()
23 locale = get_language()
24 tag_aliases = (self.filter(locale=locale)
24 tag_aliases = (self.filter(locale=locale)
25 | self.filter(Q(locale=DEFAULT_LOCALE)
25 | self.filter(Q(locale=DEFAULT_LOCALE)
26 & ~Q(parent__aliases__locale=locale)))
26 & ~Q(parent__aliases__locale=locale)))
27 return tag_aliases.filter(**kwargs)
27 return tag_aliases.filter(**kwargs)
28
28
29
29
30 class TagAlias(models.Model, Viewable):
30 class TagAlias(models.Model, Viewable):
31 class Meta:
31 class Meta:
32 app_label = 'boards'
32 app_label = 'boards'
33 ordering = ('name',)
33 ordering = ('name',)
34
34
35 objects = TagAliasManager()
35 objects = TagAliasManager()
36
36
37 name = models.CharField(max_length=100, db_index=True)
37 name = models.CharField(max_length=100, db_index=True)
38 locale = models.CharField(max_length=10, db_index=True)
38 locale = models.CharField(max_length=10, db_index=True)
39
39
40 parent = models.ForeignKey('Tag', null=True, blank=True,
40 parent = models.ForeignKey('Tag', on_delete=models.CASCADE, null=True,
41 related_name='aliases')
41 blank=True, related_name='aliases')
42
42
43
43
44 class TagManager(models.Manager):
44 class TagManager(models.Manager):
45 def get_not_empty_tags(self):
45 def get_not_empty_tags(self):
46 """
46 """
47 Gets tags that have non-archived threads.
47 Gets tags that have non-archived threads.
48 """
48 """
49
49
50 return self.annotate(num_threads=Count('thread_tags'))\
50 return self.annotate(num_threads=Count('thread_tags'))\
51 .filter(num_threads__gt=0)\
51 .filter(num_threads__gt=0)\
52 .filter(aliases__in=TagAlias.objects.filter_localized())\
52 .filter(aliases__in=TagAlias.objects.filter_localized())\
53 .order_by('aliases__name')
53 .order_by('aliases__name')
54
54
55 def get_tag_url_list(self, tags: list) -> str:
55 def get_tag_url_list(self, tags: list) -> str:
56 """
56 """
57 Gets a comma-separated list of tag links.
57 Gets a comma-separated list of tag links.
58 """
58 """
59
59
60 return ', '.join([tag.get_view() for tag in tags])
60 return ', '.join([tag.get_view() for tag in tags])
61
61
62 def get_by_alias(self, alias):
62 def get_by_alias(self, alias):
63 tag = None
63 tag = None
64 aliases = TagAlias.objects.filter(name=alias).all()
64 aliases = TagAlias.objects.filter(name=alias).all()
65 if aliases:
65 if aliases:
66 tag = aliases[0].parent
66 tag = aliases[0].parent
67
67
68 return tag
68 return tag
69
69
70 def get_or_create_with_alias(self, name, required=False):
70 def get_or_create_with_alias(self, name, required=False):
71 tag = self.get_by_alias(name)
71 tag = self.get_by_alias(name)
72 created = False
72 created = False
73 if not tag:
73 if not tag:
74 tag = self.create(required=required)
74 tag = self.create(required=required)
75 TagAlias.objects.create(name=name, locale=DEFAULT_LOCALE, parent=tag)
75 TagAlias.objects.create(name=name, locale=DEFAULT_LOCALE, parent=tag)
76 created = True
76 created = True
77 return tag, created
77 return tag, created
78
78
79
79
80 class Tag(models.Model, Viewable):
80 class Tag(models.Model, Viewable):
81 """
81 """
82 A tag is a text node assigned to the thread. The tag serves as a board
82 A tag is a text node assigned to the thread. The tag serves as a board
83 section. There can be multiple tags for each thread
83 section. There can be multiple tags for each thread
84 """
84 """
85
85
86 objects = TagManager()
86 objects = TagManager()
87
87
88 class Meta:
88 class Meta:
89 app_label = 'boards'
89 app_label = 'boards'
90
90
91 required = models.BooleanField(default=False, db_index=True)
91 required = models.BooleanField(default=False, db_index=True)
92 description = models.TextField(blank=True)
92 description = models.TextField(blank=True)
93
93
94 parent = models.ForeignKey('Tag', null=True, blank=True,
94 parent = models.ForeignKey('Tag', on_delete=models.CASCADE, null=True,
95 related_name='children')
95 blank=True, related_name='children')
96
96
97 def get_name(self):
97 def get_name(self):
98 return self.aliases.get(locale=DEFAULT_LOCALE).name
98 return self.aliases.get(locale=DEFAULT_LOCALE).name
99
99
100 def __str__(self):
100 def __str__(self):
101 return self.get_name()
101 return self.get_name()
102
102
103 def is_empty(self) -> bool:
103 def is_empty(self) -> bool:
104 """
104 """
105 Checks if the tag has some threads.
105 Checks if the tag has some threads.
106 """
106 """
107
107
108 return self.get_thread_count() == 0
108 return self.get_thread_count() == 0
109
109
110 def get_thread_count(self, status=None) -> int:
110 def get_thread_count(self, status=None) -> int:
111 threads = self.get_threads()
111 threads = self.get_threads()
112 if status is not None:
112 if status is not None:
113 threads = threads.filter(status=status)
113 threads = threads.filter(status=status)
114 return threads.count()
114 return threads.count()
115
115
116 def get_active_thread_count(self) -> int:
116 def get_active_thread_count(self) -> int:
117 return self.get_thread_count(status=STATUS_ACTIVE)
117 return self.get_thread_count(status=STATUS_ACTIVE)
118
118
119 def get_bumplimit_thread_count(self) -> int:
119 def get_bumplimit_thread_count(self) -> int:
120 return self.get_thread_count(status=STATUS_BUMPLIMIT)
120 return self.get_thread_count(status=STATUS_BUMPLIMIT)
121
121
122 def get_archived_thread_count(self) -> int:
122 def get_archived_thread_count(self) -> int:
123 return self.get_thread_count(status=STATUS_ARCHIVE)
123 return self.get_thread_count(status=STATUS_ARCHIVE)
124
124
125 @cached_result()
125 @cached_result()
126 def get_absolute_url(self):
126 def get_absolute_url(self):
127 return reverse('tag', kwargs={'tag_name': self.get_name()})
127 return reverse('tag', kwargs={'tag_name': self.get_name()})
128
128
129 def get_threads(self):
129 def get_threads(self):
130 return self.thread_tags.order_by('-bump_time')
130 return self.thread_tags.order_by('-bump_time')
131
131
132 def is_required(self):
132 def is_required(self):
133 return self.required
133 return self.required
134
134
135 def _get_locale_cache_key(self):
135 def _get_locale_cache_key(self):
136 return '{}_{}'.format(self.id, get_language())
136 return '{}_{}'.format(self.id, get_language())
137
137
138 @cached_result(key_method=_get_locale_cache_key)
138 @cached_result(key_method=_get_locale_cache_key)
139 def get_localized_name(self):
139 def get_localized_name(self):
140 locale = get_language()
140 locale = get_language()
141
141
142 aliases = self.aliases.filter(Q(locale=locale) | Q(locale=DEFAULT_LOCALE))
142 aliases = self.aliases.filter(Q(locale=locale) | Q(locale=DEFAULT_LOCALE))
143
143
144 localized_tag_name = None
144 localized_tag_name = None
145 default_tag_name = None
145 default_tag_name = None
146
146
147 for alias in aliases:
147 for alias in aliases:
148 if alias.locale == locale:
148 if alias.locale == locale:
149 localized_tag_name = alias.name
149 localized_tag_name = alias.name
150 elif alias.locale == DEFAULT_LOCALE:
150 elif alias.locale == DEFAULT_LOCALE:
151 default_tag_name = alias.name
151 default_tag_name = alias.name
152
152
153 return localized_tag_name if localized_tag_name else default_tag_name
153 return localized_tag_name if localized_tag_name else default_tag_name
154
154
155 def get_view(self):
155 def get_view(self):
156 name = self.get_localized_name()
156 name = self.get_localized_name()
157 link = '<a class="tag" href="{}">{}</a>'.format(
157 link = '<a class="tag" href="{}">{}</a>'.format(
158 self.get_absolute_url(), name)
158 self.get_absolute_url(), name)
159 if self.is_required():
159 if self.is_required():
160 link = '<b>{}</b>'.format(link)
160 link = '<b>{}</b>'.format(link)
161 return link
161 return link
162
162
163 @cached_result()
163 @cached_result()
164 def get_post_count(self):
164 def get_post_count(self):
165 return self.get_threads().aggregate(num_posts=Count('replies'))['num_posts']
165 return self.get_threads().aggregate(num_posts=Count('replies'))['num_posts']
166
166
167 def get_description(self):
167 def get_description(self):
168 return self.description
168 return self.description
169
169
170 def get_random_image_post(self, status=[STATUS_ACTIVE, STATUS_BUMPLIMIT]):
170 def get_random_image_post(self, status=[STATUS_ACTIVE, STATUS_BUMPLIMIT]):
171 posts = boards.models.Post.objects.filter(attachments__mimetype__in=FILE_TYPES_IMAGE)\
171 posts = boards.models.Post.objects.filter(attachments__mimetype__in=FILE_TYPES_IMAGE)\
172 .annotate(images_count=Count(
172 .annotate(images_count=Count(
173 'attachments')).filter(images_count__gt=0, thread__tags__in=[self])
173 'attachments')).filter(images_count__gt=0, thread__tags__in=[self])
174 if status is not None:
174 if status is not None:
175 posts = posts.filter(thread__status__in=status)
175 posts = posts.filter(thread__status__in=status)
176 return posts.order_by('?').first()
176 return posts.order_by('?').first()
177
177
178 def get_first_letter(self):
178 def get_first_letter(self):
179 name = self.get_localized_name()
179 name = self.get_localized_name()
180 return name and name[0] or ''
180 return name and name[0] or ''
181
181
182 def get_related_tags(self):
182 def get_related_tags(self):
183 return set(Tag.objects.filter(thread_tags__in=self.get_threads()).exclude(
183 return set(Tag.objects.filter(thread_tags__in=self.get_threads()).exclude(
184 id=self.id).order_by('?')[:RELATED_TAGS_COUNT])
184 id=self.id).order_by('?')[:RELATED_TAGS_COUNT])
185
185
186 @cached_result()
186 @cached_result()
187 def get_color(self):
187 def get_color(self):
188 """
188 """
189 Gets color hashed from the tag name.
189 Gets color hashed from the tag name.
190 """
190 """
191 return hashlib.md5(self.get_name().encode()).hexdigest()[:6]
191 return hashlib.md5(self.get_name().encode()).hexdigest()[:6]
192
192
193 def get_parent(self):
193 def get_parent(self):
194 return self.parent
194 return self.parent
195
195
196 def get_all_parents(self):
196 def get_all_parents(self):
197 parents = list()
197 parents = list()
198 parent = self.get_parent()
198 parent = self.get_parent()
199 if parent and parent not in parents:
199 if parent and parent not in parents:
200 parents.insert(0, parent)
200 parents.insert(0, parent)
201 parents = parent.get_all_parents() + parents
201 parents = parent.get_all_parents() + parents
202
202
203 return parents
203 return parents
204
204
205 def get_children(self):
205 def get_children(self):
206 return self.children
206 return self.children
207
207
208 def get_images(self):
208 def get_images(self):
209 return Attachment.objects.filter(
209 return Attachment.objects.filter(
210 attachment_posts__thread__tags__in=[self]).filter(
210 attachment_posts__thread__tags__in=[self]).filter(
211 mimetype__in=FILE_TYPES_IMAGE).order_by('-attachment_posts__pub_time')
211 mimetype__in=FILE_TYPES_IMAGE).order_by('-attachment_posts__pub_time')
212
212
@@ -1,45 +1,45 b''
1 from django.db import models
1 from django.db import models
2 import boards
2 import boards
3
3
4 __author__ = 'neko259'
4 __author__ = 'neko259'
5
5
6 BAN_REASON_AUTO = 'Auto'
6 BAN_REASON_AUTO = 'Auto'
7 BAN_REASON_MAX_LENGTH = 200
7 BAN_REASON_MAX_LENGTH = 200
8
8
9
9
10 class Ban(models.Model):
10 class Ban(models.Model):
11
11
12 class Meta:
12 class Meta:
13 app_label = 'boards'
13 app_label = 'boards'
14
14
15 ip = models.GenericIPAddressField()
15 ip = models.GenericIPAddressField()
16 reason = models.CharField(default=BAN_REASON_AUTO,
16 reason = models.CharField(default=BAN_REASON_AUTO,
17 max_length=BAN_REASON_MAX_LENGTH)
17 max_length=BAN_REASON_MAX_LENGTH)
18 can_read = models.BooleanField(default=True)
18 can_read = models.BooleanField(default=True)
19
19
20 def __str__(self):
20 def __str__(self):
21 return self.ip
21 return self.ip
22
22
23
23
24 class NotificationManager(models.Manager):
24 class NotificationManager(models.Manager):
25 def get_notification_posts(self, usernames: list, last: int = None):
25 def get_notification_posts(self, usernames: list, last: int = None):
26 lower_names = [username.lower() for username in usernames]
26 lower_names = [username.lower() for username in usernames]
27 posts = boards.models.post.Post.objects.filter(
27 posts = boards.models.post.Post.objects.filter(
28 notification__name__in=lower_names).distinct()
28 notification__name__in=lower_names).distinct()
29 if last is not None:
29 if last is not None:
30 posts = posts.filter(id__gt=last)
30 posts = posts.filter(id__gt=last)
31 posts = posts.order_by('-id')
31 posts = posts.order_by('-id')
32
32
33 return posts
33 return posts
34
34
35
35
36 class Notification(models.Model):
36 class Notification(models.Model):
37
37
38 class Meta:
38 class Meta:
39 app_label = 'boards'
39 app_label = 'boards'
40
40
41 objects = NotificationManager()
41 objects = NotificationManager()
42
42
43 post = models.ForeignKey('Post')
43 post = models.ForeignKey('Post', on_delete=models.CASCADE)
44 name = models.TextField()
44 name = models.TextField()
45
45
@@ -1,100 +1,96 b''
1 import re
1 import re
2
2 from django import template
3 from django.shortcuts import get_object_or_404
3 from django.shortcuts import get_object_or_404
4 from django import template
5 from django.utils.text import re_tag
4 from django.utils.text import re_tag
6 from django.core.urlresolvers import reverse
7
5
8 from boards.mdx_neboard import LINE_BREAK_HTML
6 from boards.mdx_neboard import LINE_BREAK_HTML
9 from boards import settings
10
11
7
12 IMG_ACTION_URL = '[<a href="{}">{}</a>]'
8 IMG_ACTION_URL = '[<a href="{}">{}</a>]'
13 REGEX_NEWLINE = re.compile(LINE_BREAK_HTML)
9 REGEX_NEWLINE = re.compile(LINE_BREAK_HTML)
14 TRUNCATOR = '...'
10 TRUNCATOR = '...'
15 HTML4_SINGLETS =(
11 HTML4_SINGLETS =(
16 'br', 'col', 'link', 'base', 'img', 'param', 'area', 'hr', 'input'
12 'br', 'col', 'link', 'base', 'img', 'param', 'area', 'hr', 'input'
17 )
13 )
18
14
19
15
20 register = template.Library()
16 register = template.Library()
21
17
22
18
23 @register.simple_tag(name='post_url')
19 @register.simple_tag(name='post_url')
24 def post_url(*args, **kwargs):
20 def post_url(*args, **kwargs):
25 post_id = args[0]
21 post_id = args[0]
26
22
27 post = get_object_or_404('Post', id=post_id)
23 post = get_object_or_404('Post', id=post_id)
28
24
29 return post.get_absolute_url()
25 return post.get_absolute_url()
30
26
31
27
32 @register.inclusion_tag('boards/post.html', name='post_view', takes_context=True)
28 @register.inclusion_tag('boards/post.html', name='post_view', takes_context=True)
33 def post_view(context, post, *args, **kwargs):
29 def post_view(context, post, *args, **kwargs):
34 kwargs['perms'] = context['perms']
30 kwargs['perms'] = context['perms']
35 return post.get_view_params(*args, **kwargs)
31 return post.get_view_params(*args, **kwargs)
36
32
37
33
38 @register.simple_tag(name='page_url')
34 @register.simple_tag(name='page_url')
39 def page_url(paginator, page_number, *args, **kwargs):
35 def page_url(paginator, page_number, *args, **kwargs):
40 if paginator.supports_urls():
36 if paginator.supports_urls():
41 return paginator.get_page_url(page_number)
37 return paginator.get_page_url(page_number)
42
38
43
39
44 @register.filter(name='truncatenewlines_html')
40 @register.filter(name='truncatenewlines_html')
45 def truncatenewlines_html(value, arg):
41 def truncatenewlines_html(value, arg):
46 end_pos = 0
42 end_pos = 0
47 start_pos = 0
43 start_pos = 0
48 match_count = 0
44 match_count = 0
49
45
50 # Collect places for truncation
46 # Collect places for truncation
51 while match_count <= arg:
47 while match_count <= arg:
52 m = REGEX_NEWLINE.search(value, end_pos)
48 m = REGEX_NEWLINE.search(value, end_pos)
53 if m is None:
49 if m is None:
54 break
50 break
55 else:
51 else:
56 match_count += 1
52 match_count += 1
57 end_pos = m.end()
53 end_pos = m.end()
58 start_pos = m.start()
54 start_pos = m.start()
59
55
60 # Find and close open tags
56 # Find and close open tags
61 if match_count > arg:
57 if match_count > arg:
62 truncate_pos = start_pos
58 truncate_pos = start_pos
63
59
64 open_tags = []
60 open_tags = []
65 text = value[:truncate_pos]
61 text = value[:truncate_pos]
66 current_pos = 0
62 current_pos = 0
67 while True:
63 while True:
68 tag = re_tag.search(text, current_pos)
64 tag = re_tag.search(text, current_pos)
69 if tag is None:
65 if tag is None:
70 break
66 break
71 else:
67 else:
72 closing_tag, tagname, self_closing = tag.groups()
68 closing_tag, tagname, self_closing = tag.groups()
73 tagname = tagname.lower()
69 tagname = tagname.lower()
74 if self_closing or tagname in HTML4_SINGLETS:
70 if self_closing or tagname in HTML4_SINGLETS:
75 pass
71 pass
76 elif closing_tag:
72 elif closing_tag:
77 # Check for match in open tags list
73 # Check for match in open tags list
78 try:
74 try:
79 i = open_tags.index(tagname)
75 i = open_tags.index(tagname)
80 except ValueError:
76 except ValueError:
81 pass
77 pass
82 else:
78 else:
83 # SGML: An end tag closes, back to the matching start tag,
79 # SGML: An end tag closes, back to the matching start tag,
84 # all unclosed intervening start tags with omitted end tags
80 # all unclosed intervening start tags with omitted end tags
85 open_tags = open_tags[i + 1:]
81 open_tags = open_tags[i + 1:]
86 else:
82 else:
87 # Add it to the start of the open tags list
83 # Add it to the start of the open tags list
88 open_tags.insert(0, tagname)
84 open_tags.insert(0, tagname)
89
85
90 current_pos = tag.end()
86 current_pos = tag.end()
91
87
92 if not text.endswith(TRUNCATOR):
88 if not text.endswith(TRUNCATOR):
93 text += TRUNCATOR
89 text += TRUNCATOR
94 for tag in open_tags:
90 for tag in open_tags:
95 text += '</{}>'.format(tag)
91 text += '</{}>'.format(tag)
96 else:
92 else:
97 text = value
93 text = value
98
94
99 return text
95 return text
100
96
@@ -1,96 +1,93 b''
1 from django.conf.urls import url
1 from django.conf.urls import url
2 from django.urls import path
3 from django.views.i18n import JavaScriptCatalog
2
4
3 from boards import views
5 from boards import views
4 from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed
6 from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed
5 from boards.views import api, tag_threads, all_threads, settings, feed, stickers
7 from boards.views import api, tag_threads, all_threads, settings, feed, stickers
6 from boards.views.authors import AuthorsView
8 from boards.views.authors import AuthorsView
7 from boards.views.landing import LandingView
9 from boards.views.landing import LandingView
8 from boards.views.notifications import NotificationView
10 from boards.views.notifications import NotificationView
9 from boards.views.preview import PostPreviewView
11 from boards.views.preview import PostPreviewView
10 from boards.views.random import RandomImageView
12 from boards.views.random import RandomImageView
11 from boards.views.search import BoardSearchView
13 from boards.views.search import BoardSearchView
12 from boards.views.static import StaticPageView
14 from boards.views.static import StaticPageView
13 from boards.views.sync import get_post_sync_data, response_get, response_list
15 from boards.views.sync import get_post_sync_data, response_get, response_list
14 from boards.views.tag_gallery import TagGalleryView
16 from boards.views.tag_gallery import TagGalleryView
15 from boards.views.translation import cached_javascript_catalog
16 from boards.views.utils import UtilsView
17 from boards.views.utils import UtilsView
17
18
18 js_info_dict = {
19 'packages': ('boards',),
20 }
21
19
22 urlpatterns = [
20 urlpatterns = [
23 # /boards/
21 # /boards/
24 url(r'^all/$', all_threads.AllThreadsView.as_view(), name='index'),
22 url(r'^all/$', all_threads.AllThreadsView.as_view(), name='index'),
25
23
26 # /boards/tag/tag_name/
24 # /boards/tag/tag_name/
27 url(r'^tag/(?P<tag_name>[\w\d\']+)/$', tag_threads.TagView.as_view(),
25 url(r'^tag/(?P<tag_name>[\w\d\']+)/$', tag_threads.TagView.as_view(),
28 name='tag'),
26 name='tag'),
29 url(r'^tag/(?P<tag_name>[\w\d\']+)/gallery/$', TagGalleryView.as_view(), name='tag_gallery'),
27 url(r'^tag/(?P<tag_name>[\w\d\']+)/gallery/$', TagGalleryView.as_view(), name='tag_gallery'),
30
28
31 # /boards/thread/
29 # /boards/thread/
32 url(r'^thread/(?P<post_id>\d+)/$', views.thread.NormalThreadView.as_view(),
30 url(r'^thread/(?P<post_id>\d+)/$', views.thread.NormalThreadView.as_view(),
33 name='thread'),
31 name='thread'),
34 url(r'^thread/(?P<post_id>\d+)/mode/gallery/$', views.thread.GalleryThreadView.as_view(),
32 url(r'^thread/(?P<post_id>\d+)/mode/gallery/$', views.thread.GalleryThreadView.as_view(),
35 name='thread_gallery'),
33 name='thread_gallery'),
36 url(r'^thread/(?P<post_id>\d+)/mode/tree/$', views.thread.TreeThreadView.as_view(),
34 url(r'^thread/(?P<post_id>\d+)/mode/tree/$', views.thread.TreeThreadView.as_view(),
37 name='thread_tree'),
35 name='thread_tree'),
38 # /feed/
36 # /feed/
39 url(r'^feed/$', views.feed.FeedView.as_view(), name='feed'),
37 url(r'^feed/$', views.feed.FeedView.as_view(), name='feed'),
40
38
41 url(r'^settings/$', settings.SettingsView.as_view(), name='settings'),
39 url(r'^settings/$', settings.SettingsView.as_view(), name='settings'),
42 url(r'^stickers/$', stickers.AliasesView.as_view(), name='stickers'),
40 url(r'^stickers/$', stickers.AliasesView.as_view(), name='stickers'),
43 url(r'^stickers/(?P<category>\w+)/$', stickers.AliasesView.as_view(), name='stickers'),
41 url(r'^stickers/(?P<category>\w+)/$', stickers.AliasesView.as_view(), name='stickers'),
44 url(r'^authors/$', AuthorsView.as_view(), name='authors'),
42 url(r'^authors/$', AuthorsView.as_view(), name='authors'),
45
43
46 url(r'^banned/$', views.banned.BannedView.as_view(), name='banned'),
44 url(r'^banned/$', views.banned.BannedView.as_view(), name='banned'),
47 url(r'^staticpage/(?P<name>\w+)/$', StaticPageView.as_view(),
45 url(r'^staticpage/(?P<name>\w+)/$', StaticPageView.as_view(),
48 name='staticpage'),
46 name='staticpage'),
49
47
50 url(r'^random/$', RandomImageView.as_view(), name='random'),
48 url(r'^random/$', RandomImageView.as_view(), name='random'),
51 url(r'^search/$', BoardSearchView.as_view(), name='search'),
49 url(r'^search/$', BoardSearchView.as_view(), name='search'),
52 url(r'^$', LandingView.as_view(), name='landing'),
50 url(r'^$', LandingView.as_view(), name='landing'),
53 url(r'^utils$', UtilsView.as_view(), name='utils'),
51 url(r'^utils$', UtilsView.as_view(), name='utils'),
54
52
55 # RSS feeds
53 # RSS feeds
56 url(r'^rss/$', AllThreadsFeed()),
54 url(r'^rss/$', AllThreadsFeed()),
57 url(r'^all/rss/$', AllThreadsFeed()),
55 url(r'^all/rss/$', AllThreadsFeed()),
58 url(r'^page/(?P<page>\d+)/rss/$', AllThreadsFeed()),
56 url(r'^page/(?P<page>\d+)/rss/$', AllThreadsFeed()),
59 url(r'^tag/(?P<tag_name>\w+)/rss/$', TagThreadsFeed()),
57 url(r'^tag/(?P<tag_name>\w+)/rss/$', TagThreadsFeed()),
60 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/rss/$', TagThreadsFeed()),
58 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/rss/$', TagThreadsFeed()),
61 url(r'^thread/(?P<post_id>\d+)/rss/$', ThreadPostsFeed()),
59 url(r'^thread/(?P<post_id>\d+)/rss/$', ThreadPostsFeed()),
62
60
63 # i18n
61 # i18n
64 url(r'^jsi18n/$', cached_javascript_catalog, js_info_dict,
62 path('jsi18n/', JavaScriptCatalog.as_view(packages=['boards']), name='js_info_dict'),
65 name='js_info_dict'),
66
63
67 # API
64 # API
68 url(r'^api/post/(?P<post_id>\d+)/$', api.get_post, name="get_post"),
65 url(r'^api/post/(?P<post_id>\d+)/$', api.get_post, name="get_post"),
69 url(r'^api/diff_thread/$', api.api_get_threaddiff, name="get_thread_diff"),
66 url(r'^api/diff_thread/$', api.api_get_threaddiff, name="get_thread_diff"),
70 url(r'^api/threads/(?P<count>\w+)/$', api.api_get_threads,
67 url(r'^api/threads/(?P<count>\w+)/$', api.api_get_threads,
71 name='get_threads'),
68 name='get_threads'),
72 url(r'^api/tags/$', api.api_get_tags, name='get_tags'),
69 url(r'^api/tags/$', api.api_get_tags, name='get_tags'),
73 url(r'^api/thread/(?P<opening_post_id>\w+)/$', api.api_get_thread_posts,
70 url(r'^api/thread/(?P<opening_post_id>\w+)/$', api.api_get_thread_posts,
74 name='get_thread'),
71 name='get_thread'),
75 url(r'^api/add_post/(?P<opening_post_id>\w+)/$', api.api_add_post,
72 url(r'^api/add_post/(?P<opening_post_id>\w+)/$', api.api_add_post,
76 name='add_post'),
73 name='add_post'),
77 url(r'^api/notifications/(?P<username>\w+)/$', api.api_get_notifications,
74 url(r'^api/notifications/(?P<username>\w+)/$', api.api_get_notifications,
78 name='api_notifications'),
75 name='api_notifications'),
79 url(r'^api/preview/$', api.api_get_preview, name='preview'),
76 url(r'^api/preview/$', api.api_get_preview, name='preview'),
80 url(r'^api/new_posts/$', api.api_get_new_posts, name='new_posts'),
77 url(r'^api/new_posts/$', api.api_get_new_posts, name='new_posts'),
81 url(r'^api/stickers/$', api.api_get_stickers, name='get_stickers'),
78 url(r'^api/stickers/$', api.api_get_stickers, name='get_stickers'),
82
79
83 # Sync protocol API
80 # Sync protocol API
84 url(r'^api/sync/list/$', response_list, name='api_sync_list'),
81 url(r'^api/sync/list/$', response_list, name='api_sync_list'),
85 url(r'^api/sync/get/$', response_get, name='api_sync_get'),
82 url(r'^api/sync/get/$', response_get, name='api_sync_get'),
86
83
87 # Notifications
84 # Notifications
88 url(r'^notifications/(?P<username>\w+)/$', NotificationView.as_view(), name='notifications'),
85 url(r'^notifications/(?P<username>\w+)/$', NotificationView.as_view(), name='notifications'),
89 url(r'^notifications/$', NotificationView.as_view(), name='notifications'),
86 url(r'^notifications/$', NotificationView.as_view(), name='notifications'),
90
87
91 # Post preview
88 # Post preview
92 url(r'^preview/$', PostPreviewView.as_view(), name='preview'),
89 url(r'^preview/$', PostPreviewView.as_view(), name='preview'),
93 url(r'^post_xml/(?P<post_id>\d+)$', get_post_sync_data,
90 url(r'^post_xml/(?P<post_id>\d+)$', get_post_sync_data,
94 name='post_sync_data'),
91 name='post_sync_data'),
95 ]
92 ]
96
93
@@ -1,9 +0,0 b''
1 __author__ = 'neko259'
2
3 from django.views.decorators.cache import cache_page
4 from django.views.i18n import javascript_catalog
5
6
7 @cache_page(86400)
8 def cached_js_catalog(request, domain='djangojs', packages=None):
9 return javascript_catalog(request, domain, packages)
@@ -1,180 +1,178 b''
1 from django.core.urlresolvers import reverse
2 from django.core.files import File
3 from django.core.files.temp import NamedTemporaryFile
4 from django.core.paginator import EmptyPage
1 from django.core.paginator import EmptyPage
5 from django.db import transaction
2 from django.db import transaction
6 from django.http import Http404
3 from django.http import Http404
7 from django.shortcuts import render, redirect
4 from django.shortcuts import render, redirect
5 from django.urls import reverse
8 from django.utils.decorators import method_decorator
6 from django.utils.decorators import method_decorator
9 from django.views.decorators.csrf import csrf_protect
7 from django.views.decorators.csrf import csrf_protect
10
8
11 from boards import utils, settings
9 from boards import utils, settings
12 from boards.abstracts.paginator import get_paginator
10 from boards.abstracts.paginator import get_paginator
13 from boards.abstracts.settingsmanager import get_settings_manager,\
11 from boards.abstracts.settingsmanager import get_settings_manager, \
14 SETTING_ONLY_FAVORITES
12 SETTING_ONLY_FAVORITES
15 from boards.forms import ThreadForm, PlainErrorList
13 from boards.forms import ThreadForm, PlainErrorList
16 from boards.models import Post, Thread, Ban
14 from boards.models import Post, Thread, Ban
17 from boards.views.banned import BannedView
15 from boards.views.banned import BannedView
18 from boards.views.base import BaseBoardView, CONTEXT_FORM
16 from boards.views.base import BaseBoardView, CONTEXT_FORM
17 from boards.views.mixins import FileUploadMixin, PaginatedMixin, \
18 DispatcherMixin, PARAMETER_METHOD
19 from boards.views.posting_mixin import PostMixin
19 from boards.views.posting_mixin import PostMixin
20 from boards.views.mixins import FileUploadMixin, PaginatedMixin,\
21 DispatcherMixin, PARAMETER_METHOD
22
20
23 FORM_TAGS = 'tags'
21 FORM_TAGS = 'tags'
24 FORM_TEXT = 'text'
22 FORM_TEXT = 'text'
25 FORM_TITLE = 'title'
23 FORM_TITLE = 'title'
26 FORM_IMAGE = 'image'
24 FORM_IMAGE = 'image'
27 FORM_THREADS = 'threads'
25 FORM_THREADS = 'threads'
28
26
29 TAG_DELIMITER = ' '
27 TAG_DELIMITER = ' '
30
28
31 PARAMETER_CURRENT_PAGE = 'current_page'
29 PARAMETER_CURRENT_PAGE = 'current_page'
32 PARAMETER_PAGINATOR = 'paginator'
30 PARAMETER_PAGINATOR = 'paginator'
33 PARAMETER_THREADS = 'threads'
31 PARAMETER_THREADS = 'threads'
34 PARAMETER_ADDITIONAL = 'additional_params'
32 PARAMETER_ADDITIONAL = 'additional_params'
35 PARAMETER_MAX_FILE_SIZE = 'max_file_size'
33 PARAMETER_MAX_FILE_SIZE = 'max_file_size'
36 PARAMETER_RSS_URL = 'rss_url'
34 PARAMETER_RSS_URL = 'rss_url'
37 PARAMETER_MAX_FILES = 'max_files'
35 PARAMETER_MAX_FILES = 'max_files'
38
36
39 TEMPLATE = 'boards/all_threads.html'
37 TEMPLATE = 'boards/all_threads.html'
40 DEFAULT_PAGE = 1
38 DEFAULT_PAGE = 1
41
39
42 FORM_TAGS = 'tags'
40 FORM_TAGS = 'tags'
43
41
44
42
45 class AllThreadsView(PostMixin, FileUploadMixin, BaseBoardView, PaginatedMixin, DispatcherMixin):
43 class AllThreadsView(PostMixin, FileUploadMixin, BaseBoardView, PaginatedMixin, DispatcherMixin):
46
44
47 tag_name = ''
45 tag_name = ''
48
46
49 def __init__(self):
47 def __init__(self):
50 self.settings_manager = None
48 self.settings_manager = None
51 super(AllThreadsView, self).__init__()
49 super(AllThreadsView, self).__init__()
52
50
53 @method_decorator(csrf_protect)
51 @method_decorator(csrf_protect)
54 def get(self, request, form: ThreadForm=None):
52 def get(self, request, form: ThreadForm=None):
55 page = request.GET.get('page', DEFAULT_PAGE)
53 page = request.GET.get('page', DEFAULT_PAGE)
56
54
57 params = self.get_context_data(request=request)
55 params = self.get_context_data(request=request)
58
56
59 if not form:
57 if not form:
60 form = ThreadForm(error_class=PlainErrorList,
58 form = ThreadForm(error_class=PlainErrorList,
61 initial={FORM_TAGS: self.tag_name})
59 initial={FORM_TAGS: self.tag_name})
62
60
63 self.settings_manager = get_settings_manager(request)
61 self.settings_manager = get_settings_manager(request)
64
62
65 threads = self.get_threads()
63 threads = self.get_threads()
66
64
67 order = request.GET.get('order', 'bump')
65 order = request.GET.get('order', 'bump')
68 if order == 'bump':
66 if order == 'bump':
69 threads = threads.order_by('-bump_time')
67 threads = threads.order_by('-bump_time')
70 else:
68 else:
71 threads = threads.filter(replies__opening=True)\
69 threads = threads.filter(replies__opening=True)\
72 .order_by('-replies__pub_time')
70 .order_by('-replies__pub_time')
73 filter = request.GET.get('filter')
71 filter = request.GET.get('filter')
74 threads = threads.distinct()
72 threads = threads.distinct()
75
73
76 paginator = get_paginator(threads,
74 paginator = get_paginator(threads,
77 settings.get_int('View', 'ThreadsPerPage'))
75 settings.get_int('View', 'ThreadsPerPage'))
78 paginator.current_page = int(page)
76 paginator.current_page = int(page)
79
77
80 try:
78 try:
81 threads = paginator.page(page).object_list
79 threads = paginator.page(page).object_list
82 except EmptyPage:
80 except EmptyPage:
83 raise Http404()
81 raise Http404()
84
82
85 params[PARAMETER_THREADS] = threads
83 params[PARAMETER_THREADS] = threads
86 params[CONTEXT_FORM] = form
84 params[CONTEXT_FORM] = form
87 params[PARAMETER_MAX_FILE_SIZE] = self.get_max_upload_size()
85 params[PARAMETER_MAX_FILE_SIZE] = self.get_max_upload_size()
88 params[PARAMETER_RSS_URL] = self.get_rss_url()
86 params[PARAMETER_RSS_URL] = self.get_rss_url()
89 params[PARAMETER_MAX_FILES] = settings.get_int('Forms', 'MaxFileCount')
87 params[PARAMETER_MAX_FILES] = settings.get_int('Forms', 'MaxFileCount')
90
88
91 paginator.set_url(self.get_reverse_url(), request.GET.dict())
89 paginator.set_url(self.get_reverse_url(), request.GET.dict())
92 params.update(self.get_page_context(paginator, page))
90 params.update(self.get_page_context(paginator, page))
93
91
94 return render(request, TEMPLATE, params)
92 return render(request, TEMPLATE, params)
95
93
96 @method_decorator(csrf_protect)
94 @method_decorator(csrf_protect)
97 def post(self, request):
95 def post(self, request):
98 if PARAMETER_METHOD in request.POST:
96 if PARAMETER_METHOD in request.POST:
99 self.dispatch_method(request)
97 self.dispatch_method(request)
100
98
101 return redirect('index') # FIXME Different for different modes
99 return redirect('index') # FIXME Different for different modes
102
100
103 form = ThreadForm(request.POST, request.FILES,
101 form = ThreadForm(request.POST, request.FILES,
104 error_class=PlainErrorList)
102 error_class=PlainErrorList)
105 form.session = request.session
103 form.session = request.session
106
104
107 if form.is_valid():
105 if form.is_valid():
108 return self.create_thread(request, form)
106 return self.create_thread(request, form)
109 if form.need_to_ban:
107 if form.need_to_ban:
110 # Ban user because he is suspected to be a bot
108 # Ban user because he is suspected to be a bot
111 self._ban_current_user(request)
109 self._ban_current_user(request)
112
110
113 return self.get(request, form)
111 return self.get(request, form)
114
112
115 def get_reverse_url(self):
113 def get_reverse_url(self):
116 return reverse('index')
114 return reverse('index')
117
115
118 @transaction.atomic
116 @transaction.atomic
119 def create_thread(self, request, form: ThreadForm, html_response=True):
117 def create_thread(self, request, form: ThreadForm, html_response=True):
120 """
118 """
121 Creates a new thread with an opening post.
119 Creates a new thread with an opening post.
122 """
120 """
123
121
124 ip = utils.get_client_ip(request)
122 ip = utils.get_client_ip(request)
125 is_banned = Ban.objects.filter(ip=ip).exists()
123 is_banned = Ban.objects.filter(ip=ip).exists()
126
124
127 if is_banned:
125 if is_banned:
128 if html_response:
126 if html_response:
129 return redirect(BannedView().as_view())
127 return redirect(BannedView().as_view())
130 else:
128 else:
131 return
129 return
132
130
133 data = form.cleaned_data
131 data = form.cleaned_data
134
132
135 title = form.get_title()
133 title = form.get_title()
136 text = data[FORM_TEXT]
134 text = data[FORM_TEXT]
137 files = form.get_files()
135 files = form.get_files()
138 file_urls = form.get_file_urls()
136 file_urls = form.get_file_urls()
139 images = form.get_images()
137 images = form.get_images()
140
138
141 text = self._remove_invalid_links(text)
139 text = self._remove_invalid_links(text)
142
140
143 tags = data[FORM_TAGS]
141 tags = data[FORM_TAGS]
144 monochrome = form.is_monochrome()
142 monochrome = form.is_monochrome()
145 stickerpack = form.is_stickerpack()
143 stickerpack = form.is_stickerpack()
146
144
147 post = Post.objects.create_post(title=title, text=text, files=files,
145 post = Post.objects.create_post(title=title, text=text, files=files,
148 ip=ip, tags=tags,
146 ip=ip, tags=tags,
149 tripcode=form.get_tripcode(),
147 tripcode=form.get_tripcode(),
150 monochrome=monochrome, images=images,
148 monochrome=monochrome, images=images,
151 file_urls=file_urls, stickerpack=stickerpack)
149 file_urls=file_urls, stickerpack=stickerpack)
152
150
153 if form.is_subscribe():
151 if form.is_subscribe():
154 settings_manager = get_settings_manager(request)
152 settings_manager = get_settings_manager(request)
155 settings_manager.add_or_read_fav_thread(post)
153 settings_manager.add_or_read_fav_thread(post)
156
154
157 if html_response:
155 if html_response:
158 return redirect(post.get_absolute_url())
156 return redirect(post.get_absolute_url())
159
157
160 def get_threads(self):
158 def get_threads(self):
161 """
159 """
162 Gets list of threads that will be shown on a page.
160 Gets list of threads that will be shown on a page.
163 """
161 """
164
162
165 threads = Thread.objects\
163 threads = Thread.objects\
166 .exclude(tags__in=self.settings_manager.get_hidden_tags())
164 .exclude(tags__in=self.settings_manager.get_hidden_tags())
167 if self.settings_manager.get_setting(SETTING_ONLY_FAVORITES):
165 if self.settings_manager.get_setting(SETTING_ONLY_FAVORITES):
168 fav_tags = self.settings_manager.get_fav_tags()
166 fav_tags = self.settings_manager.get_fav_tags()
169 if len(fav_tags) > 0:
167 if len(fav_tags) > 0:
170 threads = threads.filter(tags__in=fav_tags)
168 threads = threads.filter(tags__in=fav_tags)
171
169
172 return threads
170 return threads
173
171
174 def get_rss_url(self):
172 def get_rss_url(self):
175 return self.get_reverse_url() + 'rss/'
173 return self.get_reverse_url() + 'rss/'
176
174
177 def toggle_fav(self, request):
175 def toggle_fav(self, request):
178 settings_manager = get_settings_manager(request)
176 settings_manager = get_settings_manager(request)
179 settings_manager.set_setting(SETTING_ONLY_FAVORITES,
177 settings_manager.set_setting(SETTING_ONLY_FAVORITES,
180 not settings_manager.get_setting(SETTING_ONLY_FAVORITES, False))
178 not settings_manager.get_setting(SETTING_ONLY_FAVORITES, False))
@@ -1,131 +1,131 b''
1 from django.core.urlresolvers import reverse
1 from django.urls import reverse
2 from django.shortcuts import render
2 from django.shortcuts import render
3
3
4 from boards import settings
4 from boards import settings
5 from boards.abstracts.paginator import get_paginator
5 from boards.abstracts.paginator import get_paginator
6 from boards.abstracts.settingsmanager import get_settings_manager
6 from boards.abstracts.settingsmanager import get_settings_manager
7 from boards.models import Post
7 from boards.models import Post
8 from boards.views.base import BaseBoardView
8 from boards.views.base import BaseBoardView
9 from boards.views.posting_mixin import PostMixin
9 from boards.views.posting_mixin import PostMixin
10 from boards.views.mixins import PaginatedMixin
10 from boards.views.mixins import PaginatedMixin
11
11
12 POSTS_PER_PAGE = settings.get_int('View', 'PostsPerPage')
12 POSTS_PER_PAGE = settings.get_int('View', 'PostsPerPage')
13
13
14 PARAMETER_POSTS = 'posts'
14 PARAMETER_POSTS = 'posts'
15 PARAMETER_QUERIES = 'queries'
15 PARAMETER_QUERIES = 'queries'
16
16
17 TEMPLATE = 'boards/feed.html'
17 TEMPLATE = 'boards/feed.html'
18 DEFAULT_PAGE = 1
18 DEFAULT_PAGE = 1
19
19
20
20
21 class FeedFilter:
21 class FeedFilter:
22 @staticmethod
22 @staticmethod
23 def get_filtered_posts(request, posts):
23 def get_filtered_posts(request, posts):
24 return posts
24 return posts
25
25
26 @staticmethod
26 @staticmethod
27 def get_query(request):
27 def get_query(request):
28 return None
28 return None
29
29
30
30
31 class TripcodeFilter(FeedFilter):
31 class TripcodeFilter(FeedFilter):
32 @staticmethod
32 @staticmethod
33 def get_filtered_posts(request, posts):
33 def get_filtered_posts(request, posts):
34 filtered_posts = posts
34 filtered_posts = posts
35 tripcode = request.GET.get('tripcode', None)
35 tripcode = request.GET.get('tripcode', None)
36 if tripcode:
36 if tripcode:
37 filtered_posts = filtered_posts.filter(tripcode=tripcode)
37 filtered_posts = filtered_posts.filter(tripcode=tripcode)
38 return filtered_posts
38 return filtered_posts
39
39
40 @staticmethod
40 @staticmethod
41 def get_query(request):
41 def get_query(request):
42 tripcode = request.GET.get('tripcode', None)
42 tripcode = request.GET.get('tripcode', None)
43 if tripcode:
43 if tripcode:
44 return 'Tripcode: {}'.format(tripcode)
44 return 'Tripcode: {}'.format(tripcode)
45
45
46
46
47 class FavoritesFilter(FeedFilter):
47 class FavoritesFilter(FeedFilter):
48 @staticmethod
48 @staticmethod
49 def get_filtered_posts(request, posts):
49 def get_filtered_posts(request, posts):
50 filtered_posts = posts
50 filtered_posts = posts
51
51
52 favorites = 'favorites' in request.GET
52 favorites = 'favorites' in request.GET
53 if favorites:
53 if favorites:
54 settings_manager = get_settings_manager(request)
54 settings_manager = get_settings_manager(request)
55 fav_thread_ops = Post.objects.filter(id__in=settings_manager.get_fav_threads().keys())
55 fav_thread_ops = Post.objects.filter(id__in=settings_manager.get_fav_threads().keys())
56 fav_threads = [op.get_thread() for op in fav_thread_ops]
56 fav_threads = [op.get_thread() for op in fav_thread_ops]
57 filtered_posts = filtered_posts.filter(thread__in=fav_threads)
57 filtered_posts = filtered_posts.filter(thread__in=fav_threads)
58 return filtered_posts
58 return filtered_posts
59
59
60
60
61 class IpFilter(FeedFilter):
61 class IpFilter(FeedFilter):
62 @staticmethod
62 @staticmethod
63 def get_filtered_posts(request, posts):
63 def get_filtered_posts(request, posts):
64 filtered_posts = posts
64 filtered_posts = posts
65
65
66 ip = request.GET.get('ip', None)
66 ip = request.GET.get('ip', None)
67 if ip and request.user.has_perm('post_delete'):
67 if ip and request.user.has_perm('post_delete'):
68 filtered_posts = filtered_posts.filter(poster_ip=ip)
68 filtered_posts = filtered_posts.filter(poster_ip=ip)
69 return filtered_posts
69 return filtered_posts
70
70
71 @staticmethod
71 @staticmethod
72 def get_query(request):
72 def get_query(request):
73 ip = request.GET.get('ip', None)
73 ip = request.GET.get('ip', None)
74 if ip:
74 if ip:
75 return 'IP: {}'.format(ip)
75 return 'IP: {}'.format(ip)
76
76
77
77
78 class ImageFilter(FeedFilter):
78 class ImageFilter(FeedFilter):
79 @staticmethod
79 @staticmethod
80 def get_filtered_posts(request, posts):
80 def get_filtered_posts(request, posts):
81 filtered_posts = posts
81 filtered_posts = posts
82
82
83 image = request.GET.get('image', None)
83 image = request.GET.get('image', None)
84 if image:
84 if image:
85 filtered_posts = filtered_posts.filter(attachments__file=image)
85 filtered_posts = filtered_posts.filter(attachments__file=image)
86 return filtered_posts
86 return filtered_posts
87
87
88 @staticmethod
88 @staticmethod
89 def get_query(request):
89 def get_query(request):
90 image = request.GET.get('image', None)
90 image = request.GET.get('image', None)
91 if image:
91 if image:
92 return 'File: {}'.format(image)
92 return 'File: {}'.format(image)
93
93
94
94
95 class FeedView(PostMixin, PaginatedMixin, BaseBoardView):
95 class FeedView(PostMixin, PaginatedMixin, BaseBoardView):
96 filters = (
96 filters = (
97 TripcodeFilter,
97 TripcodeFilter,
98 FavoritesFilter,
98 FavoritesFilter,
99 IpFilter,
99 IpFilter,
100 ImageFilter,
100 ImageFilter,
101 )
101 )
102
102
103 def get(self, request):
103 def get(self, request):
104 page = request.GET.get('page', DEFAULT_PAGE)
104 page = request.GET.get('page', DEFAULT_PAGE)
105
105
106 params = self.get_context_data(request=request)
106 params = self.get_context_data(request=request)
107
107
108 settings_manager = get_settings_manager(request)
108 settings_manager = get_settings_manager(request)
109
109
110 posts = Post.objects.exclude(
110 posts = Post.objects.exclude(
111 thread__tags__in=settings_manager.get_hidden_tags()).order_by(
111 thread__tags__in=settings_manager.get_hidden_tags()).order_by(
112 '-pub_time').prefetch_related('attachments', 'thread')
112 '-pub_time').prefetch_related('attachments', 'thread')
113 queries = []
113 queries = []
114 for filter in self.filters:
114 for filter in self.filters:
115 posts = filter.get_filtered_posts(request, posts)
115 posts = filter.get_filtered_posts(request, posts)
116 query = filter.get_query(request)
116 query = filter.get_query(request)
117 if query:
117 if query:
118 queries.append(query)
118 queries.append(query)
119 params[PARAMETER_QUERIES] = queries
119 params[PARAMETER_QUERIES] = queries
120
120
121 paginator = get_paginator(posts, POSTS_PER_PAGE)
121 paginator = get_paginator(posts, POSTS_PER_PAGE)
122 paginator.current_page = int(page)
122 paginator.current_page = int(page)
123
123
124 params[PARAMETER_POSTS] = paginator.page(page).object_list
124 params[PARAMETER_POSTS] = paginator.page(page).object_list
125
125
126 paginator.set_url(reverse('feed'), request.GET.dict())
126 paginator.set_url(reverse('feed'), request.GET.dict())
127
127
128 params.update(self.get_page_context(paginator, page))
128 params.update(self.get_page_context(paginator, page))
129
129
130 return render(request, TEMPLATE, params)
130 return render(request, TEMPLATE, params)
131
131
@@ -1,54 +1,54 b''
1 from django.shortcuts import render
1 from django.shortcuts import render
2 from django.views.generic import View
2 from django.views.generic import View
3 from django.db.models import Q
3 from django.db.models import Q
4 from django.core.urlresolvers import reverse
4 from django.urls import reverse
5
5
6 from boards.abstracts.paginator import get_paginator
6 from boards.abstracts.paginator import get_paginator
7 from boards.forms import SearchForm, PlainErrorList
7 from boards.forms import SearchForm, PlainErrorList
8 from boards.models import Post, Tag
8 from boards.models import Post, Tag
9 from boards.views.mixins import PaginatedMixin
9 from boards.views.mixins import PaginatedMixin
10
10
11
11
12 MIN_QUERY_LENGTH = 3
12 MIN_QUERY_LENGTH = 3
13 RESULTS_PER_PAGE = 10
13 RESULTS_PER_PAGE = 10
14
14
15 FORM_QUERY = 'query'
15 FORM_QUERY = 'query'
16
16
17 CONTEXT_QUERY = 'query'
17 CONTEXT_QUERY = 'query'
18 CONTEXT_FORM = 'form'
18 CONTEXT_FORM = 'form'
19 CONTEXT_PAGE = 'page'
19 CONTEXT_PAGE = 'page'
20 CONTEXT_TAGS = 'tags'
20 CONTEXT_TAGS = 'tags'
21
21
22 REQUEST_PAGE = 'page'
22 REQUEST_PAGE = 'page'
23
23
24 __author__ = 'neko259'
24 __author__ = 'neko259'
25
25
26 TEMPLATE = 'search/search.html'
26 TEMPLATE = 'search/search.html'
27
27
28
28
29 class BoardSearchView(View, PaginatedMixin):
29 class BoardSearchView(View, PaginatedMixin):
30 def get(self, request):
30 def get(self, request):
31 params = dict()
31 params = dict()
32
32
33 form = SearchForm(request.GET, error_class=PlainErrorList)
33 form = SearchForm(request.GET, error_class=PlainErrorList)
34 params[CONTEXT_FORM] = form
34 params[CONTEXT_FORM] = form
35
35
36 if form.is_valid():
36 if form.is_valid():
37 query = form.cleaned_data[FORM_QUERY]
37 query = form.cleaned_data[FORM_QUERY]
38 if len(query) >= MIN_QUERY_LENGTH:
38 if len(query) >= MIN_QUERY_LENGTH:
39 results = Post.objects.filter(Q(text__icontains=query)
39 results = Post.objects.filter(Q(text__icontains=query)
40 | Q(title__icontains=query) | Q(opening=True,
40 | Q(title__icontains=query) | Q(opening=True,
41 thread__tags__aliases__name__icontains=query)
41 thread__tags__aliases__name__icontains=query)
42 | Q(attachments__url__icontains=query)).order_by('-id').distinct()
42 | Q(attachments__url__icontains=query)).order_by('-id').distinct()
43 paginator = get_paginator(results, RESULTS_PER_PAGE)
43 paginator = get_paginator(results, RESULTS_PER_PAGE)
44 paginator.set_url(reverse('search'), request.GET.dict())
44 paginator.set_url(reverse('search'), request.GET.dict())
45
45
46 page = int(request.GET.get(REQUEST_PAGE, '1'))
46 page = int(request.GET.get(REQUEST_PAGE, '1'))
47
47
48 params[CONTEXT_QUERY] = query
48 params[CONTEXT_QUERY] = query
49 params.update(self.get_page_context(paginator, page))
49 params.update(self.get_page_context(paginator, page))
50
50
51 tags = Tag.objects.get_tag_url_list(Tag.objects.filter(aliases__name__icontains=query))
51 tags = Tag.objects.get_tag_url_list(Tag.objects.filter(aliases__name__icontains=query))
52 params[CONTEXT_TAGS] = tags
52 params[CONTEXT_TAGS] = tags
53
53
54 return render(request, TEMPLATE, params)
54 return render(request, TEMPLATE, params)
@@ -1,32 +1,32 b''
1 from django.core.urlresolvers import reverse
2 from django.shortcuts import get_object_or_404, render
1 from django.shortcuts import get_object_or_404, render
2 from django.urls import reverse
3
3
4 from boards import settings
4 from boards import settings
5 from boards.abstracts.paginator import get_paginator
5 from boards.abstracts.paginator import get_paginator
6 from boards.models import Tag, TagAlias
6 from boards.models import TagAlias
7 from boards.views.base import BaseBoardView
7 from boards.views.base import BaseBoardView
8 from boards.views.mixins import PaginatedMixin
8 from boards.views.mixins import PaginatedMixin
9
9
10 IMAGES_PER_PAGE = settings.get_int('View', 'ImagesPerPageGallery')
10 IMAGES_PER_PAGE = settings.get_int('View', 'ImagesPerPageGallery')
11
11
12 TEMPLATE = 'boards/tag_gallery.html'
12 TEMPLATE = 'boards/tag_gallery.html'
13
13
14
14
15 class TagGalleryView(BaseBoardView, PaginatedMixin):
15 class TagGalleryView(BaseBoardView, PaginatedMixin):
16
16
17 def get(self, request, tag_name):
17 def get(self, request, tag_name):
18 page = int(request.GET.get('page', 1))
18 page = int(request.GET.get('page', 1))
19
19
20 params = dict()
20 params = dict()
21 tag_alias = get_object_or_404(TagAlias, name=tag_name)
21 tag_alias = get_object_or_404(TagAlias, name=tag_name)
22 tag = tag_alias.parent
22 tag = tag_alias.parent
23 params['tag'] = tag
23 params['tag'] = tag
24 paginator = get_paginator(tag.get_images(), IMAGES_PER_PAGE,
24 paginator = get_paginator(tag.get_images(), IMAGES_PER_PAGE,
25 current_page=page)
25 current_page=page)
26 params['paginator'] = paginator
26 params['paginator'] = paginator
27 params['images'] = paginator.page(page).object_list
27 params['images'] = paginator.page(page).object_list
28 paginator.set_url(reverse('tag_gallery', kwargs={'tag_name': tag_name}),
28 paginator.set_url(reverse('tag_gallery', kwargs={'tag_name': tag_name}),
29 request.GET.dict())
29 request.GET.dict())
30 params.update(self.get_page_context(paginator, page))
30 params.update(self.get_page_context(paginator, page))
31
31
32 return render(request, TEMPLATE, params)
32 return render(request, TEMPLATE, params)
@@ -1,124 +1,124 b''
1 from django.shortcuts import get_object_or_404, redirect
1 from django.shortcuts import get_object_or_404, redirect
2 from django.core.urlresolvers import reverse
2 from django.urls import reverse
3
3
4 from boards.abstracts.settingsmanager import get_settings_manager, \
4 from boards.abstracts.settingsmanager import get_settings_manager, \
5 SETTING_FAVORITE_TAGS, SETTING_HIDDEN_TAGS
5 SETTING_FAVORITE_TAGS, SETTING_HIDDEN_TAGS
6 from boards.models import Tag, TagAlias
6 from boards.models import Tag, TagAlias
7 from boards.views.all_threads import AllThreadsView
7 from boards.views.all_threads import AllThreadsView
8 from boards.views.mixins import DispatcherMixin, PARAMETER_METHOD
8 from boards.views.mixins import DispatcherMixin, PARAMETER_METHOD
9 from boards.forms import ThreadForm, PlainErrorList
9 from boards.forms import ThreadForm, PlainErrorList
10
10
11 PARAM_HIDDEN_TAGS = 'hidden_tags'
11 PARAM_HIDDEN_TAGS = 'hidden_tags'
12 PARAM_TAG = 'tag'
12 PARAM_TAG = 'tag'
13 PARAM_IS_FAVORITE = 'is_favorite'
13 PARAM_IS_FAVORITE = 'is_favorite'
14 PARAM_IS_HIDDEN = 'is_hidden'
14 PARAM_IS_HIDDEN = 'is_hidden'
15 PARAM_RANDOM_IMAGE_POST = 'random_image_post'
15 PARAM_RANDOM_IMAGE_POST = 'random_image_post'
16 PARAM_RELATED_TAGS = 'related_tags'
16 PARAM_RELATED_TAGS = 'related_tags'
17
17
18
18
19 __author__ = 'neko259'
19 __author__ = 'neko259'
20
20
21
21
22 class TagView(AllThreadsView, DispatcherMixin):
22 class TagView(AllThreadsView, DispatcherMixin):
23
23
24 tag_name = None
24 tag_name = None
25
25
26 def get_threads(self):
26 def get_threads(self):
27 tag_alias = get_object_or_404(TagAlias, name=self.tag_name)
27 tag_alias = get_object_or_404(TagAlias, name=self.tag_name)
28 tag = tag_alias.parent
28 tag = tag_alias.parent
29
29
30 hidden_tags = self.settings_manager.get_hidden_tags()
30 hidden_tags = self.settings_manager.get_hidden_tags()
31
31
32 try:
32 try:
33 hidden_tags.remove(tag)
33 hidden_tags.remove(tag)
34 except ValueError:
34 except ValueError:
35 pass
35 pass
36
36
37 return tag.get_threads().exclude(
37 return tag.get_threads().exclude(
38 tags__in=hidden_tags)
38 tags__in=hidden_tags)
39
39
40 def get_context_data(self, **kwargs):
40 def get_context_data(self, **kwargs):
41 params = super(TagView, self).get_context_data(**kwargs)
41 params = super(TagView, self).get_context_data(**kwargs)
42
42
43 settings_manager = get_settings_manager(kwargs['request'])
43 settings_manager = get_settings_manager(kwargs['request'])
44
44
45 tag_alias = get_object_or_404(TagAlias, name=self.tag_name)
45 tag_alias = get_object_or_404(TagAlias, name=self.tag_name)
46 tag = tag_alias.parent
46 tag = tag_alias.parent
47 params[PARAM_TAG] = tag
47 params[PARAM_TAG] = tag
48
48
49 fav_tag_names = settings_manager.get_setting(SETTING_FAVORITE_TAGS)
49 fav_tag_names = settings_manager.get_setting(SETTING_FAVORITE_TAGS)
50 hidden_tag_names = settings_manager.get_setting(SETTING_HIDDEN_TAGS)
50 hidden_tag_names = settings_manager.get_setting(SETTING_HIDDEN_TAGS)
51
51
52 params[PARAM_IS_FAVORITE] = fav_tag_names is not None and tag.get_name() in fav_tag_names
52 params[PARAM_IS_FAVORITE] = fav_tag_names is not None and tag.get_name() in fav_tag_names
53 params[PARAM_IS_HIDDEN] = hidden_tag_names is not None and tag.get_name() in hidden_tag_names
53 params[PARAM_IS_HIDDEN] = hidden_tag_names is not None and tag.get_name() in hidden_tag_names
54
54
55 params[PARAM_RANDOM_IMAGE_POST] = tag.get_random_image_post()
55 params[PARAM_RANDOM_IMAGE_POST] = tag.get_random_image_post()
56 params[PARAM_RELATED_TAGS] = tag.get_related_tags()
56 params[PARAM_RELATED_TAGS] = tag.get_related_tags()
57
57
58 return params
58 return params
59
59
60 def get_reverse_url(self):
60 def get_reverse_url(self):
61 return reverse('tag', kwargs={'tag_name': self.tag_name})
61 return reverse('tag', kwargs={'tag_name': self.tag_name})
62
62
63 def get(self, request, tag_name, form=None):
63 def get(self, request, tag_name, form=None):
64 self.tag_name = tag_name
64 self.tag_name = tag_name
65
65
66 return super(TagView, self).get(request, form)
66 return super(TagView, self).get(request, form)
67
67
68
68
69 def post(self, request, tag_name):
69 def post(self, request, tag_name):
70 self.tag_name = tag_name
70 self.tag_name = tag_name
71
71
72 if PARAMETER_METHOD in request.POST:
72 if PARAMETER_METHOD in request.POST:
73 self.dispatch_method(request)
73 self.dispatch_method(request)
74
74
75 return redirect('tag', tag_name)
75 return redirect('tag', tag_name)
76 else:
76 else:
77 form = ThreadForm(request.POST, request.FILES,
77 form = ThreadForm(request.POST, request.FILES,
78 error_class=PlainErrorList)
78 error_class=PlainErrorList)
79 form.session = request.session
79 form.session = request.session
80
80
81 if form.is_valid():
81 if form.is_valid():
82 return self.create_thread(request, form)
82 return self.create_thread(request, form)
83 if form.need_to_ban:
83 if form.need_to_ban:
84 # Ban user because he is suspected to be a bot
84 # Ban user because he is suspected to be a bot
85 self._ban_current_user(request)
85 self._ban_current_user(request)
86
86
87 return self.get(request, tag_name, form)
87 return self.get(request, tag_name, form)
88
88
89 def subscribe(self, request):
89 def subscribe(self, request):
90 alias = get_object_or_404(TagAlias, name=self.tag_name)
90 alias = get_object_or_404(TagAlias, name=self.tag_name)
91 tag = alias.parent
91 tag = alias.parent
92
92
93 settings_manager = get_settings_manager(request)
93 settings_manager = get_settings_manager(request)
94 settings_manager.add_fav_tag(tag)
94 settings_manager.add_fav_tag(tag)
95
95
96 def unsubscribe(self, request):
96 def unsubscribe(self, request):
97 alias = get_object_or_404(TagAlias, name=self.tag_name)
97 alias = get_object_or_404(TagAlias, name=self.tag_name)
98 tag = alias.parent
98 tag = alias.parent
99
99
100 settings_manager = get_settings_manager(request)
100 settings_manager = get_settings_manager(request)
101 settings_manager.del_fav_tag(tag)
101 settings_manager.del_fav_tag(tag)
102
102
103 def hide(self, request):
103 def hide(self, request):
104 """
104 """
105 Adds tag to user's hidden tags. Threads with this tag will not be
105 Adds tag to user's hidden tags. Threads with this tag will not be
106 shown.
106 shown.
107 """
107 """
108
108
109 alias = get_object_or_404(TagAlias, name=self.tag_name)
109 alias = get_object_or_404(TagAlias, name=self.tag_name)
110 tag = alias.parent
110 tag = alias.parent
111
111
112 settings_manager = get_settings_manager(request)
112 settings_manager = get_settings_manager(request)
113 settings_manager.add_hidden_tag(tag)
113 settings_manager.add_hidden_tag(tag)
114
114
115 def unhide(self, request):
115 def unhide(self, request):
116 """
116 """
117 Removed tag from user's hidden tags.
117 Removed tag from user's hidden tags.
118 """
118 """
119
119
120 alias = get_object_or_404(TagAlias, name=self.tag_name)
120 alias = get_object_or_404(TagAlias, name=self.tag_name)
121 tag = alias.parent
121 tag = alias.parent
122
122
123 settings_manager = get_settings_manager(request)
123 settings_manager = get_settings_manager(request)
124 settings_manager.del_hidden_tag(tag)
124 settings_manager.del_hidden_tag(tag)
@@ -1,165 +1,159 b''
1 from django.contrib.auth.decorators import permission_required
2
3 from django.core.exceptions import ObjectDoesNotExist
1 from django.core.exceptions import ObjectDoesNotExist
4 from django.core.urlresolvers import reverse
5 from django.http import Http404
2 from django.http import Http404
6 from django.shortcuts import get_object_or_404, render, redirect
3 from django.shortcuts import get_object_or_404, render, redirect
7 from django.template.context_processors import csrf
4 from django.urls import reverse
8 from django.utils.decorators import method_decorator
5 from django.utils.decorators import method_decorator
9 from django.views.decorators.csrf import csrf_protect
6 from django.views.decorators.csrf import csrf_protect
10 from django.views.generic.edit import FormMixin
7 from django.views.generic.edit import FormMixin
11 from django.utils import timezone
12 from django.utils.dateformat import format
13
8
14 from boards import utils, settings
9 from boards import utils
15 from boards.abstracts.settingsmanager import get_settings_manager
10 from boards.abstracts.settingsmanager import get_settings_manager
16 from boards.forms import PostForm, PlainErrorList
11 from boards.forms import PostForm, PlainErrorList
17 from boards.models import Post
12 from boards.models import Post
18 from boards.views.base import BaseBoardView, CONTEXT_FORM
13 from boards.views.base import BaseBoardView, CONTEXT_FORM
19 from boards.views.mixins import DispatcherMixin, PARAMETER_METHOD
14 from boards.views.mixins import DispatcherMixin, PARAMETER_METHOD
20 from boards.views.posting_mixin import PostMixin
15 from boards.views.posting_mixin import PostMixin
21 import neboard
22
16
23 REQ_POST_ID = 'post_id'
17 REQ_POST_ID = 'post_id'
24
18
25 CONTEXT_LASTUPDATE = "last_update"
19 CONTEXT_LASTUPDATE = "last_update"
26 CONTEXT_THREAD = 'thread'
20 CONTEXT_THREAD = 'thread'
27 CONTEXT_MODE = 'mode'
21 CONTEXT_MODE = 'mode'
28 CONTEXT_OP = 'opening_post'
22 CONTEXT_OP = 'opening_post'
29 CONTEXT_FAVORITE = 'is_favorite'
23 CONTEXT_FAVORITE = 'is_favorite'
30 CONTEXT_RSS_URL = 'rss_url'
24 CONTEXT_RSS_URL = 'rss_url'
31
25
32 FORM_TITLE = 'title'
26 FORM_TITLE = 'title'
33 FORM_TEXT = 'text'
27 FORM_TEXT = 'text'
34 FORM_IMAGE = 'image'
28 FORM_IMAGE = 'image'
35 FORM_THREADS = 'threads'
29 FORM_THREADS = 'threads'
36
30
37
31
38 class ThreadView(BaseBoardView, PostMixin, FormMixin, DispatcherMixin):
32 class ThreadView(BaseBoardView, PostMixin, FormMixin, DispatcherMixin):
39
33
40 @method_decorator(csrf_protect)
34 @method_decorator(csrf_protect)
41 def get(self, request, post_id, form: PostForm=None):
35 def get(self, request, post_id, form: PostForm=None):
42 try:
36 try:
43 opening_post = Post.objects.get(id=post_id)
37 opening_post = Post.objects.get(id=post_id)
44 except ObjectDoesNotExist:
38 except ObjectDoesNotExist:
45 raise Http404
39 raise Http404
46
40
47 # If the tag is favorite, update the counter
41 # If the tag is favorite, update the counter
48 settings_manager = get_settings_manager(request)
42 settings_manager = get_settings_manager(request)
49 favorite = settings_manager.thread_is_fav(opening_post)
43 favorite = settings_manager.thread_is_fav(opening_post)
50 if favorite:
44 if favorite:
51 settings_manager.add_or_read_fav_thread(opening_post)
45 settings_manager.add_or_read_fav_thread(opening_post)
52
46
53 # If this is not OP, don't show it as it is
47 # If this is not OP, don't show it as it is
54 if not opening_post.is_opening():
48 if not opening_post.is_opening():
55 return redirect('{}#{}'.format(opening_post.get_thread().get_opening_post()
49 return redirect('{}#{}'.format(opening_post.get_thread().get_opening_post()
56 .get_absolute_url(), opening_post.id))
50 .get_absolute_url(), opening_post.id))
57
51
58 if not form:
52 if not form:
59 form = PostForm(error_class=PlainErrorList)
53 form = PostForm(error_class=PlainErrorList)
60
54
61 thread_to_show = opening_post.get_thread()
55 thread_to_show = opening_post.get_thread()
62
56
63 params = dict()
57 params = dict()
64
58
65 params[CONTEXT_FORM] = form
59 params[CONTEXT_FORM] = form
66 params[CONTEXT_LASTUPDATE] = str(thread_to_show.last_edit_time)
60 params[CONTEXT_LASTUPDATE] = str(thread_to_show.last_edit_time)
67 params[CONTEXT_THREAD] = thread_to_show
61 params[CONTEXT_THREAD] = thread_to_show
68 params[CONTEXT_MODE] = self.get_mode()
62 params[CONTEXT_MODE] = self.get_mode()
69 params[CONTEXT_OP] = opening_post
63 params[CONTEXT_OP] = opening_post
70 params[CONTEXT_FAVORITE] = favorite
64 params[CONTEXT_FAVORITE] = favorite
71 params[CONTEXT_RSS_URL] = self.get_rss_url(post_id)
65 params[CONTEXT_RSS_URL] = self.get_rss_url(post_id)
72
66
73 params.update(self.get_data(thread_to_show))
67 params.update(self.get_data(thread_to_show))
74
68
75 return render(request, self.get_template(), params)
69 return render(request, self.get_template(), params)
76
70
77 @method_decorator(csrf_protect)
71 @method_decorator(csrf_protect)
78 def post(self, request, post_id):
72 def post(self, request, post_id):
79 opening_post = get_object_or_404(Post, id=post_id)
73 opening_post = get_object_or_404(Post, id=post_id)
80
74
81 # If this is not OP, don't show it as it is
75 # If this is not OP, don't show it as it is
82 if not opening_post.is_opening():
76 if not opening_post.is_opening():
83 raise Http404
77 raise Http404
84
78
85 if PARAMETER_METHOD in request.POST:
79 if PARAMETER_METHOD in request.POST:
86 self.dispatch_method(request, opening_post)
80 self.dispatch_method(request, opening_post)
87
81
88 return redirect('thread', post_id) # FIXME Different for different modes
82 return redirect('thread', post_id) # FIXME Different for different modes
89
83
90 if not opening_post.get_thread().is_archived():
84 if not opening_post.get_thread().is_archived():
91 form = PostForm(request.POST, request.FILES,
85 form = PostForm(request.POST, request.FILES,
92 error_class=PlainErrorList)
86 error_class=PlainErrorList)
93 form.session = request.session
87 form.session = request.session
94
88
95 if form.is_valid():
89 if form.is_valid():
96 return self.new_post(request, form, opening_post)
90 return self.new_post(request, form, opening_post)
97 if form.need_to_ban:
91 if form.need_to_ban:
98 # Ban user because he is suspected to be a bot
92 # Ban user because he is suspected to be a bot
99 self._ban_current_user(request)
93 self._ban_current_user(request)
100
94
101 return self.get(request, post_id, form)
95 return self.get(request, post_id, form)
102
96
103 def new_post(self, request, form: PostForm, opening_post: Post=None,
97 def new_post(self, request, form: PostForm, opening_post: Post=None,
104 html_response=True):
98 html_response=True):
105 """
99 """
106 Adds a new post (in thread or as a reply).
100 Adds a new post (in thread or as a reply).
107 """
101 """
108
102
109 ip = utils.get_client_ip(request)
103 ip = utils.get_client_ip(request)
110
104
111 data = form.cleaned_data
105 data = form.cleaned_data
112
106
113 title = form.get_title()
107 title = form.get_title()
114 text = data[FORM_TEXT]
108 text = data[FORM_TEXT]
115 files = form.get_files()
109 files = form.get_files()
116 file_urls = form.get_file_urls()
110 file_urls = form.get_file_urls()
117 images = form.get_images()
111 images = form.get_images()
118
112
119 text = self._remove_invalid_links(text)
113 text = self._remove_invalid_links(text)
120
114
121 post_thread = opening_post.get_thread()
115 post_thread = opening_post.get_thread()
122
116
123 post = Post.objects.create_post(title=title, text=text, files=files,
117 post = Post.objects.create_post(title=title, text=text, files=files,
124 thread=post_thread, ip=ip,
118 thread=post_thread, ip=ip,
125 tripcode=form.get_tripcode(),
119 tripcode=form.get_tripcode(),
126 images=images, file_urls=file_urls)
120 images=images, file_urls=file_urls)
127
121
128 if form.is_subscribe():
122 if form.is_subscribe():
129 settings_manager = get_settings_manager(request)
123 settings_manager = get_settings_manager(request)
130 settings_manager.add_or_read_fav_thread(
124 settings_manager.add_or_read_fav_thread(
131 post_thread.get_opening_post())
125 post_thread.get_opening_post())
132
126
133 if html_response:
127 if html_response:
134 if opening_post:
128 if opening_post:
135 return redirect(post.get_absolute_url())
129 return redirect(post.get_absolute_url())
136 else:
130 else:
137 return post
131 return post
138
132
139 def get_data(self, thread) -> dict:
133 def get_data(self, thread) -> dict:
140 """
134 """
141 Returns context params for the view.
135 Returns context params for the view.
142 """
136 """
143
137
144 return dict()
138 return dict()
145
139
146 def get_template(self) -> str:
140 def get_template(self) -> str:
147 """
141 """
148 Gets template to show the thread mode on.
142 Gets template to show the thread mode on.
149 """
143 """
150
144
151 pass
145 pass
152
146
153 def get_mode(self) -> str:
147 def get_mode(self) -> str:
154 pass
148 pass
155
149
156 def subscribe(self, request, opening_post):
150 def subscribe(self, request, opening_post):
157 settings_manager = get_settings_manager(request)
151 settings_manager = get_settings_manager(request)
158 settings_manager.add_or_read_fav_thread(opening_post)
152 settings_manager.add_or_read_fav_thread(opening_post)
159
153
160 def unsubscribe(self, request, opening_post):
154 def unsubscribe(self, request, opening_post):
161 settings_manager = get_settings_manager(request)
155 settings_manager = get_settings_manager(request)
162 settings_manager.del_fav_thread(opening_post)
156 settings_manager.del_fav_thread(opening_post)
163
157
164 def get_rss_url(self, opening_id):
158 def get_rss_url(self, opening_id):
165 return reverse('thread', kwargs={'post_id': opening_id}) + 'rss/'
159 return reverse('thread', kwargs={'post_id': opening_id}) + 'rss/'
@@ -1,198 +1,198 b''
1 # Django settings for neboard project.
1 # Django settings for neboard project.
2 import os
2 import os
3
3
4 DEBUG = True
4 DEBUG = True
5
5
6 ADMINS = (
6 ADMINS = (
7 # ('Your Name', 'your_email@example.com'),
7 # ('Your Name', 'your_email@example.com'),
8 ('admin', 'admin@example.com')
8 ('admin', 'admin@example.com')
9 )
9 )
10
10
11 MANAGERS = ADMINS
11 MANAGERS = ADMINS
12
12
13 DATABASES = {
13 DATABASES = {
14 'default': {
14 'default': {
15 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
15 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
16 'NAME': 'database.db', # Or path to database file if using sqlite3.
16 'NAME': 'database.db', # Or path to database file if using sqlite3.
17 'USER': '', # Not used with sqlite3.
17 'USER': '', # Not used with sqlite3.
18 'PASSWORD': '', # Not used with sqlite3.
18 'PASSWORD': '', # Not used with sqlite3.
19 'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
19 'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
20 'PORT': '', # Set to empty string for default. Not used with sqlite3.
20 'PORT': '', # Set to empty string for default. Not used with sqlite3.
21 'CONN_MAX_AGE': None,
21 'CONN_MAX_AGE': None,
22 }
22 }
23 }
23 }
24
24
25 # Local time zone for this installation. Choices can be found here:
25 # Local time zone for this installation. Choices can be found here:
26 # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
26 # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
27 # although not all choices may be available on all operating systems.
27 # although not all choices may be available on all operating systems.
28 # In a Windows environment this must be set to your system time zone.
28 # In a Windows environment this must be set to your system time zone.
29 TIME_ZONE = 'Europe/Kiev'
29 TIME_ZONE = 'Europe/Kiev'
30
30
31 # Language code for this installation. All choices can be found here:
31 # Language code for this installation. All choices can be found here:
32 # http://www.i18nguy.com/unicode/language-identifiers.html
32 # http://www.i18nguy.com/unicode/language-identifiers.html
33 LANGUAGE_CODE = 'en'
33 LANGUAGE_CODE = 'en'
34
34
35 SITE_ID = 1
35 SITE_ID = 1
36
36
37 # If you set this to False, Django will make some optimizations so as not
37 # If you set this to False, Django will make some optimizations so as not
38 # to load the internationalization machinery.
38 # to load the internationalization machinery.
39 USE_I18N = True
39 USE_I18N = True
40
40
41 # If you set this to False, Django will not format dates, numbers and
41 # If you set this to False, Django will not format dates, numbers and
42 # calendars according to the current locale.
42 # calendars according to the current locale.
43 USE_L10N = True
43 USE_L10N = True
44
44
45 # If you set this to False, Django will not use timezone-aware datetimes.
45 # If you set this to False, Django will not use timezone-aware datetimes.
46 USE_TZ = True
46 USE_TZ = True
47
47
48 USE_ETAGS = True
48 USE_ETAGS = True
49
49
50 # Absolute filesystem path to the directory that will hold user-uploaded files.
50 # Absolute filesystem path to the directory that will hold user-uploaded files.
51 # Example: "/home/media/media.lawrence.com/media/"
51 # Example: "/home/media/media.lawrence.com/media/"
52 MEDIA_ROOT = './media/'
52 MEDIA_ROOT = './media/'
53
53
54 # URL that handles the media served from MEDIA_ROOT. Make sure to use a
54 # URL that handles the media served from MEDIA_ROOT. Make sure to use a
55 # trailing slash.
55 # trailing slash.
56 # Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
56 # Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
57 MEDIA_URL = '/media/'
57 MEDIA_URL = '/media/'
58
58
59 # Absolute path to the directory static files should be collected to.
59 # Absolute path to the directory static files should be collected to.
60 # Don't put anything in this directory yourself; store your static files
60 # Don't put anything in this directory yourself; store your static files
61 # in apps' "static/" subdirectories and in STATICFILES_DIRS.
61 # in apps' "static/" subdirectories and in STATICFILES_DIRS.
62 # Example: "/home/media/media.lawrence.com/static/"
62 # Example: "/home/media/media.lawrence.com/static/"
63 STATIC_ROOT = ''
63 STATIC_ROOT = ''
64
64
65 # URL prefix for static files.
65 # URL prefix for static files.
66 # Example: "http://media.lawrence.com/static/"
66 # Example: "http://media.lawrence.com/static/"
67 STATIC_URL = '/static/'
67 STATIC_URL = '/static/'
68
68
69 STATICFILES_DIRS = []
69 STATICFILES_DIRS = []
70
70
71 # List of finder classes that know how to find static files in
71 # List of finder classes that know how to find static files in
72 # various locations.
72 # various locations.
73 STATICFILES_FINDERS = (
73 STATICFILES_FINDERS = (
74 'django.contrib.staticfiles.finders.FileSystemFinder',
74 'django.contrib.staticfiles.finders.FileSystemFinder',
75 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
75 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
76 )
76 )
77
77
78 if DEBUG:
78 if DEBUG:
79 STATICFILES_STORAGE = \
79 STATICFILES_STORAGE = \
80 'django.contrib.staticfiles.storage.StaticFilesStorage'
80 'django.contrib.staticfiles.storage.StaticFilesStorage'
81 else:
81 else:
82 STATICFILES_STORAGE = \
82 STATICFILES_STORAGE = \
83 'django.contrib.staticfiles.storage.CachedStaticFilesStorage'
83 'django.contrib.staticfiles.storage.CachedStaticFilesStorage'
84
84
85 # Make this unique, and don't share it with anybody.
85 # Make this unique, and don't share it with anybody.
86 SECRET_KEY = '@1rc$o(7=tt#kd+4s$u6wchm**z^)4x90)7f6z(i&amp;55@o11*8o'
86 SECRET_KEY = '@1rc$o(7=tt#kd+4s$u6wchm**z^)4x90)7f6z(i&amp;55@o11*8o'
87
87
88 TEMPLATES = [{
88 TEMPLATES = [{
89 'BACKEND': 'django.template.backends.django.DjangoTemplates',
89 'BACKEND': 'django.template.backends.django.DjangoTemplates',
90 'DIRS': ['templates'],
90 'DIRS': ['templates'],
91 'OPTIONS': {
91 'OPTIONS': {
92 'loaders': [
92 'loaders': [
93 ('django.template.loaders.cached.Loader', [
93 ('django.template.loaders.cached.Loader', [
94 'django.template.loaders.filesystem.Loader',
94 'django.template.loaders.filesystem.Loader',
95 'django.template.loaders.app_directories.Loader',
95 'django.template.loaders.app_directories.Loader',
96 ]),
96 ]),
97 ],
97 ],
98 'context_processors': [
98 'context_processors': [
99 'django.template.context_processors.csrf',
99 'django.template.context_processors.csrf',
100 'django.contrib.auth.context_processors.auth',
100 'django.contrib.auth.context_processors.auth',
101 'boards.context_processors.user_and_ui_processor',
101 'boards.context_processors.user_and_ui_processor',
102 ],
102 ],
103 },
103 },
104 }]
104 }]
105
105
106
106
107 MIDDLEWARE_CLASSES = [
107 MIDDLEWARE = [
108 'django.middleware.http.ConditionalGetMiddleware',
108 'django.middleware.http.ConditionalGetMiddleware',
109 'django.contrib.sessions.middleware.SessionMiddleware',
109 'django.contrib.sessions.middleware.SessionMiddleware',
110 'django.middleware.locale.LocaleMiddleware',
110 'django.middleware.locale.LocaleMiddleware',
111 'django.middleware.common.CommonMiddleware',
111 'django.middleware.common.CommonMiddleware',
112 'django.contrib.auth.middleware.AuthenticationMiddleware',
112 'django.contrib.auth.middleware.AuthenticationMiddleware',
113 'django.contrib.messages.middleware.MessageMiddleware',
113 'django.contrib.messages.middleware.MessageMiddleware',
114 'boards.middlewares.BanMiddleware',
114 'boards.middlewares.BanMiddleware',
115 'boards.middlewares.TimezoneMiddleware',
115 'boards.middlewares.TimezoneMiddleware',
116 ]
116 ]
117
117
118 ROOT_URLCONF = 'neboard.urls'
118 ROOT_URLCONF = 'neboard.urls'
119
119
120 # Python dotted path to the WSGI application used by Django's runserver.
120 # Python dotted path to the WSGI application used by Django's runserver.
121 WSGI_APPLICATION = 'neboard.wsgi.application'
121 WSGI_APPLICATION = 'neboard.wsgi.application'
122
122
123 INSTALLED_APPS = (
123 INSTALLED_APPS = (
124 'django.contrib.auth',
124 'django.contrib.auth',
125 'django.contrib.contenttypes',
125 'django.contrib.contenttypes',
126 'django.contrib.sessions',
126 'django.contrib.sessions',
127 'django.contrib.staticfiles',
127 'django.contrib.staticfiles',
128 # Uncomment the next line to enable the admin:
128 # Uncomment the next line to enable the admin:
129 'django.contrib.admin',
129 'django.contrib.admin',
130 # Uncomment the next line to enable admin documentation:
130 # Uncomment the next line to enable admin documentation:
131 # 'django.contrib.admindocs',
131 # 'django.contrib.admindocs',
132 'django.contrib.messages',
132 'django.contrib.messages',
133
133
134 'debug_toolbar',
134 'debug_toolbar',
135
135
136 'boards',
136 'boards',
137 )
137 )
138
138
139 # A sample logging configuration. The only tangible logging
139 # A sample logging configuration. The only tangible logging
140 # performed by this configuration is to send an email to
140 # performed by this configuration is to send an email to
141 # the site admins on every HTTP 500 error when DEBUG=False.
141 # the site admins on every HTTP 500 error when DEBUG=False.
142 # See http://docs.djangoproject.com/en/dev/topics/logging for
142 # See http://docs.djangoproject.com/en/dev/topics/logging for
143 # more details on how to customize your logging configuration.
143 # more details on how to customize your logging configuration.
144 LOGGING = {
144 LOGGING = {
145 'version': 1,
145 'version': 1,
146 'disable_existing_loggers': False,
146 'disable_existing_loggers': False,
147 'formatters': {
147 'formatters': {
148 'verbose': {
148 'verbose': {
149 'format': '%(levelname)s %(asctime)s %(name)s %(process)d %(thread)d %(message)s'
149 'format': '%(levelname)s %(asctime)s %(name)s %(process)d %(thread)d %(message)s'
150 },
150 },
151 'simple': {
151 'simple': {
152 'format': '%(levelname)s %(asctime)s [%(name)s] %(message)s'
152 'format': '%(levelname)s %(asctime)s [%(name)s] %(message)s'
153 },
153 },
154 },
154 },
155 'filters': {
155 'filters': {
156 'require_debug_false': {
156 'require_debug_false': {
157 '()': 'django.utils.log.RequireDebugFalse'
157 '()': 'django.utils.log.RequireDebugFalse'
158 }
158 }
159 },
159 },
160 'handlers': {
160 'handlers': {
161 'console': {
161 'console': {
162 'level': 'DEBUG',
162 'level': 'DEBUG',
163 'class': 'logging.StreamHandler',
163 'class': 'logging.StreamHandler',
164 'formatter': 'simple'
164 'formatter': 'simple'
165 },
165 },
166 },
166 },
167 'loggers': {
167 'loggers': {
168 'boards': {
168 'boards': {
169 'handlers': ['console'],
169 'handlers': ['console'],
170 'level': 'DEBUG',
170 'level': 'DEBUG',
171 }
171 }
172 },
172 },
173 }
173 }
174
174
175 ALLOWED_HOSTS = ['*']
175 ALLOWED_HOSTS = ['*']
176
176
177 POSTING_DELAY = 20 # seconds
177 POSTING_DELAY = 20 # seconds
178
178
179 SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db'
179 SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db'
180
180
181 # Debug middlewares
181 # Debug middlewares
182 MIDDLEWARE_CLASSES += [
182 MIDDLEWARE += [
183 'debug_toolbar.middleware.DebugToolbarMiddleware',
183 'debug_toolbar.middleware.DebugToolbarMiddleware',
184 ]
184 ]
185
185
186
186
187 def custom_show_toolbar(request):
187 def custom_show_toolbar(request):
188 return request.user.has_perm('admin.debug')
188 return request.user.has_perm('admin.debug')
189
189
190 DEBUG_TOOLBAR_CONFIG = {
190 DEBUG_TOOLBAR_CONFIG = {
191 'ENABLE_STACKTRACES': True,
191 'ENABLE_STACKTRACES': True,
192 'SHOW_TOOLBAR_CALLBACK': 'neboard.settings.custom_show_toolbar',
192 'SHOW_TOOLBAR_CALLBACK': 'neboard.settings.custom_show_toolbar',
193 }
193 }
194
194
195 # FIXME Uncommenting this fails somehow. Need to investigate this
195 # FIXME Uncommenting this fails somehow. Need to investigate this
196 #DEBUG_TOOLBAR_PANELS += (
196 #DEBUG_TOOLBAR_PANELS += (
197 # 'debug_toolbar.panels.profiling.ProfilingDebugPanel',
197 # 'debug_toolbar.panels.profiling.ProfilingDebugPanel',
198 #)
198 #)
@@ -1,24 +1,24 b''
1 from django.conf.urls import include, url
1 from django.conf.urls import include, url
2
2
3 # Uncomment the next two lines to enable the admin:
3 # Uncomment the next two lines to enable the admin:
4 from django.conf.urls.static import static
4 from django.conf.urls.static import static
5 from django.contrib import admin
5 from django.contrib import admin
6 from neboard import settings
6 from neboard import settings
7
7
8 from boards.views.not_found import NotFoundView
8 from boards.views.not_found import NotFoundView
9
9
10 admin.autodiscover()
10 admin.autodiscover()
11
11
12 urlpatterns = [
12 urlpatterns = [
13 # Uncomment the admin/doc line below to enable admin documentation:
13 # Uncomment the admin/doc line below to enable admin documentation:
14 # url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
14 # url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
15
15
16 url(r'^admin/', include(admin.site.urls), name='admin'),
16 url(r'^admin/', admin.site.urls, name='admin'),
17 url(r'^', include('boards.urls')),
17 url(r'^', include('boards.urls')),
18 ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
18 ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
19
19
20 if settings.DEBUG:
20 if settings.DEBUG:
21 import debug_toolbar
21 import debug_toolbar
22 urlpatterns.append(url(r'^__debug__/', include(debug_toolbar.urls)))
22 urlpatterns.append(url(r'^__debug__/', include(debug_toolbar.urls)))
23
23
24 handler404 = NotFoundView.as_view()
24 handler404 = NotFoundView.as_view()
1 NO CONTENT: file was removed
NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now