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