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