##// END OF EJS Templates
Updated paginator for long page lists. Removed old get_threads method in the post manager
neko259 -
r596:5657c06f default
parent child Browse files
Show More
@@ -0,0 +1,1 b''
1 __author__ = 'vurdalak'
@@ -0,0 +1,22 b''
1 from django.core.paginator import Paginator
2
3 __author__ = 'neko259'
4
5 PAGINATOR_LOOKAROUND_SIZE = 3
6
7
8 def get_paginator(*args, **kwargs):
9 return DividedPaginator(*args, **kwargs)
10
11
12 class DividedPaginator(Paginator):
13
14 lookaround_size = PAGINATOR_LOOKAROUND_SIZE
15 current_page = 0
16
17 def center_range(self):
18 index = self.page_range.index(self.current_page)
19
20 start = max(0, index - self.lookaround_size)
21 end = min(len(self.page_range), index + self.lookaround_size + 1)
22 return self.page_range[start:end] No newline at end of file
@@ -1,451 +1,431 b''
1 from datetime import datetime, timedelta, date
1 from datetime import datetime, timedelta, date
2 from datetime import time as dtime
2 from datetime import time as dtime
3 import os
3 import os
4 from random import random
4 from random import random
5 import time
5 import time
6 import math
6 import math
7 import re
7 import re
8 import hashlib
8 import hashlib
9
9
10 from django.core.cache import cache
10 from django.core.cache import cache
11 from django.core.paginator import Paginator
11 from django.core.paginator import Paginator
12 from django.core.urlresolvers import reverse
12 from django.core.urlresolvers import reverse
13
13
14 from django.db import models, transaction
14 from django.db import models, transaction
15 from django.http import Http404
15 from django.http import Http404
16 from django.utils import timezone
16 from django.utils import timezone
17 from markupfield.fields import MarkupField
17 from markupfield.fields import MarkupField
18
18
19 from neboard import settings
19 from neboard import settings
20 from boards import thumbs
20 from boards import thumbs
21
21
22 MAX_TITLE_LENGTH = 50
22 MAX_TITLE_LENGTH = 50
23
23
24 APP_LABEL_BOARDS = 'boards'
24 APP_LABEL_BOARDS = 'boards'
25
25
26 CACHE_KEY_PPD = 'ppd'
26 CACHE_KEY_PPD = 'ppd'
27 CACHE_KEY_POST_URL = 'post_url'
27 CACHE_KEY_POST_URL = 'post_url'
28 CACHE_KEY_OPENING_POST = 'opening_post'
28 CACHE_KEY_OPENING_POST = 'opening_post'
29
29
30 POSTS_PER_DAY_RANGE = range(7)
30 POSTS_PER_DAY_RANGE = range(7)
31
31
32 BAN_REASON_AUTO = 'Auto'
32 BAN_REASON_AUTO = 'Auto'
33
33
34 IMAGE_THUMB_SIZE = (200, 150)
34 IMAGE_THUMB_SIZE = (200, 150)
35
35
36 TITLE_MAX_LENGTH = 50
36 TITLE_MAX_LENGTH = 50
37
37
38 DEFAULT_MARKUP_TYPE = 'markdown'
38 DEFAULT_MARKUP_TYPE = 'markdown'
39
39
40 NO_PARENT = -1
40 NO_PARENT = -1
41 NO_IP = '0.0.0.0'
41 NO_IP = '0.0.0.0'
42 UNKNOWN_UA = ''
42 UNKNOWN_UA = ''
43 ALL_PAGES = -1
43 ALL_PAGES = -1
44 IMAGES_DIRECTORY = 'images/'
44 IMAGES_DIRECTORY = 'images/'
45 FILE_EXTENSION_DELIMITER = '.'
45 FILE_EXTENSION_DELIMITER = '.'
46
46
47 SETTING_MODERATE = "moderate"
47 SETTING_MODERATE = "moderate"
48
48
49 REGEX_REPLY = re.compile('>>(\d+)')
49 REGEX_REPLY = re.compile('>>(\d+)')
50
50
51
51
52 class PostManager(models.Manager):
52 class PostManager(models.Manager):
53
53
54 def create_post(self, title, text, image=None, thread=None,
54 def create_post(self, title, text, image=None, thread=None,
55 ip=NO_IP, tags=None, user=None):
55 ip=NO_IP, tags=None, user=None):
56 """
56 """
57 Create new post
57 Create new post
58 """
58 """
59
59
60 posting_time = timezone.now()
60 posting_time = timezone.now()
61 if not thread:
61 if not thread:
62 thread = Thread.objects.create(bump_time=posting_time,
62 thread = Thread.objects.create(bump_time=posting_time,
63 last_edit_time=posting_time)
63 last_edit_time=posting_time)
64 else:
64 else:
65 thread.bump()
65 thread.bump()
66 thread.last_edit_time = posting_time
66 thread.last_edit_time = posting_time
67 thread.save()
67 thread.save()
68
68
69 post = self.create(title=title,
69 post = self.create(title=title,
70 text=text,
70 text=text,
71 pub_time=posting_time,
71 pub_time=posting_time,
72 thread_new=thread,
72 thread_new=thread,
73 image=image,
73 image=image,
74 poster_ip=ip,
74 poster_ip=ip,
75 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
75 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
76 # last!
76 # last!
77 last_edit_time=posting_time,
77 last_edit_time=posting_time,
78 user=user)
78 user=user)
79
79
80 thread.replies.add(post)
80 thread.replies.add(post)
81 if tags:
81 if tags:
82 linked_tags = []
82 linked_tags = []
83 for tag in tags:
83 for tag in tags:
84 tag_linked_tags = tag.get_linked_tags()
84 tag_linked_tags = tag.get_linked_tags()
85 if len(tag_linked_tags) > 0:
85 if len(tag_linked_tags) > 0:
86 linked_tags.extend(tag_linked_tags)
86 linked_tags.extend(tag_linked_tags)
87
87
88 tags.extend(linked_tags)
88 tags.extend(linked_tags)
89 map(thread.add_tag, tags)
89 map(thread.add_tag, tags)
90
90
91 self._delete_old_threads()
91 self._delete_old_threads()
92 self.connect_replies(post)
92 self.connect_replies(post)
93
93
94 return post
94 return post
95
95
96 def delete_post(self, post):
96 def delete_post(self, post):
97 """
97 """
98 Delete post and update or delete its thread
98 Delete post and update or delete its thread
99 """
99 """
100
100
101 thread = post.thread_new
101 thread = post.thread_new
102
102
103 if post.is_opening():
103 if post.is_opening():
104 thread.delete_with_posts()
104 thread.delete_with_posts()
105 else:
105 else:
106 thread.last_edit_time = timezone.now()
106 thread.last_edit_time = timezone.now()
107 thread.save()
107 thread.save()
108
108
109 post.delete()
109 post.delete()
110
110
111 def delete_posts_by_ip(self, ip):
111 def delete_posts_by_ip(self, ip):
112 """
112 """
113 Delete all posts of the author with same IP
113 Delete all posts of the author with same IP
114 """
114 """
115
115
116 posts = self.filter(poster_ip=ip)
116 posts = self.filter(poster_ip=ip)
117 map(self.delete_post, posts)
117 map(self.delete_post, posts)
118
118
119 # TODO This method may not be needed any more, because django's paginator
120 # is used
121 def get_threads(self, tag=None, page=ALL_PAGES,
122 order_by='-bump_time', archived=False):
123 if tag:
124 threads = tag.threads
125
126 if not threads.exists():
127 raise Http404
128 else:
129 threads = Thread.objects.all()
130
131 threads = threads.filter(archived=archived).order_by(order_by)
132
133 if page != ALL_PAGES:
134 threads = Paginator(threads, settings.THREADS_PER_PAGE).page(
135 page).object_list
136
137 return threads
138
139 # TODO Move this method to thread manager
119 # TODO Move this method to thread manager
140 def _delete_old_threads(self):
120 def _delete_old_threads(self):
141 """
121 """
142 Preserves maximum thread count. If there are too many threads,
122 Preserves maximum thread count. If there are too many threads,
143 archive the old ones.
123 archive the old ones.
144 """
124 """
145
125
146 threads = self.get_threads()
126 threads = Thread.objects.filter(archived=False)
147 thread_count = threads.count()
127 thread_count = threads.count()
148
128
149 if thread_count > settings.MAX_THREAD_COUNT:
129 if thread_count > settings.MAX_THREAD_COUNT:
150 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
130 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
151 old_threads = threads[thread_count - num_threads_to_delete:]
131 old_threads = threads[thread_count - num_threads_to_delete:]
152
132
153 for thread in old_threads:
133 for thread in old_threads:
154 thread.archived = True
134 thread.archived = True
155 thread.last_edit_time = timezone.now()
135 thread.last_edit_time = timezone.now()
156 thread.save()
136 thread.save()
157
137
158 def connect_replies(self, post):
138 def connect_replies(self, post):
159 """
139 """
160 Connect replies to a post to show them as a reflink map
140 Connect replies to a post to show them as a reflink map
161 """
141 """
162
142
163 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
143 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
164 post_id = reply_number.group(1)
144 post_id = reply_number.group(1)
165 ref_post = self.filter(id=post_id)
145 ref_post = self.filter(id=post_id)
166 if ref_post.count() > 0:
146 if ref_post.count() > 0:
167 referenced_post = ref_post[0]
147 referenced_post = ref_post[0]
168 referenced_post.referenced_posts.add(post)
148 referenced_post.referenced_posts.add(post)
169 referenced_post.last_edit_time = post.pub_time
149 referenced_post.last_edit_time = post.pub_time
170 referenced_post.save()
150 referenced_post.save()
171
151
172 referenced_thread = referenced_post.thread_new
152 referenced_thread = referenced_post.thread_new
173 referenced_thread.last_edit_time = post.pub_time
153 referenced_thread.last_edit_time = post.pub_time
174 referenced_thread.save()
154 referenced_thread.save()
175
155
176 def get_posts_per_day(self):
156 def get_posts_per_day(self):
177 """
157 """
178 Get average count of posts per day for the last 7 days
158 Get average count of posts per day for the last 7 days
179 """
159 """
180
160
181 today = date.today()
161 today = date.today()
182 ppd = cache.get(CACHE_KEY_PPD + str(today))
162 ppd = cache.get(CACHE_KEY_PPD + str(today))
183 if ppd:
163 if ppd:
184 return ppd
164 return ppd
185
165
186 posts_per_days = []
166 posts_per_days = []
187 for i in POSTS_PER_DAY_RANGE:
167 for i in POSTS_PER_DAY_RANGE:
188 day_end = today - timedelta(i + 1)
168 day_end = today - timedelta(i + 1)
189 day_start = today - timedelta(i + 2)
169 day_start = today - timedelta(i + 2)
190
170
191 day_time_start = timezone.make_aware(datetime.combine(day_start,
171 day_time_start = timezone.make_aware(datetime.combine(day_start,
192 dtime()), timezone.get_current_timezone())
172 dtime()), timezone.get_current_timezone())
193 day_time_end = timezone.make_aware(datetime.combine(day_end,
173 day_time_end = timezone.make_aware(datetime.combine(day_end,
194 dtime()), timezone.get_current_timezone())
174 dtime()), timezone.get_current_timezone())
195
175
196 posts_per_days.append(float(self.filter(
176 posts_per_days.append(float(self.filter(
197 pub_time__lte=day_time_end,
177 pub_time__lte=day_time_end,
198 pub_time__gte=day_time_start).count()))
178 pub_time__gte=day_time_start).count()))
199
179
200 ppd = (sum(posts_per_day for posts_per_day in posts_per_days) /
180 ppd = (sum(posts_per_day for posts_per_day in posts_per_days) /
201 len(posts_per_days))
181 len(posts_per_days))
202 cache.set(CACHE_KEY_PPD + str(today), ppd)
182 cache.set(CACHE_KEY_PPD + str(today), ppd)
203 return ppd
183 return ppd
204
184
205
185
206 class Post(models.Model):
186 class Post(models.Model):
207 """A post is a message."""
187 """A post is a message."""
208
188
209 objects = PostManager()
189 objects = PostManager()
210
190
211 class Meta:
191 class Meta:
212 app_label = APP_LABEL_BOARDS
192 app_label = APP_LABEL_BOARDS
213
193
214 # TODO Save original file name to some field
194 # TODO Save original file name to some field
215 def _update_image_filename(self, filename):
195 def _update_image_filename(self, filename):
216 """Get unique image filename"""
196 """Get unique image filename"""
217
197
218 path = IMAGES_DIRECTORY
198 path = IMAGES_DIRECTORY
219 new_name = str(int(time.mktime(time.gmtime())))
199 new_name = str(int(time.mktime(time.gmtime())))
220 new_name += str(int(random() * 1000))
200 new_name += str(int(random() * 1000))
221 new_name += FILE_EXTENSION_DELIMITER
201 new_name += FILE_EXTENSION_DELIMITER
222 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
202 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
223
203
224 return os.path.join(path, new_name)
204 return os.path.join(path, new_name)
225
205
226 title = models.CharField(max_length=TITLE_MAX_LENGTH)
206 title = models.CharField(max_length=TITLE_MAX_LENGTH)
227 pub_time = models.DateTimeField()
207 pub_time = models.DateTimeField()
228 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
208 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
229 escape_html=False)
209 escape_html=False)
230
210
231 image_width = models.IntegerField(default=0)
211 image_width = models.IntegerField(default=0)
232 image_height = models.IntegerField(default=0)
212 image_height = models.IntegerField(default=0)
233
213
234 image_pre_width = models.IntegerField(default=0)
214 image_pre_width = models.IntegerField(default=0)
235 image_pre_height = models.IntegerField(default=0)
215 image_pre_height = models.IntegerField(default=0)
236
216
237 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
217 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
238 blank=True, sizes=(IMAGE_THUMB_SIZE,),
218 blank=True, sizes=(IMAGE_THUMB_SIZE,),
239 width_field='image_width',
219 width_field='image_width',
240 height_field='image_height',
220 height_field='image_height',
241 preview_width_field='image_pre_width',
221 preview_width_field='image_pre_width',
242 preview_height_field='image_pre_height')
222 preview_height_field='image_pre_height')
243 image_hash = models.CharField(max_length=36)
223 image_hash = models.CharField(max_length=36)
244
224
245 poster_ip = models.GenericIPAddressField()
225 poster_ip = models.GenericIPAddressField()
246 poster_user_agent = models.TextField()
226 poster_user_agent = models.TextField()
247
227
248 thread = models.ForeignKey('Post', null=True, default=None)
228 thread = models.ForeignKey('Post', null=True, default=None)
249 thread_new = models.ForeignKey('Thread', null=True, default=None)
229 thread_new = models.ForeignKey('Thread', null=True, default=None)
250 last_edit_time = models.DateTimeField()
230 last_edit_time = models.DateTimeField()
251 user = models.ForeignKey('User', null=True, default=None)
231 user = models.ForeignKey('User', null=True, default=None)
252
232
253 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
233 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
254 null=True,
234 null=True,
255 blank=True, related_name='rfp+')
235 blank=True, related_name='rfp+')
256
236
257 def __unicode__(self):
237 def __unicode__(self):
258 return '#' + str(self.id) + ' ' + self.title + ' (' + \
238 return '#' + str(self.id) + ' ' + self.title + ' (' + \
259 self.text.raw[:50] + ')'
239 self.text.raw[:50] + ')'
260
240
261 def get_title(self):
241 def get_title(self):
262 title = self.title
242 title = self.title
263 if len(title) == 0:
243 if len(title) == 0:
264 title = self.text.rendered
244 title = self.text.rendered
265
245
266 return title
246 return title
267
247
268 def get_sorted_referenced_posts(self):
248 def get_sorted_referenced_posts(self):
269 return self.referenced_posts.order_by('id')
249 return self.referenced_posts.order_by('id')
270
250
271 def is_referenced(self):
251 def is_referenced(self):
272 return self.referenced_posts.all().exists()
252 return self.referenced_posts.all().exists()
273
253
274 def is_opening(self):
254 def is_opening(self):
275 return self.thread_new.get_opening_post() == self
255 return self.thread_new.get_opening_post() == self
276
256
277 def save(self, *args, **kwargs):
257 def save(self, *args, **kwargs):
278 """
258 """
279 Save the model and compute the image hash
259 Save the model and compute the image hash
280 """
260 """
281
261
282 if not self.pk and self.image:
262 if not self.pk and self.image:
283 md5 = hashlib.md5()
263 md5 = hashlib.md5()
284 for chunk in self.image.chunks():
264 for chunk in self.image.chunks():
285 md5.update(chunk)
265 md5.update(chunk)
286 self.image_hash = md5.hexdigest()
266 self.image_hash = md5.hexdigest()
287 super(Post, self).save(*args, **kwargs)
267 super(Post, self).save(*args, **kwargs)
288
268
289 @transaction.atomic
269 @transaction.atomic
290 def add_tag(self, tag):
270 def add_tag(self, tag):
291 edit_time = timezone.now()
271 edit_time = timezone.now()
292
272
293 thread = self.thread_new
273 thread = self.thread_new
294 thread.add_tag(tag)
274 thread.add_tag(tag)
295 self.last_edit_time = edit_time
275 self.last_edit_time = edit_time
296 self.save()
276 self.save()
297
277
298 thread.last_edit_time = edit_time
278 thread.last_edit_time = edit_time
299 thread.save()
279 thread.save()
300
280
301 @transaction.atomic
281 @transaction.atomic
302 def remove_tag(self, tag):
282 def remove_tag(self, tag):
303 edit_time = timezone.now()
283 edit_time = timezone.now()
304
284
305 thread = self.thread_new
285 thread = self.thread_new
306 thread.remove_tag(tag)
286 thread.remove_tag(tag)
307 self.last_edit_time = edit_time
287 self.last_edit_time = edit_time
308 self.save()
288 self.save()
309
289
310 thread.last_edit_time = edit_time
290 thread.last_edit_time = edit_time
311 thread.save()
291 thread.save()
312
292
313 def get_url(self):
293 def get_url(self):
314 """
294 """
315 Get full url to this post
295 Get full url to this post
316 """
296 """
317
297
318 cache_key = CACHE_KEY_POST_URL + str(self.id)
298 cache_key = CACHE_KEY_POST_URL + str(self.id)
319 link = cache.get(cache_key)
299 link = cache.get(cache_key)
320
300
321 if not link:
301 if not link:
322 opening_post = self.thread_new.get_opening_post()
302 opening_post = self.thread_new.get_opening_post()
323 if self != opening_post:
303 if self != opening_post:
324 link = reverse('thread',
304 link = reverse('thread',
325 kwargs={'post_id': opening_post.id}) + '#' + str(
305 kwargs={'post_id': opening_post.id}) + '#' + str(
326 self.id)
306 self.id)
327 else:
307 else:
328 link = reverse('thread', kwargs={'post_id': self.id})
308 link = reverse('thread', kwargs={'post_id': self.id})
329
309
330 cache.set(cache_key, link)
310 cache.set(cache_key, link)
331
311
332 return link
312 return link
333
313
334
314
335 class Thread(models.Model):
315 class Thread(models.Model):
336
316
337 class Meta:
317 class Meta:
338 app_label = APP_LABEL_BOARDS
318 app_label = APP_LABEL_BOARDS
339
319
340 tags = models.ManyToManyField('Tag')
320 tags = models.ManyToManyField('Tag')
341 bump_time = models.DateTimeField()
321 bump_time = models.DateTimeField()
342 last_edit_time = models.DateTimeField()
322 last_edit_time = models.DateTimeField()
343 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
323 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
344 blank=True, related_name='tre+')
324 blank=True, related_name='tre+')
345 archived = models.BooleanField(default=False)
325 archived = models.BooleanField(default=False)
346
326
347 def get_tags(self):
327 def get_tags(self):
348 """
328 """
349 Get a sorted tag list
329 Get a sorted tag list
350 """
330 """
351
331
352 return self.tags.order_by('name')
332 return self.tags.order_by('name')
353
333
354 def bump(self):
334 def bump(self):
355 """
335 """
356 Bump (move to up) thread
336 Bump (move to up) thread
357 """
337 """
358
338
359 if self.can_bump():
339 if self.can_bump():
360 self.bump_time = timezone.now()
340 self.bump_time = timezone.now()
361
341
362 def get_reply_count(self):
342 def get_reply_count(self):
363 return self.replies.count()
343 return self.replies.count()
364
344
365 def get_images_count(self):
345 def get_images_count(self):
366 return self.replies.filter(image_width__gt=0).count()
346 return self.replies.filter(image_width__gt=0).count()
367
347
368 def can_bump(self):
348 def can_bump(self):
369 """
349 """
370 Check if the thread can be bumped by replying
350 Check if the thread can be bumped by replying
371 """
351 """
372
352
373 if self.archived:
353 if self.archived:
374 return False
354 return False
375
355
376 post_count = self.get_reply_count()
356 post_count = self.get_reply_count()
377
357
378 return post_count < settings.MAX_POSTS_PER_THREAD
358 return post_count < settings.MAX_POSTS_PER_THREAD
379
359
380 def delete_with_posts(self):
360 def delete_with_posts(self):
381 """
361 """
382 Completely delete thread and all its posts
362 Completely delete thread and all its posts
383 """
363 """
384
364
385 if self.replies.count() > 0:
365 if self.replies.count() > 0:
386 self.replies.all().delete()
366 self.replies.all().delete()
387
367
388 self.delete()
368 self.delete()
389
369
390 def get_last_replies(self):
370 def get_last_replies(self):
391 """
371 """
392 Get last replies, not including opening post
372 Get last replies, not including opening post
393 """
373 """
394
374
395 if settings.LAST_REPLIES_COUNT > 0:
375 if settings.LAST_REPLIES_COUNT > 0:
396 reply_count = self.get_reply_count()
376 reply_count = self.get_reply_count()
397
377
398 if reply_count > 0:
378 if reply_count > 0:
399 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
379 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
400 reply_count - 1)
380 reply_count - 1)
401 last_replies = self.replies.all().order_by('pub_time')[
381 last_replies = self.replies.all().order_by('pub_time')[
402 reply_count - reply_count_to_show:]
382 reply_count - reply_count_to_show:]
403
383
404 return last_replies
384 return last_replies
405
385
406 def get_skipped_replies_count(self):
386 def get_skipped_replies_count(self):
407 last_replies = self.get_last_replies()
387 last_replies = self.get_last_replies()
408 return self.get_reply_count() - len(last_replies) - 1
388 return self.get_reply_count() - len(last_replies) - 1
409
389
410 def get_replies(self):
390 def get_replies(self):
411 """
391 """
412 Get sorted thread posts
392 Get sorted thread posts
413 """
393 """
414
394
415 return self.replies.all().order_by('pub_time')
395 return self.replies.all().order_by('pub_time')
416
396
417 def add_tag(self, tag):
397 def add_tag(self, tag):
418 """
398 """
419 Connect thread to a tag and tag to a thread
399 Connect thread to a tag and tag to a thread
420 """
400 """
421
401
422 self.tags.add(tag)
402 self.tags.add(tag)
423 tag.threads.add(self)
403 tag.threads.add(self)
424
404
425 def remove_tag(self, tag):
405 def remove_tag(self, tag):
426 self.tags.remove(tag)
406 self.tags.remove(tag)
427 tag.threads.remove(self)
407 tag.threads.remove(self)
428
408
429 def get_opening_post(self):
409 def get_opening_post(self):
430 """
410 """
431 Get first post of the thread
411 Get first post of the thread
432 """
412 """
433
413
434 # cache_key = CACHE_KEY_OPENING_POST + str(self.id)
414 # cache_key = CACHE_KEY_OPENING_POST + str(self.id)
435 # opening_post = cache.get(cache_key)
415 # opening_post = cache.get(cache_key)
436 # if not opening_post:
416 # if not opening_post:
437 opening_post = self.get_replies()[0]
417 opening_post = self.get_replies()[0]
438 # cache.set(cache_key, opening_post)
418 # cache.set(cache_key, opening_post)
439
419
440 return opening_post
420 return opening_post
441
421
442 def __unicode__(self):
422 def __unicode__(self):
443 return str(self.id)
423 return str(self.id)
444
424
445 def get_pub_time(self):
425 def get_pub_time(self):
446 """
426 """
447 Thread does not have its own pub time, so we need to get it from
427 Thread does not have its own pub time, so we need to get it from
448 the opening post
428 the opening post
449 """
429 """
450
430
451 return self.get_opening_post().pub_time
431 return self.get_opening_post().pub_time
@@ -1,79 +1,79 b''
1 from django.contrib.syndication.views import Feed
1 from django.contrib.syndication.views import Feed
2 from django.core.urlresolvers import reverse
2 from django.core.urlresolvers import reverse
3 from django.shortcuts import get_object_or_404
3 from django.shortcuts import get_object_or_404
4 from boards.models import Post, Tag
4 from boards.models import Post, Tag, Thread
5 from neboard import settings
5 from neboard import settings
6
6
7 __author__ = 'neko259'
7 __author__ = 'neko259'
8
8
9
9
10 # TODO Make tests for all of these
10 # TODO Make tests for all of these
11 class AllThreadsFeed(Feed):
11 class AllThreadsFeed(Feed):
12
12
13 title = settings.SITE_NAME + ' - All threads'
13 title = settings.SITE_NAME + ' - All threads'
14 link = '/'
14 link = '/'
15 description_template = 'boards/rss/post.html'
15 description_template = 'boards/rss/post.html'
16
16
17 def items(self):
17 def items(self):
18 return Post.objects.get_threads(order_by='-id')
18 return Thread.objects.filter(archived=False).order_by('-id')
19
19
20 def item_title(self, item):
20 def item_title(self, item):
21 return item.get_opening_post().title
21 return item.get_opening_post().title
22
22
23 def item_link(self, item):
23 def item_link(self, item):
24 return reverse('thread', args={item.get_opening_post().id})
24 return reverse('thread', args={item.get_opening_post().id})
25
25
26 def item_pubdate(self, item):
26 def item_pubdate(self, item):
27 return item.get_pub_time()
27 return item.get_pub_time()
28
28
29
29
30 class TagThreadsFeed(Feed):
30 class TagThreadsFeed(Feed):
31
31
32 link = '/'
32 link = '/'
33 description_template = 'boards/rss/post.html'
33 description_template = 'boards/rss/post.html'
34
34
35 def items(self, obj):
35 def items(self, obj):
36 return Post.objects.get_threads(tag=obj, order_by='-id')
36 return obj.threads.filter(archived=False).order_by('-id')
37
37
38 def get_object(self, request, tag_name):
38 def get_object(self, request, tag_name):
39 return get_object_or_404(Tag, name=tag_name)
39 return get_object_or_404(Tag, name=tag_name)
40
40
41 def item_title(self, item):
41 def item_title(self, item):
42 return item.get_opening_post().title
42 return item.get_opening_post().title
43
43
44 def item_link(self, item):
44 def item_link(self, item):
45 return reverse('thread', args={item.get_opening_post().id})
45 return reverse('thread', args={item.get_opening_post().id})
46
46
47 def item_pubdate(self, item):
47 def item_pubdate(self, item):
48 return item.get_pub_time()
48 return item.get_pub_time()
49
49
50 def title(self, obj):
50 def title(self, obj):
51 return obj.name
51 return obj.name
52
52
53
53
54 class ThreadPostsFeed(Feed):
54 class ThreadPostsFeed(Feed):
55
55
56 link = '/'
56 link = '/'
57 description_template = 'boards/rss/post.html'
57 description_template = 'boards/rss/post.html'
58
58
59 def items(self, obj):
59 def items(self, obj):
60 return obj.thread_new.get_replies()
60 return obj.thread_new.get_replies()
61
61
62 def get_object(self, request, post_id):
62 def get_object(self, request, post_id):
63 return get_object_or_404(Post, id=post_id)
63 return get_object_or_404(Post, id=post_id)
64
64
65 def item_title(self, item):
65 def item_title(self, item):
66 return item.title
66 return item.title
67
67
68 def item_link(self, item):
68 def item_link(self, item):
69 if not item.is_opening():
69 if not item.is_opening():
70 return reverse('thread', args={item.thread_new.get_opening_post()
70 return reverse('thread', args={item.thread_new.get_opening_post()
71 .id}) + "#" + str(item.id)
71 .id}) + "#" + str(item.id)
72 else:
72 else:
73 return reverse('thread', args={item.id})
73 return reverse('thread', args={item.id})
74
74
75 def item_pubdate(self, item):
75 def item_pubdate(self, item):
76 return item.pub_time
76 return item.pub_time
77
77
78 def title(self, obj):
78 def title(self, obj):
79 return obj.title
79 return obj.title
@@ -1,163 +1,182 b''
1 {% extends "boards/base.html" %}
1 {% extends "boards/base.html" %}
2
2
3 {% load i18n %}
3 {% load i18n %}
4 {% load cache %}
4 {% load cache %}
5 {% load board %}
5 {% load board %}
6 {% load static %}
6 {% load static %}
7
7
8 {% block head %}
8 {% block head %}
9 {% if tag %}
9 {% if tag %}
10 <title>{{ tag.name }} - {{ site_name }}</title>
10 <title>{{ tag.name }} - {{ site_name }}</title>
11 {% else %}
11 {% else %}
12 <title>{{ site_name }}</title>
12 <title>{{ site_name }}</title>
13 {% endif %}
13 {% endif %}
14
14
15 {% if current_page.has_previous %}
15 {% if current_page.has_previous %}
16 <link rel="prev" href="
16 <link rel="prev" href="
17 {% if tag %}
17 {% if tag %}
18 {% url "tag" tag_name=tag page=current_page.previous_page_number %}
18 {% url "tag" tag_name=tag page=current_page.previous_page_number %}
19 {% elif archived %}
19 {% elif archived %}
20 {% url "archive" page=current_page.previous_page_number %}
20 {% url "archive" page=current_page.previous_page_number %}
21 {% else %}
21 {% else %}
22 {% url "index" page=current_page.previous_page_number %}
22 {% url "index" page=current_page.previous_page_number %}
23 {% endif %}
23 {% endif %}
24 " />
24 " />
25 {% endif %}
25 {% endif %}
26 {% if current_page.has_next %}
26 {% if current_page.has_next %}
27 <link rel="next" href="
27 <link rel="next" href="
28 {% if tag %}
28 {% if tag %}
29 {% url "tag" tag_name=tag page=current_page.next_page_number %}
29 {% url "tag" tag_name=tag page=current_page.next_page_number %}
30 {% elif archived %}
30 {% elif archived %}
31 {% url "archive" page=current_page.next_page_number %}
31 {% url "archive" page=current_page.next_page_number %}
32 {% else %}
32 {% else %}
33 {% url "index" page=current_page.next_page_number %}
33 {% url "index" page=current_page.next_page_number %}
34 {% endif %}
34 {% endif %}
35 " />
35 " />
36 {% endif %}
36 {% endif %}
37
37
38 {% endblock %}
38 {% endblock %}
39
39
40 {% block content %}
40 {% block content %}
41
41
42 {% get_current_language as LANGUAGE_CODE %}
42 {% get_current_language as LANGUAGE_CODE %}
43
43
44 {% if tag %}
44 {% if tag %}
45 <div class="tag_info">
45 <div class="tag_info">
46 <h2>
46 <h2>
47 {% if tag in user.fav_tags.all %}
47 {% if tag in user.fav_tags.all %}
48 <a href="{% url 'tag' tag.name %}?method=unsubscribe&next={{ request.path }}"
48 <a href="{% url 'tag' tag.name %}?method=unsubscribe&next={{ request.path }}"
49 class="fav">β˜…</a>
49 class="fav">β˜…</a>
50 {% else %}
50 {% else %}
51 <a href="{% url 'tag' tag.name %}?method=subscribe&next={{ request.path }}"
51 <a href="{% url 'tag' tag.name %}?method=subscribe&next={{ request.path }}"
52 class="not_fav">β˜…</a>
52 class="not_fav">β˜…</a>
53 {% endif %}
53 {% endif %}
54 #{{ tag.name }}
54 #{{ tag.name }}
55 </h2>
55 </h2>
56 </div>
56 </div>
57 {% endif %}
57 {% endif %}
58
58
59 {% if threads %}
59 {% if threads %}
60 {% if current_page.has_previous %}
60 {% if current_page.has_previous %}
61 <div class="page_link">
61 <div class="page_link">
62 <a href="
62 <a href="
63 {% if tag %}
63 {% if tag %}
64 {% url "tag" tag_name=tag page=current_page.previous_page_number %}
64 {% url "tag" tag_name=tag page=current_page.previous_page_number %}
65 {% elif archived %}
65 {% elif archived %}
66 {% url "archive" page=current_page.previous_page_number %}
66 {% url "archive" page=current_page.previous_page_number %}
67 {% else %}
67 {% else %}
68 {% url "index" page=current_page.previous_page_number %}
68 {% url "index" page=current_page.previous_page_number %}
69 {% endif %}
69 {% endif %}
70 ">{% trans "Previous page" %}</a>
70 ">{% trans "Previous page" %}</a>
71 </div>
71 </div>
72 {% endif %}
72 {% endif %}
73
73
74 {% for thread in threads %}
74 {% for thread in threads %}
75 {% cache 600 thread_short thread.id thread.last_edit_time moderator LANGUAGE_CODE %}
75 {% cache 600 thread_short thread.id thread.last_edit_time moderator LANGUAGE_CODE %}
76 <div class="thread">
76 <div class="thread">
77 {% with can_bump=thread.can_bump %}
77 {% with can_bump=thread.can_bump %}
78 {% post_view thread.get_opening_post moderator is_opening=True thread=thread can_bump=can_bump truncated=True need_open_link=True %}
78 {% post_view thread.get_opening_post moderator is_opening=True thread=thread can_bump=can_bump truncated=True need_open_link=True %}
79 {% if not thread.archived %}
79 {% if not thread.archived %}
80 {% if thread.get_last_replies.exists %}
80 {% if thread.get_last_replies.exists %}
81 {% if thread.get_skipped_replies_count %}
81 {% if thread.get_skipped_replies_count %}
82 <div class="skipped_replies">
82 <div class="skipped_replies">
83 <a href="{% url 'thread' thread.get_opening_post.id %}">
83 <a href="{% url 'thread' thread.get_opening_post.id %}">
84 {% blocktrans with count=thread.get_skipped_replies_count %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %}
84 {% blocktrans with count=thread.get_skipped_replies_count %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %}
85 </a>
85 </a>
86 </div>
86 </div>
87 {% endif %}
87 {% endif %}
88 <div class="last-replies">
88 <div class="last-replies">
89 {% for post in thread.get_last_replies %}
89 {% for post in thread.get_last_replies %}
90 {% post_view post moderator=moderator is_opening=False thread=thread can_bump=can_bump truncated=True %}
90 {% post_view post moderator=moderator is_opening=False thread=thread can_bump=can_bump truncated=True %}
91 {% endfor %}
91 {% endfor %}
92 </div>
92 </div>
93 {% endif %}
93 {% endif %}
94 {% endif %}
94 {% endif %}
95 {% endwith %}
95 {% endwith %}
96 </div>
96 </div>
97 {% endcache %}
97 {% endcache %}
98 {% endfor %}
98 {% endfor %}
99
99
100 {% if current_page.has_next %}
100 {% if current_page.has_next %}
101 <div class="page_link">
101 <div class="page_link">
102 <a href="
102 <a href="
103 {% if tag %}
103 {% if tag %}
104 {% url "tag" tag_name=tag page=current_page.next_page_number %}
104 {% url "tag" tag_name=tag page=current_page.next_page_number %}
105 {% elif archived %}
105 {% elif archived %}
106 {% url "archive" page=current_page.next_page_number %}
106 {% url "archive" page=current_page.next_page_number %}
107 {% else %}
107 {% else %}
108 {% url "index" page=current_page.next_page_number %}
108 {% url "index" page=current_page.next_page_number %}
109 {% endif %}
109 {% endif %}
110 ">{% trans "Next page" %}</a>
110 ">{% trans "Next page" %}</a>
111 </div>
111 </div>
112 {% endif %}
112 {% endif %}
113 {% else %}
113 {% else %}
114 <div class="post">
114 <div class="post">
115 {% trans 'No threads exist. Create the first one!' %}</div>
115 {% trans 'No threads exist. Create the first one!' %}</div>
116 {% endif %}
116 {% endif %}
117
117
118 <div class="post-form-w">
118 <div class="post-form-w">
119 <script src="{% static 'js/panel.js' %}"></script>
119 <script src="{% static 'js/panel.js' %}"></script>
120 <div class="post-form">
120 <div class="post-form">
121 <div class="form-title">{% trans "Create new thread" %}</div>
121 <div class="form-title">{% trans "Create new thread" %}</div>
122 <form enctype="multipart/form-data" method="post">{% csrf_token %}
122 <form enctype="multipart/form-data" method="post">{% csrf_token %}
123 {{ form.as_div }}
123 {{ form.as_div }}
124 <div class="form-submit">
124 <div class="form-submit">
125 <input type="submit" value="{% trans "Post" %}"/>
125 <input type="submit" value="{% trans "Post" %}"/>
126 </div>
126 </div>
127 </form>
127 </form>
128 <div>
128 <div>
129 {% trans 'Tags must be delimited by spaces. Text or image is required.' %}
129 {% trans 'Tags must be delimited by spaces. Text or image is required.' %}
130 </div>
130 </div>
131 <div><a href="{% url "staticpage" name="help" %}">
131 <div><a href="{% url "staticpage" name="help" %}">
132 {% trans 'Text syntax' %}</a></div>
132 {% trans 'Text syntax' %}</a></div>
133 </div>
133 </div>
134 </div>
134 </div>
135
135
136 {% endblock %}
136 {% endblock %}
137
137
138 {% block metapanel %}
138 {% block metapanel %}
139
139
140 <span class="metapanel">
140 <span class="metapanel">
141 <b><a href="{% url "authors" %}">{{ site_name }}</a> {{ version }}</b>
141 <b><a href="{% url "authors" %}">{{ site_name }}</a> {{ version }}</b>
142 {% trans "Pages:" %}[
142 {% trans "Pages:" %}
143 {% for page in paginator.page_range %}
143 <a href="
144 {% if tag %}
145 {% url "tag" tag_name=tag page=paginator.page_range|first %}
146 {% elif archived %}
147 {% url "archive" page=paginator.page_range|first %}
148 {% else %}
149 {% url "index" page=paginator.page_range|first %}
150 {% endif %}
151 ">&lt;&lt;</a>
152 [
153 {% for page in paginator.center_range %}
144 <a
154 <a
145 {% ifequal page current_page.number %}
155 {% ifequal page current_page.number %}
146 class="current_page"
156 class="current_page"
147 {% endifequal %}
157 {% endifequal %}
148 href="
158 href="
149 {% if tag %}
159 {% if tag %}
150 {% url "tag" tag_name=tag page=page %}
160 {% url "tag" tag_name=tag page=page %}
151 {% elif archived %}
161 {% elif archived %}
152 {% url "archive" page=page %}
162 {% url "archive" page=page %}
153 {% else %}
163 {% else %}
154 {% url "index" page=page %}
164 {% url "index" page=page %}
155 {% endif %}
165 {% endif %}
156 ">{{ page }}</a>
166 ">{{ page }}</a>
157 {% if not forloop.last %},{% endif %}
167 {% if not forloop.last %},{% endif %}
158 {% endfor %}
168 {% endfor %}
159 ]
169 ]
170 <a href="
171 {% if tag %}
172 {% url "tag" tag_name=tag page=paginator.page_range|last %}
173 {% elif archived %}
174 {% url "archive" page=paginator.page_range|last %}
175 {% else %}
176 {% url "index" page=paginator.page_range|last %}
177 {% endif %}
178 ">&gt;&gt;</a>
160 [<a href="rss/">RSS</a>]
179 [<a href="rss/">RSS</a>]
161 </span>
180 </span>
162
181
163 {% endblock %}
182 {% endblock %}
@@ -1,257 +1,260 b''
1 # coding=utf-8
1 # coding=utf-8
2 import time
2 import time
3 import logging
3 import logging
4 from django.core.paginator import Paginator
4
5
5 from django.test import TestCase
6 from django.test import TestCase
6 from django.test.client import Client
7 from django.test.client import Client
7 from django.core.urlresolvers import reverse, NoReverseMatch
8 from django.core.urlresolvers import reverse, NoReverseMatch
8
9
9 from boards.models import Post, Tag
10 from boards.models import Post, Tag, Thread
10 from boards import urls
11 from boards import urls
11 from neboard import settings
12 from neboard import settings
12
13
13 PAGE_404 = 'boards/404.html'
14 PAGE_404 = 'boards/404.html'
14
15
15 TEST_TEXT = 'test text'
16 TEST_TEXT = 'test text'
16
17
17 NEW_THREAD_PAGE = '/'
18 NEW_THREAD_PAGE = '/'
18 THREAD_PAGE_ONE = '/thread/1/'
19 THREAD_PAGE_ONE = '/thread/1/'
19 THREAD_PAGE = '/thread/'
20 THREAD_PAGE = '/thread/'
20 TAG_PAGE = '/tag/'
21 TAG_PAGE = '/tag/'
21 HTTP_CODE_REDIRECT = 302
22 HTTP_CODE_REDIRECT = 302
22 HTTP_CODE_OK = 200
23 HTTP_CODE_OK = 200
23 HTTP_CODE_NOT_FOUND = 404
24 HTTP_CODE_NOT_FOUND = 404
24
25
25 logger = logging.getLogger(__name__)
26 logger = logging.getLogger(__name__)
26
27
27
28
28 class PostTests(TestCase):
29 class PostTests(TestCase):
29
30
30 def _create_post(self):
31 def _create_post(self):
31 return Post.objects.create_post(title='title',
32 return Post.objects.create_post(title='title',
32 text='text')
33 text='text')
33
34
34 def test_post_add(self):
35 def test_post_add(self):
35 """Test adding post"""
36 """Test adding post"""
36
37
37 post = self._create_post()
38 post = self._create_post()
38
39
39 self.assertIsNotNone(post, 'No post was created')
40 self.assertIsNotNone(post, 'No post was created')
40
41
41 def test_delete_post(self):
42 def test_delete_post(self):
42 """Test post deletion"""
43 """Test post deletion"""
43
44
44 post = self._create_post()
45 post = self._create_post()
45 post_id = post.id
46 post_id = post.id
46
47
47 Post.objects.delete_post(post)
48 Post.objects.delete_post(post)
48
49
49 self.assertFalse(Post.objects.filter(id=post_id).exists())
50 self.assertFalse(Post.objects.filter(id=post_id).exists())
50
51
51 def test_delete_thread(self):
52 def test_delete_thread(self):
52 """Test thread deletion"""
53 """Test thread deletion"""
53
54
54 opening_post = self._create_post()
55 opening_post = self._create_post()
55 thread = opening_post.thread_new
56 thread = opening_post.thread_new
56 reply = Post.objects.create_post("", "", thread=thread)
57 reply = Post.objects.create_post("", "", thread=thread)
57
58
58 thread.delete_with_posts()
59 thread.delete_with_posts()
59
60
60 self.assertFalse(Post.objects.filter(id=reply.id).exists())
61 self.assertFalse(Post.objects.filter(id=reply.id).exists())
61
62
62 def test_post_to_thread(self):
63 def test_post_to_thread(self):
63 """Test adding post to a thread"""
64 """Test adding post to a thread"""
64
65
65 op = self._create_post()
66 op = self._create_post()
66 post = Post.objects.create_post("", "", thread=op.thread_new)
67 post = Post.objects.create_post("", "", thread=op.thread_new)
67
68
68 self.assertIsNotNone(post, 'Reply to thread wasn\'t created')
69 self.assertIsNotNone(post, 'Reply to thread wasn\'t created')
69 self.assertEqual(op.thread_new.last_edit_time, post.pub_time,
70 self.assertEqual(op.thread_new.last_edit_time, post.pub_time,
70 'Post\'s create time doesn\'t match thread last edit'
71 'Post\'s create time doesn\'t match thread last edit'
71 ' time')
72 ' time')
72
73
73 def test_delete_posts_by_ip(self):
74 def test_delete_posts_by_ip(self):
74 """Test deleting posts with the given ip"""
75 """Test deleting posts with the given ip"""
75
76
76 post = self._create_post()
77 post = self._create_post()
77 post_id = post.id
78 post_id = post.id
78
79
79 Post.objects.delete_posts_by_ip('0.0.0.0')
80 Post.objects.delete_posts_by_ip('0.0.0.0')
80
81
81 self.assertFalse(Post.objects.filter(id=post_id).exists())
82 self.assertFalse(Post.objects.filter(id=post_id).exists())
82
83
83 def test_get_thread(self):
84 def test_get_thread(self):
84 """Test getting all posts of a thread"""
85 """Test getting all posts of a thread"""
85
86
86 opening_post = self._create_post()
87 opening_post = self._create_post()
87
88
88 for i in range(0, 2):
89 for i in range(0, 2):
89 Post.objects.create_post('title', 'text',
90 Post.objects.create_post('title', 'text',
90 thread=opening_post.thread_new)
91 thread=opening_post.thread_new)
91
92
92 thread = opening_post.thread_new
93 thread = opening_post.thread_new
93
94
94 self.assertEqual(3, thread.replies.count())
95 self.assertEqual(3, thread.replies.count())
95
96
96 def test_create_post_with_tag(self):
97 def test_create_post_with_tag(self):
97 """Test adding tag to post"""
98 """Test adding tag to post"""
98
99
99 tag = Tag.objects.create(name='test_tag')
100 tag = Tag.objects.create(name='test_tag')
100 post = Post.objects.create_post(title='title', text='text', tags=[tag])
101 post = Post.objects.create_post(title='title', text='text', tags=[tag])
101
102
102 thread = post.thread_new
103 thread = post.thread_new
103 self.assertIsNotNone(post, 'Post not created')
104 self.assertIsNotNone(post, 'Post not created')
104 self.assertTrue(tag in thread.tags.all(), 'Tag not added to thread')
105 self.assertTrue(tag in thread.tags.all(), 'Tag not added to thread')
105 self.assertTrue(thread in tag.threads.all(), 'Thread not added to tag')
106 self.assertTrue(thread in tag.threads.all(), 'Thread not added to tag')
106
107
107 def test_thread_max_count(self):
108 def test_thread_max_count(self):
108 """Test deletion of old posts when the max thread count is reached"""
109 """Test deletion of old posts when the max thread count is reached"""
109
110
110 for i in range(settings.MAX_THREAD_COUNT + 1):
111 for i in range(settings.MAX_THREAD_COUNT + 1):
111 self._create_post()
112 self._create_post()
112
113
113 self.assertEqual(settings.MAX_THREAD_COUNT,
114 self.assertEqual(settings.MAX_THREAD_COUNT,
114 len(Post.objects.get_threads()))
115 len(Thread.objects.filter(archived=False)))
115
116
116 def test_pages(self):
117 def test_pages(self):
117 """Test that the thread list is properly split into pages"""
118 """Test that the thread list is properly split into pages"""
118
119
119 for i in range(settings.MAX_THREAD_COUNT):
120 for i in range(settings.MAX_THREAD_COUNT):
120 self._create_post()
121 self._create_post()
121
122
122 all_threads = Post.objects.get_threads()
123 all_threads = Thread.objects.filter(archived=False)
123
124
124 posts_in_second_page = Post.objects.get_threads(page=2)
125 paginator = Paginator(Thread.objects.filter(archived=False),
126 settings.THREADS_PER_PAGE)
127 posts_in_second_page = paginator.page(2).object_list
125 first_post = posts_in_second_page[0]
128 first_post = posts_in_second_page[0]
126
129
127 self.assertEqual(all_threads[settings.THREADS_PER_PAGE].id,
130 self.assertEqual(all_threads[settings.THREADS_PER_PAGE].id,
128 first_post.id)
131 first_post.id)
129
132
130 def test_linked_tag(self):
133 def test_linked_tag(self):
131 """Test adding a linked tag"""
134 """Test adding a linked tag"""
132
135
133 linked_tag = Tag.objects.create(name=u'tag1')
136 linked_tag = Tag.objects.create(name=u'tag1')
134 tag = Tag.objects.create(name=u'tag2', linked=linked_tag)
137 tag = Tag.objects.create(name=u'tag2', linked=linked_tag)
135
138
136 post = Post.objects.create_post("", "", tags=[tag])
139 post = Post.objects.create_post("", "", tags=[tag])
137
140
138 self.assertTrue(linked_tag in post.thread_new.tags.all(),
141 self.assertTrue(linked_tag in post.thread_new.tags.all(),
139 'Linked tag was not added')
142 'Linked tag was not added')
140
143
141
144
142 class PagesTest(TestCase):
145 class PagesTest(TestCase):
143
146
144 def test_404(self):
147 def test_404(self):
145 """Test receiving error 404 when opening a non-existent page"""
148 """Test receiving error 404 when opening a non-existent page"""
146
149
147 tag_name = u'test_tag'
150 tag_name = u'test_tag'
148 tag = Tag.objects.create(name=tag_name)
151 tag = Tag.objects.create(name=tag_name)
149 client = Client()
152 client = Client()
150
153
151 Post.objects.create_post('title', TEST_TEXT, tags=[tag])
154 Post.objects.create_post('title', TEST_TEXT, tags=[tag])
152
155
153 existing_post_id = Post.objects.all()[0].id
156 existing_post_id = Post.objects.all()[0].id
154 response_existing = client.get(THREAD_PAGE + str(existing_post_id) +
157 response_existing = client.get(THREAD_PAGE + str(existing_post_id) +
155 '/')
158 '/')
156 self.assertEqual(HTTP_CODE_OK, response_existing.status_code,
159 self.assertEqual(HTTP_CODE_OK, response_existing.status_code,
157 u'Cannot open existing thread')
160 u'Cannot open existing thread')
158
161
159 response_not_existing = client.get(THREAD_PAGE + str(
162 response_not_existing = client.get(THREAD_PAGE + str(
160 existing_post_id + 1) + '/')
163 existing_post_id + 1) + '/')
161 self.assertEqual(PAGE_404,
164 self.assertEqual(PAGE_404,
162 response_not_existing.templates[0].name,
165 response_not_existing.templates[0].name,
163 u'Not existing thread is opened')
166 u'Not existing thread is opened')
164
167
165 response_existing = client.get(TAG_PAGE + tag_name + '/')
168 response_existing = client.get(TAG_PAGE + tag_name + '/')
166 self.assertEqual(HTTP_CODE_OK,
169 self.assertEqual(HTTP_CODE_OK,
167 response_existing.status_code,
170 response_existing.status_code,
168 u'Cannot open existing tag')
171 u'Cannot open existing tag')
169
172
170 response_not_existing = client.get(TAG_PAGE + u'not_tag' + '/')
173 response_not_existing = client.get(TAG_PAGE + u'not_tag' + '/')
171 self.assertEqual(PAGE_404,
174 self.assertEqual(PAGE_404,
172 response_not_existing.templates[0].name,
175 response_not_existing.templates[0].name,
173 u'Not existing tag is opened')
176 u'Not existing tag is opened')
174
177
175 reply_id = Post.objects.create_post('', TEST_TEXT,
178 reply_id = Post.objects.create_post('', TEST_TEXT,
176 thread=Post.objects.all()[0]
179 thread=Post.objects.all()[0]
177 .thread)
180 .thread)
178 response_not_existing = client.get(THREAD_PAGE + str(
181 response_not_existing = client.get(THREAD_PAGE + str(
179 reply_id) + '/')
182 reply_id) + '/')
180 self.assertEqual(PAGE_404,
183 self.assertEqual(PAGE_404,
181 response_not_existing.templates[0].name,
184 response_not_existing.templates[0].name,
182 u'Reply is opened as a thread')
185 u'Reply is opened as a thread')
183
186
184
187
185 class FormTest(TestCase):
188 class FormTest(TestCase):
186 def test_post_validation(self):
189 def test_post_validation(self):
187 # Disable captcha for the test
190 # Disable captcha for the test
188 captcha_enabled = settings.ENABLE_CAPTCHA
191 captcha_enabled = settings.ENABLE_CAPTCHA
189 settings.ENABLE_CAPTCHA = False
192 settings.ENABLE_CAPTCHA = False
190
193
191 client = Client()
194 client = Client()
192
195
193 valid_tags = u'tag1 tag_2 Ρ‚Π΅Π³_3'
196 valid_tags = u'tag1 tag_2 Ρ‚Π΅Π³_3'
194 invalid_tags = u'$%_356 ---'
197 invalid_tags = u'$%_356 ---'
195
198
196 response = client.post(NEW_THREAD_PAGE, {'title': 'test title',
199 response = client.post(NEW_THREAD_PAGE, {'title': 'test title',
197 'text': TEST_TEXT,
200 'text': TEST_TEXT,
198 'tags': valid_tags})
201 'tags': valid_tags})
199 self.assertEqual(response.status_code, HTTP_CODE_REDIRECT,
202 self.assertEqual(response.status_code, HTTP_CODE_REDIRECT,
200 msg='Posting new message failed: got code ' +
203 msg='Posting new message failed: got code ' +
201 str(response.status_code))
204 str(response.status_code))
202
205
203 self.assertEqual(1, Post.objects.count(),
206 self.assertEqual(1, Post.objects.count(),
204 msg='No posts were created')
207 msg='No posts were created')
205
208
206 client.post(NEW_THREAD_PAGE, {'text': TEST_TEXT,
209 client.post(NEW_THREAD_PAGE, {'text': TEST_TEXT,
207 'tags': invalid_tags})
210 'tags': invalid_tags})
208 self.assertEqual(1, Post.objects.count(), msg='The validation passed '
211 self.assertEqual(1, Post.objects.count(), msg='The validation passed '
209 'where it should fail')
212 'where it should fail')
210
213
211 # Change posting delay so we don't have to wait for 30 seconds or more
214 # Change posting delay so we don't have to wait for 30 seconds or more
212 old_posting_delay = settings.POSTING_DELAY
215 old_posting_delay = settings.POSTING_DELAY
213 # Wait fot the posting delay or we won't be able to post
216 # Wait fot the posting delay or we won't be able to post
214 settings.POSTING_DELAY = 1
217 settings.POSTING_DELAY = 1
215 time.sleep(settings.POSTING_DELAY + 1)
218 time.sleep(settings.POSTING_DELAY + 1)
216 response = client.post(THREAD_PAGE_ONE, {'text': TEST_TEXT,
219 response = client.post(THREAD_PAGE_ONE, {'text': TEST_TEXT,
217 'tags': valid_tags})
220 'tags': valid_tags})
218 self.assertEqual(HTTP_CODE_REDIRECT, response.status_code,
221 self.assertEqual(HTTP_CODE_REDIRECT, response.status_code,
219 msg=u'Posting new message failed: got code ' +
222 msg=u'Posting new message failed: got code ' +
220 str(response.status_code))
223 str(response.status_code))
221 # Restore posting delay
224 # Restore posting delay
222 settings.POSTING_DELAY = old_posting_delay
225 settings.POSTING_DELAY = old_posting_delay
223
226
224 self.assertEqual(2, Post.objects.count(),
227 self.assertEqual(2, Post.objects.count(),
225 msg=u'No posts were created')
228 msg=u'No posts were created')
226
229
227 # Restore captcha setting
230 # Restore captcha setting
228 settings.ENABLE_CAPTCHA = captcha_enabled
231 settings.ENABLE_CAPTCHA = captcha_enabled
229
232
230
233
231 class ViewTest(TestCase):
234 class ViewTest(TestCase):
232
235
233 def test_all_views(self):
236 def test_all_views(self):
234 '''
237 '''
235 Try opening all views defined in ulrs.py that don't need additional
238 Try opening all views defined in ulrs.py that don't need additional
236 parameters
239 parameters
237 '''
240 '''
238
241
239 client = Client()
242 client = Client()
240 for url in urls.urlpatterns:
243 for url in urls.urlpatterns:
241 try:
244 try:
242 view_name = url.name
245 view_name = url.name
243 logger.debug('Testing view %s' % view_name)
246 logger.debug('Testing view %s' % view_name)
244
247
245 try:
248 try:
246 response = client.get(reverse(view_name))
249 response = client.get(reverse(view_name))
247
250
248 self.assertEqual(HTTP_CODE_OK, response.status_code,
251 self.assertEqual(HTTP_CODE_OK, response.status_code,
249 '%s view not opened' % view_name)
252 '%s view not opened' % view_name)
250 except NoReverseMatch:
253 except NoReverseMatch:
251 # This view just needs additional arguments
254 # This view just needs additional arguments
252 pass
255 pass
253 except Exception, e:
256 except Exception, e:
254 self.fail('Got exception %s at %s view' % (e, view_name))
257 self.fail('Got exception %s at %s view' % (e, view_name))
255 except AttributeError:
258 except AttributeError:
256 # This is normal, some views do not have names
259 # This is normal, some views do not have names
257 pass
260 pass
@@ -1,125 +1,124 b''
1 import string
1 import string
2
2
3 from django.core.paginator import Paginator
4 from django.core.urlresolvers import reverse
3 from django.core.urlresolvers import reverse
5 from django.db import transaction
4 from django.db import transaction
6 from django.shortcuts import render, redirect
5 from django.shortcuts import render, redirect
7
6
8 from boards import utils
7 from boards import utils
8 from boards.abstracts.paginator import get_paginator
9 from boards.forms import ThreadForm, PlainErrorList
9 from boards.forms import ThreadForm, PlainErrorList
10 from boards.models import Post, Thread, Ban, Tag
10 from boards.models import Post, Thread, Ban, Tag
11 from boards.views.banned import BannedView
11 from boards.views.banned import BannedView
12 from boards.views.base import BaseBoardView, PARAMETER_FORM
12 from boards.views.base import BaseBoardView, PARAMETER_FORM
13 from boards.views.posting_mixin import PostMixin
13 from boards.views.posting_mixin import PostMixin
14 import neboard
14 import neboard
15
15
16 PARAMETER_CURRENT_PAGE = 'current_page'
16 PARAMETER_CURRENT_PAGE = 'current_page'
17
17
18 PARAMETER_PAGINATOR = 'paginator'
18 PARAMETER_PAGINATOR = 'paginator'
19
19
20 PARAMETER_THREADS = 'threads'
20 PARAMETER_THREADS = 'threads'
21
21
22 TEMPLATE = 'boards/posting_general.html'
22 TEMPLATE = 'boards/posting_general.html'
23 DEFAULT_PAGE = 1
23 DEFAULT_PAGE = 1
24
24
25
25
26 class AllThreadsView(PostMixin, BaseBoardView):
26 class AllThreadsView(PostMixin, BaseBoardView):
27
27
28 def get(self, request, page=DEFAULT_PAGE, form=None):
28 def get(self, request, page=DEFAULT_PAGE, form=None):
29 context = self.get_context_data(request=request)
29 context = self.get_context_data(request=request)
30
30
31 if not form:
31 if not form:
32 form = ThreadForm(error_class=PlainErrorList)
32 form = ThreadForm(error_class=PlainErrorList)
33
33
34 paginator = Paginator(self.get_threads(),
34 paginator = get_paginator(self.get_threads(),
35 neboard.settings.THREADS_PER_PAGE)
35 neboard.settings.THREADS_PER_PAGE)
36 paginator.current_page = int(page)
36
37
37 threads = paginator.page(page).object_list
38 threads = paginator.page(page).object_list
38
39
39 context[PARAMETER_THREADS] = threads
40 context[PARAMETER_THREADS] = threads
40 context[PARAMETER_FORM] = form
41 context[PARAMETER_FORM] = form
41
42
42 self._get_page_context(paginator, context, page)
43 self._get_page_context(paginator, context, page)
43
44
44 return render(request, TEMPLATE, context)
45 return render(request, TEMPLATE, context)
45
46
46 def post(self, request, page=DEFAULT_PAGE):
47 def post(self, request, page=DEFAULT_PAGE):
47 context = self.get_context_data(request=request)
48
49 form = ThreadForm(request.POST, request.FILES,
48 form = ThreadForm(request.POST, request.FILES,
50 error_class=PlainErrorList)
49 error_class=PlainErrorList)
51 form.session = request.session
50 form.session = request.session
52
51
53 if form.is_valid():
52 if form.is_valid():
54 return self._new_post(request, form)
53 return self._new_post(request, form)
55 if form.need_to_ban:
54 if form.need_to_ban:
56 # Ban user because he is suspected to be a bot
55 # Ban user because he is suspected to be a bot
57 self._ban_current_user(request)
56 self._ban_current_user(request)
58
57
59 return self.get(request, page, form)
58 return self.get(request, page, form)
60
59
61 @staticmethod
60 @staticmethod
62 def _get_page_context(paginator, context, page):
61 def _get_page_context(paginator, context, page):
63 """
62 """
64 Get pagination context variables
63 Get pagination context variables
65 """
64 """
66
65
67 context[PARAMETER_PAGINATOR] = paginator
66 context[PARAMETER_PAGINATOR] = paginator
68 context[PARAMETER_CURRENT_PAGE] = paginator.page(int(page))
67 context[PARAMETER_CURRENT_PAGE] = paginator.page(int(page))
69
68
70 # TODO This method should be refactored
69 # TODO This method should be refactored
71 @transaction.atomic
70 @transaction.atomic
72 def _new_post(self, request, form, opening_post=None, html_response=True):
71 def _new_post(self, request, form, opening_post=None, html_response=True):
73 """
72 """
74 Add a new thread opening post.
73 Add a new thread opening post.
75 """
74 """
76
75
77 ip = utils.get_client_ip(request)
76 ip = utils.get_client_ip(request)
78 is_banned = Ban.objects.filter(ip=ip).exists()
77 is_banned = Ban.objects.filter(ip=ip).exists()
79
78
80 if is_banned:
79 if is_banned:
81 if html_response:
80 if html_response:
82 return redirect(BannedView().as_view())
81 return redirect(BannedView().as_view())
83 else:
82 else:
84 return
83 return
85
84
86 data = form.cleaned_data
85 data = form.cleaned_data
87
86
88 title = data['title']
87 title = data['title']
89 text = data['text']
88 text = data['text']
90
89
91 text = self._remove_invalid_links(text)
90 text = self._remove_invalid_links(text)
92
91
93 if 'image' in data.keys():
92 if 'image' in data.keys():
94 image = data['image']
93 image = data['image']
95 else:
94 else:
96 image = None
95 image = None
97
96
98 tags = []
97 tags = []
99
98
100 tag_strings = data['tags']
99 tag_strings = data['tags']
101
100
102 if tag_strings:
101 if tag_strings:
103 tag_strings = tag_strings.split(' ')
102 tag_strings = tag_strings.split(' ')
104 for tag_name in tag_strings:
103 for tag_name in tag_strings:
105 tag_name = string.lower(tag_name.strip())
104 tag_name = string.lower(tag_name.strip())
106 if len(tag_name) > 0:
105 if len(tag_name) > 0:
107 tag, created = Tag.objects.get_or_create(name=tag_name)
106 tag, created = Tag.objects.get_or_create(name=tag_name)
108 tags.append(tag)
107 tags.append(tag)
109
108
110 post = Post.objects.create_post(title=title, text=text, ip=ip,
109 post = Post.objects.create_post(title=title, text=text, ip=ip,
111 image=image, tags=tags,
110 image=image, tags=tags,
112 user=self._get_user(request))
111 user=self._get_user(request))
113
112
114 thread_to_show = (opening_post.id if opening_post else post.id)
113 thread_to_show = (opening_post.id if opening_post else post.id)
115
114
116 if html_response:
115 if html_response:
117 if opening_post:
116 if opening_post:
118 return redirect(
117 return redirect(
119 reverse('thread', kwargs={'post_id': thread_to_show}) +
118 reverse('thread', kwargs={'post_id': thread_to_show}) +
120 '#' + str(post.id))
119 '#' + str(post.id))
121 else:
120 else:
122 return redirect('thread', post_id=thread_to_show)
121 return redirect('thread', post_id=thread_to_show)
123
122
124 def get_threads(self):
123 def get_threads(self):
125 return Thread.objects.filter(archived=False).order_by('-bump_time')
124 return Thread.objects.filter(archived=False).order_by('-bump_time')
General Comments 0
You need to be logged in to leave comments. Login now