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