##// END OF EJS Templates
Fixed sending posts to websockets. Cleaned up new post view code
neko259 -
r916:2aafa436 default
parent child Browse files
Show More
@@ -1,438 +1,438 b''
1 1 from datetime import datetime, timedelta, date
2 2 from datetime import time as dtime
3 3 import logging
4 4 import re
5 5
6 6 from adjacent import Client
7 7 from django.core.cache import cache
8 8 from django.core.urlresolvers import reverse
9 9 from django.db import models, transaction
10 10 from django.db.models import TextField
11 11 from django.template import RequestContext
12 12 from django.template.loader import render_to_string
13 13 from django.utils import timezone
14 14
15 15 from boards import settings
16 16 from boards.mdx_neboard import bbcode_extended
17 17 from boards.models import PostImage
18 18 from boards.models.base import Viewable
19 19 from boards.models.thread import Thread
20 20 from boards.utils import datetime_to_epoch
21 21
22 22 WS_NOTIFICATION_TYPE_NEW_POST = 'new_post'
23 23 WS_NOTIFICATION_TYPE = 'notification_type'
24 24
25 25 WS_CHANNEL_THREAD = "thread:"
26 26
27 27 APP_LABEL_BOARDS = 'boards'
28 28
29 29 CACHE_KEY_PPD = 'ppd'
30 30 CACHE_KEY_POST_URL = 'post_url'
31 31
32 32 POSTS_PER_DAY_RANGE = 7
33 33
34 34 BAN_REASON_AUTO = 'Auto'
35 35
36 36 IMAGE_THUMB_SIZE = (200, 150)
37 37
38 38 TITLE_MAX_LENGTH = 200
39 39
40 40 # TODO This should be removed
41 41 NO_IP = '0.0.0.0'
42 42
43 43 # TODO Real user agent should be saved instead of this
44 44 UNKNOWN_UA = ''
45 45
46 46 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
47 47
48 48 PARAMETER_TRUNCATED = 'truncated'
49 49 PARAMETER_TAG = 'tag'
50 50 PARAMETER_OFFSET = 'offset'
51 51 PARAMETER_DIFF_TYPE = 'type'
52 52
53 53 DIFF_TYPE_HTML = 'html'
54 54 DIFF_TYPE_JSON = 'json'
55 55
56 56 PREPARSE_PATTERNS = {
57 57 r'>>(\d+)': r'[post]\1[/post]', # Reflink ">>123"
58 58 r'^>(.+)': r'[quote]\1[/quote]', # Quote ">text"
59 59 r'^//(.+)': r'[comment]\1[/comment]', # Comment "//text"
60 60 }
61 61
62 62
63 63 class PostManager(models.Manager):
64 64 @transaction.atomic
65 65 def create_post(self, title: str, text: str, image=None, thread=None,
66 66 ip=NO_IP, tags: list=None):
67 67 """
68 68 Creates new post
69 69 """
70 70
71 71 if not tags:
72 72 tags = []
73 73
74 74 posting_time = timezone.now()
75 75 if not thread:
76 76 thread = Thread.objects.create(bump_time=posting_time,
77 77 last_edit_time=posting_time)
78 78 new_thread = True
79 79 else:
80 80 new_thread = False
81 81
82 82 pre_text = self._preparse_text(text)
83 83
84 84 post = self.create(title=title,
85 85 text=pre_text,
86 86 pub_time=posting_time,
87 87 thread_new=thread,
88 88 poster_ip=ip,
89 89 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
90 90 # last!
91 91 last_edit_time=posting_time)
92 92
93 93 logger = logging.getLogger('boards.post.create')
94 94
95 95 logger.info('Created post {} by {}'.format(
96 96 post, post.poster_ip))
97 97
98 98 if image:
99 99 post_image = PostImage.objects.create(image=image)
100 100 post.images.add(post_image)
101 101 logger.info('Created image #{} for post #{}'.format(
102 102 post_image.id, post.id))
103 103
104 104 thread.replies.add(post)
105 105 list(map(thread.add_tag, tags))
106 106
107 107 if new_thread:
108 108 Thread.objects.process_oldest_threads()
109 109 else:
110 110 thread.bump()
111 111 thread.last_edit_time = posting_time
112 112 thread.save()
113 113
114 114 self.connect_replies(post)
115 115
116 116 return post
117 117
118 118 def delete_posts_by_ip(self, ip):
119 119 """
120 120 Deletes all posts of the author with same IP
121 121 """
122 122
123 123 posts = self.filter(poster_ip=ip)
124 124 for post in posts:
125 125 post.delete()
126 126
127 127 def connect_replies(self, post):
128 128 """
129 129 Connects replies to a post to show them as a reflink map
130 130 """
131 131
132 132 for reply_number in re.finditer(REGEX_REPLY, post.get_raw_text()):
133 133 post_id = reply_number.group(1)
134 134 ref_post = self.filter(id=post_id)
135 135 if ref_post.count() > 0:
136 136 referenced_post = ref_post[0]
137 137 referenced_post.referenced_posts.add(post)
138 138 referenced_post.last_edit_time = post.pub_time
139 139 referenced_post.build_refmap()
140 140 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
141 141
142 142 referenced_thread = referenced_post.get_thread()
143 143 referenced_thread.last_edit_time = post.pub_time
144 144 referenced_thread.save(update_fields=['last_edit_time'])
145 145
146 146 def get_posts_per_day(self):
147 147 """
148 148 Gets average count of posts per day for the last 7 days
149 149 """
150 150
151 151 day_end = date.today()
152 152 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
153 153
154 154 cache_key = CACHE_KEY_PPD + str(day_end)
155 155 ppd = cache.get(cache_key)
156 156 if ppd:
157 157 return ppd
158 158
159 159 day_time_start = timezone.make_aware(datetime.combine(
160 160 day_start, dtime()), timezone.get_current_timezone())
161 161 day_time_end = timezone.make_aware(datetime.combine(
162 162 day_end, dtime()), timezone.get_current_timezone())
163 163
164 164 posts_per_period = float(self.filter(
165 165 pub_time__lte=day_time_end,
166 166 pub_time__gte=day_time_start).count())
167 167
168 168 ppd = posts_per_period / POSTS_PER_DAY_RANGE
169 169
170 170 cache.set(cache_key, ppd)
171 171 return ppd
172 172
173 173 def _preparse_text(self, text):
174 174 """
175 175 Preparses text to change patterns like '>>' to a proper bbcode
176 176 tags.
177 177 """
178 178
179 179 for key, value in PREPARSE_PATTERNS.items():
180 180 text = re.sub(key, value, text, flags=re.MULTILINE)
181 181
182 182 return text
183 183
184 184
185 185 class Post(models.Model, Viewable):
186 186 """A post is a message."""
187 187
188 188 objects = PostManager()
189 189
190 190 class Meta:
191 191 app_label = APP_LABEL_BOARDS
192 192 ordering = ('id',)
193 193
194 194 title = models.CharField(max_length=TITLE_MAX_LENGTH)
195 195 pub_time = models.DateTimeField()
196 196 text = TextField(blank=True, null=True)
197 197 _text_rendered = TextField(blank=True, null=True, editable=False)
198 198
199 199 images = models.ManyToManyField(PostImage, null=True, blank=True,
200 200 related_name='ip+', db_index=True)
201 201
202 202 poster_ip = models.GenericIPAddressField()
203 203 poster_user_agent = models.TextField()
204 204
205 205 thread_new = models.ForeignKey('Thread', null=True, default=None,
206 206 db_index=True)
207 207 last_edit_time = models.DateTimeField()
208 208
209 209 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
210 210 null=True,
211 211 blank=True, related_name='rfp+',
212 212 db_index=True)
213 213 refmap = models.TextField(null=True, blank=True)
214 214
215 215 def __str__(self):
216 216 return 'P#{}/{}'.format(self.id, self.title)
217 217
218 218 def get_title(self) -> str:
219 219 """
220 220 Gets original post title or part of its text.
221 221 """
222 222
223 223 title = self.title
224 224 if not title:
225 225 title = self.get_text()
226 226
227 227 return title
228 228
229 229 def build_refmap(self) -> None:
230 230 """
231 231 Builds a replies map string from replies list. This is a cache to stop
232 232 the server from recalculating the map on every post show.
233 233 """
234 234 map_string = ''
235 235
236 236 first = True
237 237 for refpost in self.referenced_posts.all():
238 238 if not first:
239 239 map_string += ', '
240 240 map_string += '<a href="%s">&gt;&gt;%s</a>' % (refpost.get_url(),
241 241 refpost.id)
242 242 first = False
243 243
244 244 self.refmap = map_string
245 245
246 246 def get_sorted_referenced_posts(self):
247 247 return self.refmap
248 248
249 249 def is_referenced(self) -> bool:
250 250 if not self.refmap:
251 251 return False
252 252 else:
253 253 return len(self.refmap) > 0
254 254
255 255 def is_opening(self) -> bool:
256 256 """
257 257 Checks if this is an opening post or just a reply.
258 258 """
259 259
260 260 return self.get_thread().get_opening_post_id() == self.id
261 261
262 262 @transaction.atomic
263 263 def add_tag(self, tag):
264 264 edit_time = timezone.now()
265 265
266 266 thread = self.get_thread()
267 267 thread.add_tag(tag)
268 268 self.last_edit_time = edit_time
269 269 self.save(update_fields=['last_edit_time'])
270 270
271 271 thread.last_edit_time = edit_time
272 272 thread.save(update_fields=['last_edit_time'])
273 273
274 274 def get_url(self, thread=None):
275 275 """
276 276 Gets full url to the post.
277 277 """
278 278
279 279 cache_key = CACHE_KEY_POST_URL + str(self.id)
280 280 link = cache.get(cache_key)
281 281
282 282 if not link:
283 283 if not thread:
284 284 thread = self.get_thread()
285 285
286 286 opening_id = thread.get_opening_post_id()
287 287
288 288 if self.id != opening_id:
289 289 link = reverse('thread', kwargs={
290 290 'post_id': opening_id}) + '#' + str(self.id)
291 291 else:
292 292 link = reverse('thread', kwargs={'post_id': self.id})
293 293
294 294 cache.set(cache_key, link)
295 295
296 296 return link
297 297
298 298 def get_thread(self) -> Thread:
299 299 """
300 300 Gets post's thread.
301 301 """
302 302
303 303 return self.thread_new
304 304
305 305 def get_referenced_posts(self):
306 306 return self.referenced_posts.only('id', 'thread_new')
307 307
308 308 def get_view(self, moderator=False, need_open_link=False,
309 309 truncated=False, *args, **kwargs):
310 310 if 'is_opening' in kwargs:
311 311 is_opening = kwargs['is_opening']
312 312 else:
313 313 is_opening = self.is_opening()
314 314
315 315 if 'thread' in kwargs:
316 316 thread = kwargs['thread']
317 317 else:
318 318 thread = self.get_thread()
319 319
320 320 if 'can_bump' in kwargs:
321 321 can_bump = kwargs['can_bump']
322 322 else:
323 323 can_bump = thread.can_bump()
324 324
325 325 if is_opening:
326 326 opening_post_id = self.id
327 327 else:
328 328 opening_post_id = thread.get_opening_post_id()
329 329
330 330 return render_to_string('boards/post.html', {
331 331 'post': self,
332 332 'moderator': moderator,
333 333 'is_opening': is_opening,
334 334 'thread': thread,
335 335 'bumpable': can_bump,
336 336 'need_open_link': need_open_link,
337 337 'truncated': truncated,
338 338 'opening_post_id': opening_post_id,
339 339 })
340 340
341 341 def get_first_image(self) -> PostImage:
342 342 return self.images.earliest('id')
343 343
344 344 def delete(self, using=None):
345 345 """
346 346 Deletes all post images and the post itself. If the post is opening,
347 347 thread with all posts is deleted.
348 348 """
349 349
350 350 self.images.all().delete()
351 351
352 352 if self.is_opening():
353 353 self.get_thread().delete()
354 354 else:
355 355 thread = self.get_thread()
356 356 thread.last_edit_time = timezone.now()
357 357 thread.save()
358 358
359 359 super(Post, self).delete(using)
360 360
361 361 logging.getLogger('boards.post.delete').info(
362 362 'Deleted post {}'.format(self))
363 363
364 364 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
365 365 include_last_update=False):
366 366 """
367 367 Gets post HTML or JSON data that can be rendered on a page or used by
368 368 API.
369 369 """
370 370
371 371 if format_type == DIFF_TYPE_HTML:
372 372 context = RequestContext(request)
373 373 context['post'] = self
374 374 if PARAMETER_TRUNCATED in request.GET:
375 375 context[PARAMETER_TRUNCATED] = True
376 376
377 377 # TODO Use dict here
378 378 return render_to_string('boards/api_post.html',
379 379 context_instance=context)
380 380 elif format_type == DIFF_TYPE_JSON:
381 381 post_json = {
382 382 'id': self.id,
383 383 'title': self.title,
384 384 'text': self._text_rendered,
385 385 }
386 386 if self.images.exists():
387 387 post_image = self.get_first_image()
388 388 post_json['image'] = post_image.image.url
389 389 post_json['image_preview'] = post_image.image.url_200x150
390 390 if include_last_update:
391 391 post_json['bump_time'] = datetime_to_epoch(
392 392 self.thread_new.bump_time)
393 393 return post_json
394 394
395 395 def send_to_websocket(self, request, recursive=True):
396 396 """
397 397 Sends post HTML data to the thread web socket.
398 398 """
399 399
400 400 if not settings.WEBSOCKETS_ENABLED:
401 401 return
402 402
403 403 client = Client()
404 404
405 405 thread = self.get_thread()
406 406 thread_id = thread.id
407 407 channel_name = WS_CHANNEL_THREAD + str(thread.get_opening_post_id())
408 408 client.publish(channel_name, {
409 409 WS_NOTIFICATION_TYPE: WS_NOTIFICATION_TYPE_NEW_POST,
410 410 })
411 411 client.send()
412 412
413 413 logger = logging.getLogger('boards.post.websocket')
414 414
415 415 logger.info('Sent notification from post #{} to channel {}'.format(
416 416 self.id, channel_name))
417 417
418 418 if recursive:
419 419 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
420 420 post_id = reply_number.group(1)
421 421 ref_post = Post.objects.filter(id=post_id)[0]
422 422
423 423 # If post is in this thread, its thread was already notified.
424 424 # Otherwise, notify its thread separately.
425 if ref_post.thread_id != thread_id:
425 if ref_post.thread_new_id != thread_id:
426 426 ref_post.send_to_websocket(request, recursive=False)
427 427
428 428 def save(self, force_insert=False, force_update=False, using=None,
429 429 update_fields=None):
430 430 self._text_rendered = bbcode_extended(self.get_raw_text())
431 431
432 432 super().save(force_insert, force_update, using, update_fields)
433 433
434 434 def get_text(self) -> str:
435 435 return self._text_rendered
436 436
437 437 def get_raw_text(self) -> str:
438 438 return self.text
@@ -1,149 +1,143 b''
1 1 from django.core.urlresolvers import reverse
2 2 from django.db import transaction
3 3 from django.http import Http404
4 4 from django.shortcuts import get_object_or_404, render, redirect
5 5 from django.views.generic.edit import FormMixin
6 6
7 7 from boards import utils, settings
8 8 from boards.forms import PostForm, PlainErrorList
9 9 from boards.models import Post, Ban
10 10 from boards.views.banned import BannedView
11 11 from boards.views.base import BaseBoardView, CONTEXT_FORM
12 12 from boards.views.posting_mixin import PostMixin
13 13 import neboard
14 14
15 15 TEMPLATE_GALLERY = 'boards/thread_gallery.html'
16 16 TEMPLATE_NORMAL = 'boards/thread.html'
17 17
18 18 CONTEXT_POSTS = 'posts'
19 19 CONTEXT_OP = 'opening_post'
20 20 CONTEXT_BUMPLIMIT_PRG = 'bumplimit_progress'
21 21 CONTEXT_POSTS_LEFT = 'posts_left'
22 22 CONTEXT_LASTUPDATE = "last_update"
23 23 CONTEXT_MAX_REPLIES = 'max_replies'
24 24 CONTEXT_THREAD = 'thread'
25 25 CONTEXT_BUMPABLE = 'bumpable'
26 26 CONTEXT_WS_TOKEN = 'ws_token'
27 27 CONTEXT_WS_PROJECT = 'ws_project'
28 28 CONTEXT_WS_HOST = 'ws_host'
29 29 CONTEXT_WS_PORT = 'ws_port'
30 30
31 31 FORM_TITLE = 'title'
32 32 FORM_TEXT = 'text'
33 33 FORM_IMAGE = 'image'
34 34
35 35 MODE_GALLERY = 'gallery'
36 36 MODE_NORMAL = 'normal'
37 37
38 38
39 39 class ThreadView(BaseBoardView, PostMixin, FormMixin):
40 40
41 41 def get(self, request, post_id, mode=MODE_NORMAL, form=None):
42 42 try:
43 43 opening_post = Post.objects.filter(id=post_id).only('thread_new')[0]
44 44 except IndexError:
45 45 raise Http404
46 46
47 47 # If this is not OP, don't show it as it is
48 48 if not opening_post or not opening_post.is_opening():
49 49 raise Http404
50 50
51 51 if not form:
52 52 form = PostForm(error_class=PlainErrorList)
53 53
54 54 thread_to_show = opening_post.get_thread()
55 55
56 56 context = self.get_context_data(request=request)
57 57
58 58 context[CONTEXT_FORM] = form
59 59 context[CONTEXT_LASTUPDATE] = str(utils.datetime_to_epoch(
60 60 thread_to_show.last_edit_time))
61 61 context[CONTEXT_THREAD] = thread_to_show
62 62 context[CONTEXT_MAX_REPLIES] = settings.MAX_POSTS_PER_THREAD
63 63
64 64 if settings.WEBSOCKETS_ENABLED:
65 65 context[CONTEXT_WS_TOKEN] = utils.get_websocket_token(
66 66 timestamp=context[CONTEXT_LASTUPDATE])
67 67 context[CONTEXT_WS_PROJECT] = neboard.settings.CENTRIFUGE_PROJECT_ID
68 68 context[CONTEXT_WS_HOST] = request.get_host().split(':')[0]
69 69 context[CONTEXT_WS_PORT] = neboard.settings.CENTRIFUGE_PORT
70 70
71 71 # TODO Move this to subclasses: NormalThreadView, GalleryThreadView etc
72 72 if MODE_NORMAL == mode:
73 73 bumpable = thread_to_show.can_bump()
74 74 context[CONTEXT_BUMPABLE] = bumpable
75 75 if bumpable:
76 76 left_posts = settings.MAX_POSTS_PER_THREAD \
77 77 - thread_to_show.get_reply_count()
78 78 context[CONTEXT_POSTS_LEFT] = left_posts
79 79 context[CONTEXT_BUMPLIMIT_PRG] = str(
80 80 float(left_posts) / settings.MAX_POSTS_PER_THREAD * 100)
81 81
82 82 context[CONTEXT_OP] = opening_post
83 83
84 84 document = TEMPLATE_NORMAL
85 85 elif MODE_GALLERY == mode:
86 86 context[CONTEXT_POSTS] = thread_to_show.get_replies_with_images(
87 87 view_fields_only=True)
88 88
89 89 document = TEMPLATE_GALLERY
90 90 else:
91 91 raise Http404
92 92
93 93 # TODO Use dict here
94 94 return render(request, document, context_instance=context)
95 95
96 96 def post(self, request, post_id, mode=MODE_NORMAL):
97 97 opening_post = get_object_or_404(Post, id=post_id)
98 98
99 99 # If this is not OP, don't show it as it is
100 100 if not opening_post.is_opening():
101 101 raise Http404
102 102
103 103 if not opening_post.get_thread().archived:
104 104 form = PostForm(request.POST, request.FILES,
105 105 error_class=PlainErrorList)
106 106 form.session = request.session
107 107
108 108 if form.is_valid():
109 109 return self.new_post(request, form, opening_post)
110 110 if form.need_to_ban:
111 111 # Ban user because he is suspected to be a bot
112 112 self._ban_current_user(request)
113 113
114 114 return self.get(request, post_id, mode, form)
115 115
116 116 def new_post(self, request, form, opening_post=None, html_response=True):
117 117 """Add a new post (in thread or as a reply)."""
118 118
119 119 ip = utils.get_client_ip(request)
120 120
121 121 data = form.cleaned_data
122 122
123 123 title = data[FORM_TITLE]
124 124 text = data[FORM_TEXT]
125 image = data.get(FORM_IMAGE)
125 126
126 127 text = self._remove_invalid_links(text)
127 128
128 if FORM_IMAGE in list(data.keys()):
129 image = data[FORM_IMAGE]
130 else:
131 image = None
132
133 tags = []
134
135 129 post_thread = opening_post.get_thread()
136 130
137 131 post = Post.objects.create_post(title=title, text=text, image=image,
138 thread=post_thread, ip=ip, tags=tags)
132 thread=post_thread, ip=ip)
139 133 post.send_to_websocket(request)
140 134
141 135 thread_to_show = (opening_post.id if opening_post else post.id)
142 136
143 137 if html_response:
144 138 if opening_post:
145 139 return redirect(
146 140 reverse('thread', kwargs={'post_id': thread_to_show})
147 141 + '#' + str(post.id))
148 142 else:
149 143 return post
General Comments 0
You need to be logged in to leave comments. Login now