##// END OF EJS Templates
Implemented ini settings parser
neko259 -
r1153:bbf2916c default
parent child Browse files
Show More
@@ -0,0 +1,33 b''
1 [Version]
2 Version = 2.7.0 Chani
3 SiteName = Neboard
4
5 [Cache]
6 # Timeout for caching, if cache is used
7 CacheTimeout = 600
8
9 [Forms]
10 # Max post length in characters
11 MaxTextLength = 30000
12 MaxImageSize = 8000000
13 LimitPostingSpeed = false
14
15 [Messages]
16 # Thread bumplimit
17 MaxPostsPerThread = 10
18 # Old posts will be archived or deleted if this value is reached
19 MaxThreadCount = 5
20
21 [View]
22 DefaultTheme = md
23 DefaultImageViewer = simple
24 LastRepliesCount = 3
25 ThreadsPerPage = 3
26
27 [Storage]
28 # Enable archiving threads instead of deletion when the thread limit is reached
29 ArchiveThreads = true
30
31 [External]
32 # Thread update
33 WebsocketsEnabled = false
@@ -1,62 +1,63 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, SETTING_IMAGE_VIEWER
2 SETTING_USERNAME, SETTING_LAST_NOTIFICATION_ID, SETTING_IMAGE_VIEWER
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, utils
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 CONTEXT_IMAGE_VIEWER = 'image_viewer'
21 CONTEXT_IMAGE_VIEWER = 'image_viewer'
22
22
23
23
24 def get_notifications(context, request):
24 def get_notifications(context, request):
25 settings_manager = get_settings_manager(request)
25 settings_manager = get_settings_manager(request)
26 username = settings_manager.get_setting(SETTING_USERNAME)
26 username = settings_manager.get_setting(SETTING_USERNAME)
27 new_notifications_count = 0
27 new_notifications_count = 0
28 if username is not None and len(username) > 0:
28 if username is not None and len(username) > 0:
29 last_notification_id = settings_manager.get_setting(
29 last_notification_id = settings_manager.get_setting(
30 SETTING_LAST_NOTIFICATION_ID)
30 SETTING_LAST_NOTIFICATION_ID)
31
31
32 new_notifications_count = Notification.objects.get_notification_posts(
32 new_notifications_count = Notification.objects.get_notification_posts(
33 username=username, last=last_notification_id).count()
33 username=username, last=last_notification_id).count()
34 context[CONTEXT_NEW_NOTIFICATIONS_COUNT] = new_notifications_count
34 context[CONTEXT_NEW_NOTIFICATIONS_COUNT] = new_notifications_count
35 context[CONTEXT_USERNAME] = username
35 context[CONTEXT_USERNAME] = username
36
36
37
37
38 def user_and_ui_processor(request):
38 def user_and_ui_processor(request):
39 context = dict()
39 context = dict()
40
40
41 context[CONTEXT_PPD] = float(Post.objects.get_posts_per_day())
41 context[CONTEXT_PPD] = float(Post.objects.get_posts_per_day())
42
42
43 settings_manager = get_settings_manager(request)
43 settings_manager = get_settings_manager(request)
44 fav_tags = settings_manager.get_fav_tags()
44 fav_tags = settings_manager.get_fav_tags()
45 context[CONTEXT_TAGS] = fav_tags
45 context[CONTEXT_TAGS] = fav_tags
46 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)
47 theme = settings_manager.get_theme()
47 theme = settings_manager.get_theme()
48 context[CONTEXT_THEME] = theme
48 context[CONTEXT_THEME] = theme
49 context[CONTEXT_THEME_CSS] = 'css/' + theme + '/base_page.css'
49 context[CONTEXT_THEME_CSS] = 'css/' + theme + '/base_page.css'
50
50
51 # This shows the moderator panel
51 # This shows the moderator panel
52 context[CONTEXT_MODERATOR] = utils.is_moderator(request)
52 context[CONTEXT_MODERATOR] = utils.is_moderator(request)
53
53
54 context[CONTEXT_VERSION] = settings.VERSION
54 context[CONTEXT_VERSION] = settings.get('Version', 'Version')
55 context[CONTEXT_SITE_NAME] = settings.SITE_NAME
55 context[CONTEXT_SITE_NAME] = settings.get('Version', 'SiteName')
56
56
57 context[CONTEXT_IMAGE_VIEWER] = settings_manager.get_setting(
57 context[CONTEXT_IMAGE_VIEWER] = settings_manager.get_setting(
58 SETTING_IMAGE_VIEWER, default=settings.DEFAULT_IMAGE_VIEWER)
58 SETTING_IMAGE_VIEWER,
59 default=settings.get('View', 'DefaultImageViewer'))
59
60
60 get_notifications(context, request)
61 get_notifications(context, request)
61
62
62 return context
63 return context
@@ -1,384 +1,384 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">'
102 normal_row='<div class="form-row">'
103 '<div class="form-label">'
103 '<div class="form-label">'
104 '%(label)s'
104 '%(label)s'
105 '</div>'
105 '</div>'
106 '<div class="form-input">'
106 '<div class="form-input">'
107 '%(field)s'
107 '%(field)s'
108 '</div>'
108 '</div>'
109 '</div>'
109 '</div>'
110 '<div class="form-row">'
110 '<div class="form-row">'
111 '%(help_text)s'
111 '%(help_text)s'
112 '</div>',
112 '</div>',
113 error_row='<div class="form-row">'
113 error_row='<div class="form-row">'
114 '<div class="form-label"></div>'
114 '<div class="form-label"></div>'
115 '<div class="form-errors">%s</div>'
115 '<div class="form-errors">%s</div>'
116 '</div>',
116 '</div>',
117 row_ender='</div>',
117 row_ender='</div>',
118 help_text_html='%s',
118 help_text_html='%s',
119 errors_on_separate_row=True)
119 errors_on_separate_row=True)
120
120
121 def as_json_errors(self):
121 def as_json_errors(self):
122 errors = []
122 errors = []
123
123
124 for name, field in list(self.fields.items()):
124 for name, field in list(self.fields.items()):
125 if self[name].errors:
125 if self[name].errors:
126 errors.append({
126 errors.append({
127 'field': name,
127 'field': name,
128 'errors': self[name].errors.as_text(),
128 'errors': self[name].errors.as_text(),
129 })
129 })
130
130
131 return errors
131 return errors
132
132
133
133
134 class PostForm(NeboardForm):
134 class PostForm(NeboardForm):
135
135
136 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
136 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
137 label=LABEL_TITLE)
137 label=LABEL_TITLE)
138 text = forms.CharField(
138 text = forms.CharField(
139 widget=FormatPanel(attrs={
139 widget=FormatPanel(attrs={
140 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
140 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
141 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
141 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
142 }),
142 }),
143 required=False, label=LABEL_TEXT)
143 required=False, label=LABEL_TEXT)
144 image = forms.ImageField(required=False, label=_('Image'),
144 image = forms.ImageField(required=False, label=_('Image'),
145 widget=forms.ClearableFileInput(
145 widget=forms.ClearableFileInput(
146 attrs={'accept': 'image/*'}))
146 attrs={'accept': 'image/*'}))
147 image_url = forms.CharField(required=False, label=_('Image URL'),
147 image_url = forms.CharField(required=False, label=_('Image URL'),
148 widget=forms.TextInput(
148 widget=forms.TextInput(
149 attrs={ATTRIBUTE_PLACEHOLDER:
149 attrs={ATTRIBUTE_PLACEHOLDER:
150 'http://example.com/image.png'}))
150 'http://example.com/image.png'}))
151
151
152 # This field is for spam prevention only
152 # This field is for spam prevention only
153 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
153 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
154 widget=forms.TextInput(attrs={
154 widget=forms.TextInput(attrs={
155 'class': 'form-email'}))
155 'class': 'form-email'}))
156 threads = forms.CharField(required=False, label=_('Additional threads'),
156 threads = forms.CharField(required=False, label=_('Additional threads'),
157 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
157 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
158 '123 456 789'}))
158 '123 456 789'}))
159
159
160 session = None
160 session = None
161 need_to_ban = False
161 need_to_ban = False
162
162
163 def clean_title(self):
163 def clean_title(self):
164 title = self.cleaned_data['title']
164 title = self.cleaned_data['title']
165 if title:
165 if title:
166 if len(title) > TITLE_MAX_LENGTH:
166 if len(title) > TITLE_MAX_LENGTH:
167 raise forms.ValidationError(_('Title must have less than %s '
167 raise forms.ValidationError(_('Title must have less than %s '
168 'characters') %
168 'characters') %
169 str(TITLE_MAX_LENGTH))
169 str(TITLE_MAX_LENGTH))
170 return title
170 return title
171
171
172 def clean_text(self):
172 def clean_text(self):
173 text = self.cleaned_data['text'].strip()
173 text = self.cleaned_data['text'].strip()
174 if text:
174 if text:
175 if len(text) > board_settings.MAX_TEXT_LENGTH:
175 max_length = board_settings.get_int('Forms', 'MaxTextLength')
176 if len(text) > max_length:
176 raise forms.ValidationError(_('Text must have less than %s '
177 raise forms.ValidationError(_('Text must have less than %s '
177 'characters') %
178 'characters') % str(max_length))
178 str(board_settings
179 .MAX_TEXT_LENGTH))
180 return text
179 return text
181
180
182 def clean_image(self):
181 def clean_image(self):
183 image = self.cleaned_data['image']
182 image = self.cleaned_data['image']
184
183
185 if image:
184 if image:
186 self.validate_image_size(image.size)
185 self.validate_image_size(image.size)
187
186
188 return image
187 return image
189
188
190 def clean_image_url(self):
189 def clean_image_url(self):
191 url = self.cleaned_data['image_url']
190 url = self.cleaned_data['image_url']
192
191
193 image = None
192 image = None
194 if url:
193 if url:
195 image = self._get_image_from_url(url)
194 image = self._get_image_from_url(url)
196
195
197 if not image:
196 if not image:
198 raise forms.ValidationError(_('Invalid URL'))
197 raise forms.ValidationError(_('Invalid URL'))
199 else:
198 else:
200 self.validate_image_size(image.size)
199 self.validate_image_size(image.size)
201
200
202 return image
201 return image
203
202
204 def clean_threads(self):
203 def clean_threads(self):
205 threads_str = self.cleaned_data['threads']
204 threads_str = self.cleaned_data['threads']
206
205
207 if len(threads_str) > 0:
206 if len(threads_str) > 0:
208 threads_id_list = threads_str.split(' ')
207 threads_id_list = threads_str.split(' ')
209
208
210 threads = list()
209 threads = list()
211
210
212 for thread_id in threads_id_list:
211 for thread_id in threads_id_list:
213 try:
212 try:
214 thread = Post.objects.get(id=int(thread_id))
213 thread = Post.objects.get(id=int(thread_id))
215 if not thread.is_opening() or thread.get_thread().archived:
214 if not thread.is_opening() or thread.get_thread().archived:
216 raise ObjectDoesNotExist()
215 raise ObjectDoesNotExist()
217 threads.append(thread)
216 threads.append(thread)
218 except (ObjectDoesNotExist, ValueError):
217 except (ObjectDoesNotExist, ValueError):
219 raise forms.ValidationError(_('Invalid additional thread list'))
218 raise forms.ValidationError(_('Invalid additional thread list'))
220
219
221 return threads
220 return threads
222
221
223 def clean(self):
222 def clean(self):
224 cleaned_data = super(PostForm, self).clean()
223 cleaned_data = super(PostForm, self).clean()
225
224
226 if cleaned_data['email']:
225 if cleaned_data['email']:
227 self.need_to_ban = True
226 self.need_to_ban = True
228 raise forms.ValidationError('A human cannot enter a hidden field')
227 raise forms.ValidationError('A human cannot enter a hidden field')
229
228
230 if not self.errors:
229 if not self.errors:
231 self._clean_text_image()
230 self._clean_text_image()
232
231
233 if not self.errors and self.session:
232 if not self.errors and self.session:
234 self._validate_posting_speed()
233 self._validate_posting_speed()
235
234
236 return cleaned_data
235 return cleaned_data
237
236
238 def get_image(self):
237 def get_image(self):
239 """
238 """
240 Gets image from file or URL.
239 Gets image from file or URL.
241 """
240 """
242
241
243 image = self.cleaned_data['image']
242 image = self.cleaned_data['image']
244 return image if image else self.cleaned_data['image_url']
243 return image if image else self.cleaned_data['image_url']
245
244
246 def _clean_text_image(self):
245 def _clean_text_image(self):
247 text = self.cleaned_data.get('text')
246 text = self.cleaned_data.get('text')
248 image = self.get_image()
247 image = self.get_image()
249
248
250 if (not text) and (not image):
249 if (not text) and (not image):
251 error_message = _('Either text or image must be entered.')
250 error_message = _('Either text or image must be entered.')
252 self._errors['text'] = self.error_class([error_message])
251 self._errors['text'] = self.error_class([error_message])
253
252
254 def _validate_posting_speed(self):
253 def _validate_posting_speed(self):
255 can_post = True
254 can_post = True
256
255
257 posting_delay = settings.POSTING_DELAY
256 posting_delay = settings.POSTING_DELAY
258
257
259 if board_settings.LIMIT_POSTING_SPEED:
258 if board_settings.get_bool('Forms', 'LimitPostingSpeed'):
260 now = time.time()
259 now = time.time()
261
260
262 current_delay = 0
261 current_delay = 0
263 need_delay = False
262 need_delay = False
264
263
265 if not LAST_POST_TIME in self.session:
264 if not LAST_POST_TIME in self.session:
266 self.session[LAST_POST_TIME] = now
265 self.session[LAST_POST_TIME] = now
267
266
268 need_delay = True
267 need_delay = True
269 else:
268 else:
270 last_post_time = self.session.get(LAST_POST_TIME)
269 last_post_time = self.session.get(LAST_POST_TIME)
271 current_delay = int(now - last_post_time)
270 current_delay = int(now - last_post_time)
272
271
273 need_delay = current_delay < posting_delay
272 need_delay = current_delay < posting_delay
274
273
275 if need_delay:
274 if need_delay:
276 error_message = ERROR_SPEED % str(posting_delay
275 error_message = ERROR_SPEED % str(posting_delay
277 - current_delay)
276 - current_delay)
278 self._errors['text'] = self.error_class([error_message])
277 self._errors['text'] = self.error_class([error_message])
279
278
280 can_post = False
279 can_post = False
281
280
282 if can_post:
281 if can_post:
283 self.session[LAST_POST_TIME] = now
282 self.session[LAST_POST_TIME] = now
284
283
285 def validate_image_size(self, size: int):
284 def validate_image_size(self, size: int):
286 if size > board_settings.MAX_IMAGE_SIZE:
285 max_size = board_settings.get_int('Forms', 'MaxImageSize')
286 if size > max_size:
287 raise forms.ValidationError(
287 raise forms.ValidationError(
288 _('Image must be less than %s bytes')
288 _('Image must be less than %s bytes')
289 % str(board_settings.MAX_IMAGE_SIZE))
289 % str(max_size))
290
290
291 def _get_image_from_url(self, url: str) -> SimpleUploadedFile:
291 def _get_image_from_url(self, url: str) -> SimpleUploadedFile:
292 """
292 """
293 Gets an image file from URL.
293 Gets an image file from URL.
294 """
294 """
295
295
296 img_temp = None
296 img_temp = None
297
297
298 try:
298 try:
299 # Verify content headers
299 # Verify content headers
300 response_head = requests.head(url, verify=False)
300 response_head = requests.head(url, verify=False)
301 content_type = response_head.headers['content-type'].split(';')[0]
301 content_type = response_head.headers['content-type'].split(';')[0]
302 if content_type in CONTENT_TYPE_IMAGE:
302 if content_type in CONTENT_TYPE_IMAGE:
303 length_header = response_head.headers.get('content-length')
303 length_header = response_head.headers.get('content-length')
304 if length_header:
304 if length_header:
305 length = int(length_header)
305 length = int(length_header)
306 self.validate_image_size(length)
306 self.validate_image_size(length)
307 # Get the actual content into memory
307 # Get the actual content into memory
308 response = requests.get(url, verify=False, stream=True)
308 response = requests.get(url, verify=False, stream=True)
309
309
310 # Download image, stop if the size exceeds limit
310 # Download image, stop if the size exceeds limit
311 size = 0
311 size = 0
312 content = b''
312 content = b''
313 for chunk in response.iter_content(IMAGE_DOWNLOAD_CHUNK_BYTES):
313 for chunk in response.iter_content(IMAGE_DOWNLOAD_CHUNK_BYTES):
314 size += len(chunk)
314 size += len(chunk)
315 self.validate_image_size(size)
315 self.validate_image_size(size)
316 content += chunk
316 content += chunk
317
317
318 if response.status_code == HTTP_RESULT_OK and content:
318 if response.status_code == HTTP_RESULT_OK and content:
319 # Set a dummy file name that will be replaced
319 # Set a dummy file name that will be replaced
320 # anyway, just keep the valid extension
320 # anyway, just keep the valid extension
321 filename = 'image.' + content_type.split('/')[1]
321 filename = 'image.' + content_type.split('/')[1]
322 img_temp = SimpleUploadedFile(filename, content,
322 img_temp = SimpleUploadedFile(filename, content,
323 content_type)
323 content_type)
324 except Exception:
324 except Exception:
325 # Just return no image
325 # Just return no image
326 pass
326 pass
327
327
328 return img_temp
328 return img_temp
329
329
330
330
331 class ThreadForm(PostForm):
331 class ThreadForm(PostForm):
332
332
333 tags = forms.CharField(
333 tags = forms.CharField(
334 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
334 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
335 max_length=100, label=_('Tags'), required=True)
335 max_length=100, label=_('Tags'), required=True)
336
336
337 def clean_tags(self):
337 def clean_tags(self):
338 tags = self.cleaned_data['tags'].strip()
338 tags = self.cleaned_data['tags'].strip()
339
339
340 if not tags or not REGEX_TAGS.match(tags):
340 if not tags or not REGEX_TAGS.match(tags):
341 raise forms.ValidationError(
341 raise forms.ValidationError(
342 _('Inappropriate characters in tags.'))
342 _('Inappropriate characters in tags.'))
343
343
344 required_tag_exists = False
344 required_tag_exists = False
345 for tag in tags.split():
345 for tag in tags.split():
346 try:
346 try:
347 Tag.objects.get(name=tag.strip().lower(), required=True)
347 Tag.objects.get(name=tag.strip().lower(), required=True)
348 required_tag_exists = True
348 required_tag_exists = True
349 break
349 break
350 except ObjectDoesNotExist:
350 except ObjectDoesNotExist:
351 pass
351 pass
352
352
353 if not required_tag_exists:
353 if not required_tag_exists:
354 all_tags = Tag.objects.filter(required=True)
354 all_tags = Tag.objects.filter(required=True)
355 raise forms.ValidationError(
355 raise forms.ValidationError(
356 _('Need at least one of the tags: ')
356 _('Need at least one of the tags: ')
357 + ', '.join([tag.name for tag in all_tags]))
357 + ', '.join([tag.name for tag in all_tags]))
358
358
359 return tags
359 return tags
360
360
361 def clean(self):
361 def clean(self):
362 cleaned_data = super(ThreadForm, self).clean()
362 cleaned_data = super(ThreadForm, self).clean()
363
363
364 return cleaned_data
364 return cleaned_data
365
365
366
366
367 class SettingsForm(NeboardForm):
367 class SettingsForm(NeboardForm):
368
368
369 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
369 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
370 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('Image view mode'))
370 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('Image view mode'))
371 username = forms.CharField(label=_('User name'), required=False)
371 username = forms.CharField(label=_('User name'), required=False)
372 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
372 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
373
373
374 def clean_username(self):
374 def clean_username(self):
375 username = self.cleaned_data['username']
375 username = self.cleaned_data['username']
376
376
377 if username and not REGEX_TAGS.match(username):
377 if username and not REGEX_TAGS.match(username):
378 raise forms.ValidationError(_('Inappropriate characters.'))
378 raise forms.ValidationError(_('Inappropriate characters.'))
379
379
380 return username
380 return username
381
381
382
382
383 class SearchForm(NeboardForm):
383 class SearchForm(NeboardForm):
384 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
384 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
@@ -1,438 +1,438 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 import uuid
5 import uuid
6
6
7 from django.core.exceptions import ObjectDoesNotExist
7 from django.core.exceptions import ObjectDoesNotExist
8 from django.core.urlresolvers import reverse
8 from django.core.urlresolvers import reverse
9 from django.db import models, transaction
9 from django.db import models, transaction
10 from django.db.models import TextField
10 from django.db.models import TextField
11 from django.template.loader import render_to_string
11 from django.template.loader import render_to_string
12 from django.utils import timezone
12 from django.utils import timezone
13
13
14 from boards import settings
14 from boards import settings
15 from boards.mdx_neboard import Parser
15 from boards.mdx_neboard import Parser
16 from boards.models import PostImage
16 from boards.models import PostImage
17 from boards.models.base import Viewable
17 from boards.models.base import Viewable
18 from boards import utils
18 from boards import utils
19 from boards.models.user import Notification, Ban
19 from boards.models.user import Notification, Ban
20 import boards.models.thread
20 import boards.models.thread
21
21
22
22
23 APP_LABEL_BOARDS = 'boards'
23 APP_LABEL_BOARDS = 'boards'
24
24
25 POSTS_PER_DAY_RANGE = 7
25 POSTS_PER_DAY_RANGE = 7
26
26
27 BAN_REASON_AUTO = 'Auto'
27 BAN_REASON_AUTO = 'Auto'
28
28
29 IMAGE_THUMB_SIZE = (200, 150)
29 IMAGE_THUMB_SIZE = (200, 150)
30
30
31 TITLE_MAX_LENGTH = 200
31 TITLE_MAX_LENGTH = 200
32
32
33 # TODO This should be removed
33 # TODO This should be removed
34 NO_IP = '0.0.0.0'
34 NO_IP = '0.0.0.0'
35
35
36 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
36 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
37 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
37 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
38
38
39 PARAMETER_TRUNCATED = 'truncated'
39 PARAMETER_TRUNCATED = 'truncated'
40 PARAMETER_TAG = 'tag'
40 PARAMETER_TAG = 'tag'
41 PARAMETER_OFFSET = 'offset'
41 PARAMETER_OFFSET = 'offset'
42 PARAMETER_DIFF_TYPE = 'type'
42 PARAMETER_DIFF_TYPE = 'type'
43 PARAMETER_CSS_CLASS = 'css_class'
43 PARAMETER_CSS_CLASS = 'css_class'
44 PARAMETER_THREAD = 'thread'
44 PARAMETER_THREAD = 'thread'
45 PARAMETER_IS_OPENING = 'is_opening'
45 PARAMETER_IS_OPENING = 'is_opening'
46 PARAMETER_MODERATOR = 'moderator'
46 PARAMETER_MODERATOR = 'moderator'
47 PARAMETER_POST = 'post'
47 PARAMETER_POST = 'post'
48 PARAMETER_OP_ID = 'opening_post_id'
48 PARAMETER_OP_ID = 'opening_post_id'
49 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
49 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
50 PARAMETER_REPLY_LINK = 'reply_link'
50 PARAMETER_REPLY_LINK = 'reply_link'
51
51
52 DIFF_TYPE_HTML = 'html'
52 DIFF_TYPE_HTML = 'html'
53 DIFF_TYPE_JSON = 'json'
53 DIFF_TYPE_JSON = 'json'
54
54
55 REFMAP_STR = '<a href="{}">&gt;&gt;{}</a>'
55 REFMAP_STR = '<a href="{}">&gt;&gt;{}</a>'
56
56
57
57
58 class PostManager(models.Manager):
58 class PostManager(models.Manager):
59 @transaction.atomic
59 @transaction.atomic
60 def create_post(self, title: str, text: str, image=None, thread=None,
60 def create_post(self, title: str, text: str, image=None, thread=None,
61 ip=NO_IP, tags: list=None, threads: list=None):
61 ip=NO_IP, tags: list=None, threads: list=None):
62 """
62 """
63 Creates new post
63 Creates new post
64 """
64 """
65
65
66 is_banned = Ban.objects.filter(ip=ip).exists()
66 is_banned = Ban.objects.filter(ip=ip).exists()
67
67
68 # TODO Raise specific exception and catch it in the views
68 # TODO Raise specific exception and catch it in the views
69 if is_banned:
69 if is_banned:
70 raise Exception("This user is banned")
70 raise Exception("This user is banned")
71
71
72 if not tags:
72 if not tags:
73 tags = []
73 tags = []
74 if not threads:
74 if not threads:
75 threads = []
75 threads = []
76
76
77 posting_time = timezone.now()
77 posting_time = timezone.now()
78 if not thread:
78 if not thread:
79 thread = boards.models.thread.Thread.objects.create(
79 thread = boards.models.thread.Thread.objects.create(
80 bump_time=posting_time, last_edit_time=posting_time)
80 bump_time=posting_time, last_edit_time=posting_time)
81 new_thread = True
81 new_thread = True
82 else:
82 else:
83 new_thread = False
83 new_thread = False
84
84
85 pre_text = Parser().preparse(text)
85 pre_text = Parser().preparse(text)
86
86
87 post = self.create(title=title,
87 post = self.create(title=title,
88 text=pre_text,
88 text=pre_text,
89 pub_time=posting_time,
89 pub_time=posting_time,
90 poster_ip=ip,
90 poster_ip=ip,
91 thread=thread,
91 thread=thread,
92 last_edit_time=posting_time)
92 last_edit_time=posting_time)
93 post.threads.add(thread)
93 post.threads.add(thread)
94
94
95 logger = logging.getLogger('boards.post.create')
95 logger = logging.getLogger('boards.post.create')
96
96
97 logger.info('Created post {} by {}'.format(post, post.poster_ip))
97 logger.info('Created post {} by {}'.format(post, post.poster_ip))
98
98
99 if image:
99 if image:
100 post.images.add(PostImage.objects.create_with_hash(image))
100 post.images.add(PostImage.objects.create_with_hash(image))
101
101
102 list(map(thread.add_tag, tags))
102 list(map(thread.add_tag, tags))
103
103
104 if new_thread:
104 if new_thread:
105 boards.models.thread.Thread.objects.process_oldest_threads()
105 boards.models.thread.Thread.objects.process_oldest_threads()
106 else:
106 else:
107 thread.last_edit_time = posting_time
107 thread.last_edit_time = posting_time
108 thread.bump()
108 thread.bump()
109 thread.save()
109 thread.save()
110
110
111 post.connect_replies()
111 post.connect_replies()
112 post.connect_threads(threads)
112 post.connect_threads(threads)
113 post.connect_notifications()
113 post.connect_notifications()
114
114
115 post.build_url()
115 post.build_url()
116
116
117 return post
117 return post
118
118
119 def delete_posts_by_ip(self, ip):
119 def delete_posts_by_ip(self, ip):
120 """
120 """
121 Deletes all posts of the author with same IP
121 Deletes all posts of the author with same IP
122 """
122 """
123
123
124 posts = self.filter(poster_ip=ip)
124 posts = self.filter(poster_ip=ip)
125 for post in posts:
125 for post in posts:
126 post.delete()
126 post.delete()
127
127
128 @utils.cached_result()
128 @utils.cached_result()
129 def get_posts_per_day(self) -> float:
129 def get_posts_per_day(self) -> float:
130 """
130 """
131 Gets average count of posts per day for the last 7 days
131 Gets average count of posts per day for the last 7 days
132 """
132 """
133
133
134 day_end = date.today()
134 day_end = date.today()
135 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
135 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
136
136
137 day_time_start = timezone.make_aware(datetime.combine(
137 day_time_start = timezone.make_aware(datetime.combine(
138 day_start, dtime()), timezone.get_current_timezone())
138 day_start, dtime()), timezone.get_current_timezone())
139 day_time_end = timezone.make_aware(datetime.combine(
139 day_time_end = timezone.make_aware(datetime.combine(
140 day_end, dtime()), timezone.get_current_timezone())
140 day_end, dtime()), timezone.get_current_timezone())
141
141
142 posts_per_period = float(self.filter(
142 posts_per_period = float(self.filter(
143 pub_time__lte=day_time_end,
143 pub_time__lte=day_time_end,
144 pub_time__gte=day_time_start).count())
144 pub_time__gte=day_time_start).count())
145
145
146 ppd = posts_per_period / POSTS_PER_DAY_RANGE
146 ppd = posts_per_period / POSTS_PER_DAY_RANGE
147
147
148 return ppd
148 return ppd
149
149
150
150
151 class Post(models.Model, Viewable):
151 class Post(models.Model, Viewable):
152 """A post is a message."""
152 """A post is a message."""
153
153
154 objects = PostManager()
154 objects = PostManager()
155
155
156 class Meta:
156 class Meta:
157 app_label = APP_LABEL_BOARDS
157 app_label = APP_LABEL_BOARDS
158 ordering = ('id',)
158 ordering = ('id',)
159
159
160 title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
160 title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
161 pub_time = models.DateTimeField()
161 pub_time = models.DateTimeField()
162 text = TextField(blank=True, null=True)
162 text = TextField(blank=True, null=True)
163 _text_rendered = TextField(blank=True, null=True, editable=False)
163 _text_rendered = TextField(blank=True, null=True, editable=False)
164
164
165 images = models.ManyToManyField(PostImage, null=True, blank=True,
165 images = models.ManyToManyField(PostImage, null=True, blank=True,
166 related_name='ip+', db_index=True)
166 related_name='ip+', db_index=True)
167
167
168 poster_ip = models.GenericIPAddressField()
168 poster_ip = models.GenericIPAddressField()
169
169
170 # TODO This field can be removed cause UID is used for update now
170 # TODO This field can be removed cause UID is used for update now
171 last_edit_time = models.DateTimeField()
171 last_edit_time = models.DateTimeField()
172
172
173 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
173 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
174 null=True,
174 null=True,
175 blank=True, related_name='rfp+',
175 blank=True, related_name='rfp+',
176 db_index=True)
176 db_index=True)
177 refmap = models.TextField(null=True, blank=True)
177 refmap = models.TextField(null=True, blank=True)
178 threads = models.ManyToManyField('Thread', db_index=True)
178 threads = models.ManyToManyField('Thread', db_index=True)
179 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
179 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
180
180
181 url = models.TextField()
181 url = models.TextField()
182 uid = models.TextField(db_index=True)
182 uid = models.TextField(db_index=True)
183
183
184 def __str__(self):
184 def __str__(self):
185 return 'P#{}/{}'.format(self.id, self.title)
185 return 'P#{}/{}'.format(self.id, self.title)
186
186
187 def get_title(self) -> str:
187 def get_title(self) -> str:
188 """
188 """
189 Gets original post title or part of its text.
189 Gets original post title or part of its text.
190 """
190 """
191
191
192 title = self.title
192 title = self.title
193 if not title:
193 if not title:
194 title = self.get_text()
194 title = self.get_text()
195
195
196 return title
196 return title
197
197
198 def build_refmap(self) -> None:
198 def build_refmap(self) -> None:
199 """
199 """
200 Builds a replies map string from replies list. This is a cache to stop
200 Builds a replies map string from replies list. This is a cache to stop
201 the server from recalculating the map on every post show.
201 the server from recalculating the map on every post show.
202 """
202 """
203
203
204 post_urls = [REFMAP_STR.format(refpost.get_url(), refpost.id)
204 post_urls = [REFMAP_STR.format(refpost.get_url(), refpost.id)
205 for refpost in self.referenced_posts.all()]
205 for refpost in self.referenced_posts.all()]
206
206
207 self.refmap = ', '.join(post_urls)
207 self.refmap = ', '.join(post_urls)
208
208
209 def is_referenced(self) -> bool:
209 def is_referenced(self) -> bool:
210 return self.refmap and len(self.refmap) > 0
210 return self.refmap and len(self.refmap) > 0
211
211
212 def is_opening(self) -> bool:
212 def is_opening(self) -> bool:
213 """
213 """
214 Checks if this is an opening post or just a reply.
214 Checks if this is an opening post or just a reply.
215 """
215 """
216
216
217 return self.get_thread().get_opening_post_id() == self.id
217 return self.get_thread().get_opening_post_id() == self.id
218
218
219 # TODO Remove this and use get_absolute_url method
219 # TODO Remove this and use get_absolute_url method
220 def get_url(self):
220 def get_url(self):
221 return self.url
221 return self.url
222
222
223 def get_absolute_url(self):
223 def get_absolute_url(self):
224 return self.url
224 return self.url
225
225
226 def get_thread(self):
226 def get_thread(self):
227 return self.thread
227 return self.thread
228
228
229 def get_threads(self) -> list:
229 def get_threads(self) -> list:
230 """
230 """
231 Gets post's thread.
231 Gets post's thread.
232 """
232 """
233
233
234 return self.threads
234 return self.threads
235
235
236 def get_view(self, moderator=False, need_open_link=False,
236 def get_view(self, moderator=False, need_open_link=False,
237 truncated=False, reply_link=False, *args, **kwargs) -> str:
237 truncated=False, reply_link=False, *args, **kwargs) -> str:
238 """
238 """
239 Renders post's HTML view. Some of the post params can be passed over
239 Renders post's HTML view. Some of the post params can be passed over
240 kwargs for the means of caching (if we view the thread, some params
240 kwargs for the means of caching (if we view the thread, some params
241 are same for every post and don't need to be computed over and over.
241 are same for every post and don't need to be computed over and over.
242 """
242 """
243
243
244 thread = self.get_thread()
244 thread = self.get_thread()
245 is_opening = kwargs.get(PARAMETER_IS_OPENING, self.is_opening())
245 is_opening = kwargs.get(PARAMETER_IS_OPENING, self.is_opening())
246
246
247 if is_opening:
247 if is_opening:
248 opening_post_id = self.id
248 opening_post_id = self.id
249 else:
249 else:
250 opening_post_id = thread.get_opening_post_id()
250 opening_post_id = thread.get_opening_post_id()
251
251
252 css_class = 'post'
252 css_class = 'post'
253 if thread.archived:
253 if thread.archived:
254 css_class += ' archive_post'
254 css_class += ' archive_post'
255 elif not thread.can_bump():
255 elif not thread.can_bump():
256 css_class += ' dead_post'
256 css_class += ' dead_post'
257
257
258 return render_to_string('boards/post.html', {
258 return render_to_string('boards/post.html', {
259 PARAMETER_POST: self,
259 PARAMETER_POST: self,
260 PARAMETER_MODERATOR: moderator,
260 PARAMETER_MODERATOR: moderator,
261 PARAMETER_IS_OPENING: is_opening,
261 PARAMETER_IS_OPENING: is_opening,
262 PARAMETER_THREAD: thread,
262 PARAMETER_THREAD: thread,
263 PARAMETER_CSS_CLASS: css_class,
263 PARAMETER_CSS_CLASS: css_class,
264 PARAMETER_NEED_OPEN_LINK: need_open_link,
264 PARAMETER_NEED_OPEN_LINK: need_open_link,
265 PARAMETER_TRUNCATED: truncated,
265 PARAMETER_TRUNCATED: truncated,
266 PARAMETER_OP_ID: opening_post_id,
266 PARAMETER_OP_ID: opening_post_id,
267 PARAMETER_REPLY_LINK: reply_link,
267 PARAMETER_REPLY_LINK: reply_link,
268 })
268 })
269
269
270 def get_search_view(self, *args, **kwargs):
270 def get_search_view(self, *args, **kwargs):
271 return self.get_view(args, kwargs)
271 return self.get_view(args, kwargs)
272
272
273 def get_first_image(self) -> PostImage:
273 def get_first_image(self) -> PostImage:
274 return self.images.earliest('id')
274 return self.images.earliest('id')
275
275
276 def delete(self, using=None):
276 def delete(self, using=None):
277 """
277 """
278 Deletes all post images and the post itself.
278 Deletes all post images and the post itself.
279 """
279 """
280
280
281 for image in self.images.all():
281 for image in self.images.all():
282 image_refs_count = Post.objects.filter(images__in=[image]).count()
282 image_refs_count = Post.objects.filter(images__in=[image]).count()
283 if image_refs_count == 1:
283 if image_refs_count == 1:
284 image.delete()
284 image.delete()
285
285
286 thread = self.get_thread()
286 thread = self.get_thread()
287 thread.last_edit_time = timezone.now()
287 thread.last_edit_time = timezone.now()
288 thread.save()
288 thread.save()
289
289
290 super(Post, self).delete(using)
290 super(Post, self).delete(using)
291
291
292 logging.getLogger('boards.post.delete').info(
292 logging.getLogger('boards.post.delete').info(
293 'Deleted post {}'.format(self))
293 'Deleted post {}'.format(self))
294
294
295 # TODO Implement this with OOP, e.g. use the factory and HtmlPostData class
295 # TODO Implement this with OOP, e.g. use the factory and HtmlPostData class
296 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
296 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
297 include_last_update=False) -> str:
297 include_last_update=False) -> str:
298 """
298 """
299 Gets post HTML or JSON data that can be rendered on a page or used by
299 Gets post HTML or JSON data that can be rendered on a page or used by
300 API.
300 API.
301 """
301 """
302
302
303 if format_type == DIFF_TYPE_HTML:
303 if format_type == DIFF_TYPE_HTML:
304 if request is not None and PARAMETER_TRUNCATED in request.GET:
304 if request is not None and PARAMETER_TRUNCATED in request.GET:
305 truncated = True
305 truncated = True
306 reply_link = False
306 reply_link = False
307 else:
307 else:
308 truncated = False
308 truncated = False
309 reply_link = True
309 reply_link = True
310
310
311 return self.get_view(truncated=truncated, reply_link=reply_link,
311 return self.get_view(truncated=truncated, reply_link=reply_link,
312 moderator=utils.is_moderator(request))
312 moderator=utils.is_moderator(request))
313 elif format_type == DIFF_TYPE_JSON:
313 elif format_type == DIFF_TYPE_JSON:
314 post_json = {
314 post_json = {
315 'id': self.id,
315 'id': self.id,
316 'title': self.title,
316 'title': self.title,
317 'text': self._text_rendered,
317 'text': self._text_rendered,
318 }
318 }
319 if self.images.exists():
319 if self.images.exists():
320 post_image = self.get_first_image()
320 post_image = self.get_first_image()
321 post_json['image'] = post_image.image.url
321 post_json['image'] = post_image.image.url
322 post_json['image_preview'] = post_image.image.url_200x150
322 post_json['image_preview'] = post_image.image.url_200x150
323 if include_last_update:
323 if include_last_update:
324 post_json['bump_time'] = utils.datetime_to_epoch(
324 post_json['bump_time'] = utils.datetime_to_epoch(
325 self.get_thread().bump_time)
325 self.get_thread().bump_time)
326 return post_json
326 return post_json
327
327
328 def notify_clients(self, recursive=True):
328 def notify_clients(self, recursive=True):
329 """
329 """
330 Sends post HTML data to the thread web socket.
330 Sends post HTML data to the thread web socket.
331 """
331 """
332
332
333 if not settings.WEBSOCKETS_ENABLED:
333 if not settings.get_bool('External', 'WebsocketsEnabled'):
334 return
334 return
335
335
336 thread_ids = list()
336 thread_ids = list()
337 for thread in self.get_threads().all():
337 for thread in self.get_threads().all():
338 thread_ids.append(thread.id)
338 thread_ids.append(thread.id)
339
339
340 thread.notify_clients()
340 thread.notify_clients()
341
341
342 if recursive:
342 if recursive:
343 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
343 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
344 post_id = reply_number.group(1)
344 post_id = reply_number.group(1)
345
345
346 try:
346 try:
347 ref_post = Post.objects.get(id=post_id)
347 ref_post = Post.objects.get(id=post_id)
348
348
349 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
349 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
350 # If post is in this thread, its thread was already notified.
350 # If post is in this thread, its thread was already notified.
351 # Otherwise, notify its thread separately.
351 # Otherwise, notify its thread separately.
352 ref_post.notify_clients(recursive=False)
352 ref_post.notify_clients(recursive=False)
353 except ObjectDoesNotExist:
353 except ObjectDoesNotExist:
354 pass
354 pass
355
355
356 def build_url(self):
356 def build_url(self):
357 thread = self.get_thread()
357 thread = self.get_thread()
358 opening_id = thread.get_opening_post_id()
358 opening_id = thread.get_opening_post_id()
359 post_url = reverse('thread', kwargs={'post_id': opening_id})
359 post_url = reverse('thread', kwargs={'post_id': opening_id})
360 if self.id != opening_id:
360 if self.id != opening_id:
361 post_url += '#' + str(self.id)
361 post_url += '#' + str(self.id)
362 self.url = post_url
362 self.url = post_url
363 self.save(update_fields=['url'])
363 self.save(update_fields=['url'])
364
364
365 def save(self, force_insert=False, force_update=False, using=None,
365 def save(self, force_insert=False, force_update=False, using=None,
366 update_fields=None):
366 update_fields=None):
367 self._text_rendered = Parser().parse(self.get_raw_text())
367 self._text_rendered = Parser().parse(self.get_raw_text())
368
368
369 self.uid = str(uuid.uuid4())
369 self.uid = str(uuid.uuid4())
370 if update_fields is not None and 'uid' not in update_fields:
370 if update_fields is not None and 'uid' not in update_fields:
371 update_fields += ['uid']
371 update_fields += ['uid']
372
372
373 if self.id:
373 if self.id:
374 for thread in self.get_threads().all():
374 for thread in self.get_threads().all():
375 if thread.can_bump():
375 if thread.can_bump():
376 thread.update_bump_status(exclude_posts=[self])
376 thread.update_bump_status(exclude_posts=[self])
377 thread.last_edit_time = self.last_edit_time
377 thread.last_edit_time = self.last_edit_time
378
378
379 thread.save(update_fields=['last_edit_time', 'bumpable'])
379 thread.save(update_fields=['last_edit_time', 'bumpable'])
380
380
381 super().save(force_insert, force_update, using, update_fields)
381 super().save(force_insert, force_update, using, update_fields)
382
382
383 def get_text(self) -> str:
383 def get_text(self) -> str:
384 return self._text_rendered
384 return self._text_rendered
385
385
386 def get_raw_text(self) -> str:
386 def get_raw_text(self) -> str:
387 return self.text
387 return self.text
388
388
389 def get_absolute_id(self) -> str:
389 def get_absolute_id(self) -> str:
390 """
390 """
391 If the post has many threads, shows its main thread OP id in the post
391 If the post has many threads, shows its main thread OP id in the post
392 ID.
392 ID.
393 """
393 """
394
394
395 if self.get_threads().count() > 1:
395 if self.get_threads().count() > 1:
396 return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id)
396 return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id)
397 else:
397 else:
398 return str(self.id)
398 return str(self.id)
399
399
400 def connect_notifications(self):
400 def connect_notifications(self):
401 for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()):
401 for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()):
402 user_name = reply_number.group(1).lower()
402 user_name = reply_number.group(1).lower()
403 Notification.objects.get_or_create(name=user_name, post=self)
403 Notification.objects.get_or_create(name=user_name, post=self)
404
404
405 def connect_replies(self):
405 def connect_replies(self):
406 """
406 """
407 Connects replies to a post to show them as a reflink map
407 Connects replies to a post to show them as a reflink map
408 """
408 """
409
409
410 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
410 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
411 post_id = reply_number.group(1)
411 post_id = reply_number.group(1)
412
412
413 try:
413 try:
414 referenced_post = Post.objects.get(id=post_id)
414 referenced_post = Post.objects.get(id=post_id)
415
415
416 referenced_post.referenced_posts.add(self)
416 referenced_post.referenced_posts.add(self)
417 referenced_post.last_edit_time = self.pub_time
417 referenced_post.last_edit_time = self.pub_time
418 referenced_post.build_refmap()
418 referenced_post.build_refmap()
419 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
419 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
420 except ObjectDoesNotExist:
420 except ObjectDoesNotExist:
421 pass
421 pass
422
422
423 def connect_threads(self, opening_posts):
423 def connect_threads(self, opening_posts):
424 """
424 """
425 If the referenced post is an OP in another thread,
425 If the referenced post is an OP in another thread,
426 make this post multi-thread.
426 make this post multi-thread.
427 """
427 """
428
428
429 for opening_post in opening_posts:
429 for opening_post in opening_posts:
430 threads = opening_post.get_threads().all()
430 threads = opening_post.get_threads().all()
431 for thread in threads:
431 for thread in threads:
432 if thread.can_bump():
432 if thread.can_bump():
433 thread.update_bump_status()
433 thread.update_bump_status()
434
434
435 thread.last_edit_time = self.last_edit_time
435 thread.last_edit_time = self.last_edit_time
436 thread.save(update_fields=['last_edit_time', 'bumpable'])
436 thread.save(update_fields=['last_edit_time', 'bumpable'])
437
437
438 self.threads.add(thread)
438 self.threads.add(thread)
@@ -1,237 +1,240 b''
1 import logging
1 import logging
2 from adjacent import Client
2 from adjacent import Client
3
3
4 from django.db.models import Count, Sum
4 from django.db.models import Count, Sum
5 from django.utils import timezone
5 from django.utils import timezone
6 from django.db import models
6 from django.db import models
7
7
8 from boards import settings
8 from boards import settings
9 import boards
9 import boards
10 from boards.utils import cached_result, datetime_to_epoch
10 from boards.utils import cached_result, datetime_to_epoch
11 from boards.models.post import Post
11 from boards.models.post import Post
12 from boards.models.tag import Tag
12 from boards.models.tag import Tag
13
13
14
14
15 __author__ = 'neko259'
15 __author__ = 'neko259'
16
16
17
17
18 logger = logging.getLogger(__name__)
18 logger = logging.getLogger(__name__)
19
19
20
20
21 WS_NOTIFICATION_TYPE_NEW_POST = 'new_post'
21 WS_NOTIFICATION_TYPE_NEW_POST = 'new_post'
22 WS_NOTIFICATION_TYPE = 'notification_type'
22 WS_NOTIFICATION_TYPE = 'notification_type'
23
23
24 WS_CHANNEL_THREAD = "thread:"
24 WS_CHANNEL_THREAD = "thread:"
25
25
26
26
27 class ThreadManager(models.Manager):
27 class ThreadManager(models.Manager):
28 def process_oldest_threads(self):
28 def process_oldest_threads(self):
29 """
29 """
30 Preserves maximum thread count. If there are too many threads,
30 Preserves maximum thread count. If there are too many threads,
31 archive or delete the old ones.
31 archive or delete the old ones.
32 """
32 """
33
33
34 threads = Thread.objects.filter(archived=False).order_by('-bump_time')
34 threads = Thread.objects.filter(archived=False).order_by('-bump_time')
35 thread_count = threads.count()
35 thread_count = threads.count()
36
36
37 if thread_count > settings.MAX_THREAD_COUNT:
37 max_thread_count = settings.get_int('Messages', 'MaxThreadCount')
38 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
38 if thread_count > max_thread_count:
39 num_threads_to_delete = thread_count - max_thread_count
39 old_threads = threads[thread_count - num_threads_to_delete:]
40 old_threads = threads[thread_count - num_threads_to_delete:]
40
41
41 for thread in old_threads:
42 for thread in old_threads:
42 if settings.ARCHIVE_THREADS:
43 if settings.get_bool('Storage', 'ArchiveThreads'):
43 self._archive_thread(thread)
44 self._archive_thread(thread)
44 else:
45 else:
45 thread.delete()
46 thread.delete()
46
47
47 logger.info('Processed %d old threads' % num_threads_to_delete)
48 logger.info('Processed %d old threads' % num_threads_to_delete)
48
49
49 def _archive_thread(self, thread):
50 def _archive_thread(self, thread):
50 thread.archived = True
51 thread.archived = True
51 thread.bumpable = False
52 thread.bumpable = False
52 thread.last_edit_time = timezone.now()
53 thread.last_edit_time = timezone.now()
53 thread.update_posts_time()
54 thread.update_posts_time()
54 thread.save(update_fields=['archived', 'last_edit_time', 'bumpable'])
55 thread.save(update_fields=['archived', 'last_edit_time', 'bumpable'])
55
56
56
57
57 def get_thread_max_posts():
58 def get_thread_max_posts():
58 return settings.MAX_POSTS_PER_THREAD
59 return settings.get_int('Messages', 'MaxPostsPerThread')
59
60
60
61
61 class Thread(models.Model):
62 class Thread(models.Model):
62 objects = ThreadManager()
63 objects = ThreadManager()
63
64
64 class Meta:
65 class Meta:
65 app_label = 'boards'
66 app_label = 'boards'
66
67
67 tags = models.ManyToManyField('Tag')
68 tags = models.ManyToManyField('Tag')
68 bump_time = models.DateTimeField(db_index=True)
69 bump_time = models.DateTimeField(db_index=True)
69 last_edit_time = models.DateTimeField()
70 last_edit_time = models.DateTimeField()
70 archived = models.BooleanField(default=False)
71 archived = models.BooleanField(default=False)
71 bumpable = models.BooleanField(default=True)
72 bumpable = models.BooleanField(default=True)
72 max_posts = models.IntegerField(default=get_thread_max_posts)
73 max_posts = models.IntegerField(default=get_thread_max_posts)
73
74
74 def get_tags(self) -> list:
75 def get_tags(self) -> list:
75 """
76 """
76 Gets a sorted tag list.
77 Gets a sorted tag list.
77 """
78 """
78
79
79 return self.tags.order_by('name')
80 return self.tags.order_by('name')
80
81
81 def bump(self):
82 def bump(self):
82 """
83 """
83 Bumps (moves to up) thread if possible.
84 Bumps (moves to up) thread if possible.
84 """
85 """
85
86
86 if self.can_bump():
87 if self.can_bump():
87 self.bump_time = self.last_edit_time
88 self.bump_time = self.last_edit_time
88
89
89 self.update_bump_status()
90 self.update_bump_status()
90
91
91 logger.info('Bumped thread %d' % self.id)
92 logger.info('Bumped thread %d' % self.id)
92
93
93 def has_post_limit(self) -> bool:
94 def has_post_limit(self) -> bool:
94 return self.max_posts > 0
95 return self.max_posts > 0
95
96
96 def update_bump_status(self, exclude_posts=None):
97 def update_bump_status(self, exclude_posts=None):
97 if self.has_post_limit() and self.get_reply_count() >= self.max_posts:
98 if self.has_post_limit() and self.get_reply_count() >= self.max_posts:
98 self.bumpable = False
99 self.bumpable = False
99 self.update_posts_time(exclude_posts=exclude_posts)
100 self.update_posts_time(exclude_posts=exclude_posts)
100
101
101 def _get_cache_key(self):
102 def _get_cache_key(self):
102 return [datetime_to_epoch(self.last_edit_time)]
103 return [datetime_to_epoch(self.last_edit_time)]
103
104
104 @cached_result(key_method=_get_cache_key)
105 @cached_result(key_method=_get_cache_key)
105 def get_reply_count(self) -> int:
106 def get_reply_count(self) -> int:
106 return self.get_replies().count()
107 return self.get_replies().count()
107
108
108 @cached_result(key_method=_get_cache_key)
109 @cached_result(key_method=_get_cache_key)
109 def get_images_count(self) -> int:
110 def get_images_count(self) -> int:
110 return self.get_replies().annotate(images_count=Count(
111 return self.get_replies().annotate(images_count=Count(
111 'images')).aggregate(Sum('images_count'))['images_count__sum']
112 'images')).aggregate(Sum('images_count'))['images_count__sum']
112
113
113 def can_bump(self) -> bool:
114 def can_bump(self) -> bool:
114 """
115 """
115 Checks if the thread can be bumped by replying to it.
116 Checks if the thread can be bumped by replying to it.
116 """
117 """
117
118
118 return self.bumpable and not self.archived
119 return self.bumpable and not self.archived
119
120
120 def get_last_replies(self) -> list:
121 def get_last_replies(self) -> list:
121 """
122 """
122 Gets several last replies, not including opening post
123 Gets several last replies, not including opening post
123 """
124 """
124
125
125 if settings.LAST_REPLIES_COUNT > 0:
126 last_replies_count = settings.get_int('View', 'LastRepliesCount')
127
128 if last_replies_count > 0:
126 reply_count = self.get_reply_count()
129 reply_count = self.get_reply_count()
127
130
128 if reply_count > 0:
131 if reply_count > 0:
129 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
132 reply_count_to_show = min(last_replies_count,
130 reply_count - 1)
133 reply_count - 1)
131 replies = self.get_replies()
134 replies = self.get_replies()
132 last_replies = replies[reply_count - reply_count_to_show:]
135 last_replies = replies[reply_count - reply_count_to_show:]
133
136
134 return last_replies
137 return last_replies
135
138
136 def get_skipped_replies_count(self) -> int:
139 def get_skipped_replies_count(self) -> int:
137 """
140 """
138 Gets number of posts between opening post and last replies.
141 Gets number of posts between opening post and last replies.
139 """
142 """
140 reply_count = self.get_reply_count()
143 reply_count = self.get_reply_count()
141 last_replies_count = min(settings.LAST_REPLIES_COUNT,
144 last_replies_count = min(settings.get_int('View', 'LastRepliesCount'),
142 reply_count - 1)
145 reply_count - 1)
143 return reply_count - last_replies_count - 1
146 return reply_count - last_replies_count - 1
144
147
145 def get_replies(self, view_fields_only=False) -> list:
148 def get_replies(self, view_fields_only=False) -> list:
146 """
149 """
147 Gets sorted thread posts
150 Gets sorted thread posts
148 """
151 """
149
152
150 query = Post.objects.filter(threads__in=[self])
153 query = Post.objects.filter(threads__in=[self])
151 query = query.order_by('pub_time').prefetch_related('images', 'thread', 'threads')
154 query = query.order_by('pub_time').prefetch_related('images', 'thread', 'threads')
152 if view_fields_only:
155 if view_fields_only:
153 query = query.defer('poster_ip')
156 query = query.defer('poster_ip')
154 return query.all()
157 return query.all()
155
158
156 def get_replies_with_images(self, view_fields_only=False) -> list:
159 def get_replies_with_images(self, view_fields_only=False) -> list:
157 """
160 """
158 Gets replies that have at least one image attached
161 Gets replies that have at least one image attached
159 """
162 """
160
163
161 return self.get_replies(view_fields_only).annotate(images_count=Count(
164 return self.get_replies(view_fields_only).annotate(images_count=Count(
162 'images')).filter(images_count__gt=0)
165 'images')).filter(images_count__gt=0)
163
166
164 # TODO Do we still need this?
167 # TODO Do we still need this?
165 def add_tag(self, tag: Tag):
168 def add_tag(self, tag: Tag):
166 """
169 """
167 Connects thread to a tag and tag to a thread
170 Connects thread to a tag and tag to a thread
168 """
171 """
169
172
170 self.tags.add(tag)
173 self.tags.add(tag)
171
174
172 def get_opening_post(self, only_id=False) -> Post:
175 def get_opening_post(self, only_id=False) -> Post:
173 """
176 """
174 Gets the first post of the thread
177 Gets the first post of the thread
175 """
178 """
176
179
177 query = self.get_replies().order_by('pub_time')
180 query = self.get_replies().order_by('pub_time')
178 if only_id:
181 if only_id:
179 query = query.only('id')
182 query = query.only('id')
180 opening_post = query.first()
183 opening_post = query.first()
181
184
182 return opening_post
185 return opening_post
183
186
184 @cached_result()
187 @cached_result()
185 def get_opening_post_id(self) -> int:
188 def get_opening_post_id(self) -> int:
186 """
189 """
187 Gets ID of the first thread post.
190 Gets ID of the first thread post.
188 """
191 """
189
192
190 return self.get_opening_post(only_id=True).id
193 return self.get_opening_post(only_id=True).id
191
194
192 def get_pub_time(self):
195 def get_pub_time(self):
193 """
196 """
194 Gets opening post's pub time because thread does not have its own one.
197 Gets opening post's pub time because thread does not have its own one.
195 """
198 """
196
199
197 return self.get_opening_post().pub_time
200 return self.get_opening_post().pub_time
198
201
199 def delete(self, using=None):
202 def delete(self, using=None):
200 """
203 """
201 Deletes thread with all replies.
204 Deletes thread with all replies.
202 """
205 """
203
206
204 for reply in self.get_replies().all():
207 for reply in self.get_replies().all():
205 reply.delete()
208 reply.delete()
206
209
207 super(Thread, self).delete(using)
210 super(Thread, self).delete(using)
208
211
209 def __str__(self):
212 def __str__(self):
210 return 'T#{}/{}'.format(self.id, self.get_opening_post_id())
213 return 'T#{}/{}'.format(self.id, self.get_opening_post_id())
211
214
212 def get_tag_url_list(self) -> list:
215 def get_tag_url_list(self) -> list:
213 return boards.models.Tag.objects.get_tag_url_list(self.get_tags())
216 return boards.models.Tag.objects.get_tag_url_list(self.get_tags())
214
217
215 def update_posts_time(self, exclude_posts=None):
218 def update_posts_time(self, exclude_posts=None):
216 for post in self.post_set.all():
219 for post in self.post_set.all():
217 if exclude_posts is not None and post not in exclude_posts:
220 if exclude_posts is not None and post not in exclude_posts:
218 # Manual update is required because uids are generated on save
221 # Manual update is required because uids are generated on save
219 post.last_edit_time = self.last_edit_time
222 post.last_edit_time = self.last_edit_time
220 post.save(update_fields=['last_edit_time'])
223 post.save(update_fields=['last_edit_time'])
221
224
222 post.threads.update(last_edit_time=self.last_edit_time)
225 post.threads.update(last_edit_time=self.last_edit_time)
223
226
224 def notify_clients(self):
227 def notify_clients(self):
225 if not settings.WEBSOCKETS_ENABLED:
228 if not settings.get_bool('External', 'WebsocketsEnabled'):
226 return
229 return
227
230
228 client = Client()
231 client = Client()
229
232
230 channel_name = WS_CHANNEL_THREAD + str(self.get_opening_post_id())
233 channel_name = WS_CHANNEL_THREAD + str(self.get_opening_post_id())
231 client.publish(channel_name, {
234 client.publish(channel_name, {
232 WS_NOTIFICATION_TYPE: WS_NOTIFICATION_TYPE_NEW_POST,
235 WS_NOTIFICATION_TYPE: WS_NOTIFICATION_TYPE_NEW_POST,
233 })
236 })
234 client.send()
237 client.send()
235
238
236 def get_absolute_url(self):
239 def get_absolute_url(self):
237 return self.get_opening_post().get_absolute_url()
240 return self.get_opening_post().get_absolute_url()
@@ -1,80 +1,80 b''
1 from django.contrib.syndication.views import Feed
1 from django.contrib.syndication.views import Feed
2 from django.core.urlresolvers import reverse
2 from django.core.urlresolvers import reverse
3 from django.shortcuts import get_object_or_404
3 from django.shortcuts import get_object_or_404
4 from boards.models import Post, Tag, Thread
4 from boards.models import Post, Tag, Thread
5 from boards import settings
5 from boards import settings
6
6
7 __author__ = 'neko259'
7 __author__ = 'neko259'
8
8
9
9
10 # TODO Make tests for all of these
10 # TODO Make tests for all of these
11 class AllThreadsFeed(Feed):
11 class AllThreadsFeed(Feed):
12
12
13 title = settings.SITE_NAME + ' - All threads'
13 title = settings.get('Version', 'SiteName') + ' - All threads'
14 link = '/'
14 link = '/'
15 description_template = 'boards/rss/post.html'
15 description_template = 'boards/rss/post.html'
16
16
17 def items(self):
17 def items(self):
18 return Thread.objects.filter(archived=False).order_by('-id')
18 return Thread.objects.filter(archived=False).order_by('-id')
19
19
20 def item_title(self, item):
20 def item_title(self, item):
21 return item.get_opening_post().title
21 return item.get_opening_post().title
22
22
23 def item_link(self, item):
23 def item_link(self, item):
24 return reverse('thread', args={item.get_opening_post_id()})
24 return reverse('thread', args={item.get_opening_post_id()})
25
25
26 def item_pubdate(self, item):
26 def item_pubdate(self, item):
27 return item.get_pub_time()
27 return item.get_pub_time()
28
28
29
29
30 class TagThreadsFeed(Feed):
30 class TagThreadsFeed(Feed):
31
31
32 link = '/'
32 link = '/'
33 description_template = 'boards/rss/post.html'
33 description_template = 'boards/rss/post.html'
34
34
35 def items(self, obj):
35 def items(self, obj):
36 return obj.threads.filter(archived=False).order_by('-id')
36 return obj.threads.filter(archived=False).order_by('-id')
37
37
38 def get_object(self, request, tag_name):
38 def get_object(self, request, tag_name):
39 return get_object_or_404(Tag, name=tag_name)
39 return get_object_or_404(Tag, name=tag_name)
40
40
41 def item_title(self, item):
41 def item_title(self, item):
42 return item.get_opening_post().title
42 return item.get_opening_post().title
43
43
44 def item_link(self, item):
44 def item_link(self, item):
45 return reverse('thread', args={item.get_opening_post_id()})
45 return reverse('thread', args={item.get_opening_post_id()})
46
46
47 def item_pubdate(self, item):
47 def item_pubdate(self, item):
48 return item.get_pub_time()
48 return item.get_pub_time()
49
49
50 def title(self, obj):
50 def title(self, obj):
51 return obj.name
51 return obj.name
52
52
53
53
54 class ThreadPostsFeed(Feed):
54 class ThreadPostsFeed(Feed):
55
55
56 link = '/'
56 link = '/'
57 description_template = 'boards/rss/post.html'
57 description_template = 'boards/rss/post.html'
58
58
59 def items(self, obj):
59 def items(self, obj):
60 return obj.get_thread().get_replies()
60 return obj.get_thread().get_replies()
61
61
62 def get_object(self, request, post_id):
62 def get_object(self, request, post_id):
63 return get_object_or_404(Post, id=post_id)
63 return get_object_or_404(Post, id=post_id)
64
64
65 def item_title(self, item):
65 def item_title(self, item):
66 return item.title
66 return item.title
67
67
68 def item_link(self, item):
68 def item_link(self, item):
69 if not item.is_opening():
69 if not item.is_opening():
70 return reverse('thread', args={
70 return reverse('thread', args={
71 item.get_thread().get_opening_post_id()
71 item.get_thread().get_opening_post_id()
72 }) + "#" + str(item.id)
72 }) + "#" + str(item.id)
73 else:
73 else:
74 return reverse('thread', args={item.id})
74 return reverse('thread', args={item.id})
75
75
76 def item_pubdate(self, item):
76 def item_pubdate(self, item):
77 return item.pub_time
77 return item.pub_time
78
78
79 def title(self, obj):
79 def title(self, obj):
80 return obj.title
80 return obj.title
@@ -1,2 +1,18 b''
1 from boards.default_settings import *
1 import configparser
2
3
4 config = configparser.ConfigParser()
5 config.read('boards/config/default_settings.ini')
6 config.read('boards/config/settings.ini')
7
2
8
9 def get(section, name):
10 return config[section][name]
11
12
13 def get_int(section, name):
14 return int(get(section, name))
15
16
17 def get_bool(section, name):
18 return get(section, name) == 'true'
@@ -1,169 +1,169 b''
1 from django.core.urlresolvers import reverse
1 from django.core.urlresolvers import reverse
2 from django.core.files import File
2 from django.core.files import File
3 from django.core.files.temp import NamedTemporaryFile
3 from django.core.files.temp import NamedTemporaryFile
4 from django.core.paginator import EmptyPage
4 from django.core.paginator import EmptyPage
5 from django.db import transaction
5 from django.db import transaction
6 from django.http import Http404
6 from django.http import Http404
7 from django.shortcuts import render, redirect
7 from django.shortcuts import render, redirect
8 import requests
8 import requests
9
9
10 from boards import utils, settings
10 from boards import utils, settings
11 from boards.abstracts.paginator import get_paginator
11 from boards.abstracts.paginator import get_paginator
12 from boards.abstracts.settingsmanager import get_settings_manager
12 from boards.abstracts.settingsmanager import get_settings_manager
13 from boards.forms import ThreadForm, PlainErrorList
13 from boards.forms import ThreadForm, PlainErrorList
14 from boards.models import Post, Thread, Ban, Tag, PostImage, Banner
14 from boards.models import Post, Thread, Ban, Tag, PostImage, Banner
15 from boards.views.banned import BannedView
15 from boards.views.banned import BannedView
16 from boards.views.base import BaseBoardView, CONTEXT_FORM
16 from boards.views.base import BaseBoardView, CONTEXT_FORM
17 from boards.views.posting_mixin import PostMixin
17 from boards.views.posting_mixin import PostMixin
18
18
19
19
20 FORM_TAGS = 'tags'
20 FORM_TAGS = 'tags'
21 FORM_TEXT = 'text'
21 FORM_TEXT = 'text'
22 FORM_TITLE = 'title'
22 FORM_TITLE = 'title'
23 FORM_IMAGE = 'image'
23 FORM_IMAGE = 'image'
24 FORM_THREADS = 'threads'
24 FORM_THREADS = 'threads'
25
25
26 TAG_DELIMITER = ' '
26 TAG_DELIMITER = ' '
27
27
28 PARAMETER_CURRENT_PAGE = 'current_page'
28 PARAMETER_CURRENT_PAGE = 'current_page'
29 PARAMETER_PAGINATOR = 'paginator'
29 PARAMETER_PAGINATOR = 'paginator'
30 PARAMETER_THREADS = 'threads'
30 PARAMETER_THREADS = 'threads'
31 PARAMETER_BANNERS = 'banners'
31 PARAMETER_BANNERS = 'banners'
32
32
33 PARAMETER_PREV_LINK = 'prev_page_link'
33 PARAMETER_PREV_LINK = 'prev_page_link'
34 PARAMETER_NEXT_LINK = 'next_page_link'
34 PARAMETER_NEXT_LINK = 'next_page_link'
35
35
36 TEMPLATE = 'boards/posting_general.html'
36 TEMPLATE = 'boards/posting_general.html'
37 DEFAULT_PAGE = 1
37 DEFAULT_PAGE = 1
38
38
39
39
40 class AllThreadsView(PostMixin, BaseBoardView):
40 class AllThreadsView(PostMixin, BaseBoardView):
41
41
42 def __init__(self):
42 def __init__(self):
43 self.settings_manager = None
43 self.settings_manager = None
44 super(AllThreadsView, self).__init__()
44 super(AllThreadsView, self).__init__()
45
45
46 def get(self, request, page=DEFAULT_PAGE, form: ThreadForm=None):
46 def get(self, request, page=DEFAULT_PAGE, form: ThreadForm=None):
47 params = self.get_context_data(request=request)
47 params = self.get_context_data(request=request)
48
48
49 if not form:
49 if not form:
50 form = ThreadForm(error_class=PlainErrorList)
50 form = ThreadForm(error_class=PlainErrorList)
51
51
52 self.settings_manager = get_settings_manager(request)
52 self.settings_manager = get_settings_manager(request)
53 paginator = get_paginator(self.get_threads(),
53 paginator = get_paginator(self.get_threads(),
54 settings.THREADS_PER_PAGE)
54 settings.get_int('View', 'ThreadsPerPage'))
55 paginator.current_page = int(page)
55 paginator.current_page = int(page)
56
56
57 try:
57 try:
58 threads = paginator.page(page).object_list
58 threads = paginator.page(page).object_list
59 except EmptyPage:
59 except EmptyPage:
60 raise Http404()
60 raise Http404()
61
61
62 params[PARAMETER_THREADS] = threads
62 params[PARAMETER_THREADS] = threads
63 params[CONTEXT_FORM] = form
63 params[CONTEXT_FORM] = form
64 params[PARAMETER_BANNERS] = Banner.objects.order_by('-id').all()
64 params[PARAMETER_BANNERS] = Banner.objects.order_by('-id').all()
65
65
66 self.get_page_context(paginator, params, page)
66 self.get_page_context(paginator, params, page)
67
67
68 return render(request, TEMPLATE, params)
68 return render(request, TEMPLATE, params)
69
69
70 def post(self, request, page=DEFAULT_PAGE):
70 def post(self, request, page=DEFAULT_PAGE):
71 form = ThreadForm(request.POST, request.FILES,
71 form = ThreadForm(request.POST, request.FILES,
72 error_class=PlainErrorList)
72 error_class=PlainErrorList)
73 form.session = request.session
73 form.session = request.session
74
74
75 if form.is_valid():
75 if form.is_valid():
76 return self.create_thread(request, form)
76 return self.create_thread(request, form)
77 if form.need_to_ban:
77 if form.need_to_ban:
78 # Ban user because he is suspected to be a bot
78 # Ban user because he is suspected to be a bot
79 self._ban_current_user(request)
79 self._ban_current_user(request)
80
80
81 return self.get(request, page, form)
81 return self.get(request, page, form)
82
82
83 def get_page_context(self, paginator, params, page):
83 def get_page_context(self, paginator, params, page):
84 """
84 """
85 Get pagination context variables
85 Get pagination context variables
86 """
86 """
87
87
88 params[PARAMETER_PAGINATOR] = paginator
88 params[PARAMETER_PAGINATOR] = paginator
89 current_page = paginator.page(int(page))
89 current_page = paginator.page(int(page))
90 params[PARAMETER_CURRENT_PAGE] = current_page
90 params[PARAMETER_CURRENT_PAGE] = current_page
91 if current_page.has_previous():
91 if current_page.has_previous():
92 params[PARAMETER_PREV_LINK] = self.get_previous_page_link(
92 params[PARAMETER_PREV_LINK] = self.get_previous_page_link(
93 current_page)
93 current_page)
94 if current_page.has_next():
94 if current_page.has_next():
95 params[PARAMETER_NEXT_LINK] = self.get_next_page_link(current_page)
95 params[PARAMETER_NEXT_LINK] = self.get_next_page_link(current_page)
96
96
97 def get_previous_page_link(self, current_page):
97 def get_previous_page_link(self, current_page):
98 return reverse('index', kwargs={
98 return reverse('index', kwargs={
99 'page': current_page.previous_page_number(),
99 'page': current_page.previous_page_number(),
100 })
100 })
101
101
102 def get_next_page_link(self, current_page):
102 def get_next_page_link(self, current_page):
103 return reverse('index', kwargs={
103 return reverse('index', kwargs={
104 'page': current_page.next_page_number(),
104 'page': current_page.next_page_number(),
105 })
105 })
106
106
107 @staticmethod
107 @staticmethod
108 def parse_tags_string(tag_strings):
108 def parse_tags_string(tag_strings):
109 """
109 """
110 Parses tag list string and returns tag object list.
110 Parses tag list string and returns tag object list.
111 """
111 """
112
112
113 tags = []
113 tags = []
114
114
115 if tag_strings:
115 if tag_strings:
116 tag_strings = tag_strings.split(TAG_DELIMITER)
116 tag_strings = tag_strings.split(TAG_DELIMITER)
117 for tag_name in tag_strings:
117 for tag_name in tag_strings:
118 tag_name = tag_name.strip().lower()
118 tag_name = tag_name.strip().lower()
119 if len(tag_name) > 0:
119 if len(tag_name) > 0:
120 tag, created = Tag.objects.get_or_create(name=tag_name)
120 tag, created = Tag.objects.get_or_create(name=tag_name)
121 tags.append(tag)
121 tags.append(tag)
122
122
123 return tags
123 return tags
124
124
125 @transaction.atomic
125 @transaction.atomic
126 def create_thread(self, request, form: ThreadForm, html_response=True):
126 def create_thread(self, request, form: ThreadForm, html_response=True):
127 """
127 """
128 Creates a new thread with an opening post.
128 Creates a new thread with an opening post.
129 """
129 """
130
130
131 ip = utils.get_client_ip(request)
131 ip = utils.get_client_ip(request)
132 is_banned = Ban.objects.filter(ip=ip).exists()
132 is_banned = Ban.objects.filter(ip=ip).exists()
133
133
134 if is_banned:
134 if is_banned:
135 if html_response:
135 if html_response:
136 return redirect(BannedView().as_view())
136 return redirect(BannedView().as_view())
137 else:
137 else:
138 return
138 return
139
139
140 data = form.cleaned_data
140 data = form.cleaned_data
141
141
142 title = data[FORM_TITLE]
142 title = data[FORM_TITLE]
143 text = data[FORM_TEXT]
143 text = data[FORM_TEXT]
144 image = form.get_image()
144 image = form.get_image()
145 threads = data[FORM_THREADS]
145 threads = data[FORM_THREADS]
146
146
147 text = self._remove_invalid_links(text)
147 text = self._remove_invalid_links(text)
148
148
149 tag_strings = data[FORM_TAGS]
149 tag_strings = data[FORM_TAGS]
150
150
151 tags = self.parse_tags_string(tag_strings)
151 tags = self.parse_tags_string(tag_strings)
152
152
153 post = Post.objects.create_post(title=title, text=text, image=image,
153 post = Post.objects.create_post(title=title, text=text, image=image,
154 ip=ip, tags=tags, threads=threads)
154 ip=ip, tags=tags, threads=threads)
155
155
156 # This is required to update the threads to which posts we have replied
156 # This is required to update the threads to which posts we have replied
157 # when creating this one
157 # when creating this one
158 post.notify_clients()
158 post.notify_clients()
159
159
160 if html_response:
160 if html_response:
161 return redirect(post.get_url())
161 return redirect(post.get_url())
162
162
163 def get_threads(self):
163 def get_threads(self):
164 """
164 """
165 Gets list of threads that will be shown on a page.
165 Gets list of threads that will be shown on a page.
166 """
166 """
167
167
168 return Thread.objects.order_by('-bump_time')\
168 return Thread.objects.order_by('-bump_time')\
169 .exclude(tags__in=self.settings_manager.get_hidden_tags())
169 .exclude(tags__in=self.settings_manager.get_hidden_tags())
@@ -1,76 +1,77 b''
1 from django.db import transaction
1 from django.db import transaction
2 from django.shortcuts import render, redirect
2 from django.shortcuts import render, redirect
3 from django.utils import timezone
3 from django.utils import timezone
4
4
5 from boards.abstracts.settingsmanager import get_settings_manager, \
5 from boards.abstracts.settingsmanager import get_settings_manager, \
6 SETTING_USERNAME, SETTING_LAST_NOTIFICATION_ID, SETTING_IMAGE_VIEWER
6 SETTING_USERNAME, SETTING_LAST_NOTIFICATION_ID, SETTING_IMAGE_VIEWER
7 from boards.middlewares import SESSION_TIMEZONE
7 from boards.middlewares import SESSION_TIMEZONE
8 from boards.views.base import BaseBoardView, CONTEXT_FORM
8 from boards.views.base import BaseBoardView, CONTEXT_FORM
9 from boards.forms import SettingsForm, PlainErrorList
9 from boards.forms import SettingsForm, PlainErrorList
10 from boards import settings
10 from boards import settings
11
11
12
12
13 FORM_THEME = 'theme'
13 FORM_THEME = 'theme'
14 FORM_USERNAME = 'username'
14 FORM_USERNAME = 'username'
15 FORM_TIMEZONE = 'timezone'
15 FORM_TIMEZONE = 'timezone'
16 FORM_IMAGE_VIEWER = 'image_viewer'
16 FORM_IMAGE_VIEWER = 'image_viewer'
17
17
18 CONTEXT_HIDDEN_TAGS = 'hidden_tags'
18 CONTEXT_HIDDEN_TAGS = 'hidden_tags'
19
19
20 TEMPLATE = 'boards/settings.html'
20 TEMPLATE = 'boards/settings.html'
21
21
22
22
23 class SettingsView(BaseBoardView):
23 class SettingsView(BaseBoardView):
24
24
25 def get(self, request):
25 def get(self, request):
26 params = dict()
26 params = dict()
27 settings_manager = get_settings_manager(request)
27 settings_manager = get_settings_manager(request)
28
28
29 selected_theme = settings_manager.get_theme()
29 selected_theme = settings_manager.get_theme()
30
30
31 form = SettingsForm(
31 form = SettingsForm(
32 initial={
32 initial={
33 FORM_THEME: selected_theme,
33 FORM_THEME: selected_theme,
34 FORM_IMAGE_VIEWER: settings_manager.get_setting(
34 FORM_IMAGE_VIEWER: settings_manager.get_setting(
35 SETTING_IMAGE_VIEWER, default=settings.DEFAULT_IMAGE_VIEWER),
35 SETTING_IMAGE_VIEWER,
36 default=settings.get('View', 'DefaultImageViewer')),
36 FORM_USERNAME: settings_manager.get_setting(SETTING_USERNAME),
37 FORM_USERNAME: settings_manager.get_setting(SETTING_USERNAME),
37 FORM_TIMEZONE: request.session.get(
38 FORM_TIMEZONE: request.session.get(
38 SESSION_TIMEZONE, timezone.get_current_timezone()),
39 SESSION_TIMEZONE, timezone.get_current_timezone()),
39 },
40 },
40 error_class=PlainErrorList)
41 error_class=PlainErrorList)
41
42
42 params[CONTEXT_FORM] = form
43 params[CONTEXT_FORM] = form
43 params[CONTEXT_HIDDEN_TAGS] = settings_manager.get_hidden_tags()
44 params[CONTEXT_HIDDEN_TAGS] = settings_manager.get_hidden_tags()
44
45
45 return render(request, TEMPLATE, params)
46 return render(request, TEMPLATE, params)
46
47
47 def post(self, request):
48 def post(self, request):
48 settings_manager = get_settings_manager(request)
49 settings_manager = get_settings_manager(request)
49
50
50 with transaction.atomic():
51 with transaction.atomic():
51 form = SettingsForm(request.POST, error_class=PlainErrorList)
52 form = SettingsForm(request.POST, error_class=PlainErrorList)
52
53
53 if form.is_valid():
54 if form.is_valid():
54 selected_theme = form.cleaned_data[FORM_THEME]
55 selected_theme = form.cleaned_data[FORM_THEME]
55 username = form.cleaned_data[FORM_USERNAME].lower()
56 username = form.cleaned_data[FORM_USERNAME].lower()
56
57
57 settings_manager.set_theme(selected_theme)
58 settings_manager.set_theme(selected_theme)
58 settings_manager.set_setting(SETTING_IMAGE_VIEWER,
59 settings_manager.set_setting(SETTING_IMAGE_VIEWER,
59 form.cleaned_data[FORM_IMAGE_VIEWER])
60 form.cleaned_data[FORM_IMAGE_VIEWER])
60
61
61 old_username = settings_manager.get_setting(SETTING_USERNAME)
62 old_username = settings_manager.get_setting(SETTING_USERNAME)
62 if username != old_username:
63 if username != old_username:
63 settings_manager.set_setting(SETTING_USERNAME, username)
64 settings_manager.set_setting(SETTING_USERNAME, username)
64 settings_manager.set_setting(SETTING_LAST_NOTIFICATION_ID, None)
65 settings_manager.set_setting(SETTING_LAST_NOTIFICATION_ID, None)
65
66
66 request.session[SESSION_TIMEZONE] = form.cleaned_data[FORM_TIMEZONE]
67 request.session[SESSION_TIMEZONE] = form.cleaned_data[FORM_TIMEZONE]
67
68
68 return redirect('settings')
69 return redirect('settings')
69 else:
70 else:
70 params = dict()
71 params = dict()
71
72
72 params[CONTEXT_FORM] = form
73 params[CONTEXT_FORM] = form
73 params[CONTEXT_HIDDEN_TAGS] = settings_manager.get_hidden_tags()
74 params[CONTEXT_HIDDEN_TAGS] = settings_manager.get_hidden_tags()
74
75
75 return render(request, TEMPLATE, params)
76 return render(request, TEMPLATE, params)
76
77
@@ -1,130 +1,130 b''
1 from django.core.exceptions import ObjectDoesNotExist
1 from django.core.exceptions import ObjectDoesNotExist
2 from django.http import Http404
2 from django.http import Http404
3 from django.shortcuts import get_object_or_404, render, redirect
3 from django.shortcuts import get_object_or_404, render, redirect
4 from django.views.generic.edit import FormMixin
4 from django.views.generic.edit import FormMixin
5 from django.utils import timezone
5 from django.utils import timezone
6 from django.utils.dateformat import format
6 from django.utils.dateformat import format
7
7
8 from boards import utils, settings
8 from boards import utils, settings
9 from boards.forms import PostForm, PlainErrorList
9 from boards.forms import PostForm, PlainErrorList
10 from boards.models import Post
10 from boards.models import Post
11 from boards.views.base import BaseBoardView, CONTEXT_FORM
11 from boards.views.base import BaseBoardView, CONTEXT_FORM
12 from boards.views.posting_mixin import PostMixin
12 from boards.views.posting_mixin import PostMixin
13
13
14 import neboard
14 import neboard
15
15
16
16
17 CONTEXT_LASTUPDATE = "last_update"
17 CONTEXT_LASTUPDATE = "last_update"
18 CONTEXT_THREAD = 'thread'
18 CONTEXT_THREAD = 'thread'
19 CONTEXT_WS_TOKEN = 'ws_token'
19 CONTEXT_WS_TOKEN = 'ws_token'
20 CONTEXT_WS_PROJECT = 'ws_project'
20 CONTEXT_WS_PROJECT = 'ws_project'
21 CONTEXT_WS_HOST = 'ws_host'
21 CONTEXT_WS_HOST = 'ws_host'
22 CONTEXT_WS_PORT = 'ws_port'
22 CONTEXT_WS_PORT = 'ws_port'
23 CONTEXT_WS_TIME = 'ws_token_time'
23 CONTEXT_WS_TIME = 'ws_token_time'
24
24
25 FORM_TITLE = 'title'
25 FORM_TITLE = 'title'
26 FORM_TEXT = 'text'
26 FORM_TEXT = 'text'
27 FORM_IMAGE = 'image'
27 FORM_IMAGE = 'image'
28 FORM_THREADS = 'threads'
28 FORM_THREADS = 'threads'
29
29
30
30
31 class ThreadView(BaseBoardView, PostMixin, FormMixin):
31 class ThreadView(BaseBoardView, PostMixin, FormMixin):
32
32
33 def get(self, request, post_id, form: PostForm=None):
33 def get(self, request, post_id, form: PostForm=None):
34 try:
34 try:
35 opening_post = Post.objects.get(id=post_id)
35 opening_post = Post.objects.get(id=post_id)
36 except ObjectDoesNotExist:
36 except ObjectDoesNotExist:
37 raise Http404
37 raise Http404
38
38
39 # If this is not OP, don't show it as it is
39 # If this is not OP, don't show it as it is
40 if not opening_post.is_opening():
40 if not opening_post.is_opening():
41 return redirect(opening_post.get_thread().get_opening_post().get_url())
41 return redirect(opening_post.get_thread().get_opening_post().get_url())
42
42
43 if not form:
43 if not form:
44 form = PostForm(error_class=PlainErrorList)
44 form = PostForm(error_class=PlainErrorList)
45
45
46 thread_to_show = opening_post.get_thread()
46 thread_to_show = opening_post.get_thread()
47
47
48 params = dict()
48 params = dict()
49
49
50 params[CONTEXT_FORM] = form
50 params[CONTEXT_FORM] = form
51 params[CONTEXT_LASTUPDATE] = str(thread_to_show.last_edit_time)
51 params[CONTEXT_LASTUPDATE] = str(thread_to_show.last_edit_time)
52 params[CONTEXT_THREAD] = thread_to_show
52 params[CONTEXT_THREAD] = thread_to_show
53
53
54 if settings.WEBSOCKETS_ENABLED:
54 if settings.get_bool('External', 'WebsocketsEnabled'):
55 token_time = format(timezone.now(), u'U')
55 token_time = format(timezone.now(), u'U')
56
56
57 params[CONTEXT_WS_TIME] = token_time
57 params[CONTEXT_WS_TIME] = token_time
58 params[CONTEXT_WS_TOKEN] = utils.get_websocket_token(
58 params[CONTEXT_WS_TOKEN] = utils.get_websocket_token(
59 timestamp=token_time)
59 timestamp=token_time)
60 params[CONTEXT_WS_PROJECT] = neboard.settings.CENTRIFUGE_PROJECT_ID
60 params[CONTEXT_WS_PROJECT] = neboard.settings.CENTRIFUGE_PROJECT_ID
61 params[CONTEXT_WS_HOST] = request.get_host().split(':')[0]
61 params[CONTEXT_WS_HOST] = request.get_host().split(':')[0]
62 params[CONTEXT_WS_PORT] = neboard.settings.CENTRIFUGE_PORT
62 params[CONTEXT_WS_PORT] = neboard.settings.CENTRIFUGE_PORT
63
63
64 params.update(self.get_data(thread_to_show))
64 params.update(self.get_data(thread_to_show))
65
65
66 return render(request, self.get_template(), params)
66 return render(request, self.get_template(), params)
67
67
68 def post(self, request, post_id):
68 def post(self, request, post_id):
69 opening_post = get_object_or_404(Post, id=post_id)
69 opening_post = get_object_or_404(Post, id=post_id)
70
70
71 # If this is not OP, don't show it as it is
71 # If this is not OP, don't show it as it is
72 if not opening_post.is_opening():
72 if not opening_post.is_opening():
73 raise Http404
73 raise Http404
74
74
75 if not opening_post.get_thread().archived:
75 if not opening_post.get_thread().archived:
76 form = PostForm(request.POST, request.FILES,
76 form = PostForm(request.POST, request.FILES,
77 error_class=PlainErrorList)
77 error_class=PlainErrorList)
78 form.session = request.session
78 form.session = request.session
79
79
80 if form.is_valid():
80 if form.is_valid():
81 return self.new_post(request, form, opening_post)
81 return self.new_post(request, form, opening_post)
82 if form.need_to_ban:
82 if form.need_to_ban:
83 # Ban user because he is suspected to be a bot
83 # Ban user because he is suspected to be a bot
84 self._ban_current_user(request)
84 self._ban_current_user(request)
85
85
86 return self.get(request, post_id, form)
86 return self.get(request, post_id, form)
87
87
88 def new_post(self, request, form: PostForm, opening_post: Post=None,
88 def new_post(self, request, form: PostForm, opening_post: Post=None,
89 html_response=True):
89 html_response=True):
90 """
90 """
91 Adds a new post (in thread or as a reply).
91 Adds a new post (in thread or as a reply).
92 """
92 """
93
93
94 ip = utils.get_client_ip(request)
94 ip = utils.get_client_ip(request)
95
95
96 data = form.cleaned_data
96 data = form.cleaned_data
97
97
98 title = data[FORM_TITLE]
98 title = data[FORM_TITLE]
99 text = data[FORM_TEXT]
99 text = data[FORM_TEXT]
100 image = form.get_image()
100 image = form.get_image()
101 threads = data[FORM_THREADS]
101 threads = data[FORM_THREADS]
102
102
103 text = self._remove_invalid_links(text)
103 text = self._remove_invalid_links(text)
104
104
105 post_thread = opening_post.get_thread()
105 post_thread = opening_post.get_thread()
106
106
107 post = Post.objects.create_post(title=title, text=text, image=image,
107 post = Post.objects.create_post(title=title, text=text, image=image,
108 thread=post_thread, ip=ip,
108 thread=post_thread, ip=ip,
109 threads=threads)
109 threads=threads)
110 post.notify_clients()
110 post.notify_clients()
111
111
112 if html_response:
112 if html_response:
113 if opening_post:
113 if opening_post:
114 return redirect(post.get_url())
114 return redirect(post.get_url())
115 else:
115 else:
116 return post
116 return post
117
117
118 def get_data(self, thread):
118 def get_data(self, thread):
119 """
119 """
120 Returns context params for the view.
120 Returns context params for the view.
121 """
121 """
122
122
123 pass
123 pass
124
124
125 def get_template(self):
125 def get_template(self):
126 """
126 """
127 Gets template to show the thread mode on.
127 Gets template to show the thread mode on.
128 """
128 """
129
129
130 pass
130 pass
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