##// END OF EJS Templates
Show moderator controls when downloading a single post over API. Removed duplicate code from get_post_data
neko259 -
r1109:d995bcab default
parent child Browse files
Show More
@@ -1,68 +1,59 b''
1 from boards.abstracts.settingsmanager import get_settings_manager, \
1 from boards.abstracts.settingsmanager import get_settings_manager, \
2 SETTING_USERNAME, SETTING_LAST_NOTIFICATION_ID
2 SETTING_USERNAME, SETTING_LAST_NOTIFICATION_ID
3 from boards.models.user import Notification
3 from boards.models.user import Notification
4
4
5 __author__ = 'neko259'
5 __author__ = 'neko259'
6
6
7 from boards import settings
7 from boards import settings, utils
8 from boards.models import Post, Tag
8 from boards.models import Post, Tag
9
9
10 CONTEXT_SITE_NAME = 'site_name'
10 CONTEXT_SITE_NAME = 'site_name'
11 CONTEXT_VERSION = 'version'
11 CONTEXT_VERSION = 'version'
12 CONTEXT_MODERATOR = 'moderator'
12 CONTEXT_MODERATOR = 'moderator'
13 CONTEXT_THEME_CSS = 'theme_css'
13 CONTEXT_THEME_CSS = 'theme_css'
14 CONTEXT_THEME = 'theme'
14 CONTEXT_THEME = 'theme'
15 CONTEXT_PPD = 'posts_per_day'
15 CONTEXT_PPD = 'posts_per_day'
16 CONTEXT_TAGS = 'tags'
16 CONTEXT_TAGS = 'tags'
17 CONTEXT_USER = 'user'
17 CONTEXT_USER = 'user'
18 CONTEXT_NEW_NOTIFICATIONS_COUNT = 'new_notifications_count'
18 CONTEXT_NEW_NOTIFICATIONS_COUNT = 'new_notifications_count'
19 CONTEXT_USERNAME = 'username'
19 CONTEXT_USERNAME = 'username'
20 CONTEXT_TAGS_STR = 'tags_str'
20 CONTEXT_TAGS_STR = 'tags_str'
21
21
22 PERMISSION_MODERATE = 'moderation'
23
22
24
23
25 def get_notifications(context, request):
24 def get_notifications(context, request):
26 settings_manager = get_settings_manager(request)
25 settings_manager = get_settings_manager(request)
27 username = settings_manager.get_setting(SETTING_USERNAME)
26 username = settings_manager.get_setting(SETTING_USERNAME)
28 new_notifications_count = 0
27 new_notifications_count = 0
29 if username is not None and len(username) > 0:
28 if username is not None and len(username) > 0:
30 last_notification_id = settings_manager.get_setting(
29 last_notification_id = settings_manager.get_setting(
31 SETTING_LAST_NOTIFICATION_ID)
30 SETTING_LAST_NOTIFICATION_ID)
32
31
33 new_notifications_count = Notification.objects.get_notification_posts(
32 new_notifications_count = Notification.objects.get_notification_posts(
34 username=username, last=last_notification_id).count()
33 username=username, last=last_notification_id).count()
35 context[CONTEXT_NEW_NOTIFICATIONS_COUNT] = new_notifications_count
34 context[CONTEXT_NEW_NOTIFICATIONS_COUNT] = new_notifications_count
36 context[CONTEXT_USERNAME] = username
35 context[CONTEXT_USERNAME] = username
37
36
38
37
39 def get_moderator_permissions(context, request):
40 try:
41 moderate = request.user.has_perm(PERMISSION_MODERATE)
42 except AttributeError:
43 moderate = False
44 context[CONTEXT_MODERATOR] = moderate
45
46
47 def user_and_ui_processor(request):
38 def user_and_ui_processor(request):
48 context = dict()
39 context = dict()
49
40
50 context[CONTEXT_PPD] = float(Post.objects.get_posts_per_day())
41 context[CONTEXT_PPD] = float(Post.objects.get_posts_per_day())
51
42
52 settings_manager = get_settings_manager(request)
43 settings_manager = get_settings_manager(request)
53 fav_tags = settings_manager.get_fav_tags()
44 fav_tags = settings_manager.get_fav_tags()
54 context[CONTEXT_TAGS] = fav_tags
45 context[CONTEXT_TAGS] = fav_tags
55 context[CONTEXT_TAGS_STR] = Tag.objects.get_tag_url_list(fav_tags)
46 context[CONTEXT_TAGS_STR] = Tag.objects.get_tag_url_list(fav_tags)
56 theme = settings_manager.get_theme()
47 theme = settings_manager.get_theme()
57 context[CONTEXT_THEME] = theme
48 context[CONTEXT_THEME] = theme
58 context[CONTEXT_THEME_CSS] = 'css/' + theme + '/base_page.css'
49 context[CONTEXT_THEME_CSS] = 'css/' + theme + '/base_page.css'
59
50
60 # This shows the moderator panel
51 # This shows the moderator panel
61 get_moderator_permissions(context, request)
52 context[CONTEXT_MODERATOR] = utils.is_moderator(request)
62
53
63 context[CONTEXT_VERSION] = settings.VERSION
54 context[CONTEXT_VERSION] = settings.VERSION
64 context[CONTEXT_SITE_NAME] = settings.SITE_NAME
55 context[CONTEXT_SITE_NAME] = settings.SITE_NAME
65
56
66 get_notifications(context, request)
57 get_notifications(context, request)
67
58
68 return context
59 return context
@@ -1,381 +1,381 b''
1 import re
1 import re
2 import time
2 import time
3 import pytz
3 import pytz
4
4
5 from django import forms
5 from django import forms
6 from django.core.files.uploadedfile import SimpleUploadedFile
6 from django.core.files.uploadedfile import SimpleUploadedFile
7 from django.core.exceptions import ObjectDoesNotExist
7 from django.core.exceptions import ObjectDoesNotExist
8 from django.forms.util import ErrorList
8 from django.forms.util import ErrorList
9 from django.utils.translation import ugettext_lazy as _
9 from django.utils.translation import ugettext_lazy as _
10 import requests
10 import requests
11
11
12 from boards.mdx_neboard import formatters
12 from boards.mdx_neboard import formatters
13 from boards.models.post import TITLE_MAX_LENGTH
13 from boards.models.post import TITLE_MAX_LENGTH
14 from boards.models import Tag, Post
14 from boards.models import Tag, Post
15 from neboard import settings
15 from neboard import settings
16 import boards.settings as board_settings
16 import boards.settings as board_settings
17
17
18
18
19 CONTENT_TYPE_IMAGE = (
19 CONTENT_TYPE_IMAGE = (
20 'image/jpeg',
20 'image/jpeg',
21 'image/png',
21 'image/png',
22 'image/gif',
22 'image/gif',
23 'image/bmp',
23 'image/bmp',
24 )
24 )
25
25
26 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
26 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
27
27
28 VETERAN_POSTING_DELAY = 5
28 VETERAN_POSTING_DELAY = 5
29
29
30 ATTRIBUTE_PLACEHOLDER = 'placeholder'
30 ATTRIBUTE_PLACEHOLDER = 'placeholder'
31 ATTRIBUTE_ROWS = 'rows'
31 ATTRIBUTE_ROWS = 'rows'
32
32
33 LAST_POST_TIME = 'last_post_time'
33 LAST_POST_TIME = 'last_post_time'
34 LAST_LOGIN_TIME = 'last_login_time'
34 LAST_LOGIN_TIME = 'last_login_time'
35 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
35 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
36 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
36 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
37
37
38 LABEL_TITLE = _('Title')
38 LABEL_TITLE = _('Title')
39 LABEL_TEXT = _('Text')
39 LABEL_TEXT = _('Text')
40 LABEL_TAG = _('Tag')
40 LABEL_TAG = _('Tag')
41 LABEL_SEARCH = _('Search')
41 LABEL_SEARCH = _('Search')
42
42
43 ERROR_SPEED = _('Please wait %s seconds before sending message')
43 ERROR_SPEED = _('Please wait %s seconds before sending message')
44
44
45 TAG_MAX_LENGTH = 20
45 TAG_MAX_LENGTH = 20
46
46
47 IMAGE_DOWNLOAD_CHUNK_BYTES = 100000
47 IMAGE_DOWNLOAD_CHUNK_BYTES = 100000
48
48
49 HTTP_RESULT_OK = 200
49 HTTP_RESULT_OK = 200
50
50
51 TEXTAREA_ROWS = 4
51 TEXTAREA_ROWS = 4
52
52
53
53
54 def get_timezones():
54 def get_timezones():
55 timezones = []
55 timezones = []
56 for tz in pytz.common_timezones:
56 for tz in pytz.common_timezones:
57 timezones.append((tz, tz),)
57 timezones.append((tz, tz),)
58 return timezones
58 return timezones
59
59
60
60
61 class FormatPanel(forms.Textarea):
61 class FormatPanel(forms.Textarea):
62 """
62 """
63 Panel for text formatting. Consists of buttons to add different tags to the
63 Panel for text formatting. Consists of buttons to add different tags to the
64 form text area.
64 form text area.
65 """
65 """
66
66
67 def render(self, name, value, attrs=None):
67 def render(self, name, value, attrs=None):
68 output = '<div id="mark-panel">'
68 output = '<div id="mark-panel">'
69 for formatter in formatters:
69 for formatter in formatters:
70 output += '<span class="mark_btn"' + \
70 output += '<span class="mark_btn"' + \
71 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
71 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
72 '\', \'' + formatter.format_right + '\')">' + \
72 '\', \'' + formatter.format_right + '\')">' + \
73 formatter.preview_left + formatter.name + \
73 formatter.preview_left + formatter.name + \
74 formatter.preview_right + '</span>'
74 formatter.preview_right + '</span>'
75
75
76 output += '</div>'
76 output += '</div>'
77 output += super(FormatPanel, self).render(name, value, attrs=None)
77 output += super(FormatPanel, self).render(name, value, attrs=None)
78
78
79 return output
79 return output
80
80
81
81
82 class PlainErrorList(ErrorList):
82 class PlainErrorList(ErrorList):
83 def __unicode__(self):
83 def __unicode__(self):
84 return self.as_text()
84 return self.as_text()
85
85
86 def as_text(self):
86 def as_text(self):
87 return ''.join(['(!) %s ' % e for e in self])
87 return ''.join(['(!) %s ' % e for e in self])
88
88
89
89
90 class NeboardForm(forms.Form):
90 class NeboardForm(forms.Form):
91 """
91 """
92 Form with neboard-specific formatting.
92 Form with neboard-specific formatting.
93 """
93 """
94
94
95 def as_div(self):
95 def as_div(self):
96 """
96 """
97 Returns this form rendered as HTML <as_div>s.
97 Returns this form rendered as HTML <as_div>s.
98 """
98 """
99
99
100 return self._html_output(
100 return self._html_output(
101 # TODO Do not show hidden rows in the list here
101 # TODO Do not show hidden rows in the list here
102 normal_row='<div class="form-row"><div class="form-label">'
102 normal_row='<div class="form-row"><div class="form-label">'
103 '%(label)s'
103 '%(label)s'
104 '</div></div>'
104 '</div></div>'
105 '<div class="form-row"><div class="form-input">'
105 '<div class="form-row"><div class="form-input">'
106 '%(field)s'
106 '%(field)s'
107 '</div></div>'
107 '</div></div>'
108 '<div class="form-row">'
108 '<div class="form-row">'
109 '%(help_text)s'
109 '%(help_text)s'
110 '</div>',
110 '</div>',
111 error_row='<div class="form-row">'
111 error_row='<div class="form-row">'
112 '<div class="form-label"></div>'
112 '<div class="form-label"></div>'
113 '<div class="form-errors">%s</div>'
113 '<div class="form-errors">%s</div>'
114 '</div>',
114 '</div>',
115 row_ender='</div>',
115 row_ender='</div>',
116 help_text_html='%s',
116 help_text_html='%s',
117 errors_on_separate_row=True)
117 errors_on_separate_row=True)
118
118
119 def as_json_errors(self):
119 def as_json_errors(self):
120 errors = []
120 errors = []
121
121
122 for name, field in list(self.fields.items()):
122 for name, field in list(self.fields.items()):
123 if self[name].errors:
123 if self[name].errors:
124 errors.append({
124 errors.append({
125 'field': name,
125 'field': name,
126 'errors': self[name].errors.as_text(),
126 'errors': self[name].errors.as_text(),
127 })
127 })
128
128
129 return errors
129 return errors
130
130
131
131
132 class PostForm(NeboardForm):
132 class PostForm(NeboardForm):
133
133
134 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
134 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
135 label=LABEL_TITLE)
135 label=LABEL_TITLE)
136 text = forms.CharField(
136 text = forms.CharField(
137 widget=FormatPanel(attrs={
137 widget=FormatPanel(attrs={
138 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
138 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
139 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
139 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
140 }),
140 }),
141 required=False, label=LABEL_TEXT)
141 required=False, label=LABEL_TEXT)
142 image = forms.ImageField(required=False, label=_('Image'),
142 image = forms.ImageField(required=False, label=_('Image'),
143 widget=forms.ClearableFileInput(
143 widget=forms.ClearableFileInput(
144 attrs={'accept': 'image/*'}))
144 attrs={'accept': 'image/*'}))
145 image_url = forms.CharField(required=False, label=_('Image URL'),
145 image_url = forms.CharField(required=False, label=_('Image URL'),
146 widget=forms.TextInput(
146 widget=forms.TextInput(
147 attrs={ATTRIBUTE_PLACEHOLDER:
147 attrs={ATTRIBUTE_PLACEHOLDER:
148 'http://example.com/image.png'}))
148 'http://example.com/image.png'}))
149
149
150 # This field is for spam prevention only
150 # This field is for spam prevention only
151 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
151 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
152 widget=forms.TextInput(attrs={
152 widget=forms.TextInput(attrs={
153 'class': 'form-email'}))
153 'class': 'form-email'}))
154 threads = forms.CharField(required=False, label=_('Additional threads'),
154 threads = forms.CharField(required=False, label=_('Additional threads'),
155 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
155 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
156 '123 456 789'}))
156 '123 456 789'}))
157
157
158 session = None
158 session = None
159 need_to_ban = False
159 need_to_ban = False
160
160
161 def clean_title(self):
161 def clean_title(self):
162 title = self.cleaned_data['title']
162 title = self.cleaned_data['title']
163 if title:
163 if title:
164 if len(title) > TITLE_MAX_LENGTH:
164 if len(title) > TITLE_MAX_LENGTH:
165 raise forms.ValidationError(_('Title must have less than %s '
165 raise forms.ValidationError(_('Title must have less than %s '
166 'characters') %
166 'characters') %
167 str(TITLE_MAX_LENGTH))
167 str(TITLE_MAX_LENGTH))
168 return title
168 return title
169
169
170 def clean_text(self):
170 def clean_text(self):
171 text = self.cleaned_data['text'].strip()
171 text = self.cleaned_data['text'].strip()
172 if text:
172 if text:
173 if len(text) > board_settings.MAX_TEXT_LENGTH:
173 if len(text) > board_settings.MAX_TEXT_LENGTH:
174 raise forms.ValidationError(_('Text must have less than %s '
174 raise forms.ValidationError(_('Text must have less than %s '
175 'characters') %
175 'characters') %
176 str(board_settings
176 str(board_settings
177 .MAX_TEXT_LENGTH))
177 .MAX_TEXT_LENGTH))
178 return text
178 return text
179
179
180 def clean_image(self):
180 def clean_image(self):
181 image = self.cleaned_data['image']
181 image = self.cleaned_data['image']
182
182
183 if image:
183 if image:
184 self.validate_image_size(image.size)
184 self.validate_image_size(image.size)
185
185
186 return image
186 return image
187
187
188 def clean_image_url(self):
188 def clean_image_url(self):
189 url = self.cleaned_data['image_url']
189 url = self.cleaned_data['image_url']
190
190
191 image = None
191 image = None
192 if url:
192 if url:
193 image = self._get_image_from_url(url)
193 image = self._get_image_from_url(url)
194
194
195 if not image:
195 if not image:
196 raise forms.ValidationError(_('Invalid URL'))
196 raise forms.ValidationError(_('Invalid URL'))
197 else:
197 else:
198 self.validate_image_size(image.size)
198 self.validate_image_size(image.size)
199
199
200 return image
200 return image
201
201
202 def clean_threads(self):
202 def clean_threads(self):
203 threads_str = self.cleaned_data['threads']
203 threads_str = self.cleaned_data['threads']
204
204
205 if len(threads_str) > 0:
205 if len(threads_str) > 0:
206 threads_id_list = threads_str.split(' ')
206 threads_id_list = threads_str.split(' ')
207
207
208 threads = list()
208 threads = list()
209
209
210 for thread_id in threads_id_list:
210 for thread_id in threads_id_list:
211 try:
211 try:
212 thread = Post.objects.get(id=int(thread_id))
212 thread = Post.objects.get(id=int(thread_id))
213 if not thread.is_opening():
213 if not thread.is_opening():
214 raise ObjectDoesNotExist()
214 raise ObjectDoesNotExist()
215 threads.append(thread)
215 threads.append(thread)
216 except (ObjectDoesNotExist, ValueError):
216 except (ObjectDoesNotExist, ValueError):
217 raise forms.ValidationError(_('Invalid additional thread list'))
217 raise forms.ValidationError(_('Invalid additional thread list'))
218
218
219 return threads
219 return threads
220
220
221 def clean(self):
221 def clean(self):
222 cleaned_data = super(PostForm, self).clean()
222 cleaned_data = super(PostForm, self).clean()
223
223
224 if cleaned_data['email']:
224 if cleaned_data['email']:
225 self.need_to_ban = True
225 self.need_to_ban = True
226 raise forms.ValidationError('A human cannot enter a hidden field')
226 raise forms.ValidationError('A human cannot enter a hidden field')
227
227
228 if not self.errors:
228 if not self.errors:
229 self._clean_text_image()
229 self._clean_text_image()
230
230
231 if not self.errors and self.session:
231 if not self.errors and self.session:
232 self._validate_posting_speed()
232 self._validate_posting_speed()
233
233
234 return cleaned_data
234 return cleaned_data
235
235
236 def get_image(self):
236 def get_image(self):
237 """
237 """
238 Gets image from file or URL.
238 Gets image from file or URL.
239 """
239 """
240
240
241 image = self.cleaned_data['image']
241 image = self.cleaned_data['image']
242 return image if image else self.cleaned_data['image_url']
242 return image if image else self.cleaned_data['image_url']
243
243
244 def _clean_text_image(self):
244 def _clean_text_image(self):
245 text = self.cleaned_data.get('text')
245 text = self.cleaned_data.get('text')
246 image = self.get_image()
246 image = self.get_image()
247
247
248 if (not text) and (not image):
248 if (not text) and (not image):
249 error_message = _('Either text or image must be entered.')
249 error_message = _('Either text or image must be entered.')
250 self._errors['text'] = self.error_class([error_message])
250 self._errors['text'] = self.error_class([error_message])
251
251
252 def _validate_posting_speed(self):
252 def _validate_posting_speed(self):
253 can_post = True
253 can_post = True
254
254
255 posting_delay = settings.POSTING_DELAY
255 posting_delay = settings.POSTING_DELAY
256
256
257 if board_settings.LIMIT_POSTING_SPEED:
257 if board_settings.LIMIT_POSTING_SPEED:
258 now = time.time()
258 now = time.time()
259
259
260 current_delay = 0
260 current_delay = 0
261 need_delay = False
261 need_delay = False
262
262
263 if not LAST_POST_TIME in self.session:
263 if not LAST_POST_TIME in self.session:
264 self.session[LAST_POST_TIME] = now
264 self.session[LAST_POST_TIME] = now
265
265
266 need_delay = True
266 need_delay = True
267 else:
267 else:
268 last_post_time = self.session.get(LAST_POST_TIME)
268 last_post_time = self.session.get(LAST_POST_TIME)
269 current_delay = int(now - last_post_time)
269 current_delay = int(now - last_post_time)
270
270
271 need_delay = current_delay < posting_delay
271 need_delay = current_delay < posting_delay
272
272
273 if need_delay:
273 if need_delay:
274 error_message = ERROR_SPEED % str(posting_delay
274 error_message = ERROR_SPEED % str(posting_delay
275 - current_delay)
275 - current_delay)
276 self._errors['text'] = self.error_class([error_message])
276 self._errors['text'] = self.error_class([error_message])
277
277
278 can_post = False
278 can_post = False
279
279
280 if can_post:
280 if can_post:
281 self.session[LAST_POST_TIME] = now
281 self.session[LAST_POST_TIME] = now
282
282
283 def validate_image_size(self, size: int):
283 def validate_image_size(self, size: int):
284 if size > board_settings.MAX_IMAGE_SIZE:
284 if size > board_settings.MAX_IMAGE_SIZE:
285 raise forms.ValidationError(
285 raise forms.ValidationError(
286 _('Image must be less than %s bytes')
286 _('Image must be less than %s bytes')
287 % str(board_settings.MAX_IMAGE_SIZE))
287 % str(board_settings.MAX_IMAGE_SIZE))
288
288
289 def _get_image_from_url(self, url: str) -> SimpleUploadedFile:
289 def _get_image_from_url(self, url: str) -> SimpleUploadedFile:
290 """
290 """
291 Gets an image file from URL.
291 Gets an image file from URL.
292 """
292 """
293
293
294 img_temp = None
294 img_temp = None
295
295
296 try:
296 try:
297 # Verify content headers
297 # Verify content headers
298 response_head = requests.head(url, verify=False)
298 response_head = requests.head(url, verify=False)
299 content_type = response_head.headers['content-type'].split(';')[0]
299 content_type = response_head.headers['content-type'].split(';')[0]
300 if content_type in CONTENT_TYPE_IMAGE:
300 if content_type in CONTENT_TYPE_IMAGE:
301 length_header = response_head.headers.get('content-length')
301 length_header = response_head.headers.get('content-length')
302 if length_header:
302 if length_header:
303 length = int(length_header)
303 length = int(length_header)
304 self.validate_image_size(length)
304 self.validate_image_size(length)
305 # Get the actual content into memory
305 # Get the actual content into memory
306 response = requests.get(url, verify=False, stream=True)
306 response = requests.get(url, verify=False, stream=True)
307
307
308 # Download image, stop if the size exceeds limit
308 # Download image, stop if the size exceeds limit
309 size = 0
309 size = 0
310 content = b''
310 content = b''
311 for chunk in response.iter_content(IMAGE_DOWNLOAD_CHUNK_BYTES):
311 for chunk in response.iter_content(IMAGE_DOWNLOAD_CHUNK_BYTES):
312 size += len(chunk)
312 size += len(chunk)
313 self.validate_image_size(size)
313 self.validate_image_size(size)
314 content += chunk
314 content += chunk
315
315
316 if response.status_code == HTTP_RESULT_OK and content:
316 if response.status_code == HTTP_RESULT_OK and content:
317 # Set a dummy file name that will be replaced
317 # Set a dummy file name that will be replaced
318 # anyway, just keep the valid extension
318 # anyway, just keep the valid extension
319 filename = 'image.' + content_type.split('/')[1]
319 filename = 'image.' + content_type.split('/')[1]
320 img_temp = SimpleUploadedFile(filename, content,
320 img_temp = SimpleUploadedFile(filename, content,
321 content_type)
321 content_type)
322 except Exception:
322 except Exception:
323 # Just return no image
323 # Just return no image
324 pass
324 pass
325
325
326 return img_temp
326 return img_temp
327
327
328
328
329 class ThreadForm(PostForm):
329 class ThreadForm(PostForm):
330
330
331 tags = forms.CharField(
331 tags = forms.CharField(
332 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
332 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
333 max_length=100, label=_('Tags'), required=True)
333 max_length=100, label=_('Tags'), required=True)
334
334
335 def clean_tags(self):
335 def clean_tags(self):
336 tags = self.cleaned_data['tags'].strip()
336 tags = self.cleaned_data['tags'].strip()
337
337
338 if not tags or not REGEX_TAGS.match(tags):
338 if not tags or not REGEX_TAGS.match(tags):
339 raise forms.ValidationError(
339 raise forms.ValidationError(
340 _('Inappropriate characters in tags.'))
340 _('Inappropriate characters in tags.'))
341
341
342 required_tag_exists = False
342 required_tag_exists = False
343 for tag in tags.split():
343 for tag in tags.split():
344 try:
344 try:
345 Tag.objects.get(name=tag.strip().lower(), required=True)
345 Tag.objects.get(name=tag.strip().lower(), required=True)
346 required_tag_exists = True
346 required_tag_exists = True
347 break
347 break
348 except ObjectDoesNotExist:
348 except ObjectDoesNotExist:
349 pass
349 pass
350
350
351 if not required_tag_exists:
351 if not required_tag_exists:
352 all_tags = Tag.objects.filter(required=True)
352 all_tags = Tag.objects.filter(required=True)
353 raise forms.ValidationError(
353 raise forms.ValidationError(
354 _('Need at least one of the tags: ')
354 _('Need at least one of the tags: ')
355 + ', '.join([tag.name for tag in all_tags]))
355 + ', '.join([tag.name for tag in all_tags]))
356
356
357 return tags
357 return tags
358
358
359 def clean(self):
359 def clean(self):
360 cleaned_data = super(ThreadForm, self).clean()
360 cleaned_data = super(ThreadForm, self).clean()
361
361
362 return cleaned_data
362 return cleaned_data
363
363
364
364
365 class SettingsForm(NeboardForm):
365 class SettingsForm(NeboardForm):
366
366
367 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
367 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
368 username = forms.CharField(label=_('User name'), required=False)
368 username = forms.CharField(label=_('User name'), required=False)
369 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
369 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
370
370
371 def clean_username(self):
371 def clean_username(self):
372 username = self.cleaned_data['username']
372 username = self.cleaned_data['username']
373
373
374 if username and not REGEX_TAGS.match(username):
374 if username and not REGEX_TAGS.match(username):
375 raise forms.ValidationError(_('Inappropriate characters.'))
375 raise forms.ValidationError(_('Inappropriate characters.'))
376
376
377 return username
377 return username
378
378
379
379
380 class SearchForm(NeboardForm):
380 class SearchForm(NeboardForm):
381 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
381 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
@@ -1,425 +1,427 b''
1 from datetime import datetime, timedelta, date
1 from datetime import datetime, timedelta, date
2 from datetime import time as dtime
2 from datetime import time as dtime
3 import logging
3 import logging
4 import re
4 import re
5
5
6 from django.core.exceptions import ObjectDoesNotExist
6 from django.core.exceptions import ObjectDoesNotExist
7 from django.core.urlresolvers import reverse
7 from django.core.urlresolvers import reverse
8 from django.db import models, transaction
8 from django.db import models, transaction
9 from django.db.models import TextField
9 from django.db.models import TextField
10 from django.template.loader import render_to_string
10 from django.template.loader import render_to_string
11 from django.utils import timezone
11 from django.utils import timezone
12
12
13 from boards import settings
13 from boards import settings
14 from boards.mdx_neboard import Parser
14 from boards.mdx_neboard import Parser
15 from boards.models import PostImage
15 from boards.models import PostImage
16 from boards.models.base import Viewable
16 from boards.models.base import Viewable
17 from boards.utils import datetime_to_epoch, cached_result
17 from boards import utils
18 from boards.models.user import Notification, Ban
18 from boards.models.user import Notification, Ban
19 import boards.models.thread
19 import boards.models.thread
20
20
21
21
22 APP_LABEL_BOARDS = 'boards'
22 APP_LABEL_BOARDS = 'boards'
23
23
24 POSTS_PER_DAY_RANGE = 7
24 POSTS_PER_DAY_RANGE = 7
25
25
26 BAN_REASON_AUTO = 'Auto'
26 BAN_REASON_AUTO = 'Auto'
27
27
28 IMAGE_THUMB_SIZE = (200, 150)
28 IMAGE_THUMB_SIZE = (200, 150)
29
29
30 TITLE_MAX_LENGTH = 200
30 TITLE_MAX_LENGTH = 200
31
31
32 # TODO This should be removed
32 # TODO This should be removed
33 NO_IP = '0.0.0.0'
33 NO_IP = '0.0.0.0'
34
34
35 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
35 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
36 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
36 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
37
37
38 PARAMETER_TRUNCATED = 'truncated'
38 PARAMETER_TRUNCATED = 'truncated'
39 PARAMETER_TAG = 'tag'
39 PARAMETER_TAG = 'tag'
40 PARAMETER_OFFSET = 'offset'
40 PARAMETER_OFFSET = 'offset'
41 PARAMETER_DIFF_TYPE = 'type'
41 PARAMETER_DIFF_TYPE = 'type'
42 PARAMETER_CSS_CLASS = 'css_class'
42 PARAMETER_CSS_CLASS = 'css_class'
43 PARAMETER_THREAD = 'thread'
43 PARAMETER_THREAD = 'thread'
44 PARAMETER_IS_OPENING = 'is_opening'
44 PARAMETER_IS_OPENING = 'is_opening'
45 PARAMETER_MODERATOR = 'moderator'
45 PARAMETER_MODERATOR = 'moderator'
46 PARAMETER_POST = 'post'
46 PARAMETER_POST = 'post'
47 PARAMETER_OP_ID = 'opening_post_id'
47 PARAMETER_OP_ID = 'opening_post_id'
48 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
48 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
49 PARAMETER_REPLY_LINK = 'reply_link'
49 PARAMETER_REPLY_LINK = 'reply_link'
50
50
51 DIFF_TYPE_HTML = 'html'
51 DIFF_TYPE_HTML = 'html'
52 DIFF_TYPE_JSON = 'json'
52 DIFF_TYPE_JSON = 'json'
53
53
54 REFMAP_STR = '<a href="{}">&gt;&gt;{}</a>'
54 REFMAP_STR = '<a href="{}">&gt;&gt;{}</a>'
55
55
56
56
57 class PostManager(models.Manager):
57 class PostManager(models.Manager):
58 @transaction.atomic
58 @transaction.atomic
59 def create_post(self, title: str, text: str, image=None, thread=None,
59 def create_post(self, title: str, text: str, image=None, thread=None,
60 ip=NO_IP, tags: list=None, threads: list=None):
60 ip=NO_IP, tags: list=None, threads: list=None):
61 """
61 """
62 Creates new post
62 Creates new post
63 """
63 """
64
64
65 is_banned = Ban.objects.filter(ip=ip).exists()
65 is_banned = Ban.objects.filter(ip=ip).exists()
66
66
67 # TODO Raise specific exception and catch it in the views
67 # TODO Raise specific exception and catch it in the views
68 if is_banned:
68 if is_banned:
69 raise Exception("This user is banned")
69 raise Exception("This user is banned")
70
70
71 if not tags:
71 if not tags:
72 tags = []
72 tags = []
73 if not threads:
73 if not threads:
74 threads = []
74 threads = []
75
75
76 posting_time = timezone.now()
76 posting_time = timezone.now()
77 if not thread:
77 if not thread:
78 thread = boards.models.thread.Thread.objects.create(
78 thread = boards.models.thread.Thread.objects.create(
79 bump_time=posting_time, last_edit_time=posting_time)
79 bump_time=posting_time, last_edit_time=posting_time)
80 new_thread = True
80 new_thread = True
81 else:
81 else:
82 new_thread = False
82 new_thread = False
83
83
84 pre_text = Parser().preparse(text)
84 pre_text = Parser().preparse(text)
85
85
86 post = self.create(title=title,
86 post = self.create(title=title,
87 text=pre_text,
87 text=pre_text,
88 pub_time=posting_time,
88 pub_time=posting_time,
89 poster_ip=ip,
89 poster_ip=ip,
90 thread=thread,
90 thread=thread,
91 last_edit_time=posting_time)
91 last_edit_time=posting_time)
92 post.threads.add(thread)
92 post.threads.add(thread)
93
93
94 logger = logging.getLogger('boards.post.create')
94 logger = logging.getLogger('boards.post.create')
95
95
96 logger.info('Created post {} by {}'.format(post, post.poster_ip))
96 logger.info('Created post {} by {}'.format(post, post.poster_ip))
97
97
98 if image:
98 if image:
99 post.images.add(PostImage.objects.create_with_hash(image))
99 post.images.add(PostImage.objects.create_with_hash(image))
100
100
101 list(map(thread.add_tag, tags))
101 list(map(thread.add_tag, tags))
102
102
103 if new_thread:
103 if new_thread:
104 boards.models.thread.Thread.objects.process_oldest_threads()
104 boards.models.thread.Thread.objects.process_oldest_threads()
105 else:
105 else:
106 thread.last_edit_time = posting_time
106 thread.last_edit_time = posting_time
107 thread.bump()
107 thread.bump()
108 thread.save()
108 thread.save()
109
109
110 post.connect_replies()
110 post.connect_replies()
111 post.connect_threads(threads)
111 post.connect_threads(threads)
112 post.connect_notifications()
112 post.connect_notifications()
113
113
114 return post
114 return post
115
115
116 def delete_posts_by_ip(self, ip):
116 def delete_posts_by_ip(self, ip):
117 """
117 """
118 Deletes all posts of the author with same IP
118 Deletes all posts of the author with same IP
119 """
119 """
120
120
121 posts = self.filter(poster_ip=ip)
121 posts = self.filter(poster_ip=ip)
122 for post in posts:
122 for post in posts:
123 post.delete()
123 post.delete()
124
124
125 @cached_result()
125 @utils.cached_result()
126 def get_posts_per_day(self) -> float:
126 def get_posts_per_day(self) -> float:
127 """
127 """
128 Gets average count of posts per day for the last 7 days
128 Gets average count of posts per day for the last 7 days
129 """
129 """
130
130
131 day_end = date.today()
131 day_end = date.today()
132 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
132 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
133
133
134 day_time_start = timezone.make_aware(datetime.combine(
134 day_time_start = timezone.make_aware(datetime.combine(
135 day_start, dtime()), timezone.get_current_timezone())
135 day_start, dtime()), timezone.get_current_timezone())
136 day_time_end = timezone.make_aware(datetime.combine(
136 day_time_end = timezone.make_aware(datetime.combine(
137 day_end, dtime()), timezone.get_current_timezone())
137 day_end, dtime()), timezone.get_current_timezone())
138
138
139 posts_per_period = float(self.filter(
139 posts_per_period = float(self.filter(
140 pub_time__lte=day_time_end,
140 pub_time__lte=day_time_end,
141 pub_time__gte=day_time_start).count())
141 pub_time__gte=day_time_start).count())
142
142
143 ppd = posts_per_period / POSTS_PER_DAY_RANGE
143 ppd = posts_per_period / POSTS_PER_DAY_RANGE
144
144
145 return ppd
145 return ppd
146
146
147
147
148 class Post(models.Model, Viewable):
148 class Post(models.Model, Viewable):
149 """A post is a message."""
149 """A post is a message."""
150
150
151 objects = PostManager()
151 objects = PostManager()
152
152
153 class Meta:
153 class Meta:
154 app_label = APP_LABEL_BOARDS
154 app_label = APP_LABEL_BOARDS
155 ordering = ('id',)
155 ordering = ('id',)
156
156
157 title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
157 title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
158 pub_time = models.DateTimeField()
158 pub_time = models.DateTimeField()
159 text = TextField(blank=True, null=True)
159 text = TextField(blank=True, null=True)
160 _text_rendered = TextField(blank=True, null=True, editable=False)
160 _text_rendered = TextField(blank=True, null=True, editable=False)
161
161
162 images = models.ManyToManyField(PostImage, null=True, blank=True,
162 images = models.ManyToManyField(PostImage, null=True, blank=True,
163 related_name='ip+', db_index=True)
163 related_name='ip+', db_index=True)
164
164
165 poster_ip = models.GenericIPAddressField()
165 poster_ip = models.GenericIPAddressField()
166
166
167 last_edit_time = models.DateTimeField()
167 last_edit_time = models.DateTimeField()
168
168
169 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
169 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
170 null=True,
170 null=True,
171 blank=True, related_name='rfp+',
171 blank=True, related_name='rfp+',
172 db_index=True)
172 db_index=True)
173 refmap = models.TextField(null=True, blank=True)
173 refmap = models.TextField(null=True, blank=True)
174 threads = models.ManyToManyField('Thread', db_index=True)
174 threads = models.ManyToManyField('Thread', db_index=True)
175 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
175 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
176
176
177 def __str__(self):
177 def __str__(self):
178 return 'P#{}/{}'.format(self.id, self.title)
178 return 'P#{}/{}'.format(self.id, self.title)
179
179
180 def get_title(self) -> str:
180 def get_title(self) -> str:
181 """
181 """
182 Gets original post title or part of its text.
182 Gets original post title or part of its text.
183 """
183 """
184
184
185 title = self.title
185 title = self.title
186 if not title:
186 if not title:
187 title = self.get_text()
187 title = self.get_text()
188
188
189 return title
189 return title
190
190
191 def build_refmap(self) -> None:
191 def build_refmap(self) -> None:
192 """
192 """
193 Builds a replies map string from replies list. This is a cache to stop
193 Builds a replies map string from replies list. This is a cache to stop
194 the server from recalculating the map on every post show.
194 the server from recalculating the map on every post show.
195 """
195 """
196
196
197 post_urls = [REFMAP_STR.format(refpost.get_url(), refpost.id)
197 post_urls = [REFMAP_STR.format(refpost.get_url(), refpost.id)
198 for refpost in self.referenced_posts.all()]
198 for refpost in self.referenced_posts.all()]
199
199
200 self.refmap = ', '.join(post_urls)
200 self.refmap = ', '.join(post_urls)
201
201
202 def is_referenced(self) -> bool:
202 def is_referenced(self) -> bool:
203 return self.refmap and len(self.refmap) > 0
203 return self.refmap and len(self.refmap) > 0
204
204
205 def is_opening(self) -> bool:
205 def is_opening(self) -> bool:
206 """
206 """
207 Checks if this is an opening post or just a reply.
207 Checks if this is an opening post or just a reply.
208 """
208 """
209
209
210 return self.get_thread().get_opening_post_id() == self.id
210 return self.get_thread().get_opening_post_id() == self.id
211
211
212 @cached_result()
212 @utils.cached_result()
213 def get_url(self):
213 def get_url(self):
214 """
214 """
215 Gets full url to the post.
215 Gets full url to the post.
216 """
216 """
217
217
218 thread = self.get_thread()
218 thread = self.get_thread()
219
219
220 opening_id = thread.get_opening_post_id()
220 opening_id = thread.get_opening_post_id()
221
221
222 thread_url = reverse('thread', kwargs={'post_id': opening_id})
222 thread_url = reverse('thread', kwargs={'post_id': opening_id})
223 if self.id != opening_id:
223 if self.id != opening_id:
224 thread_url += '#' + str(self.id)
224 thread_url += '#' + str(self.id)
225
225
226 return thread_url
226 return thread_url
227
227
228 def get_thread(self):
228 def get_thread(self):
229 return self.thread
229 return self.thread
230
230
231 def get_threads(self) -> list:
231 def get_threads(self) -> list:
232 """
232 """
233 Gets post's thread.
233 Gets post's thread.
234 """
234 """
235
235
236 return self.threads
236 return self.threads
237
237
238 def get_view(self, moderator=False, need_open_link=False,
238 def get_view(self, moderator=False, need_open_link=False,
239 truncated=False, reply_link=False, *args, **kwargs) -> str:
239 truncated=False, reply_link=False, *args, **kwargs) -> str:
240 """
240 """
241 Renders post's HTML view. Some of the post params can be passed over
241 Renders post's HTML view. Some of the post params can be passed over
242 kwargs for the means of caching (if we view the thread, some params
242 kwargs for the means of caching (if we view the thread, some params
243 are same for every post and don't need to be computed over and over.
243 are same for every post and don't need to be computed over and over.
244 """
244 """
245
245
246 thread = self.get_thread()
246 thread = self.get_thread()
247 is_opening = kwargs.get(PARAMETER_IS_OPENING, self.is_opening())
247 is_opening = kwargs.get(PARAMETER_IS_OPENING, self.is_opening())
248
248
249 if is_opening:
249 if is_opening:
250 opening_post_id = self.id
250 opening_post_id = self.id
251 else:
251 else:
252 opening_post_id = thread.get_opening_post_id()
252 opening_post_id = thread.get_opening_post_id()
253
253
254 css_class = 'post'
254 css_class = 'post'
255 if thread.archived:
255 if thread.archived:
256 css_class += ' archive_post'
256 css_class += ' archive_post'
257 elif not thread.can_bump():
257 elif not thread.can_bump():
258 css_class += ' dead_post'
258 css_class += ' dead_post'
259
259
260 return render_to_string('boards/post.html', {
260 return render_to_string('boards/post.html', {
261 PARAMETER_POST: self,
261 PARAMETER_POST: self,
262 PARAMETER_MODERATOR: moderator,
262 PARAMETER_MODERATOR: moderator,
263 PARAMETER_IS_OPENING: is_opening,
263 PARAMETER_IS_OPENING: is_opening,
264 PARAMETER_THREAD: thread,
264 PARAMETER_THREAD: thread,
265 PARAMETER_CSS_CLASS: css_class,
265 PARAMETER_CSS_CLASS: css_class,
266 PARAMETER_NEED_OPEN_LINK: need_open_link,
266 PARAMETER_NEED_OPEN_LINK: need_open_link,
267 PARAMETER_TRUNCATED: truncated,
267 PARAMETER_TRUNCATED: truncated,
268 PARAMETER_OP_ID: opening_post_id,
268 PARAMETER_OP_ID: opening_post_id,
269 PARAMETER_REPLY_LINK: reply_link,
269 PARAMETER_REPLY_LINK: reply_link,
270 })
270 })
271
271
272 def get_search_view(self, *args, **kwargs):
272 def get_search_view(self, *args, **kwargs):
273 return self.get_view(args, kwargs)
273 return self.get_view(args, kwargs)
274
274
275 def get_first_image(self) -> PostImage:
275 def get_first_image(self) -> PostImage:
276 return self.images.earliest('id')
276 return self.images.earliest('id')
277
277
278 def delete(self, using=None):
278 def delete(self, using=None):
279 """
279 """
280 Deletes all post images and the post itself.
280 Deletes all post images and the post itself.
281 """
281 """
282
282
283 for image in self.images.all():
283 for image in self.images.all():
284 image_refs_count = Post.objects.filter(images__in=[image]).count()
284 image_refs_count = Post.objects.filter(images__in=[image]).count()
285 if image_refs_count == 1:
285 if image_refs_count == 1:
286 image.delete()
286 image.delete()
287
287
288 thread = self.get_thread()
288 thread = self.get_thread()
289 thread.last_edit_time = timezone.now()
289 thread.last_edit_time = timezone.now()
290 thread.save()
290 thread.save()
291
291
292 super(Post, self).delete(using)
292 super(Post, self).delete(using)
293
293
294 logging.getLogger('boards.post.delete').info(
294 logging.getLogger('boards.post.delete').info(
295 'Deleted post {}'.format(self))
295 'Deleted post {}'.format(self))
296
296
297 # TODO Implement this with OOP, e.g. use the factory and HtmlPostData class
297 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
298 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
298 include_last_update=False) -> str:
299 include_last_update=False) -> str:
299 """
300 """
300 Gets post HTML or JSON data that can be rendered on a page or used by
301 Gets post HTML or JSON data that can be rendered on a page or used by
301 API.
302 API.
302 """
303 """
303
304
304 if format_type == DIFF_TYPE_HTML:
305 if format_type == DIFF_TYPE_HTML:
305 params = dict()
306 params['post'] = self
307 if PARAMETER_TRUNCATED in request.GET:
306 if PARAMETER_TRUNCATED in request.GET:
308 params[PARAMETER_TRUNCATED] = True
307 truncated = True
308 reply_link = False
309 else:
309 else:
310 params[PARAMETER_REPLY_LINK] = True
310 truncated = False
311 reply_link = True
311
312
312 return render_to_string('boards/api_post.html', params)
313 return self.get_view(truncated=truncated, reply_link=reply_link,
314 moderator=utils.is_moderator(request))
313 elif format_type == DIFF_TYPE_JSON:
315 elif format_type == DIFF_TYPE_JSON:
314 post_json = {
316 post_json = {
315 'id': self.id,
317 'id': self.id,
316 'title': self.title,
318 'title': self.title,
317 'text': self._text_rendered,
319 'text': self._text_rendered,
318 }
320 }
319 if self.images.exists():
321 if self.images.exists():
320 post_image = self.get_first_image()
322 post_image = self.get_first_image()
321 post_json['image'] = post_image.image.url
323 post_json['image'] = post_image.image.url
322 post_json['image_preview'] = post_image.image.url_200x150
324 post_json['image_preview'] = post_image.image.url_200x150
323 if include_last_update:
325 if include_last_update:
324 post_json['bump_time'] = datetime_to_epoch(
326 post_json['bump_time'] = utils.datetime_to_epoch(
325 self.get_thread().bump_time)
327 self.get_thread().bump_time)
326 return post_json
328 return post_json
327
329
328 def notify_clients(self, recursive=True):
330 def notify_clients(self, recursive=True):
329 """
331 """
330 Sends post HTML data to the thread web socket.
332 Sends post HTML data to the thread web socket.
331 """
333 """
332
334
333 if not settings.WEBSOCKETS_ENABLED:
335 if not settings.WEBSOCKETS_ENABLED:
334 return
336 return
335
337
336 thread_ids = list()
338 thread_ids = list()
337 for thread in self.get_threads().all():
339 for thread in self.get_threads().all():
338 thread_ids.append(thread.id)
340 thread_ids.append(thread.id)
339
341
340 thread.notify_clients()
342 thread.notify_clients()
341
343
342 if recursive:
344 if recursive:
343 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
345 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
344 post_id = reply_number.group(1)
346 post_id = reply_number.group(1)
345
347
346 try:
348 try:
347 ref_post = Post.objects.get(id=post_id)
349 ref_post = Post.objects.get(id=post_id)
348
350
349 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
351 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
350 # If post is in this thread, its thread was already notified.
352 # If post is in this thread, its thread was already notified.
351 # Otherwise, notify its thread separately.
353 # Otherwise, notify its thread separately.
352 ref_post.notify_clients(recursive=False)
354 ref_post.notify_clients(recursive=False)
353 except ObjectDoesNotExist:
355 except ObjectDoesNotExist:
354 pass
356 pass
355
357
356 def save(self, force_insert=False, force_update=False, using=None,
358 def save(self, force_insert=False, force_update=False, using=None,
357 update_fields=None):
359 update_fields=None):
358 self._text_rendered = Parser().parse(self.get_raw_text())
360 self._text_rendered = Parser().parse(self.get_raw_text())
359
361
360 if self.id:
362 if self.id:
361 for thread in self.get_threads().all():
363 for thread in self.get_threads().all():
362 if thread.can_bump():
364 if thread.can_bump():
363 thread.update_bump_status()
365 thread.update_bump_status()
364 thread.last_edit_time = self.last_edit_time
366 thread.last_edit_time = self.last_edit_time
365
367
366 thread.save(update_fields=['last_edit_time', 'bumpable'])
368 thread.save(update_fields=['last_edit_time', 'bumpable'])
367
369
368 super().save(force_insert, force_update, using, update_fields)
370 super().save(force_insert, force_update, using, update_fields)
369
371
370 def get_text(self) -> str:
372 def get_text(self) -> str:
371 return self._text_rendered
373 return self._text_rendered
372
374
373 def get_raw_text(self) -> str:
375 def get_raw_text(self) -> str:
374 return self.text
376 return self.text
375
377
376 def get_absolute_id(self) -> str:
378 def get_absolute_id(self) -> str:
377 """
379 """
378 If the post has many threads, shows its main thread OP id in the post
380 If the post has many threads, shows its main thread OP id in the post
379 ID.
381 ID.
380 """
382 """
381
383
382 if self.get_threads().count() > 1:
384 if self.get_threads().count() > 1:
383 return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id)
385 return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id)
384 else:
386 else:
385 return str(self.id)
387 return str(self.id)
386
388
387 def connect_notifications(self):
389 def connect_notifications(self):
388 for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()):
390 for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()):
389 user_name = reply_number.group(1).lower()
391 user_name = reply_number.group(1).lower()
390 Notification.objects.get_or_create(name=user_name, post=self)
392 Notification.objects.get_or_create(name=user_name, post=self)
391
393
392 def connect_replies(self):
394 def connect_replies(self):
393 """
395 """
394 Connects replies to a post to show them as a reflink map
396 Connects replies to a post to show them as a reflink map
395 """
397 """
396
398
397 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
399 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
398 post_id = reply_number.group(1)
400 post_id = reply_number.group(1)
399
401
400 try:
402 try:
401 referenced_post = Post.objects.get(id=post_id)
403 referenced_post = Post.objects.get(id=post_id)
402
404
403 referenced_post.referenced_posts.add(self)
405 referenced_post.referenced_posts.add(self)
404 referenced_post.last_edit_time = self.pub_time
406 referenced_post.last_edit_time = self.pub_time
405 referenced_post.build_refmap()
407 referenced_post.build_refmap()
406 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
408 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
407 except ObjectDoesNotExist:
409 except ObjectDoesNotExist:
408 pass
410 pass
409
411
410 def connect_threads(self, opening_posts):
412 def connect_threads(self, opening_posts):
411 """
413 """
412 If the referenced post is an OP in another thread,
414 If the referenced post is an OP in another thread,
413 make this post multi-thread.
415 make this post multi-thread.
414 """
416 """
415
417
416 for opening_post in opening_posts:
418 for opening_post in opening_posts:
417 threads = opening_post.get_threads().all()
419 threads = opening_post.get_threads().all()
418 for thread in threads:
420 for thread in threads:
419 if thread.can_bump():
421 if thread.can_bump():
420 thread.update_bump_status()
422 thread.update_bump_status()
421
423
422 thread.last_edit_time = self.last_edit_time
424 thread.last_edit_time = self.last_edit_time
423 thread.save(update_fields=['last_edit_time', 'bumpable'])
425 thread.save(update_fields=['last_edit_time', 'bumpable'])
424
426
425 self.threads.add(thread)
427 self.threads.add(thread)
@@ -1,76 +1,85 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 time
4 import time
5 import hmac
5 import hmac
6 import functools
6 import functools
7
7
8 from django.core.cache import cache
8 from django.core.cache import cache
9 from django.db.models import Model
9 from django.db.models import Model
10
10
11 from django.utils import timezone
11 from django.utils import timezone
12
12
13 from neboard import settings
13 from neboard import settings
14
14
15
15
16 CACHE_KEY_DELIMITER = '_'
16 CACHE_KEY_DELIMITER = '_'
17
17 PERMISSION_MODERATE = 'moderation'
18
18
19 def get_client_ip(request):
19 def get_client_ip(request):
20 x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
20 x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
21 if x_forwarded_for:
21 if x_forwarded_for:
22 ip = x_forwarded_for.split(',')[-1].strip()
22 ip = x_forwarded_for.split(',')[-1].strip()
23 else:
23 else:
24 ip = request.META.get('REMOTE_ADDR')
24 ip = request.META.get('REMOTE_ADDR')
25 return ip
25 return ip
26
26
27
27
28 # TODO The output format is not epoch because it includes microseconds
28 # TODO The output format is not epoch because it includes microseconds
29 def datetime_to_epoch(datetime):
29 def datetime_to_epoch(datetime):
30 return int(time.mktime(timezone.localtime(
30 return int(time.mktime(timezone.localtime(
31 datetime,timezone.get_current_timezone()).timetuple())
31 datetime,timezone.get_current_timezone()).timetuple())
32 * 1000000 + datetime.microsecond)
32 * 1000000 + datetime.microsecond)
33
33
34
34
35 def get_websocket_token(user_id='', timestamp=''):
35 def get_websocket_token(user_id='', timestamp=''):
36 """
36 """
37 Create token to validate information provided by new connection.
37 Create token to validate information provided by new connection.
38 """
38 """
39
39
40 sign = hmac.new(settings.CENTRIFUGE_PROJECT_SECRET.encode())
40 sign = hmac.new(settings.CENTRIFUGE_PROJECT_SECRET.encode())
41 sign.update(settings.CENTRIFUGE_PROJECT_ID.encode())
41 sign.update(settings.CENTRIFUGE_PROJECT_ID.encode())
42 sign.update(user_id.encode())
42 sign.update(user_id.encode())
43 sign.update(timestamp.encode())
43 sign.update(timestamp.encode())
44 token = sign.hexdigest()
44 token = sign.hexdigest()
45
45
46 return token
46 return token
47
47
48
48
49 def cached_result(key_method=None):
49 def cached_result(key_method=None):
50 """
50 """
51 Caches method result in the Django's cache system, persisted by object name,
51 Caches method result in the Django's cache system, persisted by object name,
52 object name and model id if object is a Django model.
52 object name and model id if object is a Django model.
53 """
53 """
54 def _cached_result(function):
54 def _cached_result(function):
55 def inner_func(obj, *args, **kwargs):
55 def inner_func(obj, *args, **kwargs):
56 # TODO Include method arguments to the cache key
56 # TODO Include method arguments to the cache key
57 cache_key_params = [obj.__class__.__name__, function.__name__]
57 cache_key_params = [obj.__class__.__name__, function.__name__]
58 if isinstance(obj, Model):
58 if isinstance(obj, Model):
59 cache_key_params.append(str(obj.id))
59 cache_key_params.append(str(obj.id))
60
60
61 if key_method is not None:
61 if key_method is not None:
62 cache_key_params += [str(arg) for arg in key_method(obj)]
62 cache_key_params += [str(arg) for arg in key_method(obj)]
63
63
64 cache_key = CACHE_KEY_DELIMITER.join(cache_key_params)
64 cache_key = CACHE_KEY_DELIMITER.join(cache_key_params)
65
65
66 persisted_result = cache.get(cache_key)
66 persisted_result = cache.get(cache_key)
67 if persisted_result is not None:
67 if persisted_result is not None:
68 result = persisted_result
68 result = persisted_result
69 else:
69 else:
70 result = function(obj, *args, **kwargs)
70 result = function(obj, *args, **kwargs)
71 cache.set(cache_key, result)
71 cache.set(cache_key, result)
72
72
73 return result
73 return result
74
74
75 return inner_func
75 return inner_func
76 return _cached_result
76 return _cached_result
77
78
79 def is_moderator(request):
80 try:
81 moderate = request.user.has_perm(PERMISSION_MODERATE)
82 except AttributeError:
83 moderate = False
84
85 return moderate No newline at end of file
1 NO CONTENT: file was removed
NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now