diff --git a/.hgtags b/.hgtags --- a/.hgtags +++ b/.hgtags @@ -34,3 +34,4 @@ dfb6c481b1a2c33705de9a9b5304bc924c46b202 4a5bec08ccfb47a27f9e98698f12dd5b7246623b 2.8.2 604935b98f5b5e4a5e903594f048046e1fbb3519 2.8.3 c48ffdc671566069ed0f33644da1229277f3cd18 2.9.0 +d66dc192d4e089ba85325afeef5229b73cb0fde4 2.10.0 diff --git a/boards/abstracts/settingsmanager.py b/boards/abstracts/settingsmanager.py --- a/boards/abstracts/settingsmanager.py +++ b/boards/abstracts/settingsmanager.py @@ -1,4 +1,5 @@ from boards.models import Tag +from boards.models.thread import FAV_THREAD_NO_UPDATES MAX_TRIPCODE_COLLISIONS = 50 @@ -11,6 +12,7 @@ PERMISSION_MODERATE = 'moderator' SETTING_THEME = 'theme' SETTING_FAVORITE_TAGS = 'favorite_tags' +SETTING_FAVORITE_THREADS = 'favorite_threads' SETTING_HIDDEN_TAGS = 'hidden_tags' SETTING_PERMISSIONS = 'permissions' SETTING_USERNAME = 'username' @@ -118,6 +120,28 @@ class SettingsManager: tags.remove(tag.name) self.set_setting(SETTING_HIDDEN_TAGS, tags) + def get_fav_threads(self) -> dict: + return self.get_setting(SETTING_FAVORITE_THREADS, default=dict()) + + def add_or_read_fav_thread(self, opening_post): + threads = self.get_fav_threads() + thread = opening_post.get_thread() + # Don't check for new posts if the thread is archived already + if thread.is_archived(): + last_id = FAV_THREAD_NO_UPDATES + else: + last_id = thread.get_replies().last().id + threads[str(opening_post.id)] = last_id + self.set_setting(SETTING_FAVORITE_THREADS, threads) + + def del_fav_thread(self, opening_post): + threads = self.get_fav_threads() + if self.thread_is_fav(opening_post): + del threads[str(opening_post.id)] + self.set_setting(SETTING_FAVORITE_THREADS, threads) + + def thread_is_fav(self, opening_post): + return str(opening_post.id) in self.get_fav_threads() class SessionSettingsManager(SettingsManager): """ diff --git a/boards/admin.py b/boards/admin.py --- a/boards/admin.py +++ b/boards/admin.py @@ -30,7 +30,10 @@ class TagAdmin(admin.ModelAdmin): def thread_count(self, obj: Tag) -> int: return obj.get_thread_count() - list_display = ('name', 'thread_count') + def display_children(self, obj: Tag): + return ', '.join([str(child) for child in obj.get_children().all()]) + + list_display = ('name', 'thread_count', 'display_children') search_fields = ('name',) @@ -46,7 +49,14 @@ class ThreadAdmin(admin.ModelAdmin): def ip(self, obj: Thread): return obj.get_opening_post().poster_ip - list_display = ('id', 'title', 'reply_count', 'archived', 'ip') + def display_tags(self, obj: Thread): + return ', '.join([str(tag) for tag in obj.get_tags().all()]) + + def op(self, obj: Thread): + return obj.get_opening_post_id() + + list_display = ('id', 'op', 'title', 'reply_count', 'archived', 'ip', + 'display_tags') list_filter = ('bump_time', 'archived', 'bumpable') search_fields = ('id', 'title') filter_horizontal = ('tags',) diff --git a/boards/config/default_settings.ini b/boards/config/default_settings.ini --- a/boards/config/default_settings.ini +++ b/boards/config/default_settings.ini @@ -1,5 +1,5 @@ [Version] -Version = 2.9.0 Claire +Version = 2.10.0 BT SiteName = Neboard DEV [Cache] diff --git a/boards/context_processors.py b/boards/context_processors.py --- a/boards/context_processors.py +++ b/boards/context_processors.py @@ -19,6 +19,7 @@ CONTEXT_NEW_NOTIFICATIONS_COUNT = 'new_n CONTEXT_USERNAME = 'username' CONTEXT_TAGS_STR = 'tags_str' CONTEXT_IMAGE_VIEWER = 'image_viewer' +CONTEXT_HAS_FAV_THREADS = 'has_fav_threads' def get_notifications(context, request): @@ -43,6 +44,7 @@ def user_and_ui_processor(request): settings_manager = get_settings_manager(request) fav_tags = settings_manager.get_fav_tags() context[CONTEXT_TAGS] = fav_tags + context[CONTEXT_TAGS_STR] = Tag.objects.get_tag_url_list(fav_tags) theme = settings_manager.get_theme() context[CONTEXT_THEME] = theme @@ -58,6 +60,9 @@ def user_and_ui_processor(request): SETTING_IMAGE_VIEWER, default=settings.get('View', 'DefaultImageViewer')) + context[CONTEXT_HAS_FAV_THREADS] =\ + len(settings_manager.get_fav_threads()) > 0 + get_notifications(context, request) return context diff --git a/boards/forms.py b/boards/forms.py --- a/boards/forms.py +++ b/boards/forms.py @@ -7,19 +7,17 @@ from django import forms from django.core.files.uploadedfile import SimpleUploadedFile from django.core.exceptions import ObjectDoesNotExist from django.forms.util import ErrorList -from django.utils.translation import ugettext_lazy as _ -import requests +from django.utils.translation import ugettext_lazy as _, ungettext_lazy from boards.mdx_neboard import formatters +from boards.models.attachment.downloaders import Downloader from boards.models.post import TITLE_MAX_LENGTH from boards.models import Tag, Post +from boards.utils import validate_file_size from neboard import settings import boards.settings as board_settings import neboard -HEADER_CONTENT_LENGTH = 'content-length' -HEADER_CONTENT_TYPE = 'content-type' - REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE) VETERAN_POSTING_DELAY = 5 @@ -37,14 +35,11 @@ LABEL_TEXT = _('Text') LABEL_TAG = _('Tag') LABEL_SEARCH = _('Search') -ERROR_SPEED = _('Please wait %s seconds before sending message') +ERROR_SPEED = 'Please wait %(delay)d second before sending message' +ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message' TAG_MAX_LENGTH = 20 -FILE_DOWNLOAD_CHUNK_BYTES = 100000 - -HTTP_RESULT_OK = 200 - TEXTAREA_ROWS = 4 @@ -182,7 +177,7 @@ class PostForm(NeboardForm): file = self.cleaned_data['file'] if file: - self.validate_file_size(file.size) + validate_file_size(file.size) return file @@ -196,7 +191,7 @@ class PostForm(NeboardForm): if not file: raise forms.ValidationError(_('Invalid URL')) else: - self.validate_file_size(file.size) + validate_file_size(file.size) return file @@ -272,9 +267,8 @@ class PostForm(NeboardForm): now = time.time() current_delay = 0 - need_delay = False - if not LAST_POST_TIME in self.session: + if LAST_POST_TIME not in self.session: self.session[LAST_POST_TIME] = now need_delay = True @@ -285,8 +279,9 @@ class PostForm(NeboardForm): need_delay = current_delay < posting_delay if need_delay: - error_message = ERROR_SPEED % str(posting_delay - - current_delay) + delay = posting_delay - current_delay + error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL, + delay) % {'delay': delay} self._errors['text'] = self.error_class([error_message]) can_post = False @@ -294,13 +289,6 @@ class PostForm(NeboardForm): if can_post: self.session[LAST_POST_TIME] = now - def validate_file_size(self, size: int): - max_size = board_settings.get_int('Forms', 'MaxFileSize') - if size > max_size: - raise forms.ValidationError( - _('File must be less than %s bytes') - % str(max_size)) - def _get_file_from_url(self, url: str) -> SimpleUploadedFile: """ Gets an file file from URL. @@ -309,36 +297,18 @@ class PostForm(NeboardForm): img_temp = None try: - # Verify content headers - response_head = requests.head(url, verify=False) - content_type = response_head.headers[HEADER_CONTENT_TYPE].split(';')[0] - length_header = response_head.headers.get(HEADER_CONTENT_LENGTH) - if length_header: - length = int(length_header) - self.validate_file_size(length) - # Get the actual content into memory - response = requests.get(url, verify=False, stream=True) - - # Download file, stop if the size exceeds limit - size = 0 - content = b'' - for chunk in response.iter_content(FILE_DOWNLOAD_CHUNK_BYTES): - size += len(chunk) - self.validate_file_size(size) - content += chunk - - if response.status_code == HTTP_RESULT_OK and content: - # Set a dummy file name that will be replaced - # anyway, just keep the valid extension - filename = 'file.' + content_type.split('/')[1] - img_temp = SimpleUploadedFile(filename, content, - content_type) + for downloader in Downloader.__subclasses__(): + if downloader.handles(url): + return downloader.download(url) + # If nobody of the specific downloaders handles this, use generic + # one + return Downloader.download(url) + except forms.ValidationError as e: + raise e except Exception as e: # Just return no file pass - return img_temp - class ThreadForm(PostForm): @@ -354,20 +324,27 @@ class ThreadForm(PostForm): _('Inappropriate characters in tags.')) required_tag_exists = False - for tag in tags.split(): - try: - Tag.objects.get(name=tag.strip().lower(), required=True) + tag_set = set() + for tag_string in tags.split(): + tag, created = Tag.objects.get_or_create(name=tag_string.strip().lower()) + tag_set.add(tag) + + # If this is a new tag, don't check for its parents because nobody + # added them yet + if not created: + tag_set |= tag.get_all_parents() + + for tag in tag_set: + if tag.required: required_tag_exists = True break - except ObjectDoesNotExist: - pass if not required_tag_exists: all_tags = Tag.objects.filter(required=True) raise forms.ValidationError( _('Need at least one section.')) - return tags + return tag_set def clean(self): cleaned_data = super(ThreadForm, self).clean() diff --git a/boards/locale/ru/LC_MESSAGES/django.mo b/boards/locale/ru/LC_MESSAGES/django.mo index 53640416ea6f4ebdc64becbb6681abb5ca51a17d..04ae6f6ee04cdd82c74b4513472beba5839d7596 GIT binary patch literal 8988 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\n" "Language-Team: LANGUAGE \n" @@ -38,114 +38,99 @@ msgstr "разработчик javascript" msgid "designer" msgstr "дизайнер" -#: forms.py:31 +#: forms.py:30 msgid "Type message here. Use formatting panel for more advanced usage." msgstr "" "Вводите сообщение сюда. Используйте панель для более сложного форматирования." -#: forms.py:32 +#: forms.py:31 msgid "music images i_dont_like_tags" msgstr "музыка картинки теги_не_нужны" -#: forms.py:34 +#: forms.py:33 msgid "Title" msgstr "Заголовок" -#: forms.py:35 +#: forms.py:34 msgid "Text" msgstr "Текст" -#: forms.py:36 +#: forms.py:35 msgid "Tag" msgstr "Метка" -#: forms.py:37 templates/boards/base.html:40 templates/search/search.html:7 +#: forms.py:36 templates/boards/base.html:40 templates/search/search.html:7 msgid "Search" msgstr "Поиск" -#: forms.py:39 -#, python-format -msgid "Please wait %s seconds before sending message" -msgstr "Пожалуйста подождите %s секунд перед отправкой сообщения" - -#: forms.py:140 +#: forms.py:139 msgid "File" msgstr "Файл" -#: forms.py:143 +#: forms.py:142 msgid "File URL" msgstr "URL файла" -#: forms.py:149 +#: forms.py:148 msgid "e-mail" msgstr "" -#: forms.py:152 +#: forms.py:151 msgid "Additional threads" msgstr "Дополнительные темы" -#: forms.py:155 -msgid "Tripcode" -msgstr "Трипкод" - -#: forms.py:164 +#: forms.py:162 #, python-format msgid "Title must have less than %s characters" msgstr "Заголовок должен иметь меньше %s символов" -#: forms.py:174 +#: forms.py:172 #, python-format msgid "Text must have less than %s characters" msgstr "Текст должен быть короче %s символов" -#: forms.py:194 +#: forms.py:192 msgid "Invalid URL" msgstr "Неверный URL" -#: forms.py:215 +#: forms.py:213 msgid "Invalid additional thread list" msgstr "Неверный список дополнительных тем" -#: forms.py:251 +#: forms.py:258 msgid "Either text or file must be entered." msgstr "Текст или файл должны быть введены." -#: forms.py:289 -#, python-format -msgid "File must be less than %s bytes" -msgstr "Файл должен быть менее %s байт" - -#: forms.py:335 templates/boards/all_threads.html:154 +#: forms.py:317 templates/boards/all_threads.html:148 #: templates/boards/rss/post.html:10 templates/boards/tags.html:6 msgid "Tags" msgstr "Метки" -#: forms.py:342 +#: forms.py:324 msgid "Inappropriate characters in tags." msgstr "Недопустимые символы в метках." -#: forms.py:356 +#: forms.py:338 msgid "Need at least one section." msgstr "Нужен хотя бы один раздел." -#: forms.py:368 +#: forms.py:350 msgid "Theme" msgstr "Тема" -#: forms.py:369 -#| msgid "Image view mode" +#: forms.py:351 msgid "Image view mode" msgstr "Режим просмотра изображений" -#: forms.py:370 +#: forms.py:352 msgid "User name" msgstr "Имя пользователя" -#: forms.py:371 +#: forms.py:353 msgid "Time zone" msgstr "Часовой пояс" -#: forms.py:377 +#: forms.py:359 msgid "Inappropriate characters." msgstr "Недопустимые символы." @@ -161,11 +146,11 @@ msgstr "Этой страницы не существует" msgid "Related message" msgstr "Связанное сообщение" -#: templates/boards/all_threads.html:71 +#: templates/boards/all_threads.html:69 msgid "Edit tag" msgstr "Изменить метку" -#: templates/boards/all_threads.html:79 +#: templates/boards/all_threads.html:75 #, python-format msgid "" "This tag has %(thread_count)s threads (%(active_thread_count)s active) and " @@ -174,54 +159,61 @@ msgstr "" "С этой меткой есть %(thread_count)s тем (%(active_thread_count)s активных) и " "%(post_count)s сообщений." -#: templates/boards/all_threads.html:81 +#: templates/boards/all_threads.html:77 msgid "Related tags:" msgstr "Похожие метки:" -#: templates/boards/all_threads.html:96 templates/boards/feed.html:30 +#: templates/boards/all_threads.html:90 templates/boards/feed.html:30 #: templates/boards/notifications.html:17 templates/search/search.html:26 msgid "Previous page" msgstr "Предыдущая страница" -#: templates/boards/all_threads.html:110 +#: templates/boards/all_threads.html:104 #, python-format -msgid "Skipped %(count)s replies. Open thread to see all replies." -msgstr "Пропущено %(count)s ответов. Откройте тред, чтобы увидеть все ответы." +#| msgid "Skipped %(count)s replies. Open thread to see all replies." +msgid "Skipped %(count)s reply. Open thread to see all replies." +msgid_plural "Skipped %(count)s replies. Open thread to see all replies." +msgstr[0] "" +"Пропущен %(count)s ответ. Откройте тред, чтобы увидеть все ответы." +msgstr[1] "" +"Пропущено %(count)s ответа. Откройте тред, чтобы увидеть все ответы." +msgstr[2] "" +"Пропущено %(count)s ответов. Откройте тред, чтобы увидеть все ответы." -#: templates/boards/all_threads.html:128 templates/boards/feed.html:40 +#: templates/boards/all_threads.html:122 templates/boards/feed.html:40 #: templates/boards/notifications.html:27 templates/search/search.html:37 msgid "Next page" msgstr "Следующая страница" -#: templates/boards/all_threads.html:133 +#: templates/boards/all_threads.html:127 msgid "No threads exist. Create the first one!" msgstr "Нет тем. Создайте первую!" -#: templates/boards/all_threads.html:139 +#: templates/boards/all_threads.html:133 msgid "Create new thread" msgstr "Создать новую тему" -#: templates/boards/all_threads.html:144 templates/boards/preview.html:16 -#: templates/boards/thread_normal.html:38 +#: templates/boards/all_threads.html:138 templates/boards/preview.html:16 +#: templates/boards/thread_normal.html:51 msgid "Post" msgstr "Отправить" -#: templates/boards/all_threads.html:149 +#: templates/boards/all_threads.html:139 templates/boards/preview.html:6 +#: templates/boards/staticpages/help.html:21 +#: templates/boards/thread_normal.html:52 +msgid "Preview" +msgstr "Предпросмотр" + +#: templates/boards/all_threads.html:144 msgid "Tags must be delimited by spaces. Text or image is required." msgstr "" "Метки должны быть разделены пробелами. Текст или изображение обязательны." -#: templates/boards/all_threads.html:151 templates/boards/preview.html:6 -#: templates/boards/staticpages/help.html:21 -#: templates/boards/thread_normal.html:42 -msgid "Preview" -msgstr "Предпросмотр" - -#: templates/boards/all_threads.html:153 templates/boards/thread_normal.html:45 +#: templates/boards/all_threads.html:147 templates/boards/thread_normal.html:58 msgid "Text syntax" msgstr "Синтаксис текста" -#: templates/boards/all_threads.html:167 templates/boards/feed.html:53 +#: templates/boards/all_threads.html:161 templates/boards/feed.html:53 msgid "Pages:" msgstr "Страницы: " @@ -286,16 +278,16 @@ msgstr "Уведомления" msgid "Settings" msgstr "Настройки" -#: templates/boards/base.html:66 +#: templates/boards/base.html:79 msgid "Admin" msgstr "Администрирование" -#: templates/boards/base.html:68 +#: templates/boards/base.html:81 #, python-format msgid "Speed: %(ppd)s posts per day" msgstr "Скорость: %(ppd)s сообщений в день" -#: templates/boards/base.html:70 +#: templates/boards/base.html:83 msgid "Up" msgstr "Вверх" @@ -303,37 +295,52 @@ msgstr "Вверх" msgid "No posts exist. Create the first one!" msgstr "Нет сообщений. Создайте первое!" -#: templates/boards/post.html:30 +#: templates/boards/post.html:32 msgid "Open" msgstr "Открыть" -#: templates/boards/post.html:32 templates/boards/post.html.py:43 +#: templates/boards/post.html:34 templates/boards/post.html.py:45 msgid "Reply" msgstr "Ответить" -#: templates/boards/post.html:38 +#: templates/boards/post.html:40 msgid " in " msgstr " в " -#: templates/boards/post.html:48 +#: templates/boards/post.html:50 msgid "Edit" msgstr "Изменить" -#: templates/boards/post.html:50 +#: templates/boards/post.html:52 msgid "Edit thread" msgstr "Изменить тему" -#: templates/boards/post.html:97 +#: templates/boards/post.html:94 msgid "Replies" msgstr "Ответы" -#: templates/boards/post.html:109 templates/boards/thread.html:34 -msgid "messages" -msgstr "сообщений" +#: templates/boards/post.html:105 +#, python-format +msgid "%(count)s message" +msgid_plural "%(count)s messages" +msgstr[0] "%(count)s сообщение" +msgstr[1] "%(count)s сообщения" +msgstr[2] "%(count)s сообщений" -#: templates/boards/post.html:110 templates/boards/thread.html:35 -msgid "images" -msgstr "изображений" +#, python-format +msgid "Please wait %(delay)d second before sending message" +msgid_plural "Please wait %(delay)d seconds before sending message" +msgstr[0] "Пожалуйста подождите %(delay)d секунду перед отправкой сообщения" +msgstr[1] "Пожалуйста подождите %(delay)d секунды перед отправкой сообщения" +msgstr[2] "Пожалуйста подождите %(delay)d секунд перед отправкой сообщения" + +#: templates/boards/post.html:106 +#, python-format +msgid "%(count)s image" +msgid_plural "%(count)s images" +msgstr[0] "%(count)s изображение" +msgstr[1] "%(count)s изображения" +msgstr[2] "%(count)s изображений" #: templates/boards/rss/post.html:5 msgid "Post image" @@ -347,19 +354,11 @@ msgstr "Вы модератор." msgid "Hidden tags:" msgstr "Скрытые метки:" -#: templates/boards/settings.html:27 +#: templates/boards/settings.html:25 msgid "No hidden tags." msgstr "Нет скрытых меток." -#: templates/boards/settings.html:29 -msgid "Tripcode:" -msgstr "Трипкод:" - -#: templates/boards/settings.html:29 -msgid "reset" -msgstr "сбросить" - -#: templates/boards/settings.html:37 +#: templates/boards/settings.html:34 msgid "Save" msgstr "Сохранить" @@ -434,6 +433,20 @@ msgid "Tree" msgstr "Дерево" #: templates/boards/thread.html:36 +msgid "message" +msgid_plural "messages" +msgstr[0] "сообщение" +msgstr[1] "сообщения" +msgstr[2] "сообщений" + +#: templates/boards/thread.html:39 +msgid "image" +msgid_plural "images" +msgstr[0] "изображение" +msgstr[1] "изображения" +msgstr[2] "изображений" + +#: templates/boards/thread.html:41 msgid "Last update: " msgstr "Последнее обновление: " @@ -441,22 +454,39 @@ msgstr "Последнее обновление: " msgid "No images." msgstr "Нет изображений." -#: templates/boards/thread_normal.html:17 +#: templates/boards/thread_normal.html:30 msgid "posts to bumplimit" msgstr "сообщений до бамплимита" -#: templates/boards/thread_normal.html:31 +#: templates/boards/thread_normal.html:44 msgid "Reply to thread" msgstr "Ответить в тему" -#: templates/boards/thread_normal.html:31 +#: templates/boards/thread_normal.html:44 msgid "to message " msgstr "на сообщение" -#: templates/boards/thread_normal.html:46 +#: templates/boards/thread_normal.html:59 msgid "Close form" msgstr "Закрыть форму" #: templates/search/search.html:17 msgid "Ok" msgstr "Ок" + +#: utils.py:102 +#, python-format +msgid "File must be less than %s bytes" +msgstr "Файл должен быть менее %s байт" + +msgid "favorites" +msgstr "избранное" + +msgid "Loading..." +msgstr "Загрузка..." + +msgid "Category:" +msgstr "Категория:" + +msgid "Subcategories:" +msgstr "Подкатегории:" diff --git a/boards/locale/ru/LC_MESSAGES/djangojs.mo b/boards/locale/ru/LC_MESSAGES/djangojs.mo index 58d209b02d15ab1f664e6f18e0dbda7ae067c0d4..6f605db078c248373980e3357705bfd3fa87bf9c GIT binary patch literal 965 zc${sKO>fgM7{|RAKngEHV z0us_rh}$H-K{qH{kr)>a9362*9Qg!%0G_0)3T%#ke(cBpzn>?`&#}=r2*w%UGEf1| z08_~R&!jP&YUgP4W5 z3^5(ogE)i+3oCRHiIG!3`>R-#QV zLgL9jxnZxMYK8)mVd0x)I2M z?!IG}h*uz48{aLMUT*3^A@69eFGErhK9hAnjKR>OqQ&+CT%XdO6P-yq}TMwMEWC%AnlSCI~|MB)s0N z`vIH7Yma;(X=trfmPmS_i`!bKf;eoA#~<9p>4!n673(eG*U1tW4QXQ5Ot#XRnZv!M zb(LK)bjvdE!~~9)`nY8n_%_zDne6AS>Hbm1nvIua#jf9Sk9v+N!HQB-ulxcB1F%OSA>az1WJQ zXaD-5?c}zd=q22-+n02}v6sy4aRdkZi6y?}W53*q>1g|8br=0#7cMy7%5hZ}{Q;mV BS(E?( diff --git a/boards/locale/ru/LC_MESSAGES/djangojs.po b/boards/locale/ru/LC_MESSAGES/djangojs.po --- a/boards/locale/ru/LC_MESSAGES/djangojs.po +++ b/boards/locale/ru/LC_MESSAGES/djangojs.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2014-07-02 13:26+0300\n" +"POT-Creation-Date: 2015-09-04 18:47+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -19,14 +19,37 @@ msgstr "" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" -#: static/js/refpopup.js:58 +#: static/js/3party/jquery-ui.min.js:8 +msgid "'" +msgstr "" + +#: static/js/refpopup.js:72 msgid "Loading..." msgstr "Загрузка..." -#: static/js/refpopup.js:77 +#: static/js/refpopup.js:91 msgid "Post not found" msgstr "Сообщение не найдено" -#: static/js/thread_update.js:279 +#: static/js/thread_update.js:261 +msgid "message" +msgid_plural "messages" +msgstr[0] "сообщение" +msgstr[1] "сообщения" +msgstr[2] "сообщений" + +#: static/js/thread_update.js:262 +msgid "image" +msgid_plural "images" +msgstr[0] "изображение" +msgstr[1] "изображения" +msgstr[2] "изображений" + +#: static/js/thread_update.js:445 msgid "Sending message..." -msgstr "Отправка сообщения..." \ No newline at end of file +msgstr "Отправка сообщения..." + +#: static/js/thread_update.js:449 +msgid "Server error!" +msgstr "Ошибка сервера!" + diff --git a/boards/migrations/0025_auto_20150825_2049.py b/boards/migrations/0025_auto_20150825_2049.py --- a/boards/migrations/0025_auto_20150825_2049.py +++ b/boards/migrations/0025_auto_20150825_2049.py @@ -6,10 +6,17 @@ from django.db import migrations class Migration(migrations.Migration): - def refuild_refmap(apps, schema_editor): + def rebuild_refmap(apps, schema_editor): Post = apps.get_model('boards', 'Post') for post in Post.objects.all(): - post.build_refmap() + refposts = list() + for refpost in post.referenced_posts.all(): + result = '>>{}'.format(refpost.get_absolute_url(), + self.id) + if refpost.is_opening(): + result = '{}'.format(result) + refposts += result + post.refmap = ', '.join(refposts) post.save(update_fields=['refmap']) dependencies = [ @@ -17,5 +24,5 @@ class Migration(migrations.Migration): ] operations = [ - migrations.RunPython(refuild_refmap), + migrations.RunPython(rebuild_refmap), ] diff --git a/boards/migrations/0026_post_opening.py b/boards/migrations/0026_post_opening.py new file mode 100644 --- /dev/null +++ b/boards/migrations/0026_post_opening.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('boards', '0025_auto_20150825_2049'), + ] + + operations = [ + migrations.AddField( + model_name='post', + name='opening', + field=models.BooleanField(default=False), + preserve_default=False, + ), + ] diff --git a/boards/migrations/0027_auto_20150912_1632.py b/boards/migrations/0027_auto_20150912_1632.py new file mode 100644 --- /dev/null +++ b/boards/migrations/0027_auto_20150912_1632.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + def build_opening_flag(apps, schema_editor): + Post = apps.get_model('boards', 'Post') + for post in Post.objects.all(): + op = Post.objects.filter(threads__in=[post.thread]).order_by('pub_time').first() + post.opening = op.id == post.id + post.save(update_fields=['opening']) + + dependencies = [ + ('boards', '0026_post_opening'), + ] + + operations = [ + migrations.RunPython(build_opening_flag), + ] diff --git a/boards/migrations/0028_auto_20150928_2211.py b/boards/migrations/0028_auto_20150928_2211.py new file mode 100644 --- /dev/null +++ b/boards/migrations/0028_auto_20150928_2211.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('boards', '0027_auto_20150912_1632'), + ] + + operations = [ + migrations.AlterField( + model_name='post', + name='threads', + field=models.ManyToManyField(to='boards.Thread', related_name='multi_replies', db_index=True), + ), + ] diff --git a/boards/migrations/0029_tag_parent.py b/boards/migrations/0029_tag_parent.py new file mode 100644 --- /dev/null +++ b/boards/migrations/0029_tag_parent.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('boards', '0028_auto_20150928_2211'), + ] + + operations = [ + migrations.AddField( + model_name='tag', + name='parent', + field=models.ForeignKey(to='boards.Tag', null=True), + ), + ] diff --git a/boards/migrations/0030_auto_20150929_1816.py b/boards/migrations/0030_auto_20150929_1816.py new file mode 100644 --- /dev/null +++ b/boards/migrations/0030_auto_20150929_1816.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('boards', '0029_tag_parent'), + ] + + operations = [ + migrations.AlterField( + model_name='tag', + name='parent', + field=models.ForeignKey(related_name='children', null=True, to='boards.Tag'), + ), + ] diff --git a/boards/migrations/0031_merge.py b/boards/migrations/0031_merge.py new file mode 100644 --- /dev/null +++ b/boards/migrations/0031_merge.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('boards', '0030_auto_20150929_1816'), + ('boards', '0026_auto_20150830_2006'), + ] + + operations = [ + ] diff --git a/boards/models/attachment/downloaders.py b/boards/models/attachment/downloaders.py new file mode 100644 --- /dev/null +++ b/boards/models/attachment/downloaders.py @@ -0,0 +1,66 @@ +import os +import re + +from django.core.files.uploadedfile import SimpleUploadedFile +from pytube import YouTube +import requests + +from boards.utils import validate_file_size + +YOUTUBE_VIDEO_FORMAT = 'webm' + +HTTP_RESULT_OK = 200 + +HEADER_CONTENT_LENGTH = 'content-length' +HEADER_CONTENT_TYPE = 'content-type' + +FILE_DOWNLOAD_CHUNK_BYTES = 100000 + +YOUTUBE_URL = re.compile(r'https?://www\.youtube\.com/watch\?v=\w+') + + +class Downloader: + @staticmethod + def handles(url: str) -> bool: + return False + + @staticmethod + def download(url: str): + # Verify content headers + response_head = requests.head(url, verify=False) + content_type = response_head.headers[HEADER_CONTENT_TYPE].split(';')[0] + length_header = response_head.headers.get(HEADER_CONTENT_LENGTH) + if length_header: + length = int(length_header) + validate_file_size(length) + # Get the actual content into memory + response = requests.get(url, verify=False, stream=True) + + # Download file, stop if the size exceeds limit + size = 0 + content = b'' + for chunk in response.iter_content(FILE_DOWNLOAD_CHUNK_BYTES): + size += len(chunk) + validate_file_size(size) + content += chunk + + if response.status_code == HTTP_RESULT_OK and content: + # Set a dummy file name that will be replaced + # anyway, just keep the valid extension + filename = 'file.' + content_type.split('/')[1] + return SimpleUploadedFile(filename, content, content_type) + + +class YouTubeDownloader(Downloader): + @staticmethod + def download(url: str): + yt = YouTube() + yt.from_url(url) + videos = yt.filter(YOUTUBE_VIDEO_FORMAT) + if len(videos) > 0: + video = videos[0] + return Downloader.download(video.url) + + @staticmethod + def handles(url: str) -> bool: + return YOUTUBE_URL.match(url) diff --git a/boards/models/attachment/viewers.py b/boards/models/attachment/viewers.py --- a/boards/models/attachment/viewers.py +++ b/boards/models/attachment/viewers.py @@ -6,13 +6,21 @@ FILE_STUB_IMAGE = 'images/file.png' FILE_TYPES_VIDEO = ( 'webm', 'mp4', + 'mpeg', ) FILE_TYPE_SVG = 'svg' FILE_TYPES_AUDIO = ( 'ogg', 'mp3', + 'opus', ) +PLAIN_FILE_FORMATS = { + 'pdf': 'pdf', + 'djvu': 'djvu', + 'txt': 'txt', +} + def get_viewers(): return AbstractViewer.__subclasses__() @@ -35,9 +43,15 @@ class AbstractViewer: self.file_type, filesizeformat(self.file.size)) def get_format_view(self): + if self.file_type in PLAIN_FILE_FORMATS: + image = 'images/fileformats/{}.png'.format( + PLAIN_FILE_FORMATS[self.file_type]) + else: + image = FILE_STUB_IMAGE + return ''\ ''\ - ''.format(self.file.url, static(FILE_STUB_IMAGE)) + ''.format(self.file.url, static(image)) class VideoViewer(AbstractViewer): diff --git a/boards/models/post/__init__.py b/boards/models/post/__init__.py --- a/boards/models/post/__init__.py +++ b/boards/models/post/__init__.py @@ -1,5 +1,3 @@ -from datetime import datetime, timedelta, date -from datetime import time as dtime import logging import re import uuid @@ -11,21 +9,15 @@ from django.db.models import TextField, from django.template.loader import render_to_string from django.utils import timezone +from boards import settings from boards.abstracts.tripcode import Tripcode from boards.mdx_neboard import Parser -from boards.models import KeyPair, GlobalId -from boards import settings -from boards.models import PostImage, Attachment +from boards.models import PostImage, Attachment, KeyPair, GlobalId from boards.models.base import Viewable from boards.models.post.export import get_exporter, DIFF_TYPE_JSON from boards.models.post.manager import PostManager from boards.models.user import Notification -WS_NOTIFICATION_TYPE_NEW_POST = 'new_post' -WS_NOTIFICATION_TYPE = 'notification_type' - -WS_CHANNEL_THREAD = "thread:" - APP_LABEL_BOARDS = 'boards' BAN_REASON_AUTO = 'Auto' @@ -80,7 +72,7 @@ class Post(models.Model, Viewable): images = models.ManyToManyField(PostImage, null=True, blank=True, related_name='post_images', db_index=True) attachments = models.ManyToManyField(Attachment, null=True, blank=True, - related_name='attachment_posts') + related_name='attachment_posts') poster_ip = models.GenericIPAddressField() @@ -92,7 +84,8 @@ class Post(models.Model, Viewable): blank=True, related_name='refposts', db_index=True) refmap = models.TextField(null=True, blank=True) - threads = models.ManyToManyField('Thread', db_index=True) + threads = models.ManyToManyField('Thread', db_index=True, + related_name='multi_replies') thread = models.ForeignKey('Thread', db_index=True, related_name='pt+') url = models.TextField() @@ -103,23 +96,23 @@ class Post(models.Model, Viewable): global_id = models.OneToOneField('GlobalId', null=True, blank=True) tripcode = models.CharField(max_length=50, null=True) + opening = models.BooleanField() def __str__(self): - return 'P#{}/{}'.format(self.id, self.title) + return 'P#{}/{}'.format(self.id, self.get_title()) def get_referenced_posts(self): threads = self.get_threads().all() - return self.referenced_posts.filter(threads__in=threads) \ + return self.referenced_posts.filter(threads__in=threads)\ .order_by('pub_time').distinct().all() def get_title(self) -> str: - """ - Gets original post title or part of its text. - """ + return self.title - title = self.title + def get_title_or_text(self): + title = self.get_title() if not title: - title = self.get_text() + title = truncatewords(striptags(self.get_text()), TITLE_MAX_WORDS) return title @@ -142,7 +135,7 @@ class Post(models.Model, Viewable): Checks if this is an opening post or just a reply. """ - return self.get_thread().get_opening_post_id() == self.id + return self.opening def get_absolute_url(self): if self.url: @@ -172,12 +165,6 @@ class Post(models.Model, Viewable): """ thread = self.get_thread() - is_opening = kwargs.get(PARAMETER_IS_OPENING, self.is_opening()) - - if is_opening: - opening_post_id = self.id - else: - opening_post_id = thread.get_opening_post_id() css_class = 'post' if thread.archived: @@ -192,10 +179,9 @@ class Post(models.Model, Viewable): params.update({ PARAMETER_POST: self, - PARAMETER_IS_OPENING: is_opening, + PARAMETER_IS_OPENING: self.is_opening(), PARAMETER_THREAD: thread, PARAMETER_CSS_CLASS: css_class, - PARAMETER_OP_ID: opening_post_id, }) return render_to_string('boards/post.html', params) @@ -336,7 +322,7 @@ class Post(models.Model, Viewable): for thread in self.get_threads().all(): thread.last_edit_time = self.last_edit_time - thread.save(update_fields=['last_edit_time']) + thread.save(update_fields=['last_edit_time', 'bumpable']) super().save(force_insert, force_update, using, update_fields) @@ -418,7 +404,7 @@ class Post(models.Model, Viewable): """ result = '>>{}'.format(self.get_absolute_url(), - self.id) + self.id) if self.is_opening(): result = '{}'.format(result) diff --git a/boards/models/post/manager.py b/boards/models/post/manager.py --- a/boards/models/post/manager.py +++ b/boards/models/post/manager.py @@ -6,6 +6,7 @@ from django.utils import timezone from boards import utils from boards.mdx_neboard import Parser from boards.models import PostImage, Attachment +from boards.models.user import Ban import boards.models __author__ = 'vurdalak' @@ -31,7 +32,7 @@ class PostManager(models.Manager): Creates new post """ - is_banned = boards.models.Ban.objects.filter(ip=ip).exists() + is_banned = Ban.objects.filter(ip=ip).exists() # TODO Raise specific exception and catch it in the views if is_banned: @@ -59,7 +60,8 @@ class PostManager(models.Manager): poster_ip=ip, thread=thread, last_edit_time=posting_time, - tripcode=tripcode) + tripcode=tripcode, + opening=new_thread) post.threads.add(thread) logger = logging.getLogger('boards.post.create') @@ -122,7 +124,8 @@ class PostManager(models.Manager): @transaction.atomic def import_post(self, title: str, text: str, pub_time: str, global_id, opening_post=None, tags=list()): - if opening_post is None: + is_opening = opening_post is None + if is_opening: thread = boards.models.thread.Thread.objects.create( bump_time=pub_time, last_edit_time=pub_time) list(map(thread.tags.add, tags)) @@ -133,7 +136,9 @@ class PostManager(models.Manager): pub_time=pub_time, poster_ip=NO_IP, last_edit_time=pub_time, - thread_id=thread.id, global_id=global_id) + thread_id=thread.id, + global_id=global_id, + opening=is_opening) post.build_url() post.connect_replies() diff --git a/boards/models/tag.py b/boards/models/tag.py --- a/boards/models/tag.py +++ b/boards/models/tag.py @@ -1,3 +1,4 @@ +import hashlib from django.template.loader import render_to_string from django.db import models from django.db.models import Count @@ -47,6 +48,8 @@ class Tag(models.Model, Viewable): required = models.BooleanField(default=False, db_index=True) description = models.TextField(blank=True) + parent = models.ForeignKey('Tag', null=True, related_name='children') + def __str__(self): return self.name @@ -89,7 +92,7 @@ class Tag(models.Model, Viewable): @cached_result() def get_post_count(self): - return self.get_threads().aggregate(num_posts=Count('post'))['num_posts'] + return self.get_threads().aggregate(num_posts=Count('multi_replies'))['num_posts'] def get_description(self): return self.description @@ -106,4 +109,26 @@ class Tag(models.Model, Viewable): def get_related_tags(self): return set(Tag.objects.filter(thread_tags__in=self.get_threads()).exclude( - id=self.id).order_by('?')[:RELATED_TAGS_COUNT]) + id=self.id).order_by('?')[:RELATED_TAGS_COUNT]) + + @cached_result() + def get_color(self): + """ + Gets color hashed from the tag name. + """ + return hashlib.md5(self.name.encode()).hexdigest()[:6] + + def get_parent(self): + return self.parent + + def get_all_parents(self): + parents = set() + parent = self.get_parent() + if parent and parent not in parents: + parents.add(parent) + parents |= parent.get_all_parents() + + return parents + + def get_children(self): + return self.children diff --git a/boards/models/thread.py b/boards/models/thread.py --- a/boards/models/thread.py +++ b/boards/models/thread.py @@ -1,7 +1,7 @@ import logging from adjacent import Client -from django.db.models import Count, Sum, QuerySet +from django.db.models import Count, Sum, QuerySet, Q from django.utils import timezone from django.db import models @@ -11,6 +11,8 @@ from boards.utils import cached_result, from boards.models.post import Post from boards.models.tag import Tag +FAV_THREAD_NO_UPDATES = -1 + __author__ = 'neko259' @@ -54,6 +56,26 @@ class ThreadManager(models.Manager): thread.update_posts_time() thread.save(update_fields=['archived', 'last_edit_time', 'bumpable']) + def get_new_posts(self, datas): + query = None + # TODO Use classes instead of dicts + for data in datas: + if data['last_id'] != FAV_THREAD_NO_UPDATES: + q = (Q(id=data['op'].get_thread().id) + & Q(multi_replies__id__gt=data['last_id'])) + if query is None: + query = q + else: + query = query | q + if query is not None: + return self.filter(query).annotate( + new_post_count=Count('multi_replies')) + + def get_new_post_count(self, datas): + new_posts = self.get_new_posts(datas) + return new_posts.aggregate(total_count=Count('multi_replies'))\ + ['total_count'] if new_posts else 0 + def get_thread_max_posts(): return settings.get_int('Messages', 'MaxPostsPerThread') @@ -116,7 +138,7 @@ class Thread(models.Model): Checks if the thread can be bumped by replying to it. """ - return self.bumpable and not self.archived + return self.bumpable and not self.is_archived() def get_last_replies(self) -> QuerySet: """ @@ -150,8 +172,8 @@ class Thread(models.Model): Gets sorted thread posts """ - query = Post.objects.filter(threads__in=[self]) - query = query.order_by('pub_time').prefetch_related('images', 'thread', 'threads') + query = self.multi_replies.order_by('pub_time').prefetch_related( + 'images', 'thread', 'threads', 'attachments') if view_fields_only: query = query.defer('poster_ip') return query.all() @@ -203,7 +225,7 @@ class Thread(models.Model): def update_posts_time(self, exclude_posts=None): last_edit_time = self.last_edit_time - for post in self.post_set.all(): + for post in self.multi_replies.all(): if exclude_posts is None or post not in exclude_posts: # Manual update is required because uids are generated on save post.last_edit_time = last_edit_time @@ -228,3 +250,9 @@ class Thread(models.Model): def get_required_tags(self): return self.get_tags().filter(required=True) + + def get_replies_newer(self, post_id): + return self.get_replies().filter(id__gt=post_id) + + def is_archived(self): + return self.archived diff --git a/boards/static/css/base.css b/boards/static/css/base.css --- a/boards/static/css/base.css +++ b/boards/static/css/base.css @@ -133,4 +133,13 @@ textarea, input { .tripcode { padding: 2px; +} + +#fav-panel { + display: none; + margin: 1ex; +} + +#new-fav-post-count { + display: none; } \ No newline at end of file diff --git a/boards/static/css/md/base_page.css b/boards/static/css/md/base_page.css --- a/boards/static/css/md/base_page.css +++ b/boards/static/css/md/base_page.css @@ -115,6 +115,14 @@ body { visibility: hidden; } +.tag_info { + text-align: center; +} + +.tag_info > .tag-text-data { + text-align: left; +} + .header { border-bottom: solid 2px #ccc; margin-bottom: 5px; @@ -416,6 +424,8 @@ li { padding: 5px; color: #eee; font-size: 2ex; + margin-top: .5ex; + margin-bottom: .5ex; } .skipped_replies { @@ -442,6 +452,7 @@ li { border: solid 1px; margin: 0.5ex; text-align: center; + padding: 1ex; } code { @@ -520,6 +531,11 @@ ul { border: 1px solid #777; background: #000; padding: 4px; + opacity: 0.3; +} + +#up:hover { + opacity: 1; } .user-cast { @@ -557,3 +573,7 @@ ul { .tripcode { color: white; } + +#fav-panel { + border: 1px solid white; +} diff --git a/boards/static/images/fileformats/djvu.png b/boards/static/images/fileformats/djvu.png new file mode 100644 index 0000000000000000000000000000000000000000..d42604cf291024bd7450f27b780787fe0a781d0c GIT binary patch literal 8314 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 diff --git a/boards/static/images/fileformats/txt.png b/boards/static/images/fileformats/txt.png new file mode 100644 index 0000000000000000000000000000000000000000..ce0be107bb357adc621dc980a88c3b02d205262a GIT binary patch literal 3950 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 win_w) { - img_h = img_h * (win_w/img_w) - margin; - img_w = win_w - margin; + + // New image size + var w_scale = 1; + var h_scale = 1; + + var freeWidth = win_w - 2 * IMAGE_POPUP_MARGIN; + var freeHeight = win_h - 2 * IMAGE_POPUP_MARGIN; + + if (full_img_w > freeWidth) { + w_scale = full_img_w / freeWidth; } - if (img_h > win_h) { - img_w = img_w * (win_h/img_h) - margin; - img_h = win_h - margin; + if (full_img_h > freeHeight) { + h_scale = full_img_h / freeHeight; } + var scale = Math.max(w_scale, h_scale) + var img_w = full_img_w / scale; + var img_h = full_img_h / scale; + + var postNode = $(el); + var img_pv = new Image(); var newImage = $(img_pv); newImage.addClass('img-full') .attr('id', thumb_id) - .attr('src', $(el).attr('href')) - .appendTo($(el)) + .attr('src', postNode.attr('href')) + .attr(ATTR_SCALE, scale) + .appendTo(postNode) .css({ 'width': img_w, 'height': img_h, @@ -107,19 +121,26 @@ PopupImageViewer.prototype.view = functi }) //scaling preview .mousewheel(function(event, delta) { - var cx = event.originalEvent.clientX, - cy = event.originalEvent.clientY, - i_w = parseFloat(newImage.width()), - i_h = parseFloat(newImage.height()), - newIW = i_w * (delta > 0 ? 1.25 : 0.8), - newIH = i_h * (delta > 0 ? 1.25 : 0.8); + var cx = event.originalEvent.clientX; + var cy = event.originalEvent.clientY; + + var scale = newImage.attr(ATTR_SCALE) / (delta > 0 ? 1.25 : 0.8); + + var oldWidth = newImage.width(); + var oldHeight = newImage.height(); + + var newIW = full_img_w / scale; + var newIH = full_img_h / scale; newImage.width(newIW); newImage.height(newIH); - //set position + newImage.attr(ATTR_SCALE, scale); + + // Set position + var oldPosition = newImage.position(); newImage.css({ - left: parseInt(cx - (newIW/i_w) * (cx - parseInt($(img_pv).position().left, 10)), 10), - top: parseInt(cy - (newIH/i_h) * (cy - parseInt($(img_pv).position().top, 10)), 10) + left: parseInt(cx - (newIW/oldWidth) * (cx - parseInt(oldPosition.left, 10)), 10), + top: parseInt(cy - (newIH/oldHeight) * (cy - parseInt(oldPosition.top, 10)), 10) }); return false; diff --git a/boards/static/js/main.js b/boards/static/js/main.js --- a/boards/static/js/main.js +++ b/boards/static/js/main.js @@ -23,6 +23,8 @@ for the JavaScript code in this page. */ +var FAV_POST_UPDATE_PERIOD = 10000; + /** * An email is a hidden file to prevent spam bots from posting. It has to be * hidden. @@ -40,6 +42,72 @@ function highlightCode(node) { }); } +function updateFavPosts() { + var includePostBody = $('#fav-panel').is(":visible"); + var url = '/api/new_posts/'; + if (includePostBody) { + url += '?include_posts' + } + $.getJSON(url, + function(data) { + var allNewPostCount = 0; + + if (includePostBody) { + var favoriteThreadPanel = $('#fav-panel'); + favoriteThreadPanel.empty(); + } + + $.each(data, function (_, dict) { + var newPostCount = dict.new_post_count; + allNewPostCount += newPostCount; + + if (includePostBody) { + var favThreadNode = $('
'); + favThreadNode.append($(dict.post_url)); + favThreadNode.append(' '); + favThreadNode.append($('' + dict.title + '')); + + if (newPostCount > 0) { + favThreadNode.append(' (+' + newPostCount + ")"); + } + + favoriteThreadPanel.append(favThreadNode); + + addRefLinkPreview(favThreadNode[0]); + } + }); + + var newPostCountNode = $('#new-fav-post-count'); + if (allNewPostCount > 0) { + newPostCountNode.text('(+' + allNewPostCount + ')'); + newPostCountNode.show(); + } else { + newPostCountNode.hide(); + } + } + ); +} + +function initFavPanel() { + updateFavPosts(); + + if ($('#fav-panel-btn').length > 0) { + setInterval(updateFavPosts, FAV_POST_UPDATE_PERIOD); + $('#fav-panel-btn').click(function() { + $('#fav-panel').toggle(); + updateFavPosts(); + + return false; + }); + + $(document).on('keyup.removepic', function(e) { + if(e.which === 27) { + $('#fav-panel').hide(); + } + }); + } +} + $( document ).ready(function() { hideEmailFromForm(); @@ -53,4 +121,6 @@ function highlightCode(node) { addRefLinkPreview(); highlightCode($(document)); + + initFavPanel(); }); diff --git a/boards/static/js/thread_update.js b/boards/static/js/thread_update.js --- a/boards/static/js/thread_update.js +++ b/boards/static/js/thread_update.js @@ -102,11 +102,10 @@ function connectWebsocket() { * missed. */ function getThreadDiff() { - var lastUpdateTime = $('.metapanel').attr('data-last-update'); - var lastPostId = $('.post').last().attr('id'); + var all_posts = $('.post'); var uids = ''; - var posts = $('.post'); + var posts = all_posts; for (var i = 0; i < posts.length; i++) { uids += posts[i].getAttribute('data-uid') + ' '; } @@ -114,7 +113,7 @@ function getThreadDiff() { var data = { uids: uids, thread: threadId - } + }; var diffUrl = '/api/diff_thread/'; @@ -244,8 +243,10 @@ function updateMetadataPanel() { var replyCountField = $('#reply-count'); var imageCountField = $('#image-count'); - replyCountField.text(getReplyCount()); - imageCountField.text(getImageCount()); + var replyCount = getReplyCount(); + replyCountField.text(replyCount); + var imageCount = getImageCount(); + imageCountField.text(imageCount); var lastUpdate = $('.post:last').children('.post-info').first() .children('.pub_time').first().html(); @@ -257,6 +258,9 @@ function updateMetadataPanel() { blink(replyCountField); blink(imageCountField); + + $('#message-count-text').text(ngettext('message', 'messages', replyCount)); + $('#image-count-text').text(ngettext('image', 'images', imageCount)); } /** @@ -380,9 +384,6 @@ function replacePartial(oldNode, newNode // Replace children var children = oldNode.children(); if (children.length == 0) { - console.log(oldContent); - console.log(newContent) - oldNode.replaceWith(newNode); } else { var newChildren = newNode.children(); @@ -426,7 +427,7 @@ function updateNodeAttr(oldNode, newNode var newAttr = newNode.attr(attrName); if (oldAttr != newAttr) { oldNode.attr(attrName, newAttr); - }; + } } $(document).ready(function(){ @@ -439,11 +440,11 @@ function updateNodeAttr(oldNode, newNode if (form.length > 0) { var options = { beforeSubmit: function(arr, $form, options) { - showAsErrors($('form'), gettext('Sending message...')); + showAsErrors($('#form'), gettext('Sending message...')); }, success: updateOnPost, error: function() { - showAsErrors($('form'), gettext('Server error!')); + showAsErrors($('#form'), gettext('Server error!')); }, url: '/api/add_post/' + threadId + '/' }; diff --git a/boards/templates/boards/all_threads.html b/boards/templates/boards/all_threads.html --- a/boards/templates/boards/all_threads.html +++ b/boards/templates/boards/all_threads.html @@ -37,7 +37,7 @@ {% endfor %} {% if tag %} -
+
{% if random_image_post %}
{% with image=random_image_post.images.first %} @@ -73,11 +73,12 @@

{{ tag.get_description|safe }}

{% endif %}

{% 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 %}

- {% if related_tags %} -

{% trans 'Related tags:' %} - {% for rel_tag in related_tags %} - {{ rel_tag.get_view|safe }}{% if not forloop.last %}, {% else %}.{% endif %} - {% endfor %} + {% if tag.get_parent %} +

+ {% if tag.get_parent %} + {{ tag.get_parent.get_view|safe }} / + {% endif %} + {{ tag.get_view|safe }}

{% endif %}
@@ -93,7 +94,7 @@ {% for thread in threads %}
- {% post_view thread.get_opening_post moderator=moderator is_opening=True thread=thread truncated=True need_open_link=True %} + {% post_view thread.get_opening_post moderator=moderator thread=thread truncated=True need_open_link=True %} {% if not thread.archived %} {% with last_replies=thread.get_last_replies %} {% if last_replies %} @@ -101,14 +102,14 @@ {% if skipped_replies_count %} {% endif %} {% endwith %}
{% for post in last_replies %} - {% post_view post is_opening=False moderator=moderator truncated=True %} + {% post_view post moderator=moderator truncated=True %} {% endfor %}
{% endif %} diff --git a/boards/templates/boards/base.html b/boards/templates/boards/base.html --- a/boards/templates/boards/base.html +++ b/boards/templates/boards/base.html @@ -39,7 +39,10 @@ {% trans "tags" %}, {% trans 'search' %}, {% trans 'feed' %}, - {% trans 'random' %} + {% trans 'random' %}{% if has_fav_threads %}, + + {% trans 'favorites' %} + {% endif %} {% if username %} @@ -53,6 +56,8 @@ {% trans 'Settings' %}
+
{% trans "Loading..." %}
+ {% block content %}{% endblock %} diff --git a/boards/templates/boards/post.html b/boards/templates/boards/post.html --- a/boards/templates/boards/post.html +++ b/boards/templates/boards/post.html @@ -36,7 +36,7 @@ {% else %} {% if need_op_data %} {% with thread.get_opening_post as op %} - {% trans " in " %}{{ op.get_link_view|safe }} {{ op.get_title|striptags|truncatewords:5 }} + {% trans " in " %}{{ op.get_link_view|safe }} {{ op.get_title_or_text }} {% endwith %} {% endif %} {% endif %} @@ -61,16 +61,12 @@ Post images. Currently only 1 image can be posted and shown, but post model supports multiple. {% endcomment %} - {% if post.images.exists %} - {% with post.images.first as image %} - {{ image.get_view|safe }} - {% endwith %} - {% endif %} - {% if post.attachments.exists %} - {% with post.attachments.first as file %} - {{ file.get_view|safe }} - {% endwith %} - {% endif %} + {% for image in post.images.all %} + {{ image.get_view|safe }} + {% endfor %} + {% for file in post.attachments.all %} + {{ file.get_view|safe }} + {% endfor %} {% comment %} Post message (text) {% endcomment %} @@ -102,8 +98,8 @@ {% if is_opening %}