diff --git a/boards/forms.py b/boards/forms.py --- a/boards/forms.py +++ b/boards/forms.py @@ -1,12 +1,15 @@ import re +import time +import hashlib + from captcha.fields import CaptchaField from django import forms from django.forms.util import ErrorList from django.utils.translation import ugettext_lazy as _ -import time + from boards.mdx_neboard import formatters from boards.models.post import TITLE_MAX_LENGTH -from boards.models import User +from boards.models import User, Post from neboard import settings from boards import utils import boards.settings as board_settings @@ -19,6 +22,11 @@ TEXT_PLACEHOLDER = _('''Type message her this. 2 new lines are required to start new paragraph.''') TAGS_PLACEHOLDER = _('tag1 several_words_tag') +ERROR_IMAGE_DUPLICATE = _('Such image was already posted') + +LABEL_TITLE = _('Title') +LABEL_TEXT = _('Text') + class FormatPanel(forms.Textarea): def render(self, name, value, attrs=None): @@ -74,10 +82,10 @@ class NeboardForm(forms.Form): class PostForm(NeboardForm): title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False, - label=_('Title')) + label=LABEL_TITLE) text = forms.CharField( widget=FormatPanel(attrs={ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER}), - required=False, label=_('Text')) + required=False, label=LABEL_TEXT) image = forms.ImageField(required=False, label=_('Image')) # This field is for spam prevention only @@ -114,6 +122,14 @@ class PostForm(NeboardForm): raise forms.ValidationError( _('Image must be less than %s bytes') % str(board_settings.MAX_IMAGE_SIZE)) + + md5 = hashlib.md5() + for chunk in image.chunks(): + md5.update(chunk) + image_hash = md5.hexdigest() + if Post.objects.filter(image_hash=image_hash).exists(): + raise forms.ValidationError(ERROR_IMAGE_DUPLICATE) + return image def clean(self): diff --git a/boards/locale/ru/LC_MESSAGES/django.mo b/boards/locale/ru/LC_MESSAGES/django.mo index e586eefda511b95f1abff50aa25606ed057d30f1..428cbe338a232db676d17eec97d82be114dd50b7 GIT binary patch literal 6647 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 Ol|>AlKB^K*C;kIu^Z8K# diff --git a/boards/locale/ru/LC_MESSAGES/django.po b/boards/locale/ru/LC_MESSAGES/django.po --- a/boards/locale/ru/LC_MESSAGES/django.po +++ b/boards/locale/ru/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2014-01-13 10:53+0200\n" +"POT-Creation-Date: 2014-01-15 10:46+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -34,7 +34,7 @@ msgstr "разработчик javascript" msgid "designer" msgstr "дизайнер" -#: forms.py:18 +#: forms.py:21 msgid "" "Type message here. You can reply to message >>123 like\n" " this. 2 new lines are required to start new paragraph." @@ -42,75 +42,79 @@ msgstr "" "Введите сообщение здесь. Вы можете ответить на сообщение >>123 вот так. 2 " "переноса строки обязательны для создания нового абзаца." -#: forms.py:20 +#: forms.py:23 msgid "tag1 several_words_tag" msgstr "тег1 тег_из_нескольких_слов" -#: forms.py:77 +#: forms.py:25 +msgid "Such image was already posted" +msgstr "Такое изображение уже было загружено" + +#: forms.py:27 msgid "Title" msgstr "Заголовок" -#: forms.py:80 +#: forms.py:28 msgid "Text" msgstr "Текст" -#: forms.py:81 +#: forms.py:89 msgid "Image" msgstr "Изображение" -#: forms.py:84 +#: forms.py:92 msgid "e-mail" msgstr "" -#: forms.py:95 +#: forms.py:103 #, python-format msgid "Title must have less than %s characters" msgstr "Заголовок должен иметь меньше %s символов" -#: forms.py:104 +#: forms.py:112 #, python-format msgid "Text must have less than %s characters" msgstr "Текст должен быть короче %s символов" -#: forms.py:115 +#: forms.py:123 #, python-format msgid "Image must be less than %s bytes" msgstr "Изображение должно быть менее %s байт" -#: forms.py:142 +#: forms.py:158 msgid "Either text or image must be entered." msgstr "Текст или картинка должны быть введены." -#: forms.py:155 +#: forms.py:171 #, python-format msgid "Wait %s seconds after last posting" msgstr "Подождите %s секунд после последнего постинга" -#: forms.py:171 templates/boards/tags.html:6 templates/boards/rss/post.html:10 +#: forms.py:187 templates/boards/tags.html:6 templates/boards/rss/post.html:10 msgid "Tags" msgstr "Теги" -#: forms.py:179 +#: forms.py:195 msgid "Inappropriate characters in tags." msgstr "Недопустимые символы в тегах." -#: forms.py:207 forms.py:228 +#: forms.py:223 forms.py:244 msgid "Captcha validation failed" msgstr "Проверка капчи провалена" -#: forms.py:234 +#: forms.py:250 msgid "Theme" msgstr "Тема" -#: forms.py:239 +#: forms.py:255 msgid "Enable moderation panel" msgstr "Включить панель модерации" -#: forms.py:254 +#: forms.py:270 msgid "No such user found" msgstr "Данный пользователь не найден" -#: forms.py:268 +#: forms.py:284 #, python-format msgid "Wait %s minutes after last login" msgstr "Подождите %s минут после последнего входа" diff --git a/boards/migrations/0019_auto__add_field_post_image_hash.py b/boards/migrations/0019_auto__add_field_post_image_hash.py new file mode 100644 --- /dev/null +++ b/boards/migrations/0019_auto__add_field_post_image_hash.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'Post.image_hash' + db.add_column(u'boards_post', 'image_hash', + self.gf('django.db.models.fields.CharField')(default='', max_length=36), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'Post.image_hash' + db.delete_column(u'boards_post', 'image_hash') + + + models = { + 'boards.ban': { + 'Meta': {'object_name': 'Ban'}, + 'can_read': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}), + 'reason': ('django.db.models.fields.CharField', [], {'default': "'Auto'", 'max_length': '200'}) + }, + 'boards.post': { + 'Meta': {'object_name': 'Post'}, + '_text_rendered': ('django.db.models.fields.TextField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'image': ('boards.thumbs.ImageWithThumbsField', [], {'max_length': '100', 'blank': 'True'}), + 'image_hash': ('django.db.models.fields.CharField', [], {'max_length': '36'}), + 'image_height': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'image_pre_height': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'image_pre_width': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'image_width': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}), + 'poster_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}), + 'poster_user_agent': ('django.db.models.fields.TextField', [], {}), + 'pub_time': ('django.db.models.fields.DateTimeField', [], {}), + 'referenced_posts': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'rfp+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}), + 'text': ('markupfield.fields.MarkupField', [], {'rendered_field': 'True'}), + 'text_markup_type': ('django.db.models.fields.CharField', [], {'default': "'markdown'", 'max_length': '30'}), + 'thread': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.Post']", 'null': 'True'}), + 'thread_new': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.Thread']", 'null': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.User']", 'null': 'True'}) + }, + 'boards.setting': { + 'Meta': {'object_name': 'Setting'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['boards.User']"}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'boards.tag': { + 'Meta': {'object_name': 'Tag'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'linked': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['boards.Tag']", 'null': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'tag+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Thread']"}) + }, + 'boards.thread': { + 'Meta': {'object_name': 'Thread'}, + 'archived': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'bump_time': ('django.db.models.fields.DateTimeField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}), + 'replies': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'tre+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['boards.Tag']", 'symmetrical': 'False'}) + }, + 'boards.user': { + 'Meta': {'object_name': 'User'}, + 'fav_tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['boards.Tag']", 'null': 'True', 'blank': 'True'}), + 'fav_threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'rank': ('django.db.models.fields.IntegerField', [], {}), + 'registration_time': ('django.db.models.fields.DateTimeField', [], {}), + 'user_id': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + } + } + + complete_apps = ['boards'] \ No newline at end of file diff --git a/boards/migrations/0020_image_hashes.py b/boards/migrations/0020_image_hashes.py new file mode 100644 --- /dev/null +++ b/boards/migrations/0020_image_hashes.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +import hashlib + +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +class Migration(DataMigration): + + def forwards(self, orm): + # Note: Don't use "from appname.models import ModelName". + # Use orm.ModelName to refer to models in this application, + # and orm['appname.ModelName'] for models in other applications. + for post in orm.Post.objects.filter(image_width__gt=0): + md5 = hashlib.md5() + for chunk in post.image.chunks(): + md5.update(chunk) + image_hash = md5.hexdigest() + post.image_hash = image_hash + post.save() + + def backwards(self, orm): + "Write your backwards methods here." + + models = { + 'boards.ban': { + 'Meta': {'object_name': 'Ban'}, + 'can_read': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}), + 'reason': ('django.db.models.fields.CharField', [], {'default': "'Auto'", 'max_length': '200'}) + }, + 'boards.post': { + 'Meta': {'object_name': 'Post'}, + '_text_rendered': ('django.db.models.fields.TextField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'image': ('boards.thumbs.ImageWithThumbsField', [], {'max_length': '100', 'blank': 'True'}), + 'image_hash': ('django.db.models.fields.CharField', [], {'max_length': '36'}), + 'image_height': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'image_pre_height': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'image_pre_width': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'image_width': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}), + 'poster_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}), + 'poster_user_agent': ('django.db.models.fields.TextField', [], {}), + 'pub_time': ('django.db.models.fields.DateTimeField', [], {}), + 'referenced_posts': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'rfp+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}), + 'text': ('markupfield.fields.MarkupField', [], {'rendered_field': 'True'}), + 'text_markup_type': ('django.db.models.fields.CharField', [], {'default': "'markdown'", 'max_length': '30'}), + 'thread': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.Post']", 'null': 'True'}), + 'thread_new': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.Thread']", 'null': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.User']", 'null': 'True'}) + }, + 'boards.setting': { + 'Meta': {'object_name': 'Setting'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['boards.User']"}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'boards.tag': { + 'Meta': {'object_name': 'Tag'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'linked': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['boards.Tag']", 'null': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'tag+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Thread']"}) + }, + 'boards.thread': { + 'Meta': {'object_name': 'Thread'}, + 'archived': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'bump_time': ('django.db.models.fields.DateTimeField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}), + 'replies': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'tre+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['boards.Tag']", 'symmetrical': 'False'}) + }, + 'boards.user': { + 'Meta': {'object_name': 'User'}, + 'fav_tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['boards.Tag']", 'null': 'True', 'blank': 'True'}), + 'fav_threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'rank': ('django.db.models.fields.IntegerField', [], {}), + 'registration_time': ('django.db.models.fields.DateTimeField', [], {}), + 'user_id': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + } + } + + complete_apps = ['boards'] + symmetrical = True diff --git a/boards/models/post.py b/boards/models/post.py --- a/boards/models/post.py +++ b/boards/models/post.py @@ -5,6 +5,8 @@ from random import random import time import math import re +import hashlib + from django.core.cache import cache from django.core.paginator import Paginator @@ -232,6 +234,7 @@ class Post(models.Model): height_field='image_height', preview_width_field='image_pre_width', preview_height_field='image_pre_height') + image_hash = models.CharField(max_length=36) poster_ip = models.GenericIPAddressField() poster_user_agent = models.TextField() @@ -265,6 +268,18 @@ class Post(models.Model): def is_opening(self): return self.thread_new.get_replies()[0] == self + def save(self, *args, **kwargs): + """ + Save the model and compute the image hash + """ + + if not self.pk and self.image: + md5 = hashlib.md5() + for chunk in self.image.chunks(): + md5.update(chunk) + self.image_hash = md5.hexdigest() + super(Post, self).save(*args, **kwargs) + class Thread(models.Model): diff --git a/boards/thumbs.py b/boards/thumbs.py --- a/boards/thumbs.py +++ b/boards/thumbs.py @@ -160,6 +160,9 @@ class ImageWithThumbsField(ImageField): """ + preview_width_field = None + preview_height_field = None + def __init__(self, verbose_name=None, name=None, width_field=None, height_field=None, sizes=None, preview_width_field=None, preview_height_field=None,