##// END OF EJS Templates
Merged with thread autoupdate branch
neko259 -
r374:bd40633e merge default
parent child Browse files
Show More
@@ -0,0 +1,116 b''
1 /*
2 @licstart The following is the entire license notice for the
3 JavaScript code in this page.
4
5
6 Copyright (C) 2013 neko259
7
8 The JavaScript code in this page is free software: you can
9 redistribute it and/or modify it under the terms of the GNU
10 General Public License (GNU GPL) as published by the Free Software
11 Foundation, either version 3 of the License, or (at your option)
12 any later version. The code is distributed WITHOUT ANY WARRANTY;
13 without even the implied warranty of MERCHANTABILITY or FITNESS
14 FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
15
16 As additional permission under GNU GPL version 3 section 7, you
17 may distribute non-source (e.g., minimized or compacted) forms of
18 that code without the copy of the GNU GPL normally required by
19 section 4, provided you include this license notice and a URL
20 through which recipients can access the Corresponding Source.
21
22 @licend The above is the entire license notice
23 for the JavaScript code in this page.
24 */
25
26 var THREAD_UPDATE_DELAY = 10000;
27
28 var loading = false;
29 var lastUpdateTime = null;
30
31 function blink(node) {
32 var blinkCount = 2;
33 var blinkDelay = 250;
34
35 var nodeToAnimate = node;
36 for (var i = 0; i < blinkCount; i++) {
37 nodeToAnimate = nodeToAnimate.fadeOut(blinkDelay).fadeIn(blinkDelay);
38 }
39 }
40
41 function updateThread() {
42 if (loading) {
43 return;
44 }
45
46 loading = true;
47
48 var threadPosts = $('div.thread').children('.post');
49
50 var lastPost = threadPosts.last();
51 var threadId = threadPosts.first().attr('id');
52
53 var diffUrl = '/api/diff_thread/' + threadId + '/' + lastUpdateTime + '/';
54 $.getJSON(diffUrl)
55 .success(function(data) {
56 var bottom = isPageBottom();
57
58 var addedPosts = data.added;
59
60 for (var i = 0; i < addedPosts.length; i++) {
61 var postText = addedPosts[i];
62
63 var post = $(postText);
64 post.appendTo(lastPost.parent());
65 addRefLinkPreview(post[0]);
66
67 lastPost = post;
68 blink(post);
69 }
70
71 var updatedPosts = data.updated;
72 for (var i = 0; i < updatedPosts.length; i++) {
73 var postText = updatedPosts[i];
74
75 var post = $(postText);
76 var postId = post.attr('id');
77
78 var oldPost = $('div.thread').children('.post[id=' + postId + ']');
79
80 oldPost.replaceWith(post);
81 addRefLinkPreview(post[0]);
82
83 blink(post);
84 }
85
86 // TODO Process deleted posts
87
88 lastUpdateTime = data.last_update;
89 loading = false;
90
91 if (bottom) {
92 var $target = $('html,body');
93 $target.animate({scrollTop: $target.height()}, 1000);
94 }
95 })
96 .error(function(data) {
97 // TODO Show error message that server is unavailable?
98
99 loading = false;
100 });
101 }
102
103 function isPageBottom() {
104 var scroll = $(window).scrollTop() / ($(document).height()
105 - $(window).height())
106
107 return scroll == 1
108 }
109
110 function initAutoupdate() {
111 loading = false;
112
113 lastUpdateTime = $('.metapanel').attr('data-last-update');
114
115 setInterval(updateThread, THREAD_UPDATE_DELAY);
116 }
@@ -1,434 +1,439 b''
1 import os
1 import os
2 from random import random
2 from random import random
3 import time
3 import time
4 import math
4 import math
5 from django.core.cache import cache
5 from django.core.cache import cache
6
6
7 from django.db import models
7 from django.db import models
8 from django.db.models import Count
8 from django.db.models import Count
9 from django.http import Http404
9 from django.http import Http404
10 from django.utils import timezone
10 from django.utils import timezone
11 from markupfield.fields import MarkupField
11 from markupfield.fields import MarkupField
12 from boards import settings as board_settings
12 from boards import settings as board_settings
13
13
14 from neboard import settings
14 from neboard import settings
15 import thumbs
15 import thumbs
16
16
17 import re
17 import re
18
18
19 BAN_REASON_MAX_LENGTH = 200
19 BAN_REASON_MAX_LENGTH = 200
20
20
21 BAN_REASON_AUTO = 'Auto'
21 BAN_REASON_AUTO = 'Auto'
22
22
23 IMAGE_THUMB_SIZE = (200, 150)
23 IMAGE_THUMB_SIZE = (200, 150)
24
24
25 TITLE_MAX_LENGTH = 50
25 TITLE_MAX_LENGTH = 50
26
26
27 DEFAULT_MARKUP_TYPE = 'markdown'
27 DEFAULT_MARKUP_TYPE = 'markdown'
28
28
29 NO_PARENT = -1
29 NO_PARENT = -1
30 NO_IP = '0.0.0.0'
30 NO_IP = '0.0.0.0'
31 UNKNOWN_UA = ''
31 UNKNOWN_UA = ''
32 ALL_PAGES = -1
32 ALL_PAGES = -1
33 OPENING_POST_POPULARITY_WEIGHT = 2
33 OPENING_POST_POPULARITY_WEIGHT = 2
34 IMAGES_DIRECTORY = 'images/'
34 IMAGES_DIRECTORY = 'images/'
35 FILE_EXTENSION_DELIMITER = '.'
35 FILE_EXTENSION_DELIMITER = '.'
36
36
37 RANK_ADMIN = 0
37 RANK_ADMIN = 0
38 RANK_MODERATOR = 10
38 RANK_MODERATOR = 10
39 RANK_USER = 100
39 RANK_USER = 100
40
40
41 SETTING_MODERATE = "moderate"
41 SETTING_MODERATE = "moderate"
42
42
43 REGEX_REPLY = re.compile('>>(\d+)')
43 REGEX_REPLY = re.compile('>>(\d+)')
44
44
45
45
46 class PostManager(models.Manager):
46 class PostManager(models.Manager):
47
47
48 def create_post(self, title, text, image=None, thread=None,
48 def create_post(self, title, text, image=None, thread=None,
49 ip=NO_IP, tags=None, user=None):
49 ip=NO_IP, tags=None, user=None):
50 posting_time = timezone.now()
51
50 post = self.create(title=title,
52 post = self.create(title=title,
51 text=text,
53 text=text,
52 pub_time=timezone.now(),
54 pub_time=posting_time,
53 thread=thread,
55 thread=thread,
54 image=image,
56 image=image,
55 poster_ip=ip,
57 poster_ip=ip,
56 poster_user_agent=UNKNOWN_UA,
58 poster_user_agent=UNKNOWN_UA,
57 last_edit_time=timezone.now(),
59 last_edit_time=posting_time,
58 bump_time=timezone.now(),
60 bump_time=posting_time,
59 user=user)
61 user=user)
60
62
61 if tags:
63 if tags:
62 map(post.tags.add, tags)
64 map(post.tags.add, tags)
63 for tag in tags:
65 for tag in tags:
64 tag.threads.add(post)
66 tag.threads.add(post)
65
67
66 if thread:
68 if thread:
67 thread.replies.add(post)
69 thread.replies.add(post)
68 thread.bump()
70 thread.bump()
69 thread.last_edit_time = timezone.now()
71 thread.last_edit_time = posting_time
70 thread.save()
72 thread.save()
71
73
72 #cache_key = thread.get_cache_key()
74 #cache_key = thread.get_cache_key()
73 #cache.delete(cache_key)
75 #cache.delete(cache_key)
74
76
75 else:
77 else:
76 self._delete_old_threads()
78 self._delete_old_threads()
77
79
78 self.connect_replies(post)
80 self.connect_replies(post)
79
81
80 return post
82 return post
81
83
82 def delete_post(self, post):
84 def delete_post(self, post):
83 if post.replies.count() > 0:
85 if post.replies.count() > 0:
84 map(self.delete_post, post.replies.all())
86 map(self.delete_post, post.replies.all())
85
87
86 # Update thread's last edit time (used as cache key)
88 # Update thread's last edit time (used as cache key)
87 thread = post.thread
89 thread = post.thread
88 if thread:
90 if thread:
89 thread.last_edit_time = timezone.now()
91 thread.last_edit_time = timezone.now()
90 thread.save()
92 thread.save()
91
93
92 #cache_key = thread.get_cache_key()
94 #cache_key = thread.get_cache_key()
93 #cache.delete(cache_key)
95 #cache.delete(cache_key)
94
96
95 post.delete()
97 post.delete()
96
98
97 def delete_posts_by_ip(self, ip):
99 def delete_posts_by_ip(self, ip):
98 posts = self.filter(poster_ip=ip)
100 posts = self.filter(poster_ip=ip)
99 map(self.delete_post, posts)
101 map(self.delete_post, posts)
100
102
101 def get_threads(self, tag=None, page=ALL_PAGES,
103 def get_threads(self, tag=None, page=ALL_PAGES,
102 order_by='-bump_time'):
104 order_by='-bump_time'):
103 if tag:
105 if tag:
104 threads = tag.threads
106 threads = tag.threads
105
107
106 if threads.count() == 0:
108 if threads.count() == 0:
107 raise Http404
109 raise Http404
108 else:
110 else:
109 threads = self.filter(thread=None)
111 threads = self.filter(thread=None)
110
112
111 threads = threads.order_by(order_by)
113 threads = threads.order_by(order_by)
112
114
113 if page != ALL_PAGES:
115 if page != ALL_PAGES:
114 thread_count = threads.count()
116 thread_count = threads.count()
115
117
116 if page < self._get_page_count(thread_count):
118 if page < self._get_page_count(thread_count):
117 start_thread = page * settings.THREADS_PER_PAGE
119 start_thread = page * settings.THREADS_PER_PAGE
118 end_thread = min(start_thread + settings.THREADS_PER_PAGE,
120 end_thread = min(start_thread + settings.THREADS_PER_PAGE,
119 thread_count)
121 thread_count)
120 threads = threads[start_thread:end_thread]
122 threads = threads[start_thread:end_thread]
121
123
122 return threads
124 return threads
123
125
124 def get_thread(self, opening_post_id):
126 def get_thread(self, opening_post_id):
125 try:
127 try:
126 opening_post = self.get(id=opening_post_id, thread=None)
128 opening_post = self.get(id=opening_post_id, thread=None)
127 except Post.DoesNotExist:
129 except Post.DoesNotExist:
128 raise Http404
130 raise Http404
129
131
130 #cache_key = opening_post.get_cache_key()
132 #cache_key = opening_post.get_cache_key()
131 #thread = cache.get(cache_key)
133 #thread = cache.get(cache_key)
132 #if thread:
134 #if thread:
133 # return thread
135 # return thread
134
136
135 if opening_post.replies:
137 if opening_post.replies:
136 thread = [opening_post]
138 thread = [opening_post]
137 thread.extend(opening_post.replies.all().order_by('pub_time'))
139 thread.extend(opening_post.replies.all().order_by('pub_time'))
138
140
139 #cache.set(cache_key, thread, board_settings.CACHE_TIMEOUT)
141 #cache.set(cache_key, thread, board_settings.CACHE_TIMEOUT)
140
142
141 return thread
143 return thread
142
144
143 def exists(self, post_id):
145 def exists(self, post_id):
144 posts = self.filter(id=post_id)
146 posts = self.filter(id=post_id)
145
147
146 return posts.count() > 0
148 return posts.count() > 0
147
149
148 def get_thread_page_count(self, tag=None):
150 def get_thread_page_count(self, tag=None):
149 if tag:
151 if tag:
150 threads = self.filter(thread=None, tags=tag)
152 threads = self.filter(thread=None, tags=tag)
151 else:
153 else:
152 threads = self.filter(thread=None)
154 threads = self.filter(thread=None)
153
155
154 return self._get_page_count(threads.count())
156 return self._get_page_count(threads.count())
155
157
156 def _delete_old_threads(self):
158 def _delete_old_threads(self):
157 """
159 """
158 Preserves maximum thread count. If there are too many threads,
160 Preserves maximum thread count. If there are too many threads,
159 delete the old ones.
161 delete the old ones.
160 """
162 """
161
163
162 # TODO Move old threads to the archive instead of deleting them.
164 # TODO Move old threads to the archive instead of deleting them.
163 # Maybe make some 'old' field in the model to indicate the thread
165 # Maybe make some 'old' field in the model to indicate the thread
164 # must not be shown and be able for replying.
166 # must not be shown and be able for replying.
165
167
166 threads = self.get_threads()
168 threads = self.get_threads()
167 thread_count = threads.count()
169 thread_count = threads.count()
168
170
169 if thread_count > settings.MAX_THREAD_COUNT:
171 if thread_count > settings.MAX_THREAD_COUNT:
170 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
172 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
171 old_threads = threads[thread_count - num_threads_to_delete:]
173 old_threads = threads[thread_count - num_threads_to_delete:]
172
174
173 map(self.delete_post, old_threads)
175 map(self.delete_post, old_threads)
174
176
175 def connect_replies(self, post):
177 def connect_replies(self, post):
176 """Connect replies to a post to show them as a refmap"""
178 """Connect replies to a post to show them as a refmap"""
177
179
178 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
180 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
179 id = reply_number.group(1)
181 id = reply_number.group(1)
180 ref_post = self.filter(id=id)
182 ref_post = self.filter(id=id)
181 if ref_post.count() > 0:
183 if ref_post.count() > 0:
182 ref_post[0].referenced_posts.add(post)
184 referenced_post = ref_post[0]
185 referenced_post.referenced_posts.add(post)
186 referenced_post.last_edit_time = post.pub_time
187 referenced_post.save()
183
188
184 def _get_page_count(self, thread_count):
189 def _get_page_count(self, thread_count):
185 return int(math.ceil(thread_count / float(settings.THREADS_PER_PAGE)))
190 return int(math.ceil(thread_count / float(settings.THREADS_PER_PAGE)))
186
191
187
192
188 class TagManager(models.Manager):
193 class TagManager(models.Manager):
189
194
190 def get_not_empty_tags(self):
195 def get_not_empty_tags(self):
191 tags = self.annotate(Count('threads')) \
196 tags = self.annotate(Count('threads')) \
192 .filter(threads__count__gt=0).order_by('name')
197 .filter(threads__count__gt=0).order_by('name')
193
198
194 return tags
199 return tags
195
200
196
201
197 class Tag(models.Model):
202 class Tag(models.Model):
198 """
203 """
199 A tag is a text node assigned to the post. The tag serves as a board
204 A tag is a text node assigned to the post. The tag serves as a board
200 section. There can be multiple tags for each message
205 section. There can be multiple tags for each message
201 """
206 """
202
207
203 objects = TagManager()
208 objects = TagManager()
204
209
205 name = models.CharField(max_length=100)
210 name = models.CharField(max_length=100)
206 threads = models.ManyToManyField('Post', null=True,
211 threads = models.ManyToManyField('Post', null=True,
207 blank=True, related_name='tag+')
212 blank=True, related_name='tag+')
208 linked = models.ForeignKey('Tag', null=True, blank=True)
213 linked = models.ForeignKey('Tag', null=True, blank=True)
209
214
210 def __unicode__(self):
215 def __unicode__(self):
211 return self.name
216 return self.name
212
217
213 def is_empty(self):
218 def is_empty(self):
214 return self.get_post_count() == 0
219 return self.get_post_count() == 0
215
220
216 def get_post_count(self):
221 def get_post_count(self):
217 return self.threads.count()
222 return self.threads.count()
218
223
219 def get_popularity(self):
224 def get_popularity(self):
220 posts_with_tag = Post.objects.get_threads(tag=self)
225 posts_with_tag = Post.objects.get_threads(tag=self)
221 reply_count = 0
226 reply_count = 0
222 for post in posts_with_tag:
227 for post in posts_with_tag:
223 reply_count += post.get_reply_count()
228 reply_count += post.get_reply_count()
224 reply_count += OPENING_POST_POPULARITY_WEIGHT
229 reply_count += OPENING_POST_POPULARITY_WEIGHT
225
230
226 return reply_count
231 return reply_count
227
232
228 def get_linked_tags(self):
233 def get_linked_tags(self):
229 tag_list = []
234 tag_list = []
230 self.get_linked_tags_list(tag_list)
235 self.get_linked_tags_list(tag_list)
231
236
232 return tag_list
237 return tag_list
233
238
234 def get_linked_tags_list(self, tag_list=[]):
239 def get_linked_tags_list(self, tag_list=[]):
235 """
240 """
236 Returns the list of tags linked to current. The list can be got
241 Returns the list of tags linked to current. The list can be got
237 through returned value or tag_list parameter
242 through returned value or tag_list parameter
238 """
243 """
239
244
240 linked_tag = self.linked
245 linked_tag = self.linked
241
246
242 if linked_tag and not (linked_tag in tag_list):
247 if linked_tag and not (linked_tag in tag_list):
243 tag_list.append(linked_tag)
248 tag_list.append(linked_tag)
244
249
245 linked_tag.get_linked_tags_list(tag_list)
250 linked_tag.get_linked_tags_list(tag_list)
246
251
247
252
248 class Post(models.Model):
253 class Post(models.Model):
249 """A post is a message."""
254 """A post is a message."""
250
255
251 objects = PostManager()
256 objects = PostManager()
252
257
253 def _update_image_filename(self, filename):
258 def _update_image_filename(self, filename):
254 """Get unique image filename"""
259 """Get unique image filename"""
255
260
256 path = IMAGES_DIRECTORY
261 path = IMAGES_DIRECTORY
257 new_name = str(int(time.mktime(time.gmtime())))
262 new_name = str(int(time.mktime(time.gmtime())))
258 new_name += str(int(random() * 1000))
263 new_name += str(int(random() * 1000))
259 new_name += FILE_EXTENSION_DELIMITER
264 new_name += FILE_EXTENSION_DELIMITER
260 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
265 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
261
266
262 return os.path.join(path, new_name)
267 return os.path.join(path, new_name)
263
268
264 title = models.CharField(max_length=TITLE_MAX_LENGTH)
269 title = models.CharField(max_length=TITLE_MAX_LENGTH)
265 pub_time = models.DateTimeField()
270 pub_time = models.DateTimeField()
266 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
271 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
267 escape_html=False)
272 escape_html=False)
268
273
269 image_width = models.IntegerField(default=0)
274 image_width = models.IntegerField(default=0)
270 image_height = models.IntegerField(default=0)
275 image_height = models.IntegerField(default=0)
271
276
272 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
277 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
273 blank=True, sizes=(IMAGE_THUMB_SIZE,),
278 blank=True, sizes=(IMAGE_THUMB_SIZE,),
274 width_field='image_width',
279 width_field='image_width',
275 height_field='image_height')
280 height_field='image_height')
276
281
277 poster_ip = models.GenericIPAddressField()
282 poster_ip = models.GenericIPAddressField()
278 poster_user_agent = models.TextField()
283 poster_user_agent = models.TextField()
279
284
280 thread = models.ForeignKey('Post', null=True, default=None)
285 thread = models.ForeignKey('Post', null=True, default=None)
281 tags = models.ManyToManyField(Tag)
286 tags = models.ManyToManyField(Tag)
282 last_edit_time = models.DateTimeField()
287 last_edit_time = models.DateTimeField()
283 bump_time = models.DateTimeField()
288 bump_time = models.DateTimeField()
284 user = models.ForeignKey('User', null=True, default=None)
289 user = models.ForeignKey('User', null=True, default=None)
285
290
286 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
291 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
287 blank=True, related_name='re+')
292 blank=True, related_name='re+')
288 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
293 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
289 null=True,
294 null=True,
290 blank=True, related_name='rfp+')
295 blank=True, related_name='rfp+')
291
296
292 def __unicode__(self):
297 def __unicode__(self):
293 return '#' + str(self.id) + ' ' + self.title + ' (' + \
298 return '#' + str(self.id) + ' ' + self.title + ' (' + \
294 self.text.raw[:50] + ')'
299 self.text.raw[:50] + ')'
295
300
296 def get_title(self):
301 def get_title(self):
297 title = self.title
302 title = self.title
298 if len(title) == 0:
303 if len(title) == 0:
299 title = self.text.raw[:20]
304 title = self.text.raw[:20]
300
305
301 return title
306 return title
302
307
303 def get_reply_count(self):
308 def get_reply_count(self):
304 return self.replies.count()
309 return self.replies.count()
305
310
306 def get_images_count(self):
311 def get_images_count(self):
307 images_count = 1 if self.image else 0
312 images_count = 1 if self.image else 0
308 images_count += self.replies.filter(image_width__gt=0).count()
313 images_count += self.replies.filter(image_width__gt=0).count()
309
314
310 return images_count
315 return images_count
311
316
312 def can_bump(self):
317 def can_bump(self):
313 """Check if the thread can be bumped by replying"""
318 """Check if the thread can be bumped by replying"""
314
319
315 post_count = self.get_reply_count() + 1
320 post_count = self.get_reply_count()
316
321
317 return post_count <= settings.MAX_POSTS_PER_THREAD
322 return post_count <= settings.MAX_POSTS_PER_THREAD
318
323
319 def bump(self):
324 def bump(self):
320 """Bump (move to up) thread"""
325 """Bump (move to up) thread"""
321
326
322 if self.can_bump():
327 if self.can_bump():
323 self.bump_time = timezone.now()
328 self.bump_time = timezone.now()
324
329
325 def get_last_replies(self):
330 def get_last_replies(self):
326 if settings.LAST_REPLIES_COUNT > 0:
331 if settings.LAST_REPLIES_COUNT > 0:
327 reply_count = self.get_reply_count()
332 reply_count = self.get_reply_count()
328
333
329 if reply_count > 0:
334 if reply_count > 0:
330 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
335 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
331 reply_count)
336 reply_count)
332 last_replies = self.replies.all().order_by('pub_time')[
337 last_replies = self.replies.all().order_by('pub_time')[
333 reply_count - reply_count_to_show:]
338 reply_count - reply_count_to_show:]
334
339
335 return last_replies
340 return last_replies
336
341
337 def get_tags(self):
342 def get_tags(self):
338 """Get a sorted tag list"""
343 """Get a sorted tag list"""
339
344
340 return self.tags.order_by('name')
345 return self.tags.order_by('name')
341
346
342 def get_cache_key(self):
347 def get_cache_key(self):
343 return str(self.id) + str(self.last_edit_time.microsecond)
348 return str(self.id) + str(self.last_edit_time.microsecond)
344
349
345 def get_sorted_referenced_posts(self):
350 def get_sorted_referenced_posts(self):
346 return self.referenced_posts.order_by('id')
351 return self.referenced_posts.order_by('id')
347
352
348 def is_referenced(self):
353 def is_referenced(self):
349 return self.referenced_posts.count() > 0
354 return self.referenced_posts.count() > 0
350
355
351
356
352 class User(models.Model):
357 class User(models.Model):
353
358
354 user_id = models.CharField(max_length=50)
359 user_id = models.CharField(max_length=50)
355 rank = models.IntegerField()
360 rank = models.IntegerField()
356
361
357 registration_time = models.DateTimeField()
362 registration_time = models.DateTimeField()
358
363
359 fav_tags = models.ManyToManyField(Tag, null=True, blank=True)
364 fav_tags = models.ManyToManyField(Tag, null=True, blank=True)
360 fav_threads = models.ManyToManyField(Post, related_name='+', null=True,
365 fav_threads = models.ManyToManyField(Post, related_name='+', null=True,
361 blank=True)
366 blank=True)
362
367
363 def save_setting(self, name, value):
368 def save_setting(self, name, value):
364 setting, created = Setting.objects.get_or_create(name=name, user=self)
369 setting, created = Setting.objects.get_or_create(name=name, user=self)
365 setting.value = str(value)
370 setting.value = str(value)
366 setting.save()
371 setting.save()
367
372
368 return setting
373 return setting
369
374
370 def get_setting(self, name):
375 def get_setting(self, name):
371 if Setting.objects.filter(name=name, user=self).exists():
376 if Setting.objects.filter(name=name, user=self).exists():
372 setting = Setting.objects.get(name=name, user=self)
377 setting = Setting.objects.get(name=name, user=self)
373 setting_value = setting.value
378 setting_value = setting.value
374 else:
379 else:
375 setting_value = None
380 setting_value = None
376
381
377 return setting_value
382 return setting_value
378
383
379 def is_moderator(self):
384 def is_moderator(self):
380 return RANK_MODERATOR >= self.rank
385 return RANK_MODERATOR >= self.rank
381
386
382 def get_sorted_fav_tags(self):
387 def get_sorted_fav_tags(self):
383 cache_key = self._get_tag_cache_key()
388 cache_key = self._get_tag_cache_key()
384 fav_tags = cache.get(cache_key)
389 fav_tags = cache.get(cache_key)
385 if fav_tags:
390 if fav_tags:
386 return fav_tags
391 return fav_tags
387
392
388 tags = self.fav_tags.annotate(Count('threads'))\
393 tags = self.fav_tags.annotate(Count('threads'))\
389 .filter(threads__count__gt=0).order_by('name')
394 .filter(threads__count__gt=0).order_by('name')
390
395
391 if tags:
396 if tags:
392 cache.set(cache_key, tags, board_settings.CACHE_TIMEOUT)
397 cache.set(cache_key, tags, board_settings.CACHE_TIMEOUT)
393
398
394 return tags
399 return tags
395
400
396 def get_post_count(self):
401 def get_post_count(self):
397 return Post.objects.filter(user=self).count()
402 return Post.objects.filter(user=self).count()
398
403
399 def __unicode__(self):
404 def __unicode__(self):
400 return self.user_id + '(' + str(self.rank) + ')'
405 return self.user_id + '(' + str(self.rank) + ')'
401
406
402 def get_last_access_time(self):
407 def get_last_access_time(self):
403 posts = Post.objects.filter(user=self)
408 posts = Post.objects.filter(user=self)
404 if posts.count() > 0:
409 if posts.count() > 0:
405 return posts.latest('pub_time').pub_time
410 return posts.latest('pub_time').pub_time
406
411
407 def add_tag(self, tag):
412 def add_tag(self, tag):
408 self.fav_tags.add(tag)
413 self.fav_tags.add(tag)
409 cache.delete(self._get_tag_cache_key())
414 cache.delete(self._get_tag_cache_key())
410
415
411 def remove_tag(self, tag):
416 def remove_tag(self, tag):
412 self.fav_tags.remove(tag)
417 self.fav_tags.remove(tag)
413 cache.delete(self._get_tag_cache_key())
418 cache.delete(self._get_tag_cache_key())
414
419
415 def _get_tag_cache_key(self):
420 def _get_tag_cache_key(self):
416 return self.user_id + '_tags'
421 return self.user_id + '_tags'
417
422
418
423
419 class Setting(models.Model):
424 class Setting(models.Model):
420
425
421 name = models.CharField(max_length=50)
426 name = models.CharField(max_length=50)
422 value = models.CharField(max_length=50)
427 value = models.CharField(max_length=50)
423 user = models.ForeignKey(User)
428 user = models.ForeignKey(User)
424
429
425
430
426 class Ban(models.Model):
431 class Ban(models.Model):
427
432
428 ip = models.GenericIPAddressField()
433 ip = models.GenericIPAddressField()
429 reason = models.CharField(default=BAN_REASON_AUTO,
434 reason = models.CharField(default=BAN_REASON_AUTO,
430 max_length=BAN_REASON_MAX_LENGTH)
435 max_length=BAN_REASON_MAX_LENGTH)
431 can_read = models.BooleanField(default=True)
436 can_read = models.BooleanField(default=True)
432
437
433 def __unicode__(self):
438 def __unicode__(self):
434 return self.ip
439 return self.ip
@@ -1,108 +1,109 b''
1 function $X(path, root) {
1 function $X(path, root) {
2 return document.evaluate(path, root || document, null, 6, null);
2 return document.evaluate(path, root || document, null, 6, null);
3 }
3 }
4 function $x(path, root) {
4 function $x(path, root) {
5 return document.evaluate(path, root || document, null, 8, null).singleNodeValue;
5 return document.evaluate(path, root || document, null, 8, null).singleNodeValue;
6 }
6 }
7
7
8 function $del(el) {
8 function $del(el) {
9 if(el) el.parentNode.removeChild(el);
9 if(el) el.parentNode.removeChild(el);
10 }
10 }
11
11
12 function $each(list, fn) {
12 function $each(list, fn) {
13 if(!list) return;
13 if(!list) return;
14 var i = list.snapshotLength;
14 var i = list.snapshotLength;
15 if(i > 0) while(i--) fn(list.snapshotItem(i), i);
15 if(i > 0) while(i--) fn(list.snapshotItem(i), i);
16 }
16 }
17
17
18 function addRefLinkPreview(node) {
18 function addRefLinkPreview(node) {
19 $each($X('.//a[starts-with(text(),">>")]', node || document), function(link) {
19 $each($X('.//a[starts-with(text(),">>")]', node || document), function(link) {
20 link.addEventListener('mouseover', showPostPreview, false);
20 link.addEventListener('mouseover', showPostPreview, false);
21 link.addEventListener('mouseout', delPostPreview, false);
21 link.addEventListener('mouseout', delPostPreview, false);
22 });
22 });
23 }
23 }
24
24
25 function showPostPreview(e) {
25 function showPostPreview(e) {
26 var doc = document;
26 var doc = document;
27 //ref id
27 //ref id
28 var pNum = $(this).text().match(/\d+/);
28 var pNum = $(this).text().match(/\d+/);
29
29
30 if (pNum.length == 0) {
30 if (pNum.length == 0) {
31 return;
31 return;
32 }
32 }
33
33
34 //position
34 //position
35 //var x = e.clientX + (doc.documentElement.scrollLeft || doc.body.scrollLeft) - doc.documentElement.clientLeft + 1;
35 //var x = e.clientX + (doc.documentElement.scrollLeft || doc.body.scrollLeft) - doc.documentElement.clientLeft + 1;
36 //var y = e.clientY + (doc.documentElement.scrollTop || doc.body.scrollTop) - doc.documentElement.clientTop;
36 //var y = e.clientY + (doc.documentElement.scrollTop || doc.body.scrollTop) - doc.documentElement.clientTop;
37
37
38 var x = e.clientX + (doc.documentElement.scrollLeft || doc.body.scrollLeft) + 2;
38 var x = e.clientX + (doc.documentElement.scrollLeft || doc.body.scrollLeft) + 2;
39 var y = e.clientY + (doc.documentElement.scrollTop || doc.body.scrollTop);
39 var y = e.clientY + (doc.documentElement.scrollTop || doc.body.scrollTop);
40
40
41 var cln = doc.createElement('div');
41 var cln = doc.createElement('div');
42 cln.id = 'pstprev_' + pNum;
42 cln.id = 'pstprev_' + pNum;
43 cln.className = 'post_preview';
43 cln.className = 'post_preview';
44
44
45 cln.style.cssText = 'top:' + y + 'px;' + (x < doc.body.clientWidth/2 ? 'left:' + x + 'px' : 'right:' + parseInt(doc.body.clientWidth - x + 1) + 'px');
45 cln.style.cssText = 'top:' + y + 'px;' + (x < doc.body.clientWidth/2 ? 'left:' + x + 'px' : 'right:' + parseInt(doc.body.clientWidth - x + 1) + 'px');
46
46
47 cln.addEventListener('mouseout', delPostPreview, false);
47 cln.addEventListener('mouseout', delPostPreview, false);
48
48
49
49
50 var mkPreview = function(cln, html) {
50 var mkPreview = function(cln, html) {
51
51
52 cln.innerHTML = html;
52 cln.innerHTML = html;
53
53
54 addRefLinkPreview(cln);
54 addRefLinkPreview(cln);
55
55
56 //if(!$x('.//small', cln)) showRefMap(post, p_num, refMap)
56 //if(!$x('.//small', cln)) showRefMap(post, p_num, refMap)
57 };
57 };
58
58
59
59
60 cln.innerHTML = gettext('Loading...');
60 cln.innerHTML = "<div class=\"post\">" + gettext('Loading...') + "</div>";
61
61
62 //Ссли пост Π½Π°ΠΉΠ΄Π΅Π½ Π² Π΄Π΅Ρ€Π΅Π²Π΅.
62 //Ссли пост Π½Π°ΠΉΠ΄Π΅Π½ Π² Π΄Π΅Ρ€Π΅Π²Π΅.
63 if($('div[id='+pNum+']').length > 0) {
63 if($('div[id='+pNum+']').length > 0) {
64 var postdata = $('div[id='+pNum+']').wrap("<div/>").parent().html();
64 var postdata = $('div[id='+pNum+']').wrap("<div/>").parent().html();
65
65
66 //TODO: Π²Ρ€Π΅ΠΌΠ΅Π½Π½ΠΎ
66 //TODO: Π²Ρ€Π΅ΠΌΠ΅Π½Π½ΠΎ
67 //funcInit(postdata);
67 //funcInit(postdata);
68
68
69 //make preview
69 //make preview
70 mkPreview(cln, postdata);
70 mkPreview(cln, postdata);
71 }
71 }
72 //ajax api
72 //ajax api
73 else {
73 else {
74 $.ajax({
74 $.ajax({
75 url: '/api/post/' + pNum
75 url: '/api/post/' + pNum + '/?truncated'
76 })
76 })
77 .success(function(data) {
77 .success(function(data) {
78 // TODO get a json, not post itself
78 // TODO get a json, not post itself
79 var postdata = $(data).wrap("<div/>").parent().html();
79 var postdata = $(data).wrap("<div/>").parent().html();
80
80
81 //make preview
81 //make preview
82 mkPreview(cln, postdata);
82 mkPreview(cln, postdata);
83
83
84 })
84 })
85 .error(function() {
85 .error(function() {
86 cln.innerHTML = gettext('Post not found');
86 cln.innerHTML = "<div class=\"post\">"
87 });
87 + gettext('Post not found') + "</div>";
88 });
88 }
89 }
89
90
90 $del(doc.getElementById(cln.id));
91 $del(doc.getElementById(cln.id));
91
92
92 //add preview
93 //add preview
93 $('body').append(cln);
94 $('body').append(cln);
94 }
95 }
95
96
96 function delPostPreview(e) {
97 function delPostPreview(e) {
97 var el = $x('ancestor-or-self::*[starts-with(@id,"pstprev")]', e.relatedTarget);
98 var el = $x('ancestor-or-self::*[starts-with(@id,"pstprev")]', e.relatedTarget);
98 if(!el) $each($X('.//div[starts-with(@id,"pstprev")]'), function(clone) {
99 if(!el) $each($X('.//div[starts-with(@id,"pstprev")]'), function(clone) {
99 $del(clone)
100 $del(clone)
100 });
101 });
101 else while(el.nextSibling) $del(el.nextSibling);
102 else while(el.nextSibling) $del(el.nextSibling);
102 }
103 }
103
104
104 function addPreview() {
105 function addPreview() {
105 $('.post').find('a').each(function() {
106 $('.post').find('a').each(function() {
106 showPostPreview($(this));
107 showPostPreview($(this));
107 });
108 });
108 }
109 }
@@ -1,80 +1,81 b''
1 /*
1 /*
2 @licstart The following is the entire license notice for the
2 @licstart The following is the entire license notice for the
3 JavaScript code in this page.
3 JavaScript code in this page.
4
4
5
5
6 Copyright (C) 2013 neko259
6 Copyright (C) 2013 neko259
7
7
8 The JavaScript code in this page is free software: you can
8 The JavaScript code in this page is free software: you can
9 redistribute it and/or modify it under the terms of the GNU
9 redistribute it and/or modify it under the terms of the GNU
10 General Public License (GNU GPL) as published by the Free Software
10 General Public License (GNU GPL) as published by the Free Software
11 Foundation, either version 3 of the License, or (at your option)
11 Foundation, either version 3 of the License, or (at your option)
12 any later version. The code is distributed WITHOUT ANY WARRANTY;
12 any later version. The code is distributed WITHOUT ANY WARRANTY;
13 without even the implied warranty of MERCHANTABILITY or FITNESS
13 without even the implied warranty of MERCHANTABILITY or FITNESS
14 FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
14 FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
15
15
16 As additional permission under GNU GPL version 3 section 7, you
16 As additional permission under GNU GPL version 3 section 7, you
17 may distribute non-source (e.g., minimized or compacted) forms of
17 may distribute non-source (e.g., minimized or compacted) forms of
18 that code without the copy of the GNU GPL normally required by
18 that code without the copy of the GNU GPL normally required by
19 section 4, provided you include this license notice and a URL
19 section 4, provided you include this license notice and a URL
20 through which recipients can access the Corresponding Source.
20 through which recipients can access the Corresponding Source.
21
21
22 @licend The above is the entire license notice
22 @licend The above is the entire license notice
23 for the JavaScript code in this page.
23 for the JavaScript code in this page.
24 */
24 */
25
25
26 function addGalleryPanel() {
26 function addGalleryPanel() {
27 var gallery = $('a[class="thumb"]').clone(true),
27 var gallery = $('a[class="thumb"]').clone(true),
28 normal = $('.post').clone(true);
28 normal = $('.post').clone(true);
29
29
30 $('.navigation_panel').filter(':first').after(
30 $('.navigation_panel').filter(':first').after(
31 '<div class="image-mode-tab" role="radiogroup" aria-label="Image mode2">' +
31 '<div class="image-mode-tab" role="radiogroup" aria-label="Image mode2">' +
32 '<label><input type="radio" class="image-mode-normal" name="image-mode" value="0" checked="checked"/>'+ gettext('Normal') +'</label>' +
32 '<label><input type="radio" class="image-mode-normal" name="image-mode" value="0" checked="checked"/>'+ gettext('Normal') +'</label>' +
33 '<label><input type="radio" class="image-mode-table" name="image-mode" value="1"/>'+ gettext('Gallery') +'</label>' +
33 '<label><input type="radio" class="image-mode-table" name="image-mode" value="1"/>'+ gettext('Gallery') +'</label>' +
34 '</div>'
34 '</div>'
35 );
35 );
36
36
37 $('input[name="image-mode"]').change(function() {
37 $('input[name="image-mode"]').change(function() {
38 //gallery mode
38 //gallery mode
39 if($(this).val() === '1') {
39 if($(this).val() === '1') {
40 $('.thread').replaceWith(
40 $('.thread').replaceWith(
41 $('<div id="posts-table"></div>').append(gallery)
41 $('<div id="posts-table"></div>').append(gallery)
42 );
42 );
43 }
43 }
44 //normal mode
44 //normal mode
45 else {
45 else {
46 $('#posts-table').replaceWith(
46 $('#posts-table').replaceWith(
47 $('<div class="thread"></div>').append(normal)
47 $('<div class="thread"></div>').append(normal)
48 );
48 );
49 }
49 }
50 });
50 });
51 }
51 }
52
52
53 function moveCaretToEnd(el) {
53 function moveCaretToEnd(el) {
54 if (typeof el.selectionStart == "number") {
54 if (typeof el.selectionStart == "number") {
55 el.selectionStart = el.selectionEnd = el.value.length;
55 el.selectionStart = el.selectionEnd = el.value.length;
56 } else if (typeof el.createTextRange != "undefined") {
56 } else if (typeof el.createTextRange != "undefined") {
57 el.focus();
57 el.focus();
58 var range = el.createTextRange();
58 var range = el.createTextRange();
59 range.collapse(false);
59 range.collapse(false);
60 range.select();
60 range.select();
61 }
61 }
62 }
62 }
63
63
64 function addQuickReply(postId) {
64 function addQuickReply(postId) {
65 var textToAdd = '>>' + postId + '\n\n';
65 var textToAdd = '>>' + postId + '\n\n';
66 var textAreaId = '#id_text';
66 var textAreaId = '#id_text';
67 $(textAreaId).val($(textAreaId).val()+ textToAdd);
67 $(textAreaId).val($(textAreaId).val()+ textToAdd);
68
68
69 var textarea = document.getElementById('id_text');
69 var textarea = document.getElementById('id_text');
70 $(textAreaId).focus();
70 $(textAreaId).focus();
71 moveCaretToEnd(textarea);
71 moveCaretToEnd(textarea);
72
72
73 $("html, body").animate({ scrollTop: $(textAreaId).offset().top }, "slow");
73 $("html, body").animate({ scrollTop: $(textAreaId).offset().top }, "slow");
74 }
74 }
75
75
76
76
77
77
78 $(document).ready(function(){
78 $(document).ready(function(){
79 addGalleryPanel();
79 addGalleryPanel();
80 initAutoupdate();
80 });
81 });
@@ -1,55 +1,68 b''
1 {% load i18n %}
1 {% load i18n %}
2 {% load board %}
2 {% load board %}
3
3
4 <div class="post" id="{{ post.id }}">
4 {% if can_bump %}
5 <div class="post" id="{{ post.id }}">
6 {% else %}
7 <div class="post dead_post" id="{{ post.id }}">
8 {% endif %}
9
5 {% if post.image %}
10 {% if post.image %}
6 <div class="image">
11 <div class="image">
7 <a
12 <a
8 class="thumb"
13 class="thumb"
9 href="{{ post.image.url }}"><img
14 href="{{ post.image.url }}"><img
10 src="{{ post.image.url_200x150 }}"
15 src="{{ post.image.url_200x150 }}"
11 alt="{% trans 'Post image' %}"v
16 alt="{{ post.id }}"
12 data-width="{{ post.image_width }}"
17 data-width="{{ post.image_width }}"
13 data-height="{{ post.image_height }}"/>
18 data-height="{{ post.image_height }}"/>
14 </a>
19 </a>
15 </div>
20 </div>
16 {% endif %}
21 {% endif %}
17 <div class="message">
22 <div class="message">
18 <div class="post-info">
23 <div class="post-info">
19 <span class="title">{{ post.title }}</span>
24 <span class="title">{{ post.title }}</span>
20 <a class="post_id" href="#{{ post.id }}">
25 <a class="post_id" href="#{{ post.id }}">
21 (#{{ post.id }})</a>
26 ({{ post.id }})</a>
22 [{{ post.pub_time }}]
27 [{{ post.pub_time }}]
28 [<a href="#" onclick="javascript:addQuickReply('{{ post.id }}')
29 ; return false;">&gt;&gt;</a>]
23
30
24 {% if moderator %}
31 {% if moderator %}
25 <span class="moderator_info">
32 <span class="moderator_info">
26 [<a href="{% url 'delete' post_id=post.id %}"
33 [<a href="{% url 'delete' post_id=post.id %}"
27 >{% trans 'Delete' %}</a>]
34 >{% trans 'Delete' %}</a>]
28 ({{ post.poster_ip }})
35 ({{ post.poster_ip }})
29 [<a href="{% url 'ban' post_id=post.id %}?next={{ request.path }}"
36 [<a href="{% url 'ban' post_id=post.id %}?next={{ request.path }}"
30 >{% trans 'Ban IP' %}</a>]
37 >{% trans 'Ban IP' %}</a>]
31 </span>
38 </span>
32 {% endif %}
39 {% endif %}
33 </div>
40 </div>
34 {% autoescape off %}
41 {% autoescape off %}
35 {{ post.text.rendered }}
42 {% if truncated %}
43 {{ post.text.rendered|truncatewords_html:50 }}
44 {% else %}
45 {{ post.text.rendered }}
46 {% endif %}
36 {% endautoescape %}
47 {% endautoescape %}
37 <div class="refmap">
48 {% if post.is_referenced %}
38 {% trans "Replies" %}:
49 <div class="refmap">
39 {% for ref_post in post.get_sorted_referenced_posts %}
50 {% trans "Replies" %}:
40 <a href="{% post_url ref_post.id %}">&gt;&gt;{{ ref_post.id }}</a
51 {% for ref_post in post.get_sorted_referenced_posts %}
41 >{% if not forloop.last %},{% endif %}
52 <a href="{% post_url ref_post.id %}">&gt;&gt;{{ ref_post.id }}</a
42 {% endfor %}
53 >{% if not forloop.last %},{% endif %}
43 </div>
54 {% endfor %}
55 </div>
56 {% endif %}
44 </div>
57 </div>
45 {% if post.tags.exists %}
58 {% if post.tags.exists %}
46 <div class="metadata">
59 <div class="metadata">
47 <span class="tags">{% trans 'Tags' %}:
60 <span class="tags">{% trans 'Tags' %}:
48 {% for tag in post.tags.all %}
61 {% for tag in post.tags.all %}
49 <a class="tag" href="{% url 'tag' tag.name %}">
62 <a class="tag" href="{% url 'tag' tag.name %}">
50 {{ tag.name }}</a>
63 {{ tag.name }}</a>
51 {% endfor %}
64 {% endfor %}
52 </span>
65 </span>
53 </div>
66 </div>
54 {% endif %}
67 {% endif %}
55 </div> No newline at end of file
68 </div>
@@ -1,163 +1,164 b''
1 {% extends "boards/base.html" %}
1 {% extends "boards/base.html" %}
2
2
3 {% load i18n %}
3 {% load i18n %}
4 {% load markup %}
4 {% load markup %}
5 {% load cache %}
5 {% load cache %}
6 {% load static from staticfiles %}
6 {% load static from staticfiles %}
7 {% load board %}
7 {% load board %}
8
8
9 {% block head %}
9 {% block head %}
10 <title>Neboard - {{ posts.0.get_title }}</title>
10 <title>Neboard - {{ posts.0.get_title }}</title>
11 {% endblock %}
11 {% endblock %}
12
12
13 {% block content %}
13 {% block content %}
14 {% get_current_language as LANGUAGE_CODE %}
14 {% get_current_language as LANGUAGE_CODE %}
15
15
16 <script src="{% static 'js/thread_update.js' %}"></script>
16 <script src="{% static 'js/thread.js' %}"></script>
17 <script src="{% static 'js/thread.js' %}"></script>
17
18
18 {% if posts %}
19 {% if posts %}
19 {% cache 600 thread_view posts.0.last_edit_time moderator LANGUAGE_CODE %}
20 {% cache 600 thread_view posts.0.last_edit_time moderator LANGUAGE_CODE %}
20 {% if bumpable %}
21 {% if bumpable %}
21 <div class="bar-bg">
22 <div class="bar-bg">
22 <div class="bar-value" style="width:{{ bumplimit_progress }}%">
23 <div class="bar-value" style="width:{{ bumplimit_progress }}%">
23 </div>
24 </div>
24 <div class="bar-text">
25 <div class="bar-text">
25 {{ posts_left }} {% trans 'posts to bumplimit' %}
26 {{ posts_left }} {% trans 'posts to bumplimit' %}
26 </div>
27 </div>
27 </div>
28 </div>
28 {% endif %}
29 {% endif %}
29 <div class="thread">
30 <div class="thread">
30 {% for post in posts %}
31 {% for post in posts %}
31 {% if bumpable %}
32 {% if bumpable %}
32 <div class="post" id="{{ post.id }}">
33 <div class="post" id="{{ post.id }}">
33 {% else %}
34 {% else %}
34 <div class="post dead_post" id="{{ post.id }}">
35 <div class="post dead_post" id="{{ post.id }}">
35 {% endif %}
36 {% endif %}
36 {% if post.image %}
37 {% if post.image %}
37 <div class="image">
38 <div class="image">
38 <a
39 <a
39 class="thumb"
40 class="thumb"
40 href="{{ post.image.url }}"><img
41 href="{{ post.image.url }}"><img
41 src="{{ post.image.url_200x150 }}"
42 src="{{ post.image.url_200x150 }}"
42 alt="{{ post.id }}"
43 alt="{{ post.id }}"
43 data-width="{{ post.image_width }}"
44 data-width="{{ post.image_width }}"
44 data-height="{{ post.image_height }}"/>
45 data-height="{{ post.image_height }}"/>
45 </a>
46 </a>
46 </div>
47 </div>
47 {% endif %}
48 {% endif %}
48 <div class="message">
49 <div class="message">
49 <div class="post-info">
50 <div class="post-info">
50 <span class="title">{{ post.title }}</span>
51 <span class="title">{{ post.title }}</span>
51 <a class="post_id" href="#{{ post.id }}">
52 <a class="post_id" href="#{{ post.id }}">
52 ({{ post.id }})</a>
53 ({{ post.id }})</a>
53 [{{ post.pub_time }}]
54 [{{ post.pub_time }}]
54 [<a href="#" onclick="javascript:addQuickReply('{{ post.id }}')
55 [<a href="#" onclick="javascript:addQuickReply('{{ post.id }}')
55 ; return false;">&gt;&gt;</a>]
56 ; return false;">&gt;&gt;</a>]
56
57
57 {% if moderator %}
58 {% if moderator %}
58 <span class="moderator_info">
59 <span class="moderator_info">
59 [<a href="{% url 'delete' post_id=post.id %}"
60 [<a href="{% url 'delete' post_id=post.id %}"
60 >{% trans 'Delete' %}</a>]
61 >{% trans 'Delete' %}</a>]
61 ({{ post.poster_ip }})
62 ({{ post.poster_ip }})
62 [<a href="{% url 'ban' post_id=post.id %}?next={{ request.path }}"
63 [<a href="{% url 'ban' post_id=post.id %}?next={{ request.path }}"
63 >{% trans 'Ban IP' %}</a>]
64 >{% trans 'Ban IP' %}</a>]
64 </span>
65 </span>
65 {% endif %}
66 {% endif %}
66 </div>
67 </div>
67 {% autoescape off %}
68 {% autoescape off %}
68 {{ post.text.rendered }}
69 {{ post.text.rendered }}
69 {% endautoescape %}
70 {% endautoescape %}
70 {% if post.is_referenced %}
71 {% if post.is_referenced %}
71 <div class="refmap">
72 <div class="refmap">
72 {% trans "Replies" %}:
73 {% trans "Replies" %}:
73 {% for ref_post in post.get_sorted_referenced_posts %}
74 {% for ref_post in post.get_sorted_referenced_posts %}
74 <a href="{% post_url ref_post.id %}">&gt;&gt;{{ ref_post.id }}</a
75 <a href="{% post_url ref_post.id %}">&gt;&gt;{{ ref_post.id }}</a
75 >{% if not forloop.last %},{% endif %}
76 >{% if not forloop.last %},{% endif %}
76 {% endfor %}
77 {% endfor %}
77 </div>
78 </div>
78 {% endif %}
79 {% endif %}
79 </div>
80 </div>
80 {% if forloop.first %}
81 {% if forloop.first %}
81 <div class="metadata">
82 <div class="metadata">
82 <span class="tags">
83 <span class="tags">
83 {% for tag in post.get_tags %}
84 {% for tag in post.get_tags %}
84 <a class="tag" href="{% url 'tag' tag.name %}">
85 <a class="tag" href="{% url 'tag' tag.name %}">
85 #{{ tag.name }}</a
86 #{{ tag.name }}</a
86 >{% if not forloop.last %},{% endif %}
87 >{% if not forloop.last %},{% endif %}
87 {% endfor %}
88 {% endfor %}
88 </span>
89 </span>
89 </div>
90 </div>
90 {% endif %}
91 {% endif %}
91 </div>
92 </div>
92 {% endfor %}
93 {% endfor %}
93 </div>
94 </div>
94 {% endcache %}
95 {% endcache %}
95 {% endif %}
96 {% endif %}
96
97
97 <form id="form" enctype="multipart/form-data" method="post"
98 <form id="form" enctype="multipart/form-data" method="post"
98 >{% csrf_token %}
99 >{% csrf_token %}
99 <div class="post-form-w">
100 <div class="post-form-w">
100 <div class="form-title">{% trans "Reply to thread" %} #{{ posts.0.id }}</div>
101 <div class="form-title">{% trans "Reply to thread" %} #{{ posts.0.id }}</div>
101 <div class="post-form">
102 <div class="post-form">
102 <div class="form-row">
103 <div class="form-row">
103 <div class="form-label">{% trans 'Title' %}</div>
104 <div class="form-label">{% trans 'Title' %}</div>
104 <div class="form-input">{{ form.title }}</div>
105 <div class="form-input">{{ form.title }}</div>
105 <div class="form-errors">{{ form.title.errors }}</div>
106 <div class="form-errors">{{ form.title.errors }}</div>
106 </div>
107 </div>
107 <div class="form-row">
108 <div class="form-row">
108 <div class="form-label">{% trans 'Formatting' %}</div>
109 <div class="form-label">{% trans 'Formatting' %}</div>
109 <div class="form-input" id="mark_panel">
110 <div class="form-input" id="mark_panel">
110 <span class="mark_btn" id="quote"><span class="quote">&gt;{% trans 'quote' %}</span></span>
111 <span class="mark_btn" id="quote"><span class="quote">&gt;{% trans 'quote' %}</span></span>
111 <span class="mark_btn" id="italic"><i>{% trans 'italic' %}</i></span>
112 <span class="mark_btn" id="italic"><i>{% trans 'italic' %}</i></span>
112 <span class="mark_btn" id="bold"><b>{% trans 'bold' %}</b></span>
113 <span class="mark_btn" id="bold"><b>{% trans 'bold' %}</b></span>
113 <span class="mark_btn" id="spoiler"><span class="spoiler">{% trans 'spoiler' %}</span></span>
114 <span class="mark_btn" id="spoiler"><span class="spoiler">{% trans 'spoiler' %}</span></span>
114 <span class="mark_btn" id="comment"><span class="comment">// {% trans 'comment' %}</span></span>
115 <span class="mark_btn" id="comment"><span class="comment">// {% trans 'comment' %}</span></span>
115 </div>
116 </div>
116 </div>
117 </div>
117 <div class="form-row">
118 <div class="form-row">
118 <div class="form-label">{% trans 'Text' %}</div>
119 <div class="form-label">{% trans 'Text' %}</div>
119 <div class="form-input">{{ form.text }}</div>
120 <div class="form-input">{{ form.text }}</div>
120 <div class="form-errors">{{ form.text.errors }}</div>
121 <div class="form-errors">{{ form.text.errors }}</div>
121 </div>
122 </div>
122 <div class="form-row">
123 <div class="form-row">
123 <div class="form-label">{% trans 'Image' %}</div>
124 <div class="form-label">{% trans 'Image' %}</div>
124 <div class="form-input">{{ form.image }}</div>
125 <div class="form-input">{{ form.image }}</div>
125 <div class="form-errors">{{ form.image.errors }}</div>
126 <div class="form-errors">{{ form.image.errors }}</div>
126 </div>
127 </div>
127 <div class="form-row form-email">
128 <div class="form-row form-email">
128 <div class="form-label">{% trans 'e-mail' %}</div>
129 <div class="form-label">{% trans 'e-mail' %}</div>
129 <div class="form-input">{{ form.email }}</div>
130 <div class="form-input">{{ form.email }}</div>
130 <div class="form-errors">{{ form.email.errors }}</div>
131 <div class="form-errors">{{ form.email.errors }}</div>
131 </div>
132 </div>
132 <div class="form-row">
133 <div class="form-row">
133 {{ form.captcha }}
134 {{ form.captcha }}
134 <div class="form-errors">{{ form.captcha.errors }}</div>
135 <div class="form-errors">{{ form.captcha.errors }}</div>
135 </div>
136 </div>
136 <div class="form-row">
137 <div class="form-row">
137 <div class="form-errors">{{ form.other.errors }}</div>
138 <div class="form-errors">{{ form.other.errors }}</div>
138 </div>
139 </div>
139 </div>
140 </div>
140
141
141 <div class="form-submit"><input type="submit"
142 <div class="form-submit"><input type="submit"
142 value="{% trans "Post" %}"/></div>
143 value="{% trans "Post" %}"/></div>
143 <div><a href="{% url "staticpage" name="help" %}">
144 <div><a href="{% url "staticpage" name="help" %}">
144 {% trans 'Text syntax' %}</a></div>
145 {% trans 'Text syntax' %}</a></div>
145 </div>
146 </div>
146 </form>
147 </form>
147
148
148 {% endblock %}
149 {% endblock %}
149
150
150 {% block metapanel %}
151 {% block metapanel %}
151
152
152 {% get_current_language as LANGUAGE_CODE %}
153 {% get_current_language as LANGUAGE_CODE %}
153
154
154 <span class="metapanel">
155 <span class="metapanel" data-last-update="{{ last_update }}">
155 {% cache 600 thread_meta posts.0.last_edit_time moderator LANGUAGE_CODE %}
156 {% cache 600 thread_meta posts.0.last_edit_time moderator LANGUAGE_CODE %}
156 {{ posts.0.get_reply_count }} {% trans 'replies' %},
157 {{ posts.0.get_reply_count }} {% trans 'replies' %},
157 {{ posts.0.get_images_count }} {% trans 'images' %}.
158 {{ posts.0.get_images_count }} {% trans 'images' %}.
158 {% trans 'Last update: ' %}{{ posts.0.last_edit_time }}
159 {% trans 'Last update: ' %}{{ posts.0.last_edit_time }}
159 [<a href="rss/">RSS</a>]
160 [<a href="rss/">RSS</a>]
160 {% endcache %}
161 {% endcache %}
161 </span>
162 </span>
162
163
163 {% endblock %}
164 {% endblock %}
@@ -1,56 +1,58 b''
1 from django.conf.urls import patterns, url, include
1 from django.conf.urls import patterns, url, include
2 from boards import views
2 from boards import views
3 from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed
3 from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed
4
4
5 js_info_dict = {
5 js_info_dict = {
6 'packages': ('boards',),
6 'packages': ('boards',),
7 }
7 }
8
8
9 urlpatterns = patterns('',
9 urlpatterns = patterns('',
10
10
11 # /boards/
11 # /boards/
12 url(r'^$', views.index, name='index'),
12 url(r'^$', views.index, name='index'),
13 # /boards/page/
13 # /boards/page/
14 url(r'^page/(?P<page>\w+)/$', views.index, name='index'),
14 url(r'^page/(?P<page>\w+)/$', views.index, name='index'),
15
15
16 # login page
16 # login page
17 url(r'^login/$', views.login, name='login'),
17 url(r'^login/$', views.login, name='login'),
18
18
19 # /boards/tag/tag_name/
19 # /boards/tag/tag_name/
20 url(r'^tag/(?P<tag_name>\w+)/$', views.tag, name='tag'),
20 url(r'^tag/(?P<tag_name>\w+)/$', views.tag, name='tag'),
21 # /boards/tag/tag_id/page/
21 # /boards/tag/tag_id/page/
22 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/$', views.tag, name='tag'),
22 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/$', views.tag, name='tag'),
23
23
24 # /boards/tag/tag_name/unsubscribe/
24 # /boards/tag/tag_name/unsubscribe/
25 url(r'^tag/(?P<tag_name>\w+)/subscribe/$', views.tag_subscribe,
25 url(r'^tag/(?P<tag_name>\w+)/subscribe/$', views.tag_subscribe,
26 name='tag_subscribe'),
26 name='tag_subscribe'),
27 # /boards/tag/tag_name/unsubscribe/
27 # /boards/tag/tag_name/unsubscribe/
28 url(r'^tag/(?P<tag_name>\w+)/unsubscribe/$', views.tag_unsubscribe,
28 url(r'^tag/(?P<tag_name>\w+)/unsubscribe/$', views.tag_unsubscribe,
29 name='tag_unsubscribe'),
29 name='tag_unsubscribe'),
30
30
31 # /boards/thread/
31 # /boards/thread/
32 url(r'^thread/(?P<post_id>\w+)/$', views.thread, name='thread'),
32 url(r'^thread/(?P<post_id>\w+)/$', views.thread, name='thread'),
33 url(r'^settings/$', views.settings, name='settings'),
33 url(r'^settings/$', views.settings, name='settings'),
34 url(r'^tags/$', views.all_tags, name='tags'),
34 url(r'^tags/$', views.all_tags, name='tags'),
35 url(r'^captcha/', include('captcha.urls')),
35 url(r'^captcha/', include('captcha.urls')),
36 url(r'^jump/(?P<post_id>\w+)/$', views.jump_to_post, name='jumper'),
36 url(r'^jump/(?P<post_id>\w+)/$', views.jump_to_post, name='jumper'),
37 url(r'^authors/$', views.authors, name='authors'),
37 url(r'^authors/$', views.authors, name='authors'),
38 url(r'^delete/(?P<post_id>\w+)/$', views.delete, name='delete'),
38 url(r'^delete/(?P<post_id>\w+)/$', views.delete, name='delete'),
39 url(r'^ban/(?P<post_id>\w+)/$', views.ban, name='ban'),
39 url(r'^ban/(?P<post_id>\w+)/$', views.ban, name='ban'),
40
40
41 url(r'^banned/$', views.you_are_banned, name='banned'),
41 url(r'^banned/$', views.you_are_banned, name='banned'),
42 url(r'^staticpage/(?P<name>\w+)/$', views.static_page, name='staticpage'),
42 url(r'^staticpage/(?P<name>\w+)/$', views.static_page, name='staticpage'),
43
43
44 # RSS feeds
44 # RSS feeds
45 url(r'^rss/$', AllThreadsFeed()),
45 url(r'^rss/$', AllThreadsFeed()),
46 url(r'^page/(?P<page>\w+)/rss/$', AllThreadsFeed()),
46 url(r'^page/(?P<page>\w+)/rss/$', AllThreadsFeed()),
47 url(r'^tag/(?P<tag_name>\w+)/rss/$', TagThreadsFeed()),
47 url(r'^tag/(?P<tag_name>\w+)/rss/$', TagThreadsFeed()),
48 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/rss/$', TagThreadsFeed()),
48 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/rss/$', TagThreadsFeed()),
49 url(r'^thread/(?P<post_id>\w+)/rss/$', ThreadPostsFeed()),
49 url(r'^thread/(?P<post_id>\w+)/rss/$', ThreadPostsFeed()),
50
50
51 # i18n
51 # i18n
52 url(r'^jsi18n/$', 'django.views.i18n.javascript_catalog', js_info_dict),
52 url(r'^jsi18n/$', 'django.views.i18n.javascript_catalog', js_info_dict),
53
53
54 # API
54 # API
55 url(r'^api/post/(?P<post_id>\w+)/$', views.get_post, name="get_post"),
55 url(r'^api/post/(?P<post_id>\w+)/$', views.get_post, name="get_post"),
56 url(r'^api/diff_thread/(?P<thread_id>\w+)/(?P<last_update_time>\w+)/$',
57 views.api_get_threaddiff, name="get_thread_diff"),
56 )
58 )
@@ -1,520 +1,564 b''
1 import hashlib
1 import hashlib
2 import json
2 import string
3 import string
4 import time
5 import calendar
6
7 from datetime import datetime
8
3 from django.core import serializers
9 from django.core import serializers
4 from django.core.urlresolvers import reverse
10 from django.core.urlresolvers import reverse
5 from django.http import HttpResponseRedirect
11 from django.http import HttpResponseRedirect
6 from django.http.response import HttpResponse
12 from django.http.response import HttpResponse
7 from django.template import RequestContext
13 from django.template import RequestContext
8 from django.shortcuts import render, redirect, get_object_or_404
14 from django.shortcuts import render, redirect, get_object_or_404
9 from django.utils import timezone
15 from django.utils import timezone
10 from django.db import transaction
16 from django.db import transaction
17 import math
11
18
12 from boards import forms
19 from boards import forms
13 import boards
20 import boards
14 from boards import utils
21 from boards import utils
15 from boards.forms import ThreadForm, PostForm, SettingsForm, PlainErrorList, \
22 from boards.forms import ThreadForm, PostForm, SettingsForm, PlainErrorList, \
16 ThreadCaptchaForm, PostCaptchaForm, LoginForm, ModeratorSettingsForm
23 ThreadCaptchaForm, PostCaptchaForm, LoginForm, ModeratorSettingsForm
17
24
18 from boards.models import Post, Tag, Ban, User, RANK_USER, SETTING_MODERATE, \
25 from boards.models import Post, Tag, Ban, User, RANK_USER, SETTING_MODERATE, \
19 REGEX_REPLY
26 REGEX_REPLY
20 from boards import authors
27 from boards import authors
21 from boards.utils import get_client_ip
28 from boards.utils import get_client_ip
22 import neboard
29 import neboard
23 import re
30 import re
24
31
25 BAN_REASON_SPAM = 'Autoban: spam bot'
32 BAN_REASON_SPAM = 'Autoban: spam bot'
26
33
27
34
28 def index(request, page=0):
35 def index(request, page=0):
29 context = _init_default_context(request)
36 context = _init_default_context(request)
30
37
31 if utils.need_include_captcha(request):
38 if utils.need_include_captcha(request):
32 threadFormClass = ThreadCaptchaForm
39 threadFormClass = ThreadCaptchaForm
33 kwargs = {'request': request}
40 kwargs = {'request': request}
34 else:
41 else:
35 threadFormClass = ThreadForm
42 threadFormClass = ThreadForm
36 kwargs = {}
43 kwargs = {}
37
44
38 if request.method == 'POST':
45 if request.method == 'POST':
39 form = threadFormClass(request.POST, request.FILES,
46 form = threadFormClass(request.POST, request.FILES,
40 error_class=PlainErrorList, **kwargs)
47 error_class=PlainErrorList, **kwargs)
41 form.session = request.session
48 form.session = request.session
42
49
43 if form.is_valid():
50 if form.is_valid():
44 return _new_post(request, form)
51 return _new_post(request, form)
45 if form.need_to_ban:
52 if form.need_to_ban:
46 # Ban user because he is suspected to be a bot
53 # Ban user because he is suspected to be a bot
47 _ban_current_user(request)
54 _ban_current_user(request)
48 else:
55 else:
49 form = threadFormClass(error_class=PlainErrorList, **kwargs)
56 form = threadFormClass(error_class=PlainErrorList, **kwargs)
50
57
51 threads = []
58 threads = []
52 for thread in Post.objects.get_threads(page=int(page)):
59 for thread in Post.objects.get_threads(page=int(page)):
53 threads.append({
60 threads.append({
54 'thread': thread,
61 'thread': thread,
55 'bumpable': thread.can_bump(),
62 'bumpable': thread.can_bump(),
56 'last_replies': thread.get_last_replies(),
63 'last_replies': thread.get_last_replies(),
57 })
64 })
58
65
59 # TODO Make this generic for tag and threads list pages
66 # TODO Make this generic for tag and threads list pages
60 context['threads'] = None if len(threads) == 0 else threads
67 context['threads'] = None if len(threads) == 0 else threads
61 context['form'] = form
68 context['form'] = form
62
69
63 page_count = Post.objects.get_thread_page_count()
70 page_count = Post.objects.get_thread_page_count()
64 context['pages'] = range(page_count)
71 context['pages'] = range(page_count)
65 page = int(page)
72 page = int(page)
66 if page < page_count - 1:
73 if page < page_count - 1:
67 context['next_page'] = str(page + 1)
74 context['next_page'] = str(page + 1)
68 if page > 0:
75 if page > 0:
69 context['prev_page'] = str(page - 1)
76 context['prev_page'] = str(page - 1)
70
77
71 return render(request, 'boards/posting_general.html',
78 return render(request, 'boards/posting_general.html',
72 context)
79 context)
73
80
74
81
75 @transaction.commit_on_success
82 @transaction.commit_on_success
76 def _new_post(request, form, thread_id=boards.models.NO_PARENT):
83 def _new_post(request, form, thread_id=boards.models.NO_PARENT):
77 """Add a new post (in thread or as a reply)."""
84 """Add a new post (in thread or as a reply)."""
78
85
79 ip = get_client_ip(request)
86 ip = get_client_ip(request)
80 is_banned = Ban.objects.filter(ip=ip).exists()
87 is_banned = Ban.objects.filter(ip=ip).exists()
81
88
82 if is_banned:
89 if is_banned:
83 return redirect(you_are_banned)
90 return redirect(you_are_banned)
84
91
85 data = form.cleaned_data
92 data = form.cleaned_data
86
93
87 title = data['title']
94 title = data['title']
88 text = data['text']
95 text = data['text']
89
96
90 text = _remove_invalid_links(text)
97 text = _remove_invalid_links(text)
91
98
92 if 'image' in data.keys():
99 if 'image' in data.keys():
93 image = data['image']
100 image = data['image']
94 else:
101 else:
95 image = None
102 image = None
96
103
97 tags = []
104 tags = []
98
105
99 new_thread = thread_id == boards.models.NO_PARENT
106 new_thread = thread_id == boards.models.NO_PARENT
100 if new_thread:
107 if new_thread:
101 tag_strings = data['tags']
108 tag_strings = data['tags']
102
109
103 if tag_strings:
110 if tag_strings:
104 tag_strings = tag_strings.split(' ')
111 tag_strings = tag_strings.split(' ')
105 for tag_name in tag_strings:
112 for tag_name in tag_strings:
106 tag_name = string.lower(tag_name.strip())
113 tag_name = string.lower(tag_name.strip())
107 if len(tag_name) > 0:
114 if len(tag_name) > 0:
108 tag, created = Tag.objects.get_or_create(name=tag_name)
115 tag, created = Tag.objects.get_or_create(name=tag_name)
109 tags.append(tag)
116 tags.append(tag)
110
117
111 linked_tags = tag.get_linked_tags()
118 linked_tags = tag.get_linked_tags()
112 if len(linked_tags) > 0:
119 if len(linked_tags) > 0:
113 tags.extend(linked_tags)
120 tags.extend(linked_tags)
114
121
115 op = None if thread_id == boards.models.NO_PARENT else \
122 op = None if thread_id == boards.models.NO_PARENT else \
116 get_object_or_404(Post, id=thread_id)
123 get_object_or_404(Post, id=thread_id)
117 post = Post.objects.create_post(title=title, text=text, ip=ip,
124 post = Post.objects.create_post(title=title, text=text, ip=ip,
118 thread=op, image=image,
125 thread=op, image=image,
119 tags=tags, user=_get_user(request))
126 tags=tags, user=_get_user(request))
120
127
121 thread_to_show = (post.id if new_thread else thread_id)
128 thread_to_show = (post.id if new_thread else thread_id)
122
129
123 if new_thread:
130 if new_thread:
124 return redirect(thread, post_id=thread_to_show)
131 return redirect(thread, post_id=thread_to_show)
125 else:
132 else:
126 return redirect(reverse(thread, kwargs={'post_id': thread_to_show}) +
133 return redirect(reverse(thread, kwargs={'post_id': thread_to_show}) +
127 '#' + str(post.id))
134 '#' + str(post.id))
128
135
129
136
130 def tag(request, tag_name, page=0):
137 def tag(request, tag_name, page=0):
131 """
138 """
132 Get all tag threads. Threads are split in pages, so some page is
139 Get all tag threads. Threads are split in pages, so some page is
133 requested. Default page is 0.
140 requested. Default page is 0.
134 """
141 """
135
142
136 tag = get_object_or_404(Tag, name=tag_name)
143 tag = get_object_or_404(Tag, name=tag_name)
137 threads = []
144 threads = []
138 for thread in Post.objects.get_threads(tag=tag, page=int(page)):
145 for thread in Post.objects.get_threads(tag=tag, page=int(page)):
139 threads.append({
146 threads.append({
140 'thread': thread,
147 'thread': thread,
141 'bumpable': thread.can_bump(),
148 'bumpable': thread.can_bump(),
142 'last_replies': thread.get_last_replies(),
149 'last_replies': thread.get_last_replies(),
143 })
150 })
144
151
145 if request.method == 'POST':
152 if request.method == 'POST':
146 form = ThreadForm(request.POST, request.FILES,
153 form = ThreadForm(request.POST, request.FILES,
147 error_class=PlainErrorList)
154 error_class=PlainErrorList)
148 form.session = request.session
155 form.session = request.session
149
156
150 if form.is_valid():
157 if form.is_valid():
151 return _new_post(request, form)
158 return _new_post(request, form)
152 if form.need_to_ban:
159 if form.need_to_ban:
153 # Ban user because he is suspected to be a bot
160 # Ban user because he is suspected to be a bot
154 _ban_current_user(request)
161 _ban_current_user(request)
155 else:
162 else:
156 form = forms.ThreadForm(initial={'tags': tag_name},
163 form = forms.ThreadForm(initial={'tags': tag_name},
157 error_class=PlainErrorList)
164 error_class=PlainErrorList)
158
165
159 context = _init_default_context(request)
166 context = _init_default_context(request)
160 context['threads'] = None if len(threads) == 0 else threads
167 context['threads'] = None if len(threads) == 0 else threads
161 context['tag'] = tag
168 context['tag'] = tag
162
169
163 page_count = Post.objects.get_thread_page_count(tag=tag)
170 page_count = Post.objects.get_thread_page_count(tag=tag)
164 context['pages'] = range(page_count)
171 context['pages'] = range(page_count)
165 page = int(page)
172 page = int(page)
166 if page < page_count - 1:
173 if page < page_count - 1:
167 context['next_page'] = str(page + 1)
174 context['next_page'] = str(page + 1)
168 if page > 0:
175 if page > 0:
169 context['prev_page'] = str(page - 1)
176 context['prev_page'] = str(page - 1)
170
177
171 context['form'] = form
178 context['form'] = form
172
179
173 return render(request, 'boards/posting_general.html',
180 return render(request, 'boards/posting_general.html',
174 context)
181 context)
175
182
176
183
177 def thread(request, post_id):
184 def thread(request, post_id):
178 """Get all thread posts"""
185 """Get all thread posts"""
179
186
180 if utils.need_include_captcha(request):
187 if utils.need_include_captcha(request):
181 postFormClass = PostCaptchaForm
188 postFormClass = PostCaptchaForm
182 kwargs = {'request': request}
189 kwargs = {'request': request}
183 else:
190 else:
184 postFormClass = PostForm
191 postFormClass = PostForm
185 kwargs = {}
192 kwargs = {}
186
193
187 if request.method == 'POST':
194 if request.method == 'POST':
188 form = postFormClass(request.POST, request.FILES,
195 form = postFormClass(request.POST, request.FILES,
189 error_class=PlainErrorList, **kwargs)
196 error_class=PlainErrorList, **kwargs)
190 form.session = request.session
197 form.session = request.session
191
198
192 if form.is_valid():
199 if form.is_valid():
193 return _new_post(request, form, post_id)
200 return _new_post(request, form, post_id)
194 if form.need_to_ban:
201 if form.need_to_ban:
195 # Ban user because he is suspected to be a bot
202 # Ban user because he is suspected to be a bot
196 _ban_current_user(request)
203 _ban_current_user(request)
197 else:
204 else:
198 form = postFormClass(error_class=PlainErrorList, **kwargs)
205 form = postFormClass(error_class=PlainErrorList, **kwargs)
199
206
200 posts = Post.objects.get_thread(post_id)
207 posts = Post.objects.get_thread(post_id)
201
208
202 context = _init_default_context(request)
209 context = _init_default_context(request)
203
210
204 context['posts'] = posts
211 context['posts'] = posts
205 context['form'] = form
212 context['form'] = form
206 context['bumpable'] = posts[0].can_bump()
213 context['bumpable'] = posts[0].can_bump()
207 if context['bumpable']:
214 if context['bumpable']:
208 context['posts_left'] = neboard.settings.MAX_POSTS_PER_THREAD - len(
215 context['posts_left'] = neboard.settings.MAX_POSTS_PER_THREAD - len(
209 posts)
216 posts)
210 context['bumplimit_progress'] = str(
217 context['bumplimit_progress'] = str(
211 float(context['posts_left']) /
218 float(context['posts_left']) /
212 neboard.settings.MAX_POSTS_PER_THREAD * 100)
219 neboard.settings.MAX_POSTS_PER_THREAD * 100)
220 context["last_update"] = _datetime_to_epoch(posts[0].last_edit_time)
213
221
214 return render(request, 'boards/thread.html', context)
222 return render(request, 'boards/thread.html', context)
215
223
216
224
217 def login(request):
225 def login(request):
218 """Log in with user id"""
226 """Log in with user id"""
219
227
220 context = _init_default_context(request)
228 context = _init_default_context(request)
221
229
222 if request.method == 'POST':
230 if request.method == 'POST':
223 form = LoginForm(request.POST, request.FILES,
231 form = LoginForm(request.POST, request.FILES,
224 error_class=PlainErrorList)
232 error_class=PlainErrorList)
225 form.session = request.session
233 form.session = request.session
226
234
227 if form.is_valid():
235 if form.is_valid():
228 user = User.objects.get(user_id=form.cleaned_data['user_id'])
236 user = User.objects.get(user_id=form.cleaned_data['user_id'])
229 request.session['user_id'] = user.id
237 request.session['user_id'] = user.id
230 return redirect(index)
238 return redirect(index)
231
239
232 else:
240 else:
233 form = LoginForm()
241 form = LoginForm()
234
242
235 context['form'] = form
243 context['form'] = form
236
244
237 return render(request, 'boards/login.html', context)
245 return render(request, 'boards/login.html', context)
238
246
239
247
240 def settings(request):
248 def settings(request):
241 """User's settings"""
249 """User's settings"""
242
250
243 context = _init_default_context(request)
251 context = _init_default_context(request)
244 user = _get_user(request)
252 user = _get_user(request)
245 is_moderator = user.is_moderator()
253 is_moderator = user.is_moderator()
246
254
247 if request.method == 'POST':
255 if request.method == 'POST':
248 with transaction.commit_on_success():
256 with transaction.commit_on_success():
249 if is_moderator:
257 if is_moderator:
250 form = ModeratorSettingsForm(request.POST,
258 form = ModeratorSettingsForm(request.POST,
251 error_class=PlainErrorList)
259 error_class=PlainErrorList)
252 else:
260 else:
253 form = SettingsForm(request.POST, error_class=PlainErrorList)
261 form = SettingsForm(request.POST, error_class=PlainErrorList)
254
262
255 if form.is_valid():
263 if form.is_valid():
256 selected_theme = form.cleaned_data['theme']
264 selected_theme = form.cleaned_data['theme']
257
265
258 user.save_setting('theme', selected_theme)
266 user.save_setting('theme', selected_theme)
259
267
260 if is_moderator:
268 if is_moderator:
261 moderate = form.cleaned_data['moderate']
269 moderate = form.cleaned_data['moderate']
262 user.save_setting(SETTING_MODERATE, moderate)
270 user.save_setting(SETTING_MODERATE, moderate)
263
271
264 return redirect(settings)
272 return redirect(settings)
265 else:
273 else:
266 selected_theme = _get_theme(request)
274 selected_theme = _get_theme(request)
267
275
268 if is_moderator:
276 if is_moderator:
269 form = ModeratorSettingsForm(initial={'theme': selected_theme,
277 form = ModeratorSettingsForm(initial={'theme': selected_theme,
270 'moderate': context['moderator']},
278 'moderate': context['moderator']},
271 error_class=PlainErrorList)
279 error_class=PlainErrorList)
272 else:
280 else:
273 form = SettingsForm(initial={'theme': selected_theme},
281 form = SettingsForm(initial={'theme': selected_theme},
274 error_class=PlainErrorList)
282 error_class=PlainErrorList)
275
283
276 context['form'] = form
284 context['form'] = form
277
285
278 return render(request, 'boards/settings.html', context)
286 return render(request, 'boards/settings.html', context)
279
287
280
288
281 def all_tags(request):
289 def all_tags(request):
282 """All tags list"""
290 """All tags list"""
283
291
284 context = _init_default_context(request)
292 context = _init_default_context(request)
285 context['all_tags'] = Tag.objects.get_not_empty_tags()
293 context['all_tags'] = Tag.objects.get_not_empty_tags()
286
294
287 return render(request, 'boards/tags.html', context)
295 return render(request, 'boards/tags.html', context)
288
296
289
297
290 def jump_to_post(request, post_id):
298 def jump_to_post(request, post_id):
291 """Determine thread in which the requested post is and open it's page"""
299 """Determine thread in which the requested post is and open it's page"""
292
300
293 post = get_object_or_404(Post, id=post_id)
301 post = get_object_or_404(Post, id=post_id)
294
302
295 if not post.thread:
303 if not post.thread:
296 return redirect(thread, post_id=post.id)
304 return redirect(thread, post_id=post.id)
297 else:
305 else:
298 return redirect(reverse(thread, kwargs={'post_id': post.thread.id})
306 return redirect(reverse(thread, kwargs={'post_id': post.thread.id})
299 + '#' + str(post.id))
307 + '#' + str(post.id))
300
308
301
309
302 def authors(request):
310 def authors(request):
303 """Show authors list"""
311 """Show authors list"""
304
312
305 context = _init_default_context(request)
313 context = _init_default_context(request)
306 context['authors'] = boards.authors.authors
314 context['authors'] = boards.authors.authors
307
315
308 return render(request, 'boards/authors.html', context)
316 return render(request, 'boards/authors.html', context)
309
317
310
318
311 @transaction.commit_on_success
319 @transaction.commit_on_success
312 def delete(request, post_id):
320 def delete(request, post_id):
313 """Delete post"""
321 """Delete post"""
314
322
315 user = _get_user(request)
323 user = _get_user(request)
316 post = get_object_or_404(Post, id=post_id)
324 post = get_object_or_404(Post, id=post_id)
317
325
318 if user.is_moderator():
326 if user.is_moderator():
319 # TODO Show confirmation page before deletion
327 # TODO Show confirmation page before deletion
320 Post.objects.delete_post(post)
328 Post.objects.delete_post(post)
321
329
322 if not post.thread:
330 if not post.thread:
323 return _redirect_to_next(request)
331 return _redirect_to_next(request)
324 else:
332 else:
325 return redirect(thread, post_id=post.thread.id)
333 return redirect(thread, post_id=post.thread.id)
326
334
327
335
328 @transaction.commit_on_success
336 @transaction.commit_on_success
329 def ban(request, post_id):
337 def ban(request, post_id):
330 """Ban user"""
338 """Ban user"""
331
339
332 user = _get_user(request)
340 user = _get_user(request)
333 post = get_object_or_404(Post, id=post_id)
341 post = get_object_or_404(Post, id=post_id)
334
342
335 if user.is_moderator():
343 if user.is_moderator():
336 # TODO Show confirmation page before ban
344 # TODO Show confirmation page before ban
337 ban, created = Ban.objects.get_or_create(ip=post.poster_ip)
345 ban, created = Ban.objects.get_or_create(ip=post.poster_ip)
338 if created:
346 if created:
339 ban.reason = 'Banned for post ' + str(post_id)
347 ban.reason = 'Banned for post ' + str(post_id)
340 ban.save()
348 ban.save()
341
349
342 return _redirect_to_next(request)
350 return _redirect_to_next(request)
343
351
344
352
345 def you_are_banned(request):
353 def you_are_banned(request):
346 """Show the page that notifies that user is banned"""
354 """Show the page that notifies that user is banned"""
347
355
348 context = _init_default_context(request)
356 context = _init_default_context(request)
349
357
350 ban = get_object_or_404(Ban, ip=utils.get_client_ip(request))
358 ban = get_object_or_404(Ban, ip=utils.get_client_ip(request))
351 context['ban_reason'] = ban.reason
359 context['ban_reason'] = ban.reason
352 return render(request, 'boards/staticpages/banned.html', context)
360 return render(request, 'boards/staticpages/banned.html', context)
353
361
354
362
355 def page_404(request):
363 def page_404(request):
356 """Show page 404 (not found error)"""
364 """Show page 404 (not found error)"""
357
365
358 context = _init_default_context(request)
366 context = _init_default_context(request)
359 return render(request, 'boards/404.html', context)
367 return render(request, 'boards/404.html', context)
360
368
361
369
362 @transaction.commit_on_success
370 @transaction.commit_on_success
363 def tag_subscribe(request, tag_name):
371 def tag_subscribe(request, tag_name):
364 """Add tag to favorites"""
372 """Add tag to favorites"""
365
373
366 user = _get_user(request)
374 user = _get_user(request)
367 tag = get_object_or_404(Tag, name=tag_name)
375 tag = get_object_or_404(Tag, name=tag_name)
368
376
369 if not tag in user.fav_tags.all():
377 if not tag in user.fav_tags.all():
370 user.add_tag(tag)
378 user.add_tag(tag)
371
379
372 return _redirect_to_next(request)
380 return _redirect_to_next(request)
373
381
374
382
375 @transaction.commit_on_success
383 @transaction.commit_on_success
376 def tag_unsubscribe(request, tag_name):
384 def tag_unsubscribe(request, tag_name):
377 """Remove tag from favorites"""
385 """Remove tag from favorites"""
378
386
379 user = _get_user(request)
387 user = _get_user(request)
380 tag = get_object_or_404(Tag, name=tag_name)
388 tag = get_object_or_404(Tag, name=tag_name)
381
389
382 if tag in user.fav_tags.all():
390 if tag in user.fav_tags.all():
383 user.remove_tag(tag)
391 user.remove_tag(tag)
384
392
385 return _redirect_to_next(request)
393 return _redirect_to_next(request)
386
394
387
395
388 def static_page(request, name):
396 def static_page(request, name):
389 """Show a static page that needs only tags list and a CSS"""
397 """Show a static page that needs only tags list and a CSS"""
390
398
391 context = _init_default_context(request)
399 context = _init_default_context(request)
392 return render(request, 'boards/staticpages/' + name + '.html', context)
400 return render(request, 'boards/staticpages/' + name + '.html', context)
393
401
394
402
395 def api_get_post(request, post_id):
403 def api_get_post(request, post_id):
396 """
404 """
397 Get the JSON of a post. This can be
405 Get the JSON of a post. This can be
398 used as and API for external clients.
406 used as and API for external clients.
399 """
407 """
400
408
401 post = get_object_or_404(Post, id=post_id)
409 post = get_object_or_404(Post, id=post_id)
402
410
403 json = serializers.serialize("json", [post], fields=(
411 json = serializers.serialize("json", [post], fields=(
404 "pub_time", "_text_rendered", "title", "text", "image",
412 "pub_time", "_text_rendered", "title", "text", "image",
405 "image_width", "image_height", "replies", "tags"
413 "image_width", "image_height", "replies", "tags"
406 ))
414 ))
407
415
408 return HttpResponse(content=json)
416 return HttpResponse(content=json)
409
417
410
418
419 def api_get_threaddiff(request, thread_id, last_update_time):
420 """Get posts that were changed or added since time"""
421
422 thread = get_object_or_404(Post, id=thread_id)
423
424 filter_time = datetime.fromtimestamp(float(last_update_time) / 1000000,
425 timezone.get_current_timezone())
426
427 json_data = {
428 'added': [],
429 'updated': [],
430 'last_update': None,
431 }
432 added_posts = Post.objects.filter(thread=thread, pub_time__gt=filter_time)
433 updated_posts = Post.objects.filter(thread=thread,
434 pub_time__lt=filter_time,
435 last_edit_time__gt=filter_time)
436 for post in added_posts:
437 json_data['added'].append(get_post(request, post.id).content.strip())
438 for post in updated_posts:
439 json_data['updated'].append(get_post(request, post.id).content.strip())
440 json_data['last_update'] = _datetime_to_epoch(thread.last_edit_time)
441
442 return HttpResponse(content=json.dumps(json_data))
443
444
411 def get_post(request, post_id):
445 def get_post(request, post_id):
412 """Get the html of a post. Used for popups."""
446 """Get the html of a post. Used for popups."""
413
447
414 post = get_object_or_404(Post, id=post_id)
448 post = get_object_or_404(Post, id=post_id)
449 thread = post.thread
415
450
416 context = RequestContext(request)
451 context = RequestContext(request)
417 context["post"] = post
452 context["post"] = post
453 context["can_bump"] = thread.can_bump()
454 if "truncated" in request.GET:
455 context["truncated"] = True
418
456
419 return render(request, 'boards/post.html', context)
457 return render(request, 'boards/post.html', context)
420
458
421
459
422 def _get_theme(request, user=None):
460 def _get_theme(request, user=None):
423 """Get user's CSS theme"""
461 """Get user's CSS theme"""
424
462
425 if not user:
463 if not user:
426 user = _get_user(request)
464 user = _get_user(request)
427 theme = user.get_setting('theme')
465 theme = user.get_setting('theme')
428 if not theme:
466 if not theme:
429 theme = neboard.settings.DEFAULT_THEME
467 theme = neboard.settings.DEFAULT_THEME
430
468
431 return theme
469 return theme
432
470
433
471
434 def _init_default_context(request):
472 def _init_default_context(request):
435 """Create context with default values that are used in most views"""
473 """Create context with default values that are used in most views"""
436
474
437 context = RequestContext(request)
475 context = RequestContext(request)
438
476
439 user = _get_user(request)
477 user = _get_user(request)
440 context['user'] = user
478 context['user'] = user
441 context['tags'] = user.get_sorted_fav_tags()
479 context['tags'] = user.get_sorted_fav_tags()
442
480
443 theme = _get_theme(request, user)
481 theme = _get_theme(request, user)
444 context['theme'] = theme
482 context['theme'] = theme
445 context['theme_css'] = 'css/' + theme + '/base_page.css'
483 context['theme_css'] = 'css/' + theme + '/base_page.css'
446
484
447 # This shows the moderator panel
485 # This shows the moderator panel
448 moderate = user.get_setting(SETTING_MODERATE)
486 moderate = user.get_setting(SETTING_MODERATE)
449 if moderate == 'True':
487 if moderate == 'True':
450 context['moderator'] = user.is_moderator()
488 context['moderator'] = user.is_moderator()
451 else:
489 else:
452 context['moderator'] = False
490 context['moderator'] = False
453
491
454 return context
492 return context
455
493
456
494
457 def _get_user(request):
495 def _get_user(request):
458 """
496 """
459 Get current user from the session. If the user does not exist, create
497 Get current user from the session. If the user does not exist, create
460 a new one.
498 a new one.
461 """
499 """
462
500
463 session = request.session
501 session = request.session
464 if not 'user_id' in session:
502 if not 'user_id' in session:
465 request.session.save()
503 request.session.save()
466
504
467 md5 = hashlib.md5()
505 md5 = hashlib.md5()
468 md5.update(session.session_key)
506 md5.update(session.session_key)
469 new_id = md5.hexdigest()
507 new_id = md5.hexdigest()
470
508
471 time_now = timezone.now()
509 time_now = timezone.now()
472 user = User.objects.create(user_id=new_id, rank=RANK_USER,
510 user = User.objects.create(user_id=new_id, rank=RANK_USER,
473 registration_time=time_now)
511 registration_time=time_now)
474
512
475 session['user_id'] = user.id
513 session['user_id'] = user.id
476 else:
514 else:
477 user = User.objects.get(id=session['user_id'])
515 user = User.objects.get(id=session['user_id'])
478
516
479 return user
517 return user
480
518
481
519
482 def _redirect_to_next(request):
520 def _redirect_to_next(request):
483 """
521 """
484 If a 'next' parameter was specified, redirect to the next page. This is
522 If a 'next' parameter was specified, redirect to the next page. This is
485 used when the user is required to return to some page after the current
523 used when the user is required to return to some page after the current
486 view has finished its work.
524 view has finished its work.
487 """
525 """
488
526
489 if 'next' in request.GET:
527 if 'next' in request.GET:
490 next_page = request.GET['next']
528 next_page = request.GET['next']
491 return HttpResponseRedirect(next_page)
529 return HttpResponseRedirect(next_page)
492 else:
530 else:
493 return redirect(index)
531 return redirect(index)
494
532
495
533
496 @transaction.commit_on_success
534 @transaction.commit_on_success
497 def _ban_current_user(request):
535 def _ban_current_user(request):
498 """Add current user to the IP ban list"""
536 """Add current user to the IP ban list"""
499
537
500 ip = utils.get_client_ip(request)
538 ip = utils.get_client_ip(request)
501 ban, created = Ban.objects.get_or_create(ip=ip)
539 ban, created = Ban.objects.get_or_create(ip=ip)
502 if created:
540 if created:
503 ban.can_read = False
541 ban.can_read = False
504 ban.reason = BAN_REASON_SPAM
542 ban.reason = BAN_REASON_SPAM
505 ban.save()
543 ban.save()
506
544
507
545
508 def _remove_invalid_links(text):
546 def _remove_invalid_links(text):
509 """
547 """
510 Replace invalid links in posts so that they won't be parsed.
548 Replace invalid links in posts so that they won't be parsed.
511 Invalid links are links to non-existent posts
549 Invalid links are links to non-existent posts
512 """
550 """
513
551
514 for reply_number in re.finditer(REGEX_REPLY, text):
552 for reply_number in re.finditer(REGEX_REPLY, text):
515 post_id = reply_number.group(1)
553 post_id = reply_number.group(1)
516 post = Post.objects.filter(id=post_id)
554 post = Post.objects.filter(id=post_id)
517 if not post.exists():
555 if not post.exists():
518 text = string.replace(text, '>>' + id, id)
556 text = string.replace(text, '>>' + id, id)
519
557
520 return text
558 return text
559
560
561 def _datetime_to_epoch(datetime):
562 return int(time.mktime(timezone.localtime(
563 datetime,timezone.get_current_timezone()).timetuple())
564 * 1000000 + datetime.microsecond) No newline at end of file
@@ -1,240 +1,240 b''
1 # Django settings for neboard project.
1 # Django settings for neboard project.
2 import os
2 import os
3 from boards.mdx_neboard import markdown_extended
3 from boards.mdx_neboard import markdown_extended
4
4
5 DEBUG = True
5 DEBUG = True
6 TEMPLATE_DEBUG = DEBUG
6 TEMPLATE_DEBUG = DEBUG
7
7
8 ADMINS = (
8 ADMINS = (
9 # ('Your Name', 'your_email@example.com'),
9 # ('Your Name', 'your_email@example.com'),
10 ('admin', 'admin@example.com')
10 ('admin', 'admin@example.com')
11 )
11 )
12
12
13 MANAGERS = ADMINS
13 MANAGERS = ADMINS
14
14
15 DATABASES = {
15 DATABASES = {
16 'default': {
16 'default': {
17 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
17 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
18 'NAME': 'database.db', # Or path to database file if using sqlite3.
18 'NAME': 'database.db', # Or path to database file if using sqlite3.
19 'USER': '', # Not used with sqlite3.
19 'USER': '', # Not used with sqlite3.
20 'PASSWORD': '', # Not used with sqlite3.
20 'PASSWORD': '', # Not used with sqlite3.
21 'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
21 'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
22 'PORT': '', # Set to empty string for default. Not used with sqlite3.
22 'PORT': '', # Set to empty string for default. Not used with sqlite3.
23 'CONN_MAX_AGE': None,
23 'CONN_MAX_AGE': None,
24 }
24 }
25 }
25 }
26
26
27 # Local time zone for this installation. Choices can be found here:
27 # Local time zone for this installation. Choices can be found here:
28 # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
28 # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
29 # although not all choices may be available on all operating systems.
29 # although not all choices may be available on all operating systems.
30 # In a Windows environment this must be set to your system time zone.
30 # In a Windows environment this must be set to your system time zone.
31 TIME_ZONE = 'Europe/Kiev'
31 TIME_ZONE = 'Europe/Kiev'
32
32
33 # Language code for this installation. All choices can be found here:
33 # Language code for this installation. All choices can be found here:
34 # http://www.i18nguy.com/unicode/language-identifiers.html
34 # http://www.i18nguy.com/unicode/language-identifiers.html
35 LANGUAGE_CODE = 'en'
35 LANGUAGE_CODE = 'en'
36
36
37 SITE_ID = 1
37 SITE_ID = 1
38
38
39 # If you set this to False, Django will make some optimizations so as not
39 # If you set this to False, Django will make some optimizations so as not
40 # to load the internationalization machinery.
40 # to load the internationalization machinery.
41 USE_I18N = True
41 USE_I18N = True
42
42
43 # If you set this to False, Django will not format dates, numbers and
43 # If you set this to False, Django will not format dates, numbers and
44 # calendars according to the current locale.
44 # calendars according to the current locale.
45 USE_L10N = True
45 USE_L10N = True
46
46
47 # If you set this to False, Django will not use timezone-aware datetimes.
47 # If you set this to False, Django will not use timezone-aware datetimes.
48 USE_TZ = True
48 USE_TZ = True
49
49
50 # Absolute filesystem path to the directory that will hold user-uploaded files.
50 # Absolute filesystem path to the directory that will hold user-uploaded files.
51 # Example: "/home/media/media.lawrence.com/media/"
51 # Example: "/home/media/media.lawrence.com/media/"
52 MEDIA_ROOT = './media/'
52 MEDIA_ROOT = './media/'
53
53
54 # URL that handles the media served from MEDIA_ROOT. Make sure to use a
54 # URL that handles the media served from MEDIA_ROOT. Make sure to use a
55 # trailing slash.
55 # trailing slash.
56 # Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
56 # Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
57 MEDIA_URL = '/media/'
57 MEDIA_URL = '/media/'
58
58
59 # Absolute path to the directory static files should be collected to.
59 # Absolute path to the directory static files should be collected to.
60 # Don't put anything in this directory yourself; store your static files
60 # Don't put anything in this directory yourself; store your static files
61 # in apps' "static/" subdirectories and in STATICFILES_DIRS.
61 # in apps' "static/" subdirectories and in STATICFILES_DIRS.
62 # Example: "/home/media/media.lawrence.com/static/"
62 # Example: "/home/media/media.lawrence.com/static/"
63 STATIC_ROOT = ''
63 STATIC_ROOT = ''
64
64
65 # URL prefix for static files.
65 # URL prefix for static files.
66 # Example: "http://media.lawrence.com/static/"
66 # Example: "http://media.lawrence.com/static/"
67 STATIC_URL = '/static/'
67 STATIC_URL = '/static/'
68
68
69 # Additional locations of static files
69 # Additional locations of static files
70 # It is really a hack, put real paths, not related
70 # It is really a hack, put real paths, not related
71 STATICFILES_DIRS = (
71 STATICFILES_DIRS = (
72 os.path.dirname(__file__) + '/boards/static',
72 os.path.dirname(__file__) + '/boards/static',
73
73
74 # '/d/work/python/django/neboard/neboard/boards/static',
74 # '/d/work/python/django/neboard/neboard/boards/static',
75 # Put strings here, like "/home/html/static" or "C:/www/django/static".
75 # Put strings here, like "/home/html/static" or "C:/www/django/static".
76 # Always use forward slashes, even on Windows.
76 # Always use forward slashes, even on Windows.
77 # Don't forget to use absolute paths, not relative paths.
77 # Don't forget to use absolute paths, not relative paths.
78 )
78 )
79
79
80 # List of finder classes that know how to find static files in
80 # List of finder classes that know how to find static files in
81 # various locations.
81 # various locations.
82 STATICFILES_FINDERS = (
82 STATICFILES_FINDERS = (
83 'django.contrib.staticfiles.finders.FileSystemFinder',
83 'django.contrib.staticfiles.finders.FileSystemFinder',
84 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
84 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
85 )
85 )
86
86
87 if DEBUG:
87 if DEBUG:
88 STATICFILES_STORAGE = \
88 STATICFILES_STORAGE = \
89 'django.contrib.staticfiles.storage.StaticFilesStorage'
89 'django.contrib.staticfiles.storage.StaticFilesStorage'
90 else:
90 else:
91 STATICFILES_STORAGE = \
91 STATICFILES_STORAGE = \
92 'django.contrib.staticfiles.storage.CachedStaticFilesStorage'
92 'django.contrib.staticfiles.storage.CachedStaticFilesStorage'
93
93
94 # Make this unique, and don't share it with anybody.
94 # Make this unique, and don't share it with anybody.
95 SECRET_KEY = '@1rc$o(7=tt#kd+4s$u6wchm**z^)4x90)7f6z(i&amp;55@o11*8o'
95 SECRET_KEY = '@1rc$o(7=tt#kd+4s$u6wchm**z^)4x90)7f6z(i&amp;55@o11*8o'
96
96
97 # List of callables that know how to import templates from various sources.
97 # List of callables that know how to import templates from various sources.
98 TEMPLATE_LOADERS = (
98 TEMPLATE_LOADERS = (
99 'django.template.loaders.filesystem.Loader',
99 'django.template.loaders.filesystem.Loader',
100 'django.template.loaders.app_directories.Loader',
100 'django.template.loaders.app_directories.Loader',
101 )
101 )
102
102
103 TEMPLATE_CONTEXT_PROCESSORS = (
103 TEMPLATE_CONTEXT_PROCESSORS = (
104 'django.core.context_processors.media',
104 'django.core.context_processors.media',
105 'django.core.context_processors.static',
105 'django.core.context_processors.static',
106 'django.core.context_processors.request',
106 'django.core.context_processors.request',
107 'django.contrib.auth.context_processors.auth',
107 'django.contrib.auth.context_processors.auth',
108 )
108 )
109
109
110 MIDDLEWARE_CLASSES = (
110 MIDDLEWARE_CLASSES = (
111 'django.contrib.sessions.middleware.SessionMiddleware',
111 'django.contrib.sessions.middleware.SessionMiddleware',
112 'django.middleware.locale.LocaleMiddleware',
112 'django.middleware.locale.LocaleMiddleware',
113 'django.middleware.common.CommonMiddleware',
113 'django.middleware.common.CommonMiddleware',
114 'django.contrib.auth.middleware.AuthenticationMiddleware',
114 'django.contrib.auth.middleware.AuthenticationMiddleware',
115 'django.contrib.messages.middleware.MessageMiddleware',
115 'django.contrib.messages.middleware.MessageMiddleware',
116 'boards.middlewares.BanMiddleware',
116 'boards.middlewares.BanMiddleware',
117 )
117 )
118
118
119 ROOT_URLCONF = 'neboard.urls'
119 ROOT_URLCONF = 'neboard.urls'
120
120
121 # Python dotted path to the WSGI application used by Django's runserver.
121 # Python dotted path to the WSGI application used by Django's runserver.
122 WSGI_APPLICATION = 'neboard.wsgi.application'
122 WSGI_APPLICATION = 'neboard.wsgi.application'
123
123
124 TEMPLATE_DIRS = (
124 TEMPLATE_DIRS = (
125 # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
125 # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
126 # Always use forward slashes, even on Windows.
126 # Always use forward slashes, even on Windows.
127 # Don't forget to use absolute paths, not relative paths.
127 # Don't forget to use absolute paths, not relative paths.
128 'templates',
128 'templates',
129 )
129 )
130
130
131 INSTALLED_APPS = (
131 INSTALLED_APPS = (
132 'django.contrib.auth',
132 'django.contrib.auth',
133 'django.contrib.contenttypes',
133 'django.contrib.contenttypes',
134 'django.contrib.sessions',
134 'django.contrib.sessions',
135 # 'django.contrib.sites',
135 # 'django.contrib.sites',
136 'django.contrib.messages',
136 'django.contrib.messages',
137 'django.contrib.staticfiles',
137 'django.contrib.staticfiles',
138 # Uncomment the next line to enable the admin:
138 # Uncomment the next line to enable the admin:
139 'django.contrib.admin',
139 'django.contrib.admin',
140 # Uncomment the next line to enable admin documentation:
140 # Uncomment the next line to enable admin documentation:
141 # 'django.contrib.admindocs',
141 # 'django.contrib.admindocs',
142 'django.contrib.markup',
142 'django.contrib.markup',
143 'django.contrib.humanize',
143 'django.contrib.humanize',
144 'django_cleanup',
144 'django_cleanup',
145 'boards',
145 'boards',
146 'captcha',
146 'captcha',
147 'south',
147 'south',
148 'debug_toolbar',
148 'debug_toolbar',
149 )
149 )
150
150
151 DEBUG_TOOLBAR_PANELS = (
151 DEBUG_TOOLBAR_PANELS = (
152 'debug_toolbar.panels.version.VersionDebugPanel',
152 'debug_toolbar.panels.version.VersionDebugPanel',
153 'debug_toolbar.panels.timer.TimerDebugPanel',
153 'debug_toolbar.panels.timer.TimerDebugPanel',
154 'debug_toolbar.panels.settings_vars.SettingsVarsDebugPanel',
154 'debug_toolbar.panels.settings_vars.SettingsVarsDebugPanel',
155 'debug_toolbar.panels.headers.HeaderDebugPanel',
155 'debug_toolbar.panels.headers.HeaderDebugPanel',
156 'debug_toolbar.panels.request_vars.RequestVarsDebugPanel',
156 'debug_toolbar.panels.request_vars.RequestVarsDebugPanel',
157 'debug_toolbar.panels.template.TemplateDebugPanel',
157 'debug_toolbar.panels.template.TemplateDebugPanel',
158 'debug_toolbar.panels.sql.SQLDebugPanel',
158 'debug_toolbar.panels.sql.SQLDebugPanel',
159 'debug_toolbar.panels.signals.SignalDebugPanel',
159 'debug_toolbar.panels.signals.SignalDebugPanel',
160 'debug_toolbar.panels.logger.LoggingPanel',
160 'debug_toolbar.panels.logger.LoggingPanel',
161 )
161 )
162
162
163 # TODO: NEED DESIGN FIXES
163 # TODO: NEED DESIGN FIXES
164 CAPTCHA_OUTPUT_FORMAT = (u' %(hidden_field)s '
164 CAPTCHA_OUTPUT_FORMAT = (u' %(hidden_field)s '
165 u'<div class="form-label">%(image)s</div>'
165 u'<div class="form-label">%(image)s</div>'
166 u'<div class="form-text">%(text_field)s</div>')
166 u'<div class="form-text">%(text_field)s</div>')
167
167
168 # A sample logging configuration. The only tangible logging
168 # A sample logging configuration. The only tangible logging
169 # performed by this configuration is to send an email to
169 # performed by this configuration is to send an email to
170 # the site admins on every HTTP 500 error when DEBUG=False.
170 # the site admins on every HTTP 500 error when DEBUG=False.
171 # See http://docs.djangoproject.com/en/dev/topics/logging for
171 # See http://docs.djangoproject.com/en/dev/topics/logging for
172 # more details on how to customize your logging configuration.
172 # more details on how to customize your logging configuration.
173 LOGGING = {
173 LOGGING = {
174 'version': 1,
174 'version': 1,
175 'disable_existing_loggers': False,
175 'disable_existing_loggers': False,
176 'filters': {
176 'filters': {
177 'require_debug_false': {
177 'require_debug_false': {
178 '()': 'django.utils.log.RequireDebugFalse'
178 '()': 'django.utils.log.RequireDebugFalse'
179 }
179 }
180 },
180 },
181 'handlers': {
181 'handlers': {
182 'mail_admins': {
182 'mail_admins': {
183 'level': 'ERROR',
183 'level': 'ERROR',
184 'filters': ['require_debug_false'],
184 'filters': ['require_debug_false'],
185 'class': 'django.utils.log.AdminEmailHandler'
185 'class': 'django.utils.log.AdminEmailHandler'
186 }
186 }
187 },
187 },
188 'loggers': {
188 'loggers': {
189 'django.request': {
189 'django.request': {
190 'handlers': ['mail_admins'],
190 'handlers': ['mail_admins'],
191 'level': 'ERROR',
191 'level': 'ERROR',
192 'propagate': True,
192 'propagate': True,
193 },
193 },
194 }
194 }
195 }
195 }
196
196
197 MARKUP_FIELD_TYPES = (
197 MARKUP_FIELD_TYPES = (
198 ('markdown', markdown_extended),
198 ('markdown', markdown_extended),
199 )
199 )
200 # Custom imageboard settings
200 # Custom imageboard settings
201 # TODO These should me moved to
201 # TODO These should me moved to
202 MAX_POSTS_PER_THREAD = 10 # Thread bumplimit
202 MAX_POSTS_PER_THREAD = 10 # Thread bumplimit
203 MAX_THREAD_COUNT = 500 # Old threads will be deleted to preserve this count
203 MAX_THREAD_COUNT = 500 # Old threads will be deleted to preserve this count
204 THREADS_PER_PAGE = 3
204 THREADS_PER_PAGE = 3
205 SITE_NAME = 'Neboard'
205 SITE_NAME = 'Neboard'
206
206
207 THEMES = [
207 THEMES = [
208 ('md', 'Mystic Dark'),
208 ('md', 'Mystic Dark'),
209 ('md_centered', 'Mystic Dark (centered)'),
209 ('md_centered', 'Mystic Dark (centered)'),
210 ('sw', 'Snow White'),
210 ('sw', 'Snow White'),
211 ('pg', 'Photon Gray'),
211 ('pg', 'Photon Gray'),
212 ]
212 ]
213
213
214 DEFAULT_THEME = 'md'
214 DEFAULT_THEME = 'md'
215
215
216 POPULAR_TAGS = 10
216 POPULAR_TAGS = 10
217 LAST_REPLIES_COUNT = 3
217 LAST_REPLIES_COUNT = 3
218
218
219 ENABLE_CAPTCHA = False
219 ENABLE_CAPTCHA = False
220 # if user tries to post before CAPTCHA_DEFAULT_SAFE_TIME. Captcha will be shown
220 # if user tries to post before CAPTCHA_DEFAULT_SAFE_TIME. Captcha will be shown
221 CAPTCHA_DEFAULT_SAFE_TIME = 30 # seconds
221 CAPTCHA_DEFAULT_SAFE_TIME = 30 # seconds
222 POSTING_DELAY = 30 # seconds
222 POSTING_DELAY = 20 # seconds
223
223
224
224
225 def custom_show_toolbar(request):
225 def custom_show_toolbar(request):
226 return DEBUG
226 return DEBUG
227
227
228 DEBUG_TOOLBAR_CONFIG = {
228 DEBUG_TOOLBAR_CONFIG = {
229 'INTERCEPT_REDIRECTS': False,
229 'INTERCEPT_REDIRECTS': False,
230 'SHOW_TOOLBAR_CALLBACK': custom_show_toolbar,
230 'SHOW_TOOLBAR_CALLBACK': custom_show_toolbar,
231 'HIDE_DJANGO_SQL': False,
231 'HIDE_DJANGO_SQL': False,
232 'ENABLE_STACKTRACES': True,
232 'ENABLE_STACKTRACES': True,
233 }
233 }
234
234
235 # Debug mode middlewares
235 # Debug mode middlewares
236 if DEBUG:
236 if DEBUG:
237 MIDDLEWARE_CLASSES += (
237 MIDDLEWARE_CLASSES += (
238 'boards.profiler.ProfilerMiddleware',
238 'boards.profiler.ProfilerMiddleware',
239 'debug_toolbar.middleware.DebugToolbarMiddleware',
239 'debug_toolbar.middleware.DebugToolbarMiddleware',
240 )
240 )
@@ -1,41 +1,43 b''
1 = Features =
1 = Features =
2 [DONE] Connecting tags to each other
2 [DONE] Connecting tags to each other
3 [DONE] Connect posts to the replies (in messages), get rid of the JS reply map
3 [DONE] Connect posts to the replies (in messages), get rid of the JS reply map
4 [DONE] Better django admin pages to simplify admin operations
4 [DONE] Better django admin pages to simplify admin operations
5 [DONE] Regen script to update all posts
5 [DONE] Regen script to update all posts
6 [DONE] Remove jump links from refmaps
6 [DONE] Remove jump links from refmaps
7 [DONE] Ban reasons. Split bans into 2 types "read-only" and "read
7 [DONE] Ban reasons. Split bans into 2 types "read-only" and "read
8 denied". Use second only for autoban for spam
8 denied". Use second only for autoban for spam
9 [DONE] Clean up tests and make them run ALWAYS
9 [DONE] Clean up tests and make them run ALWAYS
10 [DONE] Use transactions in tests
10 [DONE] Use transactions in tests
11
11
12 [IN PROGRESS] Thread autoupdate (JS + API)
13
12 [NOT STARTED] Tree view (JS)
14 [NOT STARTED] Tree view (JS)
13 [NOT STARTED] Adding tags to images filename
15 [NOT STARTED] Adding tags to images filename
14 [NOT STARTED] Federative network for s2s communication
16 [NOT STARTED] Federative network for s2s communication
15 [NOT STARTED] XMPP gate
17 [NOT STARTED] XMPP gate
16 [NOT STARTED] Bitmessage gate
18 [NOT STARTED] Bitmessage gate
17 [NOT STARTED] Notification engine
19 [NOT STARTED] Notification engine
18 [NOT STARTED] Javascript disabling engine
20 [NOT STARTED] Javascript disabling engine
19 [NOT STARTED] Thread autoupdate (JS + API)
20 [NOT STARTED] Group tags by first letter in all tags list
21 [NOT STARTED] Group tags by first letter in all tags list
21 [NOT STARTED] Show board speed in the lower panel (posts per day)
22 [NOT STARTED] Show board speed in the lower panel (posts per day)
22 [NOT STARTED] Character counter in the post field
23 [NOT STARTED] Character counter in the post field
23 [NOT STARTED] Save image thumbnails size to the separate field
24 [NOT STARTED] Save image thumbnails size to the separate field
24 [NOT STARTED] Whitelist functionality. Permin autoban of an address
25 [NOT STARTED] Whitelist functionality. Permin autoban of an address
25 [NOT STARTED] Split up post model into post and thread,
26 [NOT STARTED] Split up post model into post and thread,
26 and move everything that is used only in 1st post to thread model.
27 and move everything that is used only in 1st post to thread model.
27 [NOT STARTED] Statistics module. Count views (optional, may result in bad
28 [NOT STARTED] Statistics module. Count views (optional, may result in bad
28 performance), posts per day/week/month, users (or IPs)
29 performance), posts per day/week/month, users (or IPs)
29 [NOT STARTED] Quote button next to "reply" for posts in thread to include full
30 [NOT STARTED] Quote button next to "reply" for posts in thread to include full
30 post or its part (delimited by N characters) into quote of the new post.
31 post or its part (delimited by N characters) into quote of the new post.
31 [NOT STARTED] Ban confirmation page with reason
32 [NOT STARTED] Ban confirmation page with reason
32 [NOT STARTED] Post deletion confirmation page
33 [NOT STARTED] Post deletion confirmation page
34 [NOT STARTED] Moderating page. Tags editing and adding
33
35
34 = Bugs =
36 = Bugs =
35 [DONE] Fix bug with creating threads from tag view
37 [DONE] Fix bug with creating threads from tag view
36 [DONE] Quote characters within quote causes quote parsing to fail
38 [DONE] Quote characters within quote causes quote parsing to fail
37
39
38 = Testing =
40 = Testing =
39 [NOT STARTED] Make tests for every view
41 [NOT STARTED] Make tests for every view
40 [NOT STARTED] Make tests for every model
42 [NOT STARTED] Make tests for every model
41 [NOT STARTED] Make tests for every form
43 [NOT STARTED] Make tests for every form
General Comments 0
You need to be logged in to leave comments. Login now