##// END OF EJS Templates
Added a server-side gallery and mode switcher
neko259 -
r458:8937aaa9 1.5-dev
parent child Browse files
Show More
@@ -0,0 +1,58 b''
1 {% extends "boards/base.html" %}
2
3 {% load i18n %}
4 {% load cache %}
5 {% load static from staticfiles %}
6 {% load board %}
7
8 {% block head %}
9 <title>Neboard - {{ thread.get_opening_post.get_title }}</title>
10 {% endblock %}
11
12 {% block content %}
13 {% spaceless %}
14 {% get_current_language as LANGUAGE_CODE %}
15
16 <script src="{% static 'js/thread.js' %}"></script>
17
18 {% cache 600 thread_gallery_view thread.id thread.last_edit_time LANGUAGE_CODE %}
19 <div class="image-mode-tab">
20 <a href="{% url 'thread' thread.get_opening_post.id %}">{% trans 'Normal mode' %}</a>,
21 <a class="current_page" href="{% url 'thread_mode' thread.get_opening_post.id 'gallery' %}">{% trans 'Gallery mode' %}</a>
22 </div>
23
24 <div id="posts-table">
25 {% for post in thread.get_replies %}
26 {% if post.image %}
27 <a
28 class="thumb"
29 href="{{ post.image.url }}"><img
30 src="{{ post.image.url_200x150 }}"
31 alt="{{ post.id }}"
32 width="{{ post.image_pre_width }}"
33 height="{{ post.image_pre_height }}"
34 data-width="{{ post.image_width }}"
35 data-height="{{ post.image_height }}"/>
36 </a>
37 {% endif %}
38 {% endfor %}
39 </div>
40 {% endcache %}
41
42 {% endspaceless %}
43 {% endblock %}
44
45 {% block metapanel %}
46
47 {% get_current_language as LANGUAGE_CODE %}
48
49 <span class="metapanel" data-last-update="{{ last_update }}">
50 {% cache 600 thread_meta thread.last_edit_time moderator LANGUAGE_CODE %}
51 <span id="reply-count">{{ thread.get_reply_count }}</span> {% trans 'replies' %},
52 <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}.
53 {% trans 'Last update: ' %}{{ thread.last_edit_time }}
54 [<a href="rss/">RSS</a>]
55 {% endcache %}
56 </span>
57
58 {% endblock %}
@@ -1,81 +1,47 b''
1 1 /*
2 2 @licstart The following is the entire license notice for the
3 3 JavaScript code in this page.
4 4
5 5
6 6 Copyright (C) 2013 neko259
7 7
8 8 The JavaScript code in this page is free software: you can
9 9 redistribute it and/or modify it under the terms of the GNU
10 10 General Public License (GNU GPL) as published by the Free Software
11 11 Foundation, either version 3 of the License, or (at your option)
12 12 any later version. The code is distributed WITHOUT ANY WARRANTY;
13 13 without even the implied warranty of MERCHANTABILITY or FITNESS
14 14 FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
15 15
16 16 As additional permission under GNU GPL version 3 section 7, you
17 17 may distribute non-source (e.g., minimized or compacted) forms of
18 18 that code without the copy of the GNU GPL normally required by
19 19 section 4, provided you include this license notice and a URL
20 20 through which recipients can access the Corresponding Source.
21 21
22 22 @licend The above is the entire license notice
23 23 for the JavaScript code in this page.
24 24 */
25 25
26 function addGalleryPanel() {
27 var gallery = $('a[class="thumb"]').clone(true),
28 normal = $('.post').clone(true);
29
30 $('.navigation_panel').filter(':first').after(
31 '<div class="image-mode-tab" role="radiogroup" aria-label="Image mode2">' +
32 '<label><input type="radio" class="image-mode-normal" name="image-mode" value="0" checked="checked"/>'+ gettext('Normal') +'</label>' +
33 '<label><input type="radio" class="image-mode-table" name="image-mode" value="1"/>'+ gettext('Gallery') +'</label>' +
34 '</div>'
35 );
36
37 $('input[name="image-mode"]').change(function() {
38 //gallery mode
39 if($(this).val() === '1') {
40 $('.thread').replaceWith(
41 $('<div id="posts-table"></div>').append(gallery)
42 );
43 }
44 //normal mode
45 else {
46 $('#posts-table').replaceWith(
47 $('<div class="thread"></div>').append(normal)
48 );
49 }
50 });
51 }
52
53 26 function moveCaretToEnd(el) {
54 27 if (typeof el.selectionStart == "number") {
55 28 el.selectionStart = el.selectionEnd = el.value.length;
56 29 } else if (typeof el.createTextRange != "undefined") {
57 30 el.focus();
58 31 var range = el.createTextRange();
59 32 range.collapse(false);
60 33 range.select();
61 34 }
62 35 }
63 36
64 37 function addQuickReply(postId) {
65 38 var textToAdd = '>>' + postId + '\n\n';
66 39 var textAreaId = 'textarea';
67 40 $(textAreaId).val($(textAreaId).val()+ textToAdd);
68 41
69 42 var textarea = document.getElementsByTagName('textarea')[0];
70 43 $(textAreaId).focus();
71 44 moveCaretToEnd(textarea);
72 45
73 46 $("html, body").animate({ scrollTop: $(textAreaId).offset().top }, "slow");
74 47 }
75
76
77
78 $(document).ready(function(){
79 addGalleryPanel();
80 initAutoupdate();
81 });
@@ -1,188 +1,192 b''
1 1 /*
2 2 @licstart The following is the entire license notice for the
3 3 JavaScript code in this page.
4 4
5 5
6 6 Copyright (C) 2013 neko259
7 7
8 8 The JavaScript code in this page is free software: you can
9 9 redistribute it and/or modify it under the terms of the GNU
10 10 General Public License (GNU GPL) as published by the Free Software
11 11 Foundation, either version 3 of the License, or (at your option)
12 12 any later version. The code is distributed WITHOUT ANY WARRANTY;
13 13 without even the implied warranty of MERCHANTABILITY or FITNESS
14 14 FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
15 15
16 16 As additional permission under GNU GPL version 3 section 7, you
17 17 may distribute non-source (e.g., minimized or compacted) forms of
18 18 that code without the copy of the GNU GPL normally required by
19 19 section 4, provided you include this license notice and a URL
20 20 through which recipients can access the Corresponding Source.
21 21
22 22 @licend The above is the entire license notice
23 23 for the JavaScript code in this page.
24 24 */
25 25
26 26 var THREAD_UPDATE_DELAY = 10000;
27 27
28 28 var loading = false;
29 29 var lastUpdateTime = null;
30 30
31 31 function blink(node) {
32 32 var blinkCount = 2;
33 33 var blinkDelay = 250;
34 34
35 35 var nodeToAnimate = node;
36 36 for (var i = 0; i < blinkCount; i++) {
37 37 nodeToAnimate = nodeToAnimate.fadeOut(blinkDelay).fadeIn(blinkDelay);
38 38 }
39 39 }
40 40
41 41 function updateThread() {
42 42 if (loading) {
43 43 return;
44 44 }
45 45
46 46 loading = true;
47 47
48 48 var threadPosts = $('div.thread').children('.post');
49 49
50 50 var lastPost = threadPosts.last();
51 51 var threadId = threadPosts.first().attr('id');
52 52
53 53 var diffUrl = '/api/diff_thread/' + threadId + '/' + lastUpdateTime + '/';
54 54 $.getJSON(diffUrl)
55 55 .success(function(data) {
56 56 var bottom = isPageBottom();
57 57
58 58 var addedPosts = data.added;
59 59 for (var i = 0; i < addedPosts.length; i++) {
60 60 var postText = addedPosts[i];
61 61
62 62 var post = $(postText);
63 63 post.appendTo(lastPost.parent());
64 64 addRefLinkPreview(post[0]);
65 65
66 66 lastPost = post;
67 67 blink(post);
68 68 }
69 69
70 70 var updatedPosts = data.updated;
71 71 for (var i = 0; i < updatedPosts.length; i++) {
72 72 var postText = updatedPosts[i];
73 73
74 74 var post = $(postText);
75 75 var postId = post.attr('id');
76 76
77 77 var oldPost = $('div.thread').children('.post[id=' + postId + ']');
78 78
79 79 oldPost.replaceWith(post);
80 80 addRefLinkPreview(post[0]);
81 81
82 82 blink(post);
83 83 }
84 84
85 85 // TODO Process deleted posts
86 86
87 87 lastUpdateTime = data.last_update;
88 88 loading = false;
89 89
90 90 if (bottom) {
91 91 var $target = $('html,body');
92 92 $target.animate({scrollTop: $target.height()}, 1000);
93 93 }
94 94
95 95 $('#reply-count').text(getReplyCount());
96 96 $('#image-count').text(getImageCount());
97 97
98 98 updateBumplimitProgress(data.added.length);
99 99 updatePostBumpableStatus();
100 100
101 101 if (data.added.length + data.updated.length > 0) {
102 102 showNewPostsTitle();
103 103 }
104 104 })
105 105 .error(function(data) {
106 106 // TODO Show error message that server is unavailable?
107 107
108 108 loading = false;
109 109 });
110 110 }
111 111
112 112 function isPageBottom() {
113 113 var scroll = $(window).scrollTop() / ($(document).height()
114 114 - $(window).height())
115 115
116 116 return scroll == 1
117 117 }
118 118
119 119 function initAutoupdate() {
120 120 loading = false;
121 121
122 122 lastUpdateTime = $('.metapanel').attr('data-last-update');
123 123
124 124 setInterval(updateThread, THREAD_UPDATE_DELAY);
125 125 }
126 126
127 127 function getReplyCount() {
128 128 return $('.thread').children('.post').length
129 129 }
130 130
131 131 function getImageCount() {
132 132 return $('.thread').find('img').length
133 133 }
134 134
135 135 /**
136 136 * Update bumplimit progress bar
137 137 */
138 138 function updateBumplimitProgress(postDelta) {
139 139 var progressBar = $('#bumplimit_progress');
140 140 if (progressBar) {
141 141 var postsToLimitElement = $('#left_to_limit');
142 142
143 143 var oldPostsToLimit = parseInt(postsToLimitElement.text());
144 144 var postCount = getReplyCount();
145 145 var bumplimit = postCount - postDelta + oldPostsToLimit;
146 146
147 147 var newPostsToLimit = bumplimit - postCount;
148 148 if (newPostsToLimit <= 0) {
149 149 $('.bar-bg').remove();
150 150 } else {
151 151 postsToLimitElement.text(newPostsToLimit);
152 152 progressBar.width((100 - postCount / bumplimit * 100.0) + '%');
153 153 }
154 154 }
155 155 }
156 156
157 157 /**
158 158 * If the bumplimit is reached, add dead_post class to all posts
159 159 */
160 160 function updatePostBumpableStatus() {
161 161 var postCount = getReplyCount();
162 162 var postsToLimitElement = $('#left_to_limit');
163 163 var postsToLimit = parseInt(postsToLimitElement.text());
164 164
165 165 if (postsToLimit <= 0) {
166 166 $('.thread').find('.post').addClass('dead_post');
167 167 }
168 168 }
169 169
170 170 var documentOriginalTitle = '';
171 171 /**
172 172 * Show 'new posts' text in the title if the document is not visible to a user
173 173 */
174 174 function showNewPostsTitle() {
175 175 if (document.hidden) {
176 176 documentOriginalTitle = document.title;
177 177 document.title = gettext('[new posts]') + ' ' + document.title;
178 178
179 179 document.addEventListener('visibilitychange', function() {
180 180 if (documentOriginalTitle !== '') {
181 181 document.title = documentOriginalTitle;
182 182 documentOriginalTitle = '';
183 183 }
184 184
185 185 document.removeEventListener('visibilitychange', null);
186 186 });
187 187 }
188 } No newline at end of file
188 }
189
190 $(document).ready(function(){
191 initAutoupdate();
192 });
@@ -1,130 +1,136 b''
1 1 {% extends "boards/base.html" %}
2 2
3 3 {% load i18n %}
4 4 {% load cache %}
5 5 {% load static from staticfiles %}
6 6 {% load board %}
7 7
8 8 {% block head %}
9 <title>Neboard - {{ thread.get_replies.0.get_title }}</title>
9 <title>Neboard - {{ thread.get_opening_post.get_title }}</title>
10 10 {% endblock %}
11 11
12 12 {% block content %}
13 13 {% spaceless %}
14 14 {% get_current_language as LANGUAGE_CODE %}
15 15
16 16 <script src="{% static 'js/thread_update.js' %}"></script>
17 17 <script src="{% static 'js/thread.js' %}"></script>
18 18
19 19 {% cache 600 thread_view thread.id thread.last_edit_time moderator LANGUAGE_CODE %}
20
21 <div class="image-mode-tab">
22 <a class="current_page" href="{% url 'thread' thread.get_opening_post.id %}">{% trans 'Normal mode' %}</a>,
23 <a href="{% url 'thread_mode' thread.get_opening_post.id 'gallery' %}">{% trans 'Gallery mode' %}</a>
24 </div>
25
20 26 {% if bumpable %}
21 27 <div class="bar-bg">
22 28 <div class="bar-value" style="width:{{ bumplimit_progress }}%" id="bumplimit_progress">
23 29 </div>
24 30 <div class="bar-text">
25 31 <span id="left_to_limit">{{ posts_left }}</span> {% trans 'posts to bumplimit' %}
26 32 </div>
27 33 </div>
28 34 {% endif %}
29 35 <div class="thread">
30 36 {% for post in thread.get_replies %}
31 37 {% if bumpable %}
32 38 <div class="post" id="{{ post.id }}">
33 39 {% else %}
34 40 <div class="post dead_post" id="{{ post.id }}">
35 41 {% endif %}
36 42 {% if post.image %}
37 43 <div class="image">
38 44 <a
39 45 class="thumb"
40 46 href="{{ post.image.url }}"><img
41 47 src="{{ post.image.url_200x150 }}"
42 48 alt="{{ post.id }}"
43 49 width="{{ post.image_pre_width }}"
44 50 height="{{ post.image_pre_height }}"
45 51 data-width="{{ post.image_width }}"
46 52 data-height="{{ post.image_height }}"/>
47 53 </a>
48 54 </div>
49 55 {% endif %}
50 56 <div class="message">
51 57 <div class="post-info">
52 58 <span class="title">{{ post.title }}</span>
53 59 <a class="post_id" href="#{{ post.id }}">
54 60 ({{ post.id }})</a>
55 61 [{{ post.pub_time }}]
56 62 [<a href="#" onclick="javascript:addQuickReply('{{ post.id }}')
57 63 ; return false;">&gt;&gt;</a>]
58 64
59 65 {% if moderator %}
60 66 <span class="moderator_info">
61 67 [<a href="{% url 'delete' post_id=post.id %}"
62 68 >{% trans 'Delete' %}</a>]
63 69 ({{ post.poster_ip }})
64 70 [<a href="{% url 'ban' post_id=post.id %}?next={{ request.path }}"
65 71 >{% trans 'Ban IP' %}</a>]
66 72 </span>
67 73 {% endif %}
68 74 </div>
69 75 {% autoescape off %}
70 76 {{ post.text.rendered }}
71 77 {% endautoescape %}
72 78 {% if post.is_referenced %}
73 79 <div class="refmap">
74 80 {% trans "Replies" %}:
75 81 {% for ref_post in post.get_sorted_referenced_posts %}
76 82 <a href="{% post_url ref_post.id %}">&gt;&gt;{{ ref_post.id }}</a
77 83 >{% if not forloop.last %},{% endif %}
78 84 {% endfor %}
79 85 </div>
80 86 {% endif %}
81 87 </div>
82 88 {% if forloop.first %}
83 89 <div class="metadata">
84 90 <span class="tags">
85 91 {% for tag in thread.get_tags %}
86 92 <a class="tag" href="{% url 'tag' tag.name %}">
87 93 #{{ tag.name }}</a
88 94 >{% if not forloop.last %},{% endif %}
89 95 {% endfor %}
90 96 </span>
91 97 </div>
92 98 {% endif %}
93 99 </div>
94 100 {% endfor %}
95 101 </div>
96 102 {% endcache %}
97 103
98 104 <div class="post-form-w">
99 105 <script src="{% static 'js/panel.js' %}"></script>
100 106 <div class="form-title">{% trans "Reply to thread" %} #{{ thread.get_opening_post.id }}</div>
101 107 <div class="post-form">
102 108 <form id="form" enctype="multipart/form-data" method="post"
103 109 >{% csrf_token %}
104 110 {{ form.as_div }}
105 111 <div class="form-submit">
106 112 <input type="submit" value="{% trans "Post" %}"/>
107 113 </div>
108 114 </form>
109 115 <div><a href="{% url "staticpage" name="help" %}">
110 116 {% trans 'Text syntax' %}</a></div>
111 117 </div>
112 118 </div>
113 119
114 120 {% endspaceless %}
115 121 {% endblock %}
116 122
117 123 {% block metapanel %}
118 124
119 125 {% get_current_language as LANGUAGE_CODE %}
120 126
121 127 <span class="metapanel" data-last-update="{{ last_update }}">
122 128 {% cache 600 thread_meta thread.last_edit_time moderator LANGUAGE_CODE %}
123 129 <span id="reply-count">{{ thread.get_reply_count }}</span> {% trans 'replies' %},
124 130 <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}.
125 131 {% trans 'Last update: ' %}{{ thread.last_edit_time }}
126 132 [<a href="rss/">RSS</a>]
127 133 {% endcache %}
128 134 </span>
129 135
130 136 {% endblock %}
@@ -1,59 +1,60 b''
1 1 from django.conf.urls import patterns, url, include
2 2 from boards import views
3 3 from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed
4 4 from boards.views.api import api_get_threaddiff
5 5
6 6 js_info_dict = {
7 7 'packages': ('boards',),
8 8 }
9 9
10 10 urlpatterns = patterns('',
11 11
12 12 # /boards/
13 13 url(r'^$', views.index, name='index'),
14 14 # /boards/page/
15 15 url(r'^page/(?P<page>\w+)/$', views.index, name='index'),
16 16
17 17 # login page
18 18 url(r'^login/$', views.login, name='login'),
19 19
20 20 # /boards/tag/tag_name/
21 21 url(r'^tag/(?P<tag_name>\w+)/$', views.tag, name='tag'),
22 22 # /boards/tag/tag_id/page/
23 23 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/$', views.tag, name='tag'),
24 24
25 25 # /boards/tag/tag_name/unsubscribe/
26 26 url(r'^tag/(?P<tag_name>\w+)/subscribe/$', views.tag_subscribe,
27 27 name='tag_subscribe'),
28 28 # /boards/tag/tag_name/unsubscribe/
29 29 url(r'^tag/(?P<tag_name>\w+)/unsubscribe/$', views.tag_unsubscribe,
30 30 name='tag_unsubscribe'),
31 31
32 32 # /boards/thread/
33 33 url(r'^thread/(?P<post_id>\w+)/$', views.thread, name='thread'),
34 url(r'^thread/(?P<post_id>\w+)/(?P<mode>\w+)/$', views.thread, name='thread_mode'),
34 35 url(r'^settings/$', views.settings, name='settings'),
35 36 url(r'^tags/$', views.all_tags, name='tags'),
36 37 url(r'^captcha/', include('captcha.urls')),
37 38 url(r'^jump/(?P<post_id>\w+)/$', views.jump_to_post, name='jumper'),
38 39 url(r'^authors/$', views.authors, name='authors'),
39 40 url(r'^delete/(?P<post_id>\w+)/$', views.delete, name='delete'),
40 41 url(r'^ban/(?P<post_id>\w+)/$', views.ban, name='ban'),
41 42
42 43 url(r'^banned/$', views.you_are_banned, name='banned'),
43 44 url(r'^staticpage/(?P<name>\w+)/$', views.static_page, name='staticpage'),
44 45
45 46 # RSS feeds
46 47 url(r'^rss/$', AllThreadsFeed()),
47 48 url(r'^page/(?P<page>\w+)/rss/$', AllThreadsFeed()),
48 49 url(r'^tag/(?P<tag_name>\w+)/rss/$', TagThreadsFeed()),
49 50 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/rss/$', TagThreadsFeed()),
50 51 url(r'^thread/(?P<post_id>\w+)/rss/$', ThreadPostsFeed()),
51 52
52 53 # i18n
53 54 url(r'^jsi18n/$', 'boards.views.cached_js_catalog', js_info_dict, name='js_info_dict'),
54 55
55 56 # API
56 57 url(r'^api/post/(?P<post_id>\w+)/$', views.get_post, name="get_post"),
57 58 url(r'^api/diff_thread/(?P<thread_id>\w+)/(?P<last_update_time>\w+)/$',
58 59 api_get_threaddiff, name="get_thread_diff"),
59 60 )
@@ -1,550 +1,558 b''
1 1 __author__ = 'neko259'
2 2
3 3 import hashlib
4 4 import string
5 5 import time
6 6 import re
7 7
8 8 from django.core import serializers
9 9 from django.core.urlresolvers import reverse
10 10 from django.http import HttpResponseRedirect
11 11 from django.http.response import HttpResponse
12 12 from django.template import RequestContext
13 13 from django.shortcuts import render, redirect, get_object_or_404
14 14 from django.utils import timezone
15 15 from django.db import transaction
16 16 from django.views.decorators.cache import cache_page
17 17 from django.views.i18n import javascript_catalog
18 18
19 19 from boards import forms
20 20 import boards
21 21 from boards import utils
22 22 from boards.forms import ThreadForm, PostForm, SettingsForm, PlainErrorList, \
23 23 ThreadCaptchaForm, PostCaptchaForm, LoginForm, ModeratorSettingsForm
24 24 from boards.models import Post, Tag, Ban, User
25 25 from boards.models.post import SETTING_MODERATE, REGEX_REPLY
26 26 from boards.models.user import RANK_USER
27 27 from boards import authors
28 28 from boards.utils import get_client_ip
29 29 import neboard
30 30
31 31
32 32 BAN_REASON_SPAM = 'Autoban: spam bot'
33 33
34 34
35 35 def index(request, page=0):
36 36 context = _init_default_context(request)
37 37
38 38 if utils.need_include_captcha(request):
39 39 threadFormClass = ThreadCaptchaForm
40 40 kwargs = {'request': request}
41 41 else:
42 42 threadFormClass = ThreadForm
43 43 kwargs = {}
44 44
45 45 if request.method == 'POST':
46 46 form = threadFormClass(request.POST, request.FILES,
47 47 error_class=PlainErrorList, **kwargs)
48 48 form.session = request.session
49 49
50 50 if form.is_valid():
51 51 return _new_post(request, form)
52 52 if form.need_to_ban:
53 53 # Ban user because he is suspected to be a bot
54 54 _ban_current_user(request)
55 55 else:
56 56 form = threadFormClass(error_class=PlainErrorList, **kwargs)
57 57
58 58 threads = []
59 59 for thread_to_show in Post.objects.get_threads(page=int(page)):
60 60 threads.append(_get_template_thread(thread_to_show))
61 61
62 62 # TODO Make this generic for tag and threads list pages
63 63 context['threads'] = None if len(threads) == 0 else threads
64 64 context['form'] = form
65 65 context['current_page'] = int(page)
66 66
67 67 page_count = Post.objects.get_thread_page_count()
68 68 context['pages'] = range(page_count)
69 69 page = int(page)
70 70 if page < page_count - 1:
71 71 context['next_page'] = str(page + 1)
72 72 if page > 0:
73 73 context['prev_page'] = str(page - 1)
74 74
75 75 return render(request, 'boards/posting_general.html',
76 76 context)
77 77
78 78
79 79 @transaction.atomic
80 80 def _new_post(request, form, opening_post=None):
81 81 """Add a new post (in thread or as a reply)."""
82 82
83 83 ip = get_client_ip(request)
84 84 is_banned = Ban.objects.filter(ip=ip).exists()
85 85
86 86 if is_banned:
87 87 return redirect(you_are_banned)
88 88
89 89 data = form.cleaned_data
90 90
91 91 title = data['title']
92 92 text = data['text']
93 93
94 94 text = _remove_invalid_links(text)
95 95
96 96 if 'image' in data.keys():
97 97 image = data['image']
98 98 else:
99 99 image = None
100 100
101 101 tags = []
102 102
103 103 if not opening_post:
104 104 tag_strings = data['tags']
105 105
106 106 if tag_strings:
107 107 tag_strings = tag_strings.split(' ')
108 108 for tag_name in tag_strings:
109 109 tag_name = string.lower(tag_name.strip())
110 110 if len(tag_name) > 0:
111 111 tag, created = Tag.objects.get_or_create(name=tag_name)
112 112 tags.append(tag)
113 113 post_thread = None
114 114 else:
115 115 post_thread = opening_post.thread_new
116 116
117 117 post = Post.objects.create_post(title=title, text=text, ip=ip,
118 118 thread=post_thread, image=image,
119 119 tags=tags, user=_get_user(request))
120 120
121 121 thread_to_show = (opening_post.id if opening_post else post.id)
122 122
123 123 if opening_post:
124 124 return redirect(reverse(thread, kwargs={'post_id': thread_to_show}) +
125 125 '#' + str(post.id))
126 126 else:
127 127 return redirect(thread, post_id=thread_to_show)
128 128
129 129
130 130 def tag(request, tag_name, page=0):
131 131 """
132 132 Get all tag threads. Threads are split in pages, so some page is
133 133 requested. Default page is 0.
134 134 """
135 135
136 136 tag = get_object_or_404(Tag, name=tag_name)
137 137 threads = []
138 138 for thread_to_show in Post.objects.get_threads(page=int(page), tag=tag):
139 139 threads.append(_get_template_thread(thread_to_show))
140 140
141 141 if request.method == 'POST':
142 142 form = ThreadForm(request.POST, request.FILES,
143 143 error_class=PlainErrorList)
144 144 form.session = request.session
145 145
146 146 if form.is_valid():
147 147 return _new_post(request, form)
148 148 if form.need_to_ban:
149 149 # Ban user because he is suspected to be a bot
150 150 _ban_current_user(request)
151 151 else:
152 152 form = forms.ThreadForm(initial={'tags': tag_name},
153 153 error_class=PlainErrorList)
154 154
155 155 context = _init_default_context(request)
156 156 context['threads'] = None if len(threads) == 0 else threads
157 157 context['tag'] = tag
158 158 context['current_page'] = int(page)
159 159
160 160 page_count = Post.objects.get_thread_page_count(tag=tag)
161 161 context['pages'] = range(page_count)
162 162 page = int(page)
163 163 if page < page_count - 1:
164 164 context['next_page'] = str(page + 1)
165 165 if page > 0:
166 166 context['prev_page'] = str(page - 1)
167 167
168 168 context['form'] = form
169 169
170 170 return render(request, 'boards/posting_general.html',
171 171 context)
172 172
173 173
174 def thread(request, post_id):
174 def thread(request, post_id, mode='normal'):
175 175 """Get all thread posts"""
176 176
177 177 if utils.need_include_captcha(request):
178 178 postFormClass = PostCaptchaForm
179 179 kwargs = {'request': request}
180 180 else:
181 181 postFormClass = PostForm
182 182 kwargs = {}
183 183
184 184 if request.method == 'POST':
185 185 form = postFormClass(request.POST, request.FILES,
186 186 error_class=PlainErrorList, **kwargs)
187 187 form.session = request.session
188 188
189 189 opening_post = get_object_or_404(Post, id=post_id)
190 190 if form.is_valid():
191 191 return _new_post(request, form, opening_post)
192 192 if form.need_to_ban:
193 193 # Ban user because he is suspected to be a bot
194 194 _ban_current_user(request)
195 195 else:
196 196 form = postFormClass(error_class=PlainErrorList, **kwargs)
197 197
198 198 thread_to_show = get_object_or_404(Post, id=post_id).thread_new
199 199
200 200 context = _init_default_context(request)
201 201
202 202 posts = thread_to_show.get_replies()
203 203 context['form'] = form
204 204 context['bumpable'] = thread_to_show.can_bump()
205 205 if context['bumpable']:
206 206 context['posts_left'] = neboard.settings.MAX_POSTS_PER_THREAD - posts \
207 207 .count()
208 208 context['bumplimit_progress'] = str(
209 209 float(context['posts_left']) /
210 210 neboard.settings.MAX_POSTS_PER_THREAD * 100)
211 211 context["last_update"] = _datetime_to_epoch(thread_to_show.last_edit_time)
212 212 context["thread"] = thread_to_show
213 213
214 return render(request, 'boards/thread.html', context)
214 if 'normal' == mode:
215 document = 'boards/thread.html'
216 elif 'gallery' == mode:
217 document = 'boards/thread_gallery.html'
218 else:
219 pass
220 # TODO raise 404 error
221
222 return render(request, document, context)
215 223
216 224
217 225 def login(request):
218 226 """Log in with user id"""
219 227
220 228 context = _init_default_context(request)
221 229
222 230 if request.method == 'POST':
223 231 form = LoginForm(request.POST, request.FILES,
224 232 error_class=PlainErrorList)
225 233 form.session = request.session
226 234
227 235 if form.is_valid():
228 236 user = User.objects.get(user_id=form.cleaned_data['user_id'])
229 237 request.session['user_id'] = user.id
230 238 return redirect(index)
231 239
232 240 else:
233 241 form = LoginForm()
234 242
235 243 context['form'] = form
236 244
237 245 return render(request, 'boards/login.html', context)
238 246
239 247
240 248 def settings(request):
241 249 """User's settings"""
242 250
243 251 context = _init_default_context(request)
244 252 user = _get_user(request)
245 253 is_moderator = user.is_moderator()
246 254
247 255 if request.method == 'POST':
248 256 with transaction.atomic():
249 257 if is_moderator:
250 258 form = ModeratorSettingsForm(request.POST,
251 259 error_class=PlainErrorList)
252 260 else:
253 261 form = SettingsForm(request.POST, error_class=PlainErrorList)
254 262
255 263 if form.is_valid():
256 264 selected_theme = form.cleaned_data['theme']
257 265
258 266 user.save_setting('theme', selected_theme)
259 267
260 268 if is_moderator:
261 269 moderate = form.cleaned_data['moderate']
262 270 user.save_setting(SETTING_MODERATE, moderate)
263 271
264 272 return redirect(settings)
265 273 else:
266 274 selected_theme = _get_theme(request)
267 275
268 276 if is_moderator:
269 277 form = ModeratorSettingsForm(initial={'theme': selected_theme,
270 278 'moderate': context['moderator']},
271 279 error_class=PlainErrorList)
272 280 else:
273 281 form = SettingsForm(initial={'theme': selected_theme},
274 282 error_class=PlainErrorList)
275 283
276 284 context['form'] = form
277 285
278 286 return render(request, 'boards/settings.html', context)
279 287
280 288
281 289 def all_tags(request):
282 290 """All tags list"""
283 291
284 292 context = _init_default_context(request)
285 293 context['all_tags'] = Tag.objects.get_not_empty_tags()
286 294
287 295 return render(request, 'boards/tags.html', context)
288 296
289 297
290 298 def jump_to_post(request, post_id):
291 299 """Determine thread in which the requested post is and open it's page"""
292 300
293 301 post = get_object_or_404(Post, id=post_id)
294 302
295 303 if not post.thread:
296 304 return redirect(thread, post_id=post.id)
297 305 else:
298 306 return redirect(reverse(thread, kwargs={'post_id': post.thread.id})
299 307 + '#' + str(post.id))
300 308
301 309
302 310 def authors(request):
303 311 """Show authors list"""
304 312
305 313 context = _init_default_context(request)
306 314 context['authors'] = boards.authors.authors
307 315
308 316 return render(request, 'boards/authors.html', context)
309 317
310 318
311 319 @transaction.atomic
312 320 def delete(request, post_id):
313 321 """Delete post"""
314 322
315 323 user = _get_user(request)
316 324 post = get_object_or_404(Post, id=post_id)
317 325
318 326 if user.is_moderator():
319 327 # TODO Show confirmation page before deletion
320 328 Post.objects.delete_post(post)
321 329
322 330 if not post.thread:
323 331 return _redirect_to_next(request)
324 332 else:
325 333 return redirect(thread, post_id=post.thread.id)
326 334
327 335
328 336 @transaction.atomic
329 337 def ban(request, post_id):
330 338 """Ban user"""
331 339
332 340 user = _get_user(request)
333 341 post = get_object_or_404(Post, id=post_id)
334 342
335 343 if user.is_moderator():
336 344 # TODO Show confirmation page before ban
337 345 ban, created = Ban.objects.get_or_create(ip=post.poster_ip)
338 346 if created:
339 347 ban.reason = 'Banned for post ' + str(post_id)
340 348 ban.save()
341 349
342 350 return _redirect_to_next(request)
343 351
344 352
345 353 def you_are_banned(request):
346 354 """Show the page that notifies that user is banned"""
347 355
348 356 context = _init_default_context(request)
349 357
350 358 ban = get_object_or_404(Ban, ip=utils.get_client_ip(request))
351 359 context['ban_reason'] = ban.reason
352 360 return render(request, 'boards/staticpages/banned.html', context)
353 361
354 362
355 363 def page_404(request):
356 364 """Show page 404 (not found error)"""
357 365
358 366 context = _init_default_context(request)
359 367 return render(request, 'boards/404.html', context)
360 368
361 369
362 370 @transaction.atomic
363 371 def tag_subscribe(request, tag_name):
364 372 """Add tag to favorites"""
365 373
366 374 user = _get_user(request)
367 375 tag = get_object_or_404(Tag, name=tag_name)
368 376
369 377 if not tag in user.fav_tags.all():
370 378 user.add_tag(tag)
371 379
372 380 return _redirect_to_next(request)
373 381
374 382
375 383 @transaction.atomic
376 384 def tag_unsubscribe(request, tag_name):
377 385 """Remove tag from favorites"""
378 386
379 387 user = _get_user(request)
380 388 tag = get_object_or_404(Tag, name=tag_name)
381 389
382 390 if tag in user.fav_tags.all():
383 391 user.remove_tag(tag)
384 392
385 393 return _redirect_to_next(request)
386 394
387 395
388 396 def static_page(request, name):
389 397 """Show a static page that needs only tags list and a CSS"""
390 398
391 399 context = _init_default_context(request)
392 400 return render(request, 'boards/staticpages/' + name + '.html', context)
393 401
394 402
395 403 def api_get_post(request, post_id):
396 404 """
397 405 Get the JSON of a post. This can be
398 406 used as and API for external clients.
399 407 """
400 408
401 409 post = get_object_or_404(Post, id=post_id)
402 410
403 411 json = serializers.serialize("json", [post], fields=(
404 412 "pub_time", "_text_rendered", "title", "text", "image",
405 413 "image_width", "image_height", "replies", "tags"
406 414 ))
407 415
408 416 return HttpResponse(content=json)
409 417
410 418
411 419 def get_post(request, post_id):
412 420 """Get the html of a post. Used for popups."""
413 421
414 422 post = get_object_or_404(Post, id=post_id)
415 423 thread = post.thread_new
416 424
417 425 context = RequestContext(request)
418 426 context["post"] = post
419 427 context["can_bump"] = thread.can_bump()
420 428 if "truncated" in request.GET:
421 429 context["truncated"] = True
422 430
423 431 return render(request, 'boards/post.html', context)
424 432
425 433 @cache_page(86400)
426 434 def cached_js_catalog(request, domain='djangojs', packages=None):
427 435 return javascript_catalog(request, domain, packages)
428 436
429 437
430 438 def _get_theme(request, user=None):
431 439 """Get user's CSS theme"""
432 440
433 441 if not user:
434 442 user = _get_user(request)
435 443 theme = user.get_setting('theme')
436 444 if not theme:
437 445 theme = neboard.settings.DEFAULT_THEME
438 446
439 447 return theme
440 448
441 449
442 450 def _init_default_context(request):
443 451 """Create context with default values that are used in most views"""
444 452
445 453 context = RequestContext(request)
446 454
447 455 user = _get_user(request)
448 456 context['user'] = user
449 457 context['tags'] = user.get_sorted_fav_tags()
450 458 context['posts_per_day'] = float(Post.objects.get_posts_per_day())
451 459
452 460 theme = _get_theme(request, user)
453 461 context['theme'] = theme
454 462 context['theme_css'] = 'css/' + theme + '/base_page.css'
455 463
456 464 # This shows the moderator panel
457 465 moderate = user.get_setting(SETTING_MODERATE)
458 466 if moderate == 'True':
459 467 context['moderator'] = user.is_moderator()
460 468 else:
461 469 context['moderator'] = False
462 470
463 471 return context
464 472
465 473
466 474 def _get_user(request):
467 475 """
468 476 Get current user from the session. If the user does not exist, create
469 477 a new one.
470 478 """
471 479
472 480 session = request.session
473 481 if not 'user_id' in session:
474 482 request.session.save()
475 483
476 484 md5 = hashlib.md5()
477 485 md5.update(session.session_key)
478 486 new_id = md5.hexdigest()
479 487
480 488 time_now = timezone.now()
481 489 user = User.objects.create(user_id=new_id, rank=RANK_USER,
482 490 registration_time=time_now)
483 491
484 492 session['user_id'] = user.id
485 493 else:
486 494 user = User.objects.get(id=session['user_id'])
487 495
488 496 return user
489 497
490 498
491 499 def _redirect_to_next(request):
492 500 """
493 501 If a 'next' parameter was specified, redirect to the next page. This is
494 502 used when the user is required to return to some page after the current
495 503 view has finished its work.
496 504 """
497 505
498 506 if 'next' in request.GET:
499 507 next_page = request.GET['next']
500 508 return HttpResponseRedirect(next_page)
501 509 else:
502 510 return redirect(index)
503 511
504 512
505 513 @transaction.atomic
506 514 def _ban_current_user(request):
507 515 """Add current user to the IP ban list"""
508 516
509 517 ip = utils.get_client_ip(request)
510 518 ban, created = Ban.objects.get_or_create(ip=ip)
511 519 if created:
512 520 ban.can_read = False
513 521 ban.reason = BAN_REASON_SPAM
514 522 ban.save()
515 523
516 524
517 525 def _remove_invalid_links(text):
518 526 """
519 527 Replace invalid links in posts so that they won't be parsed.
520 528 Invalid links are links to non-existent posts
521 529 """
522 530
523 531 for reply_number in re.finditer(REGEX_REPLY, text):
524 532 post_id = reply_number.group(1)
525 533 post = Post.objects.filter(id=post_id)
526 534 if not post.exists():
527 535 text = string.replace(text, '>>' + post_id, post_id)
528 536
529 537 return text
530 538
531 539
532 540 def _datetime_to_epoch(datetime):
533 541 return int(time.mktime(timezone.localtime(
534 542 datetime,timezone.get_current_timezone()).timetuple())
535 543 * 1000000 + datetime.microsecond)
536 544
537 545
538 546 def _get_template_thread(thread_to_show):
539 547 """Get template values for thread"""
540 548
541 549 last_replies = thread_to_show.get_last_replies()
542 550 skipped_replies_count = thread_to_show.get_replies().count() \
543 551 - len(last_replies) - 1
544 552 return {
545 553 'thread': thread_to_show,
546 554 'op': thread_to_show.get_replies()[0],
547 555 'bumpable': thread_to_show.can_bump(),
548 556 'last_replies': last_replies,
549 557 'skipped_replies': skipped_replies_count,
550 558 }
General Comments 0
You need to be logged in to leave comments. Login now