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