##// 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
1 NO CONTENT: new file 100644, binary diff hidden
1 NO CONTENT: new file 100644, binary diff hidden
@@ -34,3 +34,4 b' dfb6c481b1a2c33705de9a9b5304bc924c46b202'
34 34 4a5bec08ccfb47a27f9e98698f12dd5b7246623b 2.8.2
35 35 604935b98f5b5e4a5e903594f048046e1fbb3519 2.8.3
36 36 c48ffdc671566069ed0f33644da1229277f3cd18 2.9.0
37 d66dc192d4e089ba85325afeef5229b73cb0fde4 2.10.0
@@ -1,4 +1,5 b''
1 1 from boards.models import Tag
2 from boards.models.thread import FAV_THREAD_NO_UPDATES
2 3
3 4 MAX_TRIPCODE_COLLISIONS = 50
4 5
@@ -11,6 +12,7 b" PERMISSION_MODERATE = 'moderator'"
11 12
12 13 SETTING_THEME = 'theme'
13 14 SETTING_FAVORITE_TAGS = 'favorite_tags'
15 SETTING_FAVORITE_THREADS = 'favorite_threads'
14 16 SETTING_HIDDEN_TAGS = 'hidden_tags'
15 17 SETTING_PERMISSIONS = 'permissions'
16 18 SETTING_USERNAME = 'username'
@@ -118,6 +120,28 b' class SettingsManager:'
118 120 tags.remove(tag.name)
119 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 146 class SessionSettingsManager(SettingsManager):
123 147 """
@@ -30,7 +30,10 b' class TagAdmin(admin.ModelAdmin):'
30 30 def thread_count(self, obj: Tag) -> int:
31 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 37 search_fields = ('name',)
35 38
36 39
@@ -46,7 +49,14 b' class ThreadAdmin(admin.ModelAdmin):'
46 49 def ip(self, obj: Thread):
47 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 60 list_filter = ('bump_time', 'archived', 'bumpable')
51 61 search_fields = ('id', 'title')
52 62 filter_horizontal = ('tags',)
@@ -1,5 +1,5 b''
1 1 [Version]
2 Version = 2.9.0 Claire
2 Version = 2.10.0 BT
3 3 SiteName = Neboard DEV
4 4
5 5 [Cache]
@@ -19,6 +19,7 b" CONTEXT_NEW_NOTIFICATIONS_COUNT = 'new_n"
19 19 CONTEXT_USERNAME = 'username'
20 20 CONTEXT_TAGS_STR = 'tags_str'
21 21 CONTEXT_IMAGE_VIEWER = 'image_viewer'
22 CONTEXT_HAS_FAV_THREADS = 'has_fav_threads'
22 23
23 24
24 25 def get_notifications(context, request):
@@ -43,6 +44,7 b' def user_and_ui_processor(request):'
43 44 settings_manager = get_settings_manager(request)
44 45 fav_tags = settings_manager.get_fav_tags()
45 46 context[CONTEXT_TAGS] = fav_tags
47
46 48 context[CONTEXT_TAGS_STR] = Tag.objects.get_tag_url_list(fav_tags)
47 49 theme = settings_manager.get_theme()
48 50 context[CONTEXT_THEME] = theme
@@ -58,6 +60,9 b' def user_and_ui_processor(request):'
58 60 SETTING_IMAGE_VIEWER,
59 61 default=settings.get('View', 'DefaultImageViewer'))
60 62
63 context[CONTEXT_HAS_FAV_THREADS] =\
64 len(settings_manager.get_fav_threads()) > 0
65
61 66 get_notifications(context, request)
62 67
63 68 return context
@@ -7,19 +7,17 b' from django import forms'
7 7 from django.core.files.uploadedfile import SimpleUploadedFile
8 8 from django.core.exceptions import ObjectDoesNotExist
9 9 from django.forms.util import ErrorList
10 from django.utils.translation import ugettext_lazy as _
11 import requests
10 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
12 11
13 12 from boards.mdx_neboard import formatters
13 from boards.models.attachment.downloaders import Downloader
14 14 from boards.models.post import TITLE_MAX_LENGTH
15 15 from boards.models import Tag, Post
16 from boards.utils import validate_file_size
16 17 from neboard import settings
17 18 import boards.settings as board_settings
18 19 import neboard
19 20
20 HEADER_CONTENT_LENGTH = 'content-length'
21 HEADER_CONTENT_TYPE = 'content-type'
22
23 21 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
24 22
25 23 VETERAN_POSTING_DELAY = 5
@@ -37,14 +35,11 b" LABEL_TEXT = _('Text')"
37 35 LABEL_TAG = _('Tag')
38 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 41 TAG_MAX_LENGTH = 20
43 42
44 FILE_DOWNLOAD_CHUNK_BYTES = 100000
45
46 HTTP_RESULT_OK = 200
47
48 43 TEXTAREA_ROWS = 4
49 44
50 45
@@ -182,7 +177,7 b' class PostForm(NeboardForm):'
182 177 file = self.cleaned_data['file']
183 178
184 179 if file:
185 self.validate_file_size(file.size)
180 validate_file_size(file.size)
186 181
187 182 return file
188 183
@@ -196,7 +191,7 b' class PostForm(NeboardForm):'
196 191 if not file:
197 192 raise forms.ValidationError(_('Invalid URL'))
198 193 else:
199 self.validate_file_size(file.size)
194 validate_file_size(file.size)
200 195
201 196 return file
202 197
@@ -272,9 +267,8 b' class PostForm(NeboardForm):'
272 267 now = time.time()
273 268
274 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 272 self.session[LAST_POST_TIME] = now
279 273
280 274 need_delay = True
@@ -285,8 +279,9 b' class PostForm(NeboardForm):'
285 279 need_delay = current_delay < posting_delay
286 280
287 281 if need_delay:
288 error_message = ERROR_SPEED % str(posting_delay
289 - current_delay)
282 delay = posting_delay - current_delay
283 error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL,
284 delay) % {'delay': delay}
290 285 self._errors['text'] = self.error_class([error_message])
291 286
292 287 can_post = False
@@ -294,13 +289,6 b' class PostForm(NeboardForm):'
294 289 if can_post:
295 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 292 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
305 293 """
306 294 Gets an file file from URL.
@@ -309,36 +297,18 b' class PostForm(NeboardForm):'
309 297 img_temp = None
310 298
311 299 try:
312 # Verify content headers
313 response_head = requests.head(url, verify=False)
314 content_type = response_head.headers[HEADER_CONTENT_TYPE].split(';')[0]
315 length_header = response_head.headers.get(HEADER_CONTENT_LENGTH)
316 if length_header:
317 length = int(length_header)
318 self.validate_file_size(length)
319 # Get the actual content into memory
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)
300 for downloader in Downloader.__subclasses__():
301 if downloader.handles(url):
302 return downloader.download(url)
303 # If nobody of the specific downloaders handles this, use generic
304 # one
305 return Downloader.download(url)
306 except forms.ValidationError as e:
307 raise e
336 308 except Exception as e:
337 309 # Just return no file
338 310 pass
339 311
340 return img_temp
341
342 312
343 313 class ThreadForm(PostForm):
344 314
@@ -354,20 +324,27 b' class ThreadForm(PostForm):'
354 324 _('Inappropriate characters in tags.'))
355 325
356 326 required_tag_exists = False
357 for tag in tags.split():
358 try:
359 Tag.objects.get(name=tag.strip().lower(), required=True)
327 tag_set = set()
328 for tag_string in tags.split():
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 339 required_tag_exists = True
361 340 break
362 except ObjectDoesNotExist:
363 pass
364 341
365 342 if not required_tag_exists:
366 343 all_tags = Tag.objects.filter(required=True)
367 344 raise forms.ValidationError(
368 345 _('Need at least one section.'))
369 346
370 return tags
347 return tag_set
371 348
372 349 def clean(self):
373 350 cleaned_data = super(ThreadForm, self).clean()
1 NO CONTENT: modified file, binary diff hidden
@@ -7,7 +7,7 b' msgid ""'
7 7 msgstr ""
8 8 "Project-Id-Version: PACKAGE VERSION\n"
9 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 11 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
12 12 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
13 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 38 msgid "designer"
39 39 msgstr "Π΄ΠΈΠ·Π°ΠΉΠ½Π΅Ρ€"
40 40
41 #: forms.py:31
41 #: forms.py:30
42 42 msgid "Type message here. Use formatting panel for more advanced usage."
43 43 msgstr ""
44 44 "Π’Π²ΠΎΠ΄ΠΈΡ‚Π΅ сообщСниС сюда. Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉΡ‚Π΅ панСль для Π±ΠΎΠ»Π΅Π΅ слоТного форматирования."
45 45
46 #: forms.py:32
46 #: forms.py:31
47 47 msgid "music images i_dont_like_tags"
48 48 msgstr "ΠΌΡƒΠ·Ρ‹ΠΊΠ° ΠΊΠ°Ρ€Ρ‚ΠΈΠ½ΠΊΠΈ Ρ‚Π΅Π³ΠΈ_Π½Π΅_Π½ΡƒΠΆΠ½Ρ‹"
49 49
50 #: forms.py:34
50 #: forms.py:33
51 51 msgid "Title"
52 52 msgstr "Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ"
53 53
54 #: forms.py:35
54 #: forms.py:34
55 55 msgid "Text"
56 56 msgstr "ВСкст"
57 57
58 #: forms.py:36
58 #: forms.py:35
59 59 msgid "Tag"
60 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 63 msgid "Search"
64 64 msgstr "Поиск"
65 65
66 #: forms.py:39
67 #, python-format
68 msgid "Please wait %s seconds before sending message"
69 msgstr "ΠŸΠΎΠΆΠ°Π»ΡƒΠΉΡΡ‚Π° ΠΏΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡ‚Π΅ %s сСкунд ΠΏΠ΅Ρ€Π΅Π΄ ΠΎΡ‚ΠΏΡ€Π°Π²ΠΊΠΎΠΉ сообщСния"
70
71 #: forms.py:140
66 #: forms.py:139
72 67 msgid "File"
73 68 msgstr "Π€Π°ΠΉΠ»"
74 69
75 #: forms.py:143
70 #: forms.py:142
76 71 msgid "File URL"
77 72 msgstr "URL Ρ„Π°ΠΉΠ»Π°"
78 73
79 #: forms.py:149
74 #: forms.py:148
80 75 msgid "e-mail"
81 76 msgstr ""
82 77
83 #: forms.py:152
78 #: forms.py:151
84 79 msgid "Additional threads"
85 80 msgstr "Π”ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Π΅ Ρ‚Π΅ΠΌΡ‹"
86 81
87 #: forms.py:155
88 msgid "Tripcode"
89 msgstr "Π’Ρ€ΠΈΠΏΠΊΠΎΠ΄"
90
91 #: forms.py:164
82 #: forms.py:162
92 83 #, python-format
93 84 msgid "Title must have less than %s characters"
94 85 msgstr "Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ Π΄ΠΎΠ»ΠΆΠ΅Π½ ΠΈΠΌΠ΅Ρ‚ΡŒ мСньшС %s символов"
95 86
96 #: forms.py:174
87 #: forms.py:172
97 88 #, python-format
98 89 msgid "Text must have less than %s characters"
99 90 msgstr "ВСкст Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±Ρ‹Ρ‚ΡŒ ΠΊΠΎΡ€ΠΎΡ‡Π΅ %s символов"
100 91
101 #: forms.py:194
92 #: forms.py:192
102 93 msgid "Invalid URL"
103 94 msgstr "НСвСрный URL"
104 95
105 #: forms.py:215
96 #: forms.py:213
106 97 msgid "Invalid additional thread list"
107 98 msgstr "НСвСрный список Π΄ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Ρ… Ρ‚Π΅ΠΌ"
108 99
109 #: forms.py:251
100 #: forms.py:258
110 101 msgid "Either text or file must be entered."
111 102 msgstr "ВСкст ΠΈΠ»ΠΈ Ρ„Π°ΠΉΠ» Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ Π²Π²Π΅Π΄Π΅Π½Ρ‹."
112 103
113 #: forms.py:289
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
104 #: forms.py:317 templates/boards/all_threads.html:148
119 105 #: templates/boards/rss/post.html:10 templates/boards/tags.html:6
120 106 msgid "Tags"
121 107 msgstr "ΠœΠ΅Ρ‚ΠΊΠΈ"
122 108
123 #: forms.py:342
109 #: forms.py:324
124 110 msgid "Inappropriate characters in tags."
125 111 msgstr "НСдопустимыС символы Π² ΠΌΠ΅Ρ‚ΠΊΠ°Ρ…."
126 112
127 #: forms.py:356
113 #: forms.py:338
128 114 msgid "Need at least one section."
129 115 msgstr "НуТСн хотя Π±Ρ‹ ΠΎΠ΄ΠΈΠ½ Ρ€Π°Π·Π΄Π΅Π»."
130 116
131 #: forms.py:368
117 #: forms.py:350
132 118 msgid "Theme"
133 119 msgstr "Π’Π΅ΠΌΠ°"
134 120
135 #: forms.py:369
136 #| msgid "Image view mode"
121 #: forms.py:351
137 122 msgid "Image view mode"
138 123 msgstr "Π Π΅ΠΆΠΈΠΌ просмотра ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ"
139 124
140 #: forms.py:370
125 #: forms.py:352
141 126 msgid "User name"
142 127 msgstr "Имя ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ"
143 128
144 #: forms.py:371
129 #: forms.py:353
145 130 msgid "Time zone"
146 131 msgstr "Часовой пояс"
147 132
148 #: forms.py:377
133 #: forms.py:359
149 134 msgid "Inappropriate characters."
150 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 146 msgid "Related message"
162 147 msgstr "БвязанноС сообщСниС"
163 148
164 #: templates/boards/all_threads.html:71
149 #: templates/boards/all_threads.html:69
165 150 msgid "Edit tag"
166 151 msgstr "Π˜Π·ΠΌΠ΅Π½ΠΈΡ‚ΡŒ ΠΌΠ΅Ρ‚ΠΊΡƒ"
167 152
168 #: templates/boards/all_threads.html:79
153 #: templates/boards/all_threads.html:75
169 154 #, python-format
170 155 msgid ""
171 156 "This tag has %(thread_count)s threads (%(active_thread_count)s active) and "
@@ -174,54 +159,61 b' msgstr ""'
174 159 "Π‘ этой ΠΌΠ΅Ρ‚ΠΊΠΎΠΉ Π΅ΡΡ‚ΡŒ %(thread_count)s Ρ‚Π΅ΠΌ (%(active_thread_count)s Π°ΠΊΡ‚ΠΈΠ²Π½Ρ‹Ρ…) ΠΈ "
175 160 "%(post_count)s сообщСний."
176 161
177 #: templates/boards/all_threads.html:81
162 #: templates/boards/all_threads.html:77
178 163 msgid "Related tags:"
179 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 167 #: templates/boards/notifications.html:17 templates/search/search.html:26
183 168 msgid "Previous page"
184 169 msgstr "ΠŸΡ€Π΅Π΄Ρ‹Π΄ΡƒΡ‰Π°Ρ страница"
185 170
186 #: templates/boards/all_threads.html:110
171 #: templates/boards/all_threads.html:104
187 172 #, python-format
188 msgid "Skipped %(count)s replies. Open thread to see all replies."
189 msgstr "ΠŸΡ€ΠΎΠΏΡƒΡ‰Π΅Π½ΠΎ %(count)s ΠΎΡ‚Π²Π΅Ρ‚ΠΎΠ². ΠžΡ‚ΠΊΡ€ΠΎΠΉΡ‚Π΅ Ρ‚Ρ€Π΅Π΄, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΡƒΠ²ΠΈΠ΄Π΅Ρ‚ΡŒ всС ΠΎΡ‚Π²Π΅Ρ‚Ρ‹."
173 #| msgid "Skipped %(count)s replies. Open thread to see all replies."
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 184 #: templates/boards/notifications.html:27 templates/search/search.html:37
193 185 msgid "Next page"
194 186 msgstr "Π‘Π»Π΅Π΄ΡƒΡŽΡ‰Π°Ρ страница"
195 187
196 #: templates/boards/all_threads.html:133
188 #: templates/boards/all_threads.html:127
197 189 msgid "No threads exist. Create the first one!"
198 190 msgstr "НСт Ρ‚Π΅ΠΌ. Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ ΠΏΠ΅Ρ€Π²ΡƒΡŽ!"
199 191
200 #: templates/boards/all_threads.html:139
192 #: templates/boards/all_threads.html:133
201 193 msgid "Create new thread"
202 194 msgstr "Π‘ΠΎΠ·Π΄Π°Ρ‚ΡŒ Π½ΠΎΠ²ΡƒΡŽ Ρ‚Π΅ΠΌΡƒ"
203 195
204 #: templates/boards/all_threads.html:144 templates/boards/preview.html:16
205 #: templates/boards/thread_normal.html:38
196 #: templates/boards/all_threads.html:138 templates/boards/preview.html:16
197 #: templates/boards/thread_normal.html:51
206 198 msgid "Post"
207 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 208 msgid "Tags must be delimited by spaces. Text or image is required."
211 209 msgstr ""
212 210 "ΠœΠ΅Ρ‚ΠΊΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ Ρ€Π°Π·Π΄Π΅Π»Π΅Π½Ρ‹ ΠΏΡ€ΠΎΠ±Π΅Π»Π°ΠΌΠΈ. ВСкст ΠΈΠ»ΠΈ ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹."
213 211
214 #: templates/boards/all_threads.html:151 templates/boards/preview.html:6
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
212 #: templates/boards/all_threads.html:147 templates/boards/thread_normal.html:58
221 213 msgid "Text syntax"
222 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 217 msgid "Pages:"
226 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 278 msgid "Settings"
287 279 msgstr "Настройки"
288 280
289 #: templates/boards/base.html:66
281 #: templates/boards/base.html:79
290 282 msgid "Admin"
291 283 msgstr "АдминистрированиС"
292 284
293 #: templates/boards/base.html:68
285 #: templates/boards/base.html:81
294 286 #, python-format
295 287 msgid "Speed: %(ppd)s posts per day"
296 288 msgstr "Π‘ΠΊΠΎΡ€ΠΎΡΡ‚ΡŒ: %(ppd)s сообщСний Π² дСнь"
297 289
298 #: templates/boards/base.html:70
290 #: templates/boards/base.html:83
299 291 msgid "Up"
300 292 msgstr "Π’Π²Π΅Ρ€Ρ…"
301 293
@@ -303,37 +295,52 b' msgstr "\xd0\x92\xd0\xb2\xd0\xb5\xd1\x80\xd1\x85"'
303 295 msgid "No posts exist. Create the first one!"
304 296 msgstr "НСт сообщСний. Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ ΠΏΠ΅Ρ€Π²ΠΎΠ΅!"
305 297
306 #: templates/boards/post.html:30
298 #: templates/boards/post.html:32
307 299 msgid "Open"
308 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 303 msgid "Reply"
312 304 msgstr "ΠžΡ‚Π²Π΅Ρ‚ΠΈΡ‚ΡŒ"
313 305
314 #: templates/boards/post.html:38
306 #: templates/boards/post.html:40
315 307 msgid " in "
316 308 msgstr " Π² "
317 309
318 #: templates/boards/post.html:48
310 #: templates/boards/post.html:50
319 311 msgid "Edit"
320 312 msgstr "Π˜Π·ΠΌΠ΅Π½ΠΈΡ‚ΡŒ"
321 313
322 #: templates/boards/post.html:50
314 #: templates/boards/post.html:52
323 315 msgid "Edit thread"
324 316 msgstr "Π˜Π·ΠΌΠ΅Π½ΠΈΡ‚ΡŒ Ρ‚Π΅ΠΌΡƒ"
325 317
326 #: templates/boards/post.html:97
318 #: templates/boards/post.html:94
327 319 msgid "Replies"
328 320 msgstr "ΠžΡ‚Π²Π΅Ρ‚Ρ‹"
329 321
330 #: templates/boards/post.html:109 templates/boards/thread.html:34
331 msgid "messages"
332 msgstr "сообщСний"
322 #: templates/boards/post.html:105
323 #, python-format
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
335 msgid "images"
336 msgstr "ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ"
330 #, python-format
331 msgid "Please wait %(delay)d second before sending message"
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 345 #: templates/boards/rss/post.html:5
339 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 354 msgid "Hidden tags:"
348 355 msgstr "Π‘ΠΊΡ€Ρ‹Ρ‚Ρ‹Π΅ ΠΌΠ΅Ρ‚ΠΊΠΈ:"
349 356
350 #: templates/boards/settings.html:27
357 #: templates/boards/settings.html:25
351 358 msgid "No hidden tags."
352 359 msgstr "НСт скрытых ΠΌΠ΅Ρ‚ΠΎΠΊ."
353 360
354 #: templates/boards/settings.html:29
355 msgid "Tripcode:"
356 msgstr "Π’Ρ€ΠΈΠΏΠΊΠΎΠ΄:"
357
358 #: templates/boards/settings.html:29
359 msgid "reset"
360 msgstr "ΡΠ±Ρ€ΠΎΡΠΈΡ‚ΡŒ"
361
362 #: templates/boards/settings.html:37
361 #: templates/boards/settings.html:34
363 362 msgid "Save"
364 363 msgstr "Π‘ΠΎΡ…Ρ€Π°Π½ΠΈΡ‚ΡŒ"
365 364
@@ -434,6 +433,20 b' msgid "Tree"'
434 433 msgstr "Π”Π΅Ρ€Π΅Π²ΠΎ"
435 434
436 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 450 msgid "Last update: "
438 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 454 msgid "No images."
442 455 msgstr "НСт ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ."
443 456
444 #: templates/boards/thread_normal.html:17
457 #: templates/boards/thread_normal.html:30
445 458 msgid "posts to bumplimit"
446 459 msgstr "сообщСний Π΄ΠΎ Π±Π°ΠΌΠΏΠ»ΠΈΠΌΠΈΡ‚Π°"
447 460
448 #: templates/boards/thread_normal.html:31
461 #: templates/boards/thread_normal.html:44
449 462 msgid "Reply to thread"
450 463 msgstr "ΠžΡ‚Π²Π΅Ρ‚ΠΈΡ‚ΡŒ Π² Ρ‚Π΅ΠΌΡƒ"
451 464
452 #: templates/boards/thread_normal.html:31
465 #: templates/boards/thread_normal.html:44
453 466 msgid "to message "
454 467 msgstr "Π½Π° сообщСниС"
455 468
456 #: templates/boards/thread_normal.html:46
469 #: templates/boards/thread_normal.html:59
457 470 msgid "Close form"
458 471 msgstr "Π—Π°ΠΊΡ€Ρ‹Ρ‚ΡŒ Ρ„ΠΎΡ€ΠΌΡƒ"
459 472
460 473 #: templates/search/search.html:17
461 474 msgid "Ok"
462 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
@@ -8,7 +8,7 b' msgid ""'
8 8 msgstr ""
9 9 "Project-Id-Version: PACKAGE VERSION\n"
10 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 12 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
13 13 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
14 14 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -19,14 +19,37 b' msgstr ""'
19 19 "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
20 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 27 msgid "Loading..."
24 28 msgstr "Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ°..."
25 29
26 #: static/js/refpopup.js:77
30 #: static/js/refpopup.js:91
27 31 msgid "Post not found"
28 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 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 7 class Migration(migrations.Migration):
8 8
9 def refuild_refmap(apps, schema_editor):
9 def rebuild_refmap(apps, schema_editor):
10 10 Post = apps.get_model('boards', 'Post')
11 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 20 post.save(update_fields=['refmap'])
14 21
15 22 dependencies = [
@@ -17,5 +24,5 b' class Migration(migrations.Migration):'
17 24 ]
18 25
19 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 6 FILE_TYPES_VIDEO = (
7 7 'webm',
8 8 'mp4',
9 'mpeg',
9 10 )
10 11 FILE_TYPE_SVG = 'svg'
11 12 FILE_TYPES_AUDIO = (
12 13 'ogg',
13 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 25 def get_viewers():
18 26 return AbstractViewer.__subclasses__()
@@ -35,9 +43,15 b' class AbstractViewer:'
35 43 self.file_type, filesizeformat(self.file.size))
36 44
37 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 52 return '<a href="{}">'\
39 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 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 2 import re
5 3 import uuid
@@ -11,21 +9,15 b' from django.db.models import TextField, '
11 9 from django.template.loader import render_to_string
12 10 from django.utils import timezone
13 11
12 from boards import settings
14 13 from boards.abstracts.tripcode import Tripcode
15 14 from boards.mdx_neboard import Parser
16 from boards.models import KeyPair, GlobalId
17 from boards import settings
18 from boards.models import PostImage, Attachment
15 from boards.models import PostImage, Attachment, KeyPair, GlobalId
19 16 from boards.models.base import Viewable
20 17 from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
21 18 from boards.models.post.manager import PostManager
22 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 21 APP_LABEL_BOARDS = 'boards'
30 22
31 23 BAN_REASON_AUTO = 'Auto'
@@ -80,7 +72,7 b' class Post(models.Model, Viewable):'
80 72 images = models.ManyToManyField(PostImage, null=True, blank=True,
81 73 related_name='post_images', db_index=True)
82 74 attachments = models.ManyToManyField(Attachment, null=True, blank=True,
83 related_name='attachment_posts')
75 related_name='attachment_posts')
84 76
85 77 poster_ip = models.GenericIPAddressField()
86 78
@@ -92,7 +84,8 b' class Post(models.Model, Viewable):'
92 84 blank=True, related_name='refposts',
93 85 db_index=True)
94 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 89 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
97 90
98 91 url = models.TextField()
@@ -103,23 +96,23 b' class Post(models.Model, Viewable):'
103 96 global_id = models.OneToOneField('GlobalId', null=True, blank=True)
104 97
105 98 tripcode = models.CharField(max_length=50, null=True)
99 opening = models.BooleanField()
106 100
107 101 def __str__(self):
108 return 'P#{}/{}'.format(self.id, self.title)
102 return 'P#{}/{}'.format(self.id, self.get_title())
109 103
110 104 def get_referenced_posts(self):
111 105 threads = self.get_threads().all()
112 return self.referenced_posts.filter(threads__in=threads) \
106 return self.referenced_posts.filter(threads__in=threads)\
113 107 .order_by('pub_time').distinct().all()
114 108
115 109 def get_title(self) -> str:
116 """
117 Gets original post title or part of its text.
118 """
110 return self.title
119 111
120 title = self.title
112 def get_title_or_text(self):
113 title = self.get_title()
121 114 if not title:
122 title = self.get_text()
115 title = truncatewords(striptags(self.get_text()), TITLE_MAX_WORDS)
123 116
124 117 return title
125 118
@@ -142,7 +135,7 b' class Post(models.Model, Viewable):'
142 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 140 def get_absolute_url(self):
148 141 if self.url:
@@ -172,12 +165,6 b' class Post(models.Model, Viewable):'
172 165 """
173 166
174 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 169 css_class = 'post'
183 170 if thread.archived:
@@ -192,10 +179,9 b' class Post(models.Model, Viewable):'
192 179
193 180 params.update({
194 181 PARAMETER_POST: self,
195 PARAMETER_IS_OPENING: is_opening,
182 PARAMETER_IS_OPENING: self.is_opening(),
196 183 PARAMETER_THREAD: thread,
197 184 PARAMETER_CSS_CLASS: css_class,
198 PARAMETER_OP_ID: opening_post_id,
199 185 })
200 186
201 187 return render_to_string('boards/post.html', params)
@@ -336,7 +322,7 b' class Post(models.Model, Viewable):'
336 322 for thread in self.get_threads().all():
337 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 327 super().save(force_insert, force_update, using, update_fields)
342 328
@@ -418,7 +404,7 b' class Post(models.Model, Viewable):'
418 404 """
419 405
420 406 result = '<a href="{}">&gt;&gt;{}</a>'.format(self.get_absolute_url(),
421 self.id)
407 self.id)
422 408 if self.is_opening():
423 409 result = '<b>{}</b>'.format(result)
424 410
@@ -6,6 +6,7 b' from django.utils import timezone'
6 6 from boards import utils
7 7 from boards.mdx_neboard import Parser
8 8 from boards.models import PostImage, Attachment
9 from boards.models.user import Ban
9 10 import boards.models
10 11
11 12 __author__ = 'vurdalak'
@@ -31,7 +32,7 b' class PostManager(models.Manager):'
31 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 37 # TODO Raise specific exception and catch it in the views
37 38 if is_banned:
@@ -59,7 +60,8 b' class PostManager(models.Manager):'
59 60 poster_ip=ip,
60 61 thread=thread,
61 62 last_edit_time=posting_time,
62 tripcode=tripcode)
63 tripcode=tripcode,
64 opening=new_thread)
63 65 post.threads.add(thread)
64 66
65 67 logger = logging.getLogger('boards.post.create')
@@ -122,7 +124,8 b' class PostManager(models.Manager):'
122 124 @transaction.atomic
123 125 def import_post(self, title: str, text: str, pub_time: str, global_id,
124 126 opening_post=None, tags=list()):
125 if opening_post is None:
127 is_opening = opening_post is None
128 if is_opening:
126 129 thread = boards.models.thread.Thread.objects.create(
127 130 bump_time=pub_time, last_edit_time=pub_time)
128 131 list(map(thread.tags.add, tags))
@@ -133,7 +136,9 b' class PostManager(models.Manager):'
133 136 pub_time=pub_time,
134 137 poster_ip=NO_IP,
135 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 143 post.build_url()
139 144 post.connect_replies()
@@ -1,3 +1,4 b''
1 import hashlib
1 2 from django.template.loader import render_to_string
2 3 from django.db import models
3 4 from django.db.models import Count
@@ -47,6 +48,8 b' class Tag(models.Model, Viewable):'
47 48 required = models.BooleanField(default=False, db_index=True)
48 49 description = models.TextField(blank=True)
49 50
51 parent = models.ForeignKey('Tag', null=True, related_name='children')
52
50 53 def __str__(self):
51 54 return self.name
52 55
@@ -89,7 +92,7 b' class Tag(models.Model, Viewable):'
89 92
90 93 @cached_result()
91 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 97 def get_description(self):
95 98 return self.description
@@ -106,4 +109,26 b' class Tag(models.Model, Viewable):'
106 109
107 110 def get_related_tags(self):
108 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 1 import logging
2 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 5 from django.utils import timezone
6 6 from django.db import models
7 7
@@ -11,6 +11,8 b' from boards.utils import cached_result, '
11 11 from boards.models.post import Post
12 12 from boards.models.tag import Tag
13 13
14 FAV_THREAD_NO_UPDATES = -1
15
14 16
15 17 __author__ = 'neko259'
16 18
@@ -54,6 +56,26 b' class ThreadManager(models.Manager):'
54 56 thread.update_posts_time()
55 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 80 def get_thread_max_posts():
59 81 return settings.get_int('Messages', 'MaxPostsPerThread')
@@ -116,7 +138,7 b' class Thread(models.Model):'
116 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 143 def get_last_replies(self) -> QuerySet:
122 144 """
@@ -150,8 +172,8 b' class Thread(models.Model):'
150 172 Gets sorted thread posts
151 173 """
152 174
153 query = Post.objects.filter(threads__in=[self])
154 query = query.order_by('pub_time').prefetch_related('images', 'thread', 'threads')
175 query = self.multi_replies.order_by('pub_time').prefetch_related(
176 'images', 'thread', 'threads', 'attachments')
155 177 if view_fields_only:
156 178 query = query.defer('poster_ip')
157 179 return query.all()
@@ -203,7 +225,7 b' class Thread(models.Model):'
203 225 def update_posts_time(self, exclude_posts=None):
204 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 229 if exclude_posts is None or post not in exclude_posts:
208 230 # Manual update is required because uids are generated on save
209 231 post.last_edit_time = last_edit_time
@@ -228,3 +250,9 b' class Thread(models.Model):'
228 250
229 251 def get_required_tags(self):
230 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 134 .tripcode {
135 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 145 } No newline at end of file
@@ -115,6 +115,14 b' body {'
115 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 126 .header {
119 127 border-bottom: solid 2px #ccc;
120 128 margin-bottom: 5px;
@@ -416,6 +424,8 b' li {'
416 424 padding: 5px;
417 425 color: #eee;
418 426 font-size: 2ex;
427 margin-top: .5ex;
428 margin-bottom: .5ex;
419 429 }
420 430
421 431 .skipped_replies {
@@ -442,6 +452,7 b' li {'
442 452 border: solid 1px;
443 453 margin: 0.5ex;
444 454 text-align: center;
455 padding: 1ex;
445 456 }
446 457
447 458 code {
@@ -520,6 +531,11 b' ul {'
520 531 border: 1px solid #777;
521 532 background: #000;
522 533 padding: 4px;
534 opacity: 0.3;
535 }
536
537 #up:hover {
538 opacity: 1;
523 539 }
524 540
525 541 .user-cast {
@@ -557,3 +573,7 b' ul {'
557 573 .tripcode {
558 574 color: white;
559 575 }
576
577 #fav-panel {
578 border: 1px solid white;
579 }
@@ -23,6 +23,8 b''
23 23 for the JavaScript code in this page.
24 24 */
25 25
26 var IMAGE_POPUP_MARGIN = 10;
27
26 28
27 29 var IMAGE_VIEWERS = [
28 30 ['simple', new SimpleImageViewer()],
@@ -31,6 +33,8 b' var IMAGE_VIEWERS = ['
31 33
32 34 var FULL_IMG_CLASS = 'post-image-full';
33 35
36 var ATTR_SCALE = 'scale';
37
34 38
35 39 function ImageViewer() {}
36 40 ImageViewer.prototype.view = function (post) {};
@@ -67,38 +71,48 b' SimpleImageViewer.prototype.view = funct'
67 71
68 72 function PopupImageViewer() {}
69 73 PopupImageViewer.prototype.view = function (post) {
70 var margin = 20; //..change
71
72 74 var el = post;
73 75 var thumb_id = 'full' + el.find('img').attr('alt');
74 76
75 77 var existingPopups = $('#' + thumb_id);
76 if(!existingPopups.length) {
78 if (!existingPopups.length) {
77 79 var imgElement= el.find('img');
78 80
79 var img_w = imgElement.attr('data-width');
80 var img_h = imgElement.attr('data-height');
81 var full_img_w = imgElement.attr('data-width');
82 var full_img_h = imgElement.attr('data-height');
81 83
82 84 var win = $(window);
83 85
84 86 var win_w = win.width();
85 87 var win_h = win.height();
86 //new image size
87 if (img_w > win_w) {
88 img_h = img_h * (win_w/img_w) - margin;
89 img_w = win_w - margin;
88
89 // New image size
90 var w_scale = 1;
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) {
92 img_w = img_w * (win_h/img_h) - margin;
93 img_h = win_h - margin;
99 if (full_img_h > freeHeight) {
100 h_scale = full_img_h / freeHeight;
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 109 var img_pv = new Image();
97 110 var newImage = $(img_pv);
98 111 newImage.addClass('img-full')
99 112 .attr('id', thumb_id)
100 .attr('src', $(el).attr('href'))
101 .appendTo($(el))
113 .attr('src', postNode.attr('href'))
114 .attr(ATTR_SCALE, scale)
115 .appendTo(postNode)
102 116 .css({
103 117 'width': img_w,
104 118 'height': img_h,
@@ -107,19 +121,26 b' PopupImageViewer.prototype.view = functi'
107 121 })
108 122 //scaling preview
109 123 .mousewheel(function(event, delta) {
110 var cx = event.originalEvent.clientX,
111 cy = event.originalEvent.clientY,
112 i_w = parseFloat(newImage.width()),
113 i_h = parseFloat(newImage.height()),
114 newIW = i_w * (delta > 0 ? 1.25 : 0.8),
115 newIH = i_h * (delta > 0 ? 1.25 : 0.8);
124 var cx = event.originalEvent.clientX;
125 var cy = event.originalEvent.clientY;
126
127 var scale = newImage.attr(ATTR_SCALE) / (delta > 0 ? 1.25 : 0.8);
128
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 135 newImage.width(newIW);
118 136 newImage.height(newIH);
119 //set position
137 newImage.attr(ATTR_SCALE, scale);
138
139 // Set position
140 var oldPosition = newImage.position();
120 141 newImage.css({
121 left: parseInt(cx - (newIW/i_w) * (cx - parseInt($(img_pv).position().left, 10)), 10),
122 top: parseInt(cy - (newIH/i_h) * (cy - parseInt($(img_pv).position().top, 10)), 10)
142 left: parseInt(cx - (newIW/oldWidth) * (cx - parseInt(oldPosition.left, 10)), 10),
143 top: parseInt(cy - (newIH/oldHeight) * (cy - parseInt(oldPosition.top, 10)), 10)
123 144 });
124 145
125 146 return false;
@@ -23,6 +23,8 b''
23 23 for the JavaScript code in this page.
24 24 */
25 25
26 var FAV_POST_UPDATE_PERIOD = 10000;
27
26 28 /**
27 29 * An email is a hidden file to prevent spam bots from posting. It has to be
28 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 111 $( document ).ready(function() {
44 112 hideEmailFromForm();
45 113
@@ -53,4 +121,6 b' function highlightCode(node) {'
53 121 addRefLinkPreview();
54 122
55 123 highlightCode($(document));
124
125 initFavPanel();
56 126 });
@@ -102,11 +102,10 b' function connectWebsocket() {'
102 102 * missed.
103 103 */
104 104 function getThreadDiff() {
105 var lastUpdateTime = $('.metapanel').attr('data-last-update');
106 var lastPostId = $('.post').last().attr('id');
105 var all_posts = $('.post');
107 106
108 107 var uids = '';
109 var posts = $('.post');
108 var posts = all_posts;
110 109 for (var i = 0; i < posts.length; i++) {
111 110 uids += posts[i].getAttribute('data-uid') + ' ';
112 111 }
@@ -114,7 +113,7 b' function getThreadDiff() {'
114 113 var data = {
115 114 uids: uids,
116 115 thread: threadId
117 }
116 };
118 117
119 118 var diffUrl = '/api/diff_thread/';
120 119
@@ -244,8 +243,10 b' function updateMetadataPanel() {'
244 243 var replyCountField = $('#reply-count');
245 244 var imageCountField = $('#image-count');
246 245
247 replyCountField.text(getReplyCount());
248 imageCountField.text(getImageCount());
246 var replyCount = getReplyCount();
247 replyCountField.text(replyCount);
248 var imageCount = getImageCount();
249 imageCountField.text(imageCount);
249 250
250 251 var lastUpdate = $('.post:last').children('.post-info').first()
251 252 .children('.pub_time').first().html();
@@ -257,6 +258,9 b' function updateMetadataPanel() {'
257 258
258 259 blink(replyCountField);
259 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 384 // Replace children
381 385 var children = oldNode.children();
382 386 if (children.length == 0) {
383 console.log(oldContent);
384 console.log(newContent)
385
386 387 oldNode.replaceWith(newNode);
387 388 } else {
388 389 var newChildren = newNode.children();
@@ -426,7 +427,7 b' function updateNodeAttr(oldNode, newNode'
426 427 var newAttr = newNode.attr(attrName);
427 428 if (oldAttr != newAttr) {
428 429 oldNode.attr(attrName, newAttr);
429 };
430 }
430 431 }
431 432
432 433 $(document).ready(function(){
@@ -439,11 +440,11 b' function updateNodeAttr(oldNode, newNode'
439 440 if (form.length > 0) {
440 441 var options = {
441 442 beforeSubmit: function(arr, $form, options) {
442 showAsErrors($('form'), gettext('Sending message...'));
443 showAsErrors($('#form'), gettext('Sending message...'));
443 444 },
444 445 success: updateOnPost,
445 446 error: function() {
446 showAsErrors($('form'), gettext('Server error!'));
447 showAsErrors($('#form'), gettext('Server error!'));
447 448 },
448 449 url: '/api/add_post/' + threadId + '/'
449 450 };
@@ -37,7 +37,7 b''
37 37 {% endfor %}
38 38
39 39 {% if tag %}
40 <div class="tag_info">
40 <div class="tag_info" style="border-bottom: solid .5ex #{{ tag.get_color }}">
41 41 {% if random_image_post %}
42 42 <div class="tag-image">
43 43 {% with image=random_image_post.images.first %}
@@ -73,11 +73,12 b''
73 73 <p>{{ tag.get_description|safe }}</p>
74 74 {% endif %}
75 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 %}
77 <p>{% trans 'Related tags:' %}
78 {% for rel_tag in related_tags %}
79 {{ rel_tag.get_view|safe }}{% if not forloop.last %}, {% else %}.{% endif %}
80 {% endfor %}
76 {% if tag.get_parent %}
77 <p>
78 {% if tag.get_parent %}
79 {{ tag.get_parent.get_view|safe }} /
80 {% endif %}
81 {{ tag.get_view|safe }}
81 82 </p>
82 83 {% endif %}
83 84 </div>
@@ -93,7 +94,7 b''
93 94
94 95 {% for thread in threads %}
95 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 98 {% if not thread.archived %}
98 99 {% with last_replies=thread.get_last_replies %}
99 100 {% if last_replies %}
@@ -101,14 +102,14 b''
101 102 {% if skipped_replies_count %}
102 103 <div class="skipped_replies">
103 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 106 </a>
106 107 </div>
107 108 {% endif %}
108 109 {% endwith %}
109 110 <div class="last-replies">
110 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 113 {% endfor %}
113 114 </div>
114 115 {% endif %}
@@ -39,7 +39,10 b''
39 39 <a href="{% url 'tags' 'required'%}" title="{% trans 'Tag management' %}">{% trans "tags" %}</a>,
40 40 <a href="{% url 'search' %}" title="{% trans 'Search' %}">{% trans 'search' %}</a>,
41 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 47 {% if username %}
45 48 <a class="right-link link" href="{% url 'notifications' username %}" title="{% trans 'Notifications' %}">
@@ -53,6 +56,8 b''
53 56 <a class="right-link link" href="{% url 'settings' %}">{% trans 'Settings' %}</a>
54 57 </div>
55 58
59 <div id="fav-panel"><div class="post">{% trans "Loading..." %}</div></div>
60
56 61 {% block content %}{% endblock %}
57 62
58 63 <script src="{% static 'js/3party/highlight.min.js' %}"></script>
@@ -36,7 +36,7 b''
36 36 {% else %}
37 37 {% if need_op_data %}
38 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 40 {% endwith %}
41 41 {% endif %}
42 42 {% endif %}
@@ -61,16 +61,12 b''
61 61 Post images. Currently only 1 image can be posted and shown, but post model
62 62 supports multiple.
63 63 {% endcomment %}
64 {% if post.images.exists %}
65 {% with post.images.first as image %}
66 {{ image.get_view|safe }}
67 {% endwith %}
68 {% endif %}
69 {% if post.attachments.exists %}
70 {% with post.attachments.first as file %}
71 {{ file.get_view|safe }}
72 {% endwith %}
73 {% endif %}
64 {% for image in post.images.all %}
65 {{ image.get_view|safe }}
66 {% endfor %}
67 {% for file in post.attachments.all %}
68 {{ file.get_view|safe }}
69 {% endfor %}
74 70 {% comment %}
75 71 Post message (text)
76 72 {% endcomment %}
@@ -102,8 +98,8 b''
102 98 {% if is_opening %}
103 99 <div class="metadata">
104 100 {% if is_opening and need_open_link %}
105 {{ thread.get_reply_count }} {% trans 'messages' %},
106 {{ thread.get_images_count }} {% trans 'images' %}.
101 {% blocktrans count count=thread.get_reply_count %}{{ count }} message{% plural %}{{ count }} messages{% endblocktrans %},
102 {% blocktrans count count=thread.get_images_count %}{{ count }} image{% plural %}{{ count }} images{% endblocktrans %}.
107 103 {% endif %}
108 104 <span class="tags">
109 105 {{ thread.get_tag_url_list|safe }}
@@ -6,8 +6,7 b''
6 6 {% load tz %}
7 7
8 8 {% block head %}
9 <title>{{ opening_post.get_title|striptags|truncatewords:10 }}
10 - {{ site_name }}</title>
9 <title>{{ opening_post.get_title_or_text }} - {{ site_name }}</title>
11 10 {% endblock %}
12 11
13 12 {% block content %}
@@ -31,8 +30,13 b''
31 30 data-ws-host="{{ ws_host }}"
32 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' %},
35 <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}.
33 {% with replies_count=thread.get_reply_count%}
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 40 {% trans 'Last update: ' %}<span id="last-update"><time datetime="{{ thread.last_edit_time|date:'c' }}">{{ thread.last_edit_time }}</time></span>
37 41 [<a href="rss/">RSS</a>]
38 42 </span>
@@ -9,6 +9,19 b''
9 9 {% get_current_language as LANGUAGE_CODE %}
10 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 25 {% if bumpable and thread.has_post_limit %}
13 26 <div class="bar-bg">
14 27 <div class="bar-value" style="width:{{ bumplimit_progress }}%" id="bumplimit_progress">
@@ -70,6 +70,7 b" urlpatterns = patterns('',"
70 70 url(r'^api/notifications/(?P<username>\w+)/$', api.api_get_notifications,
71 71 name='api_notifications'),
72 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 75 # Sync protocol API
75 76 url(r'^api/sync/pull/$', response_pull, name='api_sync_pull'),
@@ -7,8 +7,11 b' import hmac'
7 7
8 8 from django.core.cache import cache
9 9 from django.db.models import Model
10 from django import forms
10 11
11 12 from django.utils import timezone
13 from django.utils.translation import ugettext_lazy as _
14 import boards
12 15
13 16 from neboard import settings
14 17
@@ -90,3 +93,11 b' def get_file_hash(file) -> str:'
90 93 for chunk in file.chunks():
91 94 md5.update(chunk)
92 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 104 return reverse('index') + '?page=' \
105 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 107 @transaction.atomic
126 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 129 text = self._remove_invalid_links(text)
148 130
149 tag_strings = data[FORM_TAGS]
150
151 tags = self.parse_tags_string(tag_strings)
131 tags = data[FORM_TAGS]
152 132
153 133 post = Post.objects.create_post(title=title, text=text, file=file,
154 134 ip=ip, tags=tags, opening_posts=threads,
@@ -1,12 +1,16 b''
1 from collections import OrderedDict
1 2 import json
2 3 import logging
3 4
4 5 import xml.etree.ElementTree as ET
5 6
6 7 from django.db import transaction
8 from django.db.models import Count
7 9 from django.http import HttpResponse
8 10 from django.shortcuts import get_object_or_404
9 11 from django.core import serializers
12 from boards.abstracts.settingsmanager import get_settings_manager,\
13 FAV_THREAD_NO_UPDATES
10 14
11 15 from boards.forms import PostForm, PlainErrorList
12 16 from boards.models import Post, Thread, Tag, GlobalId
@@ -48,7 +52,8 b' def api_get_threaddiff(request):'
48 52 uids_str = request.POST.get(PARAMETER_UIDS).strip()
49 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 58 json_data = {
54 59 PARAMETER_UPDATED: [],
@@ -59,10 +64,16 b' def api_get_threaddiff(request):'
59 64 diff_type = request.GET.get(PARAMETER_DIFF_TYPE, DIFF_TYPE_HTML)
60 65
61 66 for post in posts:
62 json_data[PARAMETER_UPDATED].append(get_post_data(post.id, diff_type,
63 request))
67 json_data[PARAMETER_UPDATED].append(post.get_post_data(
68 format_type=diff_type, request=request))
64 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 77 return HttpResponse(content=json.dumps(json_data))
67 78
68 79
@@ -146,7 +157,7 b' def api_get_threads(request, count):'
146 157 opening_post = thread.get_opening_post()
147 158
148 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 161 post_data['bumpable'] = thread.can_bump()
151 162 post_data['archived'] = thread.archived
152 163
@@ -192,7 +203,7 b' def api_get_thread_posts(request, openin'
192 203 json_post_list = []
193 204
194 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 207 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
197 208 json_data['posts'] = json_post_list
198 209
@@ -208,7 +219,7 b' def api_get_notifications(request, usern'
208 219
209 220 json_post_list = []
210 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 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 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 242 def api_get_preview(request):
240 243 raw_text = request.POST['raw_text']
241 244
242 245 parser = Parser()
243 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 7 from django.utils.dateformat import format
8 8
9 9 from boards import utils, settings
10 from boards.abstracts.settingsmanager import get_settings_manager
10 11 from boards.forms import PostForm, PlainErrorList
11 12 from boards.models import Post
12 13 from boards.views.base import BaseBoardView, CONTEXT_FORM
14 from boards.views.mixins import DispatcherMixin
13 15 from boards.views.posting_mixin import PostMixin
14 16
15 17 import neboard
@@ -24,6 +26,7 b" CONTEXT_WS_PORT = 'ws_port'"
24 26 CONTEXT_WS_TIME = 'ws_token_time'
25 27 CONTEXT_MODE = 'mode'
26 28 CONTEXT_OP = 'opening_post'
29 CONTEXT_FAVORITE = 'is_favorite'
27 30
28 31 FORM_TITLE = 'title'
29 32 FORM_TEXT = 'text'
@@ -31,7 +34,7 b" FORM_IMAGE = 'image'"
31 34 FORM_THREADS = 'threads'
32 35
33 36
34 class ThreadView(BaseBoardView, PostMixin, FormMixin):
37 class ThreadView(BaseBoardView, PostMixin, FormMixin, DispatcherMixin):
35 38
36 39 def get(self, request, post_id, form: PostForm=None):
37 40 try:
@@ -39,10 +42,16 b' class ThreadView(BaseBoardView, PostMixi'
39 42 except ObjectDoesNotExist:
40 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 51 # If this is not OP, don't show it as it is
43 52 if not opening_post.is_opening():
44 53 return redirect(opening_post.get_thread().get_opening_post()
45 .get_absolute_url())
54 .get_absolute_url())
46 55
47 56 if not form:
48 57 form = PostForm(error_class=PlainErrorList)
@@ -56,6 +65,7 b' class ThreadView(BaseBoardView, PostMixi'
56 65 params[CONTEXT_THREAD] = thread_to_show
57 66 params[CONTEXT_MODE] = self.get_mode()
58 67 params[CONTEXT_OP] = opening_post
68 params[CONTEXT_FAVORITE] = favorite
59 69
60 70 if settings.get_bool('External', 'WebsocketsEnabled'):
61 71 token_time = format(timezone.now(), u'U')
@@ -78,6 +88,11 b' class ThreadView(BaseBoardView, PostMixi'
78 88 if not opening_post.is_opening():
79 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 96 if not opening_post.get_thread().archived:
82 97 form = PostForm(request.POST, request.FILES,
83 98 error_class=PlainErrorList)
@@ -138,3 +153,11 b' class ThreadView(BaseBoardView, PostMixi'
138 153
139 154 def get_mode(self) -> str:
140 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 1 httplib2
2 2 simplejson
3 pytube
3 4 requests
4 5 adjacent
5 6 django-haystack
General Comments 0
You need to be logged in to leave comments. Login now