Show More
@@ -1,537 +1,538 b'' | |||||
1 | import logging |
|
1 | import logging | |
2 |
|
2 | |||
3 | import pytz |
|
3 | import pytz | |
4 | import re |
|
4 | import re | |
5 | from PIL import Image |
|
5 | from PIL import Image | |
6 | from django import forms |
|
6 | from django import forms | |
7 | from django.core.files.images import get_image_dimensions |
|
7 | from django.core.files.images import get_image_dimensions | |
8 | from django.core.files.uploadedfile import SimpleUploadedFile, UploadedFile |
|
8 | from django.core.files.uploadedfile import SimpleUploadedFile, UploadedFile | |
9 | from django.forms.utils import ErrorList |
|
9 | from django.forms.utils 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 | import boards.settings as board_settings |
|
12 | import boards.settings as board_settings | |
13 | from boards import utils |
|
13 | from boards import utils | |
14 | from boards.abstracts.constants import REGEX_TAGS |
|
14 | from boards.abstracts.constants import REGEX_TAGS | |
15 | from boards.abstracts.settingsmanager import get_settings_manager, \ |
|
15 | from boards.abstracts.settingsmanager import get_settings_manager, \ | |
16 | SETTING_CONFIRMED_USER |
|
16 | SETTING_CONFIRMED_USER | |
17 | from boards.abstracts.sticker_factory import get_attachment_by_alias |
|
17 | from boards.abstracts.sticker_factory import get_attachment_by_alias | |
18 | from boards.forms.fields import UrlFileField |
|
18 | from boards.forms.fields import UrlFileField | |
19 | from boards.forms.validators import TimeValidator, PowValidator |
|
19 | from boards.forms.validators import TimeValidator, PowValidator | |
20 | from boards.mdx_neboard import formatters |
|
20 | from boards.mdx_neboard import formatters | |
21 | from boards.models import Attachment |
|
21 | from boards.models import Attachment | |
22 | from boards.models import Tag |
|
22 | from boards.models import Tag | |
23 | from boards.models.attachment import StickerPack |
|
23 | from boards.models.attachment import StickerPack | |
24 | from boards.models.attachment.downloaders import download, REGEX_MAGNET |
|
24 | from boards.models.attachment.downloaders import download, REGEX_MAGNET | |
25 | from boards.models.attachment.viewers import FILE_TYPES_IMAGE |
|
25 | from boards.models.attachment.viewers import FILE_TYPES_IMAGE | |
26 | from boards.models.post import TITLE_MAX_LENGTH |
|
26 | from boards.models.post import TITLE_MAX_LENGTH | |
27 | from boards.settings import SECTION_FORMS |
|
27 | from boards.settings import SECTION_FORMS | |
28 | from boards.utils import validate_file_size, get_file_mimetype, \ |
|
28 | from boards.utils import validate_file_size, get_file_mimetype, \ | |
29 | FILE_EXTENSION_DELIMITER, get_tripcode_from_text |
|
29 | FILE_EXTENSION_DELIMITER, get_tripcode_from_text | |
30 |
|
30 | |||
31 | FORMAT_PANEL_BUTTON = '<span class="mark_btn" ' \ |
|
31 | FORMAT_PANEL_BUTTON = '<span class="mark_btn" ' \ | |
32 | 'onClick="addMarkToMsg(\'{}\', \'{}\')">{}{}{}</span>' |
|
32 | 'onClick="addMarkToMsg(\'{}\', \'{}\')">{}{}{}</span>' | |
33 |
|
33 | |||
34 |
|
34 | |||
35 | REGEX_USERNAMES = re.compile(r'^[\w\s\d,]+$', re.UNICODE) |
|
35 | REGEX_USERNAMES = re.compile(r'^[\w\s\d,]+$', re.UNICODE) | |
36 | REGEX_URL = re.compile(r'^(http|https|ftp):\/\/', re.UNICODE) |
|
36 | REGEX_URL = re.compile(r'^(http|https|ftp):\/\/', re.UNICODE) | |
37 |
|
37 | |||
38 | VETERAN_POSTING_DELAY = 5 |
|
38 | VETERAN_POSTING_DELAY = 5 | |
39 |
|
39 | |||
40 | ATTRIBUTE_PLACEHOLDER = 'placeholder' |
|
40 | ATTRIBUTE_PLACEHOLDER = 'placeholder' | |
41 | ATTRIBUTE_ROWS = 'rows' |
|
41 | ATTRIBUTE_ROWS = 'rows' | |
42 |
|
42 | |||
43 | TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.') |
|
43 | TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.') | |
44 | TAGS_PLACEHOLDER = _('music images i_dont_like_tags') |
|
44 | TAGS_PLACEHOLDER = _('music images i_dont_like_tags') | |
45 |
|
45 | |||
46 | LABEL_TITLE = _('Title') |
|
46 | LABEL_TITLE = _('Title') | |
47 | LABEL_TEXT = _('Text') |
|
47 | LABEL_TEXT = _('Text') | |
48 | LABEL_TAG = _('Tag') |
|
48 | LABEL_TAG = _('Tag') | |
49 | LABEL_SEARCH = _('Search') |
|
49 | LABEL_SEARCH = _('Search') | |
50 | LABEL_FILE = _('File') |
|
50 | LABEL_FILE = _('File') | |
51 | LABEL_DUPLICATES = _('Check for duplicates') |
|
51 | LABEL_DUPLICATES = _('Check for duplicates') | |
52 | LABEL_URL = _('Do not download URLs') |
|
52 | LABEL_URL = _('Do not download URLs') | |
53 |
|
53 | |||
54 | ERROR_MANY_FILES = 'You can post no more than %(files)d file.' |
|
54 | ERROR_MANY_FILES = 'You can post no more than %(files)d file.' | |
55 | ERROR_MANY_FILES_PLURAL = 'You can post no more than %(files)d files.' |
|
55 | ERROR_MANY_FILES_PLURAL = 'You can post no more than %(files)d files.' | |
56 | ERROR_DUPLICATES = 'Some files are already present on the board.' |
|
56 | ERROR_DUPLICATES = 'Some files are already present on the board.' | |
57 |
|
57 | |||
58 | TAG_MAX_LENGTH = 20 |
|
58 | TAG_MAX_LENGTH = 20 | |
59 |
|
59 | |||
60 | TEXTAREA_ROWS = 4 |
|
60 | TEXTAREA_ROWS = 4 | |
61 |
|
61 | |||
62 | TRIPCODE_DELIM = '##' |
|
62 | TRIPCODE_DELIM = '##' | |
63 |
|
63 | |||
64 | # TODO Maybe this may be converted into the database table? |
|
64 | # TODO Maybe this may be converted into the database table? | |
65 | MIMETYPE_EXTENSIONS = { |
|
65 | MIMETYPE_EXTENSIONS = { | |
66 | 'image/jpeg': 'jpeg', |
|
66 | 'image/jpeg': 'jpeg', | |
67 | 'image/png': 'png', |
|
67 | 'image/png': 'png', | |
68 | 'image/gif': 'gif', |
|
68 | 'image/gif': 'gif', | |
69 | 'video/webm': 'webm', |
|
69 | 'video/webm': 'webm', | |
70 | 'application/pdf': 'pdf', |
|
70 | 'application/pdf': 'pdf', | |
71 | 'x-diff': 'diff', |
|
71 | 'x-diff': 'diff', | |
72 | 'image/svg+xml': 'svg', |
|
72 | 'image/svg+xml': 'svg', | |
73 | 'application/x-shockwave-flash': 'swf', |
|
73 | 'application/x-shockwave-flash': 'swf', | |
74 | 'image/x-ms-bmp': 'bmp', |
|
74 | 'image/x-ms-bmp': 'bmp', | |
75 | 'image/bmp': 'bmp', |
|
75 | 'image/bmp': 'bmp', | |
76 | } |
|
76 | } | |
77 |
|
77 | |||
78 | DOWN_MODE_DOWNLOAD = 'DOWNLOAD' |
|
78 | DOWN_MODE_DOWNLOAD = 'DOWNLOAD' | |
79 | DOWN_MODE_DOWNLOAD_UNIQUE = 'DOWNLOAD_UNIQUE' |
|
79 | DOWN_MODE_DOWNLOAD_UNIQUE = 'DOWNLOAD_UNIQUE' | |
80 | DOWN_MODE_URL = 'URL' |
|
80 | DOWN_MODE_URL = 'URL' | |
81 | DOWN_MODE_TRY = 'TRY' |
|
81 | DOWN_MODE_TRY = 'TRY' | |
82 |
|
82 | |||
83 |
|
83 | |||
84 | logger = logging.getLogger('boards.forms') |
|
84 | logger = logging.getLogger('boards.forms') | |
85 |
|
85 | |||
86 |
|
86 | |||
87 | def get_timezones(): |
|
87 | def get_timezones(): | |
88 | timezones = [] |
|
88 | timezones = [] | |
89 | for tz in pytz.common_timezones: |
|
89 | for tz in pytz.common_timezones: | |
90 | timezones.append((tz, tz),) |
|
90 | timezones.append((tz, tz),) | |
91 | return timezones |
|
91 | return timezones | |
92 |
|
92 | |||
93 |
|
93 | |||
94 | class FormatPanel(forms.Textarea): |
|
94 | class FormatPanel(forms.Textarea): | |
95 | """ |
|
95 | """ | |
96 | Panel for text formatting. Consists of buttons to add different tags to the |
|
96 | Panel for text formatting. Consists of buttons to add different tags to the | |
97 | form text area. |
|
97 | form text area. | |
98 | """ |
|
98 | """ | |
99 |
|
99 | |||
100 | def render(self, name, value, attrs=None): |
|
100 | def render(self, name, value, attrs=None): | |
101 | output_template = '<div id="mark-panel">{}</div>' |
|
101 | output_template = '<div id="mark-panel">{}</div>' | |
102 |
|
102 | |||
103 | buttons = [self._get_button(formatter) for formatter in formatters] |
|
103 | buttons = [self._get_button(formatter) for formatter in formatters] | |
104 |
|
104 | |||
105 | output = output_template.format(''.join(buttons)) |
|
105 | output = output_template.format(''.join(buttons)) | |
106 |
|
106 | |||
107 | output += super(FormatPanel, self).render(name, value, attrs=attrs) |
|
107 | output += super(FormatPanel, self).render(name, value, attrs=attrs) | |
108 |
|
108 | |||
109 | return output |
|
109 | return output | |
110 |
|
110 | |||
111 | def _get_button(self, formatter): |
|
111 | def _get_button(self, formatter): | |
112 | return FORMAT_PANEL_BUTTON.format( |
|
112 | return FORMAT_PANEL_BUTTON.format( | |
113 | formatter.format_left, |
|
113 | formatter.format_left, | |
114 | formatter.format_right, |
|
114 | formatter.format_right, | |
115 | formatter.preview_left, |
|
115 | formatter.preview_left, | |
116 | formatter.name, |
|
116 | formatter.name, | |
117 | formatter.preview_right) |
|
117 | formatter.preview_right) | |
118 |
|
118 | |||
119 |
|
119 | |||
120 | class PlainErrorList(ErrorList): |
|
120 | class PlainErrorList(ErrorList): | |
121 | def __unicode__(self): |
|
121 | def __unicode__(self): | |
122 | return self.as_text() |
|
122 | return self.as_text() | |
123 |
|
123 | |||
124 | def as_text(self): |
|
124 | def as_text(self): | |
125 | return ''.join(['(!) %s ' % e for e in self]) |
|
125 | return ''.join(['(!) %s ' % e for e in self]) | |
126 |
|
126 | |||
127 |
|
127 | |||
128 | class NeboardForm(forms.Form): |
|
128 | class NeboardForm(forms.Form): | |
129 | """ |
|
129 | """ | |
130 | Form with neboard-specific formatting. |
|
130 | Form with neboard-specific formatting. | |
131 | """ |
|
131 | """ | |
132 | required_css_class = 'required-field' |
|
132 | required_css_class = 'required-field' | |
133 |
|
133 | |||
134 | def as_div(self): |
|
134 | def as_div(self): | |
135 | """ |
|
135 | """ | |
136 | Returns this form rendered as HTML <as_div>s. |
|
136 | Returns this form rendered as HTML <as_div>s. | |
137 | """ |
|
137 | """ | |
138 |
|
138 | |||
139 | return self._html_output( |
|
139 | return self._html_output( | |
140 | # TODO Do not show hidden rows in the list here |
|
140 | # TODO Do not show hidden rows in the list here | |
141 | normal_row='<div class="form-row">' |
|
141 | normal_row='<div class="form-row">' | |
142 | '<div class="form-label">' |
|
142 | '<div class="form-label">' | |
143 | '%(label)s' |
|
143 | '%(label)s' | |
144 | '</div>' |
|
144 | '</div>' | |
145 | '<div class="form-input">' |
|
145 | '<div class="form-input">' | |
146 | '%(field)s' |
|
146 | '%(field)s' | |
147 | '</div>' |
|
147 | '</div>' | |
148 | '</div>' |
|
148 | '</div>' | |
149 | '<div class="form-row">' |
|
149 | '<div class="form-row">' | |
150 | '%(help_text)s' |
|
150 | '%(help_text)s' | |
151 | '</div>', |
|
151 | '</div>', | |
152 | error_row='<div class="form-row">' |
|
152 | error_row='<div class="form-row">' | |
153 | '<div class="form-label"></div>' |
|
153 | '<div class="form-label"></div>' | |
154 | '<div class="form-errors">%s</div>' |
|
154 | '<div class="form-errors">%s</div>' | |
155 | '</div>', |
|
155 | '</div>', | |
156 | row_ender='</div>', |
|
156 | row_ender='</div>', | |
157 | help_text_html='%s', |
|
157 | help_text_html='%s', | |
158 | errors_on_separate_row=True) |
|
158 | errors_on_separate_row=True) | |
159 |
|
159 | |||
160 | def as_json_errors(self): |
|
160 | def as_json_errors(self): | |
161 | errors = [] |
|
161 | errors = [] | |
162 |
|
162 | |||
163 | for name, field in list(self.fields.items()): |
|
163 | for name, field in list(self.fields.items()): | |
164 | if self[name].errors: |
|
164 | if self[name].errors: | |
165 | errors.append({ |
|
165 | errors.append({ | |
166 | 'field': name, |
|
166 | 'field': name, | |
167 | 'errors': self[name].errors.as_text(), |
|
167 | 'errors': self[name].errors.as_text(), | |
168 | }) |
|
168 | }) | |
169 |
|
169 | |||
170 | return errors |
|
170 | return errors | |
171 |
|
171 | |||
172 |
|
172 | |||
173 | class PostForm(NeboardForm): |
|
173 | class PostForm(NeboardForm): | |
174 |
|
174 | |||
175 | title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False, |
|
175 | title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False, | |
176 | label=LABEL_TITLE, |
|
176 | label=LABEL_TITLE, | |
177 | widget=forms.TextInput( |
|
177 | widget=forms.TextInput( | |
178 | attrs={ATTRIBUTE_PLACEHOLDER: 'Title{}tripcode'.format(TRIPCODE_DELIM)})) |
|
178 | attrs={ATTRIBUTE_PLACEHOLDER: 'Title{}tripcode'.format(TRIPCODE_DELIM)})) | |
179 | text = forms.CharField( |
|
179 | text = forms.CharField( | |
180 | widget=FormatPanel(attrs={ |
|
180 | widget=FormatPanel(attrs={ | |
181 | ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER, |
|
181 | ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER, | |
182 | ATTRIBUTE_ROWS: TEXTAREA_ROWS, |
|
182 | ATTRIBUTE_ROWS: TEXTAREA_ROWS, | |
183 | }), |
|
183 | }), | |
184 | required=False, label=LABEL_TEXT) |
|
184 | required=False, label=LABEL_TEXT) | |
185 | download_mode = forms.ChoiceField( |
|
185 | download_mode = forms.ChoiceField( | |
186 | choices=( |
|
186 | choices=( | |
187 | (DOWN_MODE_TRY, _('Download or insert as URLs')), |
|
187 | (DOWN_MODE_TRY, _('Download or insert as URLs')), | |
188 | (DOWN_MODE_DOWNLOAD, _('Download')), |
|
188 | (DOWN_MODE_DOWNLOAD, _('Download')), | |
189 | (DOWN_MODE_DOWNLOAD_UNIQUE, _('Download and check for uniqueness')), |
|
189 | (DOWN_MODE_DOWNLOAD_UNIQUE, _('Download and check for uniqueness')), | |
190 | (DOWN_MODE_URL, _('Insert as URLs')), |
|
190 | (DOWN_MODE_URL, _('Insert as URLs')), | |
191 | ), |
|
191 | ), | |
192 | initial=DOWN_MODE_TRY, |
|
192 | initial=DOWN_MODE_TRY, | |
193 | label=_('File process mode')) |
|
193 | label=_('File process mode')) | |
194 | file = UrlFileField(required=False, label=LABEL_FILE) |
|
194 | file = UrlFileField(required=False, label=LABEL_FILE) | |
195 |
|
195 | |||
196 | # This field is for spam prevention only |
|
196 | # This field is for spam prevention only | |
197 | email = forms.CharField(max_length=100, required=False, label=_('e-mail'), |
|
197 | email = forms.CharField(max_length=100, required=False, label=_('e-mail'), | |
198 | widget=forms.TextInput(attrs={ |
|
198 | widget=forms.TextInput(attrs={ | |
199 | 'class': 'form-email'})) |
|
199 | 'class': 'form-email'})) | |
200 |
subscribe = forms.BooleanField(required=False, |
|
200 | subscribe = forms.BooleanField(required=False, | |
|
201 | label=_('Subscribe to thread'), initial=True) | |||
201 |
|
202 | |||
202 | guess = forms.CharField(widget=forms.HiddenInput(), required=False) |
|
203 | guess = forms.CharField(widget=forms.HiddenInput(), required=False) | |
203 | timestamp = forms.CharField(widget=forms.HiddenInput(), required=False) |
|
204 | timestamp = forms.CharField(widget=forms.HiddenInput(), required=False) | |
204 | iteration = forms.CharField(widget=forms.HiddenInput(), required=False) |
|
205 | iteration = forms.CharField(widget=forms.HiddenInput(), required=False) | |
205 |
|
206 | |||
206 | need_to_ban = False |
|
207 | need_to_ban = False | |
207 |
|
208 | |||
208 | def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, |
|
209 | def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, | |
209 | initial=None, error_class=ErrorList, label_suffix=None, |
|
210 | initial=None, error_class=ErrorList, label_suffix=None, | |
210 | empty_permitted=False, field_order=None, |
|
211 | empty_permitted=False, field_order=None, | |
211 | use_required_attribute=None, renderer=None, session=None): |
|
212 | use_required_attribute=None, renderer=None, session=None): | |
212 | super().__init__(data, files, auto_id, prefix, initial, error_class, |
|
213 | super().__init__(data, files, auto_id, prefix, initial, error_class, | |
213 | label_suffix, empty_permitted, field_order, |
|
214 | label_suffix, empty_permitted, field_order, | |
214 | use_required_attribute, renderer) |
|
215 | use_required_attribute, renderer) | |
215 |
|
216 | |||
216 | self.session = session |
|
217 | self.session = session | |
217 |
|
218 | |||
218 | def clean_title(self): |
|
219 | def clean_title(self): | |
219 | title = self.cleaned_data['title'] |
|
220 | title = self.cleaned_data['title'] | |
220 | if title: |
|
221 | if title: | |
221 | if len(title) > TITLE_MAX_LENGTH: |
|
222 | if len(title) > TITLE_MAX_LENGTH: | |
222 | raise forms.ValidationError(_('Title must have less than %s ' |
|
223 | raise forms.ValidationError(_('Title must have less than %s ' | |
223 | 'characters') % |
|
224 | 'characters') % | |
224 | str(TITLE_MAX_LENGTH)) |
|
225 | str(TITLE_MAX_LENGTH)) | |
225 | return title |
|
226 | return title | |
226 |
|
227 | |||
227 | def clean_text(self): |
|
228 | def clean_text(self): | |
228 | text = self.cleaned_data['text'].strip() |
|
229 | text = self.cleaned_data['text'].strip() | |
229 | if text: |
|
230 | if text: | |
230 | max_length = board_settings.get_int(SECTION_FORMS, 'MaxTextLength') |
|
231 | max_length = board_settings.get_int(SECTION_FORMS, 'MaxTextLength') | |
231 | if len(text) > max_length: |
|
232 | if len(text) > max_length: | |
232 | raise forms.ValidationError(_('Text must have less than %s ' |
|
233 | raise forms.ValidationError(_('Text must have less than %s ' | |
233 | 'characters') % str(max_length)) |
|
234 | 'characters') % str(max_length)) | |
234 | return text |
|
235 | return text | |
235 |
|
236 | |||
236 | def clean_file(self): |
|
237 | def clean_file(self): | |
237 | return self._clean_files(self.cleaned_data['file']) |
|
238 | return self._clean_files(self.cleaned_data['file']) | |
238 |
|
239 | |||
239 | def clean(self): |
|
240 | def clean(self): | |
240 | cleaned_data = super(PostForm, self).clean() |
|
241 | cleaned_data = super(PostForm, self).clean() | |
241 |
|
242 | |||
242 | if cleaned_data['email']: |
|
243 | if cleaned_data['email']: | |
243 | if board_settings.get_bool(SECTION_FORMS, 'Autoban'): |
|
244 | if board_settings.get_bool(SECTION_FORMS, 'Autoban'): | |
244 | self.need_to_ban = True |
|
245 | self.need_to_ban = True | |
245 | raise forms.ValidationError('A human cannot enter a hidden field') |
|
246 | raise forms.ValidationError('A human cannot enter a hidden field') | |
246 |
|
247 | |||
247 | if not self.errors: |
|
248 | if not self.errors: | |
248 | self._clean_text_file() |
|
249 | self._clean_text_file() | |
249 |
|
250 | |||
250 | limit_speed = board_settings.get_bool(SECTION_FORMS, 'LimitPostingSpeed') |
|
251 | limit_speed = board_settings.get_bool(SECTION_FORMS, 'LimitPostingSpeed') | |
251 | limit_first = board_settings.get_bool(SECTION_FORMS, 'LimitFirstPosting') |
|
252 | limit_first = board_settings.get_bool(SECTION_FORMS, 'LimitFirstPosting') | |
252 |
|
253 | |||
253 | settings_manager = get_settings_manager(self) |
|
254 | settings_manager = get_settings_manager(self) | |
254 | if not self.errors and limit_speed or (limit_first and not settings_manager.get_setting(SETTING_CONFIRMED_USER)): |
|
255 | if not self.errors and limit_speed or (limit_first and not settings_manager.get_setting(SETTING_CONFIRMED_USER)): | |
255 | pow_difficulty = board_settings.get_int(SECTION_FORMS, 'PowDifficulty') |
|
256 | pow_difficulty = board_settings.get_int(SECTION_FORMS, 'PowDifficulty') | |
256 | if pow_difficulty > 0: |
|
257 | if pow_difficulty > 0: | |
257 | validator = PowValidator( |
|
258 | validator = PowValidator( | |
258 | self.session, cleaned_data['timestamp'], |
|
259 | self.session, cleaned_data['timestamp'], | |
259 | cleaned_data['iteration'], cleaned_data['guess'], |
|
260 | cleaned_data['iteration'], cleaned_data['guess'], | |
260 | cleaned_data['text']) |
|
261 | cleaned_data['text']) | |
261 | else: |
|
262 | else: | |
262 | validator = TimeValidator(self.session) |
|
263 | validator = TimeValidator(self.session) | |
263 |
|
264 | |||
264 | validator.validate() |
|
265 | validator.validate() | |
265 | for error in validator.get_errors(): |
|
266 | for error in validator.get_errors(): | |
266 | self._add_general_error(error) |
|
267 | self._add_general_error(error) | |
267 |
|
268 | |||
268 | settings_manager.set_setting(SETTING_CONFIRMED_USER, True) |
|
269 | settings_manager.set_setting(SETTING_CONFIRMED_USER, True) | |
269 | if self.cleaned_data['download_mode'] == DOWN_MODE_DOWNLOAD_UNIQUE: |
|
270 | if self.cleaned_data['download_mode'] == DOWN_MODE_DOWNLOAD_UNIQUE: | |
270 | self._check_file_duplicates(self.get_files()) |
|
271 | self._check_file_duplicates(self.get_files()) | |
271 |
|
272 | |||
272 | return cleaned_data |
|
273 | return cleaned_data | |
273 |
|
274 | |||
274 | def get_files(self): |
|
275 | def get_files(self): | |
275 | """ |
|
276 | """ | |
276 | Gets file from form or URL. |
|
277 | Gets file from form or URL. | |
277 | """ |
|
278 | """ | |
278 |
|
279 | |||
279 | files = [] |
|
280 | files = [] | |
280 | for file in self.cleaned_data['file']: |
|
281 | for file in self.cleaned_data['file']: | |
281 | if isinstance(file, UploadedFile): |
|
282 | if isinstance(file, UploadedFile): | |
282 | files.append(file) |
|
283 | files.append(file) | |
283 |
|
284 | |||
284 | return files |
|
285 | return files | |
285 |
|
286 | |||
286 | def get_file_urls(self): |
|
287 | def get_file_urls(self): | |
287 | files = [] |
|
288 | files = [] | |
288 | for file in self.cleaned_data['file']: |
|
289 | for file in self.cleaned_data['file']: | |
289 | if type(file) == str: |
|
290 | if type(file) == str: | |
290 | files.append(file) |
|
291 | files.append(file) | |
291 |
|
292 | |||
292 | return files |
|
293 | return files | |
293 |
|
294 | |||
294 | def get_tripcode(self): |
|
295 | def get_tripcode(self): | |
295 | title = self.cleaned_data['title'] |
|
296 | title = self.cleaned_data['title'] | |
296 | if title is not None and TRIPCODE_DELIM in title: |
|
297 | if title is not None and TRIPCODE_DELIM in title: | |
297 | tripcode = get_tripcode_from_text(title.split(TRIPCODE_DELIM, maxsplit=1)[1]) |
|
298 | tripcode = get_tripcode_from_text(title.split(TRIPCODE_DELIM, maxsplit=1)[1]) | |
298 | else: |
|
299 | else: | |
299 | tripcode = '' |
|
300 | tripcode = '' | |
300 | return tripcode |
|
301 | return tripcode | |
301 |
|
302 | |||
302 | def get_title(self): |
|
303 | def get_title(self): | |
303 | title = self.cleaned_data['title'] |
|
304 | title = self.cleaned_data['title'] | |
304 | if title is not None and TRIPCODE_DELIM in title: |
|
305 | if title is not None and TRIPCODE_DELIM in title: | |
305 | return title.split(TRIPCODE_DELIM, maxsplit=1)[0] |
|
306 | return title.split(TRIPCODE_DELIM, maxsplit=1)[0] | |
306 | else: |
|
307 | else: | |
307 | return title |
|
308 | return title | |
308 |
|
309 | |||
309 | def get_images(self): |
|
310 | def get_images(self): | |
310 | return self.cleaned_data.get('stickers', []) |
|
311 | return self.cleaned_data.get('stickers', []) | |
311 |
|
312 | |||
312 | def is_subscribe(self): |
|
313 | def is_subscribe(self): | |
313 | return self.cleaned_data['subscribe'] |
|
314 | return self.cleaned_data['subscribe'] | |
314 |
|
315 | |||
315 | def _update_file_extension(self, file): |
|
316 | def _update_file_extension(self, file): | |
316 | if file: |
|
317 | if file: | |
317 | mimetype = get_file_mimetype(file) |
|
318 | mimetype = get_file_mimetype(file) | |
318 | extension = MIMETYPE_EXTENSIONS.get(mimetype) |
|
319 | extension = MIMETYPE_EXTENSIONS.get(mimetype) | |
319 | if extension: |
|
320 | if extension: | |
320 | filename = file.name.split(FILE_EXTENSION_DELIMITER, 1)[0] |
|
321 | filename = file.name.split(FILE_EXTENSION_DELIMITER, 1)[0] | |
321 | new_filename = filename + FILE_EXTENSION_DELIMITER + extension |
|
322 | new_filename = filename + FILE_EXTENSION_DELIMITER + extension | |
322 |
|
323 | |||
323 | file.name = new_filename |
|
324 | file.name = new_filename | |
324 | else: |
|
325 | else: | |
325 | logger.info('Unrecognized file mimetype: {}'.format(mimetype)) |
|
326 | logger.info('Unrecognized file mimetype: {}'.format(mimetype)) | |
326 |
|
327 | |||
327 | def _clean_files(self, inputs): |
|
328 | def _clean_files(self, inputs): | |
328 | files = [] |
|
329 | files = [] | |
329 |
|
330 | |||
330 | max_file_count = board_settings.get_int(SECTION_FORMS, 'MaxFileCount') |
|
331 | max_file_count = board_settings.get_int(SECTION_FORMS, 'MaxFileCount') | |
331 | if len(inputs) > max_file_count: |
|
332 | if len(inputs) > max_file_count: | |
332 | raise forms.ValidationError( |
|
333 | raise forms.ValidationError( | |
333 | ungettext_lazy(ERROR_MANY_FILES, ERROR_MANY_FILES, |
|
334 | ungettext_lazy(ERROR_MANY_FILES, ERROR_MANY_FILES, | |
334 | max_file_count) % {'files': max_file_count}) |
|
335 | max_file_count) % {'files': max_file_count}) | |
335 |
|
336 | |||
336 | size = 0 |
|
337 | size = 0 | |
337 | for file_input in inputs: |
|
338 | for file_input in inputs: | |
338 | if isinstance(file_input, UploadedFile): |
|
339 | if isinstance(file_input, UploadedFile): | |
339 | file = self._clean_file_file(file_input) |
|
340 | file = self._clean_file_file(file_input) | |
340 | size += file.size |
|
341 | size += file.size | |
341 | files.append(file) |
|
342 | files.append(file) | |
342 | else: |
|
343 | else: | |
343 | files.append(self._clean_file_url(file_input)) |
|
344 | files.append(self._clean_file_url(file_input)) | |
344 |
|
345 | |||
345 | for file in files: |
|
346 | for file in files: | |
346 | self._validate_image_dimensions(file) |
|
347 | self._validate_image_dimensions(file) | |
347 | validate_file_size(size) |
|
348 | validate_file_size(size) | |
348 |
|
349 | |||
349 | return files |
|
350 | return files | |
350 |
|
351 | |||
351 | def _validate_image_dimensions(self, file): |
|
352 | def _validate_image_dimensions(self, file): | |
352 | if isinstance(file, UploadedFile): |
|
353 | if isinstance(file, UploadedFile): | |
353 | mimetype = get_file_mimetype(file) |
|
354 | mimetype = get_file_mimetype(file) | |
354 | if mimetype.split('/')[-1] in FILE_TYPES_IMAGE: |
|
355 | if mimetype.split('/')[-1] in FILE_TYPES_IMAGE: | |
355 | Image.warnings.simplefilter('error', Image.DecompressionBombWarning) |
|
356 | Image.warnings.simplefilter('error', Image.DecompressionBombWarning) | |
356 | try: |
|
357 | try: | |
357 | print(get_image_dimensions(file)) |
|
358 | print(get_image_dimensions(file)) | |
358 | except Exception: |
|
359 | except Exception: | |
359 | raise forms.ValidationError('Possible decompression bomb or large image.') |
|
360 | raise forms.ValidationError('Possible decompression bomb or large image.') | |
360 |
|
361 | |||
361 | def _clean_file_file(self, file): |
|
362 | def _clean_file_file(self, file): | |
362 | self._update_file_extension(file) |
|
363 | self._update_file_extension(file) | |
363 |
|
364 | |||
364 | return file |
|
365 | return file | |
365 |
|
366 | |||
366 | def _clean_file_url(self, url): |
|
367 | def _clean_file_url(self, url): | |
367 | file = None |
|
368 | file = None | |
368 |
|
369 | |||
369 | if url: |
|
370 | if url: | |
370 | mode = self.cleaned_data['download_mode'] |
|
371 | mode = self.cleaned_data['download_mode'] | |
371 | if mode == DOWN_MODE_URL: |
|
372 | if mode == DOWN_MODE_URL: | |
372 | return url |
|
373 | return url | |
373 |
|
374 | |||
374 | try: |
|
375 | try: | |
375 | image = get_attachment_by_alias(url, self.session) |
|
376 | image = get_attachment_by_alias(url, self.session) | |
376 | if image is not None: |
|
377 | if image is not None: | |
377 | if 'stickers' not in self.cleaned_data: |
|
378 | if 'stickers' not in self.cleaned_data: | |
378 | self.cleaned_data['stickers'] = [] |
|
379 | self.cleaned_data['stickers'] = [] | |
379 | self.cleaned_data['stickers'].append(image) |
|
380 | self.cleaned_data['stickers'].append(image) | |
380 | return |
|
381 | return | |
381 |
|
382 | |||
382 | if file is None: |
|
383 | if file is None: | |
383 | file = self._get_file_from_url(url) |
|
384 | file = self._get_file_from_url(url) | |
384 | if not file: |
|
385 | if not file: | |
385 | raise forms.ValidationError(_('Invalid URL')) |
|
386 | raise forms.ValidationError(_('Invalid URL')) | |
386 | self._update_file_extension(file) |
|
387 | self._update_file_extension(file) | |
387 | except forms.ValidationError as e: |
|
388 | except forms.ValidationError as e: | |
388 | # Assume we will get the plain URL instead of a file and save it |
|
389 | # Assume we will get the plain URL instead of a file and save it | |
389 | if mode == DOWN_MODE_TRY and (REGEX_URL.match(url) or REGEX_MAGNET.match(url)): |
|
390 | if mode == DOWN_MODE_TRY and (REGEX_URL.match(url) or REGEX_MAGNET.match(url)): | |
390 | logger.info('Error in forms: {}'.format(e)) |
|
391 | logger.info('Error in forms: {}'.format(e)) | |
391 | return url |
|
392 | return url | |
392 | else: |
|
393 | else: | |
393 | raise e |
|
394 | raise e | |
394 |
|
395 | |||
395 | return file |
|
396 | return file | |
396 |
|
397 | |||
397 | def _clean_text_file(self): |
|
398 | def _clean_text_file(self): | |
398 | text = self.cleaned_data.get('text') |
|
399 | text = self.cleaned_data.get('text') | |
399 | file = self.get_files() |
|
400 | file = self.get_files() | |
400 | file_url = self.get_file_urls() |
|
401 | file_url = self.get_file_urls() | |
401 | images = self.get_images() |
|
402 | images = self.get_images() | |
402 |
|
403 | |||
403 | if (not text) and (not file) and (not file_url) and len(images) == 0: |
|
404 | if (not text) and (not file) and (not file_url) and len(images) == 0: | |
404 | error_message = _('Either text or file must be entered.') |
|
405 | error_message = _('Either text or file must be entered.') | |
405 | self._add_general_error(error_message) |
|
406 | self._add_general_error(error_message) | |
406 |
|
407 | |||
407 | def _get_file_from_url(self, url: str) -> SimpleUploadedFile: |
|
408 | def _get_file_from_url(self, url: str) -> SimpleUploadedFile: | |
408 | """ |
|
409 | """ | |
409 | Gets an file file from URL. |
|
410 | Gets an file file from URL. | |
410 | """ |
|
411 | """ | |
411 |
|
412 | |||
412 | try: |
|
413 | try: | |
413 | return download(url) |
|
414 | return download(url) | |
414 | except forms.ValidationError as e: |
|
415 | except forms.ValidationError as e: | |
415 | raise e |
|
416 | raise e | |
416 | except Exception as e: |
|
417 | except Exception as e: | |
417 | raise forms.ValidationError(e) |
|
418 | raise forms.ValidationError(e) | |
418 |
|
419 | |||
419 | def _check_file_duplicates(self, files): |
|
420 | def _check_file_duplicates(self, files): | |
420 | for file in files: |
|
421 | for file in files: | |
421 | file_hash = utils.get_file_hash(file) |
|
422 | file_hash = utils.get_file_hash(file) | |
422 | if Attachment.objects.get_existing_duplicate(file_hash, file): |
|
423 | if Attachment.objects.get_existing_duplicate(file_hash, file): | |
423 | self._add_general_error(_(ERROR_DUPLICATES)) |
|
424 | self._add_general_error(_(ERROR_DUPLICATES)) | |
424 |
|
425 | |||
425 | def _add_general_error(self, message): |
|
426 | def _add_general_error(self, message): | |
426 | self.add_error('text', forms.ValidationError(message)) |
|
427 | self.add_error('text', forms.ValidationError(message)) | |
427 |
|
428 | |||
428 |
|
429 | |||
429 | class ThreadForm(PostForm): |
|
430 | class ThreadForm(PostForm): | |
430 |
|
431 | |||
431 | tags = forms.CharField( |
|
432 | tags = forms.CharField( | |
432 | widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}), |
|
433 | widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}), | |
433 | max_length=100, label=_('Tags'), required=True) |
|
434 | max_length=100, label=_('Tags'), required=True) | |
434 | monochrome = forms.BooleanField(label=_('Monochrome'), required=False) |
|
435 | monochrome = forms.BooleanField(label=_('Monochrome'), required=False) | |
435 | stickerpack = forms.BooleanField(label=_('Sticker Pack'), required=False) |
|
436 | stickerpack = forms.BooleanField(label=_('Sticker Pack'), required=False) | |
436 |
|
437 | |||
437 | def clean_tags(self): |
|
438 | def clean_tags(self): | |
438 | tags = self.cleaned_data['tags'].strip() |
|
439 | tags = self.cleaned_data['tags'].strip() | |
439 |
|
440 | |||
440 | if not tags or not REGEX_TAGS.match(tags): |
|
441 | if not tags or not REGEX_TAGS.match(tags): | |
441 | raise forms.ValidationError( |
|
442 | raise forms.ValidationError( | |
442 | _('Inappropriate characters in tags.')) |
|
443 | _('Inappropriate characters in tags.')) | |
443 |
|
444 | |||
444 | default_tag_name = board_settings.get(SECTION_FORMS, 'DefaultTag')\ |
|
445 | default_tag_name = board_settings.get(SECTION_FORMS, 'DefaultTag')\ | |
445 | .strip().lower() |
|
446 | .strip().lower() | |
446 |
|
447 | |||
447 | required_tag_exists = False |
|
448 | required_tag_exists = False | |
448 | tag_set = set() |
|
449 | tag_set = set() | |
449 | for tag_string in tags.split(): |
|
450 | for tag_string in tags.split(): | |
450 | tag_name = tag_string.strip().lower() |
|
451 | tag_name = tag_string.strip().lower() | |
451 | if tag_name == default_tag_name: |
|
452 | if tag_name == default_tag_name: | |
452 | required_tag_exists = True |
|
453 | required_tag_exists = True | |
453 | tag, created = Tag.objects.get_or_create_with_alias( |
|
454 | tag, created = Tag.objects.get_or_create_with_alias( | |
454 | name=tag_name, required=True) |
|
455 | name=tag_name, required=True) | |
455 | else: |
|
456 | else: | |
456 | tag, created = Tag.objects.get_or_create_with_alias(name=tag_name) |
|
457 | tag, created = Tag.objects.get_or_create_with_alias(name=tag_name) | |
457 | tag_set.add(tag) |
|
458 | tag_set.add(tag) | |
458 |
|
459 | |||
459 | # If this is a new tag, don't check for its parents because nobody |
|
460 | # If this is a new tag, don't check for its parents because nobody | |
460 | # added them yet |
|
461 | # added them yet | |
461 | if not created: |
|
462 | if not created: | |
462 | tag_set |= set(tag.get_all_parents()) |
|
463 | tag_set |= set(tag.get_all_parents()) | |
463 |
|
464 | |||
464 | for tag in tag_set: |
|
465 | for tag in tag_set: | |
465 | if tag.required: |
|
466 | if tag.required: | |
466 | required_tag_exists = True |
|
467 | required_tag_exists = True | |
467 | break |
|
468 | break | |
468 |
|
469 | |||
469 | # Use default tag if no section exists |
|
470 | # Use default tag if no section exists | |
470 | if not required_tag_exists: |
|
471 | if not required_tag_exists: | |
471 | default_tag, created = Tag.objects.get_or_create_with_alias( |
|
472 | default_tag, created = Tag.objects.get_or_create_with_alias( | |
472 | name=default_tag_name, required=True) |
|
473 | name=default_tag_name, required=True) | |
473 | tag_set.add(default_tag) |
|
474 | tag_set.add(default_tag) | |
474 |
|
475 | |||
475 | return tag_set |
|
476 | return tag_set | |
476 |
|
477 | |||
477 | def clean(self): |
|
478 | def clean(self): | |
478 | cleaned_data = super(ThreadForm, self).clean() |
|
479 | cleaned_data = super(ThreadForm, self).clean() | |
479 |
|
480 | |||
480 | return cleaned_data |
|
481 | return cleaned_data | |
481 |
|
482 | |||
482 | def is_monochrome(self): |
|
483 | def is_monochrome(self): | |
483 | return self.cleaned_data['monochrome'] |
|
484 | return self.cleaned_data['monochrome'] | |
484 |
|
485 | |||
485 | def clean_stickerpack(self): |
|
486 | def clean_stickerpack(self): | |
486 | stickerpack = self.cleaned_data['stickerpack'] |
|
487 | stickerpack = self.cleaned_data['stickerpack'] | |
487 | if stickerpack: |
|
488 | if stickerpack: | |
488 | tripcode = self.get_tripcode() |
|
489 | tripcode = self.get_tripcode() | |
489 | if not tripcode: |
|
490 | if not tripcode: | |
490 | raise forms.ValidationError(_( |
|
491 | raise forms.ValidationError(_( | |
491 | 'Tripcode should be specified to own a stickerpack.')) |
|
492 | 'Tripcode should be specified to own a stickerpack.')) | |
492 | title = self.get_title() |
|
493 | title = self.get_title() | |
493 | if not title: |
|
494 | if not title: | |
494 | raise forms.ValidationError(_( |
|
495 | raise forms.ValidationError(_( | |
495 | 'Title should be specified as a stickerpack name.')) |
|
496 | 'Title should be specified as a stickerpack name.')) | |
496 | if not REGEX_TAGS.match(title): |
|
497 | if not REGEX_TAGS.match(title): | |
497 | raise forms.ValidationError(_('Inappropriate sticker pack name.')) |
|
498 | raise forms.ValidationError(_('Inappropriate sticker pack name.')) | |
498 |
|
499 | |||
499 | existing_pack = StickerPack.objects.filter(name=title).first() |
|
500 | existing_pack = StickerPack.objects.filter(name=title).first() | |
500 | if existing_pack: |
|
501 | if existing_pack: | |
501 | if existing_pack.tripcode != tripcode: |
|
502 | if existing_pack.tripcode != tripcode: | |
502 | raise forms.ValidationError(_( |
|
503 | raise forms.ValidationError(_( | |
503 | 'A sticker pack with this name already exists and is' |
|
504 | 'A sticker pack with this name already exists and is' | |
504 | ' owned by another tripcode.')) |
|
505 | ' owned by another tripcode.')) | |
505 | if not existing_pack.tripcode: |
|
506 | if not existing_pack.tripcode: | |
506 | raise forms.ValidationError(_( |
|
507 | raise forms.ValidationError(_( | |
507 | 'This sticker pack can only be updated by an ' |
|
508 | 'This sticker pack can only be updated by an ' | |
508 | 'administrator.')) |
|
509 | 'administrator.')) | |
509 |
|
510 | |||
510 | return stickerpack |
|
511 | return stickerpack | |
511 |
|
512 | |||
512 | def is_stickerpack(self): |
|
513 | def is_stickerpack(self): | |
513 | return self.cleaned_data['stickerpack'] |
|
514 | return self.cleaned_data['stickerpack'] | |
514 |
|
515 | |||
515 |
|
516 | |||
516 | class SettingsForm(NeboardForm): |
|
517 | class SettingsForm(NeboardForm): | |
517 |
|
518 | |||
518 | theme = forms.ChoiceField( |
|
519 | theme = forms.ChoiceField( | |
519 | choices=board_settings.get_list_dict('View', 'Themes'), |
|
520 | choices=board_settings.get_list_dict('View', 'Themes'), | |
520 | label=_('Theme')) |
|
521 | label=_('Theme')) | |
521 | image_viewer = forms.ChoiceField( |
|
522 | image_viewer = forms.ChoiceField( | |
522 | choices=board_settings.get_list_dict('View', 'ImageViewers'), |
|
523 | choices=board_settings.get_list_dict('View', 'ImageViewers'), | |
523 | label=_('Image view mode')) |
|
524 | label=_('Image view mode')) | |
524 | username = forms.CharField(label=_('User name'), required=False) |
|
525 | username = forms.CharField(label=_('User name'), required=False) | |
525 | timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone')) |
|
526 | timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone')) | |
526 |
|
527 | |||
527 | def clean_username(self): |
|
528 | def clean_username(self): | |
528 | username = self.cleaned_data['username'] |
|
529 | username = self.cleaned_data['username'] | |
529 |
|
530 | |||
530 | if username and not REGEX_USERNAMES.match(username): |
|
531 | if username and not REGEX_USERNAMES.match(username): | |
531 | raise forms.ValidationError(_('Inappropriate characters.')) |
|
532 | raise forms.ValidationError(_('Inappropriate characters.')) | |
532 |
|
533 | |||
533 | return username |
|
534 | return username | |
534 |
|
535 | |||
535 |
|
536 | |||
536 | class SearchForm(NeboardForm): |
|
537 | class SearchForm(NeboardForm): | |
537 | query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False) |
|
538 | query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False) |
@@ -1,329 +1,328 b'' | |||||
1 | var ITEM_FILE_SOURCE = 'fileSource'; |
|
1 | var ITEM_FILE_SOURCE = 'fileSource'; | |
2 | var URL_STICKERS = '/api/stickers'; |
|
2 | var URL_STICKERS = '/api/stickers'; | |
3 | var MIN_INPUT_LENGTH = 3; |
|
3 | var MIN_INPUT_LENGTH = 3; | |
4 | var URL_DELIMITER = '\n'; |
|
4 | var URL_DELIMITER = '\n'; | |
5 | // TODO This needs to be the same for attachment download time limit. |
|
5 | // TODO This needs to be the same for attachment download time limit. | |
6 | var POST_AJAX_TIMEOUT = 30000; |
|
6 | var POST_AJAX_TIMEOUT = 30000; | |
7 | var REPLY_TO_MSG = '.reply-to-message'; |
|
7 | var REPLY_TO_MSG = '.reply-to-message'; | |
8 |
|
8 | |||
9 | var pastedImages = []; |
|
9 | var pastedImages = []; | |
10 |
|
10 | |||
11 | $('input[name=image]').wrap($('<div class="file_wrap"></div>')); |
|
11 | $('input[name=image]').wrap($('<div class="file_wrap"></div>')); | |
12 |
|
12 | |||
13 | $('body').on('change', 'input[name=image]', function(event) { |
|
13 | $('body').on('change', 'input[name=image]', function(event) { | |
14 | var file = event.target.files[0]; |
|
14 | var file = event.target.files[0]; | |
15 |
|
15 | |||
16 | if(file.type.match('image.*')) { |
|
16 | if(file.type.match('image.*')) { | |
17 | var fileReader = new FileReader(); |
|
17 | var fileReader = new FileReader(); | |
18 |
|
18 | |||
19 | fileReader.addEventListener("load", function(event) { |
|
19 | fileReader.addEventListener("load", function(event) { | |
20 | var wrapper = $('.file_wrap'); |
|
20 | var wrapper = $('.file_wrap'); | |
21 |
|
21 | |||
22 | wrapper.find('.file-thumb').remove(); |
|
22 | wrapper.find('.file-thumb').remove(); | |
23 | wrapper.append( |
|
23 | wrapper.append( | |
24 | $('<div class="file-thumb" style="background-image: url('+event.target.result+')"></div>') |
|
24 | $('<div class="file-thumb" style="background-image: url('+event.target.result+')"></div>') | |
25 | ); |
|
25 | ); | |
26 | }); |
|
26 | }); | |
27 |
|
27 | |||
28 | fileReader.readAsDataURL(file); |
|
28 | fileReader.readAsDataURL(file); | |
29 | } |
|
29 | } | |
30 | }); |
|
30 | }); | |
31 |
|
31 | |||
32 | var form = $('#form'); |
|
32 | var form = $('#form'); | |
33 | $('textarea').keypress(function(event) { |
|
33 | $('textarea').keypress(function(event) { | |
34 | if ((event.which == 10 || event.which == 13) && event.ctrlKey) { |
|
34 | if ((event.which == 10 || event.which == 13) && event.ctrlKey) { | |
35 | form.find('input[type=submit]').click(); |
|
35 | form.find('input[type=submit]').click(); | |
36 | } |
|
36 | } | |
37 | }); |
|
37 | }); | |
38 |
|
38 | |||
39 | $('#preview-button').click(function() { |
|
39 | $('#preview-button').click(function() { | |
40 | var data = { |
|
40 | var data = { | |
41 | raw_text: $('textarea#id_text').val() |
|
41 | raw_text: $('textarea#id_text').val() | |
42 | } |
|
42 | } | |
43 |
|
43 | |||
44 | var diffUrl = '/api/preview/'; |
|
44 | var diffUrl = '/api/preview/'; | |
45 |
|
45 | |||
46 | $.post(diffUrl, |
|
46 | $.post(diffUrl, | |
47 | data, |
|
47 | data, | |
48 | function(data) { |
|
48 | function(data) { | |
49 | var previewTextBlock = $('#preview-text'); |
|
49 | var previewTextBlock = $('#preview-text'); | |
50 | previewTextBlock.html(data); |
|
50 | previewTextBlock.html(data); | |
51 | previewTextBlock.show(); |
|
51 | previewTextBlock.show(); | |
52 |
|
52 | |||
53 | addScriptsToPost(previewTextBlock); |
|
53 | addScriptsToPost(previewTextBlock); | |
54 | }) |
|
54 | }) | |
55 | }); |
|
55 | }); | |
56 |
|
56 | |||
57 | /** |
|
57 | /** | |
58 | * Show text in the errors row of the form. |
|
58 | * Show text in the errors row of the form. | |
59 | * @param form |
|
59 | * @param form | |
60 | * @param text |
|
60 | * @param text | |
61 | */ |
|
61 | */ | |
62 | function showAsErrors(form, text) { |
|
62 | function showAsErrors(form, text) { | |
63 | form.children('.form-errors').remove(); |
|
63 | form.children('.form-errors').remove(); | |
64 |
|
64 | |||
65 | if (text.length > 0) { |
|
65 | if (text.length > 0) { | |
66 | var errorList = $('<div class="form-errors">' + text + '<div>'); |
|
66 | var errorList = $('<div class="form-errors">' + text + '<div>'); | |
67 | errorList.appendTo(form); |
|
67 | errorList.appendTo(form); | |
68 | } |
|
68 | } | |
69 | } |
|
69 | } | |
70 |
|
70 | |||
71 | function addHiddenInput(form, name, value) { |
|
71 | function addHiddenInput(form, name, value) { | |
72 | form.find('input[name=' + name + ']').val(value); |
|
72 | form.find('input[name=' + name + ']').val(value); | |
73 | } |
|
73 | } | |
74 |
|
74 | |||
75 | function selectFileChoice() { |
|
75 | function selectFileChoice() { | |
76 | var file_input = $('#id_file'); |
|
76 | var file_input = $('#id_file'); | |
77 | var url_input = $('#id_file_url'); |
|
77 | var url_input = $('#id_file_url'); | |
78 |
|
78 | |||
79 | var file_input_row = file_input.parent().parent(); |
|
79 | var file_input_row = file_input.parent().parent(); | |
80 | var url_input_row = url_input.parent().parent(); |
|
80 | var url_input_row = url_input.parent().parent(); | |
81 |
|
81 | |||
82 | file_input_row.toggle(); |
|
82 | file_input_row.toggle(); | |
83 | url_input_row.toggle(); |
|
83 | url_input_row.toggle(); | |
84 | url_input.val(''); |
|
84 | url_input.val(''); | |
85 | file_input.val(''); |
|
85 | file_input.val(''); | |
86 |
|
86 | |||
87 | var source; |
|
87 | var source; | |
88 | if (file_input_row.is(':visible')) { |
|
88 | if (file_input_row.is(':visible')) { | |
89 | source = 'file'; |
|
89 | source = 'file'; | |
90 | } else { |
|
90 | } else { | |
91 | source = 'url'; |
|
91 | source = 'url'; | |
92 | } |
|
92 | } | |
93 | localStorage.setItem(ITEM_FILE_SOURCE, source); |
|
93 | localStorage.setItem(ITEM_FILE_SOURCE, source); | |
94 | } |
|
94 | } | |
95 |
|
95 | |||
96 | function getPostTextarea() { |
|
96 | function getPostTextarea() { | |
97 | return $('textarea#id_text'); |
|
97 | return $('textarea#id_text'); | |
98 | } |
|
98 | } | |
99 |
|
99 | |||
100 | function addOnImagePaste() { |
|
100 | function addOnImagePaste() { | |
101 | $('#id_file_1').on('paste', function(event) { |
|
101 | $('#id_file_1').on('paste', function(event) { | |
102 | var items = (event.clipboardData || event.originalEvent.clipboardData).items; |
|
102 | var items = (event.clipboardData || event.originalEvent.clipboardData).items; | |
103 | for (index in items) { |
|
103 | for (index in items) { | |
104 | var item = items[index]; |
|
104 | var item = items[index]; | |
105 | if (item.kind === 'file') { |
|
105 | if (item.kind === 'file') { | |
106 | var blob = item.getAsFile(); |
|
106 | var blob = item.getAsFile(); | |
107 |
|
107 | |||
108 | pastedImages.push(blob); |
|
108 | pastedImages.push(blob); | |
109 |
|
109 | |||
110 | var pastedImagesList = $('#pasted-images'); |
|
110 | var pastedImagesList = $('#pasted-images'); | |
111 | if (pastedImagesList.length === 0) { |
|
111 | if (pastedImagesList.length === 0) { | |
112 | pastedImagesList = $('<div id="pasted-images" />'); |
|
112 | pastedImagesList = $('<div id="pasted-images" />'); | |
113 | $('#id_file_1').parent().append(pastedImagesList); |
|
113 | $('#id_file_1').parent().append(pastedImagesList); | |
114 | } |
|
114 | } | |
115 |
|
115 | |||
116 | var fr = new FileReader(); |
|
116 | var fr = new FileReader(); | |
117 | fr.onload = function () { |
|
117 | fr.onload = function () { | |
118 | var img = $('<img class="image-preview" />'); |
|
118 | var img = $('<img class="image-preview" />'); | |
119 | img.attr('src', fr.result); |
|
119 | img.attr('src', fr.result); | |
120 | pastedImagesList.append(img); |
|
120 | pastedImagesList.append(img); | |
121 | img.on("click", function() { |
|
121 | img.on("click", function() { | |
122 | // Remove the image from all lists |
|
122 | // Remove the image from all lists | |
123 | var itemIndex = $(this).index(); |
|
123 | var itemIndex = $(this).index(); | |
124 | pastedImages.splice(itemIndex, 1); |
|
124 | pastedImages.splice(itemIndex, 1); | |
125 | $(this).remove(); |
|
125 | $(this).remove(); | |
126 | }); |
|
126 | }); | |
127 | }; |
|
127 | }; | |
128 | fr.readAsDataURL(blob); |
|
128 | fr.readAsDataURL(blob); | |
129 | } |
|
129 | } | |
130 | } |
|
130 | } | |
131 | }); |
|
131 | }); | |
132 | } |
|
132 | } | |
133 |
|
133 | |||
134 | /** |
|
134 | /** | |
135 | * When the form is posted, this method will be run as a callback |
|
135 | * When the form is posted, this method will be run as a callback | |
136 | */ |
|
136 | */ | |
137 | function updateOnPost(response, statusText, xhr, form) { |
|
137 | function updateOnPost(response, statusText, xhr, form) { | |
138 | var json = $.parseJSON(response); |
|
138 | var json = $.parseJSON(response); | |
139 | var status = json.status; |
|
139 | var status = json.status; | |
140 | var url = json.url; |
|
140 | var url = json.url; | |
141 |
|
141 | |||
142 | showAsErrors(form, ''); |
|
142 | showAsErrors(form, ''); | |
143 | $('.post-form-w').unblock(); |
|
143 | $('.post-form-w').unblock(); | |
144 |
|
144 | |||
145 | if (status === 'ok') { |
|
145 | if (status === 'ok') { | |
146 | if (url) { |
|
146 | if (url) { | |
147 | document.location = url; |
|
147 | document.location = url; | |
148 | } else { |
|
148 | } else { | |
149 | resetForm(); |
|
149 | resetForm(); | |
150 | getThreadDiff(); |
|
150 | getThreadDiff(); | |
151 | scrollToBottom(); |
|
151 | scrollToBottom(); | |
152 | } |
|
152 | } | |
153 | } else { |
|
153 | } else { | |
154 | var errors = json.errors; |
|
154 | var errors = json.errors; | |
155 | for (var i = 0; i < errors.length; i++) { |
|
155 | for (var i = 0; i < errors.length; i++) { | |
156 | var fieldErrors = errors[i]; |
|
156 | var fieldErrors = errors[i]; | |
157 |
|
157 | |||
158 | var error = fieldErrors.errors; |
|
158 | var error = fieldErrors.errors; | |
159 |
|
159 | |||
160 | showAsErrors(form, error); |
|
160 | showAsErrors(form, error); | |
161 | } |
|
161 | } | |
162 | } |
|
162 | } | |
163 | } |
|
163 | } | |
164 |
|
164 | |||
165 | function initAjaxForm(openingPostId) { |
|
165 | function initAjaxForm(openingPostId) { | |
166 | var form = $('#form'); |
|
166 | var form = $('#form'); | |
167 |
|
167 | |||
168 | var url = '/api/add_post/'; |
|
168 | var url = '/api/add_post/'; | |
169 | if (openingPostId) { |
|
169 | if (openingPostId) { | |
170 | url += openingPostId + '/'; |
|
170 | url += openingPostId + '/'; | |
171 | } |
|
171 | } | |
172 |
|
172 | |||
173 | if (form.length > 0) { |
|
173 | if (form.length > 0) { | |
174 | var options = { |
|
174 | var options = { | |
175 | beforeSubmit: function(arr, form, options) { |
|
175 | beforeSubmit: function(arr, form, options) { | |
176 | $('.post-form-w').block({ message: gettext('Sending message...') }); |
|
176 | $('.post-form-w').block({ message: gettext('Sending message...') }); | |
177 |
|
177 | |||
178 | $.each(pastedImages, function(i, blob) { |
|
178 | $.each(pastedImages, function(i, blob) { | |
179 | arr.push({ |
|
179 | arr.push({ | |
180 | name: "file_0", |
|
180 | name: "file_0", | |
181 | value: blob |
|
181 | value: blob | |
182 | }); |
|
182 | }); | |
183 | }); |
|
183 | }); | |
184 | }, |
|
184 | }, | |
185 | success: updateOnPost, |
|
185 | success: updateOnPost, | |
186 | error: function(xhr, textStatus, errorString) { |
|
186 | error: function(xhr, textStatus, errorString) { | |
187 | var errorText = gettext('Server error: ') + textStatus; |
|
187 | var errorText = gettext('Server error: ') + textStatus; | |
188 | if (errorString) { |
|
188 | if (errorString) { | |
189 | errorText += ' / ' + errorString; |
|
189 | errorText += ' / ' + errorString; | |
190 | } |
|
190 | } | |
191 | showAsErrors(form, errorText); |
|
191 | showAsErrors(form, errorText); | |
192 | $('.post-form-w').unblock(); |
|
192 | $('.post-form-w').unblock(); | |
193 | }, |
|
193 | }, | |
194 | url: url, |
|
194 | url: url, | |
195 | timeout: POST_AJAX_TIMEOUT |
|
195 | timeout: POST_AJAX_TIMEOUT | |
196 | }; |
|
196 | }; | |
197 |
|
197 | |||
198 | form.ajaxForm(options); |
|
198 | form.ajaxForm(options); | |
199 | } |
|
199 | } | |
200 | } |
|
200 | } | |
201 |
|
201 | |||
202 | function getForm() { |
|
202 | function getForm() { | |
203 | return $('.post-form-w'); |
|
203 | return $('.post-form-w'); | |
204 | } |
|
204 | } | |
205 |
|
205 | |||
206 | /** |
|
206 | /** | |
207 | * Clear all entered values in the form fields |
|
207 | * Clear all entered values in the form fields | |
208 | */ |
|
208 | */ | |
209 | function resetForm() { |
|
209 | function resetForm() { | |
210 | var form = getForm(); |
|
210 | var form = getForm(); | |
211 |
|
211 | |||
212 | form.find('input:text, input:password, input:file, textarea').val(''); |
|
212 | form.find('input:text, input:password, input:file, textarea').val(''); | |
213 | form.find('input:radio, input:checkbox').removeAttr('checked').removeAttr('selected'); |
|
|||
214 | pastedImages = []; |
|
213 | pastedImages = []; | |
215 | $('#pasted-images').remove(); |
|
214 | $('#pasted-images').remove(); | |
216 | $('.file_wrap').find('.file-thumb').remove(); |
|
215 | $('.file_wrap').find('.file-thumb').remove(); | |
217 | $('#preview-text').hide(); |
|
216 | $('#preview-text').hide(); | |
218 |
|
217 | |||
219 | resetFormPosition(form); |
|
218 | resetFormPosition(form); | |
220 | } |
|
219 | } | |
221 |
|
220 | |||
222 | function resetFormPosition(form) { |
|
221 | function resetFormPosition(form) { | |
223 | form.insertBefore($('footer')); |
|
222 | form.insertBefore($('footer')); | |
224 |
|
223 | |||
225 | $(REPLY_TO_MSG).hide(); |
|
224 | $(REPLY_TO_MSG).hide(); | |
226 | } |
|
225 | } | |
227 |
|
226 | |||
228 | $(document).ready(function() { |
|
227 | $(document).ready(function() { | |
229 | var powDifficulty = parseInt($('body').attr('data-pow-difficulty')); |
|
228 | var powDifficulty = parseInt($('body').attr('data-pow-difficulty')); | |
230 | if (powDifficulty > 0 && typeof SharedWorker != 'undefined') { |
|
229 | if (powDifficulty > 0 && typeof SharedWorker != 'undefined') { | |
231 | var worker = new SharedWorker($('.post-form').attr('data-pow-script')); |
|
230 | var worker = new SharedWorker($('.post-form').attr('data-pow-script')); | |
232 | worker.port.onmessage = function(e) { |
|
231 | worker.port.onmessage = function(e) { | |
233 | var form = $('#form'); |
|
232 | var form = $('#form'); | |
234 | addHiddenInput(form, 'timestamp', e.data.timestamp); |
|
233 | addHiddenInput(form, 'timestamp', e.data.timestamp); | |
235 | addHiddenInput(form, 'iteration', e.data.iteration); |
|
234 | addHiddenInput(form, 'iteration', e.data.iteration); | |
236 | addHiddenInput(form, 'guess', e.data.guess); |
|
235 | addHiddenInput(form, 'guess', e.data.guess); | |
237 |
|
236 | |||
238 | form.submit(); |
|
237 | form.submit(); | |
239 | $('.post-form-w').unblock(); |
|
238 | $('.post-form-w').unblock(); | |
240 | }; |
|
239 | }; | |
241 | worker.onerror = function(event){ |
|
240 | worker.onerror = function(event){ | |
242 | throw new Error(event.message + " (" + event.filename + ":" + event.lineno + ")"); |
|
241 | throw new Error(event.message + " (" + event.filename + ":" + event.lineno + ")"); | |
243 | }; |
|
242 | }; | |
244 | worker.port.start(); |
|
243 | worker.port.start(); | |
245 |
|
244 | |||
246 | var form = $('#form'); |
|
245 | var form = $('#form'); | |
247 | var submitButton = form.find('input[type=submit]'); |
|
246 | var submitButton = form.find('input[type=submit]'); | |
248 | submitButton.click(function() { |
|
247 | submitButton.click(function() { | |
249 | showAsErrors(form, gettext('Computing PoW...')); |
|
248 | showAsErrors(form, gettext('Computing PoW...')); | |
250 | $('.post-form-w').block({ message: gettext('Computing PoW...') }) |
|
249 | $('.post-form-w').block({ message: gettext('Computing PoW...') }) | |
251 |
|
250 | |||
252 | var msg = $('textarea#id_text').val().trim(); |
|
251 | var msg = $('textarea#id_text').val().trim(); | |
253 |
|
252 | |||
254 | var data = { |
|
253 | var data = { | |
255 | msg: msg, |
|
254 | msg: msg, | |
256 | difficulty: parseInt($('body').attr('data-pow-difficulty')), |
|
255 | difficulty: parseInt($('body').attr('data-pow-difficulty')), | |
257 | hasher: $('.post-form').attr('data-hasher') |
|
256 | hasher: $('.post-form').attr('data-hasher') | |
258 | }; |
|
257 | }; | |
259 | worker.port.postMessage(data); |
|
258 | worker.port.postMessage(data); | |
260 |
|
259 | |||
261 | return false; |
|
260 | return false; | |
262 | }); |
|
261 | }); | |
263 | } |
|
262 | } | |
264 |
|
263 | |||
265 | var $fileSourceButton = $('#file-source-button'); |
|
264 | var $fileSourceButton = $('#file-source-button'); | |
266 | if (window.localStorage) { |
|
265 | if (window.localStorage) { | |
267 | var source = localStorage.getItem(ITEM_FILE_SOURCE); |
|
266 | var source = localStorage.getItem(ITEM_FILE_SOURCE); | |
268 | if (source == null) { |
|
267 | if (source == null) { | |
269 | source = 'file'; |
|
268 | source = 'file'; | |
270 | } |
|
269 | } | |
271 | if (source == 'file') { |
|
270 | if (source == 'file') { | |
272 | $('#id_file_url').parent().parent().hide(); |
|
271 | $('#id_file_url').parent().parent().hide(); | |
273 | } else { |
|
272 | } else { | |
274 | $('#id_file').parent().parent().hide(); |
|
273 | $('#id_file').parent().parent().hide(); | |
275 | } |
|
274 | } | |
276 |
|
275 | |||
277 | $fileSourceButton.click(function() { |
|
276 | $fileSourceButton.click(function() { | |
278 | selectFileChoice(); |
|
277 | selectFileChoice(); | |
279 | }); |
|
278 | }); | |
280 | } else { |
|
279 | } else { | |
281 | $fileSourceButton.hide(); |
|
280 | $fileSourceButton.hide(); | |
282 | } |
|
281 | } | |
283 |
|
282 | |||
284 | addOnImagePaste(); |
|
283 | addOnImagePaste(); | |
285 |
|
284 | |||
286 | // Stickers autocomplete |
|
285 | // Stickers autocomplete | |
287 | function split( val ) { |
|
286 | function split( val ) { | |
288 | return val.split(URL_DELIMITER); |
|
287 | return val.split(URL_DELIMITER); | |
289 | } |
|
288 | } | |
290 |
|
289 | |||
291 | function extractLast( term ) { |
|
290 | function extractLast( term ) { | |
292 | return split(term).pop(); |
|
291 | return split(term).pop(); | |
293 | } |
|
292 | } | |
294 |
|
293 | |||
295 | $('#id_file_1').autocomplete({ |
|
294 | $('#id_file_1').autocomplete({ | |
296 | source: function( request, response ) { |
|
295 | source: function( request, response ) { | |
297 | $.getJSON(URL_STICKERS, { |
|
296 | $.getJSON(URL_STICKERS, { | |
298 | term: extractLast( request.term ) |
|
297 | term: extractLast( request.term ) | |
299 | }, response); |
|
298 | }, response); | |
300 | }, |
|
299 | }, | |
301 | search: function() { |
|
300 | search: function() { | |
302 | // custom minLength |
|
301 | // custom minLength | |
303 | var term = extractLast( this.value ); |
|
302 | var term = extractLast( this.value ); | |
304 | if (term.length < MIN_INPUT_LENGTH) { |
|
303 | if (term.length < MIN_INPUT_LENGTH) { | |
305 | return false; |
|
304 | return false; | |
306 | } |
|
305 | } | |
307 | }, |
|
306 | }, | |
308 | focus: function() { |
|
307 | focus: function() { | |
309 | // prevent value inserted on focus |
|
308 | // prevent value inserted on focus | |
310 | return false; |
|
309 | return false; | |
311 | }, |
|
310 | }, | |
312 | select: function( event, ui ) { |
|
311 | select: function( event, ui ) { | |
313 | var terms = split( this.value ); |
|
312 | var terms = split( this.value ); | |
314 | // remove the current input |
|
313 | // remove the current input | |
315 | terms.pop(); |
|
314 | terms.pop(); | |
316 | // add the selected item |
|
315 | // add the selected item | |
317 | terms.push( ui.item.alias ); |
|
316 | terms.push( ui.item.alias ); | |
318 | // add placeholder to get the comma-and-space at the end |
|
317 | // add placeholder to get the comma-and-space at the end | |
319 | terms.push(""); |
|
318 | terms.push(""); | |
320 | this.value = terms.join(URL_DELIMITER); |
|
319 | this.value = terms.join(URL_DELIMITER); | |
321 | return false; |
|
320 | return false; | |
322 | } |
|
321 | } | |
323 | }) |
|
322 | }) | |
324 | .autocomplete( "instance" )._renderItem = function( ul, item ) { |
|
323 | .autocomplete( "instance" )._renderItem = function( ul, item ) { | |
325 | return $( "<li>" ) |
|
324 | return $( "<li>" ) | |
326 | .append( "<div>" + '<img src="' + item.thumb + '">' + '<br />' + item.alias + "</div>" ) |
|
325 | .append( "<div>" + '<img src="' + item.thumb + '">' + '<br />' + item.alias + "</div>" ) | |
327 | .appendTo( ul ); |
|
326 | .appendTo( ul ); | |
328 | }; |
|
327 | }; | |
329 | }); |
|
328 | }); |
General Comments 0
You need to be logged in to leave comments.
Login now