##// END OF EJS Templates
Fixed getting precise and synced last update time. Added autoscroll to bottom after updating if user is at the page bottom
neko259 -
r373:2f30e48c thread_autoupdate
parent child Browse files
Show More
@@ -1,439 +1,439 b''
1 1 import os
2 2 from random import random
3 3 import time
4 4 import math
5 5 from django.core.cache import cache
6 6
7 7 from django.db import models
8 8 from django.db.models import Count
9 9 from django.http import Http404
10 10 from django.utils import timezone
11 11 from markupfield.fields import MarkupField
12 12 from boards import settings as board_settings
13 13
14 14 from neboard import settings
15 15 import thumbs
16 16
17 17 import re
18 18
19 19 BAN_REASON_MAX_LENGTH = 200
20 20
21 21 BAN_REASON_AUTO = 'Auto'
22 22
23 23 IMAGE_THUMB_SIZE = (200, 150)
24 24
25 25 TITLE_MAX_LENGTH = 50
26 26
27 27 DEFAULT_MARKUP_TYPE = 'markdown'
28 28
29 29 NO_PARENT = -1
30 30 NO_IP = '0.0.0.0'
31 31 UNKNOWN_UA = ''
32 32 ALL_PAGES = -1
33 33 OPENING_POST_POPULARITY_WEIGHT = 2
34 34 IMAGES_DIRECTORY = 'images/'
35 35 FILE_EXTENSION_DELIMITER = '.'
36 36
37 37 RANK_ADMIN = 0
38 38 RANK_MODERATOR = 10
39 39 RANK_USER = 100
40 40
41 41 SETTING_MODERATE = "moderate"
42 42
43 43 REGEX_REPLY = re.compile('>>(\d+)')
44 44
45 45
46 46 class PostManager(models.Manager):
47 47
48 48 def create_post(self, title, text, image=None, thread=None,
49 49 ip=NO_IP, tags=None, user=None):
50 50 posting_time = timezone.now()
51 51
52 52 post = self.create(title=title,
53 53 text=text,
54 pub_time=timezone.now(),
54 pub_time=posting_time,
55 55 thread=thread,
56 56 image=image,
57 57 poster_ip=ip,
58 58 poster_user_agent=UNKNOWN_UA,
59 59 last_edit_time=posting_time,
60 60 bump_time=posting_time,
61 61 user=user)
62 62
63 63 if tags:
64 64 map(post.tags.add, tags)
65 65 for tag in tags:
66 66 tag.threads.add(post)
67 67
68 68 if thread:
69 69 thread.replies.add(post)
70 70 thread.bump()
71 71 thread.last_edit_time = posting_time
72 72 thread.save()
73 73
74 74 #cache_key = thread.get_cache_key()
75 75 #cache.delete(cache_key)
76 76
77 77 else:
78 78 self._delete_old_threads()
79 79
80 80 self.connect_replies(post)
81 81
82 82 return post
83 83
84 84 def delete_post(self, post):
85 85 if post.replies.count() > 0:
86 86 map(self.delete_post, post.replies.all())
87 87
88 88 # Update thread's last edit time (used as cache key)
89 89 thread = post.thread
90 90 if thread:
91 91 thread.last_edit_time = timezone.now()
92 92 thread.save()
93 93
94 94 #cache_key = thread.get_cache_key()
95 95 #cache.delete(cache_key)
96 96
97 97 post.delete()
98 98
99 99 def delete_posts_by_ip(self, ip):
100 100 posts = self.filter(poster_ip=ip)
101 101 map(self.delete_post, posts)
102 102
103 103 def get_threads(self, tag=None, page=ALL_PAGES,
104 104 order_by='-bump_time'):
105 105 if tag:
106 106 threads = tag.threads
107 107
108 108 if threads.count() == 0:
109 109 raise Http404
110 110 else:
111 111 threads = self.filter(thread=None)
112 112
113 113 threads = threads.order_by(order_by)
114 114
115 115 if page != ALL_PAGES:
116 116 thread_count = threads.count()
117 117
118 118 if page < self._get_page_count(thread_count):
119 119 start_thread = page * settings.THREADS_PER_PAGE
120 120 end_thread = min(start_thread + settings.THREADS_PER_PAGE,
121 121 thread_count)
122 122 threads = threads[start_thread:end_thread]
123 123
124 124 return threads
125 125
126 126 def get_thread(self, opening_post_id):
127 127 try:
128 128 opening_post = self.get(id=opening_post_id, thread=None)
129 129 except Post.DoesNotExist:
130 130 raise Http404
131 131
132 132 #cache_key = opening_post.get_cache_key()
133 133 #thread = cache.get(cache_key)
134 134 #if thread:
135 135 # return thread
136 136
137 137 if opening_post.replies:
138 138 thread = [opening_post]
139 139 thread.extend(opening_post.replies.all().order_by('pub_time'))
140 140
141 141 #cache.set(cache_key, thread, board_settings.CACHE_TIMEOUT)
142 142
143 143 return thread
144 144
145 145 def exists(self, post_id):
146 146 posts = self.filter(id=post_id)
147 147
148 148 return posts.count() > 0
149 149
150 150 def get_thread_page_count(self, tag=None):
151 151 if tag:
152 152 threads = self.filter(thread=None, tags=tag)
153 153 else:
154 154 threads = self.filter(thread=None)
155 155
156 156 return self._get_page_count(threads.count())
157 157
158 158 def _delete_old_threads(self):
159 159 """
160 160 Preserves maximum thread count. If there are too many threads,
161 161 delete the old ones.
162 162 """
163 163
164 164 # TODO Move old threads to the archive instead of deleting them.
165 165 # Maybe make some 'old' field in the model to indicate the thread
166 166 # must not be shown and be able for replying.
167 167
168 168 threads = self.get_threads()
169 169 thread_count = threads.count()
170 170
171 171 if thread_count > settings.MAX_THREAD_COUNT:
172 172 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
173 173 old_threads = threads[thread_count - num_threads_to_delete:]
174 174
175 175 map(self.delete_post, old_threads)
176 176
177 177 def connect_replies(self, post):
178 178 """Connect replies to a post to show them as a refmap"""
179 179
180 180 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
181 181 id = reply_number.group(1)
182 182 ref_post = self.filter(id=id)
183 183 if ref_post.count() > 0:
184 184 referenced_post = ref_post[0]
185 185 referenced_post.referenced_posts.add(post)
186 referenced_post.last_edit_time = timezone.now()
186 referenced_post.last_edit_time = post.pub_time
187 187 referenced_post.save()
188 188
189 189 def _get_page_count(self, thread_count):
190 190 return int(math.ceil(thread_count / float(settings.THREADS_PER_PAGE)))
191 191
192 192
193 193 class TagManager(models.Manager):
194 194
195 195 def get_not_empty_tags(self):
196 196 tags = self.annotate(Count('threads')) \
197 197 .filter(threads__count__gt=0).order_by('name')
198 198
199 199 return tags
200 200
201 201
202 202 class Tag(models.Model):
203 203 """
204 204 A tag is a text node assigned to the post. The tag serves as a board
205 205 section. There can be multiple tags for each message
206 206 """
207 207
208 208 objects = TagManager()
209 209
210 210 name = models.CharField(max_length=100)
211 211 threads = models.ManyToManyField('Post', null=True,
212 212 blank=True, related_name='tag+')
213 213 linked = models.ForeignKey('Tag', null=True, blank=True)
214 214
215 215 def __unicode__(self):
216 216 return self.name
217 217
218 218 def is_empty(self):
219 219 return self.get_post_count() == 0
220 220
221 221 def get_post_count(self):
222 222 return self.threads.count()
223 223
224 224 def get_popularity(self):
225 225 posts_with_tag = Post.objects.get_threads(tag=self)
226 226 reply_count = 0
227 227 for post in posts_with_tag:
228 228 reply_count += post.get_reply_count()
229 229 reply_count += OPENING_POST_POPULARITY_WEIGHT
230 230
231 231 return reply_count
232 232
233 233 def get_linked_tags(self):
234 234 tag_list = []
235 235 self.get_linked_tags_list(tag_list)
236 236
237 237 return tag_list
238 238
239 239 def get_linked_tags_list(self, tag_list=[]):
240 240 """
241 241 Returns the list of tags linked to current. The list can be got
242 242 through returned value or tag_list parameter
243 243 """
244 244
245 245 linked_tag = self.linked
246 246
247 247 if linked_tag and not (linked_tag in tag_list):
248 248 tag_list.append(linked_tag)
249 249
250 250 linked_tag.get_linked_tags_list(tag_list)
251 251
252 252
253 253 class Post(models.Model):
254 254 """A post is a message."""
255 255
256 256 objects = PostManager()
257 257
258 258 def _update_image_filename(self, filename):
259 259 """Get unique image filename"""
260 260
261 261 path = IMAGES_DIRECTORY
262 262 new_name = str(int(time.mktime(time.gmtime())))
263 263 new_name += str(int(random() * 1000))
264 264 new_name += FILE_EXTENSION_DELIMITER
265 265 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
266 266
267 267 return os.path.join(path, new_name)
268 268
269 269 title = models.CharField(max_length=TITLE_MAX_LENGTH)
270 270 pub_time = models.DateTimeField()
271 271 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
272 272 escape_html=False)
273 273
274 274 image_width = models.IntegerField(default=0)
275 275 image_height = models.IntegerField(default=0)
276 276
277 277 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
278 278 blank=True, sizes=(IMAGE_THUMB_SIZE,),
279 279 width_field='image_width',
280 280 height_field='image_height')
281 281
282 282 poster_ip = models.GenericIPAddressField()
283 283 poster_user_agent = models.TextField()
284 284
285 285 thread = models.ForeignKey('Post', null=True, default=None)
286 286 tags = models.ManyToManyField(Tag)
287 287 last_edit_time = models.DateTimeField()
288 288 bump_time = models.DateTimeField()
289 289 user = models.ForeignKey('User', null=True, default=None)
290 290
291 291 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
292 292 blank=True, related_name='re+')
293 293 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
294 294 null=True,
295 295 blank=True, related_name='rfp+')
296 296
297 297 def __unicode__(self):
298 298 return '#' + str(self.id) + ' ' + self.title + ' (' + \
299 299 self.text.raw[:50] + ')'
300 300
301 301 def get_title(self):
302 302 title = self.title
303 303 if len(title) == 0:
304 304 title = self.text.raw[:20]
305 305
306 306 return title
307 307
308 308 def get_reply_count(self):
309 309 return self.replies.count()
310 310
311 311 def get_images_count(self):
312 312 images_count = 1 if self.image else 0
313 313 images_count += self.replies.filter(image_width__gt=0).count()
314 314
315 315 return images_count
316 316
317 317 def can_bump(self):
318 318 """Check if the thread can be bumped by replying"""
319 319
320 320 post_count = self.get_reply_count()
321 321
322 322 return post_count <= settings.MAX_POSTS_PER_THREAD
323 323
324 324 def bump(self):
325 325 """Bump (move to up) thread"""
326 326
327 327 if self.can_bump():
328 328 self.bump_time = timezone.now()
329 329
330 330 def get_last_replies(self):
331 331 if settings.LAST_REPLIES_COUNT > 0:
332 332 reply_count = self.get_reply_count()
333 333
334 334 if reply_count > 0:
335 335 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
336 336 reply_count)
337 337 last_replies = self.replies.all().order_by('pub_time')[
338 338 reply_count - reply_count_to_show:]
339 339
340 340 return last_replies
341 341
342 342 def get_tags(self):
343 343 """Get a sorted tag list"""
344 344
345 345 return self.tags.order_by('name')
346 346
347 347 def get_cache_key(self):
348 348 return str(self.id) + str(self.last_edit_time.microsecond)
349 349
350 350 def get_sorted_referenced_posts(self):
351 351 return self.referenced_posts.order_by('id')
352 352
353 353 def is_referenced(self):
354 354 return self.referenced_posts.count() > 0
355 355
356 356
357 357 class User(models.Model):
358 358
359 359 user_id = models.CharField(max_length=50)
360 360 rank = models.IntegerField()
361 361
362 362 registration_time = models.DateTimeField()
363 363
364 364 fav_tags = models.ManyToManyField(Tag, null=True, blank=True)
365 365 fav_threads = models.ManyToManyField(Post, related_name='+', null=True,
366 366 blank=True)
367 367
368 368 def save_setting(self, name, value):
369 369 setting, created = Setting.objects.get_or_create(name=name, user=self)
370 370 setting.value = str(value)
371 371 setting.save()
372 372
373 373 return setting
374 374
375 375 def get_setting(self, name):
376 376 if Setting.objects.filter(name=name, user=self).exists():
377 377 setting = Setting.objects.get(name=name, user=self)
378 378 setting_value = setting.value
379 379 else:
380 380 setting_value = None
381 381
382 382 return setting_value
383 383
384 384 def is_moderator(self):
385 385 return RANK_MODERATOR >= self.rank
386 386
387 387 def get_sorted_fav_tags(self):
388 388 cache_key = self._get_tag_cache_key()
389 389 fav_tags = cache.get(cache_key)
390 390 if fav_tags:
391 391 return fav_tags
392 392
393 393 tags = self.fav_tags.annotate(Count('threads'))\
394 394 .filter(threads__count__gt=0).order_by('name')
395 395
396 396 if tags:
397 397 cache.set(cache_key, tags, board_settings.CACHE_TIMEOUT)
398 398
399 399 return tags
400 400
401 401 def get_post_count(self):
402 402 return Post.objects.filter(user=self).count()
403 403
404 404 def __unicode__(self):
405 405 return self.user_id + '(' + str(self.rank) + ')'
406 406
407 407 def get_last_access_time(self):
408 408 posts = Post.objects.filter(user=self)
409 409 if posts.count() > 0:
410 410 return posts.latest('pub_time').pub_time
411 411
412 412 def add_tag(self, tag):
413 413 self.fav_tags.add(tag)
414 414 cache.delete(self._get_tag_cache_key())
415 415
416 416 def remove_tag(self, tag):
417 417 self.fav_tags.remove(tag)
418 418 cache.delete(self._get_tag_cache_key())
419 419
420 420 def _get_tag_cache_key(self):
421 421 return self.user_id + '_tags'
422 422
423 423
424 424 class Setting(models.Model):
425 425
426 426 name = models.CharField(max_length=50)
427 427 value = models.CharField(max_length=50)
428 428 user = models.ForeignKey(User)
429 429
430 430
431 431 class Ban(models.Model):
432 432
433 433 ip = models.GenericIPAddressField()
434 434 reason = models.CharField(default=BAN_REASON_AUTO,
435 435 max_length=BAN_REASON_MAX_LENGTH)
436 436 can_read = models.BooleanField(default=True)
437 437
438 438 def __unicode__(self):
439 439 return self.ip
@@ -1,102 +1,116 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 6 Copyright (C) 2013 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 26 var THREAD_UPDATE_DELAY = 10000;
27 27
28 28 var loading = false;
29 29 var lastUpdateTime = null;
30 30
31 31 function blink(node) {
32 32 var blinkCount = 2;
33 33 var blinkDelay = 250;
34 34
35 35 var nodeToAnimate = node;
36 36 for (var i = 0; i < blinkCount; i++) {
37 37 nodeToAnimate = nodeToAnimate.fadeOut(blinkDelay).fadeIn(blinkDelay);
38 38 }
39 39 }
40 40
41 41 function updateThread() {
42 42 if (loading) {
43 43 return;
44 44 }
45 45
46 46 loading = true;
47 47
48 48 var threadPosts = $('div.thread').children('.post');
49 49
50 50 var lastPost = threadPosts.last();
51 51 var threadId = threadPosts.first().attr('id');
52 52
53 53 var diffUrl = '/api/diff_thread/' + threadId + '/' + lastUpdateTime + '/';
54 54 $.getJSON(diffUrl)
55 55 .success(function(data) {
56 var bottom = isPageBottom();
57
56 58 var addedPosts = data.added;
57 59
58 60 for (var i = 0; i < addedPosts.length; i++) {
59 61 var postText = addedPosts[i];
60 62
61 63 var post = $(postText);
62 64 post.appendTo(lastPost.parent());
63 65 addRefLinkPreview(post[0]);
64 66
65 67 lastPost = post;
66 68 blink(post);
67 69 }
68 70
69 71 var updatedPosts = data.updated;
70 72 for (var i = 0; i < updatedPosts.length; i++) {
71 73 var postText = updatedPosts[i];
72 74
73 75 var post = $(postText);
74 76 var postId = post.attr('id');
75 77
76 78 var oldPost = $('div.thread').children('.post[id=' + postId + ']');
77 79
78 80 oldPost.replaceWith(post);
79 81 addRefLinkPreview(post[0]);
80 82
81 83 blink(post);
82 84 }
83 85
84 // TODO Process updated and deleted posts
86 // TODO Process deleted posts
85 87
86 88 lastUpdateTime = data.last_update;
87 89 loading = false;
90
91 if (bottom) {
92 var $target = $('html,body');
93 $target.animate({scrollTop: $target.height()}, 1000);
94 }
88 95 })
89 96 .error(function(data) {
90 97 // TODO Show error message that server is unavailable?
91 98
92 99 loading = false;
93 100 });
94 101 }
95 102
103 function isPageBottom() {
104 var scroll = $(window).scrollTop() / ($(document).height()
105 - $(window).height())
106
107 return scroll == 1
108 }
109
96 110 function initAutoupdate() {
97 111 loading = false;
98 112
99 113 lastUpdateTime = $('.metapanel').attr('data-last-update');
100 114
101 115 setInterval(updateThread, THREAD_UPDATE_DELAY);
102 116 }
@@ -1,563 +1,564 b''
1 1 import hashlib
2 2 import json
3 3 import string
4 4 import time
5 5 import calendar
6 6
7 7 from datetime import datetime
8 8
9 9 from django.core import serializers
10 10 from django.core.urlresolvers import reverse
11 11 from django.http import HttpResponseRedirect
12 12 from django.http.response import HttpResponse
13 13 from django.template import RequestContext
14 14 from django.shortcuts import render, redirect, get_object_or_404
15 15 from django.utils import timezone
16 16 from django.db import transaction
17 import math
17 18
18 19 from boards import forms
19 20 import boards
20 21 from boards import utils
21 22 from boards.forms import ThreadForm, PostForm, SettingsForm, PlainErrorList, \
22 23 ThreadCaptchaForm, PostCaptchaForm, LoginForm, ModeratorSettingsForm
23 24
24 25 from boards.models import Post, Tag, Ban, User, RANK_USER, SETTING_MODERATE, \
25 26 REGEX_REPLY
26 27 from boards import authors
27 28 from boards.utils import get_client_ip
28 29 import neboard
29 30 import re
30 31
31 32 BAN_REASON_SPAM = 'Autoban: spam bot'
32 33
33 34
34 35 def index(request, page=0):
35 36 context = _init_default_context(request)
36 37
37 38 if utils.need_include_captcha(request):
38 39 threadFormClass = ThreadCaptchaForm
39 40 kwargs = {'request': request}
40 41 else:
41 42 threadFormClass = ThreadForm
42 43 kwargs = {}
43 44
44 45 if request.method == 'POST':
45 46 form = threadFormClass(request.POST, request.FILES,
46 47 error_class=PlainErrorList, **kwargs)
47 48 form.session = request.session
48 49
49 50 if form.is_valid():
50 51 return _new_post(request, form)
51 52 if form.need_to_ban:
52 53 # Ban user because he is suspected to be a bot
53 54 _ban_current_user(request)
54 55 else:
55 56 form = threadFormClass(error_class=PlainErrorList, **kwargs)
56 57
57 58 threads = []
58 59 for thread in Post.objects.get_threads(page=int(page)):
59 60 threads.append({
60 61 'thread': thread,
61 62 'bumpable': thread.can_bump(),
62 63 'last_replies': thread.get_last_replies(),
63 64 })
64 65
65 66 # TODO Make this generic for tag and threads list pages
66 67 context['threads'] = None if len(threads) == 0 else threads
67 68 context['form'] = form
68 69
69 70 page_count = Post.objects.get_thread_page_count()
70 71 context['pages'] = range(page_count)
71 72 page = int(page)
72 73 if page < page_count - 1:
73 74 context['next_page'] = str(page + 1)
74 75 if page > 0:
75 76 context['prev_page'] = str(page - 1)
76 77
77 78 return render(request, 'boards/posting_general.html',
78 79 context)
79 80
80 81
81 82 @transaction.commit_on_success
82 83 def _new_post(request, form, thread_id=boards.models.NO_PARENT):
83 84 """Add a new post (in thread or as a reply)."""
84 85
85 86 ip = get_client_ip(request)
86 87 is_banned = Ban.objects.filter(ip=ip).exists()
87 88
88 89 if is_banned:
89 90 return redirect(you_are_banned)
90 91
91 92 data = form.cleaned_data
92 93
93 94 title = data['title']
94 95 text = data['text']
95 96
96 97 text = _remove_invalid_links(text)
97 98
98 99 if 'image' in data.keys():
99 100 image = data['image']
100 101 else:
101 102 image = None
102 103
103 104 tags = []
104 105
105 106 new_thread = thread_id == boards.models.NO_PARENT
106 107 if new_thread:
107 108 tag_strings = data['tags']
108 109
109 110 if tag_strings:
110 111 tag_strings = tag_strings.split(' ')
111 112 for tag_name in tag_strings:
112 113 tag_name = string.lower(tag_name.strip())
113 114 if len(tag_name) > 0:
114 115 tag, created = Tag.objects.get_or_create(name=tag_name)
115 116 tags.append(tag)
116 117
117 118 linked_tags = tag.get_linked_tags()
118 119 if len(linked_tags) > 0:
119 120 tags.extend(linked_tags)
120 121
121 122 op = None if thread_id == boards.models.NO_PARENT else \
122 123 get_object_or_404(Post, id=thread_id)
123 124 post = Post.objects.create_post(title=title, text=text, ip=ip,
124 125 thread=op, image=image,
125 126 tags=tags, user=_get_user(request))
126 127
127 128 thread_to_show = (post.id if new_thread else thread_id)
128 129
129 130 if new_thread:
130 131 return redirect(thread, post_id=thread_to_show)
131 132 else:
132 133 return redirect(reverse(thread, kwargs={'post_id': thread_to_show}) +
133 134 '#' + str(post.id))
134 135
135 136
136 137 def tag(request, tag_name, page=0):
137 138 """
138 139 Get all tag threads. Threads are split in pages, so some page is
139 140 requested. Default page is 0.
140 141 """
141 142
142 143 tag = get_object_or_404(Tag, name=tag_name)
143 144 threads = []
144 145 for thread in Post.objects.get_threads(tag=tag, page=int(page)):
145 146 threads.append({
146 147 'thread': thread,
147 148 'bumpable': thread.can_bump(),
148 149 'last_replies': thread.get_last_replies(),
149 150 })
150 151
151 152 if request.method == 'POST':
152 153 form = ThreadForm(request.POST, request.FILES,
153 154 error_class=PlainErrorList)
154 155 form.session = request.session
155 156
156 157 if form.is_valid():
157 158 return _new_post(request, form)
158 159 if form.need_to_ban:
159 160 # Ban user because he is suspected to be a bot
160 161 _ban_current_user(request)
161 162 else:
162 163 form = forms.ThreadForm(initial={'tags': tag_name},
163 164 error_class=PlainErrorList)
164 165
165 166 context = _init_default_context(request)
166 167 context['threads'] = None if len(threads) == 0 else threads
167 168 context['tag'] = tag
168 169
169 170 page_count = Post.objects.get_thread_page_count(tag=tag)
170 171 context['pages'] = range(page_count)
171 172 page = int(page)
172 173 if page < page_count - 1:
173 174 context['next_page'] = str(page + 1)
174 175 if page > 0:
175 176 context['prev_page'] = str(page - 1)
176 177
177 178 context['form'] = form
178 179
179 180 return render(request, 'boards/posting_general.html',
180 181 context)
181 182
182 183
183 184 def thread(request, post_id):
184 185 """Get all thread posts"""
185 186
186 187 if utils.need_include_captcha(request):
187 188 postFormClass = PostCaptchaForm
188 189 kwargs = {'request': request}
189 190 else:
190 191 postFormClass = PostForm
191 192 kwargs = {}
192 193
193 194 if request.method == 'POST':
194 195 form = postFormClass(request.POST, request.FILES,
195 196 error_class=PlainErrorList, **kwargs)
196 197 form.session = request.session
197 198
198 199 if form.is_valid():
199 200 return _new_post(request, form, post_id)
200 201 if form.need_to_ban:
201 202 # Ban user because he is suspected to be a bot
202 203 _ban_current_user(request)
203 204 else:
204 205 form = postFormClass(error_class=PlainErrorList, **kwargs)
205 206
206 207 posts = Post.objects.get_thread(post_id)
207 208
208 209 context = _init_default_context(request)
209 210
210 211 context['posts'] = posts
211 212 context['form'] = form
212 213 context['bumpable'] = posts[0].can_bump()
213 214 if context['bumpable']:
214 215 context['posts_left'] = neboard.settings.MAX_POSTS_PER_THREAD - len(
215 216 posts)
216 217 context['bumplimit_progress'] = str(
217 218 float(context['posts_left']) /
218 219 neboard.settings.MAX_POSTS_PER_THREAD * 100)
219 220 context["last_update"] = _datetime_to_epoch(posts[0].last_edit_time)
220 221
221 222 return render(request, 'boards/thread.html', context)
222 223
223 224
224 225 def login(request):
225 226 """Log in with user id"""
226 227
227 228 context = _init_default_context(request)
228 229
229 230 if request.method == 'POST':
230 231 form = LoginForm(request.POST, request.FILES,
231 232 error_class=PlainErrorList)
232 233 form.session = request.session
233 234
234 235 if form.is_valid():
235 236 user = User.objects.get(user_id=form.cleaned_data['user_id'])
236 237 request.session['user_id'] = user.id
237 238 return redirect(index)
238 239
239 240 else:
240 241 form = LoginForm()
241 242
242 243 context['form'] = form
243 244
244 245 return render(request, 'boards/login.html', context)
245 246
246 247
247 248 def settings(request):
248 249 """User's settings"""
249 250
250 251 context = _init_default_context(request)
251 252 user = _get_user(request)
252 253 is_moderator = user.is_moderator()
253 254
254 255 if request.method == 'POST':
255 256 with transaction.commit_on_success():
256 257 if is_moderator:
257 258 form = ModeratorSettingsForm(request.POST,
258 259 error_class=PlainErrorList)
259 260 else:
260 261 form = SettingsForm(request.POST, error_class=PlainErrorList)
261 262
262 263 if form.is_valid():
263 264 selected_theme = form.cleaned_data['theme']
264 265
265 266 user.save_setting('theme', selected_theme)
266 267
267 268 if is_moderator:
268 269 moderate = form.cleaned_data['moderate']
269 270 user.save_setting(SETTING_MODERATE, moderate)
270 271
271 272 return redirect(settings)
272 273 else:
273 274 selected_theme = _get_theme(request)
274 275
275 276 if is_moderator:
276 277 form = ModeratorSettingsForm(initial={'theme': selected_theme,
277 278 'moderate': context['moderator']},
278 279 error_class=PlainErrorList)
279 280 else:
280 281 form = SettingsForm(initial={'theme': selected_theme},
281 282 error_class=PlainErrorList)
282 283
283 284 context['form'] = form
284 285
285 286 return render(request, 'boards/settings.html', context)
286 287
287 288
288 289 def all_tags(request):
289 290 """All tags list"""
290 291
291 292 context = _init_default_context(request)
292 293 context['all_tags'] = Tag.objects.get_not_empty_tags()
293 294
294 295 return render(request, 'boards/tags.html', context)
295 296
296 297
297 298 def jump_to_post(request, post_id):
298 299 """Determine thread in which the requested post is and open it's page"""
299 300
300 301 post = get_object_or_404(Post, id=post_id)
301 302
302 303 if not post.thread:
303 304 return redirect(thread, post_id=post.id)
304 305 else:
305 306 return redirect(reverse(thread, kwargs={'post_id': post.thread.id})
306 307 + '#' + str(post.id))
307 308
308 309
309 310 def authors(request):
310 311 """Show authors list"""
311 312
312 313 context = _init_default_context(request)
313 314 context['authors'] = boards.authors.authors
314 315
315 316 return render(request, 'boards/authors.html', context)
316 317
317 318
318 319 @transaction.commit_on_success
319 320 def delete(request, post_id):
320 321 """Delete post"""
321 322
322 323 user = _get_user(request)
323 324 post = get_object_or_404(Post, id=post_id)
324 325
325 326 if user.is_moderator():
326 327 # TODO Show confirmation page before deletion
327 328 Post.objects.delete_post(post)
328 329
329 330 if not post.thread:
330 331 return _redirect_to_next(request)
331 332 else:
332 333 return redirect(thread, post_id=post.thread.id)
333 334
334 335
335 336 @transaction.commit_on_success
336 337 def ban(request, post_id):
337 338 """Ban user"""
338 339
339 340 user = _get_user(request)
340 341 post = get_object_or_404(Post, id=post_id)
341 342
342 343 if user.is_moderator():
343 344 # TODO Show confirmation page before ban
344 345 ban, created = Ban.objects.get_or_create(ip=post.poster_ip)
345 346 if created:
346 347 ban.reason = 'Banned for post ' + str(post_id)
347 348 ban.save()
348 349
349 350 return _redirect_to_next(request)
350 351
351 352
352 353 def you_are_banned(request):
353 354 """Show the page that notifies that user is banned"""
354 355
355 356 context = _init_default_context(request)
356 357
357 358 ban = get_object_or_404(Ban, ip=utils.get_client_ip(request))
358 359 context['ban_reason'] = ban.reason
359 360 return render(request, 'boards/staticpages/banned.html', context)
360 361
361 362
362 363 def page_404(request):
363 364 """Show page 404 (not found error)"""
364 365
365 366 context = _init_default_context(request)
366 367 return render(request, 'boards/404.html', context)
367 368
368 369
369 370 @transaction.commit_on_success
370 371 def tag_subscribe(request, tag_name):
371 372 """Add tag to favorites"""
372 373
373 374 user = _get_user(request)
374 375 tag = get_object_or_404(Tag, name=tag_name)
375 376
376 377 if not tag in user.fav_tags.all():
377 378 user.add_tag(tag)
378 379
379 380 return _redirect_to_next(request)
380 381
381 382
382 383 @transaction.commit_on_success
383 384 def tag_unsubscribe(request, tag_name):
384 385 """Remove tag from favorites"""
385 386
386 387 user = _get_user(request)
387 388 tag = get_object_or_404(Tag, name=tag_name)
388 389
389 390 if tag in user.fav_tags.all():
390 391 user.remove_tag(tag)
391 392
392 393 return _redirect_to_next(request)
393 394
394 395
395 396 def static_page(request, name):
396 397 """Show a static page that needs only tags list and a CSS"""
397 398
398 399 context = _init_default_context(request)
399 400 return render(request, 'boards/staticpages/' + name + '.html', context)
400 401
401 402
402 403 def api_get_post(request, post_id):
403 404 """
404 405 Get the JSON of a post. This can be
405 406 used as and API for external clients.
406 407 """
407 408
408 409 post = get_object_or_404(Post, id=post_id)
409 410
410 411 json = serializers.serialize("json", [post], fields=(
411 412 "pub_time", "_text_rendered", "title", "text", "image",
412 413 "image_width", "image_height", "replies", "tags"
413 414 ))
414 415
415 416 return HttpResponse(content=json)
416 417
417 418
418 419 def api_get_threaddiff(request, thread_id, last_update_time):
419 420 """Get posts that were changed or added since time"""
420 421
421 422 thread = get_object_or_404(Post, id=thread_id)
422 423
423 filter_time = datetime.fromtimestamp(float(last_update_time) / 1000,
424 filter_time = datetime.fromtimestamp(float(last_update_time) / 1000000,
424 425 timezone.get_current_timezone())
425 426
426 427 json_data = {
427 428 'added': [],
428 429 'updated': [],
429 430 'last_update': None,
430 431 }
431 432 added_posts = Post.objects.filter(thread=thread, pub_time__gt=filter_time)
432 433 updated_posts = Post.objects.filter(thread=thread,
433 434 pub_time__lt=filter_time,
434 435 last_edit_time__gt=filter_time)
435 436 for post in added_posts:
436 437 json_data['added'].append(get_post(request, post.id).content.strip())
437 438 for post in updated_posts:
438 439 json_data['updated'].append(get_post(request, post.id).content.strip())
439 440 json_data['last_update'] = _datetime_to_epoch(thread.last_edit_time)
440 441
441 442 return HttpResponse(content=json.dumps(json_data))
442 443
443 444
444 445 def get_post(request, post_id):
445 446 """Get the html of a post. Used for popups."""
446 447
447 448 post = get_object_or_404(Post, id=post_id)
448 449 thread = post.thread
449 450
450 451 context = RequestContext(request)
451 452 context["post"] = post
452 453 context["can_bump"] = thread.can_bump()
453 454 if "truncated" in request.GET:
454 455 context["truncated"] = True
455 456
456 457 return render(request, 'boards/post.html', context)
457 458
458 459
459 460 def _get_theme(request, user=None):
460 461 """Get user's CSS theme"""
461 462
462 463 if not user:
463 464 user = _get_user(request)
464 465 theme = user.get_setting('theme')
465 466 if not theme:
466 467 theme = neboard.settings.DEFAULT_THEME
467 468
468 469 return theme
469 470
470 471
471 472 def _init_default_context(request):
472 473 """Create context with default values that are used in most views"""
473 474
474 475 context = RequestContext(request)
475 476
476 477 user = _get_user(request)
477 478 context['user'] = user
478 479 context['tags'] = user.get_sorted_fav_tags()
479 480
480 481 theme = _get_theme(request, user)
481 482 context['theme'] = theme
482 483 context['theme_css'] = 'css/' + theme + '/base_page.css'
483 484
484 485 # This shows the moderator panel
485 486 moderate = user.get_setting(SETTING_MODERATE)
486 487 if moderate == 'True':
487 488 context['moderator'] = user.is_moderator()
488 489 else:
489 490 context['moderator'] = False
490 491
491 492 return context
492 493
493 494
494 495 def _get_user(request):
495 496 """
496 497 Get current user from the session. If the user does not exist, create
497 498 a new one.
498 499 """
499 500
500 501 session = request.session
501 502 if not 'user_id' in session:
502 503 request.session.save()
503 504
504 505 md5 = hashlib.md5()
505 506 md5.update(session.session_key)
506 507 new_id = md5.hexdigest()
507 508
508 509 time_now = timezone.now()
509 510 user = User.objects.create(user_id=new_id, rank=RANK_USER,
510 511 registration_time=time_now)
511 512
512 513 session['user_id'] = user.id
513 514 else:
514 515 user = User.objects.get(id=session['user_id'])
515 516
516 517 return user
517 518
518 519
519 520 def _redirect_to_next(request):
520 521 """
521 522 If a 'next' parameter was specified, redirect to the next page. This is
522 523 used when the user is required to return to some page after the current
523 524 view has finished its work.
524 525 """
525 526
526 527 if 'next' in request.GET:
527 528 next_page = request.GET['next']
528 529 return HttpResponseRedirect(next_page)
529 530 else:
530 531 return redirect(index)
531 532
532 533
533 534 @transaction.commit_on_success
534 535 def _ban_current_user(request):
535 536 """Add current user to the IP ban list"""
536 537
537 538 ip = utils.get_client_ip(request)
538 539 ban, created = Ban.objects.get_or_create(ip=ip)
539 540 if created:
540 541 ban.can_read = False
541 542 ban.reason = BAN_REASON_SPAM
542 543 ban.save()
543 544
544 545
545 546 def _remove_invalid_links(text):
546 547 """
547 548 Replace invalid links in posts so that they won't be parsed.
548 549 Invalid links are links to non-existent posts
549 550 """
550 551
551 552 for reply_number in re.finditer(REGEX_REPLY, text):
552 553 post_id = reply_number.group(1)
553 554 post = Post.objects.filter(id=post_id)
554 555 if not post.exists():
555 556 text = string.replace(text, '>>' + id, id)
556 557
557 558 return text
558 559
559 560
560 561 def _datetime_to_epoch(datetime):
561 562 return int(time.mktime(timezone.localtime(
562 563 datetime,timezone.get_current_timezone()).timetuple())
563 * 1000 + datetime.microsecond) No newline at end of file
564 * 1000000 + datetime.microsecond) No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now