diff --git a/boards/abstracts/settingsmanager.py b/boards/abstracts/settingsmanager.py new file mode 100644 --- /dev/null +++ b/boards/abstracts/settingsmanager.py @@ -0,0 +1,143 @@ +from django.shortcuts import get_object_or_404 +from boards.models import Tag + +__author__ = 'neko259' + +SESSION_SETTING = 'setting' + +PERMISSION_MODERATE = 'moderator' + +SETTING_THEME = 'theme' +SETTING_FAVORITE_TAGS = 'favorite_tags' +SETTING_HIDDEN_TAGS = 'hidden_tags' +SETTING_PERMISSIONS = 'permissions' + +DEFAULT_THEME = 'md' + + +def get_settings_manager(request): + """ + Get settings manager based on the request object. Currently only + session-based manager is supported. In the future, cookie-based or + database-based managers could be implemented. + """ + return SessionSettingsManager(request.session) + + +class SettingsManager: + """ + Base settings manager class. get_setting and set_setting methods should + be overriden. + """ + def __init__(self): + pass + + def get_theme(self): + theme = self.get_setting(SETTING_THEME) + if not theme: + theme = DEFAULT_THEME + self.set_setting(SETTING_THEME, theme) + + return theme + + def set_theme(self, theme): + self.set_setting(SETTING_THEME, theme) + + def has_permission(self, permission): + permissions = self.get_setting(SETTING_PERMISSIONS) + if permissions: + return permission in permissions + else: + return False + + def get_setting(self, setting): + pass + + def set_setting(self, setting, value): + pass + + def add_permission(self, permission): + permissions = self.get_setting(SETTING_PERMISSIONS) + if not permissions: + permissions = [permission] + else: + permissions.append(permission) + self.set_setting(SETTING_PERMISSIONS, permissions) + + def del_permission(self, permission): + permissions = self.get_setting(SETTING_PERMISSIONS) + if not permissions: + permissions = [] + else: + permissions.remove(permission) + self.set_setting(SETTING_PERMISSIONS, permissions) + + def get_fav_tags(self): + tag_names = self.get_setting(SETTING_FAVORITE_TAGS) + tags = [] + if tag_names: + for tag_name in tag_names: + tag = get_object_or_404(Tag, name=tag_name) + tags.append(tag) + + return tags + + def add_fav_tag(self, tag): + tags = self.get_setting(SETTING_FAVORITE_TAGS) + if not tags: + tags = [tag.name] + else: + if not tag.name in tags: + tags.append(tag.name) + self.set_setting(SETTING_FAVORITE_TAGS, tags) + + def del_fav_tag(self, tag): + tags = self.get_setting(SETTING_FAVORITE_TAGS) + if tag.name in tags: + tags.remove(tag.name) + self.set_setting(SETTING_FAVORITE_TAGS, tags) + + def get_hidden_tags(self): + tag_names = self.get_setting(SETTING_HIDDEN_TAGS) + tags = [] + if tag_names: + for tag_name in tag_names: + tag = get_object_or_404(Tag, name=tag_name) + tags.append(tag) + + return tags + + def add_hidden_tag(self, tag): + tags = self.get_setting(SETTING_HIDDEN_TAGS) + if not tags: + tags = [tag.name] + else: + if not tag.name in tags: + tags.append(tag.name) + self.set_setting(SETTING_HIDDEN_TAGS, tags) + + def del_hidden_tag(self, tag): + tags = self.get_setting(SETTING_HIDDEN_TAGS) + if tag.name in tags: + tags.remove(tag.name) + self.set_setting(SETTING_HIDDEN_TAGS, tags) + + +class SessionSettingsManager(SettingsManager): + """ + Session-based settings manager. All settings are saved to the user's + session. + """ + def __init__(self, session): + SettingsManager.__init__(self) + self.session = session + + def get_setting(self, setting): + if setting in self.session: + return self.session[setting] + else: + return None + + def set_setting(self, setting, value): + self.session[setting] = value + diff --git a/boards/admin.py b/boards/admin.py --- a/boards/admin.py +++ b/boards/admin.py @@ -1,5 +1,5 @@ from django.contrib import admin -from boards.models import Post, Tag, User, Ban, Thread +from boards.models import Post, Tag, Ban, Thread class PostAdmin(admin.ModelAdmin): @@ -11,15 +11,7 @@ class PostAdmin(admin.ModelAdmin): class TagAdmin(admin.ModelAdmin): - list_display = ('name', 'linked') - list_filter = ('linked',) - - -class UserAdmin(admin.ModelAdmin): - - list_display = ('user_id', 'rank') - search_fields = ('user_id',) - + list_display = ('name',) class ThreadAdmin(admin.ModelAdmin): @@ -35,6 +27,5 @@ class ThreadAdmin(admin.ModelAdmin): admin.site.register(Post, PostAdmin) admin.site.register(Tag, TagAdmin) -admin.site.register(User, UserAdmin) admin.site.register(Ban) admin.site.register(Thread, ThreadAdmin) diff --git a/boards/context_processors.py b/boards/context_processors.py --- a/boards/context_processors.py +++ b/boards/context_processors.py @@ -1,8 +1,10 @@ +from boards.abstracts.settingsmanager import PERMISSION_MODERATE, \ + get_settings_manager + __author__ = 'neko259' -from boards import utils, settings +from boards import settings from boards.models import Post -from boards.models.post import SETTING_MODERATE CONTEXT_SITE_NAME = 'site_name' CONTEXT_VERSION = 'version' @@ -17,21 +19,17 @@ CONTEXT_USER = 'user' def user_and_ui_processor(request): context = {} - user = utils.get_user(request) - context[CONTEXT_USER] = user - context[CONTEXT_TAGS] = user.fav_tags.all() context[CONTEXT_PPD] = float(Post.objects.get_posts_per_day()) - theme = utils.get_theme(request, user) + settings_manager = get_settings_manager(request) + context[CONTEXT_TAGS] = settings_manager.get_fav_tags() + theme = settings_manager.get_theme() context[CONTEXT_THEME] = theme context[CONTEXT_THEME_CSS] = 'css/' + theme + '/base_page.css' # This shows the moderator panel - moderate = user.get_setting(SETTING_MODERATE) - if moderate == 'True': - context[CONTEXT_MODERATOR] = user.is_moderator() - else: - context[CONTEXT_MODERATOR] = False + moderate = settings_manager.has_permission(PERMISSION_MODERATE) + context[CONTEXT_MODERATOR] = moderate context[CONTEXT_VERSION] = settings.VERSION context[CONTEXT_SITE_NAME] = settings.SITE_NAME diff --git a/boards/forms.py b/boards/forms.py --- a/boards/forms.py +++ b/boards/forms.py @@ -9,7 +9,7 @@ from django.utils.translation import uge from boards.mdx_neboard import formatters from boards.models.post import TITLE_MAX_LENGTH -from boards.models import User, PostImage +from boards.models import PostImage from neboard import settings from boards import utils import boards.settings as board_settings @@ -20,8 +20,7 @@ ATTRIBUTE_PLACEHOLDER = 'placeholder' LAST_POST_TIME = 'last_post_time' LAST_LOGIN_TIME = 'last_login_time' -TEXT_PLACEHOLDER = _('''Type message here. You can reply to message >>123 like - this. 2 new lines are required to start new paragraph.''') +TEXT_PLACEHOLDER = _('''Type message here. Use formatting panel for more advanced usage.''') TAGS_PLACEHOLDER = _('tag1 several_words_tag') ERROR_IMAGE_DUPLICATE = _('Such image was already posted') @@ -187,11 +186,7 @@ class PostForm(NeboardForm): if not 'user_id' in self.session: return - user = User.objects.get(id=self.session['user_id']) - if user.is_veteran(): - posting_delay = VETERAN_POSTING_DELAY - else: - posting_delay = settings.POSTING_DELAY + posting_delay = settings.POSTING_DELAY if board_settings.LIMIT_POSTING_SPEED and LAST_POST_TIME in \ self.session: @@ -282,57 +277,6 @@ class SettingsForm(NeboardForm): label=_('Theme')) -class ModeratorSettingsForm(SettingsForm): - - moderate = forms.BooleanField(required=False, label=_('Enable moderation ' - 'panel')) - - -class LoginForm(NeboardForm): - - user_id = forms.CharField() - - session = None - - def clean_user_id(self): - user_id = self.cleaned_data['user_id'] - if user_id: - users = User.objects.filter(user_id=user_id) - if len(users) == 0: - raise forms.ValidationError(_('No such user found')) - - return user_id - - def _validate_login_speed(self): - can_post = True - - if LAST_LOGIN_TIME in self.session: - now = time.time() - last_login_time = self.session[LAST_LOGIN_TIME] - - current_delay = int(now - last_login_time) - - if current_delay < board_settings.LOGIN_TIMEOUT: - error_message = _('Wait %s minutes after last login') % str( - (board_settings.LOGIN_TIMEOUT - current_delay) / 60) - self._errors['user_id'] = self.error_class([error_message]) - - can_post = False - - if can_post: - self.session[LAST_LOGIN_TIME] = time.time() - - def clean(self): - if not self.session: - raise forms.ValidationError('Humans have sessions') - - self._validate_login_speed() - - cleaned_data = super(LoginForm, self).clean() - - return cleaned_data - - class AddTagForm(NeboardForm): tag = forms.CharField(max_length=TAG_MAX_LENGTH, label=LABEL_TAG) @@ -354,4 +298,44 @@ class AddTagForm(NeboardForm): class SearchForm(NeboardForm): - query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False) \ No newline at end of file + query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False) + + +class LoginForm(NeboardForm): + + password = forms.CharField() + + session = None + + def clean_password(self): + password = self.cleaned_data['password'] + if board_settings.MASTER_PASSWORD != password: + raise forms.ValidationError(_('Invalid master password')) + + return password + + def _validate_login_speed(self): + can_post = True + + if LAST_LOGIN_TIME in self.session: + now = time.time() + last_login_time = self.session[LAST_LOGIN_TIME] + + current_delay = int(now - last_login_time) + + if current_delay < board_settings.LOGIN_TIMEOUT: + error_message = _('Wait %s minutes after last login') % str( + (board_settings.LOGIN_TIMEOUT - current_delay) / 60) + self._errors['password'] = self.error_class([error_message]) + + can_post = False + + if can_post: + self.session[LAST_LOGIN_TIME] = time.time() + + def clean(self): + self._validate_login_speed() + + cleaned_data = super(LoginForm, self).clean() + + return cleaned_data diff --git a/boards/locale/ru/LC_MESSAGES/django.mo b/boards/locale/ru/LC_MESSAGES/django.mo index b4d12e2f1e9cc3c1f276de818a34ae547f18bb65..1f70c1ff6647291211289c5624ab67fe2bfbb633 GIT binary patch literal 6397 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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 @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2014-06-29 13:46+0300\n" +"POT-Creation-Date: 2014-07-20 20:11+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,56 +18,53 @@ 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" -#: authors.py:5 +#: authors.py:9 msgid "author" msgstr "автор" -#: authors.py:6 +#: authors.py:10 msgid "developer" msgstr "разработчик" -#: authors.py:7 +#: authors.py:11 msgid "javascript developer" msgstr "разработчик javascript" -#: authors.py:8 +#: authors.py:12 msgid "designer" msgstr "дизайнер" #: forms.py:23 -msgid "" -"Type message here. You can reply to message >>123 like\n" -" this. 2 new lines are required to start new paragraph." +msgid "Type message here. Use formatting panel for more advanced usage." msgstr "" -"Введите сообщение здесь. Вы можете ответить на сообщение >>123 вот так. 2 " -"переноса строки обязательны для создания нового абзаца." +"Вводите сообщение сюда. Используйте панель для более сложного форматирования." -#: forms.py:25 +#: forms.py:24 msgid "tag1 several_words_tag" msgstr "тег1 тег_из_нескольких_слов" -#: forms.py:27 +#: forms.py:26 msgid "Such image was already posted" msgstr "Такое изображение уже было загружено" -#: forms.py:29 +#: forms.py:28 msgid "Title" msgstr "Заголовок" -#: forms.py:30 +#: forms.py:29 msgid "Text" msgstr "Текст" -#: forms.py:31 +#: forms.py:30 msgid "Tag" msgstr "Тег" -#: forms.py:32 templates/boards/base.html:50 templates/search/search.html:9 +#: forms.py:31 templates/boards/base.html:54 templates/search/search.html:9 #: templates/search/search.html.py:13 msgid "Search" msgstr "Поиск" -#: forms.py:109 +#: forms.py:108 msgid "Image" msgstr "Изображение" @@ -94,36 +91,32 @@ msgstr "Изображение должно быть менее %s байт" msgid "Either text or image must be entered." msgstr "Текст или картинка должны быть введены." -#: forms.py:202 +#: forms.py:199 #, python-format msgid "Wait %s seconds after last posting" msgstr "Подождите %s секунд после последнего постинга" -#: forms.py:218 templates/boards/tags.html:7 templates/boards/rss/post.html:10 +#: forms.py:215 templates/boards/tags.html:7 templates/boards/rss/post.html:10 msgid "Tags" msgstr "Теги" -#: forms.py:225 forms.py:344 +#: forms.py:222 forms.py:290 msgid "Inappropriate characters in tags." msgstr "Недопустимые символы в тегах." -#: forms.py:253 forms.py:274 +#: forms.py:250 forms.py:271 msgid "Captcha validation failed" msgstr "Проверка капчи провалена" -#: forms.py:280 +#: forms.py:277 msgid "Theme" msgstr "Тема" -#: forms.py:285 -msgid "Enable moderation panel" -msgstr "Включить панель модерации" +#: forms.py:313 +msgid "Invalid master password" +msgstr "Неверный мастер-пароль" -#: forms.py:300 -msgid "No such user found" -msgstr "Данный пользователь не найден" - -#: forms.py:314 +#: forms.py:327 #, python-format msgid "Wait %s minutes after last login" msgstr "Подождите %s минут после последнего входа" @@ -168,17 +161,21 @@ msgstr "Управление тегами" msgid "Settings" msgstr "Настройки" -#: templates/boards/base.html:49 templates/boards/login.html:6 +#: templates/boards/base.html:50 +msgid "Logout" +msgstr "Выход" + +#: templates/boards/base.html:52 templates/boards/login.html:6 #: templates/boards/login.html.py:16 msgid "Login" msgstr "Вход" -#: templates/boards/base.html:52 +#: templates/boards/base.html:56 #, python-format msgid "Speed: %(ppd)s posts per day" msgstr "Скорость: %(ppd)s сообщений в день" -#: templates/boards/base.html:54 +#: templates/boards/base.html:58 msgid "Up" msgstr "Вверх" @@ -186,7 +183,7 @@ msgstr "Вверх" msgid "Insert your user id above" msgstr "Вставьте свой ID пользователя выше" -#: templates/boards/post.html:21 templates/boards/staticpages/help.html:19 +#: templates/boards/post.html:21 templates/boards/staticpages/help.html:17 msgid "Quote" msgstr "Цитата" @@ -216,8 +213,8 @@ msgstr "Ответы" #: templates/boards/post.html:86 templates/boards/thread.html:88 #: templates/boards/thread_gallery.html:61 -msgid "replies" -msgstr "ответов" +msgid "messages" +msgstr "сообщений" #: templates/boards/post.html:87 templates/boards/thread.html:89 #: templates/boards/thread_gallery.html:62 @@ -249,7 +246,7 @@ msgstr "Предыдущая страница" msgid "Skipped %(count)s replies. Open thread to see all replies." msgstr "Пропущено %(count)s ответов. Откройте тред, чтобы увидеть все ответы." -#: templates/boards/posting_general.html:121 templates/search/search.html:35 +#: templates/boards/posting_general.html:121 templates/search/search.html:33 msgid "Next page" msgstr "Следующая страница" @@ -278,35 +275,19 @@ msgstr "Синтаксис текста" msgid "Pages:" msgstr "Страницы: " -#: templates/boards/settings.html:14 -msgid "User:" -msgstr "Пользователь:" - -#: templates/boards/settings.html:16 +#: templates/boards/settings.html:15 msgid "You are moderator." msgstr "Вы модератор." #: templates/boards/settings.html:19 -msgid "You are veteran." -msgstr "Вы ветеран." - -#: templates/boards/settings.html:22 -msgid "Posts:" -msgstr "Сообщений:" - -#: templates/boards/settings.html:23 -msgid "First access:" -msgstr "Первый доступ:" - -#: templates/boards/settings.html:25 -msgid "Last access:" -msgstr "Последний доступ: " - -#: templates/boards/settings.html:29 msgid "Hidden tags:" msgstr "Скрытые теги:" -#: templates/boards/settings.html:44 +#: templates/boards/settings.html:26 +msgid "No hidden tags." +msgstr "Нет скрытых тегов." + +#: templates/boards/settings.html:35 msgid "Save" msgstr "Сохранить" @@ -356,37 +337,25 @@ msgid "Syntax" msgstr "Синтаксис" #: templates/boards/staticpages/help.html:11 -msgid "2 line breaks for a new line." -msgstr "2 перевода строки создают новый абзац." - -#: templates/boards/staticpages/help.html:12 msgid "Italic text" msgstr "Курсивный текст" -#: templates/boards/staticpages/help.html:13 +#: templates/boards/staticpages/help.html:12 msgid "Bold text" msgstr "Полужирный текст" -#: templates/boards/staticpages/help.html:14 +#: templates/boards/staticpages/help.html:13 msgid "Spoiler" msgstr "Спойлер" -#: templates/boards/staticpages/help.html:15 +#: templates/boards/staticpages/help.html:14 msgid "Link to a post" msgstr "Ссылка на сообщение" -#: templates/boards/staticpages/help.html:16 +#: templates/boards/staticpages/help.html:15 msgid "Strikethrough text" msgstr "Зачеркнутый текст" -#: templates/boards/staticpages/help.html:17 -msgid "You need to new line before:" -msgstr "Перед этими тегами нужна новая строка:" - -#: templates/boards/staticpages/help.html:18 +#: templates/boards/staticpages/help.html:16 msgid "Comment" msgstr "Комментарий" - -#: templates/search/search.html:30 -msgid "No results found." -msgstr "Результаты не найдены." diff --git a/boards/management/commands/cleanusers.py b/boards/management/commands/cleanusers.py deleted file mode 100644 --- a/boards/management/commands/cleanusers.py +++ /dev/null @@ -1,29 +0,0 @@ -from datetime import datetime, timedelta -from django.core.management import BaseCommand -from django.db import transaction -from django.db.models import Count -from boards.models import User, Post - -__author__ = 'neko259' - -OLD_USER_AGE_DAYS = 90 - - -class Command(BaseCommand): - help = 'Removes empty users (that don\'t have posts or tags' - - @transaction.atomic - def handle(self, *args, **options): - old_registration_date = datetime.now().date() - timedelta( - OLD_USER_AGE_DAYS) - - old_users = User.objects.annotate(tags_count=Count('fav_tags')).filter( - tags_count=0).filter(registration_time__lt=old_registration_date) - deleted_users = 0 - for user in old_users: - if not Post.objects.filter(user=user).exists(): - self.stdout.write('Deleting user %s' % user.user_id) - user.delete() - deleted_users += 1 - - self.stdout.write('Deleted %d users' % deleted_users) \ No newline at end of file diff --git a/boards/management/commands/enforce_privacy.py b/boards/management/commands/enforce_privacy.py --- a/boards/management/commands/enforce_privacy.py +++ b/boards/management/commands/enforce_privacy.py @@ -13,4 +13,4 @@ class Command(BaseCommand): @transaction.atomic def handle(self, *args, **options): - Post.objects.all().update(poster_ip=NO_IP, user=None) \ No newline at end of file + Post.objects.all().update(poster_ip=NO_IP) \ No newline at end of file diff --git a/boards/mdx_neboard.py b/boards/mdx_neboard.py --- a/boards/mdx_neboard.py +++ b/boards/mdx_neboard.py @@ -1,8 +1,7 @@ # coding=utf-8 -import markdown -from markdown.inlinepatterns import Pattern -from markdown.util import etree +import re +import bbcode import boards @@ -10,13 +9,7 @@ import boards __author__ = 'neko259' -AUTOLINK_PATTERN = r'(https?://\S+)' -QUOTE_PATTERN = r'^(?)(>[^>].*)$' -REFLINK_PATTERN = r'((>>)(\d+))' -SPOILER_PATTERN = r'%%([^(%%)]+)%%' -COMMENT_PATTERN = r'^(//(.+))' -STRIKETHROUGH_PATTERN = r'~(.+)~' -DASH_PATTERN = r'--' +REFLINK_PATTERN = re.compile(r'\d+') class TextFormatter(): @@ -38,7 +31,7 @@ class TextFormatter(): format_right = '' -class AutolinkPattern(Pattern): +class AutolinkPattern(): def handleMatch(self, m): link_element = etree.Element('a') href = m.group(2) @@ -48,44 +41,22 @@ class AutolinkPattern(Pattern): return link_element -class QuotePattern(Pattern, TextFormatter): - name = '' - preview_left = '> ' +class QuotePattern(TextFormatter): + name = 'q' + preview_left = '' preview_right = '' - format_left = '>' - - def handleMatch(self, m): - quote_element = etree.Element('span') - quote_element.set('class', 'quote') - quote_element.text = m.group(2) - - return quote_element + format_left = '[quote]' + format_right = '[/quote]' -class ReflinkPattern(Pattern): - def handleMatch(self, m): - post_id = m.group(4) - - posts = boards.models.Post.objects.filter(id=post_id) - if posts.count() > 0: - ref_element = etree.Element('a') - - post = posts[0] - - ref_element.set('href', post.get_url()) - ref_element.text = m.group(2) - - return ref_element - - -class SpoilerPattern(Pattern, TextFormatter): - name = 's' +class SpoilerPattern(TextFormatter): + name = 'spoiler' preview_left = '' preview_right = '' - format_left = '%%' - format_right = '%%' + format_left = '[spoiler]' + format_right = '[/spoiler]' def handleMatch(self, m): quote_element = etree.Element('span') @@ -95,35 +66,22 @@ class SpoilerPattern(Pattern, TextFormat return quote_element -class CommentPattern(Pattern, TextFormatter): +class CommentPattern(TextFormatter): name = '' preview_left = '// ' preview_right = '' - format_left = '//' - - def handleMatch(self, m): - quote_element = etree.Element('span') - quote_element.set('class', 'comment') - quote_element.text = '//' + m.group(3) - - return quote_element + format_left = '[comment]' + format_right = '[/comment]' -class StrikeThroughPattern(Pattern, TextFormatter): +class StrikeThroughPattern(TextFormatter): name = 's' preview_left = '' preview_right = '' - format_left = '~' - format_right = '~' - - def handleMatch(self, m): - quote_element = etree.Element('span') - quote_element.set('class', 'strikethrough') - quote_element.text = m.group(2) - - return quote_element + format_left = '[s]' + format_right = '[/s]' class ItalicPattern(TextFormatter): @@ -131,8 +89,8 @@ class ItalicPattern(TextFormatter): preview_left = '' preview_right = '' - format_left = '_' - format_right = '_' + format_left = '[i]' + format_right = '[/i]' class BoldPattern(TextFormatter): @@ -140,8 +98,8 @@ class BoldPattern(TextFormatter): preview_left = '' preview_right = '' - format_left = '__' - format_right = '__' + format_left = '[b]' + format_right = '[/b]' class CodePattern(TextFormatter): @@ -149,52 +107,39 @@ class CodePattern(TextFormatter): preview_left = '' preview_right = '' - format_left = ' ' - - -class DashPattern(Pattern): - def handleMatch(self, m): - return u'—' + format_left = '[code]' + format_right = '[/code]' -class NeboardMarkdown(markdown.Extension): - def extendMarkdown(self, md, md_globals): - self._add_neboard_patterns(md) - self._delete_patterns(md) +def render_reflink(tag_name, value, options, parent, context): + if not REFLINK_PATTERN.match(value): + return u'>>%s' % value - def _delete_patterns(self, md): - del md.parser.blockprocessors['quote'] - - del md.inlinePatterns['image_link'] - del md.inlinePatterns['image_reference'] + post_id = int(value) - def _add_neboard_patterns(self, md): - autolink = AutolinkPattern(AUTOLINK_PATTERN, md) - quote = QuotePattern(QUOTE_PATTERN, md) - reflink = ReflinkPattern(REFLINK_PATTERN, md) - spoiler = SpoilerPattern(SPOILER_PATTERN, md) - comment = CommentPattern(COMMENT_PATTERN, md) - strikethrough = StrikeThroughPattern(STRIKETHROUGH_PATTERN, md) - dash = DashPattern(DASH_PATTERN, md) + posts = boards.models.Post.objects.filter(id=post_id) + if posts.exists(): + post = posts[0] - md.inlinePatterns[u'autolink_ext'] = autolink - md.inlinePatterns[u'spoiler'] = spoiler - md.inlinePatterns[u'strikethrough'] = strikethrough - md.inlinePatterns[u'comment'] = comment - md.inlinePatterns[u'reflink'] = reflink - md.inlinePatterns[u'quote'] = quote - md.inlinePatterns[u'dash'] = dash + return u'>>%s' % (post.get_url(), post_id) + else: + return u'>>%s' % value -def make_extension(configs=None): - return NeboardMarkdown(configs=configs) - -neboard_extension = make_extension() - - -def markdown_extended(markup): - return markdown.markdown(markup, [neboard_extension, 'nl2br'], - safe_mode='escape') +def bbcode_extended(markup): + parser = bbcode.Parser(newline='

