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