diff --git a/.hgtags b/.hgtags --- a/.hgtags +++ b/.hgtags @@ -35,3 +35,5 @@ 4a5bec08ccfb47a27f9e98698f12dd5b7246623b 604935b98f5b5e4a5e903594f048046e1fbb3519 2.8.3 c48ffdc671566069ed0f33644da1229277f3cd18 2.9.0 d66dc192d4e089ba85325afeef5229b73cb0fde4 2.10.0 +1c22a38cca9ae3bee13d6f263792c0629d0061f6 2.10.1 +3076e0d03339f3b41dcc71fb6af2b4169920846c 2.11.0 diff --git a/boards/abstracts/paginator.py b/boards/abstracts/paginator.py --- a/boards/abstracts/paginator.py +++ b/boards/abstracts/paginator.py @@ -12,7 +12,14 @@ def get_paginator(*args, **kwargs): class DividedPaginator(Paginator): lookaround_size = PAGINATOR_LOOKAROUND_SIZE - current_page = 0 + + def __init__(self, object_list, per_page, orphans=0, + allow_empty_first_page=True, current_page=1): + super().__init__(object_list, per_page, orphans, allow_empty_first_page) + + self.link = None + self.params = None + self.current_page = current_page def _left_range(self): return self.page_range[:self.lookaround_size] @@ -67,8 +74,18 @@ class DividedPaginator(Paginator): def get_page_url(self, page): self.params['page'] = page url_params = '?' + '&'.join(['{}={}'.format(key, self.params[key]) - for key in self.params.keys()]) + for key in self.params.keys()]) return self.link + url_params def supports_urls(self): return self.link is not None and self.params is not None + + def get_next_page_url(self): + current = self.page(self.current_page) + if current.has_next(): + return self.get_page_url(current.next_page_number()) + + def get_prev_page_url(self): + current = self.page(self.current_page) + if current.has_previous(): + return self.get_page_url(current.previous_page_number()) \ No newline at end of file diff --git a/boards/abstracts/settingsmanager.py b/boards/abstracts/settingsmanager.py --- a/boards/abstracts/settingsmanager.py +++ b/boards/abstracts/settingsmanager.py @@ -143,6 +143,14 @@ class SettingsManager: def thread_is_fav(self, opening_post): return str(opening_post.id) in self.get_fav_threads() + def get_notification_usernames(self): + name_list = self.get_setting(SETTING_USERNAME) + if name_list is not None and len(name_list) > 0: + return name_list.lower().split(',') + else: + return list() + + class SessionSettingsManager(SettingsManager): """ Session-based settings manager. All settings are saved to the user's diff --git a/boards/admin.py b/boards/admin.py --- a/boards/admin.py +++ b/boards/admin.py @@ -10,7 +10,8 @@ class PostAdmin(admin.ModelAdmin): list_filter = ('pub_time',) search_fields = ('id', 'title', 'text', 'poster_ip') exclude = ('referenced_posts', 'refmap') - readonly_fields = ('poster_ip', 'threads', 'thread', 'images', 'uid') + readonly_fields = ('poster_ip', 'threads', 'thread', 'images', + 'attachments', 'uid', 'url', 'pub_time', 'opening') def ban_poster(self, request, queryset): bans = 0 @@ -55,9 +56,9 @@ class ThreadAdmin(admin.ModelAdmin): def op(self, obj: Thread): return obj.get_opening_post_id() - list_display = ('id', 'op', 'title', 'reply_count', 'archived', 'ip', + list_display = ('id', 'op', 'title', 'reply_count', 'status', 'ip', 'display_tags') - list_filter = ('bump_time', 'archived', 'bumpable') + list_filter = ('bump_time', 'status') 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.10.0 BT +Version = 2.11.0 Yuko SiteName = Neboard DEV [Cache] @@ -10,7 +10,8 @@ CacheTimeout = 600 # Max post length in characters MaxTextLength = 30000 MaxFileSize = 8000000 -LimitPostingSpeed = false +LimitPostingSpeed = true +PowDifficulty = 20 [Messages] # Thread bumplimit @@ -24,6 +25,7 @@ DefaultTheme = md DefaultImageViewer = simple LastRepliesCount = 3 ThreadsPerPage = 3 +ImagesPerPageGallery = 20 [Storage] # Enable archiving threads instead of deletion when the thread limit is reached @@ -32,3 +34,6 @@ ArchiveThreads = true [External] # Thread update WebsocketsEnabled = false + +[RSS] +MaxItems = 20 diff --git a/boards/context_processors.py b/boards/context_processors.py --- a/boards/context_processors.py +++ b/boards/context_processors.py @@ -1,39 +1,39 @@ from boards.abstracts.settingsmanager import get_settings_manager, \ - SETTING_USERNAME, SETTING_LAST_NOTIFICATION_ID, SETTING_IMAGE_VIEWER + SETTING_LAST_NOTIFICATION_ID, SETTING_IMAGE_VIEWER from boards.models.user import Notification __author__ = 'neko259' -from boards import settings, utils +from boards import settings from boards.models import Post, Tag CONTEXT_SITE_NAME = 'site_name' CONTEXT_VERSION = 'version' -CONTEXT_MODERATOR = 'moderator' CONTEXT_THEME_CSS = 'theme_css' CONTEXT_THEME = 'theme' CONTEXT_PPD = 'posts_per_day' CONTEXT_TAGS = 'tags' CONTEXT_USER = 'user' CONTEXT_NEW_NOTIFICATIONS_COUNT = 'new_notifications_count' -CONTEXT_USERNAME = 'username' +CONTEXT_USERNAMES = 'usernames' CONTEXT_TAGS_STR = 'tags_str' CONTEXT_IMAGE_VIEWER = 'image_viewer' CONTEXT_HAS_FAV_THREADS = 'has_fav_threads' +CONTEXT_POW_DIFFICULTY = 'pow_difficulty' def get_notifications(context, request): settings_manager = get_settings_manager(request) - username = settings_manager.get_setting(SETTING_USERNAME) + usernames = settings_manager.get_notification_usernames() new_notifications_count = 0 - if username is not None and len(username) > 0: + if usernames is not None: last_notification_id = settings_manager.get_setting( SETTING_LAST_NOTIFICATION_ID) new_notifications_count = Notification.objects.get_notification_posts( - username=username, last=last_notification_id).count() + usernames=usernames, last=last_notification_id).count() context[CONTEXT_NEW_NOTIFICATIONS_COUNT] = new_notifications_count - context[CONTEXT_USERNAME] = username + context[CONTEXT_USERNAMES] = usernames def user_and_ui_processor(request): @@ -50,12 +50,12 @@ def user_and_ui_processor(request): context[CONTEXT_THEME] = theme context[CONTEXT_THEME_CSS] = 'css/' + theme + '/base_page.css' - # This shows the moderator panel - context[CONTEXT_MODERATOR] = utils.is_moderator(request) - context[CONTEXT_VERSION] = settings.get('Version', 'Version') context[CONTEXT_SITE_NAME] = settings.get('Version', 'SiteName') + if settings.get_bool('Forms', 'LimitPostingSpeed'): + context[CONTEXT_POW_DIFFICULTY] = settings.get_int('Forms', 'PowDifficulty') + context[CONTEXT_IMAGE_VIEWER] = settings_manager.get_setting( SETTING_IMAGE_VIEWER, default=settings.get('View', 'DefaultImageViewer')) diff --git a/boards/forms.py b/boards/forms.py --- a/boards/forms.py +++ b/boards/forms.py @@ -2,6 +2,7 @@ import hashlib import re import time import logging + import pytz from django import forms @@ -9,6 +10,7 @@ from django.core.files.uploadedfile impo from django.core.exceptions import ObjectDoesNotExist from django.forms.util import ErrorList from django.utils.translation import ugettext_lazy as _, ungettext_lazy +from django.utils import timezone from boards.mdx_neboard import formatters from boards.models.attachment.downloaders import Downloader @@ -20,7 +22,11 @@ from neboard import settings import boards.settings as board_settings import neboard +POW_HASH_LENGTH = 16 +POW_LIFE_MINUTES = 1 + REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE) +REGEX_USERNAMES = re.compile(r'^[\w\s\d,]+$', re.UNICODE) VETERAN_POSTING_DELAY = 5 @@ -82,7 +88,7 @@ class FormatPanel(forms.Textarea): formatter.preview_right + '' output += '' - output += super(FormatPanel, self).render(name, value, attrs=None) + output += super(FormatPanel, self).render(name, value, attrs=attrs) return output @@ -168,6 +174,10 @@ class PostForm(NeboardForm): widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: '123 456 789'})) + guess = forms.CharField(widget=forms.HiddenInput(), required=False) + timestamp = forms.CharField(widget=forms.HiddenInput(), required=False) + iteration = forms.CharField(widget=forms.HiddenInput(), required=False) + session = None need_to_ban = False @@ -238,7 +248,7 @@ class PostForm(NeboardForm): for thread_id in threads_id_list: try: thread = Post.objects.get(id=int(thread_id)) - if not thread.is_opening() or thread.get_thread().archived: + if not thread.is_opening() or thread.get_thread().is_archived(): raise ObjectDoesNotExist() threads.append(thread) except (ObjectDoesNotExist, ValueError): @@ -256,8 +266,13 @@ class PostForm(NeboardForm): if not self.errors: self._clean_text_file() - if not self.errors and self.session: - self._validate_posting_speed() + limit_speed = board_settings.get_bool('Forms', 'LimitPostingSpeed') + if not self.errors and limit_speed: + pow_difficulty = board_settings.get_int('Forms', 'PowDifficulty') + if pow_difficulty > 0 and cleaned_data['timestamp'] and cleaned_data['iteration'] and cleaned_data['guess']: + self._validate_hash(cleaned_data['timestamp'], cleaned_data['iteration'], cleaned_data['guess'], cleaned_data['text']) + else: + self._validate_posting_speed() return cleaned_data @@ -341,8 +356,26 @@ class PostForm(NeboardForm): except forms.ValidationError as e: raise e except Exception as e: - # Just return no file - pass + raise forms.ValidationError(e) + + def _validate_hash(self, timestamp: str, iteration: str, guess: str, message: str): + post_time = timezone.datetime.fromtimestamp( + int(timestamp[:-3]), tz=timezone.get_current_timezone()) + timedelta = (timezone.now() - post_time).seconds / 60 + if timedelta > POW_LIFE_MINUTES: + self._errors['text'] = self.error_class([_('Stale PoW.')]) + + payload = timestamp + message.replace('\r\n', '\n') + difficulty = board_settings.get_int('Forms', 'PowDifficulty') + target = str(int(2 ** (POW_HASH_LENGTH * 3) / difficulty)) + if len(target) < POW_HASH_LENGTH: + target = '0' * (POW_HASH_LENGTH - len(target)) + target + + computed_guess = hashlib.sha256((payload + iteration).encode())\ + .hexdigest()[0:POW_HASH_LENGTH] + if guess != computed_guess or guess > target: + self._errors['text'] = self.error_class( + [_('Invalid PoW.')]) class ThreadForm(PostForm): @@ -350,6 +383,7 @@ class ThreadForm(PostForm): tags = forms.CharField( widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}), max_length=100, label=_('Tags'), required=True) + monochrome = forms.BooleanField(label=_('Monochrome'), required=False) def clean_tags(self): tags = self.cleaned_data['tags'].strip() @@ -385,6 +419,9 @@ class ThreadForm(PostForm): return cleaned_data + def is_monochrome(self): + return self.cleaned_data['monochrome'] + class SettingsForm(NeboardForm): @@ -396,7 +433,7 @@ class SettingsForm(NeboardForm): def clean_username(self): username = self.cleaned_data['username'] - if username and not REGEX_TAGS.match(username): + if username and not REGEX_USERNAMES.match(username): raise forms.ValidationError(_('Inappropriate characters.')) return username diff --git a/boards/locale/ru/LC_MESSAGES/django.mo b/boards/locale/ru/LC_MESSAGES/django.mo index f512f1dba9e9d8f0cfc8b35c50c24c967d65a3d3..c9505039fdd1cf02f1a6df56384ccc3ef8fd3316 GIT binary patch literal 9791 zc$~#peQX@ZbsxuR+DalRy0s{Y5j&JcS*m>(QgT}PEIOtvN@mRunWW@6NkcDrOLDEd z-P7)!BpQY*nNj72ikYNNVyJa&+G&iUO>m+}NhD>eK!XmTzxUqk-I6>;Mq9Ke?f!Ol-n{SkW|n_<*N4BN@GIc@D6a3^snn~$%+D+S{oogs z`Y`bCfvvzF0`~(OHYlZmj{|oRJ@6p#OTb?M{snL|@GpUP0pA1u9PoYMM&KPA1#564 zQQLqY0X736LhS*9RP8hK?ZA3qrx`y9yc;;QQS>Wp6uHkDzLOh8uW2(sXK=yLEt~m2 zH~jtyxC!_ZLw5%Tb-<4Rn}J(^+kgi)30?mtkuwBr0-iME&jV|KUj^0zzp+W|xC-1( za?SX!&G;Y8y8qlHc5kc^xp&tHT??=U*b2NCc%nx9^n8ubU#Jmz)3|*I_-c*l|9Xw^ z{R2aP1vn18Vdk4_h5znavA?ZW_&iuEaXMZW`3ep^!zQuXS!DSUal2+ zuhoiv->emX7tQ=at@vdbw*dHVt>j%to$&u`o$wnqc&bkF=eauJI}N;({97k{F4qa) zKdh7d`?eYX2)G;gFLmOFhRve)J;1HNy_-e9L*{+=X7O+TW?BC%@DZ|av-tH-4E>*O z7Jc5`Eb@K;OaXV)i+-Q17kh{5g?`+iUoU*0tC##a5Bz1|OZ8&+oApvJe+1kCyjd@D z{>JeAXM;a6eBQ4YyNKxe9pHJFy#LNGDdhm)F}NQ>hAA(ACx{-we4gqD*a-aI-QuSo z-z|BzZHrPnfuGnS`VDW9IG@=fdSBck{(BktE5L7Uk-B&ni2v#z&HA63adU(C^+1Ez z_jH5EJ=GxVzR)0keyc&^{a%Ck?_V1vzW)LI4XTT+GCr|Y>c-wG{LTR%0DcL00QhIX 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 diff --git a/boards/locale/ru/LC_MESSAGES/django.po b/boards/locale/ru/LC_MESSAGES/django.po --- a/boards/locale/ru/LC_MESSAGES/django.po +++ b/boards/locale/ru/LC_MESSAGES/django.po @@ -143,8 +143,8 @@ msgid "This page does not exist" msgstr "Этой страницы не существует" #: templates/boards/all_threads.html:35 -msgid "Related message" -msgstr "Связанное сообщение" +msgid "Details" +msgstr "Подробности" #: templates/boards/all_threads.html:69 msgid "Edit tag" @@ -488,8 +488,8 @@ msgstr "Ок" #: utils.py:120 #, python-format -msgid "File must be less than %s bytes" -msgstr "Файл должен быть менее %s байт" +msgid "File must be less than %s but is %s." +msgstr "Файл должен быть менее %s, но его размер %s." msgid "Please wait %(delay)d second before sending message" msgid_plural "Please wait %(delay)d seconds before sending message" @@ -499,3 +499,34 @@ msgstr[2] "Пожалуйста подождите %(delay)d секунд перед отправкой сообщения" msgid "New threads" msgstr "Новые темы" + +#, python-format +msgid "Max file size is %(size)s." +msgstr "Максимальный размер файла %(size)s." + +msgid "Size of media:" +msgstr "Размер медиа:" + +msgid "Statistics" +msgstr "Статистика" + +msgid "Invalid PoW." +msgstr "Неверный PoW." + +msgid "Stale PoW." +msgstr "PoW устарел." + +msgid "Show" +msgstr "Показывать" + +msgid "Hide" +msgstr "Скрывать" + +msgid "Add to favorites" +msgstr "Добавить в избранное" + +msgid "Remove from favorites" +msgstr "Убрать из избранного" + +msgid "Monochrome" +msgstr "Монохромный" \ No newline at end of file diff --git a/boards/locale/ru/LC_MESSAGES/djangojs.mo b/boards/locale/ru/LC_MESSAGES/djangojs.mo index 6f605db078c248373980e3357705bfd3fa87bf9c..4343e52de20ae6f55c65cc2fd833df3bfde11c2e GIT binary patch literal 1018 zc${sLzi-n(6vwYWpve3X6tOW}k*KJu3wB#XaN|;wxFy0)q9iQ>AtBRTgHhs(&Q1kV zhqesBR52hHRKb6c7D6c!MQpI0R|Yl)c2*>w?M4x_XMOJb?%ns@JzM@U*#82-I0Bpn z?f^%DS12A|fs?>DKmxu4CxD;8An+SF4h-}lbPV)NPwYPj+6Kmf)3E;48|xp?VbD{3 zu_pWCd+&l~K$k(2pkF|HP+HQfp-Q8(Pdb?SL`m4=QIZpeboIRimKFL_av^+=hiSSsYpB+aSqLlMc?K2`CU ztXv}U6(UbyC8`+iD7meS1isSDRVYJkC~5B-acn(ergc;UXyJ?d+7 str: + return self.text or self.post.get_text() diff --git a/boards/models/image.py b/boards/models/image.py --- a/boards/models/image.py +++ b/boards/models/image.py @@ -4,8 +4,10 @@ from django.template.defaultfilters impo from boards import thumbs, utils import boards from boards.models.base import Viewable +from boards.models import STATUS_ARCHIVE from boards.utils import get_upload_filename + __author__ = 'neko259' @@ -27,8 +29,8 @@ class PostImageManager(models.Manager): return post_image - def get_random_images(self, count, include_archived=False, tags=None): - images = self.filter(post_images__thread__archived=include_archived) + def get_random_images(self, count, tags=None): + images = self.exclude(post_images__thread__status=STATUS_ARCHIVE) if tags is not None: images = images.filter(post_images__threads__tags__in=tags) return images.order_by('?')[:count] 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 @@ -23,6 +23,7 @@ CSS_CLS_HIDDEN_POST = 'hidden_post' CSS_CLS_DEAD_POST = 'dead_post' CSS_CLS_ARCHIVE_POST = 'archive_post' CSS_CLS_POST = 'post' +CSS_CLS_MONOCHROME = 'monochrome' TITLE_MAX_WORDS = 10 @@ -46,7 +47,6 @@ PARAMETER_DIFF_TYPE = 'type' PARAMETER_CSS_CLASS = 'css_class' PARAMETER_THREAD = 'thread' PARAMETER_IS_OPENING = 'is_opening' -PARAMETER_MODERATOR = 'moderator' PARAMETER_POST = 'post' PARAMETER_OP_ID = 'opening_post_id' PARAMETER_NEED_OPEN_LINK = 'need_open_link' @@ -56,10 +56,10 @@ PARAMETER_NEED_OP_DATA = 'need_op_data' POST_VIEW_PARAMS = ( 'need_op_data', 'reply_link', - 'moderator', 'need_open_link', 'truncated', 'mode_tree', + 'perms', ) @@ -185,12 +185,14 @@ class Post(models.Model, Viewable): thread = self.get_thread() css_classes = [CSS_CLS_POST] - if thread.archived: + if thread.is_archived(): css_classes.append(CSS_CLS_ARCHIVE_POST) elif not thread.can_bump(): css_classes.append(CSS_CLS_DEAD_POST) if self.is_hidden(): css_classes.append(CSS_CLS_HIDDEN_POST) + if thread.is_monochrome(): + css_classes.append(CSS_CLS_MONOCHROME) params = dict() for param in POST_VIEW_PARAMS: @@ -332,20 +334,29 @@ class Post(models.Model, Viewable): def save(self, force_insert=False, force_update=False, using=None, update_fields=None): + new_post = self.id is None + self._text_rendered = Parser().parse(self.get_raw_text()) self.uid = str(uuid.uuid4()) if update_fields is not None and 'uid' not in update_fields: update_fields += ['uid'] - if self.id: + if not new_post: for thread in self.get_threads().all(): thread.last_edit_time = self.last_edit_time - thread.save(update_fields=['last_edit_time', 'bumpable']) + thread.save(update_fields=['last_edit_time', 'status']) super().save(force_insert, force_update, using, update_fields) + # Post save triggers + if new_post: + self.build_url() + + self._connect_replies() + self._connect_notifications() + def get_text(self) -> str: return self._text_rendered @@ -380,12 +391,12 @@ class Post(models.Model, Viewable): else: return str(self.id) - def connect_notifications(self): + def _connect_notifications(self): for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()): user_name = reply_number.group(1).lower() Notification.objects.get_or_create(name=user_name, post=self) - def connect_replies(self): + def _connect_replies(self): """ Connects replies to a post to show them as a reflink map """ @@ -411,7 +422,7 @@ class Post(models.Model, Viewable): thread.update_bump_status() thread.last_edit_time = self.last_edit_time - thread.save(update_fields=['last_edit_time', 'bumpable']) + thread.save(update_fields=['last_edit_time', 'status']) self.threads.add(opening_post.get_thread()) def get_tripcode(self): diff --git a/boards/models/post/export.py b/boards/models/post/export.py --- a/boards/models/post/export.py +++ b/boards/models/post/export.py @@ -1,3 +1,5 @@ +from django.contrib.auth.context_processors import PermWrapper + from boards import utils @@ -24,7 +26,7 @@ class HtmlExporter(Exporter): reply_link = True return post.get_view(truncated=truncated, reply_link=reply_link, - moderator=utils.is_moderator(request)) + perms=PermWrapper(request.user)) class JsonExporter(Exporter): 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 @@ -31,13 +31,15 @@ class PostManager(models.Manager): @transaction.atomic def create_post(self, title: str, text: str, file=None, thread=None, ip=NO_IP, tags: list=None, opening_posts: list=None, - tripcode=''): + tripcode='', monochrome=False): """ Creates new post """ if not utils.is_anonymous_mode(): is_banned = Ban.objects.filter(ip=ip).exists() + else: + is_banned = False # TODO Raise specific exception and catch it in the views if is_banned: @@ -52,7 +54,8 @@ class PostManager(models.Manager): new_thread = False if not thread: thread = boards.models.thread.Thread.objects.create( - bump_time=posting_time, last_edit_time=posting_time) + bump_time=posting_time, last_edit_time=posting_time, + monochrome=monochrome) list(map(thread.tags.add, tags)) boards.models.thread.Thread.objects.process_oldest_threads() new_thread = True @@ -72,7 +75,7 @@ class PostManager(models.Manager): logger = logging.getLogger('boards.post.create') logger.info('Created post [{}] with text [{}] by {}'.format(post, - post.get_text(),post.poster_ip)) + post.get_text(),post.poster_ip)) # TODO Move this to other place if file: @@ -82,10 +85,7 @@ class PostManager(models.Manager): else: post.attachments.add(Attachment.objects.create_with_hash(file)) - post.build_url() - post.connect_replies() post.connect_threads(opening_posts) - post.connect_notifications() post.set_global_id() # Thread needs to be bumped only when the post is already created @@ -147,6 +147,3 @@ class PostManager(models.Manager): thread=thread) post.threads.add(thread) - post.build_url() - post.connect_replies() - post.connect_notifications() diff --git a/boards/models/tag.py b/boards/models/tag.py --- a/boards/models/tag.py +++ b/boards/models/tag.py @@ -4,7 +4,9 @@ from django.db import models from django.db.models import Count from django.core.urlresolvers import reverse +from boards.models import PostImage from boards.models.base import Viewable +from boards.models.thread import STATUS_ACTIVE, STATUS_BUMPLIMIT, STATUS_ARCHIVE from boards.utils import cached_result import boards @@ -61,22 +63,20 @@ class Tag(models.Model, Viewable): return self.get_thread_count() == 0 - def get_thread_count(self, archived=None, bumpable=None) -> int: + def get_thread_count(self, status=None) -> int: threads = self.get_threads() - if archived is not None: - threads = threads.filter(archived=archived) - if bumpable is not None: - threads = threads.filter(bumpable=bumpable) + if status is not None: + threads = threads.filter(status=status) return threads.count() def get_active_thread_count(self) -> int: - return self.get_thread_count(archived=False, bumpable=True) + return self.get_thread_count(status=STATUS_ACTIVE) def get_bumplimit_thread_count(self) -> int: - return self.get_thread_count(archived=False, bumpable=False) + return self.get_thread_count(status=STATUS_BUMPLIMIT) def get_archived_thread_count(self) -> int: - return self.get_thread_count(archived=True) + return self.get_thread_count(status=STATUS_ARCHIVE) def get_absolute_url(self): return reverse('tag', kwargs={'tag_name': self.name}) @@ -106,11 +106,11 @@ class Tag(models.Model, Viewable): def get_description(self): return self.description - def get_random_image_post(self, archived=False): + def get_random_image_post(self, status=[STATUS_ACTIVE, STATUS_BUMPLIMIT]): posts = boards.models.Post.objects.annotate(images_count=Count( 'images')).filter(images_count__gt=0, threads__tags__in=[self]) - if archived is not None: - posts = posts.filter(thread__archived=archived) + if status is not None: + posts = posts.filter(thread__status__in=status) return posts.order_by('?').first() def get_first_letter(self): @@ -141,3 +141,7 @@ class Tag(models.Model, Viewable): def get_children(self): return self.children + + def get_images(self): + return PostImage.objects.filter(post_images__thread__tags__in=[self])\ + .order_by('-post_images__pub_time') \ No newline at end of file diff --git a/boards/models/thread.py b/boards/models/thread.py --- a/boards/models/thread.py +++ b/boards/models/thread.py @@ -5,6 +5,8 @@ from django.db.models import Count, Sum, from django.utils import timezone from django.db import models +from boards.models import STATUS_BUMPLIMIT, STATUS_ACTIVE, STATUS_ARCHIVE + from boards import settings import boards from boards.utils import cached_result, datetime_to_epoch @@ -25,6 +27,12 @@ WS_NOTIFICATION_TYPE = 'notification_typ WS_CHANNEL_THREAD = "thread:" +STATUS_CHOICES = ( + (STATUS_ACTIVE, STATUS_ACTIVE), + (STATUS_BUMPLIMIT, STATUS_BUMPLIMIT), + (STATUS_ARCHIVE, STATUS_ARCHIVE), +) + class ThreadManager(models.Manager): def process_oldest_threads(self): @@ -33,7 +41,7 @@ class ThreadManager(models.Manager): archive or delete the old ones. """ - threads = Thread.objects.filter(archived=False).order_by('-bump_time') + threads = Thread.objects.exclude(status=STATUS_ARCHIVE).order_by('-bump_time') thread_count = threads.count() max_thread_count = settings.get_int('Messages', 'MaxThreadCount') @@ -50,11 +58,10 @@ class ThreadManager(models.Manager): logger.info('Processed %d old threads' % num_threads_to_delete) def _archive_thread(self, thread): - thread.archived = True - thread.bumpable = False + thread.status = STATUS_ARCHIVE thread.last_edit_time = timezone.now() thread.update_posts_time() - thread.save(update_fields=['archived', 'last_edit_time', 'bumpable']) + thread.save(update_fields=['last_edit_time', 'status']) def get_new_posts(self, datas): query = None @@ -90,9 +97,10 @@ class Thread(models.Model): tags = models.ManyToManyField('Tag', related_name='thread_tags') bump_time = models.DateTimeField(db_index=True) last_edit_time = models.DateTimeField() - archived = models.BooleanField(default=False) - bumpable = models.BooleanField(default=True) max_posts = models.IntegerField(default=get_thread_max_posts) + status = models.CharField(max_length=50, default=STATUS_ACTIVE, + choices=STATUS_CHOICES) + monochrome = models.BooleanField(default=False) def get_tags(self) -> QuerySet: """ @@ -118,7 +126,7 @@ class Thread(models.Model): def update_bump_status(self, exclude_posts=None): if self.has_post_limit() and self.get_reply_count() >= self.max_posts: - self.bumpable = False + self.status = STATUS_BUMPLIMIT self.update_posts_time(exclude_posts=exclude_posts) def _get_cache_key(self): @@ -138,7 +146,7 @@ class Thread(models.Model): Checks if the thread can be bumped by replying to it. """ - return self.bumpable and not self.is_archived() + return self.get_status() == STATUS_ACTIVE def get_last_replies(self) -> QuerySet: """ @@ -255,4 +263,10 @@ class Thread(models.Model): return self.get_replies().filter(id__gt=post_id) def is_archived(self): - return self.archived + return self.get_status() == STATUS_ARCHIVE + + def get_status(self): + return self.status + + def is_monochrome(self): + return self.monochrome diff --git a/boards/models/user.py b/boards/models/user.py --- a/boards/models/user.py +++ b/boards/models/user.py @@ -22,10 +22,10 @@ class Ban(models.Model): class NotificationManager(models.Manager): - def get_notification_posts(self, username: str, last: int = None): - i_username = username.lower() - - posts = boards.models.post.Post.objects.filter(notification__name=i_username) + def get_notification_posts(self, usernames: list, last: int = None): + lower_names = [username.lower() for username in usernames] + posts = boards.models.post.Post.objects.filter( + notification__name__in=lower_names).distinct() if last is not None: posts = posts.filter(id__gt=last) posts = posts.order_by('-id') diff --git a/boards/rss.py b/boards/rss.py --- a/boards/rss.py +++ b/boards/rss.py @@ -3,8 +3,12 @@ from django.core.urlresolvers import rev from django.shortcuts import get_object_or_404 from boards.models import Post, Tag, Thread from boards import settings +from boards.models.thread import STATUS_ARCHIVE -__author__ = 'neko259' +__author__ = 'nekorin' + + +MAX_ITEMS = settings.get_int('RSS', 'MaxItems') # TODO Make tests for all of these @@ -15,7 +19,7 @@ class AllThreadsFeed(Feed): description_template = 'boards/rss/post.html' def items(self): - return Thread.objects.filter(archived=False).order_by('-id') + return Thread.objects.exclude(status=STATUS_ARCHIVE).order_by('-id')[:MAX_ITEMS] def item_title(self, item): return item.get_opening_post().title @@ -33,7 +37,7 @@ class TagThreadsFeed(Feed): description_template = 'boards/rss/post.html' def items(self, obj): - return obj.threads.filter(archived=False).order_by('-id') + return obj.get_threads().exclude(status=STATUS_ARCHIVE).order_by('-id')[:MAX_ITEMS] def get_object(self, request, tag_name): return get_object_or_404(Tag, name=tag_name) @@ -57,7 +61,7 @@ class ThreadPostsFeed(Feed): description_template = 'boards/rss/post.html' def items(self, obj): - return obj.get_thread().get_replies() + return obj.get_thread().get_replies().order_by('-pub_time')[:MAX_ITEMS] def get_object(self, request, post_id): return get_object_or_404(Post, id=post_id) 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 @@ -90,6 +90,7 @@ textarea, input { padding: inherit; background: none; font-size: inherit; + cursor: pointer; } #form-close-button { @@ -151,3 +152,8 @@ textarea, input { .hidden_post:hover { opacity: 1; } + +.monochrome > .image > .thumb > img { + filter: grayscale(100%); + -webkit-filter: grayscale(100%); +} 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 @@ -388,10 +388,6 @@ li { color: #ccc; } -.role { - text-decoration: underline; -} - .form-email { display: none; } @@ -566,7 +562,6 @@ ul { } .image-metadata { - font-style: italic; font-size: 0.9em; } @@ -577,3 +572,7 @@ ul { #fav-panel { border: 1px solid white; } + +.post-blink { + background-color: #000; +} diff --git a/boards/static/css/pg/base_page.css b/boards/static/css/pg/base_page.css --- a/boards/static/css/pg/base_page.css +++ b/boards/static/css/pg/base_page.css @@ -302,10 +302,6 @@ input[type="submit"]:hover { color: #555; } -.role { - text-decoration: underline; -} - .form-email { display: none; } @@ -380,4 +376,8 @@ input[type="submit"]:hover { .image-metadata { font-style: italic; font-size: 0.9em; -} \ No newline at end of file +} + +.post-blink { + background-color: #333; +} diff --git a/boards/static/css/sw/base_page.css b/boards/static/css/sw/base_page.css --- a/boards/static/css/sw/base_page.css +++ b/boards/static/css/sw/base_page.css @@ -279,10 +279,6 @@ li { color: #ccc; } -.role { - text-decoration: underline; -} - .form-email { display: none; } @@ -416,3 +412,7 @@ li { audio { margin-top: 1em; } + +.post-blink { + background-color: #ccc; +} diff --git a/boards/static/js/3party/sha256.js b/boards/static/js/3party/sha256.js new file mode 100644 --- /dev/null +++ b/boards/static/js/3party/sha256.js @@ -0,0 +1,16 @@ +/* +CryptoJS v3.1.2 +code.google.com/p/crypto-js +(c) 2009-2013 by Jeff Mott. All rights reserved. +code.google.com/p/crypto-js/wiki/License +*/ +var CryptoJS=CryptoJS||function(h,s){var f={},t=f.lib={},g=function(){},j=t.Base={extend:function(a){g.prototype=this;var c=new g;a&&c.mixIn(a);c.hasOwnProperty("init")||(c.init=function(){c.$super.init.apply(this,arguments)});c.init.prototype=c;c.$super=this;return c},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var c in a)a.hasOwnProperty(c)&&(this[c]=a[c]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}}, +q=t.WordArray=j.extend({init:function(a,c){a=this.words=a||[];this.sigBytes=c!=s?c:4*a.length},toString:function(a){return(a||u).stringify(this)},concat:function(a){var c=this.words,d=a.words,b=this.sigBytes;a=a.sigBytes;this.clamp();if(b%4)for(var e=0;e>>2]|=(d[e>>>2]>>>24-8*(e%4)&255)<<24-8*((b+e)%4);else if(65535>>2]=d[e>>>2];else c.push.apply(c,d);this.sigBytes+=a;return this},clamp:function(){var a=this.words,c=this.sigBytes;a[c>>>2]&=4294967295<< +32-8*(c%4);a.length=h.ceil(c/4)},clone:function(){var a=j.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var c=[],d=0;d>>2]>>>24-8*(b%4)&255;d.push((e>>>4).toString(16));d.push((e&15).toString(16))}return d.join("")},parse:function(a){for(var c=a.length,d=[],b=0;b>>3]|=parseInt(a.substr(b, +2),16)<<24-4*(b%8);return new q.init(d,c/2)}},k=v.Latin1={stringify:function(a){var c=a.words;a=a.sigBytes;for(var d=[],b=0;b>>2]>>>24-8*(b%4)&255));return d.join("")},parse:function(a){for(var c=a.length,d=[],b=0;b>>2]|=(a.charCodeAt(b)&255)<<24-8*(b%4);return new q.init(d,c)}},l=v.Utf8={stringify:function(a){try{return decodeURIComponent(escape(k.stringify(a)))}catch(c){throw Error("Malformed UTF-8 data");}},parse:function(a){return k.parse(unescape(encodeURIComponent(a)))}}, +x=t.BufferedBlockAlgorithm=j.extend({reset:function(){this._data=new q.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=l.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var c=this._data,d=c.words,b=c.sigBytes,e=this.blockSize,f=b/(4*e),f=a?h.ceil(f):h.max((f|0)-this._minBufferSize,0);a=f*e;b=h.min(4*a,b);if(a){for(var m=0;mk;){var l;a:{l=u;for(var x=h.sqrt(l),w=2;w<=x;w++)if(!(l%w)){l=!1;break a}l=!0}l&&(8>k&&(j[k]=v(h.pow(u,0.5))),q[k]=v(h.pow(u,1/3)),k++);u++}var a=[],f=f.SHA256=g.extend({_doReset:function(){this._hash=new t.init(j.slice(0))},_doProcessBlock:function(c,d){for(var b=this._hash.words,e=b[0],f=b[1],m=b[2],h=b[3],p=b[4],j=b[5],k=b[6],l=b[7],n=0;64>n;n++){if(16>n)a[n]= +c[d+n]|0;else{var r=a[n-15],g=a[n-2];a[n]=((r<<25|r>>>7)^(r<<14|r>>>18)^r>>>3)+a[n-7]+((g<<15|g>>>17)^(g<<13|g>>>19)^g>>>10)+a[n-16]}r=l+((p<<26|p>>>6)^(p<<21|p>>>11)^(p<<7|p>>>25))+(p&j^~p&k)+q[n]+a[n];g=((e<<30|e>>>2)^(e<<19|e>>>13)^(e<<10|e>>>22))+(e&f^e&m^f&m);l=k;k=j;j=p;p=h+r|0;h=m;m=f;f=e;e=r+g|0}b[0]=b[0]+e|0;b[1]=b[1]+f|0;b[2]=b[2]+m|0;b[3]=b[3]+h|0;b[4]=b[4]+p|0;b[5]=b[5]+j|0;b[6]=b[6]+k|0;b[7]=b[7]+l|0},_doFinalize:function(){var a=this._data,d=a.words,b=8*this._nDataBytes,e=8*a.sigBytes; +d[e>>>5]|=128<<24-e%32;d[(e+64>>>9<<4)+14]=h.floor(b/4294967296);d[(e+64>>>9<<4)+15]=b;a.sigBytes=4*d.length;this._process();return this._hash},clone:function(){var a=g.clone.call(this);a._hash=this._hash.clone();return a}});s.SHA256=g._createHelper(f);s.HmacSHA256=g._createHmacHelper(f)})(Math); diff --git a/boards/static/js/form.js b/boards/static/js/form.js --- a/boards/static/js/form.js +++ b/boards/static/js/form.js @@ -22,7 +22,7 @@ var form = $('#form'); $('textarea').keypress(function(event) { if (event.which == 13 && event.ctrlKey) { - form.submit(); + form.find('input[type=submit]').click(); } }); @@ -40,4 +40,56 @@ var form = $('#form'); previewTextBlock.html(data); previewTextBlock.show(); }) -}) +}); + +/** + * Show text in the errors row of the form. + * @param form + * @param text + */ +function showAsErrors(form, text) { + form.children('.form-errors').remove(); + + if (text.length > 0) { + var errorList = $('
' + text + '
'); + errorList.appendTo(form); + } +} + +function addHiddenInput(form, name, value) { + form.find('input[name=' + name + ']').val(value); +} + +$(document).ready(function() { + var powDifficulty = parseInt($('body').attr('data-pow-difficulty')); + if (powDifficulty > 0) { + var worker = new Worker($('#powScript').attr('src')); + worker.onmessage = function(e) { + var form = $('#form'); + addHiddenInput(form, 'timestamp', e.data.timestamp); + addHiddenInput(form, 'iteration', e.data.iteration); + addHiddenInput(form, 'guess', e.data.guess); + + form.submit(); + form.find('input[type=submit]').toggle(); + }; + + var form = $('#form'); + var submitButton = form.find('input[type=submit]'); + submitButton.click(function() { + showAsErrors(form, gettext('Computing PoW...')); + submitButton.toggle(); + + var msg = $('textarea').val().trim(); + + var data = { + msg: msg, + difficulty: parseInt($('body').attr('data-pow-difficulty')), + hasher: $('#sha256Script').attr('src') + }; + worker.postMessage(data); + + return false; + }); + } +}); diff --git a/boards/static/js/image.js b/boards/static/js/image.js --- a/boards/static/js/image.js +++ b/boards/static/js/image.js @@ -36,6 +36,37 @@ var FULL_IMG_CLASS = 'post-image-full'; var ATTR_SCALE = 'scale'; +// Init image viewer +var viewerName = $('body').attr('data-image-viewer'); +var viewer = ImageViewer(); +for (var i = 0; i < IMAGE_VIEWERS.length; i++) { + var item = IMAGE_VIEWERS[i]; + if (item[0] === viewerName) { + viewer = item[1]; + break; + } +} + + +function getFullImageWidth(previewImage) { + var full_img_w = previewImage.attr('data-width'); + if (full_img_w == null) { + full_img_w = previewImage[0].naturalWidth; + } + + return full_img_w; +} + +function getFullImageHeight(previewImage) { + var full_img_h = previewImage.attr('data-height'); + if (full_img_h == null) { + full_img_h = previewImage[0].naturalHeight; + } + + return full_img_h; +} + + function ImageViewer() {} ImageViewer.prototype.view = function (post) {}; @@ -48,8 +79,8 @@ SimpleImageViewer.prototype.view = funct if (images.length == 1) { var thumb = images.first(); - var width = thumb.attr('data-width'); - var height = thumb.attr('data-height'); + var width = getFullImageWidth(thumb); + var height = getFullImageHeight(thumb); if (width == null || height == null) { width = '100%'; @@ -76,10 +107,10 @@ PopupImageViewer.prototype.view = functi var existingPopups = $('#' + thumb_id); if (!existingPopups.length) { - var imgElement= el.find('img'); + var imgElement = el.find('img'); - var full_img_w = imgElement.attr('data-width'); - var full_img_h = imgElement.attr('data-height'); + var full_img_w = getFullImageWidth(imgElement); + var full_img_h = getFullImageHeight(imgElement); var win = $(window); @@ -156,16 +187,6 @@ PopupImageViewer.prototype.view = functi }; function addImgPreview() { - var viewerName = $('body').attr('data-image-viewer'); - var viewer = ImageViewer(); - for (var i = 0; i < IMAGE_VIEWERS.length; i++) { - var item = IMAGE_VIEWERS[i]; - if (item[0] === viewerName) { - viewer = item[1]; - break; - } - } - //keybind $(document).on('keyup.removepic', function(e) { if(e.which === 27) { 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 @@ -24,6 +24,7 @@ */ var FAV_POST_UPDATE_PERIOD = 10000; +var ITEM_VOLUME_LEVEL = 'volumeLevel'; /** * An email is a hidden file to prevent spam bots from posting. It has to be @@ -108,6 +109,36 @@ function initFavPanel() { } } +function setVolumeLevel(level) { + localStorage.setItem(ITEM_VOLUME_LEVEL, level); +} + +function getVolumeLevel() { + var level = localStorage.getItem(ITEM_VOLUME_LEVEL); + if (level == null) { + level = 1.0; + } + return level +} + +function processVolumeUser(node) { + node.prop("volume", getVolumeLevel()); + node.on('volumechange', function(event) { + setVolumeLevel(event.target.volume); + $("video,audio").prop("volume", getVolumeLevel()); + }); +} + +/** + * Add all scripts than need to work on post, when the post is added to the + * document. + */ +function addScriptsToPost(post) { + addRefLinkPreview(post[0]); + highlightCode(post); + processVolumeUser(post.find("video,audio")); +} + $( document ).ready(function() { hideEmailFromForm(); @@ -123,4 +154,7 @@ function initFavPanel() { highlightCode($(document)); initFavPanel(); + + var volumeUsers = $("video,audio"); + processVolumeUser(volumeUsers); }); diff --git a/boards/static/js/proof_of_work.js b/boards/static/js/proof_of_work.js new file mode 100644 --- /dev/null +++ b/boards/static/js/proof_of_work.js @@ -0,0 +1,54 @@ +var POW_COMPUTING_TIMEOUT = 2; +var POW_HASH_LENGTH = 16; + + +function computeHash(iteration, guess, target, payload, timestamp, hasher) { + iteration += 1; + var hash = hasher(payload + iteration).toString(); + guess = hash.substring(0, POW_HASH_LENGTH); + + if (guess <= target) { + //console.log("Iteration: ", iteration); + //console.log("Guess: ", guess); + //console.log("Target: ", target); + + var data = { + iteration: iteration, + timestamp: timestamp, + guess: guess + }; + self.postMessage(data); + } else { + //console.log("Iteration: ", iteration); + //console.log("Guess: ", guess); + //console.log("Target: ", target); + + setTimeout(function() { + computeHash(iteration, guess, target, payload, timestamp, hasher); + }, POW_COMPUTING_TIMEOUT); + } +} + +function doWork(message, hasher, difficulty) { + var timestamp = Date.now(); + var iteration = 0; + var payload = timestamp + message; + + var target = parseInt(Math.pow(2, POW_HASH_LENGTH * 3) / difficulty).toString(); + while (target.length < POW_HASH_LENGTH) { + target = '0' + target; + } + + var guess = target + '0'; + + setTimeout(function() { + computeHash(iteration, guess, target, payload, timestamp, hasher); + }, POW_COMPUTING_TIMEOUT); +} + +self.onmessage = function(e) { + var difficulty = e.data.difficulty; + importScripts(e.data.hasher); + var hasher = CryptoJS.SHA256; + self.doWork(e.data.msg, hasher, difficulty); +}; diff --git a/boards/static/js/refpopup.js b/boards/static/js/refpopup.js --- a/boards/static/js/refpopup.js +++ b/boards/static/js/refpopup.js @@ -18,9 +18,8 @@ function $each(list, fn) { function mkPreview(cln, html) { cln.innerHTML = html; - highlightCode($(cln)); - addRefLinkPreview(cln); -}; + addScriptsToPost($(cln)); +} function isElementInViewport (el) { //special bonus for those using jQuery 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 @@ -28,7 +28,11 @@ var CLASS_POST = '.post' var POST_ADDED = 0; var POST_UPDATED = 1; +// TODO These need to be syncronized with board settings. var JS_AUTOUPDATE_PERIOD = 20000; +// TODO This needs to be the same for attachment download time limit. +var POST_AJAX_TIMEOUT = 30000; +var BLINK_SPEED = 500; var ALLOWED_FOR_PARTIAL_UPDATE = [ 'refmap', @@ -45,6 +49,7 @@ var documentOriginalTitle = ''; // Thread ID does not change, can be stored one time var threadId = $('div.thread').children(CLASS_POST).first().attr('id'); +var blinkColor = $('
').css('background-color'); /** * Connect to websocket server and subscribe to thread updates. On any update we @@ -195,12 +200,7 @@ function updatePost(postHtml) { * Initiate a blinking animation on a node to show it was updated. */ function blink(node) { - var blinkCount = 2; - - var nodeToAnimate = node; - for (var i = 0; i < blinkCount; i++) { - nodeToAnimate = nodeToAnimate.fadeTo('fast', 0.5).fadeTo('fast', 1.0); - } + node.effect('highlight', { color: blinkColor }, BLINK_SPEED); } function isPageBottom() { @@ -352,26 +352,12 @@ function updateOnPost(response, statusTe } } -/** - * Show text in the errors row of the form. - * @param form - * @param text - */ -function showAsErrors(form, text) { - form.children('.form-errors').remove(); - - if (text.length > 0) { - var errorList = $('
' + text + '
'); - errorList.appendTo(form); - } -} /** * Run js methods that are usually run on the document, on the new post */ function processNewPost(post) { - addRefLinkPreview(post[0]); - highlightCode(post); + addScriptsToPost(post); blink(post); } @@ -430,7 +416,7 @@ function updateNodeAttr(oldNode, newNode } } -$(document).ready(function(){ +$(document).ready(function() { if (initAutoupdate()) { // Post form data over AJAX var threadId = $('div.thread').children('.post').first().attr('id'); @@ -439,14 +425,15 @@ function updateNodeAttr(oldNode, newNode if (form.length > 0) { var options = { - beforeSubmit: function(arr, $form, options) { - showAsErrors($('#form'), gettext('Sending message...')); + beforeSubmit: function(arr, form, options) { + showAsErrors(form, gettext('Sending message...')); }, success: updateOnPost, error: function() { - showAsErrors($('#form'), gettext('Server error!')); + showAsErrors(form, gettext('Server error!')); }, - url: '/api/add_post/' + threadId + '/' + url: '/api/add_post/' + threadId + '/', + timeout: POST_AJAX_TIMEOUT }; form.ajaxForm(options); 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 @@ -31,8 +31,8 @@ {% for banner in banners %}
{{ banner.title }}
-
{{ banner.text }}
-
{% trans 'Related message' %}: >>{{ banner.post.id }}
+
{{ banner.get_text|safe }}
+
{% trans 'Details' %}: >>{{ banner.post.id }}
{% endfor %} @@ -44,38 +44,50 @@ + height="{{ image.pre_height }}" + alt="{{ random_image_post.id }}"/> {% endwith %}
{% endif %}

+ /{{ tag.get_view|safe }}/ + {% if perms.change_tag %} + | {% trans 'Edit tag' %} + {% endif %} +

+

{% if is_favorite %} - + {% else %} - + {% endif %}
{% if is_hidden %} - + {% else %} - + {% endif %}
- {{ tag.get_view|safe }} - {% if moderator %} - | {% trans 'Edit tag' %} - {% endif %} - + {% trans 'Gallery' %} +

{% if tag.get_description %}

{{ tag.get_description|safe }}

{% endif %}

- {% blocktrans count count=tag.get_active_thread_count %}{{ count }} active thread{% plural %}active threads{% endblocktrans %}, - {% blocktrans count count=tag.get_bumplimit_thread_count %}{{ count }} thread in bumplimit{% plural %} threads in bumplimit{% endblocktrans %}, - {% blocktrans count count=tag.get_archived_thread_count %}{{ count }} archived thread{% plural %}archived threads{% endblocktrans %}, + {% with active_count=tag.get_active_thread_count bumplimit_count=tag.get_bumplimit_thread_count archived_count=tag.get_archived_thread_count %} + {% if active_count %} + {% blocktrans count count=active_count %}{{ count }} active thread{% plural %}active threads{% endblocktrans %}, + {% endif %} + {% if bumplimit_count %} + {% blocktrans count count=bumplimit_count %}{{ count }} thread in bumplimit{% plural %} threads in bumplimit{% endblocktrans %}, + {% endif %} + {% if archived_count %} + {% blocktrans count count=archived_count %}{{ count }} archived thread{% plural %}archived threads{% endblocktrans %}, + {% endif %} + {% endwith %} {% blocktrans count count=tag.get_post_count %}{{ count }} message{% plural %}messages{% endblocktrans %}.

{% if tag.get_all_parents %} @@ -99,7 +111,7 @@ {% for thread in threads %}
- {% post_view thread.get_opening_post moderator=moderator thread=thread truncated=True need_open_link=True %} + {% post_view thread.get_opening_post thread=thread truncated=True need_open_link=True %} {% if not thread.archived %} {% with last_replies=thread.get_last_replies %} {% if last_replies %} @@ -114,7 +126,7 @@ {% endwith %}
{% for post in last_replies %} - {% post_view post moderator=moderator truncated=True %} + {% post_view post truncated=True %} {% endfor %}
{% endif %} @@ -148,6 +160,9 @@
{% trans 'Tags must be delimited by spaces. Text or image is required.' %} + {% with size=max_file_size|filesizeformat %} + {% blocktrans %}Max file size is {{ size }}.{% endblocktrans %} + {% endwith %}
@@ -156,6 +171,8 @@
+ + {% endblock %} @@ -180,7 +197,6 @@ {% endfor %} {% endwith %} ] - [RSS] {% endblock %} diff --git a/boards/templates/boards/authors.html b/boards/templates/boards/authors.html --- a/boards/templates/boards/authors.html +++ b/boards/templates/boards/authors.html @@ -9,6 +9,9 @@ {% block content %}

+

{% trans 'Statistics' %}

+

{% trans 'Size of media:' %} {{ media_size|filesizeformat }}. +

{% blocktrans count count=post_count %}{{ count }} message{% plural %}messages{% endblocktrans %}.

{% trans 'Authors' %}

{% for nick, values in authors.items %}

@@ -16,10 +19,7 @@ {% for value in values.contacts %} {{ value }} {% endfor %} - - {% for role in values.roles %} - {% trans role %} - {% if not forloop.last %}, {% endif %} - {% endfor %} + {{ values.roles|join:', ' }}

{% endfor %}
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 @@ -11,7 +11,9 @@ - + {% if rss_url %} + + {% endif %} @@ -21,11 +23,8 @@ {% block head %}{% endblock %} - + - - -