##// END OF EJS Templates
Refactoring
neko259 -
r917:3aa27a4d default
parent child Browse files
Show More
@@ -1,438 +1,438
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 adjacent import Client
6 from adjacent import Client
7 from django.core.cache import cache
7 from django.core.cache import cache
8 from django.core.urlresolvers import reverse
8 from django.core.urlresolvers import reverse
9 from django.db import models, transaction
9 from django.db import models, transaction
10 from django.db.models import TextField
10 from django.db.models import TextField
11 from django.template import RequestContext
12 from django.template.loader import render_to_string
11 from django.template.loader import render_to_string
13 from django.utils import timezone
12 from django.utils import timezone
14
13
15 from boards import settings
14 from boards import settings
16 from boards.mdx_neboard import bbcode_extended
15 from boards.mdx_neboard import bbcode_extended
17 from boards.models import PostImage
16 from boards.models import PostImage
18 from boards.models.base import Viewable
17 from boards.models.base import Viewable
19 from boards.models.thread import Thread
18 from boards.models.thread import Thread
20 from boards.utils import datetime_to_epoch
19 from boards.utils import datetime_to_epoch
21
20
21
22 WS_NOTIFICATION_TYPE_NEW_POST = 'new_post'
22 WS_NOTIFICATION_TYPE_NEW_POST = 'new_post'
23 WS_NOTIFICATION_TYPE = 'notification_type'
23 WS_NOTIFICATION_TYPE = 'notification_type'
24
24
25 WS_CHANNEL_THREAD = "thread:"
25 WS_CHANNEL_THREAD = "thread:"
26
26
27 APP_LABEL_BOARDS = 'boards'
27 APP_LABEL_BOARDS = 'boards'
28
28
29 CACHE_KEY_PPD = 'ppd'
29 CACHE_KEY_PPD = 'ppd'
30 CACHE_KEY_POST_URL = 'post_url'
30 CACHE_KEY_POST_URL = 'post_url'
31
31
32 POSTS_PER_DAY_RANGE = 7
32 POSTS_PER_DAY_RANGE = 7
33
33
34 BAN_REASON_AUTO = 'Auto'
34 BAN_REASON_AUTO = 'Auto'
35
35
36 IMAGE_THUMB_SIZE = (200, 150)
36 IMAGE_THUMB_SIZE = (200, 150)
37
37
38 TITLE_MAX_LENGTH = 200
38 TITLE_MAX_LENGTH = 200
39
39
40 # TODO This should be removed
40 # TODO This should be removed
41 NO_IP = '0.0.0.0'
41 NO_IP = '0.0.0.0'
42
42
43 # TODO Real user agent should be saved instead of this
43 # TODO Real user agent should be saved instead of this
44 UNKNOWN_UA = ''
44 UNKNOWN_UA = ''
45
45
46 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
46 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
47
47
48 PARAMETER_TRUNCATED = 'truncated'
48 PARAMETER_TRUNCATED = 'truncated'
49 PARAMETER_TAG = 'tag'
49 PARAMETER_TAG = 'tag'
50 PARAMETER_OFFSET = 'offset'
50 PARAMETER_OFFSET = 'offset'
51 PARAMETER_DIFF_TYPE = 'type'
51 PARAMETER_DIFF_TYPE = 'type'
52 PARAMETER_BUMPABLE = 'bumpable'
53 PARAMETER_THREAD = 'thread'
54 PARAMETER_IS_OPENING = 'is_opening'
55 PARAMETER_MODERATOR = 'moderator'
56 PARAMETER_POST = 'post'
57 PARAMETER_OP_ID = 'opening_post_id'
58 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
52
59
53 DIFF_TYPE_HTML = 'html'
60 DIFF_TYPE_HTML = 'html'
54 DIFF_TYPE_JSON = 'json'
61 DIFF_TYPE_JSON = 'json'
55
62
56 PREPARSE_PATTERNS = {
63 PREPARSE_PATTERNS = {
57 r'>>(\d+)': r'[post]\1[/post]', # Reflink ">>123"
64 r'>>(\d+)': r'[post]\1[/post]', # Reflink ">>123"
58 r'^>(.+)': r'[quote]\1[/quote]', # Quote ">text"
65 r'^>(.+)': r'[quote]\1[/quote]', # Quote ">text"
59 r'^//(.+)': r'[comment]\1[/comment]', # Comment "//text"
66 r'^//(.+)': r'[comment]\1[/comment]', # Comment "//text"
60 }
67 }
61
68
62
69
63 class PostManager(models.Manager):
70 class PostManager(models.Manager):
64 @transaction.atomic
71 @transaction.atomic
65 def create_post(self, title: str, text: str, image=None, thread=None,
72 def create_post(self, title: str, text: str, image=None, thread=None,
66 ip=NO_IP, tags: list=None):
73 ip=NO_IP, tags: list=None):
67 """
74 """
68 Creates new post
75 Creates new post
69 """
76 """
70
77
71 if not tags:
78 if not tags:
72 tags = []
79 tags = []
73
80
74 posting_time = timezone.now()
81 posting_time = timezone.now()
75 if not thread:
82 if not thread:
76 thread = Thread.objects.create(bump_time=posting_time,
83 thread = Thread.objects.create(bump_time=posting_time,
77 last_edit_time=posting_time)
84 last_edit_time=posting_time)
78 new_thread = True
85 new_thread = True
79 else:
86 else:
80 new_thread = False
87 new_thread = False
81
88
82 pre_text = self._preparse_text(text)
89 pre_text = self._preparse_text(text)
83
90
84 post = self.create(title=title,
91 post = self.create(title=title,
85 text=pre_text,
92 text=pre_text,
86 pub_time=posting_time,
93 pub_time=posting_time,
87 thread_new=thread,
94 thread_new=thread,
88 poster_ip=ip,
95 poster_ip=ip,
89 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
96 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
90 # last!
97 # last!
91 last_edit_time=posting_time)
98 last_edit_time=posting_time)
92
99
93 logger = logging.getLogger('boards.post.create')
100 logger = logging.getLogger('boards.post.create')
94
101
95 logger.info('Created post {} by {}'.format(
102 logger.info('Created post {} by {}'.format(
96 post, post.poster_ip))
103 post, post.poster_ip))
97
104
98 if image:
105 if image:
99 post_image = PostImage.objects.create(image=image)
106 post_image = PostImage.objects.create(image=image)
100 post.images.add(post_image)
107 post.images.add(post_image)
101 logger.info('Created image #{} for post #{}'.format(
108 logger.info('Created image #{} for post #{}'.format(
102 post_image.id, post.id))
109 post_image.id, post.id))
103
110
104 thread.replies.add(post)
111 thread.replies.add(post)
105 list(map(thread.add_tag, tags))
112 list(map(thread.add_tag, tags))
106
113
107 if new_thread:
114 if new_thread:
108 Thread.objects.process_oldest_threads()
115 Thread.objects.process_oldest_threads()
109 else:
116 else:
110 thread.bump()
117 thread.bump()
111 thread.last_edit_time = posting_time
118 thread.last_edit_time = posting_time
112 thread.save()
119 thread.save()
113
120
114 self.connect_replies(post)
121 self.connect_replies(post)
115
122
116 return post
123 return post
117
124
118 def delete_posts_by_ip(self, ip):
125 def delete_posts_by_ip(self, ip):
119 """
126 """
120 Deletes all posts of the author with same IP
127 Deletes all posts of the author with same IP
121 """
128 """
122
129
123 posts = self.filter(poster_ip=ip)
130 posts = self.filter(poster_ip=ip)
124 for post in posts:
131 for post in posts:
125 post.delete()
132 post.delete()
126
133
127 def connect_replies(self, post):
134 def connect_replies(self, post):
128 """
135 """
129 Connects replies to a post to show them as a reflink map
136 Connects replies to a post to show them as a reflink map
130 """
137 """
131
138
132 for reply_number in re.finditer(REGEX_REPLY, post.get_raw_text()):
139 for reply_number in re.finditer(REGEX_REPLY, post.get_raw_text()):
133 post_id = reply_number.group(1)
140 post_id = reply_number.group(1)
134 ref_post = self.filter(id=post_id)
141 ref_post = self.filter(id=post_id)
135 if ref_post.count() > 0:
142 if ref_post.count() > 0:
136 referenced_post = ref_post[0]
143 referenced_post = ref_post[0]
137 referenced_post.referenced_posts.add(post)
144 referenced_post.referenced_posts.add(post)
138 referenced_post.last_edit_time = post.pub_time
145 referenced_post.last_edit_time = post.pub_time
139 referenced_post.build_refmap()
146 referenced_post.build_refmap()
140 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
147 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
141
148
142 referenced_thread = referenced_post.get_thread()
149 referenced_thread = referenced_post.get_thread()
143 referenced_thread.last_edit_time = post.pub_time
150 referenced_thread.last_edit_time = post.pub_time
144 referenced_thread.save(update_fields=['last_edit_time'])
151 referenced_thread.save(update_fields=['last_edit_time'])
145
152
146 def get_posts_per_day(self):
153 def get_posts_per_day(self):
147 """
154 """
148 Gets average count of posts per day for the last 7 days
155 Gets average count of posts per day for the last 7 days
149 """
156 """
150
157
151 day_end = date.today()
158 day_end = date.today()
152 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
159 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
153
160
154 cache_key = CACHE_KEY_PPD + str(day_end)
161 cache_key = CACHE_KEY_PPD + str(day_end)
155 ppd = cache.get(cache_key)
162 ppd = cache.get(cache_key)
156 if ppd:
163 if ppd:
157 return ppd
164 return ppd
158
165
159 day_time_start = timezone.make_aware(datetime.combine(
166 day_time_start = timezone.make_aware(datetime.combine(
160 day_start, dtime()), timezone.get_current_timezone())
167 day_start, dtime()), timezone.get_current_timezone())
161 day_time_end = timezone.make_aware(datetime.combine(
168 day_time_end = timezone.make_aware(datetime.combine(
162 day_end, dtime()), timezone.get_current_timezone())
169 day_end, dtime()), timezone.get_current_timezone())
163
170
164 posts_per_period = float(self.filter(
171 posts_per_period = float(self.filter(
165 pub_time__lte=day_time_end,
172 pub_time__lte=day_time_end,
166 pub_time__gte=day_time_start).count())
173 pub_time__gte=day_time_start).count())
167
174
168 ppd = posts_per_period / POSTS_PER_DAY_RANGE
175 ppd = posts_per_period / POSTS_PER_DAY_RANGE
169
176
170 cache.set(cache_key, ppd)
177 cache.set(cache_key, ppd)
171 return ppd
178 return ppd
172
179
173 def _preparse_text(self, text):
180 def _preparse_text(self, text):
174 """
181 """
175 Preparses text to change patterns like '>>' to a proper bbcode
182 Preparses text to change patterns like '>>' to a proper bbcode
176 tags.
183 tags.
177 """
184 """
178
185
179 for key, value in PREPARSE_PATTERNS.items():
186 for key, value in PREPARSE_PATTERNS.items():
180 text = re.sub(key, value, text, flags=re.MULTILINE)
187 text = re.sub(key, value, text, flags=re.MULTILINE)
181
188
182 return text
189 return text
183
190
184
191
185 class Post(models.Model, Viewable):
192 class Post(models.Model, Viewable):
186 """A post is a message."""
193 """A post is a message."""
187
194
188 objects = PostManager()
195 objects = PostManager()
189
196
190 class Meta:
197 class Meta:
191 app_label = APP_LABEL_BOARDS
198 app_label = APP_LABEL_BOARDS
192 ordering = ('id',)
199 ordering = ('id',)
193
200
194 title = models.CharField(max_length=TITLE_MAX_LENGTH)
201 title = models.CharField(max_length=TITLE_MAX_LENGTH)
195 pub_time = models.DateTimeField()
202 pub_time = models.DateTimeField()
196 text = TextField(blank=True, null=True)
203 text = TextField(blank=True, null=True)
197 _text_rendered = TextField(blank=True, null=True, editable=False)
204 _text_rendered = TextField(blank=True, null=True, editable=False)
198
205
199 images = models.ManyToManyField(PostImage, null=True, blank=True,
206 images = models.ManyToManyField(PostImage, null=True, blank=True,
200 related_name='ip+', db_index=True)
207 related_name='ip+', db_index=True)
201
208
202 poster_ip = models.GenericIPAddressField()
209 poster_ip = models.GenericIPAddressField()
203 poster_user_agent = models.TextField()
210 poster_user_agent = models.TextField()
204
211
205 thread_new = models.ForeignKey('Thread', null=True, default=None,
212 thread_new = models.ForeignKey('Thread', null=True, default=None,
206 db_index=True)
213 db_index=True)
207 last_edit_time = models.DateTimeField()
214 last_edit_time = models.DateTimeField()
208
215
209 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
216 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
210 null=True,
217 null=True,
211 blank=True, related_name='rfp+',
218 blank=True, related_name='rfp+',
212 db_index=True)
219 db_index=True)
213 refmap = models.TextField(null=True, blank=True)
220 refmap = models.TextField(null=True, blank=True)
214
221
215 def __str__(self):
222 def __str__(self):
216 return 'P#{}/{}'.format(self.id, self.title)
223 return 'P#{}/{}'.format(self.id, self.title)
217
224
218 def get_title(self) -> str:
225 def get_title(self) -> str:
219 """
226 """
220 Gets original post title or part of its text.
227 Gets original post title or part of its text.
221 """
228 """
222
229
223 title = self.title
230 title = self.title
224 if not title:
231 if not title:
225 title = self.get_text()
232 title = self.get_text()
226
233
227 return title
234 return title
228
235
229 def build_refmap(self) -> None:
236 def build_refmap(self) -> None:
230 """
237 """
231 Builds a replies map string from replies list. This is a cache to stop
238 Builds a replies map string from replies list. This is a cache to stop
232 the server from recalculating the map on every post show.
239 the server from recalculating the map on every post show.
233 """
240 """
234 map_string = ''
241 map_string = ''
235
242
236 first = True
243 first = True
237 for refpost in self.referenced_posts.all():
244 for refpost in self.referenced_posts.all():
238 if not first:
245 if not first:
239 map_string += ', '
246 map_string += ', '
240 map_string += '<a href="%s">&gt;&gt;%s</a>' % (refpost.get_url(),
247 map_string += '<a href="%s">&gt;&gt;%s</a>' % (refpost.get_url(),
241 refpost.id)
248 refpost.id)
242 first = False
249 first = False
243
250
244 self.refmap = map_string
251 self.refmap = map_string
245
252
246 def get_sorted_referenced_posts(self):
253 def get_sorted_referenced_posts(self):
247 return self.refmap
254 return self.refmap
248
255
249 def is_referenced(self) -> bool:
256 def is_referenced(self) -> bool:
250 if not self.refmap:
257 if not self.refmap:
251 return False
258 return False
252 else:
259 else:
253 return len(self.refmap) > 0
260 return len(self.refmap) > 0
254
261
255 def is_opening(self) -> bool:
262 def is_opening(self) -> bool:
256 """
263 """
257 Checks if this is an opening post or just a reply.
264 Checks if this is an opening post or just a reply.
258 """
265 """
259
266
260 return self.get_thread().get_opening_post_id() == self.id
267 return self.get_thread().get_opening_post_id() == self.id
261
268
262 @transaction.atomic
269 @transaction.atomic
263 def add_tag(self, tag):
270 def add_tag(self, tag):
264 edit_time = timezone.now()
271 edit_time = timezone.now()
265
272
266 thread = self.get_thread()
273 thread = self.get_thread()
267 thread.add_tag(tag)
274 thread.add_tag(tag)
268 self.last_edit_time = edit_time
275 self.last_edit_time = edit_time
269 self.save(update_fields=['last_edit_time'])
276 self.save(update_fields=['last_edit_time'])
270
277
271 thread.last_edit_time = edit_time
278 thread.last_edit_time = edit_time
272 thread.save(update_fields=['last_edit_time'])
279 thread.save(update_fields=['last_edit_time'])
273
280
274 def get_url(self, thread=None):
281 def get_url(self, thread=None):
275 """
282 """
276 Gets full url to the post.
283 Gets full url to the post.
277 """
284 """
278
285
279 cache_key = CACHE_KEY_POST_URL + str(self.id)
286 cache_key = CACHE_KEY_POST_URL + str(self.id)
280 link = cache.get(cache_key)
287 link = cache.get(cache_key)
281
288
282 if not link:
289 if not link:
283 if not thread:
290 if not thread:
284 thread = self.get_thread()
291 thread = self.get_thread()
285
292
286 opening_id = thread.get_opening_post_id()
293 opening_id = thread.get_opening_post_id()
287
294
288 if self.id != opening_id:
295 if self.id != opening_id:
289 link = reverse('thread', kwargs={
296 link = reverse('thread', kwargs={
290 'post_id': opening_id}) + '#' + str(self.id)
297 'post_id': opening_id}) + '#' + str(self.id)
291 else:
298 else:
292 link = reverse('thread', kwargs={'post_id': self.id})
299 link = reverse('thread', kwargs={'post_id': self.id})
293
300
294 cache.set(cache_key, link)
301 cache.set(cache_key, link)
295
302
296 return link
303 return link
297
304
298 def get_thread(self) -> Thread:
305 def get_thread(self) -> Thread:
299 """
306 """
300 Gets post's thread.
307 Gets post's thread.
301 """
308 """
302
309
303 return self.thread_new
310 return self.thread_new
304
311
305 def get_referenced_posts(self):
312 def get_referenced_posts(self):
306 return self.referenced_posts.only('id', 'thread_new')
313 return self.referenced_posts.only('id', 'thread_new')
307
314
308 def get_view(self, moderator=False, need_open_link=False,
315 def get_view(self, moderator=False, need_open_link=False,
309 truncated=False, *args, **kwargs):
316 truncated=False, *args, **kwargs):
310 if 'is_opening' in kwargs:
317 """
311 is_opening = kwargs['is_opening']
318 Renders post's HTML view. Some of the post params can be passed over
312 else:
319 kwargs for the means of caching (if we view the thread, some params
313 is_opening = self.is_opening()
320 are same for every post and don't need to be computed over and over.
321 """
314
322
315 if 'thread' in kwargs:
323 is_opening = kwargs.get(PARAMETER_IS_OPENING, self.is_opening())
316 thread = kwargs['thread']
324 thread = kwargs.get(PARAMETER_THREAD, self.get_thread())
317 else:
325 can_bump = kwargs.get(PARAMETER_BUMPABLE, thread.can_bump())
318 thread = self.get_thread()
319
320 if 'can_bump' in kwargs:
321 can_bump = kwargs['can_bump']
322 else:
323 can_bump = thread.can_bump()
324
326
325 if is_opening:
327 if is_opening:
326 opening_post_id = self.id
328 opening_post_id = self.id
327 else:
329 else:
328 opening_post_id = thread.get_opening_post_id()
330 opening_post_id = thread.get_opening_post_id()
329
331
330 return render_to_string('boards/post.html', {
332 return render_to_string('boards/post.html', {
331 'post': self,
333 PARAMETER_POST: self,
332 'moderator': moderator,
334 PARAMETER_MODERATOR: moderator,
333 'is_opening': is_opening,
335 PARAMETER_IS_OPENING: is_opening,
334 'thread': thread,
336 PARAMETER_THREAD: thread,
335 'bumpable': can_bump,
337 PARAMETER_BUMPABLE: can_bump,
336 'need_open_link': need_open_link,
338 PARAMETER_NEED_OPEN_LINK: need_open_link,
337 'truncated': truncated,
339 PARAMETER_TRUNCATED: truncated,
338 'opening_post_id': opening_post_id,
340 PARAMETER_OP_ID: opening_post_id,
339 })
341 })
340
342
341 def get_first_image(self) -> PostImage:
343 def get_first_image(self) -> PostImage:
342 return self.images.earliest('id')
344 return self.images.earliest('id')
343
345
344 def delete(self, using=None):
346 def delete(self, using=None):
345 """
347 """
346 Deletes all post images and the post itself. If the post is opening,
348 Deletes all post images and the post itself. If the post is opening,
347 thread with all posts is deleted.
349 thread with all posts is deleted.
348 """
350 """
349
351
350 self.images.all().delete()
352 self.images.all().delete()
351
353
352 if self.is_opening():
354 if self.is_opening():
353 self.get_thread().delete()
355 self.get_thread().delete()
354 else:
356 else:
355 thread = self.get_thread()
357 thread = self.get_thread()
356 thread.last_edit_time = timezone.now()
358 thread.last_edit_time = timezone.now()
357 thread.save()
359 thread.save()
358
360
359 super(Post, self).delete(using)
361 super(Post, self).delete(using)
360
362
361 logging.getLogger('boards.post.delete').info(
363 logging.getLogger('boards.post.delete').info(
362 'Deleted post {}'.format(self))
364 'Deleted post {}'.format(self))
363
365
364 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
366 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
365 include_last_update=False):
367 include_last_update=False):
366 """
368 """
367 Gets post HTML or JSON data that can be rendered on a page or used by
369 Gets post HTML or JSON data that can be rendered on a page or used by
368 API.
370 API.
369 """
371 """
370
372
371 if format_type == DIFF_TYPE_HTML:
373 if format_type == DIFF_TYPE_HTML:
372 context = RequestContext(request)
374 params = dict()
373 context['post'] = self
375 params['post'] = self
374 if PARAMETER_TRUNCATED in request.GET:
376 if PARAMETER_TRUNCATED in request.GET:
375 context[PARAMETER_TRUNCATED] = True
377 params[PARAMETER_TRUNCATED] = True
376
378
377 # TODO Use dict here
379 return render_to_string('boards/api_post.html', params)
378 return render_to_string('boards/api_post.html',
379 context_instance=context)
380 elif format_type == DIFF_TYPE_JSON:
380 elif format_type == DIFF_TYPE_JSON:
381 post_json = {
381 post_json = {
382 'id': self.id,
382 'id': self.id,
383 'title': self.title,
383 'title': self.title,
384 'text': self._text_rendered,
384 'text': self._text_rendered,
385 }
385 }
386 if self.images.exists():
386 if self.images.exists():
387 post_image = self.get_first_image()
387 post_image = self.get_first_image()
388 post_json['image'] = post_image.image.url
388 post_json['image'] = post_image.image.url
389 post_json['image_preview'] = post_image.image.url_200x150
389 post_json['image_preview'] = post_image.image.url_200x150
390 if include_last_update:
390 if include_last_update:
391 post_json['bump_time'] = datetime_to_epoch(
391 post_json['bump_time'] = datetime_to_epoch(
392 self.thread_new.bump_time)
392 self.thread_new.bump_time)
393 return post_json
393 return post_json
394
394
395 def send_to_websocket(self, request, recursive=True):
395 def send_to_websocket(self, request, recursive=True):
396 """
396 """
397 Sends post HTML data to the thread web socket.
397 Sends post HTML data to the thread web socket.
398 """
398 """
399
399
400 if not settings.WEBSOCKETS_ENABLED:
400 if not settings.WEBSOCKETS_ENABLED:
401 return
401 return
402
402
403 client = Client()
403 client = Client()
404
404
405 thread = self.get_thread()
405 thread = self.get_thread()
406 thread_id = thread.id
406 thread_id = thread.id
407 channel_name = WS_CHANNEL_THREAD + str(thread.get_opening_post_id())
407 channel_name = WS_CHANNEL_THREAD + str(thread.get_opening_post_id())
408 client.publish(channel_name, {
408 client.publish(channel_name, {
409 WS_NOTIFICATION_TYPE: WS_NOTIFICATION_TYPE_NEW_POST,
409 WS_NOTIFICATION_TYPE: WS_NOTIFICATION_TYPE_NEW_POST,
410 })
410 })
411 client.send()
411 client.send()
412
412
413 logger = logging.getLogger('boards.post.websocket')
413 logger = logging.getLogger('boards.post.websocket')
414
414
415 logger.info('Sent notification from post #{} to channel {}'.format(
415 logger.info('Sent notification from post #{} to channel {}'.format(
416 self.id, channel_name))
416 self.id, channel_name))
417
417
418 if recursive:
418 if recursive:
419 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
419 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
420 post_id = reply_number.group(1)
420 post_id = reply_number.group(1)
421 ref_post = Post.objects.filter(id=post_id)[0]
421 ref_post = Post.objects.filter(id=post_id)[0]
422
422
423 # If post is in this thread, its thread was already notified.
423 # If post is in this thread, its thread was already notified.
424 # Otherwise, notify its thread separately.
424 # Otherwise, notify its thread separately.
425 if ref_post.thread_new_id != thread_id:
425 if ref_post.thread_new_id != thread_id:
426 ref_post.send_to_websocket(request, recursive=False)
426 ref_post.send_to_websocket(request, recursive=False)
427
427
428 def save(self, force_insert=False, force_update=False, using=None,
428 def save(self, force_insert=False, force_update=False, using=None,
429 update_fields=None):
429 update_fields=None):
430 self._text_rendered = bbcode_extended(self.get_raw_text())
430 self._text_rendered = bbcode_extended(self.get_raw_text())
431
431
432 super().save(force_insert, force_update, using, update_fields)
432 super().save(force_insert, force_update, using, update_fields)
433
433
434 def get_text(self) -> str:
434 def get_text(self) -> str:
435 return self._text_rendered
435 return self._text_rendered
436
436
437 def get_raw_text(self) -> str:
437 def get_raw_text(self) -> str:
438 return self.text
438 return self.text
@@ -1,96 +1,96
1 {% extends "boards/base.html" %}
1 {% extends "boards/base.html" %}
2
2
3 {% load i18n %}
3 {% load i18n %}
4 {% load cache %}
4 {% load cache %}
5 {% load static from staticfiles %}
5 {% load static from staticfiles %}
6 {% load board %}
6 {% load board %}
7
7
8 {% block head %}
8 {% block head %}
9 <title>{{ opening_post.get_title|striptags|truncatewords:10 }}
9 <title>{{ opening_post.get_title|striptags|truncatewords:10 }}
10 - {{ site_name }}</title>
10 - {{ site_name }}</title>
11 {% endblock %}
11 {% endblock %}
12
12
13 {% block content %}
13 {% block content %}
14 {% get_current_language as LANGUAGE_CODE %}
14 {% get_current_language as LANGUAGE_CODE %}
15
15
16 {% cache 600 thread_view thread.id thread.last_edit_time moderator LANGUAGE_CODE %}
16 {% cache 600 thread_view thread.id thread.last_edit_time moderator LANGUAGE_CODE %}
17
17
18 <div class="image-mode-tab">
18 <div class="image-mode-tab">
19 <a class="current_mode" href="{% url 'thread' opening_post.id %}">{% trans 'Normal mode' %}</a>,
19 <a class="current_mode" href="{% url 'thread' opening_post.id %}">{% trans 'Normal mode' %}</a>,
20 <a href="{% url 'thread_mode' opening_post.id 'gallery' %}">{% trans 'Gallery mode' %}</a>
20 <a href="{% url 'thread_mode' opening_post.id 'gallery' %}">{% trans 'Gallery mode' %}</a>
21 </div>
21 </div>
22
22
23 {% if bumpable %}
23 {% if bumpable %}
24 <div class="bar-bg">
24 <div class="bar-bg">
25 <div class="bar-value" style="width:{{ bumplimit_progress }}%" id="bumplimit_progress">
25 <div class="bar-value" style="width:{{ bumplimit_progress }}%" id="bumplimit_progress">
26 </div>
26 </div>
27 <div class="bar-text">
27 <div class="bar-text">
28 <span id="left_to_limit">{{ posts_left }}</span> {% trans 'posts to bumplimit' %}
28 <span id="left_to_limit">{{ posts_left }}</span> {% trans 'posts to bumplimit' %}
29 </div>
29 </div>
30 </div>
30 </div>
31 {% endif %}
31 {% endif %}
32
32
33 <div class="thread">
33 <div class="thread">
34 {% with can_bump=thread.can_bump %}
34 {% with can_bump=thread.can_bump %}
35 {% for post in thread.get_replies %}
35 {% for post in thread.get_replies %}
36 {% with is_opening=forloop.first %}
36 {% with is_opening=forloop.first %}
37 {% post_view post moderator=moderator is_opening=is_opening thread=thread can_bump=can_bump opening_post_id=opening_post.id %}
37 {% post_view post moderator=moderator is_opening=is_opening thread=thread bumpable=can_bump opening_post_id=opening_post.id %}
38 {% endwith %}
38 {% endwith %}
39 {% endfor %}
39 {% endfor %}
40 {% endwith %}
40 {% endwith %}
41 </div>
41 </div>
42
42
43 {% if not thread.archived %}
43 {% if not thread.archived %}
44 <div class="post-form-w" id="form">
44 <div class="post-form-w" id="form">
45 <script src="{% static 'js/panel.js' %}"></script>
45 <script src="{% static 'js/panel.js' %}"></script>
46 <div class="form-title">{% trans "Reply to thread" %} #{{ opening_post.id }}</div>
46 <div class="form-title">{% trans "Reply to thread" %} #{{ opening_post.id }}</div>
47 <div class="post-form" id="compact-form">
47 <div class="post-form" id="compact-form">
48 <div class="swappable-form-full">
48 <div class="swappable-form-full">
49 <form enctype="multipart/form-data" method="post"
49 <form enctype="multipart/form-data" method="post"
50 >{% csrf_token %}
50 >{% csrf_token %}
51 <div class="compact-form-text"></div>
51 <div class="compact-form-text"></div>
52 {{ form.as_div }}
52 {{ form.as_div }}
53 <div class="form-submit">
53 <div class="form-submit">
54 <input type="submit" value="{% trans "Post" %}"/>
54 <input type="submit" value="{% trans "Post" %}"/>
55 </div>
55 </div>
56 </form>
56 </form>
57 </div>
57 </div>
58 <a onclick="swapForm(); return false;" href="#">
58 <a onclick="swapForm(); return false;" href="#">
59 {% trans 'Switch mode' %}
59 {% trans 'Switch mode' %}
60 </a>
60 </a>
61 <div><a href="{% url "staticpage" name="help" %}">
61 <div><a href="{% url "staticpage" name="help" %}">
62 {% trans 'Text syntax' %}</a></div>
62 {% trans 'Text syntax' %}</a></div>
63 </div>
63 </div>
64 </div>
64 </div>
65
65
66 <script src="{% static 'js/jquery.form.min.js' %}"></script>
66 <script src="{% static 'js/jquery.form.min.js' %}"></script>
67 <script src="{% static 'js/thread_update.js' %}"></script>
67 <script src="{% static 'js/thread_update.js' %}"></script>
68 <script src="{% static 'js/3party/centrifuge.js' %}"></script>
68 <script src="{% static 'js/3party/centrifuge.js' %}"></script>
69 {% endif %}
69 {% endif %}
70
70
71 <script src="{% static 'js/form.js' %}"></script>
71 <script src="{% static 'js/form.js' %}"></script>
72 <script src="{% static 'js/thread.js' %}"></script>
72 <script src="{% static 'js/thread.js' %}"></script>
73
73
74 {% endcache %}
74 {% endcache %}
75 {% endblock %}
75 {% endblock %}
76
76
77 {% block metapanel %}
77 {% block metapanel %}
78
78
79 {% get_current_language as LANGUAGE_CODE %}
79 {% get_current_language as LANGUAGE_CODE %}
80
80
81 <span class="metapanel"
81 <span class="metapanel"
82 data-last-update="{{ last_update }}"
82 data-last-update="{{ last_update }}"
83 data-ws-token="{{ ws_token }}"
83 data-ws-token="{{ ws_token }}"
84 data-ws-project="{{ ws_project }}"
84 data-ws-project="{{ ws_project }}"
85 data-ws-host="{{ ws_host }}"
85 data-ws-host="{{ ws_host }}"
86 data-ws-port="{{ ws_port }}">
86 data-ws-port="{{ ws_port }}">
87 {% cache 600 thread_meta thread.last_edit_time moderator LANGUAGE_CODE %}
87 {% cache 600 thread_meta thread.last_edit_time moderator LANGUAGE_CODE %}
88 <span id="autoupdate">[-]</span>
88 <span id="autoupdate">[-]</span>
89 <span id="reply-count">{{ thread.get_reply_count }}</span>/{{ max_replies }} {% trans 'messages' %},
89 <span id="reply-count">{{ thread.get_reply_count }}</span>/{{ max_replies }} {% trans 'messages' %},
90 <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}.
90 <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}.
91 {% trans 'Last update: ' %}<span id="last-update">{{ thread.last_edit_time }}</span>
91 {% trans 'Last update: ' %}<span id="last-update">{{ thread.last_edit_time }}</span>
92 [<a href="rss/">RSS</a>]
92 [<a href="rss/">RSS</a>]
93 {% endcache %}
93 {% endcache %}
94 </span>
94 </span>
95
95
96 {% endblock %}
96 {% endblock %}
@@ -1,143 +1,139
1 from django.db import transaction
1 from django.db import transaction
2 from django.shortcuts import render, redirect
2 from django.shortcuts import render, redirect
3
3
4 from boards import utils, settings
4 from boards import utils, 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.forms import ThreadForm, PlainErrorList
7 from boards.forms import ThreadForm, PlainErrorList
8 from boards.models import Post, Thread, Ban, Tag
8 from boards.models import Post, Thread, Ban, Tag
9 from boards.views.banned import BannedView
9 from boards.views.banned import BannedView
10 from boards.views.base import BaseBoardView, CONTEXT_FORM
10 from boards.views.base import BaseBoardView, CONTEXT_FORM
11 from boards.views.posting_mixin import PostMixin
11 from boards.views.posting_mixin import PostMixin
12
12
13
13
14 FORM_TAGS = 'tags'
14 FORM_TAGS = 'tags'
15 FORM_TEXT = 'text'
15 FORM_TEXT = 'text'
16 FORM_TITLE = 'title'
16 FORM_TITLE = 'title'
17 FORM_IMAGE = 'image'
17 FORM_IMAGE = 'image'
18
18
19 TAG_DELIMITER = ' '
19 TAG_DELIMITER = ' '
20
20
21 PARAMETER_CURRENT_PAGE = 'current_page'
21 PARAMETER_CURRENT_PAGE = 'current_page'
22 PARAMETER_PAGINATOR = 'paginator'
22 PARAMETER_PAGINATOR = 'paginator'
23 PARAMETER_THREADS = 'threads'
23 PARAMETER_THREADS = 'threads'
24
24
25 TEMPLATE = 'boards/posting_general.html'
25 TEMPLATE = 'boards/posting_general.html'
26 DEFAULT_PAGE = 1
26 DEFAULT_PAGE = 1
27
27
28
28
29 class AllThreadsView(PostMixin, BaseBoardView):
29 class AllThreadsView(PostMixin, BaseBoardView):
30
30
31 def __init__(self):
31 def __init__(self):
32 self.settings_manager = None
32 self.settings_manager = None
33 super(AllThreadsView, self).__init__()
33 super(AllThreadsView, self).__init__()
34
34
35 def get(self, request, page=DEFAULT_PAGE, form=None):
35 def get(self, request, page=DEFAULT_PAGE, form=None):
36 context = self.get_context_data(request=request)
36 context = self.get_context_data(request=request)
37
37
38 if not form:
38 if not form:
39 form = ThreadForm(error_class=PlainErrorList)
39 form = ThreadForm(error_class=PlainErrorList)
40
40
41 self.settings_manager = get_settings_manager(request)
41 self.settings_manager = get_settings_manager(request)
42 paginator = get_paginator(self.get_threads(),
42 paginator = get_paginator(self.get_threads(),
43 settings.THREADS_PER_PAGE)
43 settings.THREADS_PER_PAGE)
44 paginator.current_page = int(page)
44 paginator.current_page = int(page)
45
45
46 threads = paginator.page(page).object_list
46 threads = paginator.page(page).object_list
47
47
48 context[PARAMETER_THREADS] = threads
48 context[PARAMETER_THREADS] = threads
49 context[CONTEXT_FORM] = form
49 context[CONTEXT_FORM] = form
50
50
51 self._get_page_context(paginator, context, page)
51 self._get_page_context(paginator, context, page)
52
52
53 # TODO Use dict here
53 # TODO Use dict here
54 return render(request, TEMPLATE, context_instance=context)
54 return render(request, TEMPLATE, context_instance=context)
55
55
56 def post(self, request, page=DEFAULT_PAGE):
56 def post(self, request, page=DEFAULT_PAGE):
57 form = ThreadForm(request.POST, request.FILES,
57 form = ThreadForm(request.POST, request.FILES,
58 error_class=PlainErrorList)
58 error_class=PlainErrorList)
59 form.session = request.session
59 form.session = request.session
60
60
61 if form.is_valid():
61 if form.is_valid():
62 return self.create_thread(request, form)
62 return self.create_thread(request, form)
63 if form.need_to_ban:
63 if form.need_to_ban:
64 # Ban user because he is suspected to be a bot
64 # Ban user because he is suspected to be a bot
65 self._ban_current_user(request)
65 self._ban_current_user(request)
66
66
67 return self.get(request, page, form)
67 return self.get(request, page, form)
68
68
69 @staticmethod
69 @staticmethod
70 def _get_page_context(paginator, context, page):
70 def _get_page_context(paginator, context, page):
71 """
71 """
72 Get pagination context variables
72 Get pagination context variables
73 """
73 """
74
74
75 context[PARAMETER_PAGINATOR] = paginator
75 context[PARAMETER_PAGINATOR] = paginator
76 context[PARAMETER_CURRENT_PAGE] = paginator.page(int(page))
76 context[PARAMETER_CURRENT_PAGE] = paginator.page(int(page))
77
77
78 @staticmethod
78 @staticmethod
79 def parse_tags_string(tag_strings):
79 def parse_tags_string(tag_strings):
80 """
80 """
81 Parses tag list string and returns tag object list.
81 Parses tag list string and returns tag object list.
82 """
82 """
83
83
84 tags = []
84 tags = []
85
85
86 if tag_strings:
86 if tag_strings:
87 tag_strings = tag_strings.split(TAG_DELIMITER)
87 tag_strings = tag_strings.split(TAG_DELIMITER)
88 for tag_name in tag_strings:
88 for tag_name in tag_strings:
89 tag_name = tag_name.strip().lower()
89 tag_name = tag_name.strip().lower()
90 if len(tag_name) > 0:
90 if len(tag_name) > 0:
91 tag, created = Tag.objects.get_or_create(name=tag_name)
91 tag, created = Tag.objects.get_or_create(name=tag_name)
92 tags.append(tag)
92 tags.append(tag)
93
93
94 return tags
94 return tags
95
95
96 @transaction.atomic
96 @transaction.atomic
97 def create_thread(self, request, form, html_response=True):
97 def create_thread(self, request, form, html_response=True):
98 """
98 """
99 Creates a new thread with an opening post.
99 Creates a new thread with an opening post.
100 """
100 """
101
101
102 ip = utils.get_client_ip(request)
102 ip = utils.get_client_ip(request)
103 is_banned = Ban.objects.filter(ip=ip).exists()
103 is_banned = Ban.objects.filter(ip=ip).exists()
104
104
105 if is_banned:
105 if is_banned:
106 if html_response:
106 if html_response:
107 return redirect(BannedView().as_view())
107 return redirect(BannedView().as_view())
108 else:
108 else:
109 return
109 return
110
110
111 data = form.cleaned_data
111 data = form.cleaned_data
112
112
113 title = data[FORM_TITLE]
113 title = data[FORM_TITLE]
114 text = data[FORM_TEXT]
114 text = data[FORM_TEXT]
115 image = data.get(FORM_IMAGE)
115
116
116 text = self._remove_invalid_links(text)
117 text = self._remove_invalid_links(text)
117
118
118 if FORM_IMAGE in list(data.keys()):
119 image = data[FORM_IMAGE]
120 else:
121 image = None
122
123 tag_strings = data[FORM_TAGS]
119 tag_strings = data[FORM_TAGS]
124
120
125 tags = self.parse_tags_string(tag_strings)
121 tags = self.parse_tags_string(tag_strings)
126
122
127 post = Post.objects.create_post(title=title, text=text, image=image,
123 post = Post.objects.create_post(title=title, text=text, image=image,
128 ip=ip, tags=tags)
124 ip=ip, tags=tags)
129
125
130 # This is required to update the threads to which posts we have replied
126 # This is required to update the threads to which posts we have replied
131 # when creating this one
127 # when creating this one
132 post.send_to_websocket(request)
128 post.send_to_websocket(request)
133
129
134 if html_response:
130 if html_response:
135 return redirect(post.get_url())
131 return redirect(post.get_url())
136
132
137 def get_threads(self):
133 def get_threads(self):
138 """
134 """
139 Gets list of threads that will be shown on a page.
135 Gets list of threads that will be shown on a page.
140 """
136 """
141
137
142 return Thread.objects.all().order_by('-bump_time')\
138 return Thread.objects.all().order_by('-bump_time')\
143 .exclude(tags__in=self.settings_manager.get_hidden_tags())
139 .exclude(tags__in=self.settings_manager.get_hidden_tags())
@@ -1,225 +1,223
1 from datetime import datetime
1 from datetime import datetime
2 import json
2 import json
3 import logging
3 import logging
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, render
7 from django.template import RequestContext
7 from django.template import RequestContext
8 from django.utils import timezone
8 from django.utils import timezone
9 from django.core import serializers
9 from django.core import serializers
10
10
11 from boards.forms import PostForm, PlainErrorList
11 from boards.forms import PostForm, PlainErrorList
12 from boards.models import Post, Thread, Tag
12 from boards.models import Post, Thread, Tag
13 from boards.utils import datetime_to_epoch
13 from boards.utils import datetime_to_epoch
14 from boards.views.thread import ThreadView
14 from boards.views.thread import ThreadView
15
15
16 __author__ = 'neko259'
16 __author__ = 'neko259'
17
17
18 PARAMETER_TRUNCATED = 'truncated'
18 PARAMETER_TRUNCATED = 'truncated'
19 PARAMETER_TAG = 'tag'
19 PARAMETER_TAG = 'tag'
20 PARAMETER_OFFSET = 'offset'
20 PARAMETER_OFFSET = 'offset'
21 PARAMETER_DIFF_TYPE = 'type'
21 PARAMETER_DIFF_TYPE = 'type'
22
22
23 DIFF_TYPE_HTML = 'html'
23 DIFF_TYPE_HTML = 'html'
24 DIFF_TYPE_JSON = 'json'
24 DIFF_TYPE_JSON = 'json'
25
25
26 STATUS_OK = 'ok'
26 STATUS_OK = 'ok'
27 STATUS_ERROR = 'error'
27 STATUS_ERROR = 'error'
28
28
29 logger = logging.getLogger(__name__)
29 logger = logging.getLogger(__name__)
30
30
31
31
32 @transaction.atomic
32 @transaction.atomic
33 def api_get_threaddiff(request, thread_id, last_update_time):
33 def api_get_threaddiff(request, thread_id, last_update_time):
34 """
34 """
35 Gets posts that were changed or added since time
35 Gets posts that were changed or added since time
36 """
36 """
37
37
38 thread = get_object_or_404(Post, id=thread_id).get_thread()
38 thread = get_object_or_404(Post, id=thread_id).get_thread()
39
39
40 # Add 1 to ensure we don't load the same post over and over
40 # Add 1 to ensure we don't load the same post over and over
41 last_update_timestamp = float(last_update_time) + 1
41 last_update_timestamp = float(last_update_time) + 1
42
42
43 filter_time = datetime.fromtimestamp(last_update_timestamp / 1000000,
43 filter_time = datetime.fromtimestamp(last_update_timestamp / 1000000,
44 timezone.get_current_timezone())
44 timezone.get_current_timezone())
45
45
46 json_data = {
46 json_data = {
47 'added': [],
47 'added': [],
48 'updated': [],
48 'updated': [],
49 'last_update': None,
49 'last_update': None,
50 }
50 }
51 added_posts = Post.objects.filter(thread_new=thread,
51 added_posts = Post.objects.filter(thread_new=thread,
52 pub_time__gt=filter_time) \
52 pub_time__gt=filter_time) \
53 .order_by('pub_time')
53 .order_by('pub_time')
54 updated_posts = Post.objects.filter(thread_new=thread,
54 updated_posts = Post.objects.filter(thread_new=thread,
55 pub_time__lte=filter_time,
55 pub_time__lte=filter_time,
56 last_edit_time__gt=filter_time)
56 last_edit_time__gt=filter_time)
57
57
58 diff_type = DIFF_TYPE_HTML
58 diff_type = request.GET.get(PARAMETER_DIFF_TYPE, DIFF_TYPE_HTML)
59 if PARAMETER_DIFF_TYPE in request.GET:
60 diff_type = request.GET[PARAMETER_DIFF_TYPE]
61
59
62 for post in added_posts:
60 for post in added_posts:
63 json_data['added'].append(get_post_data(post.id, diff_type, request))
61 json_data['added'].append(get_post_data(post.id, diff_type, request))
64 for post in updated_posts:
62 for post in updated_posts:
65 json_data['updated'].append(get_post_data(post.id, diff_type, request))
63 json_data['updated'].append(get_post_data(post.id, diff_type, request))
66 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
64 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
67
65
68 return HttpResponse(content=json.dumps(json_data))
66 return HttpResponse(content=json.dumps(json_data))
69
67
70
68
71 def api_add_post(request, opening_post_id):
69 def api_add_post(request, opening_post_id):
72 """
70 """
73 Adds a post and return the JSON response for it
71 Adds a post and return the JSON response for it
74 """
72 """
75
73
76 opening_post = get_object_or_404(Post, id=opening_post_id)
74 opening_post = get_object_or_404(Post, id=opening_post_id)
77
75
78 logger.info('Adding post via api...')
76 logger.info('Adding post via api...')
79
77
80 status = STATUS_OK
78 status = STATUS_OK
81 errors = []
79 errors = []
82
80
83 if request.method == 'POST':
81 if request.method == 'POST':
84 form = PostForm(request.POST, request.FILES, error_class=PlainErrorList)
82 form = PostForm(request.POST, request.FILES, error_class=PlainErrorList)
85 form.session = request.session
83 form.session = request.session
86
84
87 if form.need_to_ban:
85 if form.need_to_ban:
88 # Ban user because he is suspected to be a bot
86 # Ban user because he is suspected to be a bot
89 # _ban_current_user(request)
87 # _ban_current_user(request)
90 status = STATUS_ERROR
88 status = STATUS_ERROR
91 if form.is_valid():
89 if form.is_valid():
92 post = ThreadView().new_post(request, form, opening_post,
90 post = ThreadView().new_post(request, form, opening_post,
93 html_response=False)
91 html_response=False)
94 if not post:
92 if not post:
95 status = STATUS_ERROR
93 status = STATUS_ERROR
96 else:
94 else:
97 logger.info('Added post #%d via api.' % post.id)
95 logger.info('Added post #%d via api.' % post.id)
98 else:
96 else:
99 status = STATUS_ERROR
97 status = STATUS_ERROR
100 errors = form.as_json_errors()
98 errors = form.as_json_errors()
101
99
102 response = {
100 response = {
103 'status': status,
101 'status': status,
104 'errors': errors,
102 'errors': errors,
105 }
103 }
106
104
107 return HttpResponse(content=json.dumps(response))
105 return HttpResponse(content=json.dumps(response))
108
106
109
107
110 def get_post(request, post_id):
108 def get_post(request, post_id):
111 """
109 """
112 Gets the html of a post. Used for popups. Post can be truncated if used
110 Gets the html of a post. Used for popups. Post can be truncated if used
113 in threads list with 'truncated' get parameter.
111 in threads list with 'truncated' get parameter.
114 """
112 """
115
113
116 post = get_object_or_404(Post, id=post_id)
114 post = get_object_or_404(Post, id=post_id)
117
115
118 context = RequestContext(request)
116 context = RequestContext(request)
119 context['post'] = post
117 context['post'] = post
120 if PARAMETER_TRUNCATED in request.GET:
118 if PARAMETER_TRUNCATED in request.GET:
121 context[PARAMETER_TRUNCATED] = True
119 context[PARAMETER_TRUNCATED] = True
122
120
123 # TODO Use dict here
121 # TODO Use dict here
124 return render(request, 'boards/api_post.html', context_instance=context)
122 return render(request, 'boards/api_post.html', context_instance=context)
125
123
126
124
127 # TODO Test this
125 # TODO Test this
128 def api_get_threads(request, count):
126 def api_get_threads(request, count):
129 """
127 """
130 Gets the JSON thread opening posts list.
128 Gets the JSON thread opening posts list.
131 Parameters that can be used for filtering:
129 Parameters that can be used for filtering:
132 tag, offset (from which thread to get results)
130 tag, offset (from which thread to get results)
133 """
131 """
134
132
135 if PARAMETER_TAG in request.GET:
133 if PARAMETER_TAG in request.GET:
136 tag_name = request.GET[PARAMETER_TAG]
134 tag_name = request.GET[PARAMETER_TAG]
137 if tag_name is not None:
135 if tag_name is not None:
138 tag = get_object_or_404(Tag, name=tag_name)
136 tag = get_object_or_404(Tag, name=tag_name)
139 threads = tag.get_threads().filter(archived=False)
137 threads = tag.get_threads().filter(archived=False)
140 else:
138 else:
141 threads = Thread.objects.filter(archived=False)
139 threads = Thread.objects.filter(archived=False)
142
140
143 if PARAMETER_OFFSET in request.GET:
141 if PARAMETER_OFFSET in request.GET:
144 offset = request.GET[PARAMETER_OFFSET]
142 offset = request.GET[PARAMETER_OFFSET]
145 offset = int(offset) if offset is not None else 0
143 offset = int(offset) if offset is not None else 0
146 else:
144 else:
147 offset = 0
145 offset = 0
148
146
149 threads = threads.order_by('-bump_time')
147 threads = threads.order_by('-bump_time')
150 threads = threads[offset:offset + int(count)]
148 threads = threads[offset:offset + int(count)]
151
149
152 opening_posts = []
150 opening_posts = []
153 for thread in threads:
151 for thread in threads:
154 opening_post = thread.get_opening_post()
152 opening_post = thread.get_opening_post()
155
153
156 # TODO Add tags, replies and images count
154 # TODO Add tags, replies and images count
157 opening_posts.append(get_post_data(opening_post.id,
155 opening_posts.append(get_post_data(opening_post.id,
158 include_last_update=True))
156 include_last_update=True))
159
157
160 return HttpResponse(content=json.dumps(opening_posts))
158 return HttpResponse(content=json.dumps(opening_posts))
161
159
162
160
163 # TODO Test this
161 # TODO Test this
164 def api_get_tags(request):
162 def api_get_tags(request):
165 """
163 """
166 Gets all tags or user tags.
164 Gets all tags or user tags.
167 """
165 """
168
166
169 # TODO Get favorite tags for the given user ID
167 # TODO Get favorite tags for the given user ID
170
168
171 tags = Tag.objects.get_not_empty_tags()
169 tags = Tag.objects.get_not_empty_tags()
172 tag_names = []
170 tag_names = []
173 for tag in tags:
171 for tag in tags:
174 tag_names.append(tag.name)
172 tag_names.append(tag.name)
175
173
176 return HttpResponse(content=json.dumps(tag_names))
174 return HttpResponse(content=json.dumps(tag_names))
177
175
178
176
179 # TODO The result can be cached by the thread last update time
177 # TODO The result can be cached by the thread last update time
180 # TODO Test this
178 # TODO Test this
181 def api_get_thread_posts(request, opening_post_id):
179 def api_get_thread_posts(request, opening_post_id):
182 """
180 """
183 Gets the JSON array of thread posts
181 Gets the JSON array of thread posts
184 """
182 """
185
183
186 opening_post = get_object_or_404(Post, id=opening_post_id)
184 opening_post = get_object_or_404(Post, id=opening_post_id)
187 thread = opening_post.get_thread()
185 thread = opening_post.get_thread()
188 posts = thread.get_replies()
186 posts = thread.get_replies()
189
187
190 json_data = {
188 json_data = {
191 'posts': [],
189 'posts': [],
192 'last_update': None,
190 'last_update': None,
193 }
191 }
194 json_post_list = []
192 json_post_list = []
195
193
196 for post in posts:
194 for post in posts:
197 json_post_list.append(get_post_data(post.id))
195 json_post_list.append(get_post_data(post.id))
198 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
196 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
199 json_data['posts'] = json_post_list
197 json_data['posts'] = json_post_list
200
198
201 return HttpResponse(content=json.dumps(json_data))
199 return HttpResponse(content=json.dumps(json_data))
202
200
203
201
204 def api_get_post(request, post_id):
202 def api_get_post(request, post_id):
205 """
203 """
206 Gets the JSON of a post. This can be
204 Gets the JSON of a post. This can be
207 used as and API for external clients.
205 used as and API for external clients.
208 """
206 """
209
207
210 post = get_object_or_404(Post, id=post_id)
208 post = get_object_or_404(Post, id=post_id)
211
209
212 json = serializers.serialize("json", [post], fields=(
210 json = serializers.serialize("json", [post], fields=(
213 "pub_time", "_text_rendered", "title", "text", "image",
211 "pub_time", "_text_rendered", "title", "text", "image",
214 "image_width", "image_height", "replies", "tags"
212 "image_width", "image_height", "replies", "tags"
215 ))
213 ))
216
214
217 return HttpResponse(content=json)
215 return HttpResponse(content=json)
218
216
219
217
220 # TODO Remove this method and use post method directly
218 # TODO Remove this method and use post method directly
221 def get_post_data(post_id, format_type=DIFF_TYPE_JSON, request=None,
219 def get_post_data(post_id, format_type=DIFF_TYPE_JSON, request=None,
222 include_last_update=False):
220 include_last_update=False):
223 post = get_object_or_404(Post, id=post_id)
221 post = get_object_or_404(Post, id=post_id)
224 return post.get_post_data(format_type=format_type, request=request,
222 return post.get_post_data(format_type=format_type, request=request,
225 include_last_update=include_last_update)
223 include_last_update=include_last_update)
@@ -1,41 +1,43
1 from django.shortcuts import render
1 from django.shortcuts import render
2 from django.template import RequestContext
3 from django.views.generic import View
2 from django.views.generic import View
4 from haystack.query import SearchQuerySet
3 from haystack.query import SearchQuerySet
4
5 from boards.abstracts.paginator import get_paginator
5 from boards.abstracts.paginator import get_paginator
6 from boards.forms import SearchForm, PlainErrorList
6 from boards.forms import SearchForm, PlainErrorList
7
7
8
9 MIN_QUERY_LENGTH = 3
10 RESULTS_PER_PAGE = 10
11
8 FORM_QUERY = 'query'
12 FORM_QUERY = 'query'
9
13
10 CONTEXT_QUERY = 'query'
14 CONTEXT_QUERY = 'query'
11 CONTEXT_FORM = 'form'
15 CONTEXT_FORM = 'form'
12 CONTEXT_PAGE = 'page'
16 CONTEXT_PAGE = 'page'
13
17
14 REQUEST_PAGE = 'page'
18 REQUEST_PAGE = 'page'
15
19
16 __author__ = 'neko259'
20 __author__ = 'neko259'
17
21
18 TEMPLATE = 'search/search.html'
22 TEMPLATE = 'search/search.html'
19
23
20
24
21 class BoardSearchView(View):
25 class BoardSearchView(View):
22 def get(self, request):
26 def get(self, request):
23 context = RequestContext(request)
27 params = dict()
28
24 form = SearchForm(request.GET, error_class=PlainErrorList)
29 form = SearchForm(request.GET, error_class=PlainErrorList)
25 context[CONTEXT_FORM] = form
30 params[CONTEXT_FORM] = form
26
31
27 if form.is_valid():
32 if form.is_valid():
28 query = form.cleaned_data[FORM_QUERY]
33 query = form.cleaned_data[FORM_QUERY]
29 if len(query) >= 3:
34 if len(query) >= MIN_QUERY_LENGTH:
30 results = SearchQuerySet().auto_query(query).order_by('-id')
35 results = SearchQuerySet().auto_query(query).order_by('-id')
31 paginator = get_paginator(results, 10)
36 paginator = get_paginator(results, RESULTS_PER_PAGE)
32
37
33 if REQUEST_PAGE in request.GET:
38 page = int(request.GET.get(REQUEST_PAGE, '1'))
34 page = int(request.GET[REQUEST_PAGE])
35 else:
36 page = 1
37 context[CONTEXT_PAGE] = paginator.page(page)
38 context[CONTEXT_QUERY] = query
39
39
40 # TODO Use dict here
40 params[CONTEXT_PAGE] = paginator.page(page)
41 return render(request, TEMPLATE, context_instance=context)
41 params[CONTEXT_QUERY] = query
42
43 return render(request, TEMPLATE, params)
General Comments 0
You need to be logged in to leave comments. Login now