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 zc$|$_Yit}>6~2AY#%q(8lQa&LUWX<$w7cDPS`s#KnoYb(-TGDADQXLf#=8@HlJ(4X zX4Z*CDv?8=hXj-X40)*p7V#lxkK|&QOvjPeD0TLhq5)wj)mp~xCbI%>` z+SveZcfOf>&pprYoSFP$?cHw*{B`5^N&IH-6yinT7e6M{pS)Ly`+@a9{4busr5bop zKX++-0hfD#8I4DQRlpa4b-*ucxi@iH1H7!|z6)Frybin@_ygb;U=es9@Nas3)e0d# z4qOMU0d5A8i;n^&@Nr;0@Yxk?@5BoB^P-l&qR)Q|xDq(6&wqaf`}q-YE%2v$|M$Sp z0RN%&Y_4LtSQY!Tvx@z1)Xz=82(VeNcUQ5!7pmBfRmJf;`uR9;74T(!?p3Yl5^x3O z2bcuDU&VF#r9S`fD)#f92-l@9!uECm?*i_Lu>boatnV4%Zpur9?S56Se;v4$@)F_r zzNhz#z>UD4=;uF1I1hgX+Q3gi_)hY-n*Djbn&mE4a~-Y#Hv<={+0S29v;Tjo=6KfD zu)nP}9A|e8=k-f^y%)F+XxFg*;TqO6qMxq-H&9-HH|`K(4w%MsVI}irW+mtKM=Lq6 zzh24p|HDeo*FRTse030d0N4PG0XtW*-czgC|4XYlk29+{{ttnxfqw!%3A_srabU}8 zw(~M@6Y$(>=HGj(S>GIRKk&!E&jB}r{Cj}yYuKOH)-Z3s4ctaN1U?A-`x?&A{RsZk zz^%X?z^B&oxglT__|966<0D`##krRKzO$C|wnpQFwXAwGZ(g1K1a3}C9>)7sx>o}gD0h=h_>sWs8daiHRdftD2J;(PF z@C@+udgejR2DZNem;%;s;P^5dSbi9|9e92N>-_-uG}RmUTi^rt@%ejEER>jm(4J;c@_Y2ZWB&ejT45uj6{p*Kzzm)cBh^_WxgXZ2tii{So5PCXW9w zFhTqUf}Y|D!GGj4<^L(-j41s*COGdje`zkZ3bs$2YomU{?-~3)FSzda>2tdT$M=YS zepoP%sTS+>TD#yLr?~gx*MVQN;5?J>#7pAPCM{2VXvFWJSo)VLyC?2$*7r0=8U*)) z6kK2GJ(>r^^}Dp(gBmH{G&_hF3B5+MX}!LuI;JyqV zEA$@KV~4)4*Z7de&*Jx}U@lNU&nb98mhNwkU7gXWshqb$2@t&ait+`^SD}# ziBvW#eWO>TayjYuK_=^ol$+_Z227DE_z?>Gb{EvIUF%h_+r12_cMKl z958ZL*6=OImPZUL2buj&f4^x%0t$UonPcUm$;_F)DVi+Lcded+Z)Rn|&YCXvO_9!8 zzDQfRb(IAPXRUst*OdJQ&zC)>gi_NrvoVpjjh-CV9h_y;dBZky;(!VB2P_xjMkZr= zUQ!%1ayio-RI;L()bVvuX%1qrk0`fzvS-jYJ<)6%`Mm4oU5orjkgkz|Wlvf*rOS&! z!ZTf84mt%_7H})AtTcKYq^;RUJ~Eu57Ng7&^-#zoRc2C(7Rx>+eFxRZJ02vRUdtA3 z6m_0lY;&Yn$n>%9BMy>8OJtj$V(O7ACe0JbXH4pJBY*OUN`YgW^^kM>jU0#H=J;A! zd)~A~2Q0y{4sw7loNaYW%dRGfjztE$)l1JW86x$$UPGbCfJx zk(8_fD|rWmb43@}bj*Ynr_kHSsqdofF?)_19(0n0L3JJ!=^C_sMTao0+6dc+eq@Y}Tc; z^r8AaCP>z!1{m7JL83Fh>JuXi*Jv1#VqvI>C$gqz_1Y%g4463wWD}~)o;Yd@7+%J; z^1dwXq1#PN1HsgUOFf1DyrPWIEEee71Y#UO0J+1*9XIP8rm5TEI!DcnA8pP?pGCPa z#*(rlwg2hV!L)of-PzUL-WH*ra{Xwl*K1{?`wG2YwA)F_NJo2jRP{R!MVr(}X^1Bt zi^dbth6b6~o{aB!INlJCLonKjKK4p-&!tnH(bo28Q%XMBo@{M?`1$s>bcCi;wA(do zFGsmZ$^(a5T4Y&27B)4^0QhFj$m&kVe-u8KD28I6p$fjC9@y*KgsO-< zsq6*QOdO`)II(W|mg3Z9;;$xsOqSY$szmY&nm6#Qo}|&+D9@-et}}QtZ&IigZ}yM(?^##HV` z*@rRJ%OhnEWw&9^jg?IZ>YN3Yu_+#Vi?gM8deN4O=Q0fsJ%Xoc;)Qc;K{=>WRrdJ_ z1)*8g7j@A|q|?m%ArA2oSv;$R&~FkfMFQ0jU{}?kF6mMQzY6Q~i$m;u>C^QVf=*Mg znkrN{73wt42Q|Mjh+0Jg}5smkS~AwQ{R1-%F)y?KIhP-R#P&6CwB z9$};v**v8QP2)M_%uGq6o)w_z)FSiCTA<>bRa$PSKxeow!WYRkwWPG62Di@f%3-Oc z3+gqh+fw(Mb|YE#N?7uC2)%=%vH=>F>nxAZ@&QTYsF0$vDiF2F!Yti(Ris~2|p7~F&>G9AOy{s!NE%Th*n=Bjt7}e))=lezdikk|j`LY=)xiIkQJSuRJ ze5Lo?b=4Z>+z#8b@~GASdLYX@?T{<_2gKrc(wrLX%f6E;7m}i#Q3GXhR;p2?KO*oO zCoV63Nd==Z9VdM!iU0DV3cr1B8S6>jnNX33$)eZktSbF5k!K-%HI?1!x0Lc{jNdi< zEl@!h%?fa47$+wFx3&eIALr9d)GXWzdVJ5* zOKdd!h!vC{rG1(r#{!QEb-hMZqE9huiAsZD`Zm(C^sh9bzAn$G12>4Lbg04@o={&@ 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/mdx_neboard.py b/boards/mdx_neboard.py --- a/boards/mdx_neboard.py +++ b/boards/mdx_neboard.py @@ -66,11 +66,11 @@ class ReflinkPattern(Pattern): post = posts[0] if not post.is_opening(): - link = reverse(boards.views.thread, kwargs={ + link = reverse('thread', kwargs={ 'post_id': post.thread_new.get_opening_post().id})\ + '#' + post_id else: - link = reverse(boards.views.thread, kwargs={'post_id': post_id}) + link = reverse('thread', kwargs={'post_id': post_id}) ref_element.set('href', link) ref_element.text = m.group(2) diff --git a/boards/middlewares.py b/boards/middlewares.py --- a/boards/middlewares.py +++ b/boards/middlewares.py @@ -17,14 +17,14 @@ class BanMiddleware: def process_view(self, request, view_func, view_args, view_kwargs): - if view_func != views.you_are_banned: + if view_func != views.banned.BannedView.as_view: ip = utils.get_client_ip(request) bans = Ban.objects.filter(ip=ip) if bans.exists(): ban = bans[0] if not ban.can_read: - return redirect(views.you_are_banned) + return redirect('banned') class MinifyHTMLMiddleware(object): 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 @@ -94,13 +94,11 @@ class PostManager(models.Manager): """ Delete post and update or delete its thread """ - + thread = post.thread_new - if thread.get_opening_post() == self: - thread.replies.delete() - - thread.delete() + if post.is_opening(): + thread.delete_with_posts() else: thread.last_edit_time = timezone.now() thread.save() @@ -115,7 +113,8 @@ class PostManager(models.Manager): posts = self.filter(poster_ip=ip) map(self.delete_post, posts) - # TODO Move this method to thread manager + # TODO This method may not be needed any more, because django's paginator + # is used def get_threads(self, tag=None, page=ALL_PAGES, order_by='-bump_time', archived=False): if tag: @@ -197,7 +196,7 @@ class PostManager(models.Manager): ppd = (sum(posts_per_day for posts_per_day in posts_per_days) / len(posts_per_days)) - cache.set(CACHE_KEY_PPD, ppd) + cache.set(CACHE_KEY_PPD + str(today), ppd) return ppd @@ -284,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): @@ -356,6 +379,10 @@ class Thread(models.Model): return last_replies + def get_skipped_replies_count(self): + last_replies = self.get_last_replies() + return self.get_reply_count() - len(last_replies) - 1 + def get_replies(self): """ Get sorted thread posts @@ -379,7 +406,7 @@ class Thread(models.Model): return self.get_replies()[0] def __unicode__(self): - return str(self.get_replies()[0].id) + return str(self.id) def get_pub_time(self): """ 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 @@ -13,7 +13,6 @@ .img-full { position: fixed; - z-index: 9999; background-color: #CCC; border: 1px solid #000; cursor: pointer; 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 @@ -85,7 +85,10 @@ function addImgPreview() { return false; } - ).draggable() + ).draggable({ + addClasses: false, + stack: '.img-full' + }) } else { $('#'+thumb_id).remove(); diff --git a/boards/templates/boards/archive.html b/boards/templates/boards/archive.html deleted file mode 100644 --- a/boards/templates/boards/archive.html +++ /dev/null @@ -1,144 +0,0 @@ -{% extends "boards/base.html" %} - -{% load i18n %} -{% load cache %} -{% load board %} -{% load static %} - -{% block head %} -Neboard - {% trans 'Archive' %} - - {% if current_page.has_previous %} - - {% endif %} - {% if current_page.has_next %} - - {% endif %} - -{% endblock %} - -{% block content %} - - {% get_current_language as LANGUAGE_CODE %} - - {% if threads %} - {% if current_page.has_previous %} - - {% endif %} - - {% for thread in threads %} - {% cache 600 thread_short thread.thread.id thread.thread.last_edit_time moderator LANGUAGE_CODE %} -
-
- {% if thread.op.image %} -
- {{ thread.op.id }} - -
- {% endif %} -
- - {% autoescape off %} - {{ thread.op.text.rendered|truncatewords_html:50 }} - {% endautoescape %} - {% if thread.op.is_referenced %} -
- {% trans "Replies" %}: - {% for ref_post in thread.op.get_sorted_referenced_posts %} - >>{{ ref_post.id }}{% if not forloop.last %},{% endif %} - {% endfor %} -
- {% endif %} -
- -
-
- {% endcache %} - {% endfor %} - - {% if current_page.has_next %} - - {% endif %} - {% else %} -
- {% trans 'No threads exist. Create the first one!' %}
- {% endif %} - -{% endblock %} - -{% block metapanel %} - - - Neboard 1.6 Amon - {% trans "Pages:" %}[ - {% for page in paginator.page_range %} - {{ page }} - {% if not forloop.last %},{% endif %} - {% endfor %} - ] - - -{% endblock %} diff --git a/boards/templates/boards/login.html b/boards/templates/boards/login.html --- a/boards/templates/boards/login.html +++ b/boards/templates/boards/login.html @@ -3,7 +3,7 @@ {% load i18n %} {% block head %} - {% trans 'Login' %} + {% trans 'Login' %} - {{ site_name }} {% endblock %} {% block content %} 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 @@ -4,85 +4,102 @@ {% get_current_language as LANGUAGE_CODE %} -{% cache 300 post post.id post.thread_new.last_edit_time truncated moderator LANGUAGE_CODE need_open_link %} -{% spaceless %} -{% if post.thread_new.archived %} -
-{% elif post.thread_new.can_bump %} -
-{% else %} -
-{% endif %} +{% cache 600 post post.id thread.last_edit_time truncated moderator LANGUAGE_CODE need_open_link %} + + {% with is_opening=post.is_opening %} + {% spaceless %} + {% with thread=post.thread_new %} + {% if thread.archived %} +
+ {% elif thread.can_bump %} +
+ {% else %} +
+ {% endif %} -{% if post.image %} -
- {{ post.id }} - -
-{% endif %} -
- + {% if is_opening and thread.tags.exists %} + {% endif %} - {% endautoescape %} - {% if post.is_referenced %} -
- {% trans "Replies" %}: - {% for ref_post in post.get_sorted_referenced_posts %} - >>{{ ref_post.id }}{% if not forloop.last %},{% endif %} - {% endfor %}
- {% endif %} -
-{% if post.is_opening and post.thread_new.tags.exists %} - -{% endif %} -
-{% endspaceless %} + {% endwith %} + {% endspaceless %} + {% endwith %} {% endcache %} 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/templates/boards/posting_general.html b/boards/templates/boards/posting_general.html --- a/boards/templates/boards/posting_general.html +++ b/boards/templates/boards/posting_general.html @@ -7,9 +7,9 @@ {% block head %} {% if tag %} - Neboard - {{ tag.name }} + {{ tag.name }} - {{ site_name }} {% else %} - Neboard + {{ site_name }} {% endif %} {% if current_page.has_previous %} @@ -41,10 +41,10 @@

