##// END OF EJS Templates
Tag name is now stored in the alias with default locale
neko259 -
r1874:aaf6b563 default
parent child Browse files
Show More
@@ -0,0 +1,23 b''
1 # -*- coding: utf-8 -*-
2 # Generated by Django 1.10.5 on 2017-03-01 08:53
3 from __future__ import unicode_literals
4
5 from django.db import migrations
6
7
8 class Migration(migrations.Migration):
9
10 def tag_name_to_alias(apps, schema_editor):
11 Tag = apps.get_model('boards', 'Tag')
12 TagAlias = apps.get_model('boards', 'TagAlias')
13
14 for tag in Tag.objects.all():
15 TagAlias.objects.get_or_create(name=tag.name, locale='default', parent=tag)
16
17 dependencies = [
18 ('boards', '0061_auto_20170227_1739'),
19 ]
20
21 operations = [
22 migrations.RunPython(tag_name_to_alias),
23 ]
@@ -0,0 +1,23 b''
1 # -*- coding: utf-8 -*-
2 # Generated by Django 1.10.5 on 2017-03-01 08:58
3 from __future__ import unicode_literals
4
5 from django.db import migrations
6
7
8 class Migration(migrations.Migration):
9
10 dependencies = [
11 ('boards', '0062_auto_20170301_1053'),
12 ]
13
14 operations = [
15 migrations.AlterModelOptions(
16 name='tag',
17 options={},
18 ),
19 migrations.RemoveField(
20 model_name='tag',
21 name='name',
22 ),
23 ]
@@ -1,200 +1,200 b''
1 from boards import settings
1 from boards import settings
2 from boards.models import Tag
2 from boards.models import Tag
3 from boards.models.thread import FAV_THREAD_NO_UPDATES
3 from boards.models.thread import FAV_THREAD_NO_UPDATES
4
4
5 MAX_TRIPCODE_COLLISIONS = 50
5 MAX_TRIPCODE_COLLISIONS = 50
6
6
7 __author__ = 'neko259'
7 __author__ = 'neko259'
8
8
9 SESSION_SETTING = 'setting'
9 SESSION_SETTING = 'setting'
10
10
11 # Remove this, it is not used any more cause there is a user's permission
11 # Remove this, it is not used any more cause there is a user's permission
12 PERMISSION_MODERATE = 'moderator'
12 PERMISSION_MODERATE = 'moderator'
13
13
14 SETTING_THEME = 'theme'
14 SETTING_THEME = 'theme'
15 SETTING_FAVORITE_TAGS = 'favorite_tags'
15 SETTING_FAVORITE_TAGS = 'favorite_tags'
16 SETTING_FAVORITE_THREADS = 'favorite_threads'
16 SETTING_FAVORITE_THREADS = 'favorite_threads'
17 SETTING_HIDDEN_TAGS = 'hidden_tags'
17 SETTING_HIDDEN_TAGS = 'hidden_tags'
18 SETTING_PERMISSIONS = 'permissions'
18 SETTING_PERMISSIONS = 'permissions'
19 SETTING_USERNAME = 'username'
19 SETTING_USERNAME = 'username'
20 SETTING_LAST_NOTIFICATION_ID = 'last_notification'
20 SETTING_LAST_NOTIFICATION_ID = 'last_notification'
21 SETTING_IMAGE_VIEWER = 'image_viewer'
21 SETTING_IMAGE_VIEWER = 'image_viewer'
22 SETTING_TRIPCODE = 'tripcode'
22 SETTING_TRIPCODE = 'tripcode'
23 SETTING_IMAGES = 'images_aliases'
23 SETTING_IMAGES = 'images_aliases'
24 SETTING_ONLY_FAVORITES = 'only_favorites'
24 SETTING_ONLY_FAVORITES = 'only_favorites'
25
25
26 DEFAULT_THEME = 'md'
26 DEFAULT_THEME = 'md'
27
27
28
28
29 class SettingsManager:
29 class SettingsManager:
30 """
30 """
31 Base settings manager class. get_setting and set_setting methods should
31 Base settings manager class. get_setting and set_setting methods should
32 be overriden.
32 be overriden.
33 """
33 """
34 def __init__(self):
34 def __init__(self):
35 pass
35 pass
36
36
37 def get_theme(self) -> str:
37 def get_theme(self) -> str:
38 theme = self.get_setting(SETTING_THEME)
38 theme = self.get_setting(SETTING_THEME)
39 if not theme:
39 if not theme:
40 theme = DEFAULT_THEME
40 theme = DEFAULT_THEME
41 self.set_setting(SETTING_THEME, theme)
41 self.set_setting(SETTING_THEME, theme)
42
42
43 return theme
43 return theme
44
44
45 def set_theme(self, theme):
45 def set_theme(self, theme):
46 self.set_setting(SETTING_THEME, theme)
46 self.set_setting(SETTING_THEME, theme)
47
47
48 def has_permission(self, permission):
48 def has_permission(self, permission):
49 permissions = self.get_setting(SETTING_PERMISSIONS)
49 permissions = self.get_setting(SETTING_PERMISSIONS)
50 if permissions:
50 if permissions:
51 return permission in permissions
51 return permission in permissions
52 else:
52 else:
53 return False
53 return False
54
54
55 def get_setting(self, setting, default=None):
55 def get_setting(self, setting, default=None):
56 pass
56 pass
57
57
58 def set_setting(self, setting, value):
58 def set_setting(self, setting, value):
59 pass
59 pass
60
60
61 def add_permission(self, permission):
61 def add_permission(self, permission):
62 permissions = self.get_setting(SETTING_PERMISSIONS)
62 permissions = self.get_setting(SETTING_PERMISSIONS)
63 if not permissions:
63 if not permissions:
64 permissions = [permission]
64 permissions = [permission]
65 else:
65 else:
66 permissions.append(permission)
66 permissions.append(permission)
67 self.set_setting(SETTING_PERMISSIONS, permissions)
67 self.set_setting(SETTING_PERMISSIONS, permissions)
68
68
69 def del_permission(self, permission):
69 def del_permission(self, permission):
70 permissions = self.get_setting(SETTING_PERMISSIONS)
70 permissions = self.get_setting(SETTING_PERMISSIONS)
71 if not permissions:
71 if not permissions:
72 permissions = []
72 permissions = []
73 else:
73 else:
74 permissions.remove(permission)
74 permissions.remove(permission)
75 self.set_setting(SETTING_PERMISSIONS, permissions)
75 self.set_setting(SETTING_PERMISSIONS, permissions)
76
76
77 def get_fav_tags(self) -> list:
77 def get_fav_tags(self) -> list:
78 tag_names = self.get_setting(SETTING_FAVORITE_TAGS)
78 tag_names = self.get_setting(SETTING_FAVORITE_TAGS)
79 tags = []
79 tags = []
80 if tag_names:
80 if tag_names:
81 tags = list(Tag.objects.filter(name__in=tag_names))
81 tags = list(Tag.objects.filter(aliases__name__in=tag_names))
82 return tags
82 return tags
83
83
84 def add_fav_tag(self, tag):
84 def add_fav_tag(self, tag):
85 tags = self.get_setting(SETTING_FAVORITE_TAGS)
85 tags = self.get_setting(SETTING_FAVORITE_TAGS)
86 if not tags:
86 if not tags:
87 tags = [tag.name]
87 tags = [tag.get_name()]
88 else:
88 else:
89 if not tag.name in tags:
89 if not tag.get_name() in tags:
90 tags.append(tag.name)
90 tags.append(tag.get_name())
91
91
92 tags.sort()
92 tags.sort()
93 self.set_setting(SETTING_FAVORITE_TAGS, tags)
93 self.set_setting(SETTING_FAVORITE_TAGS, tags)
94
94
95 def del_fav_tag(self, tag):
95 def del_fav_tag(self, tag):
96 tags = self.get_setting(SETTING_FAVORITE_TAGS)
96 tags = self.get_setting(SETTING_FAVORITE_TAGS)
97 if tag.name in tags:
97 if tag.get_name() in tags:
98 tags.remove(tag.name)
98 tags.remove(tag.get_name())
99 self.set_setting(SETTING_FAVORITE_TAGS, tags)
99 self.set_setting(SETTING_FAVORITE_TAGS, tags)
100
100
101 def get_hidden_tags(self) -> list:
101 def get_hidden_tags(self) -> list:
102 tag_names = self.get_setting(SETTING_HIDDEN_TAGS)
102 tag_names = self.get_setting(SETTING_HIDDEN_TAGS)
103 tags = []
103 tags = []
104 if tag_names:
104 if tag_names:
105 tags = list(Tag.objects.filter(name__in=tag_names))
105 tags = list(Tag.objects.filter(aliases__name__in=tag_names))
106
106
107 return tags
107 return tags
108
108
109 def add_hidden_tag(self, tag):
109 def add_hidden_tag(self, tag):
110 tags = self.get_setting(SETTING_HIDDEN_TAGS)
110 tags = self.get_setting(SETTING_HIDDEN_TAGS)
111 if not tags:
111 if not tags:
112 tags = [tag.name]
112 tags = [tag.get_name()]
113 else:
113 else:
114 if not tag.name in tags:
114 if not tag.get_name() in tags:
115 tags.append(tag.name)
115 tags.append(tag.get_name())
116
116
117 tags.sort()
117 tags.sort()
118 self.set_setting(SETTING_HIDDEN_TAGS, tags)
118 self.set_setting(SETTING_HIDDEN_TAGS, tags)
119
119
120 def del_hidden_tag(self, tag):
120 def del_hidden_tag(self, tag):
121 tags = self.get_setting(SETTING_HIDDEN_TAGS)
121 tags = self.get_setting(SETTING_HIDDEN_TAGS)
122 if tag.name in tags:
122 if tag.get_name() in tags:
123 tags.remove(tag.name)
123 tags.remove(tag.get_name())
124 self.set_setting(SETTING_HIDDEN_TAGS, tags)
124 self.set_setting(SETTING_HIDDEN_TAGS, tags)
125
125
126 def get_fav_threads(self) -> dict:
126 def get_fav_threads(self) -> dict:
127 return self.get_setting(SETTING_FAVORITE_THREADS, default=dict())
127 return self.get_setting(SETTING_FAVORITE_THREADS, default=dict())
128
128
129 def add_or_read_fav_thread(self, opening_post):
129 def add_or_read_fav_thread(self, opening_post):
130 threads = self.get_fav_threads()
130 threads = self.get_fav_threads()
131
131
132 max_fav_threads = settings.get_int('View', 'MaxFavoriteThreads')
132 max_fav_threads = settings.get_int('View', 'MaxFavoriteThreads')
133 if (str(opening_post.id) in threads) or (len(threads) < max_fav_threads):
133 if (str(opening_post.id) in threads) or (len(threads) < max_fav_threads):
134 thread = opening_post.get_thread()
134 thread = opening_post.get_thread()
135 # Don't check for new posts if the thread is archived already
135 # Don't check for new posts if the thread is archived already
136 if thread.is_archived():
136 if thread.is_archived():
137 last_id = FAV_THREAD_NO_UPDATES
137 last_id = FAV_THREAD_NO_UPDATES
138 else:
138 else:
139 last_id = thread.get_replies().last().id
139 last_id = thread.get_replies().last().id
140 threads[str(opening_post.id)] = last_id
140 threads[str(opening_post.id)] = last_id
141 self.set_setting(SETTING_FAVORITE_THREADS, threads)
141 self.set_setting(SETTING_FAVORITE_THREADS, threads)
142
142
143 def del_fav_thread(self, opening_post):
143 def del_fav_thread(self, opening_post):
144 threads = self.get_fav_threads()
144 threads = self.get_fav_threads()
145 if self.thread_is_fav(opening_post):
145 if self.thread_is_fav(opening_post):
146 del threads[str(opening_post.id)]
146 del threads[str(opening_post.id)]
147 self.set_setting(SETTING_FAVORITE_THREADS, threads)
147 self.set_setting(SETTING_FAVORITE_THREADS, threads)
148
148
149 def thread_is_fav(self, opening_post):
149 def thread_is_fav(self, opening_post):
150 return str(opening_post.id) in self.get_fav_threads()
150 return str(opening_post.id) in self.get_fav_threads()
151
151
152 def get_notification_usernames(self):
152 def get_notification_usernames(self):
153 names = set()
153 names = set()
154 name_list = self.get_setting(SETTING_USERNAME)
154 name_list = self.get_setting(SETTING_USERNAME)
155 if name_list is not None:
155 if name_list is not None:
156 name_list = name_list.strip()
156 name_list = name_list.strip()
157 if len(name_list) > 0:
157 if len(name_list) > 0:
158 names = name_list.lower().split(',')
158 names = name_list.lower().split(',')
159 names = set(name.strip() for name in names)
159 names = set(name.strip() for name in names)
160 return names
160 return names
161
161
162 def get_image_by_alias(self, alias):
162 def get_image_by_alias(self, alias):
163 images = self.get_setting(SETTING_IMAGES)
163 images = self.get_setting(SETTING_IMAGES)
164 if images is not None and len(images) > 0:
164 if images is not None and len(images) > 0:
165 return images.get(alias)
165 return images.get(alias)
166
166
167 def add_image_alias(self, alias, image):
167 def add_image_alias(self, alias, image):
168 images = self.get_setting(SETTING_IMAGES)
168 images = self.get_setting(SETTING_IMAGES)
169 if images is None:
169 if images is None:
170 images = dict()
170 images = dict()
171 images.put(alias, image)
171 images.put(alias, image)
172
172
173
173
174 class SessionSettingsManager(SettingsManager):
174 class SessionSettingsManager(SettingsManager):
175 """
175 """
176 Session-based settings manager. All settings are saved to the user's
176 Session-based settings manager. All settings are saved to the user's
177 session.
177 session.
178 """
178 """
179 def __init__(self, session):
179 def __init__(self, session):
180 SettingsManager.__init__(self)
180 SettingsManager.__init__(self)
181 self.session = session
181 self.session = session
182
182
183 def get_setting(self, setting, default=None):
183 def get_setting(self, setting, default=None):
184 if setting in self.session:
184 if setting in self.session:
185 return self.session[setting]
185 return self.session[setting]
186 else:
186 else:
187 self.set_setting(setting, default)
187 self.set_setting(setting, default)
188 return default
188 return default
189
189
190 def set_setting(self, setting, value):
190 def set_setting(self, setting, value):
191 self.session[setting] = value
191 self.session[setting] = value
192
192
193
193
194 def get_settings_manager(request) -> SettingsManager:
194 def get_settings_manager(request) -> SettingsManager:
195 """
195 """
196 Get settings manager based on the request object. Currently only
196 Get settings manager based on the request object. Currently only
197 session-based manager is supported. In the future, cookie-based or
197 session-based manager is supported. In the future, cookie-based or
198 database-based managers could be implemented.
198 database-based managers could be implemented.
199 """
199 """
200 return SessionSettingsManager(request.session)
200 return SessionSettingsManager(request.session)
@@ -1,175 +1,179 b''
1 from boards.models.attachment import FILE_TYPES_IMAGE
1 from boards.models.attachment import FILE_TYPES_IMAGE
2 from django.contrib import admin
2 from django.contrib import admin
3 from django.utils.translation import ugettext_lazy as _
3 from django.utils.translation import ugettext_lazy as _
4 from django.core.urlresolvers import reverse
4 from django.core.urlresolvers import reverse
5 from boards.models import Post, Tag, Ban, Thread, Banner, Attachment, \
5 from boards.models import Post, Tag, Ban, Thread, Banner, Attachment, \
6 KeyPair, GlobalId, TagAlias
6 KeyPair, GlobalId, TagAlias
7
7
8
8
9 @admin.register(Post)
9 @admin.register(Post)
10 class PostAdmin(admin.ModelAdmin):
10 class PostAdmin(admin.ModelAdmin):
11
11
12 list_display = ('id', 'title', 'text', 'poster_ip', 'linked_images',
12 list_display = ('id', 'title', 'text', 'poster_ip', 'linked_images',
13 'foreign', 'tags')
13 'foreign', 'tags')
14 list_filter = ('pub_time',)
14 list_filter = ('pub_time',)
15 search_fields = ('id', 'title', 'text', 'poster_ip')
15 search_fields = ('id', 'title', 'text', 'poster_ip')
16 exclude = ('referenced_posts', 'refmap', 'images', 'global_id')
16 exclude = ('referenced_posts', 'refmap', 'images', 'global_id')
17 readonly_fields = ('poster_ip', 'thread', 'linked_images',
17 readonly_fields = ('poster_ip', 'thread', 'linked_images',
18 'attachments', 'uid', 'url', 'pub_time', 'opening', 'linked_global_id',
18 'attachments', 'uid', 'url', 'pub_time', 'opening', 'linked_global_id',
19 'version', 'foreign', 'tags')
19 'version', 'foreign', 'tags')
20
20
21 def ban_poster(self, request, queryset):
21 def ban_poster(self, request, queryset):
22 bans = 0
22 bans = 0
23 for post in queryset:
23 for post in queryset:
24 poster_ip = post.poster_ip
24 poster_ip = post.poster_ip
25 ban, created = Ban.objects.get_or_create(ip=poster_ip)
25 ban, created = Ban.objects.get_or_create(ip=poster_ip)
26 if created:
26 if created:
27 bans += 1
27 bans += 1
28 self.message_user(request, _('{} posters were banned').format(bans))
28 self.message_user(request, _('{} posters were banned').format(bans))
29
29
30 def ban_latter_with_delete(self, request, queryset):
30 def ban_latter_with_delete(self, request, queryset):
31 bans = 0
31 bans = 0
32 hidden = 0
32 hidden = 0
33 for post in queryset:
33 for post in queryset:
34 poster_ip = post.poster_ip
34 poster_ip = post.poster_ip
35 ban, created = Ban.objects.get_or_create(ip=poster_ip)
35 ban, created = Ban.objects.get_or_create(ip=poster_ip)
36 if created:
36 if created:
37 bans += 1
37 bans += 1
38 posts = Post.objects.filter(poster_ip=poster_ip, id__gte=post.id)
38 posts = Post.objects.filter(poster_ip=poster_ip, id__gte=post.id)
39 hidden += posts.count()
39 hidden += posts.count()
40 posts.delete()
40 posts.delete()
41 self.message_user(request, _('{} posters were banned, {} messages were removed.').format(bans, hidden))
41 self.message_user(request, _('{} posters were banned, {} messages were removed.').format(bans, hidden))
42 ban_latter_with_delete.short_description = _('Ban user and delete posts starting from this one and later')
42 ban_latter_with_delete.short_description = _('Ban user and delete posts starting from this one and later')
43
43
44 def linked_images(self, obj: Post):
44 def linked_images(self, obj: Post):
45 images = obj.attachments.filter(mimetype__in=FILE_TYPES_IMAGE)
45 images = obj.attachments.filter(mimetype__in=FILE_TYPES_IMAGE)
46 image_urls = ['<a href="{}"><img src="{}" /></a>'.format(
46 image_urls = ['<a href="{}"><img src="{}" /></a>'.format(
47 reverse('admin:%s_%s_change' % (image._meta.app_label,
47 reverse('admin:%s_%s_change' % (image._meta.app_label,
48 image._meta.model_name),
48 image._meta.model_name),
49 args=[image.id]), image.get_thumb_url()) for image in images]
49 args=[image.id]), image.get_thumb_url()) for image in images]
50 return ', '.join(image_urls)
50 return ', '.join(image_urls)
51 linked_images.allow_tags = True
51 linked_images.allow_tags = True
52
52
53 def linked_global_id(self, obj: Post):
53 def linked_global_id(self, obj: Post):
54 global_id = obj.global_id
54 global_id = obj.global_id
55 if global_id is not None:
55 if global_id is not None:
56 return '<a href="{}">{}</a>'.format(
56 return '<a href="{}">{}</a>'.format(
57 reverse('admin:%s_%s_change' % (global_id._meta.app_label,
57 reverse('admin:%s_%s_change' % (global_id._meta.app_label,
58 global_id._meta.model_name),
58 global_id._meta.model_name),
59 args=[global_id.id]), str(global_id))
59 args=[global_id.id]), str(global_id))
60 linked_global_id.allow_tags = True
60 linked_global_id.allow_tags = True
61
61
62 def tags(self, obj: Post):
62 def tags(self, obj: Post):
63 return ', '.join([tag.name for tag in obj.get_tags()])
63 return ', '.join([tag.name for tag in obj.get_tags()])
64
64
65 def save_model(self, request, obj, form, change):
65 def save_model(self, request, obj, form, change):
66 obj.increment_version()
66 obj.increment_version()
67 obj.save()
67 obj.save()
68 obj.clear_cache()
68 obj.clear_cache()
69
69
70 def foreign(self, obj: Post):
70 def foreign(self, obj: Post):
71 return obj is not None and obj.global_id is not None and\
71 return obj is not None and obj.global_id is not None and\
72 not obj.global_id.is_local()
72 not obj.global_id.is_local()
73
73
74 actions = ['ban_poster', 'ban_latter_with_delete']
74 actions = ['ban_poster', 'ban_latter_with_delete']
75
75
76
76
77 @admin.register(Tag)
77 @admin.register(Tag)
78 class TagAdmin(admin.ModelAdmin):
78 class TagAdmin(admin.ModelAdmin):
79
79
80 def thread_count(self, obj: Tag) -> int:
80 def thread_count(self, obj: Tag) -> int:
81 return obj.get_thread_count()
81 return obj.get_thread_count()
82
82
83 def display_children(self, obj: Tag):
83 def display_children(self, obj: Tag):
84 return ', '.join([str(child) for child in obj.get_children().all()])
84 return ', '.join([str(child) for child in obj.get_children().all()])
85
85
86 def name(self, obj: Tag):
87 return obj.get_name()
88
86 def save_model(self, request, obj, form, change):
89 def save_model(self, request, obj, form, change):
87 super().save_model(request, obj, form, change)
90 super().save_model(request, obj, form, change)
88 for thread in obj.get_threads().all():
91 for thread in obj.get_threads().all():
89 thread.refresh_tags()
92 thread.refresh_tags()
90 list_display = ('name', 'thread_count', 'display_children')
93 list_display = ('name', 'thread_count', 'display_children')
91 search_fields = ('name',)
94 search_fields = ('name',)
95 readonly_fields = ('name',)
92
96
93
97
94 @admin.register(TagAlias)
98 @admin.register(TagAlias)
95 class TagAliasAdmin(admin.ModelAdmin):
99 class TagAliasAdmin(admin.ModelAdmin):
96 list_display = ('locale', 'name', 'parent')
100 list_display = ('locale', 'name', 'parent')
97
101
98
102
99 @admin.register(Thread)
103 @admin.register(Thread)
100 class ThreadAdmin(admin.ModelAdmin):
104 class ThreadAdmin(admin.ModelAdmin):
101
105
102 def title(self, obj: Thread) -> str:
106 def title(self, obj: Thread) -> str:
103 return obj.get_opening_post().get_title()
107 return obj.get_opening_post().get_title()
104
108
105 def reply_count(self, obj: Thread) -> int:
109 def reply_count(self, obj: Thread) -> int:
106 return obj.get_reply_count()
110 return obj.get_reply_count()
107
111
108 def ip(self, obj: Thread):
112 def ip(self, obj: Thread):
109 return obj.get_opening_post().poster_ip
113 return obj.get_opening_post().poster_ip
110
114
111 def display_tags(self, obj: Thread):
115 def display_tags(self, obj: Thread):
112 return ', '.join([str(tag) for tag in obj.get_tags().all()])
116 return ', '.join([str(tag) for tag in obj.get_tags().all()])
113
117
114 def op(self, obj: Thread):
118 def op(self, obj: Thread):
115 return obj.get_opening_post_id()
119 return obj.get_opening_post_id()
116
120
117 # Save parent tags when editing tags
121 # Save parent tags when editing tags
118 def save_related(self, request, form, formsets, change):
122 def save_related(self, request, form, formsets, change):
119 super().save_related(request, form, formsets, change)
123 super().save_related(request, form, formsets, change)
120 form.instance.refresh_tags()
124 form.instance.refresh_tags()
121
125
122 def save_model(self, request, obj, form, change):
126 def save_model(self, request, obj, form, change):
123 op = obj.get_opening_post()
127 op = obj.get_opening_post()
124 op.increment_version()
128 op.increment_version()
125 op.save(update_fields=['version'])
129 op.save(update_fields=['version'])
126 obj.save()
130 obj.save()
127 op.clear_cache()
131 op.clear_cache()
128
132
129 list_display = ('id', 'op', 'title', 'reply_count', 'status', 'ip',
133 list_display = ('id', 'op', 'title', 'reply_count', 'status', 'ip',
130 'display_tags')
134 'display_tags')
131 list_filter = ('bump_time', 'status')
135 list_filter = ('bump_time', 'status')
132 search_fields = ('id', 'title')
136 search_fields = ('id', 'title')
133 filter_horizontal = ('tags',)
137 filter_horizontal = ('tags',)
134
138
135
139
136 @admin.register(KeyPair)
140 @admin.register(KeyPair)
137 class KeyPairAdmin(admin.ModelAdmin):
141 class KeyPairAdmin(admin.ModelAdmin):
138 list_display = ('public_key', 'primary')
142 list_display = ('public_key', 'primary')
139 list_filter = ('primary',)
143 list_filter = ('primary',)
140 search_fields = ('public_key',)
144 search_fields = ('public_key',)
141
145
142
146
143 @admin.register(Ban)
147 @admin.register(Ban)
144 class BanAdmin(admin.ModelAdmin):
148 class BanAdmin(admin.ModelAdmin):
145 list_display = ('ip', 'can_read')
149 list_display = ('ip', 'can_read')
146 list_filter = ('can_read',)
150 list_filter = ('can_read',)
147 search_fields = ('ip',)
151 search_fields = ('ip',)
148
152
149
153
150 @admin.register(Banner)
154 @admin.register(Banner)
151 class BannerAdmin(admin.ModelAdmin):
155 class BannerAdmin(admin.ModelAdmin):
152 list_display = ('title', 'text')
156 list_display = ('title', 'text')
153
157
154
158
155 @admin.register(Attachment)
159 @admin.register(Attachment)
156 class AttachmentAdmin(admin.ModelAdmin):
160 class AttachmentAdmin(admin.ModelAdmin):
157 list_display = ('__str__', 'mimetype', 'file', 'url', 'alias')
161 list_display = ('__str__', 'mimetype', 'file', 'url', 'alias')
158 search_fields = ('alias',)
162 search_fields = ('alias',)
159
163
160 def delete_alias(self, request, queryset):
164 def delete_alias(self, request, queryset):
161 for attachment in queryset:
165 for attachment in queryset:
162 attachment.alias = None
166 attachment.alias = None
163 attachment.save(update_fields=['alias'])
167 attachment.save(update_fields=['alias'])
164 self.message_user(request, _('Aliases removed'))
168 self.message_user(request, _('Aliases removed'))
165
169
166 actions = ['delete_alias']
170 actions = ['delete_alias']
167
171
168
172
169 @admin.register(GlobalId)
173 @admin.register(GlobalId)
170 class GlobalIdAdmin(admin.ModelAdmin):
174 class GlobalIdAdmin(admin.ModelAdmin):
171 def is_linked(self, obj):
175 def is_linked(self, obj):
172 return Post.objects.filter(global_id=obj).exists()
176 return Post.objects.filter(global_id=obj).exists()
173
177
174 list_display = ('__str__', 'is_linked',)
178 list_display = ('__str__', 'is_linked',)
175 readonly_fields = ('content',)
179 readonly_fields = ('content',)
@@ -1,534 +1,529 b''
1 import hashlib
1 import hashlib
2 import logging
2 import logging
3 import re
3 import re
4 import time
4 import time
5 import traceback
5 import traceback
6
6
7 import pytz
7 import pytz
8
8
9 from PIL import Image
9 from PIL import Image
10
10
11 from django import forms
11 from django import forms
12 from django.core.files.uploadedfile import SimpleUploadedFile, UploadedFile
12 from django.core.files.uploadedfile import SimpleUploadedFile, UploadedFile
13 from django.forms.utils import ErrorList
13 from django.forms.utils import ErrorList
14 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
14 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
15 from django.core.files.images import get_image_dimensions
15 from django.core.files.images import get_image_dimensions
16
16
17 import boards.settings as board_settings
17 import boards.settings as board_settings
18 import neboard
18 import neboard
19 from boards import utils
19 from boards import utils
20 from boards.abstracts.attachment_alias import get_image_by_alias
20 from boards.abstracts.attachment_alias import get_image_by_alias
21 from boards.abstracts.settingsmanager import get_settings_manager
21 from boards.abstracts.settingsmanager import get_settings_manager
22 from boards.forms.fields import UrlFileField
22 from boards.forms.fields import UrlFileField
23 from boards.mdx_neboard import formatters
23 from boards.mdx_neboard import formatters
24 from boards.models import Attachment
24 from boards.models import Attachment
25 from boards.models import Tag
25 from boards.models import Tag
26 from boards.models.attachment.downloaders import download, REGEX_MAGNET
26 from boards.models.attachment.downloaders import download, REGEX_MAGNET
27 from boards.models.post import TITLE_MAX_LENGTH
27 from boards.models.post import TITLE_MAX_LENGTH
28 from boards.utils import validate_file_size, get_file_mimetype, \
28 from boards.utils import validate_file_size, get_file_mimetype, \
29 FILE_EXTENSION_DELIMITER
29 FILE_EXTENSION_DELIMITER
30 from boards.models.attachment.viewers import FILE_TYPES_IMAGE
30 from boards.models.attachment.viewers import FILE_TYPES_IMAGE
31 from neboard import settings
31 from neboard import settings
32
32
33 SECTION_FORMS = 'Forms'
33 SECTION_FORMS = 'Forms'
34
34
35 POW_HASH_LENGTH = 16
35 POW_HASH_LENGTH = 16
36 POW_LIFE_MINUTES = 5
36 POW_LIFE_MINUTES = 5
37
37
38 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
38 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
39 REGEX_USERNAMES = re.compile(r'^[\w\s\d,]+$', re.UNICODE)
39 REGEX_USERNAMES = re.compile(r'^[\w\s\d,]+$', re.UNICODE)
40 REGEX_URL = re.compile(r'^(http|https|ftp):\/\/', re.UNICODE)
40 REGEX_URL = re.compile(r'^(http|https|ftp):\/\/', re.UNICODE)
41
41
42 VETERAN_POSTING_DELAY = 5
42 VETERAN_POSTING_DELAY = 5
43
43
44 ATTRIBUTE_PLACEHOLDER = 'placeholder'
44 ATTRIBUTE_PLACEHOLDER = 'placeholder'
45 ATTRIBUTE_ROWS = 'rows'
45 ATTRIBUTE_ROWS = 'rows'
46
46
47 LAST_POST_TIME = 'last_post_time'
47 LAST_POST_TIME = 'last_post_time'
48 LAST_LOGIN_TIME = 'last_login_time'
48 LAST_LOGIN_TIME = 'last_login_time'
49 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
49 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
50 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
50 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
51
51
52 LABEL_TITLE = _('Title')
52 LABEL_TITLE = _('Title')
53 LABEL_TEXT = _('Text')
53 LABEL_TEXT = _('Text')
54 LABEL_TAG = _('Tag')
54 LABEL_TAG = _('Tag')
55 LABEL_SEARCH = _('Search')
55 LABEL_SEARCH = _('Search')
56 LABEL_FILE = _('File')
56 LABEL_FILE = _('File')
57 LABEL_DUPLICATES = _('Check for duplicates')
57 LABEL_DUPLICATES = _('Check for duplicates')
58 LABEL_URL = _('Do not download URLs')
58 LABEL_URL = _('Do not download URLs')
59
59
60 ERROR_SPEED = 'Please wait %(delay)d second before sending message'
60 ERROR_SPEED = 'Please wait %(delay)d second before sending message'
61 ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message'
61 ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message'
62 ERROR_MANY_FILES = 'You can post no more than %(files)d file.'
62 ERROR_MANY_FILES = 'You can post no more than %(files)d file.'
63 ERROR_MANY_FILES_PLURAL = 'You can post no more than %(files)d files.'
63 ERROR_MANY_FILES_PLURAL = 'You can post no more than %(files)d files.'
64 ERROR_DUPLICATES = 'Some files are already present on the board.'
64 ERROR_DUPLICATES = 'Some files are already present on the board.'
65
65
66 TAG_MAX_LENGTH = 20
66 TAG_MAX_LENGTH = 20
67
67
68 TEXTAREA_ROWS = 4
68 TEXTAREA_ROWS = 4
69
69
70 TRIPCODE_DELIM = '#'
70 TRIPCODE_DELIM = '#'
71
71
72 # TODO Maybe this may be converted into the database table?
72 # TODO Maybe this may be converted into the database table?
73 MIMETYPE_EXTENSIONS = {
73 MIMETYPE_EXTENSIONS = {
74 'image/jpeg': 'jpeg',
74 'image/jpeg': 'jpeg',
75 'image/png': 'png',
75 'image/png': 'png',
76 'image/gif': 'gif',
76 'image/gif': 'gif',
77 'video/webm': 'webm',
77 'video/webm': 'webm',
78 'application/pdf': 'pdf',
78 'application/pdf': 'pdf',
79 'x-diff': 'diff',
79 'x-diff': 'diff',
80 'image/svg+xml': 'svg',
80 'image/svg+xml': 'svg',
81 'application/x-shockwave-flash': 'swf',
81 'application/x-shockwave-flash': 'swf',
82 'image/x-ms-bmp': 'bmp',
82 'image/x-ms-bmp': 'bmp',
83 'image/bmp': 'bmp',
83 'image/bmp': 'bmp',
84 }
84 }
85
85
86
86
87 logger = logging.getLogger('boards.forms')
87 logger = logging.getLogger('boards.forms')
88
88
89
89
90 def get_timezones():
90 def get_timezones():
91 timezones = []
91 timezones = []
92 for tz in pytz.common_timezones:
92 for tz in pytz.common_timezones:
93 timezones.append((tz, tz),)
93 timezones.append((tz, tz),)
94 return timezones
94 return timezones
95
95
96
96
97 class FormatPanel(forms.Textarea):
97 class FormatPanel(forms.Textarea):
98 """
98 """
99 Panel for text formatting. Consists of buttons to add different tags to the
99 Panel for text formatting. Consists of buttons to add different tags to the
100 form text area.
100 form text area.
101 """
101 """
102
102
103 def render(self, name, value, attrs=None):
103 def render(self, name, value, attrs=None):
104 output = '<div id="mark-panel">'
104 output = '<div id="mark-panel">'
105 for formatter in formatters:
105 for formatter in formatters:
106 output += '<span class="mark_btn"' + \
106 output += '<span class="mark_btn"' + \
107 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
107 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
108 '\', \'' + formatter.format_right + '\')">' + \
108 '\', \'' + formatter.format_right + '\')">' + \
109 formatter.preview_left + formatter.name + \
109 formatter.preview_left + formatter.name + \
110 formatter.preview_right + '</span>'
110 formatter.preview_right + '</span>'
111
111
112 output += '</div>'
112 output += '</div>'
113 output += super(FormatPanel, self).render(name, value, attrs=attrs)
113 output += super(FormatPanel, self).render(name, value, attrs=attrs)
114
114
115 return output
115 return output
116
116
117
117
118 class PlainErrorList(ErrorList):
118 class PlainErrorList(ErrorList):
119 def __unicode__(self):
119 def __unicode__(self):
120 return self.as_text()
120 return self.as_text()
121
121
122 def as_text(self):
122 def as_text(self):
123 return ''.join(['(!) %s ' % e for e in self])
123 return ''.join(['(!) %s ' % e for e in self])
124
124
125
125
126 class NeboardForm(forms.Form):
126 class NeboardForm(forms.Form):
127 """
127 """
128 Form with neboard-specific formatting.
128 Form with neboard-specific formatting.
129 """
129 """
130 required_css_class = 'required-field'
130 required_css_class = 'required-field'
131
131
132 def as_div(self):
132 def as_div(self):
133 """
133 """
134 Returns this form rendered as HTML <as_div>s.
134 Returns this form rendered as HTML <as_div>s.
135 """
135 """
136
136
137 return self._html_output(
137 return self._html_output(
138 # TODO Do not show hidden rows in the list here
138 # TODO Do not show hidden rows in the list here
139 normal_row='<div class="form-row">'
139 normal_row='<div class="form-row">'
140 '<div class="form-label">'
140 '<div class="form-label">'
141 '%(label)s'
141 '%(label)s'
142 '</div>'
142 '</div>'
143 '<div class="form-input">'
143 '<div class="form-input">'
144 '%(field)s'
144 '%(field)s'
145 '</div>'
145 '</div>'
146 '</div>'
146 '</div>'
147 '<div class="form-row">'
147 '<div class="form-row">'
148 '%(help_text)s'
148 '%(help_text)s'
149 '</div>',
149 '</div>',
150 error_row='<div class="form-row">'
150 error_row='<div class="form-row">'
151 '<div class="form-label"></div>'
151 '<div class="form-label"></div>'
152 '<div class="form-errors">%s</div>'
152 '<div class="form-errors">%s</div>'
153 '</div>',
153 '</div>',
154 row_ender='</div>',
154 row_ender='</div>',
155 help_text_html='%s',
155 help_text_html='%s',
156 errors_on_separate_row=True)
156 errors_on_separate_row=True)
157
157
158 def as_json_errors(self):
158 def as_json_errors(self):
159 errors = []
159 errors = []
160
160
161 for name, field in list(self.fields.items()):
161 for name, field in list(self.fields.items()):
162 if self[name].errors:
162 if self[name].errors:
163 errors.append({
163 errors.append({
164 'field': name,
164 'field': name,
165 'errors': self[name].errors.as_text(),
165 'errors': self[name].errors.as_text(),
166 })
166 })
167
167
168 return errors
168 return errors
169
169
170
170
171 class PostForm(NeboardForm):
171 class PostForm(NeboardForm):
172
172
173 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
173 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
174 label=LABEL_TITLE,
174 label=LABEL_TITLE,
175 widget=forms.TextInput(
175 widget=forms.TextInput(
176 attrs={ATTRIBUTE_PLACEHOLDER: 'title#tripcode'}))
176 attrs={ATTRIBUTE_PLACEHOLDER: 'title#tripcode'}))
177 text = forms.CharField(
177 text = forms.CharField(
178 widget=FormatPanel(attrs={
178 widget=FormatPanel(attrs={
179 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
179 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
180 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
180 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
181 }),
181 }),
182 required=False, label=LABEL_TEXT)
182 required=False, label=LABEL_TEXT)
183 no_download = forms.BooleanField(required=False, label=LABEL_URL)
183 no_download = forms.BooleanField(required=False, label=LABEL_URL)
184 file = UrlFileField(required=False, label=LABEL_FILE)
184 file = UrlFileField(required=False, label=LABEL_FILE)
185
185
186 # This field is for spam prevention only
186 # This field is for spam prevention only
187 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
187 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
188 widget=forms.TextInput(attrs={
188 widget=forms.TextInput(attrs={
189 'class': 'form-email'}))
189 'class': 'form-email'}))
190 subscribe = forms.BooleanField(required=False, label=_('Subscribe to thread'))
190 subscribe = forms.BooleanField(required=False, label=_('Subscribe to thread'))
191 check_duplicates = forms.BooleanField(required=False, label=LABEL_DUPLICATES)
191 check_duplicates = forms.BooleanField(required=False, label=LABEL_DUPLICATES)
192
192
193 guess = forms.CharField(widget=forms.HiddenInput(), required=False)
193 guess = forms.CharField(widget=forms.HiddenInput(), required=False)
194 timestamp = forms.CharField(widget=forms.HiddenInput(), required=False)
194 timestamp = forms.CharField(widget=forms.HiddenInput(), required=False)
195 iteration = forms.CharField(widget=forms.HiddenInput(), required=False)
195 iteration = forms.CharField(widget=forms.HiddenInput(), required=False)
196
196
197 session = None
197 session = None
198 need_to_ban = False
198 need_to_ban = False
199 image = None
199 image = None
200
200
201 def clean_title(self):
201 def clean_title(self):
202 title = self.cleaned_data['title']
202 title = self.cleaned_data['title']
203 if title:
203 if title:
204 if len(title) > TITLE_MAX_LENGTH:
204 if len(title) > TITLE_MAX_LENGTH:
205 raise forms.ValidationError(_('Title must have less than %s '
205 raise forms.ValidationError(_('Title must have less than %s '
206 'characters') %
206 'characters') %
207 str(TITLE_MAX_LENGTH))
207 str(TITLE_MAX_LENGTH))
208 return title
208 return title
209
209
210 def clean_text(self):
210 def clean_text(self):
211 text = self.cleaned_data['text'].strip()
211 text = self.cleaned_data['text'].strip()
212 if text:
212 if text:
213 max_length = board_settings.get_int(SECTION_FORMS, 'MaxTextLength')
213 max_length = board_settings.get_int(SECTION_FORMS, 'MaxTextLength')
214 if len(text) > max_length:
214 if len(text) > max_length:
215 raise forms.ValidationError(_('Text must have less than %s '
215 raise forms.ValidationError(_('Text must have less than %s '
216 'characters') % str(max_length))
216 'characters') % str(max_length))
217 return text
217 return text
218
218
219 def clean_file(self):
219 def clean_file(self):
220 return self._clean_files(self.cleaned_data['file'])
220 return self._clean_files(self.cleaned_data['file'])
221
221
222 def clean(self):
222 def clean(self):
223 cleaned_data = super(PostForm, self).clean()
223 cleaned_data = super(PostForm, self).clean()
224
224
225 if cleaned_data['email']:
225 if cleaned_data['email']:
226 if board_settings.get_bool(SECTION_FORMS, 'Autoban'):
226 if board_settings.get_bool(SECTION_FORMS, 'Autoban'):
227 self.need_to_ban = True
227 self.need_to_ban = True
228 raise forms.ValidationError('A human cannot enter a hidden field')
228 raise forms.ValidationError('A human cannot enter a hidden field')
229
229
230 if not self.errors:
230 if not self.errors:
231 self._clean_text_file()
231 self._clean_text_file()
232
232
233 limit_speed = board_settings.get_bool(SECTION_FORMS, 'LimitPostingSpeed')
233 limit_speed = board_settings.get_bool(SECTION_FORMS, 'LimitPostingSpeed')
234 limit_first = board_settings.get_bool(SECTION_FORMS, 'LimitFirstPosting')
234 limit_first = board_settings.get_bool(SECTION_FORMS, 'LimitFirstPosting')
235
235
236 settings_manager = get_settings_manager(self)
236 settings_manager = get_settings_manager(self)
237 if not self.errors and limit_speed or (limit_first and not settings_manager.get_setting('confirmed_user')):
237 if not self.errors and limit_speed or (limit_first and not settings_manager.get_setting('confirmed_user')):
238 pow_difficulty = board_settings.get_int(SECTION_FORMS, 'PowDifficulty')
238 pow_difficulty = board_settings.get_int(SECTION_FORMS, 'PowDifficulty')
239 if pow_difficulty > 0:
239 if pow_difficulty > 0:
240 # PoW-based
240 # PoW-based
241 if cleaned_data['timestamp'] \
241 if cleaned_data['timestamp'] \
242 and cleaned_data['iteration'] and cleaned_data['guess'] \
242 and cleaned_data['iteration'] and cleaned_data['guess'] \
243 and not settings_manager.get_setting('confirmed_user'):
243 and not settings_manager.get_setting('confirmed_user'):
244 self._validate_hash(cleaned_data['timestamp'], cleaned_data['iteration'], cleaned_data['guess'], cleaned_data['text'])
244 self._validate_hash(cleaned_data['timestamp'], cleaned_data['iteration'], cleaned_data['guess'], cleaned_data['text'])
245 else:
245 else:
246 # Time-based
246 # Time-based
247 self._validate_posting_speed()
247 self._validate_posting_speed()
248 settings_manager.set_setting('confirmed_user', True)
248 settings_manager.set_setting('confirmed_user', True)
249 if self.cleaned_data['check_duplicates']:
249 if self.cleaned_data['check_duplicates']:
250 self._check_file_duplicates(self.get_files())
250 self._check_file_duplicates(self.get_files())
251
251
252 return cleaned_data
252 return cleaned_data
253
253
254 def get_files(self):
254 def get_files(self):
255 """
255 """
256 Gets file from form or URL.
256 Gets file from form or URL.
257 """
257 """
258
258
259 files = []
259 files = []
260 for file in self.cleaned_data['file']:
260 for file in self.cleaned_data['file']:
261 if isinstance(file, UploadedFile):
261 if isinstance(file, UploadedFile):
262 files.append(file)
262 files.append(file)
263
263
264 return files
264 return files
265
265
266 def get_file_urls(self):
266 def get_file_urls(self):
267 files = []
267 files = []
268 for file in self.cleaned_data['file']:
268 for file in self.cleaned_data['file']:
269 if type(file) == str:
269 if type(file) == str:
270 files.append(file)
270 files.append(file)
271
271
272 return files
272 return files
273
273
274 def get_tripcode(self):
274 def get_tripcode(self):
275 title = self.cleaned_data['title']
275 title = self.cleaned_data['title']
276 if title is not None and TRIPCODE_DELIM in title:
276 if title is not None and TRIPCODE_DELIM in title:
277 code = title.split(TRIPCODE_DELIM, maxsplit=1)[1] + neboard.settings.SECRET_KEY
277 code = title.split(TRIPCODE_DELIM, maxsplit=1)[1] + neboard.settings.SECRET_KEY
278 tripcode = hashlib.md5(code.encode()).hexdigest()
278 tripcode = hashlib.md5(code.encode()).hexdigest()
279 else:
279 else:
280 tripcode = ''
280 tripcode = ''
281 return tripcode
281 return tripcode
282
282
283 def get_title(self):
283 def get_title(self):
284 title = self.cleaned_data['title']
284 title = self.cleaned_data['title']
285 if title is not None and TRIPCODE_DELIM in title:
285 if title is not None and TRIPCODE_DELIM in title:
286 return title.split(TRIPCODE_DELIM, maxsplit=1)[0]
286 return title.split(TRIPCODE_DELIM, maxsplit=1)[0]
287 else:
287 else:
288 return title
288 return title
289
289
290 def get_images(self):
290 def get_images(self):
291 if self.image:
291 if self.image:
292 return [self.image]
292 return [self.image]
293 else:
293 else:
294 return []
294 return []
295
295
296 def is_subscribe(self):
296 def is_subscribe(self):
297 return self.cleaned_data['subscribe']
297 return self.cleaned_data['subscribe']
298
298
299 def _update_file_extension(self, file):
299 def _update_file_extension(self, file):
300 if file:
300 if file:
301 mimetype = get_file_mimetype(file)
301 mimetype = get_file_mimetype(file)
302 extension = MIMETYPE_EXTENSIONS.get(mimetype)
302 extension = MIMETYPE_EXTENSIONS.get(mimetype)
303 if extension:
303 if extension:
304 filename = file.name.split(FILE_EXTENSION_DELIMITER, 1)[0]
304 filename = file.name.split(FILE_EXTENSION_DELIMITER, 1)[0]
305 new_filename = filename + FILE_EXTENSION_DELIMITER + extension
305 new_filename = filename + FILE_EXTENSION_DELIMITER + extension
306
306
307 file.name = new_filename
307 file.name = new_filename
308 else:
308 else:
309 logger.info('Unrecognized file mimetype: {}'.format(mimetype))
309 logger.info('Unrecognized file mimetype: {}'.format(mimetype))
310
310
311 def _clean_files(self, inputs):
311 def _clean_files(self, inputs):
312 files = []
312 files = []
313
313
314 max_file_count = board_settings.get_int(SECTION_FORMS, 'MaxFileCount')
314 max_file_count = board_settings.get_int(SECTION_FORMS, 'MaxFileCount')
315 if len(inputs) > max_file_count:
315 if len(inputs) > max_file_count:
316 raise forms.ValidationError(
316 raise forms.ValidationError(
317 ungettext_lazy(ERROR_MANY_FILES, ERROR_MANY_FILES,
317 ungettext_lazy(ERROR_MANY_FILES, ERROR_MANY_FILES,
318 max_file_count) % {'files': max_file_count})
318 max_file_count) % {'files': max_file_count})
319 for file_input in inputs:
319 for file_input in inputs:
320 if isinstance(file_input, UploadedFile):
320 if isinstance(file_input, UploadedFile):
321 files.append(self._clean_file_file(file_input))
321 files.append(self._clean_file_file(file_input))
322 else:
322 else:
323 files.append(self._clean_file_url(file_input))
323 files.append(self._clean_file_url(file_input))
324
324
325 for file in files:
325 for file in files:
326 self._validate_image_dimensions(file)
326 self._validate_image_dimensions(file)
327
327
328 return files
328 return files
329
329
330 def _validate_image_dimensions(self, file):
330 def _validate_image_dimensions(self, file):
331 if isinstance(file, UploadedFile):
331 if isinstance(file, UploadedFile):
332 mimetype = get_file_mimetype(file)
332 mimetype = get_file_mimetype(file)
333 if mimetype.split('/')[-1] in FILE_TYPES_IMAGE:
333 if mimetype.split('/')[-1] in FILE_TYPES_IMAGE:
334 Image.warnings.simplefilter('error', Image.DecompressionBombWarning)
334 Image.warnings.simplefilter('error', Image.DecompressionBombWarning)
335 try:
335 try:
336 print(get_image_dimensions(file))
336 print(get_image_dimensions(file))
337 except Exception:
337 except Exception:
338 raise forms.ValidationError('Possible decompression bomb or large image.')
338 raise forms.ValidationError('Possible decompression bomb or large image.')
339
339
340 def _clean_file_file(self, file):
340 def _clean_file_file(self, file):
341 validate_file_size(file.size)
341 validate_file_size(file.size)
342 self._update_file_extension(file)
342 self._update_file_extension(file)
343
343
344 return file
344 return file
345
345
346 def _clean_file_url(self, url):
346 def _clean_file_url(self, url):
347 file = None
347 file = None
348
348
349 if url:
349 if url:
350 if self.cleaned_data['no_download']:
350 if self.cleaned_data['no_download']:
351 return url
351 return url
352
352
353 try:
353 try:
354 file = get_image_by_alias(url, self.session)
354 file = get_image_by_alias(url, self.session)
355 self.image = file
355 self.image = file
356
356
357 if file is not None:
357 if file is not None:
358 return
358 return
359
359
360 if file is None:
360 if file is None:
361 file = self._get_file_from_url(url)
361 file = self._get_file_from_url(url)
362 if not file:
362 if not file:
363 raise forms.ValidationError(_('Invalid URL'))
363 raise forms.ValidationError(_('Invalid URL'))
364 else:
364 else:
365 validate_file_size(file.size)
365 validate_file_size(file.size)
366 self._update_file_extension(file)
366 self._update_file_extension(file)
367 except forms.ValidationError as e:
367 except forms.ValidationError as e:
368 # Assume we will get the plain URL instead of a file and save it
368 # Assume we will get the plain URL instead of a file and save it
369 if REGEX_URL.match(url) or REGEX_MAGNET.match(url):
369 if REGEX_URL.match(url) or REGEX_MAGNET.match(url):
370 logger.info('Error in forms: {}'.format(e))
370 logger.info('Error in forms: {}'.format(e))
371 return url
371 return url
372 else:
372 else:
373 raise e
373 raise e
374
374
375 return file
375 return file
376
376
377 def _clean_text_file(self):
377 def _clean_text_file(self):
378 text = self.cleaned_data.get('text')
378 text = self.cleaned_data.get('text')
379 file = self.get_files()
379 file = self.get_files()
380 file_url = self.get_file_urls()
380 file_url = self.get_file_urls()
381 images = self.get_images()
381 images = self.get_images()
382
382
383 if (not text) and (not file) and (not file_url) and len(images) == 0:
383 if (not text) and (not file) and (not file_url) and len(images) == 0:
384 error_message = _('Either text or file must be entered.')
384 error_message = _('Either text or file must be entered.')
385 self._add_general_error(error_message)
385 self._add_general_error(error_message)
386
386
387 def _validate_posting_speed(self):
387 def _validate_posting_speed(self):
388 can_post = True
388 can_post = True
389
389
390 posting_delay = board_settings.get_int(SECTION_FORMS, 'PostingDelay')
390 posting_delay = board_settings.get_int(SECTION_FORMS, 'PostingDelay')
391
391
392 if board_settings.get_bool(SECTION_FORMS, 'LimitPostingSpeed'):
392 if board_settings.get_bool(SECTION_FORMS, 'LimitPostingSpeed'):
393 now = time.time()
393 now = time.time()
394
394
395 current_delay = 0
395 current_delay = 0
396
396
397 if LAST_POST_TIME not in self.session:
397 if LAST_POST_TIME not in self.session:
398 self.session[LAST_POST_TIME] = now
398 self.session[LAST_POST_TIME] = now
399
399
400 need_delay = True
400 need_delay = True
401 else:
401 else:
402 last_post_time = self.session.get(LAST_POST_TIME)
402 last_post_time = self.session.get(LAST_POST_TIME)
403 current_delay = int(now - last_post_time)
403 current_delay = int(now - last_post_time)
404
404
405 need_delay = current_delay < posting_delay
405 need_delay = current_delay < posting_delay
406
406
407 if need_delay:
407 if need_delay:
408 delay = posting_delay - current_delay
408 delay = posting_delay - current_delay
409 error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL,
409 error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL,
410 delay) % {'delay': delay}
410 delay) % {'delay': delay}
411 self._add_general_error(error_message)
411 self._add_general_error(error_message)
412
412
413 can_post = False
413 can_post = False
414
414
415 if can_post:
415 if can_post:
416 self.session[LAST_POST_TIME] = now
416 self.session[LAST_POST_TIME] = now
417
417
418 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
418 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
419 """
419 """
420 Gets an file file from URL.
420 Gets an file file from URL.
421 """
421 """
422
422
423 try:
423 try:
424 return download(url)
424 return download(url)
425 except forms.ValidationError as e:
425 except forms.ValidationError as e:
426 raise e
426 raise e
427 except Exception as e:
427 except Exception as e:
428 raise forms.ValidationError(e)
428 raise forms.ValidationError(e)
429
429
430 def _validate_hash(self, timestamp: str, iteration: str, guess: str, message: str):
430 def _validate_hash(self, timestamp: str, iteration: str, guess: str, message: str):
431 payload = timestamp + message.replace('\r\n', '\n')
431 payload = timestamp + message.replace('\r\n', '\n')
432 difficulty = board_settings.get_int(SECTION_FORMS, 'PowDifficulty')
432 difficulty = board_settings.get_int(SECTION_FORMS, 'PowDifficulty')
433 target = str(int(2 ** (POW_HASH_LENGTH * 3) / difficulty))
433 target = str(int(2 ** (POW_HASH_LENGTH * 3) / difficulty))
434 if len(target) < POW_HASH_LENGTH:
434 if len(target) < POW_HASH_LENGTH:
435 target = '0' * (POW_HASH_LENGTH - len(target)) + target
435 target = '0' * (POW_HASH_LENGTH - len(target)) + target
436
436
437 computed_guess = hashlib.sha256((payload + iteration).encode())\
437 computed_guess = hashlib.sha256((payload + iteration).encode())\
438 .hexdigest()[0:POW_HASH_LENGTH]
438 .hexdigest()[0:POW_HASH_LENGTH]
439 if guess != computed_guess or guess > target:
439 if guess != computed_guess or guess > target:
440 self._add_general_error(_('Invalid PoW.'))
440 self._add_general_error(_('Invalid PoW.'))
441
441
442 def _check_file_duplicates(self, files):
442 def _check_file_duplicates(self, files):
443 for file in files:
443 for file in files:
444 file_hash = utils.get_file_hash(file)
444 file_hash = utils.get_file_hash(file)
445 if Attachment.objects.get_existing_duplicate(file_hash, file):
445 if Attachment.objects.get_existing_duplicate(file_hash, file):
446 self._add_general_error(_(ERROR_DUPLICATES))
446 self._add_general_error(_(ERROR_DUPLICATES))
447
447
448 def _add_general_error(self, message):
448 def _add_general_error(self, message):
449 self.add_error('text', forms.ValidationError(message))
449 self.add_error('text', forms.ValidationError(message))
450
450
451
451
452 class ThreadForm(PostForm):
452 class ThreadForm(PostForm):
453
453
454 tags = forms.CharField(
454 tags = forms.CharField(
455 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
455 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
456 max_length=100, label=_('Tags'), required=True)
456 max_length=100, label=_('Tags'), required=True)
457 monochrome = forms.BooleanField(label=_('Monochrome'), required=False)
457 monochrome = forms.BooleanField(label=_('Monochrome'), required=False)
458
458
459 def clean_tags(self):
459 def clean_tags(self):
460 tags = self.cleaned_data['tags'].strip()
460 tags = self.cleaned_data['tags'].strip()
461
461
462 if not tags or not REGEX_TAGS.match(tags):
462 if not tags or not REGEX_TAGS.match(tags):
463 raise forms.ValidationError(
463 raise forms.ValidationError(
464 _('Inappropriate characters in tags.'))
464 _('Inappropriate characters in tags.'))
465
465
466 default_tag_name = board_settings.get(SECTION_FORMS, 'DefaultTag')\
466 default_tag_name = board_settings.get(SECTION_FORMS, 'DefaultTag')\
467 .strip().lower()
467 .strip().lower()
468
468
469 required_tag_exists = False
469 required_tag_exists = False
470 tag_set = set()
470 tag_set = set()
471 for tag_string in tags.split():
471 for tag_string in tags.split():
472 tag_name = tag_string.strip().lower()
472 tag_name = tag_string.strip().lower()
473 if tag_name == default_tag_name:
473 if tag_name == default_tag_name:
474 required_tag_exists = True
474 required_tag_exists = True
475 tag, created = Tag.objects.get_or_create(
475 tag, created = Tag.objects.get_or_create_with_alias(
476 name=tag_name, required=True)
476 name=tag_name, required=True)
477 else:
477 else:
478 tag = Tag.objects.get_by_alias(tag_name)
478 tag, created = Tag.objects.get_or_create_with_alias(name=tag_name)
479 if tag:
480 created = False
481 else:
482 tag, created = Tag.objects.get_or_create(
483 name=tag_name)
484 tag_set.add(tag)
479 tag_set.add(tag)
485
480
486 # If this is a new tag, don't check for its parents because nobody
481 # If this is a new tag, don't check for its parents because nobody
487 # added them yet
482 # added them yet
488 if not created:
483 if not created:
489 tag_set |= set(tag.get_all_parents())
484 tag_set |= set(tag.get_all_parents())
490
485
491 for tag in tag_set:
486 for tag in tag_set:
492 if tag.required:
487 if tag.required:
493 required_tag_exists = True
488 required_tag_exists = True
494 break
489 break
495
490
496 # Use default tag if no section exists
491 # Use default tag if no section exists
497 if not required_tag_exists:
492 if not required_tag_exists:
498 default_tag, created = Tag.objects.get_or_create(
493 default_tag, created = Tag.objects.get_or_create(
499 name=default_tag_name, required=True)
494 name=default_tag_name, required=True)
500 tag_set.add(default_tag)
495 tag_set.add(default_tag)
501
496
502 return tag_set
497 return tag_set
503
498
504 def clean(self):
499 def clean(self):
505 cleaned_data = super(ThreadForm, self).clean()
500 cleaned_data = super(ThreadForm, self).clean()
506
501
507 return cleaned_data
502 return cleaned_data
508
503
509 def is_monochrome(self):
504 def is_monochrome(self):
510 return self.cleaned_data['monochrome']
505 return self.cleaned_data['monochrome']
511
506
512
507
513 class SettingsForm(NeboardForm):
508 class SettingsForm(NeboardForm):
514
509
515 theme = forms.ChoiceField(
510 theme = forms.ChoiceField(
516 choices=board_settings.get_list_dict('View', 'Themes'),
511 choices=board_settings.get_list_dict('View', 'Themes'),
517 label=_('Theme'))
512 label=_('Theme'))
518 image_viewer = forms.ChoiceField(
513 image_viewer = forms.ChoiceField(
519 choices=board_settings.get_list_dict('View', 'ImageViewers'),
514 choices=board_settings.get_list_dict('View', 'ImageViewers'),
520 label=_('Image view mode'))
515 label=_('Image view mode'))
521 username = forms.CharField(label=_('User name'), required=False)
516 username = forms.CharField(label=_('User name'), required=False)
522 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
517 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
523
518
524 def clean_username(self):
519 def clean_username(self):
525 username = self.cleaned_data['username']
520 username = self.cleaned_data['username']
526
521
527 if username and not REGEX_USERNAMES.match(username):
522 if username and not REGEX_USERNAMES.match(username):
528 raise forms.ValidationError(_('Inappropriate characters.'))
523 raise forms.ValidationError(_('Inappropriate characters.'))
529
524
530 return username
525 return username
531
526
532
527
533 class SearchForm(NeboardForm):
528 class SearchForm(NeboardForm):
534 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
529 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
@@ -1,177 +1,194 b''
1 import hashlib
1 import hashlib
2 import re
2 import re
3
3
4 from boards.models.attachment import FILE_TYPES_IMAGE
4 from boards.models.attachment import FILE_TYPES_IMAGE
5 from django.template.loader import render_to_string
5 from django.template.loader import render_to_string
6 from django.db import models
6 from django.db import models
7 from django.db.models import Count
7 from django.db.models import Count
8 from django.core.urlresolvers import reverse
8 from django.core.urlresolvers import reverse
9 from django.utils.translation import get_language
9 from django.utils.translation import get_language
10
10
11 from boards.models import Attachment
11 from boards.models import Attachment
12 from boards.models.base import Viewable
12 from boards.models.base import Viewable
13 from boards.models.thread import STATUS_ACTIVE, STATUS_BUMPLIMIT, STATUS_ARCHIVE
13 from boards.models.thread import STATUS_ACTIVE, STATUS_BUMPLIMIT, STATUS_ARCHIVE
14 from boards.utils import cached_result
14 from boards.utils import cached_result
15 import boards
15 import boards
16
16
17 __author__ = 'neko259'
17 __author__ = 'neko259'
18
18
19
19
20 RELATED_TAGS_COUNT = 5
20 RELATED_TAGS_COUNT = 5
21 DEFAULT_LOCALE = 'default'
21
22
22
23
23 class TagAlias(models.Model, Viewable):
24 class TagAlias(models.Model, Viewable):
24 class Meta:
25 class Meta:
25 app_label = 'boards'
26 app_label = 'boards'
26 ordering = ('name',)
27 ordering = ('name',)
27
28
28 name = models.CharField(max_length=100, db_index=True)
29 name = models.CharField(max_length=100, db_index=True)
29 locale = models.CharField(max_length=10, db_index=True)
30 locale = models.CharField(max_length=10, db_index=True)
30
31
31 parent = models.ForeignKey('Tag', null=True, blank=True,
32 parent = models.ForeignKey('Tag', null=True, blank=True,
32 related_name='aliases')
33 related_name='aliases')
33
34
34
35
35 class TagManager(models.Manager):
36 class TagManager(models.Manager):
36 def get_not_empty_tags(self):
37 def get_not_empty_tags(self):
37 """
38 """
38 Gets tags that have non-archived threads.
39 Gets tags that have non-archived threads.
39 """
40 """
40
41
41 return self.annotate(num_threads=Count('thread_tags')).filter(num_threads__gt=0)\
42 return self.annotate(num_threads=Count('thread_tags'))\
42 .order_by('name')
43 .filter(num_threads__gt=0)\
44 .filter(aliases__locale=DEFAULT_LOCALE)\
45 .order_by('aliases__name')
43
46
44 def get_tag_url_list(self, tags: list) -> str:
47 def get_tag_url_list(self, tags: list) -> str:
45 """
48 """
46 Gets a comma-separated list of tag links.
49 Gets a comma-separated list of tag links.
47 """
50 """
48
51
49 return ', '.join([tag.get_view() for tag in tags])
52 return ', '.join([tag.get_view() for tag in tags])
50
53
51 def get_by_alias(self, alias):
54 def get_by_alias(self, alias):
52 tag = None
55 tag = None
53 aliases = TagAlias.objects.filter(name=alias).all()
56 aliases = TagAlias.objects.filter(name=alias).all()
54 if aliases:
57 if aliases:
55 tag = aliases[0].parent
58 tag = aliases[0].parent
56
59
57 return tag
60 return tag
58
61
62 def get_or_create_with_alias(self, name, required=False):
63 tag = self.get_by_alias(name)
64 created = False
65 if not tag:
66 tag = self.create(required=required)
67 TagAlias.objects.create(name=alias, locale=DEFAULT_LOCALE, parent=tag)
68 created = True
69 return tag, created
70
59
71
60 class Tag(models.Model, Viewable):
72 class Tag(models.Model, Viewable):
61 """
73 """
62 A tag is a text node assigned to the thread. The tag serves as a board
74 A tag is a text node assigned to the thread. The tag serves as a board
63 section. There can be multiple tags for each thread
75 section. There can be multiple tags for each thread
64 """
76 """
65
77
66 objects = TagManager()
78 objects = TagManager()
67
79
68 class Meta:
80 class Meta:
69 app_label = 'boards'
81 app_label = 'boards'
70 ordering = ('name',)
71
82
72 name = models.CharField(max_length=100, db_index=True, unique=True)
73 required = models.BooleanField(default=False, db_index=True)
83 required = models.BooleanField(default=False, db_index=True)
74 description = models.TextField(blank=True)
84 description = models.TextField(blank=True)
75
85
76 parent = models.ForeignKey('Tag', null=True, blank=True,
86 parent = models.ForeignKey('Tag', null=True, blank=True,
77 related_name='children')
87 related_name='children')
78
88
89 @cached_result()
90 def get_name(self):
91 return self.aliases.get(locale=DEFAULT_LOCALE).name
92
79 def __str__(self):
93 def __str__(self):
80 return self.name
94 return self.get_name()
81
95
82 def is_empty(self) -> bool:
96 def is_empty(self) -> bool:
83 """
97 """
84 Checks if the tag has some threads.
98 Checks if the tag has some threads.
85 """
99 """
86
100
87 return self.get_thread_count() == 0
101 return self.get_thread_count() == 0
88
102
89 def get_thread_count(self, status=None) -> int:
103 def get_thread_count(self, status=None) -> int:
90 threads = self.get_threads()
104 threads = self.get_threads()
91 if status is not None:
105 if status is not None:
92 threads = threads.filter(status=status)
106 threads = threads.filter(status=status)
93 return threads.count()
107 return threads.count()
94
108
95 def get_active_thread_count(self) -> int:
109 def get_active_thread_count(self) -> int:
96 return self.get_thread_count(status=STATUS_ACTIVE)
110 return self.get_thread_count(status=STATUS_ACTIVE)
97
111
98 def get_bumplimit_thread_count(self) -> int:
112 def get_bumplimit_thread_count(self) -> int:
99 return self.get_thread_count(status=STATUS_BUMPLIMIT)
113 return self.get_thread_count(status=STATUS_BUMPLIMIT)
100
114
101 def get_archived_thread_count(self) -> int:
115 def get_archived_thread_count(self) -> int:
102 return self.get_thread_count(status=STATUS_ARCHIVE)
116 return self.get_thread_count(status=STATUS_ARCHIVE)
103
117
104 def get_absolute_url(self):
118 def get_absolute_url(self):
105 return reverse('tag', kwargs={'tag_name': self.name})
119 return reverse('tag', kwargs={'tag_name': self.get_name()})
106
120
107 def get_threads(self):
121 def get_threads(self):
108 return self.thread_tags.order_by('-bump_time')
122 return self.thread_tags.order_by('-bump_time')
109
123
110 def is_required(self):
124 def is_required(self):
111 return self.required
125 return self.required
112
126
113 def get_view(self):
127 def get_view(self):
114 locale = get_language()
128 locale = get_language()
115
129
116 try:
130 try:
117 localized_tag_name = self.aliases.get(locale=locale).name
131 localized_tag_name = self.aliases.get(locale=locale).name
118 except TagAlias.DoesNotExist:
132 except TagAlias.DoesNotExist:
119 localized_tag_name = ''
133 localized_tag_name = ''
120
134
121 name = '{} ({})'.format(self.name, localized_tag_name) if localized_tag_name else self.name
135 default_name = self.get_name()
136
137 name = '{} ({})'.format(default_name, localized_tag_name) if localized_tag_name else default_name
122 link = '<a class="tag" href="{}">{}</a>'.format(
138 link = '<a class="tag" href="{}">{}</a>'.format(
123 self.get_absolute_url(), name)
139 self.get_absolute_url(), name)
124 if self.is_required():
140 if self.is_required():
125 link = '<b>{}</b>'.format(link)
141 link = '<b>{}</b>'.format(link)
126 return link
142 return link
127
143
128 @cached_result()
144 @cached_result()
129 def get_post_count(self):
145 def get_post_count(self):
130 return self.get_threads().aggregate(num_posts=Count('replies'))['num_posts']
146 return self.get_threads().aggregate(num_posts=Count('replies'))['num_posts']
131
147
132 def get_description(self):
148 def get_description(self):
133 return self.description
149 return self.description
134
150
135 def get_random_image_post(self, status=[STATUS_ACTIVE, STATUS_BUMPLIMIT]):
151 def get_random_image_post(self, status=[STATUS_ACTIVE, STATUS_BUMPLIMIT]):
136 posts = boards.models.Post.objects.filter(attachments__mimetype__in=FILE_TYPES_IMAGE)\
152 posts = boards.models.Post.objects.filter(attachments__mimetype__in=FILE_TYPES_IMAGE)\
137 .annotate(images_count=Count(
153 .annotate(images_count=Count(
138 'attachments')).filter(images_count__gt=0, thread__tags__in=[self])
154 'attachments')).filter(images_count__gt=0, thread__tags__in=[self])
139 if status is not None:
155 if status is not None:
140 posts = posts.filter(thread__status__in=status)
156 posts = posts.filter(thread__status__in=status)
141 return posts.order_by('?').first()
157 return posts.order_by('?').first()
142
158
143 def get_first_letter(self):
159 def get_first_letter(self):
144 return self.name and self.name[0] or ''
160 name = self.get_name()
161 return name and name[0] or ''
145
162
146 def get_related_tags(self):
163 def get_related_tags(self):
147 return set(Tag.objects.filter(thread_tags__in=self.get_threads()).exclude(
164 return set(Tag.objects.filter(thread_tags__in=self.get_threads()).exclude(
148 id=self.id).order_by('?')[:RELATED_TAGS_COUNT])
165 id=self.id).order_by('?')[:RELATED_TAGS_COUNT])
149
166
150 @cached_result()
167 @cached_result()
151 def get_color(self):
168 def get_color(self):
152 """
169 """
153 Gets color hashed from the tag name.
170 Gets color hashed from the tag name.
154 """
171 """
155 return hashlib.md5(self.name.encode()).hexdigest()[:6]
172 return hashlib.md5(self.get_name().encode()).hexdigest()[:6]
156
173
157 def get_parent(self):
174 def get_parent(self):
158 return self.parent
175 return self.parent
159
176
160 def get_all_parents(self):
177 def get_all_parents(self):
161 parents = list()
178 parents = list()
162 parent = self.get_parent()
179 parent = self.get_parent()
163 if parent and parent not in parents:
180 if parent and parent not in parents:
164 parents.insert(0, parent)
181 parents.insert(0, parent)
165 parents = parent.get_all_parents() + parents
182 parents = parent.get_all_parents() + parents
166
183
167 return parents
184 return parents
168
185
169 def get_children(self):
186 def get_children(self):
170 return self.children
187 return self.children
171
188
172 def get_images(self):
189 def get_images(self):
173 return Attachment.objects.filter(
190 return Attachment.objects.filter(
174 attachment_posts__thread__tags__in=[self]).filter(
191 attachment_posts__thread__tags__in=[self]).filter(
175 mimetype__in=FILE_TYPES_IMAGE).order_by('-attachment_posts__pub_time')
192 mimetype__in=FILE_TYPES_IMAGE).order_by('-attachment_posts__pub_time')
176
193
177
194
@@ -1,313 +1,313 b''
1 import logging
1 import logging
2 from datetime import timedelta
2 from datetime import timedelta
3
3
4 from django.db import models, transaction
4 from django.db import models, transaction
5 from django.db.models import Count, Sum, QuerySet, Q
5 from django.db.models import Count, Sum, QuerySet, Q
6 from django.utils import timezone
6 from django.utils import timezone
7
7
8 import boards
8 import boards
9 from boards import settings
9 from boards import settings
10 from boards.models import STATUS_BUMPLIMIT, STATUS_ACTIVE, STATUS_ARCHIVE
10 from boards.models import STATUS_BUMPLIMIT, STATUS_ACTIVE, STATUS_ARCHIVE
11 from boards.models.attachment import FILE_TYPES_IMAGE
11 from boards.models.attachment import FILE_TYPES_IMAGE
12 from boards.models.post import Post
12 from boards.models.post import Post
13 from boards.models.tag import Tag
13 from boards.models.tag import Tag, DEFAULT_LOCALE
14 from boards.utils import cached_result, datetime_to_epoch
14 from boards.utils import cached_result, datetime_to_epoch
15
15
16 FAV_THREAD_NO_UPDATES = -1
16 FAV_THREAD_NO_UPDATES = -1
17
17
18
18
19 __author__ = 'neko259'
19 __author__ = 'neko259'
20
20
21
21
22 logger = logging.getLogger(__name__)
22 logger = logging.getLogger(__name__)
23
23
24
24
25 WS_NOTIFICATION_TYPE_NEW_POST = 'new_post'
25 WS_NOTIFICATION_TYPE_NEW_POST = 'new_post'
26 WS_NOTIFICATION_TYPE = 'notification_type'
26 WS_NOTIFICATION_TYPE = 'notification_type'
27
27
28 WS_CHANNEL_THREAD = "thread:"
28 WS_CHANNEL_THREAD = "thread:"
29
29
30 STATUS_CHOICES = (
30 STATUS_CHOICES = (
31 (STATUS_ACTIVE, STATUS_ACTIVE),
31 (STATUS_ACTIVE, STATUS_ACTIVE),
32 (STATUS_BUMPLIMIT, STATUS_BUMPLIMIT),
32 (STATUS_BUMPLIMIT, STATUS_BUMPLIMIT),
33 (STATUS_ARCHIVE, STATUS_ARCHIVE),
33 (STATUS_ARCHIVE, STATUS_ARCHIVE),
34 )
34 )
35
35
36
36
37 class ThreadManager(models.Manager):
37 class ThreadManager(models.Manager):
38 def process_old_threads(self):
38 def process_old_threads(self):
39 """
39 """
40 Preserves maximum thread count. If there are too many threads,
40 Preserves maximum thread count. If there are too many threads,
41 archive or delete the old ones.
41 archive or delete the old ones.
42 """
42 """
43 old_time_delta = settings.get_int('Messages', 'ThreadArchiveDays')
43 old_time_delta = settings.get_int('Messages', 'ThreadArchiveDays')
44 old_time = timezone.now() - timedelta(days=old_time_delta)
44 old_time = timezone.now() - timedelta(days=old_time_delta)
45 old_ops = Post.objects.filter(opening=True, pub_time__lte=old_time).exclude(thread__status=STATUS_ARCHIVE)
45 old_ops = Post.objects.filter(opening=True, pub_time__lte=old_time).exclude(thread__status=STATUS_ARCHIVE)
46
46
47 for op in old_ops:
47 for op in old_ops:
48 thread = op.get_thread()
48 thread = op.get_thread()
49 if settings.get_bool('Storage', 'ArchiveThreads'):
49 if settings.get_bool('Storage', 'ArchiveThreads'):
50 self._archive_thread(thread)
50 self._archive_thread(thread)
51 else:
51 else:
52 thread.delete()
52 thread.delete()
53 logger.info('Processed old thread {}'.format(thread))
53 logger.info('Processed old thread {}'.format(thread))
54
54
55
55
56 def _archive_thread(self, thread):
56 def _archive_thread(self, thread):
57 thread.status = STATUS_ARCHIVE
57 thread.status = STATUS_ARCHIVE
58 thread.last_edit_time = timezone.now()
58 thread.last_edit_time = timezone.now()
59 thread.update_posts_time()
59 thread.update_posts_time()
60 thread.save(update_fields=['last_edit_time', 'status'])
60 thread.save(update_fields=['last_edit_time', 'status'])
61
61
62 def get_new_posts(self, datas):
62 def get_new_posts(self, datas):
63 query = None
63 query = None
64 # TODO Use classes instead of dicts
64 # TODO Use classes instead of dicts
65 for data in datas:
65 for data in datas:
66 if data['last_id'] != FAV_THREAD_NO_UPDATES:
66 if data['last_id'] != FAV_THREAD_NO_UPDATES:
67 q = (Q(id=data['op'].get_thread_id())
67 q = (Q(id=data['op'].get_thread_id())
68 & Q(replies__id__gt=data['last_id']))
68 & Q(replies__id__gt=data['last_id']))
69 if query is None:
69 if query is None:
70 query = q
70 query = q
71 else:
71 else:
72 query = query | q
72 query = query | q
73 if query is not None:
73 if query is not None:
74 return self.filter(query).annotate(
74 return self.filter(query).annotate(
75 new_post_count=Count('replies'))
75 new_post_count=Count('replies'))
76
76
77 def get_new_post_count(self, datas):
77 def get_new_post_count(self, datas):
78 new_posts = self.get_new_posts(datas)
78 new_posts = self.get_new_posts(datas)
79 return new_posts.aggregate(total_count=Count('replies'))\
79 return new_posts.aggregate(total_count=Count('replies'))\
80 ['total_count'] if new_posts else 0
80 ['total_count'] if new_posts else 0
81
81
82
82
83 def get_thread_max_posts():
83 def get_thread_max_posts():
84 return settings.get_int('Messages', 'MaxPostsPerThread')
84 return settings.get_int('Messages', 'MaxPostsPerThread')
85
85
86
86
87 class Thread(models.Model):
87 class Thread(models.Model):
88 objects = ThreadManager()
88 objects = ThreadManager()
89
89
90 class Meta:
90 class Meta:
91 app_label = 'boards'
91 app_label = 'boards'
92
92
93 tags = models.ManyToManyField('Tag', related_name='thread_tags')
93 tags = models.ManyToManyField('Tag', related_name='thread_tags')
94 bump_time = models.DateTimeField(db_index=True)
94 bump_time = models.DateTimeField(db_index=True)
95 last_edit_time = models.DateTimeField()
95 last_edit_time = models.DateTimeField()
96 max_posts = models.IntegerField(default=get_thread_max_posts)
96 max_posts = models.IntegerField(default=get_thread_max_posts)
97 status = models.CharField(max_length=50, default=STATUS_ACTIVE,
97 status = models.CharField(max_length=50, default=STATUS_ACTIVE,
98 choices=STATUS_CHOICES, db_index=True)
98 choices=STATUS_CHOICES, db_index=True)
99 monochrome = models.BooleanField(default=False)
99 monochrome = models.BooleanField(default=False)
100
100
101 def get_tags(self) -> QuerySet:
101 def get_tags(self) -> QuerySet:
102 """
102 """
103 Gets a sorted tag list.
103 Gets a sorted tag list.
104 """
104 """
105
105
106 return self.tags.order_by('name')
106 return self.tags.filter(aliases__locale=DEFAULT_LOCALE).order_by('aliases__name')
107
107
108 def bump(self):
108 def bump(self):
109 """
109 """
110 Bumps (moves to up) thread if possible.
110 Bumps (moves to up) thread if possible.
111 """
111 """
112
112
113 if self.can_bump():
113 if self.can_bump():
114 self.bump_time = self.last_edit_time
114 self.bump_time = self.last_edit_time
115
115
116 self.update_bump_status()
116 self.update_bump_status()
117
117
118 logger.info('Bumped thread %d' % self.id)
118 logger.info('Bumped thread %d' % self.id)
119
119
120 def has_post_limit(self) -> bool:
120 def has_post_limit(self) -> bool:
121 return self.max_posts > 0
121 return self.max_posts > 0
122
122
123 def update_bump_status(self, exclude_posts=None):
123 def update_bump_status(self, exclude_posts=None):
124 if self.has_post_limit() and self.get_reply_count() >= self.max_posts:
124 if self.has_post_limit() and self.get_reply_count() >= self.max_posts:
125 self.status = STATUS_BUMPLIMIT
125 self.status = STATUS_BUMPLIMIT
126 self.update_posts_time(exclude_posts=exclude_posts)
126 self.update_posts_time(exclude_posts=exclude_posts)
127
127
128 def _get_cache_key(self):
128 def _get_cache_key(self):
129 return [datetime_to_epoch(self.last_edit_time)]
129 return [datetime_to_epoch(self.last_edit_time)]
130
130
131 @cached_result(key_method=_get_cache_key)
131 @cached_result(key_method=_get_cache_key)
132 def get_reply_count(self) -> int:
132 def get_reply_count(self) -> int:
133 return self.get_replies().count()
133 return self.get_replies().count()
134
134
135 @cached_result(key_method=_get_cache_key)
135 @cached_result(key_method=_get_cache_key)
136 def get_images_count(self) -> int:
136 def get_images_count(self) -> int:
137 return self.get_replies().filter(
137 return self.get_replies().filter(
138 attachments__mimetype__in=FILE_TYPES_IMAGE)\
138 attachments__mimetype__in=FILE_TYPES_IMAGE)\
139 .annotate(images_count=Count(
139 .annotate(images_count=Count(
140 'attachments')).aggregate(Sum('images_count'))['images_count__sum'] or 0
140 'attachments')).aggregate(Sum('images_count'))['images_count__sum'] or 0
141
141
142 def can_bump(self) -> bool:
142 def can_bump(self) -> bool:
143 """
143 """
144 Checks if the thread can be bumped by replying to it.
144 Checks if the thread can be bumped by replying to it.
145 """
145 """
146
146
147 return self.get_status() == STATUS_ACTIVE
147 return self.get_status() == STATUS_ACTIVE
148
148
149 def get_last_replies(self) -> QuerySet:
149 def get_last_replies(self) -> QuerySet:
150 """
150 """
151 Gets several last replies, not including opening post
151 Gets several last replies, not including opening post
152 """
152 """
153
153
154 last_replies_count = settings.get_int('View', 'LastRepliesCount')
154 last_replies_count = settings.get_int('View', 'LastRepliesCount')
155
155
156 if last_replies_count > 0:
156 if last_replies_count > 0:
157 reply_count = self.get_reply_count()
157 reply_count = self.get_reply_count()
158
158
159 if reply_count > 0:
159 if reply_count > 0:
160 reply_count_to_show = min(last_replies_count,
160 reply_count_to_show = min(last_replies_count,
161 reply_count - 1)
161 reply_count - 1)
162 replies = self.get_replies()
162 replies = self.get_replies()
163 last_replies = replies[reply_count - reply_count_to_show:]
163 last_replies = replies[reply_count - reply_count_to_show:]
164
164
165 return last_replies
165 return last_replies
166
166
167 def get_skipped_replies_count(self) -> int:
167 def get_skipped_replies_count(self) -> int:
168 """
168 """
169 Gets number of posts between opening post and last replies.
169 Gets number of posts between opening post and last replies.
170 """
170 """
171 reply_count = self.get_reply_count()
171 reply_count = self.get_reply_count()
172 last_replies_count = min(settings.get_int('View', 'LastRepliesCount'),
172 last_replies_count = min(settings.get_int('View', 'LastRepliesCount'),
173 reply_count - 1)
173 reply_count - 1)
174 return reply_count - last_replies_count - 1
174 return reply_count - last_replies_count - 1
175
175
176 # TODO Remove argument, it is not used
176 # TODO Remove argument, it is not used
177 def get_replies(self, view_fields_only=True) -> QuerySet:
177 def get_replies(self, view_fields_only=True) -> QuerySet:
178 """
178 """
179 Gets sorted thread posts
179 Gets sorted thread posts
180 """
180 """
181 query = self.replies.order_by('pub_time').prefetch_related(
181 query = self.replies.order_by('pub_time').prefetch_related(
182 'attachments')
182 'attachments')
183 return query
183 return query
184
184
185 def get_viewable_replies(self) -> QuerySet:
185 def get_viewable_replies(self) -> QuerySet:
186 """
186 """
187 Gets replies with only fields that are used for viewing.
187 Gets replies with only fields that are used for viewing.
188 """
188 """
189 return self.get_replies().defer('text', 'last_edit_time', 'version')
189 return self.get_replies().defer('text', 'last_edit_time', 'version')
190
190
191 def get_top_level_replies(self) -> QuerySet:
191 def get_top_level_replies(self) -> QuerySet:
192 return self.get_replies().exclude(refposts__threads__in=[self])
192 return self.get_replies().exclude(refposts__threads__in=[self])
193
193
194 def get_replies_with_images(self, view_fields_only=False) -> QuerySet:
194 def get_replies_with_images(self, view_fields_only=False) -> QuerySet:
195 """
195 """
196 Gets replies that have at least one image attached
196 Gets replies that have at least one image attached
197 """
197 """
198 return self.get_replies(view_fields_only).filter(
198 return self.get_replies(view_fields_only).filter(
199 attachments__mimetype__in=FILE_TYPES_IMAGE).annotate(images_count=Count(
199 attachments__mimetype__in=FILE_TYPES_IMAGE).annotate(images_count=Count(
200 'attachments')).filter(images_count__gt=0)
200 'attachments')).filter(images_count__gt=0)
201
201
202 def get_opening_post(self, only_id=False) -> Post:
202 def get_opening_post(self, only_id=False) -> Post:
203 """
203 """
204 Gets the first post of the thread
204 Gets the first post of the thread
205 """
205 """
206
206
207 query = self.get_replies().filter(opening=True)
207 query = self.get_replies().filter(opening=True)
208 if only_id:
208 if only_id:
209 query = query.only('id')
209 query = query.only('id')
210 opening_post = query.first()
210 opening_post = query.first()
211
211
212 return opening_post
212 return opening_post
213
213
214 @cached_result()
214 @cached_result()
215 def get_opening_post_id(self) -> int:
215 def get_opening_post_id(self) -> int:
216 """
216 """
217 Gets ID of the first thread post.
217 Gets ID of the first thread post.
218 """
218 """
219
219
220 return self.get_opening_post(only_id=True).id
220 return self.get_opening_post(only_id=True).id
221
221
222 def get_pub_time(self):
222 def get_pub_time(self):
223 """
223 """
224 Gets opening post's pub time because thread does not have its own one.
224 Gets opening post's pub time because thread does not have its own one.
225 """
225 """
226
226
227 return self.get_opening_post().pub_time
227 return self.get_opening_post().pub_time
228
228
229 def __str__(self):
229 def __str__(self):
230 return 'T#{}'.format(self.id)
230 return 'T#{}'.format(self.id)
231
231
232 def get_tag_url_list(self) -> list:
232 def get_tag_url_list(self) -> list:
233 return boards.models.Tag.objects.get_tag_url_list(self.get_tags())
233 return boards.models.Tag.objects.get_tag_url_list(self.get_tags().all())
234
234
235 def update_posts_time(self, exclude_posts=None):
235 def update_posts_time(self, exclude_posts=None):
236 last_edit_time = self.last_edit_time
236 last_edit_time = self.last_edit_time
237
237
238 for post in self.replies.all():
238 for post in self.replies.all():
239 if exclude_posts is None or post not in exclude_posts:
239 if exclude_posts is None or post not in exclude_posts:
240 # Manual update is required because uids are generated on save
240 # Manual update is required because uids are generated on save
241 post.last_edit_time = last_edit_time
241 post.last_edit_time = last_edit_time
242 post.save(update_fields=['last_edit_time'])
242 post.save(update_fields=['last_edit_time'])
243
243
244 def get_absolute_url(self):
244 def get_absolute_url(self):
245 return self.get_opening_post().get_absolute_url()
245 return self.get_opening_post().get_absolute_url()
246
246
247 def get_required_tags(self):
247 def get_required_tags(self):
248 return self.get_tags().filter(required=True)
248 return self.get_tags().filter(required=True)
249
249
250 def get_sections_str(self):
250 def get_sections_str(self):
251 return Tag.objects.get_tag_url_list(self.get_required_tags())
251 return Tag.objects.get_tag_url_list(self.get_required_tags())
252
252
253 def get_replies_newer(self, post_id):
253 def get_replies_newer(self, post_id):
254 return self.get_replies().filter(id__gt=post_id)
254 return self.get_replies().filter(id__gt=post_id)
255
255
256 def is_archived(self):
256 def is_archived(self):
257 return self.get_status() == STATUS_ARCHIVE
257 return self.get_status() == STATUS_ARCHIVE
258
258
259 def get_status(self):
259 def get_status(self):
260 return self.status
260 return self.status
261
261
262 def is_monochrome(self):
262 def is_monochrome(self):
263 return self.monochrome
263 return self.monochrome
264
264
265 # If tags have parent, add them to the tag list
265 # If tags have parent, add them to the tag list
266 @transaction.atomic
266 @transaction.atomic
267 def refresh_tags(self):
267 def refresh_tags(self):
268 for tag in self.get_tags().all():
268 for tag in self.get_tags().all():
269 parents = tag.get_all_parents()
269 parents = tag.get_all_parents()
270 if len(parents) > 0:
270 if len(parents) > 0:
271 self.tags.add(*parents)
271 self.tags.add(*parents)
272
272
273 def get_reply_tree(self):
273 def get_reply_tree(self):
274 replies = self.get_replies().prefetch_related('refposts')
274 replies = self.get_replies().prefetch_related('refposts')
275 tree = []
275 tree = []
276 for reply in replies:
276 for reply in replies:
277 parents = reply.refposts.all()
277 parents = reply.refposts.all()
278
278
279 found_parent = False
279 found_parent = False
280 searching_for_index = False
280 searching_for_index = False
281
281
282 if len(parents) > 0:
282 if len(parents) > 0:
283 index = 0
283 index = 0
284 parent_depth = 0
284 parent_depth = 0
285
285
286 indexes_to_insert = []
286 indexes_to_insert = []
287
287
288 for depth, element in tree:
288 for depth, element in tree:
289 index += 1
289 index += 1
290
290
291 # If this element is next after parent on the same level,
291 # If this element is next after parent on the same level,
292 # insert child before it
292 # insert child before it
293 if searching_for_index and depth <= parent_depth:
293 if searching_for_index and depth <= parent_depth:
294 indexes_to_insert.append((index - 1, parent_depth))
294 indexes_to_insert.append((index - 1, parent_depth))
295 searching_for_index = False
295 searching_for_index = False
296
296
297 if element in parents:
297 if element in parents:
298 found_parent = True
298 found_parent = True
299 searching_for_index = True
299 searching_for_index = True
300 parent_depth = depth
300 parent_depth = depth
301
301
302 if not found_parent:
302 if not found_parent:
303 tree.append((0, reply))
303 tree.append((0, reply))
304 else:
304 else:
305 if searching_for_index:
305 if searching_for_index:
306 tree.append((parent_depth + 1, reply))
306 tree.append((parent_depth + 1, reply))
307
307
308 offset = 0
308 offset = 0
309 for last_index, parent_depth in indexes_to_insert:
309 for last_index, parent_depth in indexes_to_insert:
310 tree.insert(last_index + offset, (parent_depth + 1, reply))
310 tree.insert(last_index + offset, (parent_depth + 1, reply))
311 offset += 1
311 offset += 1
312
312
313 return tree
313 return tree
@@ -1,208 +1,208 b''
1 {% extends "boards/base.html" %}
1 {% extends "boards/base.html" %}
2
2
3 {% load i18n %}
3 {% load i18n %}
4 {% load board %}
4 {% load board %}
5 {% load static %}
5 {% load static %}
6 {% load tz %}
6 {% load tz %}
7
7
8 {% block head %}
8 {% block head %}
9 <meta name="robots" content="noindex">
9 <meta name="robots" content="noindex">
10
10
11 {% if tag %}
11 {% if tag %}
12 <title>{{ tag.name }} - {{ site_name }}</title>
12 <title>{{ tag.get_name }} - {{ site_name }}</title>
13 {% else %}
13 {% else %}
14 <title>{{ site_name }}</title>
14 <title>{{ site_name }}</title>
15 {% endif %}
15 {% endif %}
16
16
17 {% if prev_page_link %}
17 {% if prev_page_link %}
18 <link rel="prev" href="{{ prev_page_link }}" />
18 <link rel="prev" href="{{ prev_page_link }}" />
19 {% endif %}
19 {% endif %}
20 {% if next_page_link %}
20 {% if next_page_link %}
21 <link rel="next" href="{{ next_page_link }}" />
21 <link rel="next" href="{{ next_page_link }}" />
22 {% endif %}
22 {% endif %}
23
23
24 {% endblock %}
24 {% endblock %}
25
25
26 {% block content %}
26 {% block content %}
27
27
28 {% get_current_language as LANGUAGE_CODE %}
28 {% get_current_language as LANGUAGE_CODE %}
29 {% get_current_timezone as TIME_ZONE %}
29 {% get_current_timezone as TIME_ZONE %}
30
30
31 {% for banner in banners %}
31 {% for banner in banners %}
32 <div class="post">
32 <div class="post">
33 <div class="title">{{ banner.title }}</div>
33 <div class="title">{{ banner.title }}</div>
34 <div>{{ banner.get_text|safe }}</div>
34 <div>{{ banner.get_text|safe }}</div>
35 <div>{% trans 'Details' %}: <a href="{{ banner.post.get_absolute_url }}">>>{{ banner.post.id }}</a></div>
35 <div>{% trans 'Details' %}: <a href="{{ banner.post.get_absolute_url }}">>>{{ banner.post.id }}</a></div>
36 </div>
36 </div>
37 {% endfor %}
37 {% endfor %}
38
38
39 {% if tag %}
39 {% if tag %}
40 <div class="tag_info" style="border-bottom: solid .5ex #{{ tag.get_color }}">
40 <div class="tag_info" style="border-bottom: solid .5ex #{{ tag.get_color }}">
41 {% if random_image_post %}
41 {% if random_image_post %}
42 <div class="tag-image">
42 <div class="tag-image">
43 {% with image=random_image_post.get_first_image %}
43 {% with image=random_image_post.get_first_image %}
44 <a href="{{ random_image_post.get_absolute_url }}"><img
44 <a href="{{ random_image_post.get_absolute_url }}"><img
45 src="{{ image.get_thumb_url }}"
45 src="{{ image.get_thumb_url }}"
46 width="{{ image.get_preview_size.0 }}"
46 width="{{ image.get_preview_size.0 }}"
47 height="{{ image.get_preview_size.1 }}"
47 height="{{ image.get_preview_size.1 }}"
48 alt="{{ random_image_post.id }}"/></a>
48 alt="{{ random_image_post.id }}"/></a>
49 {% endwith %}
49 {% endwith %}
50 </div>
50 </div>
51 {% endif %}
51 {% endif %}
52 <div class="tag-text-data">
52 <div class="tag-text-data">
53 <h2>
53 <h2>
54 /{{ tag.get_view|safe }}/
54 /{{ tag.get_view|safe }}/
55 </h2>
55 </h2>
56 {% if perms.change_tag %}
56 {% if perms.change_tag %}
57 <div class="moderator_info"><a href="{% url 'admin:boards_tag_change' tag.id %}">{% trans 'Edit tag' %}</a></div>
57 <div class="moderator_info"><a href="{% url 'admin:boards_tag_change' tag.id %}">{% trans 'Edit tag' %}</a></div>
58 {% endif %}
58 {% endif %}
59 <p>
59 <p>
60 <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form">
60 <form action="{% url 'tag' tag.get_name %}" method="post" class="post-button-form">
61 {% if is_favorite %}
61 {% if is_favorite %}
62 <button name="method" value="unsubscribe" class="fav">β˜… {% trans "Remove from favorites" %}</button>
62 <button name="method" value="unsubscribe" class="fav">β˜… {% trans "Remove from favorites" %}</button>
63 {% else %}
63 {% else %}
64 <button name="method" value="subscribe" class="not_fav">β˜… {% trans "Add to favorites" %}</button>
64 <button name="method" value="subscribe" class="not_fav">β˜… {% trans "Add to favorites" %}</button>
65 {% endif %}
65 {% endif %}
66 </form>
66 </form>
67 <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form">
67 <form action="{% url 'tag' tag.get_name %}" method="post" class="post-button-form">
68 {% if is_hidden %}
68 {% if is_hidden %}
69 <button name="method" value="unhide" class="fav">{% trans "Show" %}</button>
69 <button name="method" value="unhide" class="fav">{% trans "Show" %}</button>
70 {% else %}
70 {% else %}
71 <button name="method" value="hide" class="not_fav">{% trans "Hide" %}</button>
71 <button name="method" value="hide" class="not_fav">{% trans "Hide" %}</button>
72 {% endif %}
72 {% endif %}
73 </form>
73 </form>
74 <a href="{% url 'tag_gallery' tag.name %}">{% trans 'Gallery' %}</a>
74 <a href="{% url 'tag_gallery' tag.get_name %}">{% trans 'Gallery' %}</a>
75 </p>
75 </p>
76 {% if tag.get_description %}
76 {% if tag.get_description %}
77 <p>{{ tag.get_description|safe }}</p>
77 <p>{{ tag.get_description|safe }}</p>
78 {% endif %}
78 {% endif %}
79 <p>
79 <p>
80 {% with active_count=tag.get_active_thread_count bumplimit_count=tag.get_bumplimit_thread_count archived_count=tag.get_archived_thread_count %}
80 {% with active_count=tag.get_active_thread_count bumplimit_count=tag.get_bumplimit_thread_count archived_count=tag.get_archived_thread_count %}
81 {% if active_count %}
81 {% if active_count %}
82 ● {{ active_count }}&ensp;
82 ● {{ active_count }}&ensp;
83 {% endif %}
83 {% endif %}
84 {% if bumplimit_count %}
84 {% if bumplimit_count %}
85 ◍ {{ bumplimit_count }}&ensp;
85 ◍ {{ bumplimit_count }}&ensp;
86 {% endif %}
86 {% endif %}
87 {% if archived_count %}
87 {% if archived_count %}
88 β—‹ {{ archived_count }}&ensp;
88 β—‹ {{ archived_count }}&ensp;
89 {% endif %}
89 {% endif %}
90 {% endwith %}
90 {% endwith %}
91 β™₯ {{ tag.get_post_count }}
91 β™₯ {{ tag.get_post_count }}
92 </p>
92 </p>
93 {% if tag.get_all_parents %}
93 {% if tag.get_all_parents %}
94 <p>
94 <p>
95 {% for parent in tag.get_all_parents %}
95 {% for parent in tag.get_all_parents %}
96 {{ parent.get_view|safe }} &gt;
96 {{ parent.get_view|safe }} &gt;
97 {% endfor %}
97 {% endfor %}
98 {{ tag.get_view|safe }}
98 {{ tag.get_view|safe }}
99 </p>
99 </p>
100 {% endif %}
100 {% endif %}
101 {% if tag.get_children.all %}
101 {% if tag.get_children.all %}
102 <p>
102 <p>
103 {% trans "Subsections: " %}
103 {% trans "Subsections: " %}
104 {% for child in tag.get_children.all %}
104 {% for child in tag.get_children.all %}
105 {{ child.get_view|safe }}{% if not forloop.last%}, {% endif %}
105 {{ child.get_view|safe }}{% if not forloop.last%}, {% endif %}
106 {% endfor %}
106 {% endfor %}
107 </p>
107 </p>
108 {% endif %}
108 {% endif %}
109 </div>
109 </div>
110 </div>
110 </div>
111 {% endif %}
111 {% endif %}
112
112
113 {% if threads %}
113 {% if threads %}
114 {% if prev_page_link %}
114 {% if prev_page_link %}
115 <div class="page_link">
115 <div class="page_link">
116 <a href="{{ prev_page_link }}">&lt;&lt; {% trans "Previous page" %} &lt;&lt;</a>
116 <a href="{{ prev_page_link }}">&lt;&lt; {% trans "Previous page" %} &lt;&lt;</a>
117 </div>
117 </div>
118 {% endif %}
118 {% endif %}
119
119
120 {% for thread in threads %}
120 {% for thread in threads %}
121 <div class="thread">
121 <div class="thread">
122 {% post_view thread.get_opening_post thread=thread truncated=True need_open_link=True %}
122 {% post_view thread.get_opening_post thread=thread truncated=True need_open_link=True %}
123 {% if not thread.archived %}
123 {% if not thread.archived %}
124 {% with last_replies=thread.get_last_replies %}
124 {% with last_replies=thread.get_last_replies %}
125 {% if last_replies %}
125 {% if last_replies %}
126 {% with skipped_replies_count=thread.get_skipped_replies_count %}
126 {% with skipped_replies_count=thread.get_skipped_replies_count %}
127 {% if skipped_replies_count %}
127 {% if skipped_replies_count %}
128 <div class="skipped_replies">
128 <div class="skipped_replies">
129 <a href="{% url 'thread' thread.get_opening_post_id %}">
129 <a href="{% url 'thread' thread.get_opening_post_id %}">
130 {% blocktrans count count=skipped_replies_count %}Skipped {{ count }} reply. Open thread to see all replies.{% plural %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %}
130 {% blocktrans count count=skipped_replies_count %}Skipped {{ count }} reply. Open thread to see all replies.{% plural %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %}
131 </a>
131 </a>
132 </div>
132 </div>
133 {% endif %}
133 {% endif %}
134 {% endwith %}
134 {% endwith %}
135 <div class="last-replies">
135 <div class="last-replies">
136 {% for post in last_replies %}
136 {% for post in last_replies %}
137 {% post_view post truncated=True %}
137 {% post_view post truncated=True %}
138 {% endfor %}
138 {% endfor %}
139 </div>
139 </div>
140 {% endif %}
140 {% endif %}
141 {% endwith %}
141 {% endwith %}
142 {% endif %}
142 {% endif %}
143 </div>
143 </div>
144 {% endfor %}
144 {% endfor %}
145
145
146 {% if next_page_link %}
146 {% if next_page_link %}
147 <div class="page_link">
147 <div class="page_link">
148 <a href="{{ next_page_link }}">&gt;&gt; {% trans "Next page" %} &gt;&gt;</a>
148 <a href="{{ next_page_link }}">&gt;&gt; {% trans "Next page" %} &gt;&gt;</a>
149 </div>
149 </div>
150 {% endif %}
150 {% endif %}
151 {% else %}
151 {% else %}
152 <div class="post">
152 <div class="post">
153 {% trans 'No threads exist. Create the first one!' %}</div>
153 {% trans 'No threads exist. Create the first one!' %}</div>
154 {% endif %}
154 {% endif %}
155
155
156 <div class="post-form-w">
156 <div class="post-form-w">
157 <script src="{% static 'js/panel.js' %}"></script>
157 <script src="{% static 'js/panel.js' %}"></script>
158 <div class="post-form" data-hasher="{% static 'js/3party/sha256.js' %}"
158 <div class="post-form" data-hasher="{% static 'js/3party/sha256.js' %}"
159 data-pow-script="{% static 'js/proof_of_work.js' %}">
159 data-pow-script="{% static 'js/proof_of_work.js' %}">
160 <div class="form-title">{% trans "Create new thread" %}</div>
160 <div class="form-title">{% trans "Create new thread" %}</div>
161 <div class="swappable-form-full">
161 <div class="swappable-form-full">
162 <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %}
162 <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %}
163 {{ form.as_div }}
163 {{ form.as_div }}
164 <div class="form-submit">
164 <div class="form-submit">
165 <input type="submit" value="{% trans "Post" %}"/>
165 <input type="submit" value="{% trans "Post" %}"/>
166 <button id="preview-button" type="button" onclick="return false;">{% trans 'Preview' %}</button>
166 <button id="preview-button" type="button" onclick="return false;">{% trans 'Preview' %}</button>
167 </div>
167 </div>
168 </form>
168 </form>
169 </div>
169 </div>
170 <div>
170 <div>
171 {% trans 'Tags must be delimited by spaces. Text or image is required.' %}
171 {% trans 'Tags must be delimited by spaces. Text or image is required.' %}
172 {% with size=max_file_size|filesizeformat %}
172 {% with size=max_file_size|filesizeformat %}
173 {% blocktrans %}Max file size is {{ size }}.{% endblocktrans %}
173 {% blocktrans %}Max file size is {{ size }}.{% endblocktrans %}
174 {% endwith %}
174 {% endwith %}
175 {% blocktrans %}Max file number is {{ max_files }}.{% endblocktrans %}
175 {% blocktrans %}Max file number is {{ max_files }}.{% endblocktrans %}
176 </div>
176 </div>
177 <div id="preview-text"></div>
177 <div id="preview-text"></div>
178 <div><a href="{% url "staticpage" name="help" %}">{% trans 'Text syntax' %}</a></div>
178 <div><a href="{% url "staticpage" name="help" %}">{% trans 'Text syntax' %}</a></div>
179 </div>
179 </div>
180 </div>
180 </div>
181
181
182 <script src="{% static 'js/form.js' %}"></script>
182 <script src="{% static 'js/form.js' %}"></script>
183 <script src="{% static 'js/3party/jquery.blockUI.js' %}"></script>
183 <script src="{% static 'js/3party/jquery.blockUI.js' %}"></script>
184 <script src="{% static 'js/thread_create.js' %}"></script>
184 <script src="{% static 'js/thread_create.js' %}"></script>
185
185
186 {% endblock %}
186 {% endblock %}
187
187
188 {% block metapanel %}
188 {% block metapanel %}
189
189
190 <span class="metapanel">
190 <span class="metapanel">
191 {% trans "Pages:" %}
191 {% trans "Pages:" %}
192 [
192 [
193 {% with dividers=paginator.get_dividers %}
193 {% with dividers=paginator.get_dividers %}
194 {% for page in paginator.get_divided_range %}
194 {% for page in paginator.get_divided_range %}
195 {% if page in dividers %}
195 {% if page in dividers %}
196 …,
196 …,
197 {% endif %}
197 {% endif %}
198 <a
198 <a
199 {% ifequal page current_page.number %}
199 {% ifequal page current_page.number %}
200 class="current_page"
200 class="current_page"
201 {% endifequal %}
201 {% endifequal %}
202 href="{% page_url paginator page %}">{{ page }}</a>{% if not forloop.last %},{% endif %}
202 href="{% page_url paginator page %}">{{ page }}</a>{% if not forloop.last %},{% endif %}
203 {% endfor %}
203 {% endfor %}
204 {% endwith %}
204 {% endwith %}
205 ]
205 ]
206 </span>
206 </span>
207
207
208 {% endblock %}
208 {% endblock %}
@@ -1,315 +1,315 b''
1 import json
1 import json
2 import logging
2 import logging
3
3
4 from django.core import serializers
4 from django.core import serializers
5 from django.db import transaction
5 from django.db import transaction
6 from django.http import HttpResponse
6 from django.http import HttpResponse
7 from django.shortcuts import get_object_or_404
7 from django.shortcuts import get_object_or_404
8 from django.views.decorators.csrf import csrf_protect
8 from django.views.decorators.csrf import csrf_protect
9
9
10 from boards.abstracts.settingsmanager import get_settings_manager
10 from boards.abstracts.settingsmanager import get_settings_manager
11 from boards.forms import PostForm, PlainErrorList
11 from boards.forms import PostForm, PlainErrorList
12 from boards.mdx_neboard import Parser
12 from boards.mdx_neboard import Parser
13 from boards.models import Post, Thread, Tag, Attachment
13 from boards.models import Post, Thread, Tag, Attachment, TagAlias
14 from boards.models.thread import STATUS_ARCHIVE
14 from boards.models.thread import STATUS_ARCHIVE
15 from boards.models.user import Notification
15 from boards.models.user import Notification
16 from boards.utils import datetime_to_epoch
16 from boards.utils import datetime_to_epoch
17 from boards.views.thread import ThreadView
17 from boards.views.thread import ThreadView
18 from boards.models.attachment.viewers import FILE_TYPES_IMAGE
18 from boards.models.attachment.viewers import FILE_TYPES_IMAGE
19
19
20 __author__ = 'neko259'
20 __author__ = 'neko259'
21
21
22 PARAMETER_TRUNCATED = 'truncated'
22 PARAMETER_TRUNCATED = 'truncated'
23 PARAMETER_TAG = 'tag'
23 PARAMETER_TAG = 'tag'
24 PARAMETER_OFFSET = 'offset'
24 PARAMETER_OFFSET = 'offset'
25 PARAMETER_DIFF_TYPE = 'type'
25 PARAMETER_DIFF_TYPE = 'type'
26 PARAMETER_POST = 'post'
26 PARAMETER_POST = 'post'
27 PARAMETER_UPDATED = 'updated'
27 PARAMETER_UPDATED = 'updated'
28 PARAMETER_LAST_UPDATE = 'last_update'
28 PARAMETER_LAST_UPDATE = 'last_update'
29 PARAMETER_THREAD = 'thread'
29 PARAMETER_THREAD = 'thread'
30 PARAMETER_UIDS = 'uids'
30 PARAMETER_UIDS = 'uids'
31 PARAMETER_SUBSCRIBED = 'subscribed'
31 PARAMETER_SUBSCRIBED = 'subscribed'
32
32
33 DIFF_TYPE_HTML = 'html'
33 DIFF_TYPE_HTML = 'html'
34 DIFF_TYPE_JSON = 'json'
34 DIFF_TYPE_JSON = 'json'
35
35
36 STATUS_OK = 'ok'
36 STATUS_OK = 'ok'
37 STATUS_ERROR = 'error'
37 STATUS_ERROR = 'error'
38
38
39 logger = logging.getLogger(__name__)
39 logger = logging.getLogger(__name__)
40
40
41
41
42 @transaction.atomic
42 @transaction.atomic
43 def api_get_threaddiff(request):
43 def api_get_threaddiff(request):
44 """
44 """
45 Gets posts that were changed or added since time
45 Gets posts that were changed or added since time
46 """
46 """
47
47
48 thread_id = request.POST.get(PARAMETER_THREAD)
48 thread_id = request.POST.get(PARAMETER_THREAD)
49 uids_str = request.POST.get(PARAMETER_UIDS)
49 uids_str = request.POST.get(PARAMETER_UIDS)
50
50
51 if not thread_id or not uids_str:
51 if not thread_id or not uids_str:
52 return HttpResponse(content='Invalid request.')
52 return HttpResponse(content='Invalid request.')
53
53
54 uids = uids_str.strip().split(' ')
54 uids = uids_str.strip().split(' ')
55
55
56 opening_post = get_object_or_404(Post, id=thread_id)
56 opening_post = get_object_or_404(Post, id=thread_id)
57 thread = opening_post.get_thread()
57 thread = opening_post.get_thread()
58
58
59 json_data = {
59 json_data = {
60 PARAMETER_UPDATED: [],
60 PARAMETER_UPDATED: [],
61 PARAMETER_LAST_UPDATE: None, # TODO Maybe this can be removed already?
61 PARAMETER_LAST_UPDATE: None, # TODO Maybe this can be removed already?
62 }
62 }
63 posts = Post.objects.filter(thread=thread).exclude(uid__in=uids)
63 posts = Post.objects.filter(thread=thread).exclude(uid__in=uids)
64
64
65 diff_type = request.GET.get(PARAMETER_DIFF_TYPE, DIFF_TYPE_HTML)
65 diff_type = request.GET.get(PARAMETER_DIFF_TYPE, DIFF_TYPE_HTML)
66
66
67 for post in posts:
67 for post in posts:
68 json_data[PARAMETER_UPDATED].append(post.get_post_data(
68 json_data[PARAMETER_UPDATED].append(post.get_post_data(
69 format_type=diff_type, request=request))
69 format_type=diff_type, request=request))
70 json_data[PARAMETER_LAST_UPDATE] = str(thread.last_edit_time)
70 json_data[PARAMETER_LAST_UPDATE] = str(thread.last_edit_time)
71
71
72 settings_manager = get_settings_manager(request)
72 settings_manager = get_settings_manager(request)
73 json_data[PARAMETER_SUBSCRIBED] = str(settings_manager.thread_is_fav(opening_post))
73 json_data[PARAMETER_SUBSCRIBED] = str(settings_manager.thread_is_fav(opening_post))
74
74
75 # If the tag is favorite, update the counter
75 # If the tag is favorite, update the counter
76 settings_manager = get_settings_manager(request)
76 settings_manager = get_settings_manager(request)
77 favorite = settings_manager.thread_is_fav(opening_post)
77 favorite = settings_manager.thread_is_fav(opening_post)
78 if favorite:
78 if favorite:
79 settings_manager.add_or_read_fav_thread(opening_post)
79 settings_manager.add_or_read_fav_thread(opening_post)
80
80
81 return HttpResponse(content=json.dumps(json_data))
81 return HttpResponse(content=json.dumps(json_data))
82
82
83
83
84 @csrf_protect
84 @csrf_protect
85 def api_add_post(request, opening_post_id):
85 def api_add_post(request, opening_post_id):
86 """
86 """
87 Adds a post and return the JSON response for it
87 Adds a post and return the JSON response for it
88 """
88 """
89
89
90 opening_post = get_object_or_404(Post, id=opening_post_id)
90 opening_post = get_object_or_404(Post, id=opening_post_id)
91
91
92 logger.info('Adding post via api...')
92 logger.info('Adding post via api...')
93
93
94 status = STATUS_OK
94 status = STATUS_OK
95 errors = []
95 errors = []
96
96
97 if request.method == 'POST':
97 if request.method == 'POST':
98 form = PostForm(request.POST, request.FILES, error_class=PlainErrorList)
98 form = PostForm(request.POST, request.FILES, error_class=PlainErrorList)
99 form.session = request.session
99 form.session = request.session
100
100
101 if form.need_to_ban:
101 if form.need_to_ban:
102 # Ban user because he is suspected to be a bot
102 # Ban user because he is suspected to be a bot
103 # _ban_current_user(request)
103 # _ban_current_user(request)
104 status = STATUS_ERROR
104 status = STATUS_ERROR
105 if form.is_valid():
105 if form.is_valid():
106 post = ThreadView().new_post(request, form, opening_post,
106 post = ThreadView().new_post(request, form, opening_post,
107 html_response=False)
107 html_response=False)
108 if not post:
108 if not post:
109 status = STATUS_ERROR
109 status = STATUS_ERROR
110 else:
110 else:
111 logger.info('Added post #%d via api.' % post.id)
111 logger.info('Added post #%d via api.' % post.id)
112 else:
112 else:
113 status = STATUS_ERROR
113 status = STATUS_ERROR
114 errors = form.as_json_errors()
114 errors = form.as_json_errors()
115
115
116 response = {
116 response = {
117 'status': status,
117 'status': status,
118 'errors': errors,
118 'errors': errors,
119 }
119 }
120
120
121 return HttpResponse(content=json.dumps(response))
121 return HttpResponse(content=json.dumps(response))
122
122
123
123
124 def get_post(request, post_id):
124 def get_post(request, post_id):
125 """
125 """
126 Gets the html of a post. Used for popups. Post can be truncated if used
126 Gets the html of a post. Used for popups. Post can be truncated if used
127 in threads list with 'truncated' get parameter.
127 in threads list with 'truncated' get parameter.
128 """
128 """
129
129
130 post = get_object_or_404(Post, id=post_id)
130 post = get_object_or_404(Post, id=post_id)
131 truncated = PARAMETER_TRUNCATED in request.GET
131 truncated = PARAMETER_TRUNCATED in request.GET
132
132
133 return HttpResponse(content=post.get_view(truncated=truncated, need_op_data=True))
133 return HttpResponse(content=post.get_view(truncated=truncated, need_op_data=True))
134
134
135
135
136 def api_get_threads(request, count):
136 def api_get_threads(request, count):
137 """
137 """
138 Gets the JSON thread opening posts list.
138 Gets the JSON thread opening posts list.
139 Parameters that can be used for filtering:
139 Parameters that can be used for filtering:
140 tag, offset (from which thread to get results)
140 tag, offset (from which thread to get results)
141 """
141 """
142
142
143 if PARAMETER_TAG in request.GET:
143 if PARAMETER_TAG in request.GET:
144 tag_name = request.GET[PARAMETER_TAG]
144 tag_name = request.GET[PARAMETER_TAG]
145 if tag_name is not None:
145 if tag_name is not None:
146 tag = get_object_or_404(Tag, name=tag_name)
146 tag = get_object_or_404(Tag, name=tag_name)
147 threads = tag.get_threads().exclude(status=STATUS_ARCHIVE)
147 threads = tag.get_threads().exclude(status=STATUS_ARCHIVE)
148 else:
148 else:
149 threads = Thread.objects.exclude(status=STATUS_ARCHIVE)
149 threads = Thread.objects.exclude(status=STATUS_ARCHIVE)
150
150
151 if PARAMETER_OFFSET in request.GET:
151 if PARAMETER_OFFSET in request.GET:
152 offset = request.GET[PARAMETER_OFFSET]
152 offset = request.GET[PARAMETER_OFFSET]
153 offset = int(offset) if offset is not None else 0
153 offset = int(offset) if offset is not None else 0
154 else:
154 else:
155 offset = 0
155 offset = 0
156
156
157 threads = threads.order_by('-bump_time')
157 threads = threads.order_by('-bump_time')
158 threads = threads[offset:offset + int(count)]
158 threads = threads[offset:offset + int(count)]
159
159
160 opening_posts = []
160 opening_posts = []
161 for thread in threads:
161 for thread in threads:
162 opening_post = thread.get_opening_post()
162 opening_post = thread.get_opening_post()
163
163
164 # TODO Add tags, replies and images count
164 # TODO Add tags, replies and images count
165 post_data = opening_post.get_post_data(include_last_update=True)
165 post_data = opening_post.get_post_data(include_last_update=True)
166 post_data['status'] = thread.get_status()
166 post_data['status'] = thread.get_status()
167
167
168 opening_posts.append(post_data)
168 opening_posts.append(post_data)
169
169
170 return HttpResponse(content=json.dumps(opening_posts))
170 return HttpResponse(content=json.dumps(opening_posts))
171
171
172
172
173 # TODO Test this
173 # TODO Test this
174 def api_get_tags(request):
174 def api_get_tags(request):
175 """
175 """
176 Gets all tags or user tags.
176 Gets all tags or user tags.
177 """
177 """
178
178
179 # TODO Get favorite tags for the given user ID
179 # TODO Get favorite tags for the given user ID
180
180
181 tags = Tag.objects.get_not_empty_tags()
181 tags = TagAlias.objects.all()
182
182
183 term = request.GET.get('term')
183 term = request.GET.get('term')
184 if term is not None:
184 if term is not None:
185 tags = tags.filter(name__contains=term)
185 tags = tags.filter(name__contains=term)
186
186
187 tag_names = [tag.name for tag in tags]
187 tag_names = [tag.name for tag in tags]
188
188
189 return HttpResponse(content=json.dumps(tag_names))
189 return HttpResponse(content=json.dumps(tag_names))
190
190
191
191
192 def api_get_stickers(request):
192 def api_get_stickers(request):
193 attachments = Attachment.objects.filter(mimetype__in=FILE_TYPES_IMAGE)\
193 attachments = Attachment.objects.filter(mimetype__in=FILE_TYPES_IMAGE)\
194 .exclude(alias='').exclude(alias=None)
194 .exclude(alias='').exclude(alias=None)
195
195
196 term = request.GET.get('term')
196 term = request.GET.get('term')
197 if term:
197 if term:
198 attachments = attachments.filter(alias__contains=term)
198 attachments = attachments.filter(alias__contains=term)
199
199
200 image_dict = [{'thumb': attachment.get_thumb_url(),
200 image_dict = [{'thumb': attachment.get_thumb_url(),
201 'alias': attachment.alias}
201 'alias': attachment.alias}
202 for attachment in attachments]
202 for attachment in attachments]
203
203
204 return HttpResponse(content=json.dumps(image_dict))
204 return HttpResponse(content=json.dumps(image_dict))
205
205
206
206
207 # TODO The result can be cached by the thread last update time
207 # TODO The result can be cached by the thread last update time
208 # TODO Test this
208 # TODO Test this
209 def api_get_thread_posts(request, opening_post_id):
209 def api_get_thread_posts(request, opening_post_id):
210 """
210 """
211 Gets the JSON array of thread posts
211 Gets the JSON array of thread posts
212 """
212 """
213
213
214 opening_post = get_object_or_404(Post, id=opening_post_id)
214 opening_post = get_object_or_404(Post, id=opening_post_id)
215 thread = opening_post.get_thread()
215 thread = opening_post.get_thread()
216 posts = thread.get_replies()
216 posts = thread.get_replies()
217
217
218 json_data = {
218 json_data = {
219 'posts': [],
219 'posts': [],
220 'last_update': None,
220 'last_update': None,
221 }
221 }
222 json_post_list = []
222 json_post_list = []
223
223
224 for post in posts:
224 for post in posts:
225 json_post_list.append(post.get_post_data())
225 json_post_list.append(post.get_post_data())
226 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
226 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
227 json_data['posts'] = json_post_list
227 json_data['posts'] = json_post_list
228
228
229 return HttpResponse(content=json.dumps(json_data))
229 return HttpResponse(content=json.dumps(json_data))
230
230
231
231
232 def api_get_notifications(request, username):
232 def api_get_notifications(request, username):
233 last_notification_id_str = request.GET.get('last', None)
233 last_notification_id_str = request.GET.get('last', None)
234 last_id = int(last_notification_id_str) if last_notification_id_str is not None else None
234 last_id = int(last_notification_id_str) if last_notification_id_str is not None else None
235
235
236 posts = Notification.objects.get_notification_posts(usernames=[username],
236 posts = Notification.objects.get_notification_posts(usernames=[username],
237 last=last_id)
237 last=last_id)
238
238
239 json_post_list = []
239 json_post_list = []
240 for post in posts:
240 for post in posts:
241 json_post_list.append(post.get_post_data())
241 json_post_list.append(post.get_post_data())
242 return HttpResponse(content=json.dumps(json_post_list))
242 return HttpResponse(content=json.dumps(json_post_list))
243
243
244
244
245 def api_get_post(request, post_id):
245 def api_get_post(request, post_id):
246 """
246 """
247 Gets the JSON of a post. This can be
247 Gets the JSON of a post. This can be
248 used as and API for external clients.
248 used as and API for external clients.
249 """
249 """
250
250
251 post = get_object_or_404(Post, id=post_id)
251 post = get_object_or_404(Post, id=post_id)
252
252
253 json = serializers.serialize("json", [post], fields=(
253 json = serializers.serialize("json", [post], fields=(
254 "pub_time", "_text_rendered", "title", "text", "image",
254 "pub_time", "_text_rendered", "title", "text", "image",
255 "image_width", "image_height", "replies", "tags"
255 "image_width", "image_height", "replies", "tags"
256 ))
256 ))
257
257
258 return HttpResponse(content=json)
258 return HttpResponse(content=json)
259
259
260
260
261 def api_get_preview(request):
261 def api_get_preview(request):
262 raw_text = request.POST['raw_text']
262 raw_text = request.POST['raw_text']
263
263
264 parser = Parser()
264 parser = Parser()
265 return HttpResponse(content=parser.parse(parser.preparse(raw_text)))
265 return HttpResponse(content=parser.parse(parser.preparse(raw_text)))
266
266
267
267
268 def api_get_new_posts(request):
268 def api_get_new_posts(request):
269 """
269 """
270 Gets favorite threads and unread posts count.
270 Gets favorite threads and unread posts count.
271 """
271 """
272 posts = list()
272 posts = list()
273
273
274 include_posts = 'include_posts' in request.GET
274 include_posts = 'include_posts' in request.GET
275
275
276 settings_manager = get_settings_manager(request)
276 settings_manager = get_settings_manager(request)
277 fav_threads = settings_manager.get_fav_threads()
277 fav_threads = settings_manager.get_fav_threads()
278 fav_thread_ops = Post.objects.filter(id__in=fav_threads.keys())\
278 fav_thread_ops = Post.objects.filter(id__in=fav_threads.keys())\
279 .order_by('-pub_time').prefetch_related('thread')
279 .order_by('-pub_time').prefetch_related('thread')
280
280
281 ops = [{'op': op, 'last_id': fav_threads[str(op.id)]} for op in fav_thread_ops]
281 ops = [{'op': op, 'last_id': fav_threads[str(op.id)]} for op in fav_thread_ops]
282 if include_posts:
282 if include_posts:
283 new_post_threads = Thread.objects.get_new_posts(ops)
283 new_post_threads = Thread.objects.get_new_posts(ops)
284 if new_post_threads:
284 if new_post_threads:
285 thread_ids = {thread.id: thread for thread in new_post_threads}
285 thread_ids = {thread.id: thread for thread in new_post_threads}
286 else:
286 else:
287 thread_ids = dict()
287 thread_ids = dict()
288
288
289 for op in fav_thread_ops:
289 for op in fav_thread_ops:
290 fav_thread_dict = dict()
290 fav_thread_dict = dict()
291
291
292 op_thread = op.get_thread()
292 op_thread = op.get_thread()
293 if op_thread.id in thread_ids:
293 if op_thread.id in thread_ids:
294 thread = thread_ids[op_thread.id]
294 thread = thread_ids[op_thread.id]
295 new_post_count = thread.new_post_count
295 new_post_count = thread.new_post_count
296 fav_thread_dict['newest_post_link'] = thread.get_replies()\
296 fav_thread_dict['newest_post_link'] = thread.get_replies()\
297 .filter(id__gt=fav_threads[str(op.id)])\
297 .filter(id__gt=fav_threads[str(op.id)])\
298 .first().get_absolute_url(thread=thread)
298 .first().get_absolute_url(thread=thread)
299 else:
299 else:
300 new_post_count = 0
300 new_post_count = 0
301 fav_thread_dict['new_post_count'] = new_post_count
301 fav_thread_dict['new_post_count'] = new_post_count
302
302
303 fav_thread_dict['id'] = op.id
303 fav_thread_dict['id'] = op.id
304
304
305 fav_thread_dict['post_url'] = op.get_link_view()
305 fav_thread_dict['post_url'] = op.get_link_view()
306 fav_thread_dict['title'] = op.title
306 fav_thread_dict['title'] = op.title
307
307
308 posts.append(fav_thread_dict)
308 posts.append(fav_thread_dict)
309 else:
309 else:
310 fav_thread_dict = dict()
310 fav_thread_dict = dict()
311 fav_thread_dict['new_post_count'] = \
311 fav_thread_dict['new_post_count'] = \
312 Thread.objects.get_new_post_count(ops)
312 Thread.objects.get_new_post_count(ops)
313 posts.append(fav_thread_dict)
313 posts.append(fav_thread_dict)
314
314
315 return HttpResponse(content=json.dumps(posts))
315 return HttpResponse(content=json.dumps(posts))
@@ -1,40 +1,42 b''
1 from datetime import datetime
1 from datetime import datetime
2 from datetime import timedelta
2 from datetime import timedelta
3
3
4 from django.db.models import Count
4 from django.db.models import Count
5 from django.shortcuts import render
5 from django.shortcuts import render
6 from django.utils.decorators import method_decorator
6 from django.utils.decorators import method_decorator
7 from django.views.decorators.csrf import csrf_protect
7 from django.views.decorators.csrf import csrf_protect
8
8
9 from boards import settings
9 from boards import settings
10 from boards.models import Post
10 from boards.models import Post
11 from boards.models import Tag, Attachment, STATUS_ACTIVE
11 from boards.models import Tag, Attachment, STATUS_ACTIVE
12 from boards.models.tag import DEFAULT_LOCALE
12 from boards.views.base import BaseBoardView
13 from boards.views.base import BaseBoardView
13
14
14 PARAM_SECTION_STR = 'section_str'
15 PARAM_SECTION_STR = 'section_str'
15 PARAM_LATEST_THREADS = 'latest_threads'
16 PARAM_LATEST_THREADS = 'latest_threads'
16
17
17 TEMPLATE = 'boards/landing.html'
18 TEMPLATE = 'boards/landing.html'
18
19
19
20
20 class LandingView(BaseBoardView):
21 class LandingView(BaseBoardView):
21 @method_decorator(csrf_protect)
22 @method_decorator(csrf_protect)
22 def get(self, request):
23 def get(self, request):
23 params = dict()
24 params = dict()
24
25
25 params[PARAM_SECTION_STR] = Tag.objects.get_tag_url_list(
26 params[PARAM_SECTION_STR] = Tag.objects.get_tag_url_list(
26 Tag.objects.filter(required=True))
27 Tag.objects.filter(required=True).filter(
28 aliases__locale=DEFAULT_LOCALE).order_by('aliases__name'))
27
29
28 today = datetime.now() - timedelta(1)
30 today = datetime.now() - timedelta(1)
29 ops = Post.objects.filter(thread__replies__pub_time__gt=today, opening=True, thread__status=STATUS_ACTIVE)\
31 ops = Post.objects.filter(thread__replies__pub_time__gt=today, opening=True, thread__status=STATUS_ACTIVE)\
30 .annotate(today_post_count=Count('thread__replies'))\
32 .annotate(today_post_count=Count('thread__replies'))\
31 .order_by('-pub_time')
33 .order_by('-pub_time')
32
34
33 max_landing_threads = settings.get_int('View', 'MaxFavoriteThreads')
35 max_landing_threads = settings.get_int('View', 'MaxFavoriteThreads')
34 if max_landing_threads > 0:
36 if max_landing_threads > 0:
35 ops = ops[:max_landing_threads]
37 ops = ops[:max_landing_threads]
36
38
37 params[PARAM_LATEST_THREADS] = ops
39 params[PARAM_LATEST_THREADS] = ops
38
40
39 return render(request, TEMPLATE, params)
41 return render(request, TEMPLATE, params)
40
42
@@ -1,118 +1,124 b''
1 from django.shortcuts import get_object_or_404, redirect
1 from django.shortcuts import get_object_or_404, redirect
2 from django.core.urlresolvers import reverse
2 from django.core.urlresolvers 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
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 = get_object_or_404(Tag, name=self.tag_name)
27 tag_alias = get_object_or_404(TagAlias, name=self.tag_name)
28 tag = tag_alias.parent
28
29
29 hidden_tags = self.settings_manager.get_hidden_tags()
30 hidden_tags = self.settings_manager.get_hidden_tags()
30
31
31 try:
32 try:
32 hidden_tags.remove(tag)
33 hidden_tags.remove(tag)
33 except ValueError:
34 except ValueError:
34 pass
35 pass
35
36
36 return tag.get_threads().exclude(
37 return tag.get_threads().exclude(
37 tags__in=hidden_tags)
38 tags__in=hidden_tags)
38
39
39 def get_context_data(self, **kwargs):
40 def get_context_data(self, **kwargs):
40 params = super(TagView, self).get_context_data(**kwargs)
41 params = super(TagView, self).get_context_data(**kwargs)
41
42
42 settings_manager = get_settings_manager(kwargs['request'])
43 settings_manager = get_settings_manager(kwargs['request'])
43
44
44 tag = get_object_or_404(Tag, name=self.tag_name)
45 tag_alias = get_object_or_404(TagAlias, name=self.tag_name)
46 tag = tag_alias.parent
45 params[PARAM_TAG] = tag
47 params[PARAM_TAG] = tag
46
48
47 fav_tag_names = settings_manager.get_setting(SETTING_FAVORITE_TAGS)
49 fav_tag_names = settings_manager.get_setting(SETTING_FAVORITE_TAGS)
48 hidden_tag_names = settings_manager.get_setting(SETTING_HIDDEN_TAGS)
50 hidden_tag_names = settings_manager.get_setting(SETTING_HIDDEN_TAGS)
49
51
50 params[PARAM_IS_FAVORITE] = fav_tag_names is not None and tag.name in fav_tag_names
52 params[PARAM_IS_FAVORITE] = fav_tag_names is not None and tag.get_name() in fav_tag_names
51 params[PARAM_IS_HIDDEN] = hidden_tag_names is not None and tag.name in hidden_tag_names
53 params[PARAM_IS_HIDDEN] = hidden_tag_names is not None and tag.get_name() in hidden_tag_names
52
54
53 params[PARAM_RANDOM_IMAGE_POST] = tag.get_random_image_post()
55 params[PARAM_RANDOM_IMAGE_POST] = tag.get_random_image_post()
54 params[PARAM_RELATED_TAGS] = tag.get_related_tags()
56 params[PARAM_RELATED_TAGS] = tag.get_related_tags()
55
57
56 return params
58 return params
57
59
58 def get_reverse_url(self):
60 def get_reverse_url(self):
59 return reverse('tag', kwargs={'tag_name': self.tag_name})
61 return reverse('tag', kwargs={'tag_name': self.tag_name})
60
62
61 def get(self, request, tag_name, form=None):
63 def get(self, request, tag_name, form=None):
62 self.tag_name = tag_name
64 self.tag_name = tag_name
63
65
64 return super(TagView, self).get(request, form)
66 return super(TagView, self).get(request, form)
65
67
66
68
67 def post(self, request, tag_name):
69 def post(self, request, tag_name):
68 self.tag_name = tag_name
70 self.tag_name = tag_name
69
71
70 if PARAMETER_METHOD in request.POST:
72 if PARAMETER_METHOD in request.POST:
71 self.dispatch_method(request)
73 self.dispatch_method(request)
72
74
73 return redirect('tag', tag_name)
75 return redirect('tag', tag_name)
74 else:
76 else:
75 form = ThreadForm(request.POST, request.FILES,
77 form = ThreadForm(request.POST, request.FILES,
76 error_class=PlainErrorList)
78 error_class=PlainErrorList)
77 form.session = request.session
79 form.session = request.session
78
80
79 if form.is_valid():
81 if form.is_valid():
80 return self.create_thread(request, form)
82 return self.create_thread(request, form)
81 if form.need_to_ban:
83 if form.need_to_ban:
82 # Ban user because he is suspected to be a bot
84 # Ban user because he is suspected to be a bot
83 self._ban_current_user(request)
85 self._ban_current_user(request)
84
86
85 return self.get(request, tag_name, form)
87 return self.get(request, tag_name, form)
86
88
87 def subscribe(self, request):
89 def subscribe(self, request):
88 tag = get_object_or_404(Tag, name=self.tag_name)
90 alias = get_object_or_404(TagAlias, name=self.tag_name)
91 tag = alias.parent
89
92
90 settings_manager = get_settings_manager(request)
93 settings_manager = get_settings_manager(request)
91 settings_manager.add_fav_tag(tag)
94 settings_manager.add_fav_tag(tag)
92
95
93 def unsubscribe(self, request):
96 def unsubscribe(self, request):
94 tag = get_object_or_404(Tag, name=self.tag_name)
97 alias = get_object_or_404(TagAlias, name=self.tag_name)
98 tag = alias.parent
95
99
96 settings_manager = get_settings_manager(request)
100 settings_manager = get_settings_manager(request)
97 settings_manager.del_fav_tag(tag)
101 settings_manager.del_fav_tag(tag)
98
102
99 def hide(self, request):
103 def hide(self, request):
100 """
104 """
101 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
102 shown.
106 shown.
103 """
107 """
104
108
105 tag = get_object_or_404(Tag, name=self.tag_name)
109 alias = get_object_or_404(TagAlias, name=self.tag_name)
110 tag = alias.parent
106
111
107 settings_manager = get_settings_manager(request)
112 settings_manager = get_settings_manager(request)
108 settings_manager.add_hidden_tag(tag)
113 settings_manager.add_hidden_tag(tag)
109
114
110 def unhide(self, request):
115 def unhide(self, request):
111 """
116 """
112 Removed tag from user's hidden tags.
117 Removed tag from user's hidden tags.
113 """
118 """
114
119
115 tag = get_object_or_404(Tag, name=self.tag_name)
120 alias = get_object_or_404(TagAlias, name=self.tag_name)
121 tag = alias.parent
116
122
117 settings_manager = get_settings_manager(request)
123 settings_manager = get_settings_manager(request)
118 settings_manager.del_hidden_tag(tag)
124 settings_manager.del_hidden_tag(tag)
General Comments 0
You need to be logged in to leave comments. Login now