##// END OF EJS Templates
Thread status field instead of bumpable and archived fields (per BB-73)
neko259 -
r1414:cbf56940 default
parent child Browse files
Show More
@@ -1,75 +1,75 b''
1 from django.contrib import admin
1 from django.contrib import admin
2 from boards.models import Post, Tag, Ban, Thread, Banner
2 from boards.models import Post, Tag, Ban, Thread, Banner
3 from django.utils.translation import ugettext_lazy as _
3 from django.utils.translation import ugettext_lazy as _
4
4
5
5
6 @admin.register(Post)
6 @admin.register(Post)
7 class PostAdmin(admin.ModelAdmin):
7 class PostAdmin(admin.ModelAdmin):
8
8
9 list_display = ('id', 'title', 'text', 'poster_ip')
9 list_display = ('id', 'title', 'text', 'poster_ip')
10 list_filter = ('pub_time',)
10 list_filter = ('pub_time',)
11 search_fields = ('id', 'title', 'text', 'poster_ip')
11 search_fields = ('id', 'title', 'text', 'poster_ip')
12 exclude = ('referenced_posts', 'refmap')
12 exclude = ('referenced_posts', 'refmap')
13 readonly_fields = ('poster_ip', 'threads', 'thread', 'images',
13 readonly_fields = ('poster_ip', 'threads', 'thread', 'images',
14 'attachments', 'uid', 'url', 'pub_time', 'opening')
14 'attachments', 'uid', 'url', 'pub_time', 'opening')
15
15
16 def ban_poster(self, request, queryset):
16 def ban_poster(self, request, queryset):
17 bans = 0
17 bans = 0
18 for post in queryset:
18 for post in queryset:
19 poster_ip = post.poster_ip
19 poster_ip = post.poster_ip
20 ban, created = Ban.objects.get_or_create(ip=poster_ip)
20 ban, created = Ban.objects.get_or_create(ip=poster_ip)
21 if created:
21 if created:
22 bans += 1
22 bans += 1
23 self.message_user(request, _('{} posters were banned').format(bans))
23 self.message_user(request, _('{} posters were banned').format(bans))
24
24
25 actions = ['ban_poster']
25 actions = ['ban_poster']
26
26
27
27
28 @admin.register(Tag)
28 @admin.register(Tag)
29 class TagAdmin(admin.ModelAdmin):
29 class TagAdmin(admin.ModelAdmin):
30
30
31 def thread_count(self, obj: Tag) -> int:
31 def thread_count(self, obj: Tag) -> int:
32 return obj.get_thread_count()
32 return obj.get_thread_count()
33
33
34 def display_children(self, obj: Tag):
34 def display_children(self, obj: Tag):
35 return ', '.join([str(child) for child in obj.get_children().all()])
35 return ', '.join([str(child) for child in obj.get_children().all()])
36
36
37 list_display = ('name', 'thread_count', 'display_children')
37 list_display = ('name', 'thread_count', 'display_children')
38 search_fields = ('name',)
38 search_fields = ('name',)
39
39
40
40
41 @admin.register(Thread)
41 @admin.register(Thread)
42 class ThreadAdmin(admin.ModelAdmin):
42 class ThreadAdmin(admin.ModelAdmin):
43
43
44 def title(self, obj: Thread) -> str:
44 def title(self, obj: Thread) -> str:
45 return obj.get_opening_post().get_title()
45 return obj.get_opening_post().get_title()
46
46
47 def reply_count(self, obj: Thread) -> int:
47 def reply_count(self, obj: Thread) -> int:
48 return obj.get_reply_count()
48 return obj.get_reply_count()
49
49
50 def ip(self, obj: Thread):
50 def ip(self, obj: Thread):
51 return obj.get_opening_post().poster_ip
51 return obj.get_opening_post().poster_ip
52
52
53 def display_tags(self, obj: Thread):
53 def display_tags(self, obj: Thread):
54 return ', '.join([str(tag) for tag in obj.get_tags().all()])
54 return ', '.join([str(tag) for tag in obj.get_tags().all()])
55
55
56 def op(self, obj: Thread):
56 def op(self, obj: Thread):
57 return obj.get_opening_post_id()
57 return obj.get_opening_post_id()
58
58
59 list_display = ('id', 'op', 'title', 'reply_count', 'archived', 'ip',
59 list_display = ('id', 'op', 'title', 'reply_count', 'status', 'ip',
60 'display_tags')
60 'display_tags')
61 list_filter = ('bump_time', 'archived', 'bumpable')
61 list_filter = ('bump_time', 'status')
62 search_fields = ('id', 'title')
62 search_fields = ('id', 'title')
63 filter_horizontal = ('tags',)
63 filter_horizontal = ('tags',)
64
64
65
65
66 @admin.register(Ban)
66 @admin.register(Ban)
67 class BanAdmin(admin.ModelAdmin):
67 class BanAdmin(admin.ModelAdmin):
68 list_display = ('ip', 'can_read')
68 list_display = ('ip', 'can_read')
69 list_filter = ('can_read',)
69 list_filter = ('can_read',)
70 search_fields = ('ip',)
70 search_fields = ('ip',)
71
71
72
72
73 @admin.register(Banner)
73 @admin.register(Banner)
74 class BannerAdmin(admin.ModelAdmin):
74 class BannerAdmin(admin.ModelAdmin):
75 list_display = ('title', 'text')
75 list_display = ('title', 'text')
@@ -1,406 +1,406 b''
1 import hashlib
1 import hashlib
2 import re
2 import re
3 import time
3 import time
4 import logging
4 import logging
5 import pytz
5 import pytz
6
6
7 from django import forms
7 from django import forms
8 from django.core.files.uploadedfile import SimpleUploadedFile
8 from django.core.files.uploadedfile import SimpleUploadedFile
9 from django.core.exceptions import ObjectDoesNotExist
9 from django.core.exceptions import ObjectDoesNotExist
10 from django.forms.util import ErrorList
10 from django.forms.util import ErrorList
11 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
11 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
12
12
13 from boards.mdx_neboard import formatters
13 from boards.mdx_neboard import formatters
14 from boards.models.attachment.downloaders import Downloader
14 from boards.models.attachment.downloaders import Downloader
15 from boards.models.post import TITLE_MAX_LENGTH
15 from boards.models.post import TITLE_MAX_LENGTH
16 from boards.models import Tag, Post
16 from boards.models import Tag, Post
17 from boards.utils import validate_file_size, get_file_mimetype, \
17 from boards.utils import validate_file_size, get_file_mimetype, \
18 FILE_EXTENSION_DELIMITER
18 FILE_EXTENSION_DELIMITER
19 from neboard import settings
19 from neboard import settings
20 import boards.settings as board_settings
20 import boards.settings as board_settings
21 import neboard
21 import neboard
22
22
23 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
23 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
24
24
25 VETERAN_POSTING_DELAY = 5
25 VETERAN_POSTING_DELAY = 5
26
26
27 ATTRIBUTE_PLACEHOLDER = 'placeholder'
27 ATTRIBUTE_PLACEHOLDER = 'placeholder'
28 ATTRIBUTE_ROWS = 'rows'
28 ATTRIBUTE_ROWS = 'rows'
29
29
30 LAST_POST_TIME = 'last_post_time'
30 LAST_POST_TIME = 'last_post_time'
31 LAST_LOGIN_TIME = 'last_login_time'
31 LAST_LOGIN_TIME = 'last_login_time'
32 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
32 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
33 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
33 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
34
34
35 LABEL_TITLE = _('Title')
35 LABEL_TITLE = _('Title')
36 LABEL_TEXT = _('Text')
36 LABEL_TEXT = _('Text')
37 LABEL_TAG = _('Tag')
37 LABEL_TAG = _('Tag')
38 LABEL_SEARCH = _('Search')
38 LABEL_SEARCH = _('Search')
39
39
40 ERROR_SPEED = 'Please wait %(delay)d second before sending message'
40 ERROR_SPEED = 'Please wait %(delay)d second before sending message'
41 ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message'
41 ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message'
42
42
43 TAG_MAX_LENGTH = 20
43 TAG_MAX_LENGTH = 20
44
44
45 TEXTAREA_ROWS = 4
45 TEXTAREA_ROWS = 4
46
46
47 TRIPCODE_DELIM = '#'
47 TRIPCODE_DELIM = '#'
48
48
49 # TODO Maybe this may be converted into the database table?
49 # TODO Maybe this may be converted into the database table?
50 MIMETYPE_EXTENSIONS = {
50 MIMETYPE_EXTENSIONS = {
51 'image/jpeg': 'jpeg',
51 'image/jpeg': 'jpeg',
52 'image/png': 'png',
52 'image/png': 'png',
53 'image/gif': 'gif',
53 'image/gif': 'gif',
54 'video/webm': 'webm',
54 'video/webm': 'webm',
55 'application/pdf': 'pdf',
55 'application/pdf': 'pdf',
56 'x-diff': 'diff',
56 'x-diff': 'diff',
57 'image/svg+xml': 'svg',
57 'image/svg+xml': 'svg',
58 'application/x-shockwave-flash': 'swf',
58 'application/x-shockwave-flash': 'swf',
59 }
59 }
60
60
61
61
62 def get_timezones():
62 def get_timezones():
63 timezones = []
63 timezones = []
64 for tz in pytz.common_timezones:
64 for tz in pytz.common_timezones:
65 timezones.append((tz, tz),)
65 timezones.append((tz, tz),)
66 return timezones
66 return timezones
67
67
68
68
69 class FormatPanel(forms.Textarea):
69 class FormatPanel(forms.Textarea):
70 """
70 """
71 Panel for text formatting. Consists of buttons to add different tags to the
71 Panel for text formatting. Consists of buttons to add different tags to the
72 form text area.
72 form text area.
73 """
73 """
74
74
75 def render(self, name, value, attrs=None):
75 def render(self, name, value, attrs=None):
76 output = '<div id="mark-panel">'
76 output = '<div id="mark-panel">'
77 for formatter in formatters:
77 for formatter in formatters:
78 output += '<span class="mark_btn"' + \
78 output += '<span class="mark_btn"' + \
79 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
79 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
80 '\', \'' + formatter.format_right + '\')">' + \
80 '\', \'' + formatter.format_right + '\')">' + \
81 formatter.preview_left + formatter.name + \
81 formatter.preview_left + formatter.name + \
82 formatter.preview_right + '</span>'
82 formatter.preview_right + '</span>'
83
83
84 output += '</div>'
84 output += '</div>'
85 output += super(FormatPanel, self).render(name, value, attrs=attrs)
85 output += super(FormatPanel, self).render(name, value, attrs=attrs)
86
86
87 return output
87 return output
88
88
89
89
90 class PlainErrorList(ErrorList):
90 class PlainErrorList(ErrorList):
91 def __unicode__(self):
91 def __unicode__(self):
92 return self.as_text()
92 return self.as_text()
93
93
94 def as_text(self):
94 def as_text(self):
95 return ''.join(['(!) %s ' % e for e in self])
95 return ''.join(['(!) %s ' % e for e in self])
96
96
97
97
98 class NeboardForm(forms.Form):
98 class NeboardForm(forms.Form):
99 """
99 """
100 Form with neboard-specific formatting.
100 Form with neboard-specific formatting.
101 """
101 """
102
102
103 def as_div(self):
103 def as_div(self):
104 """
104 """
105 Returns this form rendered as HTML <as_div>s.
105 Returns this form rendered as HTML <as_div>s.
106 """
106 """
107
107
108 return self._html_output(
108 return self._html_output(
109 # TODO Do not show hidden rows in the list here
109 # TODO Do not show hidden rows in the list here
110 normal_row='<div class="form-row">'
110 normal_row='<div class="form-row">'
111 '<div class="form-label">'
111 '<div class="form-label">'
112 '%(label)s'
112 '%(label)s'
113 '</div>'
113 '</div>'
114 '<div class="form-input">'
114 '<div class="form-input">'
115 '%(field)s'
115 '%(field)s'
116 '</div>'
116 '</div>'
117 '</div>'
117 '</div>'
118 '<div class="form-row">'
118 '<div class="form-row">'
119 '%(help_text)s'
119 '%(help_text)s'
120 '</div>',
120 '</div>',
121 error_row='<div class="form-row">'
121 error_row='<div class="form-row">'
122 '<div class="form-label"></div>'
122 '<div class="form-label"></div>'
123 '<div class="form-errors">%s</div>'
123 '<div class="form-errors">%s</div>'
124 '</div>',
124 '</div>',
125 row_ender='</div>',
125 row_ender='</div>',
126 help_text_html='%s',
126 help_text_html='%s',
127 errors_on_separate_row=True)
127 errors_on_separate_row=True)
128
128
129 def as_json_errors(self):
129 def as_json_errors(self):
130 errors = []
130 errors = []
131
131
132 for name, field in list(self.fields.items()):
132 for name, field in list(self.fields.items()):
133 if self[name].errors:
133 if self[name].errors:
134 errors.append({
134 errors.append({
135 'field': name,
135 'field': name,
136 'errors': self[name].errors.as_text(),
136 'errors': self[name].errors.as_text(),
137 })
137 })
138
138
139 return errors
139 return errors
140
140
141
141
142 class PostForm(NeboardForm):
142 class PostForm(NeboardForm):
143
143
144 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
144 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
145 label=LABEL_TITLE,
145 label=LABEL_TITLE,
146 widget=forms.TextInput(
146 widget=forms.TextInput(
147 attrs={ATTRIBUTE_PLACEHOLDER:
147 attrs={ATTRIBUTE_PLACEHOLDER:
148 'test#tripcode'}))
148 'test#tripcode'}))
149 text = forms.CharField(
149 text = forms.CharField(
150 widget=FormatPanel(attrs={
150 widget=FormatPanel(attrs={
151 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
151 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
152 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
152 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
153 }),
153 }),
154 required=False, label=LABEL_TEXT)
154 required=False, label=LABEL_TEXT)
155 file = forms.FileField(required=False, label=_('File'),
155 file = forms.FileField(required=False, label=_('File'),
156 widget=forms.ClearableFileInput(
156 widget=forms.ClearableFileInput(
157 attrs={'accept': 'file/*'}))
157 attrs={'accept': 'file/*'}))
158 file_url = forms.CharField(required=False, label=_('File URL'),
158 file_url = forms.CharField(required=False, label=_('File URL'),
159 widget=forms.TextInput(
159 widget=forms.TextInput(
160 attrs={ATTRIBUTE_PLACEHOLDER:
160 attrs={ATTRIBUTE_PLACEHOLDER:
161 'http://example.com/image.png'}))
161 'http://example.com/image.png'}))
162
162
163 # This field is for spam prevention only
163 # This field is for spam prevention only
164 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
164 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
165 widget=forms.TextInput(attrs={
165 widget=forms.TextInput(attrs={
166 'class': 'form-email'}))
166 'class': 'form-email'}))
167 threads = forms.CharField(required=False, label=_('Additional threads'),
167 threads = forms.CharField(required=False, label=_('Additional threads'),
168 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
168 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
169 '123 456 789'}))
169 '123 456 789'}))
170
170
171 session = None
171 session = None
172 need_to_ban = False
172 need_to_ban = False
173
173
174 def _update_file_extension(self, file):
174 def _update_file_extension(self, file):
175 if file:
175 if file:
176 mimetype = get_file_mimetype(file)
176 mimetype = get_file_mimetype(file)
177 extension = MIMETYPE_EXTENSIONS.get(mimetype)
177 extension = MIMETYPE_EXTENSIONS.get(mimetype)
178 if extension:
178 if extension:
179 filename = file.name.split(FILE_EXTENSION_DELIMITER, 1)[0]
179 filename = file.name.split(FILE_EXTENSION_DELIMITER, 1)[0]
180 new_filename = filename + FILE_EXTENSION_DELIMITER + extension
180 new_filename = filename + FILE_EXTENSION_DELIMITER + extension
181
181
182 file.name = new_filename
182 file.name = new_filename
183 else:
183 else:
184 logger = logging.getLogger('boards.forms.extension')
184 logger = logging.getLogger('boards.forms.extension')
185
185
186 logger.info('Unrecognized file mimetype: {}'.format(mimetype))
186 logger.info('Unrecognized file mimetype: {}'.format(mimetype))
187
187
188 def clean_title(self):
188 def clean_title(self):
189 title = self.cleaned_data['title']
189 title = self.cleaned_data['title']
190 if title:
190 if title:
191 if len(title) > TITLE_MAX_LENGTH:
191 if len(title) > TITLE_MAX_LENGTH:
192 raise forms.ValidationError(_('Title must have less than %s '
192 raise forms.ValidationError(_('Title must have less than %s '
193 'characters') %
193 'characters') %
194 str(TITLE_MAX_LENGTH))
194 str(TITLE_MAX_LENGTH))
195 return title
195 return title
196
196
197 def clean_text(self):
197 def clean_text(self):
198 text = self.cleaned_data['text'].strip()
198 text = self.cleaned_data['text'].strip()
199 if text:
199 if text:
200 max_length = board_settings.get_int('Forms', 'MaxTextLength')
200 max_length = board_settings.get_int('Forms', 'MaxTextLength')
201 if len(text) > max_length:
201 if len(text) > max_length:
202 raise forms.ValidationError(_('Text must have less than %s '
202 raise forms.ValidationError(_('Text must have less than %s '
203 'characters') % str(max_length))
203 'characters') % str(max_length))
204 return text
204 return text
205
205
206 def clean_file(self):
206 def clean_file(self):
207 file = self.cleaned_data['file']
207 file = self.cleaned_data['file']
208
208
209 if file:
209 if file:
210 validate_file_size(file.size)
210 validate_file_size(file.size)
211 self._update_file_extension(file)
211 self._update_file_extension(file)
212
212
213 return file
213 return file
214
214
215 def clean_file_url(self):
215 def clean_file_url(self):
216 url = self.cleaned_data['file_url']
216 url = self.cleaned_data['file_url']
217
217
218 file = None
218 file = None
219 if url:
219 if url:
220 file = self._get_file_from_url(url)
220 file = self._get_file_from_url(url)
221
221
222 if not file:
222 if not file:
223 raise forms.ValidationError(_('Invalid URL'))
223 raise forms.ValidationError(_('Invalid URL'))
224 else:
224 else:
225 validate_file_size(file.size)
225 validate_file_size(file.size)
226 self._update_file_extension(file)
226 self._update_file_extension(file)
227
227
228 return file
228 return file
229
229
230 def clean_threads(self):
230 def clean_threads(self):
231 threads_str = self.cleaned_data['threads']
231 threads_str = self.cleaned_data['threads']
232
232
233 if len(threads_str) > 0:
233 if len(threads_str) > 0:
234 threads_id_list = threads_str.split(' ')
234 threads_id_list = threads_str.split(' ')
235
235
236 threads = list()
236 threads = list()
237
237
238 for thread_id in threads_id_list:
238 for thread_id in threads_id_list:
239 try:
239 try:
240 thread = Post.objects.get(id=int(thread_id))
240 thread = Post.objects.get(id=int(thread_id))
241 if not thread.is_opening() or thread.get_thread().archived:
241 if not thread.is_opening() or thread.get_thread().is_archived():
242 raise ObjectDoesNotExist()
242 raise ObjectDoesNotExist()
243 threads.append(thread)
243 threads.append(thread)
244 except (ObjectDoesNotExist, ValueError):
244 except (ObjectDoesNotExist, ValueError):
245 raise forms.ValidationError(_('Invalid additional thread list'))
245 raise forms.ValidationError(_('Invalid additional thread list'))
246
246
247 return threads
247 return threads
248
248
249 def clean(self):
249 def clean(self):
250 cleaned_data = super(PostForm, self).clean()
250 cleaned_data = super(PostForm, self).clean()
251
251
252 if cleaned_data['email']:
252 if cleaned_data['email']:
253 self.need_to_ban = True
253 self.need_to_ban = True
254 raise forms.ValidationError('A human cannot enter a hidden field')
254 raise forms.ValidationError('A human cannot enter a hidden field')
255
255
256 if not self.errors:
256 if not self.errors:
257 self._clean_text_file()
257 self._clean_text_file()
258
258
259 if not self.errors and self.session:
259 if not self.errors and self.session:
260 self._validate_posting_speed()
260 self._validate_posting_speed()
261
261
262 return cleaned_data
262 return cleaned_data
263
263
264 def get_file(self):
264 def get_file(self):
265 """
265 """
266 Gets file from form or URL.
266 Gets file from form or URL.
267 """
267 """
268
268
269 file = self.cleaned_data['file']
269 file = self.cleaned_data['file']
270 return file or self.cleaned_data['file_url']
270 return file or self.cleaned_data['file_url']
271
271
272 def get_tripcode(self):
272 def get_tripcode(self):
273 title = self.cleaned_data['title']
273 title = self.cleaned_data['title']
274 if title is not None and TRIPCODE_DELIM in title:
274 if title is not None and TRIPCODE_DELIM in title:
275 code = title.split(TRIPCODE_DELIM, maxsplit=1)[1] + neboard.settings.SECRET_KEY
275 code = title.split(TRIPCODE_DELIM, maxsplit=1)[1] + neboard.settings.SECRET_KEY
276 tripcode = hashlib.md5(code.encode()).hexdigest()
276 tripcode = hashlib.md5(code.encode()).hexdigest()
277 else:
277 else:
278 tripcode = ''
278 tripcode = ''
279 return tripcode
279 return tripcode
280
280
281 def get_title(self):
281 def get_title(self):
282 title = self.cleaned_data['title']
282 title = self.cleaned_data['title']
283 if title is not None and TRIPCODE_DELIM in title:
283 if title is not None and TRIPCODE_DELIM in title:
284 return title.split(TRIPCODE_DELIM, maxsplit=1)[0]
284 return title.split(TRIPCODE_DELIM, maxsplit=1)[0]
285 else:
285 else:
286 return title
286 return title
287
287
288 def _clean_text_file(self):
288 def _clean_text_file(self):
289 text = self.cleaned_data.get('text')
289 text = self.cleaned_data.get('text')
290 file = self.get_file()
290 file = self.get_file()
291
291
292 if (not text) and (not file):
292 if (not text) and (not file):
293 error_message = _('Either text or file must be entered.')
293 error_message = _('Either text or file must be entered.')
294 self._errors['text'] = self.error_class([error_message])
294 self._errors['text'] = self.error_class([error_message])
295
295
296 def _validate_posting_speed(self):
296 def _validate_posting_speed(self):
297 can_post = True
297 can_post = True
298
298
299 posting_delay = settings.POSTING_DELAY
299 posting_delay = settings.POSTING_DELAY
300
300
301 if board_settings.get_bool('Forms', 'LimitPostingSpeed'):
301 if board_settings.get_bool('Forms', 'LimitPostingSpeed'):
302 now = time.time()
302 now = time.time()
303
303
304 current_delay = 0
304 current_delay = 0
305
305
306 if LAST_POST_TIME not in self.session:
306 if LAST_POST_TIME not in self.session:
307 self.session[LAST_POST_TIME] = now
307 self.session[LAST_POST_TIME] = now
308
308
309 need_delay = True
309 need_delay = True
310 else:
310 else:
311 last_post_time = self.session.get(LAST_POST_TIME)
311 last_post_time = self.session.get(LAST_POST_TIME)
312 current_delay = int(now - last_post_time)
312 current_delay = int(now - last_post_time)
313
313
314 need_delay = current_delay < posting_delay
314 need_delay = current_delay < posting_delay
315
315
316 if need_delay:
316 if need_delay:
317 delay = posting_delay - current_delay
317 delay = posting_delay - current_delay
318 error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL,
318 error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL,
319 delay) % {'delay': delay}
319 delay) % {'delay': delay}
320 self._errors['text'] = self.error_class([error_message])
320 self._errors['text'] = self.error_class([error_message])
321
321
322 can_post = False
322 can_post = False
323
323
324 if can_post:
324 if can_post:
325 self.session[LAST_POST_TIME] = now
325 self.session[LAST_POST_TIME] = now
326
326
327 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
327 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
328 """
328 """
329 Gets an file file from URL.
329 Gets an file file from URL.
330 """
330 """
331
331
332 img_temp = None
332 img_temp = None
333
333
334 try:
334 try:
335 for downloader in Downloader.__subclasses__():
335 for downloader in Downloader.__subclasses__():
336 if downloader.handles(url):
336 if downloader.handles(url):
337 return downloader.download(url)
337 return downloader.download(url)
338 # If nobody of the specific downloaders handles this, use generic
338 # If nobody of the specific downloaders handles this, use generic
339 # one
339 # one
340 return Downloader.download(url)
340 return Downloader.download(url)
341 except forms.ValidationError as e:
341 except forms.ValidationError as e:
342 raise e
342 raise e
343 except Exception as e:
343 except Exception as e:
344 # Just return no file
344 # Just return no file
345 pass
345 pass
346
346
347
347
348 class ThreadForm(PostForm):
348 class ThreadForm(PostForm):
349
349
350 tags = forms.CharField(
350 tags = forms.CharField(
351 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
351 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
352 max_length=100, label=_('Tags'), required=True)
352 max_length=100, label=_('Tags'), required=True)
353
353
354 def clean_tags(self):
354 def clean_tags(self):
355 tags = self.cleaned_data['tags'].strip()
355 tags = self.cleaned_data['tags'].strip()
356
356
357 if not tags or not REGEX_TAGS.match(tags):
357 if not tags or not REGEX_TAGS.match(tags):
358 raise forms.ValidationError(
358 raise forms.ValidationError(
359 _('Inappropriate characters in tags.'))
359 _('Inappropriate characters in tags.'))
360
360
361 required_tag_exists = False
361 required_tag_exists = False
362 tag_set = set()
362 tag_set = set()
363 for tag_string in tags.split():
363 for tag_string in tags.split():
364 tag, created = Tag.objects.get_or_create(name=tag_string.strip().lower())
364 tag, created = Tag.objects.get_or_create(name=tag_string.strip().lower())
365 tag_set.add(tag)
365 tag_set.add(tag)
366
366
367 # If this is a new tag, don't check for its parents because nobody
367 # If this is a new tag, don't check for its parents because nobody
368 # added them yet
368 # added them yet
369 if not created:
369 if not created:
370 tag_set |= set(tag.get_all_parents())
370 tag_set |= set(tag.get_all_parents())
371
371
372 for tag in tag_set:
372 for tag in tag_set:
373 if tag.required:
373 if tag.required:
374 required_tag_exists = True
374 required_tag_exists = True
375 break
375 break
376
376
377 if not required_tag_exists:
377 if not required_tag_exists:
378 raise forms.ValidationError(
378 raise forms.ValidationError(
379 _('Need at least one section.'))
379 _('Need at least one section.'))
380
380
381 return tag_set
381 return tag_set
382
382
383 def clean(self):
383 def clean(self):
384 cleaned_data = super(ThreadForm, self).clean()
384 cleaned_data = super(ThreadForm, self).clean()
385
385
386 return cleaned_data
386 return cleaned_data
387
387
388
388
389 class SettingsForm(NeboardForm):
389 class SettingsForm(NeboardForm):
390
390
391 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
391 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
392 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('Image view mode'))
392 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('Image view mode'))
393 username = forms.CharField(label=_('User name'), required=False)
393 username = forms.CharField(label=_('User name'), required=False)
394 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
394 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
395
395
396 def clean_username(self):
396 def clean_username(self):
397 username = self.cleaned_data['username']
397 username = self.cleaned_data['username']
398
398
399 if username and not REGEX_TAGS.match(username):
399 if username and not REGEX_TAGS.match(username):
400 raise forms.ValidationError(_('Inappropriate characters.'))
400 raise forms.ValidationError(_('Inappropriate characters.'))
401
401
402 return username
402 return username
403
403
404
404
405 class SearchForm(NeboardForm):
405 class SearchForm(NeboardForm):
406 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
406 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
@@ -1,95 +1,95 b''
1 from django.db import models
1 from django.db import models
2 from django.template.defaultfilters import filesizeformat
2 from django.template.defaultfilters import filesizeformat
3
3
4 from boards import thumbs, utils
4 from boards import thumbs, utils
5 import boards
5 import boards
6 from boards.models.base import Viewable
6 from boards.models.base import Viewable
7 from boards.utils import get_upload_filename
7 from boards.utils import get_upload_filename
8
8
9 __author__ = 'neko259'
9 __author__ = 'neko259'
10
10
11
11
12 IMAGE_THUMB_SIZE = (200, 150)
12 IMAGE_THUMB_SIZE = (200, 150)
13 HASH_LENGTH = 36
13 HASH_LENGTH = 36
14
14
15 CSS_CLASS_IMAGE = 'image'
15 CSS_CLASS_IMAGE = 'image'
16 CSS_CLASS_THUMB = 'thumb'
16 CSS_CLASS_THUMB = 'thumb'
17
17
18
18
19 class PostImageManager(models.Manager):
19 class PostImageManager(models.Manager):
20 def create_with_hash(self, image):
20 def create_with_hash(self, image):
21 image_hash = utils.get_file_hash(image)
21 image_hash = utils.get_file_hash(image)
22 existing = self.filter(hash=image_hash)
22 existing = self.filter(hash=image_hash)
23 if len(existing) > 0:
23 if len(existing) > 0:
24 post_image = existing[0]
24 post_image = existing[0]
25 else:
25 else:
26 post_image = PostImage.objects.create(image=image)
26 post_image = PostImage.objects.create(image=image)
27
27
28 return post_image
28 return post_image
29
29
30 def get_random_images(self, count, include_archived=False, tags=None):
30 def get_random_images(self, count, tags=None):
31 images = self.filter(post_images__thread__archived=include_archived)
31 images = self
32 if tags is not None:
32 if tags is not None:
33 images = images.filter(post_images__threads__tags__in=tags)
33 images = images.filter(post_images__threads__tags__in=tags)
34 return images.order_by('?')[:count]
34 return images.order_by('?')[:count]
35
35
36
36
37 class PostImage(models.Model, Viewable):
37 class PostImage(models.Model, Viewable):
38 objects = PostImageManager()
38 objects = PostImageManager()
39
39
40 class Meta:
40 class Meta:
41 app_label = 'boards'
41 app_label = 'boards'
42 ordering = ('id',)
42 ordering = ('id',)
43
43
44 width = models.IntegerField(default=0)
44 width = models.IntegerField(default=0)
45 height = models.IntegerField(default=0)
45 height = models.IntegerField(default=0)
46
46
47 pre_width = models.IntegerField(default=0)
47 pre_width = models.IntegerField(default=0)
48 pre_height = models.IntegerField(default=0)
48 pre_height = models.IntegerField(default=0)
49
49
50 image = thumbs.ImageWithThumbsField(upload_to=get_upload_filename,
50 image = thumbs.ImageWithThumbsField(upload_to=get_upload_filename,
51 blank=True, sizes=(IMAGE_THUMB_SIZE,),
51 blank=True, sizes=(IMAGE_THUMB_SIZE,),
52 width_field='width',
52 width_field='width',
53 height_field='height',
53 height_field='height',
54 preview_width_field='pre_width',
54 preview_width_field='pre_width',
55 preview_height_field='pre_height')
55 preview_height_field='pre_height')
56 hash = models.CharField(max_length=HASH_LENGTH)
56 hash = models.CharField(max_length=HASH_LENGTH)
57
57
58 def save(self, *args, **kwargs):
58 def save(self, *args, **kwargs):
59 """
59 """
60 Saves the model and computes the image hash for deduplication purposes.
60 Saves the model and computes the image hash for deduplication purposes.
61 """
61 """
62
62
63 if not self.pk and self.image:
63 if not self.pk and self.image:
64 self.hash = utils.get_file_hash(self.image)
64 self.hash = utils.get_file_hash(self.image)
65 super(PostImage, self).save(*args, **kwargs)
65 super(PostImage, self).save(*args, **kwargs)
66
66
67 def __str__(self):
67 def __str__(self):
68 return self.image.url
68 return self.image.url
69
69
70 def get_view(self):
70 def get_view(self):
71 metadata = '{}, {}'.format(self.image.name.split('.')[-1],
71 metadata = '{}, {}'.format(self.image.name.split('.')[-1],
72 filesizeformat(self.image.size))
72 filesizeformat(self.image.size))
73 return '<div class="{}">' \
73 return '<div class="{}">' \
74 '<a class="{}" href="{full}">' \
74 '<a class="{}" href="{full}">' \
75 '<img class="post-image-preview"' \
75 '<img class="post-image-preview"' \
76 ' src="{}"' \
76 ' src="{}"' \
77 ' alt="{}"' \
77 ' alt="{}"' \
78 ' width="{}"' \
78 ' width="{}"' \
79 ' height="{}"' \
79 ' height="{}"' \
80 ' data-width="{}"' \
80 ' data-width="{}"' \
81 ' data-height="{}" />' \
81 ' data-height="{}" />' \
82 '</a>' \
82 '</a>' \
83 '<div class="image-metadata">'\
83 '<div class="image-metadata">'\
84 '<a href="{full}" download>{image_meta}</a>'\
84 '<a href="{full}" download>{image_meta}</a>'\
85 '</div>' \
85 '</div>' \
86 '</div>'\
86 '</div>'\
87 .format(CSS_CLASS_IMAGE, CSS_CLASS_THUMB,
87 .format(CSS_CLASS_IMAGE, CSS_CLASS_THUMB,
88 self.image.url_200x150,
88 self.image.url_200x150,
89 str(self.hash), str(self.pre_width),
89 str(self.hash), str(self.pre_width),
90 str(self.pre_height), str(self.width), str(self.height),
90 str(self.pre_height), str(self.width), str(self.height),
91 full=self.image.url, image_meta=metadata)
91 full=self.image.url, image_meta=metadata)
92
92
93 def get_random_associated_post(self):
93 def get_random_associated_post(self):
94 posts = boards.models.Post.objects.filter(images__in=[self])
94 posts = boards.models.Post.objects.filter(images__in=[self])
95 return posts.order_by('?').first()
95 return posts.order_by('?').first()
@@ -1,360 +1,360 b''
1 import logging
1 import logging
2 import re
2 import re
3 import uuid
3 import uuid
4
4
5 from django.core.exceptions import ObjectDoesNotExist
5 from django.core.exceptions import ObjectDoesNotExist
6 from django.core.urlresolvers import reverse
6 from django.core.urlresolvers import reverse
7 from django.db import models
7 from django.db import models
8 from django.db.models import TextField, QuerySet
8 from django.db.models import TextField, QuerySet
9 from django.template.defaultfilters import striptags, truncatewords
9 from django.template.defaultfilters import striptags, truncatewords
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.abstracts.tripcode import Tripcode
14 from boards.abstracts.tripcode import Tripcode
15 from boards.mdx_neboard import Parser
15 from boards.mdx_neboard import Parser
16 from boards.models import PostImage, Attachment
16 from boards.models import PostImage, Attachment
17 from boards.models.base import Viewable
17 from boards.models.base import Viewable
18 from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
18 from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
19 from boards.models.post.manager import PostManager
19 from boards.models.post.manager import PostManager
20 from boards.models.user import Notification
20 from boards.models.user import Notification
21
21
22 CSS_CLS_HIDDEN_POST = 'hidden_post'
22 CSS_CLS_HIDDEN_POST = 'hidden_post'
23 CSS_CLS_DEAD_POST = 'dead_post'
23 CSS_CLS_DEAD_POST = 'dead_post'
24 CSS_CLS_ARCHIVE_POST = 'archive_post'
24 CSS_CLS_ARCHIVE_POST = 'archive_post'
25 CSS_CLS_POST = 'post'
25 CSS_CLS_POST = 'post'
26
26
27 TITLE_MAX_WORDS = 10
27 TITLE_MAX_WORDS = 10
28
28
29 APP_LABEL_BOARDS = 'boards'
29 APP_LABEL_BOARDS = 'boards'
30
30
31 BAN_REASON_AUTO = 'Auto'
31 BAN_REASON_AUTO = 'Auto'
32
32
33 IMAGE_THUMB_SIZE = (200, 150)
33 IMAGE_THUMB_SIZE = (200, 150)
34
34
35 TITLE_MAX_LENGTH = 200
35 TITLE_MAX_LENGTH = 200
36
36
37 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
37 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
38 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
38 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
39
39
40 PARAMETER_TRUNCATED = 'truncated'
40 PARAMETER_TRUNCATED = 'truncated'
41 PARAMETER_TAG = 'tag'
41 PARAMETER_TAG = 'tag'
42 PARAMETER_OFFSET = 'offset'
42 PARAMETER_OFFSET = 'offset'
43 PARAMETER_DIFF_TYPE = 'type'
43 PARAMETER_DIFF_TYPE = 'type'
44 PARAMETER_CSS_CLASS = 'css_class'
44 PARAMETER_CSS_CLASS = 'css_class'
45 PARAMETER_THREAD = 'thread'
45 PARAMETER_THREAD = 'thread'
46 PARAMETER_IS_OPENING = 'is_opening'
46 PARAMETER_IS_OPENING = 'is_opening'
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 PARAMETER_NEED_OP_DATA = 'need_op_data'
51 PARAMETER_NEED_OP_DATA = 'need_op_data'
52
52
53 POST_VIEW_PARAMS = (
53 POST_VIEW_PARAMS = (
54 'need_op_data',
54 'need_op_data',
55 'reply_link',
55 'reply_link',
56 'need_open_link',
56 'need_open_link',
57 'truncated',
57 'truncated',
58 'mode_tree',
58 'mode_tree',
59 'perms',
59 'perms',
60 )
60 )
61
61
62
62
63 class Post(models.Model, Viewable):
63 class Post(models.Model, Viewable):
64 """A post is a message."""
64 """A post is a message."""
65
65
66 objects = PostManager()
66 objects = PostManager()
67
67
68 class Meta:
68 class Meta:
69 app_label = APP_LABEL_BOARDS
69 app_label = APP_LABEL_BOARDS
70 ordering = ('id',)
70 ordering = ('id',)
71
71
72 title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
72 title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
73 pub_time = models.DateTimeField()
73 pub_time = models.DateTimeField()
74 text = TextField(blank=True, null=True)
74 text = TextField(blank=True, null=True)
75 _text_rendered = TextField(blank=True, null=True, editable=False)
75 _text_rendered = TextField(blank=True, null=True, editable=False)
76
76
77 images = models.ManyToManyField(PostImage, null=True, blank=True,
77 images = models.ManyToManyField(PostImage, null=True, blank=True,
78 related_name='post_images', db_index=True)
78 related_name='post_images', db_index=True)
79 attachments = models.ManyToManyField(Attachment, null=True, blank=True,
79 attachments = models.ManyToManyField(Attachment, null=True, blank=True,
80 related_name='attachment_posts')
80 related_name='attachment_posts')
81
81
82 poster_ip = models.GenericIPAddressField()
82 poster_ip = models.GenericIPAddressField()
83
83
84 # TODO This field can be removed cause UID is used for update now
84 # TODO This field can be removed cause UID is used for update now
85 last_edit_time = models.DateTimeField()
85 last_edit_time = models.DateTimeField()
86
86
87 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
87 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
88 null=True,
88 null=True,
89 blank=True, related_name='refposts',
89 blank=True, related_name='refposts',
90 db_index=True)
90 db_index=True)
91 refmap = models.TextField(null=True, blank=True)
91 refmap = models.TextField(null=True, blank=True)
92 threads = models.ManyToManyField('Thread', db_index=True,
92 threads = models.ManyToManyField('Thread', db_index=True,
93 related_name='multi_replies')
93 related_name='multi_replies')
94 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
94 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
95
95
96 url = models.TextField()
96 url = models.TextField()
97 uid = models.TextField(db_index=True)
97 uid = models.TextField(db_index=True)
98
98
99 tripcode = models.CharField(max_length=50, blank=True, default='')
99 tripcode = models.CharField(max_length=50, blank=True, default='')
100 opening = models.BooleanField(db_index=True)
100 opening = models.BooleanField(db_index=True)
101 hidden = models.BooleanField(default=False)
101 hidden = models.BooleanField(default=False)
102
102
103 def __str__(self):
103 def __str__(self):
104 return 'P#{}/{}'.format(self.id, self.get_title())
104 return 'P#{}/{}'.format(self.id, self.get_title())
105
105
106 def get_referenced_posts(self):
106 def get_referenced_posts(self):
107 threads = self.get_threads().all()
107 threads = self.get_threads().all()
108 return self.referenced_posts.filter(threads__in=threads)\
108 return self.referenced_posts.filter(threads__in=threads)\
109 .order_by('pub_time').distinct().all()
109 .order_by('pub_time').distinct().all()
110
110
111 def get_title(self) -> str:
111 def get_title(self) -> str:
112 return self.title
112 return self.title
113
113
114 def get_title_or_text(self):
114 def get_title_or_text(self):
115 title = self.get_title()
115 title = self.get_title()
116 if not title:
116 if not title:
117 title = truncatewords(striptags(self.get_text()), TITLE_MAX_WORDS)
117 title = truncatewords(striptags(self.get_text()), TITLE_MAX_WORDS)
118
118
119 return title
119 return title
120
120
121 def build_refmap(self) -> None:
121 def build_refmap(self) -> None:
122 """
122 """
123 Builds a replies map string from replies list. This is a cache to stop
123 Builds a replies map string from replies list. This is a cache to stop
124 the server from recalculating the map on every post show.
124 the server from recalculating the map on every post show.
125 """
125 """
126
126
127 post_urls = [refpost.get_link_view()
127 post_urls = [refpost.get_link_view()
128 for refpost in self.referenced_posts.all()]
128 for refpost in self.referenced_posts.all()]
129
129
130 self.refmap = ', '.join(post_urls)
130 self.refmap = ', '.join(post_urls)
131
131
132 def is_referenced(self) -> bool:
132 def is_referenced(self) -> bool:
133 return self.refmap and len(self.refmap) > 0
133 return self.refmap and len(self.refmap) > 0
134
134
135 def is_opening(self) -> bool:
135 def is_opening(self) -> bool:
136 """
136 """
137 Checks if this is an opening post or just a reply.
137 Checks if this is an opening post or just a reply.
138 """
138 """
139
139
140 return self.opening
140 return self.opening
141
141
142 def get_absolute_url(self, thread=None):
142 def get_absolute_url(self, thread=None):
143 url = None
143 url = None
144
144
145 if thread is None:
145 if thread is None:
146 thread = self.get_thread()
146 thread = self.get_thread()
147
147
148 # Url is cached only for the "main" thread. When getting url
148 # Url is cached only for the "main" thread. When getting url
149 # for other threads, do it manually.
149 # for other threads, do it manually.
150 if self.url:
150 if self.url:
151 url = self.url
151 url = self.url
152
152
153 if url is None:
153 if url is None:
154 opening_id = thread.get_opening_post_id()
154 opening_id = thread.get_opening_post_id()
155 url = reverse('thread', kwargs={'post_id': opening_id})
155 url = reverse('thread', kwargs={'post_id': opening_id})
156 if self.id != opening_id:
156 if self.id != opening_id:
157 url += '#' + str(self.id)
157 url += '#' + str(self.id)
158
158
159 return url
159 return url
160
160
161 def get_thread(self):
161 def get_thread(self):
162 return self.thread
162 return self.thread
163
163
164 def get_threads(self) -> QuerySet:
164 def get_threads(self) -> QuerySet:
165 """
165 """
166 Gets post's thread.
166 Gets post's thread.
167 """
167 """
168
168
169 return self.threads
169 return self.threads
170
170
171 def get_view(self, *args, **kwargs) -> str:
171 def get_view(self, *args, **kwargs) -> str:
172 """
172 """
173 Renders post's HTML view. Some of the post params can be passed over
173 Renders post's HTML view. Some of the post params can be passed over
174 kwargs for the means of caching (if we view the thread, some params
174 kwargs for the means of caching (if we view the thread, some params
175 are same for every post and don't need to be computed over and over.
175 are same for every post and don't need to be computed over and over.
176 """
176 """
177
177
178 thread = self.get_thread()
178 thread = self.get_thread()
179
179
180 css_classes = [CSS_CLS_POST]
180 css_classes = [CSS_CLS_POST]
181 if thread.archived:
181 if thread.is_archived():
182 css_classes.append(CSS_CLS_ARCHIVE_POST)
182 css_classes.append(CSS_CLS_ARCHIVE_POST)
183 elif not thread.can_bump():
183 elif not thread.can_bump():
184 css_classes.append(CSS_CLS_DEAD_POST)
184 css_classes.append(CSS_CLS_DEAD_POST)
185 if self.is_hidden():
185 if self.is_hidden():
186 css_classes.append(CSS_CLS_HIDDEN_POST)
186 css_classes.append(CSS_CLS_HIDDEN_POST)
187
187
188 params = dict()
188 params = dict()
189 for param in POST_VIEW_PARAMS:
189 for param in POST_VIEW_PARAMS:
190 if param in kwargs:
190 if param in kwargs:
191 params[param] = kwargs[param]
191 params[param] = kwargs[param]
192
192
193 params.update({
193 params.update({
194 PARAMETER_POST: self,
194 PARAMETER_POST: self,
195 PARAMETER_IS_OPENING: self.is_opening(),
195 PARAMETER_IS_OPENING: self.is_opening(),
196 PARAMETER_THREAD: thread,
196 PARAMETER_THREAD: thread,
197 PARAMETER_CSS_CLASS: ' '.join(css_classes),
197 PARAMETER_CSS_CLASS: ' '.join(css_classes),
198 })
198 })
199
199
200 return render_to_string('boards/post.html', params)
200 return render_to_string('boards/post.html', params)
201
201
202 def get_search_view(self, *args, **kwargs):
202 def get_search_view(self, *args, **kwargs):
203 return self.get_view(need_op_data=True, *args, **kwargs)
203 return self.get_view(need_op_data=True, *args, **kwargs)
204
204
205 def get_first_image(self) -> PostImage:
205 def get_first_image(self) -> PostImage:
206 return self.images.earliest('id')
206 return self.images.earliest('id')
207
207
208 def delete(self, using=None):
208 def delete(self, using=None):
209 """
209 """
210 Deletes all post images and the post itself.
210 Deletes all post images and the post itself.
211 """
211 """
212
212
213 for image in self.images.all():
213 for image in self.images.all():
214 image_refs_count = image.post_images.count()
214 image_refs_count = image.post_images.count()
215 if image_refs_count == 1:
215 if image_refs_count == 1:
216 image.delete()
216 image.delete()
217
217
218 for attachment in self.attachments.all():
218 for attachment in self.attachments.all():
219 attachment_refs_count = attachment.attachment_posts.count()
219 attachment_refs_count = attachment.attachment_posts.count()
220 if attachment_refs_count == 1:
220 if attachment_refs_count == 1:
221 attachment.delete()
221 attachment.delete()
222
222
223 thread = self.get_thread()
223 thread = self.get_thread()
224 thread.last_edit_time = timezone.now()
224 thread.last_edit_time = timezone.now()
225 thread.save()
225 thread.save()
226
226
227 super(Post, self).delete(using)
227 super(Post, self).delete(using)
228
228
229 logging.getLogger('boards.post.delete').info(
229 logging.getLogger('boards.post.delete').info(
230 'Deleted post {}'.format(self))
230 'Deleted post {}'.format(self))
231
231
232 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
232 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
233 include_last_update=False) -> str:
233 include_last_update=False) -> str:
234 """
234 """
235 Gets post HTML or JSON data that can be rendered on a page or used by
235 Gets post HTML or JSON data that can be rendered on a page or used by
236 API.
236 API.
237 """
237 """
238
238
239 return get_exporter(format_type).export(self, request,
239 return get_exporter(format_type).export(self, request,
240 include_last_update)
240 include_last_update)
241
241
242 def notify_clients(self, recursive=True):
242 def notify_clients(self, recursive=True):
243 """
243 """
244 Sends post HTML data to the thread web socket.
244 Sends post HTML data to the thread web socket.
245 """
245 """
246
246
247 if not settings.get_bool('External', 'WebsocketsEnabled'):
247 if not settings.get_bool('External', 'WebsocketsEnabled'):
248 return
248 return
249
249
250 thread_ids = list()
250 thread_ids = list()
251 for thread in self.get_threads().all():
251 for thread in self.get_threads().all():
252 thread_ids.append(thread.id)
252 thread_ids.append(thread.id)
253
253
254 thread.notify_clients()
254 thread.notify_clients()
255
255
256 if recursive:
256 if recursive:
257 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
257 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
258 post_id = reply_number.group(1)
258 post_id = reply_number.group(1)
259
259
260 try:
260 try:
261 ref_post = Post.objects.get(id=post_id)
261 ref_post = Post.objects.get(id=post_id)
262
262
263 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
263 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
264 # If post is in this thread, its thread was already notified.
264 # If post is in this thread, its thread was already notified.
265 # Otherwise, notify its thread separately.
265 # Otherwise, notify its thread separately.
266 ref_post.notify_clients(recursive=False)
266 ref_post.notify_clients(recursive=False)
267 except ObjectDoesNotExist:
267 except ObjectDoesNotExist:
268 pass
268 pass
269
269
270 def build_url(self):
270 def build_url(self):
271 self.url = self.get_absolute_url()
271 self.url = self.get_absolute_url()
272 self.save(update_fields=['url'])
272 self.save(update_fields=['url'])
273
273
274 def save(self, force_insert=False, force_update=False, using=None,
274 def save(self, force_insert=False, force_update=False, using=None,
275 update_fields=None):
275 update_fields=None):
276 self._text_rendered = Parser().parse(self.get_raw_text())
276 self._text_rendered = Parser().parse(self.get_raw_text())
277
277
278 self.uid = str(uuid.uuid4())
278 self.uid = str(uuid.uuid4())
279 if update_fields is not None and 'uid' not in update_fields:
279 if update_fields is not None and 'uid' not in update_fields:
280 update_fields += ['uid']
280 update_fields += ['uid']
281
281
282 if self.id:
282 if self.id:
283 for thread in self.get_threads().all():
283 for thread in self.get_threads().all():
284 thread.last_edit_time = self.last_edit_time
284 thread.last_edit_time = self.last_edit_time
285
285
286 thread.save(update_fields=['last_edit_time', 'bumpable'])
286 thread.save(update_fields=['last_edit_time', 'status'])
287
287
288 super().save(force_insert, force_update, using, update_fields)
288 super().save(force_insert, force_update, using, update_fields)
289
289
290 def get_text(self) -> str:
290 def get_text(self) -> str:
291 return self._text_rendered
291 return self._text_rendered
292
292
293 def get_raw_text(self) -> str:
293 def get_raw_text(self) -> str:
294 return self.text
294 return self.text
295
295
296 def get_absolute_id(self) -> str:
296 def get_absolute_id(self) -> str:
297 """
297 """
298 If the post has many threads, shows its main thread OP id in the post
298 If the post has many threads, shows its main thread OP id in the post
299 ID.
299 ID.
300 """
300 """
301
301
302 if self.get_threads().count() > 1:
302 if self.get_threads().count() > 1:
303 return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id)
303 return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id)
304 else:
304 else:
305 return str(self.id)
305 return str(self.id)
306
306
307 def connect_notifications(self):
307 def connect_notifications(self):
308 for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()):
308 for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()):
309 user_name = reply_number.group(1).lower()
309 user_name = reply_number.group(1).lower()
310 Notification.objects.get_or_create(name=user_name, post=self)
310 Notification.objects.get_or_create(name=user_name, post=self)
311
311
312 def connect_replies(self):
312 def connect_replies(self):
313 """
313 """
314 Connects replies to a post to show them as a reflink map
314 Connects replies to a post to show them as a reflink map
315 """
315 """
316
316
317 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
317 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
318 post_id = reply_number.group(1)
318 post_id = reply_number.group(1)
319
319
320 try:
320 try:
321 referenced_post = Post.objects.get(id=post_id)
321 referenced_post = Post.objects.get(id=post_id)
322
322
323 referenced_post.referenced_posts.add(self)
323 referenced_post.referenced_posts.add(self)
324 referenced_post.last_edit_time = self.pub_time
324 referenced_post.last_edit_time = self.pub_time
325 referenced_post.build_refmap()
325 referenced_post.build_refmap()
326 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
326 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
327 except ObjectDoesNotExist:
327 except ObjectDoesNotExist:
328 pass
328 pass
329
329
330 def connect_threads(self, opening_posts):
330 def connect_threads(self, opening_posts):
331 for opening_post in opening_posts:
331 for opening_post in opening_posts:
332 threads = opening_post.get_threads().all()
332 threads = opening_post.get_threads().all()
333 for thread in threads:
333 for thread in threads:
334 if thread.can_bump():
334 if thread.can_bump():
335 thread.update_bump_status()
335 thread.update_bump_status()
336
336
337 thread.last_edit_time = self.last_edit_time
337 thread.last_edit_time = self.last_edit_time
338 thread.save(update_fields=['last_edit_time', 'bumpable'])
338 thread.save(update_fields=['last_edit_time', 'status'])
339 self.threads.add(opening_post.get_thread())
339 self.threads.add(opening_post.get_thread())
340
340
341 def get_tripcode(self):
341 def get_tripcode(self):
342 if self.tripcode:
342 if self.tripcode:
343 return Tripcode(self.tripcode)
343 return Tripcode(self.tripcode)
344
344
345 def get_link_view(self):
345 def get_link_view(self):
346 """
346 """
347 Gets view of a reflink to the post.
347 Gets view of a reflink to the post.
348 """
348 """
349 result = '<a href="{}">&gt;&gt;{}</a>'.format(self.get_absolute_url(),
349 result = '<a href="{}">&gt;&gt;{}</a>'.format(self.get_absolute_url(),
350 self.id)
350 self.id)
351 if self.is_opening():
351 if self.is_opening():
352 result = '<b>{}</b>'.format(result)
352 result = '<b>{}</b>'.format(result)
353
353
354 return result
354 return result
355
355
356 def is_hidden(self) -> bool:
356 def is_hidden(self) -> bool:
357 return self.hidden
357 return self.hidden
358
358
359 def set_hidden(self, hidden):
359 def set_hidden(self, hidden):
360 self.hidden = hidden
360 self.hidden = hidden
@@ -1,143 +1,142 b''
1 import hashlib
1 import hashlib
2 from django.template.loader import render_to_string
2 from django.template.loader import render_to_string
3 from django.db import models
3 from django.db import models
4 from django.db.models import Count
4 from django.db.models import Count
5 from django.core.urlresolvers import reverse
5 from django.core.urlresolvers import reverse
6
6
7 from boards.models.base import Viewable
7 from boards.models.base import Viewable
8 from boards.models.thread import STATUS_ACTIVE, STATUS_BUMPLIMIT, STATUS_ARCHIVE
8 from boards.utils import cached_result
9 from boards.utils import cached_result
9 import boards
10 import boards
10
11
11 __author__ = 'neko259'
12 __author__ = 'neko259'
12
13
13
14
14 RELATED_TAGS_COUNT = 5
15 RELATED_TAGS_COUNT = 5
15
16
16
17
17 class TagManager(models.Manager):
18 class TagManager(models.Manager):
18
19
19 def get_not_empty_tags(self):
20 def get_not_empty_tags(self):
20 """
21 """
21 Gets tags that have non-archived threads.
22 Gets tags that have non-archived threads.
22 """
23 """
23
24
24 return self.annotate(num_threads=Count('thread_tags')).filter(num_threads__gt=0)\
25 return self.annotate(num_threads=Count('thread_tags')).filter(num_threads__gt=0)\
25 .order_by('-required', 'name')
26 .order_by('-required', 'name')
26
27
27 def get_tag_url_list(self, tags: list) -> str:
28 def get_tag_url_list(self, tags: list) -> str:
28 """
29 """
29 Gets a comma-separated list of tag links.
30 Gets a comma-separated list of tag links.
30 """
31 """
31
32
32 return ', '.join([tag.get_view() for tag in tags])
33 return ', '.join([tag.get_view() for tag in tags])
33
34
34
35
35 class Tag(models.Model, Viewable):
36 class Tag(models.Model, Viewable):
36 """
37 """
37 A tag is a text node assigned to the thread. The tag serves as a board
38 A tag is a text node assigned to the thread. The tag serves as a board
38 section. There can be multiple tags for each thread
39 section. There can be multiple tags for each thread
39 """
40 """
40
41
41 objects = TagManager()
42 objects = TagManager()
42
43
43 class Meta:
44 class Meta:
44 app_label = 'boards'
45 app_label = 'boards'
45 ordering = ('name',)
46 ordering = ('name',)
46
47
47 name = models.CharField(max_length=100, db_index=True, unique=True)
48 name = models.CharField(max_length=100, db_index=True, unique=True)
48 required = models.BooleanField(default=False, db_index=True)
49 required = models.BooleanField(default=False, db_index=True)
49 description = models.TextField(blank=True)
50 description = models.TextField(blank=True)
50
51
51 parent = models.ForeignKey('Tag', null=True, blank=True,
52 parent = models.ForeignKey('Tag', null=True, blank=True,
52 related_name='children')
53 related_name='children')
53
54
54 def __str__(self):
55 def __str__(self):
55 return self.name
56 return self.name
56
57
57 def is_empty(self) -> bool:
58 def is_empty(self) -> bool:
58 """
59 """
59 Checks if the tag has some threads.
60 Checks if the tag has some threads.
60 """
61 """
61
62
62 return self.get_thread_count() == 0
63 return self.get_thread_count() == 0
63
64
64 def get_thread_count(self, archived=None, bumpable=None) -> int:
65 def get_thread_count(self, status=None) -> int:
65 threads = self.get_threads()
66 threads = self.get_threads()
66 if archived is not None:
67 if status is not None:
67 threads = threads.filter(archived=archived)
68 threads = threads.filter(status=status)
68 if bumpable is not None:
69 threads = threads.filter(bumpable=bumpable)
70 return threads.count()
69 return threads.count()
71
70
72 def get_active_thread_count(self) -> int:
71 def get_active_thread_count(self) -> int:
73 return self.get_thread_count(archived=False, bumpable=True)
72 return self.get_thread_count(status=STATUS_ACTIVE)
74
73
75 def get_bumplimit_thread_count(self) -> int:
74 def get_bumplimit_thread_count(self) -> int:
76 return self.get_thread_count(archived=False, bumpable=False)
75 return self.get_thread_count(status=STATUS_BUMPLIMIT)
77
76
78 def get_archived_thread_count(self) -> int:
77 def get_archived_thread_count(self) -> int:
79 return self.get_thread_count(archived=True)
78 return self.get_thread_count(status=STATUS_ARCHIVE)
80
79
81 def get_absolute_url(self):
80 def get_absolute_url(self):
82 return reverse('tag', kwargs={'tag_name': self.name})
81 return reverse('tag', kwargs={'tag_name': self.name})
83
82
84 def get_threads(self):
83 def get_threads(self):
85 return self.thread_tags.order_by('-bump_time')
84 return self.thread_tags.order_by('-bump_time')
86
85
87 def is_required(self):
86 def is_required(self):
88 return self.required
87 return self.required
89
88
90 def get_view(self):
89 def get_view(self):
91 link = '<a class="tag" href="{}">{}</a>'.format(
90 link = '<a class="tag" href="{}">{}</a>'.format(
92 self.get_absolute_url(), self.name)
91 self.get_absolute_url(), self.name)
93 if self.is_required():
92 if self.is_required():
94 link = '<b>{}</b>'.format(link)
93 link = '<b>{}</b>'.format(link)
95 return link
94 return link
96
95
97 def get_search_view(self, *args, **kwargs):
96 def get_search_view(self, *args, **kwargs):
98 return render_to_string('boards/tag.html', {
97 return render_to_string('boards/tag.html', {
99 'tag': self,
98 'tag': self,
100 })
99 })
101
100
102 @cached_result()
101 @cached_result()
103 def get_post_count(self):
102 def get_post_count(self):
104 return self.get_threads().aggregate(num_posts=Count('multi_replies'))['num_posts']
103 return self.get_threads().aggregate(num_posts=Count('multi_replies'))['num_posts']
105
104
106 def get_description(self):
105 def get_description(self):
107 return self.description
106 return self.description
108
107
109 def get_random_image_post(self, archived=False):
108 def get_random_image_post(self, status=False):
110 posts = boards.models.Post.objects.annotate(images_count=Count(
109 posts = boards.models.Post.objects.annotate(images_count=Count(
111 'images')).filter(images_count__gt=0, threads__tags__in=[self])
110 'images')).filter(images_count__gt=0, threads__tags__in=[self])
112 if archived is not None:
111 if status is not None:
113 posts = posts.filter(thread__archived=archived)
112 posts = posts.filter(thread__status=status)
114 return posts.order_by('?').first()
113 return posts.order_by('?').first()
115
114
116 def get_first_letter(self):
115 def get_first_letter(self):
117 return self.name and self.name[0] or ''
116 return self.name and self.name[0] or ''
118
117
119 def get_related_tags(self):
118 def get_related_tags(self):
120 return set(Tag.objects.filter(thread_tags__in=self.get_threads()).exclude(
119 return set(Tag.objects.filter(thread_tags__in=self.get_threads()).exclude(
121 id=self.id).order_by('?')[:RELATED_TAGS_COUNT])
120 id=self.id).order_by('?')[:RELATED_TAGS_COUNT])
122
121
123 @cached_result()
122 @cached_result()
124 def get_color(self):
123 def get_color(self):
125 """
124 """
126 Gets color hashed from the tag name.
125 Gets color hashed from the tag name.
127 """
126 """
128 return hashlib.md5(self.name.encode()).hexdigest()[:6]
127 return hashlib.md5(self.name.encode()).hexdigest()[:6]
129
128
130 def get_parent(self):
129 def get_parent(self):
131 return self.parent
130 return self.parent
132
131
133 def get_all_parents(self):
132 def get_all_parents(self):
134 parents = list()
133 parents = list()
135 parent = self.get_parent()
134 parent = self.get_parent()
136 if parent and parent not in parents:
135 if parent and parent not in parents:
137 parents.insert(0, parent)
136 parents.insert(0, parent)
138 parents = parent.get_all_parents() + parents
137 parents = parent.get_all_parents() + parents
139
138
140 return parents
139 return parents
141
140
142 def get_children(self):
141 def get_children(self):
143 return self.children
142 return self.children
@@ -1,258 +1,263 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, QuerySet, Q
4 from django.db.models import Count, Sum, QuerySet, Q
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 STATUS_ACTIVE = 'active'
9 STATUS_BUMPLIMIT = 'bumplimit'
10 STATUS_ARCHIVE = 'archived'
11
8 from boards import settings
12 from boards import settings
9 import boards
13 import boards
10 from boards.utils import cached_result, datetime_to_epoch
14 from boards.utils import cached_result, datetime_to_epoch
11 from boards.models.post import Post
15 from boards.models.post import Post
12 from boards.models.tag import Tag
16 from boards.models.tag import Tag
13
17
14 FAV_THREAD_NO_UPDATES = -1
18 FAV_THREAD_NO_UPDATES = -1
15
19
16
20
17 __author__ = 'neko259'
21 __author__ = 'neko259'
18
22
19
23
20 logger = logging.getLogger(__name__)
24 logger = logging.getLogger(__name__)
21
25
22
26
23 WS_NOTIFICATION_TYPE_NEW_POST = 'new_post'
27 WS_NOTIFICATION_TYPE_NEW_POST = 'new_post'
24 WS_NOTIFICATION_TYPE = 'notification_type'
28 WS_NOTIFICATION_TYPE = 'notification_type'
25
29
26 WS_CHANNEL_THREAD = "thread:"
30 WS_CHANNEL_THREAD = "thread:"
27
31
28
32
29 class ThreadManager(models.Manager):
33 class ThreadManager(models.Manager):
30 def process_oldest_threads(self):
34 def process_oldest_threads(self):
31 """
35 """
32 Preserves maximum thread count. If there are too many threads,
36 Preserves maximum thread count. If there are too many threads,
33 archive or delete the old ones.
37 archive or delete the old ones.
34 """
38 """
35
39
36 threads = Thread.objects.filter(archived=False).order_by('-bump_time')
40 threads = Thread.objects.exclude(status=STATUS_ARCHIVE).order_by('-bump_time')
37 thread_count = threads.count()
41 thread_count = threads.count()
38
42
39 max_thread_count = settings.get_int('Messages', 'MaxThreadCount')
43 max_thread_count = settings.get_int('Messages', 'MaxThreadCount')
40 if thread_count > max_thread_count:
44 if thread_count > max_thread_count:
41 num_threads_to_delete = thread_count - max_thread_count
45 num_threads_to_delete = thread_count - max_thread_count
42 old_threads = threads[thread_count - num_threads_to_delete:]
46 old_threads = threads[thread_count - num_threads_to_delete:]
43
47
44 for thread in old_threads:
48 for thread in old_threads:
45 if settings.get_bool('Storage', 'ArchiveThreads'):
49 if settings.get_bool('Storage', 'ArchiveThreads'):
46 self._archive_thread(thread)
50 self._archive_thread(thread)
47 else:
51 else:
48 thread.delete()
52 thread.delete()
49
53
50 logger.info('Processed %d old threads' % num_threads_to_delete)
54 logger.info('Processed %d old threads' % num_threads_to_delete)
51
55
52 def _archive_thread(self, thread):
56 def _archive_thread(self, thread):
53 thread.archived = True
57 thread.status = STATUS_ARCHIVE
54 thread.bumpable = False
55 thread.last_edit_time = timezone.now()
58 thread.last_edit_time = timezone.now()
56 thread.update_posts_time()
59 thread.update_posts_time()
57 thread.save(update_fields=['archived', 'last_edit_time', 'bumpable'])
60 thread.save(update_fields=['last_edit_time', 'status'])
58
61
59 def get_new_posts(self, datas):
62 def get_new_posts(self, datas):
60 query = None
63 query = None
61 # TODO Use classes instead of dicts
64 # TODO Use classes instead of dicts
62 for data in datas:
65 for data in datas:
63 if data['last_id'] != FAV_THREAD_NO_UPDATES:
66 if data['last_id'] != FAV_THREAD_NO_UPDATES:
64 q = (Q(id=data['op'].get_thread().id)
67 q = (Q(id=data['op'].get_thread().id)
65 & Q(multi_replies__id__gt=data['last_id']))
68 & Q(multi_replies__id__gt=data['last_id']))
66 if query is None:
69 if query is None:
67 query = q
70 query = q
68 else:
71 else:
69 query = query | q
72 query = query | q
70 if query is not None:
73 if query is not None:
71 return self.filter(query).annotate(
74 return self.filter(query).annotate(
72 new_post_count=Count('multi_replies'))
75 new_post_count=Count('multi_replies'))
73
76
74 def get_new_post_count(self, datas):
77 def get_new_post_count(self, datas):
75 new_posts = self.get_new_posts(datas)
78 new_posts = self.get_new_posts(datas)
76 return new_posts.aggregate(total_count=Count('multi_replies'))\
79 return new_posts.aggregate(total_count=Count('multi_replies'))\
77 ['total_count'] if new_posts else 0
80 ['total_count'] if new_posts else 0
78
81
79
82
80 def get_thread_max_posts():
83 def get_thread_max_posts():
81 return settings.get_int('Messages', 'MaxPostsPerThread')
84 return settings.get_int('Messages', 'MaxPostsPerThread')
82
85
83
86
84 class Thread(models.Model):
87 class Thread(models.Model):
85 objects = ThreadManager()
88 objects = ThreadManager()
86
89
87 class Meta:
90 class Meta:
88 app_label = 'boards'
91 app_label = 'boards'
89
92
90 tags = models.ManyToManyField('Tag', related_name='thread_tags')
93 tags = models.ManyToManyField('Tag', related_name='thread_tags')
91 bump_time = models.DateTimeField(db_index=True)
94 bump_time = models.DateTimeField(db_index=True)
92 last_edit_time = models.DateTimeField()
95 last_edit_time = models.DateTimeField()
93 archived = models.BooleanField(default=False)
94 bumpable = models.BooleanField(default=True)
95 max_posts = models.IntegerField(default=get_thread_max_posts)
96 max_posts = models.IntegerField(default=get_thread_max_posts)
97 status = models.CharField(max_length=50, default=STATUS_ACTIVE)
96
98
97 def get_tags(self) -> QuerySet:
99 def get_tags(self) -> QuerySet:
98 """
100 """
99 Gets a sorted tag list.
101 Gets a sorted tag list.
100 """
102 """
101
103
102 return self.tags.order_by('name')
104 return self.tags.order_by('name')
103
105
104 def bump(self):
106 def bump(self):
105 """
107 """
106 Bumps (moves to up) thread if possible.
108 Bumps (moves to up) thread if possible.
107 """
109 """
108
110
109 if self.can_bump():
111 if self.can_bump():
110 self.bump_time = self.last_edit_time
112 self.bump_time = self.last_edit_time
111
113
112 self.update_bump_status()
114 self.update_bump_status()
113
115
114 logger.info('Bumped thread %d' % self.id)
116 logger.info('Bumped thread %d' % self.id)
115
117
116 def has_post_limit(self) -> bool:
118 def has_post_limit(self) -> bool:
117 return self.max_posts > 0
119 return self.max_posts > 0
118
120
119 def update_bump_status(self, exclude_posts=None):
121 def update_bump_status(self, exclude_posts=None):
120 if self.has_post_limit() and self.get_reply_count() >= self.max_posts:
122 if self.has_post_limit() and self.get_reply_count() >= self.max_posts:
121 self.bumpable = False
123 self.status = STATUS_BUMPLIMIT
122 self.update_posts_time(exclude_posts=exclude_posts)
124 self.update_posts_time(exclude_posts=exclude_posts)
123
125
124 def _get_cache_key(self):
126 def _get_cache_key(self):
125 return [datetime_to_epoch(self.last_edit_time)]
127 return [datetime_to_epoch(self.last_edit_time)]
126
128
127 @cached_result(key_method=_get_cache_key)
129 @cached_result(key_method=_get_cache_key)
128 def get_reply_count(self) -> int:
130 def get_reply_count(self) -> int:
129 return self.get_replies().count()
131 return self.get_replies().count()
130
132
131 @cached_result(key_method=_get_cache_key)
133 @cached_result(key_method=_get_cache_key)
132 def get_images_count(self) -> int:
134 def get_images_count(self) -> int:
133 return self.get_replies().annotate(images_count=Count(
135 return self.get_replies().annotate(images_count=Count(
134 'images')).aggregate(Sum('images_count'))['images_count__sum']
136 'images')).aggregate(Sum('images_count'))['images_count__sum']
135
137
136 def can_bump(self) -> bool:
138 def can_bump(self) -> bool:
137 """
139 """
138 Checks if the thread can be bumped by replying to it.
140 Checks if the thread can be bumped by replying to it.
139 """
141 """
140
142
141 return self.bumpable and not self.is_archived()
143 return self.get_status() == STATUS_ACTIVE
142
144
143 def get_last_replies(self) -> QuerySet:
145 def get_last_replies(self) -> QuerySet:
144 """
146 """
145 Gets several last replies, not including opening post
147 Gets several last replies, not including opening post
146 """
148 """
147
149
148 last_replies_count = settings.get_int('View', 'LastRepliesCount')
150 last_replies_count = settings.get_int('View', 'LastRepliesCount')
149
151
150 if last_replies_count > 0:
152 if last_replies_count > 0:
151 reply_count = self.get_reply_count()
153 reply_count = self.get_reply_count()
152
154
153 if reply_count > 0:
155 if reply_count > 0:
154 reply_count_to_show = min(last_replies_count,
156 reply_count_to_show = min(last_replies_count,
155 reply_count - 1)
157 reply_count - 1)
156 replies = self.get_replies()
158 replies = self.get_replies()
157 last_replies = replies[reply_count - reply_count_to_show:]
159 last_replies = replies[reply_count - reply_count_to_show:]
158
160
159 return last_replies
161 return last_replies
160
162
161 def get_skipped_replies_count(self) -> int:
163 def get_skipped_replies_count(self) -> int:
162 """
164 """
163 Gets number of posts between opening post and last replies.
165 Gets number of posts between opening post and last replies.
164 """
166 """
165 reply_count = self.get_reply_count()
167 reply_count = self.get_reply_count()
166 last_replies_count = min(settings.get_int('View', 'LastRepliesCount'),
168 last_replies_count = min(settings.get_int('View', 'LastRepliesCount'),
167 reply_count - 1)
169 reply_count - 1)
168 return reply_count - last_replies_count - 1
170 return reply_count - last_replies_count - 1
169
171
170 def get_replies(self, view_fields_only=False) -> QuerySet:
172 def get_replies(self, view_fields_only=False) -> QuerySet:
171 """
173 """
172 Gets sorted thread posts
174 Gets sorted thread posts
173 """
175 """
174
176
175 query = self.multi_replies.order_by('pub_time').prefetch_related(
177 query = self.multi_replies.order_by('pub_time').prefetch_related(
176 'images', 'thread', 'threads', 'attachments')
178 'images', 'thread', 'threads', 'attachments')
177 if view_fields_only:
179 if view_fields_only:
178 query = query.defer('poster_ip')
180 query = query.defer('poster_ip')
179 return query.all()
181 return query.all()
180
182
181 def get_top_level_replies(self) -> QuerySet:
183 def get_top_level_replies(self) -> QuerySet:
182 return self.get_replies().exclude(refposts__threads__in=[self])
184 return self.get_replies().exclude(refposts__threads__in=[self])
183
185
184 def get_replies_with_images(self, view_fields_only=False) -> QuerySet:
186 def get_replies_with_images(self, view_fields_only=False) -> QuerySet:
185 """
187 """
186 Gets replies that have at least one image attached
188 Gets replies that have at least one image attached
187 """
189 """
188
190
189 return self.get_replies(view_fields_only).annotate(images_count=Count(
191 return self.get_replies(view_fields_only).annotate(images_count=Count(
190 'images')).filter(images_count__gt=0)
192 'images')).filter(images_count__gt=0)
191
193
192 def get_opening_post(self, only_id=False) -> Post:
194 def get_opening_post(self, only_id=False) -> Post:
193 """
195 """
194 Gets the first post of the thread
196 Gets the first post of the thread
195 """
197 """
196
198
197 query = self.get_replies().filter(opening=True)
199 query = self.get_replies().filter(opening=True)
198 if only_id:
200 if only_id:
199 query = query.only('id')
201 query = query.only('id')
200 opening_post = query.first()
202 opening_post = query.first()
201
203
202 return opening_post
204 return opening_post
203
205
204 @cached_result()
206 @cached_result()
205 def get_opening_post_id(self) -> int:
207 def get_opening_post_id(self) -> int:
206 """
208 """
207 Gets ID of the first thread post.
209 Gets ID of the first thread post.
208 """
210 """
209
211
210 return self.get_opening_post(only_id=True).id
212 return self.get_opening_post(only_id=True).id
211
213
212 def get_pub_time(self):
214 def get_pub_time(self):
213 """
215 """
214 Gets opening post's pub time because thread does not have its own one.
216 Gets opening post's pub time because thread does not have its own one.
215 """
217 """
216
218
217 return self.get_opening_post().pub_time
219 return self.get_opening_post().pub_time
218
220
219 def __str__(self):
221 def __str__(self):
220 return 'T#{}/{}'.format(self.id, self.get_opening_post_id())
222 return 'T#{}/{}'.format(self.id, self.get_opening_post_id())
221
223
222 def get_tag_url_list(self) -> list:
224 def get_tag_url_list(self) -> list:
223 return boards.models.Tag.objects.get_tag_url_list(self.get_tags())
225 return boards.models.Tag.objects.get_tag_url_list(self.get_tags())
224
226
225 def update_posts_time(self, exclude_posts=None):
227 def update_posts_time(self, exclude_posts=None):
226 last_edit_time = self.last_edit_time
228 last_edit_time = self.last_edit_time
227
229
228 for post in self.multi_replies.all():
230 for post in self.multi_replies.all():
229 if exclude_posts is None or post not in exclude_posts:
231 if exclude_posts is None or post not in exclude_posts:
230 # Manual update is required because uids are generated on save
232 # Manual update is required because uids are generated on save
231 post.last_edit_time = last_edit_time
233 post.last_edit_time = last_edit_time
232 post.save(update_fields=['last_edit_time'])
234 post.save(update_fields=['last_edit_time'])
233
235
234 post.get_threads().update(last_edit_time=last_edit_time)
236 post.get_threads().update(last_edit_time=last_edit_time)
235
237
236 def notify_clients(self):
238 def notify_clients(self):
237 if not settings.get_bool('External', 'WebsocketsEnabled'):
239 if not settings.get_bool('External', 'WebsocketsEnabled'):
238 return
240 return
239
241
240 client = Client()
242 client = Client()
241
243
242 channel_name = WS_CHANNEL_THREAD + str(self.get_opening_post_id())
244 channel_name = WS_CHANNEL_THREAD + str(self.get_opening_post_id())
243 client.publish(channel_name, {
245 client.publish(channel_name, {
244 WS_NOTIFICATION_TYPE: WS_NOTIFICATION_TYPE_NEW_POST,
246 WS_NOTIFICATION_TYPE: WS_NOTIFICATION_TYPE_NEW_POST,
245 })
247 })
246 client.send()
248 client.send()
247
249
248 def get_absolute_url(self):
250 def get_absolute_url(self):
249 return self.get_opening_post().get_absolute_url()
251 return self.get_opening_post().get_absolute_url()
250
252
251 def get_required_tags(self):
253 def get_required_tags(self):
252 return self.get_tags().filter(required=True)
254 return self.get_tags().filter(required=True)
253
255
254 def get_replies_newer(self, post_id):
256 def get_replies_newer(self, post_id):
255 return self.get_replies().filter(id__gt=post_id)
257 return self.get_replies().filter(id__gt=post_id)
256
258
257 def is_archived(self):
259 def is_archived(self):
258 return self.archived
260 return self.get_status() == STATUS_ARCHIVE
261
262 def get_status(self):
263 return self.status
@@ -1,83 +1,84 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 from boards.models.thread import STATUS_ARCHIVE
6
7
7 __author__ = 'nekorin'
8 __author__ = 'nekorin'
8
9
9
10
10 MAX_ITEMS = settings.get_int('RSS', 'MaxItems')
11 MAX_ITEMS = settings.get_int('RSS', 'MaxItems')
11
12
12
13
13 # TODO Make tests for all of these
14 # TODO Make tests for all of these
14 class AllThreadsFeed(Feed):
15 class AllThreadsFeed(Feed):
15
16
16 title = settings.get('Version', 'SiteName') + ' - All threads'
17 title = settings.get('Version', 'SiteName') + ' - All threads'
17 link = '/'
18 link = '/'
18 description_template = 'boards/rss/post.html'
19 description_template = 'boards/rss/post.html'
19
20
20 def items(self):
21 def items(self):
21 return Thread.objects.filter(archived=False).order_by('-id')[:MAX_ITEMS]
22 return Thread.objects.exclude(status=STATUS_ARCHIVE).order_by('-id')[:MAX_ITEMS]
22
23
23 def item_title(self, item):
24 def item_title(self, item):
24 return item.get_opening_post().title
25 return item.get_opening_post().title
25
26
26 def item_link(self, item):
27 def item_link(self, item):
27 return reverse('thread', args={item.get_opening_post_id()})
28 return reverse('thread', args={item.get_opening_post_id()})
28
29
29 def item_pubdate(self, item):
30 def item_pubdate(self, item):
30 return item.get_pub_time()
31 return item.get_pub_time()
31
32
32
33
33 class TagThreadsFeed(Feed):
34 class TagThreadsFeed(Feed):
34
35
35 link = '/'
36 link = '/'
36 description_template = 'boards/rss/post.html'
37 description_template = 'boards/rss/post.html'
37
38
38 def items(self, obj):
39 def items(self, obj):
39 return obj.get_threads().filter(archived=False).order_by('-id')[:MAX_ITEMS]
40 return obj.get_threads().exclude(status=STATUS_ARCHIVE).order_by('-id')[:MAX_ITEMS]
40
41
41 def get_object(self, request, tag_name):
42 def get_object(self, request, tag_name):
42 return get_object_or_404(Tag, name=tag_name)
43 return get_object_or_404(Tag, name=tag_name)
43
44
44 def item_title(self, item):
45 def item_title(self, item):
45 return item.get_opening_post().title
46 return item.get_opening_post().title
46
47
47 def item_link(self, item):
48 def item_link(self, item):
48 return reverse('thread', args={item.get_opening_post_id()})
49 return reverse('thread', args={item.get_opening_post_id()})
49
50
50 def item_pubdate(self, item):
51 def item_pubdate(self, item):
51 return item.get_pub_time()
52 return item.get_pub_time()
52
53
53 def title(self, obj):
54 def title(self, obj):
54 return obj.name
55 return obj.name
55
56
56
57
57 class ThreadPostsFeed(Feed):
58 class ThreadPostsFeed(Feed):
58
59
59 link = '/'
60 link = '/'
60 description_template = 'boards/rss/post.html'
61 description_template = 'boards/rss/post.html'
61
62
62 def items(self, obj):
63 def items(self, obj):
63 return obj.get_thread().get_replies().order_by('-pub_time')[:MAX_ITEMS]
64 return obj.get_thread().get_replies().order_by('-pub_time')[:MAX_ITEMS]
64
65
65 def get_object(self, request, post_id):
66 def get_object(self, request, post_id):
66 return get_object_or_404(Post, id=post_id)
67 return get_object_or_404(Post, id=post_id)
67
68
68 def item_title(self, item):
69 def item_title(self, item):
69 return item.title
70 return item.title
70
71
71 def item_link(self, item):
72 def item_link(self, item):
72 if not item.is_opening():
73 if not item.is_opening():
73 return reverse('thread', args={
74 return reverse('thread', args={
74 item.get_thread().get_opening_post_id()
75 item.get_thread().get_opening_post_id()
75 }) + "#" + str(item.id)
76 }) + "#" + str(item.id)
76 else:
77 else:
77 return reverse('thread', args={item.id})
78 return reverse('thread', args={item.id})
78
79
79 def item_pubdate(self, item):
80 def item_pubdate(self, item):
80 return item.pub_time
81 return item.pub_time
81
82
82 def title(self, obj):
83 def title(self, obj):
83 return obj.title
84 return obj.title
@@ -1,114 +1,114 b''
1 {% load i18n %}
1 {% load i18n %}
2 {% load board %}
2 {% load board %}
3
3
4 {% get_current_language as LANGUAGE_CODE %}
4 {% get_current_language as LANGUAGE_CODE %}
5
5
6 <div class="{{ css_class }}" id="{{ post.id }}" data-uid="{{ post.uid }}">
6 <div class="{{ css_class }}" id="{{ post.id }}" data-uid="{{ post.uid }}">
7 <div class="post-info">
7 <div class="post-info">
8 <a class="post_id" href="{{ post.get_absolute_url }}">#{{ post.get_absolute_id }}</a>
8 <a class="post_id" href="{{ post.get_absolute_url }}">#{{ post.get_absolute_id }}</a>
9 <span class="title">{{ post.title }}</span>
9 <span class="title">{{ post.title }}</span>
10 <span class="pub_time"><time datetime="{{ post.pub_time|date:'c' }}">{{ post.pub_time }}</time></span>
10 <span class="pub_time"><time datetime="{{ post.pub_time|date:'c' }}">{{ post.pub_time }}</time></span>
11 {% if post.tripcode %}
11 {% if post.tripcode %}
12 /
12 /
13 {% with tripcode=post.get_tripcode %}
13 {% with tripcode=post.get_tripcode %}
14 <a href="{% url 'feed' %}?tripcode={{ tripcode.get_full_text }}"
14 <a href="{% url 'feed' %}?tripcode={{ tripcode.get_full_text }}"
15 class="tripcode" title="{{ tripcode.get_full_text }}"
15 class="tripcode" title="{{ tripcode.get_full_text }}"
16 style="border: solid 2px #{{ tripcode.get_color }}; border-left: solid 1ex #{{ tripcode.get_color }};">{{ tripcode.get_short_text }}</a>
16 style="border: solid 2px #{{ tripcode.get_color }}; border-left: solid 1ex #{{ tripcode.get_color }};">{{ tripcode.get_short_text }}</a>
17 {% endwith %}
17 {% endwith %}
18 {% endif %}
18 {% endif %}
19 {% comment %}
19 {% comment %}
20 Thread death time needs to be shown only if the thread is alredy archived
20 Thread death time needs to be shown only if the thread is alredy archived
21 and this is an opening post (thread death time) or a post for popup
21 and this is an opening post (thread death time) or a post for popup
22 (we don't see OP here so we show the death time in the post itself).
22 (we don't see OP here so we show the death time in the post itself).
23 {% endcomment %}
23 {% endcomment %}
24 {% if thread.archived %}
24 {% if thread.is_archived %}
25 {% if is_opening %}
25 {% if is_opening %}
26 β€” <time datetime="{{ thread.bump_time|date:'c' }}">{{ thread.bump_time }}</time>
26 β€” <time datetime="{{ thread.bump_time|date:'c' }}">{{ thread.bump_time }}</time>
27 {% endif %}
27 {% endif %}
28 {% endif %}
28 {% endif %}
29 {% if is_opening %}
29 {% if is_opening %}
30 {% if need_open_link %}
30 {% if need_open_link %}
31 {% if thread.archived %}
31 {% if thread.is_archived %}
32 <a class="link" href="{% url 'thread' post.id %}">{% trans "Open" %}</a>
32 <a class="link" href="{% url 'thread' post.id %}">{% trans "Open" %}</a>
33 {% else %}
33 {% else %}
34 <a class="link" href="{% url 'thread' post.id %}#form">{% trans "Reply" %}</a>
34 <a class="link" href="{% url 'thread' post.id %}#form">{% trans "Reply" %}</a>
35 {% endif %}
35 {% endif %}
36 {% endif %}
36 {% endif %}
37 {% else %}
37 {% else %}
38 {% if need_op_data %}
38 {% if need_op_data %}
39 {% with thread.get_opening_post as op %}
39 {% with thread.get_opening_post as op %}
40 {% trans " in " %}{{ op.get_link_view|safe }} <span class="title">{{ op.get_title_or_text }}</span>
40 {% trans " in " %}{{ op.get_link_view|safe }} <span class="title">{{ op.get_title_or_text }}</span>
41 {% endwith %}
41 {% endwith %}
42 {% endif %}
42 {% endif %}
43 {% endif %}
43 {% endif %}
44 {% if reply_link and not thread.archived %}
44 {% if reply_link and not thread.is_archived %}
45 <a href="#form" onclick="addQuickReply('{{ post.id }}'); return false;">{% trans 'Reply' %}</a>
45 <a href="#form" onclick="addQuickReply('{{ post.id }}'); return false;">{% trans 'Reply' %}</a>
46 {% endif %}
46 {% endif %}
47
47
48 {% if perms.boards.change_post or perms.boards.delete_post or perms.boards.change_thread or perms_boards.delete_thread %}
48 {% if perms.boards.change_post or perms.boards.delete_post or perms.boards.change_thread or perms_boards.delete_thread %}
49 <span class="moderator_info">
49 <span class="moderator_info">
50 {% if perms.boards.change_post or perms.boards.delete_post %}
50 {% if perms.boards.change_post or perms.boards.delete_post %}
51 | <a href="{% url 'admin:boards_post_change' post.id %}">{% trans 'Edit' %}</a>
51 | <a href="{% url 'admin:boards_post_change' post.id %}">{% trans 'Edit' %}</a>
52 <form action="{% url 'thread' thread.get_opening_post_id %}?post_id={{ post.id }}" method="post" class="post-button-form">
52 <form action="{% url 'thread' thread.get_opening_post_id %}?post_id={{ post.id }}" method="post" class="post-button-form">
53 | <button name="method" value="toggle_hide_post">H</button>
53 | <button name="method" value="toggle_hide_post">H</button>
54 </form>
54 </form>
55 {% endif %}
55 {% endif %}
56 {% if perms.boards.change_thread or perms_boards.delete_thread %}
56 {% if perms.boards.change_thread or perms_boards.delete_thread %}
57 {% if is_opening %}
57 {% if is_opening %}
58 | <a href="{% url 'admin:boards_thread_change' thread.id %}">{% trans 'Edit thread' %}</a>
58 | <a href="{% url 'admin:boards_thread_change' thread.id %}">{% trans 'Edit thread' %}</a>
59 {% endif %}
59 {% endif %}
60 {% endif %}
60 {% endif %}
61 </form>
61 </form>
62 </span>
62 </span>
63 {% endif %}
63 {% endif %}
64 </div>
64 </div>
65 {% comment %}
65 {% comment %}
66 Post images. Currently only 1 image can be posted and shown, but post model
66 Post images. Currently only 1 image can be posted and shown, but post model
67 supports multiple.
67 supports multiple.
68 {% endcomment %}
68 {% endcomment %}
69 {% for image in post.images.all %}
69 {% for image in post.images.all %}
70 {{ image.get_view|safe }}
70 {{ image.get_view|safe }}
71 {% endfor %}
71 {% endfor %}
72 {% for file in post.attachments.all %}
72 {% for file in post.attachments.all %}
73 {{ file.get_view|safe }}
73 {{ file.get_view|safe }}
74 {% endfor %}
74 {% endfor %}
75 {% comment %}
75 {% comment %}
76 Post message (text)
76 Post message (text)
77 {% endcomment %}
77 {% endcomment %}
78 <div class="message">
78 <div class="message">
79 {% autoescape off %}
79 {% autoescape off %}
80 {% if truncated %}
80 {% if truncated %}
81 {{ post.get_text|truncatewords_html:50 }}
81 {{ post.get_text|truncatewords_html:50 }}
82 {% else %}
82 {% else %}
83 {{ post.get_text }}
83 {{ post.get_text }}
84 {% endif %}
84 {% endif %}
85 {% endautoescape %}
85 {% endautoescape %}
86 </div>
86 </div>
87 {% if post.is_referenced %}
87 {% if post.is_referenced %}
88 {% if mode_tree %}
88 {% if mode_tree %}
89 <div class="tree_reply">
89 <div class="tree_reply">
90 {% for refpost in post.get_referenced_posts %}
90 {% for refpost in post.get_referenced_posts %}
91 {% post_view refpost mode_tree=True %}
91 {% post_view refpost mode_tree=True %}
92 {% endfor %}
92 {% endfor %}
93 </div>
93 </div>
94 {% else %}
94 {% else %}
95 <div class="refmap">
95 <div class="refmap">
96 {% trans "Replies" %}: {{ post.refmap|safe }}
96 {% trans "Replies" %}: {{ post.refmap|safe }}
97 </div>
97 </div>
98 {% endif %}
98 {% endif %}
99 {% endif %}
99 {% endif %}
100 {% comment %}
100 {% comment %}
101 Thread metadata: counters, tags etc
101 Thread metadata: counters, tags etc
102 {% endcomment %}
102 {% endcomment %}
103 {% if is_opening %}
103 {% if is_opening %}
104 <div class="metadata">
104 <div class="metadata">
105 {% if is_opening and need_open_link %}
105 {% if is_opening and need_open_link %}
106 {% blocktrans count count=thread.get_reply_count %}{{ count }} message{% plural %}{{ count }} messages{% endblocktrans %},
106 {% blocktrans count count=thread.get_reply_count %}{{ count }} message{% plural %}{{ count }} messages{% endblocktrans %},
107 {% blocktrans count count=thread.get_images_count %}{{ count }} image{% plural %}{{ count }} images{% endblocktrans %}.
107 {% blocktrans count count=thread.get_images_count %}{{ count }} image{% plural %}{{ count }} images{% endblocktrans %}.
108 {% endif %}
108 {% endif %}
109 <span class="tags">
109 <span class="tags">
110 {{ thread.get_tag_url_list|safe }}
110 {{ thread.get_tag_url_list|safe }}
111 </span>
111 </span>
112 </div>
112 </div>
113 {% endif %}
113 {% endif %}
114 </div>
114 </div>
@@ -1,75 +1,75 b''
1 {% extends "boards/thread.html" %}
1 {% extends "boards/thread.html" %}
2
2
3 {% load i18n %}
3 {% load i18n %}
4 {% load static from staticfiles %}
4 {% load static from staticfiles %}
5 {% load board %}
5 {% load board %}
6 {% load tz %}
6 {% load tz %}
7
7
8 {% block thread_content %}
8 {% block thread_content %}
9 {% get_current_language as LANGUAGE_CODE %}
9 {% get_current_language as LANGUAGE_CODE %}
10 {% get_current_timezone as TIME_ZONE %}
10 {% get_current_timezone as TIME_ZONE %}
11
11
12 <div class="tag_info">
12 <div class="tag_info">
13 <h2>
13 <h2>
14 <form action="{% url 'thread' opening_post.id %}" method="post" class="post-button-form">
14 <form action="{% url 'thread' opening_post.id %}" method="post" class="post-button-form">
15 {% if is_favorite %}
15 {% if is_favorite %}
16 <button name="method" value="unsubscribe" class="fav">β˜…</button>
16 <button name="method" value="unsubscribe" class="fav">β˜…</button>
17 {% else %}
17 {% else %}
18 <button name="method" value="subscribe" class="not_fav">β˜…</button>
18 <button name="method" value="subscribe" class="not_fav">β˜…</button>
19 {% endif %}
19 {% endif %}
20 </form>
20 </form>
21 {{ opening_post.get_title_or_text }}
21 {{ opening_post.get_title_or_text }}
22 </h2>
22 </h2>
23 </div>
23 </div>
24
24
25 {% if bumpable and thread.has_post_limit %}
25 {% if bumpable and thread.has_post_limit %}
26 <div class="bar-bg">
26 <div class="bar-bg">
27 <div class="bar-value" style="width:{{ bumplimit_progress }}%" id="bumplimit_progress">
27 <div class="bar-value" style="width:{{ bumplimit_progress }}%" id="bumplimit_progress">
28 </div>
28 </div>
29 <div class="bar-text">
29 <div class="bar-text">
30 <span id="left_to_limit">{{ posts_left }}</span> {% trans 'posts to bumplimit' %}
30 <span id="left_to_limit">{{ posts_left }}</span> {% trans 'posts to bumplimit' %}
31 </div>
31 </div>
32 </div>
32 </div>
33 {% endif %}
33 {% endif %}
34
34
35 <div class="thread">
35 <div class="thread">
36 {% for post in thread.get_replies %}
36 {% for post in thread.get_replies %}
37 {% post_view post reply_link=True %}
37 {% post_view post reply_link=True %}
38 {% endfor %}
38 {% endfor %}
39 </div>
39 </div>
40
40
41 {% if not thread.archived %}
41 {% if not thread.is_archived %}
42 <div class="post-form-w">
42 <div class="post-form-w">
43 <script src="{% static 'js/panel.js' %}"></script>
43 <script src="{% static 'js/panel.js' %}"></script>
44 <div class="form-title">{% trans "Reply to thread" %} #{{ opening_post.id }}<span class="reply-to-message"> {% trans "to message " %} #<span id="reply-to-message-id"></span></span></div>
44 <div class="form-title">{% trans "Reply to thread" %} #{{ opening_post.id }}<span class="reply-to-message"> {% trans "to message " %} #<span id="reply-to-message-id"></span></span></div>
45 <div class="post-form" id="compact-form">
45 <div class="post-form" id="compact-form">
46 <div class="swappable-form-full">
46 <div class="swappable-form-full">
47 <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %}
47 <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %}
48 <div class="compact-form-text"></div>
48 <div class="compact-form-text"></div>
49 {{ form.as_div }}
49 {{ form.as_div }}
50 <div class="form-submit">
50 <div class="form-submit">
51 <input type="submit" value="{% trans "Post" %}"/>
51 <input type="submit" value="{% trans "Post" %}"/>
52 <button id="preview-button" onclick="return false;">{% trans 'Preview' %}</button>
52 <button id="preview-button" onclick="return false;">{% trans 'Preview' %}</button>
53 </div>
53 </div>
54 </form>
54 </form>
55 </div>
55 </div>
56 <div id="preview-text"></div>
56 <div id="preview-text"></div>
57 <div>
57 <div>
58 {% with size=max_file_size|filesizeformat %}
58 {% with size=max_file_size|filesizeformat %}
59 {% blocktrans %}Max file size is {{ size }}.{% endblocktrans %}
59 {% blocktrans %}Max file size is {{ size }}.{% endblocktrans %}
60 {% endwith %}
60 {% endwith %}
61 </div>
61 </div>
62 <div><a href="{% url "staticpage" name="help" %}">
62 <div><a href="{% url "staticpage" name="help" %}">
63 {% trans 'Text syntax' %}</a></div>
63 {% trans 'Text syntax' %}</a></div>
64 <div><a id="form-close-button" href="#" onClick="resetFormPosition(); return false;">{% trans 'Close form' %}</a></div>
64 <div><a id="form-close-button" href="#" onClick="resetFormPosition(); return false;">{% trans 'Close form' %}</a></div>
65 </div>
65 </div>
66 </div>
66 </div>
67
67
68 <script src="{% static 'js/jquery.form.min.js' %}"></script>
68 <script src="{% static 'js/jquery.form.min.js' %}"></script>
69 {% endif %}
69 {% endif %}
70
70
71 <script src="{% static 'js/form.js' %}"></script>
71 <script src="{% static 'js/form.js' %}"></script>
72 <script src="{% static 'js/thread.js' %}"></script>
72 <script src="{% static 'js/thread.js' %}"></script>
73 <script src="{% static 'js/thread_update.js' %}"></script>
73 <script src="{% static 'js/thread_update.js' %}"></script>
74 <script src="{% static 'js/3party/centrifuge.js' %}"></script>
74 <script src="{% static 'js/3party/centrifuge.js' %}"></script>
75 {% endblock %}
75 {% endblock %}
@@ -1,69 +1,70 b''
1 import simplejson
1 import simplejson
2
2
3 from django.test import TestCase
3 from django.test import TestCase
4 from boards.views import api
4 from boards.views import api
5
5
6 from boards.models import Tag, Post
6 from boards.models import Tag, Post
7 from boards.tests.mocks import MockRequest
7 from boards.tests.mocks import MockRequest
8 from boards.utils import datetime_to_epoch
8 from boards.utils import datetime_to_epoch
9
9
10
10
11 class ApiTest(TestCase):
11 class ApiTest(TestCase):
12 def test_thread_diff(self):
12 def test_thread_diff(self):
13 tag = Tag.objects.create(name='test_tag')
13 tag = Tag.objects.create(name='test_tag')
14 opening_post = Post.objects.create_post(title='title', text='text',
14 opening_post = Post.objects.create_post(title='title', text='text',
15 tags=[tag])
15 tags=[tag])
16
16
17 req = MockRequest()
17 req = MockRequest()
18 req.POST['thread'] = opening_post.id
18 req.POST['thread'] = opening_post.id
19 req.POST['uids'] = opening_post.uid
19 req.POST['uids'] = opening_post.uid
20 # Check the exact timestamp post was added
20 # Check the exact timestamp post was added
21 empty_response = api.api_get_threaddiff(req)
21 empty_response = api.api_get_threaddiff(req)
22 diff = simplejson.loads(empty_response.content)
22 diff = simplejson.loads(empty_response.content)
23 self.assertEqual(0, len(diff['updated']),
23 self.assertEqual(0, len(diff['updated']),
24 'There must be no updated posts in the diff.')
24 'There must be no updated posts in the diff.')
25
25
26 uids = [opening_post.uid]
26 uids = [opening_post.uid]
27
27
28 reply = Post.objects.create_post(title='',
28 reply = Post.objects.create_post(title='',
29 text='[post]%d[/post]\ntext' % opening_post.id,
29 text='[post]%d[/post]\ntext' % opening_post.id,
30 thread=opening_post.get_thread())
30 thread=opening_post.get_thread())
31 req = MockRequest()
31 req = MockRequest()
32 req.POST['thread'] = opening_post.id
32 req.POST['thread'] = opening_post.id
33 req.POST['uids'] = ' '.join(uids)
33 req.POST['uids'] = ' '.join(uids)
34 req.user = None
34 # Check the timestamp before post was added
35 # Check the timestamp before post was added
35 response = api.api_get_threaddiff(req)
36 response = api.api_get_threaddiff(req)
36 diff = simplejson.loads(response.content)
37 diff = simplejson.loads(response.content)
37 self.assertEqual(2, len(diff['updated']),
38 self.assertEqual(2, len(diff['updated']),
38 'There must be 2 updated posts in the diff.')
39 'There must be 2 updated posts in the diff.')
39
40
40 # Reload post to get the new UID
41 # Reload post to get the new UID
41 opening_post = Post.objects.get(id=opening_post.id)
42 opening_post = Post.objects.get(id=opening_post.id)
42 req = MockRequest()
43 req = MockRequest()
43 req.POST['thread'] = opening_post.id
44 req.POST['thread'] = opening_post.id
44 req.POST['uids'] = ' '.join([opening_post.uid, reply.uid])
45 req.POST['uids'] = ' '.join([opening_post.uid, reply.uid])
45 empty_response = api.api_get_threaddiff(req)
46 empty_response = api.api_get_threaddiff(req)
46 diff = simplejson.loads(empty_response.content)
47 diff = simplejson.loads(empty_response.content)
47 self.assertEqual(0, len(diff['updated']),
48 self.assertEqual(0, len(diff['updated']),
48 'There must be no updated posts in the diff.')
49 'There must be no updated posts in the diff.')
49
50
50 def test_get_threads(self):
51 def test_get_threads(self):
51 # Create 10 threads
52 # Create 10 threads
52 tag = Tag.objects.create(name='test_tag')
53 tag = Tag.objects.create(name='test_tag')
53 for i in range(5):
54 for i in range(5):
54 Post.objects.create_post(title='title', text='text', tags=[tag])
55 Post.objects.create_post(title='title', text='text', tags=[tag])
55
56
56 # Get all threads
57 # Get all threads
57 response = api.api_get_threads(MockRequest(), 5)
58 response = api.api_get_threads(MockRequest(), 5)
58 diff = simplejson.loads(response.content)
59 diff = simplejson.loads(response.content)
59 self.assertEqual(5, len(diff), 'Invalid thread list response.')
60 self.assertEqual(5, len(diff), 'Invalid thread list response.')
60
61
61 # Get less threads then exist
62 # Get less threads then exist
62 response = api.api_get_threads(MockRequest(), 3)
63 response = api.api_get_threads(MockRequest(), 3)
63 diff = simplejson.loads(response.content)
64 diff = simplejson.loads(response.content)
64 self.assertEqual(3, len(diff), 'Invalid thread list response.')
65 self.assertEqual(3, len(diff), 'Invalid thread list response.')
65
66
66 # Get more threads then exist
67 # Get more threads then exist
67 response = api.api_get_threads(MockRequest(), 10)
68 response = api.api_get_threads(MockRequest(), 10)
68 diff = simplejson.loads(response.content)
69 diff = simplejson.loads(response.content)
69 self.assertEqual(5, len(diff), 'Invalid thread list response.')
70 self.assertEqual(5, len(diff), 'Invalid thread list response.')
@@ -1,178 +1,179 b''
1 from django.core.paginator import Paginator
1 from django.core.paginator import Paginator
2 from django.test import TestCase
2 from django.test import TestCase
3
3
4 from boards import settings
4 from boards import settings
5 from boards.models import Tag, Post, Thread
5 from boards.models import Tag, Post, Thread
6 from boards.models.thread import STATUS_ARCHIVE
6
7
7
8
8 class PostTests(TestCase):
9 class PostTests(TestCase):
9
10
10 def _create_post(self):
11 def _create_post(self):
11 tag, created = Tag.objects.get_or_create(name='test_tag')
12 tag, created = Tag.objects.get_or_create(name='test_tag')
12 return Post.objects.create_post(title='title', text='text',
13 return Post.objects.create_post(title='title', text='text',
13 tags=[tag])
14 tags=[tag])
14
15
15 def test_post_add(self):
16 def test_post_add(self):
16 """Test adding post"""
17 """Test adding post"""
17
18
18 post = self._create_post()
19 post = self._create_post()
19
20
20 self.assertIsNotNone(post, 'No post was created.')
21 self.assertIsNotNone(post, 'No post was created.')
21 self.assertEqual('test_tag', post.get_thread().tags.all()[0].name,
22 self.assertEqual('test_tag', post.get_thread().tags.all()[0].name,
22 'No tags were added to the post.')
23 'No tags were added to the post.')
23
24
24 def test_delete_post(self):
25 def test_delete_post(self):
25 """Test post deletion"""
26 """Test post deletion"""
26
27
27 post = self._create_post()
28 post = self._create_post()
28 post_id = post.id
29 post_id = post.id
29
30
30 post.delete()
31 post.delete()
31
32
32 self.assertFalse(Post.objects.filter(id=post_id).exists())
33 self.assertFalse(Post.objects.filter(id=post_id).exists())
33
34
34 def test_delete_thread(self):
35 def test_delete_thread(self):
35 """Test thread deletion"""
36 """Test thread deletion"""
36
37
37 opening_post = self._create_post()
38 opening_post = self._create_post()
38 thread = opening_post.get_thread()
39 thread = opening_post.get_thread()
39 reply = Post.objects.create_post("", "", thread=thread)
40 reply = Post.objects.create_post("", "", thread=thread)
40
41
41 thread.delete()
42 thread.delete()
42
43
43 self.assertFalse(Post.objects.filter(id=reply.id).exists(),
44 self.assertFalse(Post.objects.filter(id=reply.id).exists(),
44 'Reply was not deleted with the thread.')
45 'Reply was not deleted with the thread.')
45 self.assertFalse(Post.objects.filter(id=opening_post.id).exists(),
46 self.assertFalse(Post.objects.filter(id=opening_post.id).exists(),
46 'Opening post was not deleted with the thread.')
47 'Opening post was not deleted with the thread.')
47
48
48 def test_post_to_thread(self):
49 def test_post_to_thread(self):
49 """Test adding post to a thread"""
50 """Test adding post to a thread"""
50
51
51 op = self._create_post()
52 op = self._create_post()
52 post = Post.objects.create_post("", "", thread=op.get_thread())
53 post = Post.objects.create_post("", "", thread=op.get_thread())
53
54
54 self.assertIsNotNone(post, 'Reply to thread wasn\'t created')
55 self.assertIsNotNone(post, 'Reply to thread wasn\'t created')
55 self.assertEqual(op.get_thread().last_edit_time, post.pub_time,
56 self.assertEqual(op.get_thread().last_edit_time, post.pub_time,
56 'Post\'s create time doesn\'t match thread last edit'
57 'Post\'s create time doesn\'t match thread last edit'
57 ' time')
58 ' time')
58
59
59 def test_delete_posts_by_ip(self):
60 def test_delete_posts_by_ip(self):
60 """Test deleting posts with the given ip"""
61 """Test deleting posts with the given ip"""
61
62
62 post = self._create_post()
63 post = self._create_post()
63 post_id = post.id
64 post_id = post.id
64
65
65 Post.objects.delete_posts_by_ip('0.0.0.0')
66 Post.objects.delete_posts_by_ip('0.0.0.0')
66
67
67 self.assertFalse(Post.objects.filter(id=post_id).exists())
68 self.assertFalse(Post.objects.filter(id=post_id).exists())
68
69
69 def test_get_thread(self):
70 def test_get_thread(self):
70 """Test getting all posts of a thread"""
71 """Test getting all posts of a thread"""
71
72
72 opening_post = self._create_post()
73 opening_post = self._create_post()
73
74
74 for i in range(2):
75 for i in range(2):
75 Post.objects.create_post('title', 'text',
76 Post.objects.create_post('title', 'text',
76 thread=opening_post.get_thread())
77 thread=opening_post.get_thread())
77
78
78 thread = opening_post.get_thread()
79 thread = opening_post.get_thread()
79
80
80 self.assertEqual(3, thread.get_replies().count())
81 self.assertEqual(3, thread.get_replies().count())
81
82
82 def test_create_post_with_tag(self):
83 def test_create_post_with_tag(self):
83 """Test adding tag to post"""
84 """Test adding tag to post"""
84
85
85 tag = Tag.objects.create(name='test_tag')
86 tag = Tag.objects.create(name='test_tag')
86 post = Post.objects.create_post(title='title', text='text', tags=[tag])
87 post = Post.objects.create_post(title='title', text='text', tags=[tag])
87
88
88 thread = post.get_thread()
89 thread = post.get_thread()
89 self.assertIsNotNone(post, 'Post not created')
90 self.assertIsNotNone(post, 'Post not created')
90 self.assertTrue(tag in thread.tags.all(), 'Tag not added to thread')
91 self.assertTrue(tag in thread.tags.all(), 'Tag not added to thread')
91
92
92 def test_thread_max_count(self):
93 def test_thread_max_count(self):
93 """Test deletion of old posts when the max thread count is reached"""
94 """Test deletion of old posts when the max thread count is reached"""
94
95
95 for i in range(settings.get_int('Messages', 'MaxThreadCount') + 1):
96 for i in range(settings.get_int('Messages', 'MaxThreadCount') + 1):
96 self._create_post()
97 self._create_post()
97
98
98 self.assertEqual(settings.get_int('Messages', 'MaxThreadCount'),
99 self.assertEqual(settings.get_int('Messages', 'MaxThreadCount'),
99 len(Thread.objects.filter(archived=False)))
100 len(Thread.objects.exclude(status=STATUS_ARCHIVE)))
100
101
101 def test_pages(self):
102 def test_pages(self):
102 """Test that the thread list is properly split into pages"""
103 """Test that the thread list is properly split into pages"""
103
104
104 for i in range(settings.get_int('Messages', 'MaxThreadCount')):
105 for i in range(settings.get_int('Messages', 'MaxThreadCount')):
105 self._create_post()
106 self._create_post()
106
107
107 all_threads = Thread.objects.filter(archived=False)
108 all_threads = Thread.objects.exclude(status=STATUS_ARCHIVE)
108
109
109 paginator = Paginator(Thread.objects.filter(archived=False),
110 paginator = Paginator(Thread.objects.exclude(status=STATUS_ARCHIVE),
110 settings.get_int('View', 'ThreadsPerPage'))
111 settings.get_int('View', 'ThreadsPerPage'))
111 posts_in_second_page = paginator.page(2).object_list
112 posts_in_second_page = paginator.page(2).object_list
112 first_post = posts_in_second_page[0]
113 first_post = posts_in_second_page[0]
113
114
114 self.assertEqual(all_threads[settings.get_int('View', 'ThreadsPerPage')].id,
115 self.assertEqual(all_threads[settings.get_int('View', 'ThreadsPerPage')].id,
115 first_post.id)
116 first_post.id)
116
117
117 def test_thread_replies(self):
118 def test_thread_replies(self):
118 """
119 """
119 Tests that the replies can be queried from a thread in all possible
120 Tests that the replies can be queried from a thread in all possible
120 ways.
121 ways.
121 """
122 """
122
123
123 tag = Tag.objects.create(name='test_tag')
124 tag = Tag.objects.create(name='test_tag')
124 opening_post = Post.objects.create_post(title='title', text='text',
125 opening_post = Post.objects.create_post(title='title', text='text',
125 tags=[tag])
126 tags=[tag])
126 thread = opening_post.get_thread()
127 thread = opening_post.get_thread()
127
128
128 Post.objects.create_post(title='title', text='text', thread=thread)
129 Post.objects.create_post(title='title', text='text', thread=thread)
129 Post.objects.create_post(title='title', text='text', thread=thread)
130 Post.objects.create_post(title='title', text='text', thread=thread)
130
131
131 replies = thread.get_replies()
132 replies = thread.get_replies()
132 self.assertTrue(len(replies) > 0, 'No replies found for thread.')
133 self.assertTrue(len(replies) > 0, 'No replies found for thread.')
133
134
134 replies = thread.get_replies(view_fields_only=True)
135 replies = thread.get_replies(view_fields_only=True)
135 self.assertTrue(len(replies) > 0,
136 self.assertTrue(len(replies) > 0,
136 'No replies found for thread with view fields only.')
137 'No replies found for thread with view fields only.')
137
138
138 def test_bumplimit(self):
139 def test_bumplimit(self):
139 """
140 """
140 Tests that the thread bumpable status is changed and post uids and
141 Tests that the thread bumpable status is changed and post uids and
141 last update times are updated across all post threads.
142 last update times are updated across all post threads.
142 """
143 """
143
144
144 op1 = Post.objects.create_post(title='title', text='text')
145 op1 = Post.objects.create_post(title='title', text='text')
145 op2 = Post.objects.create_post(title='title', text='text')
146 op2 = Post.objects.create_post(title='title', text='text')
146
147
147 thread1 = op1.get_thread()
148 thread1 = op1.get_thread()
148 thread1.max_posts = 5
149 thread1.max_posts = 5
149 thread1.save()
150 thread1.save()
150
151
151 uid_1 = op1.uid
152 uid_1 = op1.uid
152 uid_2 = op2.uid
153 uid_2 = op2.uid
153
154
154 # Create multi reply
155 # Create multi reply
155 Post.objects.create_post(
156 Post.objects.create_post(
156 title='title', text='text', thread=thread1,
157 title='title', text='text', thread=thread1,
157 opening_posts=[op1, op2])
158 opening_posts=[op1, op2])
158 thread_update_time_2 = op2.get_thread().last_edit_time
159 thread_update_time_2 = op2.get_thread().last_edit_time
159 for i in range(6):
160 for i in range(6):
160 Post.objects.create_post(title='title', text='text',
161 Post.objects.create_post(title='title', text='text',
161 thread=thread1)
162 thread=thread1)
162
163
163 self.assertFalse(op1.get_thread().can_bump(),
164 self.assertFalse(op1.get_thread().can_bump(),
164 'Thread is bumpable when it should not be.')
165 'Thread is bumpable when it should not be.')
165 self.assertTrue(op2.get_thread().can_bump(),
166 self.assertTrue(op2.get_thread().can_bump(),
166 'Thread is not bumpable when it should be.')
167 'Thread is not bumpable when it should be.')
167 self.assertNotEqual(
168 self.assertNotEqual(
168 uid_1, Post.objects.get(id=op1.id).uid,
169 uid_1, Post.objects.get(id=op1.id).uid,
169 'UID of the first OP should be changed but it is not.')
170 'UID of the first OP should be changed but it is not.')
170 self.assertEqual(
171 self.assertEqual(
171 uid_2, Post.objects.get(id=op2.id).uid,
172 uid_2, Post.objects.get(id=op2.id).uid,
172 'UID of the first OP should not be changed but it is.')
173 'UID of the first OP should not be changed but it is.')
173
174
174 self.assertNotEqual(
175 self.assertNotEqual(
175 thread_update_time_2,
176 thread_update_time_2,
176 Thread.objects.get(id=op2.get_thread().id).last_edit_time,
177 Thread.objects.get(id=op2.get_thread().id).last_edit_time,
177 'Thread last update time should change when the other thread '
178 'Thread last update time should change when the other thread '
178 'changes status.')
179 'changes status.')
@@ -1,293 +1,293 b''
1 from collections import OrderedDict
1 from collections import OrderedDict
2 import json
2 import json
3 import logging
3 import logging
4
4
5 from django.db import transaction
5 from django.db import transaction
6 from django.db.models import Count
6 from django.db.models import Count
7 from django.http import HttpResponse
7 from django.http import HttpResponse
8 from django.shortcuts import get_object_or_404
8 from django.shortcuts import get_object_or_404
9 from django.core import serializers
9 from django.core import serializers
10 from boards.abstracts.settingsmanager import get_settings_manager,\
10 from boards.abstracts.settingsmanager import get_settings_manager,\
11 FAV_THREAD_NO_UPDATES
11 FAV_THREAD_NO_UPDATES
12
12
13 from boards.forms import PostForm, PlainErrorList
13 from boards.forms import PostForm, PlainErrorList
14 from boards.models import Post, Thread, Tag
14 from boards.models import Post, Thread, Tag
15 from boards.models.thread import STATUS_ARCHIVE
15 from boards.utils import datetime_to_epoch
16 from boards.utils import datetime_to_epoch
16 from boards.views.thread import ThreadView
17 from boards.views.thread import ThreadView
17 from boards.models.user import Notification
18 from boards.models.user import Notification
18 from boards.mdx_neboard import Parser
19 from boards.mdx_neboard import Parser
19
20
20
21
21 __author__ = 'neko259'
22 __author__ = 'neko259'
22
23
23 PARAMETER_TRUNCATED = 'truncated'
24 PARAMETER_TRUNCATED = 'truncated'
24 PARAMETER_TAG = 'tag'
25 PARAMETER_TAG = 'tag'
25 PARAMETER_OFFSET = 'offset'
26 PARAMETER_OFFSET = 'offset'
26 PARAMETER_DIFF_TYPE = 'type'
27 PARAMETER_DIFF_TYPE = 'type'
27 PARAMETER_POST = 'post'
28 PARAMETER_POST = 'post'
28 PARAMETER_UPDATED = 'updated'
29 PARAMETER_UPDATED = 'updated'
29 PARAMETER_LAST_UPDATE = 'last_update'
30 PARAMETER_LAST_UPDATE = 'last_update'
30 PARAMETER_THREAD = 'thread'
31 PARAMETER_THREAD = 'thread'
31 PARAMETER_UIDS = 'uids'
32 PARAMETER_UIDS = 'uids'
32
33
33 DIFF_TYPE_HTML = 'html'
34 DIFF_TYPE_HTML = 'html'
34 DIFF_TYPE_JSON = 'json'
35 DIFF_TYPE_JSON = 'json'
35
36
36 STATUS_OK = 'ok'
37 STATUS_OK = 'ok'
37 STATUS_ERROR = 'error'
38 STATUS_ERROR = 'error'
38
39
39 logger = logging.getLogger(__name__)
40 logger = logging.getLogger(__name__)
40
41
41
42
42 @transaction.atomic
43 @transaction.atomic
43 def api_get_threaddiff(request):
44 def api_get_threaddiff(request):
44 """
45 """
45 Gets posts that were changed or added since time
46 Gets posts that were changed or added since time
46 """
47 """
47
48
48 thread_id = request.POST.get(PARAMETER_THREAD)
49 thread_id = request.POST.get(PARAMETER_THREAD)
49 uids_str = request.POST.get(PARAMETER_UIDS).strip()
50 uids_str = request.POST.get(PARAMETER_UIDS).strip()
50 uids = uids_str.split(' ')
51 uids = uids_str.split(' ')
51
52
52 opening_post = get_object_or_404(Post, id=thread_id)
53 opening_post = get_object_or_404(Post, id=thread_id)
53 thread = opening_post.get_thread()
54 thread = opening_post.get_thread()
54
55
55 json_data = {
56 json_data = {
56 PARAMETER_UPDATED: [],
57 PARAMETER_UPDATED: [],
57 PARAMETER_LAST_UPDATE: None, # TODO Maybe this can be removed already?
58 PARAMETER_LAST_UPDATE: None, # TODO Maybe this can be removed already?
58 }
59 }
59 posts = Post.objects.filter(threads__in=[thread]).exclude(uid__in=uids)
60 posts = Post.objects.filter(threads__in=[thread]).exclude(uid__in=uids)
60
61
61 diff_type = request.GET.get(PARAMETER_DIFF_TYPE, DIFF_TYPE_HTML)
62 diff_type = request.GET.get(PARAMETER_DIFF_TYPE, DIFF_TYPE_HTML)
62
63
63 for post in posts:
64 for post in posts:
64 json_data[PARAMETER_UPDATED].append(post.get_post_data(
65 json_data[PARAMETER_UPDATED].append(post.get_post_data(
65 format_type=diff_type, request=request))
66 format_type=diff_type, request=request))
66 json_data[PARAMETER_LAST_UPDATE] = str(thread.last_edit_time)
67 json_data[PARAMETER_LAST_UPDATE] = str(thread.last_edit_time)
67
68
68 # If the tag is favorite, update the counter
69 # If the tag is favorite, update the counter
69 settings_manager = get_settings_manager(request)
70 settings_manager = get_settings_manager(request)
70 favorite = settings_manager.thread_is_fav(opening_post)
71 favorite = settings_manager.thread_is_fav(opening_post)
71 if favorite:
72 if favorite:
72 settings_manager.add_or_read_fav_thread(opening_post)
73 settings_manager.add_or_read_fav_thread(opening_post)
73
74
74 return HttpResponse(content=json.dumps(json_data))
75 return HttpResponse(content=json.dumps(json_data))
75
76
76
77
77 def api_add_post(request, opening_post_id):
78 def api_add_post(request, opening_post_id):
78 """
79 """
79 Adds a post and return the JSON response for it
80 Adds a post and return the JSON response for it
80 """
81 """
81
82
82 opening_post = get_object_or_404(Post, id=opening_post_id)
83 opening_post = get_object_or_404(Post, id=opening_post_id)
83
84
84 logger.info('Adding post via api...')
85 logger.info('Adding post via api...')
85
86
86 status = STATUS_OK
87 status = STATUS_OK
87 errors = []
88 errors = []
88
89
89 if request.method == 'POST':
90 if request.method == 'POST':
90 form = PostForm(request.POST, request.FILES, error_class=PlainErrorList)
91 form = PostForm(request.POST, request.FILES, error_class=PlainErrorList)
91 form.session = request.session
92 form.session = request.session
92
93
93 if form.need_to_ban:
94 if form.need_to_ban:
94 # Ban user because he is suspected to be a bot
95 # Ban user because he is suspected to be a bot
95 # _ban_current_user(request)
96 # _ban_current_user(request)
96 status = STATUS_ERROR
97 status = STATUS_ERROR
97 if form.is_valid():
98 if form.is_valid():
98 post = ThreadView().new_post(request, form, opening_post,
99 post = ThreadView().new_post(request, form, opening_post,
99 html_response=False)
100 html_response=False)
100 if not post:
101 if not post:
101 status = STATUS_ERROR
102 status = STATUS_ERROR
102 else:
103 else:
103 logger.info('Added post #%d via api.' % post.id)
104 logger.info('Added post #%d via api.' % post.id)
104 else:
105 else:
105 status = STATUS_ERROR
106 status = STATUS_ERROR
106 errors = form.as_json_errors()
107 errors = form.as_json_errors()
107
108
108 response = {
109 response = {
109 'status': status,
110 'status': status,
110 'errors': errors,
111 'errors': errors,
111 }
112 }
112
113
113 return HttpResponse(content=json.dumps(response))
114 return HttpResponse(content=json.dumps(response))
114
115
115
116
116 def get_post(request, post_id):
117 def get_post(request, post_id):
117 """
118 """
118 Gets the html of a post. Used for popups. Post can be truncated if used
119 Gets the html of a post. Used for popups. Post can be truncated if used
119 in threads list with 'truncated' get parameter.
120 in threads list with 'truncated' get parameter.
120 """
121 """
121
122
122 post = get_object_or_404(Post, id=post_id)
123 post = get_object_or_404(Post, id=post_id)
123 truncated = PARAMETER_TRUNCATED in request.GET
124 truncated = PARAMETER_TRUNCATED in request.GET
124
125
125 return HttpResponse(content=post.get_view(truncated=truncated, need_op_data=True))
126 return HttpResponse(content=post.get_view(truncated=truncated, need_op_data=True))
126
127
127
128
128 def api_get_threads(request, count):
129 def api_get_threads(request, count):
129 """
130 """
130 Gets the JSON thread opening posts list.
131 Gets the JSON thread opening posts list.
131 Parameters that can be used for filtering:
132 Parameters that can be used for filtering:
132 tag, offset (from which thread to get results)
133 tag, offset (from which thread to get results)
133 """
134 """
134
135
135 if PARAMETER_TAG in request.GET:
136 if PARAMETER_TAG in request.GET:
136 tag_name = request.GET[PARAMETER_TAG]
137 tag_name = request.GET[PARAMETER_TAG]
137 if tag_name is not None:
138 if tag_name is not None:
138 tag = get_object_or_404(Tag, name=tag_name)
139 tag = get_object_or_404(Tag, name=tag_name)
139 threads = tag.get_threads().filter(archived=False)
140 threads = tag.get_threads().exclude(status=STATUS_ARCHIVE)
140 else:
141 else:
141 threads = Thread.objects.filter(archived=False)
142 threads = Thread.objects.exclude(status=STATUS_ARCHIVE)
142
143
143 if PARAMETER_OFFSET in request.GET:
144 if PARAMETER_OFFSET in request.GET:
144 offset = request.GET[PARAMETER_OFFSET]
145 offset = request.GET[PARAMETER_OFFSET]
145 offset = int(offset) if offset is not None else 0
146 offset = int(offset) if offset is not None else 0
146 else:
147 else:
147 offset = 0
148 offset = 0
148
149
149 threads = threads.order_by('-bump_time')
150 threads = threads.order_by('-bump_time')
150 threads = threads[offset:offset + int(count)]
151 threads = threads[offset:offset + int(count)]
151
152
152 opening_posts = []
153 opening_posts = []
153 for thread in threads:
154 for thread in threads:
154 opening_post = thread.get_opening_post()
155 opening_post = thread.get_opening_post()
155
156
156 # TODO Add tags, replies and images count
157 # TODO Add tags, replies and images count
157 post_data = opening_post.get_post_data(include_last_update=True)
158 post_data = opening_post.get_post_data(include_last_update=True)
158 post_data['bumpable'] = thread.can_bump()
159 post_data['status'] = thread.get_status()
159 post_data['archived'] = thread.archived
160
160
161 opening_posts.append(post_data)
161 opening_posts.append(post_data)
162
162
163 return HttpResponse(content=json.dumps(opening_posts))
163 return HttpResponse(content=json.dumps(opening_posts))
164
164
165
165
166 # TODO Test this
166 # TODO Test this
167 def api_get_tags(request):
167 def api_get_tags(request):
168 """
168 """
169 Gets all tags or user tags.
169 Gets all tags or user tags.
170 """
170 """
171
171
172 # TODO Get favorite tags for the given user ID
172 # TODO Get favorite tags for the given user ID
173
173
174 tags = Tag.objects.get_not_empty_tags()
174 tags = Tag.objects.get_not_empty_tags()
175
175
176 term = request.GET.get('term')
176 term = request.GET.get('term')
177 if term is not None:
177 if term is not None:
178 tags = tags.filter(name__contains=term)
178 tags = tags.filter(name__contains=term)
179
179
180 tag_names = [tag.name for tag in tags]
180 tag_names = [tag.name for tag in tags]
181
181
182 return HttpResponse(content=json.dumps(tag_names))
182 return HttpResponse(content=json.dumps(tag_names))
183
183
184
184
185 # TODO The result can be cached by the thread last update time
185 # TODO The result can be cached by the thread last update time
186 # TODO Test this
186 # TODO Test this
187 def api_get_thread_posts(request, opening_post_id):
187 def api_get_thread_posts(request, opening_post_id):
188 """
188 """
189 Gets the JSON array of thread posts
189 Gets the JSON array of thread posts
190 """
190 """
191
191
192 opening_post = get_object_or_404(Post, id=opening_post_id)
192 opening_post = get_object_or_404(Post, id=opening_post_id)
193 thread = opening_post.get_thread()
193 thread = opening_post.get_thread()
194 posts = thread.get_replies()
194 posts = thread.get_replies()
195
195
196 json_data = {
196 json_data = {
197 'posts': [],
197 'posts': [],
198 'last_update': None,
198 'last_update': None,
199 }
199 }
200 json_post_list = []
200 json_post_list = []
201
201
202 for post in posts:
202 for post in posts:
203 json_post_list.append(post.get_post_data())
203 json_post_list.append(post.get_post_data())
204 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
204 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
205 json_data['posts'] = json_post_list
205 json_data['posts'] = json_post_list
206
206
207 return HttpResponse(content=json.dumps(json_data))
207 return HttpResponse(content=json.dumps(json_data))
208
208
209
209
210 def api_get_notifications(request, username):
210 def api_get_notifications(request, username):
211 last_notification_id_str = request.GET.get('last', None)
211 last_notification_id_str = request.GET.get('last', None)
212 last_id = int(last_notification_id_str) if last_notification_id_str is not None else None
212 last_id = int(last_notification_id_str) if last_notification_id_str is not None else None
213
213
214 posts = Notification.objects.get_notification_posts(username=username,
214 posts = Notification.objects.get_notification_posts(username=username,
215 last=last_id)
215 last=last_id)
216
216
217 json_post_list = []
217 json_post_list = []
218 for post in posts:
218 for post in posts:
219 json_post_list.append(post.get_post_data())
219 json_post_list.append(post.get_post_data())
220 return HttpResponse(content=json.dumps(json_post_list))
220 return HttpResponse(content=json.dumps(json_post_list))
221
221
222
222
223 def api_get_post(request, post_id):
223 def api_get_post(request, post_id):
224 """
224 """
225 Gets the JSON of a post. This can be
225 Gets the JSON of a post. This can be
226 used as and API for external clients.
226 used as and API for external clients.
227 """
227 """
228
228
229 post = get_object_or_404(Post, id=post_id)
229 post = get_object_or_404(Post, id=post_id)
230
230
231 json = serializers.serialize("json", [post], fields=(
231 json = serializers.serialize("json", [post], fields=(
232 "pub_time", "_text_rendered", "title", "text", "image",
232 "pub_time", "_text_rendered", "title", "text", "image",
233 "image_width", "image_height", "replies", "tags"
233 "image_width", "image_height", "replies", "tags"
234 ))
234 ))
235
235
236 return HttpResponse(content=json)
236 return HttpResponse(content=json)
237
237
238
238
239 def api_get_preview(request):
239 def api_get_preview(request):
240 raw_text = request.POST['raw_text']
240 raw_text = request.POST['raw_text']
241
241
242 parser = Parser()
242 parser = Parser()
243 return HttpResponse(content=parser.parse(parser.preparse(raw_text)))
243 return HttpResponse(content=parser.parse(parser.preparse(raw_text)))
244
244
245
245
246 def api_get_new_posts(request):
246 def api_get_new_posts(request):
247 """
247 """
248 Gets favorite threads and unread posts count.
248 Gets favorite threads and unread posts count.
249 """
249 """
250 posts = list()
250 posts = list()
251
251
252 include_posts = 'include_posts' in request.GET
252 include_posts = 'include_posts' in request.GET
253
253
254 settings_manager = get_settings_manager(request)
254 settings_manager = get_settings_manager(request)
255 fav_threads = settings_manager.get_fav_threads()
255 fav_threads = settings_manager.get_fav_threads()
256 fav_thread_ops = Post.objects.filter(id__in=fav_threads.keys())\
256 fav_thread_ops = Post.objects.filter(id__in=fav_threads.keys())\
257 .order_by('-pub_time').prefetch_related('thread')
257 .order_by('-pub_time').prefetch_related('thread')
258
258
259 ops = [{'op': op, 'last_id': fav_threads[str(op.id)]} for op in fav_thread_ops]
259 ops = [{'op': op, 'last_id': fav_threads[str(op.id)]} for op in fav_thread_ops]
260 if include_posts:
260 if include_posts:
261 new_post_threads = Thread.objects.get_new_posts(ops)
261 new_post_threads = Thread.objects.get_new_posts(ops)
262 if new_post_threads:
262 if new_post_threads:
263 thread_ids = {thread.id: thread for thread in new_post_threads}
263 thread_ids = {thread.id: thread for thread in new_post_threads}
264 else:
264 else:
265 thread_ids = dict()
265 thread_ids = dict()
266
266
267 for op in fav_thread_ops:
267 for op in fav_thread_ops:
268 fav_thread_dict = dict()
268 fav_thread_dict = dict()
269
269
270 op_thread = op.get_thread()
270 op_thread = op.get_thread()
271 if op_thread.id in thread_ids:
271 if op_thread.id in thread_ids:
272 thread = thread_ids[op_thread.id]
272 thread = thread_ids[op_thread.id]
273 new_post_count = thread.new_post_count
273 new_post_count = thread.new_post_count
274 fav_thread_dict['newest_post_link'] = thread.get_replies()\
274 fav_thread_dict['newest_post_link'] = thread.get_replies()\
275 .filter(id__gt=fav_threads[str(op.id)])\
275 .filter(id__gt=fav_threads[str(op.id)])\
276 .first().get_absolute_url(thread=thread)
276 .first().get_absolute_url(thread=thread)
277 else:
277 else:
278 new_post_count = 0
278 new_post_count = 0
279 fav_thread_dict['new_post_count'] = new_post_count
279 fav_thread_dict['new_post_count'] = new_post_count
280
280
281 fav_thread_dict['id'] = op.id
281 fav_thread_dict['id'] = op.id
282
282
283 fav_thread_dict['post_url'] = op.get_link_view()
283 fav_thread_dict['post_url'] = op.get_link_view()
284 fav_thread_dict['title'] = op.title
284 fav_thread_dict['title'] = op.title
285
285
286 posts.append(fav_thread_dict)
286 posts.append(fav_thread_dict)
287 else:
287 else:
288 fav_thread_dict = dict()
288 fav_thread_dict = dict()
289 fav_thread_dict['new_post_count'] = \
289 fav_thread_dict['new_post_count'] = \
290 Thread.objects.get_new_post_count(ops)
290 Thread.objects.get_new_post_count(ops)
291 posts.append(fav_thread_dict)
291 posts.append(fav_thread_dict)
292
292
293 return HttpResponse(content=json.dumps(posts))
293 return HttpResponse(content=json.dumps(posts))
@@ -1,179 +1,179 b''
1 from django.contrib.auth.decorators import permission_required
1 from django.contrib.auth.decorators import permission_required
2
2
3 from django.core.exceptions import ObjectDoesNotExist
3 from django.core.exceptions import ObjectDoesNotExist
4 from django.core.urlresolvers import reverse
4 from django.core.urlresolvers import reverse
5 from django.http import Http404
5 from django.http import Http404
6 from django.shortcuts import get_object_or_404, render, redirect
6 from django.shortcuts import get_object_or_404, render, redirect
7 from django.views.generic.edit import FormMixin
7 from django.views.generic.edit import FormMixin
8 from django.utils import timezone
8 from django.utils import timezone
9 from django.utils.dateformat import format
9 from django.utils.dateformat import format
10
10
11 from boards import utils, settings
11 from boards import utils, settings
12 from boards.abstracts.settingsmanager import get_settings_manager
12 from boards.abstracts.settingsmanager import get_settings_manager
13 from boards.forms import PostForm, PlainErrorList
13 from boards.forms import PostForm, PlainErrorList
14 from boards.models import Post
14 from boards.models import Post
15 from boards.views.base import BaseBoardView, CONTEXT_FORM
15 from boards.views.base import BaseBoardView, CONTEXT_FORM
16 from boards.views.mixins import DispatcherMixin, PARAMETER_METHOD
16 from boards.views.mixins import DispatcherMixin, PARAMETER_METHOD
17 from boards.views.posting_mixin import PostMixin
17 from boards.views.posting_mixin import PostMixin
18 import neboard
18 import neboard
19
19
20 REQ_POST_ID = 'post_id'
20 REQ_POST_ID = 'post_id'
21
21
22 CONTEXT_LASTUPDATE = "last_update"
22 CONTEXT_LASTUPDATE = "last_update"
23 CONTEXT_THREAD = 'thread'
23 CONTEXT_THREAD = 'thread'
24 CONTEXT_WS_TOKEN = 'ws_token'
24 CONTEXT_WS_TOKEN = 'ws_token'
25 CONTEXT_WS_PROJECT = 'ws_project'
25 CONTEXT_WS_PROJECT = 'ws_project'
26 CONTEXT_WS_HOST = 'ws_host'
26 CONTEXT_WS_HOST = 'ws_host'
27 CONTEXT_WS_PORT = 'ws_port'
27 CONTEXT_WS_PORT = 'ws_port'
28 CONTEXT_WS_TIME = 'ws_token_time'
28 CONTEXT_WS_TIME = 'ws_token_time'
29 CONTEXT_MODE = 'mode'
29 CONTEXT_MODE = 'mode'
30 CONTEXT_OP = 'opening_post'
30 CONTEXT_OP = 'opening_post'
31 CONTEXT_FAVORITE = 'is_favorite'
31 CONTEXT_FAVORITE = 'is_favorite'
32 CONTEXT_RSS_URL = 'rss_url'
32 CONTEXT_RSS_URL = 'rss_url'
33
33
34 FORM_TITLE = 'title'
34 FORM_TITLE = 'title'
35 FORM_TEXT = 'text'
35 FORM_TEXT = 'text'
36 FORM_IMAGE = 'image'
36 FORM_IMAGE = 'image'
37 FORM_THREADS = 'threads'
37 FORM_THREADS = 'threads'
38
38
39
39
40 class ThreadView(BaseBoardView, PostMixin, FormMixin, DispatcherMixin):
40 class ThreadView(BaseBoardView, PostMixin, FormMixin, DispatcherMixin):
41
41
42 def get(self, request, post_id, form: PostForm=None):
42 def get(self, request, post_id, form: PostForm=None):
43 try:
43 try:
44 opening_post = Post.objects.get(id=post_id)
44 opening_post = Post.objects.get(id=post_id)
45 except ObjectDoesNotExist:
45 except ObjectDoesNotExist:
46 raise Http404
46 raise Http404
47
47
48 # If the tag is favorite, update the counter
48 # If the tag is favorite, update the counter
49 settings_manager = get_settings_manager(request)
49 settings_manager = get_settings_manager(request)
50 favorite = settings_manager.thread_is_fav(opening_post)
50 favorite = settings_manager.thread_is_fav(opening_post)
51 if favorite:
51 if favorite:
52 settings_manager.add_or_read_fav_thread(opening_post)
52 settings_manager.add_or_read_fav_thread(opening_post)
53
53
54 # If this is not OP, don't show it as it is
54 # If this is not OP, don't show it as it is
55 if not opening_post.is_opening():
55 if not opening_post.is_opening():
56 return redirect(opening_post.get_thread().get_opening_post()
56 return redirect(opening_post.get_thread().get_opening_post()
57 .get_absolute_url())
57 .get_absolute_url())
58
58
59 if not form:
59 if not form:
60 form = PostForm(error_class=PlainErrorList)
60 form = PostForm(error_class=PlainErrorList)
61
61
62 thread_to_show = opening_post.get_thread()
62 thread_to_show = opening_post.get_thread()
63
63
64 params = dict()
64 params = dict()
65
65
66 params[CONTEXT_FORM] = form
66 params[CONTEXT_FORM] = form
67 params[CONTEXT_LASTUPDATE] = str(thread_to_show.last_edit_time)
67 params[CONTEXT_LASTUPDATE] = str(thread_to_show.last_edit_time)
68 params[CONTEXT_THREAD] = thread_to_show
68 params[CONTEXT_THREAD] = thread_to_show
69 params[CONTEXT_MODE] = self.get_mode()
69 params[CONTEXT_MODE] = self.get_mode()
70 params[CONTEXT_OP] = opening_post
70 params[CONTEXT_OP] = opening_post
71 params[CONTEXT_FAVORITE] = favorite
71 params[CONTEXT_FAVORITE] = favorite
72 params[CONTEXT_RSS_URL] = self.get_rss_url(post_id)
72 params[CONTEXT_RSS_URL] = self.get_rss_url(post_id)
73
73
74 if settings.get_bool('External', 'WebsocketsEnabled'):
74 if settings.get_bool('External', 'WebsocketsEnabled'):
75 token_time = format(timezone.now(), u'U')
75 token_time = format(timezone.now(), u'U')
76
76
77 params[CONTEXT_WS_TIME] = token_time
77 params[CONTEXT_WS_TIME] = token_time
78 params[CONTEXT_WS_TOKEN] = utils.get_websocket_token(
78 params[CONTEXT_WS_TOKEN] = utils.get_websocket_token(
79 timestamp=token_time)
79 timestamp=token_time)
80 params[CONTEXT_WS_PROJECT] = neboard.settings.CENTRIFUGE_PROJECT_ID
80 params[CONTEXT_WS_PROJECT] = neboard.settings.CENTRIFUGE_PROJECT_ID
81 params[CONTEXT_WS_HOST] = request.get_host().split(':')[0]
81 params[CONTEXT_WS_HOST] = request.get_host().split(':')[0]
82 params[CONTEXT_WS_PORT] = neboard.settings.CENTRIFUGE_PORT
82 params[CONTEXT_WS_PORT] = neboard.settings.CENTRIFUGE_PORT
83
83
84 params.update(self.get_data(thread_to_show))
84 params.update(self.get_data(thread_to_show))
85
85
86 return render(request, self.get_template(), params)
86 return render(request, self.get_template(), params)
87
87
88 def post(self, request, post_id):
88 def post(self, request, post_id):
89 opening_post = get_object_or_404(Post, id=post_id)
89 opening_post = get_object_or_404(Post, id=post_id)
90
90
91 # If this is not OP, don't show it as it is
91 # If this is not OP, don't show it as it is
92 if not opening_post.is_opening():
92 if not opening_post.is_opening():
93 raise Http404
93 raise Http404
94
94
95 if PARAMETER_METHOD in request.POST:
95 if PARAMETER_METHOD in request.POST:
96 self.dispatch_method(request, opening_post)
96 self.dispatch_method(request, opening_post)
97
97
98 return redirect('thread', post_id) # FIXME Different for different modes
98 return redirect('thread', post_id) # FIXME Different for different modes
99
99
100 if not opening_post.get_thread().archived:
100 if not opening_post.get_thread().is_archived():
101 form = PostForm(request.POST, request.FILES,
101 form = PostForm(request.POST, request.FILES,
102 error_class=PlainErrorList)
102 error_class=PlainErrorList)
103 form.session = request.session
103 form.session = request.session
104
104
105 if form.is_valid():
105 if form.is_valid():
106 return self.new_post(request, form, opening_post)
106 return self.new_post(request, form, opening_post)
107 if form.need_to_ban:
107 if form.need_to_ban:
108 # Ban user because he is suspected to be a bot
108 # Ban user because he is suspected to be a bot
109 self._ban_current_user(request)
109 self._ban_current_user(request)
110
110
111 return self.get(request, post_id, form)
111 return self.get(request, post_id, form)
112
112
113 def new_post(self, request, form: PostForm, opening_post: Post=None,
113 def new_post(self, request, form: PostForm, opening_post: Post=None,
114 html_response=True):
114 html_response=True):
115 """
115 """
116 Adds a new post (in thread or as a reply).
116 Adds a new post (in thread or as a reply).
117 """
117 """
118
118
119 ip = utils.get_client_ip(request)
119 ip = utils.get_client_ip(request)
120
120
121 data = form.cleaned_data
121 data = form.cleaned_data
122
122
123 title = form.get_title()
123 title = form.get_title()
124 text = data[FORM_TEXT]
124 text = data[FORM_TEXT]
125 file = form.get_file()
125 file = form.get_file()
126 threads = data[FORM_THREADS]
126 threads = data[FORM_THREADS]
127
127
128 text = self._remove_invalid_links(text)
128 text = self._remove_invalid_links(text)
129
129
130 post_thread = opening_post.get_thread()
130 post_thread = opening_post.get_thread()
131
131
132 post = Post.objects.create_post(title=title, text=text, file=file,
132 post = Post.objects.create_post(title=title, text=text, file=file,
133 thread=post_thread, ip=ip,
133 thread=post_thread, ip=ip,
134 opening_posts=threads,
134 opening_posts=threads,
135 tripcode=form.get_tripcode())
135 tripcode=form.get_tripcode())
136 post.notify_clients()
136 post.notify_clients()
137
137
138 if html_response:
138 if html_response:
139 if opening_post:
139 if opening_post:
140 return redirect(post.get_absolute_url())
140 return redirect(post.get_absolute_url())
141 else:
141 else:
142 return post
142 return post
143
143
144 def get_data(self, thread) -> dict:
144 def get_data(self, thread) -> dict:
145 """
145 """
146 Returns context params for the view.
146 Returns context params for the view.
147 """
147 """
148
148
149 return dict()
149 return dict()
150
150
151 def get_template(self) -> str:
151 def get_template(self) -> str:
152 """
152 """
153 Gets template to show the thread mode on.
153 Gets template to show the thread mode on.
154 """
154 """
155
155
156 pass
156 pass
157
157
158 def get_mode(self) -> str:
158 def get_mode(self) -> str:
159 pass
159 pass
160
160
161 def subscribe(self, request, opening_post):
161 def subscribe(self, request, opening_post):
162 settings_manager = get_settings_manager(request)
162 settings_manager = get_settings_manager(request)
163 settings_manager.add_or_read_fav_thread(opening_post)
163 settings_manager.add_or_read_fav_thread(opening_post)
164
164
165 def unsubscribe(self, request, opening_post):
165 def unsubscribe(self, request, opening_post):
166 settings_manager = get_settings_manager(request)
166 settings_manager = get_settings_manager(request)
167 settings_manager.del_fav_thread(opening_post)
167 settings_manager.del_fav_thread(opening_post)
168
168
169 @permission_required('boards.post_hide_unhide')
169 @permission_required('boards.post_hide_unhide')
170 def toggle_hide_post(self, request, opening_post):
170 def toggle_hide_post(self, request, opening_post):
171 post_id = request.GET.get(REQ_POST_ID)
171 post_id = request.GET.get(REQ_POST_ID)
172
172
173 if post_id:
173 if post_id:
174 post = get_object_or_404(Post, id=post_id)
174 post = get_object_or_404(Post, id=post_id)
175 post.set_hidden(not post.is_hidden())
175 post.set_hidden(not post.is_hidden())
176 post.save(update_fields=['hidden'])
176 post.save(update_fields=['hidden'])
177
177
178 def get_rss_url(self, opening_id):
178 def get_rss_url(self, opening_id):
179 return reverse('thread', kwargs={'post_id': opening_id}) + 'rss/'
179 return reverse('thread', kwargs={'post_id': opening_id}) + 'rss/'
General Comments 0
You need to be logged in to leave comments. Login now