{% if tag in user.fav_tags.all %} - {% else %} - {% endif %} #{{ tag.name }} @@ -66,23 +66,25 @@ {% endif %} {% for thread in threads %} - {% cache 600 thread_short thread.thread.id thread.thread.last_edit_time moderator LANGUAGE_CODE %} + {% cache 600 thread_short thread.id thread.last_edit_time moderator LANGUAGE_CODE %}
- {% post_view_truncated thread.op True moderator %} - {% if thread.last_replies.exists %} - {% if thread.skipped_replies %} + {% post_view_truncated thread.get_opening_post True moderator %} + {% if not thread.archived %} + {% if thread.get_last_replies.exists %} + {% if thread.get_skipped_replies_count %} {% endif %}
- {% for post in thread.last_replies %} + {% for post in thread.get_last_replies %} {% post_view_truncated post moderator=moderator%} {% endfor %}
{% endif %} + {% endif %}
{% endcache %} {% endfor %} @@ -126,7 +128,7 @@ {% block metapanel %} - Neboard 1.6 Amon + {{ site_name }} {{ version }} {% trans "Pages:" %}[ {% for page in paginator.page_range %} Neboard settings + {% trans 'Settings' %} - {{ site_name }} {% endblock %} {% block content %} diff --git a/boards/templates/boards/tags.html b/boards/templates/boards/tags.html --- a/boards/templates/boards/tags.html +++ b/boards/templates/boards/tags.html @@ -13,10 +13,10 @@ {% for tag in all_tags %}
{% if tag in user.fav_tags.all %} - {% else %} - {% endif %} diff --git a/boards/templates/boards/thread.html b/boards/templates/boards/thread.html --- a/boards/templates/boards/thread.html +++ b/boards/templates/boards/thread.html @@ -6,38 +6,42 @@ {% load board %} {% block head %} - {{ thread.get_opening_post.get_title|striptags|truncatewords:10 }} - Neboard + {{ opening_post.get_title|striptags|truncatewords:10 }} + - {{ site_name }} {% endblock %} {% block content %} {% spaceless %} {% get_current_language as LANGUAGE_CODE %} - + {% cache 600 thread_view thread.id thread.last_edit_time moderator LANGUAGE_CODE %} + + - {% if bumpable %} -
-
-
-
- {{ posts_left }} {% trans 'posts to bumplimit' %} -
+ {% if bumpable %} +
+
+
+
+ {{ posts_left }} {% trans 'posts to bumplimit' %}
- {% endif %} +
+ {% endif %} +
- {% for post in posts %} - {% post_view post moderator=moderator %} - {% endfor %} + {% for post in thread.get_replies %} + {% post_view post moderator=moderator %} + {% endfor %}
{% if not thread.archived %}
-
{% trans "Reply to thread" %} #{{ thread.get_opening_post.id }}
+
{% trans "Reply to thread" %} #{{ opening_post.id }}
{% csrf_token %} @@ -57,6 +61,8 @@ + {% endcache %} + {% endspaceless %} {% endblock %} diff --git a/boards/templates/boards/thread_gallery.html b/boards/templates/boards/thread_gallery.html --- a/boards/templates/boards/thread_gallery.html +++ b/boards/templates/boards/thread_gallery.html @@ -6,7 +6,8 @@ {% load board %} {% block head %} - {{ thread.get_opening_post.get_title|striptags|truncatewords:10 }} - Neboard + {{ thread.get_opening_post.get_title|striptags|truncatewords:10 }} + - {{ site_name }} {% endblock %} {% block content %} diff --git a/boards/templatetags/board.py b/boards/templatetags/board.py --- a/boards/templatetags/board.py +++ b/boards/templatetags/board.py @@ -25,11 +25,11 @@ def post_url(*args, **kwargs): post = get_object_or_404(Post, id=post_id) if not post.is_opening(): - link = reverse(thread, kwargs={ + link = reverse('thread', kwargs={ 'post_id': post.thread_new.get_opening_post().id}) + '#' + str( post_id) else: - link = reverse(thread, kwargs={'post_id': post_id}) + link = reverse('thread', kwargs={'post_id': post_id}) return link @@ -73,4 +73,4 @@ def post_view_truncated(post, need_open_ 'truncated': True, 'need_open_link': need_open_link, 'moderator': moderator, - } \ No newline at end of file + } diff --git a/boards/tests.py b/boards/tests.py --- a/boards/tests.py +++ b/boards/tests.py @@ -1,9 +1,13 @@ # coding=utf-8 +import time +import logging + from django.test import TestCase from django.test.client import Client -import time +from django.core.urlresolvers import reverse, NoReverseMatch from boards.models import Post, Tag +from boards import urls from neboard import settings PAGE_404 = 'boards/404.html' @@ -18,6 +22,8 @@ HTTP_CODE_REDIRECT = 302 HTTP_CODE_OK = 200 HTTP_CODE_NOT_FOUND = 404 +logger = logging.getLogger(__name__) + class PostTests(TestCase): @@ -223,11 +229,29 @@ class FormTest(TestCase): class ViewTest(TestCase): - def test_index(self): + + def test_all_views(self): + ''' + Try opening all views defined in ulrs.py that don't need additional + parameters + ''' + client = Client() + for url in urls.urlpatterns: + try: + view_name = url.name + logger.debug('Testing view %s' % view_name) - response = client.get('/') - self.assertEqual(HTTP_CODE_OK, response.status_code, 'Index page not ' - 'opened') - self.assertEqual('boards/posting_general.html', response.templates[0] - .name, 'Index page should open posting_general template') + try: + response = client.get(reverse(view_name)) + + self.assertEqual(HTTP_CODE_OK, response.status_code, + '%s view not opened' % view_name) + except NoReverseMatch: + # This view just needs additional arguments + pass + except Exception, e: + self.fail('Got exception %s at %s view' % (e, view_name)) + except AttributeError: + # This is normal, some views do not have names + pass diff --git a/boards/urls.py b/boards/urls.py --- a/boards/urls.py +++ b/boards/urls.py @@ -1,7 +1,13 @@ from django.conf.urls import patterns, url, include from boards import views from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed -from boards.views import api +from boards.views import api, tag_threads, all_threads, archived_threads, \ + login, settings, all_tags +from boards.views.authors import AuthorsView +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',), @@ -10,41 +16,46 @@ js_info_dict = { urlpatterns = patterns('', # /boards/ - url(r'^$', views.index, name='index'), + url(r'^$', all_threads.AllThreadsView.as_view(), name='index'), # /boards/page/ - url(r'^page/(?P\w+)/$', views.index, name='index'), + url(r'^page/(?P\w+)/$', all_threads.AllThreadsView.as_view(), + name='index'), - url(r'^archive/$', views.archive, name='archive'), - url(r'^archive/page/(?P\w+)/$', views.archive, name='archive'), + url(r'^archive/$', archived_threads.ArchiveView.as_view(), name='archive'), + url(r'^archive/page/(?P\w+)/$', + archived_threads.ArchiveView.as_view(), name='archive'), # login page - url(r'^login/$', views.login, name='login'), + url(r'^login/$', login.LoginView.as_view(), name='login'), # /boards/tag/tag_name/ - url(r'^tag/(?P\w+)/$', views.tag, name='tag'), + url(r'^tag/(?P\w+)/$', tag_threads.TagView.as_view(), + name='tag'), # /boards/tag/tag_id/page/ - url(r'^tag/(?P\w+)/page/(?P\w+)/$', views.tag, name='tag'), - - # /boards/tag/tag_name/unsubscribe/ - url(r'^tag/(?P\w+)/subscribe/$', views.tag_subscribe, - name='tag_subscribe'), - # /boards/tag/tag_name/unsubscribe/ - url(r'^tag/(?P\w+)/unsubscribe/$', views.tag_unsubscribe, - name='tag_unsubscribe'), + url(r'^tag/(?P\w+)/page/(?P\w+)/$', + tag_threads.TagView.as_view(), name='tag'), # /boards/thread/ - url(r'^thread/(?P\w+)/$', views.thread, name='thread'), - url(r'^thread/(?P\w+)/(?P\w+)/$', views.thread, name='thread_mode'), - url(r'^settings/$', views.settings, name='settings'), - url(r'^tags/$', views.all_tags, name='tags'), + url(r'^thread/(?P\w+)/$', views.thread.ThreadView.as_view(), + name='thread'), + 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'^jump/(?P\w+)/$', views.jump_to_post, name='jumper'), - url(r'^authors/$', views.authors, name='authors'), - url(r'^delete/(?P\w+)/$', views.delete, name='delete'), - url(r'^ban/(?P\w+)/$', views.ban, name='ban'), + url(r'^authors/$', AuthorsView.as_view(), name='authors'), + url(r'^delete/(?P\w+)/$', DeletePostView.as_view(), + name='delete'), + url(r'^ban/(?P\w+)/$', BanUserView.as_view(), name='ban'), - url(r'^banned/$', views.you_are_banned, name='banned'), - url(r'^staticpage/(?P\w+)/$', views.static_page, name='staticpage'), + url(r'^banned/$', views.banned.BannedView.as_view(), name='banned'), + url(r'^staticpage/(?P\w+)/$', StaticPageView.as_view(), + name='staticpage'), # RSS feeds url(r'^rss/$', AllThreadsFeed()), @@ -54,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"), @@ -62,9 +74,10 @@ urlpatterns = patterns('', api.api_get_threaddiff, name="get_thread_diff"), url(r'^api/threads/(?P\w+)/$', api.api_get_threads, name='get_threads'), - url(r'api/tags/$', api.api_get_tags, name='get_tags'), - url(r'api/thread/(?P\w+)/$', api.api_get_thread_posts, + 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/utils.py b/boards/utils.py --- a/boards/utils.py +++ b/boards/utils.py @@ -1,6 +1,7 @@ """ This module contains helper functions and helper classes. """ +from django.utils import timezone from neboard import settings import time @@ -70,4 +71,10 @@ def get_client_ip(request): ip = x_forwarded_for.split(',')[-1].strip() else: ip = request.META.get('REMOTE_ADDR') - return ip \ No newline at end of file + return ip + + +def datetime_to_epoch(datetime): + return int(time.mktime(timezone.localtime( + datetime,timezone.get_current_timezone()).timetuple()) + * 1000000 + datetime.microsecond) \ No newline at end of file diff --git a/boards/views/__init__.py b/boards/views/__init__.py --- a/boards/views/__init__.py +++ b/boards/views/__init__.py @@ -1,609 +1,9 @@ -from datetime import datetime, timedelta - -from django.db.models import Count - - -OLD_USER_AGE_DAYS = 90 - __author__ = 'neko259' -import hashlib -import string -import time -import re - -from django.core import serializers -from django.core.urlresolvers import reverse -from django.http import HttpResponseRedirect, Http404 -from django.http.response import HttpResponse -from django.template import RequestContext -from django.shortcuts import render, redirect, get_object_or_404 -from django.utils import timezone -from django.db import transaction from django.views.decorators.cache import cache_page from django.views.i18n import javascript_catalog -from django.core.paginator import Paginator - -from boards import forms -import boards -from boards import utils -from boards.forms import ThreadForm, PostForm, SettingsForm, PlainErrorList, \ - ThreadCaptchaForm, PostCaptchaForm, LoginForm, ModeratorSettingsForm -from boards.models import Post, Tag, Ban, User, Thread -from boards.models.post import SETTING_MODERATE, REGEX_REPLY -from boards.models.user import RANK_USER -from boards import authors -from boards.utils import get_client_ip -import neboard - - -BAN_REASON_SPAM = 'Autoban: spam bot' -MODE_GALLERY = 'gallery' -MODE_NORMAL = 'normal' - -DEFAULT_PAGE = 1 - - -def index(request, page=DEFAULT_PAGE): - context = _init_default_context(request) - - if utils.need_include_captcha(request): - threadFormClass = ThreadCaptchaForm - kwargs = {'request': request} - else: - threadFormClass = ThreadForm - kwargs = {} - - if request.method == 'POST': - form = threadFormClass(request.POST, request.FILES, - error_class=PlainErrorList, **kwargs) - form.session = request.session - - if form.is_valid(): - return _new_post(request, form) - if form.need_to_ban: - # Ban user because he is suspected to be a bot - _ban_current_user(request) - else: - form = threadFormClass(error_class=PlainErrorList, **kwargs) - - threads = [] - for thread_to_show in Post.objects.get_threads(page=int(page)): - threads.append(_get_template_thread(thread_to_show)) - - # TODO Make this generic for tag and threads list pages - context['threads'] = None if len(threads) == 0 else threads - context['form'] = form - - paginator = Paginator(Thread.objects.filter(archived=False), - neboard.settings.THREADS_PER_PAGE) - _get_page_context(paginator, context, page) - - return render(request, 'boards/posting_general.html', - context) - - -def archive(request, page=DEFAULT_PAGE): - """ - Get archived posts - """ - - context = _init_default_context(request) - - threads = [] - for thread_to_show in Post.objects.get_threads(page=int(page), - archived=True): - threads.append(_get_template_thread(thread_to_show)) - - context['threads'] = threads - - paginator = Paginator(Thread.objects.filter(archived=True), - neboard.settings.THREADS_PER_PAGE) - _get_page_context(paginator, context, page) - - return render(request, 'boards/archive.html', context) - - -@transaction.atomic -def _new_post(request, form, opening_post=None, html_response=True): - """Add a new post (in thread or as a reply).""" - - ip = get_client_ip(request) - is_banned = Ban.objects.filter(ip=ip).exists() - - if is_banned: - if html_response: - return redirect(you_are_banned) - else: - return - - data = form.cleaned_data - - title = data['title'] - text = data['text'] - - text = _remove_invalid_links(text) - - if 'image' in data.keys(): - image = data['image'] - else: - image = None - - tags = [] - - if not opening_post: - tag_strings = data['tags'] - - if tag_strings: - tag_strings = tag_strings.split(' ') - for tag_name in tag_strings: - tag_name = string.lower(tag_name.strip()) - if len(tag_name) > 0: - tag, created = Tag.objects.get_or_create(name=tag_name) - tags.append(tag) - post_thread = None - else: - post_thread = opening_post.thread_new - - post = Post.objects.create_post(title=title, text=text, ip=ip, - thread=post_thread, image=image, - tags=tags, user=_get_user(request)) - - thread_to_show = (opening_post.id if opening_post else post.id) - - if html_response: - if opening_post: - return redirect(reverse(thread, kwargs={'post_id': thread_to_show}) + - '#' + str(post.id)) - else: - return redirect(thread, post_id=thread_to_show) - - -def tag(request, tag_name, page=DEFAULT_PAGE): - """ - Get all tag threads. Threads are split in pages, so some page is - requested. - """ - - tag = get_object_or_404(Tag, name=tag_name) - threads = [] - for thread_to_show in Post.objects.get_threads(page=int(page), tag=tag): - threads.append(_get_template_thread(thread_to_show)) - - if request.method == 'POST': - form = ThreadForm(request.POST, request.FILES, - error_class=PlainErrorList) - form.session = request.session - - if form.is_valid(): - return _new_post(request, form) - if form.need_to_ban: - # Ban user because he is suspected to be a bot - _ban_current_user(request) - else: - form = forms.ThreadForm(initial={'tags': tag_name}, - error_class=PlainErrorList) - - context = _init_default_context(request) - context['threads'] = None if len(threads) == 0 else threads - context['tag'] = tag - - paginator = Paginator(Post.objects.get_threads(tag=tag), - neboard.settings.THREADS_PER_PAGE) - _get_page_context(paginator, context, page) - - context['form'] = form - - return render(request, 'boards/posting_general.html', - context) - - -def thread(request, post_id, mode=MODE_NORMAL): - """Get all thread posts""" - - if utils.need_include_captcha(request): - postFormClass = PostCaptchaForm - kwargs = {'request': request} - else: - postFormClass = PostForm - kwargs = {} - - opening_post = get_object_or_404(Post, id=post_id) - - # If this is not OP, don't show it as it is - if not opening_post.is_opening(): - raise Http404 - - if request.method == 'POST' and not opening_post.thread_new.archived: - form = postFormClass(request.POST, request.FILES, - error_class=PlainErrorList, **kwargs) - form.session = request.session - - if form.is_valid(): - return _new_post(request, form, opening_post) - if form.need_to_ban: - # Ban user because he is suspected to be a bot - _ban_current_user(request) - else: - form = postFormClass(error_class=PlainErrorList, **kwargs) - - thread_to_show = opening_post.thread_new - - context = _init_default_context(request) - - posts = thread_to_show.get_replies() - context['form'] = form - context["last_update"] = _datetime_to_epoch(thread_to_show.last_edit_time) - context["thread"] = thread_to_show - - if MODE_NORMAL == mode: - context['bumpable'] = thread_to_show.can_bump() - if context['bumpable']: - context['posts_left'] = neboard.settings.MAX_POSTS_PER_THREAD - posts \ - .count() - context['bumplimit_progress'] = str( - float(context['posts_left']) / - neboard.settings.MAX_POSTS_PER_THREAD * 100) - - context['posts'] = posts - - document = 'boards/thread.html' - elif MODE_GALLERY == mode: - context['posts'] = posts.filter(image_width__gt=0) - - document = 'boards/thread_gallery.html' - else: - raise Http404 - - return render(request, document, context) - - -def login(request): - """Log in with user id""" - - context = _init_default_context(request) - - if request.method == 'POST': - form = LoginForm(request.POST, request.FILES, - error_class=PlainErrorList) - form.session = request.session - - if form.is_valid(): - user = User.objects.get(user_id=form.cleaned_data['user_id']) - request.session['user_id'] = user.id - return redirect(index) - - else: - form = LoginForm() - - context['form'] = form - - return render(request, 'boards/login.html', context) - - -def settings(request): - """User's settings""" - - context = _init_default_context(request) - user = _get_user(request) - is_moderator = user.is_moderator() - - if request.method == 'POST': - with transaction.atomic(): - if is_moderator: - form = ModeratorSettingsForm(request.POST, - error_class=PlainErrorList) - else: - form = SettingsForm(request.POST, error_class=PlainErrorList) - - if form.is_valid(): - selected_theme = form.cleaned_data['theme'] - - user.save_setting('theme', selected_theme) - - if is_moderator: - moderate = form.cleaned_data['moderate'] - user.save_setting(SETTING_MODERATE, moderate) - - return redirect(settings) - else: - selected_theme = _get_theme(request) - - if is_moderator: - form = ModeratorSettingsForm(initial={'theme': selected_theme, - 'moderate': context['moderator']}, - error_class=PlainErrorList) - else: - form = SettingsForm(initial={'theme': selected_theme}, - error_class=PlainErrorList) - - context['form'] = form - - return render(request, 'boards/settings.html', context) - - -def all_tags(request): - """All tags list""" - - context = _init_default_context(request) - context['all_tags'] = Tag.objects.get_not_empty_tags() - - return render(request, 'boards/tags.html', context) - - -def jump_to_post(request, post_id): - """Determine thread in which the requested post is and open it's page""" - - post = get_object_or_404(Post, id=post_id) - - if not post.thread: - return redirect(thread, post_id=post.id) - else: - return redirect(reverse(thread, kwargs={'post_id': post.thread.id}) - + '#' + str(post.id)) - - -def authors(request): - """Show authors list""" - - context = _init_default_context(request) - context['authors'] = boards.authors.authors - - return render(request, 'boards/authors.html', context) - - -@transaction.atomic -def delete(request, post_id): - """Delete post""" - - user = _get_user(request) - post = get_object_or_404(Post, id=post_id) - - if user.is_moderator(): - # TODO Show confirmation page before deletion - Post.objects.delete_post(post) - - if not post.thread: - return _redirect_to_next(request) - else: - return redirect(thread, post_id=post.thread.id) - - -@transaction.atomic -def ban(request, post_id): - """Ban user""" - - user = _get_user(request) - post = get_object_or_404(Post, id=post_id) - - if user.is_moderator(): - # TODO Show confirmation page before ban - ban, created = Ban.objects.get_or_create(ip=post.poster_ip) - if created: - ban.reason = 'Banned for post ' + str(post_id) - ban.save() - - return _redirect_to_next(request) - - -def you_are_banned(request): - """Show the page that notifies that user is banned""" - - context = _init_default_context(request) - - ban = get_object_or_404(Ban, ip=utils.get_client_ip(request)) - context['ban_reason'] = ban.reason - return render(request, 'boards/staticpages/banned.html', context) - - -def page_404(request): - """Show page 404 (not found error)""" - - context = _init_default_context(request) - return render(request, 'boards/404.html', context) - - -@transaction.atomic -def tag_subscribe(request, tag_name): - """Add tag to favorites""" - - user = _get_user(request) - tag = get_object_or_404(Tag, name=tag_name) - - if not tag in user.fav_tags.all(): - user.add_tag(tag) - - return _redirect_to_next(request) - - -@transaction.atomic -def tag_unsubscribe(request, tag_name): - """Remove tag from favorites""" - - user = _get_user(request) - tag = get_object_or_404(Tag, name=tag_name) - - if tag in user.fav_tags.all(): - user.remove_tag(tag) - - return _redirect_to_next(request) - - -def static_page(request, name): - """Show a static page that needs only tags list and a CSS""" - - context = _init_default_context(request) - return render(request, 'boards/staticpages/' + name + '.html', context) - - -def api_get_post(request, post_id): - """ - Get the JSON of a post. This can be - used as and API for external clients. - """ - - post = get_object_or_404(Post, id=post_id) - - json = serializers.serialize("json", [post], fields=( - "pub_time", "_text_rendered", "title", "text", "image", - "image_width", "image_height", "replies", "tags" - )) - - return HttpResponse(content=json) @cache_page(86400) def cached_js_catalog(request, domain='djangojs', packages=None): return javascript_catalog(request, domain, packages) - - -def _get_theme(request, user=None): - """Get user's CSS theme""" - - if not user: - user = _get_user(request) - theme = user.get_setting('theme') - if not theme: - theme = neboard.settings.DEFAULT_THEME - - return theme - - -def _init_default_context(request): - """Create context with default values that are used in most views""" - - context = RequestContext(request) - - user = _get_user(request) - context['user'] = user - context['tags'] = user.get_sorted_fav_tags() - context['posts_per_day'] = float(Post.objects.get_posts_per_day()) - - theme = _get_theme(request, user) - context['theme'] = theme - context['theme_css'] = 'css/' + theme + '/base_page.css' - - # This shows the moderator panel - moderate = user.get_setting(SETTING_MODERATE) - if moderate == 'True': - context['moderator'] = user.is_moderator() - else: - context['moderator'] = False - - return context - - -def _get_user(request): - """ - Get current user from the session. If the user does not exist, create - a new one. - """ - - session = request.session - if not 'user_id' in session: - request.session.save() - - md5 = hashlib.md5() - md5.update(session.session_key) - new_id = md5.hexdigest() - - while User.objects.filter(user_id=new_id).exists(): - md5.update(str(timezone.now())) - new_id = md5.hexdigest() - - time_now = timezone.now() - user = User.objects.create(user_id=new_id, rank=RANK_USER, - registration_time=time_now) - - _delete_old_users() - - session['user_id'] = user.id - else: - user = User.objects.get(id=session['user_id']) - - return user - - -def _redirect_to_next(request): - """ - If a 'next' parameter was specified, redirect to the next page. This is - used when the user is required to return to some page after the current - view has finished its work. - """ - - if 'next' in request.GET: - next_page = request.GET['next'] - return HttpResponseRedirect(next_page) - else: - return redirect(index) - - -@transaction.atomic -def _ban_current_user(request): - """Add current user to the IP ban list""" - - ip = utils.get_client_ip(request) - ban, created = Ban.objects.get_or_create(ip=ip) - if created: - ban.can_read = False - ban.reason = BAN_REASON_SPAM - ban.save() - - -def _remove_invalid_links(text): - """ - Replace invalid links in posts so that they won't be parsed. - Invalid links are links to non-existent posts - """ - - for reply_number in re.finditer(REGEX_REPLY, text): - post_id = reply_number.group(1) - post = Post.objects.filter(id=post_id) - if not post.exists(): - text = string.replace(text, '>>' + post_id, post_id) - - return text - - -def _datetime_to_epoch(datetime): - return int(time.mktime(timezone.localtime( - datetime,timezone.get_current_timezone()).timetuple()) - * 1000000 + datetime.microsecond) - - -def _get_template_thread(thread_to_show): - """Get template values for thread""" - - last_replies = thread_to_show.get_last_replies() - skipped_replies_count = thread_to_show.get_replies().count() \ - - len(last_replies) - 1 - return { - 'thread': thread_to_show, - 'op': thread_to_show.get_replies()[0], - 'bumpable': thread_to_show.can_bump(), - 'last_replies': last_replies, - 'skipped_replies': skipped_replies_count, - } - - -def _delete_old_users(): - """ - Delete users with no favorite tags and posted messages. These can be spam - bots or just old user accounts - """ - - old_registration_date = datetime.now().date() - timedelta(OLD_USER_AGE_DAYS) - - for user in User.objects.annotate(tags_count=Count('fav_tags')).filter( - tags_count=0).filter(registration_time__lt=old_registration_date): - if not Post.objects.filter(user=user).exists(): - user.delete() - - -def _get_page_context(paginator, context, page): - """ - Get pagination context variables - """ - - context['paginator'] = paginator - context['current_page'] = paginator.page(int(page)) diff --git a/boards/views/all_tags.py b/boards/views/all_tags.py new file mode 100644 --- /dev/null +++ b/boards/views/all_tags.py @@ -0,0 +1,12 @@ +from django.shortcuts import render + +from boards.views.base import BaseBoardView +from boards.models.tag import Tag + +class AllTagsView(BaseBoardView): + + def get(self, request): + context = self.get_context_data(request=request) + context['all_tags'] = Tag.objects.get_not_empty_tags() + + return render(request, 'boards/tags.html', context) diff --git a/boards/views/all_threads.py b/boards/views/all_threads.py new file mode 100644 --- /dev/null +++ b/boards/views/all_threads.py @@ -0,0 +1,125 @@ +import string + +from django.core.paginator import Paginator +from django.core.urlresolvers import reverse +from django.db import transaction +from django.shortcuts import render, redirect + +from boards import utils +from boards.forms import ThreadForm, PlainErrorList +from boards.models import Post, Thread, Ban, Tag +from boards.views.banned import BannedView +from boards.views.base import BaseBoardView, PARAMETER_FORM +from boards.views.posting_mixin import PostMixin +import neboard + +PARAMETER_CURRENT_PAGE = 'current_page' + +PARAMETER_PAGINATOR = 'paginator' + +PARAMETER_THREADS = 'threads' + +TEMPLATE = 'boards/posting_general.html' +DEFAULT_PAGE = 1 + + +class AllThreadsView(PostMixin, BaseBoardView): + + def get(self, request, page=DEFAULT_PAGE, form=None): + context = self.get_context_data(request=request) + + if not form: + form = ThreadForm(error_class=PlainErrorList) + + paginator = Paginator(self.get_threads(), + neboard.settings.THREADS_PER_PAGE) + + threads = paginator.page(page).object_list + + context[PARAMETER_THREADS] = threads + context[PARAMETER_FORM] = form + + self._get_page_context(paginator, context, page) + + return render(request, TEMPLATE, context) + + def post(self, request, page=DEFAULT_PAGE): + context = self.get_context_data(request=request) + + form = ThreadForm(request.POST, request.FILES, + error_class=PlainErrorList) + form.session = request.session + + if form.is_valid(): + return self._new_post(request, form) + if form.need_to_ban: + # Ban user because he is suspected to be a bot + self._ban_current_user(request) + + return self.get(request, page, form) + + @staticmethod + def _get_page_context(paginator, context, page): + """ + Get pagination context variables + """ + + context[PARAMETER_PAGINATOR] = paginator + context[PARAMETER_CURRENT_PAGE] = paginator.page(int(page)) + + # TODO This method should be refactored + @transaction.atomic + def _new_post(self, request, form, opening_post=None, html_response=True): + """ + Add a new thread opening post. + """ + + ip = utils.get_client_ip(request) + is_banned = Ban.objects.filter(ip=ip).exists() + + if is_banned: + if html_response: + return redirect(BannedView().as_view()) + else: + return + + data = form.cleaned_data + + title = data['title'] + text = data['text'] + + text = self._remove_invalid_links(text) + + if 'image' in data.keys(): + image = data['image'] + else: + image = None + + tags = [] + + tag_strings = data['tags'] + + if tag_strings: + tag_strings = tag_strings.split(' ') + for tag_name in tag_strings: + tag_name = string.lower(tag_name.strip()) + if len(tag_name) > 0: + tag, created = Tag.objects.get_or_create(name=tag_name) + tags.append(tag) + + post = Post.objects.create_post(title=title, text=text, ip=ip, + image=image, tags=tags, + user=self._get_user(request)) + + thread_to_show = (opening_post.id if opening_post else post.id) + + if html_response: + if opening_post: + return redirect( + reverse('thread', kwargs={'post_id': thread_to_show}) + + '#' + str(post.id)) + else: + return redirect('thread', post_id=thread_to_show) + + def get_threads(self): + return Thread.objects.filter(archived=False).order_by('-bump_time') diff --git a/boards/views/api.py b/boards/views/api.py --- a/boards/views/api.py +++ b/boards/views/api.py @@ -5,10 +5,12 @@ from django.http import HttpResponse from django.shortcuts import get_object_or_404, render from django.template import RequestContext from django.utils import timezone +from django.core import serializers + from boards.forms import PostForm, PlainErrorList from boards.models import Post, Thread, Tag -from boards.views import _datetime_to_epoch, _new_post, \ - _ban_current_user +from boards.utils import datetime_to_epoch +from boards.views.thread import ThreadView __author__ = 'neko259' @@ -53,7 +55,7 @@ def api_get_threaddiff(request, thread_i json_data['added'].append(_get_post_data(post.id, diff_type, request)) for post in updated_posts: json_data['updated'].append(_get_post_data(post.id, diff_type, request)) - json_data['last_update'] = _datetime_to_epoch(thread.last_edit_time) + json_data['last_update'] = datetime_to_epoch(thread.last_edit_time) return HttpResponse(content=json.dumps(json_data)) @@ -78,7 +80,8 @@ def api_add_post(request, opening_post_i # _ban_current_user(request) # status = STATUS_ERROR if form.is_valid(): - _new_post(request, form, opening_post, html_response=False) + ThreadView().new_post(request, form, opening_post, + html_response=False) else: status = STATUS_ERROR errors = form.as_json_errors() @@ -178,12 +181,28 @@ def api_get_thread_posts(request, openin for post in posts: json_post_list.append(_get_post_data(post.id)) - json_data['last_update'] = _datetime_to_epoch(thread.last_edit_time) + json_data['last_update'] = datetime_to_epoch(thread.last_edit_time) json_data['posts'] = json_post_list return HttpResponse(content=json.dumps(json_data)) +def api_get_post(request, post_id): + """ + Get the JSON of a post. This can be + used as and API for external clients. + """ + + post = get_object_or_404(Post, id=post_id) + + json = serializers.serialize("json", [post], fields=( + "pub_time", "_text_rendered", "title", "text", "image", + "image_width", "image_height", "replies", "tags" + )) + + return HttpResponse(content=json) + + # TODO Add pub time and replies def _get_post_data(post_id, format_type=DIFF_TYPE_JSON, request=None, include_last_update=False): @@ -200,6 +219,6 @@ def _get_post_data(post_id, format_type= post_json['image'] = post.image.url post_json['image_preview'] = post.image.url_200x150 if include_last_update: - post_json['bump_time'] = _datetime_to_epoch( + post_json['bump_time'] = datetime_to_epoch( post.thread_new.bump_time) return post_json diff --git a/boards/views/archived_threads.py b/boards/views/archived_threads.py new file mode 100644 --- /dev/null +++ b/boards/views/archived_threads.py @@ -0,0 +1,10 @@ +from boards.models import Thread +from boards.views.all_threads import AllThreadsView + +__author__ = 'neko259' + + +class ArchiveView(AllThreadsView): + + def get_threads(self): + return Thread.objects.filter(archived=True).order_by('-bump_time') diff --git a/boards/views/authors.py b/boards/views/authors.py new file mode 100644 --- /dev/null +++ b/boards/views/authors.py @@ -0,0 +1,13 @@ +from django.shortcuts import render + +from boards.authors import authors +from boards.views.base import BaseBoardView + + +class AuthorsView(BaseBoardView): + + def get(self, request): + context = self.get_context_data(request=request) + context['authors'] = authors + + return render(request, 'boards/authors.html', context) diff --git a/boards/views/ban.py b/boards/views/ban.py new file mode 100644 --- /dev/null +++ b/boards/views/ban.py @@ -0,0 +1,23 @@ +from django.db import transaction +from django.shortcuts import get_object_or_404 + +from boards.views.base import BaseBoardView +from boards.models import Post, Ban +from boards.views.mixins import RedirectNextMixin + + +class BanUserView(BaseBoardView, RedirectNextMixin): + + @transaction.atomic + def get(self, request, post_id): + user = self._get_user(request) + post = get_object_or_404(Post, id=post_id) + + if user.is_moderator(): + # TODO Show confirmation page before ban + ban, created = Ban.objects.get_or_create(ip=post.poster_ip) + if created: + ban.reason = 'Banned for post ' + str(post_id) + ban.save() + + return self.redirect_to_next(request) diff --git a/boards/views/banned.py b/boards/views/banned.py new file mode 100644 --- /dev/null +++ b/boards/views/banned.py @@ -0,0 +1,16 @@ +from django.shortcuts import get_object_or_404, render +from boards import utils +from boards.models import Ban +from boards.views.base import BaseBoardView + + +class BannedView(BaseBoardView): + + def get(self, request): + """Show the page that notifies that user is banned""" + + context = self.get_context_data(request=request) + + ban = get_object_or_404(Ban, ip=utils.get_client_ip(request)) + context['ban_reason'] = ban.reason + return render(request, 'boards/staticpages/banned.html', context) diff --git a/boards/views/base.py b/boards/views/base.py new file mode 100644 --- /dev/null +++ b/boards/views/base.py @@ -0,0 +1,125 @@ +from datetime import datetime, timedelta +import hashlib +from django.db import transaction +from django.db.models import Count +from django.template import RequestContext +from django.utils import timezone +from django.views.generic import View +from boards import utils +from boards.models import User, Post +from boards.models.post import SETTING_MODERATE +from boards.models.user import RANK_USER, Ban +import neboard + +BAN_REASON_SPAM = 'Autoban: spam bot' + +OLD_USER_AGE_DAYS = 90 + +PARAMETER_FORM = 'form' + + +class BaseBoardView(View): + + def get_context_data(self, **kwargs): + request = kwargs['request'] + context = self._default_context(request) + + context['version'] = neboard.settings.VERSION + context['site_name'] = neboard.settings.SITE_NAME + + return context + + def _default_context(self, request): + """Create context with default values that are used in most views""" + + context = RequestContext(request) + + user = self._get_user(request) + context['user'] = user + context['tags'] = user.get_sorted_fav_tags() + context['posts_per_day'] = float(Post.objects.get_posts_per_day()) + + theme = self._get_theme(request, user) + context['theme'] = theme + context['theme_css'] = 'css/' + theme + '/base_page.css' + + # This shows the moderator panel + moderate = user.get_setting(SETTING_MODERATE) + if moderate == 'True': + context['moderator'] = user.is_moderator() + else: + context['moderator'] = False + + return context + + def _get_user(self, request): + """ + Get current user from the session. If the user does not exist, create + a new one. + """ + + session = request.session + if not 'user_id' in session: + request.session.save() + + md5 = hashlib.md5() + md5.update(session.session_key) + new_id = md5.hexdigest() + + while User.objects.filter(user_id=new_id).exists(): + md5.update(str(timezone.now())) + new_id = md5.hexdigest() + + time_now = timezone.now() + user = User.objects.create(user_id=new_id, rank=RANK_USER, + registration_time=time_now) + + self._delete_old_users() + + session['user_id'] = user.id + else: + user = User.objects.get(id=session['user_id']) + + return user + + def _get_theme(self, request, user=None): + """ + Get user's CSS theme + """ + + if not user: + user = self._get_user(request) + theme = user.get_setting('theme') + if not theme: + theme = neboard.settings.DEFAULT_THEME + + return theme + + def _delete_old_users(self): + """ + Delete users with no favorite tags and posted messages. These can be spam + bots or just old user accounts + """ + + old_registration_date = datetime.now().date() - timedelta( + OLD_USER_AGE_DAYS) + + for user in User.objects.annotate(tags_count=Count('fav_tags')).filter( + tags_count=0).filter( + registration_time__lt=old_registration_date): + if not Post.objects.filter(user=user).exists(): + user.delete() + + @transaction.atomic + def _ban_current_user(self, request): + """ + Add current user to the IP ban list + """ + + ip = utils.get_client_ip(request) + ban, created = Ban.objects.get_or_create(ip=ip) + if created: + ban.can_read = False + ban.reason = BAN_REASON_SPAM + ban.save() + diff --git a/boards/views/delete_post.py b/boards/views/delete_post.py new file mode 100644 --- /dev/null +++ b/boards/views/delete_post.py @@ -0,0 +1,26 @@ +from django.shortcuts import redirect, get_object_or_404 +from django.db import transaction + +from boards.views.base import BaseBoardView +from boards.views.mixins import RedirectNextMixin +from boards.models import Post + + +class DeletePostView(BaseBoardView, RedirectNextMixin): + + @transaction.atomic + def get(self, request, post_id): + user = self._get_user(request) + post = get_object_or_404(Post, id=post_id) + + opening_post = post.is_opening() + + if user.is_moderator(): + # TODO Show confirmation page before deletion + Post.objects.delete_post(post) + + if not opening_post: + thread = post.thread_new + return redirect('thread', post_id=thread.get_opening_post().id) + else: + return self.redirect_to_next(request) diff --git a/boards/views/login.py b/boards/views/login.py new file mode 100644 --- /dev/null +++ b/boards/views/login.py @@ -0,0 +1,30 @@ +from django.shortcuts import render, redirect +from boards.forms import LoginForm, PlainErrorList +from boards.models import User +from boards.views.base import BaseBoardView, PARAMETER_FORM + +__author__ = 'neko259' + + +class LoginView(BaseBoardView): + + def get(self, request, form=None): + context = self.get_context_data(request=request) + + if not form: + form = LoginForm() + context[PARAMETER_FORM] = form + + return render(request, 'boards/login.html', context) + + def post(self, request): + form = LoginForm(request.POST, request.FILES, + error_class=PlainErrorList) + form.session = request.session + + if form.is_valid(): + user = User.objects.get(user_id=form.cleaned_data['user_id']) + request.session['user_id'] = user.id + return redirect('index') + else: + return self.get(request, form) diff --git a/boards/views/mixins.py b/boards/views/mixins.py new file mode 100644 --- /dev/null +++ b/boards/views/mixins.py @@ -0,0 +1,39 @@ +PARAMETER_METHOD = 'method' + +from django.shortcuts import redirect +from django.http import HttpResponseRedirect + + +class RedirectNextMixin: + + def redirect_to_next(self, request): + """ + If a 'next' parameter was specified, redirect to the next page. This + is used when the user is required to return to some page after the + current view has finished its work. + """ + + if 'next' in request.GET: + next_page = request.GET['next'] + return HttpResponseRedirect(next_page) + else: + return redirect('index') + + +class DispatcherMixin: + """ + This class contains a dispather method that can run a method specified by + 'method' request parameter. + """ + + def dispatch_method(self, *args, **kwargs): + request = args[0] + + method_name = None + if PARAMETER_METHOD in request.GET: + method_name = request.GET[PARAMETER_METHOD] + 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/not_found.py b/boards/views/not_found.py new file mode 100644 --- /dev/null +++ b/boards/views/not_found.py @@ -0,0 +1,13 @@ +from django.shortcuts import render + +from boards.views.base import BaseBoardView + + +class NotFoundView(BaseBoardView): + """ + Page 404 (not found) + """ + + def get(self, request): + context = self.get_context_data(request=request) + return render(request, 'boards/404.html', context) 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) diff --git a/boards/views/posting_mixin.py b/boards/views/posting_mixin.py new file mode 100644 --- /dev/null +++ b/boards/views/posting_mixin.py @@ -0,0 +1,25 @@ +import re +import string + +from boards.models import Post +from boards.models.post import REGEX_REPLY + +REFLINK_PREFIX = '>>' + + +class PostMixin: + + @staticmethod + def _remove_invalid_links(text): + """ + Replace invalid links in posts so that they won't be parsed. + Invalid links are links to non-existent posts + """ + + for reply_number in re.finditer(REGEX_REPLY, text): + post_id = reply_number.group(1) + post = Post.objects.filter(id=post_id) + if not post.exists(): + text = string.replace(text, REFLINK_PREFIX + post_id, post_id) + + return text diff --git a/boards/views/settings.py b/boards/views/settings.py new file mode 100644 --- /dev/null +++ b/boards/views/settings.py @@ -0,0 +1,52 @@ +from django.db import transaction +from django.shortcuts import render, redirect + +from boards.views.base import BaseBoardView, PARAMETER_FORM +from boards.forms import SettingsForm, ModeratorSettingsForm, PlainErrorList +from boards.models.post import SETTING_MODERATE + + +class SettingsView(BaseBoardView): + + def get(self, request): + context = self.get_context_data(request=request) + user = context['user'] + is_moderator = user.is_moderator() + + selected_theme = context['theme'] + + if is_moderator: + form = ModeratorSettingsForm(initial={ + 'theme': selected_theme, + 'moderate': context['moderator'] + }, error_class=PlainErrorList) + else: + form = SettingsForm(initial={'theme': selected_theme}, + error_class=PlainErrorList) + + context[PARAMETER_FORM] = form + + return render(request, 'boards/settings.html', context) + + def post(self, request): + context = self.get_context_data(request=request) + user = context['user'] + is_moderator = user.is_moderator() + + with transaction.atomic(): + if is_moderator: + form = ModeratorSettingsForm(request.POST, + error_class=PlainErrorList) + else: + form = SettingsForm(request.POST, error_class=PlainErrorList) + + if form.is_valid(): + selected_theme = form.cleaned_data['theme'] + + user.save_setting('theme', selected_theme) + + if is_moderator: + moderate = form.cleaned_data['moderate'] + user.save_setting(SETTING_MODERATE, moderate) + + return redirect('settings') diff --git a/boards/views/static.py b/boards/views/static.py new file mode 100644 --- /dev/null +++ b/boards/views/static.py @@ -0,0 +1,13 @@ +from django.shortcuts import render + +from boards.views.base import BaseBoardView + +class StaticPageView(BaseBoardView): + + def get(request, name): + """ + Show a static page that needs only tags list and a CSS + """ + + context = self.get_context_data(request=request) + return render(request, 'boards/staticpages/' + name + '.html', context) diff --git a/boards/views/tag_threads.py b/boards/views/tag_threads.py new file mode 100644 --- /dev/null +++ b/boards/views/tag_threads.py @@ -0,0 +1,51 @@ +from django.shortcuts import get_object_or_404 +from boards.models import Tag, Post +from boards.views.all_threads import AllThreadsView, DEFAULT_PAGE +from boards.views.mixins import DispatcherMixin, RedirectNextMixin + +__author__ = 'neko259' + + +class TagView(AllThreadsView, DispatcherMixin, RedirectNextMixin): + + tag_name = None + + def get_threads(self): + tag = get_object_or_404(Tag, name=self.tag_name) + + return tag.threads.filter(archived=False).order_by('-bump_time') + + def get_context_data(self, **kwargs): + context = super(TagView, self).get_context_data(**kwargs) + + tag = get_object_or_404(Tag, name=self.tag_name) + context['tag'] = tag + + return context + + def get(self, request, tag_name, page=DEFAULT_PAGE): + self.tag_name = tag_name + + dispatch_result = self.dispatch_method(request) + if dispatch_result: + return dispatch_result + else: + return super(TagView, self).get(request, page) + + def subscribe(self, request): + user = self._get_user(request) + tag = get_object_or_404(Tag, name=self.tag_name) + + if not tag in user.fav_tags.all(): + user.add_tag(tag) + + return self.redirect_to_next(request) + + def unsubscribe(self, request): + user = self._get_user(request) + tag = get_object_or_404(Tag, name=self.tag_name) + + if tag in user.fav_tags.all(): + user.remove_tag(tag) + + return self.redirect_to_next(request) diff --git a/boards/views/thread.py b/boards/views/thread.py new file mode 100644 --- /dev/null +++ b/boards/views/thread.py @@ -0,0 +1,121 @@ +import string +from django.core.urlresolvers import reverse +from django.db import transaction +from django.http import Http404 +from django.shortcuts import get_object_or_404, render, redirect +from boards import utils +from boards.forms import PostForm, PlainErrorList +from boards.models import Post, Ban, Tag +from boards.views.banned import BannedView +from boards.views.base import BaseBoardView, PARAMETER_FORM +from boards.views.posting_mixin import PostMixin +import neboard + +MODE_GALLERY = 'gallery' +MODE_NORMAL = 'normal' + + +class ThreadView(BaseBoardView, PostMixin): + + def get(self, request, post_id, mode=MODE_NORMAL, form=None): + opening_post = get_object_or_404(Post, id=post_id) + + # If this is not OP, don't show it as it is + if not opening_post.is_opening(): + raise Http404 + + if not form: + form = PostForm(error_class=PlainErrorList) + + thread_to_show = opening_post.thread_new + + context = self.get_context_data(request=request) + + context[PARAMETER_FORM] = form + context["last_update"] = utils.datetime_to_epoch( + thread_to_show.last_edit_time) + context["thread"] = thread_to_show + + if MODE_NORMAL == mode: + context['bumpable'] = thread_to_show.can_bump() + if context['bumpable']: + context['posts_left'] = neboard.settings.MAX_POSTS_PER_THREAD \ + - thread_to_show.get_reply_count() + context['bumplimit_progress'] = str( + float(context['posts_left']) / + neboard.settings.MAX_POSTS_PER_THREAD * 100) + + context['opening_post'] = thread_to_show.get_opening_post() + + document = 'boards/thread.html' + elif MODE_GALLERY == mode: + posts = thread_to_show.get_replies() + context['posts'] = posts.filter(image_width__gt=0) + + document = 'boards/thread_gallery.html' + else: + raise Http404 + + return render(request, document, context) + + def post(self, request, post_id, mode=MODE_NORMAL): + opening_post = get_object_or_404(Post, id=post_id) + + # If this is not OP, don't show it as it is + if not opening_post.is_opening(): + raise Http404 + + if not opening_post.thread_new.archived: + form = PostForm(request.POST, request.FILES, + error_class=PlainErrorList) + form.session = request.session + + if form.is_valid(): + return self.new_post(request, form, opening_post) + if form.need_to_ban: + # Ban user because he is suspected to be a bot + self._ban_current_user(request) + + return self.get(request, post_id, mode, form) + + @transaction.atomic + def new_post(self, request, form, opening_post=None, html_response=True): + """Add a new post (in thread or as a reply).""" + + ip = utils.get_client_ip(request) + is_banned = Ban.objects.filter(ip=ip).exists() + + if is_banned: + if html_response: + return redirect(BannedView().as_view()) + else: + return + + data = form.cleaned_data + + title = data['title'] + text = data['text'] + + text = self._remove_invalid_links(text) + + if 'image' in data.keys(): + image = data['image'] + else: + image = None + + tags = [] + + post_thread = opening_post.thread_new + + post = Post.objects.create_post(title=title, text=text, ip=ip, + thread=post_thread, image=image, + tags=tags, + user=self._get_user(request)) + + thread_to_show = (opening_post.id if opening_post else post.id) + + if html_response: + if opening_post: + return redirect(reverse( + 'thread', + kwargs={'post_id': thread_to_show}) + '#' + str(post.id)) diff --git a/changelog.markdown b/changelog.markdown --- a/changelog.markdown +++ b/changelog.markdown @@ -14,3 +14,15 @@ thread. * Added API for viewing threads and posts * New tag popularity algorithm * Tags list page changes. Now tags list is more like a tag cloud + +# 1.7 Anubis +* [ADMIN] Added admin page for post editing, capable of adding and removing tags +* [CODE] Post view unification +* Post caching instead of thread caching +* Simplified tag list page +* [API] Added api for thread update in json +* Image duplicate check +* Posting over ajax (no page reload now) +* Update last update time with thread update +* Added z-index to the images to move the dragged image to front +* [CODE] Major view refactoring. Now almost all views are class-based diff --git a/neboard/settings.py b/neboard/settings.py --- a/neboard/settings.py +++ b/neboard/settings.py @@ -223,6 +223,8 @@ POSTING_DELAY = 20 # seconds COMPRESS_HTML = True +VERSION = '1.7 Anubis' + # Debug mode middlewares if DEBUG: MIDDLEWARE_CLASSES += ( diff --git a/neboard/urls.py b/neboard/urls.py --- a/neboard/urls.py +++ b/neboard/urls.py @@ -5,6 +5,8 @@ from django.conf.urls.static import stat from django.contrib import admin from neboard import settings +from boards.views.not_found import NotFoundView + admin.autodiscover() urlpatterns = patterns('', @@ -20,4 +22,4 @@ urlpatterns = patterns('', url(r'^', include('boards.urls')), ) + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) -handler404 = 'boards.views.page_404' +handler404 = NotFoundView.as_view() diff --git a/requirements.txt b/requirements.txt new file mode 100644 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +pillow +django>=1.6 +django_cleanup +django-markupfield +markdown +python-markdown +django-simple-captcha +line-profiler diff --git a/todo.txt b/todo.txt --- a/todo.txt +++ b/todo.txt @@ -15,7 +15,6 @@ post or its part (delimited by N characters) into quote of the new post. [NOT STARTED] Ban confirmation page with reason [NOT STARTED] Post deletion confirmation page -[NOT STARTED] Moderating page. Tags editing and adding [NOT STARTED] Get thread graph image using pygraphviz [NOT STARTED] Subscribing to tag via AJAX