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