##// END OF EJS Templates
Trigger post update on a new post instead of sending the post itself. This fixed issues with posts sent in a fixed locale instead of a different locale for each client
neko259 -
r895:ffaaf497 default
parent child Browse files
Show More
@@ -1,445 +1,442 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
23 23 WS_CHANNEL_THREAD = "thread:"
24 24
25 25 APP_LABEL_BOARDS = 'boards'
26 26
27 27 CACHE_KEY_PPD = 'ppd'
28 28 CACHE_KEY_POST_URL = 'post_url'
29 29
30 30 POSTS_PER_DAY_RANGE = 7
31 31
32 32 BAN_REASON_AUTO = 'Auto'
33 33
34 34 IMAGE_THUMB_SIZE = (200, 150)
35 35
36 36 TITLE_MAX_LENGTH = 200
37 37
38 38 # TODO This should be removed
39 39 NO_IP = '0.0.0.0'
40 40
41 41 # TODO Real user agent should be saved instead of this
42 42 UNKNOWN_UA = ''
43 43
44 44 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
45 45
46 46 PARAMETER_TRUNCATED = 'truncated'
47 47 PARAMETER_TAG = 'tag'
48 48 PARAMETER_OFFSET = 'offset'
49 49 PARAMETER_DIFF_TYPE = 'type'
50 50
51 51 DIFF_TYPE_HTML = 'html'
52 52 DIFF_TYPE_JSON = 'json'
53 53
54 54 PREPARSE_PATTERNS = {
55 55 r'>>(\d+)': r'[post]\1[/post]', # Reflink ">>123"
56 56 r'^>(.+)': r'[quote]\1[/quote]', # Quote ">text"
57 57 r'^//(.+)': r'[comment]\1[/comment]', # Comment "//text"
58 58 }
59 59
60 60
61 61 class PostManager(models.Manager):
62 62 def create_post(self, title, text, image=None, thread=None, ip=NO_IP,
63 63 tags=None):
64 64 """
65 65 Creates new post
66 66 """
67 67
68 68 if not tags:
69 69 tags = []
70 70
71 71 posting_time = timezone.now()
72 72 if not thread:
73 73 thread = Thread.objects.create(bump_time=posting_time,
74 74 last_edit_time=posting_time)
75 75 new_thread = True
76 76 else:
77 77 new_thread = False
78 78
79 79 pre_text = self._preparse_text(text)
80 80
81 81 post = self.create(title=title,
82 82 text=pre_text,
83 83 pub_time=posting_time,
84 84 thread_new=thread,
85 85 poster_ip=ip,
86 86 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
87 87 # last!
88 88 last_edit_time=posting_time)
89 89
90 90 logger = logging.getLogger('boards.post.create')
91 91
92 92 logger.info('Created post {} by {}'.format(
93 93 post, post.poster_ip))
94 94
95 95 if image:
96 96 post_image = PostImage.objects.create(image=image)
97 97 post.images.add(post_image)
98 98 logger.info('Created image #{} for post #{}'.format(
99 99 post_image.id, post.id))
100 100
101 101 thread.replies.add(post)
102 102 list(map(thread.add_tag, tags))
103 103
104 104 if new_thread:
105 105 Thread.objects.process_oldest_threads()
106 106 else:
107 107 thread.bump()
108 108 thread.last_edit_time = posting_time
109 109 thread.save()
110 110
111 111 self.connect_replies(post)
112 112
113 113 return post
114 114
115 115 def delete_posts_by_ip(self, ip):
116 116 """
117 117 Deletes all posts of the author with same IP
118 118 """
119 119
120 120 posts = self.filter(poster_ip=ip)
121 121 for post in posts:
122 122 post.delete()
123 123
124 124 def connect_replies(self, post):
125 125 """
126 126 Connects replies to a post to show them as a reflink map
127 127 """
128 128
129 129 for reply_number in re.finditer(REGEX_REPLY, post.get_raw_text()):
130 130 post_id = reply_number.group(1)
131 131 ref_post = self.filter(id=post_id)
132 132 if ref_post.count() > 0:
133 133 referenced_post = ref_post[0]
134 134 referenced_post.referenced_posts.add(post)
135 135 referenced_post.last_edit_time = post.pub_time
136 136 referenced_post.build_refmap()
137 137 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
138 138
139 139 referenced_thread = referenced_post.get_thread()
140 140 referenced_thread.last_edit_time = post.pub_time
141 141 referenced_thread.save(update_fields=['last_edit_time'])
142 142
143 143 def get_posts_per_day(self):
144 144 """
145 145 Gets average count of posts per day for the last 7 days
146 146 """
147 147
148 148 day_end = date.today()
149 149 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
150 150
151 151 cache_key = CACHE_KEY_PPD + str(day_end)
152 152 ppd = cache.get(cache_key)
153 153 if ppd:
154 154 return ppd
155 155
156 156 day_time_start = timezone.make_aware(datetime.combine(
157 157 day_start, dtime()), timezone.get_current_timezone())
158 158 day_time_end = timezone.make_aware(datetime.combine(
159 159 day_end, dtime()), timezone.get_current_timezone())
160 160
161 161 posts_per_period = float(self.filter(
162 162 pub_time__lte=day_time_end,
163 163 pub_time__gte=day_time_start).count())
164 164
165 165 ppd = posts_per_period / POSTS_PER_DAY_RANGE
166 166
167 167 cache.set(cache_key, ppd)
168 168 return ppd
169 169
170 170 def _preparse_text(self, text):
171 171 """
172 172 Preparses text to change patterns like '>>' to a proper bbcode
173 173 tags.
174 174 """
175 175
176 176 for key, value in PREPARSE_PATTERNS.items():
177 177 text = re.sub(key, value, text, flags=re.MULTILINE)
178 178
179 179 return text
180 180
181 181
182 182 class Post(models.Model, Viewable):
183 183 """A post is a message."""
184 184
185 185 objects = PostManager()
186 186
187 187 class Meta:
188 188 app_label = APP_LABEL_BOARDS
189 189 ordering = ('id',)
190 190
191 191 title = models.CharField(max_length=TITLE_MAX_LENGTH)
192 192 pub_time = models.DateTimeField()
193 193 text = TextField(blank=True, null=True)
194 194 _text_rendered = TextField(blank=True, null=True, editable=False)
195 195
196 196 images = models.ManyToManyField(PostImage, null=True, blank=True,
197 197 related_name='ip+', db_index=True)
198 198
199 199 poster_ip = models.GenericIPAddressField()
200 200 poster_user_agent = models.TextField()
201 201
202 202 thread_new = models.ForeignKey('Thread', null=True, default=None,
203 203 db_index=True)
204 204 last_edit_time = models.DateTimeField()
205 205
206 206 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
207 207 null=True,
208 208 blank=True, related_name='rfp+',
209 209 db_index=True)
210 210 refmap = models.TextField(null=True, blank=True)
211 211
212 212 def __str__(self):
213 213 return 'P#{}/{}'.format(self.id, self.title)
214 214
215 215 def get_title(self):
216 216 """
217 217 Gets original post title or part of its text.
218 218 """
219 219
220 220 title = self.title
221 221 if not title:
222 222 title = self.get_text()
223 223
224 224 return title
225 225
226 226 def build_refmap(self):
227 227 """
228 228 Builds a replies map string from replies list. This is a cache to stop
229 229 the server from recalculating the map on every post show.
230 230 """
231 231 map_string = ''
232 232
233 233 first = True
234 234 for refpost in self.referenced_posts.all():
235 235 if not first:
236 236 map_string += ', '
237 237 map_string += '<a href="%s">&gt;&gt;%s</a>' % (refpost.get_url(),
238 238 refpost.id)
239 239 first = False
240 240
241 241 self.refmap = map_string
242 242
243 243 def get_sorted_referenced_posts(self):
244 244 return self.refmap
245 245
246 246 def is_referenced(self):
247 247 if not self.refmap:
248 248 return False
249 249 else:
250 250 return len(self.refmap) > 0
251 251
252 252 def is_opening(self):
253 253 """
254 254 Checks if this is an opening post or just a reply.
255 255 """
256 256
257 257 return self.get_thread().get_opening_post_id() == self.id
258 258
259 259 @transaction.atomic
260 260 def add_tag(self, tag):
261 261 edit_time = timezone.now()
262 262
263 263 thread = self.get_thread()
264 264 thread.add_tag(tag)
265 265 self.last_edit_time = edit_time
266 266 self.save(update_fields=['last_edit_time'])
267 267
268 268 thread.last_edit_time = edit_time
269 269 thread.save(update_fields=['last_edit_time'])
270 270
271 271 @transaction.atomic
272 272 def remove_tag(self, tag):
273 273 edit_time = timezone.now()
274 274
275 275 thread = self.get_thread()
276 276 thread.remove_tag(tag)
277 277 self.last_edit_time = edit_time
278 278 self.save(update_fields=['last_edit_time'])
279 279
280 280 thread.last_edit_time = edit_time
281 281 thread.save(update_fields=['last_edit_time'])
282 282
283 283 def get_url(self, thread=None):
284 284 """
285 285 Gets full url to the post.
286 286 """
287 287
288 288 cache_key = CACHE_KEY_POST_URL + str(self.id)
289 289 link = cache.get(cache_key)
290 290
291 291 if not link:
292 292 if not thread:
293 293 thread = self.get_thread()
294 294
295 295 opening_id = thread.get_opening_post_id()
296 296
297 297 if self.id != opening_id:
298 298 link = reverse('thread', kwargs={
299 299 'post_id': opening_id}) + '#' + str(self.id)
300 300 else:
301 301 link = reverse('thread', kwargs={'post_id': self.id})
302 302
303 303 cache.set(cache_key, link)
304 304
305 305 return link
306 306
307 307 def get_thread(self):
308 308 """
309 309 Gets post's thread.
310 310 """
311 311
312 312 return self.thread_new
313 313
314 314 def get_referenced_posts(self):
315 315 return self.referenced_posts.only('id', 'thread_new')
316 316
317 317 def get_view(self, moderator=False, need_open_link=False,
318 318 truncated=False, *args, **kwargs):
319 319 if 'is_opening' in kwargs:
320 320 is_opening = kwargs['is_opening']
321 321 else:
322 322 is_opening = self.is_opening()
323 323
324 324 if 'thread' in kwargs:
325 325 thread = kwargs['thread']
326 326 else:
327 327 thread = self.get_thread()
328 328
329 329 if 'can_bump' in kwargs:
330 330 can_bump = kwargs['can_bump']
331 331 else:
332 332 can_bump = thread.can_bump()
333 333
334 334 if is_opening:
335 335 opening_post_id = self.id
336 336 else:
337 337 opening_post_id = thread.get_opening_post_id()
338 338
339 339 return render_to_string('boards/post.html', {
340 340 'post': self,
341 341 'moderator': moderator,
342 342 'is_opening': is_opening,
343 343 'thread': thread,
344 344 'bumpable': can_bump,
345 345 'need_open_link': need_open_link,
346 346 'truncated': truncated,
347 347 'opening_post_id': opening_post_id,
348 348 })
349 349
350 350 def get_first_image(self):
351 351 return self.images.earliest('id')
352 352
353 353 def delete(self, using=None):
354 354 """
355 355 Deletes all post images and the post itself. If the post is opening,
356 356 thread with all posts is deleted.
357 357 """
358 358
359 359 self.images.all().delete()
360 360
361 361 if self.is_opening():
362 362 self.get_thread().delete()
363 363 else:
364 364 thread = self.get_thread()
365 365 thread.last_edit_time = timezone.now()
366 366 thread.save()
367 367
368 368 super(Post, self).delete(using)
369 369
370 370 logging.getLogger('boards.post.delete').info(
371 371 'Deleted post {}'.format(self))
372 372
373 373 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
374 374 include_last_update=False):
375 375 """
376 376 Gets post HTML or JSON data that can be rendered on a page or used by
377 377 API.
378 378 """
379 379
380 380 if format_type == DIFF_TYPE_HTML:
381 381 context = RequestContext(request)
382 382 context['post'] = self
383 383 if PARAMETER_TRUNCATED in request.GET:
384 384 context[PARAMETER_TRUNCATED] = True
385 385
386 386 # TODO Use dict here
387 387 return render_to_string('boards/api_post.html',
388 388 context_instance=context)
389 389 elif format_type == DIFF_TYPE_JSON:
390 390 post_json = {
391 391 'id': self.id,
392 392 'title': self.title,
393 393 'text': self._text_rendered,
394 394 }
395 395 if self.images.exists():
396 396 post_image = self.get_first_image()
397 397 post_json['image'] = post_image.image.url
398 398 post_json['image_preview'] = post_image.image.url_200x150
399 399 if include_last_update:
400 400 post_json['bump_time'] = datetime_to_epoch(
401 401 self.thread_new.bump_time)
402 402 return post_json
403 403
404 404 def send_to_websocket(self, request, recursive=True):
405 405 """
406 406 Sends post HTML data to the thread web socket.
407 407 """
408 408
409 409 if not settings.WEBSOCKETS_ENABLED:
410 410 return
411 411
412 412 client = Client()
413 413
414 414 channel_name = WS_CHANNEL_THREAD + str(self.get_thread()
415 415 .get_opening_post_id())
416 416 client.publish(channel_name, {
417 'html': self.get_post_data(
418 format_type=DIFF_TYPE_HTML,
419 request=request),
420 'diff_type': 'added' if recursive else 'updated',
417 'notification_type': 'new_post',
421 418 })
422 419 client.send()
423 420
424 421 logger = logging.getLogger('boards.post.websocket')
425 422
426 423 logger.info('Sent post #{} to channel {}'.format(self.id, channel_name))
427 424
428 425 if recursive:
429 426 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
430 427 post_id = reply_number.group(1)
431 428 ref_post = Post.objects.filter(id=post_id)[0]
432 429
433 430 ref_post.send_to_websocket(request, recursive=False)
434 431
435 432 def save(self, force_insert=False, force_update=False, using=None,
436 433 update_fields=None):
437 434 self._text_rendered = bbcode_extended(self.get_raw_text())
438 435
439 436 super().save(force_insert, force_update, using, update_fields)
440 437
441 438 def get_text(self):
442 439 return self._text_rendered
443 440
444 441 def get_raw_text(self):
445 442 return self.text
@@ -1,333 +1,335 b''
1 1 /*
2 2 @licstart The following is the entire license notice for the
3 3 JavaScript code in this page.
4 4
5 5
6 Copyright (C) 2013 neko259
6 Copyright (C) 2013-2014 neko259
7 7
8 8 The JavaScript code in this page is free software: you can
9 9 redistribute it and/or modify it under the terms of the GNU
10 10 General Public License (GNU GPL) as published by the Free Software
11 11 Foundation, either version 3 of the License, or (at your option)
12 12 any later version. The code is distributed WITHOUT ANY WARRANTY;
13 13 without even the implied warranty of MERCHANTABILITY or FITNESS
14 14 FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
15 15
16 16 As additional permission under GNU GPL version 3 section 7, you
17 17 may distribute non-source (e.g., minimized or compacted) forms of
18 18 that code without the copy of the GNU GPL normally required by
19 19 section 4, provided you include this license notice and a URL
20 20 through which recipients can access the Corresponding Source.
21 21
22 22 @licend The above is the entire license notice
23 23 for the JavaScript code in this page.
24 24 */
25 25
26 var wsUrl = 'ws://localhost:9090/connection/websocket';
27 26 var wsUser = '';
28 27
29 28 var loading = false;
30 29 var unreadPosts = 0;
31 30 var documentOriginalTitle = '';
32 31
33 32 // Thread ID does not change, can be stored one time
34 33 var threadId = $('div.thread').children('.post').first().attr('id');
35 34
35 /**
36 * Connect to websocket server and subscribe to thread updates. On any update we
37 * request a thread diff.
38 *
39 * @returns {boolean} true if connected, false otherwise
40 */
36 41 function connectWebsocket() {
37 42 var metapanel = $('.metapanel')[0];
38 43
39 44 var wsHost = metapanel.getAttribute('data-ws-host');
40 45 var wsPort = metapanel.getAttribute('data-ws-port');
41 46
42 47 if (wsHost.length > 0 && wsPort.length > 0)
43 48 var centrifuge = new Centrifuge({
44 49 "url": 'ws://' + wsHost + ':' + wsPort + "/connection/websocket",
45 50 "project": metapanel.getAttribute('data-ws-project'),
46 51 "user": wsUser,
47 52 "timestamp": metapanel.getAttribute('data-last-update'),
48 53 "token": metapanel.getAttribute('data-ws-token'),
49 54 "debug": false
50 55 });
51 56
52 57 centrifuge.on('error', function(error_message) {
53 58 console.log("Error connecting to websocket server.");
54 59 return false;
55 60 });
56 61
57 62 centrifuge.on('connect', function() {
58 63 var channelName = 'thread:' + threadId;
59 64 centrifuge.subscribe(channelName, function(message) {
60 var postHtml = message.data['html'];
61 var isAdded = (message.data['diff_type'] === 'added');
62
63 if (postHtml) {
64 updatePost(postHtml, isAdded);
65 }
65 getThreadDiff();
66 66 });
67 67
68 68 // For the case we closed the browser and missed some updates
69 69 getThreadDiff();
70 70 $('#autoupdate').text('[+]');
71 71 });
72 72
73 73 centrifuge.connect();
74 74
75 75 return true;
76 76 }
77 77
78 78 /**
79 79 * Get diff of the posts from the current thread timestamp.
80 80 * This is required if the browser was closed and some post updates were
81 81 * missed.
82 82 */
83 83 function getThreadDiff() {
84 84 var lastUpdateTime = $('.metapanel').attr('data-last-update');
85 85
86 86 var diffUrl = '/api/diff_thread/' + threadId + '/' + lastUpdateTime + '/';
87 87
88 88 $.getJSON(diffUrl)
89 89 .success(function(data) {
90 var bottom = isPageBottom();
91 90 var addedPosts = data.added;
92 91
93 92 for (var i = 0; i < addedPosts.length; i++) {
94 93 var postText = addedPosts[i];
95 94 var post = $(postText);
96 95
97 updatePost(post, true)
96 updatePost(post)
98 97
99 98 lastPost = post;
100 99 }
101 100
102 101 var updatedPosts = data.updated;
103 102
104 103 for (var i = 0; i < updatedPosts.length; i++) {
105 104 var postText = updatedPosts[i];
106 105 var post = $(postText);
107 106
108 updatePost(post, false)
107 updatePost(post)
109 108 }
110 109
111 110 // TODO Process removed posts if any
111 $('.metapanel').attr('data-last-update', data.last_update);
112 112 })
113 113 }
114 114
115 115 /**
116 * Add or update the post on thml page.
116 * Add or update the post on html page.
117 117 */
118 function updatePost(postHtml, isAdded) {
118 function updatePost(postHtml) {
119 119 // This needs to be set on start because the page is scrolled after posts
120 120 // are added or updated
121 121 var bottom = isPageBottom();
122 122
123 123 var post = $(postHtml);
124 124
125 var threadPosts = $('div.thread').children('.post');
125 var threadBlock = $('div.thread');
126 126
127 127 var lastUpdate = '';
128 128
129 if (isAdded) {
129 var postId = post.attr('id');
130
131 // If the post already exists, replace it. Otherwise add as a new one.
132 var existingPosts = threadBlock.children('.post[id=' + postId + ']');
133
134 if (existingPosts.size() > 0) {
135 existingPosts.replaceWith(post);
136 } else {
137 var threadPosts = threadBlock.children('.post');
130 138 var lastPost = threadPosts.last();
131 139
132 140 post.appendTo(lastPost.parent());
133 141
134 142 updateBumplimitProgress(1);
135 143 showNewPostsTitle(1);
136 144
137 145 lastUpdate = post.children('.post-info').first()
138 146 .children('.pub_time').first().text();
139 147
140 148 if (bottom) {
141 149 scrollToBottom();
142 150 }
143 } else {
144 var postId = post.attr('id');
145
146 var oldPost = $('div.thread').children('.post[id=' + postId + ']');
147
148 oldPost.replaceWith(post);
149 151 }
150 152
151 153 processNewPost(post);
152 154 updateMetadataPanel(lastUpdate)
153 155 }
154 156
155 157 /**
156 158 * Initiate a blinking animation on a node to show it was updated.
157 159 */
158 160 function blink(node) {
159 161 var blinkCount = 2;
160 162
161 163 var nodeToAnimate = node;
162 164 for (var i = 0; i < blinkCount; i++) {
163 165 nodeToAnimate = nodeToAnimate.fadeTo('fast', 0.5).fadeTo('fast', 1.0);
164 166 }
165 167 }
166 168
167 169 function isPageBottom() {
168 170 var scroll = $(window).scrollTop() / ($(document).height()
169 171 - $(window).height());
170 172
171 173 return scroll == 1
172 174 }
173 175
174 176 function initAutoupdate() {
175 177 return connectWebsocket();
176 178 }
177 179
178 180 function getReplyCount() {
179 181 return $('.thread').children('.post').length
180 182 }
181 183
182 184 function getImageCount() {
183 185 return $('.thread').find('img').length
184 186 }
185 187
186 188 /**
187 189 * Update post count, images count and last update time in the metadata
188 190 * panel.
189 191 */
190 192 function updateMetadataPanel(lastUpdate) {
191 193 var replyCountField = $('#reply-count');
192 194 var imageCountField = $('#image-count');
193 195
194 196 replyCountField.text(getReplyCount());
195 197 imageCountField.text(getImageCount());
196 198
197 199 if (lastUpdate !== '') {
198 200 var lastUpdateField = $('#last-update');
199 201 lastUpdateField.text(lastUpdate);
200 202 blink(lastUpdateField);
201 203 }
202 204
203 205 blink(replyCountField);
204 206 blink(imageCountField);
205 207 }
206 208
207 209 /**
208 210 * Update bumplimit progress bar
209 211 */
210 212 function updateBumplimitProgress(postDelta) {
211 213 var progressBar = $('#bumplimit_progress');
212 214 if (progressBar) {
213 215 var postsToLimitElement = $('#left_to_limit');
214 216
215 217 var oldPostsToLimit = parseInt(postsToLimitElement.text());
216 218 var postCount = getReplyCount();
217 219 var bumplimit = postCount - postDelta + oldPostsToLimit;
218 220
219 221 var newPostsToLimit = bumplimit - postCount;
220 222 if (newPostsToLimit <= 0) {
221 223 $('.bar-bg').remove();
222 224 $('.thread').children('.post').addClass('dead_post');
223 225 } else {
224 226 postsToLimitElement.text(newPostsToLimit);
225 227 progressBar.width((100 - postCount / bumplimit * 100.0) + '%');
226 228 }
227 229 }
228 230 }
229 231
230 232 /**
231 233 * Show 'new posts' text in the title if the document is not visible to a user
232 234 */
233 235 function showNewPostsTitle(newPostCount) {
234 236 if (document.hidden) {
235 237 if (documentOriginalTitle === '') {
236 238 documentOriginalTitle = document.title;
237 239 }
238 240 unreadPosts = unreadPosts + newPostCount;
239 241 document.title = '[' + unreadPosts + '] ' + documentOriginalTitle;
240 242
241 243 document.addEventListener('visibilitychange', function() {
242 244 if (documentOriginalTitle !== '') {
243 245 document.title = documentOriginalTitle;
244 246 documentOriginalTitle = '';
245 247 unreadPosts = 0;
246 248 }
247 249
248 250 document.removeEventListener('visibilitychange', null);
249 251 });
250 252 }
251 253 }
252 254
253 255 /**
254 256 * Clear all entered values in the form fields
255 257 */
256 258 function resetForm(form) {
257 259 form.find('input:text, input:password, input:file, select, textarea').val('');
258 260 form.find('input:radio, input:checkbox')
259 261 .removeAttr('checked').removeAttr('selected');
260 262 $('.file_wrap').find('.file-thumb').remove();
261 263 }
262 264
263 265 /**
264 266 * When the form is posted, this method will be run as a callback
265 267 */
266 268 function updateOnPost(response, statusText, xhr, form) {
267 269 var json = $.parseJSON(response);
268 270 var status = json.status;
269 271
270 272 showAsErrors(form, '');
271 273
272 274 if (status === 'ok') {
273 275 resetForm(form);
274 276 } else {
275 277 var errors = json.errors;
276 278 for (var i = 0; i < errors.length; i++) {
277 279 var fieldErrors = errors[i];
278 280
279 281 var error = fieldErrors.errors;
280 282
281 283 showAsErrors(form, error);
282 284 }
283 285 }
284 286
285 287 scrollToBottom();
286 288 }
287 289
288 290 /**
289 291 * Show text in the errors row of the form.
290 292 * @param form
291 293 * @param text
292 294 */
293 295 function showAsErrors(form, text) {
294 296 form.children('.form-errors').remove();
295 297
296 298 if (text.length > 0) {
297 299 var errorList = $('<div class="form-errors">' + text
298 300 + '<div>');
299 301 errorList.appendTo(form);
300 302 }
301 303 }
302 304
303 305 /**
304 306 * Run js methods that are usually run on the document, on the new post
305 307 */
306 308 function processNewPost(post) {
307 309 addRefLinkPreview(post[0]);
308 310 highlightCode(post);
309 311 blink(post);
310 312 }
311 313
312 314 $(document).ready(function(){
313 315 if ('WebSocket' in window) {
314 316 if (initAutoupdate()) {
315 317 // Post form data over AJAX
316 318 var threadId = $('div.thread').children('.post').first().attr('id');
317 319
318 320 var form = $('#form');
319 321
320 322 var options = {
321 323 beforeSubmit: function(arr, $form, options) {
322 324 showAsErrors($('form'), gettext('Sending message...'));
323 325 },
324 326 success: updateOnPost,
325 327 url: '/api/add_post/' + threadId + '/'
326 328 };
327 329
328 330 form.ajaxForm(options);
329 331
330 332 resetForm(form);
331 333 }
332 334 }
333 335 });
@@ -1,157 +1,157 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 @transaction.atomic
117 117 def new_post(self, request, form, opening_post=None, html_response=True):
118 118 """Add a new post (in thread or as a reply)."""
119 119
120 120 ip = utils.get_client_ip(request)
121 121 is_banned = Ban.objects.filter(ip=ip).exists()
122 122
123 123 if is_banned:
124 124 if html_response:
125 125 return redirect(BannedView().as_view())
126 126 else:
127 127 return None
128 128
129 129 data = form.cleaned_data
130 130
131 131 title = data[FORM_TITLE]
132 132 text = data[FORM_TEXT]
133 133
134 134 text = self._remove_invalid_links(text)
135 135
136 136 if FORM_IMAGE in list(data.keys()):
137 137 image = data[FORM_IMAGE]
138 138 else:
139 139 image = None
140 140
141 141 tags = []
142 142
143 143 post_thread = opening_post.get_thread()
144 144
145 145 post = Post.objects.create_post(title=title, text=text, image=image,
146 146 thread=post_thread, ip=ip, tags=tags)
147 post.send_to_websocket(request)
147 post.send_to_websocket(request, recursive=False)
148 148
149 149 thread_to_show = (opening_post.id if opening_post else post.id)
150 150
151 151 if html_response:
152 152 if opening_post:
153 153 return redirect(
154 154 reverse('thread', kwargs={'post_id': thread_to_show})
155 155 + '#' + str(post.id))
156 156 else:
157 157 return post
General Comments 0
You need to be logged in to leave comments. Login now