##// END OF EJS Templates
Added error 404 for opening not existing thread or tag. This fixes #8
neko259 -
r79:b95f96a3 default
parent child Browse files
Show More
@@ -1,91 +1,90 b''
1 import re
1 import re
2 from django import forms
2 from django import forms
3 from django.forms.util import ErrorList
3 from django.forms.util import ErrorList
4 from boards.models import TITLE_MAX_LENGTH
4 from neboard import settings
5 from neboard import settings
5
6
6 TITLE_MAX_LENGTH = 50
7
8
7
9 class PlainErrorList(ErrorList):
8 class PlainErrorList(ErrorList):
10 def __unicode__(self):
9 def __unicode__(self):
11 return self.as_text()
10 return self.as_text()
12
11
13 def as_text(self):
12 def as_text(self):
14 return ''.join([u'(!) %s ' % e for e in self])
13 return ''.join([u'(!) %s ' % e for e in self])
15
14
16
15
17 class PostForm(forms.Form):
16 class PostForm(forms.Form):
18
17
19 MAX_TEXT_LENGTH = 10000
18 MAX_TEXT_LENGTH = 10000
20 MAX_IMAGE_SIZE = 8 * 1024 * 1024
19 MAX_IMAGE_SIZE = 8 * 1024 * 1024
21
20
22 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False)
21 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False)
23 text = forms.CharField(widget=forms.Textarea, required=False)
22 text = forms.CharField(widget=forms.Textarea, required=False)
24 image = forms.ImageField(required=False)
23 image = forms.ImageField(required=False)
25
24
26 def clean_title(self):
25 def clean_title(self):
27 title = self.cleaned_data['title']
26 title = self.cleaned_data['title']
28 if title:
27 if title:
29 if len(title) > TITLE_MAX_LENGTH:
28 if len(title) > TITLE_MAX_LENGTH:
30 raise forms.ValidationError('Title must have less than' +
29 raise forms.ValidationError('Title must have less than' +
31 str(TITLE_MAX_LENGTH) +
30 str(TITLE_MAX_LENGTH) +
32 ' characters.')
31 ' characters.')
33 return title
32 return title
34
33
35 def clean_text(self):
34 def clean_text(self):
36 text = self.cleaned_data['text']
35 text = self.cleaned_data['text']
37 if text:
36 if text:
38 if len(text) > self.MAX_TEXT_LENGTH:
37 if len(text) > self.MAX_TEXT_LENGTH:
39 raise forms.ValidationError('Text must have less than ' +
38 raise forms.ValidationError('Text must have less than ' +
40 str(self.MAX_TEXT_LENGTH) +
39 str(self.MAX_TEXT_LENGTH) +
41 ' characters.')
40 ' characters.')
42 return text
41 return text
43
42
44 def clean_image(self):
43 def clean_image(self):
45 image = self.cleaned_data['image']
44 image = self.cleaned_data['image']
46 if image:
45 if image:
47 if image._size > self.MAX_IMAGE_SIZE:
46 if image._size > self.MAX_IMAGE_SIZE:
48 raise forms.ValidationError('Image must be less than ' +
47 raise forms.ValidationError('Image must be less than ' +
49 str(self.MAX_IMAGE_SIZE) +
48 str(self.MAX_IMAGE_SIZE) +
50 ' bytes.')
49 ' bytes.')
51 return image
50 return image
52
51
53 def clean(self):
52 def clean(self):
54 cleaned_data = super(PostForm, self).clean()
53 cleaned_data = super(PostForm, self).clean()
55
54
56 self._clean_text_image()
55 self._clean_text_image()
57
56
58 return cleaned_data
57 return cleaned_data
59
58
60 def _clean_text_image(self):
59 def _clean_text_image(self):
61 text = self.cleaned_data.get('text')
60 text = self.cleaned_data.get('text')
62 image = self.cleaned_data.get('image')
61 image = self.cleaned_data.get('image')
63
62
64 if (not text) and (not image):
63 if (not text) and (not image):
65 error_message = 'Either text or image must be entered.'
64 error_message = 'Either text or image must be entered.'
66 self._errors['text'] = self.error_class([error_message])
65 self._errors['text'] = self.error_class([error_message])
67 self._errors['image'] = self.error_class([error_message])
66 self._errors['image'] = self.error_class([error_message])
68
67
69
68
70 class ThreadForm(PostForm):
69 class ThreadForm(PostForm):
71 regex_tags = re.compile(ur'^[\w\s\d]+$', re.UNICODE)
70 regex_tags = re.compile(ur'^[\w\s\d]+$', re.UNICODE)
72 tags = forms.CharField(max_length=100)
71 tags = forms.CharField(max_length=100)
73
72
74 def clean_tags(self):
73 def clean_tags(self):
75 tags = self.cleaned_data['tags']
74 tags = self.cleaned_data['tags']
76
75
77 if tags:
76 if tags:
78 if not self.regex_tags.match(tags):
77 if not self.regex_tags.match(tags):
79 raise forms.ValidationError(
78 raise forms.ValidationError(
80 'Inappropriate characters in tags.')
79 'Inappropriate characters in tags.')
81
80
82 return tags
81 return tags
83
82
84 def clean(self):
83 def clean(self):
85 cleaned_data = super(ThreadForm, self).clean()
84 cleaned_data = super(ThreadForm, self).clean()
86
85
87 return cleaned_data
86 return cleaned_data
88
87
89
88
90 class SettingsForm(forms.Form):
89 class SettingsForm(forms.Form):
91 theme = forms.ChoiceField(choices=settings.THEMES, widget=forms.RadioSelect) No newline at end of file
90 theme = forms.ChoiceField(choices=settings.THEMES, widget=forms.RadioSelect)
@@ -1,278 +1,288 b''
1 import os
1 import os
2 from random import random
2 from random import random
3 import re
3 import re
4 import time
4 import time
5 import math
5 import math
6
6
7 from django.db import models
7 from django.db import models
8 from django.http import Http404
8 from django.http import Http404
9 from django.utils import timezone
9 from django.utils import timezone
10 from markupfield.fields import MarkupField
10 from markupfield.fields import MarkupField
11
11
12 from neboard import settings
12 from neboard import settings
13 import thumbs
13 import thumbs
14
14
15 IMAGE_THUMB_SIZE = (200, 150)
16
17 TITLE_MAX_LENGTH = 50
18
19 DEFAULT_MARKUP_TYPE = 'markdown'
15
20
16 NO_PARENT = -1
21 NO_PARENT = -1
17 NO_IP = '0.0.0.0'
22 NO_IP = '0.0.0.0'
18 UNKNOWN_UA = ''
23 UNKNOWN_UA = ''
19 ALL_PAGES = -1
24 ALL_PAGES = -1
20 OPENING_POST_WEIGHT = 5
25 OPENING_POST_WEIGHT = 5
21 IMAGES_DIRECTORY = 'images/'
26 IMAGES_DIRECTORY = 'images/'
22 FILE_EXTENSION_DELIMITER = '.'
27 FILE_EXTENSION_DELIMITER = '.'
23
28
24 REGEX_PRETTY = re.compile(r'^\d(0)+$')
29 REGEX_PRETTY = re.compile(r'^\d(0)+$')
25 REGEX_SAME = re.compile(r'^(.)\1+$')
30 REGEX_SAME = re.compile(r'^(.)\1+$')
26
31
27
32
28 class PostManager(models.Manager):
33 class PostManager(models.Manager):
29 def create_post(self, title, text, image=None, parent_id=NO_PARENT,
34 def create_post(self, title, text, image=None, parent_id=NO_PARENT,
30 ip=NO_IP, tags=None):
35 ip=NO_IP, tags=None):
31 post = self.create(title=title,
36 post = self.create(title=title,
32 text=text,
37 text=text,
33 pub_time=timezone.now(),
38 pub_time=timezone.now(),
34 parent=parent_id,
39 parent=parent_id,
35 image=image,
40 image=image,
36 poster_ip=ip,
41 poster_ip=ip,
37 poster_user_agent=UNKNOWN_UA,
42 poster_user_agent=UNKNOWN_UA,
38 last_edit_time=timezone.now())
43 last_edit_time=timezone.now())
39
44
40 if tags:
45 if tags:
41 for tag in tags:
46 map(post.tags.add, tags)
42 post.tags.add(tag)
43
47
44 if parent_id != NO_PARENT:
48 if parent_id != NO_PARENT:
45 self._bump_thread(parent_id)
49 self._bump_thread(parent_id)
46 else:
50 else:
47 self._delete_old_threads()
51 self._delete_old_threads()
48
52
49 return post
53 return post
50
54
51 def delete_post(self, post):
55 def delete_post(self, post):
52 children = self.filter(parent=post.id)
56 children = self.filter(parent=post.id)
53 for child in children:
57 for child in children:
54 self.delete_post(child)
58 self.delete_post(child)
55 post.delete()
59 post.delete()
56
60
57 def delete_posts_by_ip(self, ip):
61 def delete_posts_by_ip(self, ip):
58 posts = self.filter(poster_ip=ip)
62 posts = self.filter(poster_ip=ip)
59 for post in posts:
63 for post in posts:
60 self.delete_post(post)
64 self.delete_post(post)
61
65
62 def get_threads(self, tag=None, page=ALL_PAGES):
66 def get_threads(self, tag=None, page=ALL_PAGES):
63 if tag:
67 if tag:
64 threads = self.filter(parent=NO_PARENT, tags=tag)
68 threads = self.filter(parent=NO_PARENT, tags=tag)
65 else:
69 else:
66 threads = self.filter(parent=NO_PARENT)
70 threads = self.filter(parent=NO_PARENT)
71
72 if not threads:
73 raise Http404
74
67 threads = threads.order_by('-last_edit_time')
75 threads = threads.order_by('-last_edit_time')
68
76
69 if page != ALL_PAGES:
77 if page != ALL_PAGES:
70 thread_count = len(threads)
78 thread_count = len(threads)
71
79
72 if page < self.get_thread_page_count(tag=tag):
80 if page < self.get_thread_page_count(tag=tag):
73 start_thread = page * settings.THREADS_PER_PAGE
81 start_thread = page * settings.THREADS_PER_PAGE
74 end_thread = min(start_thread + settings.THREADS_PER_PAGE,
82 end_thread = min(start_thread + settings.THREADS_PER_PAGE,
75 thread_count)
83 thread_count)
76 threads = threads[start_thread:end_thread]
84 threads = threads[start_thread:end_thread]
77
85
78 return threads
86 return threads
79
87
80 def get_thread(self, opening_post_id):
88 def get_thread(self, opening_post_id):
81 opening_post = self.get(id=opening_post_id)
89 try:
82
90 opening_post = self.get(id=opening_post_id)
83 if not opening_post:
91 except Post.DoesNotExist:
84 raise Http404
92 raise Http404
85
93
86 if opening_post.parent == NO_PARENT:
94 if opening_post.parent == NO_PARENT:
87 replies = self.filter(parent=opening_post_id)
95 replies = self.filter(parent=opening_post_id)
88
96
89 thread = [opening_post]
97 thread = [opening_post]
90 thread.extend(replies)
98 thread.extend(replies)
91
99
92 return thread
100 return thread
93
101
94 def exists(self, post_id):
102 def exists(self, post_id):
95 posts = self.filter(id=post_id)
103 posts = self.filter(id=post_id)
96
104
97 return posts.count() > 0
105 return posts.count() > 0
98
106
99 def get_thread_page_count(self, tag=None):
107 def get_thread_page_count(self, tag=None):
100 if tag:
108 if tag:
101 threads = self.filter(parent=NO_PARENT, tags=tag)
109 threads = self.filter(parent=NO_PARENT, tags=tag)
102 else:
110 else:
103 threads = self.filter(parent=NO_PARENT)
111 threads = self.filter(parent=NO_PARENT)
104
112
105 return int(math.ceil(threads.count() / float(
113 return int(math.ceil(threads.count() / float(
106 settings.THREADS_PER_PAGE)))
114 settings.THREADS_PER_PAGE)))
107
115
108 def _delete_old_threads(self):
116 def _delete_old_threads(self):
109 """
117 """
110 Preserves maximum thread count. If there are too many threads,
118 Preserves maximum thread count. If there are too many threads,
111 delete the old ones.
119 delete the old ones.
112 """
120 """
113
121
114 # TODO Move old threads to the archive instead of deleting them.
122 # TODO Move old threads to the archive instead of deleting them.
115 # Maybe make some 'old' field in the model to indicate the thread
123 # Maybe make some 'old' field in the model to indicate the thread
116 # must not be shown and be able for replying.
124 # must not be shown and be able for replying.
117
125
118 threads = self.get_threads()
126 threads = self.get_threads()
119 thread_count = len(threads)
127 thread_count = len(threads)
120
128
121 if thread_count > settings.MAX_THREAD_COUNT:
129 if thread_count > settings.MAX_THREAD_COUNT:
122 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
130 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
123 old_threads = threads[thread_count - num_threads_to_delete:]
131 old_threads = threads[thread_count - num_threads_to_delete:]
124
132
125 for thread in old_threads:
133 for thread in old_threads:
126 self.delete_post(thread)
134 self.delete_post(thread)
127
135
128 def _bump_thread(self, thread_id):
136 def _bump_thread(self, thread_id):
129 thread = self.get(id=thread_id)
137 thread = self.get(id=thread_id)
130
138
131 if thread.can_bump():
139 if thread.can_bump():
132 thread.last_edit_time = timezone.now()
140 thread.last_edit_time = timezone.now()
133 thread.save()
141 thread.save()
134
142
135
143
136 class TagManager(models.Manager):
144 class TagManager(models.Manager):
137 def get_not_empty_tags(self):
145 def get_not_empty_tags(self):
138 all_tags = self.all().order_by('name')
146 all_tags = self.all().order_by('name')
139 tags = []
147 tags = []
140 for tag in all_tags:
148 for tag in all_tags:
141 if not tag.is_empty():
149 if not tag.is_empty():
142 tags.append(tag)
150 tags.append(tag)
143
151
144 return tags
152 return tags
145
153
146 def get_popular_tags(self):
154 def get_popular_tags(self):
147 all_tags = self.get_not_empty_tags()
155 all_tags = self.get_not_empty_tags()
148
156
149 sorted_tags = sorted(all_tags, key=lambda tag: tag.get_popularity(),
157 sorted_tags = sorted(all_tags, key=lambda tag: tag.get_popularity(),
150 reverse=True)
158 reverse=True)
151
159
152 return sorted_tags[:settings.POPULAR_TAGS]
160 return sorted_tags[:settings.POPULAR_TAGS]
153
161
154
162
155 class Tag(models.Model):
163 class Tag(models.Model):
156 """
164 """
157 A tag is a text node assigned to the post. The tag serves as a board
165 A tag is a text node assigned to the post. The tag serves as a board
158 section. There can be multiple tags for each message
166 section. There can be multiple tags for each message
159 """
167 """
160
168
161 objects = TagManager()
169 objects = TagManager()
162
170
163 name = models.CharField(max_length=100)
171 name = models.CharField(max_length=100)
164 # TODO Connect the tag to its posts to check the number of threads for
172 # TODO Connect the tag to its posts to check the number of threads for
165 # the tag.
173 # the tag.
166
174
167 def __unicode__(self):
175 def __unicode__(self):
168 return self.name
176 return self.name
169
177
170 def is_empty(self):
178 def is_empty(self):
171 return self.get_post_count() == 0
179 return self.get_post_count() == 0
172
180
173 def get_post_count(self):
181 def get_post_count(self):
174 posts_with_tag = Post.objects.get_threads(tag=self)
182 posts_with_tag = Post.objects.get_threads(tag=self)
175 return posts_with_tag.count()
183 return posts_with_tag.count()
176
184
177 def get_popularity(self):
185 def get_popularity(self):
178 posts_with_tag = Post.objects.get_threads(tag=self)
186 posts_with_tag = Post.objects.get_threads(tag=self)
179 reply_count = 0
187 reply_count = 0
180 for post in posts_with_tag:
188 for post in posts_with_tag:
181 reply_count += post.get_reply_count()
189 reply_count += post.get_reply_count()
182 reply_count += OPENING_POST_WEIGHT
190 reply_count += OPENING_POST_WEIGHT
183
191
184 return reply_count
192 return reply_count
185
193
186
194
187 class Post(models.Model):
195 class Post(models.Model):
188 """A post is a message."""
196 """A post is a message."""
189
197
190 objects = PostManager()
198 objects = PostManager()
191
199
192 def _update_image_filename(self, filename):
200 def _update_image_filename(self, filename):
193 """Get unique image filename"""
201 """Get unique image filename"""
194
202
195 path = IMAGES_DIRECTORY
203 path = IMAGES_DIRECTORY
196 new_name = str(int(time.mktime(time.gmtime())))
204 new_name = str(int(time.mktime(time.gmtime())))
197 new_name += str(int(random() * 1000))
205 new_name += str(int(random() * 1000))
198 new_name += FILE_EXTENSION_DELIMITER
206 new_name += FILE_EXTENSION_DELIMITER
199 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
207 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
200
208
201 return os.path.join(path, new_name)
209 return os.path.join(path, new_name)
202
210
203 title = models.CharField(max_length=50)
211 title = models.CharField(max_length=TITLE_MAX_LENGTH)
204 pub_time = models.DateTimeField()
212 pub_time = models.DateTimeField()
205 text = MarkupField(default_markup_type='markdown', escape_html=True)
213 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
214 escape_html=True)
206 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
215 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
207 blank=True, sizes=((200, 150),))
216 blank=True, sizes=(IMAGE_THUMB_SIZE,))
208 poster_ip = models.IPAddressField()
217 poster_ip = models.IPAddressField()
209 poster_user_agent = models.TextField()
218 poster_user_agent = models.TextField()
210 parent = models.BigIntegerField()
219 parent = models.BigIntegerField()
211 tags = models.ManyToManyField(Tag)
220 tags = models.ManyToManyField(Tag)
212 last_edit_time = models.DateTimeField()
221 last_edit_time = models.DateTimeField()
213
222
214 def __unicode__(self):
223 def __unicode__(self):
215 return self.title + ' (' + self.text.raw + ')'
224 return '#' + str(self.id) + ' ' + self.title + ' (' + self.text.raw + \
225 ')'
216
226
217 def _get_replies(self):
227 def _get_replies(self):
218 return Post.objects.filter(parent=self.id)
228 return Post.objects.filter(parent=self.id)
219
229
220 def get_reply_count(self):
230 def get_reply_count(self):
221 return self._get_replies().count()
231 return self._get_replies().count()
222
232
223 def get_images_count(self):
233 def get_images_count(self):
224 images_count = 1 if self.image else 0
234 images_count = 1 if self.image else 0
225 for reply in self._get_replies():
235 for reply in self._get_replies():
226 if reply.image:
236 if reply.image:
227 images_count += 1
237 images_count += 1
228
238
229 return images_count
239 return images_count
230
240
231 def get_gets_count(self):
241 def get_gets_count(self):
232 gets_count = 1 if self.is_get() else 0
242 gets_count = 1 if self.is_get() else 0
233 for reply in self._get_replies():
243 for reply in self._get_replies():
234 if reply.is_get():
244 if reply.is_get():
235 gets_count += 1
245 gets_count += 1
236
246
237 return gets_count
247 return gets_count
238
248
239 def is_get(self):
249 def is_get(self):
240 """If the post has pretty id (1, 1000, 77777), than it is called GET"""
250 """If the post has pretty id (1, 1000, 77777), than it is called GET"""
241
251
242 first = self.id == 1
252 first = self.id == 1
243
253
244 id_str = str(self.id)
254 id_str = str(self.id)
245 pretty = REGEX_PRETTY.match(id_str)
255 pretty = REGEX_PRETTY.match(id_str)
246 same_digits = REGEX_SAME.match(id_str)
256 same_digits = REGEX_SAME.match(id_str)
247
257
248 return first or pretty or same_digits
258 return first or pretty or same_digits
249
259
250 def can_bump(self):
260 def can_bump(self):
251 """Check if the thread can be bumped by replying"""
261 """Check if the thread can be bumped by replying"""
252
262
253 replies_count = len(Post.objects.get_thread(self.id))
263 replies_count = len(Post.objects.get_thread(self.id))
254
264
255 return replies_count <= settings.MAX_POSTS_PER_THREAD
265 return replies_count <= settings.MAX_POSTS_PER_THREAD
256
266
257 def get_last_replies(self):
267 def get_last_replies(self):
258 if settings.LAST_REPLIES_COUNT > 0:
268 if settings.LAST_REPLIES_COUNT > 0:
259 reply_count = self.get_reply_count()
269 reply_count = self.get_reply_count()
260
270
261 if reply_count > 0:
271 if reply_count > 0:
262 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
272 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
263 reply_count)
273 reply_count)
264 last_replies = self._get_replies()[reply_count
274 last_replies = self._get_replies()[reply_count
265 - reply_count_to_show:]
275 - reply_count_to_show:]
266
276
267 return last_replies
277 return last_replies
268
278
269
279
270 class Admin(models.Model):
280 class Admin(models.Model):
271 """
281 """
272 Model for admin users
282 Model for admin users
273 """
283 """
274 name = models.CharField(max_length=100)
284 name = models.CharField(max_length=100)
275 password = models.CharField(max_length=100)
285 password = models.CharField(max_length=100)
276
286
277 def __unicode__(self):
287 def __unicode__(self):
278 return self.name + '/' + '*' * len(self.password)
288 return self.name + '/' + '*' * len(self.password)
@@ -1,172 +1,210 b''
1 # coding=utf-8
1 # coding=utf-8
2 from django.utils.unittest import TestCase
2 from django.utils.unittest import TestCase
3 from django.test.client import Client
3 from django.test.client import Client
4
4
5 import boards
5 import boards
6
6
7 from boards.models import Post, Admin, Tag
7 from boards.models import Post, Admin, Tag
8 from neboard import settings
8 from neboard import settings
9
9
10 TEST_TEXT = 'test text'
10 TEST_TEXT = 'test text'
11
11
12 NEW_THREAD_PAGE = '/'
12 NEW_THREAD_PAGE = '/'
13 THREAD_PAGE = '/thread/1/'
13 THREAD_PAGE_ONE = '/thread/1/'
14 THREAD_PAGE = '/thread/'
15 TAG_PAGE = '/tag/'
14 HTTP_CODE_REDIRECT = 302
16 HTTP_CODE_REDIRECT = 302
17 HTTP_CODE_OK = 200
18 HTTP_CODE_NOT_FOUND = 404
15
19
16
20
17 class BoardTests(TestCase):
21 class BoardTests(TestCase):
18 def _create_post(self):
22 def _create_post(self):
19 return Post.objects.create_post(title='title',
23 return Post.objects.create_post(title='title',
20 text='text')
24 text='text')
21
25
22 def test_post_add(self):
26 def test_post_add(self):
23 post = self._create_post()
27 post = self._create_post()
24
28
25 self.assertIsNotNone(post)
29 self.assertIsNotNone(post)
26 self.assertEqual(boards.models.NO_PARENT, post.parent)
30 self.assertEqual(boards.models.NO_PARENT, post.parent)
27
31
28 def test_delete_post(self):
32 def test_delete_post(self):
29 post = self._create_post()
33 post = self._create_post()
30 post_id = post.id
34 post_id = post.id
31
35
32 Post.objects.delete_post(post)
36 Post.objects.delete_post(post)
33
37
34 self.assertFalse(Post.objects.exists(post_id))
38 self.assertFalse(Post.objects.exists(post_id))
35
39
36 def test_delete_posts_by_ip(self):
40 def test_delete_posts_by_ip(self):
37 post = self._create_post()
41 post = self._create_post()
38 post_id = post.id
42 post_id = post.id
39
43
40 Post.objects.delete_posts_by_ip('0.0.0.0')
44 Post.objects.delete_posts_by_ip('0.0.0.0')
41
45
42 self.assertFalse(Post.objects.exists(post_id))
46 self.assertFalse(Post.objects.exists(post_id))
43
47
44 # Authentication tests
48 # Authentication tests
45
49
46 def _create_test_user(self):
50 def _create_test_user(self):
47 admin = Admin(name='test_username12313584353165',
51 admin = Admin(name='test_username12313584353165',
48 password='test_userpassword135135512')
52 password='test_userpassword135135512')
49
53
50 admin.save()
54 admin.save()
51 return admin
55 return admin
52
56
53 def test_admin_login(self):
57 def test_admin_login(self):
54 client = Client()
58 client = Client()
55
59
56 self.assertFalse('admin' in client.session)
60 self.assertFalse('admin' in client.session)
57
61
58 admin = self._create_test_user()
62 admin = self._create_test_user()
59
63
60 response = client.post('/login',
64 response = client.post('/login',
61 {'name': admin.name, 'password': admin.password})
65 {'name': admin.name, 'password': admin.password})
62
66
63 # it means that login passed and user are redirected to another page
67 # it means that login passed and user are redirected to another page
64 self.assertEqual(302, response.status_code)
68 self.assertEqual(302, response.status_code)
65
69
66 self.assertTrue('admin' in client.session)
70 self.assertTrue('admin' in client.session)
67 self.assertTrue(client.session['admin'])
71 self.assertTrue(client.session['admin'])
68
72
69 admin.delete()
73 admin.delete()
70
74
71 wrong_name = 'sd2f1s3d21fs3d21f'
75 wrong_name = 'sd2f1s3d21fs3d21f'
72 wrong_password = 'sd2f1s3d21fs3d21fsdfsd'
76 wrong_password = 'sd2f1s3d21fs3d21fsdfsd'
73
77
74 client.post('/login', {'name': wrong_name, 'password': wrong_password})
78 client.post('/login', {'name': wrong_name, 'password': wrong_password})
75 self.assertFalse(client.session['admin'])
79 self.assertFalse(client.session['admin'])
76
80
77 def test_admin_logout(self):
81 def test_admin_logout(self):
78 client = Client()
82 client = Client()
79
83
80 self.assertFalse('admin' in client.session)
84 self.assertFalse('admin' in client.session)
81
85
82 admin = self._create_test_user()
86 admin = self._create_test_user()
83
87
84 client.post('/login',
88 client.post('/login',
85 {'name': admin.name, 'password': admin.password})
89 {'name': admin.name, 'password': admin.password})
86
90
87 self.assertTrue(client.session['admin'])
91 self.assertTrue(client.session['admin'])
88
92
89 client.get('/logout')
93 client.get('/logout')
90
94
91 self.assertFalse(client.session['admin'])
95 self.assertFalse(client.session['admin'])
92
96
93 admin.delete()
97 admin.delete()
94
98
95 def test_get_thread(self):
99 def test_get_thread(self):
96 opening_post = self._create_post()
100 opening_post = self._create_post()
97 op_id = opening_post.id
101 op_id = opening_post.id
98
102
99 for i in range(0, 2):
103 for i in range(0, 2):
100 Post.objects.create_post('title', 'text',
104 Post.objects.create_post('title', 'text',
101 parent_id=op_id)
105 parent_id=op_id)
102
106
103 thread = Post.objects.get_thread(op_id)
107 thread = Post.objects.get_thread(op_id)
104
108
105 self.assertEqual(3, len(thread))
109 self.assertEqual(3, len(thread))
106
110
107 def test_create_post_with_tag(self):
111 def test_create_post_with_tag(self):
108 tag = Tag.objects.create(name='test_tag')
112 tag = Tag.objects.create(name='test_tag')
109 post = Post.objects.create_post(title='title', text='text', tags=[tag])
113 post = Post.objects.create_post(title='title', text='text', tags=[tag])
110 self.assertIsNotNone(post)
114 self.assertIsNotNone(post)
111
115
112 def test_thread_max_count(self):
116 def test_thread_max_count(self):
113 for i in range(settings.MAX_THREAD_COUNT + 1):
117 for i in range(settings.MAX_THREAD_COUNT + 1):
114 self._create_post()
118 self._create_post()
115
119
116 self.assertEqual(settings.MAX_THREAD_COUNT,
120 self.assertEqual(settings.MAX_THREAD_COUNT,
117 len(Post.objects.get_threads()))
121 len(Post.objects.get_threads()))
118
122
119 def test_get(self):
123 def test_get(self):
120 """Test if the get computes properly"""
124 """Test if the get computes properly"""
121
125
122 post = self._create_post()
126 post = self._create_post()
123
127
124 self.assertTrue(post.is_get())
128 self.assertTrue(post.is_get())
125
129
126 def test_pages(self):
130 def test_pages(self):
127 """Test that the thread list is properly split into pages"""
131 """Test that the thread list is properly split into pages"""
128
132
129 for i in range(settings.MAX_THREAD_COUNT):
133 for i in range(settings.MAX_THREAD_COUNT):
130 self._create_post()
134 self._create_post()
131
135
132 all_threads = Post.objects.get_threads()
136 all_threads = Post.objects.get_threads()
133
137
134 posts_in_second_page = Post.objects.get_threads(page=1)
138 posts_in_second_page = Post.objects.get_threads(page=1)
135 first_post = posts_in_second_page[0]
139 first_post = posts_in_second_page[0]
136
140
137 self.assertEqual(all_threads[settings.THREADS_PER_PAGE].id,
141 self.assertEqual(all_threads[settings.THREADS_PER_PAGE].id,
138 first_post.id)
142 first_post.id)
139
143
140 def test_post_validation(self):
144 def test_post_validation(self):
141 """Test the validation of the post form"""
145 """Test the validation of the post form"""
142
146
143 Post.objects.all().delete()
147 Post.objects.all().delete()
144
148
145 client = Client()
149 client = Client()
146
150
147 valid_tags = u'tag1 tag_2 Ρ‚Π΅Π³_3'
151 valid_tags = u'tag1 tag_2 Ρ‚Π΅Π³_3'
148 invalid_tags = u'$%_356 ---'
152 invalid_tags = u'$%_356 ---'
149
153
150 response = client.post(NEW_THREAD_PAGE, {'title': 'test title',
154 response = client.post(NEW_THREAD_PAGE, {'title': 'test title',
151 'text': TEST_TEXT,
155 'text': TEST_TEXT,
152 'tags': valid_tags})
156 'tags': valid_tags})
153 self.assertEqual(response.status_code, HTTP_CODE_REDIRECT,
157 self.assertEqual(response.status_code, HTTP_CODE_REDIRECT,
154 msg='Posting new message failed: got code ' +
158 msg='Posting new message failed: got code ' +
155 str(response.status_code))
159 str(response.status_code))
156
160
157 self.assertEqual(1, Post.objects.count(),
161 self.assertEqual(1, Post.objects.count(),
158 msg='No posts were created')
162 msg='No posts were created')
159
163
160 client.post(NEW_THREAD_PAGE, {'text': TEST_TEXT,
164 client.post(NEW_THREAD_PAGE, {'text': TEST_TEXT,
161 'tags': invalid_tags})
165 'tags': invalid_tags})
162 self.assertEqual(1, Post.objects.count(), msg='The validation passed '
166 self.assertEqual(1, Post.objects.count(), msg='The validation passed '
163 'where it should fail')
167 'where it should fail')
164
168
165 response = client.post(THREAD_PAGE, {'text': TEST_TEXT,
169 response = client.post(THREAD_PAGE_ONE, {'text': TEST_TEXT,
166 'tags': valid_tags})
170 'tags': valid_tags})
167 self.assertEqual(response.status_code, HTTP_CODE_REDIRECT,
171 self.assertEqual(HTTP_CODE_REDIRECT, response.status_code,
168 msg='Posting new message failed: got code ' +
172 msg=u'Posting new message failed: got code ' +
169 str(response.status_code))
173 str(response.status_code))
170
174
171 self.assertEqual(2, Post.objects.count(),
175 self.assertEqual(2, Post.objects.count(),
172 msg='No posts were created') No newline at end of file
176 msg=u'No posts were created')
177
178 def test_404(self):
179 """Test receiving error 404 when opening a non-existent page"""
180
181 Post.objects.all().delete()
182 Tag.objects.all().delete()
183
184 tag_name = u'test_tag'
185 tags, = [Tag.objects.get_or_create(name=tag_name)]
186 client = Client()
187
188 Post.objects.create_post('title', TEST_TEXT, tags=tags)
189
190 existing_post_id = Post.objects.all()[0].id
191 response_existing = client.get(THREAD_PAGE + str(existing_post_id) +
192 '/')
193 self.assertEqual(HTTP_CODE_OK, response_existing.status_code,
194 u'Cannot open existing thread')
195
196 response_not_existing = client.get(THREAD_PAGE + str(
197 existing_post_id + 1) + '/')
198 self.assertEqual(HTTP_CODE_NOT_FOUND,
199 response_not_existing.status_code,
200 u'Not existing thread is opened')
201
202 response_existing = client.get(TAG_PAGE + tag_name + '/')
203 self.assertEqual(HTTP_CODE_OK,
204 response_existing.status_code,
205 u'Cannot open existing tag')
206
207 response_not_existing = client.get(TAG_PAGE + u'not_tag' + '/')
208 self.assertEqual(HTTP_CODE_NOT_FOUND,
209 response_not_existing.status_code,
210 u'Not existing tag is opened') No newline at end of file
@@ -1,201 +1,201 b''
1 from django.core.urlresolvers import reverse
1 from django.core.urlresolvers import reverse
2 from django.template import RequestContext
2 from django.template import RequestContext
3 from django.shortcuts import render, redirect
3 from django.shortcuts import render, redirect, get_object_or_404
4 from django.http import HttpResponseRedirect
4 from django.http import HttpResponseRedirect
5
5
6 from boards import forms
6 from boards import forms
7 import boards
7 import boards
8 from boards.forms import ThreadForm, PostForm, SettingsForm, PlainErrorList
8 from boards.forms import ThreadForm, PostForm, SettingsForm, PlainErrorList
9 from boards.models import Post, Admin, Tag
9 from boards.models import Post, Admin, Tag
10 import neboard
10 import neboard
11
11
12
12
13 def index(request, page=0):
13 def index(request, page=0):
14 context = RequestContext(request)
14 context = RequestContext(request)
15
15
16 if request.method == 'POST':
16 if request.method == 'POST':
17 form = ThreadForm(request.POST, request.FILES,
17 form = ThreadForm(request.POST, request.FILES,
18 error_class=PlainErrorList)
18 error_class=PlainErrorList)
19 if form.is_valid():
19 if form.is_valid():
20 return _new_post(request, form)
20 return _new_post(request, form)
21 else:
21 else:
22 form = forms.ThreadForm(error_class=PlainErrorList)
22 form = forms.ThreadForm(error_class=PlainErrorList)
23
23
24 threads = Post.objects.get_threads(page=int(page))
24 threads = Post.objects.get_threads(page=int(page))
25
25
26 # TODO Get rid of the duplicate code in index and tag views
26 # TODO Get rid of the duplicate code in index and tag views
27 context['threads'] = None if len(threads) == 0 else threads
27 context['threads'] = None if len(threads) == 0 else threads
28 context['form'] = form
28 context['form'] = form
29 context['tags'] = Tag.objects.get_popular_tags()
29 context['tags'] = Tag.objects.get_popular_tags()
30 context['theme'] = _get_theme(request)
30 context['theme'] = _get_theme(request)
31 context['pages'] = range(Post.objects.get_thread_page_count())
31 context['pages'] = range(Post.objects.get_thread_page_count())
32
32
33 return render(request, 'posting_general.html',
33 return render(request, 'posting_general.html',
34 context)
34 context)
35
35
36
36
37 def _new_post(request, form, thread_id=boards.models.NO_PARENT):
37 def _new_post(request, form, thread_id=boards.models.NO_PARENT):
38 """Add a new post (in thread or as a reply)."""
38 """Add a new post (in thread or as a reply)."""
39
39
40 data = form.cleaned_data
40 data = form.cleaned_data
41
41
42 title = data['title']
42 title = data['title']
43 text = data['text']
43 text = data['text']
44
44
45 if 'image' in data.keys():
45 if 'image' in data.keys():
46 image = data['image']
46 image = data['image']
47 else:
47 else:
48 image = None
48 image = None
49
49
50 ip = _get_client_ip(request)
50 ip = _get_client_ip(request)
51
51
52 tags = []
52 tags = []
53
53
54 new_thread = thread_id == boards.models.NO_PARENT
54 new_thread = thread_id == boards.models.NO_PARENT
55 if new_thread:
55 if new_thread:
56 tag_strings = data['tags']
56 tag_strings = data['tags']
57
57
58 if tag_strings:
58 if tag_strings:
59 tag_strings = tag_strings.split(' ')
59 tag_strings = tag_strings.split(' ')
60 for tag_name in tag_strings:
60 for tag_name in tag_strings:
61 tag_name = tag_name.strip()
61 tag_name = tag_name.strip()
62 if len(tag_name) > 0:
62 if len(tag_name) > 0:
63 tag, created = Tag.objects.get_or_create(name=tag_name)
63 tag, created = Tag.objects.get_or_create(name=tag_name)
64 tags.append(tag)
64 tags.append(tag)
65
65
66 # TODO Add a possibility to define a link image instead of an image file.
66 # TODO Add a possibility to define a link image instead of an image file.
67 # If a link is given, download the image automatically.
67 # If a link is given, download the image automatically.
68
68
69 post = Post.objects.create_post(title=title, text=text, ip=ip,
69 post = Post.objects.create_post(title=title, text=text, ip=ip,
70 parent_id=thread_id, image=image,
70 parent_id=thread_id, image=image,
71 tags=tags)
71 tags=tags)
72
72
73 thread_to_show = (post.id if new_thread else thread_id)
73 thread_to_show = (post.id if new_thread else thread_id)
74
74
75 if new_thread:
75 if new_thread:
76 return redirect(thread, post_id=thread_to_show)
76 return redirect(thread, post_id=thread_to_show)
77 else:
77 else:
78 return redirect(reverse(thread,
78 return redirect(reverse(thread,
79 kwargs={'post_id': thread_to_show}) + '#'
79 kwargs={'post_id': thread_to_show}) + '#'
80 + str(post.id))
80 + str(post.id))
81
81
82
82
83 def tag(request, tag_name, page=0):
83 def tag(request, tag_name, page=0):
84 """Get all tag threads (posts without a parent)."""
84 """Get all tag threads (posts without a parent)."""
85
85
86 tag = Tag.objects.get(name=tag_name)
86 tag = get_object_or_404(Tag, name=tag_name)
87 threads = Post.objects.get_threads(tag=tag, page=int(page))
87 threads = Post.objects.get_threads(tag=tag, page=int(page))
88
88
89 if request.method == 'POST':
89 if request.method == 'POST':
90 form = ThreadForm(request.POST, request.FILES,
90 form = ThreadForm(request.POST, request.FILES,
91 error_class=PlainErrorList)
91 error_class=PlainErrorList)
92 if form.is_valid():
92 if form.is_valid():
93 return _new_post(request, form)
93 return _new_post(request, form)
94 else:
94 else:
95 form = forms.ThreadForm(initial={'tags': tag_name},
95 form = forms.ThreadForm(initial={'tags': tag_name},
96 error_class=PlainErrorList)
96 error_class=PlainErrorList)
97
97
98 context = RequestContext(request)
98 context = RequestContext(request)
99 context['threads'] = None if len(threads) == 0 else threads
99 context['threads'] = None if len(threads) == 0 else threads
100 context['tag'] = tag_name
100 context['tag'] = tag_name
101 context['tags'] = Tag.objects.get_popular_tags()
101 context['tags'] = Tag.objects.get_popular_tags()
102 context['theme'] = _get_theme(request)
102 context['theme'] = _get_theme(request)
103 context['pages'] = range(Post.objects.get_thread_page_count(tag=tag))
103 context['pages'] = range(Post.objects.get_thread_page_count(tag=tag))
104
104
105 context['form'] = form
105 context['form'] = form
106
106
107 return render(request, 'posting_general.html',
107 return render(request, 'posting_general.html',
108 context)
108 context)
109
109
110
110
111 def thread(request, post_id):
111 def thread(request, post_id):
112 """Get all thread posts"""
112 """Get all thread posts"""
113
113
114 if request.method == 'POST':
114 if request.method == 'POST':
115 form = PostForm(request.POST, request.FILES,
115 form = PostForm(request.POST, request.FILES,
116 error_class=PlainErrorList)
116 error_class=PlainErrorList)
117 if form.is_valid():
117 if form.is_valid():
118 return _new_post(request, form, post_id)
118 return _new_post(request, form, post_id)
119 else:
119 else:
120 form = forms.PostForm(error_class=PlainErrorList)
120 form = forms.PostForm(error_class=PlainErrorList)
121
121
122 posts = Post.objects.get_thread(post_id)
122 posts = Post.objects.get_thread(post_id)
123
123
124 context = RequestContext(request)
124 context = RequestContext(request)
125
125
126 context['posts'] = posts
126 context['posts'] = posts
127 context['form'] = form
127 context['form'] = form
128 context['tags'] = Tag.objects.get_popular_tags()
128 context['tags'] = Tag.objects.get_popular_tags()
129 context['theme'] = _get_theme(request)
129 context['theme'] = _get_theme(request)
130
130
131 return render(request, 'thread.html', context)
131 return render(request, 'thread.html', context)
132
132
133
133
134 def login(request):
134 def login(request):
135 """Log in as admin"""
135 """Log in as admin"""
136
136
137 if 'name' in request.POST and 'password' in request.POST:
137 if 'name' in request.POST and 'password' in request.POST:
138 request.session['admin'] = False
138 request.session['admin'] = False
139
139
140 isAdmin = len(Admin.objects.filter(name=request.POST['name'],
140 isAdmin = len(Admin.objects.filter(name=request.POST['name'],
141 password=request.POST[
141 password=request.POST[
142 'password'])) > 0
142 'password'])) > 0
143
143
144 if isAdmin:
144 if isAdmin:
145 request.session['admin'] = True
145 request.session['admin'] = True
146
146
147 response = HttpResponseRedirect('/')
147 response = HttpResponseRedirect('/')
148
148
149 else:
149 else:
150 response = render(request, 'login.html', {'error': 'Login error'})
150 response = render(request, 'login.html', {'error': 'Login error'})
151 else:
151 else:
152 response = render(request, 'login.html', {})
152 response = render(request, 'login.html', {})
153
153
154 return response
154 return response
155
155
156
156
157 def logout(request):
157 def logout(request):
158 request.session['admin'] = False
158 request.session['admin'] = False
159 return HttpResponseRedirect('/')
159 return HttpResponseRedirect('/')
160
160
161
161
162 def settings(request):
162 def settings(request):
163 context = RequestContext(request)
163 context = RequestContext(request)
164
164
165 if request.method == 'POST':
165 if request.method == 'POST':
166 form = SettingsForm(request.POST)
166 form = SettingsForm(request.POST)
167 if form.is_valid():
167 if form.is_valid():
168 selected_theme = form.cleaned_data['theme']
168 selected_theme = form.cleaned_data['theme']
169 request.session['theme'] = selected_theme
169 request.session['theme'] = selected_theme
170
170
171 return redirect(settings)
171 return redirect(settings)
172 else:
172 else:
173 selected_theme = _get_theme(request)
173 selected_theme = _get_theme(request)
174 form = SettingsForm(initial={'theme': selected_theme})
174 form = SettingsForm(initial={'theme': selected_theme})
175 context['form'] = form
175 context['form'] = form
176 context['tags'] = Tag.objects.get_popular_tags()
176 context['tags'] = Tag.objects.get_popular_tags()
177 context['theme'] = _get_theme(request)
177 context['theme'] = _get_theme(request)
178
178
179 return render(request, 'settings.html', context)
179 return render(request, 'settings.html', context)
180
180
181
181
182 def all_tags(request):
182 def all_tags(request):
183 context = RequestContext(request)
183 context = RequestContext(request)
184 context['tags'] = Tag.objects.get_popular_tags()
184 context['tags'] = Tag.objects.get_popular_tags()
185 context['theme'] = _get_theme(request)
185 context['theme'] = _get_theme(request)
186 context['all_tags'] = Tag.objects.get_not_empty_tags()
186 context['all_tags'] = Tag.objects.get_not_empty_tags()
187
187
188 return render(request, 'tags.html', context)
188 return render(request, 'tags.html', context)
189
189
190
190
191 def _get_theme(request):
191 def _get_theme(request):
192 return request.session.get('theme', neboard.settings.DEFAULT_THEME)
192 return request.session.get('theme', neboard.settings.DEFAULT_THEME)
193
193
194
194
195 def _get_client_ip(request):
195 def _get_client_ip(request):
196 x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
196 x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
197 if x_forwarded_for:
197 if x_forwarded_for:
198 ip = x_forwarded_for.split(',')[-1].strip()
198 ip = x_forwarded_for.split(',')[-1].strip()
199 else:
199 else:
200 ip = request.META.get('REMOTE_ADDR')
200 ip = request.META.get('REMOTE_ADDR')
201 return ip
201 return ip
General Comments 0
You need to be logged in to leave comments. Login now