diff --git a/.hgtags b/.hgtags --- a/.hgtags +++ b/.hgtags @@ -25,3 +25,4 @@ 957e2fec91468f739b0fc2b9936d564505048c68 bb91141c6ea5c822ccbe2d46c3c48bdab683b77d 2.4.0 97eb184637e5691b288eaf6b03e8971f3364c239 2.5.0 119fafc5381b933bf30d97be0b278349f6135075 2.5.1 +d528d76d3242cced614fa11bb63f3d342e4e1d09 2.5.2 diff --git a/boards/context_processors.py b/boards/context_processors.py --- a/boards/context_processors.py +++ b/boards/context_processors.py @@ -5,7 +5,7 @@ from boards.models.user import Notificat __author__ = 'neko259' from boards import settings -from boards.models import Post +from boards.models import Post, Tag CONTEXT_SITE_NAME = 'site_name' CONTEXT_VERSION = 'version' @@ -17,6 +17,7 @@ CONTEXT_TAGS = 'tags' CONTEXT_USER = 'user' CONTEXT_NEW_NOTIFICATIONS_COUNT = 'new_notifications_count' CONTEXT_USERNAME = 'username' +CONTEXT_TAGS_STR = 'tags_str' PERMISSION_MODERATE = 'moderation' @@ -49,7 +50,9 @@ def user_and_ui_processor(request): context[CONTEXT_PPD] = float(Post.objects.get_posts_per_day()) settings_manager = get_settings_manager(request) - context[CONTEXT_TAGS] = settings_manager.get_fav_tags() + fav_tags = settings_manager.get_fav_tags() + context[CONTEXT_TAGS] = fav_tags + context[CONTEXT_TAGS_STR] = Tag.objects.get_tag_url_list(fav_tags) theme = settings_manager.get_theme() context[CONTEXT_THEME] = theme context[CONTEXT_THEME_CSS] = 'css/' + theme + '/base_page.css' diff --git a/boards/default_settings.py b/boards/default_settings.py --- a/boards/default_settings.py +++ b/boards/default_settings.py @@ -1,4 +1,4 @@ -VERSION = '2.5.1 Yasako' +VERSION = '2.5.2 Yasako' SITE_NAME = 'Neboard' CACHE_TIMEOUT = 600 # Timeout for caching, if cache is used diff --git a/boards/forms.py b/boards/forms.py --- a/boards/forms.py +++ b/boards/forms.py @@ -166,7 +166,8 @@ class PostForm(NeboardForm): def clean_image(self): image = self.cleaned_data['image'] - self._validate_image(image) + if image: + self.validate_image_size(image.size) return image @@ -179,8 +180,8 @@ class PostForm(NeboardForm): if not image: raise forms.ValidationError(_('Invalid URL')) - - self._validate_image(image) + else: + self.validate_image_size(image.size) return image @@ -218,13 +219,6 @@ class PostForm(NeboardForm): error_message = _('Either text or image must be entered.') self._errors['text'] = self.error_class([error_message]) - def _validate_image(self, image): - if image: - if image.size > board_settings.MAX_IMAGE_SIZE: - raise forms.ValidationError( - _('Image must be less than %s bytes') - % str(board_settings.MAX_IMAGE_SIZE)) - def _validate_posting_speed(self): can_post = True @@ -247,6 +241,12 @@ class PostForm(NeboardForm): if can_post: self.session[LAST_POST_TIME] = time.time() + def validate_image_size(self, size: int): + if size > board_settings.MAX_IMAGE_SIZE: + raise forms.ValidationError( + _('Image must be less than %s bytes') + % str(board_settings.MAX_IMAGE_SIZE)) + def _get_image_from_url(self, url: str) -> SimpleUploadedFile: """ Gets an image file from URL. @@ -262,11 +262,7 @@ class PostForm(NeboardForm): length_header = response_head.headers.get('content-length') if length_header: length = int(length_header) - if length > board_settings.MAX_IMAGE_SIZE: - raise forms.ValidationError( - _('Image must be less than %s bytes') - % str(board_settings.MAX_IMAGE_SIZE)) - + self.validate_image_size(length) # Get the actual content into memory response = requests.get(url, verify=False, stream=True) @@ -275,11 +271,7 @@ class PostForm(NeboardForm): content = b'' for chunk in response.iter_content(IMAGE_DOWNLOAD_CHUNK_BYTES): size += len(chunk) - if size > board_settings.MAX_IMAGE_SIZE: - # TODO Dedup this code into a method - raise forms.ValidationError( - _('Image must be less than %s bytes') - % str(board_settings.MAX_IMAGE_SIZE)) + self.validate_image_size(size) content += chunk if response.status_code == HTTP_RESULT_OK and content: diff --git a/boards/models/image.py b/boards/models/image.py --- a/boards/models/image.py +++ b/boards/models/image.py @@ -18,7 +18,30 @@ CSS_CLASS_IMAGE = 'image' CSS_CLASS_THUMB = 'thumb' +class PostImageManager(models.Manager): + def create_with_hash(self, image): + image_hash = self.get_hash(image) + existing = self.filter(hash=image_hash) + if len(existing) > 0: + post_image = existing[0] + else: + post_image = PostImage.objects.create(image=image) + + 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() + + class PostImage(models.Model, Viewable): + objects = PostImageManager() + class Meta: app_label = 'boards' ordering = ('id',) @@ -29,10 +52,12 @@ class PostImage(models.Model, Viewable): """ path = IMAGES_DIRECTORY - new_name = str(int(time.mktime(time.gmtime()))) - new_name += str(int(random() * 1000)) - new_name += FILE_EXTENSION_DELIMITER - new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0] + + # 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) @@ -56,7 +81,7 @@ class PostImage(models.Model, Viewable): """ if not self.pk and self.image: - self.hash = PostImage.get_hash(self.image) + self.hash = PostImage.objects.get_hash(self.image) super(PostImage, self).save(*args, **kwargs) def __str__(self): @@ -78,13 +103,3 @@ class PostImage(models.Model, Viewable): self.image.url_200x150, str(self.hash), str(self.pre_width), str(self.pre_height), str(self.width), str(self.height)) - - @staticmethod - def get_hash(image): - """ - Gets hash of an image. - """ - md5 = hashlib.md5() - for chunk in image.chunks(): - md5.update(chunk) - return md5.hexdigest() diff --git a/boards/models/post.py b/boards/models/post.py --- a/boards/models/post.py +++ b/boards/models/post.py @@ -137,25 +137,15 @@ class PostManager(models.Manager): post, post.poster_ip)) if image: - # Try to find existing image. If it exists, assign it to the post - # instead of createing the new one - image_hash = PostImage.get_hash(image) - existing = PostImage.objects.filter(hash=image_hash) - if len(existing) > 0: - post_image = existing[0] - else: - post_image = PostImage.objects.create(image=image) - logger.info('Created new image #{} for post #{}'.format( - post_image.id, post.id)) - post.images.add(post_image) + post.images.add(PostImage.objects.create_with_hash(image)) list(map(thread.add_tag, tags)) if new_thread: boards.models.thread.Thread.objects.process_oldest_threads() else: + thread.last_edit_time = posting_time thread.bump() - thread.last_edit_time = posting_time thread.save() post.connect_replies() diff --git a/boards/models/tag.py b/boards/models/tag.py --- a/boards/models/tag.py +++ b/boards/models/tag.py @@ -21,6 +21,13 @@ class TagManager(models.Manager): .annotate(num_threads=Count('thread')).filter(num_threads__gt=0)\ .order_by('-required', 'name') + def get_tag_url_list(self, tags: list) -> str: + """ + Gets a comma-separated list of tag links. + """ + + return ', '.join([tag.get_view() for tag in tags]) + class Tag(models.Model, Viewable): """ diff --git a/boards/models/thread.py b/boards/models/thread.py --- a/boards/models/thread.py +++ b/boards/models/thread.py @@ -5,6 +5,7 @@ from django.utils import timezone from django.db import models from boards import settings +import boards from boards.utils import cached_result from boards.models.post import Post @@ -41,6 +42,7 @@ class ThreadManager(models.Manager): thread.archived = True thread.bumpable = False thread.last_edit_time = timezone.now() + thread.update_posts_time() thread.save(update_fields=['archived', 'last_edit_time', 'bumpable']) @@ -69,10 +71,11 @@ class Thread(models.Model): """ if self.can_bump(): - self.bump_time = timezone.now() + self.bump_time = self.last_edit_time if self.get_reply_count() >= settings.MAX_POSTS_PER_THREAD: self.bumpable = False + self.update_posts_time() logger.info('Bumped thread %d' % self.id) @@ -88,7 +91,7 @@ class Thread(models.Model): Checks if the thread can be bumped by replying to it. """ - return self.bumpable + return self.bumpable and not self.archived def get_last_replies(self): """ @@ -161,9 +164,6 @@ class Thread(models.Model): return self.get_opening_post(only_id=True).id - def __unicode__(self): - return str(self.id) - def get_pub_time(self): """ Gets opening post's pub time because thread does not have its own one. @@ -183,3 +183,9 @@ class Thread(models.Model): def __str__(self): return 'T#{}/{}'.format(self.id, self.get_opening_post_id()) + + def get_tag_url_list(self): + return boards.models.Tag.objects.get_tag_url_list(self.get_tags()) + + def update_posts_time(self): + self.post_set.update(last_edit_time=self.last_edit_time) diff --git a/boards/static/js/main.js b/boards/static/js/main.js --- a/boards/static/js/main.js +++ b/boards/static/js/main.js @@ -23,14 +23,16 @@ for the JavaScript code in this page. */ -var LOCALE = window.navigator.language; -var FORMATTER = new Intl.DateTimeFormat( - LOCALE, - { - weekday: 'short', year: 'numeric', month: 'short', day: 'numeric', - hour: 'numeric', minute: '2-digit', second: '2-digit' - } -); +if (window.Intl) { + var LOCALE = window.navigator.language; + var FORMATTER = new Intl.DateTimeFormat( + LOCALE, + { + weekday: 'short', year: 'numeric', month: 'short', day: 'numeric', + hour: 'numeric', minute: '2-digit', second: '2-digit' + } + ); +} /** * An email is a hidden file to prevent spam bots from posting. It has to be @@ -53,6 +55,10 @@ function highlightCode(node) { * Translate timestamps to local ones for all