##// END OF EJS Templates
Get mimetype for file in the form and use it for both images and attachments
neko259 -
r1371:ca879451 default
parent child Browse files
Show More
@@ -1,376 +1,387 b''
1 import hashlib
1 import hashlib
2 import re
2 import re
3 import time
3 import time
4
4
5 import pytz
5 import pytz
6 from django import forms
6 from django import forms
7 from django.core.files.uploadedfile import SimpleUploadedFile
7 from django.core.files.uploadedfile import SimpleUploadedFile
8 from django.core.exceptions import ObjectDoesNotExist
8 from django.core.exceptions import ObjectDoesNotExist
9 from django.forms.util import ErrorList
9 from django.forms.util import ErrorList
10 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
10 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
11
11
12 from boards.mdx_neboard import formatters
12 from boards.mdx_neboard import formatters
13 from boards.models.attachment.downloaders import Downloader
13 from boards.models.attachment.downloaders import Downloader
14 from boards.models.post import TITLE_MAX_LENGTH
14 from boards.models.post import TITLE_MAX_LENGTH
15 from boards.models import Tag, Post
15 from boards.models import Tag, Post
16 from boards.utils import validate_file_size
16 from boards.utils import validate_file_size, get_file_mimetype, \
17 FILE_EXTENSION_DELIMITER
17 from neboard import settings
18 from neboard import settings
18 import boards.settings as board_settings
19 import boards.settings as board_settings
19 import neboard
20 import neboard
20
21
21 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
22 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
22
23
23 VETERAN_POSTING_DELAY = 5
24 VETERAN_POSTING_DELAY = 5
24
25
25 ATTRIBUTE_PLACEHOLDER = 'placeholder'
26 ATTRIBUTE_PLACEHOLDER = 'placeholder'
26 ATTRIBUTE_ROWS = 'rows'
27 ATTRIBUTE_ROWS = 'rows'
27
28
28 LAST_POST_TIME = 'last_post_time'
29 LAST_POST_TIME = 'last_post_time'
29 LAST_LOGIN_TIME = 'last_login_time'
30 LAST_LOGIN_TIME = 'last_login_time'
30 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
31 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
31 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
32 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
32
33
33 LABEL_TITLE = _('Title')
34 LABEL_TITLE = _('Title')
34 LABEL_TEXT = _('Text')
35 LABEL_TEXT = _('Text')
35 LABEL_TAG = _('Tag')
36 LABEL_TAG = _('Tag')
36 LABEL_SEARCH = _('Search')
37 LABEL_SEARCH = _('Search')
37
38
38 ERROR_SPEED = 'Please wait %(delay)d second before sending message'
39 ERROR_SPEED = 'Please wait %(delay)d second before sending message'
39 ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message'
40 ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message'
40
41
41 TAG_MAX_LENGTH = 20
42 TAG_MAX_LENGTH = 20
42
43
43 TEXTAREA_ROWS = 4
44 TEXTAREA_ROWS = 4
44
45
45 TRIPCODE_DELIM = '#'
46 TRIPCODE_DELIM = '#'
46
47
47
48
48 def get_timezones():
49 def get_timezones():
49 timezones = []
50 timezones = []
50 for tz in pytz.common_timezones:
51 for tz in pytz.common_timezones:
51 timezones.append((tz, tz),)
52 timezones.append((tz, tz),)
52 return timezones
53 return timezones
53
54
54
55
55 class FormatPanel(forms.Textarea):
56 class FormatPanel(forms.Textarea):
56 """
57 """
57 Panel for text formatting. Consists of buttons to add different tags to the
58 Panel for text formatting. Consists of buttons to add different tags to the
58 form text area.
59 form text area.
59 """
60 """
60
61
61 def render(self, name, value, attrs=None):
62 def render(self, name, value, attrs=None):
62 output = '<div id="mark-panel">'
63 output = '<div id="mark-panel">'
63 for formatter in formatters:
64 for formatter in formatters:
64 output += '<span class="mark_btn"' + \
65 output += '<span class="mark_btn"' + \
65 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
66 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
66 '\', \'' + formatter.format_right + '\')">' + \
67 '\', \'' + formatter.format_right + '\')">' + \
67 formatter.preview_left + formatter.name + \
68 formatter.preview_left + formatter.name + \
68 formatter.preview_right + '</span>'
69 formatter.preview_right + '</span>'
69
70
70 output += '</div>'
71 output += '</div>'
71 output += super(FormatPanel, self).render(name, value, attrs=None)
72 output += super(FormatPanel, self).render(name, value, attrs=None)
72
73
73 return output
74 return output
74
75
75
76
76 class PlainErrorList(ErrorList):
77 class PlainErrorList(ErrorList):
77 def __unicode__(self):
78 def __unicode__(self):
78 return self.as_text()
79 return self.as_text()
79
80
80 def as_text(self):
81 def as_text(self):
81 return ''.join(['(!) %s ' % e for e in self])
82 return ''.join(['(!) %s ' % e for e in self])
82
83
83
84
84 class NeboardForm(forms.Form):
85 class NeboardForm(forms.Form):
85 """
86 """
86 Form with neboard-specific formatting.
87 Form with neboard-specific formatting.
87 """
88 """
88
89
89 def as_div(self):
90 def as_div(self):
90 """
91 """
91 Returns this form rendered as HTML <as_div>s.
92 Returns this form rendered as HTML <as_div>s.
92 """
93 """
93
94
94 return self._html_output(
95 return self._html_output(
95 # TODO Do not show hidden rows in the list here
96 # TODO Do not show hidden rows in the list here
96 normal_row='<div class="form-row">'
97 normal_row='<div class="form-row">'
97 '<div class="form-label">'
98 '<div class="form-label">'
98 '%(label)s'
99 '%(label)s'
99 '</div>'
100 '</div>'
100 '<div class="form-input">'
101 '<div class="form-input">'
101 '%(field)s'
102 '%(field)s'
102 '</div>'
103 '</div>'
103 '</div>'
104 '</div>'
104 '<div class="form-row">'
105 '<div class="form-row">'
105 '%(help_text)s'
106 '%(help_text)s'
106 '</div>',
107 '</div>',
107 error_row='<div class="form-row">'
108 error_row='<div class="form-row">'
108 '<div class="form-label"></div>'
109 '<div class="form-label"></div>'
109 '<div class="form-errors">%s</div>'
110 '<div class="form-errors">%s</div>'
110 '</div>',
111 '</div>',
111 row_ender='</div>',
112 row_ender='</div>',
112 help_text_html='%s',
113 help_text_html='%s',
113 errors_on_separate_row=True)
114 errors_on_separate_row=True)
114
115
115 def as_json_errors(self):
116 def as_json_errors(self):
116 errors = []
117 errors = []
117
118
118 for name, field in list(self.fields.items()):
119 for name, field in list(self.fields.items()):
119 if self[name].errors:
120 if self[name].errors:
120 errors.append({
121 errors.append({
121 'field': name,
122 'field': name,
122 'errors': self[name].errors.as_text(),
123 'errors': self[name].errors.as_text(),
123 })
124 })
124
125
125 return errors
126 return errors
126
127
127
128
128 class PostForm(NeboardForm):
129 class PostForm(NeboardForm):
129
130
130 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
131 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
131 label=LABEL_TITLE,
132 label=LABEL_TITLE,
132 widget=forms.TextInput(
133 widget=forms.TextInput(
133 attrs={ATTRIBUTE_PLACEHOLDER:
134 attrs={ATTRIBUTE_PLACEHOLDER:
134 'test#tripcode'}))
135 'test#tripcode'}))
135 text = forms.CharField(
136 text = forms.CharField(
136 widget=FormatPanel(attrs={
137 widget=FormatPanel(attrs={
137 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
138 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
138 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
139 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
139 }),
140 }),
140 required=False, label=LABEL_TEXT)
141 required=False, label=LABEL_TEXT)
141 file = forms.FileField(required=False, label=_('File'),
142 file = forms.FileField(required=False, label=_('File'),
142 widget=forms.ClearableFileInput(
143 widget=forms.ClearableFileInput(
143 attrs={'accept': 'file/*'}))
144 attrs={'accept': 'file/*'}))
144 file_url = forms.CharField(required=False, label=_('File URL'),
145 file_url = forms.CharField(required=False, label=_('File URL'),
145 widget=forms.TextInput(
146 widget=forms.TextInput(
146 attrs={ATTRIBUTE_PLACEHOLDER:
147 attrs={ATTRIBUTE_PLACEHOLDER:
147 'http://example.com/image.png'}))
148 'http://example.com/image.png'}))
148
149
149 # This field is for spam prevention only
150 # This field is for spam prevention only
150 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
151 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
151 widget=forms.TextInput(attrs={
152 widget=forms.TextInput(attrs={
152 'class': 'form-email'}))
153 'class': 'form-email'}))
153 threads = forms.CharField(required=False, label=_('Additional threads'),
154 threads = forms.CharField(required=False, label=_('Additional threads'),
154 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
155 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
155 '123 456 789'}))
156 '123 456 789'}))
156
157
157 session = None
158 session = None
158 need_to_ban = False
159 need_to_ban = False
159
160
161 def _update_file_extension(self, file):
162 if file:
163 extension = get_file_mimetype(file)
164 filename = file.name.split(FILE_EXTENSION_DELIMITER, 1)[0]
165 new_filename = filename + FILE_EXTENSION_DELIMITER + extension
166
167 file.name = new_filename
168
160 def clean_title(self):
169 def clean_title(self):
161 title = self.cleaned_data['title']
170 title = self.cleaned_data['title']
162 if title:
171 if title:
163 if len(title) > TITLE_MAX_LENGTH:
172 if len(title) > TITLE_MAX_LENGTH:
164 raise forms.ValidationError(_('Title must have less than %s '
173 raise forms.ValidationError(_('Title must have less than %s '
165 'characters') %
174 'characters') %
166 str(TITLE_MAX_LENGTH))
175 str(TITLE_MAX_LENGTH))
167 return title
176 return title
168
177
169 def clean_text(self):
178 def clean_text(self):
170 text = self.cleaned_data['text'].strip()
179 text = self.cleaned_data['text'].strip()
171 if text:
180 if text:
172 max_length = board_settings.get_int('Forms', 'MaxTextLength')
181 max_length = board_settings.get_int('Forms', 'MaxTextLength')
173 if len(text) > max_length:
182 if len(text) > max_length:
174 raise forms.ValidationError(_('Text must have less than %s '
183 raise forms.ValidationError(_('Text must have less than %s '
175 'characters') % str(max_length))
184 'characters') % str(max_length))
176 return text
185 return text
177
186
178 def clean_file(self):
187 def clean_file(self):
179 file = self.cleaned_data['file']
188 file = self.cleaned_data['file']
180
189
181 if file:
190 if file:
182 validate_file_size(file.size)
191 validate_file_size(file.size)
192 self._update_file_extension(file)
183
193
184 return file
194 return file
185
195
186 def clean_file_url(self):
196 def clean_file_url(self):
187 url = self.cleaned_data['file_url']
197 url = self.cleaned_data['file_url']
188
198
189 file = None
199 file = None
190 if url:
200 if url:
191 file = self._get_file_from_url(url)
201 file = self._get_file_from_url(url)
192
202
193 if not file:
203 if not file:
194 raise forms.ValidationError(_('Invalid URL'))
204 raise forms.ValidationError(_('Invalid URL'))
195 else:
205 else:
196 validate_file_size(file.size)
206 validate_file_size(file.size)
207 self._update_file_extension(file)
197
208
198 return file
209 return file
199
210
200 def clean_threads(self):
211 def clean_threads(self):
201 threads_str = self.cleaned_data['threads']
212 threads_str = self.cleaned_data['threads']
202
213
203 if len(threads_str) > 0:
214 if len(threads_str) > 0:
204 threads_id_list = threads_str.split(' ')
215 threads_id_list = threads_str.split(' ')
205
216
206 threads = list()
217 threads = list()
207
218
208 for thread_id in threads_id_list:
219 for thread_id in threads_id_list:
209 try:
220 try:
210 thread = Post.objects.get(id=int(thread_id))
221 thread = Post.objects.get(id=int(thread_id))
211 if not thread.is_opening() or thread.get_thread().archived:
222 if not thread.is_opening() or thread.get_thread().archived:
212 raise ObjectDoesNotExist()
223 raise ObjectDoesNotExist()
213 threads.append(thread)
224 threads.append(thread)
214 except (ObjectDoesNotExist, ValueError):
225 except (ObjectDoesNotExist, ValueError):
215 raise forms.ValidationError(_('Invalid additional thread list'))
226 raise forms.ValidationError(_('Invalid additional thread list'))
216
227
217 return threads
228 return threads
218
229
219 def clean(self):
230 def clean(self):
220 cleaned_data = super(PostForm, self).clean()
231 cleaned_data = super(PostForm, self).clean()
221
232
222 if cleaned_data['email']:
233 if cleaned_data['email']:
223 self.need_to_ban = True
234 self.need_to_ban = True
224 raise forms.ValidationError('A human cannot enter a hidden field')
235 raise forms.ValidationError('A human cannot enter a hidden field')
225
236
226 if not self.errors:
237 if not self.errors:
227 self._clean_text_file()
238 self._clean_text_file()
228
239
229 if not self.errors and self.session:
240 if not self.errors and self.session:
230 self._validate_posting_speed()
241 self._validate_posting_speed()
231
242
232 return cleaned_data
243 return cleaned_data
233
244
234 def get_file(self):
245 def get_file(self):
235 """
246 """
236 Gets file from form or URL.
247 Gets file from form or URL.
237 """
248 """
238
249
239 file = self.cleaned_data['file']
250 file = self.cleaned_data['file']
240 return file or self.cleaned_data['file_url']
251 return file or self.cleaned_data['file_url']
241
252
242 def get_tripcode(self):
253 def get_tripcode(self):
243 title = self.cleaned_data['title']
254 title = self.cleaned_data['title']
244 if title is not None and TRIPCODE_DELIM in title:
255 if title is not None and TRIPCODE_DELIM in title:
245 code = title.split(TRIPCODE_DELIM, maxsplit=1)[1] + neboard.settings.SECRET_KEY
256 code = title.split(TRIPCODE_DELIM, maxsplit=1)[1] + neboard.settings.SECRET_KEY
246 tripcode = hashlib.md5(code.encode()).hexdigest()
257 tripcode = hashlib.md5(code.encode()).hexdigest()
247 else:
258 else:
248 tripcode = ''
259 tripcode = ''
249 return tripcode
260 return tripcode
250
261
251 def get_title(self):
262 def get_title(self):
252 title = self.cleaned_data['title']
263 title = self.cleaned_data['title']
253 if title is not None and TRIPCODE_DELIM in title:
264 if title is not None and TRIPCODE_DELIM in title:
254 return title.split(TRIPCODE_DELIM, maxsplit=1)[0]
265 return title.split(TRIPCODE_DELIM, maxsplit=1)[0]
255 else:
266 else:
256 return title
267 return title
257
268
258 def _clean_text_file(self):
269 def _clean_text_file(self):
259 text = self.cleaned_data.get('text')
270 text = self.cleaned_data.get('text')
260 file = self.get_file()
271 file = self.get_file()
261
272
262 if (not text) and (not file):
273 if (not text) and (not file):
263 error_message = _('Either text or file must be entered.')
274 error_message = _('Either text or file must be entered.')
264 self._errors['text'] = self.error_class([error_message])
275 self._errors['text'] = self.error_class([error_message])
265
276
266 def _validate_posting_speed(self):
277 def _validate_posting_speed(self):
267 can_post = True
278 can_post = True
268
279
269 posting_delay = settings.POSTING_DELAY
280 posting_delay = settings.POSTING_DELAY
270
281
271 if board_settings.get_bool('Forms', 'LimitPostingSpeed'):
282 if board_settings.get_bool('Forms', 'LimitPostingSpeed'):
272 now = time.time()
283 now = time.time()
273
284
274 current_delay = 0
285 current_delay = 0
275
286
276 if LAST_POST_TIME not in self.session:
287 if LAST_POST_TIME not in self.session:
277 self.session[LAST_POST_TIME] = now
288 self.session[LAST_POST_TIME] = now
278
289
279 need_delay = True
290 need_delay = True
280 else:
291 else:
281 last_post_time = self.session.get(LAST_POST_TIME)
292 last_post_time = self.session.get(LAST_POST_TIME)
282 current_delay = int(now - last_post_time)
293 current_delay = int(now - last_post_time)
283
294
284 need_delay = current_delay < posting_delay
295 need_delay = current_delay < posting_delay
285
296
286 if need_delay:
297 if need_delay:
287 delay = posting_delay - current_delay
298 delay = posting_delay - current_delay
288 error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL,
299 error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL,
289 delay) % {'delay': delay}
300 delay) % {'delay': delay}
290 self._errors['text'] = self.error_class([error_message])
301 self._errors['text'] = self.error_class([error_message])
291
302
292 can_post = False
303 can_post = False
293
304
294 if can_post:
305 if can_post:
295 self.session[LAST_POST_TIME] = now
306 self.session[LAST_POST_TIME] = now
296
307
297 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
308 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
298 """
309 """
299 Gets an file file from URL.
310 Gets an file file from URL.
300 """
311 """
301
312
302 img_temp = None
313 img_temp = None
303
314
304 try:
315 try:
305 for downloader in Downloader.__subclasses__():
316 for downloader in Downloader.__subclasses__():
306 if downloader.handles(url):
317 if downloader.handles(url):
307 return downloader.download(url)
318 return downloader.download(url)
308 # If nobody of the specific downloaders handles this, use generic
319 # If nobody of the specific downloaders handles this, use generic
309 # one
320 # one
310 return Downloader.download(url)
321 return Downloader.download(url)
311 except forms.ValidationError as e:
322 except forms.ValidationError as e:
312 raise e
323 raise e
313 except Exception as e:
324 except Exception as e:
314 # Just return no file
325 # Just return no file
315 pass
326 pass
316
327
317
328
318 class ThreadForm(PostForm):
329 class ThreadForm(PostForm):
319
330
320 tags = forms.CharField(
331 tags = forms.CharField(
321 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
332 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
322 max_length=100, label=_('Tags'), required=True)
333 max_length=100, label=_('Tags'), required=True)
323
334
324 def clean_tags(self):
335 def clean_tags(self):
325 tags = self.cleaned_data['tags'].strip()
336 tags = self.cleaned_data['tags'].strip()
326
337
327 if not tags or not REGEX_TAGS.match(tags):
338 if not tags or not REGEX_TAGS.match(tags):
328 raise forms.ValidationError(
339 raise forms.ValidationError(
329 _('Inappropriate characters in tags.'))
340 _('Inappropriate characters in tags.'))
330
341
331 required_tag_exists = False
342 required_tag_exists = False
332 tag_set = set()
343 tag_set = set()
333 for tag_string in tags.split():
344 for tag_string in tags.split():
334 tag, created = Tag.objects.get_or_create(name=tag_string.strip().lower())
345 tag, created = Tag.objects.get_or_create(name=tag_string.strip().lower())
335 tag_set.add(tag)
346 tag_set.add(tag)
336
347
337 # If this is a new tag, don't check for its parents because nobody
348 # If this is a new tag, don't check for its parents because nobody
338 # added them yet
349 # added them yet
339 if not created:
350 if not created:
340 tag_set |= set(tag.get_all_parents())
351 tag_set |= set(tag.get_all_parents())
341
352
342 for tag in tag_set:
353 for tag in tag_set:
343 if tag.required:
354 if tag.required:
344 required_tag_exists = True
355 required_tag_exists = True
345 break
356 break
346
357
347 if not required_tag_exists:
358 if not required_tag_exists:
348 raise forms.ValidationError(
359 raise forms.ValidationError(
349 _('Need at least one section.'))
360 _('Need at least one section.'))
350
361
351 return tag_set
362 return tag_set
352
363
353 def clean(self):
364 def clean(self):
354 cleaned_data = super(ThreadForm, self).clean()
365 cleaned_data = super(ThreadForm, self).clean()
355
366
356 return cleaned_data
367 return cleaned_data
357
368
358
369
359 class SettingsForm(NeboardForm):
370 class SettingsForm(NeboardForm):
360
371
361 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
372 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
362 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('Image view mode'))
373 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('Image view mode'))
363 username = forms.CharField(label=_('User name'), required=False)
374 username = forms.CharField(label=_('User name'), required=False)
364 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
375 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
365
376
366 def clean_username(self):
377 def clean_username(self):
367 username = self.cleaned_data['username']
378 username = self.cleaned_data['username']
368
379
369 if username and not REGEX_TAGS.match(username):
380 if username and not REGEX_TAGS.match(username):
370 raise forms.ValidationError(_('Inappropriate characters.'))
381 raise forms.ValidationError(_('Inappropriate characters.'))
371
382
372 return username
383 return username
373
384
374
385
375 class SearchForm(NeboardForm):
386 class SearchForm(NeboardForm):
376 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
387 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
@@ -1,46 +1,40 b''
1 import magic
2
3 from django.db import models
1 from django.db import models
4
2
5 from boards import utils
3 from boards import utils
6 from boards.models.attachment.viewers import get_viewers, AbstractViewer
4 from boards.models.attachment.viewers import get_viewers, AbstractViewer
7 from boards.utils import get_upload_filename
5 from boards.utils import get_upload_filename, get_file_mimetype
8
9 FILES_DIRECTORY = 'files/'
10 FILE_EXTENSION_DELIMITER = '.'
11
6
12
7
13 class AttachmentManager(models.Manager):
8 class AttachmentManager(models.Manager):
14 def create_with_hash(self, file):
9 def create_with_hash(self, file):
15 file_hash = utils.get_file_hash(file)
10 file_hash = utils.get_file_hash(file)
16 existing = self.filter(hash=file_hash)
11 existing = self.filter(hash=file_hash)
17 if len(existing) > 0:
12 if len(existing) > 0:
18 attachment = existing[0]
13 attachment = existing[0]
19 else:
14 else:
20 file_type = magic.from_buffer(file.chunks().__next__(), mime=True)\
15 file_type = get_file_mimetype(file)
21 .decode().split('/')[-1]
16 attachment = self.create(file=file, mimetype=file_type,
22 attachment = Attachment.objects.create(
17 hash=file_hash)
23 file=file, mimetype=file_type, hash=file_hash)
24
18
25 return attachment
19 return attachment
26
20
27
21
28 class Attachment(models.Model):
22 class Attachment(models.Model):
29 objects = AttachmentManager()
23 objects = AttachmentManager()
30
24
31 file = models.FileField(upload_to=get_upload_filename)
25 file = models.FileField(upload_to=get_upload_filename)
32 mimetype = models.CharField(max_length=50)
26 mimetype = models.CharField(max_length=50)
33 hash = models.CharField(max_length=36)
27 hash = models.CharField(max_length=36)
34
28
35 def get_view(self):
29 def get_view(self):
36 file_viewer = None
30 file_viewer = None
37 for viewer in get_viewers():
31 for viewer in get_viewers():
38 if viewer.supports(self.mimetype):
32 if viewer.supports(self.mimetype):
39 file_viewer = viewer
33 file_viewer = viewer
40 break
34 break
41 if file_viewer is None:
35 if file_viewer is None:
42 file_viewer = AbstractViewer
36 file_viewer = AbstractViewer
43
37
44 return file_viewer(self.file, self.mimetype).get_view()
38 return file_viewer(self.file, self.mimetype).get_view()
45
39
46
40
@@ -1,100 +1,95 b''
1 import hashlib
2 import os
3 from random import random
4 import time
5
6 from django.db import models
1 from django.db import models
7 from django.template.defaultfilters import filesizeformat
2 from django.template.defaultfilters import filesizeformat
8
3
9 from boards import thumbs, utils
4 from boards import thumbs, utils
10 import boards
5 import boards
11 from boards.models.base import Viewable
6 from boards.models.base import Viewable
12 from boards.utils import get_upload_filename
7 from boards.utils import get_upload_filename
13
8
14 __author__ = 'neko259'
9 __author__ = 'neko259'
15
10
16
11
17 IMAGE_THUMB_SIZE = (200, 150)
12 IMAGE_THUMB_SIZE = (200, 150)
18 HASH_LENGTH = 36
13 HASH_LENGTH = 36
19
14
20 CSS_CLASS_IMAGE = 'image'
15 CSS_CLASS_IMAGE = 'image'
21 CSS_CLASS_THUMB = 'thumb'
16 CSS_CLASS_THUMB = 'thumb'
22
17
23
18
24 class PostImageManager(models.Manager):
19 class PostImageManager(models.Manager):
25 def create_with_hash(self, image):
20 def create_with_hash(self, image):
26 image_hash = utils.get_file_hash(image)
21 image_hash = utils.get_file_hash(image)
27 existing = self.filter(hash=image_hash)
22 existing = self.filter(hash=image_hash)
28 if len(existing) > 0:
23 if len(existing) > 0:
29 post_image = existing[0]
24 post_image = existing[0]
30 else:
25 else:
31 post_image = PostImage.objects.create(image=image)
26 post_image = PostImage.objects.create(image=image)
32
27
33 return post_image
28 return post_image
34
29
35 def get_random_images(self, count, include_archived=False, tags=None):
30 def get_random_images(self, count, include_archived=False, tags=None):
36 images = self.filter(post_images__thread__archived=include_archived)
31 images = self.filter(post_images__thread__archived=include_archived)
37 if tags is not None:
32 if tags is not None:
38 images = images.filter(post_images__threads__tags__in=tags)
33 images = images.filter(post_images__threads__tags__in=tags)
39 return images.order_by('?')[:count]
34 return images.order_by('?')[:count]
40
35
41
36
42 class PostImage(models.Model, Viewable):
37 class PostImage(models.Model, Viewable):
43 objects = PostImageManager()
38 objects = PostImageManager()
44
39
45 class Meta:
40 class Meta:
46 app_label = 'boards'
41 app_label = 'boards'
47 ordering = ('id',)
42 ordering = ('id',)
48
43
49 width = models.IntegerField(default=0)
44 width = models.IntegerField(default=0)
50 height = models.IntegerField(default=0)
45 height = models.IntegerField(default=0)
51
46
52 pre_width = models.IntegerField(default=0)
47 pre_width = models.IntegerField(default=0)
53 pre_height = models.IntegerField(default=0)
48 pre_height = models.IntegerField(default=0)
54
49
55 image = thumbs.ImageWithThumbsField(upload_to=get_upload_filename,
50 image = thumbs.ImageWithThumbsField(upload_to=get_upload_filename,
56 blank=True, sizes=(IMAGE_THUMB_SIZE,),
51 blank=True, sizes=(IMAGE_THUMB_SIZE,),
57 width_field='width',
52 width_field='width',
58 height_field='height',
53 height_field='height',
59 preview_width_field='pre_width',
54 preview_width_field='pre_width',
60 preview_height_field='pre_height')
55 preview_height_field='pre_height')
61 hash = models.CharField(max_length=HASH_LENGTH)
56 hash = models.CharField(max_length=HASH_LENGTH)
62
57
63 def save(self, *args, **kwargs):
58 def save(self, *args, **kwargs):
64 """
59 """
65 Saves the model and computes the image hash for deduplication purposes.
60 Saves the model and computes the image hash for deduplication purposes.
66 """
61 """
67
62
68 if not self.pk and self.image:
63 if not self.pk and self.image:
69 self.hash = utils.get_file_hash(self.image)
64 self.hash = utils.get_file_hash(self.image)
70 super(PostImage, self).save(*args, **kwargs)
65 super(PostImage, self).save(*args, **kwargs)
71
66
72 def __str__(self):
67 def __str__(self):
73 return self.image.url
68 return self.image.url
74
69
75 def get_view(self):
70 def get_view(self):
76 metadata = '{}, {}'.format(self.image.name.split('.')[-1],
71 metadata = '{}, {}'.format(self.image.name.split('.')[-1],
77 filesizeformat(self.image.size))
72 filesizeformat(self.image.size))
78 return '<div class="{}">' \
73 return '<div class="{}">' \
79 '<a class="{}" href="{full}">' \
74 '<a class="{}" href="{full}">' \
80 '<img class="post-image-preview"' \
75 '<img class="post-image-preview"' \
81 ' src="{}"' \
76 ' src="{}"' \
82 ' alt="{}"' \
77 ' alt="{}"' \
83 ' width="{}"' \
78 ' width="{}"' \
84 ' height="{}"' \
79 ' height="{}"' \
85 ' data-width="{}"' \
80 ' data-width="{}"' \
86 ' data-height="{}" />' \
81 ' data-height="{}" />' \
87 '</a>' \
82 '</a>' \
88 '<div class="image-metadata">'\
83 '<div class="image-metadata">'\
89 '<a href="{full}" download>{image_meta}</a>'\
84 '<a href="{full}" download>{image_meta}</a>'\
90 '</div>' \
85 '</div>' \
91 '</div>'\
86 '</div>'\
92 .format(CSS_CLASS_IMAGE, CSS_CLASS_THUMB,
87 .format(CSS_CLASS_IMAGE, CSS_CLASS_THUMB,
93 self.image.url_200x150,
88 self.image.url_200x150,
94 str(self.hash), str(self.pre_width),
89 str(self.hash), str(self.pre_width),
95 str(self.pre_height), str(self.width), str(self.height),
90 str(self.pre_height), str(self.width), str(self.height),
96 full=self.image.url, image_meta=metadata)
91 full=self.image.url, image_meta=metadata)
97
92
98 def get_random_associated_post(self):
93 def get_random_associated_post(self):
99 posts = boards.models.Post.objects.filter(images__in=[self])
94 posts = boards.models.Post.objects.filter(images__in=[self])
100 return posts.order_by('?').first()
95 return posts.order_by('?').first()
@@ -1,142 +1,148 b''
1 """
1 """
2 This module contains helper functions and helper classes.
2 This module contains helper functions and helper classes.
3 """
3 """
4 import hashlib
4 import hashlib
5 from random import random
5 from random import random
6 import time
6 import time
7 import hmac
7 import hmac
8
8
9 from django.core.cache import cache
9 from django.core.cache import cache
10 from django.db.models import Model
10 from django.db.models import Model
11 from django import forms
11 from django import forms
12 from django.utils import timezone
12 from django.utils import timezone
13 from django.utils.translation import ugettext_lazy as _
13 from django.utils.translation import ugettext_lazy as _
14 import magic
14 from portage import os
15 from portage import os
15
16
16 import boards
17 import boards
17 from boards.settings import get_bool
18 from boards.settings import get_bool
18 from neboard import settings
19 from neboard import settings
19
20
20 CACHE_KEY_DELIMITER = '_'
21 CACHE_KEY_DELIMITER = '_'
21 PERMISSION_MODERATE = 'moderation'
22 PERMISSION_MODERATE = 'moderation'
22
23
23 HTTP_FORWARDED = 'HTTP_X_FORWARDED_FOR'
24 HTTP_FORWARDED = 'HTTP_X_FORWARDED_FOR'
24 META_REMOTE_ADDR = 'REMOTE_ADDR'
25 META_REMOTE_ADDR = 'REMOTE_ADDR'
25
26
26 SETTING_MESSAGES = 'Messages'
27 SETTING_MESSAGES = 'Messages'
27 SETTING_ANON_MODE = 'AnonymousMode'
28 SETTING_ANON_MODE = 'AnonymousMode'
28
29
29 ANON_IP = '127.0.0.1'
30 ANON_IP = '127.0.0.1'
30
31
31 UPLOAD_DIRS ={
32 UPLOAD_DIRS ={
32 'PostImage': 'images/',
33 'PostImage': 'images/',
33 'Attachment': 'files/',
34 'Attachment': 'files/',
34 }
35 }
35 FILE_EXTENSION_DELIMITER = '.'
36 FILE_EXTENSION_DELIMITER = '.'
36
37
37
38
38 def is_anonymous_mode():
39 def is_anonymous_mode():
39 return get_bool(SETTING_MESSAGES, SETTING_ANON_MODE)
40 return get_bool(SETTING_MESSAGES, SETTING_ANON_MODE)
40
41
41
42
42 def get_client_ip(request):
43 def get_client_ip(request):
43 if is_anonymous_mode():
44 if is_anonymous_mode():
44 ip = ANON_IP
45 ip = ANON_IP
45 else:
46 else:
46 x_forwarded_for = request.META.get(HTTP_FORWARDED)
47 x_forwarded_for = request.META.get(HTTP_FORWARDED)
47 if x_forwarded_for:
48 if x_forwarded_for:
48 ip = x_forwarded_for.split(',')[-1].strip()
49 ip = x_forwarded_for.split(',')[-1].strip()
49 else:
50 else:
50 ip = request.META.get(META_REMOTE_ADDR)
51 ip = request.META.get(META_REMOTE_ADDR)
51 return ip
52 return ip
52
53
53
54
54 # TODO The output format is not epoch because it includes microseconds
55 # TODO The output format is not epoch because it includes microseconds
55 def datetime_to_epoch(datetime):
56 def datetime_to_epoch(datetime):
56 return int(time.mktime(timezone.localtime(
57 return int(time.mktime(timezone.localtime(
57 datetime,timezone.get_current_timezone()).timetuple())
58 datetime,timezone.get_current_timezone()).timetuple())
58 * 1000000 + datetime.microsecond)
59 * 1000000 + datetime.microsecond)
59
60
60
61
61 def get_websocket_token(user_id='', timestamp=''):
62 def get_websocket_token(user_id='', timestamp=''):
62 """
63 """
63 Create token to validate information provided by new connection.
64 Create token to validate information provided by new connection.
64 """
65 """
65
66
66 sign = hmac.new(settings.CENTRIFUGE_PROJECT_SECRET.encode())
67 sign = hmac.new(settings.CENTRIFUGE_PROJECT_SECRET.encode())
67 sign.update(settings.CENTRIFUGE_PROJECT_ID.encode())
68 sign.update(settings.CENTRIFUGE_PROJECT_ID.encode())
68 sign.update(user_id.encode())
69 sign.update(user_id.encode())
69 sign.update(timestamp.encode())
70 sign.update(timestamp.encode())
70 token = sign.hexdigest()
71 token = sign.hexdigest()
71
72
72 return token
73 return token
73
74
74
75
75 def cached_result(key_method=None):
76 def cached_result(key_method=None):
76 """
77 """
77 Caches method result in the Django's cache system, persisted by object name,
78 Caches method result in the Django's cache system, persisted by object name,
78 object name and model id if object is a Django model.
79 object name and model id if object is a Django model.
79 """
80 """
80 def _cached_result(function):
81 def _cached_result(function):
81 def inner_func(obj, *args, **kwargs):
82 def inner_func(obj, *args, **kwargs):
82 # TODO Include method arguments to the cache key
83 # TODO Include method arguments to the cache key
83 cache_key_params = [obj.__class__.__name__, function.__name__]
84 cache_key_params = [obj.__class__.__name__, function.__name__]
84 if isinstance(obj, Model):
85 if isinstance(obj, Model):
85 cache_key_params.append(str(obj.id))
86 cache_key_params.append(str(obj.id))
86
87
87 if key_method is not None:
88 if key_method is not None:
88 cache_key_params += [str(arg) for arg in key_method(obj)]
89 cache_key_params += [str(arg) for arg in key_method(obj)]
89
90
90 cache_key = CACHE_KEY_DELIMITER.join(cache_key_params)
91 cache_key = CACHE_KEY_DELIMITER.join(cache_key_params)
91
92
92 persisted_result = cache.get(cache_key)
93 persisted_result = cache.get(cache_key)
93 if persisted_result is not None:
94 if persisted_result is not None:
94 result = persisted_result
95 result = persisted_result
95 else:
96 else:
96 result = function(obj, *args, **kwargs)
97 result = function(obj, *args, **kwargs)
97 cache.set(cache_key, result)
98 cache.set(cache_key, result)
98
99
99 return result
100 return result
100
101
101 return inner_func
102 return inner_func
102 return _cached_result
103 return _cached_result
103
104
104
105
105 def is_moderator(request):
106 def is_moderator(request):
106 try:
107 try:
107 moderate = request.user.has_perm(PERMISSION_MODERATE)
108 moderate = request.user.has_perm(PERMISSION_MODERATE)
108 except AttributeError:
109 except AttributeError:
109 moderate = False
110 moderate = False
110
111
111 return moderate
112 return moderate
112
113
113
114
114 def get_file_hash(file) -> str:
115 def get_file_hash(file) -> str:
115 md5 = hashlib.md5()
116 md5 = hashlib.md5()
116 for chunk in file.chunks():
117 for chunk in file.chunks():
117 md5.update(chunk)
118 md5.update(chunk)
118 return md5.hexdigest()
119 return md5.hexdigest()
119
120
120
121
121 def validate_file_size(size: int):
122 def validate_file_size(size: int):
122 max_size = boards.settings.get_int('Forms', 'MaxFileSize')
123 max_size = boards.settings.get_int('Forms', 'MaxFileSize')
123 if size > max_size:
124 if size > max_size:
124 raise forms.ValidationError(
125 raise forms.ValidationError(
125 _('File must be less than %s bytes')
126 _('File must be less than %s bytes')
126 % str(max_size))
127 % str(max_size))
127
128
128
129
129 def get_upload_filename(model_instance, old_filename):
130 def get_upload_filename(model_instance, old_filename):
130 # TODO Use something other than random number in file name
131 # TODO Use something other than random number in file name
131 if hasattr(model_instance, 'mimetype'):
132 if hasattr(model_instance, 'mimetype'):
132 extension = model_instance.mimetype
133 extension = model_instance.mimetype
133 else:
134 else:
134 extension = old_filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
135 extension = old_filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
135 new_name = '{}{}.{}'.format(
136 new_name = '{}{}.{}'.format(
136 str(int(time.mktime(time.gmtime()))),
137 str(int(time.mktime(time.gmtime()))),
137 str(int(random() * 1000)),
138 str(int(random() * 1000)),
138 extension)
139 extension)
139
140
140 directory = UPLOAD_DIRS[type(model_instance).__name__]
141 directory = UPLOAD_DIRS[type(model_instance).__name__]
141
142
142 return os.path.join(directory, new_name)
143 return os.path.join(directory, new_name)
144
145
146 def get_file_mimetype(file) -> str:
147 return magic.from_buffer(file.chunks().__next__(), mime=True) \
148 .decode().split('/')[-1]
General Comments 0
You need to be logged in to leave comments. Login now