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