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. |
|
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 = ' ' |
|
27 | SPOILER_SPACE = ' ' | |
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">>{}</span>'.format( |
|
174 | result = '<span class="quote">>{}</span>'.format( | |
174 | value.replace(LINE_BREAK_HTML, |
|
175 | value.replace(LINE_BREAK_HTML, | |
175 | '{}>'.format(LINE_BREAK_HTML))) |
|
176 | '{}>'.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. |
|
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 |
|
|
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="{}">⋮ </a></div>'\ |
|
53 | ' <a class="file-menu" href="#" data-type="{}" data-search-url="{}" data-filename="{}" data-id="{}">⋮ </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', |
|
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="{}">>>{}</a>'.format(self.get_absolute_url(), |
|
330 | result = '<a href="{}">>>{}</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. |
|
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, |
|
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, |
|
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 |
|
|
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 |
|
|
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. |
|
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. |
|
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 |
|
|
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 |
|
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. |
|
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 |
|
|
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 |
|
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&55@o11*8o' |
|
86 | SECRET_KEY = '@1rc$o(7=tt#kd+4s$u6wchm**z^)4x90)7f6z(i&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 |
|
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 |
|
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/', |
|
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