##// END OF EJS Templates
Merged with default branch
neko259 -
r1360:94773499 merge decentral
parent child Browse files
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.9.0 Claire
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 = _('Please wait %s seconds before sending message')
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 self.validate_file_size(file.size)
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 self.validate_file_size(file.size)
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 LAST_POST_TIME in self.session:
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 if length_header:
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 for tag in tags.split():
327 tag_set = set()
358 try:
328 for tag_string in tags.split():
359 Tag.objects.get(name=tag.strip().lower(), required=True)
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-08-22 15:07+0300\n"
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:31
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:32
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:34
50 #: forms.py:33
51 msgid "Title"
51 msgid "Title"
52 msgstr "Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ"
52 msgstr "Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ"
53
53
54 #: forms.py:35
54 #: forms.py:34
55 msgid "Text"
55 msgid "Text"
56 msgstr "ВСкст"
56 msgstr "ВСкст"
57
57
58 #: forms.py:36
58 #: forms.py:35
59 msgid "Tag"
59 msgid "Tag"
60 msgstr "ΠœΠ΅Ρ‚ΠΊΠ°"
60 msgstr "ΠœΠ΅Ρ‚ΠΊΠ°"
61
61
62 #: forms.py:37 templates/boards/base.html:40 templates/search/search.html:7
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:143
70 #: forms.py:142
76 msgid "File URL"
71 msgid "File URL"
77 msgstr "URL Ρ„Π°ΠΉΠ»Π°"
72 msgstr "URL Ρ„Π°ΠΉΠ»Π°"
78
73
79 #: forms.py:149
74 #: forms.py:148
80 msgid "e-mail"
75 msgid "e-mail"
81 msgstr ""
76 msgstr ""
82
77
83 #: forms.py:152
78 #: forms.py:151
84 msgid "Additional threads"
79 msgid "Additional threads"
85 msgstr "Π”ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Π΅ Ρ‚Π΅ΠΌΡ‹"
80 msgstr "Π”ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Π΅ Ρ‚Π΅ΠΌΡ‹"
86
81
87 #: forms.py:155
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:174
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:194
92 #: forms.py:192
102 msgid "Invalid URL"
93 msgid "Invalid URL"
103 msgstr "НСвСрный URL"
94 msgstr "НСвСрный URL"
104
95
105 #: forms.py:215
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:251
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:342
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:356
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:368
117 #: forms.py:350
132 msgid "Theme"
118 msgid "Theme"
133 msgstr "Π’Π΅ΠΌΠ°"
119 msgstr "Π’Π΅ΠΌΠ°"
134
120
135 #: forms.py:369
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:370
125 #: forms.py:352
141 msgid "User name"
126 msgid "User name"
142 msgstr "Имя ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ"
127 msgstr "Имя ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ"
143
128
144 #: forms.py:371
129 #: forms.py:353
145 msgid "Time zone"
130 msgid "Time zone"
146 msgstr "Часовой пояс"
131 msgstr "Часовой пояс"
147
132
148 #: forms.py:377
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:71
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:79
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:81
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:96 templates/boards/feed.html:30
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:110
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:128 templates/boards/feed.html:40
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:133
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:139
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:144 templates/boards/preview.html:16
196 #: templates/boards/all_threads.html:138 templates/boards/preview.html:16
205 #: templates/boards/thread_normal.html:38
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:149
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:151 templates/boards/preview.html:6
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:167 templates/boards/feed.html:53
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:66
281 #: templates/boards/base.html:79
290 msgid "Admin"
282 msgid "Admin"
291 msgstr "АдминистрированиС"
283 msgstr "АдминистрированиС"
292
284
293 #: templates/boards/base.html:68
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:70
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:30
298 #: templates/boards/post.html:32
307 msgid "Open"
299 msgid "Open"
308 msgstr "ΠžΡ‚ΠΊΡ€Ρ‹Ρ‚ΡŒ"
300 msgstr "ΠžΡ‚ΠΊΡ€Ρ‹Ρ‚ΡŒ"
309
301
310 #: templates/boards/post.html:32 templates/boards/post.html.py:43
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:38
306 #: templates/boards/post.html:40
315 msgid " in "
307 msgid " in "
316 msgstr " Π² "
308 msgstr " Π² "
317
309
318 #: templates/boards/post.html:48
310 #: templates/boards/post.html:50
319 msgid "Edit"
311 msgid "Edit"
320 msgstr "Π˜Π·ΠΌΠ΅Π½ΠΈΡ‚ΡŒ"
312 msgstr "Π˜Π·ΠΌΠ΅Π½ΠΈΡ‚ΡŒ"
321
313
322 #: templates/boards/post.html:50
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:97
318 #: templates/boards/post.html:94
327 msgid "Replies"
319 msgid "Replies"
328 msgstr "ΠžΡ‚Π²Π΅Ρ‚Ρ‹"
320 msgstr "ΠžΡ‚Π²Π΅Ρ‚Ρ‹"
329
321
330 #: templates/boards/post.html:109 templates/boards/thread.html:34
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:27
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:29
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:17
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:31
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:31
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:46
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: 2014-07-02 13:26+0300\n"
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/refpopup.js:58
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:77
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:279
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 refuild_refmap(apps, schema_editor):
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 post.build_refmap()
12 refposts = list()
13 for refpost in post.referenced_posts.all():
14 result = '<a href="{}">&gt;&gt;{}</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(refuild_refmap),
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(FILE_STUB_IMAGE))
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 import logging
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 = boards.models.Ban.objects.filter(ip=ip).exists()
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 if opening_post is None:
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, global_id=global_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('post'))['num_posts']
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 id=self.id).order_by('?')[:RELATED_TAGS_COUNT])
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.post_set.all():
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 > win_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', $(el).attr('href'))
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/i_w) * (cx - parseInt($(img_pv).position().left, 10)), 10),
142 left: parseInt(cx - (newIW/oldWidth) * (cx - parseInt(oldPosition.left, 10)), 10),
122 top: parseInt(cy - (newIH/i_h) * (cy - parseInt($(img_pv).position().top, 10)), 10)
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 = $('.post');
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 replyCountField.text(getReplyCount());
246 var replyCount = getReplyCount();
248 imageCountField.text(getImageCount());
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 related_tags %}
76 {% if tag.get_parent %}
77 <p>{% trans 'Related tags:' %}
77 <p>
78 {% for rel_tag in related_tags %}
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 {% endfor %}
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 is_opening=True thread=thread truncated=True need_open_link=True %}
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 with count=skipped_replies_count %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %}
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 is_opening=False moderator=moderator truncated=True %}
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|striptags|truncatewords:5 }}</span>
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 {% if post.images.exists %}
64 {% for image in post.images.all %}
65 {% with post.images.first as image %}
66 {{ image.get_view|safe }}
65 {{ image.get_view|safe }}
67 {% endwith %}
66 {% endfor %}
68 {% endif %}
67 {% for file in post.attachments.all %}
69 {% if post.attachments.exists %}
70 {% with post.attachments.first as file %}
71 {{ file.get_view|safe }}
68 {{ file.get_view|safe }}
72 {% endwith %}
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 {{ thread.get_reply_count }} {% trans 'messages' %},
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|striptags|truncatewords:10 }}
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="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}.
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_strings = data[FORM_TAGS]
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 thread = get_object_or_404(Post, id=thread_id).get_thread()
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(post.id, diff_type,
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(opening_post.id, include_last_update=True)
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(post.id))
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(post.id))
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)
@@ -1,5 +1,6 b''
1 httplib2
1 httplib2
2 simplejson
2 simplejson
3 pytube
3 requests
4 requests
4 adjacent
5 adjacent
5 django-haystack
6 django-haystack
General Comments 0
You need to be logged in to leave comments. Login now