Show More
@@ -0,0 +1,20 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | from __future__ import unicode_literals | |||
|
3 | ||||
|
4 | from django.db import models, migrations | |||
|
5 | ||||
|
6 | ||||
|
7 | class Migration(migrations.Migration): | |||
|
8 | ||||
|
9 | dependencies = [ | |||
|
10 | ('boards', '0025_auto_20150825_2049'), | |||
|
11 | ] | |||
|
12 | ||||
|
13 | operations = [ | |||
|
14 | migrations.AddField( | |||
|
15 | model_name='post', | |||
|
16 | name='opening', | |||
|
17 | field=models.BooleanField(default=False), | |||
|
18 | preserve_default=False, | |||
|
19 | ), | |||
|
20 | ] |
@@ -0,0 +1,22 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | from __future__ import unicode_literals | |||
|
3 | ||||
|
4 | from django.db import migrations | |||
|
5 | ||||
|
6 | ||||
|
7 | class Migration(migrations.Migration): | |||
|
8 | ||||
|
9 | def build_opening_flag(apps, schema_editor): | |||
|
10 | Post = apps.get_model('boards', 'Post') | |||
|
11 | for post in Post.objects.all(): | |||
|
12 | op = Post.objects.filter(threads__in=[post.thread]).order_by('pub_time').first() | |||
|
13 | post.opening = op.id == post.id | |||
|
14 | post.save(update_fields=['opening']) | |||
|
15 | ||||
|
16 | dependencies = [ | |||
|
17 | ('boards', '0026_post_opening'), | |||
|
18 | ] | |||
|
19 | ||||
|
20 | operations = [ | |||
|
21 | migrations.RunPython(build_opening_flag), | |||
|
22 | ] |
@@ -0,0 +1,19 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | from __future__ import unicode_literals | |||
|
3 | ||||
|
4 | from django.db import models, migrations | |||
|
5 | ||||
|
6 | ||||
|
7 | class Migration(migrations.Migration): | |||
|
8 | ||||
|
9 | dependencies = [ | |||
|
10 | ('boards', '0027_auto_20150912_1632'), | |||
|
11 | ] | |||
|
12 | ||||
|
13 | operations = [ | |||
|
14 | migrations.AlterField( | |||
|
15 | model_name='post', | |||
|
16 | name='threads', | |||
|
17 | field=models.ManyToManyField(to='boards.Thread', related_name='multi_replies', db_index=True), | |||
|
18 | ), | |||
|
19 | ] |
@@ -0,0 +1,19 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | from __future__ import unicode_literals | |||
|
3 | ||||
|
4 | from django.db import models, migrations | |||
|
5 | ||||
|
6 | ||||
|
7 | class Migration(migrations.Migration): | |||
|
8 | ||||
|
9 | dependencies = [ | |||
|
10 | ('boards', '0028_auto_20150928_2211'), | |||
|
11 | ] | |||
|
12 | ||||
|
13 | operations = [ | |||
|
14 | migrations.AddField( | |||
|
15 | model_name='tag', | |||
|
16 | name='parent', | |||
|
17 | field=models.ForeignKey(to='boards.Tag', null=True), | |||
|
18 | ), | |||
|
19 | ] |
@@ -0,0 +1,19 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | from __future__ import unicode_literals | |||
|
3 | ||||
|
4 | from django.db import models, migrations | |||
|
5 | ||||
|
6 | ||||
|
7 | class Migration(migrations.Migration): | |||
|
8 | ||||
|
9 | dependencies = [ | |||
|
10 | ('boards', '0029_tag_parent'), | |||
|
11 | ] | |||
|
12 | ||||
|
13 | operations = [ | |||
|
14 | migrations.AlterField( | |||
|
15 | model_name='tag', | |||
|
16 | name='parent', | |||
|
17 | field=models.ForeignKey(related_name='children', null=True, to='boards.Tag'), | |||
|
18 | ), | |||
|
19 | ] |
@@ -0,0 +1,15 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | from __future__ import unicode_literals | |||
|
3 | ||||
|
4 | from django.db import models, migrations | |||
|
5 | ||||
|
6 | ||||
|
7 | class Migration(migrations.Migration): | |||
|
8 | ||||
|
9 | dependencies = [ | |||
|
10 | ('boards', '0030_auto_20150929_1816'), | |||
|
11 | ('boards', '0026_auto_20150830_2006'), | |||
|
12 | ] | |||
|
13 | ||||
|
14 | operations = [ | |||
|
15 | ] |
@@ -0,0 +1,66 b'' | |||||
|
1 | import os | |||
|
2 | import re | |||
|
3 | ||||
|
4 | from django.core.files.uploadedfile import SimpleUploadedFile | |||
|
5 | from pytube import YouTube | |||
|
6 | import requests | |||
|
7 | ||||
|
8 | from boards.utils import validate_file_size | |||
|
9 | ||||
|
10 | YOUTUBE_VIDEO_FORMAT = 'webm' | |||
|
11 | ||||
|
12 | HTTP_RESULT_OK = 200 | |||
|
13 | ||||
|
14 | HEADER_CONTENT_LENGTH = 'content-length' | |||
|
15 | HEADER_CONTENT_TYPE = 'content-type' | |||
|
16 | ||||
|
17 | FILE_DOWNLOAD_CHUNK_BYTES = 100000 | |||
|
18 | ||||
|
19 | YOUTUBE_URL = re.compile(r'https?://www\.youtube\.com/watch\?v=\w+') | |||
|
20 | ||||
|
21 | ||||
|
22 | class Downloader: | |||
|
23 | @staticmethod | |||
|
24 | def handles(url: str) -> bool: | |||
|
25 | return False | |||
|
26 | ||||
|
27 | @staticmethod | |||
|
28 | def download(url: str): | |||
|
29 | # Verify content headers | |||
|
30 | response_head = requests.head(url, verify=False) | |||
|
31 | content_type = response_head.headers[HEADER_CONTENT_TYPE].split(';')[0] | |||
|
32 | length_header = response_head.headers.get(HEADER_CONTENT_LENGTH) | |||
|
33 | if length_header: | |||
|
34 | length = int(length_header) | |||
|
35 | validate_file_size(length) | |||
|
36 | # Get the actual content into memory | |||
|
37 | response = requests.get(url, verify=False, stream=True) | |||
|
38 | ||||
|
39 | # Download file, stop if the size exceeds limit | |||
|
40 | size = 0 | |||
|
41 | content = b'' | |||
|
42 | for chunk in response.iter_content(FILE_DOWNLOAD_CHUNK_BYTES): | |||
|
43 | size += len(chunk) | |||
|
44 | validate_file_size(size) | |||
|
45 | content += chunk | |||
|
46 | ||||
|
47 | if response.status_code == HTTP_RESULT_OK and content: | |||
|
48 | # Set a dummy file name that will be replaced | |||
|
49 | # anyway, just keep the valid extension | |||
|
50 | filename = 'file.' + content_type.split('/')[1] | |||
|
51 | return SimpleUploadedFile(filename, content, content_type) | |||
|
52 | ||||
|
53 | ||||
|
54 | class YouTubeDownloader(Downloader): | |||
|
55 | @staticmethod | |||
|
56 | def download(url: str): | |||
|
57 | yt = YouTube() | |||
|
58 | yt.from_url(url) | |||
|
59 | videos = yt.filter(YOUTUBE_VIDEO_FORMAT) | |||
|
60 | if len(videos) > 0: | |||
|
61 | video = videos[0] | |||
|
62 | return Downloader.download(video.url) | |||
|
63 | ||||
|
64 | @staticmethod | |||
|
65 | def handles(url: str) -> bool: | |||
|
66 | return YOUTUBE_URL.match(url) |
1 | NO CONTENT: new file 100644, binary diff hidden |
|
NO CONTENT: new file 100644, binary diff hidden |
1 | NO CONTENT: new file 100644, binary diff hidden |
|
NO CONTENT: new file 100644, binary diff hidden |
1 | NO CONTENT: new file 100644, binary diff hidden |
|
NO CONTENT: new file 100644, binary diff hidden |
@@ -34,3 +34,4 b' dfb6c481b1a2c33705de9a9b5304bc924c46b202' | |||||
34 | 4a5bec08ccfb47a27f9e98698f12dd5b7246623b 2.8.2 |
|
34 | 4a5bec08ccfb47a27f9e98698f12dd5b7246623b 2.8.2 | |
35 | 604935b98f5b5e4a5e903594f048046e1fbb3519 2.8.3 |
|
35 | 604935b98f5b5e4a5e903594f048046e1fbb3519 2.8.3 | |
36 | c48ffdc671566069ed0f33644da1229277f3cd18 2.9.0 |
|
36 | c48ffdc671566069ed0f33644da1229277f3cd18 2.9.0 | |
|
37 | d66dc192d4e089ba85325afeef5229b73cb0fde4 2.10.0 |
@@ -1,4 +1,5 b'' | |||||
1 | from boards.models import Tag |
|
1 | from boards.models import Tag | |
|
2 | from boards.models.thread import FAV_THREAD_NO_UPDATES | |||
2 |
|
3 | |||
3 | MAX_TRIPCODE_COLLISIONS = 50 |
|
4 | MAX_TRIPCODE_COLLISIONS = 50 | |
4 |
|
5 | |||
@@ -11,6 +12,7 b" PERMISSION_MODERATE = 'moderator'" | |||||
11 |
|
12 | |||
12 | SETTING_THEME = 'theme' |
|
13 | SETTING_THEME = 'theme' | |
13 | SETTING_FAVORITE_TAGS = 'favorite_tags' |
|
14 | SETTING_FAVORITE_TAGS = 'favorite_tags' | |
|
15 | SETTING_FAVORITE_THREADS = 'favorite_threads' | |||
14 | SETTING_HIDDEN_TAGS = 'hidden_tags' |
|
16 | SETTING_HIDDEN_TAGS = 'hidden_tags' | |
15 | SETTING_PERMISSIONS = 'permissions' |
|
17 | SETTING_PERMISSIONS = 'permissions' | |
16 | SETTING_USERNAME = 'username' |
|
18 | SETTING_USERNAME = 'username' | |
@@ -118,6 +120,28 b' class SettingsManager:' | |||||
118 | tags.remove(tag.name) |
|
120 | tags.remove(tag.name) | |
119 | self.set_setting(SETTING_HIDDEN_TAGS, tags) |
|
121 | self.set_setting(SETTING_HIDDEN_TAGS, tags) | |
120 |
|
122 | |||
|
123 | def get_fav_threads(self) -> dict: | |||
|
124 | return self.get_setting(SETTING_FAVORITE_THREADS, default=dict()) | |||
|
125 | ||||
|
126 | def add_or_read_fav_thread(self, opening_post): | |||
|
127 | threads = self.get_fav_threads() | |||
|
128 | thread = opening_post.get_thread() | |||
|
129 | # Don't check for new posts if the thread is archived already | |||
|
130 | if thread.is_archived(): | |||
|
131 | last_id = FAV_THREAD_NO_UPDATES | |||
|
132 | else: | |||
|
133 | last_id = thread.get_replies().last().id | |||
|
134 | threads[str(opening_post.id)] = last_id | |||
|
135 | self.set_setting(SETTING_FAVORITE_THREADS, threads) | |||
|
136 | ||||
|
137 | def del_fav_thread(self, opening_post): | |||
|
138 | threads = self.get_fav_threads() | |||
|
139 | if self.thread_is_fav(opening_post): | |||
|
140 | del threads[str(opening_post.id)] | |||
|
141 | self.set_setting(SETTING_FAVORITE_THREADS, threads) | |||
|
142 | ||||
|
143 | def thread_is_fav(self, opening_post): | |||
|
144 | return str(opening_post.id) in self.get_fav_threads() | |||
121 |
|
145 | |||
122 | class SessionSettingsManager(SettingsManager): |
|
146 | class SessionSettingsManager(SettingsManager): | |
123 | """ |
|
147 | """ |
@@ -30,7 +30,10 b' class TagAdmin(admin.ModelAdmin):' | |||||
30 | def thread_count(self, obj: Tag) -> int: |
|
30 | def thread_count(self, obj: Tag) -> int: | |
31 | return obj.get_thread_count() |
|
31 | return obj.get_thread_count() | |
32 |
|
32 | |||
33 | list_display = ('name', 'thread_count') |
|
33 | def display_children(self, obj: Tag): | |
|
34 | return ', '.join([str(child) for child in obj.get_children().all()]) | |||
|
35 | ||||
|
36 | list_display = ('name', 'thread_count', 'display_children') | |||
34 | search_fields = ('name',) |
|
37 | search_fields = ('name',) | |
35 |
|
38 | |||
36 |
|
39 | |||
@@ -46,7 +49,14 b' class ThreadAdmin(admin.ModelAdmin):' | |||||
46 | def ip(self, obj: Thread): |
|
49 | def ip(self, obj: Thread): | |
47 | return obj.get_opening_post().poster_ip |
|
50 | return obj.get_opening_post().poster_ip | |
48 |
|
51 | |||
49 | list_display = ('id', 'title', 'reply_count', 'archived', 'ip') |
|
52 | def display_tags(self, obj: Thread): | |
|
53 | return ', '.join([str(tag) for tag in obj.get_tags().all()]) | |||
|
54 | ||||
|
55 | def op(self, obj: Thread): | |||
|
56 | return obj.get_opening_post_id() | |||
|
57 | ||||
|
58 | list_display = ('id', 'op', 'title', 'reply_count', 'archived', 'ip', | |||
|
59 | 'display_tags') | |||
50 | list_filter = ('bump_time', 'archived', 'bumpable') |
|
60 | list_filter = ('bump_time', 'archived', 'bumpable') | |
51 | search_fields = ('id', 'title') |
|
61 | search_fields = ('id', 'title') | |
52 | filter_horizontal = ('tags',) |
|
62 | filter_horizontal = ('tags',) |
@@ -1,5 +1,5 b'' | |||||
1 | [Version] |
|
1 | [Version] | |
2 |
Version = 2. |
|
2 | Version = 2.10.0 BT | |
3 | SiteName = Neboard DEV |
|
3 | SiteName = Neboard DEV | |
4 |
|
4 | |||
5 | [Cache] |
|
5 | [Cache] |
@@ -19,6 +19,7 b" CONTEXT_NEW_NOTIFICATIONS_COUNT = 'new_n" | |||||
19 | CONTEXT_USERNAME = 'username' |
|
19 | CONTEXT_USERNAME = 'username' | |
20 | CONTEXT_TAGS_STR = 'tags_str' |
|
20 | CONTEXT_TAGS_STR = 'tags_str' | |
21 | CONTEXT_IMAGE_VIEWER = 'image_viewer' |
|
21 | CONTEXT_IMAGE_VIEWER = 'image_viewer' | |
|
22 | CONTEXT_HAS_FAV_THREADS = 'has_fav_threads' | |||
22 |
|
23 | |||
23 |
|
24 | |||
24 | def get_notifications(context, request): |
|
25 | def get_notifications(context, request): | |
@@ -43,6 +44,7 b' def user_and_ui_processor(request):' | |||||
43 | settings_manager = get_settings_manager(request) |
|
44 | settings_manager = get_settings_manager(request) | |
44 | fav_tags = settings_manager.get_fav_tags() |
|
45 | fav_tags = settings_manager.get_fav_tags() | |
45 | context[CONTEXT_TAGS] = fav_tags |
|
46 | context[CONTEXT_TAGS] = fav_tags | |
|
47 | ||||
46 | context[CONTEXT_TAGS_STR] = Tag.objects.get_tag_url_list(fav_tags) |
|
48 | context[CONTEXT_TAGS_STR] = Tag.objects.get_tag_url_list(fav_tags) | |
47 | theme = settings_manager.get_theme() |
|
49 | theme = settings_manager.get_theme() | |
48 | context[CONTEXT_THEME] = theme |
|
50 | context[CONTEXT_THEME] = theme | |
@@ -58,6 +60,9 b' def user_and_ui_processor(request):' | |||||
58 | SETTING_IMAGE_VIEWER, |
|
60 | SETTING_IMAGE_VIEWER, | |
59 | default=settings.get('View', 'DefaultImageViewer')) |
|
61 | default=settings.get('View', 'DefaultImageViewer')) | |
60 |
|
62 | |||
|
63 | context[CONTEXT_HAS_FAV_THREADS] =\ | |||
|
64 | len(settings_manager.get_fav_threads()) > 0 | |||
|
65 | ||||
61 | get_notifications(context, request) |
|
66 | get_notifications(context, request) | |
62 |
|
67 | |||
63 | return context |
|
68 | return context |
@@ -7,19 +7,17 b' from django import forms' | |||||
7 | from django.core.files.uploadedfile import SimpleUploadedFile |
|
7 | from django.core.files.uploadedfile import SimpleUploadedFile | |
8 | from django.core.exceptions import ObjectDoesNotExist |
|
8 | from django.core.exceptions import ObjectDoesNotExist | |
9 | from django.forms.util import ErrorList |
|
9 | from django.forms.util import ErrorList | |
10 | from django.utils.translation import ugettext_lazy as _ |
|
10 | from django.utils.translation import ugettext_lazy as _, ungettext_lazy | |
11 | import requests |
|
|||
12 |
|
11 | |||
13 | from boards.mdx_neboard import formatters |
|
12 | from boards.mdx_neboard import formatters | |
|
13 | from boards.models.attachment.downloaders import Downloader | |||
14 | from boards.models.post import TITLE_MAX_LENGTH |
|
14 | from boards.models.post import TITLE_MAX_LENGTH | |
15 | from boards.models import Tag, Post |
|
15 | from boards.models import Tag, Post | |
|
16 | from boards.utils import validate_file_size | |||
16 | from neboard import settings |
|
17 | from neboard import settings | |
17 | import boards.settings as board_settings |
|
18 | import boards.settings as board_settings | |
18 | import neboard |
|
19 | import neboard | |
19 |
|
20 | |||
20 | HEADER_CONTENT_LENGTH = 'content-length' |
|
|||
21 | HEADER_CONTENT_TYPE = 'content-type' |
|
|||
22 |
|
||||
23 | REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE) |
|
21 | REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE) | |
24 |
|
22 | |||
25 | VETERAN_POSTING_DELAY = 5 |
|
23 | VETERAN_POSTING_DELAY = 5 | |
@@ -37,14 +35,11 b" LABEL_TEXT = _('Text')" | |||||
37 | LABEL_TAG = _('Tag') |
|
35 | LABEL_TAG = _('Tag') | |
38 | LABEL_SEARCH = _('Search') |
|
36 | LABEL_SEARCH = _('Search') | |
39 |
|
37 | |||
40 |
ERROR_SPEED = |
|
38 | ERROR_SPEED = 'Please wait %(delay)d second before sending message' | |
|
39 | ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message' | |||
41 |
|
40 | |||
42 | TAG_MAX_LENGTH = 20 |
|
41 | TAG_MAX_LENGTH = 20 | |
43 |
|
42 | |||
44 | FILE_DOWNLOAD_CHUNK_BYTES = 100000 |
|
|||
45 |
|
||||
46 | HTTP_RESULT_OK = 200 |
|
|||
47 |
|
||||
48 | TEXTAREA_ROWS = 4 |
|
43 | TEXTAREA_ROWS = 4 | |
49 |
|
44 | |||
50 |
|
45 | |||
@@ -182,7 +177,7 b' class PostForm(NeboardForm):' | |||||
182 | file = self.cleaned_data['file'] |
|
177 | file = self.cleaned_data['file'] | |
183 |
|
178 | |||
184 | if file: |
|
179 | if file: | |
185 |
|
|
180 | validate_file_size(file.size) | |
186 |
|
181 | |||
187 | return file |
|
182 | return file | |
188 |
|
183 | |||
@@ -196,7 +191,7 b' class PostForm(NeboardForm):' | |||||
196 | if not file: |
|
191 | if not file: | |
197 | raise forms.ValidationError(_('Invalid URL')) |
|
192 | raise forms.ValidationError(_('Invalid URL')) | |
198 | else: |
|
193 | else: | |
199 |
|
|
194 | validate_file_size(file.size) | |
200 |
|
195 | |||
201 | return file |
|
196 | return file | |
202 |
|
197 | |||
@@ -272,9 +267,8 b' class PostForm(NeboardForm):' | |||||
272 | now = time.time() |
|
267 | now = time.time() | |
273 |
|
268 | |||
274 | current_delay = 0 |
|
269 | current_delay = 0 | |
275 | need_delay = False |
|
|||
276 |
|
270 | |||
277 |
if not |
|
271 | if LAST_POST_TIME not in self.session: | |
278 | self.session[LAST_POST_TIME] = now |
|
272 | self.session[LAST_POST_TIME] = now | |
279 |
|
273 | |||
280 | need_delay = True |
|
274 | need_delay = True | |
@@ -285,8 +279,9 b' class PostForm(NeboardForm):' | |||||
285 | need_delay = current_delay < posting_delay |
|
279 | need_delay = current_delay < posting_delay | |
286 |
|
280 | |||
287 | if need_delay: |
|
281 | if need_delay: | |
288 | error_message = ERROR_SPEED % str(posting_delay |
|
282 | delay = posting_delay - current_delay | |
289 | - current_delay) |
|
283 | error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL, | |
|
284 | delay) % {'delay': delay} | |||
290 | self._errors['text'] = self.error_class([error_message]) |
|
285 | self._errors['text'] = self.error_class([error_message]) | |
291 |
|
286 | |||
292 | can_post = False |
|
287 | can_post = False | |
@@ -294,13 +289,6 b' class PostForm(NeboardForm):' | |||||
294 | if can_post: |
|
289 | if can_post: | |
295 | self.session[LAST_POST_TIME] = now |
|
290 | self.session[LAST_POST_TIME] = now | |
296 |
|
291 | |||
297 | def validate_file_size(self, size: int): |
|
|||
298 | max_size = board_settings.get_int('Forms', 'MaxFileSize') |
|
|||
299 | if size > max_size: |
|
|||
300 | raise forms.ValidationError( |
|
|||
301 | _('File must be less than %s bytes') |
|
|||
302 | % str(max_size)) |
|
|||
303 |
|
||||
304 | def _get_file_from_url(self, url: str) -> SimpleUploadedFile: |
|
292 | def _get_file_from_url(self, url: str) -> SimpleUploadedFile: | |
305 | """ |
|
293 | """ | |
306 | Gets an file file from URL. |
|
294 | Gets an file file from URL. | |
@@ -309,36 +297,18 b' class PostForm(NeboardForm):' | |||||
309 | img_temp = None |
|
297 | img_temp = None | |
310 |
|
298 | |||
311 | try: |
|
299 | try: | |
312 | # Verify content headers |
|
300 | for downloader in Downloader.__subclasses__(): | |
313 | response_head = requests.head(url, verify=False) |
|
301 | if downloader.handles(url): | |
314 | content_type = response_head.headers[HEADER_CONTENT_TYPE].split(';')[0] |
|
302 | return downloader.download(url) | |
315 | length_header = response_head.headers.get(HEADER_CONTENT_LENGTH) |
|
303 | # If nobody of the specific downloaders handles this, use generic | |
316 |
|
|
304 | # one | |
317 | length = int(length_header) |
|
305 | return Downloader.download(url) | |
318 | self.validate_file_size(length) |
|
306 | except forms.ValidationError as e: | |
319 | # Get the actual content into memory |
|
307 | raise e | |
320 | response = requests.get(url, verify=False, stream=True) |
|
|||
321 |
|
||||
322 | # Download file, stop if the size exceeds limit |
|
|||
323 | size = 0 |
|
|||
324 | content = b'' |
|
|||
325 | for chunk in response.iter_content(FILE_DOWNLOAD_CHUNK_BYTES): |
|
|||
326 | size += len(chunk) |
|
|||
327 | self.validate_file_size(size) |
|
|||
328 | content += chunk |
|
|||
329 |
|
||||
330 | if response.status_code == HTTP_RESULT_OK and content: |
|
|||
331 | # Set a dummy file name that will be replaced |
|
|||
332 | # anyway, just keep the valid extension |
|
|||
333 | filename = 'file.' + content_type.split('/')[1] |
|
|||
334 | img_temp = SimpleUploadedFile(filename, content, |
|
|||
335 | content_type) |
|
|||
336 | except Exception as e: |
|
308 | except Exception as e: | |
337 | # Just return no file |
|
309 | # Just return no file | |
338 | pass |
|
310 | pass | |
339 |
|
311 | |||
340 | return img_temp |
|
|||
341 |
|
||||
342 |
|
312 | |||
343 | class ThreadForm(PostForm): |
|
313 | class ThreadForm(PostForm): | |
344 |
|
314 | |||
@@ -354,20 +324,27 b' class ThreadForm(PostForm):' | |||||
354 | _('Inappropriate characters in tags.')) |
|
324 | _('Inappropriate characters in tags.')) | |
355 |
|
325 | |||
356 | required_tag_exists = False |
|
326 | required_tag_exists = False | |
357 |
|
|
327 | tag_set = set() | |
358 | try: |
|
328 | for tag_string in tags.split(): | |
359 |
|
|
329 | tag, created = Tag.objects.get_or_create(name=tag_string.strip().lower()) | |
|
330 | tag_set.add(tag) | |||
|
331 | ||||
|
332 | # If this is a new tag, don't check for its parents because nobody | |||
|
333 | # added them yet | |||
|
334 | if not created: | |||
|
335 | tag_set |= tag.get_all_parents() | |||
|
336 | ||||
|
337 | for tag in tag_set: | |||
|
338 | if tag.required: | |||
360 | required_tag_exists = True |
|
339 | required_tag_exists = True | |
361 | break |
|
340 | break | |
362 | except ObjectDoesNotExist: |
|
|||
363 | pass |
|
|||
364 |
|
341 | |||
365 | if not required_tag_exists: |
|
342 | if not required_tag_exists: | |
366 | all_tags = Tag.objects.filter(required=True) |
|
343 | all_tags = Tag.objects.filter(required=True) | |
367 | raise forms.ValidationError( |
|
344 | raise forms.ValidationError( | |
368 | _('Need at least one section.')) |
|
345 | _('Need at least one section.')) | |
369 |
|
346 | |||
370 | return tags |
|
347 | return tag_set | |
371 |
|
348 | |||
372 | def clean(self): |
|
349 | def clean(self): | |
373 | cleaned_data = super(ThreadForm, self).clean() |
|
350 | cleaned_data = super(ThreadForm, self).clean() |
1 | NO CONTENT: modified file, binary diff hidden |
|
NO CONTENT: modified file, binary diff hidden |
@@ -7,7 +7,7 b' msgid ""' | |||||
7 | msgstr "" |
|
7 | msgstr "" | |
8 | "Project-Id-Version: PACKAGE VERSION\n" |
|
8 | "Project-Id-Version: PACKAGE VERSION\n" | |
9 | "Report-Msgid-Bugs-To: \n" |
|
9 | "Report-Msgid-Bugs-To: \n" | |
10 |
"POT-Creation-Date: 2015-0 |
|
10 | "POT-Creation-Date: 2015-09-12 12:48+0300\n" | |
11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" |
|
11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" | |
12 | "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" |
|
12 | "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" | |
13 | "Language-Team: LANGUAGE <LL@li.org>\n" |
|
13 | "Language-Team: LANGUAGE <LL@li.org>\n" | |
@@ -38,114 +38,99 b' msgstr "\xd1\x80\xd0\xb0\xd0\xb7\xd1\x80\xd0\xb0\xd0\xb1\xd0\xbe\xd1\x82\xd1\x87\xd0\xb8\xd0\xba javascript"' | |||||
38 | msgid "designer" |
|
38 | msgid "designer" | |
39 | msgstr "Π΄ΠΈΠ·Π°ΠΉΠ½Π΅Ρ" |
|
39 | msgstr "Π΄ΠΈΠ·Π°ΠΉΠ½Π΅Ρ" | |
40 |
|
40 | |||
41 |
#: forms.py:3 |
|
41 | #: forms.py:30 | |
42 | msgid "Type message here. Use formatting panel for more advanced usage." |
|
42 | msgid "Type message here. Use formatting panel for more advanced usage." | |
43 | msgstr "" |
|
43 | msgstr "" | |
44 | "ΠΠ²ΠΎΠ΄ΠΈΡΠ΅ ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΠ΅ ΡΡΠ΄Π°. ΠΡΠΏΠΎΠ»ΡΠ·ΡΠΉΡΠ΅ ΠΏΠ°Π½Π΅Π»Ρ Π΄Π»Ρ Π±ΠΎΠ»Π΅Π΅ ΡΠ»ΠΎΠΆΠ½ΠΎΠ³ΠΎ ΡΠΎΡΠΌΠ°ΡΠΈΡΠΎΠ²Π°Π½ΠΈΡ." |
|
44 | "ΠΠ²ΠΎΠ΄ΠΈΡΠ΅ ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΠ΅ ΡΡΠ΄Π°. ΠΡΠΏΠΎΠ»ΡΠ·ΡΠΉΡΠ΅ ΠΏΠ°Π½Π΅Π»Ρ Π΄Π»Ρ Π±ΠΎΠ»Π΅Π΅ ΡΠ»ΠΎΠΆΠ½ΠΎΠ³ΠΎ ΡΠΎΡΠΌΠ°ΡΠΈΡΠΎΠ²Π°Π½ΠΈΡ." | |
45 |
|
45 | |||
46 |
#: forms.py:3 |
|
46 | #: forms.py:31 | |
47 | msgid "music images i_dont_like_tags" |
|
47 | msgid "music images i_dont_like_tags" | |
48 | msgstr "ΠΌΡΠ·ΡΠΊΠ° ΠΊΠ°ΡΡΠΈΠ½ΠΊΠΈ ΡΠ΅Π³ΠΈ_Π½Π΅_Π½ΡΠΆΠ½Ρ" |
|
48 | msgstr "ΠΌΡΠ·ΡΠΊΠ° ΠΊΠ°ΡΡΠΈΠ½ΠΊΠΈ ΡΠ΅Π³ΠΈ_Π½Π΅_Π½ΡΠΆΠ½Ρ" | |
49 |
|
49 | |||
50 |
#: forms.py:3 |
|
50 | #: forms.py:33 | |
51 | msgid "Title" |
|
51 | msgid "Title" | |
52 | msgstr "ΠΠ°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ" |
|
52 | msgstr "ΠΠ°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ" | |
53 |
|
53 | |||
54 |
#: forms.py:3 |
|
54 | #: forms.py:34 | |
55 | msgid "Text" |
|
55 | msgid "Text" | |
56 | msgstr "Π’Π΅ΠΊΡΡ" |
|
56 | msgstr "Π’Π΅ΠΊΡΡ" | |
57 |
|
57 | |||
58 |
#: forms.py:3 |
|
58 | #: forms.py:35 | |
59 | msgid "Tag" |
|
59 | msgid "Tag" | |
60 | msgstr "ΠΠ΅ΡΠΊΠ°" |
|
60 | msgstr "ΠΠ΅ΡΠΊΠ°" | |
61 |
|
61 | |||
62 |
#: forms.py:3 |
|
62 | #: forms.py:36 templates/boards/base.html:40 templates/search/search.html:7 | |
63 | msgid "Search" |
|
63 | msgid "Search" | |
64 | msgstr "ΠΠΎΠΈΡΠΊ" |
|
64 | msgstr "ΠΠΎΠΈΡΠΊ" | |
65 |
|
65 | |||
66 | #: forms.py:39 |
|
66 | #: forms.py:139 | |
67 | #, python-format |
|
|||
68 | msgid "Please wait %s seconds before sending message" |
|
|||
69 | msgstr "ΠΠΎΠΆΠ°Π»ΡΠΉΡΡΠ° ΠΏΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡΠ΅ %s ΡΠ΅ΠΊΡΠ½Π΄ ΠΏΠ΅ΡΠ΅Π΄ ΠΎΡΠΏΡΠ°Π²ΠΊΠΎΠΉ ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΡ" |
|
|||
70 |
|
||||
71 | #: forms.py:140 |
|
|||
72 | msgid "File" |
|
67 | msgid "File" | |
73 | msgstr "Π€Π°ΠΉΠ»" |
|
68 | msgstr "Π€Π°ΠΉΠ»" | |
74 |
|
69 | |||
75 |
#: forms.py:14 |
|
70 | #: forms.py:142 | |
76 | msgid "File URL" |
|
71 | msgid "File URL" | |
77 | msgstr "URL ΡΠ°ΠΉΠ»Π°" |
|
72 | msgstr "URL ΡΠ°ΠΉΠ»Π°" | |
78 |
|
73 | |||
79 |
#: forms.py:14 |
|
74 | #: forms.py:148 | |
80 | msgid "e-mail" |
|
75 | msgid "e-mail" | |
81 | msgstr "" |
|
76 | msgstr "" | |
82 |
|
77 | |||
83 |
#: forms.py:15 |
|
78 | #: forms.py:151 | |
84 | msgid "Additional threads" |
|
79 | msgid "Additional threads" | |
85 | msgstr "ΠΠΎΠΏΠΎΠ»Π½ΠΈΡΠ΅Π»ΡΠ½ΡΠ΅ ΡΠ΅ΠΌΡ" |
|
80 | msgstr "ΠΠΎΠΏΠΎΠ»Π½ΠΈΡΠ΅Π»ΡΠ½ΡΠ΅ ΡΠ΅ΠΌΡ" | |
86 |
|
81 | |||
87 |
#: forms.py:1 |
|
82 | #: forms.py:162 | |
88 | msgid "Tripcode" |
|
|||
89 | msgstr "Π’ΡΠΈΠΏΠΊΠΎΠ΄" |
|
|||
90 |
|
||||
91 | #: forms.py:164 |
|
|||
92 | #, python-format |
|
83 | #, python-format | |
93 | msgid "Title must have less than %s characters" |
|
84 | msgid "Title must have less than %s characters" | |
94 | msgstr "ΠΠ°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ Π΄ΠΎΠ»ΠΆΠ΅Π½ ΠΈΠΌΠ΅ΡΡ ΠΌΠ΅Π½ΡΡΠ΅ %s ΡΠΈΠΌΠ²ΠΎΠ»ΠΎΠ²" |
|
85 | msgstr "ΠΠ°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ Π΄ΠΎΠ»ΠΆΠ΅Π½ ΠΈΠΌΠ΅ΡΡ ΠΌΠ΅Π½ΡΡΠ΅ %s ΡΠΈΠΌΠ²ΠΎΠ»ΠΎΠ²" | |
95 |
|
86 | |||
96 |
#: forms.py:17 |
|
87 | #: forms.py:172 | |
97 | #, python-format |
|
88 | #, python-format | |
98 | msgid "Text must have less than %s characters" |
|
89 | msgid "Text must have less than %s characters" | |
99 | msgstr "Π’Π΅ΠΊΡΡ Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±ΡΡΡ ΠΊΠΎΡΠΎΡΠ΅ %s ΡΠΈΠΌΠ²ΠΎΠ»ΠΎΠ²" |
|
90 | msgstr "Π’Π΅ΠΊΡΡ Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±ΡΡΡ ΠΊΠΎΡΠΎΡΠ΅ %s ΡΠΈΠΌΠ²ΠΎΠ»ΠΎΠ²" | |
100 |
|
91 | |||
101 |
#: forms.py:19 |
|
92 | #: forms.py:192 | |
102 | msgid "Invalid URL" |
|
93 | msgid "Invalid URL" | |
103 | msgstr "ΠΠ΅Π²Π΅ΡΠ½ΡΠΉ URL" |
|
94 | msgstr "ΠΠ΅Π²Π΅ΡΠ½ΡΠΉ URL" | |
104 |
|
95 | |||
105 |
#: forms.py:21 |
|
96 | #: forms.py:213 | |
106 | msgid "Invalid additional thread list" |
|
97 | msgid "Invalid additional thread list" | |
107 | msgstr "ΠΠ΅Π²Π΅ΡΠ½ΡΠΉ ΡΠΏΠΈΡΠΎΠΊ Π΄ΠΎΠΏΠΎΠ»Π½ΠΈΡΠ΅Π»ΡΠ½ΡΡ ΡΠ΅ΠΌ" |
|
98 | msgstr "ΠΠ΅Π²Π΅ΡΠ½ΡΠΉ ΡΠΏΠΈΡΠΎΠΊ Π΄ΠΎΠΏΠΎΠ»Π½ΠΈΡΠ΅Π»ΡΠ½ΡΡ ΡΠ΅ΠΌ" | |
108 |
|
99 | |||
109 |
#: forms.py:25 |
|
100 | #: forms.py:258 | |
110 | msgid "Either text or file must be entered." |
|
101 | msgid "Either text or file must be entered." | |
111 | msgstr "Π’Π΅ΠΊΡΡ ΠΈΠ»ΠΈ ΡΠ°ΠΉΠ» Π΄ΠΎΠ»ΠΆΠ½Ρ Π±ΡΡΡ Π²Π²Π΅Π΄Π΅Π½Ρ." |
|
102 | msgstr "Π’Π΅ΠΊΡΡ ΠΈΠ»ΠΈ ΡΠ°ΠΉΠ» Π΄ΠΎΠ»ΠΆΠ½Ρ Π±ΡΡΡ Π²Π²Π΅Π΄Π΅Π½Ρ." | |
112 |
|
103 | |||
113 | #: forms.py:289 |
|
104 | #: forms.py:317 templates/boards/all_threads.html:148 | |
114 | #, python-format |
|
|||
115 | msgid "File must be less than %s bytes" |
|
|||
116 | msgstr "Π€Π°ΠΉΠ» Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±ΡΡΡ ΠΌΠ΅Π½Π΅Π΅ %s Π±Π°ΠΉΡ" |
|
|||
117 |
|
||||
118 | #: forms.py:335 templates/boards/all_threads.html:154 |
|
|||
119 | #: templates/boards/rss/post.html:10 templates/boards/tags.html:6 |
|
105 | #: templates/boards/rss/post.html:10 templates/boards/tags.html:6 | |
120 | msgid "Tags" |
|
106 | msgid "Tags" | |
121 | msgstr "ΠΠ΅ΡΠΊΠΈ" |
|
107 | msgstr "ΠΠ΅ΡΠΊΠΈ" | |
122 |
|
108 | |||
123 |
#: forms.py:3 |
|
109 | #: forms.py:324 | |
124 | msgid "Inappropriate characters in tags." |
|
110 | msgid "Inappropriate characters in tags." | |
125 | msgstr "ΠΠ΅Π΄ΠΎΠΏΡΡΡΠΈΠΌΡΠ΅ ΡΠΈΠΌΠ²ΠΎΠ»Ρ Π² ΠΌΠ΅ΡΠΊΠ°Ρ ." |
|
111 | msgstr "ΠΠ΅Π΄ΠΎΠΏΡΡΡΠΈΠΌΡΠ΅ ΡΠΈΠΌΠ²ΠΎΠ»Ρ Π² ΠΌΠ΅ΡΠΊΠ°Ρ ." | |
126 |
|
112 | |||
127 |
#: forms.py:3 |
|
113 | #: forms.py:338 | |
128 | msgid "Need at least one section." |
|
114 | msgid "Need at least one section." | |
129 | msgstr "ΠΡΠΆΠ΅Π½ Ρ ΠΎΡΡ Π±Ρ ΠΎΠ΄ΠΈΠ½ ΡΠ°Π·Π΄Π΅Π»." |
|
115 | msgstr "ΠΡΠΆΠ΅Π½ Ρ ΠΎΡΡ Π±Ρ ΠΎΠ΄ΠΈΠ½ ΡΠ°Π·Π΄Π΅Π»." | |
130 |
|
116 | |||
131 |
#: forms.py:3 |
|
117 | #: forms.py:350 | |
132 | msgid "Theme" |
|
118 | msgid "Theme" | |
133 | msgstr "Π’Π΅ΠΌΠ°" |
|
119 | msgstr "Π’Π΅ΠΌΠ°" | |
134 |
|
120 | |||
135 |
#: forms.py:3 |
|
121 | #: forms.py:351 | |
136 | #| msgid "Image view mode" |
|
|||
137 | msgid "Image view mode" |
|
122 | msgid "Image view mode" | |
138 | msgstr "Π Π΅ΠΆΠΈΠΌ ΠΏΡΠΎΡΠΌΠΎΡΡΠ° ΠΈΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΠΉ" |
|
123 | msgstr "Π Π΅ΠΆΠΈΠΌ ΠΏΡΠΎΡΠΌΠΎΡΡΠ° ΠΈΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΠΉ" | |
139 |
|
124 | |||
140 |
#: forms.py:3 |
|
125 | #: forms.py:352 | |
141 | msgid "User name" |
|
126 | msgid "User name" | |
142 | msgstr "ΠΠΌΡ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Ρ" |
|
127 | msgstr "ΠΠΌΡ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Ρ" | |
143 |
|
128 | |||
144 |
#: forms.py:3 |
|
129 | #: forms.py:353 | |
145 | msgid "Time zone" |
|
130 | msgid "Time zone" | |
146 | msgstr "Π§Π°ΡΠΎΠ²ΠΎΠΉ ΠΏΠΎΡΡ" |
|
131 | msgstr "Π§Π°ΡΠΎΠ²ΠΎΠΉ ΠΏΠΎΡΡ" | |
147 |
|
132 | |||
148 |
#: forms.py:3 |
|
133 | #: forms.py:359 | |
149 | msgid "Inappropriate characters." |
|
134 | msgid "Inappropriate characters." | |
150 | msgstr "ΠΠ΅Π΄ΠΎΠΏΡΡΡΠΈΠΌΡΠ΅ ΡΠΈΠΌΠ²ΠΎΠ»Ρ." |
|
135 | msgstr "ΠΠ΅Π΄ΠΎΠΏΡΡΡΠΈΠΌΡΠ΅ ΡΠΈΠΌΠ²ΠΎΠ»Ρ." | |
151 |
|
136 | |||
@@ -161,11 +146,11 b' msgstr "\xd0\xad\xd1\x82\xd0\xbe\xd0\xb9 \xd1\x81\xd1\x82\xd1\x80\xd0\xb0\xd0\xbd\xd0\xb8\xd1\x86\xd1\x8b \xd0\xbd\xd0\xb5 \xd1\x81\xd1\x83\xd1\x89\xd0\xb5\xd1\x81\xd1\x82\xd0\xb2\xd1\x83\xd0\xb5\xd1\x82"' | |||||
161 | msgid "Related message" |
|
146 | msgid "Related message" | |
162 | msgstr "Π‘Π²ΡΠ·Π°Π½Π½ΠΎΠ΅ ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΠ΅" |
|
147 | msgstr "Π‘Π²ΡΠ·Π°Π½Π½ΠΎΠ΅ ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΠ΅" | |
163 |
|
148 | |||
164 |
#: templates/boards/all_threads.html: |
|
149 | #: templates/boards/all_threads.html:69 | |
165 | msgid "Edit tag" |
|
150 | msgid "Edit tag" | |
166 | msgstr "ΠΠ·ΠΌΠ΅Π½ΠΈΡΡ ΠΌΠ΅ΡΠΊΡ" |
|
151 | msgstr "ΠΠ·ΠΌΠ΅Π½ΠΈΡΡ ΠΌΠ΅ΡΠΊΡ" | |
167 |
|
152 | |||
168 |
#: templates/boards/all_threads.html:7 |
|
153 | #: templates/boards/all_threads.html:75 | |
169 | #, python-format |
|
154 | #, python-format | |
170 | msgid "" |
|
155 | msgid "" | |
171 | "This tag has %(thread_count)s threads (%(active_thread_count)s active) and " |
|
156 | "This tag has %(thread_count)s threads (%(active_thread_count)s active) and " | |
@@ -174,54 +159,61 b' msgstr ""' | |||||
174 | "Π‘ ΡΡΠΎΠΉ ΠΌΠ΅ΡΠΊΠΎΠΉ Π΅ΡΡΡ %(thread_count)s ΡΠ΅ΠΌ (%(active_thread_count)s Π°ΠΊΡΠΈΠ²Π½ΡΡ ) ΠΈ " |
|
159 | "Π‘ ΡΡΠΎΠΉ ΠΌΠ΅ΡΠΊΠΎΠΉ Π΅ΡΡΡ %(thread_count)s ΡΠ΅ΠΌ (%(active_thread_count)s Π°ΠΊΡΠΈΠ²Π½ΡΡ ) ΠΈ " | |
175 | "%(post_count)s ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΠΉ." |
|
160 | "%(post_count)s ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΠΉ." | |
176 |
|
161 | |||
177 |
#: templates/boards/all_threads.html: |
|
162 | #: templates/boards/all_threads.html:77 | |
178 | msgid "Related tags:" |
|
163 | msgid "Related tags:" | |
179 | msgstr "ΠΠΎΡ ΠΎΠΆΠΈΠ΅ ΠΌΠ΅ΡΠΊΠΈ:" |
|
164 | msgstr "ΠΠΎΡ ΠΎΠΆΠΈΠ΅ ΠΌΠ΅ΡΠΊΠΈ:" | |
180 |
|
165 | |||
181 |
#: templates/boards/all_threads.html:9 |
|
166 | #: templates/boards/all_threads.html:90 templates/boards/feed.html:30 | |
182 | #: templates/boards/notifications.html:17 templates/search/search.html:26 |
|
167 | #: templates/boards/notifications.html:17 templates/search/search.html:26 | |
183 | msgid "Previous page" |
|
168 | msgid "Previous page" | |
184 | msgstr "ΠΡΠ΅Π΄ΡΠ΄ΡΡΠ°Ρ ΡΡΡΠ°Π½ΠΈΡΠ°" |
|
169 | msgstr "ΠΡΠ΅Π΄ΡΠ΄ΡΡΠ°Ρ ΡΡΡΠ°Π½ΠΈΡΠ°" | |
185 |
|
170 | |||
186 |
#: templates/boards/all_threads.html:1 |
|
171 | #: templates/boards/all_threads.html:104 | |
187 | #, python-format |
|
172 | #, python-format | |
188 | msgid "Skipped %(count)s replies. Open thread to see all replies." |
|
173 | #| msgid "Skipped %(count)s replies. Open thread to see all replies." | |
189 | msgstr "ΠΡΠΎΠΏΡΡΠ΅Π½ΠΎ %(count)s ΠΎΡΠ²Π΅ΡΠΎΠ². ΠΡΠΊΡΠΎΠΉΡΠ΅ ΡΡΠ΅Π΄, ΡΡΠΎΠ±Ρ ΡΠ²ΠΈΠ΄Π΅ΡΡ Π²ΡΠ΅ ΠΎΡΠ²Π΅ΡΡ." |
|
174 | msgid "Skipped %(count)s reply. Open thread to see all replies." | |
|
175 | msgid_plural "Skipped %(count)s replies. Open thread to see all replies." | |||
|
176 | msgstr[0] "" | |||
|
177 | "ΠΡΠΎΠΏΡΡΠ΅Π½ %(count)s ΠΎΡΠ²Π΅Ρ. ΠΡΠΊΡΠΎΠΉΡΠ΅ ΡΡΠ΅Π΄, ΡΡΠΎΠ±Ρ ΡΠ²ΠΈΠ΄Π΅ΡΡ Π²ΡΠ΅ ΠΎΡΠ²Π΅ΡΡ." | |||
|
178 | msgstr[1] "" | |||
|
179 | "ΠΡΠΎΠΏΡΡΠ΅Π½ΠΎ %(count)s ΠΎΡΠ²Π΅ΡΠ°. ΠΡΠΊΡΠΎΠΉΡΠ΅ ΡΡΠ΅Π΄, ΡΡΠΎΠ±Ρ ΡΠ²ΠΈΠ΄Π΅ΡΡ Π²ΡΠ΅ ΠΎΡΠ²Π΅ΡΡ." | |||
|
180 | msgstr[2] "" | |||
|
181 | "ΠΡΠΎΠΏΡΡΠ΅Π½ΠΎ %(count)s ΠΎΡΠ²Π΅ΡΠΎΠ². ΠΡΠΊΡΠΎΠΉΡΠ΅ ΡΡΠ΅Π΄, ΡΡΠΎΠ±Ρ ΡΠ²ΠΈΠ΄Π΅ΡΡ Π²ΡΠ΅ ΠΎΡΠ²Π΅ΡΡ." | |||
190 |
|
182 | |||
191 |
#: templates/boards/all_threads.html:12 |
|
183 | #: templates/boards/all_threads.html:122 templates/boards/feed.html:40 | |
192 | #: templates/boards/notifications.html:27 templates/search/search.html:37 |
|
184 | #: templates/boards/notifications.html:27 templates/search/search.html:37 | |
193 | msgid "Next page" |
|
185 | msgid "Next page" | |
194 | msgstr "Π‘Π»Π΅Π΄ΡΡΡΠ°Ρ ΡΡΡΠ°Π½ΠΈΡΠ°" |
|
186 | msgstr "Π‘Π»Π΅Π΄ΡΡΡΠ°Ρ ΡΡΡΠ°Π½ΠΈΡΠ°" | |
195 |
|
187 | |||
196 |
#: templates/boards/all_threads.html:1 |
|
188 | #: templates/boards/all_threads.html:127 | |
197 | msgid "No threads exist. Create the first one!" |
|
189 | msgid "No threads exist. Create the first one!" | |
198 | msgstr "ΠΠ΅Ρ ΡΠ΅ΠΌ. Π‘ΠΎΠ·Π΄Π°ΠΉΡΠ΅ ΠΏΠ΅ΡΠ²ΡΡ!" |
|
190 | msgstr "ΠΠ΅Ρ ΡΠ΅ΠΌ. Π‘ΠΎΠ·Π΄Π°ΠΉΡΠ΅ ΠΏΠ΅ΡΠ²ΡΡ!" | |
199 |
|
191 | |||
200 |
#: templates/boards/all_threads.html:13 |
|
192 | #: templates/boards/all_threads.html:133 | |
201 | msgid "Create new thread" |
|
193 | msgid "Create new thread" | |
202 | msgstr "Π‘ΠΎΠ·Π΄Π°ΡΡ Π½ΠΎΠ²ΡΡ ΡΠ΅ΠΌΡ" |
|
194 | msgstr "Π‘ΠΎΠ·Π΄Π°ΡΡ Π½ΠΎΠ²ΡΡ ΡΠ΅ΠΌΡ" | |
203 |
|
195 | |||
204 |
#: templates/boards/all_threads.html:1 |
|
196 | #: templates/boards/all_threads.html:138 templates/boards/preview.html:16 | |
205 |
#: templates/boards/thread_normal.html: |
|
197 | #: templates/boards/thread_normal.html:51 | |
206 | msgid "Post" |
|
198 | msgid "Post" | |
207 | msgstr "ΠΡΠΏΡΠ°Π²ΠΈΡΡ" |
|
199 | msgstr "ΠΡΠΏΡΠ°Π²ΠΈΡΡ" | |
208 |
|
200 | |||
209 |
#: templates/boards/all_threads.html:1 |
|
201 | #: templates/boards/all_threads.html:139 templates/boards/preview.html:6 | |
|
202 | #: templates/boards/staticpages/help.html:21 | |||
|
203 | #: templates/boards/thread_normal.html:52 | |||
|
204 | msgid "Preview" | |||
|
205 | msgstr "ΠΡΠ΅Π΄ΠΏΡΠΎΡΠΌΠΎΡΡ" | |||
|
206 | ||||
|
207 | #: templates/boards/all_threads.html:144 | |||
210 | msgid "Tags must be delimited by spaces. Text or image is required." |
|
208 | msgid "Tags must be delimited by spaces. Text or image is required." | |
211 | msgstr "" |
|
209 | msgstr "" | |
212 | "ΠΠ΅ΡΠΊΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ Π±ΡΡΡ ΡΠ°Π·Π΄Π΅Π»Π΅Π½Ρ ΠΏΡΠΎΠ±Π΅Π»Π°ΠΌΠΈ. Π’Π΅ΠΊΡΡ ΠΈΠ»ΠΈ ΠΈΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΠ΅ ΠΎΠ±ΡΠ·Π°ΡΠ΅Π»ΡΠ½Ρ." |
|
210 | "ΠΠ΅ΡΠΊΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ Π±ΡΡΡ ΡΠ°Π·Π΄Π΅Π»Π΅Π½Ρ ΠΏΡΠΎΠ±Π΅Π»Π°ΠΌΠΈ. Π’Π΅ΠΊΡΡ ΠΈΠ»ΠΈ ΠΈΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΠ΅ ΠΎΠ±ΡΠ·Π°ΡΠ΅Π»ΡΠ½Ρ." | |
213 |
|
211 | |||
214 |
#: templates/boards/all_threads.html:1 |
|
212 | #: templates/boards/all_threads.html:147 templates/boards/thread_normal.html:58 | |
215 | #: templates/boards/staticpages/help.html:21 |
|
|||
216 | #: templates/boards/thread_normal.html:42 |
|
|||
217 | msgid "Preview" |
|
|||
218 | msgstr "ΠΡΠ΅Π΄ΠΏΡΠΎΡΠΌΠΎΡΡ" |
|
|||
219 |
|
||||
220 | #: templates/boards/all_threads.html:153 templates/boards/thread_normal.html:45 |
|
|||
221 | msgid "Text syntax" |
|
213 | msgid "Text syntax" | |
222 | msgstr "Π‘ΠΈΠ½ΡΠ°ΠΊΡΠΈΡ ΡΠ΅ΠΊΡΡΠ°" |
|
214 | msgstr "Π‘ΠΈΠ½ΡΠ°ΠΊΡΠΈΡ ΡΠ΅ΠΊΡΡΠ°" | |
223 |
|
215 | |||
224 |
#: templates/boards/all_threads.html:16 |
|
216 | #: templates/boards/all_threads.html:161 templates/boards/feed.html:53 | |
225 | msgid "Pages:" |
|
217 | msgid "Pages:" | |
226 | msgstr "Π‘ΡΡΠ°Π½ΠΈΡΡ: " |
|
218 | msgstr "Π‘ΡΡΠ°Π½ΠΈΡΡ: " | |
227 |
|
219 | |||
@@ -286,16 +278,16 b' msgstr "\xd0\xa3\xd0\xb2\xd0\xb5\xd0\xb4\xd0\xbe\xd0\xbc\xd0\xbb\xd0\xb5\xd0\xbd\xd0\xb8\xd1\x8f"' | |||||
286 | msgid "Settings" |
|
278 | msgid "Settings" | |
287 | msgstr "ΠΠ°ΡΡΡΠΎΠΉΠΊΠΈ" |
|
279 | msgstr "ΠΠ°ΡΡΡΠΎΠΉΠΊΠΈ" | |
288 |
|
280 | |||
289 |
#: templates/boards/base.html: |
|
281 | #: templates/boards/base.html:79 | |
290 | msgid "Admin" |
|
282 | msgid "Admin" | |
291 | msgstr "ΠΠ΄ΠΌΠΈΠ½ΠΈΡΡΡΠΈΡΠΎΠ²Π°Π½ΠΈΠ΅" |
|
283 | msgstr "ΠΠ΄ΠΌΠΈΠ½ΠΈΡΡΡΠΈΡΠΎΠ²Π°Π½ΠΈΠ΅" | |
292 |
|
284 | |||
293 |
#: templates/boards/base.html: |
|
285 | #: templates/boards/base.html:81 | |
294 | #, python-format |
|
286 | #, python-format | |
295 | msgid "Speed: %(ppd)s posts per day" |
|
287 | msgid "Speed: %(ppd)s posts per day" | |
296 | msgstr "Π‘ΠΊΠΎΡΠΎΡΡΡ: %(ppd)s ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΠΉ Π² Π΄Π΅Π½Ρ" |
|
288 | msgstr "Π‘ΠΊΠΎΡΠΎΡΡΡ: %(ppd)s ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΠΉ Π² Π΄Π΅Π½Ρ" | |
297 |
|
289 | |||
298 |
#: templates/boards/base.html: |
|
290 | #: templates/boards/base.html:83 | |
299 | msgid "Up" |
|
291 | msgid "Up" | |
300 | msgstr "ΠΠ²Π΅ΡΡ " |
|
292 | msgstr "ΠΠ²Π΅ΡΡ " | |
301 |
|
293 | |||
@@ -303,37 +295,52 b' msgstr "\xd0\x92\xd0\xb2\xd0\xb5\xd1\x80\xd1\x85"' | |||||
303 | msgid "No posts exist. Create the first one!" |
|
295 | msgid "No posts exist. Create the first one!" | |
304 | msgstr "ΠΠ΅Ρ ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΠΉ. Π‘ΠΎΠ·Π΄Π°ΠΉΡΠ΅ ΠΏΠ΅ΡΠ²ΠΎΠ΅!" |
|
296 | msgstr "ΠΠ΅Ρ ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΠΉ. Π‘ΠΎΠ·Π΄Π°ΠΉΡΠ΅ ΠΏΠ΅ΡΠ²ΠΎΠ΅!" | |
305 |
|
297 | |||
306 |
#: templates/boards/post.html:3 |
|
298 | #: templates/boards/post.html:32 | |
307 | msgid "Open" |
|
299 | msgid "Open" | |
308 | msgstr "ΠΡΠΊΡΡΡΡ" |
|
300 | msgstr "ΠΡΠΊΡΡΡΡ" | |
309 |
|
301 | |||
310 |
#: templates/boards/post.html:3 |
|
302 | #: templates/boards/post.html:34 templates/boards/post.html.py:45 | |
311 | msgid "Reply" |
|
303 | msgid "Reply" | |
312 | msgstr "ΠΡΠ²Π΅ΡΠΈΡΡ" |
|
304 | msgstr "ΠΡΠ²Π΅ΡΠΈΡΡ" | |
313 |
|
305 | |||
314 |
#: templates/boards/post.html: |
|
306 | #: templates/boards/post.html:40 | |
315 | msgid " in " |
|
307 | msgid " in " | |
316 | msgstr " Π² " |
|
308 | msgstr " Π² " | |
317 |
|
309 | |||
318 |
#: templates/boards/post.html: |
|
310 | #: templates/boards/post.html:50 | |
319 | msgid "Edit" |
|
311 | msgid "Edit" | |
320 | msgstr "ΠΠ·ΠΌΠ΅Π½ΠΈΡΡ" |
|
312 | msgstr "ΠΠ·ΠΌΠ΅Π½ΠΈΡΡ" | |
321 |
|
313 | |||
322 |
#: templates/boards/post.html:5 |
|
314 | #: templates/boards/post.html:52 | |
323 | msgid "Edit thread" |
|
315 | msgid "Edit thread" | |
324 | msgstr "ΠΠ·ΠΌΠ΅Π½ΠΈΡΡ ΡΠ΅ΠΌΡ" |
|
316 | msgstr "ΠΠ·ΠΌΠ΅Π½ΠΈΡΡ ΡΠ΅ΠΌΡ" | |
325 |
|
317 | |||
326 |
#: templates/boards/post.html:9 |
|
318 | #: templates/boards/post.html:94 | |
327 | msgid "Replies" |
|
319 | msgid "Replies" | |
328 | msgstr "ΠΡΠ²Π΅ΡΡ" |
|
320 | msgstr "ΠΡΠ²Π΅ΡΡ" | |
329 |
|
321 | |||
330 |
#: templates/boards/post.html:10 |
|
322 | #: templates/boards/post.html:105 | |
331 | msgid "messages" |
|
323 | #, python-format | |
332 | msgstr "ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΠΉ" |
|
324 | msgid "%(count)s message" | |
|
325 | msgid_plural "%(count)s messages" | |||
|
326 | msgstr[0] "%(count)s ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΠ΅" | |||
|
327 | msgstr[1] "%(count)s ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΡ" | |||
|
328 | msgstr[2] "%(count)s ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΠΉ" | |||
333 |
|
329 | |||
334 | #: templates/boards/post.html:110 templates/boards/thread.html:35 |
|
330 | #, python-format | |
335 | msgid "images" |
|
331 | msgid "Please wait %(delay)d second before sending message" | |
336 | msgstr "ΠΈΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΠΉ" |
|
332 | msgid_plural "Please wait %(delay)d seconds before sending message" | |
|
333 | msgstr[0] "ΠΠΎΠΆΠ°Π»ΡΠΉΡΡΠ° ΠΏΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡΠ΅ %(delay)d ΡΠ΅ΠΊΡΠ½Π΄Ρ ΠΏΠ΅ΡΠ΅Π΄ ΠΎΡΠΏΡΠ°Π²ΠΊΠΎΠΉ ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΡ" | |||
|
334 | msgstr[1] "ΠΠΎΠΆΠ°Π»ΡΠΉΡΡΠ° ΠΏΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡΠ΅ %(delay)d ΡΠ΅ΠΊΡΠ½Π΄Ρ ΠΏΠ΅ΡΠ΅Π΄ ΠΎΡΠΏΡΠ°Π²ΠΊΠΎΠΉ ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΡ" | |||
|
335 | msgstr[2] "ΠΠΎΠΆΠ°Π»ΡΠΉΡΡΠ° ΠΏΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡΠ΅ %(delay)d ΡΠ΅ΠΊΡΠ½Π΄ ΠΏΠ΅ΡΠ΅Π΄ ΠΎΡΠΏΡΠ°Π²ΠΊΠΎΠΉ ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΡ" | |||
|
336 | ||||
|
337 | #: templates/boards/post.html:106 | |||
|
338 | #, python-format | |||
|
339 | msgid "%(count)s image" | |||
|
340 | msgid_plural "%(count)s images" | |||
|
341 | msgstr[0] "%(count)s ΠΈΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΠ΅" | |||
|
342 | msgstr[1] "%(count)s ΠΈΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΡ" | |||
|
343 | msgstr[2] "%(count)s ΠΈΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΠΉ" | |||
337 |
|
344 | |||
338 | #: templates/boards/rss/post.html:5 |
|
345 | #: templates/boards/rss/post.html:5 | |
339 | msgid "Post image" |
|
346 | msgid "Post image" | |
@@ -347,19 +354,11 b' msgstr "\xd0\x92\xd1\x8b \xd0\xbc\xd0\xbe\xd0\xb4\xd0\xb5\xd1\x80\xd0\xb0\xd1\x82\xd0\xbe\xd1\x80."' | |||||
347 | msgid "Hidden tags:" |
|
354 | msgid "Hidden tags:" | |
348 | msgstr "Π‘ΠΊΡΡΡΡΠ΅ ΠΌΠ΅ΡΠΊΠΈ:" |
|
355 | msgstr "Π‘ΠΊΡΡΡΡΠ΅ ΠΌΠ΅ΡΠΊΠΈ:" | |
349 |
|
356 | |||
350 |
#: templates/boards/settings.html:2 |
|
357 | #: templates/boards/settings.html:25 | |
351 | msgid "No hidden tags." |
|
358 | msgid "No hidden tags." | |
352 | msgstr "ΠΠ΅Ρ ΡΠΊΡΡΡΡΡ ΠΌΠ΅ΡΠΎΠΊ." |
|
359 | msgstr "ΠΠ΅Ρ ΡΠΊΡΡΡΡΡ ΠΌΠ΅ΡΠΎΠΊ." | |
353 |
|
360 | |||
354 |
#: templates/boards/settings.html: |
|
361 | #: templates/boards/settings.html:34 | |
355 | msgid "Tripcode:" |
|
|||
356 | msgstr "Π’ΡΠΈΠΏΠΊΠΎΠ΄:" |
|
|||
357 |
|
||||
358 | #: templates/boards/settings.html:29 |
|
|||
359 | msgid "reset" |
|
|||
360 | msgstr "ΡΠ±ΡΠΎΡΠΈΡΡ" |
|
|||
361 |
|
||||
362 | #: templates/boards/settings.html:37 |
|
|||
363 | msgid "Save" |
|
362 | msgid "Save" | |
364 | msgstr "Π‘ΠΎΡ ΡΠ°Π½ΠΈΡΡ" |
|
363 | msgstr "Π‘ΠΎΡ ΡΠ°Π½ΠΈΡΡ" | |
365 |
|
364 | |||
@@ -434,6 +433,20 b' msgid "Tree"' | |||||
434 | msgstr "ΠΠ΅ΡΠ΅Π²ΠΎ" |
|
433 | msgstr "ΠΠ΅ΡΠ΅Π²ΠΎ" | |
435 |
|
434 | |||
436 | #: templates/boards/thread.html:36 |
|
435 | #: templates/boards/thread.html:36 | |
|
436 | msgid "message" | |||
|
437 | msgid_plural "messages" | |||
|
438 | msgstr[0] "ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΠ΅" | |||
|
439 | msgstr[1] "ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΡ" | |||
|
440 | msgstr[2] "ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΠΉ" | |||
|
441 | ||||
|
442 | #: templates/boards/thread.html:39 | |||
|
443 | msgid "image" | |||
|
444 | msgid_plural "images" | |||
|
445 | msgstr[0] "ΠΈΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΠ΅" | |||
|
446 | msgstr[1] "ΠΈΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΡ" | |||
|
447 | msgstr[2] "ΠΈΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΠΉ" | |||
|
448 | ||||
|
449 | #: templates/boards/thread.html:41 | |||
437 | msgid "Last update: " |
|
450 | msgid "Last update: " | |
438 | msgstr "ΠΠΎΡΠ»Π΅Π΄Π½Π΅Π΅ ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅: " |
|
451 | msgstr "ΠΠΎΡΠ»Π΅Π΄Π½Π΅Π΅ ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅: " | |
439 |
|
452 | |||
@@ -441,22 +454,39 b' msgstr "\xd0\x9f\xd0\xbe\xd1\x81\xd0\xbb\xd0\xb5\xd0\xb4\xd0\xbd\xd0\xb5\xd0\xb5 \xd0\xbe\xd0\xb1\xd0\xbd\xd0\xbe\xd0\xb2\xd0\xbb\xd0\xb5\xd0\xbd\xd0\xb8\xd0\xb5: "' | |||||
441 | msgid "No images." |
|
454 | msgid "No images." | |
442 | msgstr "ΠΠ΅Ρ ΠΈΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΠΉ." |
|
455 | msgstr "ΠΠ΅Ρ ΠΈΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΠΉ." | |
443 |
|
456 | |||
444 |
#: templates/boards/thread_normal.html: |
|
457 | #: templates/boards/thread_normal.html:30 | |
445 | msgid "posts to bumplimit" |
|
458 | msgid "posts to bumplimit" | |
446 | msgstr "ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΠΉ Π΄ΠΎ Π±Π°ΠΌΠΏΠ»ΠΈΠΌΠΈΡΠ°" |
|
459 | msgstr "ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΠΉ Π΄ΠΎ Π±Π°ΠΌΠΏΠ»ΠΈΠΌΠΈΡΠ°" | |
447 |
|
460 | |||
448 |
#: templates/boards/thread_normal.html: |
|
461 | #: templates/boards/thread_normal.html:44 | |
449 | msgid "Reply to thread" |
|
462 | msgid "Reply to thread" | |
450 | msgstr "ΠΡΠ²Π΅ΡΠΈΡΡ Π² ΡΠ΅ΠΌΡ" |
|
463 | msgstr "ΠΡΠ²Π΅ΡΠΈΡΡ Π² ΡΠ΅ΠΌΡ" | |
451 |
|
464 | |||
452 |
#: templates/boards/thread_normal.html: |
|
465 | #: templates/boards/thread_normal.html:44 | |
453 | msgid "to message " |
|
466 | msgid "to message " | |
454 | msgstr "Π½Π° ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΠ΅" |
|
467 | msgstr "Π½Π° ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΠ΅" | |
455 |
|
468 | |||
456 |
#: templates/boards/thread_normal.html: |
|
469 | #: templates/boards/thread_normal.html:59 | |
457 | msgid "Close form" |
|
470 | msgid "Close form" | |
458 | msgstr "ΠΠ°ΠΊΡΡΡΡ ΡΠΎΡΠΌΡ" |
|
471 | msgstr "ΠΠ°ΠΊΡΡΡΡ ΡΠΎΡΠΌΡ" | |
459 |
|
472 | |||
460 | #: templates/search/search.html:17 |
|
473 | #: templates/search/search.html:17 | |
461 | msgid "Ok" |
|
474 | msgid "Ok" | |
462 | msgstr "ΠΠΊ" |
|
475 | msgstr "ΠΠΊ" | |
|
476 | ||||
|
477 | #: utils.py:102 | |||
|
478 | #, python-format | |||
|
479 | msgid "File must be less than %s bytes" | |||
|
480 | msgstr "Π€Π°ΠΉΠ» Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±ΡΡΡ ΠΌΠ΅Π½Π΅Π΅ %s Π±Π°ΠΉΡ" | |||
|
481 | ||||
|
482 | msgid "favorites" | |||
|
483 | msgstr "ΠΈΠ·Π±ΡΠ°Π½Π½ΠΎΠ΅" | |||
|
484 | ||||
|
485 | msgid "Loading..." | |||
|
486 | msgstr "ΠΠ°Π³ΡΡΠ·ΠΊΠ°..." | |||
|
487 | ||||
|
488 | msgid "Category:" | |||
|
489 | msgstr "ΠΠ°ΡΠ΅Π³ΠΎΡΠΈΡ:" | |||
|
490 | ||||
|
491 | msgid "Subcategories:" | |||
|
492 | msgstr "ΠΠΎΠ΄ΠΊΠ°ΡΠ΅Π³ΠΎΡΠΈΠΈ:" |
1 | NO CONTENT: modified file, binary diff hidden |
|
NO CONTENT: modified file, binary diff hidden |
@@ -8,7 +8,7 b' msgid ""' | |||||
8 | msgstr "" |
|
8 | msgstr "" | |
9 | "Project-Id-Version: PACKAGE VERSION\n" |
|
9 | "Project-Id-Version: PACKAGE VERSION\n" | |
10 | "Report-Msgid-Bugs-To: \n" |
|
10 | "Report-Msgid-Bugs-To: \n" | |
11 |
"POT-Creation-Date: 201 |
|
11 | "POT-Creation-Date: 2015-09-04 18:47+0300\n" | |
12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" |
|
12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" | |
13 | "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" |
|
13 | "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" | |
14 | "Language-Team: LANGUAGE <LL@li.org>\n" |
|
14 | "Language-Team: LANGUAGE <LL@li.org>\n" | |
@@ -19,14 +19,37 b' msgstr ""' | |||||
19 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" |
|
19 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" | |
20 | "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" |
|
20 | "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" | |
21 |
|
21 | |||
22 |
#: static/js/ |
|
22 | #: static/js/3party/jquery-ui.min.js:8 | |
|
23 | msgid "'" | |||
|
24 | msgstr "" | |||
|
25 | ||||
|
26 | #: static/js/refpopup.js:72 | |||
23 | msgid "Loading..." |
|
27 | msgid "Loading..." | |
24 | msgstr "ΠΠ°Π³ΡΡΠ·ΠΊΠ°..." |
|
28 | msgstr "ΠΠ°Π³ΡΡΠ·ΠΊΠ°..." | |
25 |
|
29 | |||
26 |
#: static/js/refpopup.js: |
|
30 | #: static/js/refpopup.js:91 | |
27 | msgid "Post not found" |
|
31 | msgid "Post not found" | |
28 | msgstr "Π‘ΠΎΠΎΠ±ΡΠ΅Π½ΠΈΠ΅ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½ΠΎ" |
|
32 | msgstr "Π‘ΠΎΠΎΠ±ΡΠ΅Π½ΠΈΠ΅ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½ΠΎ" | |
29 |
|
33 | |||
30 |
#: static/js/thread_update.js:2 |
|
34 | #: static/js/thread_update.js:261 | |
|
35 | msgid "message" | |||
|
36 | msgid_plural "messages" | |||
|
37 | msgstr[0] "ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΠ΅" | |||
|
38 | msgstr[1] "ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΡ" | |||
|
39 | msgstr[2] "ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΠΉ" | |||
|
40 | ||||
|
41 | #: static/js/thread_update.js:262 | |||
|
42 | msgid "image" | |||
|
43 | msgid_plural "images" | |||
|
44 | msgstr[0] "ΠΈΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΠ΅" | |||
|
45 | msgstr[1] "ΠΈΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΡ" | |||
|
46 | msgstr[2] "ΠΈΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΠΉ" | |||
|
47 | ||||
|
48 | #: static/js/thread_update.js:445 | |||
31 | msgid "Sending message..." |
|
49 | msgid "Sending message..." | |
32 | msgstr "ΠΡΠΏΡΠ°Π²ΠΊΠ° ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΡ..." No newline at end of file |
|
50 | msgstr "ΠΡΠΏΡΠ°Π²ΠΊΠ° ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΡ..." | |
|
51 | ||||
|
52 | #: static/js/thread_update.js:449 | |||
|
53 | msgid "Server error!" | |||
|
54 | msgstr "ΠΡΠΈΠ±ΠΊΠ° ΡΠ΅ΡΠ²Π΅ΡΠ°!" | |||
|
55 |
@@ -6,10 +6,17 b' from django.db import migrations' | |||||
6 |
|
6 | |||
7 | class Migration(migrations.Migration): |
|
7 | class Migration(migrations.Migration): | |
8 |
|
8 | |||
9 |
def re |
|
9 | def rebuild_refmap(apps, schema_editor): | |
10 | Post = apps.get_model('boards', 'Post') |
|
10 | Post = apps.get_model('boards', 'Post') | |
11 | for post in Post.objects.all(): |
|
11 | for post in Post.objects.all(): | |
12 |
|
|
12 | refposts = list() | |
|
13 | for refpost in post.referenced_posts.all(): | |||
|
14 | result = '<a href="{}">>>{}</a>'.format(refpost.get_absolute_url(), | |||
|
15 | self.id) | |||
|
16 | if refpost.is_opening(): | |||
|
17 | result = '<b>{}</b>'.format(result) | |||
|
18 | refposts += result | |||
|
19 | post.refmap = ', '.join(refposts) | |||
13 | post.save(update_fields=['refmap']) |
|
20 | post.save(update_fields=['refmap']) | |
14 |
|
21 | |||
15 | dependencies = [ |
|
22 | dependencies = [ | |
@@ -17,5 +24,5 b' class Migration(migrations.Migration):' | |||||
17 | ] |
|
24 | ] | |
18 |
|
25 | |||
19 | operations = [ |
|
26 | operations = [ | |
20 |
migrations.RunPython(re |
|
27 | migrations.RunPython(rebuild_refmap), | |
21 | ] |
|
28 | ] |
@@ -6,13 +6,21 b" FILE_STUB_IMAGE = 'images/file.png'" | |||||
6 | FILE_TYPES_VIDEO = ( |
|
6 | FILE_TYPES_VIDEO = ( | |
7 | 'webm', |
|
7 | 'webm', | |
8 | 'mp4', |
|
8 | 'mp4', | |
|
9 | 'mpeg', | |||
9 | ) |
|
10 | ) | |
10 | FILE_TYPE_SVG = 'svg' |
|
11 | FILE_TYPE_SVG = 'svg' | |
11 | FILE_TYPES_AUDIO = ( |
|
12 | FILE_TYPES_AUDIO = ( | |
12 | 'ogg', |
|
13 | 'ogg', | |
13 | 'mp3', |
|
14 | 'mp3', | |
|
15 | 'opus', | |||
14 | ) |
|
16 | ) | |
15 |
|
17 | |||
|
18 | PLAIN_FILE_FORMATS = { | |||
|
19 | 'pdf': 'pdf', | |||
|
20 | 'djvu': 'djvu', | |||
|
21 | 'txt': 'txt', | |||
|
22 | } | |||
|
23 | ||||
16 |
|
24 | |||
17 | def get_viewers(): |
|
25 | def get_viewers(): | |
18 | return AbstractViewer.__subclasses__() |
|
26 | return AbstractViewer.__subclasses__() | |
@@ -35,9 +43,15 b' class AbstractViewer:' | |||||
35 | self.file_type, filesizeformat(self.file.size)) |
|
43 | self.file_type, filesizeformat(self.file.size)) | |
36 |
|
44 | |||
37 | def get_format_view(self): |
|
45 | def get_format_view(self): | |
|
46 | if self.file_type in PLAIN_FILE_FORMATS: | |||
|
47 | image = 'images/fileformats/{}.png'.format( | |||
|
48 | PLAIN_FILE_FORMATS[self.file_type]) | |||
|
49 | else: | |||
|
50 | image = FILE_STUB_IMAGE | |||
|
51 | ||||
38 | return '<a href="{}">'\ |
|
52 | return '<a href="{}">'\ | |
39 | '<img src="{}" width="200" height="150"/>'\ |
|
53 | '<img src="{}" width="200" height="150"/>'\ | |
40 |
'</a>'.format(self.file.url, static( |
|
54 | '</a>'.format(self.file.url, static(image)) | |
41 |
|
55 | |||
42 |
|
56 | |||
43 | class VideoViewer(AbstractViewer): |
|
57 | class VideoViewer(AbstractViewer): |
@@ -1,5 +1,3 b'' | |||||
1 | from datetime import datetime, timedelta, date |
|
|||
2 | from datetime import time as dtime |
|
|||
3 |
|
|
1 | import logging | |
4 | import re |
|
2 | import re | |
5 | import uuid |
|
3 | import uuid | |
@@ -11,21 +9,15 b' from django.db.models import TextField, ' | |||||
11 | from django.template.loader import render_to_string |
|
9 | from django.template.loader import render_to_string | |
12 | from django.utils import timezone |
|
10 | from django.utils import timezone | |
13 |
|
11 | |||
|
12 | from boards import settings | |||
14 | from boards.abstracts.tripcode import Tripcode |
|
13 | from boards.abstracts.tripcode import Tripcode | |
15 | from boards.mdx_neboard import Parser |
|
14 | from boards.mdx_neboard import Parser | |
16 | from boards.models import KeyPair, GlobalId |
|
15 | from boards.models import PostImage, Attachment, KeyPair, GlobalId | |
17 | from boards import settings |
|
|||
18 | from boards.models import PostImage, Attachment |
|
|||
19 | from boards.models.base import Viewable |
|
16 | from boards.models.base import Viewable | |
20 | from boards.models.post.export import get_exporter, DIFF_TYPE_JSON |
|
17 | from boards.models.post.export import get_exporter, DIFF_TYPE_JSON | |
21 | from boards.models.post.manager import PostManager |
|
18 | from boards.models.post.manager import PostManager | |
22 | from boards.models.user import Notification |
|
19 | from boards.models.user import Notification | |
23 |
|
20 | |||
24 | WS_NOTIFICATION_TYPE_NEW_POST = 'new_post' |
|
|||
25 | WS_NOTIFICATION_TYPE = 'notification_type' |
|
|||
26 |
|
||||
27 | WS_CHANNEL_THREAD = "thread:" |
|
|||
28 |
|
||||
29 | APP_LABEL_BOARDS = 'boards' |
|
21 | APP_LABEL_BOARDS = 'boards' | |
30 |
|
22 | |||
31 | BAN_REASON_AUTO = 'Auto' |
|
23 | BAN_REASON_AUTO = 'Auto' | |
@@ -92,7 +84,8 b' class Post(models.Model, Viewable):' | |||||
92 | blank=True, related_name='refposts', |
|
84 | blank=True, related_name='refposts', | |
93 | db_index=True) |
|
85 | db_index=True) | |
94 | refmap = models.TextField(null=True, blank=True) |
|
86 | refmap = models.TextField(null=True, blank=True) | |
95 |
threads = models.ManyToManyField('Thread', db_index=True |
|
87 | threads = models.ManyToManyField('Thread', db_index=True, | |
|
88 | related_name='multi_replies') | |||
96 | thread = models.ForeignKey('Thread', db_index=True, related_name='pt+') |
|
89 | thread = models.ForeignKey('Thread', db_index=True, related_name='pt+') | |
97 |
|
90 | |||
98 | url = models.TextField() |
|
91 | url = models.TextField() | |
@@ -103,9 +96,10 b' class Post(models.Model, Viewable):' | |||||
103 | global_id = models.OneToOneField('GlobalId', null=True, blank=True) |
|
96 | global_id = models.OneToOneField('GlobalId', null=True, blank=True) | |
104 |
|
97 | |||
105 | tripcode = models.CharField(max_length=50, null=True) |
|
98 | tripcode = models.CharField(max_length=50, null=True) | |
|
99 | opening = models.BooleanField() | |||
106 |
|
100 | |||
107 | def __str__(self): |
|
101 | def __str__(self): | |
108 | return 'P#{}/{}'.format(self.id, self.title) |
|
102 | return 'P#{}/{}'.format(self.id, self.get_title()) | |
109 |
|
103 | |||
110 | def get_referenced_posts(self): |
|
104 | def get_referenced_posts(self): | |
111 | threads = self.get_threads().all() |
|
105 | threads = self.get_threads().all() | |
@@ -113,13 +107,12 b' class Post(models.Model, Viewable):' | |||||
113 | .order_by('pub_time').distinct().all() |
|
107 | .order_by('pub_time').distinct().all() | |
114 |
|
108 | |||
115 | def get_title(self) -> str: |
|
109 | def get_title(self) -> str: | |
116 | """ |
|
110 | return self.title | |
117 | Gets original post title or part of its text. |
|
|||
118 | """ |
|
|||
119 |
|
111 | |||
120 | title = self.title |
|
112 | def get_title_or_text(self): | |
|
113 | title = self.get_title() | |||
121 | if not title: |
|
114 | if not title: | |
122 | title = self.get_text() |
|
115 | title = truncatewords(striptags(self.get_text()), TITLE_MAX_WORDS) | |
123 |
|
116 | |||
124 | return title |
|
117 | return title | |
125 |
|
118 | |||
@@ -142,7 +135,7 b' class Post(models.Model, Viewable):' | |||||
142 | Checks if this is an opening post or just a reply. |
|
135 | Checks if this is an opening post or just a reply. | |
143 | """ |
|
136 | """ | |
144 |
|
137 | |||
145 | return self.get_thread().get_opening_post_id() == self.id |
|
138 | return self.opening | |
146 |
|
139 | |||
147 | def get_absolute_url(self): |
|
140 | def get_absolute_url(self): | |
148 | if self.url: |
|
141 | if self.url: | |
@@ -172,12 +165,6 b' class Post(models.Model, Viewable):' | |||||
172 | """ |
|
165 | """ | |
173 |
|
166 | |||
174 | thread = self.get_thread() |
|
167 | thread = self.get_thread() | |
175 | is_opening = kwargs.get(PARAMETER_IS_OPENING, self.is_opening()) |
|
|||
176 |
|
||||
177 | if is_opening: |
|
|||
178 | opening_post_id = self.id |
|
|||
179 | else: |
|
|||
180 | opening_post_id = thread.get_opening_post_id() |
|
|||
181 |
|
168 | |||
182 | css_class = 'post' |
|
169 | css_class = 'post' | |
183 | if thread.archived: |
|
170 | if thread.archived: | |
@@ -192,10 +179,9 b' class Post(models.Model, Viewable):' | |||||
192 |
|
179 | |||
193 | params.update({ |
|
180 | params.update({ | |
194 | PARAMETER_POST: self, |
|
181 | PARAMETER_POST: self, | |
195 | PARAMETER_IS_OPENING: is_opening, |
|
182 | PARAMETER_IS_OPENING: self.is_opening(), | |
196 | PARAMETER_THREAD: thread, |
|
183 | PARAMETER_THREAD: thread, | |
197 | PARAMETER_CSS_CLASS: css_class, |
|
184 | PARAMETER_CSS_CLASS: css_class, | |
198 | PARAMETER_OP_ID: opening_post_id, |
|
|||
199 | }) |
|
185 | }) | |
200 |
|
186 | |||
201 | return render_to_string('boards/post.html', params) |
|
187 | return render_to_string('boards/post.html', params) | |
@@ -336,7 +322,7 b' class Post(models.Model, Viewable):' | |||||
336 | for thread in self.get_threads().all(): |
|
322 | for thread in self.get_threads().all(): | |
337 | thread.last_edit_time = self.last_edit_time |
|
323 | thread.last_edit_time = self.last_edit_time | |
338 |
|
324 | |||
339 | thread.save(update_fields=['last_edit_time']) |
|
325 | thread.save(update_fields=['last_edit_time', 'bumpable']) | |
340 |
|
326 | |||
341 | super().save(force_insert, force_update, using, update_fields) |
|
327 | super().save(force_insert, force_update, using, update_fields) | |
342 |
|
328 |
@@ -6,6 +6,7 b' from django.utils import timezone' | |||||
6 | from boards import utils |
|
6 | from boards import utils | |
7 | from boards.mdx_neboard import Parser |
|
7 | from boards.mdx_neboard import Parser | |
8 | from boards.models import PostImage, Attachment |
|
8 | from boards.models import PostImage, Attachment | |
|
9 | from boards.models.user import Ban | |||
9 | import boards.models |
|
10 | import boards.models | |
10 |
|
11 | |||
11 | __author__ = 'vurdalak' |
|
12 | __author__ = 'vurdalak' | |
@@ -31,7 +32,7 b' class PostManager(models.Manager):' | |||||
31 | Creates new post |
|
32 | Creates new post | |
32 | """ |
|
33 | """ | |
33 |
|
34 | |||
34 |
is_banned = |
|
35 | is_banned = Ban.objects.filter(ip=ip).exists() | |
35 |
|
36 | |||
36 | # TODO Raise specific exception and catch it in the views |
|
37 | # TODO Raise specific exception and catch it in the views | |
37 | if is_banned: |
|
38 | if is_banned: | |
@@ -59,7 +60,8 b' class PostManager(models.Manager):' | |||||
59 | poster_ip=ip, |
|
60 | poster_ip=ip, | |
60 | thread=thread, |
|
61 | thread=thread, | |
61 | last_edit_time=posting_time, |
|
62 | last_edit_time=posting_time, | |
62 |
tripcode=tripcode |
|
63 | tripcode=tripcode, | |
|
64 | opening=new_thread) | |||
63 | post.threads.add(thread) |
|
65 | post.threads.add(thread) | |
64 |
|
66 | |||
65 | logger = logging.getLogger('boards.post.create') |
|
67 | logger = logging.getLogger('boards.post.create') | |
@@ -122,7 +124,8 b' class PostManager(models.Manager):' | |||||
122 | @transaction.atomic |
|
124 | @transaction.atomic | |
123 | def import_post(self, title: str, text: str, pub_time: str, global_id, |
|
125 | def import_post(self, title: str, text: str, pub_time: str, global_id, | |
124 | opening_post=None, tags=list()): |
|
126 | opening_post=None, tags=list()): | |
125 |
i |
|
127 | is_opening = opening_post is None | |
|
128 | if is_opening: | |||
126 | thread = boards.models.thread.Thread.objects.create( |
|
129 | thread = boards.models.thread.Thread.objects.create( | |
127 | bump_time=pub_time, last_edit_time=pub_time) |
|
130 | bump_time=pub_time, last_edit_time=pub_time) | |
128 | list(map(thread.tags.add, tags)) |
|
131 | list(map(thread.tags.add, tags)) | |
@@ -133,7 +136,9 b' class PostManager(models.Manager):' | |||||
133 | pub_time=pub_time, |
|
136 | pub_time=pub_time, | |
134 | poster_ip=NO_IP, |
|
137 | poster_ip=NO_IP, | |
135 | last_edit_time=pub_time, |
|
138 | last_edit_time=pub_time, | |
136 |
thread_id=thread.id, |
|
139 | thread_id=thread.id, | |
|
140 | global_id=global_id, | |||
|
141 | opening=is_opening) | |||
137 |
|
142 | |||
138 | post.build_url() |
|
143 | post.build_url() | |
139 | post.connect_replies() |
|
144 | post.connect_replies() |
@@ -1,3 +1,4 b'' | |||||
|
1 | import hashlib | |||
1 | from django.template.loader import render_to_string |
|
2 | from django.template.loader import render_to_string | |
2 | from django.db import models |
|
3 | from django.db import models | |
3 | from django.db.models import Count |
|
4 | from django.db.models import Count | |
@@ -47,6 +48,8 b' class Tag(models.Model, Viewable):' | |||||
47 | required = models.BooleanField(default=False, db_index=True) |
|
48 | required = models.BooleanField(default=False, db_index=True) | |
48 | description = models.TextField(blank=True) |
|
49 | description = models.TextField(blank=True) | |
49 |
|
50 | |||
|
51 | parent = models.ForeignKey('Tag', null=True, related_name='children') | |||
|
52 | ||||
50 | def __str__(self): |
|
53 | def __str__(self): | |
51 | return self.name |
|
54 | return self.name | |
52 |
|
55 | |||
@@ -89,7 +92,7 b' class Tag(models.Model, Viewable):' | |||||
89 |
|
92 | |||
90 | @cached_result() |
|
93 | @cached_result() | |
91 | def get_post_count(self): |
|
94 | def get_post_count(self): | |
92 |
return self.get_threads().aggregate(num_posts=Count(' |
|
95 | return self.get_threads().aggregate(num_posts=Count('multi_replies'))['num_posts'] | |
93 |
|
96 | |||
94 | def get_description(self): |
|
97 | def get_description(self): | |
95 | return self.description |
|
98 | return self.description | |
@@ -107,3 +110,25 b' class Tag(models.Model, Viewable):' | |||||
107 | def get_related_tags(self): |
|
110 | def get_related_tags(self): | |
108 | return set(Tag.objects.filter(thread_tags__in=self.get_threads()).exclude( |
|
111 | return set(Tag.objects.filter(thread_tags__in=self.get_threads()).exclude( | |
109 |
|
|
112 | id=self.id).order_by('?')[:RELATED_TAGS_COUNT]) | |
|
113 | ||||
|
114 | @cached_result() | |||
|
115 | def get_color(self): | |||
|
116 | """ | |||
|
117 | Gets color hashed from the tag name. | |||
|
118 | """ | |||
|
119 | return hashlib.md5(self.name.encode()).hexdigest()[:6] | |||
|
120 | ||||
|
121 | def get_parent(self): | |||
|
122 | return self.parent | |||
|
123 | ||||
|
124 | def get_all_parents(self): | |||
|
125 | parents = set() | |||
|
126 | parent = self.get_parent() | |||
|
127 | if parent and parent not in parents: | |||
|
128 | parents.add(parent) | |||
|
129 | parents |= parent.get_all_parents() | |||
|
130 | ||||
|
131 | return parents | |||
|
132 | ||||
|
133 | def get_children(self): | |||
|
134 | return self.children |
@@ -1,7 +1,7 b'' | |||||
1 | import logging |
|
1 | import logging | |
2 | from adjacent import Client |
|
2 | from adjacent import Client | |
3 |
|
3 | |||
4 | from django.db.models import Count, Sum, QuerySet |
|
4 | from django.db.models import Count, Sum, QuerySet, Q | |
5 | from django.utils import timezone |
|
5 | from django.utils import timezone | |
6 | from django.db import models |
|
6 | from django.db import models | |
7 |
|
7 | |||
@@ -11,6 +11,8 b' from boards.utils import cached_result, ' | |||||
11 | from boards.models.post import Post |
|
11 | from boards.models.post import Post | |
12 | from boards.models.tag import Tag |
|
12 | from boards.models.tag import Tag | |
13 |
|
13 | |||
|
14 | FAV_THREAD_NO_UPDATES = -1 | |||
|
15 | ||||
14 |
|
16 | |||
15 | __author__ = 'neko259' |
|
17 | __author__ = 'neko259' | |
16 |
|
18 | |||
@@ -54,6 +56,26 b' class ThreadManager(models.Manager):' | |||||
54 | thread.update_posts_time() |
|
56 | thread.update_posts_time() | |
55 | thread.save(update_fields=['archived', 'last_edit_time', 'bumpable']) |
|
57 | thread.save(update_fields=['archived', 'last_edit_time', 'bumpable']) | |
56 |
|
58 | |||
|
59 | def get_new_posts(self, datas): | |||
|
60 | query = None | |||
|
61 | # TODO Use classes instead of dicts | |||
|
62 | for data in datas: | |||
|
63 | if data['last_id'] != FAV_THREAD_NO_UPDATES: | |||
|
64 | q = (Q(id=data['op'].get_thread().id) | |||
|
65 | & Q(multi_replies__id__gt=data['last_id'])) | |||
|
66 | if query is None: | |||
|
67 | query = q | |||
|
68 | else: | |||
|
69 | query = query | q | |||
|
70 | if query is not None: | |||
|
71 | return self.filter(query).annotate( | |||
|
72 | new_post_count=Count('multi_replies')) | |||
|
73 | ||||
|
74 | def get_new_post_count(self, datas): | |||
|
75 | new_posts = self.get_new_posts(datas) | |||
|
76 | return new_posts.aggregate(total_count=Count('multi_replies'))\ | |||
|
77 | ['total_count'] if new_posts else 0 | |||
|
78 | ||||
57 |
|
79 | |||
58 | def get_thread_max_posts(): |
|
80 | def get_thread_max_posts(): | |
59 | return settings.get_int('Messages', 'MaxPostsPerThread') |
|
81 | return settings.get_int('Messages', 'MaxPostsPerThread') | |
@@ -116,7 +138,7 b' class Thread(models.Model):' | |||||
116 | Checks if the thread can be bumped by replying to it. |
|
138 | Checks if the thread can be bumped by replying to it. | |
117 | """ |
|
139 | """ | |
118 |
|
140 | |||
119 | return self.bumpable and not self.archived |
|
141 | return self.bumpable and not self.is_archived() | |
120 |
|
142 | |||
121 | def get_last_replies(self) -> QuerySet: |
|
143 | def get_last_replies(self) -> QuerySet: | |
122 | """ |
|
144 | """ | |
@@ -150,8 +172,8 b' class Thread(models.Model):' | |||||
150 | Gets sorted thread posts |
|
172 | Gets sorted thread posts | |
151 | """ |
|
173 | """ | |
152 |
|
174 | |||
153 | query = Post.objects.filter(threads__in=[self]) |
|
175 | query = self.multi_replies.order_by('pub_time').prefetch_related( | |
154 | query = query.order_by('pub_time').prefetch_related('images', 'thread', 'threads') |
|
176 | 'images', 'thread', 'threads', 'attachments') | |
155 | if view_fields_only: |
|
177 | if view_fields_only: | |
156 | query = query.defer('poster_ip') |
|
178 | query = query.defer('poster_ip') | |
157 | return query.all() |
|
179 | return query.all() | |
@@ -203,7 +225,7 b' class Thread(models.Model):' | |||||
203 | def update_posts_time(self, exclude_posts=None): |
|
225 | def update_posts_time(self, exclude_posts=None): | |
204 | last_edit_time = self.last_edit_time |
|
226 | last_edit_time = self.last_edit_time | |
205 |
|
227 | |||
206 |
for post in self. |
|
228 | for post in self.multi_replies.all(): | |
207 | if exclude_posts is None or post not in exclude_posts: |
|
229 | if exclude_posts is None or post not in exclude_posts: | |
208 | # Manual update is required because uids are generated on save |
|
230 | # Manual update is required because uids are generated on save | |
209 | post.last_edit_time = last_edit_time |
|
231 | post.last_edit_time = last_edit_time | |
@@ -228,3 +250,9 b' class Thread(models.Model):' | |||||
228 |
|
250 | |||
229 | def get_required_tags(self): |
|
251 | def get_required_tags(self): | |
230 | return self.get_tags().filter(required=True) |
|
252 | return self.get_tags().filter(required=True) | |
|
253 | ||||
|
254 | def get_replies_newer(self, post_id): | |||
|
255 | return self.get_replies().filter(id__gt=post_id) | |||
|
256 | ||||
|
257 | def is_archived(self): | |||
|
258 | return self.archived |
@@ -133,4 +133,13 b' textarea, input {' | |||||
133 |
|
133 | |||
134 | .tripcode { |
|
134 | .tripcode { | |
135 | padding: 2px; |
|
135 | padding: 2px; | |
|
136 | } | |||
|
137 | ||||
|
138 | #fav-panel { | |||
|
139 | display: none; | |||
|
140 | margin: 1ex; | |||
|
141 | } | |||
|
142 | ||||
|
143 | #new-fav-post-count { | |||
|
144 | display: none; | |||
136 | } No newline at end of file |
|
145 | } |
@@ -115,6 +115,14 b' body {' | |||||
115 | visibility: hidden; |
|
115 | visibility: hidden; | |
116 | } |
|
116 | } | |
117 |
|
117 | |||
|
118 | .tag_info { | |||
|
119 | text-align: center; | |||
|
120 | } | |||
|
121 | ||||
|
122 | .tag_info > .tag-text-data { | |||
|
123 | text-align: left; | |||
|
124 | } | |||
|
125 | ||||
118 | .header { |
|
126 | .header { | |
119 | border-bottom: solid 2px #ccc; |
|
127 | border-bottom: solid 2px #ccc; | |
120 | margin-bottom: 5px; |
|
128 | margin-bottom: 5px; | |
@@ -416,6 +424,8 b' li {' | |||||
416 | padding: 5px; |
|
424 | padding: 5px; | |
417 | color: #eee; |
|
425 | color: #eee; | |
418 | font-size: 2ex; |
|
426 | font-size: 2ex; | |
|
427 | margin-top: .5ex; | |||
|
428 | margin-bottom: .5ex; | |||
419 | } |
|
429 | } | |
420 |
|
430 | |||
421 | .skipped_replies { |
|
431 | .skipped_replies { | |
@@ -442,6 +452,7 b' li {' | |||||
442 | border: solid 1px; |
|
452 | border: solid 1px; | |
443 | margin: 0.5ex; |
|
453 | margin: 0.5ex; | |
444 | text-align: center; |
|
454 | text-align: center; | |
|
455 | padding: 1ex; | |||
445 | } |
|
456 | } | |
446 |
|
457 | |||
447 | code { |
|
458 | code { | |
@@ -520,6 +531,11 b' ul {' | |||||
520 | border: 1px solid #777; |
|
531 | border: 1px solid #777; | |
521 | background: #000; |
|
532 | background: #000; | |
522 | padding: 4px; |
|
533 | padding: 4px; | |
|
534 | opacity: 0.3; | |||
|
535 | } | |||
|
536 | ||||
|
537 | #up:hover { | |||
|
538 | opacity: 1; | |||
523 | } |
|
539 | } | |
524 |
|
540 | |||
525 | .user-cast { |
|
541 | .user-cast { | |
@@ -557,3 +573,7 b' ul {' | |||||
557 | .tripcode { |
|
573 | .tripcode { | |
558 | color: white; |
|
574 | color: white; | |
559 | } |
|
575 | } | |
|
576 | ||||
|
577 | #fav-panel { | |||
|
578 | border: 1px solid white; | |||
|
579 | } |
@@ -23,6 +23,8 b'' | |||||
23 | for the JavaScript code in this page. |
|
23 | for the JavaScript code in this page. | |
24 | */ |
|
24 | */ | |
25 |
|
25 | |||
|
26 | var IMAGE_POPUP_MARGIN = 10; | |||
|
27 | ||||
26 |
|
28 | |||
27 | var IMAGE_VIEWERS = [ |
|
29 | var IMAGE_VIEWERS = [ | |
28 | ['simple', new SimpleImageViewer()], |
|
30 | ['simple', new SimpleImageViewer()], | |
@@ -31,6 +33,8 b' var IMAGE_VIEWERS = [' | |||||
31 |
|
33 | |||
32 | var FULL_IMG_CLASS = 'post-image-full'; |
|
34 | var FULL_IMG_CLASS = 'post-image-full'; | |
33 |
|
35 | |||
|
36 | var ATTR_SCALE = 'scale'; | |||
|
37 | ||||
34 |
|
38 | |||
35 | function ImageViewer() {} |
|
39 | function ImageViewer() {} | |
36 | ImageViewer.prototype.view = function (post) {}; |
|
40 | ImageViewer.prototype.view = function (post) {}; | |
@@ -67,8 +71,6 b' SimpleImageViewer.prototype.view = funct' | |||||
67 |
|
71 | |||
68 | function PopupImageViewer() {} |
|
72 | function PopupImageViewer() {} | |
69 | PopupImageViewer.prototype.view = function (post) { |
|
73 | PopupImageViewer.prototype.view = function (post) { | |
70 | var margin = 20; //..change |
|
|||
71 |
|
||||
72 | var el = post; |
|
74 | var el = post; | |
73 | var thumb_id = 'full' + el.find('img').attr('alt'); |
|
75 | var thumb_id = 'full' + el.find('img').attr('alt'); | |
74 |
|
76 | |||
@@ -76,29 +78,41 b' PopupImageViewer.prototype.view = functi' | |||||
76 | if(!existingPopups.length) { |
|
78 | if (!existingPopups.length) { | |
77 | var imgElement= el.find('img'); |
|
79 | var imgElement= el.find('img'); | |
78 |
|
80 | |||
79 | var img_w = imgElement.attr('data-width'); |
|
81 | var full_img_w = imgElement.attr('data-width'); | |
80 | var img_h = imgElement.attr('data-height'); |
|
82 | var full_img_h = imgElement.attr('data-height'); | |
81 |
|
83 | |||
82 | var win = $(window); |
|
84 | var win = $(window); | |
83 |
|
85 | |||
84 | var win_w = win.width(); |
|
86 | var win_w = win.width(); | |
85 | var win_h = win.height(); |
|
87 | var win_h = win.height(); | |
86 | //new image size |
|
88 | ||
87 | if (img_w > win_w) { |
|
89 | // New image size | |
88 | img_h = img_h * (win_w/img_w) - margin; |
|
90 | var w_scale = 1; | |
89 | img_w = win_w - margin; |
|
91 | var h_scale = 1; | |
|
92 | ||||
|
93 | var freeWidth = win_w - 2 * IMAGE_POPUP_MARGIN; | |||
|
94 | var freeHeight = win_h - 2 * IMAGE_POPUP_MARGIN; | |||
|
95 | ||||
|
96 | if (full_img_w > freeWidth) { | |||
|
97 | w_scale = full_img_w / freeWidth; | |||
90 | } |
|
98 | } | |
91 |
if (img_h > |
|
99 | if (full_img_h > freeHeight) { | |
92 | img_w = img_w * (win_h/img_h) - margin; |
|
100 | h_scale = full_img_h / freeHeight; | |
93 | img_h = win_h - margin; |
|
|||
94 | } |
|
101 | } | |
95 |
|
102 | |||
|
103 | var scale = Math.max(w_scale, h_scale) | |||
|
104 | var img_w = full_img_w / scale; | |||
|
105 | var img_h = full_img_h / scale; | |||
|
106 | ||||
|
107 | var postNode = $(el); | |||
|
108 | ||||
96 | var img_pv = new Image(); |
|
109 | var img_pv = new Image(); | |
97 | var newImage = $(img_pv); |
|
110 | var newImage = $(img_pv); | |
98 | newImage.addClass('img-full') |
|
111 | newImage.addClass('img-full') | |
99 | .attr('id', thumb_id) |
|
112 | .attr('id', thumb_id) | |
100 |
.attr('src', |
|
113 | .attr('src', postNode.attr('href')) | |
101 | .appendTo($(el)) |
|
114 | .attr(ATTR_SCALE, scale) | |
|
115 | .appendTo(postNode) | |||
102 | .css({ |
|
116 | .css({ | |
103 | 'width': img_w, |
|
117 | 'width': img_w, | |
104 | 'height': img_h, |
|
118 | 'height': img_h, | |
@@ -107,19 +121,26 b' PopupImageViewer.prototype.view = functi' | |||||
107 | }) |
|
121 | }) | |
108 | //scaling preview |
|
122 | //scaling preview | |
109 | .mousewheel(function(event, delta) { |
|
123 | .mousewheel(function(event, delta) { | |
110 |
var cx = event.originalEvent.clientX |
|
124 | var cx = event.originalEvent.clientX; | |
111 |
cy = event.originalEvent.clientY |
|
125 | var cy = event.originalEvent.clientY; | |
112 | i_w = parseFloat(newImage.width()), |
|
126 | ||
113 | i_h = parseFloat(newImage.height()), |
|
127 | var scale = newImage.attr(ATTR_SCALE) / (delta > 0 ? 1.25 : 0.8); | |
114 | newIW = i_w * (delta > 0 ? 1.25 : 0.8), |
|
128 | ||
115 | newIH = i_h * (delta > 0 ? 1.25 : 0.8); |
|
129 | var oldWidth = newImage.width(); | |
|
130 | var oldHeight = newImage.height(); | |||
|
131 | ||||
|
132 | var newIW = full_img_w / scale; | |||
|
133 | var newIH = full_img_h / scale; | |||
116 |
|
134 | |||
117 | newImage.width(newIW); |
|
135 | newImage.width(newIW); | |
118 | newImage.height(newIH); |
|
136 | newImage.height(newIH); | |
119 | //set position |
|
137 | newImage.attr(ATTR_SCALE, scale); | |
|
138 | ||||
|
139 | // Set position | |||
|
140 | var oldPosition = newImage.position(); | |||
120 | newImage.css({ |
|
141 | newImage.css({ | |
121 |
left: parseInt(cx - (newIW/ |
|
142 | left: parseInt(cx - (newIW/oldWidth) * (cx - parseInt(oldPosition.left, 10)), 10), | |
122 |
top: parseInt(cy - (newIH/ |
|
143 | top: parseInt(cy - (newIH/oldHeight) * (cy - parseInt(oldPosition.top, 10)), 10) | |
123 | }); |
|
144 | }); | |
124 |
|
145 | |||
125 | return false; |
|
146 | return false; |
@@ -23,6 +23,8 b'' | |||||
23 | for the JavaScript code in this page. |
|
23 | for the JavaScript code in this page. | |
24 | */ |
|
24 | */ | |
25 |
|
25 | |||
|
26 | var FAV_POST_UPDATE_PERIOD = 10000; | |||
|
27 | ||||
26 | /** |
|
28 | /** | |
27 | * An email is a hidden file to prevent spam bots from posting. It has to be |
|
29 | * An email is a hidden file to prevent spam bots from posting. It has to be | |
28 | * hidden. |
|
30 | * hidden. | |
@@ -40,6 +42,72 b' function highlightCode(node) {' | |||||
40 | }); |
|
42 | }); | |
41 | } |
|
43 | } | |
42 |
|
44 | |||
|
45 | function updateFavPosts() { | |||
|
46 | var includePostBody = $('#fav-panel').is(":visible"); | |||
|
47 | var url = '/api/new_posts/'; | |||
|
48 | if (includePostBody) { | |||
|
49 | url += '?include_posts' | |||
|
50 | } | |||
|
51 | $.getJSON(url, | |||
|
52 | function(data) { | |||
|
53 | var allNewPostCount = 0; | |||
|
54 | ||||
|
55 | if (includePostBody) { | |||
|
56 | var favoriteThreadPanel = $('#fav-panel'); | |||
|
57 | favoriteThreadPanel.empty(); | |||
|
58 | } | |||
|
59 | ||||
|
60 | $.each(data, function (_, dict) { | |||
|
61 | var newPostCount = dict.new_post_count; | |||
|
62 | allNewPostCount += newPostCount; | |||
|
63 | ||||
|
64 | if (includePostBody) { | |||
|
65 | var favThreadNode = $('<div class="post"></div>'); | |||
|
66 | favThreadNode.append($(dict.post_url)); | |||
|
67 | favThreadNode.append(' '); | |||
|
68 | favThreadNode.append($('<span class="title">' + dict.title + '</span>')); | |||
|
69 | ||||
|
70 | if (newPostCount > 0) { | |||
|
71 | favThreadNode.append(' (<a href="' + dict.newest_post_link + '">+' + newPostCount + "</a>)"); | |||
|
72 | } | |||
|
73 | ||||
|
74 | favoriteThreadPanel.append(favThreadNode); | |||
|
75 | ||||
|
76 | addRefLinkPreview(favThreadNode[0]); | |||
|
77 | } | |||
|
78 | }); | |||
|
79 | ||||
|
80 | var newPostCountNode = $('#new-fav-post-count'); | |||
|
81 | if (allNewPostCount > 0) { | |||
|
82 | newPostCountNode.text('(+' + allNewPostCount + ')'); | |||
|
83 | newPostCountNode.show(); | |||
|
84 | } else { | |||
|
85 | newPostCountNode.hide(); | |||
|
86 | } | |||
|
87 | } | |||
|
88 | ); | |||
|
89 | } | |||
|
90 | ||||
|
91 | function initFavPanel() { | |||
|
92 | updateFavPosts(); | |||
|
93 | ||||
|
94 | if ($('#fav-panel-btn').length > 0) { | |||
|
95 | setInterval(updateFavPosts, FAV_POST_UPDATE_PERIOD); | |||
|
96 | $('#fav-panel-btn').click(function() { | |||
|
97 | $('#fav-panel').toggle(); | |||
|
98 | updateFavPosts(); | |||
|
99 | ||||
|
100 | return false; | |||
|
101 | }); | |||
|
102 | ||||
|
103 | $(document).on('keyup.removepic', function(e) { | |||
|
104 | if(e.which === 27) { | |||
|
105 | $('#fav-panel').hide(); | |||
|
106 | } | |||
|
107 | }); | |||
|
108 | } | |||
|
109 | } | |||
|
110 | ||||
43 | $( document ).ready(function() { |
|
111 | $( document ).ready(function() { | |
44 | hideEmailFromForm(); |
|
112 | hideEmailFromForm(); | |
45 |
|
113 | |||
@@ -53,4 +121,6 b' function highlightCode(node) {' | |||||
53 | addRefLinkPreview(); |
|
121 | addRefLinkPreview(); | |
54 |
|
122 | |||
55 | highlightCode($(document)); |
|
123 | highlightCode($(document)); | |
|
124 | ||||
|
125 | initFavPanel(); | |||
56 | }); |
|
126 | }); |
@@ -102,11 +102,10 b' function connectWebsocket() {' | |||||
102 | * missed. |
|
102 | * missed. | |
103 | */ |
|
103 | */ | |
104 | function getThreadDiff() { |
|
104 | function getThreadDiff() { | |
105 | var lastUpdateTime = $('.metapanel').attr('data-last-update'); |
|
105 | var all_posts = $('.post'); | |
106 | var lastPostId = $('.post').last().attr('id'); |
|
|||
107 |
|
106 | |||
108 | var uids = ''; |
|
107 | var uids = ''; | |
109 |
var posts = |
|
108 | var posts = all_posts; | |
110 | for (var i = 0; i < posts.length; i++) { |
|
109 | for (var i = 0; i < posts.length; i++) { | |
111 | uids += posts[i].getAttribute('data-uid') + ' '; |
|
110 | uids += posts[i].getAttribute('data-uid') + ' '; | |
112 | } |
|
111 | } | |
@@ -114,7 +113,7 b' function getThreadDiff() {' | |||||
114 | var data = { |
|
113 | var data = { | |
115 | uids: uids, |
|
114 | uids: uids, | |
116 | thread: threadId |
|
115 | thread: threadId | |
117 | } |
|
116 | }; | |
118 |
|
117 | |||
119 | var diffUrl = '/api/diff_thread/'; |
|
118 | var diffUrl = '/api/diff_thread/'; | |
120 |
|
119 | |||
@@ -244,8 +243,10 b' function updateMetadataPanel() {' | |||||
244 | var replyCountField = $('#reply-count'); |
|
243 | var replyCountField = $('#reply-count'); | |
245 | var imageCountField = $('#image-count'); |
|
244 | var imageCountField = $('#image-count'); | |
246 |
|
245 | |||
247 |
replyCount |
|
246 | var replyCount = getReplyCount(); | |
248 |
|
|
247 | replyCountField.text(replyCount); | |
|
248 | var imageCount = getImageCount(); | |||
|
249 | imageCountField.text(imageCount); | |||
249 |
|
250 | |||
250 | var lastUpdate = $('.post:last').children('.post-info').first() |
|
251 | var lastUpdate = $('.post:last').children('.post-info').first() | |
251 | .children('.pub_time').first().html(); |
|
252 | .children('.pub_time').first().html(); | |
@@ -257,6 +258,9 b' function updateMetadataPanel() {' | |||||
257 |
|
258 | |||
258 | blink(replyCountField); |
|
259 | blink(replyCountField); | |
259 | blink(imageCountField); |
|
260 | blink(imageCountField); | |
|
261 | ||||
|
262 | $('#message-count-text').text(ngettext('message', 'messages', replyCount)); | |||
|
263 | $('#image-count-text').text(ngettext('image', 'images', imageCount)); | |||
260 | } |
|
264 | } | |
261 |
|
265 | |||
262 | /** |
|
266 | /** | |
@@ -380,9 +384,6 b' function replacePartial(oldNode, newNode' | |||||
380 | // Replace children |
|
384 | // Replace children | |
381 | var children = oldNode.children(); |
|
385 | var children = oldNode.children(); | |
382 | if (children.length == 0) { |
|
386 | if (children.length == 0) { | |
383 | console.log(oldContent); |
|
|||
384 | console.log(newContent) |
|
|||
385 |
|
||||
386 | oldNode.replaceWith(newNode); |
|
387 | oldNode.replaceWith(newNode); | |
387 | } else { |
|
388 | } else { | |
388 | var newChildren = newNode.children(); |
|
389 | var newChildren = newNode.children(); | |
@@ -426,7 +427,7 b' function updateNodeAttr(oldNode, newNode' | |||||
426 | var newAttr = newNode.attr(attrName); |
|
427 | var newAttr = newNode.attr(attrName); | |
427 | if (oldAttr != newAttr) { |
|
428 | if (oldAttr != newAttr) { | |
428 | oldNode.attr(attrName, newAttr); |
|
429 | oldNode.attr(attrName, newAttr); | |
429 |
} |
|
430 | } | |
430 | } |
|
431 | } | |
431 |
|
432 | |||
432 | $(document).ready(function(){ |
|
433 | $(document).ready(function(){ | |
@@ -439,11 +440,11 b' function updateNodeAttr(oldNode, newNode' | |||||
439 | if (form.length > 0) { |
|
440 | if (form.length > 0) { | |
440 | var options = { |
|
441 | var options = { | |
441 | beforeSubmit: function(arr, $form, options) { |
|
442 | beforeSubmit: function(arr, $form, options) { | |
442 | showAsErrors($('form'), gettext('Sending message...')); |
|
443 | showAsErrors($('#form'), gettext('Sending message...')); | |
443 | }, |
|
444 | }, | |
444 | success: updateOnPost, |
|
445 | success: updateOnPost, | |
445 | error: function() { |
|
446 | error: function() { | |
446 | showAsErrors($('form'), gettext('Server error!')); |
|
447 | showAsErrors($('#form'), gettext('Server error!')); | |
447 | }, |
|
448 | }, | |
448 | url: '/api/add_post/' + threadId + '/' |
|
449 | url: '/api/add_post/' + threadId + '/' | |
449 | }; |
|
450 | }; |
@@ -37,7 +37,7 b'' | |||||
37 | {% endfor %} |
|
37 | {% endfor %} | |
38 |
|
38 | |||
39 | {% if tag %} |
|
39 | {% if tag %} | |
40 | <div class="tag_info"> |
|
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.images.first %} |
|
43 | {% with image=random_image_post.images.first %} | |
@@ -73,11 +73,12 b'' | |||||
73 | <p>{{ tag.get_description|safe }}</p> |
|
73 | <p>{{ tag.get_description|safe }}</p> | |
74 | {% endif %} |
|
74 | {% endif %} | |
75 | <p>{% blocktrans with active_thread_count=tag.get_active_thread_count thread_count=tag.get_thread_count post_count=tag.get_post_count %}This tag has {{ thread_count }} threads ({{ active_thread_count}} active) and {{ post_count }} posts.{% endblocktrans %}</p> |
|
75 | <p>{% blocktrans with active_thread_count=tag.get_active_thread_count thread_count=tag.get_thread_count post_count=tag.get_post_count %}This tag has {{ thread_count }} threads ({{ active_thread_count}} active) and {{ post_count }} posts.{% endblocktrans %}</p> | |
76 |
{% if |
|
76 | {% if tag.get_parent %} | |
77 | <p>{% trans 'Related tags:' %} |
|
77 | <p> | |
78 |
|
|
78 | {% if tag.get_parent %} | |
79 | {{ rel_tag.get_view|safe }}{% if not forloop.last %}, {% else %}.{% endif %} |
|
79 | {{ tag.get_parent.get_view|safe }} / | |
80 |
{% endf |
|
80 | {% endif %} | |
|
81 | {{ tag.get_view|safe }} | |||
81 | </p> |
|
82 | </p> | |
82 | {% endif %} |
|
83 | {% endif %} | |
83 | </div> |
|
84 | </div> | |
@@ -93,7 +94,7 b'' | |||||
93 |
|
94 | |||
94 | {% for thread in threads %} |
|
95 | {% for thread in threads %} | |
95 | <div class="thread"> |
|
96 | <div class="thread"> | |
96 |
{% post_view thread.get_opening_post moderator=moderator |
|
97 | {% post_view thread.get_opening_post moderator=moderator thread=thread truncated=True need_open_link=True %} | |
97 | {% if not thread.archived %} |
|
98 | {% if not thread.archived %} | |
98 | {% with last_replies=thread.get_last_replies %} |
|
99 | {% with last_replies=thread.get_last_replies %} | |
99 | {% if last_replies %} |
|
100 | {% if last_replies %} | |
@@ -101,14 +102,14 b'' | |||||
101 | {% if skipped_replies_count %} |
|
102 | {% if skipped_replies_count %} | |
102 | <div class="skipped_replies"> |
|
103 | <div class="skipped_replies"> | |
103 | <a href="{% url 'thread' thread.get_opening_post_id %}"> |
|
104 | <a href="{% url 'thread' thread.get_opening_post_id %}"> | |
104 |
{% blocktrans |
|
105 | {% 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 %} | |
105 | </a> |
|
106 | </a> | |
106 | </div> |
|
107 | </div> | |
107 | {% endif %} |
|
108 | {% endif %} | |
108 | {% endwith %} |
|
109 | {% endwith %} | |
109 | <div class="last-replies"> |
|
110 | <div class="last-replies"> | |
110 | {% for post in last_replies %} |
|
111 | {% for post in last_replies %} | |
111 |
{% post_view post |
|
112 | {% post_view post moderator=moderator truncated=True %} | |
112 | {% endfor %} |
|
113 | {% endfor %} | |
113 | </div> |
|
114 | </div> | |
114 | {% endif %} |
|
115 | {% endif %} |
@@ -39,7 +39,10 b'' | |||||
39 | <a href="{% url 'tags' 'required'%}" title="{% trans 'Tag management' %}">{% trans "tags" %}</a>, |
|
39 | <a href="{% url 'tags' 'required'%}" title="{% trans 'Tag management' %}">{% trans "tags" %}</a>, | |
40 | <a href="{% url 'search' %}" title="{% trans 'Search' %}">{% trans 'search' %}</a>, |
|
40 | <a href="{% url 'search' %}" title="{% trans 'Search' %}">{% trans 'search' %}</a>, | |
41 | <a href="{% url 'feed' %}" title="{% trans 'Feed' %}">{% trans 'feed' %}</a>, |
|
41 | <a href="{% url 'feed' %}" title="{% trans 'Feed' %}">{% trans 'feed' %}</a>, | |
42 | <a href="{% url 'random' %}" title="{% trans 'Random images' %}">{% trans 'random' %}</a> |
|
42 | <a href="{% url 'random' %}" title="{% trans 'Random images' %}">{% trans 'random' %}</a>{% if has_fav_threads %}, | |
|
43 | ||||
|
44 | <a href="#" id="fav-panel-btn">{% trans 'favorites' %} <span id="new-fav-post-count"></span></a> | |||
|
45 | {% endif %} | |||
43 |
|
46 | |||
44 | {% if username %} |
|
47 | {% if username %} | |
45 | <a class="right-link link" href="{% url 'notifications' username %}" title="{% trans 'Notifications' %}"> |
|
48 | <a class="right-link link" href="{% url 'notifications' username %}" title="{% trans 'Notifications' %}"> | |
@@ -53,6 +56,8 b'' | |||||
53 | <a class="right-link link" href="{% url 'settings' %}">{% trans 'Settings' %}</a> |
|
56 | <a class="right-link link" href="{% url 'settings' %}">{% trans 'Settings' %}</a> | |
54 | </div> |
|
57 | </div> | |
55 |
|
58 | |||
|
59 | <div id="fav-panel"><div class="post">{% trans "Loading..." %}</div></div> | |||
|
60 | ||||
56 | {% block content %}{% endblock %} |
|
61 | {% block content %}{% endblock %} | |
57 |
|
62 | |||
58 | <script src="{% static 'js/3party/highlight.min.js' %}"></script> |
|
63 | <script src="{% static 'js/3party/highlight.min.js' %}"></script> |
@@ -36,7 +36,7 b'' | |||||
36 | {% else %} |
|
36 | {% else %} | |
37 | {% if need_op_data %} |
|
37 | {% if need_op_data %} | |
38 | {% with thread.get_opening_post as op %} |
|
38 | {% with thread.get_opening_post as op %} | |
39 |
{% trans " in " %}{{ op.get_link_view|safe }} <span class="title">{{ op.get_title |
|
39 | {% trans " in " %}{{ op.get_link_view|safe }} <span class="title">{{ op.get_title_or_text }}</span> | |
40 | {% endwith %} |
|
40 | {% endwith %} | |
41 | {% endif %} |
|
41 | {% endif %} | |
42 | {% endif %} |
|
42 | {% endif %} | |
@@ -61,16 +61,12 b'' | |||||
61 | Post images. Currently only 1 image can be posted and shown, but post model |
|
61 | Post images. Currently only 1 image can be posted and shown, but post model | |
62 | supports multiple. |
|
62 | supports multiple. | |
63 | {% endcomment %} |
|
63 | {% endcomment %} | |
64 |
{% |
|
64 | {% for image in post.images.all %} | |
65 | {% with post.images.first as image %} |
|
|||
66 |
|
|
65 | {{ image.get_view|safe }} | |
67 |
|
|
66 | {% endfor %} | |
68 | {% endif %} |
|
67 | {% for file in post.attachments.all %} | |
69 | {% if post.attachments.exists %} |
|
|||
70 | {% with post.attachments.first as file %} |
|
|||
71 |
|
|
68 | {{ file.get_view|safe }} | |
72 |
|
|
69 | {% endfor %} | |
73 | {% endif %} |
|
|||
74 | {% comment %} |
|
70 | {% comment %} | |
75 | Post message (text) |
|
71 | Post message (text) | |
76 | {% endcomment %} |
|
72 | {% endcomment %} | |
@@ -102,8 +98,8 b'' | |||||
102 | {% if is_opening %} |
|
98 | {% if is_opening %} | |
103 | <div class="metadata"> |
|
99 | <div class="metadata"> | |
104 | {% if is_opening and need_open_link %} |
|
100 | {% if is_opening and need_open_link %} | |
105 |
{ |
|
101 | {% blocktrans count count=thread.get_reply_count %}{{ count }} message{% plural %}{{ count }} messages{% endblocktrans %}, | |
106 | {{ thread.get_images_count }} {% trans 'images' %}. |
|
102 | {% blocktrans count count=thread.get_images_count %}{{ count }} image{% plural %}{{ count }} images{% endblocktrans %}. | |
107 | {% endif %} |
|
103 | {% endif %} | |
108 | <span class="tags"> |
|
104 | <span class="tags"> | |
109 | {{ thread.get_tag_url_list|safe }} |
|
105 | {{ thread.get_tag_url_list|safe }} |
@@ -6,8 +6,7 b'' | |||||
6 | {% load tz %} |
|
6 | {% load tz %} | |
7 |
|
7 | |||
8 | {% block head %} |
|
8 | {% block head %} | |
9 |
<title>{{ opening_post.get_title |
|
9 | <title>{{ opening_post.get_title_or_text }} - {{ site_name }}</title> | |
10 | - {{ site_name }}</title> |
|
|||
11 | {% endblock %} |
|
10 | {% endblock %} | |
12 |
|
11 | |||
13 | {% block content %} |
|
12 | {% block content %} | |
@@ -31,8 +30,13 b'' | |||||
31 | data-ws-host="{{ ws_host }}" |
|
30 | data-ws-host="{{ ws_host }}" | |
32 | data-ws-port="{{ ws_port }}"> |
|
31 | data-ws-port="{{ ws_port }}"> | |
33 |
|
32 | |||
34 | <span id="reply-count">{{ thread.get_reply_count }}</span>{% if thread.has_post_limit %}/{{ thread.max_posts }}{% endif %} {% trans 'messages' %}, |
|
33 | {% with replies_count=thread.get_reply_count%} | |
35 |
<span id=" |
|
34 | <span id="reply-count">{{ thread.get_reply_count }}</span>{% if thread.has_post_limit %}/{{ thread.max_posts }}{% endif %} | |
|
35 | <span id="message-count-text">{% blocktrans count repliess_count=replies_count %}message{% plural %}messages{% endblocktrans %}</span>, | |||
|
36 | {% endwith %} | |||
|
37 | {% with images_count=thread.get_images_count%} | |||
|
38 | <span id="image-count">{{ images_count }}</span> <span id="image-count-text">{% blocktrans count count=images_count %}image{% plural %}images{% endblocktrans %}</span>. | |||
|
39 | {% endwith %} | |||
36 | {% trans 'Last update: ' %}<span id="last-update"><time datetime="{{ thread.last_edit_time|date:'c' }}">{{ thread.last_edit_time }}</time></span> |
|
40 | {% trans 'Last update: ' %}<span id="last-update"><time datetime="{{ thread.last_edit_time|date:'c' }}">{{ thread.last_edit_time }}</time></span> | |
37 | [<a href="rss/">RSS</a>] |
|
41 | [<a href="rss/">RSS</a>] | |
38 | </span> |
|
42 | </span> |
@@ -9,6 +9,19 b'' | |||||
9 | {% get_current_language as LANGUAGE_CODE %} |
|
9 | {% get_current_language as LANGUAGE_CODE %} | |
10 | {% get_current_timezone as TIME_ZONE %} |
|
10 | {% get_current_timezone as TIME_ZONE %} | |
11 |
|
11 | |||
|
12 | <div class="tag_info"> | |||
|
13 | <h2> | |||
|
14 | <form action="{% url 'thread' opening_post.id %}" method="post" class="post-button-form"> | |||
|
15 | {% if is_favorite %} | |||
|
16 | <button name="method" value="unsubscribe" class="fav">β </button> | |||
|
17 | {% else %} | |||
|
18 | <button name="method" value="subscribe" class="not_fav">β </button> | |||
|
19 | {% endif %} | |||
|
20 | </form> | |||
|
21 | {{ opening_post.get_title_or_text }} | |||
|
22 | </h2> | |||
|
23 | </div> | |||
|
24 | ||||
12 | {% if bumpable and thread.has_post_limit %} |
|
25 | {% if bumpable and thread.has_post_limit %} | |
13 | <div class="bar-bg"> |
|
26 | <div class="bar-bg"> | |
14 | <div class="bar-value" style="width:{{ bumplimit_progress }}%" id="bumplimit_progress"> |
|
27 | <div class="bar-value" style="width:{{ bumplimit_progress }}%" id="bumplimit_progress"> |
@@ -70,6 +70,7 b" urlpatterns = patterns(''," | |||||
70 | url(r'^api/notifications/(?P<username>\w+)/$', api.api_get_notifications, |
|
70 | url(r'^api/notifications/(?P<username>\w+)/$', api.api_get_notifications, | |
71 | name='api_notifications'), |
|
71 | name='api_notifications'), | |
72 | url(r'^api/preview/$', api.api_get_preview, name='preview'), |
|
72 | url(r'^api/preview/$', api.api_get_preview, name='preview'), | |
|
73 | url(r'^api/new_posts/$', api.api_get_new_posts, name='new_posts'), | |||
73 |
|
74 | |||
74 | # Sync protocol API |
|
75 | # Sync protocol API | |
75 | url(r'^api/sync/pull/$', response_pull, name='api_sync_pull'), |
|
76 | url(r'^api/sync/pull/$', response_pull, name='api_sync_pull'), |
@@ -7,8 +7,11 b' import hmac' | |||||
7 |
|
7 | |||
8 | from django.core.cache import cache |
|
8 | from django.core.cache import cache | |
9 | from django.db.models import Model |
|
9 | from django.db.models import Model | |
|
10 | from django import forms | |||
10 |
|
11 | |||
11 | from django.utils import timezone |
|
12 | from django.utils import timezone | |
|
13 | from django.utils.translation import ugettext_lazy as _ | |||
|
14 | import boards | |||
12 |
|
15 | |||
13 | from neboard import settings |
|
16 | from neboard import settings | |
14 |
|
17 | |||
@@ -90,3 +93,11 b' def get_file_hash(file) -> str:' | |||||
90 | for chunk in file.chunks(): |
|
93 | for chunk in file.chunks(): | |
91 | md5.update(chunk) |
|
94 | md5.update(chunk) | |
92 | return md5.hexdigest() |
|
95 | return md5.hexdigest() | |
|
96 | ||||
|
97 | ||||
|
98 | def validate_file_size(size: int): | |||
|
99 | max_size = boards.settings.get_int('Forms', 'MaxFileSize') | |||
|
100 | if size > max_size: | |||
|
101 | raise forms.ValidationError( | |||
|
102 | _('File must be less than %s bytes') | |||
|
103 | % str(max_size)) |
@@ -104,24 +104,6 b' class AllThreadsView(PostMixin, BaseBoar' | |||||
104 | return reverse('index') + '?page=' \ |
|
104 | return reverse('index') + '?page=' \ | |
105 | + str(current_page.next_page_number()) |
|
105 | + str(current_page.next_page_number()) | |
106 |
|
106 | |||
107 | @staticmethod |
|
|||
108 | def parse_tags_string(tag_strings): |
|
|||
109 | """ |
|
|||
110 | Parses tag list string and returns tag object list. |
|
|||
111 | """ |
|
|||
112 |
|
||||
113 | tags = [] |
|
|||
114 |
|
||||
115 | if tag_strings: |
|
|||
116 | tag_strings = tag_strings.split(TAG_DELIMITER) |
|
|||
117 | for tag_name in tag_strings: |
|
|||
118 | tag_name = tag_name.strip().lower() |
|
|||
119 | if len(tag_name) > 0: |
|
|||
120 | tag, created = Tag.objects.get_or_create(name=tag_name) |
|
|||
121 | tags.append(tag) |
|
|||
122 |
|
||||
123 | return tags |
|
|||
124 |
|
||||
125 | @transaction.atomic |
|
107 | @transaction.atomic | |
126 | def create_thread(self, request, form: ThreadForm, html_response=True): |
|
108 | def create_thread(self, request, form: ThreadForm, html_response=True): | |
127 | """ |
|
109 | """ | |
@@ -146,9 +128,7 b' class AllThreadsView(PostMixin, BaseBoar' | |||||
146 |
|
128 | |||
147 | text = self._remove_invalid_links(text) |
|
129 | text = self._remove_invalid_links(text) | |
148 |
|
130 | |||
149 |
tag |
|
131 | tags = data[FORM_TAGS] | |
150 |
|
||||
151 | tags = self.parse_tags_string(tag_strings) |
|
|||
152 |
|
132 | |||
153 | post = Post.objects.create_post(title=title, text=text, file=file, |
|
133 | post = Post.objects.create_post(title=title, text=text, file=file, | |
154 | ip=ip, tags=tags, opening_posts=threads, |
|
134 | ip=ip, tags=tags, opening_posts=threads, |
@@ -1,12 +1,16 b'' | |||||
|
1 | from collections import OrderedDict | |||
1 | import json |
|
2 | import json | |
2 | import logging |
|
3 | import logging | |
3 |
|
4 | |||
4 | import xml.etree.ElementTree as ET |
|
5 | import xml.etree.ElementTree as ET | |
5 |
|
6 | |||
6 | from django.db import transaction |
|
7 | from django.db import transaction | |
|
8 | from django.db.models import Count | |||
7 | from django.http import HttpResponse |
|
9 | from django.http import HttpResponse | |
8 | from django.shortcuts import get_object_or_404 |
|
10 | from django.shortcuts import get_object_or_404 | |
9 | from django.core import serializers |
|
11 | from django.core import serializers | |
|
12 | from boards.abstracts.settingsmanager import get_settings_manager,\ | |||
|
13 | FAV_THREAD_NO_UPDATES | |||
10 |
|
14 | |||
11 | from boards.forms import PostForm, PlainErrorList |
|
15 | from boards.forms import PostForm, PlainErrorList | |
12 | from boards.models import Post, Thread, Tag, GlobalId |
|
16 | from boards.models import Post, Thread, Tag, GlobalId | |
@@ -48,7 +52,8 b' def api_get_threaddiff(request):' | |||||
48 | uids_str = request.POST.get(PARAMETER_UIDS).strip() |
|
52 | uids_str = request.POST.get(PARAMETER_UIDS).strip() | |
49 | uids = uids_str.split(' ') |
|
53 | uids = uids_str.split(' ') | |
50 |
|
54 | |||
51 |
|
|
55 | opening_post = get_object_or_404(Post, id=thread_id) | |
|
56 | thread = opening_post.get_thread() | |||
52 |
|
57 | |||
53 | json_data = { |
|
58 | json_data = { | |
54 | PARAMETER_UPDATED: [], |
|
59 | PARAMETER_UPDATED: [], | |
@@ -59,10 +64,16 b' def api_get_threaddiff(request):' | |||||
59 | diff_type = request.GET.get(PARAMETER_DIFF_TYPE, DIFF_TYPE_HTML) |
|
64 | diff_type = request.GET.get(PARAMETER_DIFF_TYPE, DIFF_TYPE_HTML) | |
60 |
|
65 | |||
61 | for post in posts: |
|
66 | for post in posts: | |
62 |
json_data[PARAMETER_UPDATED].append(get_post_data( |
|
67 | json_data[PARAMETER_UPDATED].append(post.get_post_data( | |
63 | request)) |
|
68 | format_type=diff_type, request=request)) | |
64 | json_data[PARAMETER_LAST_UPDATE] = str(thread.last_edit_time) |
|
69 | json_data[PARAMETER_LAST_UPDATE] = str(thread.last_edit_time) | |
65 |
|
70 | |||
|
71 | # If the tag is favorite, update the counter | |||
|
72 | settings_manager = get_settings_manager(request) | |||
|
73 | favorite = settings_manager.thread_is_fav(opening_post) | |||
|
74 | if favorite: | |||
|
75 | settings_manager.add_or_read_fav_thread(opening_post) | |||
|
76 | ||||
66 | return HttpResponse(content=json.dumps(json_data)) |
|
77 | return HttpResponse(content=json.dumps(json_data)) | |
67 |
|
78 | |||
68 |
|
79 | |||
@@ -146,7 +157,7 b' def api_get_threads(request, count):' | |||||
146 | opening_post = thread.get_opening_post() |
|
157 | opening_post = thread.get_opening_post() | |
147 |
|
158 | |||
148 | # TODO Add tags, replies and images count |
|
159 | # TODO Add tags, replies and images count | |
149 |
post_data = get_post_data( |
|
160 | post_data = opening_post.get_post_data(include_last_update=True) | |
150 | post_data['bumpable'] = thread.can_bump() |
|
161 | post_data['bumpable'] = thread.can_bump() | |
151 | post_data['archived'] = thread.archived |
|
162 | post_data['archived'] = thread.archived | |
152 |
|
163 | |||
@@ -192,7 +203,7 b' def api_get_thread_posts(request, openin' | |||||
192 | json_post_list = [] |
|
203 | json_post_list = [] | |
193 |
|
204 | |||
194 | for post in posts: |
|
205 | for post in posts: | |
195 |
json_post_list.append(get_post_data( |
|
206 | json_post_list.append(post.get_post_data()) | |
196 | json_data['last_update'] = datetime_to_epoch(thread.last_edit_time) |
|
207 | json_data['last_update'] = datetime_to_epoch(thread.last_edit_time) | |
197 | json_data['posts'] = json_post_list |
|
208 | json_data['posts'] = json_post_list | |
198 |
|
209 | |||
@@ -208,7 +219,7 b' def api_get_notifications(request, usern' | |||||
208 |
|
219 | |||
209 | json_post_list = [] |
|
220 | json_post_list = [] | |
210 | for post in posts: |
|
221 | for post in posts: | |
211 |
json_post_list.append(get_post_data( |
|
222 | json_post_list.append(post.get_post_data()) | |
212 | return HttpResponse(content=json.dumps(json_post_list)) |
|
223 | return HttpResponse(content=json.dumps(json_post_list)) | |
213 |
|
224 | |||
214 |
|
225 | |||
@@ -228,16 +239,58 b' def api_get_post(request, post_id):' | |||||
228 | return HttpResponse(content=json) |
|
239 | return HttpResponse(content=json) | |
229 |
|
240 | |||
230 |
|
241 | |||
231 | # TODO Remove this method and use post method directly |
|
|||
232 | def get_post_data(post_id, format_type=DIFF_TYPE_JSON, request=None, |
|
|||
233 | include_last_update=False): |
|
|||
234 | post = get_object_or_404(Post, id=post_id) |
|
|||
235 | return post.get_post_data(format_type=format_type, request=request, |
|
|||
236 | include_last_update=include_last_update) |
|
|||
237 |
|
||||
238 |
|
||||
239 | def api_get_preview(request): |
|
242 | def api_get_preview(request): | |
240 | raw_text = request.POST['raw_text'] |
|
243 | raw_text = request.POST['raw_text'] | |
241 |
|
244 | |||
242 | parser = Parser() |
|
245 | parser = Parser() | |
243 | return HttpResponse(content=parser.parse(parser.preparse(raw_text))) |
|
246 | return HttpResponse(content=parser.parse(parser.preparse(raw_text))) | |
|
247 | ||||
|
248 | ||||
|
249 | def api_get_new_posts(request): | |||
|
250 | """ | |||
|
251 | Gets favorite threads and unread posts count. | |||
|
252 | """ | |||
|
253 | posts = list() | |||
|
254 | ||||
|
255 | include_posts = 'include_posts' in request.GET | |||
|
256 | ||||
|
257 | settings_manager = get_settings_manager(request) | |||
|
258 | fav_threads = settings_manager.get_fav_threads() | |||
|
259 | fav_thread_ops = Post.objects.filter(id__in=fav_threads.keys())\ | |||
|
260 | .order_by('-pub_time').prefetch_related('thread') | |||
|
261 | ||||
|
262 | ops = [{'op': op, 'last_id': fav_threads[str(op.id)]} for op in fav_thread_ops] | |||
|
263 | if include_posts: | |||
|
264 | new_post_threads = Thread.objects.get_new_posts(ops) | |||
|
265 | if new_post_threads: | |||
|
266 | thread_ids = {thread.id: thread for thread in new_post_threads} | |||
|
267 | else: | |||
|
268 | thread_ids = dict() | |||
|
269 | ||||
|
270 | for op in fav_thread_ops: | |||
|
271 | fav_thread_dict = dict() | |||
|
272 | ||||
|
273 | op_thread = op.get_thread() | |||
|
274 | if op_thread.id in thread_ids: | |||
|
275 | thread = thread_ids[op_thread.id] | |||
|
276 | new_post_count = thread.new_post_count | |||
|
277 | fav_thread_dict['newest_post_link'] = thread.get_replies()\ | |||
|
278 | .filter(id__gt=fav_threads[str(op.id)])\ | |||
|
279 | .first().get_absolute_url() | |||
|
280 | else: | |||
|
281 | new_post_count = 0 | |||
|
282 | fav_thread_dict['new_post_count'] = new_post_count | |||
|
283 | ||||
|
284 | fav_thread_dict['id'] = op.id | |||
|
285 | ||||
|
286 | fav_thread_dict['post_url'] = op.get_link_view() | |||
|
287 | fav_thread_dict['title'] = op.title | |||
|
288 | ||||
|
289 | posts.append(fav_thread_dict) | |||
|
290 | else: | |||
|
291 | fav_thread_dict = dict() | |||
|
292 | fav_thread_dict['new_post_count'] = \ | |||
|
293 | Thread.objects.get_new_post_count(ops) | |||
|
294 | posts.append(fav_thread_dict) | |||
|
295 | ||||
|
296 | return HttpResponse(content=json.dumps(posts)) |
@@ -7,9 +7,11 b' from django.utils import timezone' | |||||
7 | from django.utils.dateformat import format |
|
7 | from django.utils.dateformat import format | |
8 |
|
8 | |||
9 | from boards import utils, settings |
|
9 | from boards import utils, settings | |
|
10 | from boards.abstracts.settingsmanager import get_settings_manager | |||
10 | from boards.forms import PostForm, PlainErrorList |
|
11 | from boards.forms import PostForm, PlainErrorList | |
11 | from boards.models import Post |
|
12 | from boards.models import Post | |
12 | from boards.views.base import BaseBoardView, CONTEXT_FORM |
|
13 | from boards.views.base import BaseBoardView, CONTEXT_FORM | |
|
14 | from boards.views.mixins import DispatcherMixin | |||
13 | from boards.views.posting_mixin import PostMixin |
|
15 | from boards.views.posting_mixin import PostMixin | |
14 |
|
16 | |||
15 | import neboard |
|
17 | import neboard | |
@@ -24,6 +26,7 b" CONTEXT_WS_PORT = 'ws_port'" | |||||
24 | CONTEXT_WS_TIME = 'ws_token_time' |
|
26 | CONTEXT_WS_TIME = 'ws_token_time' | |
25 | CONTEXT_MODE = 'mode' |
|
27 | CONTEXT_MODE = 'mode' | |
26 | CONTEXT_OP = 'opening_post' |
|
28 | CONTEXT_OP = 'opening_post' | |
|
29 | CONTEXT_FAVORITE = 'is_favorite' | |||
27 |
|
30 | |||
28 | FORM_TITLE = 'title' |
|
31 | FORM_TITLE = 'title' | |
29 | FORM_TEXT = 'text' |
|
32 | FORM_TEXT = 'text' | |
@@ -31,7 +34,7 b" FORM_IMAGE = 'image'" | |||||
31 | FORM_THREADS = 'threads' |
|
34 | FORM_THREADS = 'threads' | |
32 |
|
35 | |||
33 |
|
36 | |||
34 | class ThreadView(BaseBoardView, PostMixin, FormMixin): |
|
37 | class ThreadView(BaseBoardView, PostMixin, FormMixin, DispatcherMixin): | |
35 |
|
38 | |||
36 | def get(self, request, post_id, form: PostForm=None): |
|
39 | def get(self, request, post_id, form: PostForm=None): | |
37 | try: |
|
40 | try: | |
@@ -39,6 +42,12 b' class ThreadView(BaseBoardView, PostMixi' | |||||
39 | except ObjectDoesNotExist: |
|
42 | except ObjectDoesNotExist: | |
40 | raise Http404 |
|
43 | raise Http404 | |
41 |
|
44 | |||
|
45 | # If the tag is favorite, update the counter | |||
|
46 | settings_manager = get_settings_manager(request) | |||
|
47 | favorite = settings_manager.thread_is_fav(opening_post) | |||
|
48 | if favorite: | |||
|
49 | settings_manager.add_or_read_fav_thread(opening_post) | |||
|
50 | ||||
42 | # If this is not OP, don't show it as it is |
|
51 | # If this is not OP, don't show it as it is | |
43 | if not opening_post.is_opening(): |
|
52 | if not opening_post.is_opening(): | |
44 | return redirect(opening_post.get_thread().get_opening_post() |
|
53 | return redirect(opening_post.get_thread().get_opening_post() | |
@@ -56,6 +65,7 b' class ThreadView(BaseBoardView, PostMixi' | |||||
56 | params[CONTEXT_THREAD] = thread_to_show |
|
65 | params[CONTEXT_THREAD] = thread_to_show | |
57 | params[CONTEXT_MODE] = self.get_mode() |
|
66 | params[CONTEXT_MODE] = self.get_mode() | |
58 | params[CONTEXT_OP] = opening_post |
|
67 | params[CONTEXT_OP] = opening_post | |
|
68 | params[CONTEXT_FAVORITE] = favorite | |||
59 |
|
69 | |||
60 | if settings.get_bool('External', 'WebsocketsEnabled'): |
|
70 | if settings.get_bool('External', 'WebsocketsEnabled'): | |
61 | token_time = format(timezone.now(), u'U') |
|
71 | token_time = format(timezone.now(), u'U') | |
@@ -78,6 +88,11 b' class ThreadView(BaseBoardView, PostMixi' | |||||
78 | if not opening_post.is_opening(): |
|
88 | if not opening_post.is_opening(): | |
79 | raise Http404 |
|
89 | raise Http404 | |
80 |
|
90 | |||
|
91 | if 'method' in request.POST: | |||
|
92 | self.dispatch_method(request, opening_post) | |||
|
93 | ||||
|
94 | return redirect('thread', post_id) # FIXME Different for different modes | |||
|
95 | ||||
81 | if not opening_post.get_thread().archived: |
|
96 | if not opening_post.get_thread().archived: | |
82 | form = PostForm(request.POST, request.FILES, |
|
97 | form = PostForm(request.POST, request.FILES, | |
83 | error_class=PlainErrorList) |
|
98 | error_class=PlainErrorList) | |
@@ -138,3 +153,11 b' class ThreadView(BaseBoardView, PostMixi' | |||||
138 |
|
153 | |||
139 | def get_mode(self) -> str: |
|
154 | def get_mode(self) -> str: | |
140 | pass |
|
155 | pass | |
|
156 | ||||
|
157 | def subscribe(self, request, opening_post): | |||
|
158 | settings_manager = get_settings_manager(request) | |||
|
159 | settings_manager.add_or_read_fav_thread(opening_post) | |||
|
160 | ||||
|
161 | def unsubscribe(self, request, opening_post): | |||
|
162 | settings_manager = get_settings_manager(request) | |||
|
163 | settings_manager.del_fav_thread(opening_post) |
General Comments 0
You need to be logged in to leave comments.
Login now