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