##// END OF EJS Templates
AJAX-based thread creation
neko259 -
r1998:8255ca3c default
parent child Browse files
Show More
@@ -1,232 +1,302 b''
1 var ITEM_FILE_SOURCE = 'fileSource';
1 var ITEM_FILE_SOURCE = 'fileSource';
2 var URL_STICKERS = '/api/stickers';
2 var URL_STICKERS = '/api/stickers';
3 var MIN_INPUT_LENGTH = 3;
3 var MIN_INPUT_LENGTH = 3;
4 var URL_DELIMITER = '\n';
4 var URL_DELIMITER = '\n';
5 // TODO This needs to be the same for attachment download time limit.
6 var POST_AJAX_TIMEOUT = 30000;
5
7
6 var pastedImages = [];
8 var pastedImages = [];
7
9
8 $('input[name=image]').wrap($('<div class="file_wrap"></div>'));
10 $('input[name=image]').wrap($('<div class="file_wrap"></div>'));
9
11
10 $('body').on('change', 'input[name=image]', function(event) {
12 $('body').on('change', 'input[name=image]', function(event) {
11 var file = event.target.files[0];
13 var file = event.target.files[0];
12
14
13 if(file.type.match('image.*')) {
15 if(file.type.match('image.*')) {
14 var fileReader = new FileReader();
16 var fileReader = new FileReader();
15
17
16 fileReader.addEventListener("load", function(event) {
18 fileReader.addEventListener("load", function(event) {
17 var wrapper = $('.file_wrap');
19 var wrapper = $('.file_wrap');
18
20
19 wrapper.find('.file-thumb').remove();
21 wrapper.find('.file-thumb').remove();
20 wrapper.append(
22 wrapper.append(
21 $('<div class="file-thumb" style="background-image: url('+event.target.result+')"></div>')
23 $('<div class="file-thumb" style="background-image: url('+event.target.result+')"></div>')
22 );
24 );
23 });
25 });
24
26
25 fileReader.readAsDataURL(file);
27 fileReader.readAsDataURL(file);
26 }
28 }
27 });
29 });
28
30
29 var form = $('#form');
31 var form = $('#form');
30 $('textarea').keypress(function(event) {
32 $('textarea').keypress(function(event) {
31 if ((event.which == 10 || event.which == 13) && event.ctrlKey) {
33 if ((event.which == 10 || event.which == 13) && event.ctrlKey) {
32 form.find('input[type=submit]').click();
34 form.find('input[type=submit]').click();
33 }
35 }
34 });
36 });
35
37
36 $('#preview-button').click(function() {
38 $('#preview-button').click(function() {
37 var data = {
39 var data = {
38 raw_text: $('textarea#id_text').val()
40 raw_text: $('textarea#id_text').val()
39 }
41 }
40
42
41 var diffUrl = '/api/preview/';
43 var diffUrl = '/api/preview/';
42
44
43 $.post(diffUrl,
45 $.post(diffUrl,
44 data,
46 data,
45 function(data) {
47 function(data) {
46 var previewTextBlock = $('#preview-text');
48 var previewTextBlock = $('#preview-text');
47 previewTextBlock.html(data);
49 previewTextBlock.html(data);
48 previewTextBlock.show();
50 previewTextBlock.show();
49
51
50 addScriptsToPost(previewTextBlock);
52 addScriptsToPost(previewTextBlock);
51 })
53 })
52 });
54 });
53
55
54 /**
56 /**
55 * Show text in the errors row of the form.
57 * Show text in the errors row of the form.
56 * @param form
58 * @param form
57 * @param text
59 * @param text
58 */
60 */
59 function showAsErrors(form, text) {
61 function showAsErrors(form, text) {
60 form.children('.form-errors').remove();
62 form.children('.form-errors').remove();
61
63
62 if (text.length > 0) {
64 if (text.length > 0) {
63 var errorList = $('<div class="form-errors">' + text + '<div>');
65 var errorList = $('<div class="form-errors">' + text + '<div>');
64 errorList.appendTo(form);
66 errorList.appendTo(form);
65 }
67 }
66 }
68 }
67
69
68 function addHiddenInput(form, name, value) {
70 function addHiddenInput(form, name, value) {
69 form.find('input[name=' + name + ']').val(value);
71 form.find('input[name=' + name + ']').val(value);
70 }
72 }
71
73
72 function selectFileChoice() {
74 function selectFileChoice() {
73 var file_input = $('#id_file');
75 var file_input = $('#id_file');
74 var url_input = $('#id_file_url');
76 var url_input = $('#id_file_url');
75
77
76 var file_input_row = file_input.parent().parent();
78 var file_input_row = file_input.parent().parent();
77 var url_input_row = url_input.parent().parent();
79 var url_input_row = url_input.parent().parent();
78
80
79 file_input_row.toggle();
81 file_input_row.toggle();
80 url_input_row.toggle();
82 url_input_row.toggle();
81 url_input.val('');
83 url_input.val('');
82 file_input.val('');
84 file_input.val('');
83
85
84 var source;
86 var source;
85 if (file_input_row.is(':visible')) {
87 if (file_input_row.is(':visible')) {
86 source = 'file';
88 source = 'file';
87 } else {
89 } else {
88 source = 'url';
90 source = 'url';
89 }
91 }
90 localStorage.setItem(ITEM_FILE_SOURCE, source);
92 localStorage.setItem(ITEM_FILE_SOURCE, source);
91 }
93 }
92
94
93 function getPostTextarea() {
95 function getPostTextarea() {
94 return $('textarea#id_text');
96 return $('textarea#id_text');
95 }
97 }
96
98
97 function addOnImagePaste() {
99 function addOnImagePaste() {
98 $('#id_file_1').on('paste', function(event) {
100 $('#id_file_1').on('paste', function(event) {
99 var items = (event.clipboardData || event.originalEvent.clipboardData).items;
101 var items = (event.clipboardData || event.originalEvent.clipboardData).items;
100 for (index in items) {
102 for (index in items) {
101 var item = items[index];
103 var item = items[index];
102 if (item.kind === 'file') {
104 if (item.kind === 'file') {
103 var blob = item.getAsFile();
105 var blob = item.getAsFile();
104
106
105 pastedImages.push(blob);
107 pastedImages.push(blob);
106
108
107 var pastedImagesList = $('#pasted-images');
109 var pastedImagesList = $('#pasted-images');
108 if (pastedImagesList.length === 0) {
110 if (pastedImagesList.length === 0) {
109 pastedImagesList = $('<div id="pasted-images" />');
111 pastedImagesList = $('<div id="pasted-images" />');
110 $('#id_file_1').parent().append(pastedImagesList);
112 $('#id_file_1').parent().append(pastedImagesList);
111 }
113 }
112
114
113 var fr = new FileReader();
115 var fr = new FileReader();
114 fr.onload = function () {
116 fr.onload = function () {
115 var img = $('<img class="image-preview" />');
117 var img = $('<img class="image-preview" />');
116 img.attr('src', fr.result);
118 img.attr('src', fr.result);
117 pastedImagesList.append(img);
119 pastedImagesList.append(img);
118 img.on("click", function() {
120 img.on("click", function() {
119 // Remove the image from all lists
121 // Remove the image from all lists
120 var itemIndex = $(this).index();
122 var itemIndex = $(this).index();
121 pastedImages.splice(itemIndex, 1);
123 pastedImages.splice(itemIndex, 1);
122 $(this).remove();
124 $(this).remove();
123 });
125 });
124 };
126 };
125 fr.readAsDataURL(blob);
127 fr.readAsDataURL(blob);
126 }
128 }
127 }
129 }
128 });
130 });
129 }
131 }
130
132
133 /**
134 * When the form is posted, this method will be run as a callback
135 */
136 function updateOnPost(response, statusText, xhr, form) {
137 var json = $.parseJSON(response);
138 var status = json.status;
139 var url = json.url;
140
141 showAsErrors(form, '');
142 $('.post-form-w').unblock();
143
144 if (status === 'ok') {
145 if (url) {
146 document.location = url;
147 } else {
148 resetForm();
149 getThreadDiff();
150 scrollToBottom();
151 }
152 } else {
153 var errors = json.errors;
154 for (var i = 0; i < errors.length; i++) {
155 var fieldErrors = errors[i];
156
157 var error = fieldErrors.errors;
158
159 showAsErrors(form, error);
160 }
161 }
162 }
163
164 function initAjaxForm(openingPostId) {
165 var form = $('#form');
166
167 var url = '/api/add_post/';
168 if (openingPostId) {
169 url += openingPostId + '/';
170 }
171
172 if (form.length > 0) {
173 var options = {
174 beforeSubmit: function(arr, form, options) {
175 $('.post-form-w').block({ message: gettext('Sending message...') });
176
177 $.each(pastedImages, function(i, blob) {
178 arr.push({
179 name: "file_0",
180 value: blob
181 });
182 });
183 },
184 success: updateOnPost,
185 error: function(xhr, textStatus, errorString) {
186 var errorText = gettext('Server error: ') + textStatus;
187 if (errorString) {
188 errorText += ' / ' + errorString;
189 }
190 showAsErrors(form, errorText);
191 $('.post-form-w').unblock();
192 },
193 url: url,
194 timeout: POST_AJAX_TIMEOUT
195 };
196
197 form.ajaxForm(options);
198 }
199 }
200
131 $(document).ready(function() {
201 $(document).ready(function() {
132 var powDifficulty = parseInt($('body').attr('data-pow-difficulty'));
202 var powDifficulty = parseInt($('body').attr('data-pow-difficulty'));
133 if (powDifficulty > 0 && typeof SharedWorker != 'undefined') {
203 if (powDifficulty > 0 && typeof SharedWorker != 'undefined') {
134 var worker = new SharedWorker($('.post-form').attr('data-pow-script'));
204 var worker = new SharedWorker($('.post-form').attr('data-pow-script'));
135 worker.port.onmessage = function(e) {
205 worker.port.onmessage = function(e) {
136 var form = $('#form');
206 var form = $('#form');
137 addHiddenInput(form, 'timestamp', e.data.timestamp);
207 addHiddenInput(form, 'timestamp', e.data.timestamp);
138 addHiddenInput(form, 'iteration', e.data.iteration);
208 addHiddenInput(form, 'iteration', e.data.iteration);
139 addHiddenInput(form, 'guess', e.data.guess);
209 addHiddenInput(form, 'guess', e.data.guess);
140
210
141 form.submit();
211 form.submit();
142 $('.post-form-w').unblock();
212 $('.post-form-w').unblock();
143 };
213 };
144 worker.onerror = function(event){
214 worker.onerror = function(event){
145 throw new Error(event.message + " (" + event.filename + ":" + event.lineno + ")");
215 throw new Error(event.message + " (" + event.filename + ":" + event.lineno + ")");
146 };
216 };
147 worker.port.start();
217 worker.port.start();
148
218
149 var form = $('#form');
219 var form = $('#form');
150 var submitButton = form.find('input[type=submit]');
220 var submitButton = form.find('input[type=submit]');
151 submitButton.click(function() {
221 submitButton.click(function() {
152 showAsErrors(form, gettext('Computing PoW...'));
222 showAsErrors(form, gettext('Computing PoW...'));
153 $('.post-form-w').block({ message: gettext('Computing PoW...') })
223 $('.post-form-w').block({ message: gettext('Computing PoW...') })
154
224
155 var msg = $('textarea#id_text').val().trim();
225 var msg = $('textarea#id_text').val().trim();
156
226
157 var data = {
227 var data = {
158 msg: msg,
228 msg: msg,
159 difficulty: parseInt($('body').attr('data-pow-difficulty')),
229 difficulty: parseInt($('body').attr('data-pow-difficulty')),
160 hasher: $('.post-form').attr('data-hasher')
230 hasher: $('.post-form').attr('data-hasher')
161 };
231 };
162 worker.port.postMessage(data);
232 worker.port.postMessage(data);
163
233
164 return false;
234 return false;
165 });
235 });
166 }
236 }
167
237
168 var $fileSourceButton = $('#file-source-button');
238 var $fileSourceButton = $('#file-source-button');
169 if (window.localStorage) {
239 if (window.localStorage) {
170 var source = localStorage.getItem(ITEM_FILE_SOURCE);
240 var source = localStorage.getItem(ITEM_FILE_SOURCE);
171 if (source == null) {
241 if (source == null) {
172 source = 'file';
242 source = 'file';
173 }
243 }
174 if (source == 'file') {
244 if (source == 'file') {
175 $('#id_file_url').parent().parent().hide();
245 $('#id_file_url').parent().parent().hide();
176 } else {
246 } else {
177 $('#id_file').parent().parent().hide();
247 $('#id_file').parent().parent().hide();
178 }
248 }
179
249
180 $fileSourceButton.click(function() {
250 $fileSourceButton.click(function() {
181 selectFileChoice();
251 selectFileChoice();
182 });
252 });
183 } else {
253 } else {
184 $fileSourceButton.hide();
254 $fileSourceButton.hide();
185 }
255 }
186
256
187 addOnImagePaste();
257 addOnImagePaste();
188
258
189 // Stickers autocomplete
259 // Stickers autocomplete
190 function split( val ) {
260 function split( val ) {
191 return val.split(URL_DELIMITER);
261 return val.split(URL_DELIMITER);
192 }
262 }
193
263
194 function extractLast( term ) {
264 function extractLast( term ) {
195 return split(term).pop();
265 return split(term).pop();
196 }
266 }
197
267
198 $('#id_file_1').autocomplete({
268 $('#id_file_1').autocomplete({
199 source: function( request, response ) {
269 source: function( request, response ) {
200 $.getJSON(URL_STICKERS, {
270 $.getJSON(URL_STICKERS, {
201 term: extractLast( request.term )
271 term: extractLast( request.term )
202 }, response);
272 }, response);
203 },
273 },
204 search: function() {
274 search: function() {
205 // custom minLength
275 // custom minLength
206 var term = extractLast( this.value );
276 var term = extractLast( this.value );
207 if (term.length < MIN_INPUT_LENGTH) {
277 if (term.length < MIN_INPUT_LENGTH) {
208 return false;
278 return false;
209 }
279 }
210 },
280 },
211 focus: function() {
281 focus: function() {
212 // prevent value inserted on focus
282 // prevent value inserted on focus
213 return false;
283 return false;
214 },
284 },
215 select: function( event, ui ) {
285 select: function( event, ui ) {
216 var terms = split( this.value );
286 var terms = split( this.value );
217 // remove the current input
287 // remove the current input
218 terms.pop();
288 terms.pop();
219 // add the selected item
289 // add the selected item
220 terms.push( ui.item.alias );
290 terms.push( ui.item.alias );
221 // add placeholder to get the comma-and-space at the end
291 // add placeholder to get the comma-and-space at the end
222 terms.push("");
292 terms.push("");
223 this.value = terms.join(URL_DELIMITER);
293 this.value = terms.join(URL_DELIMITER);
224 return false;
294 return false;
225 }
295 }
226 })
296 })
227 .autocomplete( "instance" )._renderItem = function( ul, item ) {
297 .autocomplete( "instance" )._renderItem = function( ul, item ) {
228 return $( "<li>" )
298 return $( "<li>" )
229 .append( "<div>" + '<img src="' + item.thumb + '">' + '<br />' + item.alias + "</div>" )
299 .append( "<div>" + '<img src="' + item.thumb + '">' + '<br />' + item.alias + "</div>" )
230 .appendTo( ul );
300 .appendTo( ul );
231 };
301 };
232 });
302 });
@@ -1,43 +1,45 b''
1 var MIN_INPUT_LENGTH = 2;
1 var MIN_INPUT_LENGTH = 2;
2 var URL_TAGS = '/api/tags';
2 var URL_TAGS = '/api/tags';
3 var TAG_DELIMITER = ' ';
3 var TAG_DELIMITER = ' ';
4
4
5 function split( val ) {
5 function split( val ) {
6 return val.split(TAG_DELIMITER);
6 return val.split(TAG_DELIMITER);
7 }
7 }
8
8
9 function extractLast( term ) {
9 function extractLast( term ) {
10 return split( term ).pop();
10 return split( term ).pop();
11 }
11 }
12
12
13 $( document ).ready(function() {
13 $( document ).ready(function() {
14 $('#id_tags').autocomplete({
14 $('#id_tags').autocomplete({
15 source: function( request, response ) {
15 source: function( request, response ) {
16 $.getJSON(URL_TAGS, {
16 $.getJSON(URL_TAGS, {
17 term: extractLast( request.term )
17 term: extractLast( request.term )
18 }, response);
18 }, response);
19 },
19 },
20 search: function() {
20 search: function() {
21 // custom minLength
21 // custom minLength
22 var term = extractLast( this.value );
22 var term = extractLast( this.value );
23 if (term.length < MIN_INPUT_LENGTH) {
23 if (term.length < MIN_INPUT_LENGTH) {
24 return false;
24 return false;
25 }
25 }
26 },
26 },
27 focus: function() {
27 focus: function() {
28 // prevent value inserted on focus
28 // prevent value inserted on focus
29 return false;
29 return false;
30 },
30 },
31 select: function( event, ui ) {
31 select: function( event, ui ) {
32 var terms = split( this.value );
32 var terms = split( this.value );
33 // remove the current input
33 // remove the current input
34 terms.pop();
34 terms.pop();
35 // add the selected item
35 // add the selected item
36 terms.push( ui.item.value );
36 terms.push( ui.item.value );
37 // add placeholder to get the comma-and-space at the end
37 // add placeholder to get the comma-and-space at the end
38 terms.push("");
38 terms.push("");
39 this.value = terms.join(TAG_DELIMITER);
39 this.value = terms.join(TAG_DELIMITER);
40 return false;
40 return false;
41 }
41 }
42 });
42 });
43
44 initAjaxForm(null);
43 }); No newline at end of file
45 });
@@ -1,395 +1,340 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-2014 neko259
6 Copyright (C) 2013-2014 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 CLASS_POST = '.post';
26 var CLASS_POST = '.post';
27
27
28 var POST_ADDED = 0;
28 var POST_ADDED = 0;
29 var POST_UPDATED = 1;
29 var POST_UPDATED = 1;
30
30
31 // TODO These need to be syncronized with board settings.
31 // TODO These need to be syncronized with board settings.
32 var JS_AUTOUPDATE_PERIOD = 20000;
32 var JS_AUTOUPDATE_PERIOD = 20000;
33 // TODO This needs to be the same for attachment download time limit.
34 var POST_AJAX_TIMEOUT = 30000;
35 var BLINK_SPEED = 500;
33 var BLINK_SPEED = 500;
36
34
37 var ALLOWED_FOR_PARTIAL_UPDATE = [
35 var ALLOWED_FOR_PARTIAL_UPDATE = [
38 'refmap',
36 'refmap',
39 'post-info'
37 'post-info'
40 ];
38 ];
41
39
42 var ATTR_CLASS = 'class';
40 var ATTR_CLASS = 'class';
43 var ATTR_UID = 'data-uid';
41 var ATTR_UID = 'data-uid';
44
42
45 var unreadPosts = 0;
43 var unreadPosts = 0;
46 var documentOriginalTitle = '';
44 var documentOriginalTitle = '';
47
45
48 // Thread ID does not change, can be stored one time
46 // Thread ID does not change, can be stored one time
49 var threadId = $('div.thread').children(CLASS_POST).first().attr('id');
47 var threadId = $('div.thread').children(CLASS_POST).first().attr('id');
50 var blinkColor = $('<div class="post-blink"></div>').css('background-color');
48 var blinkColor = $('<div class="post-blink"></div>').css('background-color');
51
49
52 /**
50 /**
53 * Get diff of the posts from the current thread timestamp.
51 * Get diff of the posts from the current thread timestamp.
54 * This is required if the browser was closed and some post updates were
52 * This is required if the browser was closed and some post updates were
55 * missed.
53 * missed.
56 */
54 */
57 function getThreadDiff() {
55 function getThreadDiff() {
58 var all_posts = $('.post');
56 var all_posts = $('.post');
59
57
60 var uids = '';
58 var uids = '';
61 var posts = all_posts;
59 var posts = all_posts;
62 for (var i = 0; i < posts.length; i++) {
60 for (var i = 0; i < posts.length; i++) {
63 uids += posts[i].getAttribute('data-uid') + ' ';
61 uids += posts[i].getAttribute('data-uid') + ' ';
64 }
62 }
65
63
66 var data = {
64 var data = {
67 uids: uids,
65 uids: uids,
68 thread: threadId
66 thread: threadId
69 };
67 };
70
68
71 var diffUrl = '/api/diff_thread/';
69 var diffUrl = '/api/diff_thread/';
72
70
73 $.post(diffUrl,
71 $.post(diffUrl,
74 data,
72 data,
75 function(data) {
73 function(data) {
76 var updatedPosts = data.updated;
74 var updatedPosts = data.updated;
77 var addedPostCount = 0;
75 var addedPostCount = 0;
78
76
79 for (var i = 0; i < updatedPosts.length; i++) {
77 for (var i = 0; i < updatedPosts.length; i++) {
80 var postText = updatedPosts[i];
78 var postText = updatedPosts[i];
81 var post = $(postText);
79 var post = $(postText);
82
80
83 if (updatePost(post) == POST_ADDED) {
81 if (updatePost(post) == POST_ADDED) {
84 addedPostCount++;
82 addedPostCount++;
85 }
83 }
86 }
84 }
87
85
88 var hasMetaUpdates = updatedPosts.length > 0;
86 var hasMetaUpdates = updatedPosts.length > 0;
89 if (hasMetaUpdates) {
87 if (hasMetaUpdates) {
90 updateMetadataPanel();
88 updateMetadataPanel();
91 }
89 }
92
90
93 if (addedPostCount > 0) {
91 if (addedPostCount > 0) {
94 updateBumplimitProgress(addedPostCount);
92 updateBumplimitProgress(addedPostCount);
95 }
93 }
96
94
97 if (updatedPosts.length > 0) {
95 if (updatedPosts.length > 0) {
98 showNewPostsTitle(addedPostCount);
96 showNewPostsTitle(addedPostCount);
99 }
97 }
100
98
101 // TODO Process removed posts if any
99 // TODO Process removed posts if any
102 $('.metapanel').attr('data-last-update', data.last_update);
100 $('.metapanel').attr('data-last-update', data.last_update);
103
101
104 if (data.subscribed == 'True') {
102 if (data.subscribed == 'True') {
105 var favButton = $('#thread-fav-button .not_fav');
103 var favButton = $('#thread-fav-button .not_fav');
106
104
107 if (favButton.length > 0) {
105 if (favButton.length > 0) {
108 favButton.attr('value', 'unsubscribe');
106 favButton.attr('value', 'unsubscribe');
109 favButton.removeClass('not_fav');
107 favButton.removeClass('not_fav');
110 favButton.addClass('fav');
108 favButton.addClass('fav');
111 }
109 }
112 }
110 }
113 },
111 },
114 'json'
112 'json'
115 )
113 )
116 }
114 }
117
115
118 /**
116 /**
119 * Add or update the post on html page.
117 * Add or update the post on html page.
120 */
118 */
121 function updatePost(postHtml) {
119 function updatePost(postHtml) {
122 // This needs to be set on start because the page is scrolled after posts
120 // This needs to be set on start because the page is scrolled after posts
123 // are added or updated
121 // are added or updated
124 var bottom = isPageBottom();
122 var bottom = isPageBottom();
125
123
126 var post = $(postHtml);
124 var post = $(postHtml);
127
125
128 var threadBlock = $('div.thread');
126 var threadBlock = $('div.thread');
129
127
130 var postId = post.attr('id');
128 var postId = post.attr('id');
131
129
132 // If the post already exists, replace it. Otherwise add as a new one.
130 // If the post already exists, replace it. Otherwise add as a new one.
133 var existingPosts = threadBlock.children('.post[id=' + postId + ']');
131 var existingPosts = threadBlock.children('.post[id=' + postId + ']');
134
132
135 var type;
133 var type;
136
134
137 if (existingPosts.size() > 0) {
135 if (existingPosts.size() > 0) {
138 replacePartial(existingPosts.first(), post, false);
136 replacePartial(existingPosts.first(), post, false);
139 post = existingPosts.first();
137 post = existingPosts.first();
140
138
141 type = POST_UPDATED;
139 type = POST_UPDATED;
142 } else {
140 } else {
143 post.appendTo(threadBlock);
141 post.appendTo(threadBlock);
144
142
145 if (bottom) {
143 if (bottom) {
146 scrollToBottom();
144 scrollToBottom();
147 }
145 }
148
146
149 type = POST_ADDED;
147 type = POST_ADDED;
150 }
148 }
151
149
152 processNewPost(post);
150 processNewPost(post);
153
151
154 return type;
152 return type;
155 }
153 }
156
154
157 /**
155 /**
158 * Initiate a blinking animation on a node to show it was updated.
156 * Initiate a blinking animation on a node to show it was updated.
159 */
157 */
160 function blink(node) {
158 function blink(node) {
161 node.effect('highlight', { color: blinkColor }, BLINK_SPEED);
159 node.effect('highlight', { color: blinkColor }, BLINK_SPEED);
162 }
160 }
163
161
164 function isPageBottom() {
162 function isPageBottom() {
165 var scroll = $(window).scrollTop() / ($(document).height()
163 var scroll = $(window).scrollTop() / ($(document).height()
166 - $(window).height());
164 - $(window).height());
167
165
168 return scroll == 1
166 return scroll == 1
169 }
167 }
170
168
171 function enableJsUpdate() {
169 function enableJsUpdate() {
172 setInterval(getThreadDiff, JS_AUTOUPDATE_PERIOD);
170 setInterval(getThreadDiff, JS_AUTOUPDATE_PERIOD);
173 return true;
171 return true;
174 }
172 }
175
173
176 function initAutoupdate() {
174 function initAutoupdate() {
177 return enableJsUpdate();
175 return enableJsUpdate();
178 }
176 }
179
177
180 function getReplyCount() {
178 function getReplyCount() {
181 return $('.thread').children(CLASS_POST).length
179 return $('.thread').children(CLASS_POST).length
182 }
180 }
183
181
184 function getImageCount() {
182 function getImageCount() {
185 return $('.thread').find('.image').length
183 return $('.thread').find('.image').length
186 }
184 }
187
185
188 /**
186 /**
189 * Update post count, images count and last update time in the metadata
187 * Update post count, images count and last update time in the metadata
190 * panel.
188 * panel.
191 */
189 */
192 function updateMetadataPanel() {
190 function updateMetadataPanel() {
193 var replyCountField = $('#reply-count');
191 var replyCountField = $('#reply-count');
194 var imageCountField = $('#image-count');
192 var imageCountField = $('#image-count');
195
193
196 var replyCount = getReplyCount();
194 var replyCount = getReplyCount();
197 replyCountField.text(replyCount);
195 replyCountField.text(replyCount);
198 var imageCount = getImageCount();
196 var imageCount = getImageCount();
199 imageCountField.text(imageCount);
197 imageCountField.text(imageCount);
200
198
201 var lastUpdate = $('.post:last').children('.post-info').first()
199 var lastUpdate = $('.post:last').children('.post-info').first()
202 .children('.pub_time').first().html();
200 .children('.pub_time').first().html();
203 if (lastUpdate !== '') {
201 if (lastUpdate !== '') {
204 var lastUpdateField = $('#last-update');
202 var lastUpdateField = $('#last-update');
205 lastUpdateField.html(lastUpdate);
203 lastUpdateField.html(lastUpdate);
206 blink(lastUpdateField);
204 blink(lastUpdateField);
207 }
205 }
208
206
209 blink(replyCountField);
207 blink(replyCountField);
210 blink(imageCountField);
208 blink(imageCountField);
211 }
209 }
212
210
213 /**
211 /**
214 * Update bumplimit progress bar
212 * Update bumplimit progress bar
215 */
213 */
216 function updateBumplimitProgress(postDelta) {
214 function updateBumplimitProgress(postDelta) {
217 var progressBar = $('#bumplimit_progress');
215 var progressBar = $('#bumplimit_progress');
218 if (progressBar) {
216 if (progressBar) {
219 var postsToLimitElement = $('#left_to_limit');
217 var postsToLimitElement = $('#left_to_limit');
220
218
221 var oldPostsToLimit = parseInt(postsToLimitElement.text());
219 var oldPostsToLimit = parseInt(postsToLimitElement.text());
222 var postCount = getReplyCount();
220 var postCount = getReplyCount();
223 var bumplimit = postCount - postDelta + oldPostsToLimit;
221 var bumplimit = postCount - postDelta + oldPostsToLimit;
224
222
225 var newPostsToLimit = bumplimit - postCount;
223 var newPostsToLimit = bumplimit - postCount;
226 if (newPostsToLimit <= 0) {
224 if (newPostsToLimit <= 0) {
227 $('.bar-bg').remove();
225 $('.bar-bg').remove();
228 } else {
226 } else {
229 postsToLimitElement.text(newPostsToLimit);
227 postsToLimitElement.text(newPostsToLimit);
230 progressBar.width((100 - postCount / bumplimit * 100.0) + '%');
228 progressBar.width((100 - postCount / bumplimit * 100.0) + '%');
231 }
229 }
232 }
230 }
233 }
231 }
234
232
235 /**
233 /**
236 * Show 'new posts' text in the title if the document is not visible to a user
234 * Show 'new posts' text in the title if the document is not visible to a user
237 */
235 */
238 function showNewPostsTitle(newPostCount) {
236 function showNewPostsTitle(newPostCount) {
239 if (document.hidden) {
237 if (document.hidden) {
240 if (documentOriginalTitle === '') {
238 if (documentOriginalTitle === '') {
241 documentOriginalTitle = document.title;
239 documentOriginalTitle = document.title;
242 }
240 }
243 unreadPosts = unreadPosts + newPostCount;
241 unreadPosts = unreadPosts + newPostCount;
244
242
245 var newTitle = null;
243 var newTitle = null;
246 if (unreadPosts > 0) {
244 if (unreadPosts > 0) {
247 newTitle = '[' + unreadPosts + '] ';
245 newTitle = '[' + unreadPosts + '] ';
248 } else {
246 } else {
249 newTitle = '* ';
247 newTitle = '* ';
250 }
248 }
251 newTitle += documentOriginalTitle;
249 newTitle += documentOriginalTitle;
252
250
253 document.title = newTitle;
251 document.title = newTitle;
254
252
255 document.addEventListener('visibilitychange', function() {
253 document.addEventListener('visibilitychange', function() {
256 if (documentOriginalTitle !== '') {
254 if (documentOriginalTitle !== '') {
257 document.title = documentOriginalTitle;
255 document.title = documentOriginalTitle;
258 documentOriginalTitle = '';
256 documentOriginalTitle = '';
259 unreadPosts = 0;
257 unreadPosts = 0;
260 }
258 }
261
259
262 document.removeEventListener('visibilitychange', null);
260 document.removeEventListener('visibilitychange', null);
263 });
261 });
264 }
262 }
265 }
263 }
266
264
267
268 /**
269 * When the form is posted, this method will be run as a callback
270 */
271 function updateOnPost(response, statusText, xhr, form) {
272 var json = $.parseJSON(response);
273 var status = json.status;
274
275 showAsErrors(form, '');
276 $('.post-form-w').unblock();
277
278 if (status === 'ok') {
279 resetForm();
280 getThreadDiff();
281 scrollToBottom();
282 } else {
283 var errors = json.errors;
284 for (var i = 0; i < errors.length; i++) {
285 var fieldErrors = errors[i];
286
287 var error = fieldErrors.errors;
288
289 showAsErrors(form, error);
290 }
291 }
292 }
293
294
295 /**
265 /**
296 * Run js methods that are usually run on the document, on the new post
266 * Run js methods that are usually run on the document, on the new post
297 */
267 */
298 function processNewPost(post) {
268 function processNewPost(post) {
299 addScriptsToPost(post);
269 addScriptsToPost(post);
300 blink(post);
270 blink(post);
301 }
271 }
302
272
303 function replacePartial(oldNode, newNode, recursive) {
273 function replacePartial(oldNode, newNode, recursive) {
304 if (!equalNodes(oldNode, newNode)) {
274 if (!equalNodes(oldNode, newNode)) {
305 // Update parent node attributes
275 // Update parent node attributes
306 updateNodeAttr(oldNode, newNode, ATTR_CLASS);
276 updateNodeAttr(oldNode, newNode, ATTR_CLASS);
307 updateNodeAttr(oldNode, newNode, ATTR_UID);
277 updateNodeAttr(oldNode, newNode, ATTR_UID);
308
278
309 // Replace children
279 // Replace children
310 var children = oldNode.children();
280 var children = oldNode.children();
311 if (children.length == 0) {
281 if (children.length == 0) {
312 oldNode.replaceWith(newNode);
282 oldNode.replaceWith(newNode);
313 } else {
283 } else {
314 var newChildren = newNode.children();
284 var newChildren = newNode.children();
315 newChildren.each(function(i) {
285 newChildren.each(function(i) {
316 var newChild = newChildren.eq(i);
286 var newChild = newChildren.eq(i);
317 var newChildClass = newChild.attr(ATTR_CLASS);
287 var newChildClass = newChild.attr(ATTR_CLASS);
318
288
319 // Update only certain allowed blocks (e.g. not images)
289 // Update only certain allowed blocks (e.g. not images)
320 if (ALLOWED_FOR_PARTIAL_UPDATE.indexOf(newChildClass) > -1) {
290 if (ALLOWED_FOR_PARTIAL_UPDATE.indexOf(newChildClass) > -1) {
321 var oldChild = oldNode.children('.' + newChildClass);
291 var oldChild = oldNode.children('.' + newChildClass);
322
292
323 if (oldChild.length == 0) {
293 if (oldChild.length == 0) {
324 oldNode.append(newChild);
294 oldNode.append(newChild);
325 } else {
295 } else {
326 if (!equalNodes(oldChild, newChild)) {
296 if (!equalNodes(oldChild, newChild)) {
327 if (recursive) {
297 if (recursive) {
328 replacePartial(oldChild, newChild, false);
298 replacePartial(oldChild, newChild, false);
329 } else {
299 } else {
330 oldChild.replaceWith(newChild);
300 oldChild.replaceWith(newChild);
331 }
301 }
332 }
302 }
333 }
303 }
334 }
304 }
335 });
305 });
336 }
306 }
337 }
307 }
338 }
308 }
339
309
340 /**
310 /**
341 * Compare nodes by content
311 * Compare nodes by content
342 */
312 */
343 function equalNodes(node1, node2) {
313 function equalNodes(node1, node2) {
344 return node1[0].outerHTML == node2[0].outerHTML;
314 return node1[0].outerHTML == node2[0].outerHTML;
345 }
315 }
346
316
347 /**
317 /**
348 * Update attribute of a node if it has changed
318 * Update attribute of a node if it has changed
349 */
319 */
350 function updateNodeAttr(oldNode, newNode, attrName) {
320 function updateNodeAttr(oldNode, newNode, attrName) {
351 var oldAttr = oldNode.attr(attrName);
321 var oldAttr = oldNode.attr(attrName);
352 var newAttr = newNode.attr(attrName);
322 var newAttr = newNode.attr(attrName);
353 if (oldAttr != newAttr) {
323 if (oldAttr != newAttr) {
354 oldNode.attr(attrName, newAttr);
324 oldNode.attr(attrName, newAttr);
355 }
325 }
356 }
326 }
357
327
358 $(document).ready(function() {
328 $(document).ready(function() {
359 if (initAutoupdate()) {
329 if (initAutoupdate()) {
360 // Post form data over AJAX
330 // Post form data over AJAX
361 var threadId = $('div.thread').children('.post').first().attr('id');
331 var threadId = $('div.thread').children('.post').first().attr('id');
362
332
363 var form = $('#form');
333 var form = $('#form');
364
334
335 initAjaxForm(threadId);
365 if (form.length > 0) {
336 if (form.length > 0) {
366 var options = {
367 beforeSubmit: function(arr, form, options) {
368 $('.post-form-w').block({ message: gettext('Sending message...') });
369
370 $.each(pastedImages, function(i, blob) {
371 arr.push({
372 name: "file_0",
373 value: blob
374 });
375 });
376 },
377 success: updateOnPost,
378 error: function(xhr, textStatus, errorString) {
379 var errorText = gettext('Server error: ') + textStatus;
380 if (errorString) {
381 errorText += ' / ' + errorString;
382 }
383 showAsErrors(form, errorText);
384 $('.post-form-w').unblock();
385 },
386 url: '/api/add_post/' + threadId + '/',
387 timeout: POST_AJAX_TIMEOUT
388 };
389
390 form.ajaxForm(options);
391
392 resetForm();
337 resetForm();
393 }
338 }
394 }
339 }
395 });
340 });
@@ -1,210 +1,211 b''
1 {% extends "boards/paginated.html" %}
1 {% extends "boards/paginated.html" %}
2
2
3 {% load i18n %}
3 {% load i18n %}
4 {% load board %}
4 {% load board %}
5 {% load static %}
5 {% load static %}
6 {% load tz %}
6 {% load tz %}
7
7
8 {% block head %}
8 {% block head %}
9 <meta name="robots" content="noindex">
9 <meta name="robots" content="noindex">
10
10
11 {% if tag %}
11 {% if tag %}
12 <title>{{ tag.get_localized_name }} - {{ site_name }}</title>
12 <title>{{ tag.get_localized_name }} - {{ site_name }}</title>
13 {% else %}
13 {% else %}
14 <title>{{ site_name }}</title>
14 <title>{{ site_name }}</title>
15 {% endif %}
15 {% endif %}
16
16
17 {% if prev_page_link %}
17 {% if prev_page_link %}
18 <link rel="prev" href="{{ prev_page_link|safe }}" />
18 <link rel="prev" href="{{ prev_page_link|safe }}" />
19 {% endif %}
19 {% endif %}
20 {% if next_page_link %}
20 {% if next_page_link %}
21 <link rel="next" href="{{ next_page_link|safe }}" />
21 <link rel="next" href="{{ next_page_link|safe }}" />
22 {% endif %}
22 {% endif %}
23
23
24 {% endblock %}
24 {% endblock %}
25
25
26 {% block content %}
26 {% block content %}
27
27
28 {% get_current_language as LANGUAGE_CODE %}
28 {% get_current_language as LANGUAGE_CODE %}
29 {% get_current_timezone as TIME_ZONE %}
29 {% get_current_timezone as TIME_ZONE %}
30
30
31 {% for banner in banners %}
31 {% for banner in banners %}
32 <div class="post">
32 <div class="post">
33 <div class="title">{{ banner.title }}</div>
33 <div class="title">{{ banner.title }}</div>
34 <div>{{ banner.get_text|safe }}</div>
34 <div>{{ banner.get_text|safe }}</div>
35 <div>{% trans 'Details' %}: <a href="{{ banner.post.get_absolute_url|safe }}">>>{{ banner.post.id }}</a></div>
35 <div>{% trans 'Details' %}: <a href="{{ banner.post.get_absolute_url|safe }}">>>{{ banner.post.id }}</a></div>
36 </div>
36 </div>
37 {% endfor %}
37 {% endfor %}
38
38
39 {% if tag %}
39 {% if tag %}
40 <div class="tag_info" style="border-bottom: solid .5ex #{{ tag.get_color }}">
40 <div class="tag_info" style="border-bottom: solid .5ex #{{ tag.get_color }}">
41 {% if random_image_post %}
41 {% if random_image_post %}
42 <div class="tag-image">
42 <div class="tag-image">
43 {% with image=random_image_post.get_first_image %}
43 {% with image=random_image_post.get_first_image %}
44 <a href="{{ random_image_post.get_absolute_url|safe }}"><img
44 <a href="{{ random_image_post.get_absolute_url|safe }}"><img
45 src="{{ image.get_thumb_url|safe }}"
45 src="{{ image.get_thumb_url|safe }}"
46 width="{{ image.get_preview_size.0 }}"
46 width="{{ image.get_preview_size.0 }}"
47 height="{{ image.get_preview_size.1 }}"
47 height="{{ image.get_preview_size.1 }}"
48 alt="{{ random_image_post.id }}"/></a>
48 alt="{{ random_image_post.id }}"/></a>
49 {% endwith %}
49 {% endwith %}
50 </div>
50 </div>
51 {% endif %}
51 {% endif %}
52 <div class="tag-text-data">
52 <div class="tag-text-data">
53 <h2>
53 <h2>
54 /{{ tag.get_view|safe }}/
54 /{{ tag.get_view|safe }}/
55 </h2>
55 </h2>
56 {% if perms.change_tag %}
56 {% if perms.change_tag %}
57 <div class="moderator_info"><a href="{% url 'admin:boards_tag_change' tag.id %}">{% trans 'Edit tag' %}</a></div>
57 <div class="moderator_info"><a href="{% url 'admin:boards_tag_change' tag.id %}">{% trans 'Edit tag' %}</a></div>
58 {% endif %}
58 {% endif %}
59 <p>
59 <p>
60 <form action="{% url 'tag' tag.get_name %}" method="post" class="post-button-form">
60 <form action="{% url 'tag' tag.get_name %}" method="post" class="post-button-form">
61 {% if is_favorite %}
61 {% if is_favorite %}
62 <button name="method" value="unsubscribe" class="fav">β˜… {% trans "Remove from favorites" %}</button>
62 <button name="method" value="unsubscribe" class="fav">β˜… {% trans "Remove from favorites" %}</button>
63 {% else %}
63 {% else %}
64 <button name="method" value="subscribe" class="not_fav">β˜… {% trans "Add to favorites" %}</button>
64 <button name="method" value="subscribe" class="not_fav">β˜… {% trans "Add to favorites" %}</button>
65 {% endif %}
65 {% endif %}
66 </form>
66 </form>
67 &bull;
67 &bull;
68 <form action="{% url 'tag' tag.get_name %}" method="post" class="post-button-form">
68 <form action="{% url 'tag' tag.get_name %}" method="post" class="post-button-form">
69 {% if is_hidden %}
69 {% if is_hidden %}
70 <button name="method" value="unhide" class="fav">{% trans "Show" %}</button>
70 <button name="method" value="unhide" class="fav">{% trans "Show" %}</button>
71 {% else %}
71 {% else %}
72 <button name="method" value="hide" class="not_fav">{% trans "Hide" %}</button>
72 <button name="method" value="hide" class="not_fav">{% trans "Hide" %}</button>
73 {% endif %}
73 {% endif %}
74 </form>
74 </form>
75 &bull;
75 &bull;
76 <a href="{% url 'tag_gallery' tag.get_name %}">{% trans 'Gallery' %}</a>
76 <a href="{% url 'tag_gallery' tag.get_name %}">{% trans 'Gallery' %}</a>
77 </p>
77 </p>
78 {% if tag.get_description %}
78 {% if tag.get_description %}
79 <p>{{ tag.get_description|safe }}</p>
79 <p>{{ tag.get_description|safe }}</p>
80 {% endif %}
80 {% endif %}
81 <p>
81 <p>
82 {% with active_count=tag.get_active_thread_count bumplimit_count=tag.get_bumplimit_thread_count archived_count=tag.get_archived_thread_count %}
82 {% with active_count=tag.get_active_thread_count bumplimit_count=tag.get_bumplimit_thread_count archived_count=tag.get_archived_thread_count %}
83 {% if active_count %}
83 {% if active_count %}
84 ● {{ active_count }}&ensp;
84 ● {{ active_count }}&ensp;
85 {% endif %}
85 {% endif %}
86 {% if bumplimit_count %}
86 {% if bumplimit_count %}
87 ◍ {{ bumplimit_count }}&ensp;
87 ◍ {{ bumplimit_count }}&ensp;
88 {% endif %}
88 {% endif %}
89 {% if archived_count %}
89 {% if archived_count %}
90 β—‹ {{ archived_count }}&ensp;
90 β—‹ {{ archived_count }}&ensp;
91 {% endif %}
91 {% endif %}
92 {% endwith %}
92 {% endwith %}
93 β™₯ {{ tag.get_post_count }}
93 β™₯ {{ tag.get_post_count }}
94 </p>
94 </p>
95 {% if tag.get_all_parents %}
95 {% if tag.get_all_parents %}
96 <p>
96 <p>
97 {% for parent in tag.get_all_parents %}
97 {% for parent in tag.get_all_parents %}
98 {{ parent.get_view|safe }} &gt;
98 {{ parent.get_view|safe }} &gt;
99 {% endfor %}
99 {% endfor %}
100 {{ tag.get_view|safe }}
100 {{ tag.get_view|safe }}
101 </p>
101 </p>
102 {% endif %}
102 {% endif %}
103 {% if tag.get_children.all %}
103 {% if tag.get_children.all %}
104 <p>
104 <p>
105 {% trans "Subsections: " %}
105 {% trans "Subsections: " %}
106 {% for child in tag.get_children.all %}
106 {% for child in tag.get_children.all %}
107 {{ child.get_view|safe }}{% if not forloop.last%}, {% endif %}
107 {{ child.get_view|safe }}{% if not forloop.last%}, {% endif %}
108 {% endfor %}
108 {% endfor %}
109 </p>
109 </p>
110 {% endif %}
110 {% endif %}
111 </div>
111 </div>
112 </div>
112 </div>
113 {% endif %}
113 {% endif %}
114
114
115 {% if threads %}
115 {% if threads %}
116 {% if prev_page_link %}
116 {% if prev_page_link %}
117 <div class="page_link">
117 <div class="page_link">
118 <a href="{{ prev_page_link }}">&lt;&lt; {% trans "Previous page" %} &lt;&lt;</a>
118 <a href="{{ prev_page_link }}">&lt;&lt; {% trans "Previous page" %} &lt;&lt;</a>
119 </div>
119 </div>
120 {% endif %}
120 {% endif %}
121
121
122 {% for thread in threads %}
122 {% for thread in threads %}
123 <div class="thread">
123 <div class="thread">
124 {% post_view thread.get_opening_post thread=thread truncated=True need_open_link=True %}
124 {% post_view thread.get_opening_post thread=thread truncated=True need_open_link=True %}
125 {% if not thread.archived %}
125 {% if not thread.archived %}
126 {% with last_replies=thread.get_last_replies %}
126 {% with last_replies=thread.get_last_replies %}
127 {% if last_replies %}
127 {% if last_replies %}
128 {% with skipped_replies_count=thread.get_skipped_replies_count %}
128 {% with skipped_replies_count=thread.get_skipped_replies_count %}
129 {% if skipped_replies_count %}
129 {% if skipped_replies_count %}
130 <div class="skipped_replies">
130 <div class="skipped_replies">
131 <a href="{% url 'thread' thread.get_opening_post_id %}">
131 <a href="{% url 'thread' thread.get_opening_post_id %}">
132 {% blocktrans count count=skipped_replies_count %}Skipped {{ count }} reply. Open thread to see all replies.{% plural %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %}
132 {% blocktrans count count=skipped_replies_count %}Skipped {{ count }} reply. Open thread to see all replies.{% plural %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %}
133 </a>
133 </a>
134 </div>
134 </div>
135 {% endif %}
135 {% endif %}
136 {% endwith %}
136 {% endwith %}
137 <div class="last-replies">
137 <div class="last-replies">
138 {% for post in last_replies %}
138 {% for post in last_replies %}
139 {% post_view post truncated=True %}
139 {% post_view post truncated=True %}
140 {% endfor %}
140 {% endfor %}
141 </div>
141 </div>
142 {% endif %}
142 {% endif %}
143 {% endwith %}
143 {% endwith %}
144 {% endif %}
144 {% endif %}
145 </div>
145 </div>
146 {% endfor %}
146 {% endfor %}
147
147
148 {% if next_page_link %}
148 {% if next_page_link %}
149 <div class="page_link">
149 <div class="page_link">
150 <a href="{{ next_page_link }}">&gt;&gt; {% trans "Next page" %} &gt;&gt;</a>
150 <a href="{{ next_page_link }}">&gt;&gt; {% trans "Next page" %} &gt;&gt;</a>
151 </div>
151 </div>
152 {% endif %}
152 {% endif %}
153 {% else %}
153 {% else %}
154 <div class="post">
154 <div class="post">
155 {% trans 'No threads exist. Create the first one!' %}</div>
155 {% trans 'No threads exist. Create the first one!' %}</div>
156 {% endif %}
156 {% endif %}
157
157
158 <div class="post-form-w">
158 <div class="post-form-w">
159 <script src="{% static 'js/panel.js' %}"></script>
159 <script src="{% static 'js/panel.js' %}"></script>
160 <div class="post-form" data-hasher="{% static 'js/3party/sha256.js' %}"
160 <div class="post-form" data-hasher="{% static 'js/3party/sha256.js' %}"
161 data-pow-script="{% static 'js/proof_of_work.js' %}">
161 data-pow-script="{% static 'js/proof_of_work.js' %}">
162 <div class="form-title">{% trans "Create new thread" %}</div>
162 <div class="form-title">{% trans "Create new thread" %}</div>
163 <div class="swappable-form-full">
163 <div class="swappable-form-full">
164 <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %}
164 <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %}
165 {{ form.as_div }}
165 {{ form.as_div }}
166 <div class="form-submit">
166 <div class="form-submit">
167 <input type="submit" value="{% trans "Post" %}"/>
167 <input type="submit" value="{% trans "Post" %}"/>
168 <button id="preview-button" type="button" onclick="return false;">{% trans 'Preview' %}</button>
168 <button id="preview-button" type="button" onclick="return false;">{% trans 'Preview' %}</button>
169 </div>
169 </div>
170 </form>
170 </form>
171 </div>
171 </div>
172 <div>
172 <div>
173 {% trans 'Tags must be delimited by spaces. Text or image is required.' %}
173 {% trans 'Tags must be delimited by spaces. Text or image is required.' %}
174 {% with size=max_file_size|filesizeformat %}
174 {% with size=max_file_size|filesizeformat %}
175 {% blocktrans %}Max total file size is {{ size }}.{% endblocktrans %}
175 {% blocktrans %}Max total file size is {{ size }}.{% endblocktrans %}
176 {% endwith %}
176 {% endwith %}
177 {% blocktrans %}Max file number is {{ max_files }}.{% endblocktrans %}
177 {% blocktrans %}Max file number is {{ max_files }}.{% endblocktrans %}
178 </div>
178 </div>
179 <div id="preview-text"></div>
179 <div id="preview-text"></div>
180 <div><a href="{% url "staticpage" name="help" %}">{% trans 'Help' %}</a></div>
180 <div><a href="{% url "staticpage" name="help" %}">{% trans 'Help' %}</a></div>
181 </div>
181 </div>
182 </div>
182 </div>
183
183
184 <script src="{% static 'js/form.js' %}"></script>
184 <script src="{% static 'js/form.js' %}"></script>
185 <script src="{% static 'js/3party/jquery.blockUI.js' %}"></script>
185 <script src="{% static 'js/jquery.form.min.js' %}"></script>
186 <script src="{% static 'js/3party/jquery.blockUI.js' %}"></script>
186 <script src="{% static 'js/thread_create.js' %}"></script>
187 <script src="{% static 'js/thread_create.js' %}"></script>
187
188
188 {% endblock %}
189 {% endblock %}
189
190
190 {% block metapanel %}
191 {% block metapanel %}
191
192
192 <span class="metapanel">
193 <span class="metapanel">
193 {% trans "Pages:" %}
194 {% trans "Pages:" %}
194 [
195 [
195 {% with dividers=paginator.get_dividers %}
196 {% with dividers=paginator.get_dividers %}
196 {% for page in paginator.get_divided_range %}
197 {% for page in paginator.get_divided_range %}
197 {% if page in dividers %}
198 {% if page in dividers %}
198 …,
199 …,
199 {% endif %}
200 {% endif %}
200 <a
201 <a
201 {% ifequal page current_page.number %}
202 {% ifequal page current_page.number %}
202 class="current_page"
203 class="current_page"
203 {% endifequal %}
204 {% endifequal %}
204 href="{% page_url paginator page %}">{{ page }}</a>{% if not forloop.last %},{% endif %}
205 href="{% page_url paginator page %}">{{ page }}</a>{% if not forloop.last %},{% endif %}
205 {% endfor %}
206 {% endfor %}
206 {% endwith %}
207 {% endwith %}
207 ]
208 ]
208 </span>
209 </span>
209
210
210 {% endblock %}
211 {% endblock %}
@@ -1,90 +1,91 b''
1 from django.conf.urls import url
1 from django.conf.urls import url
2 from django.urls import path
2 from django.urls import path
3 from django.views.i18n import JavaScriptCatalog
3 from django.views.i18n import JavaScriptCatalog
4
4
5 from boards import views
5 from boards import views
6 from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed
6 from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed
7 from boards.views import api, tag_threads, all_threads, settings, feed, stickers, thread, banned
7 from boards.views import api, tag_threads, all_threads, settings, feed, stickers, thread, banned
8 from boards.views.authors import AuthorsView
8 from boards.views.authors import AuthorsView
9 from boards.views.landing import LandingView
9 from boards.views.landing import LandingView
10 from boards.views.notifications import NotificationView
10 from boards.views.notifications import NotificationView
11 from boards.views.preview import PostPreviewView
11 from boards.views.preview import PostPreviewView
12 from boards.views.random import RandomImageView
12 from boards.views.random import RandomImageView
13 from boards.views.search import BoardSearchView
13 from boards.views.search import BoardSearchView
14 from boards.views.static import StaticPageView
14 from boards.views.static import StaticPageView
15 from boards.views.sync import get_post_sync_data, response_get, response_list
15 from boards.views.sync import get_post_sync_data, response_get, response_list
16 from boards.views.tag_gallery import TagGalleryView
16 from boards.views.tag_gallery import TagGalleryView
17 from boards.views.utils import UtilsView
17 from boards.views.utils import UtilsView
18
18
19
19
20 urlpatterns = [
20 urlpatterns = [
21 # /boards/
21 # /boards/
22 path('all/', all_threads.AllThreadsView.as_view(), name='index'),
22 path('all/', all_threads.AllThreadsView.as_view(), name='index'),
23
23
24 # /boards/tag/tag_name/
24 # /boards/tag/tag_name/
25 url(r'^tag/(?P<tag_name>[\w\d\']+)/$', tag_threads.TagView.as_view(),
25 url(r'^tag/(?P<tag_name>[\w\d\']+)/$', tag_threads.TagView.as_view(),
26 name='tag'),
26 name='tag'),
27 url(r'^tag/(?P<tag_name>[\w\d\']+)/gallery/$', TagGalleryView.as_view(), name='tag_gallery'),
27 url(r'^tag/(?P<tag_name>[\w\d\']+)/gallery/$', TagGalleryView.as_view(), name='tag_gallery'),
28
28
29 # /boards/thread/
29 # /boards/thread/
30 path('thread/<int:post_id>/', views.thread.NormalThreadView.as_view(),
30 path('thread/<int:post_id>/', views.thread.NormalThreadView.as_view(),
31 name='thread'),
31 name='thread'),
32 path('thread/<int:post_id>/mode/gallery/', views.thread.GalleryThreadView.as_view(),
32 path('thread/<int:post_id>/mode/gallery/', views.thread.GalleryThreadView.as_view(),
33 name='thread_gallery'),
33 name='thread_gallery'),
34 path('thread/<int:post_id>/mode/tree/', views.thread.TreeThreadView.as_view(),
34 path('thread/<int:post_id>/mode/tree/', views.thread.TreeThreadView.as_view(),
35 name='thread_tree'),
35 name='thread_tree'),
36 # /feed/
36 # /feed/
37 path('feed/', views.feed.FeedView.as_view(), name='feed'),
37 path('feed/', views.feed.FeedView.as_view(), name='feed'),
38
38
39 path('settings/', settings.SettingsView.as_view(), name='settings'),
39 path('settings/', settings.SettingsView.as_view(), name='settings'),
40 path('stickers/', stickers.AliasesView.as_view(), name='stickers'),
40 path('stickers/', stickers.AliasesView.as_view(), name='stickers'),
41 path('stickers/<str:category>/', stickers.AliasesView.as_view(), name='stickers'),
41 path('stickers/<str:category>/', stickers.AliasesView.as_view(), name='stickers'),
42 path('authors/', AuthorsView.as_view(), name='authors'),
42 path('authors/', AuthorsView.as_view(), name='authors'),
43
43
44 path('banned/', views.banned.BannedView.as_view(), name='banned'),
44 path('banned/', views.banned.BannedView.as_view(), name='banned'),
45 path('staticpage/<str:name>/', StaticPageView.as_view(), name='staticpage'),
45 path('staticpage/<str:name>/', StaticPageView.as_view(), name='staticpage'),
46
46
47 path('random/', RandomImageView.as_view(), name='random'),
47 path('random/', RandomImageView.as_view(), name='random'),
48 path('search/', BoardSearchView.as_view(), name='search'),
48 path('search/', BoardSearchView.as_view(), name='search'),
49 path('', LandingView.as_view(), name='landing'),
49 path('', LandingView.as_view(), name='landing'),
50 path('utils', UtilsView.as_view(), name='utils'),
50 path('utils', UtilsView.as_view(), name='utils'),
51
51
52 # RSS feeds
52 # RSS feeds
53 path('rss/', AllThreadsFeed()),
53 path('rss/', AllThreadsFeed()),
54 path('all/rss/', AllThreadsFeed()),
54 path('all/rss/', AllThreadsFeed()),
55 url(r'^tag/(?P<tag_name>\w+)/rss/$', TagThreadsFeed()),
55 url(r'^tag/(?P<tag_name>\w+)/rss/$', TagThreadsFeed()),
56 path('thread/<int:post_id>/rss/', ThreadPostsFeed()),
56 path('thread/<int:post_id>/rss/', ThreadPostsFeed()),
57
57
58 # i18n
58 # i18n
59 path('jsi18n/', JavaScriptCatalog.as_view(packages=['boards']), name='js_info_dict'),
59 path('jsi18n/', JavaScriptCatalog.as_view(packages=['boards']), name='js_info_dict'),
60
60
61 # API
61 # API
62 url(r'^api/post/(?P<post_id>\d+)/$', api.get_post, name="get_post"),
62 url(r'^api/post/(?P<post_id>\d+)/$', api.get_post, name="get_post"),
63 url(r'^api/diff_thread/$', api.api_get_threaddiff, name="get_thread_diff"),
63 url(r'^api/diff_thread/$', api.api_get_threaddiff, name="get_thread_diff"),
64 url(r'^api/threads/(?P<count>\w+)/$', api.api_get_threads,
64 url(r'^api/threads/(?P<count>\w+)/$', api.api_get_threads,
65 name='get_threads'),
65 name='get_threads'),
66 url(r'^api/tags/$', api.api_get_tags, name='get_tags'),
66 url(r'^api/tags/$', api.api_get_tags, name='get_tags'),
67 url(r'^api/thread/(?P<opening_post_id>\w+)/$', api.api_get_thread_posts,
67 url(r'^api/thread/(?P<opening_post_id>\w+)/$', api.api_get_thread_posts,
68 name='get_thread'),
68 name='get_thread'),
69 url(r'^api/add_post/(?P<opening_post_id>\w+)/$', api.api_add_post,
69 url(r'^api/add_post/(?P<opening_post_id>\w+)/$', api.api_add_post,
70 name='add_post'),
70 name='add_post'),
71 url(r'^api/add_post/$', api.api_add_post, name='add_post'),
71 url(r'^api/notifications/(?P<username>\w+)/$', api.api_get_notifications,
72 url(r'^api/notifications/(?P<username>\w+)/$', api.api_get_notifications,
72 name='api_notifications'),
73 name='api_notifications'),
73 url(r'^api/preview/$', api.api_get_preview, name='preview'),
74 url(r'^api/preview/$', api.api_get_preview, name='preview'),
74 url(r'^api/new_posts/$', api.api_get_new_posts, name='new_posts'),
75 url(r'^api/new_posts/$', api.api_get_new_posts, name='new_posts'),
75 url(r'^api/stickers/$', api.api_get_stickers, name='get_stickers'),
76 url(r'^api/stickers/$', api.api_get_stickers, name='get_stickers'),
76
77
77 # Sync protocol API
78 # Sync protocol API
78 url(r'^api/sync/list/$', response_list, name='api_sync_list'),
79 url(r'^api/sync/list/$', response_list, name='api_sync_list'),
79 url(r'^api/sync/get/$', response_get, name='api_sync_get'),
80 url(r'^api/sync/get/$', response_get, name='api_sync_get'),
80
81
81 # Notifications
82 # Notifications
82 path('notifications/<str:username>/', NotificationView.as_view(), name='notifications'),
83 path('notifications/<str:username>/', NotificationView.as_view(), name='notifications'),
83 path('notifications/', NotificationView.as_view(), name='notifications'),
84 path('notifications/', NotificationView.as_view(), name='notifications'),
84
85
85 # Post preview
86 # Post preview
86 path('preview/', PostPreviewView.as_view(), name='preview'),
87 path('preview/', PostPreviewView.as_view(), name='preview'),
87 path('post_xml/<int:post_id>', get_post_sync_data,
88 path('post_xml/<int:post_id>', get_post_sync_data,
88 name='post_sync_data'),
89 name='post_sync_data'),
89 ]
90 ]
90
91
@@ -1,321 +1,331 b''
1 import json
1 import json
2 import logging
2 import logging
3
3
4 from django.core import serializers
4 from django.core import serializers
5 from django.db import transaction
5 from django.db import transaction
6 from django.db.models import Q
6 from django.db.models import Q
7 from django.http import HttpResponse, HttpResponseBadRequest
7 from django.http import HttpResponse, HttpResponseBadRequest
8 from django.shortcuts import get_object_or_404
8 from django.shortcuts import get_object_or_404
9 from django.views.decorators.csrf import csrf_protect
9 from django.views.decorators.csrf import csrf_protect
10
10
11 from boards.abstracts.settingsmanager import get_settings_manager
11 from boards.abstracts.settingsmanager import get_settings_manager
12 from boards.forms import PostForm, PlainErrorList
12 from boards.forms import PostForm, PlainErrorList, ThreadForm
13 from boards.mdx_neboard import Parser
13 from boards.mdx_neboard import Parser
14 from boards.models import Post, Thread, Tag, TagAlias
14 from boards.models import Post, Thread, Tag, TagAlias
15 from boards.models.attachment import AttachmentSticker
15 from boards.models.attachment import AttachmentSticker
16 from boards.models.thread import STATUS_ARCHIVE
16 from boards.models.thread import STATUS_ARCHIVE
17 from boards.models.user import Notification
17 from boards.models.user import Notification
18 from boards.utils import datetime_to_epoch
18 from boards.utils import datetime_to_epoch
19
19
20 __author__ = 'neko259'
20 __author__ = 'neko259'
21
21
22 PARAMETER_TRUNCATED = 'truncated'
22 PARAMETER_TRUNCATED = 'truncated'
23 PARAMETER_TAG = 'tag'
23 PARAMETER_TAG = 'tag'
24 PARAMETER_OFFSET = 'offset'
24 PARAMETER_OFFSET = 'offset'
25 PARAMETER_DIFF_TYPE = 'type'
25 PARAMETER_DIFF_TYPE = 'type'
26 PARAMETER_POST = 'post'
26 PARAMETER_POST = 'post'
27 PARAMETER_UPDATED = 'updated'
27 PARAMETER_UPDATED = 'updated'
28 PARAMETER_LAST_UPDATE = 'last_update'
28 PARAMETER_LAST_UPDATE = 'last_update'
29 PARAMETER_THREAD = 'thread'
29 PARAMETER_THREAD = 'thread'
30 PARAMETER_UIDS = 'uids'
30 PARAMETER_UIDS = 'uids'
31 PARAMETER_SUBSCRIBED = 'subscribed'
31 PARAMETER_SUBSCRIBED = 'subscribed'
32
32
33 DIFF_TYPE_HTML = 'html'
33 DIFF_TYPE_HTML = 'html'
34 DIFF_TYPE_JSON = 'json'
34 DIFF_TYPE_JSON = 'json'
35
35
36 STATUS_OK = 'ok'
36 STATUS_OK = 'ok'
37 STATUS_ERROR = 'error'
37 STATUS_ERROR = 'error'
38
38
39 logger = logging.getLogger(__name__)
39 logger = logging.getLogger(__name__)
40
40
41
41
42 @transaction.atomic
42 @transaction.atomic
43 def api_get_threaddiff(request):
43 def api_get_threaddiff(request):
44 """
44 """
45 Gets posts that were changed or added since time
45 Gets posts that were changed or added since time
46 """
46 """
47
47
48 thread_id = request.POST.get(PARAMETER_THREAD)
48 thread_id = request.POST.get(PARAMETER_THREAD)
49 uids_str = request.POST.get(PARAMETER_UIDS)
49 uids_str = request.POST.get(PARAMETER_UIDS)
50
50
51 if not thread_id or not uids_str:
51 if not thread_id or not uids_str:
52 return HttpResponse(content='Invalid request.')
52 return HttpResponse(content='Invalid request.')
53
53
54 uids = uids_str.strip().split(' ')
54 uids = uids_str.strip().split(' ')
55
55
56 opening_post = get_object_or_404(Post, id=thread_id)
56 opening_post = get_object_or_404(Post, id=thread_id)
57 thread = opening_post.get_thread()
57 thread = opening_post.get_thread()
58
58
59 json_data = {
59 json_data = {
60 PARAMETER_UPDATED: [],
60 PARAMETER_UPDATED: [],
61 PARAMETER_LAST_UPDATE: None, # TODO Maybe this can be removed already?
61 PARAMETER_LAST_UPDATE: None, # TODO Maybe this can be removed already?
62 }
62 }
63 posts = Post.objects.filter(thread=thread).exclude(uid__in=uids)
63 posts = Post.objects.filter(thread=thread).exclude(uid__in=uids)
64
64
65 diff_type = request.GET.get(PARAMETER_DIFF_TYPE, DIFF_TYPE_HTML)
65 diff_type = request.GET.get(PARAMETER_DIFF_TYPE, DIFF_TYPE_HTML)
66
66
67 for post in posts:
67 for post in posts:
68 json_data[PARAMETER_UPDATED].append(post.get_post_data(
68 json_data[PARAMETER_UPDATED].append(post.get_post_data(
69 format_type=diff_type, request=request))
69 format_type=diff_type, request=request))
70 json_data[PARAMETER_LAST_UPDATE] = str(thread.last_edit_time)
70 json_data[PARAMETER_LAST_UPDATE] = str(thread.last_edit_time)
71
71
72 settings_manager = get_settings_manager(request)
72 settings_manager = get_settings_manager(request)
73 json_data[PARAMETER_SUBSCRIBED] = str(settings_manager.thread_is_fav(opening_post))
73 json_data[PARAMETER_SUBSCRIBED] = str(settings_manager.thread_is_fav(opening_post))
74
74
75 # If the tag is favorite, update the counter
75 # If the tag is favorite, update the counter
76 settings_manager = get_settings_manager(request)
76 settings_manager = get_settings_manager(request)
77 favorite = settings_manager.thread_is_fav(opening_post)
77 favorite = settings_manager.thread_is_fav(opening_post)
78 if favorite:
78 if favorite:
79 settings_manager.add_or_read_fav_thread(opening_post)
79 settings_manager.add_or_read_fav_thread(opening_post)
80
80
81 return HttpResponse(content=json.dumps(json_data))
81 return HttpResponse(content=json.dumps(json_data))
82
82
83
83
84 @csrf_protect
84 @csrf_protect
85 def api_add_post(request, opening_post_id):
85 def api_add_post(request, opening_post_id=None):
86 """
86 """
87 Adds a post and return the JSON response for it
87 Adds a post and return the JSON response for it
88 """
88 """
89
89
90 # TODO Allow thread creation here too, without specifying opening post
90 if opening_post_id:
91 opening_post = get_object_or_404(Post, id=opening_post_id)
91 opening_post = get_object_or_404(Post, id=opening_post_id)
92 else:
93 opening_post = None
92
94
93 status = STATUS_OK
95 status = STATUS_OK
94 errors = []
96 errors = []
95
97
96 post = None
98 post = None
97 if request.method == 'POST':
99 if request.method == 'POST':
98 form = PostForm(request.POST, request.FILES, error_class=PlainErrorList)
100 if opening_post:
101 form = PostForm(request.POST, request.FILES, error_class=PlainErrorList)
102 else:
103 form = ThreadForm(request.POST, request.FILES, error_class=PlainErrorList)
104
99 form.session = request.session
105 form.session = request.session
100
106
101 if form.need_to_ban:
107 if form.need_to_ban:
102 # Ban user because he is suspected to be a bot
108 # Ban user because he is suspected to be a bot
103 # _ban_current_user(request)
109 # _ban_current_user(request)
104 status = STATUS_ERROR
110 status = STATUS_ERROR
105 if form.is_valid():
111 if form.is_valid():
106 post = Post.objects.create_from_form(request, form, opening_post,
112 post = Post.objects.create_from_form(request, form, opening_post,
107 html_response=False)
113 html_response=False)
108 if not post:
114 if not post:
109 status = STATUS_ERROR
115 status = STATUS_ERROR
110 else:
116 else:
111 logger.info('Added post #%d via api.' % post.id)
117 logger.info('Added post #%d via api.' % post.id)
112 else:
118 else:
113 status = STATUS_ERROR
119 status = STATUS_ERROR
114 errors = form.as_json_errors()
120 errors = form.as_json_errors()
115 else:
121 else:
116 status = STATUS_ERROR
122 status = STATUS_ERROR
117
123
118 response = {
124 response = {
119 'status': status,
125 'status': status,
120 'errors': errors,
126 'errors': errors,
121 }
127 }
122
128
123 if post:
129 if post:
124 response['post_id'] = post.id
130 response['post_id'] = post.id
131 if not opening_post:
132 # FIXME For now we include URL only for threads to navigate to them.
133 # This needs to become something universal, just not yet sure how.
134 response['url'] = post.get_absolute_url()
125
135
126 return HttpResponse(content=json.dumps(response))
136 return HttpResponse(content=json.dumps(response))
127
137
128
138
129 def get_post(request, post_id):
139 def get_post(request, post_id):
130 """
140 """
131 Gets the html of a post. Used for popups. Post can be truncated if used
141 Gets the html of a post. Used for popups. Post can be truncated if used
132 in threads list with 'truncated' get parameter.
142 in threads list with 'truncated' get parameter.
133 """
143 """
134
144
135 post = get_object_or_404(Post, id=post_id)
145 post = get_object_or_404(Post, id=post_id)
136 truncated = PARAMETER_TRUNCATED in request.GET
146 truncated = PARAMETER_TRUNCATED in request.GET
137
147
138 return HttpResponse(content=post.get_view(truncated=truncated, need_op_data=True))
148 return HttpResponse(content=post.get_view(truncated=truncated, need_op_data=True))
139
149
140
150
141 def api_get_threads(request, count):
151 def api_get_threads(request, count):
142 """
152 """
143 Gets the JSON thread opening posts list.
153 Gets the JSON thread opening posts list.
144 Parameters that can be used for filtering:
154 Parameters that can be used for filtering:
145 tag, offset (from which thread to get results)
155 tag, offset (from which thread to get results)
146 """
156 """
147
157
148 if PARAMETER_TAG in request.GET:
158 if PARAMETER_TAG in request.GET:
149 tag_name = request.GET[PARAMETER_TAG]
159 tag_name = request.GET[PARAMETER_TAG]
150 if tag_name is not None:
160 if tag_name is not None:
151 tag = get_object_or_404(Tag, name=tag_name)
161 tag = get_object_or_404(Tag, name=tag_name)
152 threads = tag.get_threads().exclude(status=STATUS_ARCHIVE)
162 threads = tag.get_threads().exclude(status=STATUS_ARCHIVE)
153 else:
163 else:
154 threads = Thread.objects.exclude(status=STATUS_ARCHIVE)
164 threads = Thread.objects.exclude(status=STATUS_ARCHIVE)
155
165
156 if PARAMETER_OFFSET in request.GET:
166 if PARAMETER_OFFSET in request.GET:
157 offset = request.GET[PARAMETER_OFFSET]
167 offset = request.GET[PARAMETER_OFFSET]
158 offset = int(offset) if offset is not None else 0
168 offset = int(offset) if offset is not None else 0
159 else:
169 else:
160 offset = 0
170 offset = 0
161
171
162 threads = threads.order_by('-bump_time')
172 threads = threads.order_by('-bump_time')
163 threads = threads[offset:offset + int(count)]
173 threads = threads[offset:offset + int(count)]
164
174
165 opening_posts = []
175 opening_posts = []
166 for thread in threads:
176 for thread in threads:
167 opening_post = thread.get_opening_post()
177 opening_post = thread.get_opening_post()
168
178
169 # TODO Add tags, replies and images count
179 # TODO Add tags, replies and images count
170 post_data = opening_post.get_post_data(include_last_update=True)
180 post_data = opening_post.get_post_data(include_last_update=True)
171 post_data['status'] = thread.get_status()
181 post_data['status'] = thread.get_status()
172
182
173 opening_posts.append(post_data)
183 opening_posts.append(post_data)
174
184
175 return HttpResponse(content=json.dumps(opening_posts))
185 return HttpResponse(content=json.dumps(opening_posts))
176
186
177
187
178 # TODO Test this
188 # TODO Test this
179 def api_get_tags(request):
189 def api_get_tags(request):
180 """
190 """
181 Gets all tags or user tags.
191 Gets all tags or user tags.
182 """
192 """
183
193
184 # TODO Get favorite tags for the given user ID
194 # TODO Get favorite tags for the given user ID
185
195
186 tags = TagAlias.objects.all()
196 tags = TagAlias.objects.all()
187
197
188 term = request.GET.get('term')
198 term = request.GET.get('term')
189 if term is not None:
199 if term is not None:
190 tags = tags.filter(name__contains=term)
200 tags = tags.filter(name__contains=term)
191
201
192 tag_names = [tag.name for tag in tags]
202 tag_names = [tag.name for tag in tags]
193
203
194 return HttpResponse(content=json.dumps(tag_names))
204 return HttpResponse(content=json.dumps(tag_names))
195
205
196
206
197 def api_get_stickers(request):
207 def api_get_stickers(request):
198 term = request.GET.get('term')
208 term = request.GET.get('term')
199 if not term:
209 if not term:
200 return HttpResponseBadRequest()
210 return HttpResponseBadRequest()
201
211
202 global_stickers = AttachmentSticker.objects.filter(Q(name__icontains=term) | Q(stickerpack__name__icontains=term))
212 global_stickers = AttachmentSticker.objects.filter(Q(name__icontains=term) | Q(stickerpack__name__icontains=term))
203 local_stickers = [sticker for sticker in get_settings_manager(request).get_stickers() if term in sticker.name]
213 local_stickers = [sticker for sticker in get_settings_manager(request).get_stickers() if term in sticker.name]
204 stickers = list(global_stickers) + local_stickers
214 stickers = list(global_stickers) + local_stickers
205
215
206 image_dict = [{'thumb': sticker.attachment.get_thumb_url(),
216 image_dict = [{'thumb': sticker.attachment.get_thumb_url(),
207 'alias': str(sticker)}
217 'alias': str(sticker)}
208 for sticker in stickers]
218 for sticker in stickers]
209
219
210 return HttpResponse(content=json.dumps(image_dict))
220 return HttpResponse(content=json.dumps(image_dict))
211
221
212
222
213 # TODO The result can be cached by the thread last update time
223 # TODO The result can be cached by the thread last update time
214 # TODO Test this
224 # TODO Test this
215 def api_get_thread_posts(request, opening_post_id):
225 def api_get_thread_posts(request, opening_post_id):
216 """
226 """
217 Gets the JSON array of thread posts
227 Gets the JSON array of thread posts
218 """
228 """
219
229
220 opening_post = get_object_or_404(Post, id=opening_post_id)
230 opening_post = get_object_or_404(Post, id=opening_post_id)
221 thread = opening_post.get_thread()
231 thread = opening_post.get_thread()
222 posts = thread.get_replies()
232 posts = thread.get_replies()
223
233
224 json_data = {
234 json_data = {
225 'posts': [],
235 'posts': [],
226 'last_update': None,
236 'last_update': None,
227 }
237 }
228 json_post_list = []
238 json_post_list = []
229
239
230 for post in posts:
240 for post in posts:
231 json_post_list.append(post.get_post_data())
241 json_post_list.append(post.get_post_data())
232 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
242 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
233 json_data['posts'] = json_post_list
243 json_data['posts'] = json_post_list
234
244
235 return HttpResponse(content=json.dumps(json_data))
245 return HttpResponse(content=json.dumps(json_data))
236
246
237
247
238 def api_get_notifications(request, username):
248 def api_get_notifications(request, username):
239 last_notification_id_str = request.GET.get('last', None)
249 last_notification_id_str = request.GET.get('last', None)
240 last_id = int(last_notification_id_str) if last_notification_id_str is not None else None
250 last_id = int(last_notification_id_str) if last_notification_id_str is not None else None
241
251
242 posts = Notification.objects.get_notification_posts(usernames=[username],
252 posts = Notification.objects.get_notification_posts(usernames=[username],
243 last=last_id)
253 last=last_id)
244
254
245 json_post_list = []
255 json_post_list = []
246 for post in posts:
256 for post in posts:
247 json_post_list.append(post.get_post_data())
257 json_post_list.append(post.get_post_data())
248 return HttpResponse(content=json.dumps(json_post_list))
258 return HttpResponse(content=json.dumps(json_post_list))
249
259
250
260
251 def api_get_post(request, post_id):
261 def api_get_post(request, post_id):
252 """
262 """
253 Gets the JSON of a post. This can be
263 Gets the JSON of a post. This can be
254 used as and API for external clients.
264 used as and API for external clients.
255 """
265 """
256
266
257 post = get_object_or_404(Post, id=post_id)
267 post = get_object_or_404(Post, id=post_id)
258
268
259 json = serializers.serialize("json", [post], fields=(
269 json = serializers.serialize("json", [post], fields=(
260 "pub_time", "_text_rendered", "title", "text", "image",
270 "pub_time", "_text_rendered", "title", "text", "image",
261 "image_width", "image_height", "replies", "tags"
271 "image_width", "image_height", "replies", "tags"
262 ))
272 ))
263
273
264 return HttpResponse(content=json)
274 return HttpResponse(content=json)
265
275
266
276
267 def api_get_preview(request):
277 def api_get_preview(request):
268 raw_text = request.POST['raw_text']
278 raw_text = request.POST['raw_text']
269
279
270 parser = Parser()
280 parser = Parser()
271 return HttpResponse(content=parser.parse(parser.preparse(raw_text)))
281 return HttpResponse(content=parser.parse(parser.preparse(raw_text)))
272
282
273
283
274 def api_get_new_posts(request):
284 def api_get_new_posts(request):
275 """
285 """
276 Gets favorite threads and unread posts count.
286 Gets favorite threads and unread posts count.
277 """
287 """
278 posts = list()
288 posts = list()
279
289
280 include_posts = 'include_posts' in request.GET
290 include_posts = 'include_posts' in request.GET
281
291
282 settings_manager = get_settings_manager(request)
292 settings_manager = get_settings_manager(request)
283 fav_threads = settings_manager.get_fav_threads()
293 fav_threads = settings_manager.get_fav_threads()
284 fav_thread_ops = Post.objects.filter(id__in=fav_threads.keys())\
294 fav_thread_ops = Post.objects.filter(id__in=fav_threads.keys())\
285 .order_by('-pub_time').prefetch_related('thread')
295 .order_by('-pub_time').prefetch_related('thread')
286
296
287 ops = [{'op': op, 'last_id': fav_threads[str(op.id)]} for op in fav_thread_ops]
297 ops = [{'op': op, 'last_id': fav_threads[str(op.id)]} for op in fav_thread_ops]
288 if include_posts:
298 if include_posts:
289 new_post_threads = Thread.objects.get_new_posts(ops)
299 new_post_threads = Thread.objects.get_new_posts(ops)
290 if new_post_threads:
300 if new_post_threads:
291 thread_ids = {thread.id: thread for thread in new_post_threads}
301 thread_ids = {thread.id: thread for thread in new_post_threads}
292 else:
302 else:
293 thread_ids = dict()
303 thread_ids = dict()
294
304
295 for op in fav_thread_ops:
305 for op in fav_thread_ops:
296 fav_thread_dict = dict()
306 fav_thread_dict = dict()
297
307
298 op_thread = op.get_thread()
308 op_thread = op.get_thread()
299 if op_thread.id in thread_ids:
309 if op_thread.id in thread_ids:
300 thread = thread_ids[op_thread.id]
310 thread = thread_ids[op_thread.id]
301 new_post_count = thread.new_post_count
311 new_post_count = thread.new_post_count
302 fav_thread_dict['newest_post_link'] = thread.get_replies()\
312 fav_thread_dict['newest_post_link'] = thread.get_replies()\
303 .filter(id__gt=fav_threads[str(op.id)])\
313 .filter(id__gt=fav_threads[str(op.id)])\
304 .first().get_absolute_url(thread=thread)
314 .first().get_absolute_url(thread=thread)
305 else:
315 else:
306 new_post_count = 0
316 new_post_count = 0
307 fav_thread_dict['new_post_count'] = new_post_count
317 fav_thread_dict['new_post_count'] = new_post_count
308
318
309 fav_thread_dict['id'] = op.id
319 fav_thread_dict['id'] = op.id
310
320
311 fav_thread_dict['post_url'] = op.get_link_view()
321 fav_thread_dict['post_url'] = op.get_link_view()
312 fav_thread_dict['title'] = op.title
322 fav_thread_dict['title'] = op.title
313
323
314 posts.append(fav_thread_dict)
324 posts.append(fav_thread_dict)
315 else:
325 else:
316 fav_thread_dict = dict()
326 fav_thread_dict = dict()
317 fav_thread_dict['new_post_count'] = \
327 fav_thread_dict['new_post_count'] = \
318 Thread.objects.get_new_post_count(ops)
328 Thread.objects.get_new_post_count(ops)
319 posts.append(fav_thread_dict)
329 posts.append(fav_thread_dict)
320
330
321 return HttpResponse(content=json.dumps(posts))
331 return HttpResponse(content=json.dumps(posts))
General Comments 0
You need to be logged in to leave comments. Login now