##// END OF EJS Templates
Fixed issue in rendering post without having a request
neko259 -
r1117:899891b1 default
parent child Browse files
Show More
@@ -1,426 +1,426
1 from datetime import datetime, timedelta, date
1 from datetime import datetime, timedelta, date
2 from datetime import time as dtime
2 from datetime import time as dtime
3 import logging
3 import logging
4 import re
4 import re
5
5
6 from django.core.exceptions import ObjectDoesNotExist
6 from django.core.exceptions import ObjectDoesNotExist
7 from django.core.urlresolvers import reverse
7 from django.core.urlresolvers import reverse
8 from django.db import models, transaction
8 from django.db import models, transaction
9 from django.db.models import TextField
9 from django.db.models import TextField
10 from django.template.loader import render_to_string
10 from django.template.loader import render_to_string
11 from django.utils import timezone
11 from django.utils import timezone
12
12
13 from boards import settings
13 from boards import settings
14 from boards.mdx_neboard import Parser
14 from boards.mdx_neboard import Parser
15 from boards.models import PostImage
15 from boards.models import PostImage
16 from boards.models.base import Viewable
16 from boards.models.base import Viewable
17 from boards import utils
17 from boards import utils
18 from boards.models.user import Notification, Ban
18 from boards.models.user import Notification, Ban
19 import boards.models.thread
19 import boards.models.thread
20
20
21
21
22 APP_LABEL_BOARDS = 'boards'
22 APP_LABEL_BOARDS = 'boards'
23
23
24 POSTS_PER_DAY_RANGE = 7
24 POSTS_PER_DAY_RANGE = 7
25
25
26 BAN_REASON_AUTO = 'Auto'
26 BAN_REASON_AUTO = 'Auto'
27
27
28 IMAGE_THUMB_SIZE = (200, 150)
28 IMAGE_THUMB_SIZE = (200, 150)
29
29
30 TITLE_MAX_LENGTH = 200
30 TITLE_MAX_LENGTH = 200
31
31
32 # TODO This should be removed
32 # TODO This should be removed
33 NO_IP = '0.0.0.0'
33 NO_IP = '0.0.0.0'
34
34
35 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
35 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
36 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
36 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
37
37
38 PARAMETER_TRUNCATED = 'truncated'
38 PARAMETER_TRUNCATED = 'truncated'
39 PARAMETER_TAG = 'tag'
39 PARAMETER_TAG = 'tag'
40 PARAMETER_OFFSET = 'offset'
40 PARAMETER_OFFSET = 'offset'
41 PARAMETER_DIFF_TYPE = 'type'
41 PARAMETER_DIFF_TYPE = 'type'
42 PARAMETER_CSS_CLASS = 'css_class'
42 PARAMETER_CSS_CLASS = 'css_class'
43 PARAMETER_THREAD = 'thread'
43 PARAMETER_THREAD = 'thread'
44 PARAMETER_IS_OPENING = 'is_opening'
44 PARAMETER_IS_OPENING = 'is_opening'
45 PARAMETER_MODERATOR = 'moderator'
45 PARAMETER_MODERATOR = 'moderator'
46 PARAMETER_POST = 'post'
46 PARAMETER_POST = 'post'
47 PARAMETER_OP_ID = 'opening_post_id'
47 PARAMETER_OP_ID = 'opening_post_id'
48 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
48 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
49 PARAMETER_REPLY_LINK = 'reply_link'
49 PARAMETER_REPLY_LINK = 'reply_link'
50
50
51 DIFF_TYPE_HTML = 'html'
51 DIFF_TYPE_HTML = 'html'
52 DIFF_TYPE_JSON = 'json'
52 DIFF_TYPE_JSON = 'json'
53
53
54 REFMAP_STR = '<a href="{}">&gt;&gt;{}</a>'
54 REFMAP_STR = '<a href="{}">&gt;&gt;{}</a>'
55
55
56
56
57 class PostManager(models.Manager):
57 class PostManager(models.Manager):
58 @transaction.atomic
58 @transaction.atomic
59 def create_post(self, title: str, text: str, image=None, thread=None,
59 def create_post(self, title: str, text: str, image=None, thread=None,
60 ip=NO_IP, tags: list=None, threads: list=None):
60 ip=NO_IP, tags: list=None, threads: list=None):
61 """
61 """
62 Creates new post
62 Creates new post
63 """
63 """
64
64
65 is_banned = Ban.objects.filter(ip=ip).exists()
65 is_banned = Ban.objects.filter(ip=ip).exists()
66
66
67 # TODO Raise specific exception and catch it in the views
67 # TODO Raise specific exception and catch it in the views
68 if is_banned:
68 if is_banned:
69 raise Exception("This user is banned")
69 raise Exception("This user is banned")
70
70
71 if not tags:
71 if not tags:
72 tags = []
72 tags = []
73 if not threads:
73 if not threads:
74 threads = []
74 threads = []
75
75
76 posting_time = timezone.now()
76 posting_time = timezone.now()
77 if not thread:
77 if not thread:
78 thread = boards.models.thread.Thread.objects.create(
78 thread = boards.models.thread.Thread.objects.create(
79 bump_time=posting_time, last_edit_time=posting_time)
79 bump_time=posting_time, last_edit_time=posting_time)
80 new_thread = True
80 new_thread = True
81 else:
81 else:
82 new_thread = False
82 new_thread = False
83
83
84 pre_text = Parser().preparse(text)
84 pre_text = Parser().preparse(text)
85
85
86 post = self.create(title=title,
86 post = self.create(title=title,
87 text=pre_text,
87 text=pre_text,
88 pub_time=posting_time,
88 pub_time=posting_time,
89 poster_ip=ip,
89 poster_ip=ip,
90 thread=thread,
90 thread=thread,
91 last_edit_time=posting_time)
91 last_edit_time=posting_time)
92 post.threads.add(thread)
92 post.threads.add(thread)
93
93
94 logger = logging.getLogger('boards.post.create')
94 logger = logging.getLogger('boards.post.create')
95
95
96 logger.info('Created post {} by {}'.format(post, post.poster_ip))
96 logger.info('Created post {} by {}'.format(post, post.poster_ip))
97
97
98 if image:
98 if image:
99 post.images.add(PostImage.objects.create_with_hash(image))
99 post.images.add(PostImage.objects.create_with_hash(image))
100
100
101 list(map(thread.add_tag, tags))
101 list(map(thread.add_tag, tags))
102
102
103 if new_thread:
103 if new_thread:
104 boards.models.thread.Thread.objects.process_oldest_threads()
104 boards.models.thread.Thread.objects.process_oldest_threads()
105 else:
105 else:
106 thread.last_edit_time = posting_time
106 thread.last_edit_time = posting_time
107 thread.bump()
107 thread.bump()
108 thread.save()
108 thread.save()
109
109
110 post.connect_replies()
110 post.connect_replies()
111 post.connect_threads(threads)
111 post.connect_threads(threads)
112 post.connect_notifications()
112 post.connect_notifications()
113
113
114 post.build_url()
114 post.build_url()
115
115
116 return post
116 return post
117
117
118 def delete_posts_by_ip(self, ip):
118 def delete_posts_by_ip(self, ip):
119 """
119 """
120 Deletes all posts of the author with same IP
120 Deletes all posts of the author with same IP
121 """
121 """
122
122
123 posts = self.filter(poster_ip=ip)
123 posts = self.filter(poster_ip=ip)
124 for post in posts:
124 for post in posts:
125 post.delete()
125 post.delete()
126
126
127 @utils.cached_result()
127 @utils.cached_result()
128 def get_posts_per_day(self) -> float:
128 def get_posts_per_day(self) -> float:
129 """
129 """
130 Gets average count of posts per day for the last 7 days
130 Gets average count of posts per day for the last 7 days
131 """
131 """
132
132
133 day_end = date.today()
133 day_end = date.today()
134 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
134 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
135
135
136 day_time_start = timezone.make_aware(datetime.combine(
136 day_time_start = timezone.make_aware(datetime.combine(
137 day_start, dtime()), timezone.get_current_timezone())
137 day_start, dtime()), timezone.get_current_timezone())
138 day_time_end = timezone.make_aware(datetime.combine(
138 day_time_end = timezone.make_aware(datetime.combine(
139 day_end, dtime()), timezone.get_current_timezone())
139 day_end, dtime()), timezone.get_current_timezone())
140
140
141 posts_per_period = float(self.filter(
141 posts_per_period = float(self.filter(
142 pub_time__lte=day_time_end,
142 pub_time__lte=day_time_end,
143 pub_time__gte=day_time_start).count())
143 pub_time__gte=day_time_start).count())
144
144
145 ppd = posts_per_period / POSTS_PER_DAY_RANGE
145 ppd = posts_per_period / POSTS_PER_DAY_RANGE
146
146
147 return ppd
147 return ppd
148
148
149
149
150 class Post(models.Model, Viewable):
150 class Post(models.Model, Viewable):
151 """A post is a message."""
151 """A post is a message."""
152
152
153 objects = PostManager()
153 objects = PostManager()
154
154
155 class Meta:
155 class Meta:
156 app_label = APP_LABEL_BOARDS
156 app_label = APP_LABEL_BOARDS
157 ordering = ('id',)
157 ordering = ('id',)
158
158
159 title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
159 title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
160 pub_time = models.DateTimeField()
160 pub_time = models.DateTimeField()
161 text = TextField(blank=True, null=True)
161 text = TextField(blank=True, null=True)
162 _text_rendered = TextField(blank=True, null=True, editable=False)
162 _text_rendered = TextField(blank=True, null=True, editable=False)
163
163
164 images = models.ManyToManyField(PostImage, null=True, blank=True,
164 images = models.ManyToManyField(PostImage, null=True, blank=True,
165 related_name='ip+', db_index=True)
165 related_name='ip+', db_index=True)
166
166
167 poster_ip = models.GenericIPAddressField()
167 poster_ip = models.GenericIPAddressField()
168
168
169 last_edit_time = models.DateTimeField()
169 last_edit_time = models.DateTimeField()
170
170
171 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
171 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
172 null=True,
172 null=True,
173 blank=True, related_name='rfp+',
173 blank=True, related_name='rfp+',
174 db_index=True)
174 db_index=True)
175 refmap = models.TextField(null=True, blank=True)
175 refmap = models.TextField(null=True, blank=True)
176 threads = models.ManyToManyField('Thread', db_index=True)
176 threads = models.ManyToManyField('Thread', db_index=True)
177 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
177 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
178 url = models.TextField()
178 url = models.TextField()
179
179
180 def __str__(self):
180 def __str__(self):
181 return 'P#{}/{}'.format(self.id, self.title)
181 return 'P#{}/{}'.format(self.id, self.title)
182
182
183 def get_title(self) -> str:
183 def get_title(self) -> str:
184 """
184 """
185 Gets original post title or part of its text.
185 Gets original post title or part of its text.
186 """
186 """
187
187
188 title = self.title
188 title = self.title
189 if not title:
189 if not title:
190 title = self.get_text()
190 title = self.get_text()
191
191
192 return title
192 return title
193
193
194 def build_refmap(self) -> None:
194 def build_refmap(self) -> None:
195 """
195 """
196 Builds a replies map string from replies list. This is a cache to stop
196 Builds a replies map string from replies list. This is a cache to stop
197 the server from recalculating the map on every post show.
197 the server from recalculating the map on every post show.
198 """
198 """
199
199
200 post_urls = [REFMAP_STR.format(refpost.get_url(), refpost.id)
200 post_urls = [REFMAP_STR.format(refpost.get_url(), refpost.id)
201 for refpost in self.referenced_posts.all()]
201 for refpost in self.referenced_posts.all()]
202
202
203 self.refmap = ', '.join(post_urls)
203 self.refmap = ', '.join(post_urls)
204
204
205 def is_referenced(self) -> bool:
205 def is_referenced(self) -> bool:
206 return self.refmap and len(self.refmap) > 0
206 return self.refmap and len(self.refmap) > 0
207
207
208 def is_opening(self) -> bool:
208 def is_opening(self) -> bool:
209 """
209 """
210 Checks if this is an opening post or just a reply.
210 Checks if this is an opening post or just a reply.
211 """
211 """
212
212
213 return self.get_thread().get_opening_post_id() == self.id
213 return self.get_thread().get_opening_post_id() == self.id
214
214
215 def get_url(self):
215 def get_url(self):
216 return self.url
216 return self.url
217
217
218 def get_thread(self):
218 def get_thread(self):
219 return self.thread
219 return self.thread
220
220
221 def get_threads(self) -> list:
221 def get_threads(self) -> list:
222 """
222 """
223 Gets post's thread.
223 Gets post's thread.
224 """
224 """
225
225
226 return self.threads
226 return self.threads
227
227
228 def get_view(self, moderator=False, need_open_link=False,
228 def get_view(self, moderator=False, need_open_link=False,
229 truncated=False, reply_link=False, *args, **kwargs) -> str:
229 truncated=False, reply_link=False, *args, **kwargs) -> str:
230 """
230 """
231 Renders post's HTML view. Some of the post params can be passed over
231 Renders post's HTML view. Some of the post params can be passed over
232 kwargs for the means of caching (if we view the thread, some params
232 kwargs for the means of caching (if we view the thread, some params
233 are same for every post and don't need to be computed over and over.
233 are same for every post and don't need to be computed over and over.
234 """
234 """
235
235
236 thread = self.get_thread()
236 thread = self.get_thread()
237 is_opening = kwargs.get(PARAMETER_IS_OPENING, self.is_opening())
237 is_opening = kwargs.get(PARAMETER_IS_OPENING, self.is_opening())
238
238
239 if is_opening:
239 if is_opening:
240 opening_post_id = self.id
240 opening_post_id = self.id
241 else:
241 else:
242 opening_post_id = thread.get_opening_post_id()
242 opening_post_id = thread.get_opening_post_id()
243
243
244 css_class = 'post'
244 css_class = 'post'
245 if thread.archived:
245 if thread.archived:
246 css_class += ' archive_post'
246 css_class += ' archive_post'
247 elif not thread.can_bump():
247 elif not thread.can_bump():
248 css_class += ' dead_post'
248 css_class += ' dead_post'
249
249
250 return render_to_string('boards/post.html', {
250 return render_to_string('boards/post.html', {
251 PARAMETER_POST: self,
251 PARAMETER_POST: self,
252 PARAMETER_MODERATOR: moderator,
252 PARAMETER_MODERATOR: moderator,
253 PARAMETER_IS_OPENING: is_opening,
253 PARAMETER_IS_OPENING: is_opening,
254 PARAMETER_THREAD: thread,
254 PARAMETER_THREAD: thread,
255 PARAMETER_CSS_CLASS: css_class,
255 PARAMETER_CSS_CLASS: css_class,
256 PARAMETER_NEED_OPEN_LINK: need_open_link,
256 PARAMETER_NEED_OPEN_LINK: need_open_link,
257 PARAMETER_TRUNCATED: truncated,
257 PARAMETER_TRUNCATED: truncated,
258 PARAMETER_OP_ID: opening_post_id,
258 PARAMETER_OP_ID: opening_post_id,
259 PARAMETER_REPLY_LINK: reply_link,
259 PARAMETER_REPLY_LINK: reply_link,
260 })
260 })
261
261
262 def get_search_view(self, *args, **kwargs):
262 def get_search_view(self, *args, **kwargs):
263 return self.get_view(args, kwargs)
263 return self.get_view(args, kwargs)
264
264
265 def get_first_image(self) -> PostImage:
265 def get_first_image(self) -> PostImage:
266 return self.images.earliest('id')
266 return self.images.earliest('id')
267
267
268 def delete(self, using=None):
268 def delete(self, using=None):
269 """
269 """
270 Deletes all post images and the post itself.
270 Deletes all post images and the post itself.
271 """
271 """
272
272
273 for image in self.images.all():
273 for image in self.images.all():
274 image_refs_count = Post.objects.filter(images__in=[image]).count()
274 image_refs_count = Post.objects.filter(images__in=[image]).count()
275 if image_refs_count == 1:
275 if image_refs_count == 1:
276 image.delete()
276 image.delete()
277
277
278 thread = self.get_thread()
278 thread = self.get_thread()
279 thread.last_edit_time = timezone.now()
279 thread.last_edit_time = timezone.now()
280 thread.save()
280 thread.save()
281
281
282 super(Post, self).delete(using)
282 super(Post, self).delete(using)
283
283
284 logging.getLogger('boards.post.delete').info(
284 logging.getLogger('boards.post.delete').info(
285 'Deleted post {}'.format(self))
285 'Deleted post {}'.format(self))
286
286
287 # TODO Implement this with OOP, e.g. use the factory and HtmlPostData class
287 # TODO Implement this with OOP, e.g. use the factory and HtmlPostData class
288 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
288 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
289 include_last_update=False) -> str:
289 include_last_update=False) -> str:
290 """
290 """
291 Gets post HTML or JSON data that can be rendered on a page or used by
291 Gets post HTML or JSON data that can be rendered on a page or used by
292 API.
292 API.
293 """
293 """
294
294
295 if format_type == DIFF_TYPE_HTML:
295 if format_type == DIFF_TYPE_HTML:
296 if PARAMETER_TRUNCATED in request.GET:
296 if request is not None and PARAMETER_TRUNCATED in request.GET:
297 truncated = True
297 truncated = True
298 reply_link = False
298 reply_link = False
299 else:
299 else:
300 truncated = False
300 truncated = False
301 reply_link = True
301 reply_link = True
302
302
303 return self.get_view(truncated=truncated, reply_link=reply_link,
303 return self.get_view(truncated=truncated, reply_link=reply_link,
304 moderator=utils.is_moderator(request))
304 moderator=utils.is_moderator(request))
305 elif format_type == DIFF_TYPE_JSON:
305 elif format_type == DIFF_TYPE_JSON:
306 post_json = {
306 post_json = {
307 'id': self.id,
307 'id': self.id,
308 'title': self.title,
308 'title': self.title,
309 'text': self._text_rendered,
309 'text': self._text_rendered,
310 }
310 }
311 if self.images.exists():
311 if self.images.exists():
312 post_image = self.get_first_image()
312 post_image = self.get_first_image()
313 post_json['image'] = post_image.image.url
313 post_json['image'] = post_image.image.url
314 post_json['image_preview'] = post_image.image.url_200x150
314 post_json['image_preview'] = post_image.image.url_200x150
315 if include_last_update:
315 if include_last_update:
316 post_json['bump_time'] = utils.datetime_to_epoch(
316 post_json['bump_time'] = utils.datetime_to_epoch(
317 self.get_thread().bump_time)
317 self.get_thread().bump_time)
318 return post_json
318 return post_json
319
319
320 def notify_clients(self, recursive=True):
320 def notify_clients(self, recursive=True):
321 """
321 """
322 Sends post HTML data to the thread web socket.
322 Sends post HTML data to the thread web socket.
323 """
323 """
324
324
325 if not settings.WEBSOCKETS_ENABLED:
325 if not settings.WEBSOCKETS_ENABLED:
326 return
326 return
327
327
328 thread_ids = list()
328 thread_ids = list()
329 for thread in self.get_threads().all():
329 for thread in self.get_threads().all():
330 thread_ids.append(thread.id)
330 thread_ids.append(thread.id)
331
331
332 thread.notify_clients()
332 thread.notify_clients()
333
333
334 if recursive:
334 if recursive:
335 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
335 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
336 post_id = reply_number.group(1)
336 post_id = reply_number.group(1)
337
337
338 try:
338 try:
339 ref_post = Post.objects.get(id=post_id)
339 ref_post = Post.objects.get(id=post_id)
340
340
341 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
341 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
342 # If post is in this thread, its thread was already notified.
342 # If post is in this thread, its thread was already notified.
343 # Otherwise, notify its thread separately.
343 # Otherwise, notify its thread separately.
344 ref_post.notify_clients(recursive=False)
344 ref_post.notify_clients(recursive=False)
345 except ObjectDoesNotExist:
345 except ObjectDoesNotExist:
346 pass
346 pass
347
347
348 def build_url(self):
348 def build_url(self):
349 thread = self.get_thread()
349 thread = self.get_thread()
350 opening_id = thread.get_opening_post_id()
350 opening_id = thread.get_opening_post_id()
351 post_url = reverse('thread', kwargs={'post_id': opening_id})
351 post_url = reverse('thread', kwargs={'post_id': opening_id})
352 if self.id != opening_id:
352 if self.id != opening_id:
353 post_url += '#' + str(self.id)
353 post_url += '#' + str(self.id)
354 self.url = post_url
354 self.url = post_url
355 self.save(update_fields=['url'])
355 self.save(update_fields=['url'])
356
356
357 def save(self, force_insert=False, force_update=False, using=None,
357 def save(self, force_insert=False, force_update=False, using=None,
358 update_fields=None):
358 update_fields=None):
359 self._text_rendered = Parser().parse(self.get_raw_text())
359 self._text_rendered = Parser().parse(self.get_raw_text())
360
360
361 if self.id:
361 if self.id:
362 for thread in self.get_threads().all():
362 for thread in self.get_threads().all():
363 if thread.can_bump():
363 if thread.can_bump():
364 thread.update_bump_status()
364 thread.update_bump_status()
365 thread.last_edit_time = self.last_edit_time
365 thread.last_edit_time = self.last_edit_time
366
366
367 thread.save(update_fields=['last_edit_time', 'bumpable'])
367 thread.save(update_fields=['last_edit_time', 'bumpable'])
368
368
369 super().save(force_insert, force_update, using, update_fields)
369 super().save(force_insert, force_update, using, update_fields)
370
370
371 def get_text(self) -> str:
371 def get_text(self) -> str:
372 return self._text_rendered
372 return self._text_rendered
373
373
374 def get_raw_text(self) -> str:
374 def get_raw_text(self) -> str:
375 return self.text
375 return self.text
376
376
377 def get_absolute_id(self) -> str:
377 def get_absolute_id(self) -> str:
378 """
378 """
379 If the post has many threads, shows its main thread OP id in the post
379 If the post has many threads, shows its main thread OP id in the post
380 ID.
380 ID.
381 """
381 """
382
382
383 if self.get_threads().count() > 1:
383 if self.get_threads().count() > 1:
384 return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id)
384 return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id)
385 else:
385 else:
386 return str(self.id)
386 return str(self.id)
387
387
388 def connect_notifications(self):
388 def connect_notifications(self):
389 for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()):
389 for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()):
390 user_name = reply_number.group(1).lower()
390 user_name = reply_number.group(1).lower()
391 Notification.objects.get_or_create(name=user_name, post=self)
391 Notification.objects.get_or_create(name=user_name, post=self)
392
392
393 def connect_replies(self):
393 def connect_replies(self):
394 """
394 """
395 Connects replies to a post to show them as a reflink map
395 Connects replies to a post to show them as a reflink map
396 """
396 """
397
397
398 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
398 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
399 post_id = reply_number.group(1)
399 post_id = reply_number.group(1)
400
400
401 try:
401 try:
402 referenced_post = Post.objects.get(id=post_id)
402 referenced_post = Post.objects.get(id=post_id)
403
403
404 referenced_post.referenced_posts.add(self)
404 referenced_post.referenced_posts.add(self)
405 referenced_post.last_edit_time = self.pub_time
405 referenced_post.last_edit_time = self.pub_time
406 referenced_post.build_refmap()
406 referenced_post.build_refmap()
407 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
407 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
408 except ObjectDoesNotExist:
408 except ObjectDoesNotExist:
409 pass
409 pass
410
410
411 def connect_threads(self, opening_posts):
411 def connect_threads(self, opening_posts):
412 """
412 """
413 If the referenced post is an OP in another thread,
413 If the referenced post is an OP in another thread,
414 make this post multi-thread.
414 make this post multi-thread.
415 """
415 """
416
416
417 for opening_post in opening_posts:
417 for opening_post in opening_posts:
418 threads = opening_post.get_threads().all()
418 threads = opening_post.get_threads().all()
419 for thread in threads:
419 for thread in threads:
420 if thread.can_bump():
420 if thread.can_bump():
421 thread.update_bump_status()
421 thread.update_bump_status()
422
422
423 thread.last_edit_time = self.last_edit_time
423 thread.last_edit_time = self.last_edit_time
424 thread.save(update_fields=['last_edit_time', 'bumpable'])
424 thread.save(update_fields=['last_edit_time', 'bumpable'])
425
425
426 self.threads.add(thread)
426 self.threads.add(thread)
@@ -1,240 +1,238
1 from datetime import datetime
2 import json
1 import json
3 import logging
2 import logging
3
4 from django.db import transaction
4 from django.db import transaction
5 from django.http import HttpResponse
5 from django.http import HttpResponse
6 from django.shortcuts import get_object_or_404, render
6 from django.shortcuts import get_object_or_404
7 from django.template import RequestContext
8 from django.utils import timezone
9 from django.core import serializers
7 from django.core import serializers
10
8
11 from boards.forms import PostForm, PlainErrorList
9 from boards.forms import PostForm, PlainErrorList
12 from boards.models import Post, Thread, Tag
10 from boards.models import Post, Thread, Tag
13 from boards.utils import datetime_to_epoch
11 from boards.utils import datetime_to_epoch
14 from boards.views.thread import ThreadView
12 from boards.views.thread import ThreadView
15 from boards.models.user import Notification
13 from boards.models.user import Notification
16
14
15
17 __author__ = 'neko259'
16 __author__ = 'neko259'
18
17
19 PARAMETER_TRUNCATED = 'truncated'
18 PARAMETER_TRUNCATED = 'truncated'
20 PARAMETER_TAG = 'tag'
19 PARAMETER_TAG = 'tag'
21 PARAMETER_OFFSET = 'offset'
20 PARAMETER_OFFSET = 'offset'
22 PARAMETER_DIFF_TYPE = 'type'
21 PARAMETER_DIFF_TYPE = 'type'
23 PARAMETER_POST = 'post'
22 PARAMETER_POST = 'post'
24 PARAMETER_ADDED = 'added'
23 PARAMETER_ADDED = 'added'
25 PARAMETER_UPDATED = 'updated'
24 PARAMETER_UPDATED = 'updated'
26 PARAMETER_LAST_UPDATE = 'last_update'
25 PARAMETER_LAST_UPDATE = 'last_update'
27
26
28 DIFF_TYPE_HTML = 'html'
27 DIFF_TYPE_HTML = 'html'
29 DIFF_TYPE_JSON = 'json'
28 DIFF_TYPE_JSON = 'json'
30
29
31 STATUS_OK = 'ok'
30 STATUS_OK = 'ok'
32 STATUS_ERROR = 'error'
31 STATUS_ERROR = 'error'
33
32
34 logger = logging.getLogger(__name__)
33 logger = logging.getLogger(__name__)
35
34
36
35
37 @transaction.atomic
36 @transaction.atomic
38 def api_get_threaddiff(request):
37 def api_get_threaddiff(request):
39 """
38 """
40 Gets posts that were changed or added since time
39 Gets posts that were changed or added since time
41 """
40 """
42
41
43 thread_id = request.GET.get('thread')
42 thread_id = request.GET.get('thread')
44 last_update_time = request.GET.get('last_update')
43 last_update_time = request.GET.get('last_update')
45 last_post = request.GET.get('last_post')
44 last_post = request.GET.get('last_post')
46
45
47 thread = get_object_or_404(Post, id=thread_id).get_thread()
46 thread = get_object_or_404(Post, id=thread_id).get_thread()
48
47
49 json_data = {
48 json_data = {
50 PARAMETER_ADDED: [],
49 PARAMETER_ADDED: [],
51 PARAMETER_UPDATED: [],
50 PARAMETER_UPDATED: [],
52 'last_update': None,
51 'last_update': None,
53 }
52 }
54 added_posts = Post.objects.filter(threads__in=[thread],
53 added_posts = Post.objects.filter(threads__in=[thread],
55 id__gt=int(last_post)) \
54 id__gt=int(last_post)) \
56 .order_by('pub_time')
55 .order_by('pub_time')
57 updated_posts = Post.objects.filter(threads__in=[thread],
56 updated_posts = Post.objects.filter(threads__in=[thread],
58 pub_time__lte=last_update_time,
57 pub_time__lte=last_update_time,
59 last_edit_time__gt=last_update_time)
58 last_edit_time__gt=last_update_time)
60
59
61 diff_type = request.GET.get(PARAMETER_DIFF_TYPE, DIFF_TYPE_HTML)
60 diff_type = request.GET.get(PARAMETER_DIFF_TYPE, DIFF_TYPE_HTML)
62
61
63 for post in added_posts:
62 for post in added_posts:
64 json_data[PARAMETER_ADDED].append(get_post_data(post.id, diff_type, request))
63 json_data[PARAMETER_ADDED].append(get_post_data(post.id, diff_type, request))
65 for post in updated_posts:
64 for post in updated_posts:
66 json_data[PARAMETER_UPDATED].append(get_post_data(post.id, diff_type, request))
65 json_data[PARAMETER_UPDATED].append(get_post_data(post.id, diff_type, request))
67 json_data[PARAMETER_LAST_UPDATE] = str(thread.last_edit_time)
66 json_data[PARAMETER_LAST_UPDATE] = str(thread.last_edit_time)
68
67
69 return HttpResponse(content=json.dumps(json_data))
68 return HttpResponse(content=json.dumps(json_data))
70
69
71
70
72 def api_add_post(request, opening_post_id):
71 def api_add_post(request, opening_post_id):
73 """
72 """
74 Adds a post and return the JSON response for it
73 Adds a post and return the JSON response for it
75 """
74 """
76
75
77 opening_post = get_object_or_404(Post, id=opening_post_id)
76 opening_post = get_object_or_404(Post, id=opening_post_id)
78
77
79 logger.info('Adding post via api...')
78 logger.info('Adding post via api...')
80
79
81 status = STATUS_OK
80 status = STATUS_OK
82 errors = []
81 errors = []
83
82
84 if request.method == 'POST':
83 if request.method == 'POST':
85 form = PostForm(request.POST, request.FILES, error_class=PlainErrorList)
84 form = PostForm(request.POST, request.FILES, error_class=PlainErrorList)
86 form.session = request.session
85 form.session = request.session
87
86
88 if form.need_to_ban:
87 if form.need_to_ban:
89 # Ban user because he is suspected to be a bot
88 # Ban user because he is suspected to be a bot
90 # _ban_current_user(request)
89 # _ban_current_user(request)
91 status = STATUS_ERROR
90 status = STATUS_ERROR
92 if form.is_valid():
91 if form.is_valid():
93 post = ThreadView().new_post(request, form, opening_post,
92 post = ThreadView().new_post(request, form, opening_post,
94 html_response=False)
93 html_response=False)
95 if not post:
94 if not post:
96 status = STATUS_ERROR
95 status = STATUS_ERROR
97 else:
96 else:
98 logger.info('Added post #%d via api.' % post.id)
97 logger.info('Added post #%d via api.' % post.id)
99 else:
98 else:
100 status = STATUS_ERROR
99 status = STATUS_ERROR
101 errors = form.as_json_errors()
100 errors = form.as_json_errors()
102
101
103 response = {
102 response = {
104 'status': status,
103 'status': status,
105 'errors': errors,
104 'errors': errors,
106 }
105 }
107
106
108 return HttpResponse(content=json.dumps(response))
107 return HttpResponse(content=json.dumps(response))
109
108
110
109
111 def get_post(request, post_id):
110 def get_post(request, post_id):
112 """
111 """
113 Gets the html of a post. Used for popups. Post can be truncated if used
112 Gets the html of a post. Used for popups. Post can be truncated if used
114 in threads list with 'truncated' get parameter.
113 in threads list with 'truncated' get parameter.
115 """
114 """
116
115
117 post = get_object_or_404(Post, id=post_id)
116 post = get_object_or_404(Post, id=post_id)
118 truncated = PARAMETER_TRUNCATED in request.GET
117 truncated = PARAMETER_TRUNCATED in request.GET
119
118
120 return HttpResponse(content=post.get_view(truncated=truncated))
119 return HttpResponse(content=post.get_view(truncated=truncated))
121
120
122
121
123 def api_get_threads(request, count):
122 def api_get_threads(request, count):
124 """
123 """
125 Gets the JSON thread opening posts list.
124 Gets the JSON thread opening posts list.
126 Parameters that can be used for filtering:
125 Parameters that can be used for filtering:
127 tag, offset (from which thread to get results)
126 tag, offset (from which thread to get results)
128 """
127 """
129
128
130 if PARAMETER_TAG in request.GET:
129 if PARAMETER_TAG in request.GET:
131 tag_name = request.GET[PARAMETER_TAG]
130 tag_name = request.GET[PARAMETER_TAG]
132 if tag_name is not None:
131 if tag_name is not None:
133 tag = get_object_or_404(Tag, name=tag_name)
132 tag = get_object_or_404(Tag, name=tag_name)
134 threads = tag.get_threads().filter(archived=False)
133 threads = tag.get_threads().filter(archived=False)
135 else:
134 else:
136 threads = Thread.objects.filter(archived=False)
135 threads = Thread.objects.filter(archived=False)
137
136
138 if PARAMETER_OFFSET in request.GET:
137 if PARAMETER_OFFSET in request.GET:
139 offset = request.GET[PARAMETER_OFFSET]
138 offset = request.GET[PARAMETER_OFFSET]
140 offset = int(offset) if offset is not None else 0
139 offset = int(offset) if offset is not None else 0
141 else:
140 else:
142 offset = 0
141 offset = 0
143
142
144 threads = threads.order_by('-bump_time')
143 threads = threads.order_by('-bump_time')
145 threads = threads[offset:offset + int(count)]
144 threads = threads[offset:offset + int(count)]
146
145
147 opening_posts = []
146 opening_posts = []
148 for thread in threads:
147 for thread in threads:
149 opening_post = thread.get_opening_post()
148 opening_post = thread.get_opening_post()
150
149
151 # TODO Add tags, replies and images count
150 # TODO Add tags, replies and images count
152 post_data = get_post_data(opening_post.id, include_last_update=True)
151 post_data = get_post_data(opening_post.id, include_last_update=True)
153 post_data['bumpable'] = thread.can_bump()
152 post_data['bumpable'] = thread.can_bump()
154 post_data['archived'] = thread.archived
153 post_data['archived'] = thread.archived
155
154
156 opening_posts.append(post_data)
155 opening_posts.append(post_data)
157
156
158 return HttpResponse(content=json.dumps(opening_posts))
157 return HttpResponse(content=json.dumps(opening_posts))
159
158
160
159
161 # TODO Test this
160 # TODO Test this
162 def api_get_tags(request):
161 def api_get_tags(request):
163 """
162 """
164 Gets all tags or user tags.
163 Gets all tags or user tags.
165 """
164 """
166
165
167 # TODO Get favorite tags for the given user ID
166 # TODO Get favorite tags for the given user ID
168
167
169 tags = Tag.objects.get_not_empty_tags()
168 tags = Tag.objects.get_not_empty_tags()
170
169
171 term = request.GET.get('term')
170 term = request.GET.get('term')
172 if term is not None:
171 if term is not None:
173 tags = tags.filter(name__contains=term)
172 tags = tags.filter(name__contains=term)
174
173
175 tag_names = [tag.name for tag in tags]
174 tag_names = [tag.name for tag in tags]
176
175
177 return HttpResponse(content=json.dumps(tag_names))
176 return HttpResponse(content=json.dumps(tag_names))
178
177
179
178
180 # TODO The result can be cached by the thread last update time
179 # TODO The result can be cached by the thread last update time
181 # TODO Test this
180 # TODO Test this
182 def api_get_thread_posts(request, opening_post_id):
181 def api_get_thread_posts(request, opening_post_id):
183 """
182 """
184 Gets the JSON array of thread posts
183 Gets the JSON array of thread posts
185 """
184 """
186
185
187 opening_post = get_object_or_404(Post, id=opening_post_id)
186 opening_post = get_object_or_404(Post, id=opening_post_id)
188 thread = opening_post.get_thread()
187 thread = opening_post.get_thread()
189 posts = thread.get_replies()
188 posts = thread.get_replies()
190
189
191 json_data = {
190 json_data = {
192 'posts': [],
191 'posts': [],
193 'last_update': None,
192 'last_update': None,
194 }
193 }
195 json_post_list = []
194 json_post_list = []
196
195
197 for post in posts:
196 for post in posts:
198 json_post_list.append(get_post_data(post.id))
197 json_post_list.append(get_post_data(post.id))
199 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
198 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
200 json_data['posts'] = json_post_list
199 json_data['posts'] = json_post_list
201
200
202 return HttpResponse(content=json.dumps(json_data))
201 return HttpResponse(content=json.dumps(json_data))
203
202
204
203
205 def api_get_notifications(request, username):
204 def api_get_notifications(request, username):
206 last_notification_id_str = request.GET.get('last', None)
205 last_notification_id_str = request.GET.get('last', None)
207 last_id = int(last_notification_id_str) if last_notification_id_str is not None else None
206 last_id = int(last_notification_id_str) if last_notification_id_str is not None else None
208
207
209 posts = Notification.objects.get_notification_posts(username=username,
208 posts = Notification.objects.get_notification_posts(username=username,
210 last=last_id)
209 last=last_id)
211
210
212 json_post_list = []
211 json_post_list = []
213 for post in posts:
212 for post in posts:
214 json_post_list.append(get_post_data(post.id))
213 json_post_list.append(get_post_data(post.id))
215 return HttpResponse(content=json.dumps(json_post_list))
214 return HttpResponse(content=json.dumps(json_post_list))
216
215
217
216
218
219 def api_get_post(request, post_id):
217 def api_get_post(request, post_id):
220 """
218 """
221 Gets the JSON of a post. This can be
219 Gets the JSON of a post. This can be
222 used as and API for external clients.
220 used as and API for external clients.
223 """
221 """
224
222
225 post = get_object_or_404(Post, id=post_id)
223 post = get_object_or_404(Post, id=post_id)
226
224
227 json = serializers.serialize("json", [post], fields=(
225 json = serializers.serialize("json", [post], fields=(
228 "pub_time", "_text_rendered", "title", "text", "image",
226 "pub_time", "_text_rendered", "title", "text", "image",
229 "image_width", "image_height", "replies", "tags"
227 "image_width", "image_height", "replies", "tags"
230 ))
228 ))
231
229
232 return HttpResponse(content=json)
230 return HttpResponse(content=json)
233
231
234
232
235 # TODO Remove this method and use post method directly
233 # TODO Remove this method and use post method directly
236 def get_post_data(post_id, format_type=DIFF_TYPE_JSON, request=None,
234 def get_post_data(post_id, format_type=DIFF_TYPE_JSON, request=None,
237 include_last_update=False):
235 include_last_update=False):
238 post = get_object_or_404(Post, id=post_id)
236 post = get_object_or_404(Post, id=post_id)
239 return post.get_post_data(format_type=format_type, request=request,
237 return post.get_post_data(format_type=format_type, request=request,
240 include_last_update=include_last_update)
238 include_last_update=include_last_update)
General Comments 0
You need to be logged in to leave comments. Login now