##// END OF EJS Templates
Removed old forms, refactored some code for forms and images. Use the same code to compute hash of an image in form and when saving image
neko259 -
r937:eaceb3a3 default
parent child Browse files
Show More
@@ -1,305 +1,246 b''
1 import re
1 import re
2 import time
2 import time
3 import hashlib
4
3
5 from django import forms
4 from django import forms
6 from django.forms.util import ErrorList
5 from django.forms.util import ErrorList
7 from django.utils.translation import ugettext_lazy as _
6 from django.utils.translation import ugettext_lazy as _
8
7
9 from boards.mdx_neboard import formatters
8 from boards.mdx_neboard import formatters
10 from boards.models.post import TITLE_MAX_LENGTH
9 from boards.models.post import TITLE_MAX_LENGTH
11 from boards.models import PostImage, Tag
10 from boards.models import PostImage, Tag
12 from neboard import settings
11 from neboard import settings
13 from boards import utils
14 import boards.settings as board_settings
12 import boards.settings as board_settings
15
13
14 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
15
16 VETERAN_POSTING_DELAY = 5
16 VETERAN_POSTING_DELAY = 5
17
17
18 ATTRIBUTE_PLACEHOLDER = 'placeholder'
18 ATTRIBUTE_PLACEHOLDER = 'placeholder'
19
19
20 LAST_POST_TIME = 'last_post_time'
20 LAST_POST_TIME = 'last_post_time'
21 LAST_LOGIN_TIME = 'last_login_time'
21 LAST_LOGIN_TIME = 'last_login_time'
22 TEXT_PLACEHOLDER = _('''Type message here. Use formatting panel for more advanced usage.''')
22 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
23 TAGS_PLACEHOLDER = _('tag1 several_words_tag')
23 TAGS_PLACEHOLDER = _('tag1 several_words_tag')
24
24
25 ERROR_IMAGE_DUPLICATE = _('Such image was already posted')
25 ERROR_IMAGE_DUPLICATE = _('Such image was already posted')
26
26
27 LABEL_TITLE = _('Title')
27 LABEL_TITLE = _('Title')
28 LABEL_TEXT = _('Text')
28 LABEL_TEXT = _('Text')
29 LABEL_TAG = _('Tag')
29 LABEL_TAG = _('Tag')
30 LABEL_SEARCH = _('Search')
30 LABEL_SEARCH = _('Search')
31
31
32 TAG_MAX_LENGTH = 20
32 TAG_MAX_LENGTH = 20
33
33
34 REGEX_TAG = r'^[\w\d]+$'
35
36
34
37 class FormatPanel(forms.Textarea):
35 class FormatPanel(forms.Textarea):
36 """
37 Panel for text formatting. Consists of buttons to add different tags to the
38 form text area.
39 """
40
38 def render(self, name, value, attrs=None):
41 def render(self, name, value, attrs=None):
39 output = '<div id="mark-panel">'
42 output = '<div id="mark-panel">'
40 for formatter in formatters:
43 for formatter in formatters:
41 output += '<span class="mark_btn"' + \
44 output += '<span class="mark_btn"' + \
42 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
45 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
43 '\', \'' + formatter.format_right + '\')">' + \
46 '\', \'' + formatter.format_right + '\')">' + \
44 formatter.preview_left + formatter.name + \
47 formatter.preview_left + formatter.name + \
45 formatter.preview_right + '</span>'
48 formatter.preview_right + '</span>'
46
49
47 output += '</div>'
50 output += '</div>'
48 output += super(FormatPanel, self).render(name, value, attrs=None)
51 output += super(FormatPanel, self).render(name, value, attrs=None)
49
52
50 return output
53 return output
51
54
52
55
53 class PlainErrorList(ErrorList):
56 class PlainErrorList(ErrorList):
54 def __unicode__(self):
57 def __unicode__(self):
55 return self.as_text()
58 return self.as_text()
56
59
57 def as_text(self):
60 def as_text(self):
58 return ''.join(['(!) %s ' % e for e in self])
61 return ''.join(['(!) %s ' % e for e in self])
59
62
60
63
61 class NeboardForm(forms.Form):
64 class NeboardForm(forms.Form):
65 """
66 Form with neboard-specific formatting.
67 """
62
68
63 def as_div(self):
69 def as_div(self):
64 """
70 """
65 Returns this form rendered as HTML <as_div>s.
71 Returns this form rendered as HTML <as_div>s.
66 """
72 """
67
73
68 return self._html_output(
74 return self._html_output(
69 # TODO Do not show hidden rows in the list here
75 # TODO Do not show hidden rows in the list here
70 normal_row='<div class="form-row"><div class="form-label">'
76 normal_row='<div class="form-row"><div class="form-label">'
71 '%(label)s'
77 '%(label)s'
72 '</div></div>'
78 '</div></div>'
73 '<div class="form-row"><div class="form-input">'
79 '<div class="form-row"><div class="form-input">'
74 '%(field)s'
80 '%(field)s'
75 '</div></div>'
81 '</div></div>'
76 '<div class="form-row">'
82 '<div class="form-row">'
77 '%(help_text)s'
83 '%(help_text)s'
78 '</div>',
84 '</div>',
79 error_row='<div class="form-row">'
85 error_row='<div class="form-row">'
80 '<div class="form-label"></div>'
86 '<div class="form-label"></div>'
81 '<div class="form-errors">%s</div>'
87 '<div class="form-errors">%s</div>'
82 '</div>',
88 '</div>',
83 row_ender='</div>',
89 row_ender='</div>',
84 help_text_html='%s',
90 help_text_html='%s',
85 errors_on_separate_row=True)
91 errors_on_separate_row=True)
86
92
87 def as_json_errors(self):
93 def as_json_errors(self):
88 errors = []
94 errors = []
89
95
90 for name, field in list(self.fields.items()):
96 for name, field in list(self.fields.items()):
91 if self[name].errors:
97 if self[name].errors:
92 errors.append({
98 errors.append({
93 'field': name,
99 'field': name,
94 'errors': self[name].errors.as_text(),
100 'errors': self[name].errors.as_text(),
95 })
101 })
96
102
97 return errors
103 return errors
98
104
99
105
100 class PostForm(NeboardForm):
106 class PostForm(NeboardForm):
101
107
102 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
108 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
103 label=LABEL_TITLE)
109 label=LABEL_TITLE)
104 text = forms.CharField(
110 text = forms.CharField(
105 widget=FormatPanel(attrs={ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER}),
111 widget=FormatPanel(attrs={ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER}),
106 required=False, label=LABEL_TEXT)
112 required=False, label=LABEL_TEXT)
107 image = forms.ImageField(required=False, label=_('Image'),
113 image = forms.ImageField(required=False, label=_('Image'),
108 widget=forms.ClearableFileInput(
114 widget=forms.ClearableFileInput(
109 attrs={'accept': 'image/*'}))
115 attrs={'accept': 'image/*'}))
110
116
111 # This field is for spam prevention only
117 # This field is for spam prevention only
112 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
118 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
113 widget=forms.TextInput(attrs={
119 widget=forms.TextInput(attrs={
114 'class': 'form-email'}))
120 'class': 'form-email'}))
115
121
116 session = None
122 session = None
117 need_to_ban = False
123 need_to_ban = False
118
124
119 def clean_title(self):
125 def clean_title(self):
120 title = self.cleaned_data['title']
126 title = self.cleaned_data['title']
121 if title:
127 if title:
122 if len(title) > TITLE_MAX_LENGTH:
128 if len(title) > TITLE_MAX_LENGTH:
123 raise forms.ValidationError(_('Title must have less than %s '
129 raise forms.ValidationError(_('Title must have less than %s '
124 'characters') %
130 'characters') %
125 str(TITLE_MAX_LENGTH))
131 str(TITLE_MAX_LENGTH))
126 return title
132 return title
127
133
128 def clean_text(self):
134 def clean_text(self):
129 text = self.cleaned_data['text'].strip()
135 text = self.cleaned_data['text'].strip()
130 if text:
136 if text:
131 if len(text) > board_settings.MAX_TEXT_LENGTH:
137 if len(text) > board_settings.MAX_TEXT_LENGTH:
132 raise forms.ValidationError(_('Text must have less than %s '
138 raise forms.ValidationError(_('Text must have less than %s '
133 'characters') %
139 'characters') %
134 str(board_settings
140 str(board_settings
135 .MAX_TEXT_LENGTH))
141 .MAX_TEXT_LENGTH))
136 return text
142 return text
137
143
138 def clean_image(self):
144 def clean_image(self):
139 image = self.cleaned_data['image']
145 image = self.cleaned_data['image']
140 if image:
146 if image:
141 if image.size > board_settings.MAX_IMAGE_SIZE:
147 if image.size > board_settings.MAX_IMAGE_SIZE:
142 raise forms.ValidationError(
148 raise forms.ValidationError(
143 _('Image must be less than %s bytes')
149 _('Image must be less than %s bytes')
144 % str(board_settings.MAX_IMAGE_SIZE))
150 % str(board_settings.MAX_IMAGE_SIZE))
145
151
146 md5 = hashlib.md5()
152 image_hash = PostImage.get_hash(image)
147 for chunk in image.chunks():
148 md5.update(chunk)
149 image_hash = md5.hexdigest()
150 if PostImage.objects.filter(hash=image_hash).exists():
153 if PostImage.objects.filter(hash=image_hash).exists():
151 raise forms.ValidationError(ERROR_IMAGE_DUPLICATE)
154 raise forms.ValidationError(ERROR_IMAGE_DUPLICATE)
152
155
153 return image
156 return image
154
157
155 def clean(self):
158 def clean(self):
156 cleaned_data = super(PostForm, self).clean()
159 cleaned_data = super(PostForm, self).clean()
157
160
158 if not self.session:
161 if not self.session:
159 raise forms.ValidationError('Humans have sessions')
162 raise forms.ValidationError('Humans have sessions')
160
163
161 if cleaned_data['email']:
164 if cleaned_data['email']:
162 self.need_to_ban = True
165 self.need_to_ban = True
163 raise forms.ValidationError('A human cannot enter a hidden field')
166 raise forms.ValidationError('A human cannot enter a hidden field')
164
167
165 if not self.errors:
168 if not self.errors:
166 self._clean_text_image()
169 self._clean_text_image()
167
170
168 if not self.errors and self.session:
171 if not self.errors and self.session:
169 self._validate_posting_speed()
172 self._validate_posting_speed()
170
173
171 return cleaned_data
174 return cleaned_data
172
175
173 def _clean_text_image(self):
176 def _clean_text_image(self):
174 text = self.cleaned_data.get('text')
177 text = self.cleaned_data.get('text')
175 image = self.cleaned_data.get('image')
178 image = self.cleaned_data.get('image')
176
179
177 if (not text) and (not image):
180 if (not text) and (not image):
178 error_message = _('Either text or image must be entered.')
181 error_message = _('Either text or image must be entered.')
179 self._errors['text'] = self.error_class([error_message])
182 self._errors['text'] = self.error_class([error_message])
180
183
181 def _validate_posting_speed(self):
184 def _validate_posting_speed(self):
182 can_post = True
185 can_post = True
183
186
184 posting_delay = settings.POSTING_DELAY
187 posting_delay = settings.POSTING_DELAY
185
188
186 if board_settings.LIMIT_POSTING_SPEED and LAST_POST_TIME in \
189 if board_settings.LIMIT_POSTING_SPEED and LAST_POST_TIME in \
187 self.session:
190 self.session:
188 now = time.time()
191 now = time.time()
189 last_post_time = self.session[LAST_POST_TIME]
192 last_post_time = self.session[LAST_POST_TIME]
190
193
191 current_delay = int(now - last_post_time)
194 current_delay = int(now - last_post_time)
192
195
193 if current_delay < posting_delay:
196 if current_delay < posting_delay:
194 error_message = _('Wait %s seconds after last posting') % str(
197 error_message = _('Wait %s seconds after last posting') % str(
195 posting_delay - current_delay)
198 posting_delay - current_delay)
196 self._errors['text'] = self.error_class([error_message])
199 self._errors['text'] = self.error_class([error_message])
197
200
198 can_post = False
201 can_post = False
199
202
200 if can_post:
203 if can_post:
201 self.session[LAST_POST_TIME] = time.time()
204 self.session[LAST_POST_TIME] = time.time()
202
205
203
206
204 class ThreadForm(PostForm):
207 class ThreadForm(PostForm):
205
208
206 regex_tags = re.compile(r'^[\w\s\d]+$', re.UNICODE)
207
208 tags = forms.CharField(
209 tags = forms.CharField(
209 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
210 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
210 max_length=100, label=_('Tags'), required=True)
211 max_length=100, label=_('Tags'), required=True)
211
212
212 def clean_tags(self):
213 def clean_tags(self):
213 tags = self.cleaned_data['tags'].strip()
214 tags = self.cleaned_data['tags'].strip()
214
215
215 if not tags or not self.regex_tags.match(tags):
216 if not tags or not REGEX_TAGS.match(tags):
216 raise forms.ValidationError(
217 raise forms.ValidationError(
217 _('Inappropriate characters in tags.'))
218 _('Inappropriate characters in tags.'))
218
219
219 tag_models = []
220 required_tag_exists = False
220 required_tag_exists = False
221 for tag in tags.split():
221 for tag in tags.split():
222 tag_model = Tag.objects.filter(name=tag.strip().lower(),
222 tag_model = Tag.objects.filter(name=tag.strip().lower(),
223 required=True)
223 required=True)
224 if tag_model.exists():
224 if tag_model.exists():
225 required_tag_exists = True
225 required_tag_exists = True
226 break
226
227
227 if not required_tag_exists:
228 if not required_tag_exists:
228 raise forms.ValidationError(_('Need at least 1 required tag.'))
229 raise forms.ValidationError(_('Need at least 1 required tag.'))
229
230
230 return tags
231 return tags
231
232
232 def clean(self):
233 def clean(self):
233 cleaned_data = super(ThreadForm, self).clean()
234 cleaned_data = super(ThreadForm, self).clean()
234
235
235 return cleaned_data
236 return cleaned_data
236
237
237
238
238 class SettingsForm(NeboardForm):
239 class SettingsForm(NeboardForm):
239
240
240 theme = forms.ChoiceField(choices=settings.THEMES,
241 theme = forms.ChoiceField(choices=settings.THEMES,
241 label=_('Theme'))
242 label=_('Theme'))
242
243
243
244
244 class AddTagForm(NeboardForm):
245
246 tag = forms.CharField(max_length=TAG_MAX_LENGTH, label=LABEL_TAG)
247 method = forms.CharField(widget=forms.HiddenInput(), initial='add_tag')
248
249 def clean_tag(self):
250 tag = self.cleaned_data['tag']
251
252 regex_tag = re.compile(REGEX_TAG, re.UNICODE)
253 if not regex_tag.match(tag):
254 raise forms.ValidationError(_('Inappropriate characters in tags.'))
255
256 return tag
257
258 def clean(self):
259 cleaned_data = super(AddTagForm, self).clean()
260
261 return cleaned_data
262
263
264 class SearchForm(NeboardForm):
245 class SearchForm(NeboardForm):
265 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
246 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
266
267
268 class LoginForm(NeboardForm):
269
270 password = forms.CharField()
271
272 session = None
273
274 def clean_password(self):
275 password = self.cleaned_data['password']
276 if board_settings.MASTER_PASSWORD != password:
277 raise forms.ValidationError(_('Invalid master password'))
278
279 return password
280
281 def _validate_login_speed(self):
282 can_post = True
283
284 if LAST_LOGIN_TIME in self.session:
285 now = time.time()
286 last_login_time = self.session[LAST_LOGIN_TIME]
287
288 current_delay = int(now - last_login_time)
289
290 if current_delay < board_settings.LOGIN_TIMEOUT:
291 error_message = _('Wait %s minutes after last login') % str(
292 (board_settings.LOGIN_TIMEOUT - current_delay) / 60)
293 self._errors['password'] = self.error_class([error_message])
294
295 can_post = False
296
297 if can_post:
298 self.session[LAST_LOGIN_TIME] = time.time()
299
300 def clean(self):
301 self._validate_login_speed()
302
303 cleaned_data = super(LoginForm, self).clean()
304
305 return cleaned_data
@@ -1,83 +1,90 b''
1 import hashlib
1 import hashlib
2 import os
2 import os
3 from random import random
3 from random import random
4 import time
4 import time
5 from django.db import models
5 from django.db import models
6 from boards import thumbs
6 from boards import thumbs
7 from boards.models.base import Viewable
7 from boards.models.base import Viewable
8
8
9 __author__ = 'neko259'
9 __author__ = 'neko259'
10
10
11
11
12 IMAGE_THUMB_SIZE = (200, 150)
12 IMAGE_THUMB_SIZE = (200, 150)
13 IMAGES_DIRECTORY = 'images/'
13 IMAGES_DIRECTORY = 'images/'
14 FILE_EXTENSION_DELIMITER = '.'
14 FILE_EXTENSION_DELIMITER = '.'
15 HASH_LENGTH = 36
15 HASH_LENGTH = 36
16
16
17 CSS_CLASS_IMAGE = 'image'
17 CSS_CLASS_IMAGE = 'image'
18 CSS_CLASS_THUMB = 'thumb'
18 CSS_CLASS_THUMB = 'thumb'
19
19
20
20
21 class PostImage(models.Model, Viewable):
21 class PostImage(models.Model, Viewable):
22 class Meta:
22 class Meta:
23 app_label = 'boards'
23 app_label = 'boards'
24 ordering = ('id',)
24 ordering = ('id',)
25
25
26 def _update_image_filename(self, filename):
26 def _update_image_filename(self, filename):
27 """
27 """
28 Gets unique image filename
28 Gets unique image filename
29 """
29 """
30
30
31 path = IMAGES_DIRECTORY
31 path = IMAGES_DIRECTORY
32 new_name = str(int(time.mktime(time.gmtime())))
32 new_name = str(int(time.mktime(time.gmtime())))
33 new_name += str(int(random() * 1000))
33 new_name += str(int(random() * 1000))
34 new_name += FILE_EXTENSION_DELIMITER
34 new_name += FILE_EXTENSION_DELIMITER
35 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
35 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
36
36
37 return os.path.join(path, new_name)
37 return os.path.join(path, new_name)
38
38
39 width = models.IntegerField(default=0)
39 width = models.IntegerField(default=0)
40 height = models.IntegerField(default=0)
40 height = models.IntegerField(default=0)
41
41
42 pre_width = models.IntegerField(default=0)
42 pre_width = models.IntegerField(default=0)
43 pre_height = models.IntegerField(default=0)
43 pre_height = models.IntegerField(default=0)
44
44
45 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
45 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
46 blank=True, sizes=(IMAGE_THUMB_SIZE,),
46 blank=True, sizes=(IMAGE_THUMB_SIZE,),
47 width_field='width',
47 width_field='width',
48 height_field='height',
48 height_field='height',
49 preview_width_field='pre_width',
49 preview_width_field='pre_width',
50 preview_height_field='pre_height')
50 preview_height_field='pre_height')
51 hash = models.CharField(max_length=HASH_LENGTH)
51 hash = models.CharField(max_length=HASH_LENGTH)
52
52
53 def save(self, *args, **kwargs):
53 def save(self, *args, **kwargs):
54 """
54 """
55 Saves the model and computes the image hash for deduplication purposes.
55 Saves the model and computes the image hash for deduplication purposes.
56 """
56 """
57
57
58 if not self.pk and self.image:
58 if not self.pk and self.image:
59 md5 = hashlib.md5()
59 self.hash = PostImage.get_hash(self.image)
60 for chunk in self.image.chunks():
61 md5.update(chunk)
62 self.hash = md5.hexdigest()
63 super(PostImage, self).save(*args, **kwargs)
60 super(PostImage, self).save(*args, **kwargs)
64
61
65 def __str__(self):
62 def __str__(self):
66 return self.image.url
63 return self.image.url
67
64
68 def get_view(self):
65 def get_view(self):
69 return '<div class="{}">' \
66 return '<div class="{}">' \
70 '<a class="{}" href="{}">' \
67 '<a class="{}" href="{}">' \
71 '<img' \
68 '<img' \
72 ' src="{}"' \
69 ' src="{}"' \
73 ' alt="{}"' \
70 ' alt="{}"' \
74 ' width="{}"' \
71 ' width="{}"' \
75 ' height="{}"' \
72 ' height="{}"' \
76 ' data-width="{}"' \
73 ' data-width="{}"' \
77 ' data-height="{}" />' \
74 ' data-height="{}" />' \
78 '</a>' \
75 '</a>' \
79 '</div>'\
76 '</div>'\
80 .format(CSS_CLASS_IMAGE, CSS_CLASS_THUMB, self.image.url,
77 .format(CSS_CLASS_IMAGE, CSS_CLASS_THUMB, self.image.url,
81 self.image.url_200x150,
78 self.image.url_200x150,
82 str(self.hash), str(self.pre_width),
79 str(self.hash), str(self.pre_width),
83 str(self.pre_height), str(self.width), str(self.height))
80 str(self.pre_height), str(self.width), str(self.height))
81
82 @staticmethod
83 def get_hash(image):
84 """
85 Gets hash of an image.
86 """
87 md5 = hashlib.md5()
88 for chunk in image.chunks():
89 md5.update(chunk)
90 return md5.hexdigest()
General Comments 0
You need to be logged in to leave comments. Login now