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