diff --git a/boards/forms.py b/boards/forms.py --- a/boards/forms.py +++ b/boards/forms.py @@ -26,6 +26,11 @@ ERROR_IMAGE_DUPLICATE = _('Such image wa LABEL_TITLE = _('Title') LABEL_TEXT = _('Text') +LABEL_TAG = _('Tag') + +TAG_MAX_LENGTH = 20 + +REGEX_TAG = ur'^[\w\d]+$' class FormatPanel(forms.Textarea): @@ -311,3 +316,24 @@ class LoginForm(NeboardForm): cleaned_data = super(LoginForm, self).clean() return cleaned_data + + +class AddTagForm(NeboardForm): + + tag = forms.CharField(max_length=TAG_MAX_LENGTH, label=LABEL_TAG) + method = forms.CharField(widget=forms.HiddenInput(), initial='add_tag') + + def clean_tag(self): + tag = self.cleaned_data['tag'] + + regex_tag = re.compile(REGEX_TAG, re.UNICODE) + if not regex_tag.match(tag): + raise forms.ValidationError(_('Inappropriate characters in tags.')) + + return tag + + def clean(self): + cleaned_data = super(AddTagForm, self).clean() + + return cleaned_data + diff --git a/boards/locale/ru/LC_MESSAGES/django.mo b/boards/locale/ru/LC_MESSAGES/django.mo index 428cbe338a232db676d17eec97d82be114dd50b7..40b3a653ae16ba333d138863a755df5192dae032 GIT binary patch literal 6816 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 oDqRb7guLh8Uh\n" "Language-Team: LANGUAGE \n" @@ -58,63 +58,67 @@ msgstr "Заголовок" msgid "Text" msgstr "Текст" -#: forms.py:89 +#: forms.py:29 +msgid "Tag" +msgstr "Тег" + +#: forms.py:106 msgid "Image" msgstr "Изображение" -#: forms.py:92 +#: forms.py:109 msgid "e-mail" msgstr "" -#: forms.py:103 +#: forms.py:120 #, python-format msgid "Title must have less than %s characters" msgstr "Заголовок должен иметь меньше %s символов" -#: forms.py:112 +#: forms.py:129 #, python-format msgid "Text must have less than %s characters" msgstr "Текст должен быть короче %s символов" -#: forms.py:123 +#: forms.py:140 #, python-format msgid "Image must be less than %s bytes" msgstr "Изображение должно быть менее %s байт" -#: forms.py:158 +#: forms.py:175 msgid "Either text or image must be entered." msgstr "Текст или картинка должны быть введены." -#: forms.py:171 +#: forms.py:188 #, python-format msgid "Wait %s seconds after last posting" msgstr "Подождите %s секунд после последнего постинга" -#: forms.py:187 templates/boards/tags.html:6 templates/boards/rss/post.html:10 +#: forms.py:204 templates/boards/tags.html:6 templates/boards/rss/post.html:10 msgid "Tags" msgstr "Теги" -#: forms.py:195 +#: forms.py:212 forms.py:331 msgid "Inappropriate characters in tags." msgstr "Недопустимые символы в тегах." -#: forms.py:223 forms.py:244 +#: forms.py:240 forms.py:261 msgid "Captcha validation failed" msgstr "Проверка капчи провалена" -#: forms.py:250 +#: forms.py:267 msgid "Theme" msgstr "Тема" -#: forms.py:255 +#: forms.py:272 msgid "Enable moderation panel" msgstr "Включить панель модерации" -#: forms.py:270 +#: forms.py:287 msgid "No such user found" msgstr "Данный пользователь не найден" -#: forms.py:284 +#: forms.py:301 #, python-format msgid "Wait %s minutes after last login" msgstr "Подождите %s минут после последнего входа" @@ -127,69 +131,19 @@ msgstr "Не найдено" msgid "This page does not exist" msgstr "Этой страницы не существует" -#: templates/boards/archive.html:9 templates/boards/base.html:51 -msgid "Archive" -msgstr "Архив" - -#: templates/boards/archive.html:39 templates/boards/posting_general.html:64 -msgid "Previous page" -msgstr "Предыдущая страница" - -#: templates/boards/archive.html:68 -msgid "Open" -msgstr "Открыть" - -#: templates/boards/archive.html:74 templates/boards/post.html:37 -#: templates/boards/posting_general.html:103 templates/boards/thread.html:69 -msgid "Delete" -msgstr "Удалить" - -#: templates/boards/archive.html:78 templates/boards/post.html:40 -#: templates/boards/posting_general.html:107 templates/boards/thread.html:72 -msgid "Ban IP" -msgstr "Заблокировать IP" - -#: templates/boards/archive.html:87 templates/boards/post.html:53 -#: templates/boards/posting_general.html:116 -#: templates/boards/posting_general.html:180 templates/boards/thread.html:81 -msgid "Replies" -msgstr "Ответы" - -#: templates/boards/archive.html:96 templates/boards/posting_general.html:125 -#: templates/boards/thread.html:138 templates/boards/thread_gallery.html:58 -msgid "images" -msgstr "изображений" - -#: templates/boards/archive.html:97 templates/boards/thread.html:137 -#: templates/boards/thread_gallery.html:57 -msgid "replies" -msgstr "ответов" - -#: templates/boards/archive.html:116 templates/boards/posting_general.html:203 -msgid "Next page" -msgstr "Следующая страница" - -#: templates/boards/archive.html:121 templates/boards/posting_general.html:208 -msgid "No threads exist. Create the first one!" -msgstr "Нет тем. Создайте первую!" - -#: templates/boards/archive.html:130 templates/boards/posting_general.html:235 -msgid "Pages:" -msgstr "Страницы: " - #: templates/boards/authors.html:6 templates/boards/authors.html.py:12 msgid "Authors" msgstr "Авторы" -#: templates/boards/authors.html:25 +#: templates/boards/authors.html:26 msgid "Distributed under the" msgstr "Распространяется под" -#: templates/boards/authors.html:27 +#: templates/boards/authors.html:28 msgid "license" msgstr "лицензией" -#: templates/boards/authors.html:29 +#: templates/boards/authors.html:30 msgid "Repository" msgstr "Репозиторий" @@ -205,7 +159,7 @@ msgstr "Все темы" msgid "Tag management" msgstr "Управление тегами" -#: templates/boards/base.html:38 +#: templates/boards/base.html:38 templates/boards/settings.html:7 msgid "Settings" msgstr "Настройки" @@ -214,6 +168,10 @@ msgstr "Настройки" msgid "Login" msgstr "Вход" +#: templates/boards/base.html:51 +msgid "Archive" +msgstr "Архив" + #: templates/boards/base.html:53 #, python-format msgid "Speed: %(ppd)s posts per day" @@ -231,32 +189,81 @@ msgstr "ID пользователя" msgid "Insert your user id above" msgstr "Вставьте свой ID пользователя выше" -#: templates/boards/posting_general.html:97 +#: templates/boards/post.html:47 +msgid "Open" +msgstr "Открыть" + +#: templates/boards/post.html:49 msgid "Reply" msgstr "Ответ" -#: templates/boards/posting_general.html:142 +#: templates/boards/post.html:56 +msgid "Edit" +msgstr "Изменить" + +#: templates/boards/post.html:58 +msgid "Delete" +msgstr "Удалить" + +#: templates/boards/post.html:61 +msgid "Ban IP" +msgstr "Заблокировать IP" + +#: templates/boards/post.html:74 +msgid "Replies" +msgstr "Ответы" + +#: templates/boards/post.html:85 templates/boards/thread.html:74 +#: templates/boards/thread_gallery.html:59 +msgid "images" +msgstr "изображений" + +#: templates/boards/post_admin.html:19 +msgid "Tags:" +msgstr "Теги:" + +#: templates/boards/post_admin.html:30 +msgid "Add tag" +msgstr "Добавить тег" + +#: templates/boards/posting_general.html:64 +msgid "Previous page" +msgstr "Предыдущая страница" + +#: templates/boards/posting_general.html:77 #, python-format msgid "Skipped %(count)s replies. Open thread to see all replies." msgstr "Пропущено %(count)s ответов. Откройте тред, чтобы увидеть все ответы." -#: templates/boards/posting_general.html:214 +#: templates/boards/posting_general.html:100 +msgid "Next page" +msgstr "Следующая страница" + +#: templates/boards/posting_general.html:105 +msgid "No threads exist. Create the first one!" +msgstr "Нет тем. Создайте первую!" + +#: templates/boards/posting_general.html:111 msgid "Create new thread" msgstr "Создать новую тему" -#: templates/boards/posting_general.html:218 templates/boards/thread.html:115 +#: templates/boards/posting_general.html:115 templates/boards/thread.html:50 msgid "Post" msgstr "Отправить" -#: templates/boards/posting_general.html:222 +#: templates/boards/posting_general.html:119 msgid "Tags must be delimited by spaces. Text or image is required." msgstr "" "Теги должны быть разделены пробелами. Текст или изображение обязательны." -#: templates/boards/posting_general.html:225 templates/boards/thread.html:119 +#: templates/boards/posting_general.html:122 templates/boards/thread.html:54 msgid "Text syntax" msgstr "Синтаксис текста" +#: templates/boards/posting_general.html:132 +msgid "Pages:" +msgstr "Страницы: " + #: templates/boards/settings.html:14 msgid "User:" msgstr "Пользователь:" @@ -285,23 +292,27 @@ msgstr "Сохранить" msgid "No tags found." msgstr "Теги не найдены." -#: templates/boards/thread.html:19 templates/boards/thread_gallery.html:20 +#: templates/boards/thread.html:20 templates/boards/thread_gallery.html:21 msgid "Normal mode" msgstr "Нормальный режим" -#: templates/boards/thread.html:20 templates/boards/thread_gallery.html:21 +#: templates/boards/thread.html:21 templates/boards/thread_gallery.html:22 msgid "Gallery mode" msgstr "Режим галереи" -#: templates/boards/thread.html:28 +#: templates/boards/thread.html:29 msgid "posts to bumplimit" msgstr "сообщений до бамплимита" -#: templates/boards/thread.html:109 +#: templates/boards/thread.html:44 msgid "Reply to thread" msgstr "Ответить в тему" -#: templates/boards/thread.html:139 templates/boards/thread_gallery.html:59 +#: templates/boards/thread.html:73 templates/boards/thread_gallery.html:58 +msgid "replies" +msgstr "ответов" + +#: templates/boards/thread.html:75 templates/boards/thread_gallery.html:60 msgid "Last update: " msgstr "Последнее обновление: " diff --git a/boards/models/post.py b/boards/models/post.py --- a/boards/models/post.py +++ b/boards/models/post.py @@ -10,7 +10,7 @@ import hashlib from django.core.cache import cache from django.core.paginator import Paginator -from django.db import models +from django.db import models, transaction from django.http import Http404 from django.utils import timezone from markupfield.fields import MarkupField @@ -283,6 +283,30 @@ class Post(models.Model): self.image_hash = md5.hexdigest() super(Post, self).save(*args, **kwargs) + @transaction.atomic + def add_tag(self, tag): + edit_time = timezone.now() + + thread = self.thread_new + thread.add_tag(tag) + self.last_edit_time = edit_time + self.save() + + thread.last_edit_time = edit_time + thread.save() + + @transaction.atomic + def remove_tag(self, tag): + edit_time = timezone.now() + + thread = self.thread_new + thread.tags.remove(tag) + self.last_edit_time = edit_time + self.save() + + thread.last_edit_time = edit_time + thread.save() + class Thread(models.Model): diff --git a/boards/templates/boards/post.html b/boards/templates/boards/post.html --- a/boards/templates/boards/post.html +++ b/boards/templates/boards/post.html @@ -52,6 +52,8 @@ {% if moderator %} + [{% trans 'Edit' %}] [{% trans 'Delete' %}] ({{ post.poster_ip }}) diff --git a/boards/templates/boards/post_admin.html b/boards/templates/boards/post_admin.html new file mode 100644 --- /dev/null +++ b/boards/templates/boards/post_admin.html @@ -0,0 +1,38 @@ +{% extends "boards/base.html" %} + +{% load i18n %} +{% load cache %} +{% load static from staticfiles %} +{% load board %} + +{% block head %} +#{{ post.id }} - {{ site_name }} +{% endblock %} + +{% block content %} + {% spaceless %} + + {% post_view post moderator=moderator %} + + {% if post.is_opening %} +
+ {% trans 'Tags:' %} + {% for tag in post.thread_new.get_tags %} + #{{ tag.name }} + [X] + {% if not forloop.last %},{% endif %} + {% endfor %} +
+
{% csrf_token %} + {{ tag_form.as_div }} +
+ +
+
+
+
+ {% endif %} + + {% endspaceless %} +{% endblock %} diff --git a/boards/urls.py b/boards/urls.py --- a/boards/urls.py +++ b/boards/urls.py @@ -7,6 +7,7 @@ from boards.views.authors import Authors from boards.views.delete_post import DeletePostView from boards.views.ban import BanUserView from boards.views.static import StaticPageView +from boards.views.post_admin import PostAdminView js_info_dict = { 'packages': ('boards',), @@ -40,17 +41,21 @@ urlpatterns = patterns('', url(r'^thread/(?P\w+)/(?P\w+)/$', views.thread.ThreadView .as_view(), name='thread_mode'), + # /boards/post_admin/ + url(r'^post_admin/(?P\w+)/$', PostAdminView.as_view(), + name='post_admin'), + url(r'^settings/$', settings.SettingsView.as_view(), name='settings'), url(r'^tags/$', all_tags.AllTagsView.as_view(), name='tags'), url(r'^captcha/', include('captcha.urls')), url(r'^authors/$', AuthorsView.as_view(), name='authors'), url(r'^delete/(?P\w+)/$', DeletePostView.as_view(), - name='delete'), + name='delete'), url(r'^ban/(?P\w+)/$', BanUserView.as_view(), name='ban'), url(r'^banned/$', views.banned.BannedView.as_view(), name='banned'), url(r'^staticpage/(?P\w+)/$', StaticPageView.as_view(), - name='staticpage'), + name='staticpage'), # RSS feeds url(r'^rss/$', AllThreadsFeed()), @@ -60,7 +65,8 @@ urlpatterns = patterns('', url(r'^thread/(?P\w+)/rss/$', ThreadPostsFeed()), # i18n - url(r'^jsi18n/$', 'boards.views.cached_js_catalog', js_info_dict, name='js_info_dict'), + url(r'^jsi18n/$', 'boards.views.cached_js_catalog', js_info_dict, + name='js_info_dict'), # API url(r'^api/post/(?P\w+)/$', api.get_post, name="get_post"), @@ -71,6 +77,7 @@ urlpatterns = patterns('', url(r'^api/tags/$', api.api_get_tags, name='get_tags'), url(r'^api/thread/(?P\w+)/$', api.api_get_thread_posts, name='get_thread'), - url(r'^api/add_post/(?P\w+)/$', api.api_add_post, name='add_post'), + url(r'^api/add_post/(?P\w+)/$', api.api_add_post, + name='add_post'), ) diff --git a/boards/views/mixins.py b/boards/views/mixins.py --- a/boards/views/mixins.py +++ b/boards/views/mixins.py @@ -26,8 +26,14 @@ class DispatcherMixin: 'method' request parameter. """ - def dispatch_method(self, request): + def dispatch_method(self, *args, **kwargs): + request = args[0] + + method_name = None if PARAMETER_METHOD in request.GET: method_name = request.GET[PARAMETER_METHOD] - return getattr(self, method_name)(request) + elif PARAMETER_METHOD in request.POST: + method_name = request.POST[PARAMETER_METHOD] + if method_name: + return getattr(self, method_name)(*args, **kwargs) diff --git a/boards/views/post_admin.py b/boards/views/post_admin.py new file mode 100644 --- /dev/null +++ b/boards/views/post_admin.py @@ -0,0 +1,57 @@ +from django.shortcuts import render, get_object_or_404, redirect + +from boards.views.base import BaseBoardView +from boards.views.mixins import DispatcherMixin +from boards.models.post import Post +from boards.models.tag import Tag +from boards.forms import AddTagForm, PlainErrorList + +class PostAdminView(BaseBoardView, DispatcherMixin): + + def get(self, request, post_id, form=None): + user = self._get_user(request) + if not user.is_moderator: + redirect('index') + + post = get_object_or_404(Post, id=post_id) + + if not form: + dispatch_result = self.dispatch_method(request, post) + if dispatch_result: + return dispatch_result + form = AddTagForm() + + context = self.get_context_data(request=request) + + context['post'] = post + + context['tag_form'] = form + + return render(request, 'boards/post_admin.html', context) + + def post(self, request, post_id): + user = self._get_user(request) + if not user.is_moderator: + redirect('index') + + post = get_object_or_404(Post, id=post_id) + return self.dispatch_method(request, post) + + def delete_tag(self, request, post): + tag_name = request.GET['tag'] + tag = get_object_or_404(Tag, name=tag_name) + + post.remove_tag(tag) + + return redirect('post_admin', post.id) + + def add_tag(self, request, post): + form = AddTagForm(request.POST, error_class=PlainErrorList) + if form.is_valid(): + tag_name = form.cleaned_data['tag'] + tag, created = Tag.objects.get_or_create(name=tag_name) + + post.add_tag(tag) + return redirect('post_admin', post.id) + else: + return self.get(request, post.id, form)