##// END OF EJS Templates
Unify thread and post creation into one method inside post manager, that can be called from almost anywhere (one step closer to ajax thread creation)
neko259 -
r1997:be673d04 default
parent child Browse files
Show More
@@ -1,4 +1,5 b''
1 import re
1 import re
2
2
3 FILE_DIRECTORY = 'files/'
3 FILE_DIRECTORY = 'files/'
4 REGEX_TAGS = re.compile(r'^[\w\s\d\']+$', re.UNICODE)
4 REGEX_TAGS = re.compile(r'^[\w\s\d\']+$', re.UNICODE)
5 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]') No newline at end of file
@@ -1,360 +1,360 b''
1 import uuid
1 import uuid
2
2
3 import hashlib
3 import hashlib
4 import re
4 import re
5 from django.db import models
5 from django.db import models
6 from django.db.models import TextField
6 from django.db.models import TextField
7 from django.template.defaultfilters import truncatewords, striptags
7 from django.template.defaultfilters import truncatewords, striptags
8 from django.template.loader import render_to_string
8 from django.template.loader import render_to_string
9 from django.urls import reverse
9 from django.urls import reverse
10
10
11 from boards.abstracts.constants import REGEX_REPLY
11 from boards.abstracts.tripcode import Tripcode
12 from boards.abstracts.tripcode import Tripcode
12 from boards.models import Attachment, KeyPair, GlobalId
13 from boards.models import Attachment, KeyPair, GlobalId
13 from boards.models.attachment import FILE_TYPES_IMAGE
14 from boards.models.attachment import FILE_TYPES_IMAGE
14 from boards.models.base import Viewable
15 from boards.models.base import Viewable
15 from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
16 from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
16 from boards.models.post.manager import PostManager, NO_IP
17 from boards.models.post.manager import PostManager, NO_IP
17 from boards.utils import datetime_to_epoch
18 from boards.utils import datetime_to_epoch
18
19
19 CSS_CLS_HIDDEN_POST = 'hidden_post'
20 CSS_CLS_HIDDEN_POST = 'hidden_post'
20 CSS_CLS_DEAD_POST = 'dead_post'
21 CSS_CLS_DEAD_POST = 'dead_post'
21 CSS_CLS_ARCHIVE_POST = 'archive_post'
22 CSS_CLS_ARCHIVE_POST = 'archive_post'
22 CSS_CLS_POST = 'post'
23 CSS_CLS_POST = 'post'
23 CSS_CLS_MONOCHROME = 'monochrome'
24 CSS_CLS_MONOCHROME = 'monochrome'
24
25
25 TITLE_MAX_WORDS = 10
26 TITLE_MAX_WORDS = 10
26
27
27 APP_LABEL_BOARDS = 'boards'
28 APP_LABEL_BOARDS = 'boards'
28
29
29 BAN_REASON_AUTO = 'Auto'
30 BAN_REASON_AUTO = 'Auto'
30
31
31 TITLE_MAX_LENGTH = 200
32 TITLE_MAX_LENGTH = 200
32
33
33 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
34 REGEX_GLOBAL_REPLY = re.compile(r'\[post\](\w+)::([^:]+)::(\d+)\[/post\]')
34 REGEX_GLOBAL_REPLY = re.compile(r'\[post\](\w+)::([^:]+)::(\d+)\[/post\]')
35 REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?')
35 REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?')
36 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
36 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
37
37
38 PARAMETER_TRUNCATED = 'truncated'
38 PARAMETER_TRUNCATED = 'truncated'
39 PARAMETER_TAG = 'tag'
39 PARAMETER_TAG = 'tag'
40 PARAMETER_OFFSET = 'offset'
40 PARAMETER_OFFSET = 'offset'
41 PARAMETER_DIFF_TYPE = 'type'
41 PARAMETER_DIFF_TYPE = 'type'
42 PARAMETER_CSS_CLASS = 'css_class'
42 PARAMETER_CSS_CLASS = 'css_class'
43 PARAMETER_THREAD = 'thread'
43 PARAMETER_THREAD = 'thread'
44 PARAMETER_IS_OPENING = 'is_opening'
44 PARAMETER_IS_OPENING = 'is_opening'
45 PARAMETER_POST = 'post'
45 PARAMETER_POST = 'post'
46 PARAMETER_OP_ID = 'opening_post_id'
46 PARAMETER_OP_ID = 'opening_post_id'
47 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
47 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
48 PARAMETER_REPLY_LINK = 'reply_link'
48 PARAMETER_REPLY_LINK = 'reply_link'
49 PARAMETER_NEED_OP_DATA = 'need_op_data'
49 PARAMETER_NEED_OP_DATA = 'need_op_data'
50
50
51 POST_VIEW_PARAMS = (
51 POST_VIEW_PARAMS = (
52 'need_op_data',
52 'need_op_data',
53 'reply_link',
53 'reply_link',
54 'need_open_link',
54 'need_open_link',
55 'truncated',
55 'truncated',
56 'mode_tree',
56 'mode_tree',
57 'perms',
57 'perms',
58 'tree_depth',
58 'tree_depth',
59 )
59 )
60
60
61
61
62 class Post(models.Model, Viewable):
62 class Post(models.Model, Viewable):
63 """A post is a message."""
63 """A post is a message."""
64
64
65 objects = PostManager()
65 objects = PostManager()
66
66
67 class Meta:
67 class Meta:
68 app_label = APP_LABEL_BOARDS
68 app_label = APP_LABEL_BOARDS
69 ordering = ('id',)
69 ordering = ('id',)
70
70
71 title = models.CharField(max_length=TITLE_MAX_LENGTH, blank=True, default='')
71 title = models.CharField(max_length=TITLE_MAX_LENGTH, blank=True, default='')
72 pub_time = models.DateTimeField(db_index=True)
72 pub_time = models.DateTimeField(db_index=True)
73 text = TextField(blank=True, default='')
73 text = TextField(blank=True, default='')
74 _text_rendered = TextField(blank=True, null=True, editable=False)
74 _text_rendered = TextField(blank=True, null=True, editable=False)
75
75
76 attachments = models.ManyToManyField(Attachment, null=True, blank=True,
76 attachments = models.ManyToManyField(Attachment, null=True, blank=True,
77 related_name='attachment_posts')
77 related_name='attachment_posts')
78
78
79 poster_ip = models.GenericIPAddressField()
79 poster_ip = models.GenericIPAddressField()
80
80
81 # Used for cache and threads updating
81 # Used for cache and threads updating
82 last_edit_time = models.DateTimeField()
82 last_edit_time = models.DateTimeField()
83
83
84 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
84 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
85 null=True,
85 null=True,
86 blank=True, related_name='refposts',
86 blank=True, related_name='refposts',
87 db_index=True)
87 db_index=True)
88 refmap = models.TextField(null=True, blank=True)
88 refmap = models.TextField(null=True, blank=True)
89 thread = models.ForeignKey('Thread', on_delete=models.CASCADE,
89 thread = models.ForeignKey('Thread', on_delete=models.CASCADE,
90 db_index=True, related_name='replies')
90 db_index=True, related_name='replies')
91
91
92 url = models.TextField()
92 url = models.TextField()
93 uid = models.TextField()
93 uid = models.TextField()
94
94
95 # Global ID with author key. If the message was downloaded from another
95 # Global ID with author key. If the message was downloaded from another
96 # server, this indicates the server.
96 # server, this indicates the server.
97 global_id = models.OneToOneField(GlobalId, null=True, blank=True,
97 global_id = models.OneToOneField(GlobalId, null=True, blank=True,
98 on_delete=models.CASCADE)
98 on_delete=models.CASCADE)
99
99
100 tripcode = models.CharField(max_length=50, blank=True, default='')
100 tripcode = models.CharField(max_length=50, blank=True, default='')
101 opening = models.BooleanField(db_index=True)
101 opening = models.BooleanField(db_index=True)
102 hidden = models.BooleanField(default=False)
102 hidden = models.BooleanField(default=False)
103
103
104 def __str__(self):
104 def __str__(self):
105 return 'P#{}/{}'.format(self.id, self.get_title())
105 return 'P#{}/{}'.format(self.id, self.get_title())
106
106
107 def get_title(self) -> str:
107 def get_title(self) -> str:
108 return self.title
108 return self.title
109
109
110 def get_title_or_text(self):
110 def get_title_or_text(self):
111 title = self.get_title()
111 title = self.get_title()
112 if not title:
112 if not title:
113 title = truncatewords(striptags(self.get_text()), TITLE_MAX_WORDS)
113 title = truncatewords(striptags(self.get_text()), TITLE_MAX_WORDS)
114
114
115 return title
115 return title
116
116
117 def build_refmap(self, excluded_ids=None) -> None:
117 def build_refmap(self, excluded_ids=None) -> None:
118 """
118 """
119 Builds a replies map string from replies list. This is a cache to stop
119 Builds a replies map string from replies list. This is a cache to stop
120 the server from recalculating the map on every post show.
120 the server from recalculating the map on every post show.
121 """
121 """
122
122
123 replies = self.referenced_posts
123 replies = self.referenced_posts
124 if excluded_ids is not None:
124 if excluded_ids is not None:
125 replies = replies.exclude(id__in=excluded_ids)
125 replies = replies.exclude(id__in=excluded_ids)
126 else:
126 else:
127 replies = replies.all()
127 replies = replies.all()
128
128
129 post_urls = [refpost.get_link_view() for refpost in replies]
129 post_urls = [refpost.get_link_view() for refpost in replies]
130
130
131 self.refmap = ', '.join(post_urls)
131 self.refmap = ', '.join(post_urls)
132
132
133 def is_referenced(self) -> bool:
133 def is_referenced(self) -> bool:
134 return self.refmap and len(self.refmap) > 0
134 return self.refmap and len(self.refmap) > 0
135
135
136 def is_opening(self) -> bool:
136 def is_opening(self) -> bool:
137 """
137 """
138 Checks if this is an opening post or just a reply.
138 Checks if this is an opening post or just a reply.
139 """
139 """
140
140
141 return self.opening
141 return self.opening
142
142
143 def get_absolute_url(self, thread=None):
143 def get_absolute_url(self, thread=None):
144 # Url is cached only for the "main" thread. When getting url
144 # Url is cached only for the "main" thread. When getting url
145 # for other threads, do it manually.
145 # for other threads, do it manually.
146 return self.url
146 return self.url
147
147
148 def get_thread(self):
148 def get_thread(self):
149 return self.thread
149 return self.thread
150
150
151 def get_thread_id(self):
151 def get_thread_id(self):
152 return self.thread_id
152 return self.thread_id
153
153
154 def _get_cache_key(self):
154 def _get_cache_key(self):
155 return [datetime_to_epoch(self.last_edit_time)]
155 return [datetime_to_epoch(self.last_edit_time)]
156
156
157 def get_view_params(self, *args, **kwargs):
157 def get_view_params(self, *args, **kwargs):
158 """
158 """
159 Gets the parameters required for viewing the post based on the arguments
159 Gets the parameters required for viewing the post based on the arguments
160 given and the post itself.
160 given and the post itself.
161 """
161 """
162 thread = kwargs.get('thread') or self.get_thread()
162 thread = kwargs.get('thread') or self.get_thread()
163
163
164 css_classes = [CSS_CLS_POST]
164 css_classes = [CSS_CLS_POST]
165 if thread.is_archived():
165 if thread.is_archived():
166 css_classes.append(CSS_CLS_ARCHIVE_POST)
166 css_classes.append(CSS_CLS_ARCHIVE_POST)
167 elif not thread.can_bump():
167 elif not thread.can_bump():
168 css_classes.append(CSS_CLS_DEAD_POST)
168 css_classes.append(CSS_CLS_DEAD_POST)
169 if self.is_hidden():
169 if self.is_hidden():
170 css_classes.append(CSS_CLS_HIDDEN_POST)
170 css_classes.append(CSS_CLS_HIDDEN_POST)
171 if thread.is_monochrome():
171 if thread.is_monochrome():
172 css_classes.append(CSS_CLS_MONOCHROME)
172 css_classes.append(CSS_CLS_MONOCHROME)
173
173
174 params = dict()
174 params = dict()
175 for param in POST_VIEW_PARAMS:
175 for param in POST_VIEW_PARAMS:
176 if param in kwargs:
176 if param in kwargs:
177 params[param] = kwargs[param]
177 params[param] = kwargs[param]
178
178
179 params.update({
179 params.update({
180 PARAMETER_POST: self,
180 PARAMETER_POST: self,
181 PARAMETER_IS_OPENING: self.is_opening(),
181 PARAMETER_IS_OPENING: self.is_opening(),
182 PARAMETER_THREAD: thread,
182 PARAMETER_THREAD: thread,
183 PARAMETER_CSS_CLASS: ' '.join(css_classes),
183 PARAMETER_CSS_CLASS: ' '.join(css_classes),
184 })
184 })
185
185
186 return params
186 return params
187
187
188 def get_view(self, *args, **kwargs) -> str:
188 def get_view(self, *args, **kwargs) -> str:
189 """
189 """
190 Renders post's HTML view. Some of the post params can be passed over
190 Renders post's HTML view. Some of the post params can be passed over
191 kwargs for the means of caching (if we view the thread, some params
191 kwargs for the means of caching (if we view the thread, some params
192 are same for every post and don't need to be computed over and over.
192 are same for every post and don't need to be computed over and over.
193 """
193 """
194 params = self.get_view_params(*args, **kwargs)
194 params = self.get_view_params(*args, **kwargs)
195
195
196 return render_to_string('boards/post.html', params)
196 return render_to_string('boards/post.html', params)
197
197
198 def get_images(self) -> Attachment:
198 def get_images(self) -> Attachment:
199 return self.attachments.filter(mimetype__in=FILE_TYPES_IMAGE)
199 return self.attachments.filter(mimetype__in=FILE_TYPES_IMAGE)
200
200
201 def get_first_image(self) -> Attachment:
201 def get_first_image(self) -> Attachment:
202 try:
202 try:
203 return self.get_images().earliest('-id')
203 return self.get_images().earliest('-id')
204 except Attachment.DoesNotExist:
204 except Attachment.DoesNotExist:
205 return None
205 return None
206
206
207 def set_global_id(self, key_pair=None):
207 def set_global_id(self, key_pair=None):
208 """
208 """
209 Sets global id based on the given key pair. If no key pair is given,
209 Sets global id based on the given key pair. If no key pair is given,
210 default one is used.
210 default one is used.
211 """
211 """
212
212
213 if key_pair:
213 if key_pair:
214 key = key_pair
214 key = key_pair
215 else:
215 else:
216 try:
216 try:
217 key = KeyPair.objects.get(primary=True)
217 key = KeyPair.objects.get(primary=True)
218 except KeyPair.DoesNotExist:
218 except KeyPair.DoesNotExist:
219 # Do not update the global id because there is no key defined
219 # Do not update the global id because there is no key defined
220 return
220 return
221 global_id = GlobalId(key_type=key.key_type,
221 global_id = GlobalId(key_type=key.key_type,
222 key=key.public_key,
222 key=key.public_key,
223 local_id=self.id)
223 local_id=self.id)
224 global_id.save()
224 global_id.save()
225
225
226 self.global_id = global_id
226 self.global_id = global_id
227
227
228 self.save(update_fields=['global_id'])
228 self.save(update_fields=['global_id'])
229
229
230 def get_pub_time_str(self):
230 def get_pub_time_str(self):
231 return str(self.pub_time)
231 return str(self.pub_time)
232
232
233 def get_replied_ids(self):
233 def get_replied_ids(self):
234 """
234 """
235 Gets ID list of the posts that this post replies.
235 Gets ID list of the posts that this post replies.
236 """
236 """
237
237
238 raw_text = self.get_raw_text()
238 raw_text = self.get_raw_text()
239
239
240 local_replied = REGEX_REPLY.findall(raw_text)
240 local_replied = REGEX_REPLY.findall(raw_text)
241 global_replied = []
241 global_replied = []
242 for match in REGEX_GLOBAL_REPLY.findall(raw_text):
242 for match in REGEX_GLOBAL_REPLY.findall(raw_text):
243 key_type = match[0]
243 key_type = match[0]
244 key = match[1]
244 key = match[1]
245 local_id = match[2]
245 local_id = match[2]
246
246
247 try:
247 try:
248 global_id = GlobalId.objects.get(key_type=key_type,
248 global_id = GlobalId.objects.get(key_type=key_type,
249 key=key, local_id=local_id)
249 key=key, local_id=local_id)
250 for post in Post.objects.filter(global_id=global_id).only('id'):
250 for post in Post.objects.filter(global_id=global_id).only('id'):
251 global_replied.append(post.id)
251 global_replied.append(post.id)
252 except GlobalId.DoesNotExist:
252 except GlobalId.DoesNotExist:
253 pass
253 pass
254 return local_replied + global_replied
254 return local_replied + global_replied
255
255
256 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
256 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
257 include_last_update=False) -> str:
257 include_last_update=False) -> str:
258 """
258 """
259 Gets post HTML or JSON data that can be rendered on a page or used by
259 Gets post HTML or JSON data that can be rendered on a page or used by
260 API.
260 API.
261 """
261 """
262
262
263 return get_exporter(format_type).export(self, request,
263 return get_exporter(format_type).export(self, request,
264 include_last_update)
264 include_last_update)
265
265
266 def _build_url(self):
266 def _build_url(self):
267 opening = self.is_opening()
267 opening = self.is_opening()
268 opening_id = self.id if opening else self.get_thread().get_opening_post_id()
268 opening_id = self.id if opening else self.get_thread().get_opening_post_id()
269 url = reverse('thread', kwargs={'post_id': opening_id})
269 url = reverse('thread', kwargs={'post_id': opening_id})
270 if not opening:
270 if not opening:
271 url += '#' + str(self.id)
271 url += '#' + str(self.id)
272
272
273 return url
273 return url
274
274
275 def save(self, force_insert=False, force_update=False, using=None,
275 def save(self, force_insert=False, force_update=False, using=None,
276 update_fields=None):
276 update_fields=None):
277 new_post = self.id is None
277 new_post = self.id is None
278
278
279 self.uid = str(uuid.uuid4())
279 self.uid = str(uuid.uuid4())
280 if update_fields is not None and 'uid' not in update_fields:
280 if update_fields is not None and 'uid' not in update_fields:
281 update_fields += ['uid']
281 update_fields += ['uid']
282
282
283 if not new_post:
283 if not new_post:
284 thread = self.get_thread()
284 thread = self.get_thread()
285 if thread:
285 if thread:
286 thread.last_edit_time = self.last_edit_time
286 thread.last_edit_time = self.last_edit_time
287 thread.save(update_fields=['last_edit_time', 'status'])
287 thread.save(update_fields=['last_edit_time', 'status'])
288
288
289 super().save(force_insert, force_update, using, update_fields)
289 super().save(force_insert, force_update, using, update_fields)
290
290
291 if new_post:
291 if new_post:
292 self.url = self._build_url()
292 self.url = self._build_url()
293 super().save(update_fields=['url'])
293 super().save(update_fields=['url'])
294
294
295 def get_text(self) -> str:
295 def get_text(self) -> str:
296 return self._text_rendered
296 return self._text_rendered
297
297
298 def get_raw_text(self) -> str:
298 def get_raw_text(self) -> str:
299 return self.text
299 return self.text
300
300
301 def get_sync_text(self) -> str:
301 def get_sync_text(self) -> str:
302 """
302 """
303 Returns text applicable for sync. It has absolute post reflinks.
303 Returns text applicable for sync. It has absolute post reflinks.
304 """
304 """
305
305
306 replacements = dict()
306 replacements = dict()
307 for post_id in REGEX_REPLY.findall(self.get_raw_text()):
307 for post_id in REGEX_REPLY.findall(self.get_raw_text()):
308 try:
308 try:
309 absolute_post_id = str(Post.objects.get(id=post_id).global_id)
309 absolute_post_id = str(Post.objects.get(id=post_id).global_id)
310 replacements[post_id] = absolute_post_id
310 replacements[post_id] = absolute_post_id
311 except Post.DoesNotExist:
311 except Post.DoesNotExist:
312 pass
312 pass
313
313
314 text = self.get_raw_text() or ''
314 text = self.get_raw_text() or ''
315 for key in replacements:
315 for key in replacements:
316 text = text.replace('[post]{}[/post]'.format(key),
316 text = text.replace('[post]{}[/post]'.format(key),
317 '[post]{}[/post]'.format(replacements[key]))
317 '[post]{}[/post]'.format(replacements[key]))
318 text = text.replace('\r\n', '\n').replace('\r', '\n')
318 text = text.replace('\r\n', '\n').replace('\r', '\n')
319
319
320 return text
320 return text
321
321
322 def get_tripcode(self):
322 def get_tripcode(self):
323 if self.tripcode:
323 if self.tripcode:
324 return Tripcode(self.tripcode)
324 return Tripcode(self.tripcode)
325
325
326 def get_link_view(self):
326 def get_link_view(self):
327 """
327 """
328 Gets view of a reflink to the post.
328 Gets view of a reflink to the post.
329 """
329 """
330 result = '<a href="{}">&gt;&gt;{}</a>'.format(self.get_absolute_url(),
330 result = '<a href="{}">&gt;&gt;{}</a>'.format(self.get_absolute_url(),
331 self.id)
331 self.id)
332 if self.is_opening():
332 if self.is_opening():
333 result = '<b>{}</b>'.format(result)
333 result = '<b>{}</b>'.format(result)
334
334
335 return result
335 return result
336
336
337 def is_hidden(self) -> bool:
337 def is_hidden(self) -> bool:
338 return self.hidden
338 return self.hidden
339
339
340 def set_hidden(self, hidden):
340 def set_hidden(self, hidden):
341 self.hidden = hidden
341 self.hidden = hidden
342
342
343 def clear_cache(self):
343 def clear_cache(self):
344 """
344 """
345 Clears sync data (content cache, signatures etc).
345 Clears sync data (content cache, signatures etc).
346 """
346 """
347 global_id = self.global_id
347 global_id = self.global_id
348 if global_id is not None and global_id.is_local()\
348 if global_id is not None and global_id.is_local()\
349 and global_id.content is not None:
349 and global_id.content is not None:
350 global_id.clear_cache()
350 global_id.clear_cache()
351
351
352 def get_tags(self):
352 def get_tags(self):
353 return self.get_thread().get_tags()
353 return self.get_thread().get_tags()
354
354
355 def get_ip_color(self):
355 def get_ip_color(self):
356 return hashlib.md5(self.poster_ip.encode()).hexdigest()[:6]
356 return hashlib.md5(self.poster_ip.encode()).hexdigest()[:6]
357
357
358 def has_ip(self):
358 def has_ip(self):
359 return self.poster_ip != NO_IP
359 return self.poster_ip != NO_IP
360
360
@@ -1,228 +1,291 b''
1 import logging
1 import logging
2 import re
2 from datetime import datetime, timedelta, date
3 from datetime import datetime, timedelta, date
3 from datetime import time as dtime
4 from datetime import time as dtime
4
5
5 from django.core.exceptions import PermissionDenied
6 from django.core.exceptions import PermissionDenied
6 from django.db import models, transaction
7 from django.db import models, transaction
7 from django.dispatch import Signal
8 from django.dispatch import Signal
9 from django.shortcuts import redirect
8 from django.utils import timezone
10 from django.utils import timezone
9
11
10 import boards
12 import boards
11 from boards import utils
13 from boards import utils
12 from boards.abstracts.exceptions import ArchiveException
14 from boards.abstracts.exceptions import ArchiveException
13 from boards.abstracts.constants import REGEX_TAGS
15 from boards.abstracts.constants import REGEX_TAGS, REGEX_REPLY
14 from boards.mdx_neboard import Parser
16 from boards.mdx_neboard import Parser
15 from boards.models import Attachment
17 from boards.models import Attachment
16 from boards.models.attachment import StickerPack, AttachmentSticker
18 from boards.models.attachment import StickerPack, AttachmentSticker
17 from boards.models.user import Ban
19 from boards.models.user import Ban
18
20
19 __author__ = 'neko259'
21 __author__ = 'neko259'
20
22
21 POSTS_PER_DAY_RANGE = 7
23 POSTS_PER_DAY_RANGE = 7
22 NO_IP = '0.0.0.0'
24 NO_IP = '0.0.0.0'
23
25
24
26
25 post_import_deps = Signal()
27 post_import_deps = Signal()
26
28
29 FORM_TEXT = 'text'
30 FORM_TAGS = 'tags'
31
32 REFLINK_PREFIX = '>>'
33
27
34
28 class PostManager(models.Manager):
35 class PostManager(models.Manager):
29 @transaction.atomic
36 @transaction.atomic
30 def create_post(self, title: str, text: str, files=[], thread=None,
37 def create_post(self, title: str, text: str, files=[], thread=None,
31 ip=NO_IP, tags: list=None,
38 ip=NO_IP, tags: list=None,
32 tripcode='', monochrome=False, images=[],
39 tripcode='', monochrome=False, images=[],
33 file_urls=[], stickerpack=False):
40 file_urls=[], stickerpack=False):
34 """
41 """
35 Creates new post
42 Creates new post
36 """
43 """
37
44
38 if thread is not None and thread.is_archived():
45 if thread is not None and thread.is_archived():
39 raise ArchiveException('Cannot post into an archived thread')
46 raise ArchiveException('Cannot post into an archived thread')
40
47
41 if not utils.is_anonymous_mode():
48 if not utils.is_anonymous_mode():
42 is_banned = Ban.objects.filter(ip=ip).exists()
49 is_banned = Ban.objects.filter(ip=ip).exists()
43 else:
50 else:
44 is_banned = False
51 is_banned = False
45
52
46 if is_banned:
53 if is_banned:
47 raise PermissionDenied()
54 raise PermissionDenied()
48
55
49 if not tags:
56 if not tags:
50 tags = []
57 tags = []
51
58
52 posting_time = timezone.now()
59 posting_time = timezone.now()
53 new_thread = False
60 new_thread = False
54 if not thread:
61 if not thread:
55 thread = boards.models.thread.Thread.objects.create(
62 thread = boards.models.thread.Thread.objects.create(
56 bump_time=posting_time, last_edit_time=posting_time,
63 bump_time=posting_time, last_edit_time=posting_time,
57 monochrome=monochrome, stickerpack=stickerpack)
64 monochrome=monochrome, stickerpack=stickerpack)
58 list(map(thread.tags.add, tags))
65 list(map(thread.tags.add, tags))
59 new_thread = True
66 new_thread = True
60
67
61 pre_text = Parser().preparse(text)
68 pre_text = Parser().preparse(text)
62
69
63 post = self.create(title=title,
70 post = self.create(title=title,
64 text=pre_text,
71 text=pre_text,
65 pub_time=posting_time,
72 pub_time=posting_time,
66 poster_ip=ip,
73 poster_ip=ip,
67 thread=thread,
74 thread=thread,
68 last_edit_time=posting_time,
75 last_edit_time=posting_time,
69 tripcode=tripcode,
76 tripcode=tripcode,
70 opening=new_thread)
77 opening=new_thread)
71
78
72 logger = logging.getLogger('boards.post.create')
79 logger = logging.getLogger('boards.post.create')
73
80
74 logger.info('Created post [{}] with text [{}] by {}'.format(post,
81 logger.info('Created post [{}] with text [{}] by {}'.format(post,
75 post.get_text(),post.poster_ip))
82 post.get_text(),post.poster_ip))
76
83
77 for file in files:
84 for file in files:
78 self._add_file_to_post(file, post)
85 self._add_file_to_post(file, post)
79 for image in images:
86 for image in images:
80 post.attachments.add(image)
87 post.attachments.add(image)
81 for file_url in file_urls:
88 for file_url in file_urls:
82 post.attachments.add(Attachment.objects.create_from_url(file_url))
89 post.attachments.add(Attachment.objects.create_from_url(file_url))
83
90
84 post.set_global_id()
91 post.set_global_id()
85
92
86 # Thread needs to be bumped only when the post is already created
93 # Thread needs to be bumped only when the post is already created
87 if not new_thread:
94 if not new_thread:
88 thread.last_edit_time = posting_time
95 thread.last_edit_time = posting_time
89 thread.bump()
96 thread.bump()
90 thread.save()
97 thread.save()
91
98
92 self._create_stickers(post)
99 self._create_stickers(post)
93
100
94 return post
101 return post
95
102
96 def delete_posts_by_ip(self, ip):
103 def delete_posts_by_ip(self, ip):
97 """
104 """
98 Deletes all posts of the author with same IP
105 Deletes all posts of the author with same IP
99 """
106 """
100
107
101 posts = self.filter(poster_ip=ip)
108 posts = self.filter(poster_ip=ip)
102 for post in posts:
109 for post in posts:
103 post.delete()
110 post.delete()
104
111
105 @utils.cached_result()
112 @utils.cached_result()
106 def get_posts_per_day(self) -> float:
113 def get_posts_per_day(self) -> float:
107 """
114 """
108 Gets average count of posts per day for the last 7 days
115 Gets average count of posts per day for the last 7 days
109 """
116 """
110
117
111 day_end = date.today()
118 day_end = date.today()
112 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
119 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
113
120
114 day_time_start = timezone.make_aware(datetime.combine(
121 day_time_start = timezone.make_aware(datetime.combine(
115 day_start, dtime()), timezone.get_current_timezone())
122 day_start, dtime()), timezone.get_current_timezone())
116 day_time_end = timezone.make_aware(datetime.combine(
123 day_time_end = timezone.make_aware(datetime.combine(
117 day_end, dtime()), timezone.get_current_timezone())
124 day_end, dtime()), timezone.get_current_timezone())
118
125
119 posts_per_period = float(self.filter(
126 posts_per_period = float(self.filter(
120 pub_time__lte=day_time_end,
127 pub_time__lte=day_time_end,
121 pub_time__gte=day_time_start).count())
128 pub_time__gte=day_time_start).count())
122
129
123 ppd = posts_per_period / POSTS_PER_DAY_RANGE
130 ppd = posts_per_period / POSTS_PER_DAY_RANGE
124
131
125 return ppd
132 return ppd
126
133
127 def get_post_per_days(self, days) -> int:
134 def get_post_per_days(self, days) -> int:
128 day_end = date.today() + timedelta(1)
135 day_end = date.today() + timedelta(1)
129 day_start = day_end - timedelta(days)
136 day_start = day_end - timedelta(days)
130
137
131 day_time_start = timezone.make_aware(datetime.combine(
138 day_time_start = timezone.make_aware(datetime.combine(
132 day_start, dtime()), timezone.get_current_timezone())
139 day_start, dtime()), timezone.get_current_timezone())
133 day_time_end = timezone.make_aware(datetime.combine(
140 day_time_end = timezone.make_aware(datetime.combine(
134 day_end, dtime()), timezone.get_current_timezone())
141 day_end, dtime()), timezone.get_current_timezone())
135
142
136 return self.filter(
143 return self.filter(
137 pub_time__lte=day_time_end,
144 pub_time__lte=day_time_end,
138 pub_time__gte=day_time_start).count()
145 pub_time__gte=day_time_start).count()
139
146
140 @transaction.atomic
147 @transaction.atomic
141 def import_post(self, title: str, text: str, pub_time: str, global_id,
148 def import_post(self, title: str, text: str, pub_time: str, global_id,
142 opening_post=None, tags=list(), files=list(),
149 opening_post=None, tags=list(), files=list(),
143 file_urls=list(), tripcode=None, last_edit_time=None):
150 file_urls=list(), tripcode=None, last_edit_time=None):
144 is_opening = opening_post is None
151 is_opening = opening_post is None
145 if is_opening:
152 if is_opening:
146 thread = boards.models.thread.Thread.objects.create(
153 thread = boards.models.thread.Thread.objects.create(
147 bump_time=pub_time, last_edit_time=pub_time)
154 bump_time=pub_time, last_edit_time=pub_time)
148 list(map(thread.tags.add, tags))
155 list(map(thread.tags.add, tags))
149 else:
156 else:
150 thread = opening_post.get_thread()
157 thread = opening_post.get_thread()
151
158
152 post = self.create(title=title,
159 post = self.create(title=title,
153 text=text,
160 text=text,
154 pub_time=pub_time,
161 pub_time=pub_time,
155 poster_ip=NO_IP,
162 poster_ip=NO_IP,
156 last_edit_time=last_edit_time or pub_time,
163 last_edit_time=last_edit_time or pub_time,
157 global_id=global_id,
164 global_id=global_id,
158 opening=is_opening,
165 opening=is_opening,
159 thread=thread,
166 thread=thread,
160 tripcode=tripcode)
167 tripcode=tripcode)
161
168
162 for file in files:
169 for file in files:
163 self._add_file_to_post(file, post)
170 self._add_file_to_post(file, post)
164 for file_url in file_urls:
171 for file_url in file_urls:
165 post.attachments.add(Attachment.objects.create_from_url(file_url))
172 post.attachments.add(Attachment.objects.create_from_url(file_url))
166
173
167 url_to_post = '[post]{}[/post]'.format(str(global_id))
174 url_to_post = '[post]{}[/post]'.format(str(global_id))
168 replies = self.filter(text__contains=url_to_post)
175 replies = self.filter(text__contains=url_to_post)
169 for reply in replies:
176 for reply in replies:
170 post_import_deps.send(reply)
177 post_import_deps.send(reply)
171
178
172 @transaction.atomic
179 @transaction.atomic
173 def update_post(self, post, title: str, text: str, pub_time: str,
180 def update_post(self, post, title: str, text: str, pub_time: str,
174 tags=list(), files=list(), file_urls=list(), tripcode=None):
181 tags=list(), files=list(), file_urls=list(), tripcode=None):
175 post.title = title
182 post.title = title
176 post.text = text
183 post.text = text
177 post.pub_time = pub_time
184 post.pub_time = pub_time
178 post.tripcode = tripcode
185 post.tripcode = tripcode
179 post.save()
186 post.save()
180
187
181 post.clear_cache()
188 post.clear_cache()
182
189
183 post.attachments.clear()
190 post.attachments.clear()
184 for file in files:
191 for file in files:
185 self._add_file_to_post(file, post)
192 self._add_file_to_post(file, post)
186 for file_url in file_urls:
193 for file_url in file_urls:
187 post.attachments.add(Attachment.objects.create_from_url(file_url))
194 post.attachments.add(Attachment.objects.create_from_url(file_url))
188
195
189 thread = post.get_thread()
196 thread = post.get_thread()
190 thread.tags.clear()
197 thread.tags.clear()
191 list(map(thread.tags.add, tags))
198 list(map(thread.tags.add, tags))
192
199
200 def create_from_form(self, request, form, opening_post, html_response=True):
201 ip = utils.get_client_ip(request)
202
203 data = form.cleaned_data
204
205 title = form.get_title()
206 text = data[FORM_TEXT]
207 files = form.get_files()
208 file_urls = form.get_file_urls()
209 images = form.get_images()
210
211 text = self._remove_invalid_links(text)
212
213 if opening_post:
214 post_thread = opening_post.get_thread()
215 monochrome = False
216 stickerpack = False
217 tags = []
218 else:
219 tags = data[FORM_TAGS]
220 monochrome = form.is_monochrome()
221 stickerpack = form.is_stickerpack()
222 post_thread = None
223
224 post = self.create_post(title=title, text=text, files=files,
225 thread=post_thread, ip=ip,
226 tripcode=form.get_tripcode(),
227 images=images, file_urls=file_urls,
228 monochrome=monochrome,
229 stickerpack=stickerpack, tags=tags)
230
231 if form.is_subscribe():
232 from boards.abstracts.settingsmanager import get_settings_manager
233 settings_manager = get_settings_manager(request)
234 settings_manager.add_or_read_fav_thread(
235 post_thread.get_opening_post())
236
237 if html_response:
238 return redirect(post.get_absolute_url())
239 else:
240 return post
241
193 def _add_file_to_post(self, file, post):
242 def _add_file_to_post(self, file, post):
194 post.attachments.add(Attachment.objects.create_with_hash(file))
243 post.attachments.add(Attachment.objects.create_with_hash(file))
195
244
196 def _create_stickers(self, post):
245 def _create_stickers(self, post):
197 thread = post.get_thread()
246 thread = post.get_thread()
198 stickerpack_thread = thread.is_stickerpack()
247 stickerpack_thread = thread.is_stickerpack()
199 if stickerpack_thread:
248 if stickerpack_thread:
200 logger = logging.getLogger('boards.stickers')
249 logger = logging.getLogger('boards.stickers')
201 if not post.is_opening():
250 if not post.is_opening():
202 has_title = len(post.title) > 0
251 has_title = len(post.title) > 0
203 has_one_attachment = post.attachments.count() == 1
252 has_one_attachment = post.attachments.count() == 1
204 opening_post = thread.get_opening_post()
253 opening_post = thread.get_opening_post()
205 valid_name = REGEX_TAGS.match(post.title)
254 valid_name = REGEX_TAGS.match(post.title)
206 if has_title and has_one_attachment and valid_name:
255 if has_title and has_one_attachment and valid_name:
207 existing_sticker = AttachmentSticker.objects.filter(
256 existing_sticker = AttachmentSticker.objects.filter(
208 name=post.get_title()).first()
257 name=post.get_title()).first()
209 attachment = post.attachments.first()
258 attachment = post.attachments.first()
210 if existing_sticker:
259 if existing_sticker:
211 existing_sticker.attachment = attachment
260 existing_sticker.attachment = attachment
212 existing_sticker.save()
261 existing_sticker.save()
213 logger.info('Updated sticker {} with new attachment'.format(existing_sticker))
262 logger.info('Updated sticker {} with new attachment'.format(existing_sticker))
214 else:
263 else:
215 try:
264 try:
216 stickerpack = StickerPack.objects.get(
265 stickerpack = StickerPack.objects.get(
217 name=opening_post.get_title(), tripcode=post.tripcode)
266 name=opening_post.get_title(), tripcode=post.tripcode)
218 sticker = AttachmentSticker.objects.create(
267 sticker = AttachmentSticker.objects.create(
219 stickerpack=stickerpack, name=post.get_title(),
268 stickerpack=stickerpack, name=post.get_title(),
220 attachment=attachment)
269 attachment=attachment)
221 logger.info('Created sticker {}'.format(sticker))
270 logger.info('Created sticker {}'.format(sticker))
222 except StickerPack.DoesNotExist:
271 except StickerPack.DoesNotExist:
223 pass
272 pass
224 else:
273 else:
225 stickerpack, created = StickerPack.objects.get_or_create(
274 stickerpack, created = StickerPack.objects.get_or_create(
226 name=post.get_title(), tripcode=post.tripcode)
275 name=post.get_title(), tripcode=post.tripcode)
227 if created:
276 if created:
228 logger.info('Created stickerpack {}'.format(stickerpack))
277 logger.info('Created stickerpack {}'.format(stickerpack))
278
279 def _remove_invalid_links(self, text):
280 """
281 Replace invalid links in posts so that they won't be parsed.
282 Invalid links are links to non-existent posts
283 """
284
285 for reply_number in re.finditer(REGEX_REPLY, text):
286 post_id = reply_number.group(1)
287 post = self.filter(id=post_id)
288 if not post.exists():
289 text = text.replace(REFLINK_PREFIX + post_id, post_id)
290
291 return text
@@ -1,143 +1,143 b''
1 import re
1 import re
2 import os
2 import os
3 import logging
3 import logging
4
4
5 from django.db.models.signals import post_save, pre_save, pre_delete, \
5 from django.db.models.signals import post_save, pre_save, pre_delete, \
6 post_delete
6 post_delete
7 from django.dispatch import receiver
7 from django.dispatch import receiver
8 from django.utils import timezone
8 from django.utils import timezone
9
9
10 from boards import thumbs
10 from boards import thumbs
11 from boards.mdx_neboard import get_parser
11 from boards.mdx_neboard import get_parser
12
12
13 from boards.models import Post, GlobalId, Attachment, Thread
13 from boards.models import Post, GlobalId, Attachment, Thread
14 from boards.models.attachment import StickerPack, AttachmentSticker
14 from boards.models.attachment import StickerPack, AttachmentSticker
15 from boards.models.attachment.viewers import FILE_TYPES_IMAGE
15 from boards.models.attachment.viewers import FILE_TYPES_IMAGE
16 from boards.models.post import REGEX_NOTIFICATION, REGEX_REPLY,\
16 from boards.models.post import REGEX_NOTIFICATION, REGEX_GLOBAL_REPLY
17 REGEX_GLOBAL_REPLY
17 from boards.abstracts.constants import REGEX_REPLY
18 from boards.models.post.manager import post_import_deps
18 from boards.models.post.manager import post_import_deps
19 from boards.models.user import Notification
19 from boards.models.user import Notification
20 from neboard.settings import MEDIA_ROOT
20 from neboard.settings import MEDIA_ROOT
21
21
22
22
23 THUMB_SIZES = ((200, 150),)
23 THUMB_SIZES = ((200, 150),)
24
24
25
25
26 @receiver(post_save, sender=Post)
26 @receiver(post_save, sender=Post)
27 def connect_replies(instance, **kwargs):
27 def connect_replies(instance, **kwargs):
28 if not kwargs['update_fields']:
28 if not kwargs['update_fields']:
29 for reply_number in re.finditer(REGEX_REPLY, instance.get_raw_text()):
29 for reply_number in re.finditer(REGEX_REPLY, instance.get_raw_text()):
30 post_id = reply_number.group(1)
30 post_id = reply_number.group(1)
31
31
32 try:
32 try:
33 referenced_post = Post.objects.get(id=post_id)
33 referenced_post = Post.objects.get(id=post_id)
34
34
35 if not referenced_post.referenced_posts.filter(
35 if not referenced_post.referenced_posts.filter(
36 id=instance.id).exists():
36 id=instance.id).exists():
37 referenced_post.referenced_posts.add(instance)
37 referenced_post.referenced_posts.add(instance)
38 referenced_post.last_edit_time = instance.pub_time
38 referenced_post.last_edit_time = instance.pub_time
39 referenced_post.build_refmap()
39 referenced_post.build_refmap()
40 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
40 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
41 except Post.DoesNotExist:
41 except Post.DoesNotExist:
42 pass
42 pass
43
43
44
44
45 @receiver(post_save, sender=Post)
45 @receiver(post_save, sender=Post)
46 @receiver(post_import_deps, sender=Post)
46 @receiver(post_import_deps, sender=Post)
47 def connect_global_replies(instance, **kwargs):
47 def connect_global_replies(instance, **kwargs):
48 if not kwargs['update_fields']:
48 if not kwargs['update_fields']:
49 for reply_number in re.finditer(REGEX_GLOBAL_REPLY, instance.get_raw_text()):
49 for reply_number in re.finditer(REGEX_GLOBAL_REPLY, instance.get_raw_text()):
50 key_type = reply_number.group(1)
50 key_type = reply_number.group(1)
51 key = reply_number.group(2)
51 key = reply_number.group(2)
52 local_id = reply_number.group(3)
52 local_id = reply_number.group(3)
53
53
54 try:
54 try:
55 global_id = GlobalId.objects.get(key_type=key_type, key=key,
55 global_id = GlobalId.objects.get(key_type=key_type, key=key,
56 local_id=local_id)
56 local_id=local_id)
57 referenced_post = Post.objects.get(global_id=global_id)
57 referenced_post = Post.objects.get(global_id=global_id)
58 referenced_post.referenced_posts.add(instance)
58 referenced_post.referenced_posts.add(instance)
59 referenced_post.last_edit_time = instance.pub_time
59 referenced_post.last_edit_time = instance.pub_time
60 referenced_post.build_refmap()
60 referenced_post.build_refmap()
61 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
61 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
62 except (GlobalId.DoesNotExist, Post.DoesNotExist):
62 except (GlobalId.DoesNotExist, Post.DoesNotExist):
63 pass
63 pass
64
64
65
65
66 @receiver(post_save, sender=Post)
66 @receiver(post_save, sender=Post)
67 def connect_notifications(instance, **kwargs):
67 def connect_notifications(instance, **kwargs):
68 if not kwargs['update_fields']:
68 if not kwargs['update_fields']:
69 for reply_number in re.finditer(REGEX_NOTIFICATION, instance.get_raw_text()):
69 for reply_number in re.finditer(REGEX_NOTIFICATION, instance.get_raw_text()):
70 user_name = reply_number.group(1).lower()
70 user_name = reply_number.group(1).lower()
71 Notification.objects.get_or_create(name=user_name, post=instance)
71 Notification.objects.get_or_create(name=user_name, post=instance)
72
72
73
73
74 @receiver(pre_save, sender=Post)
74 @receiver(pre_save, sender=Post)
75 @receiver(post_import_deps, sender=Post)
75 @receiver(post_import_deps, sender=Post)
76 def parse_text(instance, **kwargs):
76 def parse_text(instance, **kwargs):
77 instance._text_rendered = get_parser().parse(instance.get_raw_text())
77 instance._text_rendered = get_parser().parse(instance.get_raw_text())
78
78
79
79
80 @receiver(pre_delete, sender=Post)
80 @receiver(pre_delete, sender=Post)
81 def delete_attachments(instance, **kwargs):
81 def delete_attachments(instance, **kwargs):
82 for attachment in instance.attachments.all():
82 for attachment in instance.attachments.all():
83 attachment_refs_count = attachment.attachment_posts.count()
83 attachment_refs_count = attachment.attachment_posts.count()
84 if attachment_refs_count == 1:
84 if attachment_refs_count == 1:
85 attachment.delete()
85 attachment.delete()
86
86
87
87
88 @receiver(post_delete, sender=Post)
88 @receiver(post_delete, sender=Post)
89 def update_thread_on_delete(instance, **kwargs):
89 def update_thread_on_delete(instance, **kwargs):
90 thread = instance.get_thread()
90 thread = instance.get_thread()
91 thread.last_edit_time = timezone.now()
91 thread.last_edit_time = timezone.now()
92 thread.save()
92 thread.save()
93
93
94
94
95 @receiver(post_delete, sender=Post)
95 @receiver(post_delete, sender=Post)
96 def delete_global_id(instance, **kwargs):
96 def delete_global_id(instance, **kwargs):
97 if instance.global_id and instance.global_id.id:
97 if instance.global_id and instance.global_id.id:
98 instance.global_id.delete()
98 instance.global_id.delete()
99
99
100
100
101 @receiver(post_save, sender=Attachment)
101 @receiver(post_save, sender=Attachment)
102 def generate_thumb(instance, **kwargs):
102 def generate_thumb(instance, **kwargs):
103 if instance.mimetype in FILE_TYPES_IMAGE:
103 if instance.mimetype in FILE_TYPES_IMAGE:
104 for size in THUMB_SIZES:
104 for size in THUMB_SIZES:
105 (w, h) = size
105 (w, h) = size
106 split = instance.file.name.rsplit('.', 1)
106 split = instance.file.name.rsplit('.', 1)
107 thumb_name = '%s.%sx%s.%s' % (split[0], w, h, split[1])
107 thumb_name = '%s.%sx%s.%s' % (split[0], w, h, split[1])
108
108
109 if not instance.file.storage.exists(thumb_name):
109 if not instance.file.storage.exists(thumb_name):
110 # you can use another thumbnailing function if you like
110 # you can use another thumbnailing function if you like
111 thumb_content = thumbs.generate_thumb(instance.file, size, split[1])
111 thumb_content = thumbs.generate_thumb(instance.file, size, split[1])
112
112
113 thumb_name_ = instance.file.storage.save(thumb_name, thumb_content)
113 thumb_name_ = instance.file.storage.save(thumb_name, thumb_content)
114
114
115 if not thumb_name == thumb_name_:
115 if not thumb_name == thumb_name_:
116 raise ValueError(
116 raise ValueError(
117 'There is already a file named %s' % thumb_name_)
117 'There is already a file named %s' % thumb_name_)
118
118
119
119
120 @receiver(pre_delete, sender=Post)
120 @receiver(pre_delete, sender=Post)
121 def rebuild_refmap(instance, **kwargs):
121 def rebuild_refmap(instance, **kwargs):
122 for referenced_post in instance.refposts.all():
122 for referenced_post in instance.refposts.all():
123 referenced_post.build_refmap(excluded_ids=[instance.id])
123 referenced_post.build_refmap(excluded_ids=[instance.id])
124 referenced_post.save(update_fields=['refmap'])
124 referenced_post.save(update_fields=['refmap'])
125
125
126
126
127 @receiver(post_delete, sender=Attachment)
127 @receiver(post_delete, sender=Attachment)
128 def delete_file(instance, **kwargs):
128 def delete_file(instance, **kwargs):
129 if instance.is_internal():
129 if instance.is_internal():
130 file = MEDIA_ROOT + instance.file.name
130 file = MEDIA_ROOT + instance.file.name
131 try:
131 try:
132 os.remove(file)
132 os.remove(file)
133 except FileNotFoundError:
133 except FileNotFoundError:
134 pass
134 pass
135 if instance.mimetype in FILE_TYPES_IMAGE:
135 if instance.mimetype in FILE_TYPES_IMAGE:
136 for size in THUMB_SIZES:
136 for size in THUMB_SIZES:
137 file_name_parts = instance.file.name.split('.')
137 file_name_parts = instance.file.name.split('.')
138 thumb_file = MEDIA_ROOT + '{}.{}x{}.{}'.format(file_name_parts[0], size[0], size[1], file_name_parts[1])
138 thumb_file = MEDIA_ROOT + '{}.{}x{}.{}'.format(file_name_parts[0], size[0], size[1], file_name_parts[1])
139 try:
139 try:
140 os.remove(thumb_file)
140 os.remove(thumb_file)
141 except FileNotFoundError:
141 except FileNotFoundError:
142 pass
142 pass
143
143
@@ -1,90 +1,90 b''
1 from django.conf.urls import url
1 from django.conf.urls import url
2 from django.urls import path
2 from django.urls import path
3 from django.views.i18n import JavaScriptCatalog
3 from django.views.i18n import JavaScriptCatalog
4
4
5 from boards import views
5 from boards import views
6 from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed
6 from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed
7 from boards.views import api, tag_threads, all_threads, settings, feed, stickers
7 from boards.views import api, tag_threads, all_threads, settings, feed, stickers, thread, banned
8 from boards.views.authors import AuthorsView
8 from boards.views.authors import AuthorsView
9 from boards.views.landing import LandingView
9 from boards.views.landing import LandingView
10 from boards.views.notifications import NotificationView
10 from boards.views.notifications import NotificationView
11 from boards.views.preview import PostPreviewView
11 from boards.views.preview import PostPreviewView
12 from boards.views.random import RandomImageView
12 from boards.views.random import RandomImageView
13 from boards.views.search import BoardSearchView
13 from boards.views.search import BoardSearchView
14 from boards.views.static import StaticPageView
14 from boards.views.static import StaticPageView
15 from boards.views.sync import get_post_sync_data, response_get, response_list
15 from boards.views.sync import get_post_sync_data, response_get, response_list
16 from boards.views.tag_gallery import TagGalleryView
16 from boards.views.tag_gallery import TagGalleryView
17 from boards.views.utils import UtilsView
17 from boards.views.utils import UtilsView
18
18
19
19
20 urlpatterns = [
20 urlpatterns = [
21 # /boards/
21 # /boards/
22 path('all/', all_threads.AllThreadsView.as_view(), name='index'),
22 path('all/', all_threads.AllThreadsView.as_view(), name='index'),
23
23
24 # /boards/tag/tag_name/
24 # /boards/tag/tag_name/
25 url(r'^tag/(?P<tag_name>[\w\d\']+)/$', tag_threads.TagView.as_view(),
25 url(r'^tag/(?P<tag_name>[\w\d\']+)/$', tag_threads.TagView.as_view(),
26 name='tag'),
26 name='tag'),
27 url(r'^tag/(?P<tag_name>[\w\d\']+)/gallery/$', TagGalleryView.as_view(), name='tag_gallery'),
27 url(r'^tag/(?P<tag_name>[\w\d\']+)/gallery/$', TagGalleryView.as_view(), name='tag_gallery'),
28
28
29 # /boards/thread/
29 # /boards/thread/
30 path('thread/<int:post_id>/', views.thread.NormalThreadView.as_view(),
30 path('thread/<int:post_id>/', views.thread.NormalThreadView.as_view(),
31 name='thread'),
31 name='thread'),
32 path('thread/<int:post_id>/mode/gallery/', views.thread.GalleryThreadView.as_view(),
32 path('thread/<int:post_id>/mode/gallery/', views.thread.GalleryThreadView.as_view(),
33 name='thread_gallery'),
33 name='thread_gallery'),
34 path('thread/<int:post_id>/mode/tree/', views.thread.TreeThreadView.as_view(),
34 path('thread/<int:post_id>/mode/tree/', views.thread.TreeThreadView.as_view(),
35 name='thread_tree'),
35 name='thread_tree'),
36 # /feed/
36 # /feed/
37 path('feed/', views.feed.FeedView.as_view(), name='feed'),
37 path('feed/', views.feed.FeedView.as_view(), name='feed'),
38
38
39 path('settings/', settings.SettingsView.as_view(), name='settings'),
39 path('settings/', settings.SettingsView.as_view(), name='settings'),
40 path('stickers/', stickers.AliasesView.as_view(), name='stickers'),
40 path('stickers/', stickers.AliasesView.as_view(), name='stickers'),
41 path('stickers/<str:category>/', stickers.AliasesView.as_view(), name='stickers'),
41 path('stickers/<str:category>/', stickers.AliasesView.as_view(), name='stickers'),
42 path('authors/', AuthorsView.as_view(), name='authors'),
42 path('authors/', AuthorsView.as_view(), name='authors'),
43
43
44 path('banned/', views.banned.BannedView.as_view(), name='banned'),
44 path('banned/', views.banned.BannedView.as_view(), name='banned'),
45 path('staticpage/<str:name>/', StaticPageView.as_view(), name='staticpage'),
45 path('staticpage/<str:name>/', StaticPageView.as_view(), name='staticpage'),
46
46
47 path('random/', RandomImageView.as_view(), name='random'),
47 path('random/', RandomImageView.as_view(), name='random'),
48 path('search/', BoardSearchView.as_view(), name='search'),
48 path('search/', BoardSearchView.as_view(), name='search'),
49 path('', LandingView.as_view(), name='landing'),
49 path('', LandingView.as_view(), name='landing'),
50 path('utils', UtilsView.as_view(), name='utils'),
50 path('utils', UtilsView.as_view(), name='utils'),
51
51
52 # RSS feeds
52 # RSS feeds
53 path('rss/', AllThreadsFeed()),
53 path('rss/', AllThreadsFeed()),
54 path('all/rss/', AllThreadsFeed()),
54 path('all/rss/', AllThreadsFeed()),
55 url(r'^tag/(?P<tag_name>\w+)/rss/$', TagThreadsFeed()),
55 url(r'^tag/(?P<tag_name>\w+)/rss/$', TagThreadsFeed()),
56 path('thread/<int:post_id>/rss/', ThreadPostsFeed()),
56 path('thread/<int:post_id>/rss/', ThreadPostsFeed()),
57
57
58 # i18n
58 # i18n
59 path('jsi18n/', JavaScriptCatalog.as_view(packages=['boards']), name='js_info_dict'),
59 path('jsi18n/', JavaScriptCatalog.as_view(packages=['boards']), name='js_info_dict'),
60
60
61 # API
61 # API
62 url(r'^api/post/(?P<post_id>\d+)/$', api.get_post, name="get_post"),
62 url(r'^api/post/(?P<post_id>\d+)/$', api.get_post, name="get_post"),
63 url(r'^api/diff_thread/$', api.api_get_threaddiff, name="get_thread_diff"),
63 url(r'^api/diff_thread/$', api.api_get_threaddiff, name="get_thread_diff"),
64 url(r'^api/threads/(?P<count>\w+)/$', api.api_get_threads,
64 url(r'^api/threads/(?P<count>\w+)/$', api.api_get_threads,
65 name='get_threads'),
65 name='get_threads'),
66 url(r'^api/tags/$', api.api_get_tags, name='get_tags'),
66 url(r'^api/tags/$', api.api_get_tags, name='get_tags'),
67 url(r'^api/thread/(?P<opening_post_id>\w+)/$', api.api_get_thread_posts,
67 url(r'^api/thread/(?P<opening_post_id>\w+)/$', api.api_get_thread_posts,
68 name='get_thread'),
68 name='get_thread'),
69 url(r'^api/add_post/(?P<opening_post_id>\w+)/$', api.api_add_post,
69 url(r'^api/add_post/(?P<opening_post_id>\w+)/$', api.api_add_post,
70 name='add_post'),
70 name='add_post'),
71 url(r'^api/notifications/(?P<username>\w+)/$', api.api_get_notifications,
71 url(r'^api/notifications/(?P<username>\w+)/$', api.api_get_notifications,
72 name='api_notifications'),
72 name='api_notifications'),
73 url(r'^api/preview/$', api.api_get_preview, name='preview'),
73 url(r'^api/preview/$', api.api_get_preview, name='preview'),
74 url(r'^api/new_posts/$', api.api_get_new_posts, name='new_posts'),
74 url(r'^api/new_posts/$', api.api_get_new_posts, name='new_posts'),
75 url(r'^api/stickers/$', api.api_get_stickers, name='get_stickers'),
75 url(r'^api/stickers/$', api.api_get_stickers, name='get_stickers'),
76
76
77 # Sync protocol API
77 # Sync protocol API
78 url(r'^api/sync/list/$', response_list, name='api_sync_list'),
78 url(r'^api/sync/list/$', response_list, name='api_sync_list'),
79 url(r'^api/sync/get/$', response_get, name='api_sync_get'),
79 url(r'^api/sync/get/$', response_get, name='api_sync_get'),
80
80
81 # Notifications
81 # Notifications
82 path('notifications/<str:username>/', NotificationView.as_view(), name='notifications'),
82 path('notifications/<str:username>/', NotificationView.as_view(), name='notifications'),
83 path('notifications/', NotificationView.as_view(), name='notifications'),
83 path('notifications/', NotificationView.as_view(), name='notifications'),
84
84
85 # Post preview
85 # Post preview
86 path('preview/', PostPreviewView.as_view(), name='preview'),
86 path('preview/', PostPreviewView.as_view(), name='preview'),
87 path('post_xml/<int:post_id>', get_post_sync_data,
87 path('post_xml/<int:post_id>', get_post_sync_data,
88 name='post_sync_data'),
88 name='post_sync_data'),
89 ]
89 ]
90
90
@@ -1,178 +1,132 b''
1 from django.core.paginator import EmptyPage
1 from django.core.paginator import EmptyPage
2 from django.db import transaction
3 from django.http import Http404
2 from django.http import Http404
4 from django.shortcuts import render, redirect
3 from django.shortcuts import render, redirect
5 from django.urls import reverse
4 from django.urls import reverse
6 from django.utils.decorators import method_decorator
5 from django.utils.decorators import method_decorator
7 from django.views.decorators.csrf import csrf_protect
6 from django.views.decorators.csrf import csrf_protect
8
7
9 from boards import utils, settings
8 from boards import settings
10 from boards.abstracts.paginator import get_paginator
9 from boards.abstracts.paginator import get_paginator
11 from boards.abstracts.settingsmanager import get_settings_manager, \
10 from boards.abstracts.settingsmanager import get_settings_manager, \
12 SETTING_ONLY_FAVORITES
11 SETTING_ONLY_FAVORITES
13 from boards.forms import ThreadForm, PlainErrorList
12 from boards.forms import ThreadForm, PlainErrorList
14 from boards.models import Post, Thread, Ban
13 from boards.models import Post, Thread
15 from boards.views.banned import BannedView
16 from boards.views.base import BaseBoardView, CONTEXT_FORM
14 from boards.views.base import BaseBoardView, CONTEXT_FORM
17 from boards.views.mixins import FileUploadMixin, PaginatedMixin, \
15 from boards.views.mixins import FileUploadMixin, PaginatedMixin, \
18 DispatcherMixin, PARAMETER_METHOD
16 DispatcherMixin, PARAMETER_METHOD
19 from boards.views.posting_mixin import PostMixin
20
17
21 FORM_TAGS = 'tags'
18 FORM_TAGS = 'tags'
22 FORM_TEXT = 'text'
19 FORM_TEXT = 'text'
23 FORM_TITLE = 'title'
20 FORM_TITLE = 'title'
24 FORM_IMAGE = 'image'
21 FORM_IMAGE = 'image'
25 FORM_THREADS = 'threads'
22 FORM_THREADS = 'threads'
26
23
27 TAG_DELIMITER = ' '
24 TAG_DELIMITER = ' '
28
25
29 PARAMETER_CURRENT_PAGE = 'current_page'
26 PARAMETER_CURRENT_PAGE = 'current_page'
30 PARAMETER_PAGINATOR = 'paginator'
27 PARAMETER_PAGINATOR = 'paginator'
31 PARAMETER_THREADS = 'threads'
28 PARAMETER_THREADS = 'threads'
32 PARAMETER_ADDITIONAL = 'additional_params'
29 PARAMETER_ADDITIONAL = 'additional_params'
33 PARAMETER_MAX_FILE_SIZE = 'max_file_size'
30 PARAMETER_MAX_FILE_SIZE = 'max_file_size'
34 PARAMETER_RSS_URL = 'rss_url'
31 PARAMETER_RSS_URL = 'rss_url'
35 PARAMETER_MAX_FILES = 'max_files'
32 PARAMETER_MAX_FILES = 'max_files'
36
33
37 TEMPLATE = 'boards/all_threads.html'
34 TEMPLATE = 'boards/all_threads.html'
38 DEFAULT_PAGE = 1
35 DEFAULT_PAGE = 1
39
36
40 FORM_TAGS = 'tags'
41
37
42
38 class AllThreadsView(FileUploadMixin, BaseBoardView, PaginatedMixin,
43 class AllThreadsView(PostMixin, FileUploadMixin, BaseBoardView, PaginatedMixin, DispatcherMixin):
39 DispatcherMixin):
44
40
45 tag_name = ''
41 tag_name = ''
46
42
47 def __init__(self):
43 def __init__(self):
48 self.settings_manager = None
44 self.settings_manager = None
49 super(AllThreadsView, self).__init__()
45 super(AllThreadsView, self).__init__()
50
46
51 @method_decorator(csrf_protect)
47 @method_decorator(csrf_protect)
52 def get(self, request, form: ThreadForm=None):
48 def get(self, request, form: ThreadForm=None):
53 page = request.GET.get('page', DEFAULT_PAGE)
49 page = request.GET.get('page', DEFAULT_PAGE)
54
50
55 params = self.get_context_data(request=request)
51 params = self.get_context_data(request=request)
56
52
57 if not form:
53 if not form:
58 form = ThreadForm(error_class=PlainErrorList,
54 form = ThreadForm(error_class=PlainErrorList,
59 initial={FORM_TAGS: self.tag_name})
55 initial={FORM_TAGS: self.tag_name})
60
56
61 self.settings_manager = get_settings_manager(request)
57 self.settings_manager = get_settings_manager(request)
62
58
63 threads = self.get_threads()
59 threads = self.get_threads()
64
60
65 order = request.GET.get('order', 'bump')
61 order = request.GET.get('order', 'bump')
66 if order == 'bump':
62 if order == 'bump':
67 threads = threads.order_by('-bump_time')
63 threads = threads.order_by('-bump_time')
68 else:
64 else:
69 threads = threads.filter(replies__opening=True)\
65 threads = threads.filter(replies__opening=True)\
70 .order_by('-replies__pub_time')
66 .order_by('-replies__pub_time')
71 filter = request.GET.get('filter')
67 filter = request.GET.get('filter')
72 threads = threads.distinct()
68 threads = threads.distinct()
73
69
74 paginator = get_paginator(threads,
70 paginator = get_paginator(threads,
75 settings.get_int('View', 'ThreadsPerPage'))
71 settings.get_int('View', 'ThreadsPerPage'))
76 paginator.current_page = int(page)
72 paginator.current_page = int(page)
77
73
78 try:
74 try:
79 threads = paginator.page(page).object_list
75 threads = paginator.page(page).object_list
80 except EmptyPage:
76 except EmptyPage:
81 raise Http404()
77 raise Http404()
82
78
83 params[PARAMETER_THREADS] = threads
79 params[PARAMETER_THREADS] = threads
84 params[CONTEXT_FORM] = form
80 params[CONTEXT_FORM] = form
85 params[PARAMETER_MAX_FILE_SIZE] = self.get_max_upload_size()
81 params[PARAMETER_MAX_FILE_SIZE] = self.get_max_upload_size()
86 params[PARAMETER_RSS_URL] = self.get_rss_url()
82 params[PARAMETER_RSS_URL] = self.get_rss_url()
87 params[PARAMETER_MAX_FILES] = settings.get_int('Forms', 'MaxFileCount')
83 params[PARAMETER_MAX_FILES] = settings.get_int('Forms', 'MaxFileCount')
88
84
89 paginator.set_url(self.get_reverse_url(), request.GET.dict())
85 paginator.set_url(self.get_reverse_url(), request.GET.dict())
90 params.update(self.get_page_context(paginator, page))
86 params.update(self.get_page_context(paginator, page))
91
87
92 return render(request, TEMPLATE, params)
88 return render(request, TEMPLATE, params)
93
89
94 @method_decorator(csrf_protect)
90 @method_decorator(csrf_protect)
95 def post(self, request):
91 def post(self, request):
96 if PARAMETER_METHOD in request.POST:
92 if PARAMETER_METHOD in request.POST:
97 self.dispatch_method(request)
93 self.dispatch_method(request)
98
94
99 return redirect('index') # FIXME Different for different modes
95 return redirect('index') # FIXME Different for different modes
100
96
101 form = ThreadForm(request.POST, request.FILES,
97 form = ThreadForm(request.POST, request.FILES,
102 error_class=PlainErrorList)
98 error_class=PlainErrorList)
103 form.session = request.session
99 form.session = request.session
104
100
105 if form.is_valid():
101 if form.is_valid():
106 return self.create_thread(request, form)
102 return Post.objects.create_from_form(request, form, None)
107 if form.need_to_ban:
103 if form.need_to_ban:
108 # Ban user because he is suspected to be a bot
104 # Ban user because he is suspected to be a bot
109 self._ban_current_user(request)
105 self._ban_current_user(request)
110
106
111 return self.get(request, form)
107 return self.get(request, form)
112
108
113 def get_reverse_url(self):
109 def get_reverse_url(self):
114 return reverse('index')
110 return reverse('index')
115
111
116 @transaction.atomic
117 def create_thread(self, request, form: ThreadForm, html_response=True):
118 """
119 Creates a new thread with an opening post.
120 """
121
122 ip = utils.get_client_ip(request)
123 is_banned = Ban.objects.filter(ip=ip).exists()
124
125 if is_banned:
126 if html_response:
127 return redirect(BannedView().as_view())
128 else:
129 return
130
131 data = form.cleaned_data
132
133 title = form.get_title()
134 text = data[FORM_TEXT]
135 files = form.get_files()
136 file_urls = form.get_file_urls()
137 images = form.get_images()
138
139 text = self._remove_invalid_links(text)
140
141 tags = data[FORM_TAGS]
142 monochrome = form.is_monochrome()
143 stickerpack = form.is_stickerpack()
144
145 post = Post.objects.create_post(title=title, text=text, files=files,
146 ip=ip, tags=tags,
147 tripcode=form.get_tripcode(),
148 monochrome=monochrome, images=images,
149 file_urls=file_urls, stickerpack=stickerpack)
150
151 if form.is_subscribe():
152 settings_manager = get_settings_manager(request)
153 settings_manager.add_or_read_fav_thread(post)
154
155 if html_response:
156 return redirect(post.get_absolute_url())
157
158 def get_threads(self):
112 def get_threads(self):
159 """
113 """
160 Gets list of threads that will be shown on a page.
114 Gets list of threads that will be shown on a page.
161 """
115 """
162
116
163 threads = Thread.objects\
117 threads = Thread.objects\
164 .exclude(tags__in=self.settings_manager.get_hidden_tags())
118 .exclude(tags__in=self.settings_manager.get_hidden_tags())
165 if self.settings_manager.get_setting(SETTING_ONLY_FAVORITES):
119 if self.settings_manager.get_setting(SETTING_ONLY_FAVORITES):
166 fav_tags = self.settings_manager.get_fav_tags()
120 fav_tags = self.settings_manager.get_fav_tags()
167 if len(fav_tags) > 0:
121 if len(fav_tags) > 0:
168 threads = threads.filter(tags__in=fav_tags)
122 threads = threads.filter(tags__in=fav_tags)
169
123
170 return threads
124 return threads
171
125
172 def get_rss_url(self):
126 def get_rss_url(self):
173 return self.get_reverse_url() + 'rss/'
127 return self.get_reverse_url() + 'rss/'
174
128
175 def toggle_fav(self, request):
129 def toggle_fav(self, request):
176 settings_manager = get_settings_manager(request)
130 settings_manager = get_settings_manager(request)
177 settings_manager.set_setting(SETTING_ONLY_FAVORITES,
131 settings_manager.set_setting(SETTING_ONLY_FAVORITES,
178 not settings_manager.get_setting(SETTING_ONLY_FAVORITES, False))
132 not settings_manager.get_setting(SETTING_ONLY_FAVORITES, False))
@@ -1,322 +1,321 b''
1 import json
1 import json
2 import logging
2 import logging
3
3
4 from django.core import serializers
4 from django.core import serializers
5 from django.db import transaction
5 from django.db import transaction
6 from django.db.models import Q
6 from django.db.models import Q
7 from django.http import HttpResponse, HttpResponseBadRequest
7 from django.http import HttpResponse, HttpResponseBadRequest
8 from django.shortcuts import get_object_or_404
8 from django.shortcuts import get_object_or_404
9 from django.views.decorators.csrf import csrf_protect
9 from django.views.decorators.csrf import csrf_protect
10
10
11 from boards.abstracts.settingsmanager import get_settings_manager
11 from boards.abstracts.settingsmanager import get_settings_manager
12 from boards.forms import PostForm, PlainErrorList
12 from boards.forms import PostForm, PlainErrorList
13 from boards.mdx_neboard import Parser
13 from boards.mdx_neboard import Parser
14 from boards.models import Post, Thread, Tag, TagAlias
14 from boards.models import Post, Thread, Tag, TagAlias
15 from boards.models.attachment import AttachmentSticker
15 from boards.models.attachment import AttachmentSticker
16 from boards.models.thread import STATUS_ARCHIVE
16 from boards.models.thread import STATUS_ARCHIVE
17 from boards.models.user import Notification
17 from boards.models.user import Notification
18 from boards.utils import datetime_to_epoch
18 from boards.utils import datetime_to_epoch
19 from boards.views.thread import ThreadView
20
19
21 __author__ = 'neko259'
20 __author__ = 'neko259'
22
21
23 PARAMETER_TRUNCATED = 'truncated'
22 PARAMETER_TRUNCATED = 'truncated'
24 PARAMETER_TAG = 'tag'
23 PARAMETER_TAG = 'tag'
25 PARAMETER_OFFSET = 'offset'
24 PARAMETER_OFFSET = 'offset'
26 PARAMETER_DIFF_TYPE = 'type'
25 PARAMETER_DIFF_TYPE = 'type'
27 PARAMETER_POST = 'post'
26 PARAMETER_POST = 'post'
28 PARAMETER_UPDATED = 'updated'
27 PARAMETER_UPDATED = 'updated'
29 PARAMETER_LAST_UPDATE = 'last_update'
28 PARAMETER_LAST_UPDATE = 'last_update'
30 PARAMETER_THREAD = 'thread'
29 PARAMETER_THREAD = 'thread'
31 PARAMETER_UIDS = 'uids'
30 PARAMETER_UIDS = 'uids'
32 PARAMETER_SUBSCRIBED = 'subscribed'
31 PARAMETER_SUBSCRIBED = 'subscribed'
33
32
34 DIFF_TYPE_HTML = 'html'
33 DIFF_TYPE_HTML = 'html'
35 DIFF_TYPE_JSON = 'json'
34 DIFF_TYPE_JSON = 'json'
36
35
37 STATUS_OK = 'ok'
36 STATUS_OK = 'ok'
38 STATUS_ERROR = 'error'
37 STATUS_ERROR = 'error'
39
38
40 logger = logging.getLogger(__name__)
39 logger = logging.getLogger(__name__)
41
40
42
41
43 @transaction.atomic
42 @transaction.atomic
44 def api_get_threaddiff(request):
43 def api_get_threaddiff(request):
45 """
44 """
46 Gets posts that were changed or added since time
45 Gets posts that were changed or added since time
47 """
46 """
48
47
49 thread_id = request.POST.get(PARAMETER_THREAD)
48 thread_id = request.POST.get(PARAMETER_THREAD)
50 uids_str = request.POST.get(PARAMETER_UIDS)
49 uids_str = request.POST.get(PARAMETER_UIDS)
51
50
52 if not thread_id or not uids_str:
51 if not thread_id or not uids_str:
53 return HttpResponse(content='Invalid request.')
52 return HttpResponse(content='Invalid request.')
54
53
55 uids = uids_str.strip().split(' ')
54 uids = uids_str.strip().split(' ')
56
55
57 opening_post = get_object_or_404(Post, id=thread_id)
56 opening_post = get_object_or_404(Post, id=thread_id)
58 thread = opening_post.get_thread()
57 thread = opening_post.get_thread()
59
58
60 json_data = {
59 json_data = {
61 PARAMETER_UPDATED: [],
60 PARAMETER_UPDATED: [],
62 PARAMETER_LAST_UPDATE: None, # TODO Maybe this can be removed already?
61 PARAMETER_LAST_UPDATE: None, # TODO Maybe this can be removed already?
63 }
62 }
64 posts = Post.objects.filter(thread=thread).exclude(uid__in=uids)
63 posts = Post.objects.filter(thread=thread).exclude(uid__in=uids)
65
64
66 diff_type = request.GET.get(PARAMETER_DIFF_TYPE, DIFF_TYPE_HTML)
65 diff_type = request.GET.get(PARAMETER_DIFF_TYPE, DIFF_TYPE_HTML)
67
66
68 for post in posts:
67 for post in posts:
69 json_data[PARAMETER_UPDATED].append(post.get_post_data(
68 json_data[PARAMETER_UPDATED].append(post.get_post_data(
70 format_type=diff_type, request=request))
69 format_type=diff_type, request=request))
71 json_data[PARAMETER_LAST_UPDATE] = str(thread.last_edit_time)
70 json_data[PARAMETER_LAST_UPDATE] = str(thread.last_edit_time)
72
71
73 settings_manager = get_settings_manager(request)
72 settings_manager = get_settings_manager(request)
74 json_data[PARAMETER_SUBSCRIBED] = str(settings_manager.thread_is_fav(opening_post))
73 json_data[PARAMETER_SUBSCRIBED] = str(settings_manager.thread_is_fav(opening_post))
75
74
76 # If the tag is favorite, update the counter
75 # If the tag is favorite, update the counter
77 settings_manager = get_settings_manager(request)
76 settings_manager = get_settings_manager(request)
78 favorite = settings_manager.thread_is_fav(opening_post)
77 favorite = settings_manager.thread_is_fav(opening_post)
79 if favorite:
78 if favorite:
80 settings_manager.add_or_read_fav_thread(opening_post)
79 settings_manager.add_or_read_fav_thread(opening_post)
81
80
82 return HttpResponse(content=json.dumps(json_data))
81 return HttpResponse(content=json.dumps(json_data))
83
82
84
83
85 @csrf_protect
84 @csrf_protect
86 def api_add_post(request, opening_post_id):
85 def api_add_post(request, opening_post_id):
87 """
86 """
88 Adds a post and return the JSON response for it
87 Adds a post and return the JSON response for it
89 """
88 """
90
89
91 # TODO Allow thread creation here too, without specifying opening post
90 # TODO Allow thread creation here too, without specifying opening post
92 opening_post = get_object_or_404(Post, id=opening_post_id)
91 opening_post = get_object_or_404(Post, id=opening_post_id)
93
92
94 status = STATUS_OK
93 status = STATUS_OK
95 errors = []
94 errors = []
96
95
97 post = None
96 post = None
98 if request.method == 'POST':
97 if request.method == 'POST':
99 form = PostForm(request.POST, request.FILES, error_class=PlainErrorList)
98 form = PostForm(request.POST, request.FILES, error_class=PlainErrorList)
100 form.session = request.session
99 form.session = request.session
101
100
102 if form.need_to_ban:
101 if form.need_to_ban:
103 # Ban user because he is suspected to be a bot
102 # Ban user because he is suspected to be a bot
104 # _ban_current_user(request)
103 # _ban_current_user(request)
105 status = STATUS_ERROR
104 status = STATUS_ERROR
106 if form.is_valid():
105 if form.is_valid():
107 post = ThreadView().new_post(request, form, opening_post,
106 post = Post.objects.create_from_form(request, form, opening_post,
108 html_response=False)
107 html_response=False)
109 if not post:
108 if not post:
110 status = STATUS_ERROR
109 status = STATUS_ERROR
111 else:
110 else:
112 logger.info('Added post #%d via api.' % post.id)
111 logger.info('Added post #%d via api.' % post.id)
113 else:
112 else:
114 status = STATUS_ERROR
113 status = STATUS_ERROR
115 errors = form.as_json_errors()
114 errors = form.as_json_errors()
116 else:
115 else:
117 status = STATUS_ERROR
116 status = STATUS_ERROR
118
117
119 response = {
118 response = {
120 'status': status,
119 'status': status,
121 'errors': errors,
120 'errors': errors,
122 }
121 }
123
122
124 if post:
123 if post:
125 response['post_id'] = post.id
124 response['post_id'] = post.id
126
125
127 return HttpResponse(content=json.dumps(response))
126 return HttpResponse(content=json.dumps(response))
128
127
129
128
130 def get_post(request, post_id):
129 def get_post(request, post_id):
131 """
130 """
132 Gets the html of a post. Used for popups. Post can be truncated if used
131 Gets the html of a post. Used for popups. Post can be truncated if used
133 in threads list with 'truncated' get parameter.
132 in threads list with 'truncated' get parameter.
134 """
133 """
135
134
136 post = get_object_or_404(Post, id=post_id)
135 post = get_object_or_404(Post, id=post_id)
137 truncated = PARAMETER_TRUNCATED in request.GET
136 truncated = PARAMETER_TRUNCATED in request.GET
138
137
139 return HttpResponse(content=post.get_view(truncated=truncated, need_op_data=True))
138 return HttpResponse(content=post.get_view(truncated=truncated, need_op_data=True))
140
139
141
140
142 def api_get_threads(request, count):
141 def api_get_threads(request, count):
143 """
142 """
144 Gets the JSON thread opening posts list.
143 Gets the JSON thread opening posts list.
145 Parameters that can be used for filtering:
144 Parameters that can be used for filtering:
146 tag, offset (from which thread to get results)
145 tag, offset (from which thread to get results)
147 """
146 """
148
147
149 if PARAMETER_TAG in request.GET:
148 if PARAMETER_TAG in request.GET:
150 tag_name = request.GET[PARAMETER_TAG]
149 tag_name = request.GET[PARAMETER_TAG]
151 if tag_name is not None:
150 if tag_name is not None:
152 tag = get_object_or_404(Tag, name=tag_name)
151 tag = get_object_or_404(Tag, name=tag_name)
153 threads = tag.get_threads().exclude(status=STATUS_ARCHIVE)
152 threads = tag.get_threads().exclude(status=STATUS_ARCHIVE)
154 else:
153 else:
155 threads = Thread.objects.exclude(status=STATUS_ARCHIVE)
154 threads = Thread.objects.exclude(status=STATUS_ARCHIVE)
156
155
157 if PARAMETER_OFFSET in request.GET:
156 if PARAMETER_OFFSET in request.GET:
158 offset = request.GET[PARAMETER_OFFSET]
157 offset = request.GET[PARAMETER_OFFSET]
159 offset = int(offset) if offset is not None else 0
158 offset = int(offset) if offset is not None else 0
160 else:
159 else:
161 offset = 0
160 offset = 0
162
161
163 threads = threads.order_by('-bump_time')
162 threads = threads.order_by('-bump_time')
164 threads = threads[offset:offset + int(count)]
163 threads = threads[offset:offset + int(count)]
165
164
166 opening_posts = []
165 opening_posts = []
167 for thread in threads:
166 for thread in threads:
168 opening_post = thread.get_opening_post()
167 opening_post = thread.get_opening_post()
169
168
170 # TODO Add tags, replies and images count
169 # TODO Add tags, replies and images count
171 post_data = opening_post.get_post_data(include_last_update=True)
170 post_data = opening_post.get_post_data(include_last_update=True)
172 post_data['status'] = thread.get_status()
171 post_data['status'] = thread.get_status()
173
172
174 opening_posts.append(post_data)
173 opening_posts.append(post_data)
175
174
176 return HttpResponse(content=json.dumps(opening_posts))
175 return HttpResponse(content=json.dumps(opening_posts))
177
176
178
177
179 # TODO Test this
178 # TODO Test this
180 def api_get_tags(request):
179 def api_get_tags(request):
181 """
180 """
182 Gets all tags or user tags.
181 Gets all tags or user tags.
183 """
182 """
184
183
185 # TODO Get favorite tags for the given user ID
184 # TODO Get favorite tags for the given user ID
186
185
187 tags = TagAlias.objects.all()
186 tags = TagAlias.objects.all()
188
187
189 term = request.GET.get('term')
188 term = request.GET.get('term')
190 if term is not None:
189 if term is not None:
191 tags = tags.filter(name__contains=term)
190 tags = tags.filter(name__contains=term)
192
191
193 tag_names = [tag.name for tag in tags]
192 tag_names = [tag.name for tag in tags]
194
193
195 return HttpResponse(content=json.dumps(tag_names))
194 return HttpResponse(content=json.dumps(tag_names))
196
195
197
196
198 def api_get_stickers(request):
197 def api_get_stickers(request):
199 term = request.GET.get('term')
198 term = request.GET.get('term')
200 if not term:
199 if not term:
201 return HttpResponseBadRequest()
200 return HttpResponseBadRequest()
202
201
203 global_stickers = AttachmentSticker.objects.filter(Q(name__icontains=term) | Q(stickerpack__name__icontains=term))
202 global_stickers = AttachmentSticker.objects.filter(Q(name__icontains=term) | Q(stickerpack__name__icontains=term))
204 local_stickers = [sticker for sticker in get_settings_manager(request).get_stickers() if term in sticker.name]
203 local_stickers = [sticker for sticker in get_settings_manager(request).get_stickers() if term in sticker.name]
205 stickers = list(global_stickers) + local_stickers
204 stickers = list(global_stickers) + local_stickers
206
205
207 image_dict = [{'thumb': sticker.attachment.get_thumb_url(),
206 image_dict = [{'thumb': sticker.attachment.get_thumb_url(),
208 'alias': str(sticker)}
207 'alias': str(sticker)}
209 for sticker in stickers]
208 for sticker in stickers]
210
209
211 return HttpResponse(content=json.dumps(image_dict))
210 return HttpResponse(content=json.dumps(image_dict))
212
211
213
212
214 # TODO The result can be cached by the thread last update time
213 # TODO The result can be cached by the thread last update time
215 # TODO Test this
214 # TODO Test this
216 def api_get_thread_posts(request, opening_post_id):
215 def api_get_thread_posts(request, opening_post_id):
217 """
216 """
218 Gets the JSON array of thread posts
217 Gets the JSON array of thread posts
219 """
218 """
220
219
221 opening_post = get_object_or_404(Post, id=opening_post_id)
220 opening_post = get_object_or_404(Post, id=opening_post_id)
222 thread = opening_post.get_thread()
221 thread = opening_post.get_thread()
223 posts = thread.get_replies()
222 posts = thread.get_replies()
224
223
225 json_data = {
224 json_data = {
226 'posts': [],
225 'posts': [],
227 'last_update': None,
226 'last_update': None,
228 }
227 }
229 json_post_list = []
228 json_post_list = []
230
229
231 for post in posts:
230 for post in posts:
232 json_post_list.append(post.get_post_data())
231 json_post_list.append(post.get_post_data())
233 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
232 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
234 json_data['posts'] = json_post_list
233 json_data['posts'] = json_post_list
235
234
236 return HttpResponse(content=json.dumps(json_data))
235 return HttpResponse(content=json.dumps(json_data))
237
236
238
237
239 def api_get_notifications(request, username):
238 def api_get_notifications(request, username):
240 last_notification_id_str = request.GET.get('last', None)
239 last_notification_id_str = request.GET.get('last', None)
241 last_id = int(last_notification_id_str) if last_notification_id_str is not None else None
240 last_id = int(last_notification_id_str) if last_notification_id_str is not None else None
242
241
243 posts = Notification.objects.get_notification_posts(usernames=[username],
242 posts = Notification.objects.get_notification_posts(usernames=[username],
244 last=last_id)
243 last=last_id)
245
244
246 json_post_list = []
245 json_post_list = []
247 for post in posts:
246 for post in posts:
248 json_post_list.append(post.get_post_data())
247 json_post_list.append(post.get_post_data())
249 return HttpResponse(content=json.dumps(json_post_list))
248 return HttpResponse(content=json.dumps(json_post_list))
250
249
251
250
252 def api_get_post(request, post_id):
251 def api_get_post(request, post_id):
253 """
252 """
254 Gets the JSON of a post. This can be
253 Gets the JSON of a post. This can be
255 used as and API for external clients.
254 used as and API for external clients.
256 """
255 """
257
256
258 post = get_object_or_404(Post, id=post_id)
257 post = get_object_or_404(Post, id=post_id)
259
258
260 json = serializers.serialize("json", [post], fields=(
259 json = serializers.serialize("json", [post], fields=(
261 "pub_time", "_text_rendered", "title", "text", "image",
260 "pub_time", "_text_rendered", "title", "text", "image",
262 "image_width", "image_height", "replies", "tags"
261 "image_width", "image_height", "replies", "tags"
263 ))
262 ))
264
263
265 return HttpResponse(content=json)
264 return HttpResponse(content=json)
266
265
267
266
268 def api_get_preview(request):
267 def api_get_preview(request):
269 raw_text = request.POST['raw_text']
268 raw_text = request.POST['raw_text']
270
269
271 parser = Parser()
270 parser = Parser()
272 return HttpResponse(content=parser.parse(parser.preparse(raw_text)))
271 return HttpResponse(content=parser.parse(parser.preparse(raw_text)))
273
272
274
273
275 def api_get_new_posts(request):
274 def api_get_new_posts(request):
276 """
275 """
277 Gets favorite threads and unread posts count.
276 Gets favorite threads and unread posts count.
278 """
277 """
279 posts = list()
278 posts = list()
280
279
281 include_posts = 'include_posts' in request.GET
280 include_posts = 'include_posts' in request.GET
282
281
283 settings_manager = get_settings_manager(request)
282 settings_manager = get_settings_manager(request)
284 fav_threads = settings_manager.get_fav_threads()
283 fav_threads = settings_manager.get_fav_threads()
285 fav_thread_ops = Post.objects.filter(id__in=fav_threads.keys())\
284 fav_thread_ops = Post.objects.filter(id__in=fav_threads.keys())\
286 .order_by('-pub_time').prefetch_related('thread')
285 .order_by('-pub_time').prefetch_related('thread')
287
286
288 ops = [{'op': op, 'last_id': fav_threads[str(op.id)]} for op in fav_thread_ops]
287 ops = [{'op': op, 'last_id': fav_threads[str(op.id)]} for op in fav_thread_ops]
289 if include_posts:
288 if include_posts:
290 new_post_threads = Thread.objects.get_new_posts(ops)
289 new_post_threads = Thread.objects.get_new_posts(ops)
291 if new_post_threads:
290 if new_post_threads:
292 thread_ids = {thread.id: thread for thread in new_post_threads}
291 thread_ids = {thread.id: thread for thread in new_post_threads}
293 else:
292 else:
294 thread_ids = dict()
293 thread_ids = dict()
295
294
296 for op in fav_thread_ops:
295 for op in fav_thread_ops:
297 fav_thread_dict = dict()
296 fav_thread_dict = dict()
298
297
299 op_thread = op.get_thread()
298 op_thread = op.get_thread()
300 if op_thread.id in thread_ids:
299 if op_thread.id in thread_ids:
301 thread = thread_ids[op_thread.id]
300 thread = thread_ids[op_thread.id]
302 new_post_count = thread.new_post_count
301 new_post_count = thread.new_post_count
303 fav_thread_dict['newest_post_link'] = thread.get_replies()\
302 fav_thread_dict['newest_post_link'] = thread.get_replies()\
304 .filter(id__gt=fav_threads[str(op.id)])\
303 .filter(id__gt=fav_threads[str(op.id)])\
305 .first().get_absolute_url(thread=thread)
304 .first().get_absolute_url(thread=thread)
306 else:
305 else:
307 new_post_count = 0
306 new_post_count = 0
308 fav_thread_dict['new_post_count'] = new_post_count
307 fav_thread_dict['new_post_count'] = new_post_count
309
308
310 fav_thread_dict['id'] = op.id
309 fav_thread_dict['id'] = op.id
311
310
312 fav_thread_dict['post_url'] = op.get_link_view()
311 fav_thread_dict['post_url'] = op.get_link_view()
313 fav_thread_dict['title'] = op.title
312 fav_thread_dict['title'] = op.title
314
313
315 posts.append(fav_thread_dict)
314 posts.append(fav_thread_dict)
316 else:
315 else:
317 fav_thread_dict = dict()
316 fav_thread_dict = dict()
318 fav_thread_dict['new_post_count'] = \
317 fav_thread_dict['new_post_count'] = \
319 Thread.objects.get_new_post_count(ops)
318 Thread.objects.get_new_post_count(ops)
320 posts.append(fav_thread_dict)
319 posts.append(fav_thread_dict)
321
320
322 return HttpResponse(content=json.dumps(posts))
321 return HttpResponse(content=json.dumps(posts))
@@ -1,131 +1,130 b''
1 from django.urls import reverse
1 from django.urls import reverse
2 from django.shortcuts import render
2 from django.shortcuts import render
3
3
4 from boards import settings
4 from boards import settings
5 from boards.abstracts.paginator import get_paginator
5 from boards.abstracts.paginator import get_paginator
6 from boards.abstracts.settingsmanager import get_settings_manager
6 from boards.abstracts.settingsmanager import get_settings_manager
7 from boards.models import Post
7 from boards.models import Post
8 from boards.views.base import BaseBoardView
8 from boards.views.base import BaseBoardView
9 from boards.views.posting_mixin import PostMixin
10 from boards.views.mixins import PaginatedMixin
9 from boards.views.mixins import PaginatedMixin
11
10
12 POSTS_PER_PAGE = settings.get_int('View', 'PostsPerPage')
11 POSTS_PER_PAGE = settings.get_int('View', 'PostsPerPage')
13
12
14 PARAMETER_POSTS = 'posts'
13 PARAMETER_POSTS = 'posts'
15 PARAMETER_QUERIES = 'queries'
14 PARAMETER_QUERIES = 'queries'
16
15
17 TEMPLATE = 'boards/feed.html'
16 TEMPLATE = 'boards/feed.html'
18 DEFAULT_PAGE = 1
17 DEFAULT_PAGE = 1
19
18
20
19
21 class FeedFilter:
20 class FeedFilter:
22 @staticmethod
21 @staticmethod
23 def get_filtered_posts(request, posts):
22 def get_filtered_posts(request, posts):
24 return posts
23 return posts
25
24
26 @staticmethod
25 @staticmethod
27 def get_query(request):
26 def get_query(request):
28 return None
27 return None
29
28
30
29
31 class TripcodeFilter(FeedFilter):
30 class TripcodeFilter(FeedFilter):
32 @staticmethod
31 @staticmethod
33 def get_filtered_posts(request, posts):
32 def get_filtered_posts(request, posts):
34 filtered_posts = posts
33 filtered_posts = posts
35 tripcode = request.GET.get('tripcode', None)
34 tripcode = request.GET.get('tripcode', None)
36 if tripcode:
35 if tripcode:
37 filtered_posts = filtered_posts.filter(tripcode=tripcode)
36 filtered_posts = filtered_posts.filter(tripcode=tripcode)
38 return filtered_posts
37 return filtered_posts
39
38
40 @staticmethod
39 @staticmethod
41 def get_query(request):
40 def get_query(request):
42 tripcode = request.GET.get('tripcode', None)
41 tripcode = request.GET.get('tripcode', None)
43 if tripcode:
42 if tripcode:
44 return 'Tripcode: {}'.format(tripcode)
43 return 'Tripcode: {}'.format(tripcode)
45
44
46
45
47 class FavoritesFilter(FeedFilter):
46 class FavoritesFilter(FeedFilter):
48 @staticmethod
47 @staticmethod
49 def get_filtered_posts(request, posts):
48 def get_filtered_posts(request, posts):
50 filtered_posts = posts
49 filtered_posts = posts
51
50
52 favorites = 'favorites' in request.GET
51 favorites = 'favorites' in request.GET
53 if favorites:
52 if favorites:
54 settings_manager = get_settings_manager(request)
53 settings_manager = get_settings_manager(request)
55 fav_thread_ops = Post.objects.filter(id__in=settings_manager.get_fav_threads().keys())
54 fav_thread_ops = Post.objects.filter(id__in=settings_manager.get_fav_threads().keys())
56 fav_threads = [op.get_thread() for op in fav_thread_ops]
55 fav_threads = [op.get_thread() for op in fav_thread_ops]
57 filtered_posts = filtered_posts.filter(thread__in=fav_threads)
56 filtered_posts = filtered_posts.filter(thread__in=fav_threads)
58 return filtered_posts
57 return filtered_posts
59
58
60
59
61 class IpFilter(FeedFilter):
60 class IpFilter(FeedFilter):
62 @staticmethod
61 @staticmethod
63 def get_filtered_posts(request, posts):
62 def get_filtered_posts(request, posts):
64 filtered_posts = posts
63 filtered_posts = posts
65
64
66 ip = request.GET.get('ip', None)
65 ip = request.GET.get('ip', None)
67 if ip and request.user.has_perm('post_delete'):
66 if ip and request.user.has_perm('post_delete'):
68 filtered_posts = filtered_posts.filter(poster_ip=ip)
67 filtered_posts = filtered_posts.filter(poster_ip=ip)
69 return filtered_posts
68 return filtered_posts
70
69
71 @staticmethod
70 @staticmethod
72 def get_query(request):
71 def get_query(request):
73 ip = request.GET.get('ip', None)
72 ip = request.GET.get('ip', None)
74 if ip:
73 if ip:
75 return 'IP: {}'.format(ip)
74 return 'IP: {}'.format(ip)
76
75
77
76
78 class ImageFilter(FeedFilter):
77 class ImageFilter(FeedFilter):
79 @staticmethod
78 @staticmethod
80 def get_filtered_posts(request, posts):
79 def get_filtered_posts(request, posts):
81 filtered_posts = posts
80 filtered_posts = posts
82
81
83 image = request.GET.get('image', None)
82 image = request.GET.get('image', None)
84 if image:
83 if image:
85 filtered_posts = filtered_posts.filter(attachments__file=image)
84 filtered_posts = filtered_posts.filter(attachments__file=image)
86 return filtered_posts
85 return filtered_posts
87
86
88 @staticmethod
87 @staticmethod
89 def get_query(request):
88 def get_query(request):
90 image = request.GET.get('image', None)
89 image = request.GET.get('image', None)
91 if image:
90 if image:
92 return 'File: {}'.format(image)
91 return 'File: {}'.format(image)
93
92
94
93
95 class FeedView(PostMixin, PaginatedMixin, BaseBoardView):
94 class FeedView(PaginatedMixin, BaseBoardView):
96 filters = (
95 filters = (
97 TripcodeFilter,
96 TripcodeFilter,
98 FavoritesFilter,
97 FavoritesFilter,
99 IpFilter,
98 IpFilter,
100 ImageFilter,
99 ImageFilter,
101 )
100 )
102
101
103 def get(self, request):
102 def get(self, request):
104 page = request.GET.get('page', DEFAULT_PAGE)
103 page = request.GET.get('page', DEFAULT_PAGE)
105
104
106 params = self.get_context_data(request=request)
105 params = self.get_context_data(request=request)
107
106
108 settings_manager = get_settings_manager(request)
107 settings_manager = get_settings_manager(request)
109
108
110 posts = Post.objects.exclude(
109 posts = Post.objects.exclude(
111 thread__tags__in=settings_manager.get_hidden_tags()).order_by(
110 thread__tags__in=settings_manager.get_hidden_tags()).order_by(
112 '-pub_time').prefetch_related('attachments', 'thread')
111 '-pub_time').prefetch_related('attachments', 'thread')
113 queries = []
112 queries = []
114 for filter in self.filters:
113 for filter in self.filters:
115 posts = filter.get_filtered_posts(request, posts)
114 posts = filter.get_filtered_posts(request, posts)
116 query = filter.get_query(request)
115 query = filter.get_query(request)
117 if query:
116 if query:
118 queries.append(query)
117 queries.append(query)
119 params[PARAMETER_QUERIES] = queries
118 params[PARAMETER_QUERIES] = queries
120
119
121 paginator = get_paginator(posts, POSTS_PER_PAGE)
120 paginator = get_paginator(posts, POSTS_PER_PAGE)
122 paginator.current_page = int(page)
121 paginator.current_page = int(page)
123
122
124 params[PARAMETER_POSTS] = paginator.page(page).object_list
123 params[PARAMETER_POSTS] = paginator.page(page).object_list
125
124
126 paginator.set_url(reverse('feed'), request.GET.dict())
125 paginator.set_url(reverse('feed'), request.GET.dict())
127
126
128 params.update(self.get_page_context(paginator, page))
127 params.update(self.get_page_context(paginator, page))
129
128
130 return render(request, TEMPLATE, params)
129 return render(request, TEMPLATE, params)
131
130
@@ -1,61 +1,60 b''
1 import boards
1 import boards
2
2
3
4 PARAM_NEXT = 'next'
3 PARAM_NEXT = 'next'
5 PARAMETER_METHOD = 'method'
4 PARAMETER_METHOD = 'method'
6
5
7 PARAMETER_CURRENT_PAGE = 'current_page'
6 PARAMETER_CURRENT_PAGE = 'current_page'
8 PARAMETER_PAGINATOR = 'paginator'
7 PARAMETER_PAGINATOR = 'paginator'
9
8
10 PARAMETER_PREV_LINK = 'prev_page_link'
9 PARAMETER_PREV_LINK = 'prev_page_link'
11 PARAMETER_NEXT_LINK = 'next_page_link'
10 PARAMETER_NEXT_LINK = 'next_page_link'
12
11
12
13 class DispatcherMixin:
13 class DispatcherMixin:
14 """
14 """
15 This class contains a dispather method that can run a method specified by
15 This class contains a dispather method that can run a method specified by
16 'method' request parameter.
16 'method' request parameter.
17 """
17 """
18
18
19 def __init__(self):
19 def __init__(self):
20 self.user = None
20 self.user = None
21
21
22 def dispatch_method(self, *args, **kwargs):
22 def dispatch_method(self, *args, **kwargs):
23 request = args[0]
23 request = args[0]
24
24
25 self.user = request.user
25 self.user = request.user
26
26
27 method_name = None
27 method_name = None
28 if PARAMETER_METHOD in request.GET:
28 if PARAMETER_METHOD in request.GET:
29 method_name = request.GET[PARAMETER_METHOD]
29 method_name = request.GET[PARAMETER_METHOD]
30 elif PARAMETER_METHOD in request.POST:
30 elif PARAMETER_METHOD in request.POST:
31 method_name = request.POST[PARAMETER_METHOD]
31 method_name = request.POST[PARAMETER_METHOD]
32
32
33 if method_name:
33 if method_name:
34 return getattr(self, method_name)(*args, **kwargs)
34 return getattr(self, method_name)(*args, **kwargs)
35
35
36
36
37 class FileUploadMixin:
37 class FileUploadMixin:
38 def get_max_upload_size(self):
38 def get_max_upload_size(self):
39 return boards.settings.get_int('Forms', 'MaxFileSize')
39 return boards.settings.get_int('Forms', 'MaxFileSize')
40
40
41
41
42 class PaginatedMixin:
42 class PaginatedMixin:
43 def get_page_context(self, paginator, page):
43 def get_page_context(self, paginator, page):
44 """
44 """
45 Get pagination context variables
45 Get pagination context variables
46 """
46 """
47
47
48 params = {}
48 params = {}
49
49
50 params[PARAMETER_PAGINATOR] = paginator
50 params[PARAMETER_PAGINATOR] = paginator
51 current_page = paginator.page(int(page))
51 current_page = paginator.page(int(page))
52 params[PARAMETER_CURRENT_PAGE] = current_page
52 params[PARAMETER_CURRENT_PAGE] = current_page
53 if current_page.has_previous():
53 if current_page.has_previous():
54 params[PARAMETER_PREV_LINK] = paginator.get_page_url(
54 params[PARAMETER_PREV_LINK] = paginator.get_page_url(
55 current_page.previous_page_number())
55 current_page.previous_page_number())
56 if current_page.has_next():
56 if current_page.has_next():
57 params[PARAMETER_NEXT_LINK] = paginator.get_page_url(
57 params[PARAMETER_NEXT_LINK] = paginator.get_page_url(
58 current_page.next_page_number())
58 current_page.next_page_number())
59
59
60 return params
60 return params
61
@@ -1,124 +1,124 b''
1 from django.shortcuts import get_object_or_404, redirect
1 from django.shortcuts import get_object_or_404, redirect
2 from django.urls import reverse
2 from django.urls import reverse
3
3
4 from boards.abstracts.settingsmanager import get_settings_manager, \
4 from boards.abstracts.settingsmanager import get_settings_manager, \
5 SETTING_FAVORITE_TAGS, SETTING_HIDDEN_TAGS
5 SETTING_FAVORITE_TAGS, SETTING_HIDDEN_TAGS
6 from boards.models import Tag, TagAlias
6 from boards.models import Tag, TagAlias, Post
7 from boards.views.all_threads import AllThreadsView
7 from boards.views.all_threads import AllThreadsView
8 from boards.views.mixins import DispatcherMixin, PARAMETER_METHOD
8 from boards.views.mixins import DispatcherMixin, PARAMETER_METHOD
9 from boards.forms import ThreadForm, PlainErrorList
9 from boards.forms import ThreadForm, PlainErrorList
10
10
11 PARAM_HIDDEN_TAGS = 'hidden_tags'
11 PARAM_HIDDEN_TAGS = 'hidden_tags'
12 PARAM_TAG = 'tag'
12 PARAM_TAG = 'tag'
13 PARAM_IS_FAVORITE = 'is_favorite'
13 PARAM_IS_FAVORITE = 'is_favorite'
14 PARAM_IS_HIDDEN = 'is_hidden'
14 PARAM_IS_HIDDEN = 'is_hidden'
15 PARAM_RANDOM_IMAGE_POST = 'random_image_post'
15 PARAM_RANDOM_IMAGE_POST = 'random_image_post'
16 PARAM_RELATED_TAGS = 'related_tags'
16 PARAM_RELATED_TAGS = 'related_tags'
17
17
18
18
19 __author__ = 'neko259'
19 __author__ = 'neko259'
20
20
21
21
22 class TagView(AllThreadsView, DispatcherMixin):
22 class TagView(AllThreadsView, DispatcherMixin):
23
23
24 tag_name = None
24 tag_name = None
25
25
26 def get_threads(self):
26 def get_threads(self):
27 tag_alias = get_object_or_404(TagAlias, name=self.tag_name)
27 tag_alias = get_object_or_404(TagAlias, name=self.tag_name)
28 tag = tag_alias.parent
28 tag = tag_alias.parent
29
29
30 hidden_tags = self.settings_manager.get_hidden_tags()
30 hidden_tags = self.settings_manager.get_hidden_tags()
31
31
32 try:
32 try:
33 hidden_tags.remove(tag)
33 hidden_tags.remove(tag)
34 except ValueError:
34 except ValueError:
35 pass
35 pass
36
36
37 return tag.get_threads().exclude(
37 return tag.get_threads().exclude(
38 tags__in=hidden_tags)
38 tags__in=hidden_tags)
39
39
40 def get_context_data(self, **kwargs):
40 def get_context_data(self, **kwargs):
41 params = super(TagView, self).get_context_data(**kwargs)
41 params = super(TagView, self).get_context_data(**kwargs)
42
42
43 settings_manager = get_settings_manager(kwargs['request'])
43 settings_manager = get_settings_manager(kwargs['request'])
44
44
45 tag_alias = get_object_or_404(TagAlias, name=self.tag_name)
45 tag_alias = get_object_or_404(TagAlias, name=self.tag_name)
46 tag = tag_alias.parent
46 tag = tag_alias.parent
47 params[PARAM_TAG] = tag
47 params[PARAM_TAG] = tag
48
48
49 fav_tag_names = settings_manager.get_setting(SETTING_FAVORITE_TAGS)
49 fav_tag_names = settings_manager.get_setting(SETTING_FAVORITE_TAGS)
50 hidden_tag_names = settings_manager.get_setting(SETTING_HIDDEN_TAGS)
50 hidden_tag_names = settings_manager.get_setting(SETTING_HIDDEN_TAGS)
51
51
52 params[PARAM_IS_FAVORITE] = fav_tag_names is not None and tag.get_name() in fav_tag_names
52 params[PARAM_IS_FAVORITE] = fav_tag_names is not None and tag.get_name() in fav_tag_names
53 params[PARAM_IS_HIDDEN] = hidden_tag_names is not None and tag.get_name() in hidden_tag_names
53 params[PARAM_IS_HIDDEN] = hidden_tag_names is not None and tag.get_name() in hidden_tag_names
54
54
55 params[PARAM_RANDOM_IMAGE_POST] = tag.get_random_image_post()
55 params[PARAM_RANDOM_IMAGE_POST] = tag.get_random_image_post()
56 params[PARAM_RELATED_TAGS] = tag.get_related_tags()
56 params[PARAM_RELATED_TAGS] = tag.get_related_tags()
57
57
58 return params
58 return params
59
59
60 def get_reverse_url(self):
60 def get_reverse_url(self):
61 return reverse('tag', kwargs={'tag_name': self.tag_name})
61 return reverse('tag', kwargs={'tag_name': self.tag_name})
62
62
63 def get(self, request, tag_name, form=None):
63 def get(self, request, tag_name, form=None):
64 self.tag_name = tag_name
64 self.tag_name = tag_name
65
65
66 return super(TagView, self).get(request, form)
66 return super(TagView, self).get(request, form)
67
67
68
68
69 def post(self, request, tag_name):
69 def post(self, request, tag_name):
70 self.tag_name = tag_name
70 self.tag_name = tag_name
71
71
72 if PARAMETER_METHOD in request.POST:
72 if PARAMETER_METHOD in request.POST:
73 self.dispatch_method(request)
73 self.dispatch_method(request)
74
74
75 return redirect('tag', tag_name)
75 return redirect('tag', tag_name)
76 else:
76 else:
77 form = ThreadForm(request.POST, request.FILES,
77 form = ThreadForm(request.POST, request.FILES,
78 error_class=PlainErrorList)
78 error_class=PlainErrorList)
79 form.session = request.session
79 form.session = request.session
80
80
81 if form.is_valid():
81 if form.is_valid():
82 return self.create_thread(request, form)
82 return Post.objects.create_from_form(request, form, opening_post=None)
83 if form.need_to_ban:
83 if form.need_to_ban:
84 # Ban user because he is suspected to be a bot
84 # Ban user because he is suspected to be a bot
85 self._ban_current_user(request)
85 self._ban_current_user(request)
86
86
87 return self.get(request, tag_name, form)
87 return self.get(request, tag_name, form)
88
88
89 def subscribe(self, request):
89 def subscribe(self, request):
90 alias = get_object_or_404(TagAlias, name=self.tag_name)
90 alias = get_object_or_404(TagAlias, name=self.tag_name)
91 tag = alias.parent
91 tag = alias.parent
92
92
93 settings_manager = get_settings_manager(request)
93 settings_manager = get_settings_manager(request)
94 settings_manager.add_fav_tag(tag)
94 settings_manager.add_fav_tag(tag)
95
95
96 def unsubscribe(self, request):
96 def unsubscribe(self, request):
97 alias = get_object_or_404(TagAlias, name=self.tag_name)
97 alias = get_object_or_404(TagAlias, name=self.tag_name)
98 tag = alias.parent
98 tag = alias.parent
99
99
100 settings_manager = get_settings_manager(request)
100 settings_manager = get_settings_manager(request)
101 settings_manager.del_fav_tag(tag)
101 settings_manager.del_fav_tag(tag)
102
102
103 def hide(self, request):
103 def hide(self, request):
104 """
104 """
105 Adds tag to user's hidden tags. Threads with this tag will not be
105 Adds tag to user's hidden tags. Threads with this tag will not be
106 shown.
106 shown.
107 """
107 """
108
108
109 alias = get_object_or_404(TagAlias, name=self.tag_name)
109 alias = get_object_or_404(TagAlias, name=self.tag_name)
110 tag = alias.parent
110 tag = alias.parent
111
111
112 settings_manager = get_settings_manager(request)
112 settings_manager = get_settings_manager(request)
113 settings_manager.add_hidden_tag(tag)
113 settings_manager.add_hidden_tag(tag)
114
114
115 def unhide(self, request):
115 def unhide(self, request):
116 """
116 """
117 Removed tag from user's hidden tags.
117 Removed tag from user's hidden tags.
118 """
118 """
119
119
120 alias = get_object_or_404(TagAlias, name=self.tag_name)
120 alias = get_object_or_404(TagAlias, name=self.tag_name)
121 tag = alias.parent
121 tag = alias.parent
122
122
123 settings_manager = get_settings_manager(request)
123 settings_manager = get_settings_manager(request)
124 settings_manager.del_hidden_tag(tag)
124 settings_manager.del_hidden_tag(tag)
@@ -1,159 +1,116 b''
1 from django.core.exceptions import ObjectDoesNotExist
1 from django.core.exceptions import ObjectDoesNotExist
2 from django.http import Http404
2 from django.http import Http404
3 from django.shortcuts import get_object_or_404, render, redirect
3 from django.shortcuts import get_object_or_404, render, redirect
4 from django.urls import reverse
4 from django.urls import reverse
5 from django.utils.decorators import method_decorator
5 from django.utils.decorators import method_decorator
6 from django.views.decorators.csrf import csrf_protect
6 from django.views.decorators.csrf import csrf_protect
7 from django.views.generic.edit import FormMixin
7 from django.views.generic.edit import FormMixin
8
8
9 from boards import utils
10 from boards.abstracts.settingsmanager import get_settings_manager
9 from boards.abstracts.settingsmanager import get_settings_manager
11 from boards.forms import PostForm, PlainErrorList
10 from boards.forms import PostForm, PlainErrorList
12 from boards.models import Post
11 from boards.models import Post
13 from boards.views.base import BaseBoardView, CONTEXT_FORM
12 from boards.views.base import BaseBoardView, CONTEXT_FORM
14 from boards.views.mixins import DispatcherMixin, PARAMETER_METHOD
13 from boards.views.mixins import DispatcherMixin, PARAMETER_METHOD
15 from boards.views.posting_mixin import PostMixin
16
14
17 REQ_POST_ID = 'post_id'
15 REQ_POST_ID = 'post_id'
18
16
19 CONTEXT_LASTUPDATE = "last_update"
17 CONTEXT_LASTUPDATE = "last_update"
20 CONTEXT_THREAD = 'thread'
18 CONTEXT_THREAD = 'thread'
21 CONTEXT_MODE = 'mode'
19 CONTEXT_MODE = 'mode'
22 CONTEXT_OP = 'opening_post'
20 CONTEXT_OP = 'opening_post'
23 CONTEXT_FAVORITE = 'is_favorite'
21 CONTEXT_FAVORITE = 'is_favorite'
24 CONTEXT_RSS_URL = 'rss_url'
22 CONTEXT_RSS_URL = 'rss_url'
25
23
26 FORM_TITLE = 'title'
27 FORM_TEXT = 'text'
28 FORM_IMAGE = 'image'
29 FORM_THREADS = 'threads'
30
24
31
25 class ThreadView(BaseBoardView, FormMixin, DispatcherMixin):
32 class ThreadView(BaseBoardView, PostMixin, FormMixin, DispatcherMixin):
33
26
34 @method_decorator(csrf_protect)
27 @method_decorator(csrf_protect)
35 def get(self, request, post_id, form: PostForm=None):
28 def get(self, request, post_id, form: PostForm=None):
36 try:
29 try:
37 opening_post = Post.objects.get(id=post_id)
30 opening_post = Post.objects.get(id=post_id)
38 except ObjectDoesNotExist:
31 except ObjectDoesNotExist:
39 raise Http404
32 raise Http404
40
33
41 # If the tag is favorite, update the counter
34 # If the tag is favorite, update the counter
42 settings_manager = get_settings_manager(request)
35 settings_manager = get_settings_manager(request)
43 favorite = settings_manager.thread_is_fav(opening_post)
36 favorite = settings_manager.thread_is_fav(opening_post)
44 if favorite:
37 if favorite:
45 settings_manager.add_or_read_fav_thread(opening_post)
38 settings_manager.add_or_read_fav_thread(opening_post)
46
39
47 # If this is not OP, don't show it as it is
40 # If this is not OP, don't show it as it is
48 if not opening_post.is_opening():
41 if not opening_post.is_opening():
49 return redirect('{}#{}'.format(opening_post.get_thread().get_opening_post()
42 return redirect('{}#{}'.format(opening_post.get_thread().get_opening_post()
50 .get_absolute_url(), opening_post.id))
43 .get_absolute_url(), opening_post.id))
51
44
52 if not form:
45 if not form:
53 form = PostForm(error_class=PlainErrorList)
46 form = PostForm(error_class=PlainErrorList)
54
47
55 thread_to_show = opening_post.get_thread()
48 thread_to_show = opening_post.get_thread()
56
49
57 params = dict()
50 params = dict()
58
51
59 params[CONTEXT_FORM] = form
52 params[CONTEXT_FORM] = form
60 params[CONTEXT_LASTUPDATE] = str(thread_to_show.last_edit_time)
53 params[CONTEXT_LASTUPDATE] = str(thread_to_show.last_edit_time)
61 params[CONTEXT_THREAD] = thread_to_show
54 params[CONTEXT_THREAD] = thread_to_show
62 params[CONTEXT_MODE] = self.get_mode()
55 params[CONTEXT_MODE] = self.get_mode()
63 params[CONTEXT_OP] = opening_post
56 params[CONTEXT_OP] = opening_post
64 params[CONTEXT_FAVORITE] = favorite
57 params[CONTEXT_FAVORITE] = favorite
65 params[CONTEXT_RSS_URL] = self.get_rss_url(post_id)
58 params[CONTEXT_RSS_URL] = self.get_rss_url(post_id)
66
59
67 params.update(self.get_data(thread_to_show))
60 params.update(self.get_data(thread_to_show))
68
61
69 return render(request, self.get_template(), params)
62 return render(request, self.get_template(), params)
70
63
71 @method_decorator(csrf_protect)
64 @method_decorator(csrf_protect)
72 def post(self, request, post_id):
65 def post(self, request, post_id):
73 opening_post = get_object_or_404(Post, id=post_id)
66 opening_post = get_object_or_404(Post, id=post_id)
74
67
75 # If this is not OP, don't show it as it is
68 # If this is not OP, don't show it as it is
76 if not opening_post.is_opening():
69 if not opening_post.is_opening():
77 raise Http404
70 raise Http404
78
71
79 if PARAMETER_METHOD in request.POST:
72 if PARAMETER_METHOD in request.POST:
80 self.dispatch_method(request, opening_post)
73 self.dispatch_method(request, opening_post)
81
74
82 return redirect('thread', post_id) # FIXME Different for different modes
75 return redirect('thread', post_id) # FIXME Different for different modes
83
76
84 if not opening_post.get_thread().is_archived():
77 if not opening_post.get_thread().is_archived():
85 form = PostForm(request.POST, request.FILES,
78 form = PostForm(request.POST, request.FILES,
86 error_class=PlainErrorList)
79 error_class=PlainErrorList)
87 form.session = request.session
80 form.session = request.session
88
81
89 if form.is_valid():
82 if form.is_valid():
90 return self.new_post(request, form, opening_post)
83 return Post.objects.create_from_form(request, form, opening_post)
91 if form.need_to_ban:
84 if form.need_to_ban:
92 # Ban user because he is suspected to be a bot
85 # Ban user because he is suspected to be a bot
93 self._ban_current_user(request)
86 self._ban_current_user(request)
94
87
95 return self.get(request, post_id, form)
88 return self.get(request, post_id, form)
96
89
97 def new_post(self, request, form: PostForm, opening_post: Post=None,
98 html_response=True):
99 """
100 Adds a new post (in thread or as a reply).
101 """
102
103 ip = utils.get_client_ip(request)
104
105 data = form.cleaned_data
106
107 title = form.get_title()
108 text = data[FORM_TEXT]
109 files = form.get_files()
110 file_urls = form.get_file_urls()
111 images = form.get_images()
112
113 text = self._remove_invalid_links(text)
114
115 post_thread = opening_post.get_thread()
116
117 post = Post.objects.create_post(title=title, text=text, files=files,
118 thread=post_thread, ip=ip,
119 tripcode=form.get_tripcode(),
120 images=images, file_urls=file_urls)
121
122 if form.is_subscribe():
123 settings_manager = get_settings_manager(request)
124 settings_manager.add_or_read_fav_thread(
125 post_thread.get_opening_post())
126
127 if html_response:
128 if opening_post:
129 return redirect(post.get_absolute_url())
130 else:
131 return post
132
133 def get_data(self, thread) -> dict:
90 def get_data(self, thread) -> dict:
134 """
91 """
135 Returns context params for the view.
92 Returns context params for the view.
136 """
93 """
137
94
138 return dict()
95 return dict()
139
96
140 def get_template(self) -> str:
97 def get_template(self) -> str:
141 """
98 """
142 Gets template to show the thread mode on.
99 Gets template to show the thread mode on.
143 """
100 """
144
101
145 pass
102 pass
146
103
147 def get_mode(self) -> str:
104 def get_mode(self) -> str:
148 pass
105 pass
149
106
150 def subscribe(self, request, opening_post):
107 def subscribe(self, request, opening_post):
151 settings_manager = get_settings_manager(request)
108 settings_manager = get_settings_manager(request)
152 settings_manager.add_or_read_fav_thread(opening_post)
109 settings_manager.add_or_read_fav_thread(opening_post)
153
110
154 def unsubscribe(self, request, opening_post):
111 def unsubscribe(self, request, opening_post):
155 settings_manager = get_settings_manager(request)
112 settings_manager = get_settings_manager(request)
156 settings_manager.del_fav_thread(opening_post)
113 settings_manager.del_fav_thread(opening_post)
157
114
158 def get_rss_url(self, opening_id):
115 def get_rss_url(self, opening_id):
159 return reverse('thread', kwargs={'post_id': opening_id}) + 'rss/'
116 return reverse('thread', kwargs={'post_id': opening_id}) + 'rss/'
1 NO CONTENT: file was removed
NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now