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