##// END OF EJS Templates
Rewriting views to class-based
neko259 -
r542:8b7899f5 1.7-dev
parent child Browse files
Show More
@@ -0,0 +1,134 b''
1 import string
2
3 from django.core.paginator import Paginator
4 from django.core.urlresolvers import reverse
5 from django.db import transaction
6 from django.shortcuts import render, redirect
7
8 from boards import utils
9 from boards.forms import ThreadForm, PlainErrorList
10 from boards.models import Post, Thread, Ban, Tag
11 from boards.views.banned import BannedView
12 from boards.views.base import BaseBoardView, PARAMETER_FORM
13 from boards.views.posting_mixin import PostMixin
14 import neboard
15
16 PARAMETER_CURRENT_PAGE = 'current_page'
17
18 PARAMETER_PAGINATOR = 'paginator'
19
20 PARAMETER_THREADS = 'threads'
21
22 TEMPLATE = 'boards/posting_general.html'
23 DEFAULT_PAGE = 1
24
25
26 class AllThreadsView(PostMixin, BaseBoardView):
27
28 def get(self, request, page=DEFAULT_PAGE):
29 context = self.get_context_data(request=request)
30
31 form = ThreadForm(error_class=PlainErrorList)
32
33 paginator = Paginator(self.get_threads(),
34 neboard.settings.THREADS_PER_PAGE)
35
36 threads = paginator.page(page).object_list
37
38 context[PARAMETER_THREADS] = threads
39 context[PARAMETER_FORM] = form
40
41 self._get_page_context(paginator, context, page)
42
43 return render(request, TEMPLATE, context)
44
45 def post(self, request, page=DEFAULT_PAGE):
46 context = self.get_context_data(request=request)
47
48 form = ThreadForm(request.POST, request.FILES,
49 error_class=PlainErrorList)
50 form.session = request.session
51
52 if form.is_valid():
53 return self._new_post(request, form)
54 if form.need_to_ban:
55 # Ban user because he is suspected to be a bot
56 self._ban_current_user(request)
57
58 paginator = Paginator(self.get_threads(),
59 neboard.settings.THREADS_PER_PAGE)
60
61 threads = paginator.page(page).object_list
62
63 context[PARAMETER_THREADS] = threads
64 context[PARAMETER_FORM] = form
65
66 self._get_page_context(paginator, context, page)
67
68 return render(request, TEMPLATE, context)
69
70 @staticmethod
71 def _get_page_context(paginator, context, page):
72 """
73 Get pagination context variables
74 """
75
76 context[PARAMETER_PAGINATOR] = paginator
77 context[PARAMETER_CURRENT_PAGE] = paginator.page(int(page))
78
79 # TODO This method should be refactored
80 @transaction.atomic
81 def _new_post(self, request, form, opening_post=None, html_response=True):
82 """
83 Add a new thread opening post.
84 """
85
86 ip = utils.get_client_ip(request)
87 is_banned = Ban.objects.filter(ip=ip).exists()
88
89 if is_banned:
90 if html_response:
91 return redirect(BannedView().as_view())
92 else:
93 return
94
95 data = form.cleaned_data
96
97 title = data['title']
98 text = data['text']
99
100 text = self._remove_invalid_links(text)
101
102 if 'image' in data.keys():
103 image = data['image']
104 else:
105 image = None
106
107 tags = []
108
109 tag_strings = data['tags']
110
111 if tag_strings:
112 tag_strings = tag_strings.split(' ')
113 for tag_name in tag_strings:
114 tag_name = string.lower(tag_name.strip())
115 if len(tag_name) > 0:
116 tag, created = Tag.objects.get_or_create(name=tag_name)
117 tags.append(tag)
118
119 post = Post.objects.create_post(title=title, text=text, ip=ip,
120 image=image, tags=tags,
121 user=self._get_user(request))
122
123 thread_to_show = (opening_post.id if opening_post else post.id)
124
125 if html_response:
126 if opening_post:
127 return redirect(
128 reverse('thread', kwargs={'post_id': thread_to_show}) +
129 '#' + str(post.id))
130 else:
131 return redirect('thread', post_id=thread_to_show)
132
133 def get_threads(self):
134 return Thread.objects.filter(archived=False) No newline at end of file
@@ -0,0 +1,10 b''
1 from boards.models import Thread
2 from boards.views.all_threads import AllThreadsView
3
4 __author__ = 'neko259'
5
6
7 class ArchiveView(AllThreadsView):
8
9 def get_threads(self):
10 return Thread.objects.filter(archived=True) No newline at end of file
@@ -0,0 +1,16 b''
1 from django.shortcuts import get_object_or_404, render
2 from boards import utils
3 from boards.models import Ban
4 from boards.views.base import BaseBoardView
5
6
7 class BannedView(BaseBoardView):
8
9 def get(self, request):
10 """Show the page that notifies that user is banned"""
11
12 context = self.get_context_data(request)
13
14 ban = get_object_or_404(Ban, ip=utils.get_client_ip(request))
15 context['ban_reason'] = ban.reason
16 return render(request, 'boards/staticpages/banned.html', context)
@@ -0,0 +1,125 b''
1 from datetime import datetime, timedelta
2 import hashlib
3 from django.db import transaction
4 from django.db.models import Count
5 from django.template import RequestContext
6 from django.utils import timezone
7 from django.views.generic import View
8 from boards import utils
9 from boards.models import User, Post
10 from boards.models.post import SETTING_MODERATE
11 from boards.models.user import RANK_USER, Ban
12 import neboard
13
14 BAN_REASON_SPAM = 'Autoban: spam bot'
15
16 OLD_USER_AGE_DAYS = 90
17
18 PARAMETER_FORM = 'form'
19
20
21 class BaseBoardView(View):
22
23 def get_context_data(self, **kwargs):
24 request = kwargs['request']
25 context = self._default_context(request)
26
27 context['version'] = neboard.settings.VERSION
28 context['site_name'] = neboard.settings.SITE_NAME
29
30 return context
31
32 def _default_context(self, request):
33 """Create context with default values that are used in most views"""
34
35 context = RequestContext(request)
36
37 user = self._get_user(request)
38 context['user'] = user
39 context['tags'] = user.get_sorted_fav_tags()
40 context['posts_per_day'] = float(Post.objects.get_posts_per_day())
41
42 theme = self._get_theme(request, user)
43 context['theme'] = theme
44 context['theme_css'] = 'css/' + theme + '/base_page.css'
45
46 # This shows the moderator panel
47 moderate = user.get_setting(SETTING_MODERATE)
48 if moderate == 'True':
49 context['moderator'] = user.is_moderator()
50 else:
51 context['moderator'] = False
52
53 return context
54
55 def _get_user(self, request):
56 """
57 Get current user from the session. If the user does not exist, create
58 a new one.
59 """
60
61 session = request.session
62 if not 'user_id' in session:
63 request.session.save()
64
65 md5 = hashlib.md5()
66 md5.update(session.session_key)
67 new_id = md5.hexdigest()
68
69 while User.objects.filter(user_id=new_id).exists():
70 md5.update(str(timezone.now()))
71 new_id = md5.hexdigest()
72
73 time_now = timezone.now()
74 user = User.objects.create(user_id=new_id, rank=RANK_USER,
75 registration_time=time_now)
76
77 self._delete_old_users()
78
79 session['user_id'] = user.id
80 else:
81 user = User.objects.get(id=session['user_id'])
82
83 return user
84
85 def _get_theme(self, request, user=None):
86 """
87 Get user's CSS theme
88 """
89
90 if not user:
91 user = self._get_user(request)
92 theme = user.get_setting('theme')
93 if not theme:
94 theme = neboard.settings.DEFAULT_THEME
95
96 return theme
97
98 def _delete_old_users(self):
99 """
100 Delete users with no favorite tags and posted messages. These can be spam
101 bots or just old user accounts
102 """
103
104 old_registration_date = datetime.now().date() - timedelta(
105 OLD_USER_AGE_DAYS)
106
107 for user in User.objects.annotate(tags_count=Count('fav_tags')).filter(
108 tags_count=0).filter(
109 registration_time__lt=old_registration_date):
110 if not Post.objects.filter(user=user).exists():
111 user.delete()
112
113 @transaction.atomic
114 def _ban_current_user(self, request):
115 """
116 Add current user to the IP ban list
117 """
118
119 ip = utils.get_client_ip(request)
120 ban, created = Ban.objects.get_or_create(ip=ip)
121 if created:
122 ban.can_read = False
123 ban.reason = BAN_REASON_SPAM
124 ban.save()
125
@@ -0,0 +1,25 b''
1 import re
2 import string
3
4 from boards.models import Post
5 from boards.models.post import REGEX_REPLY
6
7 REFLINK_PREFIX = '>>'
8
9
10 class PostMixin:
11
12 @staticmethod
13 def _remove_invalid_links(text):
14 """
15 Replace invalid links in posts so that they won't be parsed.
16 Invalid links are links to non-existent posts
17 """
18
19 for reply_number in re.finditer(REGEX_REPLY, text):
20 post_id = reply_number.group(1)
21 post = Post.objects.filter(id=post_id)
22 if not post.exists():
23 text = string.replace(text, REFLINK_PREFIX + post_id, post_id)
24
25 return text
@@ -0,0 +1,28 b''
1 from django.shortcuts import get_object_or_404
2 from boards.models import Tag, Post
3 from boards.views.all_threads import AllThreadsView, DEFAULT_PAGE
4
5 __author__ = 'neko259'
6
7
8 class TagView(AllThreadsView):
9
10 tag_name = None
11
12 def get_threads(self):
13 tag = get_object_or_404(Tag, name=self.tag_name)
14
15 return tag.threads.filter(archived=False)
16
17 def get_context_data(self, **kwargs):
18 context = super(TagView, self).get_context_data(**kwargs)
19
20 tag = get_object_or_404(Tag, name=self.tag_name)
21 context['tag'] = tag
22
23 return context
24
25 def get(self, request, tag_name, page=DEFAULT_PAGE):
26 self.tag_name = tag_name
27
28 return super(TagView, self).get(request, page) No newline at end of file
@@ -0,0 +1,149 b''
1 import string
2 from django.core.urlresolvers import reverse
3 from django.db import transaction
4 from django.http import Http404
5 from django.shortcuts import get_object_or_404, render, redirect
6 from boards import utils
7 from boards.forms import PostForm, PlainErrorList
8 from boards.models import Post, Ban, Tag
9 from boards.views.banned import BannedView
10 from boards.views.base import BaseBoardView, PARAMETER_FORM
11 from boards.views.posting_mixin import PostMixin
12 import neboard
13
14 MODE_GALLERY = 'gallery'
15 MODE_NORMAL = 'normal'
16
17
18 class ThreadView(BaseBoardView, PostMixin):
19
20 def get(self, request, post_id, mode=MODE_NORMAL):
21 opening_post = get_object_or_404(Post, id=post_id)
22
23 # If this is not OP, don't show it as it is
24 if not opening_post.is_opening():
25 raise Http404
26
27 form = PostForm(error_class=PlainErrorList)
28
29 thread_to_show = opening_post.thread_new
30
31 context = self.get_context_data(request=request)
32
33 posts = thread_to_show.get_replies()
34 context[PARAMETER_FORM] = form
35 context["last_update"] = utils.datetime_to_epoch(
36 thread_to_show.last_edit_time)
37 context["thread"] = thread_to_show
38
39 if MODE_NORMAL == mode:
40 context['bumpable'] = thread_to_show.can_bump()
41 if context['bumpable']:
42 context['posts_left'] = neboard.settings.MAX_POSTS_PER_THREAD - posts \
43 .count()
44 context['bumplimit_progress'] = str(
45 float(context['posts_left']) /
46 neboard.settings.MAX_POSTS_PER_THREAD * 100)
47
48 context['posts'] = posts
49
50 document = 'boards/thread.html'
51 elif MODE_GALLERY == mode:
52 context['posts'] = posts.filter(image_width__gt=0)
53
54 document = 'boards/thread_gallery.html'
55 else:
56 raise Http404
57
58 return render(request, document, context)
59
60 def post(self, request, post_id, mode=MODE_NORMAL):
61 opening_post = get_object_or_404(Post, id=post_id)
62
63 # If this is not OP, don't show it as it is
64 if not opening_post.is_opening():
65 raise Http404
66
67 if not opening_post.thread_new.archived:
68 form = PostForm(request.POST, request.FILES,
69 error_class=PlainErrorList)
70 form.session = request.session
71
72 if form.is_valid():
73 return self.new_post(request, form, opening_post)
74 if form.need_to_ban:
75 # Ban user because he is suspected to be a bot
76 self._ban_current_user(request)
77
78 thread_to_show = opening_post.thread_new
79
80 context = self.get_context_data(request=request)
81
82 posts = thread_to_show.get_replies()
83 context[PARAMETER_FORM] = form
84 context["last_update"] = utils.datetime_to_epoch(
85 thread_to_show.last_edit_time)
86 context["thread"] = thread_to_show
87
88 if MODE_NORMAL == mode:
89 context['bumpable'] = thread_to_show.can_bump()
90 if context['bumpable']:
91 context['posts_left'] = neboard.settings.MAX_POSTS_PER_THREAD - posts \
92 .count()
93 context['bumplimit_progress'] = str(
94 float(context['posts_left']) /
95 neboard.settings.MAX_POSTS_PER_THREAD * 100)
96
97 context['posts'] = posts
98
99 document = 'boards/thread.html'
100 elif MODE_GALLERY == mode:
101 context['posts'] = posts.filter(image_width__gt=0)
102
103 document = 'boards/thread_gallery.html'
104 else:
105 raise Http404
106
107 return render(request, document, context)
108
109 @transaction.atomic
110 def new_post(self, request, form, opening_post=None, html_response=True):
111 """Add a new post (in thread or as a reply)."""
112
113 ip = utils.get_client_ip(request)
114 is_banned = Ban.objects.filter(ip=ip).exists()
115
116 if is_banned:
117 if html_response:
118 return redirect(BannedView().as_view())
119 else:
120 return
121
122 data = form.cleaned_data
123
124 title = data['title']
125 text = data['text']
126
127 text = self._remove_invalid_links(text)
128
129 if 'image' in data.keys():
130 image = data['image']
131 else:
132 image = None
133
134 tags = []
135
136 post_thread = opening_post.thread_new
137
138 post = Post.objects.create_post(title=title, text=text, ip=ip,
139 thread=post_thread, image=image,
140 tags=tags,
141 user=self._get_user(request))
142
143 thread_to_show = (opening_post.id if opening_post else post.id)
144
145 if html_response:
146 if opening_post:
147 return redirect(reverse('thread',
148 kwargs={'post_id': thread_to_show}) + '#'
149 + str(post.id)) No newline at end of file
@@ -1,41 +1,41 b''
1 1 from django.shortcuts import redirect
2 2 from boards import views, utils
3 3 from boards.models import Ban
4 4 from django.utils.html import strip_spaces_between_tags
5 5 from django.conf import settings
6 6
7 7 RESPONSE_CONTENT_TYPE = 'Content-Type'
8 8
9 9 TYPE_HTML = 'text/html'
10 10
11 11
12 12 class BanMiddleware:
13 13 """
14 14 This is run before showing the thread. Banned users don't need to see
15 15 anything
16 16 """
17 17
18 18 def process_view(self, request, view_func, view_args, view_kwargs):
19 19
20 if view_func != views.you_are_banned:
20 if view_func != views.banned.BannedView.as_view:
21 21 ip = utils.get_client_ip(request)
22 22 bans = Ban.objects.filter(ip=ip)
23 23
24 24 if bans.exists():
25 25 ban = bans[0]
26 26 if not ban.can_read:
27 return redirect(views.you_are_banned)
27 return redirect('banned')
28 28
29 29
30 30 class MinifyHTMLMiddleware(object):
31 31 def process_response(self, request, response):
32 32 try:
33 33 compress_html = settings.COMPRESS_HTML
34 34 except AttributeError:
35 35 compress_html = False
36 36
37 37 if RESPONSE_CONTENT_TYPE in response\
38 38 and TYPE_HTML in response[RESPONSE_CONTENT_TYPE] and compress_html:
39 39 response.content = strip_spaces_between_tags(
40 40 response.content.strip())
41 41 return response No newline at end of file
@@ -1,390 +1,394 b''
1 1 from datetime import datetime, timedelta
2 2 from datetime import time as dtime
3 3 import os
4 4 from random import random
5 5 import time
6 6 import math
7 7 import re
8 8 import hashlib
9 9
10 10 from django.core.cache import cache
11 11 from django.core.paginator import Paginator
12 12
13 13 from django.db import models
14 14 from django.http import Http404
15 15 from django.utils import timezone
16 16 from markupfield.fields import MarkupField
17 17
18 18 from neboard import settings
19 19 from boards import thumbs
20 20
21 21 MAX_TITLE_LENGTH = 50
22 22
23 23 APP_LABEL_BOARDS = 'boards'
24 24
25 25 CACHE_KEY_PPD = 'ppd'
26 26
27 27 POSTS_PER_DAY_RANGE = range(7)
28 28
29 29 BAN_REASON_AUTO = 'Auto'
30 30
31 31 IMAGE_THUMB_SIZE = (200, 150)
32 32
33 33 TITLE_MAX_LENGTH = 50
34 34
35 35 DEFAULT_MARKUP_TYPE = 'markdown'
36 36
37 37 NO_PARENT = -1
38 38 NO_IP = '0.0.0.0'
39 39 UNKNOWN_UA = ''
40 40 ALL_PAGES = -1
41 41 IMAGES_DIRECTORY = 'images/'
42 42 FILE_EXTENSION_DELIMITER = '.'
43 43
44 44 SETTING_MODERATE = "moderate"
45 45
46 46 REGEX_REPLY = re.compile('>>(\d+)')
47 47
48 48
49 49 class PostManager(models.Manager):
50 50
51 51 def create_post(self, title, text, image=None, thread=None,
52 52 ip=NO_IP, tags=None, user=None):
53 53 """
54 54 Create new post
55 55 """
56 56
57 57 posting_time = timezone.now()
58 58 if not thread:
59 59 thread = Thread.objects.create(bump_time=posting_time,
60 60 last_edit_time=posting_time)
61 61 else:
62 62 thread.bump()
63 63 thread.last_edit_time = posting_time
64 64 thread.save()
65 65
66 66 post = self.create(title=title,
67 67 text=text,
68 68 pub_time=posting_time,
69 69 thread_new=thread,
70 70 image=image,
71 71 poster_ip=ip,
72 72 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
73 73 # last!
74 74 last_edit_time=posting_time,
75 75 user=user)
76 76
77 77 thread.replies.add(post)
78 78 if tags:
79 79 linked_tags = []
80 80 for tag in tags:
81 81 tag_linked_tags = tag.get_linked_tags()
82 82 if len(tag_linked_tags) > 0:
83 83 linked_tags.extend(tag_linked_tags)
84 84
85 85 tags.extend(linked_tags)
86 86 map(thread.add_tag, tags)
87 87
88 88 self._delete_old_threads()
89 89 self.connect_replies(post)
90 90
91 91 return post
92 92
93 93 def delete_post(self, post):
94 94 """
95 95 Delete post and update or delete its thread
96 96 """
97 97
98 98 thread = post.thread_new
99 99
100 100 if thread.get_opening_post() == self:
101 101 thread.replies.delete()
102 102
103 103 thread.delete()
104 104 else:
105 105 thread.last_edit_time = timezone.now()
106 106 thread.save()
107 107
108 108 post.delete()
109 109
110 110 def delete_posts_by_ip(self, ip):
111 111 """
112 112 Delete all posts of the author with same IP
113 113 """
114 114
115 115 posts = self.filter(poster_ip=ip)
116 116 map(self.delete_post, posts)
117 117
118 118 # TODO Move this method to thread manager
119 119 def get_threads(self, tag=None, page=ALL_PAGES,
120 120 order_by='-bump_time', archived=False):
121 121 if tag:
122 122 threads = tag.threads
123 123
124 124 if not threads.exists():
125 125 raise Http404
126 126 else:
127 127 threads = Thread.objects.all()
128 128
129 129 threads = threads.filter(archived=archived).order_by(order_by)
130 130
131 131 if page != ALL_PAGES:
132 132 threads = Paginator(threads, settings.THREADS_PER_PAGE).page(
133 133 page).object_list
134 134
135 135 return threads
136 136
137 137 # TODO Move this method to thread manager
138 138 def _delete_old_threads(self):
139 139 """
140 140 Preserves maximum thread count. If there are too many threads,
141 141 archive the old ones.
142 142 """
143 143
144 144 threads = self.get_threads()
145 145 thread_count = threads.count()
146 146
147 147 if thread_count > settings.MAX_THREAD_COUNT:
148 148 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
149 149 old_threads = threads[thread_count - num_threads_to_delete:]
150 150
151 151 for thread in old_threads:
152 152 thread.archived = True
153 153 thread.last_edit_time = timezone.now()
154 154 thread.save()
155 155
156 156 def connect_replies(self, post):
157 157 """
158 158 Connect replies to a post to show them as a reflink map
159 159 """
160 160
161 161 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
162 162 post_id = reply_number.group(1)
163 163 ref_post = self.filter(id=post_id)
164 164 if ref_post.count() > 0:
165 165 referenced_post = ref_post[0]
166 166 referenced_post.referenced_posts.add(post)
167 167 referenced_post.last_edit_time = post.pub_time
168 168 referenced_post.save()
169 169
170 170 referenced_thread = referenced_post.thread_new
171 171 referenced_thread.last_edit_time = post.pub_time
172 172 referenced_thread.save()
173 173
174 174 def get_posts_per_day(self):
175 175 """
176 176 Get average count of posts per day for the last 7 days
177 177 """
178 178
179 179 today = datetime.now().date()
180 180 ppd = cache.get(CACHE_KEY_PPD + str(today))
181 181 if ppd:
182 182 return ppd
183 183
184 184 posts_per_days = []
185 185 for i in POSTS_PER_DAY_RANGE:
186 186 day_end = today - timedelta(i + 1)
187 187 day_start = today - timedelta(i + 2)
188 188
189 189 day_time_start = timezone.make_aware(datetime.combine(day_start,
190 190 dtime()), timezone.get_current_timezone())
191 191 day_time_end = timezone.make_aware(datetime.combine(day_end,
192 192 dtime()), timezone.get_current_timezone())
193 193
194 194 posts_per_days.append(float(self.filter(
195 195 pub_time__lte=day_time_end,
196 196 pub_time__gte=day_time_start).count()))
197 197
198 198 ppd = (sum(posts_per_day for posts_per_day in posts_per_days) /
199 199 len(posts_per_days))
200 200 cache.set(CACHE_KEY_PPD, ppd)
201 201 return ppd
202 202
203 203
204 204 class Post(models.Model):
205 205 """A post is a message."""
206 206
207 207 objects = PostManager()
208 208
209 209 class Meta:
210 210 app_label = APP_LABEL_BOARDS
211 211
212 212 # TODO Save original file name to some field
213 213 def _update_image_filename(self, filename):
214 214 """Get unique image filename"""
215 215
216 216 path = IMAGES_DIRECTORY
217 217 new_name = str(int(time.mktime(time.gmtime())))
218 218 new_name += str(int(random() * 1000))
219 219 new_name += FILE_EXTENSION_DELIMITER
220 220 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
221 221
222 222 return os.path.join(path, new_name)
223 223
224 224 title = models.CharField(max_length=TITLE_MAX_LENGTH)
225 225 pub_time = models.DateTimeField()
226 226 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
227 227 escape_html=False)
228 228
229 229 image_width = models.IntegerField(default=0)
230 230 image_height = models.IntegerField(default=0)
231 231
232 232 image_pre_width = models.IntegerField(default=0)
233 233 image_pre_height = models.IntegerField(default=0)
234 234
235 235 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
236 236 blank=True, sizes=(IMAGE_THUMB_SIZE,),
237 237 width_field='image_width',
238 238 height_field='image_height',
239 239 preview_width_field='image_pre_width',
240 240 preview_height_field='image_pre_height')
241 241 image_hash = models.CharField(max_length=36)
242 242
243 243 poster_ip = models.GenericIPAddressField()
244 244 poster_user_agent = models.TextField()
245 245
246 246 thread = models.ForeignKey('Post', null=True, default=None)
247 247 thread_new = models.ForeignKey('Thread', null=True, default=None)
248 248 last_edit_time = models.DateTimeField()
249 249 user = models.ForeignKey('User', null=True, default=None)
250 250
251 251 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
252 252 null=True,
253 253 blank=True, related_name='rfp+')
254 254
255 255 def __unicode__(self):
256 256 return '#' + str(self.id) + ' ' + self.title + ' (' + \
257 257 self.text.raw[:50] + ')'
258 258
259 259 def get_title(self):
260 260 title = self.title
261 261 if len(title) == 0:
262 262 title = self.text.rendered
263 263
264 264 return title
265 265
266 266 def get_sorted_referenced_posts(self):
267 267 return self.referenced_posts.order_by('id')
268 268
269 269 def is_referenced(self):
270 270 return self.referenced_posts.all().exists()
271 271
272 272 def is_opening(self):
273 273 return self.thread_new.get_replies()[0] == self
274 274
275 275 def save(self, *args, **kwargs):
276 276 """
277 277 Save the model and compute the image hash
278 278 """
279 279
280 280 if not self.pk and self.image:
281 281 md5 = hashlib.md5()
282 282 for chunk in self.image.chunks():
283 283 md5.update(chunk)
284 284 self.image_hash = md5.hexdigest()
285 285 super(Post, self).save(*args, **kwargs)
286 286
287 287
288 288 class Thread(models.Model):
289 289
290 290 class Meta:
291 291 app_label = APP_LABEL_BOARDS
292 292
293 293 tags = models.ManyToManyField('Tag')
294 294 bump_time = models.DateTimeField()
295 295 last_edit_time = models.DateTimeField()
296 296 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
297 297 blank=True, related_name='tre+')
298 298 archived = models.BooleanField(default=False)
299 299
300 300 def get_tags(self):
301 301 """
302 302 Get a sorted tag list
303 303 """
304 304
305 305 return self.tags.order_by('name')
306 306
307 307 def bump(self):
308 308 """
309 309 Bump (move to up) thread
310 310 """
311 311
312 312 if self.can_bump():
313 313 self.bump_time = timezone.now()
314 314
315 315 def get_reply_count(self):
316 316 return self.replies.count()
317 317
318 318 def get_images_count(self):
319 319 return self.replies.filter(image_width__gt=0).count()
320 320
321 321 def can_bump(self):
322 322 """
323 323 Check if the thread can be bumped by replying
324 324 """
325 325
326 326 if self.archived:
327 327 return False
328 328
329 329 post_count = self.get_reply_count()
330 330
331 331 return post_count < settings.MAX_POSTS_PER_THREAD
332 332
333 333 def delete_with_posts(self):
334 334 """
335 335 Completely delete thread and all its posts
336 336 """
337 337
338 338 if self.replies.count() > 0:
339 339 self.replies.all().delete()
340 340
341 341 self.delete()
342 342
343 343 def get_last_replies(self):
344 344 """
345 345 Get last replies, not including opening post
346 346 """
347 347
348 348 if settings.LAST_REPLIES_COUNT > 0:
349 349 reply_count = self.get_reply_count()
350 350
351 351 if reply_count > 0:
352 352 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
353 353 reply_count - 1)
354 354 last_replies = self.replies.all().order_by('pub_time')[
355 355 reply_count - reply_count_to_show:]
356 356
357 357 return last_replies
358 358
359 def get_skipped_replies_count(self):
360 last_replies = self.get_last_replies()
361 return self.get_reply_count() - len(last_replies) - 1
362
359 363 def get_replies(self):
360 364 """
361 365 Get sorted thread posts
362 366 """
363 367
364 368 return self.replies.all().order_by('pub_time')
365 369
366 370 def add_tag(self, tag):
367 371 """
368 372 Connect thread to a tag and tag to a thread
369 373 """
370 374
371 375 self.tags.add(tag)
372 376 tag.threads.add(self)
373 377
374 378 def get_opening_post(self):
375 379 """
376 380 Get first post of the thread
377 381 """
378 382
379 383 return self.get_replies()[0]
380 384
381 385 def __unicode__(self):
382 386 return str(self.get_replies()[0].id)
383 387
384 388 def get_pub_time(self):
385 389 """
386 390 Thread does not have its own pub time, so we need to get it from
387 391 the opening post
388 392 """
389 393
390 394 return self.get_opening_post().pub_time
@@ -1,29 +1,29 b''
1 1 {% extends "boards/base.html" %}
2 2
3 3 {% load i18n %}
4 4
5 5 {% block head %}
6 <title>{% trans 'Login' %}</title>
6 <title>{% trans 'Login' %} - {{ site_name }}</title>
7 7 {% endblock %}
8 8
9 9 {% block content %}
10 10
11 11 <form enctype="multipart/form-data" method="post">
12 12 <div class="post-form-w">
13 13 <div class="post-form">
14 14 <div class="form-row">
15 15 <div class="form-label">{% trans 'User ID' %}</div>
16 16 <div class="form-input">{{ form.user_id }}</div>
17 17 <div class="form-errors">{{ form.user_id.errors }}</div>
18 18 </div>
19 19 </div>
20 20 <div class="form-submit">
21 21 <input type="submit" value="{% trans "Login" %}"/>
22 22 </div>
23 23 <div>
24 24 {% trans 'Insert your user id above' %}
25 25 </div>
26 26 </div>
27 27 </form>
28 28
29 29 {% endblock %} No newline at end of file
@@ -1,83 +1,91 b''
1 1 {% load i18n %}
2 2 {% load board %}
3 3
4 4 {% spaceless %}
5 {% if post.thread_new.archived %}
6 <div class="post archive_post" id="{{ post.id }}">
7 {% elif post.thread_new.can_bump %}
8 <div class="post" id="{{ post.id }}">
9 {% else %}
10 <div class="post dead_post" id="{{ post.id }}">
11 {% endif %}
12
13 {% if post.image %}
14 <div class="image">
15 <a
16 class="thumb"
17 href="{{ post.image.url }}"><img
18 src="{{ post.image.url_200x150 }}"
19 alt="{{ post.id }}"
20 width="{{ post.image_pre_width }}"
21 height="{{ post.image_pre_height }}"
22 data-width="{{ post.image_width }}"
23 data-height="{{ post.image_height }}"/>
24 </a>
25 </div>
26 {% endif %}
27 <div class="message">
28 <div class="post-info">
29 <span class="title">{{ post.title }}</span>
30 <a class="post_id" href="{% post_url post.id %}">
31 ({{ post.id }}) </a>
32 [<span class="pub_time">{{ post.pub_time }}</span>]
33 {% if not truncated %}
34 [<a href="#" onclick="javascript:addQuickReply('{{ post.id }}')
35 ; return false;">&gt;&gt;</a>]
36 {% endif %}
37 {% if post.is_opening and need_open_link %}
38 [<a class="link" href="
39 {% url 'thread' post.id %}#form">{% trans "Reply" %}</a>]
5 {% with thread=post.thread_new %}
6 {% if thread.archived %}
7 <div class="post archive_post" id="{{ post.id }}">
8 {% elif thread.can_bump %}
9 <div class="post" id="{{ post.id }}">
10 {% else %}
11 <div class="post dead_post" id="{{ post.id }}">
40 12 {% endif %}
41 13
42 {% if moderator %}
43 <span class="moderator_info">
44 [<a href="{% url 'delete' post_id=post.id %}"
45 >{% trans 'Delete' %}</a>]
46 ({{ post.poster_ip }})
47 [<a href="{% url 'ban' post_id=post.id %}?next={{ request.path }}"
48 >{% trans 'Ban IP' %}</a>]
49 </span>
14 {% if post.image %}
15 <div class="image">
16 <a
17 class="thumb"
18 href="{{ post.image.url }}"><img
19 src="{{ post.image.url_200x150 }}"
20 alt="{{ post.id }}"
21 width="{{ post.image_pre_width }}"
22 height="{{ post.image_pre_height }}"
23 data-width="{{ post.image_width }}"
24 data-height="{{ post.image_height }}"/>
25 </a>
26 </div>
27 {% endif %}
28 <div class="message">
29 <div class="post-info">
30 <span class="title">{{ post.title }}</span>
31 <a class="post_id" href="{% post_url post.id %}">
32 ({{ post.id }}) </a>
33 [<span class="pub_time">{{ post.pub_time }}</span>]
34 {% if thread.archived %}
35 — [{{ thread.bump_time }}]
36 {% endif %}
37 {% if not truncated %}
38 [<a href="#" onclick="javascript:addQuickReply('{{ post.id }}')
39 ; return false;">&gt;&gt;</a>]
40 {% endif %}
41 {% if post.is_opening and need_open_link %}
42 {% if post.thread_new.archived %}
43 [<a class="link" href="{% url 'thread' post.id %}">{% trans "Open" %}</a>]
44 {% else %}
45 [<a class="link" href="{% url 'thread' post.id %}#form">{% trans "Reply" %}</a>]
46 {% endif %}
47 {% endif %}
48
49 {% if moderator %}
50 <span class="moderator_info">
51 [<a href="{% url 'delete' post_id=post.id %}"
52 >{% trans 'Delete' %}</a>]
53 ({{ post.poster_ip }})
54 [<a href="{% url 'ban' post_id=post.id %}?next={{ request.path }}"
55 >{% trans 'Ban IP' %}</a>]
56 </span>
57 {% endif %}
58 </div>
59 {% autoescape off %}
60 {% if truncated %}
61 {{ post.text.rendered|truncatewords_html:50 }}
62 {% else %}
63 {{ post.text.rendered }}
64 {% endif %}
65 {% endautoescape %}
66 {% if post.is_referenced %}
67 <div class="refmap">
68 {% trans "Replies" %}:
69 {% for ref_post in post.get_sorted_referenced_posts %}
70 <a href="{% post_url ref_post.id %}">&gt;&gt;{{ ref_post.id }}</a
71 >{% if not forloop.last %},{% endif %}
72 {% endfor %}
73 </div>
50 74 {% endif %}
51 75 </div>
52 {% autoescape off %}
53 {% if truncated %}
54 {{ post.text.rendered|truncatewords_html:50 }}
55 {% else %}
56 {{ post.text.rendered }}
57 {% endif %}
58 {% endautoescape %}
59 {% if post.is_referenced %}
60 <div class="refmap">
61 {% trans "Replies" %}:
62 {% for ref_post in post.get_sorted_referenced_posts %}
63 <a href="{% post_url ref_post.id %}">&gt;&gt;{{ ref_post.id }}</a
64 >{% if not forloop.last %},{% endif %}
65 {% endfor %}
76 {% if post.is_opening and thread.tags.exists %}
77 <div class="metadata">
78 {% if post.is_opening and need_open_link %}
79 {{ thread.get_images_count }} {% trans 'images' %}.
80 {% endif %}
81 <span class="tags">
82 {% for tag in thread.get_tags %}
83 <a class="tag" href="{% url 'tag' tag.name %}">
84 #{{ tag.name }}</a>{% if not forloop.last %},{% endif %}
85 {% endfor %}
86 </span>
66 87 </div>
67 88 {% endif %}
68 </div>
69 {% if post.is_opening and post.thread_new.tags.exists %}
70 <div class="metadata">
71 {% if post.is_opening and need_open_link %}
72 {{ post.thread_new.get_images_count }} {% trans 'images' %}.
73 {% endif %}
74 <span class="tags">
75 {% for tag in post.thread_new.get_tags %}
76 <a class="tag" href="{% url 'tag' tag.name %}">
77 #{{ tag.name }}</a>{% if not forloop.last %},{% endif %}
78 {% endfor %}
79 </span>
80 89 </div>
81 {% endif %}
82 </div>
90 {% endwith %}
83 91 {% endspaceless %}
@@ -1,149 +1,151 b''
1 1 {% extends "boards/base.html" %}
2 2
3 3 {% load i18n %}
4 4 {% load cache %}
5 5 {% load board %}
6 6 {% load static %}
7 7
8 8 {% block head %}
9 9 {% if tag %}
10 <title>Neboard - {{ tag.name }}</title>
10 <title>{{ tag.name }} - {{ site_name }}</title>
11 11 {% else %}
12 <title>Neboard</title>
12 <title>{{ site_name }}</title>
13 13 {% endif %}
14 14
15 15 {% if current_page.has_previous %}
16 16 <link rel="prev" href="
17 17 {% if tag %}
18 18 {% url "tag" tag_name=tag page=current_page.previous_page_number %}
19 19 {% else %}
20 20 {% url "index" page=current_page.previous_page_number %}
21 21 {% endif %}
22 22 " />
23 23 {% endif %}
24 24 {% if current_page.has_next %}
25 25 <link rel="next" href="
26 26 {% if tag %}
27 27 {% url "tag" tag_name=tag page=current_page.next_page_number %}
28 28 {% else %}
29 29 {% url "index" page=current_page.next_page_number %}
30 30 {% endif %}
31 31 " />
32 32 {% endif %}
33 33
34 34 {% endblock %}
35 35
36 36 {% block content %}
37 37
38 38 {% get_current_language as LANGUAGE_CODE %}
39 39
40 40 {% if tag %}
41 41 <div class="tag_info">
42 42 <h2>
43 43 {% if tag in user.fav_tags.all %}
44 44 <a href="{% url 'tag_unsubscribe' tag.name %}?next={{ request.path }}"
45 45 class="fav"></a>
46 46 {% else %}
47 47 <a href="{% url 'tag_subscribe' tag.name %}?next={{ request.path }}"
48 48 class="not_fav"></a>
49 49 {% endif %}
50 50 #{{ tag.name }}
51 51 </h2>
52 52 </div>
53 53 {% endif %}
54 54
55 55 {% if threads %}
56 56 {% if current_page.has_previous %}
57 57 <div class="page_link">
58 58 <a href="
59 59 {% if tag %}
60 60 {% url "tag" tag_name=tag page=current_page.previous_page_number %}
61 61 {% else %}
62 62 {% url "index" page=current_page.previous_page_number %}
63 63 {% endif %}
64 64 ">{% trans "Previous page" %}</a>
65 65 </div>
66 66 {% endif %}
67 67
68 68 {% for thread in threads %}
69 {% cache 600 thread_short thread.thread.id thread.thread.last_edit_time moderator LANGUAGE_CODE %}
69 {% cache 600 thread_short thread.id thread.last_edit_time moderator LANGUAGE_CODE %}
70 70 <div class="thread">
71 {% post_view_truncated thread.op True moderator %}
72 {% if thread.last_replies.exists %}
73 {% if thread.skipped_replies %}
71 {% post_view_truncated thread.get_opening_post True moderator %}
72 {% if not thread.archived %}
73 {% if thread.get_last_replies.exists %}
74 {% if thread.get_skipped_replies_count %}
74 75 <div class="skipped_replies">
75 <a href="{% url 'thread' thread.op.id %}">
76 {% blocktrans with count=thread.skipped_replies %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %}
76 <a href="{% url 'thread' thread.get_opening_post.id %}">
77 {% blocktrans with count=thread.get_skipped_replies_count %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %}
77 78 </a>
78 79 </div>
79 80 {% endif %}
80 81 <div class="last-replies">
81 {% for post in thread.last_replies %}
82 {% for post in thread.get_last_replies %}
82 83 {% post_view_truncated post moderator=moderator%}
83 84 {% endfor %}
84 85 </div>
85 86 {% endif %}
87 {% endif %}
86 88 </div>
87 89 {% endcache %}
88 90 {% endfor %}
89 91
90 92 {% if current_page.has_next %}
91 93 <div class="page_link">
92 94 <a href="
93 95 {% if tag %}
94 96 {% url "tag" tag_name=tag page=current_page.next_page_number %}
95 97 {% else %}
96 98 {% url "index" page=current_page.next_page_number %}
97 99 {% endif %}
98 100 ">{% trans "Next page" %}</a>
99 101 </div>
100 102 {% endif %}
101 103 {% else %}
102 104 <div class="post">
103 105 {% trans 'No threads exist. Create the first one!' %}</div>
104 106 {% endif %}
105 107
106 108 <div class="post-form-w">
107 109 <script src="{% static 'js/panel.js' %}"></script>
108 110 <div class="post-form">
109 111 <div class="form-title">{% trans "Create new thread" %}</div>
110 112 <form enctype="multipart/form-data" method="post">{% csrf_token %}
111 113 {{ form.as_div }}
112 114 <div class="form-submit">
113 115 <input type="submit" value="{% trans "Post" %}"/>
114 116 </div>
115 117 </form>
116 118 <div>
117 119 {% trans 'Tags must be delimited by spaces. Text or image is required.' %}
118 120 </div>
119 121 <div><a href="{% url "staticpage" name="help" %}">
120 122 {% trans 'Text syntax' %}</a></div>
121 123 </div>
122 124 </div>
123 125
124 126 {% endblock %}
125 127
126 128 {% block metapanel %}
127 129
128 130 <span class="metapanel">
129 <b><a href="{% url "authors" %}">Neboard</a> 1.6 Amon</b>
131 <b><a href="{% url "authors" %}">{{ site_name }}</a> {{ version }}</b>
130 132 {% trans "Pages:" %}[
131 133 {% for page in paginator.page_range %}
132 134 <a
133 135 {% ifequal page current_page.number %}
134 136 class="current_page"
135 137 {% endifequal %}
136 138 href="
137 139 {% if tag %}
138 140 {% url "tag" tag_name=tag page=page %}
139 141 {% else %}
140 142 {% url "index" page=page %}
141 143 {% endif %}
142 144 ">{{ page }}</a>
143 145 {% if not forloop.last %},{% endif %}
144 146 {% endfor %}
145 147 ]
146 148 [<a href="rss/">RSS</a>]
147 149 </span>
148 150
149 151 {% endblock %}
@@ -1,37 +1,37 b''
1 1 {% extends "boards/base.html" %}
2 2
3 3 {% load i18n %}
4 4 {% load humanize %}
5 5
6 6 {% block head %}
7 <title>Neboard settings</title>
7 <title>{% trans 'Settings' %} - {{ site_name }}</title>
8 8 {% endblock %}
9 9
10 10 {% block content %}
11 11
12 12 <div class="post">
13 13 <p>
14 14 {% trans 'User:' %} <b>{{ user.user_id }}</b>.
15 15 {% if user.is_moderator %}
16 16 {% trans 'You are moderator.' %}
17 17 {% endif %}
18 18 </p>
19 19 <p>{% trans 'Posts:' %} {{ user.get_post_count }}</p>
20 20 <p>{% trans 'First access:' %} {{ user.registration_time|naturaltime }}</p>
21 21 {% if user.get_last_access_time %}
22 22 <p>{% trans 'Last access:' %} {{ user.get_last_access_time|naturaltime }}</p>
23 23 {% endif %}
24 24 </div>
25 25
26 26 <div class="post-form-w">
27 27 <div class="post-form">
28 28 <form method="post">{% csrf_token %}
29 29 {{ form.as_div }}
30 30 <div class="form-submit">
31 31 <input type="submit" value="{% trans "Save" %}" />
32 32 </div>
33 33 </form>
34 34 </div>
35 35 </div>
36 36
37 37 {% endblock %}
@@ -1,79 +1,80 b''
1 1 {% extends "boards/base.html" %}
2 2
3 3 {% load i18n %}
4 4 {% load cache %}
5 5 {% load static from staticfiles %}
6 6 {% load board %}
7 7
8 8 {% block head %}
9 <title>{{ thread.get_opening_post.get_title|striptags|truncatewords:10 }} - Neboard</title>
9 <title>{{ thread.get_opening_post.get_title|striptags|truncatewords:10 }}
10 - {{ site_name }}</title>
10 11 {% endblock %}
11 12
12 13 {% block content %}
13 14 {% spaceless %}
14 15 {% get_current_language as LANGUAGE_CODE %}
15 16
16 17 {% cache 600 thread_view thread.id thread.last_edit_time moderator LANGUAGE_CODE %}
17 18
18 19 <div class="image-mode-tab">
19 20 <a class="current_mode" href="{% url 'thread' thread.get_opening_post.id %}">{% trans 'Normal mode' %}</a>,
20 21 <a href="{% url 'thread_mode' thread.get_opening_post.id 'gallery' %}">{% trans 'Gallery mode' %}</a>
21 22 </div>
22 23
23 24 {% if bumpable %}
24 25 <div class="bar-bg">
25 26 <div class="bar-value" style="width:{{ bumplimit_progress }}%" id="bumplimit_progress">
26 27 </div>
27 28 <div class="bar-text">
28 29 <span id="left_to_limit">{{ posts_left }}</span> {% trans 'posts to bumplimit' %}
29 30 </div>
30 31 </div>
31 32 {% endif %}
32 33 <div class="thread">
33 34 {% for post in posts %}
34 35 {% post_view post moderator=moderator %}
35 36 {% endfor %}
36 37 </div>
37 38 {% endcache %}
38 39
39 40 {% if not thread.archived %}
40 41
41 42 <div class="post-form-w">
42 43 <script src="{% static 'js/panel.js' %}"></script>
43 44 <div class="form-title">{% trans "Reply to thread" %} #{{ thread.get_opening_post.id }}</div>
44 45 <div class="post-form">
45 46 <form id="form" enctype="multipart/form-data" method="post"
46 47 >{% csrf_token %}
47 48 {{ form.as_div }}
48 49 <div class="form-submit">
49 50 <input type="submit" value="{% trans "Post" %}"/>
50 51 </div>
51 52 </form>
52 53 <div><a href="{% url "staticpage" name="help" %}">
53 54 {% trans 'Text syntax' %}</a></div>
54 55 </div>
55 56 </div>
56 57
57 58 <script src="{% static 'js/jquery.form.min.js' %}"></script>
58 59 <script src="{% static 'js/thread_update.js' %}"></script>
59 60 {% endif %}
60 61
61 62 <script src="{% static 'js/thread.js' %}"></script>
62 63
63 64 {% endspaceless %}
64 65 {% endblock %}
65 66
66 67 {% block metapanel %}
67 68
68 69 {% get_current_language as LANGUAGE_CODE %}
69 70
70 71 <span class="metapanel" data-last-update="{{ last_update }}">
71 72 {% cache 600 thread_meta thread.last_edit_time moderator LANGUAGE_CODE %}
72 73 <span id="reply-count">{{ thread.get_reply_count }}</span> {% trans 'replies' %},
73 74 <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}.
74 75 {% trans 'Last update: ' %}<span id="last-update">{{ thread.last_edit_time }}</span>
75 76 [<a href="rss/">RSS</a>]
76 77 {% endcache %}
77 78 </span>
78 79
79 80 {% endblock %}
@@ -1,64 +1,65 b''
1 1 {% extends "boards/base.html" %}
2 2
3 3 {% load i18n %}
4 4 {% load cache %}
5 5 {% load static from staticfiles %}
6 6 {% load board %}
7 7
8 8 {% block head %}
9 <title>{{ thread.get_opening_post.get_title|striptags|truncatewords:10 }} - Neboard</title>
9 <title>{{ thread.get_opening_post.get_title|striptags|truncatewords:10 }}
10 - {{ site_name }}</title>
10 11 {% endblock %}
11 12
12 13 {% block content %}
13 14 {% spaceless %}
14 15 {% get_current_language as LANGUAGE_CODE %}
15 16
16 17 <script src="{% static 'js/thread.js' %}"></script>
17 18
18 19 {% cache 600 thread_gallery_view thread.id thread.last_edit_time LANGUAGE_CODE request.get_host %}
19 20 <div class="image-mode-tab">
20 21 <a href="{% url 'thread' thread.get_opening_post.id %}">{% trans 'Normal mode' %}</a>,
21 22 <a class="current_mode" href="{% url 'thread_mode' thread.get_opening_post.id 'gallery' %}">{% trans 'Gallery mode' %}</a>
22 23 </div>
23 24
24 25 <div id="posts-table">
25 26 {% for post in posts %}
26 27 <div class="gallery_image">
27 28 <div>
28 29 <a
29 30 class="thumb"
30 31 href="{{ post.image.url }}"><img
31 32 src="{{ post.image.url_200x150 }}"
32 33 alt="{{ post.id }}"
33 34 width="{{ post.image_pre_width }}"
34 35 height="{{ post.image_pre_height }}"
35 36 data-width="{{ post.image_width }}"
36 37 data-height="{{ post.image_height }}"/>
37 38 </a>
38 39 </div>
39 40 <div class="gallery_image_metadata">
40 41 {{ post.image_width }}x{{ post.image_height }}
41 42 {% image_actions post.image.url request.get_host %}
42 43 </div>
43 44 </div>
44 45 {% endfor %}
45 46 </div>
46 47 {% endcache %}
47 48
48 49 {% endspaceless %}
49 50 {% endblock %}
50 51
51 52 {% block metapanel %}
52 53
53 54 {% get_current_language as LANGUAGE_CODE %}
54 55
55 56 <span class="metapanel" data-last-update="{{ last_update }}">
56 57 {% cache 600 thread_meta thread.last_edit_time moderator LANGUAGE_CODE %}
57 58 <span id="reply-count">{{ thread.get_reply_count }}</span> {% trans 'replies' %},
58 59 <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}.
59 60 {% trans 'Last update: ' %}{{ thread.last_edit_time }}
60 61 [<a href="rss/">RSS</a>]
61 62 {% endcache %}
62 63 </span>
63 64
64 65 {% endblock %}
@@ -1,76 +1,76 b''
1 1 from django.core.urlresolvers import reverse
2 2 from django.shortcuts import get_object_or_404
3 3 from boards.models import Post
4 4 from boards.views import thread, api
5 5 from django import template
6 6
7 7 register = template.Library()
8 8
9 9 actions = [
10 10 {
11 11 'name': 'google',
12 12 'link': 'http://google.com/searchbyimage?image_url=%s',
13 13 },
14 14 {
15 15 'name': 'iqdb',
16 16 'link': 'http://iqdb.org/?url=%s',
17 17 },
18 18 ]
19 19
20 20
21 21 @register.simple_tag(name='post_url')
22 22 def post_url(*args, **kwargs):
23 23 post_id = args[0]
24 24
25 25 post = get_object_or_404(Post, id=post_id)
26 26
27 27 if not post.is_opening():
28 link = reverse(thread, kwargs={
28 link = reverse('thread', kwargs={
29 29 'post_id': post.thread_new.get_opening_post().id}) + '#' + str(
30 30 post_id)
31 31 else:
32 link = reverse(thread, kwargs={'post_id': post_id})
32 link = reverse('thread', kwargs={'post_id': post_id})
33 33
34 34 return link
35 35
36 36
37 37 @register.simple_tag(name='image_actions')
38 38 def image_actions(*args, **kwargs):
39 39 image_link = args[0]
40 40 if len(args) > 1:
41 41 image_link = 'http://' + args[1] + image_link # TODO https?
42 42
43 43 result = ''
44 44
45 45 for action in actions:
46 46 result += '[<a href="' + action['link'] % image_link + '">' + \
47 47 action['name'] + '</a>]'
48 48
49 49 return result
50 50
51 51
52 52 @register.inclusion_tag('boards/post.html', name='post_view')
53 53 def post_view(post, moderator=False):
54 54 """
55 55 Get post
56 56 """
57 57
58 58 return {
59 59 'post': post,
60 60 'moderator': moderator,
61 61 }
62 62
63 63
64 64 @register.inclusion_tag('boards/post.html', name='post_view_truncated')
65 65 def post_view_truncated(post, need_open_link=False, moderator=False):
66 66 """
67 67 Get post with truncated text. If the 'open' or 'reply' link is needed, pass
68 68 the second parameter as True.
69 69 """
70 70
71 71 return {
72 72 'post': post,
73 73 'truncated': True,
74 74 'need_open_link': need_open_link,
75 75 'moderator': moderator,
76 76 } No newline at end of file
@@ -1,70 +1,76 b''
1 1 from django.conf.urls import patterns, url, include
2 2 from boards import views
3 3 from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed
4 from boards.views import api
4 from boards.views import api, tag_threads, all_threads, archived_threads
5 5
6 6 js_info_dict = {
7 7 'packages': ('boards',),
8 8 }
9 9
10 10 urlpatterns = patterns('',
11 11
12 12 # /boards/
13 url(r'^$', views.index, name='index'),
13 url(r'^$', all_threads.AllThreadsView.as_view(), name='index'),
14 14 # /boards/page/
15 url(r'^page/(?P<page>\w+)/$', views.index, name='index'),
15 url(r'^page/(?P<page>\w+)/$', all_threads.AllThreadsView.as_view(),
16 name='index'),
16 17
17 url(r'^archive/$', views.archive, name='archive'),
18 url(r'^archive/page/(?P<page>\w+)/$', views.archive, name='archive'),
18 url(r'^archive/$', archived_threads.ArchiveView.as_view(), name='archive'),
19 url(r'^archive/page/(?P<page>\w+)/$',
20 archived_threads.ArchiveView.as_view(), name='archive'),
19 21
20 22 # login page
21 23 url(r'^login/$', views.login, name='login'),
22 24
23 25 # /boards/tag/tag_name/
24 url(r'^tag/(?P<tag_name>\w+)/$', views.tag, name='tag'),
26 url(r'^tag/(?P<tag_name>\w+)/$', tag_threads.TagView.as_view(),
27 name='tag'),
25 28 # /boards/tag/tag_id/page/
26 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/$', views.tag, name='tag'),
29 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/$',
30 tag_threads.TagView.as_view(), name='tag'),
27 31
28 32 # /boards/tag/tag_name/unsubscribe/
29 33 url(r'^tag/(?P<tag_name>\w+)/subscribe/$', views.tag_subscribe,
30 34 name='tag_subscribe'),
31 35 # /boards/tag/tag_name/unsubscribe/
32 36 url(r'^tag/(?P<tag_name>\w+)/unsubscribe/$', views.tag_unsubscribe,
33 37 name='tag_unsubscribe'),
34 38
35 39 # /boards/thread/
36 url(r'^thread/(?P<post_id>\w+)/$', views.thread, name='thread'),
37 url(r'^thread/(?P<post_id>\w+)/(?P<mode>\w+)/$', views.thread, name='thread_mode'),
40 url(r'^thread/(?P<post_id>\w+)/$', views.thread.ThreadView.as_view(),
41 name='thread'),
42 url(r'^thread/(?P<post_id>\w+)/(?P<mode>\w+)/$', views.thread.ThreadView
43 .as_view(), name='thread_mode'),
38 44 url(r'^settings/$', views.settings, name='settings'),
39 45 url(r'^tags/$', views.all_tags, name='tags'),
40 46 url(r'^captcha/', include('captcha.urls')),
41 47 url(r'^jump/(?P<post_id>\w+)/$', views.jump_to_post, name='jumper'),
42 48 url(r'^authors/$', views.authors, name='authors'),
43 49 url(r'^delete/(?P<post_id>\w+)/$', views.delete, name='delete'),
44 50 url(r'^ban/(?P<post_id>\w+)/$', views.ban, name='ban'),
45 51
46 url(r'^banned/$', views.you_are_banned, name='banned'),
52 url(r'^banned/$', views.banned.BannedView.as_view, name='banned'),
47 53 url(r'^staticpage/(?P<name>\w+)/$', views.static_page, name='staticpage'),
48 54
49 55 # RSS feeds
50 56 url(r'^rss/$', AllThreadsFeed()),
51 57 url(r'^page/(?P<page>\w+)/rss/$', AllThreadsFeed()),
52 58 url(r'^tag/(?P<tag_name>\w+)/rss/$', TagThreadsFeed()),
53 59 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/rss/$', TagThreadsFeed()),
54 60 url(r'^thread/(?P<post_id>\w+)/rss/$', ThreadPostsFeed()),
55 61
56 62 # i18n
57 63 url(r'^jsi18n/$', 'boards.views.cached_js_catalog', js_info_dict, name='js_info_dict'),
58 64
59 65 # API
60 66 url(r'^api/post/(?P<post_id>\w+)/$', api.get_post, name="get_post"),
61 67 url(r'^api/diff_thread/(?P<thread_id>\w+)/(?P<last_update_time>\w+)/$',
62 68 api.api_get_threaddiff, name="get_thread_diff"),
63 69 url(r'^api/threads/(?P<count>\w+)/$', api.api_get_threads,
64 70 name='get_threads'),
65 url(r'api/tags/$', api.api_get_tags, name='get_tags'),
66 url(r'api/thread/(?P<opening_post_id>\w+)/$', api.api_get_thread_posts,
71 url(r'^api/tags/$', api.api_get_tags, name='get_tags'),
72 url(r'^api/thread/(?P<opening_post_id>\w+)/$', api.api_get_thread_posts,
67 73 name='get_thread'),
68 url(r'api/add_post/(?P<opening_post_id>\w+)/$', api.api_add_post, name='add_post'),
74 url(r'^api/add_post/(?P<opening_post_id>\w+)/$', api.api_add_post, name='add_post'),
69 75
70 76 )
@@ -1,73 +1,80 b''
1 1 """
2 2 This module contains helper functions and helper classes.
3 3 """
4 from django.utils import timezone
4 5
5 6 from neboard import settings
6 7 import time
7 8
8 9
9 10 KEY_CAPTCHA_FAILS = 'key_captcha_fails'
10 11 KEY_CAPTCHA_DELAY_TIME = 'key_captcha_delay_time'
11 12 KEY_CAPTCHA_LAST_ACTIVITY = 'key_captcha_last_activity'
12 13
13 14
14 15 def need_include_captcha(request):
15 16 """
16 17 Check if request is made by a user.
17 18 It contains rules which check for bots.
18 19 """
19 20
20 21 if not settings.ENABLE_CAPTCHA:
21 22 return False
22 23
23 24 enable_captcha = False
24 25
25 26 #newcomer
26 27 if KEY_CAPTCHA_LAST_ACTIVITY not in request.session:
27 28 return settings.ENABLE_CAPTCHA
28 29
29 30 last_activity = request.session[KEY_CAPTCHA_LAST_ACTIVITY]
30 31 current_delay = int(time.time()) - last_activity
31 32
32 33 delay_time = (request.session[KEY_CAPTCHA_DELAY_TIME]
33 34 if KEY_CAPTCHA_DELAY_TIME in request.session
34 35 else settings.CAPTCHA_DEFAULT_SAFE_TIME)
35 36
36 37 if current_delay < delay_time:
37 38 enable_captcha = True
38 39
39 40 print 'ENABLING' + str(enable_captcha)
40 41
41 42 return enable_captcha
42 43
43 44
44 45 def update_captcha_access(request, passed):
45 46 """
46 47 Update captcha fields.
47 48 It will reduce delay time if user passed captcha verification and
48 49 it will increase it otherwise.
49 50 """
50 51 session = request.session
51 52
52 53 delay_time = (request.session[KEY_CAPTCHA_DELAY_TIME]
53 54 if KEY_CAPTCHA_DELAY_TIME in request.session
54 55 else settings.CAPTCHA_DEFAULT_SAFE_TIME)
55 56
56 57 print "DELAY TIME = " + str(delay_time)
57 58
58 59 if passed:
59 60 delay_time -= 2 if delay_time >= 7 else 5
60 61 else:
61 62 delay_time += 10
62 63
63 64 session[KEY_CAPTCHA_LAST_ACTIVITY] = int(time.time())
64 65 session[KEY_CAPTCHA_DELAY_TIME] = delay_time
65 66
66 67
67 68 def get_client_ip(request):
68 69 x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
69 70 if x_forwarded_for:
70 71 ip = x_forwarded_for.split(',')[-1].strip()
71 72 else:
72 73 ip = request.META.get('REMOTE_ADDR')
73 return ip No newline at end of file
74 return ip
75
76
77 def datetime_to_epoch(datetime):
78 return int(time.mktime(timezone.localtime(
79 datetime,timezone.get_current_timezone()).timetuple())
80 * 1000000 + datetime.microsecond) No newline at end of file
@@ -1,609 +1,356 b''
1 1 from datetime import datetime, timedelta
2 2
3 3 from django.db.models import Count
4 4
5
6 OLD_USER_AGE_DAYS = 90
7
8 5 __author__ = 'neko259'
9 6
10 7 import hashlib
11 8 import string
12 import time
13 9 import re
14 10
15 11 from django.core import serializers
16 12 from django.core.urlresolvers import reverse
17 from django.http import HttpResponseRedirect, Http404
13 from django.http import HttpResponseRedirect
18 14 from django.http.response import HttpResponse
19 15 from django.template import RequestContext
20 16 from django.shortcuts import render, redirect, get_object_or_404
21 17 from django.utils import timezone
22 18 from django.db import transaction
23 19 from django.views.decorators.cache import cache_page
24 20 from django.views.i18n import javascript_catalog
25 from django.core.paginator import Paginator
26 21
27 from boards import forms
28 22 import boards
29 23 from boards import utils
30 from boards.forms import ThreadForm, PostForm, SettingsForm, PlainErrorList, \
31 ThreadCaptchaForm, PostCaptchaForm, LoginForm, ModeratorSettingsForm
32 from boards.models import Post, Tag, Ban, User, Thread
24 from boards.forms import SettingsForm, PlainErrorList, \
25 LoginForm, ModeratorSettingsForm
26 from boards.models import Post, Tag, Ban, User
33 27 from boards.models.post import SETTING_MODERATE, REGEX_REPLY
34 28 from boards.models.user import RANK_USER
35 29 from boards import authors
36 from boards.utils import get_client_ip
37 30 import neboard
38 31
32 import all_threads
33
39 34
40 35 BAN_REASON_SPAM = 'Autoban: spam bot'
41 MODE_GALLERY = 'gallery'
42 MODE_NORMAL = 'normal'
43 36
44 37 DEFAULT_PAGE = 1
45 38
46 39
47 def index(request, page=DEFAULT_PAGE):
48 context = _init_default_context(request)
49
50 if utils.need_include_captcha(request):
51 threadFormClass = ThreadCaptchaForm
52 kwargs = {'request': request}
53 else:
54 threadFormClass = ThreadForm
55 kwargs = {}
56
57 if request.method == 'POST':
58 form = threadFormClass(request.POST, request.FILES,
59 error_class=PlainErrorList, **kwargs)
60 form.session = request.session
61
62 if form.is_valid():
63 return _new_post(request, form)
64 if form.need_to_ban:
65 # Ban user because he is suspected to be a bot
66 _ban_current_user(request)
67 else:
68 form = threadFormClass(error_class=PlainErrorList, **kwargs)
69
70 threads = []
71 for thread_to_show in Post.objects.get_threads(page=int(page)):
72 threads.append(_get_template_thread(thread_to_show))
73
74 # TODO Make this generic for tag and threads list pages
75 context['threads'] = None if len(threads) == 0 else threads
76 context['form'] = form
77
78 paginator = Paginator(Thread.objects.filter(archived=False),
79 neboard.settings.THREADS_PER_PAGE)
80 _get_page_context(paginator, context, page)
81
82 return render(request, 'boards/posting_general.html',
83 context)
84
85
86 def archive(request, page=DEFAULT_PAGE):
87 """
88 Get archived posts
89 """
90
91 context = _init_default_context(request)
92
93 threads = []
94 for thread_to_show in Post.objects.get_threads(page=int(page),
95 archived=True):
96 threads.append(_get_template_thread(thread_to_show))
97
98 context['threads'] = threads
99
100 paginator = Paginator(Thread.objects.filter(archived=True),
101 neboard.settings.THREADS_PER_PAGE)
102 _get_page_context(paginator, context, page)
103
104 return render(request, 'boards/archive.html', context)
105
106
107 @transaction.atomic
108 def _new_post(request, form, opening_post=None, html_response=True):
109 """Add a new post (in thread or as a reply)."""
110
111 ip = get_client_ip(request)
112 is_banned = Ban.objects.filter(ip=ip).exists()
113
114 if is_banned:
115 if html_response:
116 return redirect(you_are_banned)
117 else:
118 return
119
120 data = form.cleaned_data
121
122 title = data['title']
123 text = data['text']
124
125 text = _remove_invalid_links(text)
126
127 if 'image' in data.keys():
128 image = data['image']
129 else:
130 image = None
131
132 tags = []
133
134 if not opening_post:
135 tag_strings = data['tags']
136
137 if tag_strings:
138 tag_strings = tag_strings.split(' ')
139 for tag_name in tag_strings:
140 tag_name = string.lower(tag_name.strip())
141 if len(tag_name) > 0:
142 tag, created = Tag.objects.get_or_create(name=tag_name)
143 tags.append(tag)
144 post_thread = None
145 else:
146 post_thread = opening_post.thread_new
147
148 post = Post.objects.create_post(title=title, text=text, ip=ip,
149 thread=post_thread, image=image,
150 tags=tags, user=_get_user(request))
151
152 thread_to_show = (opening_post.id if opening_post else post.id)
153
154 if html_response:
155 if opening_post:
156 return redirect(reverse(thread, kwargs={'post_id': thread_to_show}) +
157 '#' + str(post.id))
158 else:
159 return redirect(thread, post_id=thread_to_show)
160
161
162 def tag(request, tag_name, page=DEFAULT_PAGE):
163 """
164 Get all tag threads. Threads are split in pages, so some page is
165 requested.
166 """
167
168 tag = get_object_or_404(Tag, name=tag_name)
169 threads = []
170 for thread_to_show in Post.objects.get_threads(page=int(page), tag=tag):
171 threads.append(_get_template_thread(thread_to_show))
172
173 if request.method == 'POST':
174 form = ThreadForm(request.POST, request.FILES,
175 error_class=PlainErrorList)
176 form.session = request.session
177
178 if form.is_valid():
179 return _new_post(request, form)
180 if form.need_to_ban:
181 # Ban user because he is suspected to be a bot
182 _ban_current_user(request)
183 else:
184 form = forms.ThreadForm(initial={'tags': tag_name},
185 error_class=PlainErrorList)
186
187 context = _init_default_context(request)
188 context['threads'] = None if len(threads) == 0 else threads
189 context['tag'] = tag
190
191 paginator = Paginator(Post.objects.get_threads(tag=tag),
192 neboard.settings.THREADS_PER_PAGE)
193 _get_page_context(paginator, context, page)
194
195 context['form'] = form
196
197 return render(request, 'boards/posting_general.html',
198 context)
199
200
201 def thread(request, post_id, mode=MODE_NORMAL):
202 """Get all thread posts"""
203
204 if utils.need_include_captcha(request):
205 postFormClass = PostCaptchaForm
206 kwargs = {'request': request}
207 else:
208 postFormClass = PostForm
209 kwargs = {}
210
211 opening_post = get_object_or_404(Post, id=post_id)
212
213 # If this is not OP, don't show it as it is
214 if not opening_post.is_opening():
215 raise Http404
216
217 if request.method == 'POST' and not opening_post.thread_new.archived:
218 form = postFormClass(request.POST, request.FILES,
219 error_class=PlainErrorList, **kwargs)
220 form.session = request.session
221
222 if form.is_valid():
223 return _new_post(request, form, opening_post)
224 if form.need_to_ban:
225 # Ban user because he is suspected to be a bot
226 _ban_current_user(request)
227 else:
228 form = postFormClass(error_class=PlainErrorList, **kwargs)
229
230 thread_to_show = opening_post.thread_new
231
232 context = _init_default_context(request)
233
234 posts = thread_to_show.get_replies()
235 context['form'] = form
236 context["last_update"] = _datetime_to_epoch(thread_to_show.last_edit_time)
237 context["thread"] = thread_to_show
238
239 if MODE_NORMAL == mode:
240 context['bumpable'] = thread_to_show.can_bump()
241 if context['bumpable']:
242 context['posts_left'] = neboard.settings.MAX_POSTS_PER_THREAD - posts \
243 .count()
244 context['bumplimit_progress'] = str(
245 float(context['posts_left']) /
246 neboard.settings.MAX_POSTS_PER_THREAD * 100)
247
248 context['posts'] = posts
249
250 document = 'boards/thread.html'
251 elif MODE_GALLERY == mode:
252 context['posts'] = posts.filter(image_width__gt=0)
253
254 document = 'boards/thread_gallery.html'
255 else:
256 raise Http404
257
258 return render(request, document, context)
259
260
261 40 def login(request):
262 41 """Log in with user id"""
263 42
264 43 context = _init_default_context(request)
265 44
266 45 if request.method == 'POST':
267 46 form = LoginForm(request.POST, request.FILES,
268 47 error_class=PlainErrorList)
269 48 form.session = request.session
270 49
271 50 if form.is_valid():
272 51 user = User.objects.get(user_id=form.cleaned_data['user_id'])
273 52 request.session['user_id'] = user.id
274 return redirect(index)
53 return redirect('index')
275 54
276 55 else:
277 56 form = LoginForm()
278 57
279 58 context['form'] = form
280 59
281 60 return render(request, 'boards/login.html', context)
282 61
283 62
284 63 def settings(request):
285 64 """User's settings"""
286 65
287 66 context = _init_default_context(request)
288 67 user = _get_user(request)
289 68 is_moderator = user.is_moderator()
290 69
291 70 if request.method == 'POST':
292 71 with transaction.atomic():
293 72 if is_moderator:
294 73 form = ModeratorSettingsForm(request.POST,
295 74 error_class=PlainErrorList)
296 75 else:
297 76 form = SettingsForm(request.POST, error_class=PlainErrorList)
298 77
299 78 if form.is_valid():
300 79 selected_theme = form.cleaned_data['theme']
301 80
302 81 user.save_setting('theme', selected_theme)
303 82
304 83 if is_moderator:
305 84 moderate = form.cleaned_data['moderate']
306 85 user.save_setting(SETTING_MODERATE, moderate)
307 86
308 87 return redirect(settings)
309 88 else:
310 89 selected_theme = _get_theme(request)
311 90
312 91 if is_moderator:
313 92 form = ModeratorSettingsForm(initial={'theme': selected_theme,
314 93 'moderate': context['moderator']},
315 94 error_class=PlainErrorList)
316 95 else:
317 96 form = SettingsForm(initial={'theme': selected_theme},
318 97 error_class=PlainErrorList)
319 98
320 99 context['form'] = form
321 100
322 101 return render(request, 'boards/settings.html', context)
323 102
324 103
325 104 def all_tags(request):
326 105 """All tags list"""
327 106
328 107 context = _init_default_context(request)
329 108 context['all_tags'] = Tag.objects.get_not_empty_tags()
330 109
331 110 return render(request, 'boards/tags.html', context)
332 111
333 112
334 113 def jump_to_post(request, post_id):
335 114 """Determine thread in which the requested post is and open it's page"""
336 115
337 116 post = get_object_or_404(Post, id=post_id)
338 117
339 118 if not post.thread:
340 return redirect(thread, post_id=post.id)
119 return redirect('thread', post_id=post.id)
341 120 else:
342 return redirect(reverse(thread, kwargs={'post_id': post.thread.id})
121 return redirect(reverse('thread', kwargs={'post_id': post.thread.id})
343 122 + '#' + str(post.id))
344 123
345 124
346 125 def authors(request):
347 126 """Show authors list"""
348 127
349 128 context = _init_default_context(request)
350 129 context['authors'] = boards.authors.authors
351 130
352 131 return render(request, 'boards/authors.html', context)
353 132
354 133
355 134 @transaction.atomic
356 135 def delete(request, post_id):
357 136 """Delete post"""
358 137
359 138 user = _get_user(request)
360 139 post = get_object_or_404(Post, id=post_id)
361 140
362 141 if user.is_moderator():
363 142 # TODO Show confirmation page before deletion
364 143 Post.objects.delete_post(post)
365 144
366 145 if not post.thread:
367 146 return _redirect_to_next(request)
368 147 else:
369 return redirect(thread, post_id=post.thread.id)
148 return redirect('thread', post_id=post.thread.id)
370 149
371 150
372 151 @transaction.atomic
373 152 def ban(request, post_id):
374 153 """Ban user"""
375 154
376 155 user = _get_user(request)
377 156 post = get_object_or_404(Post, id=post_id)
378 157
379 158 if user.is_moderator():
380 159 # TODO Show confirmation page before ban
381 160 ban, created = Ban.objects.get_or_create(ip=post.poster_ip)
382 161 if created:
383 162 ban.reason = 'Banned for post ' + str(post_id)
384 163 ban.save()
385 164
386 165 return _redirect_to_next(request)
387 166
388 167
389 def you_are_banned(request):
390 """Show the page that notifies that user is banned"""
391
392 context = _init_default_context(request)
393
394 ban = get_object_or_404(Ban, ip=utils.get_client_ip(request))
395 context['ban_reason'] = ban.reason
396 return render(request, 'boards/staticpages/banned.html', context)
397
398
399 168 def page_404(request):
400 169 """Show page 404 (not found error)"""
401 170
402 171 context = _init_default_context(request)
403 172 return render(request, 'boards/404.html', context)
404 173
405 174
406 175 @transaction.atomic
407 176 def tag_subscribe(request, tag_name):
408 177 """Add tag to favorites"""
409 178
410 179 user = _get_user(request)
411 180 tag = get_object_or_404(Tag, name=tag_name)
412 181
413 182 if not tag in user.fav_tags.all():
414 183 user.add_tag(tag)
415 184
416 185 return _redirect_to_next(request)
417 186
418 187
419 188 @transaction.atomic
420 189 def tag_unsubscribe(request, tag_name):
421 190 """Remove tag from favorites"""
422 191
423 192 user = _get_user(request)
424 193 tag = get_object_or_404(Tag, name=tag_name)
425 194
426 195 if tag in user.fav_tags.all():
427 196 user.remove_tag(tag)
428 197
429 198 return _redirect_to_next(request)
430 199
431 200
432 201 def static_page(request, name):
433 202 """Show a static page that needs only tags list and a CSS"""
434 203
435 204 context = _init_default_context(request)
436 205 return render(request, 'boards/staticpages/' + name + '.html', context)
437 206
438 207
439 208 def api_get_post(request, post_id):
440 209 """
441 210 Get the JSON of a post. This can be
442 211 used as and API for external clients.
443 212 """
444 213
445 214 post = get_object_or_404(Post, id=post_id)
446 215
447 216 json = serializers.serialize("json", [post], fields=(
448 217 "pub_time", "_text_rendered", "title", "text", "image",
449 218 "image_width", "image_height", "replies", "tags"
450 219 ))
451 220
452 221 return HttpResponse(content=json)
453 222
454 223
455 224 @cache_page(86400)
456 225 def cached_js_catalog(request, domain='djangojs', packages=None):
457 226 return javascript_catalog(request, domain, packages)
458 227
459 228
229 # TODO This method is deprecated and should be removed after switching to
230 # class-based view
460 231 def _get_theme(request, user=None):
461 232 """Get user's CSS theme"""
462 233
463 234 if not user:
464 235 user = _get_user(request)
465 236 theme = user.get_setting('theme')
466 237 if not theme:
467 238 theme = neboard.settings.DEFAULT_THEME
468 239
469 240 return theme
470 241
471 242
243 # TODO This method is deprecated and should be removed after switching to
244 # class-based view
472 245 def _init_default_context(request):
473 246 """Create context with default values that are used in most views"""
474 247
475 248 context = RequestContext(request)
476 249
477 250 user = _get_user(request)
478 251 context['user'] = user
479 252 context['tags'] = user.get_sorted_fav_tags()
480 253 context['posts_per_day'] = float(Post.objects.get_posts_per_day())
481 254
482 255 theme = _get_theme(request, user)
483 256 context['theme'] = theme
484 257 context['theme_css'] = 'css/' + theme + '/base_page.css'
485 258
486 259 # This shows the moderator panel
487 260 moderate = user.get_setting(SETTING_MODERATE)
488 261 if moderate == 'True':
489 262 context['moderator'] = user.is_moderator()
490 263 else:
491 264 context['moderator'] = False
492 265
493 266 return context
494 267
495 268
269 # TODO This method is deprecated and should be removed after switching to
270 # class-based view
496 271 def _get_user(request):
497 272 """
498 273 Get current user from the session. If the user does not exist, create
499 274 a new one.
500 275 """
501 276
502 277 session = request.session
503 278 if not 'user_id' in session:
504 279 request.session.save()
505 280
506 281 md5 = hashlib.md5()
507 282 md5.update(session.session_key)
508 283 new_id = md5.hexdigest()
509 284
510 285 while User.objects.filter(user_id=new_id).exists():
511 286 md5.update(str(timezone.now()))
512 287 new_id = md5.hexdigest()
513 288
514 289 time_now = timezone.now()
515 290 user = User.objects.create(user_id=new_id, rank=RANK_USER,
516 291 registration_time=time_now)
517 292
518 _delete_old_users()
293 # TODO This is just a test. This method should be removed
294 # _delete_old_users()
519 295
520 296 session['user_id'] = user.id
521 297 else:
522 298 user = User.objects.get(id=session['user_id'])
523 299
524 300 return user
525 301
526 302
527 303 def _redirect_to_next(request):
528 304 """
529 305 If a 'next' parameter was specified, redirect to the next page. This is
530 306 used when the user is required to return to some page after the current
531 307 view has finished its work.
532 308 """
533 309
534 310 if 'next' in request.GET:
535 311 next_page = request.GET['next']
536 312 return HttpResponseRedirect(next_page)
537 313 else:
538 return redirect(index)
314 return redirect('index')
539 315
540 316
541 317 @transaction.atomic
542 318 def _ban_current_user(request):
543 319 """Add current user to the IP ban list"""
544 320
545 321 ip = utils.get_client_ip(request)
546 322 ban, created = Ban.objects.get_or_create(ip=ip)
547 323 if created:
548 324 ban.can_read = False
549 325 ban.reason = BAN_REASON_SPAM
550 326 ban.save()
551 327
552 328
553 329 def _remove_invalid_links(text):
554 330 """
555 331 Replace invalid links in posts so that they won't be parsed.
556 332 Invalid links are links to non-existent posts
557 333 """
558 334
559 335 for reply_number in re.finditer(REGEX_REPLY, text):
560 336 post_id = reply_number.group(1)
561 337 post = Post.objects.filter(id=post_id)
562 338 if not post.exists():
563 339 text = string.replace(text, '>>' + post_id, post_id)
564 340
565 341 return text
566 342
567 343
568 def _datetime_to_epoch(datetime):
569 return int(time.mktime(timezone.localtime(
570 datetime,timezone.get_current_timezone()).timetuple())
571 * 1000000 + datetime.microsecond)
572
573
574 344 def _get_template_thread(thread_to_show):
575 345 """Get template values for thread"""
576 346
577 347 last_replies = thread_to_show.get_last_replies()
578 348 skipped_replies_count = thread_to_show.get_replies().count() \
579 349 - len(last_replies) - 1
580 350 return {
581 351 'thread': thread_to_show,
582 352 'op': thread_to_show.get_replies()[0],
583 353 'bumpable': thread_to_show.can_bump(),
584 354 'last_replies': last_replies,
585 355 'skipped_replies': skipped_replies_count,
586 356 }
587
588
589 def _delete_old_users():
590 """
591 Delete users with no favorite tags and posted messages. These can be spam
592 bots or just old user accounts
593 """
594
595 old_registration_date = datetime.now().date() - timedelta(OLD_USER_AGE_DAYS)
596
597 for user in User.objects.annotate(tags_count=Count('fav_tags')).filter(
598 tags_count=0).filter(registration_time__lt=old_registration_date):
599 if not Post.objects.filter(user=user).exists():
600 user.delete()
601
602
603 def _get_page_context(paginator, context, page):
604 """
605 Get pagination context variables
606 """
607
608 context['paginator'] = paginator
609 context['current_page'] = paginator.page(int(page))
@@ -1,205 +1,206 b''
1 1 from datetime import datetime
2 2 import json
3 3 from django.db import transaction
4 4 from django.http import HttpResponse
5 5 from django.shortcuts import get_object_or_404, render
6 6 from django.template import RequestContext
7 7 from django.utils import timezone
8 8 from boards.forms import PostForm, PlainErrorList
9 9 from boards.models import Post, Thread, Tag
10 from boards.views import _datetime_to_epoch, _new_post, \
11 _ban_current_user
10 from boards.utils import datetime_to_epoch
11 from boards.views.thread import ThreadView
12 12
13 13 __author__ = 'neko259'
14 14
15 15 PARAMETER_TRUNCATED = 'truncated'
16 16 PARAMETER_TAG = 'tag'
17 17 PARAMETER_OFFSET = 'offset'
18 18 PARAMETER_DIFF_TYPE = 'type'
19 19
20 20 DIFF_TYPE_HTML = 'html'
21 21 DIFF_TYPE_JSON = 'json'
22 22
23 23 STATUS_OK = 'ok'
24 24 STATUS_ERROR = 'error'
25 25
26 26
27 27 @transaction.atomic
28 28 def api_get_threaddiff(request, thread_id, last_update_time):
29 29 """Get posts that were changed or added since time"""
30 30
31 31 thread = get_object_or_404(Post, id=thread_id).thread_new
32 32
33 33 filter_time = datetime.fromtimestamp(float(last_update_time) / 1000000,
34 34 timezone.get_current_timezone())
35 35
36 36 json_data = {
37 37 'added': [],
38 38 'updated': [],
39 39 'last_update': None,
40 40 }
41 41 added_posts = Post.objects.filter(thread_new=thread,
42 42 pub_time__gt=filter_time) \
43 43 .order_by('pub_time')
44 44 updated_posts = Post.objects.filter(thread_new=thread,
45 45 pub_time__lte=filter_time,
46 46 last_edit_time__gt=filter_time)
47 47
48 48 diff_type = DIFF_TYPE_HTML
49 49 if PARAMETER_DIFF_TYPE in request.GET:
50 50 diff_type = request.GET[PARAMETER_DIFF_TYPE]
51 51
52 52 for post in added_posts:
53 53 json_data['added'].append(_get_post_data(post.id, diff_type, request))
54 54 for post in updated_posts:
55 55 json_data['updated'].append(_get_post_data(post.id, diff_type, request))
56 json_data['last_update'] = _datetime_to_epoch(thread.last_edit_time)
56 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
57 57
58 58 return HttpResponse(content=json.dumps(json_data))
59 59
60 60
61 61 def api_add_post(request, opening_post_id):
62 62 """
63 63 Add a post and return the JSON response for it
64 64 """
65 65
66 66 opening_post = get_object_or_404(Post, id=opening_post_id)
67 67
68 68 status = STATUS_OK
69 69 errors = []
70 70
71 71 if request.method == 'POST':
72 72 form = PostForm(request.POST, request.FILES,
73 73 error_class=PlainErrorList)
74 74 form.session = request.session
75 75
76 76 #if form.need_to_ban:
77 77 # # Ban user because he is suspected to be a bot
78 78 # _ban_current_user(request)
79 79 # status = STATUS_ERROR
80 80 if form.is_valid():
81 _new_post(request, form, opening_post, html_response=False)
81 ThreadView().new_post(request, form, opening_post,
82 html_response=False)
82 83 else:
83 84 status = STATUS_ERROR
84 85 errors = form.as_json_errors()
85 86
86 87 response = {
87 88 'status': status,
88 89 'errors': errors,
89 90 }
90 91
91 92 return HttpResponse(content=json.dumps(response))
92 93
93 94
94 95 def get_post(request, post_id):
95 96 """
96 97 Get the html of a post. Used for popups. Post can be truncated if used
97 98 in threads list with 'truncated' get parameter.
98 99 """
99 100
100 101 post = get_object_or_404(Post, id=post_id)
101 102
102 103 context = RequestContext(request)
103 104 context['post'] = post
104 105 if PARAMETER_TRUNCATED in request.GET:
105 106 context[PARAMETER_TRUNCATED] = True
106 107
107 108 return render(request, 'boards/post.html', context)
108 109
109 110
110 111 # TODO Test this
111 112 def api_get_threads(request, count):
112 113 """
113 114 Get the JSON thread opening posts list.
114 115 Parameters that can be used for filtering:
115 116 tag, offset (from which thread to get results)
116 117 """
117 118
118 119 if PARAMETER_TAG in request.GET:
119 120 tag_name = request.GET[PARAMETER_TAG]
120 121 if tag_name is not None:
121 122 tag = get_object_or_404(Tag, name=tag_name)
122 123 threads = tag.threads.filter(archived=False)
123 124 else:
124 125 threads = Thread.objects.filter(archived=False)
125 126
126 127 if PARAMETER_OFFSET in request.GET:
127 128 offset = request.GET[PARAMETER_OFFSET]
128 129 offset = int(offset) if offset is not None else 0
129 130 else:
130 131 offset = 0
131 132
132 133 threads = threads.order_by('-bump_time')
133 134 threads = threads[offset:offset + int(count)]
134 135
135 136 opening_posts = []
136 137 for thread in threads:
137 138 opening_post = thread.get_opening_post()
138 139
139 140 # TODO Add tags, replies and images count
140 141 opening_posts.append(_get_post_data(opening_post.id,
141 142 include_last_update=True))
142 143
143 144 return HttpResponse(content=json.dumps(opening_posts))
144 145
145 146
146 147 # TODO Test this
147 148 def api_get_tags(request):
148 149 """
149 150 Get all tags or user tags.
150 151 """
151 152
152 153 # TODO Get favorite tags for the given user ID
153 154
154 155 tags = Tag.objects.get_not_empty_tags()
155 156 tag_names = []
156 157 for tag in tags:
157 158 tag_names.append(tag.name)
158 159
159 160 return HttpResponse(content=json.dumps(tag_names))
160 161
161 162
162 163 # TODO The result can be cached by the thread last update time
163 164 # TODO Test this
164 165 def api_get_thread_posts(request, opening_post_id):
165 166 """
166 167 Get the JSON array of thread posts
167 168 """
168 169
169 170 opening_post = get_object_or_404(Post, id=opening_post_id)
170 171 thread = opening_post.thread_new
171 172 posts = thread.get_replies()
172 173
173 174 json_data = {
174 175 'posts': [],
175 176 'last_update': None,
176 177 }
177 178 json_post_list = []
178 179
179 180 for post in posts:
180 181 json_post_list.append(_get_post_data(post.id))
181 json_data['last_update'] = _datetime_to_epoch(thread.last_edit_time)
182 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
182 183 json_data['posts'] = json_post_list
183 184
184 185 return HttpResponse(content=json.dumps(json_data))
185 186
186 187
187 188 # TODO Add pub time and replies
188 189 def _get_post_data(post_id, format_type=DIFF_TYPE_JSON, request=None,
189 190 include_last_update=False):
190 191 if format_type == DIFF_TYPE_HTML:
191 192 return get_post(request, post_id).content.strip()
192 193 elif format_type == DIFF_TYPE_JSON:
193 194 post = get_object_or_404(Post, id=post_id)
194 195 post_json = {
195 196 'id': post.id,
196 197 'title': post.title,
197 198 'text': post.text.rendered,
198 199 }
199 200 if post.image:
200 201 post_json['image'] = post.image.url
201 202 post_json['image_preview'] = post.image.url_200x150
202 203 if include_last_update:
203 post_json['bump_time'] = _datetime_to_epoch(
204 post_json['bump_time'] = datetime_to_epoch(
204 205 post.thread_new.bump_time)
205 206 return post_json
@@ -1,247 +1,249 b''
1 1 # Django settings for neboard project.
2 2 import os
3 3 from boards.mdx_neboard import markdown_extended
4 4
5 5 DEBUG = True
6 6 TEMPLATE_DEBUG = DEBUG
7 7
8 8 ADMINS = (
9 9 # ('Your Name', 'your_email@example.com'),
10 10 ('admin', 'admin@example.com')
11 11 )
12 12
13 13 MANAGERS = ADMINS
14 14
15 15 DATABASES = {
16 16 'default': {
17 17 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
18 18 'NAME': 'database.db', # Or path to database file if using sqlite3.
19 19 'USER': '', # Not used with sqlite3.
20 20 'PASSWORD': '', # Not used with sqlite3.
21 21 'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
22 22 'PORT': '', # Set to empty string for default. Not used with sqlite3.
23 23 'CONN_MAX_AGE': None,
24 24 }
25 25 }
26 26
27 27 # Local time zone for this installation. Choices can be found here:
28 28 # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
29 29 # although not all choices may be available on all operating systems.
30 30 # In a Windows environment this must be set to your system time zone.
31 31 TIME_ZONE = 'Europe/Kiev'
32 32
33 33 # Language code for this installation. All choices can be found here:
34 34 # http://www.i18nguy.com/unicode/language-identifiers.html
35 35 LANGUAGE_CODE = 'en'
36 36
37 37 SITE_ID = 1
38 38
39 39 # If you set this to False, Django will make some optimizations so as not
40 40 # to load the internationalization machinery.
41 41 USE_I18N = True
42 42
43 43 # If you set this to False, Django will not format dates, numbers and
44 44 # calendars according to the current locale.
45 45 USE_L10N = True
46 46
47 47 # If you set this to False, Django will not use timezone-aware datetimes.
48 48 USE_TZ = True
49 49
50 50 # Absolute filesystem path to the directory that will hold user-uploaded files.
51 51 # Example: "/home/media/media.lawrence.com/media/"
52 52 MEDIA_ROOT = './media/'
53 53
54 54 # URL that handles the media served from MEDIA_ROOT. Make sure to use a
55 55 # trailing slash.
56 56 # Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
57 57 MEDIA_URL = '/media/'
58 58
59 59 # Absolute path to the directory static files should be collected to.
60 60 # Don't put anything in this directory yourself; store your static files
61 61 # in apps' "static/" subdirectories and in STATICFILES_DIRS.
62 62 # Example: "/home/media/media.lawrence.com/static/"
63 63 STATIC_ROOT = ''
64 64
65 65 # URL prefix for static files.
66 66 # Example: "http://media.lawrence.com/static/"
67 67 STATIC_URL = '/static/'
68 68
69 69 # Additional locations of static files
70 70 # It is really a hack, put real paths, not related
71 71 STATICFILES_DIRS = (
72 72 os.path.dirname(__file__) + '/boards/static',
73 73
74 74 # '/d/work/python/django/neboard/neboard/boards/static',
75 75 # Put strings here, like "/home/html/static" or "C:/www/django/static".
76 76 # Always use forward slashes, even on Windows.
77 77 # Don't forget to use absolute paths, not relative paths.
78 78 )
79 79
80 80 # List of finder classes that know how to find static files in
81 81 # various locations.
82 82 STATICFILES_FINDERS = (
83 83 'django.contrib.staticfiles.finders.FileSystemFinder',
84 84 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
85 85 )
86 86
87 87 if DEBUG:
88 88 STATICFILES_STORAGE = \
89 89 'django.contrib.staticfiles.storage.StaticFilesStorage'
90 90 else:
91 91 STATICFILES_STORAGE = \
92 92 'django.contrib.staticfiles.storage.CachedStaticFilesStorage'
93 93
94 94 # Make this unique, and don't share it with anybody.
95 95 SECRET_KEY = '@1rc$o(7=tt#kd+4s$u6wchm**z^)4x90)7f6z(i&amp;55@o11*8o'
96 96
97 97 # List of callables that know how to import templates from various sources.
98 98 TEMPLATE_LOADERS = (
99 99 'django.template.loaders.filesystem.Loader',
100 100 'django.template.loaders.app_directories.Loader',
101 101 )
102 102
103 103 TEMPLATE_CONTEXT_PROCESSORS = (
104 104 'django.core.context_processors.media',
105 105 'django.core.context_processors.static',
106 106 'django.core.context_processors.request',
107 107 'django.contrib.auth.context_processors.auth',
108 108 )
109 109
110 110 MIDDLEWARE_CLASSES = (
111 111 'django.contrib.sessions.middleware.SessionMiddleware',
112 112 'django.middleware.locale.LocaleMiddleware',
113 113 'django.middleware.common.CommonMiddleware',
114 114 'django.contrib.auth.middleware.AuthenticationMiddleware',
115 115 'django.contrib.messages.middleware.MessageMiddleware',
116 116 'boards.middlewares.BanMiddleware',
117 117 'boards.middlewares.MinifyHTMLMiddleware',
118 118 )
119 119
120 120 ROOT_URLCONF = 'neboard.urls'
121 121
122 122 # Python dotted path to the WSGI application used by Django's runserver.
123 123 WSGI_APPLICATION = 'neboard.wsgi.application'
124 124
125 125 TEMPLATE_DIRS = (
126 126 # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
127 127 # Always use forward slashes, even on Windows.
128 128 # Don't forget to use absolute paths, not relative paths.
129 129 'templates',
130 130 )
131 131
132 132 INSTALLED_APPS = (
133 133 'django.contrib.auth',
134 134 'django.contrib.contenttypes',
135 135 'django.contrib.sessions',
136 136 # 'django.contrib.sites',
137 137 'django.contrib.messages',
138 138 'django.contrib.staticfiles',
139 139 # Uncomment the next line to enable the admin:
140 140 'django.contrib.admin',
141 141 # Uncomment the next line to enable admin documentation:
142 142 # 'django.contrib.admindocs',
143 143 'django.contrib.humanize',
144 144 'django_cleanup',
145 145 'boards',
146 146 'captcha',
147 147 'south',
148 148 'debug_toolbar',
149 149 )
150 150
151 151 DEBUG_TOOLBAR_PANELS = (
152 152 'debug_toolbar.panels.version.VersionDebugPanel',
153 153 'debug_toolbar.panels.timer.TimerDebugPanel',
154 154 'debug_toolbar.panels.settings_vars.SettingsVarsDebugPanel',
155 155 'debug_toolbar.panels.headers.HeaderDebugPanel',
156 156 'debug_toolbar.panels.request_vars.RequestVarsDebugPanel',
157 157 'debug_toolbar.panels.template.TemplateDebugPanel',
158 158 'debug_toolbar.panels.sql.SQLDebugPanel',
159 159 'debug_toolbar.panels.signals.SignalDebugPanel',
160 160 'debug_toolbar.panels.logger.LoggingPanel',
161 161 )
162 162
163 163 # TODO: NEED DESIGN FIXES
164 164 CAPTCHA_OUTPUT_FORMAT = (u' %(hidden_field)s '
165 165 u'<div class="form-label">%(image)s</div>'
166 166 u'<div class="form-text">%(text_field)s</div>')
167 167
168 168 # A sample logging configuration. The only tangible logging
169 169 # performed by this configuration is to send an email to
170 170 # the site admins on every HTTP 500 error when DEBUG=False.
171 171 # See http://docs.djangoproject.com/en/dev/topics/logging for
172 172 # more details on how to customize your logging configuration.
173 173 LOGGING = {
174 174 'version': 1,
175 175 'disable_existing_loggers': False,
176 176 'filters': {
177 177 'require_debug_false': {
178 178 '()': 'django.utils.log.RequireDebugFalse'
179 179 }
180 180 },
181 181 'handlers': {
182 182 'mail_admins': {
183 183 'level': 'ERROR',
184 184 'filters': ['require_debug_false'],
185 185 'class': 'django.utils.log.AdminEmailHandler'
186 186 }
187 187 },
188 188 'loggers': {
189 189 'django.request': {
190 190 'handlers': ['mail_admins'],
191 191 'level': 'ERROR',
192 192 'propagate': True,
193 193 },
194 194 }
195 195 }
196 196
197 197 MARKUP_FIELD_TYPES = (
198 198 ('markdown', markdown_extended),
199 199 )
200 200 # Custom imageboard settings
201 201 # TODO These should me moved to
202 202 MAX_POSTS_PER_THREAD = 10 # Thread bumplimit
203 203 MAX_THREAD_COUNT = 500 # Old threads will be deleted to preserve this count
204 204 THREADS_PER_PAGE = 3
205 205 SITE_NAME = 'Neboard'
206 206
207 207 THEMES = [
208 208 ('md', 'Mystic Dark'),
209 209 ('md_centered', 'Mystic Dark (centered)'),
210 210 ('sw', 'Snow White'),
211 211 ('pg', 'Photon Gray'),
212 212 ]
213 213
214 214 DEFAULT_THEME = 'md'
215 215
216 216 POPULAR_TAGS = 10
217 217 LAST_REPLIES_COUNT = 3
218 218
219 219 ENABLE_CAPTCHA = False
220 220 # if user tries to post before CAPTCHA_DEFAULT_SAFE_TIME. Captcha will be shown
221 221 CAPTCHA_DEFAULT_SAFE_TIME = 30 # seconds
222 222 POSTING_DELAY = 20 # seconds
223 223
224 224 COMPRESS_HTML = True
225 225
226 VERSION = '1.7 Anubis'
227
226 228 # Debug mode middlewares
227 229 if DEBUG:
228 230 MIDDLEWARE_CLASSES += (
229 231 'boards.profiler.ProfilerMiddleware',
230 232 'debug_toolbar.middleware.DebugToolbarMiddleware',
231 233 )
232 234
233 235 def custom_show_toolbar(request):
234 236 return DEBUG
235 237
236 238 DEBUG_TOOLBAR_CONFIG = {
237 239 'INTERCEPT_REDIRECTS': False,
238 240 'SHOW_TOOLBAR_CALLBACK': custom_show_toolbar,
239 241 'HIDE_DJANGO_SQL': False,
240 242 'ENABLE_STACKTRACES': True,
241 243 }
242 244
243 245 # FIXME Uncommenting this fails somehow. Need to investigate this
244 246 #DEBUG_TOOLBAR_PANELS += (
245 247 # 'debug_toolbar.panels.profiling.ProfilingDebugPanel',
246 248 #)
247 249
1 NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now