##// END OF EJS Templates
Added thread field to the post. 'parent' field is deprecated now.
neko259 -
r174:481c6224 default
parent child Browse files
Show More
@@ -0,0 +1,72 b''
1 # -*- coding: utf-8 -*-
2 import datetime
3 from south.db import db
4 from south.v2 import SchemaMigration
5 from django.db import models
6
7
8 class Migration(SchemaMigration):
9
10 def forwards(self, orm):
11 # Adding field 'Post.thread'
12 db.add_column(u'boards_post', 'thread',
13 self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['boards.Post'], null=True),
14 keep_default=False)
15
16
17 def backwards(self, orm):
18 # Deleting field 'Post.thread'
19 db.delete_column(u'boards_post', 'thread_id')
20
21
22 models = {
23 u'boards.ban': {
24 'Meta': {'object_name': 'Ban'},
25 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
26 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'})
27 },
28 u'boards.post': {
29 'Meta': {'object_name': 'Post'},
30 '_text_rendered': ('django.db.models.fields.TextField', [], {}),
31 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
32 'image': ('boards.thumbs.ImageWithThumbsField', [], {'max_length': '100', 'blank': 'True'}),
33 'image_height': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
34 'image_width': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
35 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}),
36 'parent': ('django.db.models.fields.BigIntegerField', [], {}),
37 'poster_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
38 'poster_user_agent': ('django.db.models.fields.TextField', [], {}),
39 'pub_time': ('django.db.models.fields.DateTimeField', [], {}),
40 'replies': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'re+'", 'null': 'True', 'symmetrical': 'False', 'to': u"orm['boards.Post']"}),
41 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['boards.Tag']", 'symmetrical': 'False'}),
42 'text': ('markupfield.fields.MarkupField', [], {'rendered_field': 'True'}),
43 'text_markup_type': ('django.db.models.fields.CharField', [], {'default': "'markdown'", 'max_length': '30'}),
44 'thread': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['boards.Post']", 'null': 'True'}),
45 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
46 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['boards.User']", 'null': 'True'})
47 },
48 u'boards.setting': {
49 'Meta': {'object_name': 'Setting'},
50 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
51 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
52 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['boards.User']"}),
53 'value': ('django.db.models.fields.CharField', [], {'max_length': '50'})
54 },
55 u'boards.tag': {
56 'Meta': {'object_name': 'Tag'},
57 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
58 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
59 },
60 u'boards.user': {
61 'Meta': {'object_name': 'User'},
62 'fav_tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['boards.Tag']", 'null': 'True', 'blank': 'True'}),
63 'fav_threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'symmetrical': 'False', 'to': u"orm['boards.Post']"}),
64 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
65 'last_access_time': ('django.db.models.fields.DateTimeField', [], {}),
66 'rank': ('django.db.models.fields.IntegerField', [], {}),
67 'registration_time': ('django.db.models.fields.DateTimeField', [], {}),
68 'user_id': ('django.db.models.fields.CharField', [], {'max_length': '50'})
69 }
70 }
71
72 complete_apps = ['boards'] No newline at end of file
@@ -1,332 +1,341 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.db.models import Count
9 9 from django.http import Http404
10 10 from django.utils import timezone
11 11 from markupfield.fields import MarkupField
12 12
13 13 from neboard import settings
14 14 import thumbs
15 15
16 16 IMAGE_THUMB_SIZE = (200, 150)
17 17
18 18 TITLE_MAX_LENGTH = 50
19 19
20 20 DEFAULT_MARKUP_TYPE = 'markdown'
21 21
22 22 NO_PARENT = -1
23 23 NO_IP = '0.0.0.0'
24 24 UNKNOWN_UA = ''
25 25 ALL_PAGES = -1
26 26 OPENING_POST_POPULARITY_WEIGHT = 2
27 27 IMAGES_DIRECTORY = 'images/'
28 28 FILE_EXTENSION_DELIMITER = '.'
29 29
30 30 RANK_ADMIN = 0
31 31 RANK_MODERATOR = 10
32 32 RANK_USER = 100
33 33
34 34
35 35 class PostManager(models.Manager):
36 def create_post(self, title, text, image=None, parent_id=NO_PARENT,
36 def create_post(self, title, text, image=None, thread=None,
37 37 ip=NO_IP, tags=None, user=None):
38 38 post = self.create(title=title,
39 39 text=text,
40 40 pub_time=timezone.now(),
41 parent=parent_id,
41 thread=thread,
42 42 image=image,
43 43 poster_ip=ip,
44 44 poster_user_agent=UNKNOWN_UA,
45 45 last_edit_time=timezone.now(),
46 46 user=user)
47 47
48 if parent_id != NO_PARENT:
49 parent = self.get(id=parent_id)
50 parent.replies.add(post)
48 if thread:
49 thread.replies.add(post)
51 50
52 51 if tags:
53 52 map(post.tags.add, tags)
54 53
55 if parent_id != NO_PARENT:
56 self._bump_thread(parent_id)
54 if thread:
55 thread.bump()
57 56 else:
58 57 self._delete_old_threads()
59 58
60 59 return post
61 60
62 61 def delete_post(self, post):
63 62 if post.replies.count() > 0:
64 map(self.delete_post, post.replies)
63 map(self.delete_post, post.replies.all())
65 64 post.delete()
66 65
67 66 def delete_posts_by_ip(self, ip):
68 67 posts = self.filter(poster_ip=ip)
69 68 map(self.delete_post, posts)
70 69
71 70 def get_threads(self, tag=None, page=ALL_PAGES,
72 71 order_by='-last_edit_time'):
73 72 if tag:
74 threads = self.filter(parent=NO_PARENT, tags=tag)
73 threads = self.filter(thread=None, tags=tag)
74
75 # TODO This needs to be uncommented when 'all tags' view won't
76 # use this method to get threads for tag
75 77
76 # TODO Throw error 404 if no threads for tag found?
78 # if threads.count() == 0:
79 # raise Http404
77 80 else:
78 threads = self.filter(parent=NO_PARENT)
81 threads = self.filter(thread=None)
79 82
80 83 threads = threads.order_by(order_by)
81 84
82 85 if page != ALL_PAGES:
83 86 thread_count = threads.count()
84 87
85 88 if page < self.get_thread_page_count(tag=tag):
86 89 start_thread = page * settings.THREADS_PER_PAGE
87 90 end_thread = min(start_thread + settings.THREADS_PER_PAGE,
88 91 thread_count)
89 92 threads = threads[start_thread:end_thread]
90 93
91 94 return threads
92 95
93 96 def get_thread(self, opening_post_id):
94 97 try:
95 opening_post = self.get(id=opening_post_id, parent=NO_PARENT)
98 opening_post = self.get(id=opening_post_id, thread=None)
96 99 except Post.DoesNotExist:
97 100 raise Http404
98 101
99 102 if opening_post.replies:
100 103 thread = [opening_post]
101 104 thread.extend(opening_post.replies.all())
102 105
103 106 return thread
104 107
105 108 def exists(self, post_id):
106 109 posts = self.filter(id=post_id)
107 110
108 111 return posts.count() > 0
109 112
110 113 def get_thread_page_count(self, tag=None):
111 114 if tag:
112 threads = self.filter(parent=NO_PARENT, tags=tag)
115 threads = self.filter(thread=None, tags=tag)
113 116 else:
114 threads = self.filter(parent=NO_PARENT)
117 threads = self.filter(thread=None)
115 118
116 119 return int(math.ceil(threads.count() / float(
117 120 settings.THREADS_PER_PAGE)))
118 121
119 122 def _delete_old_threads(self):
120 123 """
121 124 Preserves maximum thread count. If there are too many threads,
122 125 delete the old ones.
123 126 """
124 127
125 128 # TODO Move old threads to the archive instead of deleting them.
126 129 # Maybe make some 'old' field in the model to indicate the thread
127 130 # must not be shown and be able for replying.
128 131
129 132 threads = self.get_threads()
130 133 thread_count = len(threads)
131 134
132 135 if thread_count > settings.MAX_THREAD_COUNT:
133 136 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
134 137 old_threads = threads[thread_count - num_threads_to_delete:]
135 138
136 139 map(self.delete_post, old_threads)
137 140
138 141 def _bump_thread(self, thread_id):
139 142 thread = self.get(id=thread_id)
140 143
141 144 if thread.can_bump():
142 145 thread.last_edit_time = timezone.now()
143 146 thread.save()
144 147
145 148
146 149 class TagManager(models.Manager):
147 150 def get_not_empty_tags(self):
148 151 all_tags = self.all().order_by('name')
149 152 tags = []
150 153 for tag in all_tags:
151 154 if not tag.is_empty():
152 155 tags.append(tag)
153 156
154 157 return tags
155 158
156 159 def get_popular_tags(self):
157 160 all_tags = self.get_not_empty_tags()
158 161
159 162 sorted_tags = sorted(all_tags, key=lambda tag: tag.get_popularity(),
160 163 reverse=True)
161 164
162 165 return sorted_tags[:settings.POPULAR_TAGS]
163 166
164 167
165 168 class Tag(models.Model):
166 169 """
167 170 A tag is a text node assigned to the post. The tag serves as a board
168 171 section. There can be multiple tags for each message
169 172 """
170 173
171 174 objects = TagManager()
172 175
173 176 name = models.CharField(max_length=100)
174 177
175 178 def __unicode__(self):
176 179 return self.name
177 180
178 181 def is_empty(self):
179 182 return self.get_post_count() == 0
180 183
181 184 def get_post_count(self):
182 185 posts_with_tag = Post.objects.get_threads(tag=self)
183 186 return posts_with_tag.count()
184 187
185 188 def get_popularity(self):
186 189 posts_with_tag = Post.objects.get_threads(tag=self)
187 190 reply_count = 0
188 191 for post in posts_with_tag:
189 192 reply_count += post.get_reply_count()
190 193 reply_count += OPENING_POST_POPULARITY_WEIGHT
191 194
192 195 return reply_count
193 196
194 197
195 198 class Post(models.Model):
196 199 """A post is a message."""
197 200
198 201 objects = PostManager()
199 202
200 203 def _update_image_filename(self, filename):
201 204 """Get unique image filename"""
202 205
203 206 path = IMAGES_DIRECTORY
204 207 new_name = str(int(time.mktime(time.gmtime())))
205 208 new_name += str(int(random() * 1000))
206 209 new_name += FILE_EXTENSION_DELIMITER
207 210 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
208 211
209 212 return os.path.join(path, new_name)
210 213
211 214 title = models.CharField(max_length=TITLE_MAX_LENGTH)
212 215 pub_time = models.DateTimeField()
213 216 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
214 217 escape_html=False)
215 218
216 219 image_width = models.IntegerField(default=0)
217 220 image_height = models.IntegerField(default=0)
218 221
219 222 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
220 223 blank=True, sizes=(IMAGE_THUMB_SIZE,),
221 224 width_field='image_width',
222 225 height_field='image_height')
223 226
224 227 poster_ip = models.GenericIPAddressField()
225 228 poster_user_agent = models.TextField()
226 229
227 230 # TODO Convert this field to ForeignKey
228 parent = models.BigIntegerField()
231 parent = models.BigIntegerField(default=NO_PARENT)
229 232
233 thread = models.ForeignKey('Post', null=True, default=None)
230 234 tags = models.ManyToManyField(Tag)
231 235 last_edit_time = models.DateTimeField()
232 236 user = models.ForeignKey('User', null=True, default=None)
233 237
234 238 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
235 239 blank=True, related_name='re+')
236 240
237 241 def __unicode__(self):
238 242 return '#' + str(self.id) + ' ' + self.title + ' (' + \
239 243 self.text.raw[:50] + ')'
240 244
241 245 def get_title(self):
242 246 title = self.title
243 247 if len(title) == 0:
244 248 title = self.text.raw[:20]
245 249
246 250 return title
247 251
248 252 def _get_replies(self):
249 253 return self.replies
250 254
251 255 def get_reply_count(self):
252 256 return self.replies.count()
253 257
254 258 def get_images_count(self):
255 259 images_count = 1 if self.image else 0
256 260
257 261 for reply in self.replies:
258 262 if reply.image:
259 263 images_count += 1
260 264
261 265 return images_count
262 266
263 267 def can_bump(self):
264 268 """Check if the thread can be bumped by replying"""
265 269
266 270 post_count = self.get_reply_count() + 1
267 271
268 272 return post_count <= settings.MAX_POSTS_PER_THREAD
269 273
270 def get_last_replies(self):
274 def bump(self):
275 if self.can_bump():
276 self.last_edit_time = timezone.now()
277 self.save()
278
279 def get_last_replies(self):
271 280 if settings.LAST_REPLIES_COUNT > 0:
272 281 reply_count = self.get_reply_count()
273 282
274 283 if reply_count > 0:
275 284 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
276 285 reply_count)
277 286 last_replies = self.replies.all()[reply_count -
278 287 reply_count_to_show:]
279 288
280 289 return last_replies
281 290
282 291
283 292 class User(models.Model):
284 293
285 294 user_id = models.CharField(max_length=50)
286 295 rank = models.IntegerField()
287 296
288 297 registration_time = models.DateTimeField()
289 298 last_access_time = models.DateTimeField()
290 299
291 300 fav_tags = models.ManyToManyField(Tag, null=True, blank=True)
292 301 fav_threads = models.ManyToManyField(Post, related_name='+', null=True,
293 302 blank=True)
294 303
295 304 def save_setting(self, name, value):
296 305 setting, created = Setting.objects.get_or_create(name=name, user=self)
297 306 setting.value = value
298 307 setting.save()
299 308
300 309 return setting
301 310
302 311 def get_setting(self, name):
303 312 if Setting.objects.filter(name=name, user=self).exists():
304 313 setting = Setting.objects.get(name=name, user=self)
305 314 setting_value = setting.value
306 315 else:
307 316 setting_value = None
308 317
309 318 return setting_value
310 319
311 320 def is_moderator(self):
312 321 return RANK_MODERATOR >= self.rank
313 322
314 323 def get_sorted_fav_tags(self):
315 324 return self.fav_tags.order_by('name')
316 325
317 326 def __unicode__(self):
318 327 return self.user_id + '(' + str(self.rank) + ')'
319 328
320 329
321 330 class Setting(models.Model):
322 331
323 332 name = models.CharField(max_length=50)
324 333 value = models.CharField(max_length=50)
325 334 user = models.ForeignKey(User)
326 335
327 336
328 337 class Ban(models.Model):
329 338 ip = models.GenericIPAddressField()
330 339
331 340 def __unicode__(self):
332 341 return self.ip
@@ -1,48 +1,50 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 <link rel="alternate" type="application/rss+xml" href="rss/" title="
12 12 {% trans 'Feed' %}"/>
13 13
14 14 <link rel="icon" type="image/png"
15 15 href="{{ STATIC_URL }}favicon.png">
16 16
17 17 <meta name="viewport" content="width=device-width, initial-scale=1"/>
18 18 <meta charset="utf-8"/>
19 19 {% block head %}{% endblock %}
20 20 </head>
21 21 <body>
22 22 <script src="{{ STATIC_URL }}js/jquery-2.0.1.min.js"></script>
23 23 <script src="{{ STATIC_URL }}js/jquery.fancybox.pack.js"></script>
24 24 <script src="{% url 'django.views.i18n.javascript_catalog' %}"></script>
25 25 <script src="{{ STATIC_URL }}js/refmaps.js"></script>
26 26 <script src="{{ STATIC_URL }}js/main.js"></script>
27 27
28 28 <div class="navigation_panel">
29 29 <a class="link" href="{% url 'index' %}">{% trans "All threads" %}</a>
30 30 {% for tag in tags %}
31 <a class="tag" href="{% url 'tag' tag_name=tag.name %}"
32 >{{ tag.name }}</a>
31 {% if not tag.is_empty %}
32 <a class="tag" href="{% url 'tag' tag_name=tag.name %}"
33 >{{ tag.name }}</a>
34 {% endif %}
33 35 {% endfor %}
34 36 <a class="tag" href="{% url 'tags' %}" alt="{% trans 'Tag management' %}"
35 37 >[...]</a>
36 38 <a class="link" href="{% url 'settings' %}">{% trans 'Settings' %}</a>
37 39 </div>
38 40
39 41 {% block content %}{% endblock %}
40 42
41 43 <div class="navigation_panel">
42 44 {% block metapanel %}{% endblock %}
43 45 [<a href="{% url "login" %}">{% trans 'Login' %}</a>]
44 46 <a class="link" href="#top">{% trans 'Up' %}</a>
45 47 </div>
46 48
47 49 </body>
48 50 </html>
@@ -1,181 +1,172 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, 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 def _create_test_user(self):
51 admin = Admin(name='test_username12313584353165',
52 password='test_userpassword135135512')
53
54 admin.save()
55 return admin
56
57 50 def test_get_thread(self):
58 51 opening_post = self._create_post()
59 op_id = opening_post.id
60 52
61 53 for i in range(0, 2):
62 Post.objects.create_post('title', 'text',
63 parent_id=op_id)
54 Post.objects.create_post('title', 'text',thread=opening_post)
64 55
65 thread = Post.objects.get_thread(op_id)
56 thread = Post.objects.get_thread(opening_post.id)
66 57
67 58 self.assertEqual(3, len(thread))
68 59
69 60 def test_create_post_with_tag(self):
70 61 tag = Tag.objects.create(name='test_tag')
71 62 post = Post.objects.create_post(title='title', text='text', tags=[tag])
72 63 self.assertIsNotNone(post)
73 64
74 65 def test_thread_max_count(self):
75 66 for i in range(settings.MAX_THREAD_COUNT + 1):
76 67 self._create_post()
77 68
78 69 self.assertEqual(settings.MAX_THREAD_COUNT,
79 70 len(Post.objects.get_threads()))
80 71
81 72 def test_pages(self):
82 73 """Test that the thread list is properly split into pages"""
83 74
84 75 for i in range(settings.MAX_THREAD_COUNT):
85 76 self._create_post()
86 77
87 78 all_threads = Post.objects.get_threads()
88 79
89 80 posts_in_second_page = Post.objects.get_threads(page=1)
90 81 first_post = posts_in_second_page[0]
91 82
92 83 self.assertEqual(all_threads[settings.THREADS_PER_PAGE].id,
93 84 first_post.id)
94 85
95 86 def test_post_validation(self):
96 87 """Test the validation of the post form"""
97 88
98 89 # Disable captcha for the test
99 90 captcha_enabled = settings.ENABLE_CAPTCHA
100 91 settings.ENABLE_CAPTCHA = False
101 92
102 93 Post.objects.all().delete()
103 94
104 95 client = Client()
105 96
106 97 valid_tags = u'tag1 tag_2 Ρ‚Π΅Π³_3'
107 98 invalid_tags = u'$%_356 ---'
108 99
109 100 response = client.post(NEW_THREAD_PAGE, {'title': 'test title',
110 101 'text': TEST_TEXT,
111 102 'tags': valid_tags})
112 103 self.assertEqual(response.status_code, HTTP_CODE_REDIRECT,
113 104 msg='Posting new message failed: got code ' +
114 105 str(response.status_code))
115 106
116 107 self.assertEqual(1, Post.objects.count(),
117 108 msg='No posts were created')
118 109
119 110 client.post(NEW_THREAD_PAGE, {'text': TEST_TEXT,
120 111 'tags': invalid_tags})
121 112 self.assertEqual(1, Post.objects.count(), msg='The validation passed '
122 113 'where it should fail')
123 114
124 115 # TODO Some workaround and test for the "waiting" validation should
125 116 # exist here
126 117 response = client.post(THREAD_PAGE_ONE, {'text': TEST_TEXT,
127 118 'tags': valid_tags})
128 119 self.assertEqual(HTTP_CODE_REDIRECT, response.status_code,
129 120 msg=u'Posting new message failed: got code ' +
130 121 str(response.status_code))
131 122
132 123 self.assertEqual(2, Post.objects.count(),
133 124 msg=u'No posts were created')
134 125
135 126 # Restore captcha setting
136 127 settings.ENABLE_CAPTCHA = captcha_enabled
137 128
138 129 # TODO This test fails for now. We must check for 404.html instead of
139 130 # code 404
140 131 def test_404(self):
141 132 """Test receiving error 404 when opening a non-existent page"""
142 133
143 134 Post.objects.all().delete()
144 135 Tag.objects.all().delete()
145 136
146 137 tag_name = u'test_tag'
147 138 tags, = [Tag.objects.get_or_create(name=tag_name)]
148 139 client = Client()
149 140
150 141 Post.objects.create_post('title', TEST_TEXT, tags=tags)
151 142
152 143 existing_post_id = Post.objects.all()[0].id
153 144 response_existing = client.get(THREAD_PAGE + str(existing_post_id) +
154 145 '/')
155 146 self.assertEqual(HTTP_CODE_OK, response_existing.status_code,
156 147 u'Cannot open existing thread')
157 148
158 149 response_not_existing = client.get(THREAD_PAGE + str(
159 150 existing_post_id + 1) + '/')
160 151 response_not_existing.get_full_path()
161 152 self.assertEqual(HTTP_CODE_NOT_FOUND,
162 153 response_not_existing.status_code,
163 154 u'Not existing thread is opened')
164 155
165 156 response_existing = client.get(TAG_PAGE + tag_name + '/')
166 157 self.assertEqual(HTTP_CODE_OK,
167 158 response_existing.status_code,
168 159 u'Cannot open existing tag')
169 160
170 161 response_not_existing = client.get(TAG_PAGE + u'not_tag' + '/')
171 162 self.assertEqual(HTTP_CODE_NOT_FOUND,
172 163 response_not_existing.status_code,
173 164 u'Not existing tag is opened')
174 165
175 166 reply_id = Post.objects.create_post('', TEST_TEXT,
176 167 parent_id=existing_post_id)
177 168 response_not_existing = client.get(THREAD_PAGE + str(
178 169 reply_id) + '/')
179 170 self.assertEqual(HTTP_CODE_NOT_FOUND,
180 171 response_not_existing.status_code,
181 172 u'Not existing thread is opened')
@@ -1,359 +1,358 b''
1 1 import hashlib
2 2 from django.core.urlresolvers import reverse
3 3 from django.http import HttpResponseRedirect
4 4 from django.template import RequestContext
5 5 from django.shortcuts import render, redirect, get_object_or_404
6 6 from django.utils import timezone
7 7
8 8 from boards import forms
9 9 import boards
10 10 from boards import utils
11 11 from boards.forms import ThreadForm, PostForm, SettingsForm, PlainErrorList, \
12 12 ThreadCaptchaForm, PostCaptchaForm, LoginForm
13 13
14 14 from boards.models import Post, Tag, Ban, User, RANK_USER, NO_PARENT
15 15 from boards import authors
16 16 import neboard
17 17
18 18
19 19 def index(request, page=0):
20 20 context = _init_default_context(request)
21 21
22 22 if utils.need_include_captcha(request):
23 23 threadFormClass = ThreadCaptchaForm
24 24 kwargs = {'request': request}
25 25 else:
26 26 threadFormClass = ThreadForm
27 27 kwargs = {}
28 28
29 29 if request.method == 'POST':
30 30 form = threadFormClass(request.POST, request.FILES,
31 31 error_class=PlainErrorList, **kwargs)
32 32 form.session = request.session
33 33
34 34 if form.is_valid():
35 35 return _new_post(request, form)
36 36 else:
37 37 form = threadFormClass(error_class=PlainErrorList, **kwargs)
38 38
39 39 threads = []
40 40 for thread in Post.objects.get_threads(page=int(page)):
41 41 threads.append({'thread': thread,
42 42 'bumpable': thread.can_bump()})
43 43
44 44 context['threads'] = None if len(threads) == 0 else threads
45 45 context['form'] = form
46 46 context['pages'] = range(Post.objects.get_thread_page_count())
47 47
48 48 return render(request, 'boards/posting_general.html',
49 49 context)
50 50
51 51
52 52 def _new_post(request, form, thread_id=boards.models.NO_PARENT):
53 53 """Add a new post (in thread or as a reply)."""
54 54
55 55 ip = _get_client_ip(request)
56 56 is_banned = Ban.objects.filter(ip=ip).count() > 0
57 57
58 58 if is_banned:
59 59 return redirect(you_are_banned)
60 60
61 61 data = form.cleaned_data
62 62
63 63 title = data['title']
64 64 text = data['text']
65 65
66 66 if 'image' in data.keys():
67 67 image = data['image']
68 68 else:
69 69 image = None
70 70
71 71 tags = []
72 72
73 73 new_thread = thread_id == boards.models.NO_PARENT
74 74 if new_thread:
75 75 tag_strings = data['tags']
76 76
77 77 if tag_strings:
78 78 tag_strings = tag_strings.split(' ')
79 79 for tag_name in tag_strings:
80 80 tag_name = tag_name.strip()
81 81 if len(tag_name) > 0:
82 82 tag, created = Tag.objects.get_or_create(name=tag_name)
83 83 tags.append(tag)
84 84
85 85 # TODO Add a possibility to define a link image instead of an image file.
86 86 # If a link is given, download the image automatically.
87 87
88 op = None if thread_id == boards.models.NO_PARENT else \
89 get_object_or_404(Post, id=thread_id)
88 90 post = Post.objects.create_post(title=title, text=text, ip=ip,
89 parent_id=thread_id, image=image,
91 thread=op, image=image,
90 92 tags=tags)
91 93
92 94 thread_to_show = (post.id if new_thread else thread_id)
93 95
94 96 if new_thread:
95 97 return redirect(thread, post_id=thread_to_show)
96 98 else:
97 return redirect(reverse(thread,
98 kwargs={'post_id': thread_to_show}) + '#'
99 + str(post.id))
99 return redirect(reverse(thread, kwargs={'post_id': thread_to_show}) +
100 '#' + str(post.id))
100 101
101 102
102 103 def tag(request, tag_name, page=0):
103 104 """Get all tag threads (posts without a parent)."""
104 105
105 106 tag = get_object_or_404(Tag, name=tag_name)
106 107 threads = []
107 108 for thread in Post.objects.get_threads(tag=tag, page=int(page)):
108 109 threads.append({'thread': thread,
109 110 'bumpable': thread.can_bump()})
110 111
111 112 if request.method == 'POST':
112 113 form = ThreadForm(request.POST, request.FILES,
113 114 error_class=PlainErrorList)
114 115 if form.is_valid():
115 116 return _new_post(request, form)
116 117 else:
117 118 form = forms.ThreadForm(initial={'tags': tag_name},
118 119 error_class=PlainErrorList)
119 120
120 121 context = _init_default_context(request)
121 122 context['threads'] = None if len(threads) == 0 else threads
122 123 context['tag'] = tag_name
123 124 context['pages'] = range(Post.objects.get_thread_page_count(tag=tag))
124 125
125 126 context['form'] = form
126 127
127 128 return render(request, 'boards/posting_general.html',
128 129 context)
129 130
130 131
131 132 def thread(request, post_id):
132 133 """Get all thread posts"""
133 134
134 135 if utils.need_include_captcha(request):
135 136 postFormClass = PostCaptchaForm
136 137 kwargs = {'request': request}
137 138 else:
138 139 postFormClass = PostForm
139 140 kwargs = {}
140 141
141 142 if request.method == 'POST':
142 143 form = postFormClass(request.POST, request.FILES,
143 144 error_class=PlainErrorList, **kwargs)
144 145 form.session = request.session
145 146
146 147 if form.is_valid():
147 148 return _new_post(request, form, post_id)
148 149 else:
149 150 form = postFormClass(error_class=PlainErrorList, **kwargs)
150 151
151 152 posts = Post.objects.get_thread(post_id)
152 153
153 154 context = _init_default_context(request)
154 155
155 156 context['posts'] = posts
156 157 context['form'] = form
157 158 context['bumpable'] = posts[0].can_bump()
158 159
159 160 return render(request, 'boards/thread.html', context)
160 161
161 162
162 163 def login(request):
163 164 """Log in with user id"""
164 165
165 166 context = _init_default_context(request)
166 167
167 168 if request.method == 'POST':
168 169 form = LoginForm(request.POST, request.FILES,
169 170 error_class=PlainErrorList)
170 171 if form.is_valid():
171 172 user = User.objects.get(user_id=form.cleaned_data['user_id'])
172 173 request.session['user_id'] = user.id
173 174 return redirect(index)
174 175
175 176 else:
176 177 form = LoginForm()
177 178
178 179 context['form'] = form
179 180
180 181 return render(request, 'boards/login.html', context)
181 182
182 183
183 184 def settings(request):
184 185 """User's settings"""
185 186
186 187 context = _init_default_context(request)
187 188
188 189 if request.method == 'POST':
189 190 form = SettingsForm(request.POST)
190 191 if form.is_valid():
191 192 selected_theme = form.cleaned_data['theme']
192 193
193 194 user = _get_user(request)
194 195 user.save_setting('theme', selected_theme)
195 196
196 197 return redirect(settings)
197 198 else:
198 199 selected_theme = _get_theme(request)
199 200 form = SettingsForm(initial={'theme': selected_theme})
200 201 context['form'] = form
201 202
202 203 return render(request, 'boards/settings.html', context)
203 204
204 205
205 206 def all_tags(request):
206 207 """All tags list"""
207 208
208 209 context = _init_default_context(request)
209 210 context['all_tags'] = Tag.objects.get_not_empty_tags()
210 211
211 212 return render(request, 'boards/tags.html', context)
212 213
213 214
214 215 def jump_to_post(request, post_id):
215 216 """Determine thread in which the requested post is and open it's page"""
216 217
217 218 post = get_object_or_404(Post, id=post_id)
218 219
219 if boards.models.NO_PARENT == post.parent:
220 if not post.thread:
220 221 return redirect(thread, post_id=post.id)
221 222 else:
222 # TODO Change this code to not use 'parent' field anymore
223 parent_thread = get_object_or_404(Post, id=post.parent)
224 return redirect(reverse(thread, kwargs={'post_id': parent_thread.id})
223 return redirect(reverse(thread, kwargs={'post_id': post.thread.id})
225 224 + '#' + str(post.id))
226 225
227 226
228 227 def authors(request):
229 228 context = _init_default_context(request)
230 229 context['authors'] = boards.authors.authors
231 230
232 231 return render(request, 'boards/authors.html', context)
233 232
234 233
235 234 def delete(request, post_id):
236 235 user = _get_user(request)
237 236 post = get_object_or_404(Post, id=post_id)
238 237
239 238 if user.is_moderator():
240 239 # TODO Show confirmation page before deletion
241 240 Post.objects.delete_post(post)
242 241
243 if NO_PARENT == post.parent:
242 if not post.thread:
244 243 return _redirect_to_next(request)
245 244 else:
246 return redirect(thread, post_id=post.parent)
245 return redirect(thread, post_id=post.thread.id)
247 246
248 247
249 248 def ban(request, post_id):
250 249 user = _get_user(request)
251 250 post = get_object_or_404(Post, id=post_id)
252 251
253 252 if user.is_moderator():
254 253 # TODO Show confirmation page before ban
255 254 Ban.objects.get_or_create(ip=post.poster_ip)
256 255
257 256 return _redirect_to_next(request)
258 257
259 258
260 259 def you_are_banned(request):
261 260 context = _init_default_context(request)
262 261 return render(request, 'boards/staticpages/banned.html', context)
263 262
264 263
265 264 def page_404(request):
266 265 context = _init_default_context(request)
267 266 return render(request, 'boards/404.html', context)
268 267
269 268
270 269 def tag_subscribe(request, tag_name):
271 270 user = _get_user(request)
272 271 tag = get_object_or_404(Tag, name=tag_name)
273 272
274 273 if not tag in user.fav_tags.all():
275 274 user.fav_tags.add(tag)
276 275
277 276 return redirect(all_tags)
278 277
279 278
280 279 def tag_unsubscribe(request, tag_name):
281 280 user = _get_user(request)
282 281 tag = get_object_or_404(Tag, name=tag_name)
283 282
284 283 if tag in user.fav_tags.all():
285 284 user.fav_tags.remove(tag)
286 285
287 286 return redirect(all_tags)
288 287
289 288
290 289 def static_page(request, name):
291 290 context = _init_default_context(request)
292 291 return render(request, 'boards/staticpages/' + name + '.html', context)
293 292
294 293
295 294 def _get_theme(request, user=None):
296 295 """Get user's CSS theme"""
297 296
298 297 if not user:
299 298 user = _get_user(request)
300 299 theme = user.get_setting('theme')
301 300 if not theme:
302 301 theme = neboard.settings.DEFAULT_THEME
303 302
304 303 return theme
305 304
306 305
307 306 def _get_client_ip(request):
308 307 x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
309 308 if x_forwarded_for:
310 309 ip = x_forwarded_for.split(',')[-1].strip()
311 310 else:
312 311 ip = request.META.get('REMOTE_ADDR')
313 312 return ip
314 313
315 314
316 315 def _init_default_context(request):
317 316 """Create context with default values that are used in most views"""
318 317
319 318 context = RequestContext(request)
320 319
321 320 user = _get_user(request)
322 321 context['user'] = user
323 322 context['tags'] = user.get_sorted_fav_tags()
324 323 context['theme'] = _get_theme(request, user)
325 324
326 325 return context
327 326
328 327
329 328 def _get_user(request):
330 329 """Get current user from the session"""
331 330
332 331 session = request.session
333 332 if not 'user_id' in session:
334 333 request.session.save()
335 334
336 335 md5 = hashlib.md5()
337 336 md5.update(session.session_key)
338 337 new_id = md5.hexdigest()
339 338
340 339 time_now = timezone.now()
341 340 user = User.objects.create(user_id=new_id, rank=RANK_USER,
342 341 registration_time=time_now,
343 342 last_access_time=time_now)
344 343
345 344 session['user_id'] = user.id
346 345 else:
347 346 user = User.objects.get(id=session['user_id'])
348 347 user.last_access_time = timezone.now()
349 348 user.save()
350 349
351 350 return user
352 351
353 352
354 353 def _redirect_to_next(request):
355 354 if 'next' in request.GET:
356 355 next_page = request.GET['next']
357 356 return HttpResponseRedirect(next_page)
358 357 else:
359 358 return redirect(index)
General Comments 0
You need to be logged in to leave comments. Login now