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