diff --git a/.hgtags b/.hgtags --- a/.hgtags +++ b/.hgtags @@ -33,3 +33,4 @@ 836d8bb9fcd930b952b9a02029442c71c2441983 dfb6c481b1a2c33705de9a9b5304bc924c46b202 2.8.1 4a5bec08ccfb47a27f9e98698f12dd5b7246623b 2.8.2 604935b98f5b5e4a5e903594f048046e1fbb3519 2.8.3 +c48ffdc671566069ed0f33644da1229277f3cd18 2.9.0 diff --git a/boards/abstracts/settingsmanager.py b/boards/abstracts/settingsmanager.py --- a/boards/abstracts/settingsmanager.py +++ b/boards/abstracts/settingsmanager.py @@ -1,6 +1,7 @@ -from django.shortcuts import get_object_or_404 from boards.models import Tag +MAX_TRIPCODE_COLLISIONS = 50 + __author__ = 'neko259' SESSION_SETTING = 'setting' @@ -15,6 +16,7 @@ SETTING_PERMISSIONS = 'permissions' SETTING_USERNAME = 'username' SETTING_LAST_NOTIFICATION_ID = 'last_notification' SETTING_IMAGE_VIEWER = 'image_viewer' +SETTING_TRIPCODE = 'tripcode' DEFAULT_THEME = 'md' @@ -130,6 +132,7 @@ class SessionSettingsManager(SettingsMan if setting in self.session: return self.session[setting] else: + self.set_setting(setting, default) return default def set_setting(self, setting, value): diff --git a/boards/abstracts/tripcode.py b/boards/abstracts/tripcode.py new file mode 100644 --- /dev/null +++ b/boards/abstracts/tripcode.py @@ -0,0 +1,26 @@ +class Tripcode: + def __init__(self, code_str): + self.tripcode = code_str + + def get_color(self): + return self.tripcode[:6] + + def get_background(self): + code = self.get_color() + result = '' + + for i in range(0, len(code), 2): + p = code[i:i+2] + background = hex(255 - int(p, 16))[2:] + if len(background) < 2: + background = '0' + background + result += background + + return result + + def get_short_text(self): + return self.tripcode[:8] + + def get_full_text(self): + return self.tripcode + diff --git a/boards/config/default_settings.ini b/boards/config/default_settings.ini --- a/boards/config/default_settings.ini +++ b/boards/config/default_settings.ini @@ -1,5 +1,5 @@ [Version] -Version = 2.8.3 Charlie +Version = 2.9.0 Claire SiteName = Neboard DEV [Cache] @@ -9,7 +9,7 @@ CacheTimeout = 600 [Forms] # Max post length in characters MaxTextLength = 30000 -MaxImageSize = 8000000 +MaxFileSize = 8000000 LimitPostingSpeed = false [Messages] diff --git a/boards/forms.py b/boards/forms.py --- a/boards/forms.py +++ b/boards/forms.py @@ -1,7 +1,8 @@ +import hashlib import re import time + import pytz - from django import forms from django.core.files.uploadedfile import SimpleUploadedFile from django.core.exceptions import ObjectDoesNotExist @@ -14,18 +15,11 @@ from boards.models.post import TITLE_MAX from boards.models import Tag, Post from neboard import settings import boards.settings as board_settings +import neboard HEADER_CONTENT_LENGTH = 'content-length' HEADER_CONTENT_TYPE = 'content-type' -CONTENT_TYPE_IMAGE = ( - 'image/jpeg', - 'image/jpg', - 'image/png', - 'image/gif', - 'image/bmp', -) - REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE) VETERAN_POSTING_DELAY = 5 @@ -47,7 +41,7 @@ ERROR_SPEED = _('Please wait %s seconds TAG_MAX_LENGTH = 20 -IMAGE_DOWNLOAD_CHUNK_BYTES = 100000 +FILE_DOWNLOAD_CHUNK_BYTES = 100000 HTTP_RESULT_OK = 200 @@ -137,17 +131,20 @@ class NeboardForm(forms.Form): class PostForm(NeboardForm): title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False, - label=LABEL_TITLE) + label=LABEL_TITLE, + widget=forms.TextInput( + attrs={ATTRIBUTE_PLACEHOLDER: + 'test#tripcode'})) text = forms.CharField( widget=FormatPanel(attrs={ ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER, ATTRIBUTE_ROWS: TEXTAREA_ROWS, }), required=False, label=LABEL_TEXT) - image = forms.ImageField(required=False, label=_('Image'), + file = forms.FileField(required=False, label=_('File'), widget=forms.ClearableFileInput( - attrs={'accept': 'image/*'})) - image_url = forms.CharField(required=False, label=_('Image URL'), + attrs={'accept': 'file/*'})) + file_url = forms.CharField(required=False, label=_('File URL'), widget=forms.TextInput( attrs={ATTRIBUTE_PLACEHOLDER: 'http://example.com/image.png'})) @@ -181,27 +178,27 @@ class PostForm(NeboardForm): 'characters') % str(max_length)) return text - def clean_image(self): - image = self.cleaned_data['image'] + def clean_file(self): + file = self.cleaned_data['file'] - if image: - self.validate_image_size(image.size) + if file: + self.validate_file_size(file.size) - return image + return file - def clean_image_url(self): - url = self.cleaned_data['image_url'] + def clean_file_url(self): + url = self.cleaned_data['file_url'] - image = None + file = None if url: - image = self._get_image_from_url(url) + file = self._get_file_from_url(url) - if not image: + if not file: raise forms.ValidationError(_('Invalid URL')) else: - self.validate_image_size(image.size) + self.validate_file_size(file.size) - return image + return file def clean_threads(self): threads_str = self.cleaned_data['threads'] @@ -230,27 +227,40 @@ class PostForm(NeboardForm): raise forms.ValidationError('A human cannot enter a hidden field') if not self.errors: - self._clean_text_image() + self._clean_text_file() if not self.errors and self.session: self._validate_posting_speed() return cleaned_data - def get_image(self): + def get_file(self): """ - Gets image from file or URL. + Gets file from form or URL. """ - image = self.cleaned_data['image'] - return image if image else self.cleaned_data['image_url'] + file = self.cleaned_data['file'] + return file or self.cleaned_data['file_url'] + + def get_tripcode(self): + title = self.cleaned_data['title'] + if title is not None and '#' in title: + code = title.split('#', maxsplit=1)[1] + neboard.settings.SECRET_KEY + return hashlib.md5(code.encode()).hexdigest() - def _clean_text_image(self): + def get_title(self): + title = self.cleaned_data['title'] + if title is not None and '#' in title: + return title.split('#', maxsplit=1)[0] + else: + return title + + def _clean_text_file(self): text = self.cleaned_data.get('text') - image = self.get_image() + file = self.get_file() - if (not text) and (not image): - error_message = _('Either text or image must be entered.') + if (not text) and (not file): + error_message = _('Either text or file must be entered.') self._errors['text'] = self.error_class([error_message]) def _validate_posting_speed(self): @@ -284,16 +294,16 @@ class PostForm(NeboardForm): if can_post: self.session[LAST_POST_TIME] = now - def validate_image_size(self, size: int): - max_size = board_settings.get_int('Forms', 'MaxImageSize') + def validate_file_size(self, size: int): + max_size = board_settings.get_int('Forms', 'MaxFileSize') if size > max_size: raise forms.ValidationError( - _('Image must be less than %s bytes') + _('File must be less than %s bytes') % str(max_size)) - def _get_image_from_url(self, url: str) -> SimpleUploadedFile: + def _get_file_from_url(self, url: str) -> SimpleUploadedFile: """ - Gets an image file from URL. + Gets an file file from URL. """ img_temp = None @@ -302,30 +312,29 @@ class PostForm(NeboardForm): # Verify content headers response_head = requests.head(url, verify=False) content_type = response_head.headers[HEADER_CONTENT_TYPE].split(';')[0] - if content_type in CONTENT_TYPE_IMAGE: - length_header = response_head.headers.get(HEADER_CONTENT_LENGTH) - if length_header: - length = int(length_header) - self.validate_image_size(length) - # Get the actual content into memory - response = requests.get(url, verify=False, stream=True) + length_header = response_head.headers.get(HEADER_CONTENT_LENGTH) + if length_header: + length = int(length_header) + self.validate_file_size(length) + # Get the actual content into memory + response = requests.get(url, verify=False, stream=True) - # Download image, stop if the size exceeds limit - size = 0 - content = b'' - for chunk in response.iter_content(IMAGE_DOWNLOAD_CHUNK_BYTES): - size += len(chunk) - self.validate_image_size(size) - content += chunk + # Download file, stop if the size exceeds limit + size = 0 + content = b'' + for chunk in response.iter_content(FILE_DOWNLOAD_CHUNK_BYTES): + size += len(chunk) + self.validate_file_size(size) + content += chunk - if response.status_code == HTTP_RESULT_OK and content: - # Set a dummy file name that will be replaced - # anyway, just keep the valid extension - filename = 'image.' + content_type.split('/')[1] - img_temp = SimpleUploadedFile(filename, content, - content_type) - except Exception: - # Just return no image + if response.status_code == HTTP_RESULT_OK and content: + # Set a dummy file name that will be replaced + # anyway, just keep the valid extension + filename = 'file.' + content_type.split('/')[1] + img_temp = SimpleUploadedFile(filename, content, + content_type) + except Exception as e: + # Just return no file pass return img_temp @@ -356,8 +365,7 @@ class ThreadForm(PostForm): if not required_tag_exists: all_tags = Tag.objects.filter(required=True) raise forms.ValidationError( - _('Need at least one of the tags: ') - + ', '.join([tag.name for tag in all_tags])) + _('Need at least one section.')) return tags diff --git a/boards/locale/ru/LC_MESSAGES/django.mo b/boards/locale/ru/LC_MESSAGES/django.mo index c67215db9768547eaf1a41b6336ac9266f9fe8d3..53640416ea6f4ebdc64becbb6681abb5ca51a17d GIT binary patch literal 7907 zc$|$_dr(}}89%i)8XyobJo5BJO&S_^19{ZA2^tb0(m+5UT2m{%us3j%Wp}-I7p3F0 zd6Y&=G`>38`iQY=t$hqZ2mume$C*x@&Nz4586TZ#`-jtx?X+WO`bRq*`}lp|cP_gD z(#mk}@1ECpzQ^zT&e`9VT={KQgU^Q)D*68IJn|LSeLN`UVI zKLh*|Fdz6=gC7I00$#mPDH5?5h<{a%R~~TnLK(jiSPa|@ECn_!lzr}ADEn9oMczT+ z4Zwag{*1vF%)FO@*8+cHC+dc0u9Ut1*p99|@H-diO4{l?7u18@iNZISr3x={Kz7mA*@6p9|V z7m7WP6pG$&6^b0+%zwX7@mCXYG4ZxU_8BaZ`KL?7UsEN*gO5wZuB)z*_$>x*0bXm``>qk*M}eDx zUk7dljsn+FJg*Ub%kZiNu3sYl=>t{*hn7gbjsZ6Se+|4Im{%%!7%r8(YX#m6Oq2@$ ze^4rN-vwd|_2*KtlV0S%@-oqHMVa_zeVN3gu1spzou>UpndtE=Gw)wz;_tF@@$2?- z8E-6?ey?2Y{f-$QEf+ujv|RGwm*t|zN97`aL528nX@$h=h6?H54D1K)t`L7sSBSoU zuMoQzER}idmdg5VOT{k@rhO3jATVv(|6VHoUARo*zh{}m?GSJi#Toc5;H$uL;^lIY zzX!Mnc+YaN=e6ac$M|y5_h-OIf$uDreR@`ie0PQT>v7-!mYXtp8 z@pKSB@pR$&q7vTQO7x~!hLvz($&pIwyB4 z;S2p$DXFc&U#Y=eN;rFylANkG{kP-Us3Z<`IqND7T1s+=>Ivoit$1$1)1ai5QCz;P zq~6`3ByRLqn+yVvCpdAfYNJt|vU)uFa8gbpZpCz}*R`!Eb@e%MRU3=(jBq$C-@#}) z)thiVwbP2n?Wo$Bh(VHle@g9&B|KXnNVt7!SE8@aj-v}pQnrrU_nGCY&hb*N)0Ixy zQJs!QZ5IQ!s)r8zq8-hL%&&Ja?(zmY;pziU%+`HrFQvO|jU8;)j)ql(4MiF-#4o+C ztw}zDi80&rAkd2IHJ0EkDyNfCh>K{| z>b5^?O-WrI9qQ#|U#dvqV7Mh3N#>GTpiRI6)Khy)X9Pt3kOok-cL&5B18 zea2wbX2&cdS|BNCi!yC?GUo8CWNbj*`#sq^?=l4L77JJ?)C*uqFotp@wMBwl#P+Gp^c`h-d^WlgTL7NpO;ISJc9` z$plF3s&*vIAsbQ>>7HI;N&7%NW!4stMwWB`tb zKo16sQ#u2W1%z9-W*tO3{dVW93DR4s5npl_95pkm<7=<%5ri>KI7c0dq7cY=`weQ{OC(Jj{&SG3r3I0B@~mzZLJMx<*UFYF|=efve*d z{Blu+aszhk9s~cOXq_|`l zd9}+1+q$?cBWDwcN_UD^T2#=f@Y2T0;sUOsw&(Q3ZTjf9V+pWM9iTdDa_6AcZ+YGD zMoMQVpiJ3u&sN4Qo`QQ(xnzlUI-}UMGX}PH^4TJM2JySneMx4aayda2bhcBOs<(uywuUxt)YV%eRX44#+Ei7AiJ>;Q*UQektG>1^)Z7xP ztJSx)M4B7d?{C>#pGO5b)B&}<7Cd>Akhh^?G}Kb8TbOj@r7qw)*z=TQD`= zlZFAI4%_OB=%(7eyZ4dG+nbtp#GG)#?a9uGXg8hL+}K>7wXix|l}AxS8i$B{5oW@M zWXy8n+nBNNSonw#e{L&|^DAH||`Jm&~x90nf7su4&JO8i z{)F;Rqk9}C42$^}XsgVz%p-wjSos15kN6i!4Pzlx$($r>&_1czNu$bt8iH89_;OCi zj0Z^Q?6K^2F9z0VXgKAcBP)je34cmMO^U`@vRnISjFE872$qbpWkc}nGl7lDe@Rv< zTx@2<ME-dl_zlC; z;yW^HoM^|6JS#q+P&~(0fOGIa%%|nTwIlQiBPsMl#M_y@hBJfVPrA@d9AN{5IdtEf z(@C)=E))60LGva-X3)e8)WW`pd9#suD0zy_pJE-wsho4nH16gs!3EMkjRiyKnj|M( zz>Ae2PdyY&gSyOqidSZkU3!uOM?yy_4K=%UocS;nrYP~&#A%2b#`G+SVbM2<4$qN0 zX}6hx!aCz*cp)6jm_Ksi908ZAI}z5|1S1ZS@fX?G6qK>mGb`DX|6?|(&$Bt7iHD{X z32)e4U^FLzKdt=lnVg`UH$EoHKJQQB#W`qd1j-yfkNK~YcP}OJ>0CEuhA2W*e$Mjl z$Dkl_ZP=tg4~z;$C~K&?QKn<{Xkf({eIjI>6I632v1($D+gQ<@Svl2Yw(rO0U3^OR znw@$x18^Jh;c{$vg{>Ht3UZok%^YX%O6i}E1X2!0P!z{m5|MkZZbVdoip^#K` zE$N3fC)l0CnFr~IDTI#lt?mdX23-ut=f8pT5lwaetOS81X<9Zs=VySikd9zQT1!*$ zEoEHJ6RuKH75III531vs0Fq5*ud(xA80TN8vp-u5yUB`I-jL9dDb~fU1!VzZO*2P*|RC< KXV_&fg8u~*9\n" "Language-Team: LANGUAGE \n" @@ -38,80 +38,84 @@ msgstr "разработчик javascript" msgid "designer" msgstr "дизайнер" -#: forms.py:35 +#: forms.py:31 msgid "Type message here. Use formatting panel for more advanced usage." msgstr "" "Вводите сообщение сюда. Используйте панель для более сложного форматирования." -#: forms.py:36 +#: forms.py:32 msgid "music images i_dont_like_tags" msgstr "музыка картинки теги_не_нужны" -#: forms.py:38 +#: forms.py:34 msgid "Title" msgstr "Заголовок" -#: forms.py:39 +#: forms.py:35 msgid "Text" msgstr "Текст" -#: forms.py:40 +#: forms.py:36 msgid "Tag" msgstr "Метка" -#: forms.py:41 templates/boards/base.html:40 templates/search/search.html:7 +#: forms.py:37 templates/boards/base.html:40 templates/search/search.html:7 msgid "Search" msgstr "Поиск" -#: forms.py:43 +#: forms.py:39 #, python-format msgid "Please wait %s seconds before sending message" msgstr "Пожалуйста подождите %s секунд перед отправкой сообщения" -#: forms.py:144 -msgid "Image" -msgstr "Изображение" +#: forms.py:140 +msgid "File" +msgstr "Файл" -#: forms.py:147 -msgid "Image URL" -msgstr "URL изображения" +#: forms.py:143 +msgid "File URL" +msgstr "URL файла" -#: forms.py:153 +#: forms.py:149 msgid "e-mail" msgstr "" -#: forms.py:156 +#: forms.py:152 msgid "Additional threads" msgstr "Дополнительные темы" -#: forms.py:167 +#: forms.py:155 +msgid "Tripcode" +msgstr "Трипкод" + +#: forms.py:164 #, python-format msgid "Title must have less than %s characters" msgstr "Заголовок должен иметь меньше %s символов" -#: forms.py:177 +#: forms.py:174 #, python-format msgid "Text must have less than %s characters" msgstr "Текст должен быть короче %s символов" -#: forms.py:197 +#: forms.py:194 msgid "Invalid URL" msgstr "Неверный URL" -#: forms.py:218 +#: forms.py:215 msgid "Invalid additional thread list" msgstr "Неверный список дополнительных тем" -#: forms.py:250 -msgid "Either text or image must be entered." -msgstr "Текст или картинка должны быть введены." +#: forms.py:251 +msgid "Either text or file must be entered." +msgstr "Текст или файл должны быть введены." -#: forms.py:288 +#: forms.py:289 #, python-format -msgid "Image must be less than %s bytes" -msgstr "Изображение должно быть менее %s байт" +msgid "File must be less than %s bytes" +msgstr "Файл должен быть менее %s байт" -#: forms.py:335 templates/boards/all_threads.html:129 +#: forms.py:335 templates/boards/all_threads.html:154 #: templates/boards/rss/post.html:10 templates/boards/tags.html:6 msgid "Tags" msgstr "Метки" @@ -121,26 +125,27 @@ msgid "Inappropriate characters in tags. msgstr "Недопустимые символы в метках." #: forms.py:356 -msgid "Need at least one of the tags: " -msgstr "Нужна хотя бы одна из меток: " +msgid "Need at least one section." +msgstr "Нужен хотя бы один раздел." -#: forms.py:369 +#: forms.py:368 msgid "Theme" msgstr "Тема" -#: forms.py:370 +#: forms.py:369 +#| msgid "Image view mode" msgid "Image view mode" msgstr "Режим просмотра изображений" -#: forms.py:371 +#: forms.py:370 msgid "User name" msgstr "Имя пользователя" -#: forms.py:372 +#: forms.py:371 msgid "Time zone" msgstr "Часовой пояс" -#: forms.py:378 +#: forms.py:377 msgid "Inappropriate characters." msgstr "Недопустимые символы." @@ -156,54 +161,67 @@ msgstr "Этой страницы не существует" msgid "Related message" msgstr "Связанное сообщение" -#: templates/boards/all_threads.html:60 +#: templates/boards/all_threads.html:71 msgid "Edit tag" msgstr "Изменить метку" -#: templates/boards/all_threads.html:63 +#: templates/boards/all_threads.html:79 #, python-format -msgid "This tag has %(thread_count)s threads and %(post_count)s posts." -msgstr "С этой меткой есть %(thread_count)s тем и %(post_count)s сообщений." +msgid "" +"This tag has %(thread_count)s threads (%(active_thread_count)s active) and " +"%(post_count)s posts." +msgstr "" +"С этой меткой есть %(thread_count)s тем (%(active_thread_count)s активных) и " +"%(post_count)s сообщений." -#: templates/boards/all_threads.html:70 templates/boards/feed.html:30 +#: templates/boards/all_threads.html:81 +msgid "Related tags:" +msgstr "Похожие метки:" + +#: templates/boards/all_threads.html:96 templates/boards/feed.html:30 #: templates/boards/notifications.html:17 templates/search/search.html:26 msgid "Previous page" msgstr "Предыдущая страница" -#: templates/boards/all_threads.html:84 +#: templates/boards/all_threads.html:110 #, python-format msgid "Skipped %(count)s replies. Open thread to see all replies." msgstr "Пропущено %(count)s ответов. Откройте тред, чтобы увидеть все ответы." -#: templates/boards/all_threads.html:102 templates/boards/feed.html:40 +#: templates/boards/all_threads.html:128 templates/boards/feed.html:40 #: templates/boards/notifications.html:27 templates/search/search.html:37 msgid "Next page" msgstr "Следующая страница" -#: templates/boards/all_threads.html:107 +#: templates/boards/all_threads.html:133 msgid "No threads exist. Create the first one!" msgstr "Нет тем. Создайте первую!" -#: templates/boards/all_threads.html:113 +#: templates/boards/all_threads.html:139 msgid "Create new thread" msgstr "Создать новую тему" -#: templates/boards/all_threads.html:118 templates/boards/preview.html:16 +#: templates/boards/all_threads.html:144 templates/boards/preview.html:16 #: templates/boards/thread_normal.html:38 msgid "Post" msgstr "Отправить" -#: templates/boards/all_threads.html:123 +#: templates/boards/all_threads.html:149 msgid "Tags must be delimited by spaces. Text or image is required." msgstr "" "Метки должны быть разделены пробелами. Текст или изображение обязательны." -#: templates/boards/all_threads.html:126 -#: templates/boards/thread_normal.html:43 +#: templates/boards/all_threads.html:151 templates/boards/preview.html:6 +#: templates/boards/staticpages/help.html:21 +#: templates/boards/thread_normal.html:42 +msgid "Preview" +msgstr "Предпросмотр" + +#: templates/boards/all_threads.html:153 templates/boards/thread_normal.html:45 msgid "Text syntax" msgstr "Синтаксис текста" -#: templates/boards/all_threads.html:143 templates/boards/feed.html:53 +#: templates/boards/all_threads.html:167 templates/boards/feed.html:53 msgid "Pages:" msgstr "Страницы: " @@ -251,25 +269,33 @@ msgstr "поиск" msgid "feed" msgstr "лента" -#: templates/boards/base.html:44 templates/boards/base.html.py:45 +#: templates/boards/base.html:42 templates/boards/random.html:6 +msgid "Random images" +msgstr "Случайные изображения" + +#: templates/boards/base.html:42 +msgid "random" +msgstr "случайные" + +#: templates/boards/base.html:45 templates/boards/base.html.py:46 #: templates/boards/notifications.html:8 msgid "Notifications" msgstr "Уведомления" -#: templates/boards/base.html:52 templates/boards/settings.html:8 +#: templates/boards/base.html:53 templates/boards/settings.html:8 msgid "Settings" msgstr "Настройки" -#: templates/boards/base.html:65 +#: templates/boards/base.html:66 msgid "Admin" msgstr "Администрирование" -#: templates/boards/base.html:67 +#: templates/boards/base.html:68 #, python-format msgid "Speed: %(ppd)s posts per day" msgstr "Скорость: %(ppd)s сообщений в день" -#: templates/boards/base.html:69 +#: templates/boards/base.html:70 msgid "Up" msgstr "Вверх" @@ -277,58 +303,62 @@ msgstr "Вверх" msgid "No posts exist. Create the first one!" msgstr "Нет сообщений. Создайте первое!" -#: templates/boards/post.html:25 +#: templates/boards/post.html:30 msgid "Open" msgstr "Открыть" -#: templates/boards/post.html:27 templates/boards/post.html.py:38 +#: templates/boards/post.html:32 templates/boards/post.html.py:43 msgid "Reply" msgstr "Ответить" -#: templates/boards/post.html:33 +#: templates/boards/post.html:38 msgid " in " msgstr " в " -#: templates/boards/post.html:43 +#: templates/boards/post.html:48 msgid "Edit" msgstr "Изменить" -#: templates/boards/post.html:45 +#: templates/boards/post.html:50 msgid "Edit thread" msgstr "Изменить тему" -#: templates/boards/post.html:84 +#: templates/boards/post.html:97 msgid "Replies" msgstr "Ответы" -#: templates/boards/post.html:97 templates/boards/thread.html:37 +#: templates/boards/post.html:109 templates/boards/thread.html:34 msgid "messages" msgstr "сообщений" -#: templates/boards/post.html:98 templates/boards/thread.html:38 +#: templates/boards/post.html:110 templates/boards/thread.html:35 msgid "images" msgstr "изображений" -#: templates/boards/preview.html:6 templates/boards/staticpages/help.html:20 -msgid "Preview" -msgstr "Предпросмотр" - #: templates/boards/rss/post.html:5 msgid "Post image" msgstr "Изображение сообщения" -#: templates/boards/settings.html:16 +#: templates/boards/settings.html:15 msgid "You are moderator." msgstr "Вы модератор." -#: templates/boards/settings.html:20 +#: templates/boards/settings.html:19 msgid "Hidden tags:" msgstr "Скрытые метки:" -#: templates/boards/settings.html:28 +#: templates/boards/settings.html:27 msgid "No hidden tags." msgstr "Нет скрытых меток." +#: templates/boards/settings.html:29 +msgid "Tripcode:" +msgstr "Трипкод:" + +#: templates/boards/settings.html:29 +msgid "reset" +msgstr "сбросить" + #: templates/boards/settings.html:37 msgid "Save" msgstr "Сохранить" @@ -375,34 +405,35 @@ msgstr "Комментарий" msgid "Quote" msgstr "Цитата" -#: templates/boards/staticpages/help.html:20 +#: templates/boards/staticpages/help.html:21 msgid "You can try pasting the text and previewing the result here:" msgstr "Вы можете попробовать вставить текст и проверить результат здесь:" -#: templates/boards/tags.html:21 -msgid "No tags found." -msgstr "Метки не найдены." +#: templates/boards/tags.html:17 +msgid "Sections:" +msgstr "Разделы:" -#: templates/boards/tags.html:24 -msgid "All tags" -msgstr "Все метки" +#: templates/boards/tags.html:30 +msgid "Other tags:" +msgstr "Другие метки:" + +#: templates/boards/tags.html:43 +msgid "All tags..." +msgstr "Все метки..." #: templates/boards/thread.html:15 -#| msgid "Normal mode" msgid "Normal" msgstr "Нормальный" #: templates/boards/thread.html:16 -#| msgid "Gallery mode" msgid "Gallery" msgstr "Галерея" #: templates/boards/thread.html:17 -#| msgid "Tree mode" msgid "Tree" msgstr "Дерево" -#: templates/boards/thread.html:39 +#: templates/boards/thread.html:36 msgid "Last update: " msgstr "Последнее обновление: " @@ -418,14 +449,14 @@ msgstr "сообщений до бамплимита" msgid "Reply to thread" msgstr "Ответить в тему" -#: templates/boards/thread_normal.html:44 +#: templates/boards/thread_normal.html:31 +msgid "to message " +msgstr "на сообщение" + +#: templates/boards/thread_normal.html:46 msgid "Close form" msgstr "Закрыть форму" -#: templates/boards/thread_normal.html:58 -msgid "Update" -msgstr "Обновить" - #: templates/search/search.html:17 msgid "Ok" msgstr "Ок" diff --git a/boards/management/commands/cleanimages.py b/boards/management/commands/cleanfiles.py rename from boards/management/commands/cleanimages.py rename to boards/management/commands/cleanfiles.py --- a/boards/management/commands/cleanimages.py +++ b/boards/management/commands/cleanfiles.py @@ -2,6 +2,8 @@ import os from django.core.management import BaseCommand from django.db import transaction +from boards.models import Attachment +from boards.models.attachment import FILES_DIRECTORY from boards.models.image import IMAGES_DIRECTORY, PostImage, IMAGE_THUMB_SIZE from neboard.settings import MEDIA_ROOT @@ -11,7 +13,7 @@ from neboard.settings import MEDIA_ROOT class Command(BaseCommand): - help = 'Remove image files whose models were deleted' + help = 'Remove files whose models were deleted' @transaction.atomic def handle(self, *args, **options): @@ -21,10 +23,24 @@ class Command(BaseCommand): model_files = os.listdir(MEDIA_ROOT + IMAGES_DIRECTORY) for file in model_files: image_name = file if thumb_prefix not in file else file.replace(thumb_prefix, '') - found = PostImage.objects.filter(image=IMAGES_DIRECTORY + image_name).exists() + found = PostImage.objects.filter( + image=IMAGES_DIRECTORY + image_name).exists() if not found: print('Missing {}'.format(image_name)) os.remove(MEDIA_ROOT + IMAGES_DIRECTORY + file) count += 1 - print('Deleted {} image files.'.format(count)) \ No newline at end of file + print('Deleted {} image files.'.format(count)) + + count = 0 + model_files = os.listdir(MEDIA_ROOT + FILES_DIRECTORY) + for file in model_files: + found = Attachment.objects.filter(file=FILES_DIRECTORY + file)\ + .exists() + + if not found: + print('Missing {}'.format(file)) + os.remove(MEDIA_ROOT + FILES_DIRECTORY + file) + count += 1 + + print('Deleted {} attachment files.'.format(count)) diff --git a/boards/mdx_neboard.py b/boards/mdx_neboard.py --- a/boards/mdx_neboard.py +++ b/boards/mdx_neboard.py @@ -130,7 +130,7 @@ def render_reflink(tag_name, value, opti try: post = boards.models.Post.objects.get(id=post_id) - result = '>>%s' % (post.get_absolute_url(), post_id) + result = post.get_link_view() except ObjectDoesNotExist: pass diff --git a/boards/migrations/0012_auto_20150307_1323.py b/boards/migrations/0012_auto_20150307_1323.py deleted file mode 100644 --- a/boards/migrations/0012_auto_20150307_1323.py +++ /dev/null @@ -1,63 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('boards', '0011_notification'), - ] - - operations = [ - migrations.CreateModel( - name='GlobalId', - fields=[ - ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')), - ('key', models.TextField()), - ('key_type', models.TextField()), - ('local_id', models.IntegerField()), - ], - options={ - }, - bases=(models.Model,), - ), - migrations.CreateModel( - name='KeyPair', - fields=[ - ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')), - ('public_key', models.TextField()), - ('private_key', models.TextField()), - ('key_type', models.TextField()), - ('primary', models.BooleanField(default=False)), - ], - options={ - }, - bases=(models.Model,), - ), - migrations.CreateModel( - name='Signature', - fields=[ - ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')), - ('key_type', models.TextField()), - ('key', models.TextField()), - ('signature', models.TextField()), - ], - options={ - }, - bases=(models.Model,), - ), - migrations.AddField( - model_name='post', - name='global_id', - field=models.OneToOneField(to='boards.GlobalId', null=True, blank=True), - preserve_default=True, - ), - migrations.AddField( - model_name='post', - name='signature', - field=models.ManyToManyField(null=True, blank=True, to='boards.Signature'), - preserve_default=True, - ), - ] diff --git a/boards/migrations/0018_merge.py b/boards/migrations/0018_merge.py deleted file mode 100644 --- a/boards/migrations/0018_merge.py +++ /dev/null @@ -1,15 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('boards', '0012_auto_20150307_1323'), - ('boards', '0017_auto_20150503_1847'), - ] - - operations = [ - ] diff --git a/boards/migrations/0020_auto_20150731_1738.py b/boards/migrations/0020_auto_20150731_1738.py new file mode 100644 --- /dev/null +++ b/boards/migrations/0020_auto_20150731_1738.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('boards', '0019_auto_20150519_1323'), + ] + + operations = [ + migrations.AlterField( + model_name='post', + name='images', + field=models.ManyToManyField(to='boards.PostImage', blank=True, null=True, db_index=True, related_name='post_images'), + ), + ] diff --git a/boards/migrations/0020_merge.py b/boards/migrations/0020_merge.py deleted file mode 100644 --- a/boards/migrations/0020_merge.py +++ /dev/null @@ -1,15 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('boards', '0018_merge'), - ('boards', '0019_auto_20150519_1323'), - ] - - operations = [ - ] diff --git a/boards/migrations/0021_auto_20150716_1408.py b/boards/migrations/0021_auto_20150716_1408.py deleted file mode 100644 --- a/boards/migrations/0021_auto_20150716_1408.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('boards', '0020_merge'), - ] - - operations = [ - migrations.RemoveField( - model_name='post', - name='signature', - ), - migrations.AddField( - model_name='signature', - name='global_id', - field=models.ForeignKey(default=0, to='boards.GlobalId'), - preserve_default=False, - ), - ] diff --git a/boards/migrations/0021_tag_description.py b/boards/migrations/0021_tag_description.py new file mode 100644 --- /dev/null +++ b/boards/migrations/0021_tag_description.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('boards', '0020_auto_20150731_1738'), + ] + + operations = [ + migrations.AddField( + model_name='tag', + name='description', + field=models.TextField(blank=True), + ), + ] diff --git a/boards/migrations/0022_auto_20150812_1819.py b/boards/migrations/0022_auto_20150812_1819.py new file mode 100644 --- /dev/null +++ b/boards/migrations/0022_auto_20150812_1819.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('boards', '0021_tag_description'), + ] + + operations = [ + migrations.AlterField( + model_name='thread', + name='tags', + field=models.ManyToManyField(related_name='thread_tags', to='boards.Tag'), + ), + ] diff --git a/boards/migrations/0023_auto_20150818_1026.py b/boards/migrations/0023_auto_20150818_1026.py new file mode 100644 --- /dev/null +++ b/boards/migrations/0023_auto_20150818_1026.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import boards.models.attachment + + +class Migration(migrations.Migration): + + dependencies = [ + ('boards', '0022_auto_20150812_1819'), + ] + + operations = [ + migrations.CreateModel( + name='Attachment', + fields=[ + ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)), + ('file', models.FileField(upload_to=boards.models.attachment.Attachment._update_filename)), + ('mimetype', models.CharField(max_length=50)), + ('hash', models.CharField(max_length=36)), + ], + ), + migrations.AddField( + model_name='post', + name='attachments', + field=models.ManyToManyField(blank=True, null=True, related_name='attachment_posts', to='boards.Attachment'), + ), + ] diff --git a/boards/migrations/0024_post_tripcode.py b/boards/migrations/0024_post_tripcode.py new file mode 100644 --- /dev/null +++ b/boards/migrations/0024_post_tripcode.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('boards', '0023_auto_20150818_1026'), + ] + + operations = [ + migrations.AddField( + model_name='post', + name='tripcode', + field=models.CharField(max_length=50, null=True), + ), + ] diff --git a/boards/migrations/0025_auto_20150825_2049.py b/boards/migrations/0025_auto_20150825_2049.py new file mode 100644 --- /dev/null +++ b/boards/migrations/0025_auto_20150825_2049.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + def refuild_refmap(apps, schema_editor): + Post = apps.get_model('boards', 'Post') + for post in Post.objects.all(): + post.build_refmap() + post.save(update_fields=['refmap']) + + dependencies = [ + ('boards', '0024_post_tripcode'), + ] + + operations = [ + migrations.RunPython(refuild_refmap), + ] diff --git a/boards/migrations/0026_auto_20150830_2006.py b/boards/migrations/0026_auto_20150830_2006.py new file mode 100644 --- /dev/null +++ b/boards/migrations/0026_auto_20150830_2006.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('boards', '0025_auto_20150825_2049'), + ] + + operations = [ + migrations.CreateModel( + name='GlobalId', + fields=[ + ('id', models.AutoField(serialize=False, verbose_name='ID', primary_key=True, auto_created=True)), + ('key', models.TextField()), + ('key_type', models.TextField()), + ('local_id', models.IntegerField()), + ], + ), + migrations.CreateModel( + name='KeyPair', + fields=[ + ('id', models.AutoField(serialize=False, verbose_name='ID', primary_key=True, auto_created=True)), + ('public_key', models.TextField()), + ('private_key', models.TextField()), + ('key_type', models.TextField()), + ('primary', models.BooleanField(default=False)), + ], + ), + migrations.CreateModel( + name='Signature', + fields=[ + ('id', models.AutoField(serialize=False, verbose_name='ID', primary_key=True, auto_created=True)), + ('key_type', models.TextField()), + ('key', models.TextField()), + ('signature', models.TextField()), + ('global_id', models.ForeignKey(to='boards.GlobalId')), + ], + ), + migrations.AddField( + model_name='post', + name='global_id', + field=models.OneToOneField(to='boards.GlobalId', null=True, blank=True), + ), + ] diff --git a/boards/models/__init__.py b/boards/models/__init__.py --- a/boards/models/__init__.py +++ b/boards/models/__init__.py @@ -3,6 +3,7 @@ from boards.models.signature import GlobalId, Signature from boards.models.sync_key import KeyPair from boards.models.image import PostImage +from boards.models.attachment import Attachment from boards.models.thread import Thread from boards.models.post import Post from boards.models.tag import Tag diff --git a/boards/models/attachment/__init__.py b/boards/models/attachment/__init__.py new file mode 100644 --- /dev/null +++ b/boards/models/attachment/__init__.py @@ -0,0 +1,60 @@ +import os +import time +from random import random + +from django.db import models + +from boards import utils +from boards.models.attachment.viewers import get_viewers, AbstractViewer + +FILES_DIRECTORY = 'files/' +FILE_EXTENSION_DELIMITER = '.' + + +class AttachmentManager(models.Manager): + def create_with_hash(self, file): + file_hash = utils.get_file_hash(file) + existing = self.filter(hash=file_hash) + if len(existing) > 0: + attachment = existing[0] + else: + file_type = file.name.split(FILE_EXTENSION_DELIMITER)[-1].lower() + attachment = Attachment.objects.create( + file=file, mimetype=file_type, hash=file_hash) + + return attachment + + +class Attachment(models.Model): + objects = AttachmentManager() + + # TODO Dedup the method + def _update_filename(self, filename): + """ + Gets unique filename + """ + + # TODO Use something other than random number in file name + new_name = '{}{}.{}'.format( + str(int(time.mktime(time.gmtime()))), + str(int(random() * 1000)), + filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]) + + return os.path.join(FILES_DIRECTORY, new_name) + + file = models.FileField(upload_to=_update_filename) + mimetype = models.CharField(max_length=50) + hash = models.CharField(max_length=36) + + def get_view(self): + file_viewer = None + for viewer in get_viewers(): + if viewer.supports(self.mimetype): + file_viewer = viewer + break + if file_viewer is None: + file_viewer = AbstractViewer + + return file_viewer(self.file, self.mimetype).get_view() + + diff --git a/boards/models/attachment/viewers.py b/boards/models/attachment/viewers.py new file mode 100644 --- /dev/null +++ b/boards/models/attachment/viewers.py @@ -0,0 +1,70 @@ +from django.template.defaultfilters import filesizeformat +from django.contrib.staticfiles.templatetags.staticfiles import static + +FILE_STUB_IMAGE = 'images/file.png' + +FILE_TYPES_VIDEO = ( + 'webm', + 'mp4', +) +FILE_TYPE_SVG = 'svg' +FILE_TYPES_AUDIO = ( + 'ogg', + 'mp3', +) + + +def get_viewers(): + return AbstractViewer.__subclasses__() + + +class AbstractViewer: + def __init__(self, file, file_type): + self.file = file + self.file_type = file_type + + @staticmethod + def supports(file_type): + return True + + def get_view(self): + return '
'\ + '{}'\ + ''\ + '
'.format(self.get_format_view(), self.file.url, + self.file_type, filesizeformat(self.file.size)) + + def get_format_view(self): + return ''\ + ''\ + ''.format(self.file.url, static(FILE_STUB_IMAGE)) + + +class VideoViewer(AbstractViewer): + @staticmethod + def supports(file_type): + return file_type in FILE_TYPES_VIDEO + + def get_format_view(self): + return ''\ + .format(self.file.url) + + +class AudioViewer(AbstractViewer): + @staticmethod + def supports(file_type): + return file_type in FILE_TYPES_AUDIO + + def get_format_view(self): + return ''.format(self.file.url) + + +class SvgViewer(AbstractViewer): + @staticmethod + def supports(file_type): + return file_type == FILE_TYPE_SVG + + def get_format_view(self): + return ''\ + ''\ + ''.format(self.file.url, self.file.url) diff --git a/boards/models/image.py b/boards/models/image.py --- a/boards/models/image.py +++ b/boards/models/image.py @@ -2,8 +2,12 @@ import hashlib import os from random import random import time + from django.db import models -from boards import thumbs +from django.template.defaultfilters import filesizeformat + +from boards import thumbs, utils +import boards from boards.models.base import Viewable __author__ = 'neko259' @@ -20,7 +24,7 @@ CSS_CLASS_THUMB = 'thumb' class PostImageManager(models.Manager): def create_with_hash(self, image): - image_hash = self.get_hash(image) + image_hash = utils.get_file_hash(image) existing = self.filter(hash=image_hash) if len(existing) > 0: post_image = existing[0] @@ -29,14 +33,11 @@ class PostImageManager(models.Manager): return post_image - def get_hash(self, image): - """ - Gets hash of an image. - """ - md5 = hashlib.md5() - for chunk in image.chunks(): - md5.update(chunk) - return md5.hexdigest() + def get_random_images(self, count, include_archived=False, tags=None): + images = self.filter(post_images__thread__archived=include_archived) + if tags is not None: + images = images.filter(post_images__threads__tags__in=tags) + return images.order_by('?')[:count] class PostImage(models.Model, Viewable): @@ -51,15 +52,13 @@ class PostImage(models.Model, Viewable): Gets unique image filename """ - path = IMAGES_DIRECTORY - # TODO Use something other than random number in file name new_name = '{}{}.{}'.format( str(int(time.mktime(time.gmtime()))), str(int(random() * 1000)), filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]) - return os.path.join(path, new_name) + return os.path.join(IMAGES_DIRECTORY, new_name) width = models.IntegerField(default=0) height = models.IntegerField(default=0) @@ -81,13 +80,15 @@ class PostImage(models.Model, Viewable): """ if not self.pk and self.image: - self.hash = PostImage.objects.get_hash(self.image) + self.hash = utils.get_file_hash(self.image) super(PostImage, self).save(*args, **kwargs) def __str__(self): return self.image.url def get_view(self): + metadata = '{}, {}'.format(self.image.name.split('.')[-1], + filesizeformat(self.image.size)) return '
' \ '' \ '' \ '' \ + '' \ '
'\ .format(CSS_CLASS_IMAGE, CSS_CLASS_THUMB, self.image.url_200x150, str(self.hash), str(self.pre_width), - str(self.pre_height), str(self.width), str(self.height), full=self.image.url) + str(self.pre_height), str(self.width), str(self.height), + full=self.image.url, image_meta=metadata) + + def get_random_associated_post(self): + posts = boards.models.Post.objects.filter(images__in=[self]) + return posts.order_by('?').first() diff --git a/boards/models/post/__init__.py b/boards/models/post/__init__.py --- a/boards/models/post/__init__.py +++ b/boards/models/post/__init__.py @@ -1,3 +1,5 @@ +from datetime import datetime, timedelta, date +from datetime import time as dtime import logging import re import uuid @@ -6,15 +8,14 @@ from django.core.exceptions import Objec from django.core.urlresolvers import reverse from django.db import models from django.db.models import TextField, QuerySet - from django.template.loader import render_to_string - from django.utils import timezone +from boards.abstracts.tripcode import Tripcode from boards.mdx_neboard import Parser from boards.models import KeyPair, GlobalId from boards import settings -from boards.models import PostImage +from boards.models import PostImage, Attachment from boards.models.base import Viewable from boards.models.post.export import get_exporter, DIFF_TYPE_JSON from boards.models.post.manager import PostManager @@ -61,8 +62,6 @@ POST_VIEW_PARAMS = ( 'mode_tree', ) -REFMAP_STR = '>>{}' - class Post(models.Model, Viewable): """A post is a message.""" @@ -79,7 +78,9 @@ class Post(models.Model, Viewable): _text_rendered = TextField(blank=True, null=True, editable=False) images = models.ManyToManyField(PostImage, null=True, blank=True, - related_name='ip+', db_index=True) + related_name='post_images', db_index=True) + attachments = models.ManyToManyField(Attachment, null=True, blank=True, + related_name='attachment_posts') poster_ip = models.GenericIPAddressField() @@ -101,6 +102,8 @@ class Post(models.Model, Viewable): # server, this indicates the server. global_id = models.OneToOneField('GlobalId', null=True, blank=True) + tripcode = models.CharField(max_length=50, null=True) + def __str__(self): return 'P#{}/{}'.format(self.id, self.title) @@ -126,7 +129,7 @@ class Post(models.Model, Viewable): the server from recalculating the map on every post show. """ - post_urls = [REFMAP_STR.format(refpost.get_absolute_url(), refpost.id) + post_urls = [refpost.get_link_view() for refpost in self.referenced_posts.all()] self.refmap = ', '.join(post_urls) @@ -209,10 +212,15 @@ class Post(models.Model, Viewable): """ for image in self.images.all(): - image_refs_count = Post.objects.filter(images__in=[image]).count() + image_refs_count = image.post_images.count() if image_refs_count == 1: image.delete() + for attachment in self.attachments.all(): + attachment_refs_count = attachment.attachment_posts.count() + if attachment_refs_count == 1: + attachment.delete() + if self.global_id: self.global_id.delete() @@ -399,3 +407,19 @@ class Post(models.Model, Viewable): thread.last_edit_time = self.last_edit_time thread.save(update_fields=['last_edit_time', 'bumpable']) self.threads.add(opening_post.get_thread()) + + def get_tripcode(self): + if self.tripcode: + return Tripcode(self.tripcode) + + def get_link_view(self): + """ + Gets view of a reflink to the post. + """ + + result = '>>{}'.format(self.get_absolute_url(), + self.id) + if self.is_opening(): + result = '{}'.format(result) + + return result diff --git a/boards/models/post/manager.py b/boards/models/post/manager.py --- a/boards/models/post/manager.py +++ b/boards/models/post/manager.py @@ -5,7 +5,7 @@ from django.db import models, transactio from django.utils import timezone from boards import utils from boards.mdx_neboard import Parser -from boards.models import PostImage +from boards.models import PostImage, Attachment import boards.models __author__ = 'vurdalak' @@ -14,11 +14,19 @@ import boards.models NO_IP = '0.0.0.0' POSTS_PER_DAY_RANGE = 7 +IMAGE_TYPES = ( + 'jpeg', + 'jpg', + 'png', + 'bmp', + 'gif', +) + class PostManager(models.Manager): @transaction.atomic - def create_post(self, title: str, text: str, image=None, thread=None, - ip=NO_IP, tags: list=None, opening_posts: list=None): + def create_post(self, title: str, text: str, file=None, thread=None, + ip=NO_IP, tags: list=None, opening_posts: list=None, tripcode=None): """ Creates new post """ @@ -50,49 +58,33 @@ class PostManager(models.Manager): pub_time=posting_time, poster_ip=ip, thread=thread, - last_edit_time=posting_time) + last_edit_time=posting_time, + tripcode=tripcode) post.threads.add(thread) - post.set_global_id() - logger = logging.getLogger('boards.post.create') logger.info('Created post {} by {}'.format(post, post.poster_ip)) - if image: - post.images.add(PostImage.objects.create_with_hash(image)) - - if not new_thread: - thread.last_edit_time = posting_time - thread.bump() - thread.save() + # TODO Move this to other place + if file: + file_type = file.name.split('.')[-1].lower() + if file_type in IMAGE_TYPES: + post.images.add(PostImage.objects.create_with_hash(file)) + else: + post.attachments.add(Attachment.objects.create_with_hash(file)) post.build_url() post.connect_replies() post.connect_threads(opening_posts) post.connect_notifications() - - return post + post.set_global_id() - @transaction.atomic - def import_post(self, title: str, text: str, pub_time: str, global_id, - opening_post=None, tags=list()): - if opening_post is None: - thread = boards.models.thread.Thread.objects.create( - bump_time=pub_time, last_edit_time=pub_time) - list(map(thread.tags.add, tags)) - else: - thread = opening_post.get_thread() - - post = self.create(title=title, text=text, - pub_time=pub_time, - poster_ip=NO_IP, - last_edit_time=pub_time, - thread_id=thread.id, global_id=global_id) - - post.build_url() - post.connect_replies() - post.connect_notifications() + # Thread needs to be bumped only when the post is already created + if not new_thread: + thread.last_edit_time = posting_time + thread.bump() + thread.save() return post @@ -126,3 +118,23 @@ class PostManager(models.Manager): ppd = posts_per_period / POSTS_PER_DAY_RANGE return ppd + + @transaction.atomic + def import_post(self, title: str, text: str, pub_time: str, global_id, + opening_post=None, tags=list()): + if opening_post is None: + thread = boards.models.thread.Thread.objects.create( + bump_time=pub_time, last_edit_time=pub_time) + list(map(thread.tags.add, tags)) + else: + thread = opening_post.get_thread() + + post = self.create(title=title, text=text, + pub_time=pub_time, + poster_ip=NO_IP, + last_edit_time=pub_time, + thread_id=thread.id, global_id=global_id) + + post.build_url() + post.connect_replies() + post.connect_notifications() diff --git a/boards/models/tag.py b/boards/models/tag.py --- a/boards/models/tag.py +++ b/boards/models/tag.py @@ -5,9 +5,12 @@ from django.core.urlresolvers import rev from boards.models.base import Viewable from boards.utils import cached_result +import boards + +__author__ = 'neko259' -__author__ = 'neko259' +RELATED_TAGS_COUNT = 5 class TagManager(models.Manager): @@ -17,7 +20,7 @@ class TagManager(models.Manager): Gets tags that have non-archived threads. """ - return self.annotate(num_threads=Count('thread')).filter(num_threads__gt=0)\ + return self.annotate(num_threads=Count('thread_tags')).filter(num_threads__gt=0)\ .order_by('-required', 'name') def get_tag_url_list(self, tags: list) -> str: @@ -42,6 +45,7 @@ class Tag(models.Model, Viewable): name = models.CharField(max_length=100, db_index=True, unique=True) required = models.BooleanField(default=False, db_index=True) + description = models.TextField(blank=True) def __str__(self): return self.name @@ -53,14 +57,20 @@ class Tag(models.Model, Viewable): return self.get_thread_count() == 0 - def get_thread_count(self) -> int: - return self.get_threads().count() + def get_thread_count(self, archived=None) -> int: + threads = self.get_threads() + if archived is not None: + threads = threads.filter(archived=archived) + return threads.count() + + def get_active_thread_count(self) -> int: + return self.get_thread_count(archived=False) def get_absolute_url(self): return reverse('tag', kwargs={'tag_name': self.name}) def get_threads(self): - return self.thread_set.order_by('-bump_time') + return self.thread_tags.order_by('-bump_time') def is_required(self): return self.required @@ -80,3 +90,20 @@ class Tag(models.Model, Viewable): @cached_result() def get_post_count(self): return self.get_threads().aggregate(num_posts=Count('post'))['num_posts'] + + def get_description(self): + return self.description + + def get_random_image_post(self, archived=False): + posts = boards.models.Post.objects.annotate(images_count=Count( + 'images')).filter(images_count__gt=0, threads__tags__in=[self]) + if archived is not None: + posts = posts.filter(thread__archived=archived) + return posts.order_by('?').first() + + def get_first_letter(self): + return self.name and self.name[0] or '' + + def get_related_tags(self): + return set(Tag.objects.filter(thread_tags__in=self.get_threads()).exclude( + id=self.id).order_by('?')[:RELATED_TAGS_COUNT]) diff --git a/boards/models/thread.py b/boards/models/thread.py --- a/boards/models/thread.py +++ b/boards/models/thread.py @@ -65,7 +65,7 @@ class Thread(models.Model): class Meta: app_label = 'boards' - tags = models.ManyToManyField('Tag') + tags = models.ManyToManyField('Tag', related_name='thread_tags') bump_time = models.DateTimeField(db_index=True) last_edit_time = models.DateTimeField() archived = models.BooleanField(default=False) @@ -225,3 +225,6 @@ class Thread(models.Model): def get_absolute_url(self): return self.get_opening_post().get_absolute_url() + + def get_required_tags(self): + return self.get_tags().filter(required=True) 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 @@ -98,8 +98,39 @@ textarea, input { .post-image-full { width: 100%; + height: auto; } #preview-text { display: none; } + +.random-images-table { + text-align: center; + width: 100%; +} + +.random-images-table > div { + margin-left: auto; + margin-right: auto; +} + +.tag-image, .tag-text-data { + display: inline-block; +} + +.tag-text-data > h2 { + margin: 0; +} + +.tag-image { + margin-right: 5px; +} + +.reply-to-message { + display: none; +} + +.tripcode { + padding: 2px; +} \ 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 @@ -167,7 +167,7 @@ p, .br { display: table-cell; } -.post-form input:not([name="image"]), .post-form textarea, .post-form select { +.post-form input:not([name="image"]):not([type="checkbox"]):not([type="submit"]), .post-form textarea, .post-form select { background: #333; color: #fff; border: solid 1px; @@ -192,17 +192,22 @@ p, .br { margin-bottom: 0.5ex; } -.post-form input[type="submit"], input[type="submit"] { +input[type="submit"], button { background: #222; border: solid 2px #fff; color: #fff; padding: 0.5ex; + margin-right: 0.5ex; } input[type="submit"]:hover { background: #060; } +.form-submit > button:hover { + background: #006; +} + blockquote { border-left: solid 2px; padding-left: 5px; @@ -231,12 +236,12 @@ blockquote { text-decoration: none; } -.dead_post { - border-left: solid 5px #982C2C; +.dead_post > .post-info { + font-style: italic; } -.archive_post { - border-left: solid 5px #B7B7B7; +.archive_post > .post-info { + text-decoration: line-through; } .mark_btn { @@ -354,8 +359,6 @@ li { .moderator_info { color: #e99d41; - float: right; - font-weight: bold; opacity: 0.4; } @@ -437,7 +440,6 @@ li { .gallery_image { border: solid 1px; - padding: 0.5ex; margin: 0.5ex; text-align: center; } @@ -528,7 +530,7 @@ ul { } .highlight { - background-color: #222; + background: #222; } .post-button-form > button:hover { @@ -536,10 +538,9 @@ ul { } .tree_reply > .post { - margin-left: 1ex; margin-top: 1ex; border-left: solid 1px #777; - border-right: solid 1px #777; + padding-right: 0; } #preview-text { @@ -548,8 +549,11 @@ ul { padding: 1ex; } -button { - border: 1px solid white; - margin-bottom: .5ex; - margin-top: .5ex; +.image-metadata { + font-style: italic; + font-size: 0.9em; } + +.tripcode { + color: white; +} diff --git a/boards/static/css/pg/base_page.css b/boards/static/css/pg/base_page.css --- a/boards/static/css/pg/base_page.css +++ b/boards/static/css/pg/base_page.css @@ -376,3 +376,8 @@ input[type="submit"]:hover { margin: 1ex 0 1ex 0; padding: 1ex; } + +.image-metadata { + font-style: italic; + font-size: 0.9em; +} \ No newline at end of file diff --git a/boards/static/css/sw/base_page.css b/boards/static/css/sw/base_page.css --- a/boards/static/css/sw/base_page.css +++ b/boards/static/css/sw/base_page.css @@ -407,3 +407,12 @@ li { margin: 1ex 0 1ex 0; padding: 1ex; } + +.image-metadata { + font-style: italic; + font-size: 0.9em; +} + +audio { + margin-top: 1em; +} diff --git a/boards/static/images/file.png b/boards/static/images/file.png new file mode 100644 index 0000000000000000000000000000000000000000..82bdefe6ecf5166733a158ee930526796d87207a GIT binary patch literal 2307 zc$@(P3HSeR|L-E`YU zB{m5Bz+-z%jKd-ugdE9gR7H){huX@mRFw|KX263P9>!3axpN=gFfJbEUeC)D-*bLH z$s25hIrsj~_x;Z~_l^>1i1X*qe|KPD;E#iYgMGg5Ga$QT`@X*}B7cg-V)K?|T^}AE z{%_R&6BgHTcXGMhYa1II|8yLukI`tDq*Ce|N~w=L&->JIoM(%RiyuYpKcTpe;mpm= zeRFwv`CpFX#Eu?4Do2hS5h_UQxUM^U^5n^1P(d_ca&q$cty{NlxUL&xG}^=WeQ{k^ zMC9iS3kx43NW{rzv#%G6#U;ma7>o|c;d$Q6fArghg@vOC5>c*Rz54C7wY7h|nDL;q za#Z;??%hy1pfNnp z3zI`c)p-}i6a_wP=Xqk<7-}!b<;$0Uu)e$+O* za~Lhi^z`&QD=RC1K#uk+hpD+s+mJpwIyzh^6u#)>tXB=_CfzIz96F?eM2(r5nRiR2 z(ibLQSmZ#Ey5w@X_io?5{UvgcIS}O4w(7*{>gw;gu0 zorP{!j$_A;Nq>L8HWz%Cw;e(h1vz)_+^IsLaIISxwzs!eGMS7VK73d#OP|9ACO*$$ zgdl^1gVzQJ2lsqtO{G$jPN&7PEbV4l<2t1nR)yLj-{k zImjJk5Ux}WN-6O?j}H%aBr~_xMfC>`QTCpJr2tsQG2tsQG2tsQGW`^`` z?m@622$2({TX>$QU2CP3YG}f#{W&HH1gTp+bfQwJ$mZtej*U*svgFXALy|}&w*PEv zYfGLzd$vQ0cswqdOs1}2WDo<fEWYm>-EL7qMBnGj+nxPQ zuO0~UqQxMP0)iM4%z+Hj(O|A~_3(W^uuW_d3Q91w(H4hYg5IzD3#g!GQ}vehTzOwItio#qW2AElJ^_V$MUOu1YRJ@gs%)kAGp zv0T?>S+`9Z5yWvEtxrW;x?Wgb9SR5%03Ub*WDo>tsFJojuI*%qz21nVg0ujiMEn1n zGnq`*{azxGka(PT1{xRAecva0yu^V62gJ7R9UG;wSWHr>6qAUFg0N%$D#>Iru#$j5 zf>f&qXRgAtMziCyTZYiR5@{3CtB=vrI_#{dPoF-m+j>iND$xmIgl7qMOyhDXp})rKakkU zxK$?=%d+;gRbr#?Ylh?Otyhq0NRP#0d-m_gVzJ0iUXd1rK018%?&;~N`@O!tz8#|M z#oI?L%aU|DO^el~nXuyfexmjUVC5>DbUH1`Wb&1L5X;&*FZTEMcl>p%mSydhbIJ(P zx!LfRh#p>^6T1=36W-&4AQx*@d?#KJ3d8_4srVQ>Hm11 z_bU-rt~6Od_;_SwWHl1`H%bFLJv}{JE|-6XAdzNfWMt%*QTb&fx1D<7!i7^Ja*g{e zlAJnw_U!d2{lbwvZ!k7CmUdnDL#5P@ecyjuM2?6^n)`2G@mxgiipWwF0xPIo$ dnaG95{{g(WC2_(+eVhOQ002ovPDHLkV1k?cDs}(> 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 @@ -42,12 +42,24 @@ SimpleImageViewer.prototype.view = funct // When we first enlarge an image, a full image needs to be created if (images.length == 1) { + var thumb = images.first(); + + var width = thumb.attr('data-width'); + var height = thumb.attr('data-height'); + + if (width == null || height == null) { + width = '100%'; + height = '100%'; + } + var parent = images.first().parent(); var link = parent.attr('href'); var fullImg = $('') .addClass(FULL_IMG_CLASS) - .attr('src', link); + .attr('src', link) + .attr('width', width) + .attr('height', height); parent.append(fullImg); } 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 @@ -24,6 +24,9 @@ */ var CLOSE_BUTTON = '#form-close-button'; +var REPLY_TO_MSG = '.reply-to-message'; +var REPLY_TO_MSG_ID = '#reply-to-message-id'; + var $html = $("html, body"); function moveCaretToEnd(el) { @@ -46,6 +49,7 @@ function resetFormPosition() { form.insertAfter($('.thread')); $(CLOSE_BUTTON).hide(); + $(REPLY_TO_MSG).hide(); } function showFormAfter(blockToInsertAfter) { @@ -54,6 +58,8 @@ function showFormAfter(blockToInsertAfte $(CLOSE_BUTTON).show(); form.show(); + $(REPLY_TO_MSG_ID).text(blockToInsertAfter.attr('id')); + $(REPLY_TO_MSG).show(); } function addQuickReply(postId) { diff --git a/boards/static/js/thread_update.js b/boards/static/js/thread_update.js --- a/boards/static/js/thread_update.js +++ b/boards/static/js/thread_update.js @@ -30,6 +30,14 @@ var POST_UPDATED = 1; var JS_AUTOUPDATE_PERIOD = 20000; +var ALLOWED_FOR_PARTIAL_UPDATE = [ + 'refmap', + 'post-info' +]; + +var ATTR_CLASS = 'class'; +var ATTR_UID = 'data-uid'; + var wsUser = ''; var unreadPosts = 0; @@ -165,7 +173,8 @@ function updatePost(postHtml) { var type; if (existingPosts.size() > 0) { - existingPosts.replaceWith(post); + replacePartial(existingPosts.first(), post, false); + post = existingPosts.first(); type = POST_UPDATED; } else { @@ -362,6 +371,64 @@ function processNewPost(post) { blink(post); } +function replacePartial(oldNode, newNode, recursive) { + if (!equalNodes(oldNode, newNode)) { + // Update parent node attributes + updateNodeAttr(oldNode, newNode, ATTR_CLASS); + updateNodeAttr(oldNode, newNode, ATTR_UID); + + // Replace children + var children = oldNode.children(); + if (children.length == 0) { + console.log(oldContent); + console.log(newContent) + + oldNode.replaceWith(newNode); + } else { + var newChildren = newNode.children(); + newChildren.each(function(i) { + var newChild = newChildren.eq(i); + var newChildClass = newChild.attr(ATTR_CLASS); + + // Update only certain allowed blocks (e.g. not images) + if (ALLOWED_FOR_PARTIAL_UPDATE.indexOf(newChildClass) > -1) { + var oldChild = oldNode.children('.' + newChildClass); + + if (oldChild.length == 0) { + oldNode.append(newChild); + } else { + if (!equalNodes(oldChild, newChild)) { + if (recursive) { + replacePartial(oldChild, newChild, false); + } else { + oldChild.replaceWith(newChild); + } + } + } + } + }); + } + } +} + +/** + * Compare nodes by content + */ +function equalNodes(node1, node2) { + return node1[0].outerHTML == node2[0].outerHTML; +} + +/** + * Update attribute of a node if it has changed + */ +function updateNodeAttr(oldNode, newNode, attrName) { + var oldAttr = oldNode.attr(attrName); + var newAttr = newNode.attr(attrName); + if (oldAttr != newAttr) { + oldNode.attr(attrName, newAttr); + }; +} + $(document).ready(function(){ if (initAutoupdate()) { // Post form data over AJAX @@ -386,6 +453,4 @@ function processNewPost(post) { resetForm(form); } } - - $('#autoupdate').click(getThreadDiff); }); diff --git a/boards/templates/boards/all_threads.html b/boards/templates/boards/all_threads.html --- a/boards/templates/boards/all_threads.html +++ b/boards/templates/boards/all_threads.html @@ -38,29 +38,49 @@ {% if tag %}
-

-
- {% if is_favorite %} - - {% else %} - + {% if random_image_post %} +
+ {% with image=random_image_post.images.first %} + + {% endwith %} +
+ {% endif %} +
+

+ + {% if is_favorite %} + + {% else %} + + {% endif %} + +
+ {% if is_hidden %} + + {% else %} + + {% endif %} +
+ {{ tag.get_view|safe }} + {% if moderator %} + | {% trans 'Edit tag' %} {% endif %} - -
- {% if is_hidden %} - - {% else %} - - {% endif %} -
- {% autoescape off %} - {{ tag.get_view }} - {% endautoescape %} - {% if moderator %} - [{% trans 'Edit tag' %}] +

+ {% if tag.get_description %} +

{{ tag.get_description|safe }}

{% endif %} -

-

{% blocktrans with thread_count=tag.get_thread_count post_count=tag.get_post_count %}This tag has {{ thread_count }} threads and {{ post_count }} posts.{% endblocktrans %}

+

{% blocktrans with active_thread_count=tag.get_active_thread_count thread_count=tag.get_thread_count post_count=tag.get_post_count %}This tag has {{ thread_count }} threads ({{ active_thread_count}} active) and {{ post_count }} posts.{% endblocktrans %}

+ {% if related_tags %} +

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

+ {% endif %} +
{% endif %} @@ -116,13 +136,13 @@ {{ form.as_div }}
+
{% trans 'Tags must be delimited by spaces. Text or image is required.' %}
-
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 @@ -37,8 +37,9 @@ {% trans 'Add tags' %} → {% endif %} {% trans "tags" %}, - {% trans 'search' %}, - {% trans 'feed' %} + {% trans 'search' %}, + {% trans 'feed' %}, + {% trans 'random' %} {% if username %} diff --git a/boards/templates/boards/feed.html b/boards/templates/boards/feed.html --- a/boards/templates/boards/feed.html +++ b/boards/templates/boards/feed.html @@ -61,7 +61,7 @@ {% ifequal page current_page.number %} class="current_page" {% endifequal %} - href="{% url "feed" %}?page={{ page }}">{{ page }} + href="{% url "feed" %}?page={{ page }}{{ additional_attrs }}">{{ page }} {% if not forloop.last %},{% endif %} {% endfor %} {% endwith %} 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 @@ -5,9 +5,16 @@
+ {% if post.is_referenced %} + {% if mode_tree %} +
+ {% for refpost in post.get_referenced_posts %} + {% post_view refpost mode_tree=True %} + {% endfor %} +
+ {% else %} +
+ {% trans "Replies" %}: {{ post.refmap|safe }} +
{% endif %} -
+ {% endif %} {% comment %} Thread metadata: counters, tags etc {% endcomment %} @@ -98,9 +106,7 @@ {{ thread.get_images_count }} {% trans 'images' %}. {% endif %} - {% autoescape off %} - {{ thread.get_tag_url_list }} - {% endautoescape %} + {{ thread.get_tag_url_list|safe }} {% endif %} diff --git a/boards/templates/boards/random.html b/boards/templates/boards/random.html new file mode 100644 --- /dev/null +++ b/boards/templates/boards/random.html @@ -0,0 +1,37 @@ +{% extends "boards/base.html" %} + +{% load i18n %} + +{% block head %} + {% trans 'Random images' %} - {{ site_name }} +{% endblock %} + +{% block content %} + + {% if images %} +
+
+ {% for image in images %} + + {% if forloop.counter|divisibleby:"3" %} +
+
+ {% endif %} + {% endfor %} +
+
+ {% endif %} + +{% endblock %} diff --git a/boards/templates/boards/settings.html b/boards/templates/boards/settings.html --- a/boards/templates/boards/settings.html +++ b/boards/templates/boards/settings.html @@ -9,7 +9,6 @@ {% endblock %} {% block content %} -

{% if moderator %} @@ -19,9 +18,7 @@ {% if hidden_tags %}

{% trans 'Hidden tags:' %} {% for tag in hidden_tags %} - {% autoescape off %} - {{ tag.get_view }} - {% endautoescape %} + {{ tag.get_view|safe }} {% endfor %}

{% else %} diff --git a/boards/templates/boards/tag.html b/boards/templates/boards/tag.html --- a/boards/templates/boards/tag.html +++ b/boards/templates/boards/tag.html @@ -1,5 +1,3 @@
- {% autoescape off %} - {{ tag.get_view }} - {% endautoescape %} + {{ tag.get_view|safe }}
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 @@ -8,20 +8,39 @@ {% block content %} +{% regroup section_tags by get_first_letter as section_tag_list %} +{% regroup all_tags by get_first_letter as other_tag_list %} +
- {% if all_tags %} - {% for tag in all_tags %} -
+ {% if section_tags %} +
+ {% trans 'Sections:' %} + {% for letter in section_tag_list %} +
({{ letter.grouper|upper }}) + {% for tag in letter.list %} {% autoescape off %} - {{ tag.get_view }} + {{ tag.get_view }}{% if not forloop.last %},{% endif %} {% endautoescape %} -
+ {% endfor %} {% endfor %} - {% else %} - {% trans 'No tags found.' %} +
{% endif %} + {% if all_tags %} +
+ {% trans 'Other tags:' %} + {% for letter in other_tag_list %} +
({{ letter.grouper|upper }}) + {% for tag in letter.list %} + {% autoescape off %} + {{ tag.get_view }}{% if not forloop.last %},{% endif %} + {% endautoescape %} + {% endfor %} + {% endfor %} +
+ {% endif %} + {% if query %} - + {% 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 @@ -31,9 +31,6 @@ data-ws-host="{{ ws_host }}" data-ws-port="{{ ws_port }}"> - {% block thread_meta_panel %} - {% endblock %} - {{ thread.get_reply_count }}{% if thread.has_post_limit %}/{{ thread.max_posts }}{% endif %} {% trans 'messages' %}, {{ thread.get_images_count }} {% trans 'images' %}. {% trans 'Last update: ' %} diff --git a/boards/templates/boards/thread_normal.html b/boards/templates/boards/thread_normal.html --- a/boards/templates/boards/thread_normal.html +++ b/boards/templates/boards/thread_normal.html @@ -28,7 +28,7 @@ {% if not thread.archived %}
-
{% trans "Reply to thread" %} #{{ opening_post.id }}
+
{% trans "Reply to thread" %} #{{ opening_post.id }} {% trans "to message " %} #
{% csrf_token %} @@ -36,10 +36,10 @@ {{ form.as_div }}
+
-
@@ -55,7 +55,3 @@ {% endblock %} - -{% block thread_meta_panel %} - -{% endblock %} diff --git a/boards/urls.py b/boards/urls.py --- a/boards/urls.py +++ b/boards/urls.py @@ -11,6 +11,7 @@ from boards.views.search import BoardSea from boards.views.static import StaticPageView from boards.views.preview import PostPreviewView from boards.views.sync import get_post_sync_data, response_get +from boards.views.random import RandomImageView js_info_dict = { @@ -43,6 +44,8 @@ urlpatterns = patterns('', url(r'^staticpage/(?P\w+)/$', StaticPageView.as_view(), name='staticpage'), + url(r'^random/$', RandomImageView.as_view(), name='random'), + # RSS feeds url(r'^rss/$', AllThreadsFeed()), url(r'^page/(?P\d+)/rss/$', AllThreadsFeed()), 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. """ +import hashlib import time import hmac @@ -81,4 +82,11 @@ def is_moderator(request): except AttributeError: moderate = False - return moderate \ No newline at end of file + return moderate + + +def get_file_hash(file) -> str: + md5 = hashlib.md5() + for chunk in file.chunks(): + md5.update(chunk) + return md5.hexdigest() diff --git a/boards/views/all_tags.py b/boards/views/all_tags.py --- a/boards/views/all_tags.py +++ b/boards/views/all_tags.py @@ -4,6 +4,7 @@ from boards.views.base import BaseBoardV from boards.models.tag import Tag +PARAM_SECTION_TAGS = 'section_tags' PARAM_TAGS = 'all_tags' PARAM_QUERY = 'query' @@ -13,10 +14,10 @@ class AllTagsView(BaseBoardView): def get(self, request, query=None): params = dict() - if query == 'required': - params[PARAM_TAGS] = Tag.objects.filter(required=True) - else: - params[PARAM_TAGS] = Tag.objects.get_not_empty_tags() + params[PARAM_SECTION_TAGS] = Tag.objects.filter(required=True) + if query != 'required': + params[PARAM_TAGS] = Tag.objects.get_not_empty_tags().filter( + required=False) params[PARAM_QUERY] = query return render(request, 'boards/tags.html', params) diff --git a/boards/views/all_threads.py b/boards/views/all_threads.py --- a/boards/views/all_threads.py +++ b/boards/views/all_threads.py @@ -139,9 +139,9 @@ class AllThreadsView(PostMixin, BaseBoar data = form.cleaned_data - title = data[FORM_TITLE] + title = form.get_title() text = data[FORM_TEXT] - image = form.get_image() + file = form.get_file() threads = data[FORM_THREADS] text = self._remove_invalid_links(text) @@ -150,8 +150,9 @@ class AllThreadsView(PostMixin, BaseBoar tags = self.parse_tags_string(tag_strings) - post = Post.objects.create_post(title=title, text=text, image=image, - ip=ip, tags=tags, opening_posts=threads) + post = Post.objects.create_post(title=title, text=text, file=file, + ip=ip, tags=tags, opening_posts=threads, + tripcode=form.get_tripcode()) # This is required to update the threads to which posts we have replied # when creating this one diff --git a/boards/views/feed.py b/boards/views/feed.py --- a/boards/views/feed.py +++ b/boards/views/feed.py @@ -1,23 +1,18 @@ from django.core.urlresolvers import reverse -from django.core.files import File -from django.core.files.temp import NamedTemporaryFile -from django.core.paginator import EmptyPage -from django.db import transaction -from django.http import Http404 -from django.shortcuts import render, redirect -import requests +from django.shortcuts import render -from boards import utils, settings from boards.abstracts.paginator import get_paginator from boards.abstracts.settingsmanager import get_settings_manager -from boards.models import Post, Thread, Ban, Tag, PostImage, Banner +from boards.models import Post from boards.views.base import BaseBoardView from boards.views.posting_mixin import PostMixin +POSTS_PER_PAGE = 10 PARAMETER_CURRENT_PAGE = 'current_page' PARAMETER_PAGINATOR = 'paginator' PARAMETER_POSTS = 'posts' +PARAMETER_ADDITONAL_ATTRS = 'additional_attrs' PARAMETER_PREV_LINK = 'prev_page_link' PARAMETER_NEXT_LINK = 'next_page_link' @@ -30,25 +25,34 @@ class FeedView(PostMixin, BaseBoardView) def get(self, request): page = request.GET.get('page', DEFAULT_PAGE) + tripcode = request.GET.get('tripcode', None) params = self.get_context_data(request=request) settings_manager = get_settings_manager(request) - paginator = get_paginator(Post.objects - .exclude(threads__tags__in=settings_manager.get_hidden_tags()) - .order_by('-pub_time') - .prefetch_related('images', 'thread', 'threads'), 10) + posts = Post.objects.exclude( + threads__tags__in=settings_manager.get_hidden_tags()).order_by( + '-pub_time').prefetch_related('images', 'thread', 'threads') + if tripcode: + posts = posts.filter(tripcode=tripcode) + + paginator = get_paginator(posts, POSTS_PER_PAGE) paginator.current_page = int(page) params[PARAMETER_POSTS] = paginator.page(page).object_list - self.get_page_context(paginator, params, page) + additional_params = dict() + if tripcode: + additional_params['tripcode'] = tripcode + params[PARAMETER_ADDITONAL_ATTRS] = '&tripcode=' + tripcode + + self.get_page_context(paginator, params, page, additional_params) return render(request, TEMPLATE, params) # TODO Dedup this into PagedMixin - def get_page_context(self, paginator, params, page): + def get_page_context(self, paginator, params, page, additional_params): """ Get pagination context variables """ @@ -59,8 +63,14 @@ class FeedView(PostMixin, BaseBoardView) if current_page.has_previous(): params[PARAMETER_PREV_LINK] = self.get_previous_page_link( current_page) + for param in additional_params.keys(): + params[PARAMETER_PREV_LINK] += '&{}={}'.format( + param, additional_params[param]) if current_page.has_next(): params[PARAMETER_NEXT_LINK] = self.get_next_page_link(current_page) + for param in additional_params.keys(): + params[PARAMETER_NEXT_LINK] += '&{}={}'.format( + param, additional_params[param]) def get_previous_page_link(self, current_page): return reverse('feed') + '?page={}'.format( diff --git a/boards/views/random.py b/boards/views/random.py new file mode 100644 --- /dev/null +++ b/boards/views/random.py @@ -0,0 +1,22 @@ +from django.shortcuts import render +from django.views.generic import View + +from boards.models import PostImage + +__author__ = 'neko259' + +TEMPLATE = 'boards/random.html' + +CONTEXT_IMAGES = 'images' + +RANDOM_POST_COUNT = 9 + + +class RandomImageView(View): + def get(self, request): + params = dict() + + params[CONTEXT_IMAGES] = PostImage.objects.get_random_images( + RANDOM_POST_COUNT) + + return render(request, TEMPLATE, params) diff --git a/boards/views/settings.py b/boards/views/settings.py --- a/boards/views/settings.py +++ b/boards/views/settings.py @@ -9,7 +9,6 @@ from boards.views.base import BaseBoardV from boards.forms import SettingsForm, PlainErrorList from boards import settings - FORM_THEME = 'theme' FORM_USERNAME = 'username' FORM_TIMEZONE = 'timezone' diff --git a/boards/views/tag_threads.py b/boards/views/tag_threads.py --- a/boards/views/tag_threads.py +++ b/boards/views/tag_threads.py @@ -3,7 +3,7 @@ from django.core.urlresolvers import rev from boards.abstracts.settingsmanager import get_settings_manager, \ SETTING_FAVORITE_TAGS, SETTING_HIDDEN_TAGS -from boards.models import Tag +from boards.models import Tag, PostImage from boards.views.all_threads import AllThreadsView, DEFAULT_PAGE from boards.views.mixins import DispatcherMixin from boards.forms import ThreadForm, PlainErrorList @@ -12,6 +12,9 @@ PARAM_HIDDEN_TAGS = 'hidden_tags' PARAM_TAG = 'tag' PARAM_IS_FAVORITE = 'is_favorite' PARAM_IS_HIDDEN = 'is_hidden' +PARAM_RANDOM_IMAGE_POST = 'random_image_post' +PARAM_RELATED_TAGS = 'related_tags' + __author__ = 'neko259' @@ -47,6 +50,9 @@ class TagView(AllThreadsView, Dispatcher params[PARAM_IS_FAVORITE] = fav_tag_names is not None and tag.name in fav_tag_names params[PARAM_IS_HIDDEN] = hidden_tag_names is not None and tag.name in hidden_tag_names + params[PARAM_RANDOM_IMAGE_POST] = tag.get_random_image_post() + params[PARAM_RELATED_TAGS] = tag.get_related_tags() + return params def get_previous_page_link(self, current_page): @@ -84,7 +90,7 @@ class TagView(AllThreadsView, Dispatcher # Ban user because he is suspected to be a bot self._ban_current_user(request) - return self.get(request, tag_name, page, form) + return self.get(request, tag_name, form) def subscribe(self, request): tag = get_object_or_404(Tag, name=self.tag_name) diff --git a/boards/views/thread/thread.py b/boards/views/thread/thread.py --- a/boards/views/thread/thread.py +++ b/boards/views/thread/thread.py @@ -1,3 +1,4 @@ +import hashlib from django.core.exceptions import ObjectDoesNotExist from django.http import Http404 from django.shortcuts import get_object_or_404, render, redirect @@ -100,18 +101,19 @@ class ThreadView(BaseBoardView, PostMixi data = form.cleaned_data - title = data[FORM_TITLE] + title = form.get_title() text = data[FORM_TEXT] - image = form.get_image() + file = form.get_file() threads = data[FORM_THREADS] text = self._remove_invalid_links(text) post_thread = opening_post.get_thread() - post = Post.objects.create_post(title=title, text=text, image=image, + post = Post.objects.create_post(title=title, text=text, file=file, thread=post_thread, ip=ip, - opening_posts=threads) + opening_posts=threads, + tripcode=form.get_tripcode()) post.notify_clients() if html_response: