##// END OF EJS Templates
Speed up tags ordering in the navigation panel.
neko259 -
r149:71e2d696 default
parent child Browse files
Show More
@@ -1,326 +1,329 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 from threading import Thread
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 36 def create_post(self, title, text, image=None, parent_id=NO_PARENT,
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 41 parent=parent_id,
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 48 if tags:
49 49 map(post.tags.add, tags)
50 50
51 51 if parent_id != NO_PARENT:
52 52 self._bump_thread(parent_id)
53 53 else:
54 54 self._delete_old_threads()
55 55
56 56 return post
57 57
58 58 def delete_post(self, post):
59 59 children = self.filter(parent=post.id)
60 60
61 61 map(self.delete_post, children)
62 62 post.delete()
63 63
64 64 def delete_posts_by_ip(self, ip):
65 65 posts = self.filter(poster_ip=ip)
66 66 map(self.delete_post, posts)
67 67
68 68 def get_threads(self, tag=None, page=ALL_PAGES,
69 69 order_by='-last_edit_time'):
70 70 if tag:
71 71 threads = self.filter(parent=NO_PARENT, tags=tag)
72 72
73 73 # TODO Throw error 404 if no threads for tag found?
74 74 else:
75 75 threads = self.filter(parent=NO_PARENT)
76 76
77 77 threads = threads.order_by(order_by)
78 78
79 79 if page != ALL_PAGES:
80 80 thread_count = len(threads)
81 81
82 82 if page < self.get_thread_page_count(tag=tag):
83 83 start_thread = page * settings.THREADS_PER_PAGE
84 84 end_thread = min(start_thread + settings.THREADS_PER_PAGE,
85 85 thread_count)
86 86 threads = threads[start_thread:end_thread]
87 87
88 88 return threads
89 89
90 90 def get_thread(self, opening_post_id):
91 91 try:
92 92 opening_post = self.get(id=opening_post_id, parent=NO_PARENT)
93 93 except Post.DoesNotExist:
94 94 raise Http404
95 95
96 96 if opening_post.parent == NO_PARENT:
97 97 replies = self.filter(parent=opening_post_id)
98 98
99 99 thread = [opening_post]
100 100 thread.extend(replies)
101 101
102 102 return thread
103 103
104 104 def exists(self, post_id):
105 105 posts = self.filter(id=post_id)
106 106
107 107 return posts.count() > 0
108 108
109 109 def get_thread_page_count(self, tag=None):
110 110 if tag:
111 111 threads = self.filter(parent=NO_PARENT, tags=tag)
112 112 else:
113 113 threads = self.filter(parent=NO_PARENT)
114 114
115 115 return int(math.ceil(threads.count() / float(
116 116 settings.THREADS_PER_PAGE)))
117 117
118 118 def _delete_old_threads(self):
119 119 """
120 120 Preserves maximum thread count. If there are too many threads,
121 121 delete the old ones.
122 122 """
123 123
124 124 # TODO Move old threads to the archive instead of deleting them.
125 125 # Maybe make some 'old' field in the model to indicate the thread
126 126 # must not be shown and be able for replying.
127 127
128 128 threads = self.get_threads()
129 129 thread_count = len(threads)
130 130
131 131 if thread_count > settings.MAX_THREAD_COUNT:
132 132 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
133 133 old_threads = threads[thread_count - num_threads_to_delete:]
134 134
135 135 map(self.delete_post, old_threads)
136 136
137 137 def _bump_thread(self, thread_id):
138 138 thread = self.get(id=thread_id)
139 139
140 140 if thread.can_bump():
141 141 thread.last_edit_time = timezone.now()
142 142 thread.save()
143 143
144 144
145 145 class TagManager(models.Manager):
146 146 def get_not_empty_tags(self):
147 147 all_tags = self.all().order_by('name')
148 148 tags = []
149 149 for tag in all_tags:
150 150 if not tag.is_empty():
151 151 tags.append(tag)
152 152
153 153 return tags
154 154
155 155 def get_popular_tags(self):
156 156 all_tags = self.get_not_empty_tags()
157 157
158 158 sorted_tags = sorted(all_tags, key=lambda tag: tag.get_popularity(),
159 159 reverse=True)
160 160
161 161 return sorted_tags[:settings.POPULAR_TAGS]
162 162
163 163
164 164 class Tag(models.Model):
165 165 """
166 166 A tag is a text node assigned to the post. The tag serves as a board
167 167 section. There can be multiple tags for each message
168 168 """
169 169
170 170 objects = TagManager()
171 171
172 172 name = models.CharField(max_length=100)
173 173
174 174 def __unicode__(self):
175 175 return self.name
176 176
177 177 def is_empty(self):
178 178 return self.get_post_count() == 0
179 179
180 180 def get_post_count(self):
181 181 posts_with_tag = Post.objects.get_threads(tag=self)
182 182 return posts_with_tag.count()
183 183
184 184 def get_popularity(self):
185 185 posts_with_tag = Post.objects.get_threads(tag=self)
186 186 reply_count = 0
187 187 for post in posts_with_tag:
188 188 reply_count += post.get_reply_count()
189 189 reply_count += OPENING_POST_POPULARITY_WEIGHT
190 190
191 191 return reply_count
192 192
193 193
194 194 class Post(models.Model):
195 195 """A post is a message."""
196 196
197 197 objects = PostManager()
198 198
199 199 def _update_image_filename(self, filename):
200 200 """Get unique image filename"""
201 201
202 202 path = IMAGES_DIRECTORY
203 203 new_name = str(int(time.mktime(time.gmtime())))
204 204 new_name += str(int(random() * 1000))
205 205 new_name += FILE_EXTENSION_DELIMITER
206 206 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
207 207
208 208 return os.path.join(path, new_name)
209 209
210 210 title = models.CharField(max_length=TITLE_MAX_LENGTH)
211 211 pub_time = models.DateTimeField()
212 212 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
213 213 escape_html=False)
214 214
215 215 image_width = models.IntegerField(default=0)
216 216 image_height = models.IntegerField(default=0)
217 217
218 218 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
219 219 blank=True, sizes=(IMAGE_THUMB_SIZE,),
220 220 width_field='image_width',
221 221 height_field='image_height')
222 222
223 223 poster_ip = models.GenericIPAddressField()
224 224 poster_user_agent = models.TextField()
225 225 parent = models.BigIntegerField()
226 226 tags = models.ManyToManyField(Tag)
227 227 last_edit_time = models.DateTimeField()
228 228 user = models.ForeignKey('User', null=True, default=None)
229 229
230 230 def __unicode__(self):
231 231 return '#' + str(self.id) + ' ' + self.title + ' (' + \
232 232 self.text.raw[:50] + ')'
233 233
234 234 def get_title(self):
235 235 title = self.title
236 236 if len(title) == 0:
237 237 title = self.text.raw[:20]
238 238
239 239 return title
240 240
241 241 def _get_replies(self):
242 242 return Post.objects.filter(parent=self.id)
243 243
244 244 def get_reply_count(self):
245 245 return self._get_replies().count()
246 246
247 247 def get_images_count(self):
248 248 images_count = 1 if self.image else 0
249 249 for reply in self._get_replies():
250 250 if reply.image:
251 251 images_count += 1
252 252
253 253 return images_count
254 254
255 255 def can_bump(self):
256 256 """Check if the thread can be bumped by replying"""
257 257
258 258 replies_count = len(Post.objects.get_thread(self.id))
259 259
260 260 return replies_count <= settings.MAX_POSTS_PER_THREAD
261 261
262 262 def get_last_replies(self):
263 263 if settings.LAST_REPLIES_COUNT > 0:
264 264 reply_count = self.get_reply_count()
265 265
266 266 if reply_count > 0:
267 267 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
268 268 reply_count)
269 269 last_replies = self._get_replies()[reply_count
270 270 - reply_count_to_show:]
271 271
272 272 return last_replies
273 273
274 274
275 275 class User(models.Model):
276 276
277 277 user_id = models.CharField(max_length=50)
278 278 rank = models.IntegerField()
279 279
280 280 registration_time = models.DateTimeField()
281 281 last_access_time = models.DateTimeField()
282 282
283 283 fav_tags = models.ManyToManyField(Tag, null=True, blank=True)
284 284 fav_threads = models.ManyToManyField(Post, related_name='+', null=True,
285 285 blank=True)
286 286
287 287 def save_setting(self, name, value):
288 288 setting, created = Setting.objects.get_or_create(name=name, user=self)
289 289 setting.value = value
290 290 setting.save()
291 291
292 292 return setting
293 293
294 294 def get_setting(self, name):
295 295 settings = Setting.objects.filter(name=name, user=self)
296 296 if len(settings) > 0:
297 297 setting = settings[0]
298 298 else:
299 299 setting = None
300 300
301 301 if setting:
302 302 setting_value = setting.value
303 303 else:
304 304 setting_value = None
305 305
306 306 return setting_value
307 307
308 308 def is_moderator(self):
309 309 return RANK_MODERATOR >= self.rank
310 310
311 def get_sorted_fav_tags(self):
312 return self.fav_tags.order_by('name')
313
311 314 def __unicode__(self):
312 315 return self.user_id + '(' + self.rank + ')'
313 316
314 317
315 318 class Setting(models.Model):
316 319
317 320 name = models.CharField(max_length=50)
318 321 value = models.CharField(max_length=50)
319 322 user = models.ForeignKey(User)
320 323
321 324
322 325 class Ban(models.Model):
323 326 ip = models.GenericIPAddressField()
324 327
325 328 def __unicode__(self):
326 329 return self.ip
@@ -1,323 +1,323 b''
1 1 import hashlib
2 2 from django.core.urlresolvers import reverse
3 3 from django.template import RequestContext
4 4 from django.shortcuts import render, redirect, get_object_or_404
5 5 from django.utils import timezone
6 6
7 7 from boards import forms
8 8 import boards
9 9 from boards import utils
10 10 from boards.forms import ThreadForm, PostForm, SettingsForm, PlainErrorList, \
11 11 ThreadCaptchaForm, PostCaptchaForm, LoginForm
12 12
13 13 from boards.models import Post, Tag, Ban, User, RANK_USER, NO_PARENT
14 14 from boards import authors
15 15 import neboard
16 16
17 17
18 18 def index(request, page=0):
19 19 context = _init_default_context(request)
20 20
21 21 if utils.need_include_captcha(request):
22 22 threadFormClass = ThreadCaptchaForm
23 23 kwargs = {'request': request}
24 24 else:
25 25 threadFormClass = ThreadForm
26 26 kwargs = {}
27 27
28 28 if request.method == 'POST':
29 29 form = threadFormClass(request.POST, request.FILES,
30 30 error_class=PlainErrorList, **kwargs)
31 31
32 32 if form.is_valid():
33 33 return _new_post(request, form)
34 34 else:
35 35 form = threadFormClass(error_class=PlainErrorList, **kwargs)
36 36
37 37 threads = Post.objects.get_threads(page=int(page))
38 38
39 39 context['threads'] = None if len(threads) == 0 else threads
40 40 context['form'] = form
41 41 context['pages'] = range(Post.objects.get_thread_page_count())
42 42
43 43 return render(request, 'boards/posting_general.html',
44 44 context)
45 45
46 46
47 47 def _new_post(request, form, thread_id=boards.models.NO_PARENT):
48 48 """Add a new post (in thread or as a reply)."""
49 49
50 50 ip = _get_client_ip(request)
51 51 is_banned = Ban.objects.filter(ip=ip).count() > 0
52 52
53 53 if is_banned:
54 54 return redirect(you_are_banned)
55 55
56 56 data = form.cleaned_data
57 57
58 58 title = data['title']
59 59 text = data['text']
60 60
61 61 if 'image' in data.keys():
62 62 image = data['image']
63 63 else:
64 64 image = None
65 65
66 66 tags = []
67 67
68 68 new_thread = thread_id == boards.models.NO_PARENT
69 69 if new_thread:
70 70 tag_strings = data['tags']
71 71
72 72 if tag_strings:
73 73 tag_strings = tag_strings.split(' ')
74 74 for tag_name in tag_strings:
75 75 tag_name = tag_name.strip()
76 76 if len(tag_name) > 0:
77 77 tag, created = Tag.objects.get_or_create(name=tag_name)
78 78 tags.append(tag)
79 79
80 80 # TODO Add a possibility to define a link image instead of an image file.
81 81 # If a link is given, download the image automatically.
82 82
83 83 post = Post.objects.create_post(title=title, text=text, ip=ip,
84 84 parent_id=thread_id, image=image,
85 85 tags=tags)
86 86
87 87 thread_to_show = (post.id if new_thread else thread_id)
88 88
89 89 if new_thread:
90 90 return redirect(thread, post_id=thread_to_show)
91 91 else:
92 92 return redirect(reverse(thread,
93 93 kwargs={'post_id': thread_to_show}) + '#'
94 94 + str(post.id))
95 95
96 96
97 97 def tag(request, tag_name, page=0):
98 98 """Get all tag threads (posts without a parent)."""
99 99
100 100 tag = get_object_or_404(Tag, name=tag_name)
101 101 threads = Post.objects.get_threads(tag=tag, page=int(page))
102 102
103 103 if request.method == 'POST':
104 104 form = ThreadForm(request.POST, request.FILES,
105 105 error_class=PlainErrorList)
106 106 if form.is_valid():
107 107 return _new_post(request, form)
108 108 else:
109 109 form = forms.ThreadForm(initial={'tags': tag_name},
110 110 error_class=PlainErrorList)
111 111
112 112 context = _init_default_context(request)
113 113 context['threads'] = None if len(threads) == 0 else threads
114 114 context['tag'] = tag_name
115 115 context['pages'] = range(Post.objects.get_thread_page_count(tag=tag))
116 116
117 117 context['form'] = form
118 118
119 119 return render(request, 'boards/posting_general.html',
120 120 context)
121 121
122 122
123 123 def thread(request, post_id):
124 124 """Get all thread posts"""
125 125
126 126 if utils.need_include_captcha(request):
127 127 postFormClass = PostCaptchaForm
128 128 kwargs = {'request': request}
129 129 else:
130 130 postFormClass = PostForm
131 131 kwargs = {}
132 132
133 133 if request.method == 'POST':
134 134 form = postFormClass(request.POST, request.FILES,
135 135 error_class=PlainErrorList, **kwargs)
136 136 if form.is_valid():
137 137 return _new_post(request, form, post_id)
138 138 else:
139 139 form = postFormClass(error_class=PlainErrorList, **kwargs)
140 140
141 141 posts = Post.objects.get_thread(post_id)
142 142
143 143 context = _init_default_context(request)
144 144
145 145 context['posts'] = posts
146 146 context['form'] = form
147 147
148 148 return render(request, 'boards/thread.html', context)
149 149
150 150
151 151 def login(request):
152 152 """Log in with user id"""
153 153
154 154 context = _init_default_context(request)
155 155
156 156 if request.method == 'POST':
157 157 form = LoginForm(request.POST, request.FILES,
158 158 error_class=PlainErrorList)
159 159 if form.is_valid():
160 160 user = User.objects.get(user_id=form.cleaned_data['user_id'])
161 161 request.session['user_id'] = user.id
162 162 return redirect(index)
163 163
164 164 else:
165 165 form = LoginForm()
166 166
167 167 context['form'] = form
168 168
169 169 return render(request, 'boards/login.html', context)
170 170
171 171
172 172 def settings(request):
173 173 """User's settings"""
174 174
175 175 context = _init_default_context(request)
176 176
177 177 if request.method == 'POST':
178 178 form = SettingsForm(request.POST)
179 179 if form.is_valid():
180 180 selected_theme = form.cleaned_data['theme']
181 181
182 182 user = _get_user(request)
183 183 user.save_setting('theme', selected_theme)
184 184
185 185 return redirect(settings)
186 186 else:
187 187 selected_theme = _get_theme(request)
188 188 form = SettingsForm(initial={'theme': selected_theme})
189 189 context['form'] = form
190 190
191 191 return render(request, 'boards/settings.html', context)
192 192
193 193
194 194 def all_tags(request):
195 195 """All tags list"""
196 196
197 197 context = _init_default_context(request)
198 198 context['all_tags'] = Tag.objects.get_not_empty_tags()
199 199
200 200 return render(request, 'boards/tags.html', context)
201 201
202 202
203 203 def jump_to_post(request, post_id):
204 204 """Determine thread in which the requested post is and open it's page"""
205 205
206 206 post = get_object_or_404(Post, id=post_id)
207 207
208 208 if boards.models.NO_PARENT == post.parent:
209 209 return redirect(thread, post_id=post.id)
210 210 else:
211 211 parent_thread = get_object_or_404(Post, id=post.parent)
212 212 return redirect(reverse(thread, kwargs={'post_id': parent_thread.id})
213 213 + '#' + str(post.id))
214 214
215 215
216 216 def authors(request):
217 217 context = _init_default_context(request)
218 218 context['authors'] = boards.authors.authors
219 219
220 220 return render(request, 'boards/authors.html', context)
221 221
222 222
223 223 def delete(request, post_id):
224 224 user = _get_user(request)
225 225 post = get_object_or_404(Post, id=post_id)
226 226
227 227 if user.is_moderator():
228 228 # TODO Show confirmation page before deletion
229 229 Post.objects.delete_post(post)
230 230
231 231 if NO_PARENT == post.parent:
232 232 return redirect(index)
233 233 else:
234 234 return redirect(thread, post_id=post.parent)
235 235
236 236
237 237 def you_are_banned(request):
238 238 context = _init_default_context(request)
239 239 return render(request, 'boards/banned.html', context)
240 240
241 241
242 242 def page_404(request):
243 243 context = _init_default_context(request)
244 244 return render(request, 'boards/404.html', context)
245 245
246 246
247 247 def tag_subscribe(request, tag_name):
248 248 user = _get_user(request)
249 249 tag = get_object_or_404(Tag, name=tag_name)
250 250
251 251 if not tag in user.fav_tags.all():
252 252 user.fav_tags.add(tag)
253 253
254 254 return redirect(all_tags)
255 255
256 256
257 257 def tag_unsubscribe(request, tag_name):
258 258 user = _get_user(request)
259 259 tag = get_object_or_404(Tag, name=tag_name)
260 260
261 261 if tag in user.fav_tags.all():
262 262 user.fav_tags.remove(tag)
263 263
264 264 return redirect(all_tags)
265 265
266 266
267 267 def _get_theme(request, user=None):
268 268 """Get user's CSS theme"""
269 269
270 270 if not user:
271 271 user = _get_user(request)
272 272 theme = user.get_setting('theme')
273 273 if not theme:
274 274 theme = neboard.settings.DEFAULT_THEME
275 275
276 276 return theme
277 277
278 278
279 279 def _get_client_ip(request):
280 280 x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
281 281 if x_forwarded_for:
282 282 ip = x_forwarded_for.split(',')[-1].strip()
283 283 else:
284 284 ip = request.META.get('REMOTE_ADDR')
285 285 return ip
286 286
287 287
288 288 def _init_default_context(request):
289 289 """Create context with default values that are used in most views"""
290 290
291 291 context = RequestContext(request)
292 292
293 293 user = _get_user(request)
294 294 context['user'] = user
295 context['tags'] = sorted(user.fav_tags.all(), key=lambda tag: tag.name)
295 context['tags'] = user.get_sorted_fav_tags()
296 296 context['theme'] = _get_theme(request, user)
297 297
298 298 return context
299 299
300 300
301 301 def _get_user(request):
302 302 """Get current user from the session"""
303 303
304 304 session = request.session
305 305 if not 'user_id' in session:
306 306 request.session.save()
307 307
308 308 md5 = hashlib.md5()
309 309 md5.update(session.session_key)
310 310 new_id = md5.hexdigest()
311 311
312 312 time_now = timezone.now()
313 313 user = User.objects.create(user_id=new_id, rank=RANK_USER,
314 314 registration_time=time_now,
315 315 last_access_time=time_now)
316 316
317 317 session['user_id'] = user.id
318 318 else:
319 319 user = User.objects.get(id=session['user_id'])
320 320 user.last_access_time = timezone.now()
321 321 user.save()
322 322
323 323 return user
General Comments 0
You need to be logged in to leave comments. Login now