') + parser.add_formatter('post', render_reflink, strip=True) + parser.add_simple_formatter('quote', + u'%(value)s') + parser.add_simple_formatter('comment', + u'//%(value)s') + parser.add_simple_formatter('spoiler', + u'%(value)s') + parser.add_simple_formatter('s', + u'%(value)s') + parser.add_simple_formatter('code', + u'

%(value)s
') + return '

%s

' % parser.format(markup) formatters = [ QuotePattern, diff --git a/boards/migrations/0029_auto__del_setting__del_user__del_field_post_user.py b/boards/migrations/0029_auto__del_setting__del_user__del_field_post_user.py new file mode 100644 --- /dev/null +++ b/boards/migrations/0029_auto__del_setting__del_user__del_field_post_user.py @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Deleting model 'Setting' + db.delete_table(u'boards_setting') + + # Deleting model 'User' + db.delete_table(u'boards_user') + + # Removing M2M table for field hidden_threads on 'User' + db.delete_table(db.shorten_name(u'boards_user_hidden_threads')) + + # Removing M2M table for field fav_threads on 'User' + db.delete_table(db.shorten_name(u'boards_user_fav_threads')) + + # Removing M2M table for field fav_tags on 'User' + db.delete_table(db.shorten_name(u'boards_user_fav_tags')) + + # Removing M2M table for field hidden_tags on 'User' + db.delete_table(db.shorten_name(u'boards_user_hidden_tags')) + + # Deleting field 'Post.user' + db.delete_column(u'boards_post', 'user_id') + + + def backwards(self, orm): + # Adding model 'Setting' + db.create_table(u'boards_setting', ( + ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['boards.User'])), + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('value', self.gf('django.db.models.fields.CharField')(max_length=50)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=50)), + )) + db.send_create_signal('boards', ['Setting']) + + # Adding model 'User' + db.create_table(u'boards_user', ( + ('registration_time', self.gf('django.db.models.fields.DateTimeField')()), + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('user_id', self.gf('django.db.models.fields.CharField')(max_length=50)), + ('rank', self.gf('django.db.models.fields.IntegerField')()), + )) + db.send_create_signal('boards', ['User']) + + # Adding M2M table for field hidden_threads on 'User' + m2m_table_name = db.shorten_name(u'boards_user_hidden_threads') + db.create_table(m2m_table_name, ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('user', models.ForeignKey(orm['boards.user'], null=False)), + ('post', models.ForeignKey(orm['boards.post'], null=False)) + )) + db.create_unique(m2m_table_name, ['user_id', 'post_id']) + + # Adding M2M table for field fav_threads on 'User' + m2m_table_name = db.shorten_name(u'boards_user_fav_threads') + db.create_table(m2m_table_name, ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('user', models.ForeignKey(orm['boards.user'], null=False)), + ('post', models.ForeignKey(orm['boards.post'], null=False)) + )) + db.create_unique(m2m_table_name, ['user_id', 'post_id']) + + # Adding M2M table for field fav_tags on 'User' + m2m_table_name = db.shorten_name(u'boards_user_fav_tags') + db.create_table(m2m_table_name, ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('user', models.ForeignKey(orm['boards.user'], null=False)), + ('tag', models.ForeignKey(orm['boards.tag'], null=False)) + )) + db.create_unique(m2m_table_name, ['user_id', 'tag_id']) + + # Adding M2M table for field hidden_tags on 'User' + m2m_table_name = db.shorten_name(u'boards_user_hidden_tags') + db.create_table(m2m_table_name, ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('user', models.ForeignKey(orm['boards.user'], null=False)), + ('tag', models.ForeignKey(orm['boards.tag'], null=False)) + )) + db.create_unique(m2m_table_name, ['user_id', 'tag_id']) + + # Adding field 'Post.user' + db.add_column(u'boards_post', 'user', + self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['boards.User'], null=True), + keep_default=False) + + + models = { + 'boards.ban': { + 'Meta': {'object_name': 'Ban'}, + 'can_read': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}), + 'reason': ('django.db.models.fields.CharField', [], {'default': "'Auto'", 'max_length': '200'}) + }, + 'boards.post': { + 'Meta': {'ordering': "('id',)", 'object_name': 'Post'}, + '_text_rendered': ('django.db.models.fields.TextField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'images': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'ip+'", 'to': "orm['boards.PostImage']", 'blank': 'True', 'symmetrical': 'False', 'null': 'True', 'db_index': 'True'}), + 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}), + 'poster_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}), + 'poster_user_agent': ('django.db.models.fields.TextField', [], {}), + 'pub_time': ('django.db.models.fields.DateTimeField', [], {}), + 'referenced_posts': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'rfp+'", 'to': "orm['boards.Post']", 'blank': 'True', 'symmetrical': 'False', 'null': 'True', 'db_index': 'True'}), + 'refmap': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'text': ('markupfield.fields.MarkupField', [], {'rendered_field': 'True'}), + 'text_markup_type': ('django.db.models.fields.CharField', [], {'default': "'markdown'", 'max_length': '30'}), + 'thread_new': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.Thread']", 'null': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '200'}) + }, + 'boards.postimage': { + 'Meta': {'ordering': "('id',)", 'object_name': 'PostImage'}, + 'hash': ('django.db.models.fields.CharField', [], {'max_length': '36'}), + 'height': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'image': ('boards.thumbs.ImageWithThumbsField', [], {'max_length': '100', 'blank': 'True'}), + 'pre_height': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'pre_width': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'width': ('django.db.models.fields.IntegerField', [], {'default': '0'}) + }, + 'boards.tag': { + 'Meta': {'ordering': "('name',)", 'object_name': 'Tag'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'linked': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['boards.Tag']", 'null': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}), + 'threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'tag+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Thread']"}) + }, + 'boards.thread': { + 'Meta': {'object_name': 'Thread'}, + 'archived': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'bump_time': ('django.db.models.fields.DateTimeField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}), + 'replies': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'tre+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['boards.Tag']", 'symmetrical': 'False'}) + } + } + + complete_apps = ['boards'] \ No newline at end of file diff --git a/boards/migrations/0030_auto__del_field_tag_linked.py b/boards/migrations/0030_auto__del_field_tag_linked.py new file mode 100644 --- /dev/null +++ b/boards/migrations/0030_auto__del_field_tag_linked.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Deleting field 'Tag.linked' + db.delete_column(u'boards_tag', 'linked_id') + + + def backwards(self, orm): + # Adding field 'Tag.linked' + db.add_column(u'boards_tag', 'linked', + self.gf('django.db.models.fields.related.ForeignKey')(to=orm['boards.Tag'], null=True, blank=True), + keep_default=False) + + + models = { + 'boards.ban': { + 'Meta': {'object_name': 'Ban'}, + 'can_read': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}), + 'reason': ('django.db.models.fields.CharField', [], {'default': "'Auto'", 'max_length': '200'}) + }, + 'boards.post': { + 'Meta': {'ordering': "('id',)", 'object_name': 'Post'}, + '_text_rendered': ('django.db.models.fields.TextField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'images': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'ip+'", 'to': "orm['boards.PostImage']", 'blank': 'True', 'symmetrical': 'False', 'null': 'True', 'db_index': 'True'}), + 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}), + 'poster_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}), + 'poster_user_agent': ('django.db.models.fields.TextField', [], {}), + 'pub_time': ('django.db.models.fields.DateTimeField', [], {}), + 'referenced_posts': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'rfp+'", 'to': "orm['boards.Post']", 'blank': 'True', 'symmetrical': 'False', 'null': 'True', 'db_index': 'True'}), + 'refmap': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'text': ('markupfield.fields.MarkupField', [], {'rendered_field': 'True'}), + 'text_markup_type': ('django.db.models.fields.CharField', [], {'default': "'bbcode'", 'max_length': '30'}), + 'thread_new': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.Thread']", 'null': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '200'}) + }, + 'boards.postimage': { + 'Meta': {'ordering': "('id',)", 'object_name': 'PostImage'}, + 'hash': ('django.db.models.fields.CharField', [], {'max_length': '36'}), + 'height': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'image': ('boards.thumbs.ImageWithThumbsField', [], {'max_length': '100', 'blank': 'True'}), + 'pre_height': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'pre_width': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'width': ('django.db.models.fields.IntegerField', [], {'default': '0'}) + }, + 'boards.tag': { + 'Meta': {'ordering': "('name',)", 'object_name': 'Tag'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}), + 'threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'tag+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Thread']"}) + }, + 'boards.thread': { + 'Meta': {'object_name': 'Thread'}, + 'archived': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'bump_time': ('django.db.models.fields.DateTimeField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}), + 'replies': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'tre+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['boards.Tag']", 'symmetrical': 'False'}) + } + } + + complete_apps = ['boards'] \ No newline at end of file diff --git a/boards/models/__init__.py b/boards/models/__init__.py --- a/boards/models/__init__.py +++ b/boards/models/__init__.py @@ -5,5 +5,3 @@ from boards.models.thread import Thread from boards.models.post import Post from boards.models.tag import Tag from boards.models.user import Ban -from boards.models.user import Setting -from boards.models.user import User diff --git a/boards/models/post.py b/boards/models/post.py --- a/boards/models/post.py +++ b/boards/models/post.py @@ -20,7 +20,7 @@ APP_LABEL_BOARDS = 'boards' CACHE_KEY_PPD = 'ppd' CACHE_KEY_POST_URL = 'post_url' -POSTS_PER_DAY_RANGE = range(7) +POSTS_PER_DAY_RANGE = 7 BAN_REASON_AUTO = 'Auto' @@ -28,7 +28,7 @@ IMAGE_THUMB_SIZE = (200, 150) TITLE_MAX_LENGTH = 200 -DEFAULT_MARKUP_TYPE = 'markdown' +DEFAULT_MARKUP_TYPE = 'bbcode' # TODO This should be removed NO_IP = '0.0.0.0' @@ -36,17 +36,14 @@ NO_IP = '0.0.0.0' # TODO Real user agent should be saved instead of this UNKNOWN_UA = '' -SETTING_MODERATE = "moderate" - -REGEX_REPLY = re.compile('>>(\d+)') +REGEX_REPLY = re.compile(r'>>(\d+)') logger = logging.getLogger(__name__) class PostManager(models.Manager): - - def create_post(self, title, text, image=None, thread=None, - ip=NO_IP, tags=None, user=None): + def create_post(self, title, text, image=None, thread=None, ip=NO_IP, + tags=None): """ Creates new post """ @@ -69,8 +66,7 @@ class PostManager(models.Manager): poster_ip=ip, poster_user_agent=UNKNOWN_UA, # TODO Get UA at # last! - last_edit_time=posting_time, - user=user) + last_edit_time=posting_time) if image: post_image = PostImage.objects.create(image=image) @@ -80,20 +76,14 @@ class PostManager(models.Manager): thread.replies.add(post) if tags: - linked_tags = [] - for tag in tags: - tag_linked_tags = tag.get_linked_tags() - if len(tag_linked_tags) > 0: - linked_tags.extend(tag_linked_tags) - - tags.extend(linked_tags) map(thread.add_tag, tags) if new_thread: Thread.objects.process_oldest_threads() self.connect_replies(post) - logger.info('Created post #%d' % post.id) + logger.info('Created post #%d with title %s' % (post.id, + post.get_title())) return post @@ -114,7 +104,7 @@ class PostManager(models.Manager): post.delete() - logger.info('Deleted post #%d' % post_id) + logger.info('Deleted post #%d (%s)' % (post_id, post.get_title())) def delete_posts_by_ip(self, ip): """ @@ -129,7 +119,7 @@ class PostManager(models.Manager): Connects replies to a post to show them as a reflink map """ - for reply_number in re.finditer(REGEX_REPLY, post.text.raw): + for reply_number in re.finditer(REGEX_REPLY, post.text.rendered): post_id = reply_number.group(1) ref_post = self.filter(id=post_id) if ref_post.count() > 0: @@ -148,28 +138,26 @@ class PostManager(models.Manager): Gets average count of posts per day for the last 7 days """ - today = date.today() - ppd = cache.get(CACHE_KEY_PPD + str(today)) + day_end = date.today() + day_start = day_end - timedelta(POSTS_PER_DAY_RANGE) + + cache_key = CACHE_KEY_PPD + str(day_end) + ppd = cache.get(cache_key) if ppd: return ppd - posts_per_days = [] - for i in POSTS_PER_DAY_RANGE: - day_end = today - timedelta(i + 1) - day_start = today - timedelta(i + 2) + day_time_start = timezone.make_aware(datetime.combine( + day_start, dtime()), timezone.get_current_timezone()) + day_time_end = timezone.make_aware(datetime.combine( + day_end, dtime()), timezone.get_current_timezone()) - day_time_start = timezone.make_aware(datetime.combine( - day_start, dtime()), timezone.get_current_timezone()) - day_time_end = timezone.make_aware(datetime.combine( - day_end, dtime()), timezone.get_current_timezone()) + posts_per_period = float(self.filter( + pub_time__lte=day_time_end, + pub_time__gte=day_time_start).count()) - posts_per_days.append(float(self.filter( - pub_time__lte=day_time_end, - pub_time__gte=day_time_start).count())) + ppd = posts_per_period / POSTS_PER_DAY_RANGE - ppd = (sum(posts_per_day for posts_per_day in posts_per_days) / - len(posts_per_days)) - cache.set(CACHE_KEY_PPD + str(today), ppd) + cache.set(cache_key, ppd) return ppd @@ -196,7 +184,6 @@ class Post(models.Model, Viewable): thread_new = models.ForeignKey('Thread', null=True, default=None, db_index=True) last_edit_time = models.DateTimeField() - user = models.ForeignKey('User', null=True, default=None, db_index=True) referenced_posts = models.ManyToManyField('Post', symmetrical=False, null=True, @@ -220,13 +207,18 @@ class Post(models.Model, Viewable): return title def build_refmap(self): + """ + Builds a replies map string from replies list. This is a cache to stop + the server from recalculating the map on every post show. + """ map_string = '' first = True for refpost in self.referenced_posts.all(): if not first: map_string += ', ' - map_string += '>>%s' % (refpost.get_url(), refpost.id) + map_string += '>>%s' % (refpost.get_url(), + refpost.id) first = False self.refmap = map_string @@ -251,10 +243,10 @@ class Post(models.Model, Viewable): thread = self.get_thread() thread.add_tag(tag) self.last_edit_time = edit_time - self.save() + self.save(update_fields=['last_edit_time']) thread.last_edit_time = edit_time - thread.save() + thread.save(update_fields=['last_edit_time']) @transaction.atomic def remove_tag(self, tag): @@ -263,10 +255,10 @@ class Post(models.Model, Viewable): thread = self.get_thread() thread.remove_tag(tag) self.last_edit_time = edit_time - self.save() + self.save(update_fields=['last_edit_time']) thread.last_edit_time = edit_time - thread.save() + thread.save(update_fields=['last_edit_time']) def get_url(self, thread=None): """ @@ -343,9 +335,9 @@ class Post(models.Model, Viewable): def delete(self, using=None): """ - Delete all post images and the post itself. + Deletes all post images and the post itself. """ self.images.all().delete() - super(Post, self).delete(using) \ No newline at end of file + super(Post, self).delete(using) diff --git a/boards/models/tag.py b/boards/models/tag.py --- a/boards/models/tag.py +++ b/boards/models/tag.py @@ -1,10 +1,12 @@ from django.template.loader import render_to_string -from boards.models import Thread, Post from django.db import models from django.db.models import Count, Sum from django.core.urlresolvers import reverse + +from boards.models import Thread from boards.models.base import Viewable + __author__ = 'neko259' @@ -36,7 +38,6 @@ class Tag(models.Model, Viewable): name = models.CharField(max_length=100, db_index=True) threads = models.ManyToManyField(Thread, null=True, blank=True, related_name='tag+') - linked = models.ForeignKey('Tag', null=True, blank=True) def __unicode__(self): return self.name @@ -51,29 +52,6 @@ class Tag(models.Model, Viewable): def get_thread_count(self): return self.threads.count() - def get_linked_tags(self): - """ - Gets tags linked to the current one. - """ - - tag_list = [] - self.get_linked_tags_list(tag_list) - - return tag_list - - def get_linked_tags_list(self, tag_list=[]): - """ - Returns the list of tags linked to current. The list can be got - through returned value or tag_list parameter - """ - - linked_tag = self.linked - - if linked_tag and not (linked_tag in tag_list): - tag_list.append(linked_tag) - - linked_tag.get_linked_tags_list(tag_list) - def get_post_count(self, archived=False): """ Gets posts count for the tag's threads. diff --git a/boards/models/user.py b/boards/models/user.py --- a/boards/models/user.py +++ b/boards/models/user.py @@ -1,122 +1,10 @@ from django.db import models -from django.db.models import Count -from boards import settings -from boards.models import Post -from django.core.cache import cache __author__ = 'neko259' -RANK_ADMIN = 0 -RANK_MODERATOR = 10 -RANK_USER = 100 - BAN_REASON_AUTO = 'Auto' BAN_REASON_MAX_LENGTH = 200 -VETERAN_POSTS = 1000 - - -class User(models.Model): - - class Meta: - app_label = 'boards' - - user_id = models.CharField(max_length=50) - rank = models.IntegerField() - - registration_time = models.DateTimeField() - - fav_tags = models.ManyToManyField('Tag', null=True, blank=True) - fav_threads = models.ManyToManyField(Post, related_name='+', null=True, - blank=True) - - hidden_tags = models.ManyToManyField('Tag', null=True, blank=True, - related_name='ht+') - hidden_threads = models.ManyToManyField('Post', null=True, blank=True, - related_name='hth+') - - def save_setting(self, name, value): - setting, created = Setting.objects.get_or_create(name=name, user=self) - setting.value = str(value) - setting.save() - - return setting - - def get_setting(self, name): - if Setting.objects.filter(name=name, user=self).exists(): - setting = Setting.objects.get(name=name, user=self) - setting_value = setting.value - else: - setting_value = None - - return setting_value - - def is_moderator(self): - return RANK_MODERATOR >= self.rank - - def get_sorted_fav_tags(self): - cache_key = self._get_tag_cache_key() - fav_tags = cache.get(cache_key) - if fav_tags: - return fav_tags - - tags = self.fav_tags.annotate(Count('threads')) \ - .filter(threads__count__gt=0).order_by('name') - - if tags: - cache.set(cache_key, tags) - - return tags - - def get_post_count(self): - return Post.objects.filter(user=self).count() - - def __unicode__(self): - return self.user_id + '(' + str(self.rank) + ')' - - def get_last_access_time(self): - """ - Gets user's last post time. - """ - - posts = Post.objects.filter(user=self) - if posts.exists() > 0: - return posts.latest('pub_time').pub_time - - def add_tag(self, tag): - self.fav_tags.add(tag) - cache.delete(self._get_tag_cache_key()) - - def remove_tag(self, tag): - self.fav_tags.remove(tag) - cache.delete(self._get_tag_cache_key()) - - def hide_tag(self, tag): - self.hidden_tags.add(tag) - - def unhide_tag(self, tag): - self.hidden_tags.remove(tag) - - def is_veteran(self): - """ - Returns if a user is old (veteran). - """ - - return self.get_post_count() >= VETERAN_POSTS - - def _get_tag_cache_key(self): - return self.user_id + '_tags' - - -class Setting(models.Model): - - class Meta: - app_label = 'boards' - - name = models.CharField(max_length=50) - value = models.CharField(max_length=50) - user = models.ForeignKey(User) - class Ban(models.Model): diff --git a/boards/settings.py b/boards/settings.py --- a/boards/settings.py +++ b/boards/settings.py @@ -17,4 +17,7 @@ LAST_REPLIES_COUNT = 3 # Enable archiving threads instead of deletion when the thread limit is reached ARCHIVE_THREADS = True # Limit posting speed -LIMIT_POSTING_SPEED = False \ No newline at end of file +LIMIT_POSTING_SPEED = False + +# This password is used to add admin permissions to the user +MASTER_PASSWORD = u'password' \ 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 @@ -217,6 +217,16 @@ blockquote { font-style: italic; } +.multiquote { + border-left: solid 4px #ccc; + padding: 3px; + display: inline-block; + background: #222; + border-right: solid 1px #ccc; + border-top: solid 1px #ccc; + border-bottom: solid 1px #ccc; +} + .spoiler { background: white; color: white; diff --git a/boards/static/js/thread.js b/boards/static/js/thread.js --- a/boards/static/js/thread.js +++ b/boards/static/js/thread.js @@ -35,10 +35,10 @@ function moveCaretToEnd(el) { } function addQuickReply(postId) { - var textToAdd = '>>' + postId + '\n\n'; + var textToAdd = '[post]' + postId + '[/post]\n'; var selection = window.getSelection().toString(); if (selection.length > 0) { - textToAdd += '> ' + selection + '\n\n'; + textToAdd += '[quote]' + selection + '[/quote]\n'; } var textAreaId = 'textarea'; 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 @@ -46,7 +46,11 @@