##// END OF EJS Templates
Merged with default branch
neko259 -
r933:ce97c754 merge decentral
parent child Browse files
Show More
@@ -0,0 +1,23 b''
1 # -*- coding: utf-8 -*-
2 from __future__ import unicode_literals
3
4 from django.db import models, migrations
5
6
7 class Migration(migrations.Migration):
8
9 dependencies = [
10 ('boards', '0001_initial'),
11 ]
12
13 operations = [
14 migrations.RemoveField(
15 model_name='post',
16 name='text_markup_type',
17 ),
18 migrations.AlterField(
19 model_name='post',
20 name='_text_rendered',
21 field=models.TextField(null=True, blank=True, editable=False),
22 ),
23 ]
@@ -0,0 +1,18 b''
1 # -*- coding: utf-8 -*-
2 from __future__ import unicode_literals
3
4 from django.db import models, migrations
5
6
7 class Migration(migrations.Migration):
8
9 dependencies = [
10 ('boards', '0002_auto_20141118_2234'),
11 ]
12
13 operations = [
14 migrations.RemoveField(
15 model_name='tag',
16 name='threads',
17 ),
18 ]
@@ -0,0 +1,20 b''
1 # -*- coding: utf-8 -*-
2 from __future__ import unicode_literals
3
4 from django.db import models, migrations
5
6
7 class Migration(migrations.Migration):
8
9 dependencies = [
10 ('boards', '0003_remove_tag_threads'),
11 ]
12
13 operations = [
14 migrations.AddField(
15 model_name='tag',
16 name='required',
17 field=models.BooleanField(default=False),
18 preserve_default=True,
19 ),
20 ]
This diff has been collapsed as it changes many lines, (1256 lines changed) Show them Hide them
@@ -0,0 +1,1256 b''
1 /**
2 * Centrifuge javascript client
3 * v0.5.2
4 */
5 ;(function () {
6 'use strict';
7
8 /**
9 * Oliver Caldwell
10 * http://oli.me.uk/2013/06/01/prototypical-inheritance-done-right/
11 */
12
13 if (!Object.create) {
14 Object.create = (function(){
15 function F(){}
16
17 return function(o){
18 if (arguments.length != 1) {
19 throw new Error('Object.create implementation only accepts one parameter.');
20 }
21 F.prototype = o;
22 return new F()
23 }
24 })()
25 }
26
27 if (!Array.prototype.indexOf) {
28 Array.prototype.indexOf = function (searchElement /*, fromIndex */) {
29 'use strict';
30 if (this == null) {
31 throw new TypeError();
32 }
33 var n, k, t = Object(this),
34 len = t.length >>> 0;
35
36 if (len === 0) {
37 return -1;
38 }
39 n = 0;
40 if (arguments.length > 1) {
41 n = Number(arguments[1]);
42 if (n != n) { // shortcut for verifying if it's NaN
43 n = 0;
44 } else if (n != 0 && n != Infinity && n != -Infinity) {
45 n = (n > 0 || -1) * Math.floor(Math.abs(n));
46 }
47 }
48 if (n >= len) {
49 return -1;
50 }
51 for (k = n >= 0 ? n : Math.max(len - Math.abs(n), 0); k < len; k++) {
52 if (k in t && t[k] === searchElement) {
53 return k;
54 }
55 }
56 return -1;
57 };
58 }
59
60 function extend(destination, source) {
61 destination.prototype = Object.create(source.prototype);
62 destination.prototype.constructor = destination;
63 return source.prototype;
64 }
65
66 /**
67 * EventEmitter v4.2.3 - git.io/ee
68 * Oliver Caldwell
69 * MIT license
70 * @preserve
71 */
72
73 /**
74 * Class for managing events.
75 * Can be extended to provide event functionality in other classes.
76 *
77 * @class EventEmitter Manages event registering and emitting.
78 */
79 function EventEmitter() {}
80
81 // Shortcuts to improve speed and size
82
83 // Easy access to the prototype
84 var proto = EventEmitter.prototype;
85
86 /**
87 * Finds the index of the listener for the event in it's storage array.
88 *
89 * @param {Function[]} listeners Array of listeners to search through.
90 * @param {Function} listener Method to look for.
91 * @return {Number} Index of the specified listener, -1 if not found
92 * @api private
93 */
94 function indexOfListener(listeners, listener) {
95 var i = listeners.length;
96 while (i--) {
97 if (listeners[i].listener === listener) {
98 return i;
99 }
100 }
101
102 return -1;
103 }
104
105 /**
106 * Alias a method while keeping the context correct, to allow for overwriting of target method.
107 *
108 * @param {String} name The name of the target method.
109 * @return {Function} The aliased method
110 * @api private
111 */
112 function alias(name) {
113 return function aliasClosure() {
114 return this[name].apply(this, arguments);
115 };
116 }
117
118 /**
119 * Returns the listener array for the specified event.
120 * Will initialise the event object and listener arrays if required.
121 * Will return an object if you use a regex search. The object contains keys for each matched event. So /ba[rz]/ might return an object containing bar and baz. But only if you have either defined them with defineEvent or added some listeners to them.
122 * Each property in the object response is an array of listener functions.
123 *
124 * @param {String|RegExp} evt Name of the event to return the listeners from.
125 * @return {Function[]|Object} All listener functions for the event.
126 */
127 proto.getListeners = function getListeners(evt) {
128 var events = this._getEvents();
129 var response;
130 var key;
131
132 // Return a concatenated array of all matching events if
133 // the selector is a regular expression.
134 if (typeof evt === 'object') {
135 response = {};
136 for (key in events) {
137 if (events.hasOwnProperty(key) && evt.test(key)) {
138 response[key] = events[key];
139 }
140 }
141 }
142 else {
143 response = events[evt] || (events[evt] = []);
144 }
145
146 return response;
147 };
148
149 /**
150 * Takes a list of listener objects and flattens it into a list of listener functions.
151 *
152 * @param {Object[]} listeners Raw listener objects.
153 * @return {Function[]} Just the listener functions.
154 */
155 proto.flattenListeners = function flattenListeners(listeners) {
156 var flatListeners = [];
157 var i;
158
159 for (i = 0; i < listeners.length; i += 1) {
160 flatListeners.push(listeners[i].listener);
161 }
162
163 return flatListeners;
164 };
165
166 /**
167 * Fetches the requested listeners via getListeners but will always return the results inside an object. This is mainly for internal use but others may find it useful.
168 *
169 * @param {String|RegExp} evt Name of the event to return the listeners from.
170 * @return {Object} All listener functions for an event in an object.
171 */
172 proto.getListenersAsObject = function getListenersAsObject(evt) {
173 var listeners = this.getListeners(evt);
174 var response;
175
176 if (listeners instanceof Array) {
177 response = {};
178 response[evt] = listeners;
179 }
180
181 return response || listeners;
182 };
183
184 /**
185 * Adds a listener function to the specified event.
186 * The listener will not be added if it is a duplicate.
187 * If the listener returns true then it will be removed after it is called.
188 * If you pass a regular expression as the event name then the listener will be added to all events that match it.
189 *
190 * @param {String|RegExp} evt Name of the event to attach the listener to.
191 * @param {Function} listener Method to be called when the event is emitted. If the function returns true then it will be removed after calling.
192 * @return {Object} Current instance of EventEmitter for chaining.
193 */
194 proto.addListener = function addListener(evt, listener) {
195 var listeners = this.getListenersAsObject(evt);
196 var listenerIsWrapped = typeof listener === 'object';
197 var key;
198
199 for (key in listeners) {
200 if (listeners.hasOwnProperty(key) && indexOfListener(listeners[key], listener) === -1) {
201 listeners[key].push(listenerIsWrapped ? listener : {
202 listener: listener,
203 once: false
204 });
205 }
206 }
207
208 return this;
209 };
210
211 /**
212 * Alias of addListener
213 */
214 proto.on = alias('addListener');
215
216 /**
217 * Semi-alias of addListener. It will add a listener that will be
218 * automatically removed after it's first execution.
219 *
220 * @param {String|RegExp} evt Name of the event to attach the listener to.
221 * @param {Function} listener Method to be called when the event is emitted. If the function returns true then it will be removed after calling.
222 * @return {Object} Current instance of EventEmitter for chaining.
223 */
224 proto.addOnceListener = function addOnceListener(evt, listener) {
225 //noinspection JSValidateTypes
226 return this.addListener(evt, {
227 listener: listener,
228 once: true
229 });
230 };
231
232 /**
233 * Alias of addOnceListener.
234 */
235 proto.once = alias('addOnceListener');
236
237 /**
238 * Defines an event name. This is required if you want to use a regex to add a listener to multiple events at once. If you don't do this then how do you expect it to know what event to add to? Should it just add to every possible match for a regex? No. That is scary and bad.
239 * You need to tell it what event names should be matched by a regex.
240 *
241 * @param {String} evt Name of the event to create.
242 * @return {Object} Current instance of EventEmitter for chaining.
243 */
244 proto.defineEvent = function defineEvent(evt) {
245 this.getListeners(evt);
246 return this;
247 };
248
249 /**
250 * Uses defineEvent to define multiple events.
251 *
252 * @param {String[]} evts An array of event names to define.
253 * @return {Object} Current instance of EventEmitter for chaining.
254 */
255 proto.defineEvents = function defineEvents(evts) {
256 for (var i = 0; i < evts.length; i += 1) {
257 this.defineEvent(evts[i]);
258 }
259 return this;
260 };
261
262 /**
263 * Removes a listener function from the specified event.
264 * When passed a regular expression as the event name, it will remove the listener from all events that match it.
265 *
266 * @param {String|RegExp} evt Name of the event to remove the listener from.
267 * @param {Function} listener Method to remove from the event.
268 * @return {Object} Current instance of EventEmitter for chaining.
269 */
270 proto.removeListener = function removeListener(evt, listener) {
271 var listeners = this.getListenersAsObject(evt);
272 var index;
273 var key;
274
275 for (key in listeners) {
276 if (listeners.hasOwnProperty(key)) {
277 index = indexOfListener(listeners[key], listener);
278
279 if (index !== -1) {
280 listeners[key].splice(index, 1);
281 }
282 }
283 }
284
285 return this;
286 };
287
288 /**
289 * Alias of removeListener
290 */
291 proto.off = alias('removeListener');
292
293 /**
294 * Adds listeners in bulk using the manipulateListeners method.
295 * If you pass an object as the second argument you can add to multiple events at once. The object should contain key value pairs of events and listeners or listener arrays. You can also pass it an event name and an array of listeners to be added.
296 * You can also pass it a regular expression to add the array of listeners to all events that match it.
297 * Yeah, this function does quite a bit. That's probably a bad thing.
298 *
299 * @param {String|Object|RegExp} evt An event name if you will pass an array of listeners next. An object if you wish to add to multiple events at once.
300 * @param {Function[]} [listeners] An optional array of listener functions to add.
301 * @return {Object} Current instance of EventEmitter for chaining.
302 */
303 proto.addListeners = function addListeners(evt, listeners) {
304 // Pass through to manipulateListeners
305 return this.manipulateListeners(false, evt, listeners);
306 };
307
308 /**
309 * Removes listeners in bulk using the manipulateListeners method.
310 * If you pass an object as the second argument you can remove from multiple events at once. The object should contain key value pairs of events and listeners or listener arrays.
311 * You can also pass it an event name and an array of listeners to be removed.
312 * You can also pass it a regular expression to remove the listeners from all events that match it.
313 *
314 * @param {String|Object|RegExp} evt An event name if you will pass an array of listeners next. An object if you wish to remove from multiple events at once.
315 * @param {Function[]} [listeners] An optional array of listener functions to remove.
316 * @return {Object} Current instance of EventEmitter for chaining.
317 */
318 proto.removeListeners = function removeListeners(evt, listeners) {
319 // Pass through to manipulateListeners
320 return this.manipulateListeners(true, evt, listeners);
321 };
322
323 /**
324 * Edits listeners in bulk. The addListeners and removeListeners methods both use this to do their job. You should really use those instead, this is a little lower level.
325 * The first argument will determine if the listeners are removed (true) or added (false).
326 * If you pass an object as the second argument you can add/remove from multiple events at once. The object should contain key value pairs of events and listeners or listener arrays.
327 * You can also pass it an event name and an array of listeners to be added/removed.
328 * You can also pass it a regular expression to manipulate the listeners of all events that match it.
329 *
330 * @param {Boolean} remove True if you want to remove listeners, false if you want to add.
331 * @param {String|Object|RegExp} evt An event name if you will pass an array of listeners next. An object if you wish to add/remove from multiple events at once.
332 * @param {Function[]} [listeners] An optional array of listener functions to add/remove.
333 * @return {Object} Current instance of EventEmitter for chaining.
334 */
335 proto.manipulateListeners = function manipulateListeners(remove, evt, listeners) {
336 var i;
337 var value;
338 var single = remove ? this.removeListener : this.addListener;
339 var multiple = remove ? this.removeListeners : this.addListeners;
340
341 // If evt is an object then pass each of it's properties to this method
342 if (typeof evt === 'object' && !(evt instanceof RegExp)) {
343 for (i in evt) {
344 if (evt.hasOwnProperty(i) && (value = evt[i])) {
345 // Pass the single listener straight through to the singular method
346 if (typeof value === 'function') {
347 single.call(this, i, value);
348 }
349 else {
350 // Otherwise pass back to the multiple function
351 multiple.call(this, i, value);
352 }
353 }
354 }
355 }
356 else {
357 // So evt must be a string
358 // And listeners must be an array of listeners
359 // Loop over it and pass each one to the multiple method
360 i = listeners.length;
361 while (i--) {
362 single.call(this, evt, listeners[i]);
363 }
364 }
365
366 return this;
367 };
368
369 /**
370 * Removes all listeners from a specified event.
371 * If you do not specify an event then all listeners will be removed.
372 * That means every event will be emptied.
373 * You can also pass a regex to remove all events that match it.
374 *
375 * @param {String|RegExp} [evt] Optional name of the event to remove all listeners for. Will remove from every event if not passed.
376 * @return {Object} Current instance of EventEmitter for chaining.
377 */
378 proto.removeEvent = function removeEvent(evt) {
379 var type = typeof evt;
380 var events = this._getEvents();
381 var key;
382
383 // Remove different things depending on the state of evt
384 if (type === 'string') {
385 // Remove all listeners for the specified event
386 delete events[evt];
387 }
388 else if (type === 'object') {
389 // Remove all events matching the regex.
390 for (key in events) {
391 //noinspection JSUnresolvedFunction
392 if (events.hasOwnProperty(key) && evt.test(key)) {
393 delete events[key];
394 }
395 }
396 }
397 else {
398 // Remove all listeners in all events
399 delete this._events;
400 }
401
402 return this;
403 };
404
405 /**
406 * Emits an event of your choice.
407 * When emitted, every listener attached to that event will be executed.
408 * If you pass the optional argument array then those arguments will be passed to every listener upon execution.
409 * Because it uses `apply`, your array of arguments will be passed as if you wrote them out separately.
410 * So they will not arrive within the array on the other side, they will be separate.
411 * You can also pass a regular expression to emit to all events that match it.
412 *
413 * @param {String|RegExp} evt Name of the event to emit and execute listeners for.
414 * @param {Array} [args] Optional array of arguments to be passed to each listener.
415 * @return {Object} Current instance of EventEmitter for chaining.
416 */
417 proto.emitEvent = function emitEvent(evt, args) {
418 var listeners = this.getListenersAsObject(evt);
419 var listener;
420 var i;
421 var key;
422 var response;
423
424 for (key in listeners) {
425 if (listeners.hasOwnProperty(key)) {
426 i = listeners[key].length;
427
428 while (i--) {
429 // If the listener returns true then it shall be removed from the event
430 // The function is executed either with a basic call or an apply if there is an args array
431 listener = listeners[key][i];
432
433 if (listener.once === true) {
434 this.removeListener(evt, listener.listener);
435 }
436
437 response = listener.listener.apply(this, args || []);
438
439 if (response === this._getOnceReturnValue()) {
440 this.removeListener(evt, listener.listener);
441 }
442 }
443 }
444 }
445
446 return this;
447 };
448
449 /**
450 * Alias of emitEvent
451 */
452 proto.trigger = alias('emitEvent');
453
454 //noinspection JSValidateJSDoc,JSCommentMatchesSignature
455 /**
456 * Subtly different from emitEvent in that it will pass its arguments on to the listeners, as opposed to taking a single array of arguments to pass on.
457 * As with emitEvent, you can pass a regex in place of the event name to emit to all events that match it.
458 *
459 * @param {String|RegExp} evt Name of the event to emit and execute listeners for.
460 * @param {...*} Optional additional arguments to be passed to each listener.
461 * @return {Object} Current instance of EventEmitter for chaining.
462 */
463 proto.emit = function emit(evt) {
464 var args = Array.prototype.slice.call(arguments, 1);
465 return this.emitEvent(evt, args);
466 };
467
468 /**
469 * Sets the current value to check against when executing listeners. If a
470 * listeners return value matches the one set here then it will be removed
471 * after execution. This value defaults to true.
472 *
473 * @param {*} value The new value to check for when executing listeners.
474 * @return {Object} Current instance of EventEmitter for chaining.
475 */
476 proto.setOnceReturnValue = function setOnceReturnValue(value) {
477 this._onceReturnValue = value;
478 return this;
479 };
480
481 /**
482 * Fetches the current value to check against when executing listeners. If
483 * the listeners return value matches this one then it should be removed
484 * automatically. It will return true by default.
485 *
486 * @return {*|Boolean} The current value to check for or the default, true.
487 * @api private
488 */
489 proto._getOnceReturnValue = function _getOnceReturnValue() {
490 if (this.hasOwnProperty('_onceReturnValue')) {
491 return this._onceReturnValue;
492 }
493 else {
494 return true;
495 }
496 };
497
498 /**
499 * Fetches the events object and creates one if required.
500 *
501 * @return {Object} The events storage object.
502 * @api private
503 */
504 proto._getEvents = function _getEvents() {
505 return this._events || (this._events = {});
506 };
507
508 /**
509 * Mixes in the given objects into the target object by copying the properties.
510 * @param deep if the copy must be deep
511 * @param target the target object
512 * @param objects the objects whose properties are copied into the target
513 */
514 function mixin(deep, target, objects) {
515 var result = target || {};
516
517 // Skip first 2 parameters (deep and target), and loop over the others
518 for (var i = 2; i < arguments.length; ++i) {
519 var object = arguments[i];
520
521 if (object === undefined || object === null) {
522 continue;
523 }
524
525 for (var propName in object) {
526 //noinspection JSUnfilteredForInLoop
527 var prop = fieldValue(object, propName);
528 //noinspection JSUnfilteredForInLoop
529 var targ = fieldValue(result, propName);
530
531 // Avoid infinite loops
532 if (prop === target) {
533 continue;
534 }
535 // Do not mixin undefined values
536 if (prop === undefined) {
537 continue;
538 }
539
540 if (deep && typeof prop === 'object' && prop !== null) {
541 if (prop instanceof Array) {
542 //noinspection JSUnfilteredForInLoop
543 result[propName] = mixin(deep, targ instanceof Array ? targ : [], prop);
544 } else {
545 var source = typeof targ === 'object' && !(targ instanceof Array) ? targ : {};
546 //noinspection JSUnfilteredForInLoop
547 result[propName] = mixin(deep, source, prop);
548 }
549 } else {
550 //noinspection JSUnfilteredForInLoop
551 result[propName] = prop;
552 }
553 }
554 }
555
556 return result;
557 }
558
559 function fieldValue(object, name) {
560 try {
561 return object[name];
562 } catch (x) {
563 return undefined;
564 }
565 }
566
567 function endsWith(value, suffix) {
568 return value.indexOf(suffix, value.length - suffix.length) !== -1;
569 }
570
571 function stripSlash(value) {
572 if (value.substring(value.length - 1) == "/") {
573 value = value.substring(0, value.length - 1);
574 }
575 return value;
576 }
577
578 function isString(value) {
579 if (value === undefined || value === null) {
580 return false;
581 }
582 return typeof value === 'string' || value instanceof String;
583 }
584
585 function isFunction(value) {
586 if (value === undefined || value === null) {
587 return false;
588 }
589 return typeof value === 'function';
590 }
591
592 function log(level, args) {
593 if (window.console) {
594 var logger = window.console[level];
595 if (isFunction(logger)) {
596 logger.apply(window.console, args);
597 }
598 }
599 }
600
601 function Centrifuge(options) {
602 this._sockjs = false;
603 this._status = 'disconnected';
604 this._reconnect = true;
605 this._transport = null;
606 this._messageId = 0;
607 this._clientId = null;
608 this._subscriptions = {};
609 this._messages = [];
610 this._isBatching = false;
611 this._config = {
612 retry: 3000,
613 info: null,
614 debug: false,
615 server: null,
616 protocols_whitelist: [
617 'websocket',
618 'xdr-streaming',
619 'xhr-streaming',
620 'iframe-eventsource',
621 'iframe-htmlfile',
622 'xdr-polling',
623 'xhr-polling',
624 'iframe-xhr-polling',
625 'jsonp-polling'
626 ]
627 };
628 if (options) {
629 this.configure(options);
630 }
631 }
632
633 extend(Centrifuge, EventEmitter);
634
635 var centrifuge_proto = Centrifuge.prototype;
636
637 centrifuge_proto._debug = function () {
638 if (this._config.debug === true) {
639 log('debug', arguments);
640 }
641 };
642
643 centrifuge_proto._configure = function (configuration) {
644 this._debug('Configuring centrifuge object with', configuration);
645
646 if (!configuration) {
647 configuration = {};
648 }
649
650 this._config = mixin(false, this._config, configuration);
651
652 if (!this._config.url) {
653 throw 'Missing required configuration parameter \'url\' specifying the Centrifuge server URL';
654 }
655
656 if (!this._config.token) {
657 throw 'Missing required configuration parameter \'token\' specifying the sign of authorization request';
658 }
659
660 if (!this._config.project) {
661 throw 'Missing required configuration parameter \'project\' specifying project ID in Centrifuge';
662 }
663
664 if (!this._config.user && this._config.user !== '') {
665 throw 'Missing required configuration parameter \'user\' specifying user\'s unique ID in your application';
666 }
667
668 if (!this._config.timestamp) {
669 throw 'Missing required configuration parameter \'timestamp\'';
670 }
671
672 this._config.url = stripSlash(this._config.url);
673
674 if (endsWith(this._config.url, 'connection')) {
675 //noinspection JSUnresolvedVariable
676 if (typeof window.SockJS === 'undefined') {
677 throw 'You need to include SockJS client library before Centrifuge javascript client library or use pure Websocket connection endpoint';
678 }
679 this._sockjs = true;
680 }
681 };
682
683 centrifuge_proto._setStatus = function (newStatus) {
684 if (this._status !== newStatus) {
685 this._debug('Status', this._status, '->', newStatus);
686 this._status = newStatus;
687 }
688 };
689
690 centrifuge_proto._isDisconnected = function () {
691 return this._isConnected() === false;
692 };
693
694 centrifuge_proto._isConnected = function () {
695 return this._status === 'connected';
696 };
697
698 centrifuge_proto._nextMessageId = function () {
699 return ++this._messageId;
700 };
701
702 centrifuge_proto._clearSubscriptions = function () {
703 this._subscriptions = {};
704 };
705
706 centrifuge_proto._send = function (messages) {
707 // We must be sure that the messages have a clientId.
708 // This is not guaranteed since the handshake may take time to return
709 // (and hence the clientId is not known yet) and the application
710 // may create other messages.
711 for (var i = 0; i < messages.length; ++i) {
712 var message = messages[i];
713 message.uid = '' + this._nextMessageId();
714
715 if (this._clientId) {
716 message.clientId = this._clientId;
717 }
718
719 this._debug('Send', message);
720 this._transport.send(JSON.stringify(message));
721 }
722 };
723
724 centrifuge_proto._connect = function (callback) {
725
726 this._clientId = null;
727
728 this._reconnect = true;
729
730 this._clearSubscriptions();
731
732 this._setStatus('connecting');
733
734 var self = this;
735
736 if (callback) {
737 this.on('connect', callback);
738 }
739
740 if (this._sockjs === true) {
741 //noinspection JSUnresolvedFunction
742 var sockjs_options = {
743 protocols_whitelist: this._config.protocols_whitelist
744 };
745 if (this._config.server !== null) {
746 sockjs_options['server'] = this._config.server;
747 }
748
749 this._transport = new SockJS(this._config.url, null, sockjs_options);
750
751 } else {
752 this._transport = new WebSocket(this._config.url);
753 }
754
755 this._setStatus('connecting');
756
757 this._transport.onopen = function () {
758
759 var centrifugeMessage = {
760 'method': 'connect',
761 'params': {
762 'token': self._config.token,
763 'user': self._config.user,
764 'project': self._config.project,
765 'timestamp': self._config.timestamp
766 }
767 };
768
769 if (self._config.info !== null) {
770 self._debug("connect using additional info");
771 centrifugeMessage['params']['info'] = self._config.info;
772 } else {
773 self._debug("connect without additional info");
774 }
775 self.send(centrifugeMessage);
776 };
777
778 this._transport.onerror = function (error) {
779 self._debug(error);
780 };
781
782 this._transport.onclose = function () {
783 self._setStatus('disconnected');
784 self.trigger('disconnect');
785 if (self._reconnect === true) {
786 window.setTimeout(function () {
787 if (self._reconnect === true) {
788 self._connect.call(self);
789 }
790 }, self._config.retry);
791 }
792 };
793
794 this._transport.onmessage = function (event) {
795 var data;
796 data = JSON.parse(event.data);
797 self._debug('Received', data);
798 self._receive(data);
799 };
800 };
801
802 centrifuge_proto._disconnect = function () {
803 this._clientId = null;
804 this._setStatus('disconnected');
805 this._subscriptions = {};
806 this._reconnect = false;
807 this._transport.close();
808 };
809
810 centrifuge_proto._getSubscription = function (channel) {
811 var subscription;
812 subscription = this._subscriptions[channel];
813 if (!subscription) {
814 return null;
815 }
816 return subscription;
817 };
818
819 centrifuge_proto._removeSubscription = function (channel) {
820 try {
821 delete this._subscriptions[channel];
822 } catch (e) {
823 this._debug('nothing to delete for channel ', channel);
824 }
825 };
826
827 centrifuge_proto._connectResponse = function (message) {
828 if (message.error === null) {
829 this._clientId = message.body;
830 this._setStatus('connected');
831 this.trigger('connect', [message]);
832 } else {
833 this.trigger('error', [message]);
834 this.trigger('connect:error', [message]);
835 }
836 };
837
838 centrifuge_proto._disconnectResponse = function (message) {
839 if (message.error === null) {
840 this.disconnect();
841 //this.trigger('disconnect', [message]);
842 //this.trigger('disconnect:success', [message]);
843 } else {
844 this.trigger('error', [message]);
845 this.trigger('disconnect:error', [message.error]);
846 }
847 };
848
849 centrifuge_proto._subscribeResponse = function (message) {
850 if (message.error !== null) {
851 this.trigger('error', [message]);
852 }
853 var body = message.body;
854 if (body === null) {
855 return;
856 }
857 var channel = body.channel;
858 var subscription = this.getSubscription(channel);
859 if (!subscription) {
860 return;
861 }
862 if (message.error === null) {
863 subscription.trigger('subscribe:success', [body]);
864 subscription.trigger('ready', [body]);
865 } else {
866 subscription.trigger('subscribe:error', [message.error]);
867 subscription.trigger('error', [message]);
868 }
869 };
870
871 centrifuge_proto._unsubscribeResponse = function (message) {
872 var body = message.body;
873 var channel = body.channel;
874 var subscription = this.getSubscription(channel);
875 if (!subscription) {
876 return;
877 }
878 if (message.error === null) {
879 subscription.trigger('unsubscribe', [body]);
880 this._centrifuge._removeSubscription(channel);
881 }
882 };
883
884 centrifuge_proto._publishResponse = function (message) {
885 var body = message.body;
886 var channel = body.channel;
887 var subscription = this.getSubscription(channel);
888 if (!subscription) {
889 return;
890 }
891 if (message.error === null) {
892 subscription.trigger('publish:success', [body]);
893 } else {
894 subscription.trigger('publish:error', [message.error]);
895 this.trigger('error', [message]);
896 }
897 };
898
899 centrifuge_proto._presenceResponse = function (message) {
900 var body = message.body;
901 var channel = body.channel;
902 var subscription = this.getSubscription(channel);
903 if (!subscription) {
904 return;
905 }
906 if (message.error === null) {
907 subscription.trigger('presence', [body]);
908 subscription.trigger('presence:success', [body]);
909 } else {
910 subscription.trigger('presence:error', [message.error]);
911 this.trigger('error', [message]);
912 }
913 };
914
915 centrifuge_proto._historyResponse = function (message) {
916 var body = message.body;
917 var channel = body.channel;
918 var subscription = this.getSubscription(channel);
919 if (!subscription) {
920 return;
921 }
922 if (message.error === null) {
923 subscription.trigger('history', [body]);
924 subscription.trigger('history:success', [body]);
925 } else {
926 subscription.trigger('history:error', [message.error]);
927 this.trigger('error', [message]);
928 }
929 };
930
931 centrifuge_proto._joinResponse = function(message) {
932 var body = message.body;
933 var channel = body.channel;
934 var subscription = this.getSubscription(channel);
935 if (!subscription) {
936 return;
937 }
938 subscription.trigger('join', [body]);
939 };
940
941 centrifuge_proto._leaveResponse = function(message) {
942 var body = message.body;
943 var channel = body.channel;
944 var subscription = this.getSubscription(channel);
945 if (!subscription) {
946 return;
947 }
948 subscription.trigger('leave', [body]);
949 };
950
951 centrifuge_proto._messageResponse = function (message) {
952 var body = message.body;
953 var channel = body.channel;
954 var subscription = this.getSubscription(channel);
955 if (subscription === null) {
956 return;
957 }
958 subscription.trigger('message', [body]);
959 };
960
961 centrifuge_proto._dispatchMessage = function(message) {
962 if (message === undefined || message === null) {
963 return;
964 }
965
966 var method = message.method;
967
968 if (!method) {
969 return;
970 }
971
972 switch (method) {
973 case 'connect':
974 this._connectResponse(message);
975 break;
976 case 'disconnect':
977 this._disconnectResponse(message);
978 break;
979 case 'subscribe':
980 this._subscribeResponse(message);
981 break;
982 case 'unsubscribe':
983 this._unsubscribeResponse(message);
984 break;
985 case 'publish':
986 this._publishResponse(message);
987 break;
988 case 'presence':
989 this._presenceResponse(message);
990 break;
991 case 'history':
992 this._historyResponse(message);
993 break;
994 case 'join':
995 this._joinResponse(message);
996 break;
997 case 'leave':
998 this._leaveResponse(message);
999 break;
1000 case 'ping':
1001 break;
1002 case 'message':
1003 this._messageResponse(message);
1004 break;
1005 default:
1006 break;
1007 }
1008 };
1009
1010 centrifuge_proto._receive = function (data) {
1011 if (Object.prototype.toString.call(data) === Object.prototype.toString.call([])) {
1012 for (var i in data) {
1013 if (data.hasOwnProperty(i)) {
1014 var msg = data[i];
1015 this._dispatchMessage(msg);
1016 }
1017 }
1018 } else if (Object.prototype.toString.call(data) === Object.prototype.toString.call({})) {
1019 this._dispatchMessage(data);
1020 }
1021 };
1022
1023 centrifuge_proto._flush = function() {
1024 var messages = this._messages.slice(0);
1025 this._messages = [];
1026 this._send(messages);
1027 };
1028
1029 centrifuge_proto._ping = function () {
1030 var centrifugeMessage = {
1031 "method": "ping",
1032 "params": {}
1033 };
1034 this.send(centrifugeMessage);
1035 };
1036
1037 /* PUBLIC API */
1038
1039 centrifuge_proto.getClientId = function () {
1040 return this._clientId;
1041 };
1042
1043 centrifuge_proto.isConnected = centrifuge_proto._isConnected;
1044
1045 centrifuge_proto.isDisconnected = centrifuge_proto._isDisconnected;
1046
1047 centrifuge_proto.configure = function (configuration) {
1048 this._configure.call(this, configuration);
1049 };
1050
1051 centrifuge_proto.connect = centrifuge_proto._connect;
1052
1053 centrifuge_proto.disconnect = centrifuge_proto._disconnect;
1054
1055 centrifuge_proto.getSubscription = centrifuge_proto._getSubscription;
1056
1057 centrifuge_proto.ping = centrifuge_proto._ping;
1058
1059 centrifuge_proto.send = function (message) {
1060 if (this._isBatching === true) {
1061 this._messages.push(message);
1062 } else {
1063 this._send([message]);
1064 }
1065 };
1066
1067 centrifuge_proto.startBatching = function () {
1068 // start collecting messages without sending them to Centrifuge until flush
1069 // method called
1070 this._isBatching = true;
1071 };
1072
1073 centrifuge_proto.stopBatching = function(flush) {
1074 // stop collecting messages
1075 flush = flush || false;
1076 this._isBatching = false;
1077 if (flush === true) {
1078 this.flush();
1079 }
1080 };
1081
1082 centrifuge_proto.flush = function() {
1083 this._flush();
1084 };
1085
1086 centrifuge_proto.subscribe = function (channel, callback) {
1087
1088 if (arguments.length < 1) {
1089 throw 'Illegal arguments number: required 1, got ' + arguments.length;
1090 }
1091 if (!isString(channel)) {
1092 throw 'Illegal argument type: channel must be a string';
1093 }
1094 if (this.isDisconnected()) {
1095 throw 'Illegal state: already disconnected';
1096 }
1097
1098 var current_subscription = this.getSubscription(channel);
1099
1100 if (current_subscription !== null) {
1101 return current_subscription;
1102 } else {
1103 var subscription = new Subscription(this, channel);
1104 this._subscriptions[channel] = subscription;
1105 subscription.subscribe(callback);
1106 return subscription;
1107 }
1108 };
1109
1110 centrifuge_proto.unsubscribe = function (channel) {
1111 if (arguments.length < 1) {
1112 throw 'Illegal arguments number: required 1, got ' + arguments.length;
1113 }
1114 if (!isString(channel)) {
1115 throw 'Illegal argument type: channel must be a string';
1116 }
1117 if (this.isDisconnected()) {
1118 return;
1119 }
1120
1121 var subscription = this.getSubscription(channel);
1122 if (subscription !== null) {
1123 subscription.unsubscribe();
1124 }
1125 };
1126
1127 centrifuge_proto.publish = function (channel, data, callback) {
1128 var subscription = this.getSubscription(channel);
1129 if (subscription === null) {
1130 this._debug("subscription not found for channel " + channel);
1131 return null;
1132 }
1133 subscription.publish(data, callback);
1134 return subscription;
1135 };
1136
1137 centrifuge_proto.presence = function (channel, callback) {
1138 var subscription = this.getSubscription(channel);
1139 if (subscription === null) {
1140 this._debug("subscription not found for channel " + channel);
1141 return null;
1142 }
1143 subscription.presence(callback);
1144 return subscription;
1145 };
1146
1147 centrifuge_proto.history = function (channel, callback) {
1148 var subscription = this.getSubscription(channel);
1149 if (subscription === null) {
1150 this._debug("subscription not found for channel " + channel);
1151 return null;
1152 }
1153 subscription.history(callback);
1154 return subscription;
1155 };
1156
1157 function Subscription(centrifuge, channel) {
1158 /**
1159 * The constructor for a centrifuge object, identified by an optional name.
1160 * The default name is the string 'default'.
1161 * @param name the optional name of this centrifuge object
1162 */
1163 this._centrifuge = centrifuge;
1164 this.channel = channel;
1165 }
1166
1167 extend(Subscription, EventEmitter);
1168
1169 var sub_proto = Subscription.prototype;
1170
1171 sub_proto.getChannel = function () {
1172 return this.channel;
1173 };
1174
1175 sub_proto.getCentrifuge = function () {
1176 return this._centrifuge;
1177 };
1178
1179 sub_proto.subscribe = function (callback) {
1180 var centrifugeMessage = {
1181 "method": "subscribe",
1182 "params": {
1183 "channel": this.channel
1184 }
1185 };
1186 this._centrifuge.send(centrifugeMessage);
1187 if (callback) {
1188 this.on('message', callback);
1189 }
1190 };
1191
1192 sub_proto.unsubscribe = function () {
1193 this._centrifuge._removeSubscription(this.channel);
1194 var centrifugeMessage = {
1195 "method": "unsubscribe",
1196 "params": {
1197 "channel": this.channel
1198 }
1199 };
1200 this._centrifuge.send(centrifugeMessage);
1201 };
1202
1203 sub_proto.publish = function (data, callback) {
1204 var centrifugeMessage = {
1205 "method": "publish",
1206 "params": {
1207 "channel": this.channel,
1208 "data": data
1209 }
1210 };
1211 if (callback) {
1212 this.on('publish:success', callback);
1213 }
1214 this._centrifuge.send(centrifugeMessage);
1215 };
1216
1217 sub_proto.presence = function (callback) {
1218 var centrifugeMessage = {
1219 "method": "presence",
1220 "params": {
1221 "channel": this.channel
1222 }
1223 };
1224 if (callback) {
1225 this.on('presence', callback);
1226 }
1227 this._centrifuge.send(centrifugeMessage);
1228 };
1229
1230 sub_proto.history = function (callback) {
1231 var centrifugeMessage = {
1232 "method": "history",
1233 "params": {
1234 "channel": this.channel
1235 }
1236 };
1237 if (callback) {
1238 this.on('history', callback);
1239 }
1240 this._centrifuge.send(centrifugeMessage);
1241 };
1242
1243 // Expose the class either via AMD, CommonJS or the global object
1244 if (typeof define === 'function' && define.amd) {
1245 define(function () {
1246 return Centrifuge;
1247 });
1248 } else if (typeof module === 'object' && module.exports) {
1249 //noinspection JSUnresolvedVariable
1250 module.exports = Centrifuge;
1251 } else {
1252 //noinspection JSUnusedGlobalSymbols
1253 this.Centrifuge = Centrifuge;
1254 }
1255
1256 }.call(this));
@@ -0,0 +1,27 b''
1 from django.test import TestCase
2 from boards.models import Post
3
4
5 class ParserTest(TestCase):
6 def test_preparse_quote(self):
7 raw_text = '>quote\nQuote in >line\nLine\n>Quote'
8 preparsed_text = Post.objects._preparse_text(raw_text)
9
10 self.assertEqual(
11 '[quote]quote[/quote]\nQuote in >line\nLine\n[quote]Quote[/quote]',
12 preparsed_text, 'Quote not preparsed.')
13
14 def test_preparse_comment(self):
15 raw_text = '//comment'
16 preparsed_text = Post.objects._preparse_text(raw_text)
17
18 self.assertEqual('[comment]comment[/comment]', preparsed_text,
19 'Comment not preparsed.')
20
21 def test_preparse_reflink(self):
22 raw_text = '>>12\nText'
23 preparsed_text = Post.objects._preparse_text(raw_text)
24
25 self.assertEqual('[post]12[/post]\nText',
26 preparsed_text, 'Reflink not preparsed.')
27
@@ -0,0 +1,10 b''
1 [Unit]
2 Description=Neboard imageboard
3 After=network.target
4
5 [Service]
6 ExecStart=/usr/bin/uwsgi_python33 --ini uwsgi.ini
7 WorkingDirectory=<where is your neboard located>
8
9 [Install]
10 WantedBy=multi-user.target
@@ -0,0 +1,11 b''
1 [uwsgi]
2 module = neboard.wsgi:application
3 master = true
4 pidfile = /tmp/neboard.pid
5 socket = 127.0.0.1:8080
6 processes = 5
7 harakiri = 20
8 max-requests = 5000
9 disable-logging = true
10 vacuum = true
11 # socket=/var/run/neboard.sock
@@ -16,3 +16,7 b' 7f7c33ba6e3f3797ca866c5ed5d358a2393f1371'
16 a6b9dd9547bdc17b681502efcceb17aa5c09adf4 1.8.1
16 a6b9dd9547bdc17b681502efcceb17aa5c09adf4 1.8.1
17 8318fa1615d1946e4519f5735ae880909521990d 2.0
17 8318fa1615d1946e4519f5735ae880909521990d 2.0
18 e23590ee7e2067a3f0fc3cbcfd66404b47127feb 2.1
18 e23590ee7e2067a3f0fc3cbcfd66404b47127feb 2.1
19 4d998aba79e4abf0a2e78e93baaa2c2800b1c49c 2.2
20 07fdef4ac33a859250d03f17c594089792bca615 2.2.1
21 bcc74d45f060ecd3ff06ff448165aea0d026cb3e 2.2.2
22 b0e629ff24eb47a449ecfb455dc6cc600d18c9e2 2.2.3
@@ -5,6 +5,7 b' from boards.models import Tag'
5
5
6 SESSION_SETTING = 'setting'
6 SESSION_SETTING = 'setting'
7
7
8 # Remove this, it is not used any more cause there is a user's permission
8 PERMISSION_MODERATE = 'moderator'
9 PERMISSION_MODERATE = 'moderator'
9
10
10 SETTING_THEME = 'theme'
11 SETTING_THEME = 'theme'
@@ -2,42 +2,53 b' from django.contrib import admin'
2 from boards.models import Post, Tag, Ban, Thread, KeyPair
2 from boards.models import Post, Tag, Ban, Thread, KeyPair
3
3
4
4
5 @admin.register(Post)
5 class PostAdmin(admin.ModelAdmin):
6 class PostAdmin(admin.ModelAdmin):
6
7
7 list_display = ('id', 'title', 'text')
8 list_display = ('id', 'title', 'text')
8 list_filter = ('pub_time', 'thread_new')
9 list_filter = ('pub_time', 'thread_new')
9 search_fields = ('id', 'title', 'text')
10 search_fields = ('id', 'title', 'text')
11 exclude = ('referenced_posts', 'refmap')
12 readonly_fields = ('poster_ip', 'thread_new')
10
13
11
14
15 @admin.register(Tag)
12 class TagAdmin(admin.ModelAdmin):
16 class TagAdmin(admin.ModelAdmin):
13
17
14 list_display = ('name',)
18 def thread_count(self, obj: Tag) -> int:
19 return obj.get_thread_count()
15
20
21 list_display = ('name', 'thread_count')
22 search_fields = ('name',)
23
24
25 @admin.register(Thread)
16 class ThreadAdmin(admin.ModelAdmin):
26 class ThreadAdmin(admin.ModelAdmin):
17
27
18 def title(self, obj):
28 def title(self, obj: Thread) -> str:
19 return obj.get_opening_post().title
29 return obj.get_opening_post().get_title()
20
30
21 def reply_count(self, obj):
31 def reply_count(self, obj: Thread) -> int:
22 return obj.get_reply_count()
32 return obj.get_reply_count()
23
33
24 list_display = ('id', 'title', 'reply_count', 'archived')
34 def ip(self, obj: Thread):
25 list_filter = ('bump_time', 'archived')
35 return obj.get_opening_post().poster_ip
36
37 list_display = ('id', 'title', 'reply_count', 'archived', 'ip')
38 list_filter = ('bump_time', 'archived', 'bumpable')
26 search_fields = ('id', 'title')
39 search_fields = ('id', 'title')
40 filter_horizontal = ('tags',)
27
41
28
42
43 @admin.register(KeyPair)
29 class KeyPairAdmin(admin.ModelAdmin):
44 class KeyPairAdmin(admin.ModelAdmin):
30 list_display = ('public_key', 'primary')
45 list_display = ('public_key', 'primary')
31 list_filter = ('primary',)
46 list_filter = ('primary',)
32 search_fields = ('public_key',)
47 search_fields = ('public_key',)
33
48
49
50 @admin.register(Ban)
34 class BanAdmin(admin.ModelAdmin):
51 class BanAdmin(admin.ModelAdmin):
35 list_display = ('ip', 'can_read')
52 list_display = ('ip', 'can_read')
36 list_filter = ('can_read',)
53 list_filter = ('can_read',)
37 search_fields = ('ip',)
54 search_fields = ('ip',)
38
39 admin.site.register(Post, PostAdmin)
40 admin.site.register(Tag, TagAdmin)
41 admin.site.register(Ban, BanAdmin)
42 admin.site.register(Thread, ThreadAdmin)
43 admin.site.register(KeyPair, KeyPairAdmin)
@@ -1,5 +1,4 b''
1 from boards.abstracts.settingsmanager import PERMISSION_MODERATE, \
1 from boards.abstracts.settingsmanager import get_settings_manager
2 get_settings_manager
3
2
4 __author__ = 'neko259'
3 __author__ = 'neko259'
5
4
@@ -19,7 +18,7 b" PERMISSION_MODERATE = 'moderation'"
19
18
20
19
21 def user_and_ui_processor(request):
20 def user_and_ui_processor(request):
22 context = {}
21 context = dict()
23
22
24 context[CONTEXT_PPD] = float(Post.objects.get_posts_per_day())
23 context[CONTEXT_PPD] = float(Post.objects.get_posts_per_day())
25
24
@@ -31,7 +30,7 b' def user_and_ui_processor(request):'
31
30
32 # This shows the moderator panel
31 # This shows the moderator panel
33 try:
32 try:
34 moderate = request.user.has_perm('moderation')
33 moderate = request.user.has_perm(PERMISSION_MODERATE)
35 except AttributeError:
34 except AttributeError:
36 moderate = False
35 moderate = False
37 context[CONTEXT_MODERATOR] = moderate
36 context[CONTEXT_MODERATOR] = moderate
@@ -8,7 +8,7 b' from django.utils.translation import uge'
8
8
9 from boards.mdx_neboard import formatters
9 from boards.mdx_neboard import formatters
10 from boards.models.post import TITLE_MAX_LENGTH
10 from boards.models.post import TITLE_MAX_LENGTH
11 from boards.models import PostImage
11 from boards.models import PostImage, Tag
12 from neboard import settings
12 from neboard import settings
13 from boards import utils
13 from boards import utils
14 import boards.settings as board_settings
14 import boards.settings as board_settings
@@ -216,6 +216,17 b' class ThreadForm(PostForm):'
216 raise forms.ValidationError(
216 raise forms.ValidationError(
217 _('Inappropriate characters in tags.'))
217 _('Inappropriate characters in tags.'))
218
218
219 tag_models = []
220 required_tag_exists = False
221 for tag in tags.split():
222 tag_model = Tag.objects.filter(name=tag.strip().lower(),
223 required=True)
224 if tag_model.exists():
225 required_tag_exists = True
226
227 if not required_tag_exists:
228 raise forms.ValidationError(_('Need at least 1 required tag.'))
229
219 return tags
230 return tags
220
231
221 def clean(self):
232 def clean(self):
1 NO CONTENT: modified file, binary diff hidden
NO CONTENT: modified file, binary diff hidden
@@ -7,7 +7,7 b' msgid ""'
7 msgstr ""
7 msgstr ""
8 "Project-Id-Version: PACKAGE VERSION\n"
8 "Project-Id-Version: PACKAGE VERSION\n"
9 "Report-Msgid-Bugs-To: \n"
9 "Report-Msgid-Bugs-To: \n"
10 "POT-Creation-Date: 2014-08-19 15:51+0300\n"
10 "POT-Creation-Date: 2015-01-08 16:36+0200\n"
11 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
11 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
12 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
12 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
13 "Language-Team: LANGUAGE <LL@li.org>\n"
13 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -41,7 +41,7 b' msgstr ""'
41
41
42 #: forms.py:23
42 #: forms.py:23
43 msgid "tag1 several_words_tag"
43 msgid "tag1 several_words_tag"
44 msgstr "тег1 тег_из_нескольких_слов"
44 msgstr "метка1 метка_из_нескольких_слов"
45
45
46 #: forms.py:25
46 #: forms.py:25
47 msgid "Such image was already posted"
47 msgid "Such image was already posted"
@@ -57,9 +57,9 b' msgstr "\xd0\xa2\xd0\xb5\xd0\xba\xd1\x81\xd1\x82"'
57
57
58 #: forms.py:29
58 #: forms.py:29
59 msgid "Tag"
59 msgid "Tag"
60 msgstr "Тег"
60 msgstr "Метка"
61
61
62 #: forms.py:30 templates/boards/base.html:36 templates/search/search.html:9
62 #: forms.py:30 templates/boards/base.html:38 templates/search/search.html:9
63 #: templates/search/search.html.py:13
63 #: templates/search/search.html.py:13
64 msgid "Search"
64 msgid "Search"
65 msgstr "Поиск"
65 msgstr "Поиск"
@@ -91,28 +91,32 b' msgstr "\xd0\x98\xd0\xb7\xd0\xbe\xd0\xb1\xd1\x80\xd0\xb0\xd0\xb6\xd0\xb5\xd0\xbd\xd0\xb8\xd0\xb5 \xd0\xb4\xd0\xbe\xd0\xbb\xd0\xb6\xd0\xbd\xd0\xbe \xd0\xb1\xd1\x8b\xd1\x82\xd1\x8c \xd0\xbc\xd0\xb5\xd0\xbd\xd0\xb5\xd0\xb5 %s \xd0\xb1\xd0\xb0\xd0\xb9\xd1\x82"'
91 msgid "Either text or image must be entered."
91 msgid "Either text or image must be entered."
92 msgstr "Текст или картинка должны быть введены."
92 msgstr "Текст или картинка должны быть введены."
93
93
94 #: forms.py:198
94 #: forms.py:194
95 #, python-format
95 #, python-format
96 msgid "Wait %s seconds after last posting"
96 msgid "Wait %s seconds after last posting"
97 msgstr "Подождите %s секунд после последнего постинга"
97 msgstr "Подождите %s секунд после последнего постинга"
98
98
99 #: forms.py:214 templates/boards/tags.html:7 templates/boards/rss/post.html:10
99 #: forms.py:210 templates/boards/rss/post.html:10 templates/boards/tags.html:7
100 msgid "Tags"
100 msgid "Tags"
101 msgstr "Теги"
101 msgstr "Метки"
102
102
103 #: forms.py:221 forms.py:247
103 #: forms.py:217 forms.py:254
104 msgid "Inappropriate characters in tags."
104 msgid "Inappropriate characters in tags."
105 msgstr "Недопустимые символы в тегах."
105 msgstr "Недопустимые символы в метках."
106
106
107 #: forms.py:234
107 #: forms.py:228
108 msgid "Need at least 1 required tag."
109 msgstr "Нужна хотя бы 1 обязательная метка."
110
111 #: forms.py:241
108 msgid "Theme"
112 msgid "Theme"
109 msgstr "Тема"
113 msgstr "Тема"
110
114
111 #: forms.py:270
115 #: forms.py:277
112 msgid "Invalid master password"
116 msgid "Invalid master password"
113 msgstr "Неверный мастер-пароль"
117 msgstr "Неверный мастер-пароль"
114
118
115 #: forms.py:284
119 #: forms.py:291
116 #, python-format
120 #, python-format
117 msgid "Wait %s minutes after last login"
121 msgid "Wait %s minutes after last login"
118 msgstr "Подождите %s минут после последнего входа"
122 msgstr "Подождите %s минут после последнего входа"
@@ -141,32 +145,32 b' msgstr "\xd0\xbb\xd0\xb8\xd1\x86\xd0\xb5\xd0\xbd\xd0\xb7\xd0\xb8\xd0\xb5\xd0\xb9"'
141 msgid "Repository"
145 msgid "Repository"
142 msgstr "Репозиторий"
146 msgstr "Репозиторий"
143
147
144 #: templates/boards/base.html:12
148 #: templates/boards/base.html:13
145 msgid "Feed"
149 msgid "Feed"
146 msgstr "Лента"
150 msgstr "Лента"
147
151
148 #: templates/boards/base.html:29
152 #: templates/boards/base.html:30
149 msgid "All threads"
153 msgid "All threads"
150 msgstr "Все темы"
154 msgstr "Все темы"
151
155
152 #: templates/boards/base.html:34
156 #: templates/boards/base.html:36
153 msgid "Tag management"
157 msgid "Tag management"
154 msgstr "Управление тегами"
158 msgstr "Управление метками"
155
159
156 #: templates/boards/base.html:37 templates/boards/settings.html:7
160 #: templates/boards/base.html:39 templates/boards/settings.html:7
157 msgid "Settings"
161 msgid "Settings"
158 msgstr "Настройки"
162 msgstr "Настройки"
159
163
160 #: templates/boards/base.html:50
164 #: templates/boards/base.html:52
161 msgid "Admin"
165 msgid "Admin"
162 msgstr ""
166 msgstr ""
163
167
164 #: templates/boards/base.html:52
168 #: templates/boards/base.html:54
165 #, python-format
169 #, python-format
166 msgid "Speed: %(ppd)s posts per day"
170 msgid "Speed: %(ppd)s posts per day"
167 msgstr "Скорость: %(ppd)s сообщений в день"
171 msgstr "Скорость: %(ppd)s сообщений в день"
168
172
169 #: templates/boards/base.html:54
173 #: templates/boards/base.html:56
170 msgid "Up"
174 msgid "Up"
171 msgstr "Вверх"
175 msgstr "Вверх"
172
176
@@ -178,96 +182,96 b' msgstr "\xd0\x92\xd1\x85\xd0\xbe\xd0\xb4"'
178 msgid "Insert your user id above"
182 msgid "Insert your user id above"
179 msgstr "Вставьте свой ID пользователя выше"
183 msgstr "Вставьте свой ID пользователя выше"
180
184
181 #: templates/boards/post.html:21 templates/boards/staticpages/help.html:17
185 #: templates/boards/post.html:19 templates/boards/staticpages/help.html:17
182 msgid "Quote"
186 msgid "Quote"
183 msgstr "Цитата"
187 msgstr "Цитата"
184
188
185 #: templates/boards/post.html:31
189 #: templates/boards/post.html:27
186 msgid "Open"
190 msgid "Open"
187 msgstr "Открыть"
191 msgstr "Открыть"
188
192
189 #: templates/boards/post.html:33
193 #: templates/boards/post.html:29
190 msgid "Reply"
194 msgid "Reply"
191 msgstr "Ответ"
195 msgstr "Ответ"
192
196
193 #: templates/boards/post.html:40
197 #: templates/boards/post.html:36
194 msgid "Edit"
198 msgid "Edit"
195 msgstr "Изменить"
199 msgstr "Изменить"
196
200
197 #: templates/boards/post.html:42
201 #: templates/boards/post.html:39
198 msgid "Delete"
202 msgid "Edit thread"
199 msgstr "Удалить"
203 msgstr "Изменить тему"
200
204
201 #: templates/boards/post.html:45
205 #: templates/boards/post.html:71
202 msgid "Ban IP"
203 msgstr "Заблокировать IP"
204
205 #: templates/boards/post.html:76
206 msgid "Replies"
206 msgid "Replies"
207 msgstr "Ответы"
207 msgstr "Ответы"
208
208
209 #: templates/boards/post.html:86 templates/boards/thread.html:88
209 #: templates/boards/post.html:79 templates/boards/thread.html:89
210 #: templates/boards/thread_gallery.html:59
210 #: templates/boards/thread_gallery.html:59
211 msgid "messages"
211 msgid "messages"
212 msgstr "сообщений"
212 msgstr "сообщений"
213
213
214 #: templates/boards/post.html:87 templates/boards/thread.html:89
214 #: templates/boards/post.html:80 templates/boards/thread.html:90
215 #: templates/boards/thread_gallery.html:60
215 #: templates/boards/thread_gallery.html:60
216 msgid "images"
216 msgid "images"
217 msgstr "изображений"
217 msgstr "изображений"
218
218
219 #: templates/boards/post_admin.html:19
219 #: templates/boards/post_admin.html:19
220 msgid "Tags:"
220 msgid "Tags:"
221 msgstr "Теги:"
221 msgstr "Метки:"
222
222
223 #: templates/boards/post_admin.html:30
223 #: templates/boards/post_admin.html:30
224 msgid "Add tag"
224 msgid "Add tag"
225 msgstr "Добавить тег"
225 msgstr "Добавить метку"
226
226
227 #: templates/boards/posting_general.html:56
227 #: templates/boards/posting_general.html:56
228 msgid "Show tag"
228 msgid "Show tag"
229 msgstr "Показывать тег"
229 msgstr "Показывать метку"
230
230
231 #: templates/boards/posting_general.html:60
231 #: templates/boards/posting_general.html:60
232 msgid "Hide tag"
232 msgid "Hide tag"
233 msgstr "Скрывать тег"
233 msgstr "Скрывать метку"
234
234
235 #: templates/boards/posting_general.html:79 templates/search/search.html:22
235 #: templates/boards/posting_general.html:66
236 msgid "Edit tag"
237 msgstr "Изменить метку"
238
239 #: templates/boards/posting_general.html:82 templates/search/search.html:22
236 msgid "Previous page"
240 msgid "Previous page"
237 msgstr "Предыдущая страница"
241 msgstr "Предыдущая страница"
238
242
239 #: templates/boards/posting_general.html:94
243 #: templates/boards/posting_general.html:97
240 #, python-format
244 #, python-format
241 msgid "Skipped %(count)s replies. Open thread to see all replies."
245 msgid "Skipped %(count)s replies. Open thread to see all replies."
242 msgstr "Пропущено %(count)s ответов. Откройте тред, чтобы увидеть все ответы."
246 msgstr "Пропущено %(count)s ответов. Откройте тред, чтобы увидеть все ответы."
243
247
244 #: templates/boards/posting_general.html:121 templates/search/search.html:33
248 #: templates/boards/posting_general.html:124 templates/search/search.html:33
245 msgid "Next page"
249 msgid "Next page"
246 msgstr "Следующая страница"
250 msgstr "Следующая страница"
247
251
248 #: templates/boards/posting_general.html:126
252 #: templates/boards/posting_general.html:129
249 msgid "No threads exist. Create the first one!"
253 msgid "No threads exist. Create the first one!"
250 msgstr "Нет тем. Создайте первую!"
254 msgstr "Нет тем. Создайте первую!"
251
255
252 #: templates/boards/posting_general.html:132
256 #: templates/boards/posting_general.html:135
253 msgid "Create new thread"
257 msgid "Create new thread"
254 msgstr "Создать новую тему"
258 msgstr "Создать новую тему"
255
259
256 #: templates/boards/posting_general.html:137 templates/boards/preview.html:16
260 #: templates/boards/posting_general.html:140 templates/boards/preview.html:16
257 #: templates/boards/thread.html:58
261 #: templates/boards/thread.html:54
258 msgid "Post"
262 msgid "Post"
259 msgstr "Отправить"
263 msgstr "Отправить"
260
264
261 #: templates/boards/posting_general.html:142
265 #: templates/boards/posting_general.html:145
262 msgid "Tags must be delimited by spaces. Text or image is required."
266 msgid "Tags must be delimited by spaces. Text or image is required."
263 msgstr ""
267 msgstr ""
264 "Теги должны быть разделены пробелами. Текст или изображение обязательны."
268 "Метки должны быть разделены пробелами. Текст или изображение обязательны."
265
269
266 #: templates/boards/posting_general.html:145 templates/boards/thread.html:66
270 #: templates/boards/posting_general.html:148 templates/boards/thread.html:62
267 msgid "Text syntax"
271 msgid "Text syntax"
268 msgstr "Синтаксис текста"
272 msgstr "Синтаксис текста"
269
273
270 #: templates/boards/posting_general.html:157
274 #: templates/boards/posting_general.html:160
271 msgid "Pages:"
275 msgid "Pages:"
272 msgstr "Страницы: "
276 msgstr "Страницы: "
273
277
@@ -275,54 +279,26 b' msgstr "\xd0\xa1\xd1\x82\xd1\x80\xd0\xb0\xd0\xbd\xd0\xb8\xd1\x86\xd1\x8b: "'
275 msgid "Preview"
279 msgid "Preview"
276 msgstr "Предпросмотр"
280 msgstr "Предпросмотр"
277
281
282 #: templates/boards/rss/post.html:5
283 msgid "Post image"
284 msgstr "Изображение сообщения"
285
278 #: templates/boards/settings.html:15
286 #: templates/boards/settings.html:15
279 msgid "You are moderator."
287 msgid "You are moderator."
280 msgstr "Вы модератор."
288 msgstr "Вы модератор."
281
289
282 #: templates/boards/settings.html:19
290 #: templates/boards/settings.html:19
283 msgid "Hidden tags:"
291 msgid "Hidden tags:"
284 msgstr "Скрытые теги:"
292 msgstr "Скрытые метки:"
285
293
286 #: templates/boards/settings.html:26
294 #: templates/boards/settings.html:26
287 msgid "No hidden tags."
295 msgid "No hidden tags."
288 msgstr "Нет скрытых тегов."
296 msgstr "Нет скрытых меток."
289
297
290 #: templates/boards/settings.html:35
298 #: templates/boards/settings.html:35
291 msgid "Save"
299 msgid "Save"
292 msgstr "Сохранить"
300 msgstr "Сохранить"
293
301
294 #: templates/boards/tags.html:22
295 msgid "No tags found."
296 msgstr "Теги не найдены."
297
298 #: templates/boards/thread.html:20 templates/boards/thread_gallery.html:19
299 msgid "Normal mode"
300 msgstr "Нормальный режим"
301
302 #: templates/boards/thread.html:21 templates/boards/thread_gallery.html:20
303 msgid "Gallery mode"
304 msgstr "Режим галереи"
305
306 #: templates/boards/thread.html:29
307 msgid "posts to bumplimit"
308 msgstr "сообщений до бамплимита"
309
310 #: templates/boards/thread.html:50
311 msgid "Reply to thread"
312 msgstr "Ответить в тему"
313
314 #: templates/boards/thread.html:63
315 msgid "Switch mode"
316 msgstr "Переключить режим"
317
318 #: templates/boards/thread.html:90 templates/boards/thread_gallery.html:61
319 msgid "Last update: "
320 msgstr "Последнее обновление: "
321
322 #: templates/boards/rss/post.html:5
323 msgid "Post image"
324 msgstr "Изображение сообщения"
325
326 #: templates/boards/staticpages/banned.html:6
302 #: templates/boards/staticpages/banned.html:6
327 msgid "Banned"
303 msgid "Banned"
328 msgstr "Заблокирован"
304 msgstr "Заблокирован"
@@ -363,3 +339,32 b' msgstr "\xd0\x9a\xd0\xbe\xd0\xbc\xd0\xbc\xd0\xb5\xd0\xbd\xd1\x82\xd0\xb0\xd1\x80\xd0\xb8\xd0\xb9"'
363 #: templates/boards/staticpages/help.html:19
339 #: templates/boards/staticpages/help.html:19
364 msgid "You can try pasting the text and previewing the result here:"
340 msgid "You can try pasting the text and previewing the result here:"
365 msgstr "Вы можете попробовать вставить текст и проверить результат здесь:"
341 msgstr "Вы можете попробовать вставить текст и проверить результат здесь:"
342
343 #: templates/boards/tags.html:23
344 msgid "No tags found."
345 msgstr "Метки не найдены."
346
347 #: templates/boards/thread.html:19 templates/boards/thread_gallery.html:19
348 msgid "Normal mode"
349 msgstr "Нормальный режим"
350
351 #: templates/boards/thread.html:20 templates/boards/thread_gallery.html:20
352 msgid "Gallery mode"
353 msgstr "Режим галереи"
354
355 #: templates/boards/thread.html:28
356 msgid "posts to bumplimit"
357 msgstr "сообщений до бамплимита"
358
359 #: templates/boards/thread.html:46
360 msgid "Reply to thread"
361 msgstr "Ответить в тему"
362
363 #: templates/boards/thread.html:59
364 msgid "Switch mode"
365 msgstr "Переключить режим"
366
367 #: templates/boards/thread.html:91 templates/boards/thread_gallery.html:61
368 msgid "Last update: "
369 msgstr "Последнее обновление: "
370
@@ -177,15 +177,16 b' def bbcode_extended(markup):'
177 parser.add_formatter('post', render_reflink, strip=True)
177 parser.add_formatter('post', render_reflink, strip=True)
178 parser.add_formatter('quote', render_quote, strip=True)
178 parser.add_formatter('quote', render_quote, strip=True)
179 parser.add_simple_formatter('comment',
179 parser.add_simple_formatter('comment',
180 u'<span class="comment">//%(value)s</span>')
180 '<span class="comment">//%(value)s</span>')
181 parser.add_simple_formatter('spoiler',
181 parser.add_simple_formatter('spoiler',
182 u'<span class="spoiler">%(value)s</span>')
182 '<span class="spoiler">%(value)s</span>')
183 # TODO Use <s> here
183 # TODO Use <s> here
184 parser.add_simple_formatter('s',
184 parser.add_simple_formatter('s',
185 u'<span class="strikethrough">%(value)s</span>')
185 '<span class="strikethrough">%(value)s</span>')
186 # TODO Why not use built-in tag?
186 # TODO Why not use built-in tag?
187 parser.add_simple_formatter('code',
187 parser.add_simple_formatter('code',
188 u'<pre><code>%(value)s</pre></code>', render_embedded=False)
188 '<pre><code>%(value)s</pre></code>',
189 render_embedded=False)
189
190
190 text = preparse_text(markup)
191 text = preparse_text(markup)
191 return parser.format(text)
192 return parser.format(text)
@@ -1,9 +1,6 b''
1 from django.shortcuts import redirect
1 from django.shortcuts import redirect
2 from boards import utils
2 from boards import utils
3 from boards.models import Ban
3 from boards.models import Ban
4 from django.utils.html import strip_spaces_between_tags
5 from django.conf import settings
6 from boards.views.banned import BannedView
7
4
8 RESPONSE_CONTENT_TYPE = 'Content-Type'
5 RESPONSE_CONTENT_TYPE = 'Content-Type'
9
6
@@ -21,7 +18,7 b' class BanMiddleware:'
21
18
22 def process_view(self, request, view_func, view_args, view_kwargs):
19 def process_view(self, request, view_func, view_args, view_kwargs):
23
20
24 if view_func != BannedView.as_view:
21 if request.path != '/banned/':
25 ip = utils.get_client_ip(request)
22 ip = utils.get_client_ip(request)
26 bans = Ban.objects.filter(ip=ip)
23 bans = Ban.objects.filter(ip=ip)
27
24
@@ -29,18 +26,3 b' class BanMiddleware:'
29 ban = bans[0]
26 ban = bans[0]
30 if not ban.can_read:
27 if not ban.can_read:
31 return redirect('banned')
28 return redirect('banned')
32
33
34 class MinifyHTMLMiddleware(object):
35 def process_response(self, request, response):
36 try:
37 compress_html = settings.COMPRESS_HTML
38 except AttributeError:
39 compress_html = False
40
41 if RESPONSE_CONTENT_TYPE in response\
42 and TYPE_HTML in response[RESPONSE_CONTENT_TYPE]\
43 and compress_html:
44 response.content = strip_spaces_between_tags(
45 response.content.strip())
46 return response No newline at end of file
@@ -1,95 +1,113 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 import datetime
2 from __future__ import unicode_literals
3 from south.db import db
3
4 from south.v2 import SchemaMigration
4 from django.db import models, migrations
5 from django.db import models
5 import boards.models.image
6 import boards.models.base
7 import boards.thumbs
6
8
7
9
8 class Migration(SchemaMigration):
10 class Migration(migrations.Migration):
9
10 def forwards(self, orm):
11 # Adding model 'Tag'
12 db.create_table(u'boards_tag', (
13 (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
14 ('name', self.gf('django.db.models.fields.CharField')(max_length=100)),
15 ))
16 db.send_create_signal(u'boards', ['Tag'])
17
11
18 # Adding model 'Post'
12 dependencies = [
19 db.create_table(u'boards_post', (
13 ]
20 (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
21 ('title', self.gf('django.db.models.fields.CharField')(max_length=50)),
22 ('pub_time', self.gf('django.db.models.fields.DateTimeField')()),
23 ('text', self.gf('markupfield.fields.MarkupField')(rendered_field=True)),
24 ('text_markup_type', self.gf('django.db.models.fields.CharField')(default='markdown', max_length=30)),
25 ('image', self.gf('boards.thumbs.ImageWithThumbsField')(max_length=100, blank=True)),
26 ('poster_ip', self.gf('django.db.models.fields.IPAddressField')(max_length=15)),
27 ('_text_rendered', self.gf('django.db.models.fields.TextField')()),
28 ('poster_user_agent', self.gf('django.db.models.fields.TextField')()),
29 ('parent', self.gf('django.db.models.fields.BigIntegerField')()),
30 ('last_edit_time', self.gf('django.db.models.fields.DateTimeField')()),
31 ))
32 db.send_create_signal(u'boards', ['Post'])
33
34 # Adding M2M table for field tags on 'Post'
35 m2m_table_name = db.shorten_name(u'boards_post_tags')
36 db.create_table(m2m_table_name, (
37 ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
38 ('post', models.ForeignKey(orm[u'boards.post'], null=False)),
39 ('tag', models.ForeignKey(orm[u'boards.tag'], null=False))
40 ))
41 db.create_unique(m2m_table_name, ['post_id', 'tag_id'])
42
43 # Adding model 'Admin'
44 db.create_table(u'boards_admin', (
45 (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
46 ('name', self.gf('django.db.models.fields.CharField')(max_length=100)),
47 ('password', self.gf('django.db.models.fields.CharField')(max_length=100)),
48 ))
49 db.send_create_signal(u'boards', ['Admin'])
50
51
14
52 def backwards(self, orm):
15 operations = [
53 # Deleting model 'Tag'
16 migrations.CreateModel(
54 db.delete_table(u'boards_tag')
17 name='Ban',
55
18 fields=[
56 # Deleting model 'Post'
19 ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')),
57 db.delete_table(u'boards_post')
20 ('ip', models.GenericIPAddressField()),
58
21 ('reason', models.CharField(max_length=200, default='Auto')),
59 # Removing M2M table for field tags on 'Post'
22 ('can_read', models.BooleanField(default=True)),
60 db.delete_table(db.shorten_name(u'boards_post_tags'))
23 ],
61
24 options={
62 # Deleting model 'Admin'
25 },
63 db.delete_table(u'boards_admin')
26 bases=(models.Model,),
64
27 ),
65
28 migrations.CreateModel(
66 models = {
29 name='Post',
67 u'boards.admin': {
30 fields=[
68 'Meta': {'object_name': 'Admin'},
31 ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')),
69 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
32 ('title', models.CharField(max_length=200)),
70 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
33 ('pub_time', models.DateTimeField()),
71 'password': ('django.db.models.fields.CharField', [], {'max_length': '100'})
34 ('text', models.TextField(null=True, blank=True)),
72 },
35 ('text_markup_type', models.CharField(choices=[('', '--'), ('bbcode', 'bbcode')], max_length=30, default='bbcode')),
73 u'boards.post': {
36 ('poster_ip', models.GenericIPAddressField()),
74 'Meta': {'object_name': 'Post'},
37 ('_text_rendered', models.TextField(editable=False)),
75 '_text_rendered': ('django.db.models.fields.TextField', [], {}),
38 ('poster_user_agent', models.TextField()),
76 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
39 ('last_edit_time', models.DateTimeField()),
77 'image': ('boards.thumbs.ImageWithThumbsField', [], {'max_length': '100', 'blank': 'True'}),
40 ('refmap', models.TextField(null=True, blank=True)),
78 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}),
41 ],
79 'parent': ('django.db.models.fields.BigIntegerField', [], {}),
42 options={
80 'poster_ip': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}),
43 'ordering': ('id',),
81 'poster_user_agent': ('django.db.models.fields.TextField', [], {}),
44 },
82 'pub_time': ('django.db.models.fields.DateTimeField', [], {}),
45 bases=(models.Model, boards.models.base.Viewable),
83 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['boards.Tag']", 'symmetrical': 'False'}),
46 ),
84 'text': ('markupfield.fields.MarkupField', [], {'rendered_field': 'True'}),
47 migrations.CreateModel(
85 'text_markup_type': ('django.db.models.fields.CharField', [], {'default': "'markdown'", 'max_length': '30'}),
48 name='PostImage',
86 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'})
49 fields=[
87 },
50 ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')),
88 u'boards.tag': {
51 ('width', models.IntegerField(default=0)),
89 'Meta': {'object_name': 'Tag'},
52 ('height', models.IntegerField(default=0)),
90 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
53 ('pre_width', models.IntegerField(default=0)),
91 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
54 ('pre_height', models.IntegerField(default=0)),
92 }
55 ('image', boards.thumbs.ImageWithThumbsField(height_field='height', width_field='width', upload_to=boards.models.image.PostImage._update_image_filename, blank=True)),
93 }
56 ('hash', models.CharField(max_length=36)),
94
57 ],
95 complete_apps = ['boards'] No newline at end of file
58 options={
59 'ordering': ('id',),
60 },
61 bases=(models.Model,),
62 ),
63 migrations.CreateModel(
64 name='Tag',
65 fields=[
66 ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')),
67 ('name', models.CharField(db_index=True, max_length=100)),
68 ],
69 options={
70 'ordering': ('name',),
71 },
72 bases=(models.Model, boards.models.base.Viewable),
73 ),
74 migrations.CreateModel(
75 name='Thread',
76 fields=[
77 ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')),
78 ('bump_time', models.DateTimeField()),
79 ('last_edit_time', models.DateTimeField()),
80 ('archived', models.BooleanField(default=False)),
81 ('bumpable', models.BooleanField(default=True)),
82 ('replies', models.ManyToManyField(null=True, related_name='tre+', to='boards.Post', blank=True)),
83 ('tags', models.ManyToManyField(to='boards.Tag')),
84 ],
85 options={
86 },
87 bases=(models.Model,),
88 ),
89 migrations.AddField(
90 model_name='tag',
91 name='threads',
92 field=models.ManyToManyField(null=True, related_name='tag+', to='boards.Thread', blank=True),
93 preserve_default=True,
94 ),
95 migrations.AddField(
96 model_name='post',
97 name='images',
98 field=models.ManyToManyField(null=True, db_index=True, related_name='ip+', to='boards.PostImage', blank=True),
99 preserve_default=True,
100 ),
101 migrations.AddField(
102 model_name='post',
103 name='referenced_posts',
104 field=models.ManyToManyField(null=True, db_index=True, related_name='rfp+', to='boards.Post', blank=True),
105 preserve_default=True,
106 ),
107 migrations.AddField(
108 model_name='post',
109 name='thread_new',
110 field=models.ForeignKey(null=True, default=None, to='boards.Thread'),
111 preserve_default=True,
112 ),
113 ]
@@ -6,5 +6,13 b' class Viewable():'
6 pass
6 pass
7
7
8 def get_view(self, *args, **kwargs):
8 def get_view(self, *args, **kwargs):
9 """Get an HTML view for a model"""
9 """
10 pass No newline at end of file
10 Gets an HTML view for a model
11 """
12 pass
13
14 def get_search_view(self, *args, **kwargs):
15 """
16 Gets an HTML view for search.
17 """
18 pass
@@ -4,6 +4,7 b' from random import random'
4 import time
4 import time
5 from django.db import models
5 from django.db import models
6 from boards import thumbs
6 from boards import thumbs
7 from boards.models.base import Viewable
7
8
8 __author__ = 'neko259'
9 __author__ = 'neko259'
9
10
@@ -11,9 +12,13 b' from boards import thumbs'
11 IMAGE_THUMB_SIZE = (200, 150)
12 IMAGE_THUMB_SIZE = (200, 150)
12 IMAGES_DIRECTORY = 'images/'
13 IMAGES_DIRECTORY = 'images/'
13 FILE_EXTENSION_DELIMITER = '.'
14 FILE_EXTENSION_DELIMITER = '.'
15 HASH_LENGTH = 36
16
17 CSS_CLASS_IMAGE = 'image'
18 CSS_CLASS_THUMB = 'thumb'
14
19
15
20
16 class PostImage(models.Model):
21 class PostImage(models.Model, Viewable):
17 class Meta:
22 class Meta:
18 app_label = 'boards'
23 app_label = 'boards'
19 ordering = ('id',)
24 ordering = ('id',)
@@ -43,7 +48,7 b' class PostImage(models.Model):'
43 height_field='height',
48 height_field='height',
44 preview_width_field='pre_width',
49 preview_width_field='pre_width',
45 preview_height_field='pre_height')
50 preview_height_field='pre_height')
46 hash = models.CharField(max_length=36)
51 hash = models.CharField(max_length=HASH_LENGTH)
47
52
48 def save(self, *args, **kwargs):
53 def save(self, *args, **kwargs):
49 """
54 """
@@ -60,3 +65,19 b' class PostImage(models.Model):'
60 def __str__(self):
65 def __str__(self):
61 return self.image.url
66 return self.image.url
62
67
68 def get_view(self):
69 return '<div class="{}">' \
70 '<a class="{}" href="{}">' \
71 '<img' \
72 ' src="{}"' \
73 ' alt="{}"' \
74 ' width="{}"' \
75 ' height="{}"' \
76 ' data-width="{}"' \
77 ' data-height="{}" />' \
78 '</a>' \
79 '</div>'\
80 .format(CSS_CLASS_IMAGE, CSS_CLASS_THUMB, self.image.url,
81 self.image.url_200x150,
82 str(self.hash), str(self.pre_width),
83 str(self.pre_height), str(self.width), str(self.height))
@@ -4,21 +4,30 b' import logging'
4 import re
4 import re
5 import xml.etree.ElementTree as et
5 import xml.etree.ElementTree as et
6
6
7 from adjacent import Client
7 from django.core.cache import cache
8 from django.core.cache import cache
8 from django.core.urlresolvers import reverse
9 from django.core.urlresolvers import reverse
9 from django.db import models, transaction
10 from django.db import models, transaction
11 from django.db.models import TextField
10 from django.template.loader import render_to_string
12 from django.template.loader import render_to_string
11 from django.utils import timezone
13 from django.utils import timezone
12
14
13 from markupfield.fields import MarkupField
14
15 from boards.models import PostImage, KeyPair, GlobalId, Signature
15 from boards.models import PostImage, KeyPair, GlobalId, Signature
16 from boards import settings
17 from boards.mdx_neboard import bbcode_extended
18 from boards.models import PostImage
16 from boards.models.base import Viewable
19 from boards.models.base import Viewable
17 from boards.models.thread import Thread
20 from boards.models.thread import Thread
18 from boards import utils
21 from boards import utils
22 from boards.utils import datetime_to_epoch
19
23
20 ENCODING_UNICODE = 'unicode'
24 ENCODING_UNICODE = 'unicode'
21
25
26 WS_NOTIFICATION_TYPE_NEW_POST = 'new_post'
27 WS_NOTIFICATION_TYPE = 'notification_type'
28
29 WS_CHANNEL_THREAD = "thread:"
30
22 APP_LABEL_BOARDS = 'boards'
31 APP_LABEL_BOARDS = 'boards'
23
32
24 CACHE_KEY_PPD = 'ppd'
33 CACHE_KEY_PPD = 'ppd'
@@ -32,8 +41,6 b' IMAGE_THUMB_SIZE = (200, 150)'
32
41
33 TITLE_MAX_LENGTH = 200
42 TITLE_MAX_LENGTH = 200
34
43
35 DEFAULT_MARKUP_TYPE = 'bbcode'
36
37 # TODO This should be removed
44 # TODO This should be removed
38 NO_IP = '0.0.0.0'
45 NO_IP = '0.0.0.0'
39
46
@@ -69,12 +76,32 b" ATTR_MIMETYPE = 'mimetype'"
69
76
70 STATUS_SUCCESS = 'success'
77 STATUS_SUCCESS = 'success'
71
78
72 logger = logging.getLogger(__name__)
79 PARAMETER_TRUNCATED = 'truncated'
80 PARAMETER_TAG = 'tag'
81 PARAMETER_OFFSET = 'offset'
82 PARAMETER_DIFF_TYPE = 'type'
83 PARAMETER_BUMPABLE = 'bumpable'
84 PARAMETER_THREAD = 'thread'
85 PARAMETER_IS_OPENING = 'is_opening'
86 PARAMETER_MODERATOR = 'moderator'
87 PARAMETER_POST = 'post'
88 PARAMETER_OP_ID = 'opening_post_id'
89 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
90
91 DIFF_TYPE_HTML = 'html'
92 DIFF_TYPE_JSON = 'json'
93
94 PREPARSE_PATTERNS = {
95 r'>>(\d+)': r'[post]\1[/post]', # Reflink ">>123"
96 r'^>([^>].+)': r'[quote]\1[/quote]', # Quote ">text"
97 r'^//(.+)': r'[comment]\1[/comment]', # Comment "//text"
98 }
73
99
74
100
75 class PostManager(models.Manager):
101 class PostManager(models.Manager):
76 def create_post(self, title, text, image=None, thread=None, ip=NO_IP,
102 @transaction.atomic
77 tags=None):
103 def create_post(self, title: str, text: str, image=None, thread=None,
104 ip=NO_IP, tags: list=None):
78 """
105 """
79 Creates new post
106 Creates new post
80 """
107 """
@@ -88,13 +115,12 b' class PostManager(models.Manager):'
88 last_edit_time=posting_time)
115 last_edit_time=posting_time)
89 new_thread = True
116 new_thread = True
90 else:
117 else:
91 thread.bump()
92 thread.last_edit_time = posting_time
93 thread.save()
94 new_thread = False
118 new_thread = False
95
119
120 pre_text = self._preparse_text(text)
121
96 post = self.create(title=title,
122 post = self.create(title=title,
97 text=text,
123 text=pre_text,
98 pub_time=posting_time,
124 pub_time=posting_time,
99 thread_new=thread,
125 thread_new=thread,
100 poster_ip=ip,
126 poster_ip=ip,
@@ -104,43 +130,31 b' class PostManager(models.Manager):'
104
130
105 post.set_global_id()
131 post.set_global_id()
106
132
133 logger = logging.getLogger('boards.post.create')
134
135 logger.info('Created post {} by {}'.format(
136 post, post.poster_ip))
137
107 if image:
138 if image:
108 post_image = PostImage.objects.create(image=image)
139 post_image = PostImage.objects.create(image=image)
109 post.images.add(post_image)
140 post.images.add(post_image)
110 logger.info('Created image #%d for post #%d' % (post_image.id,
141 logger.info('Created image #{} for post #{}'.format(
111 post.id))
142 post_image.id, post.id))
112
143
113 thread.replies.add(post)
144 thread.replies.add(post)
114 list(map(thread.add_tag, tags))
145 list(map(thread.add_tag, tags))
115
146
116 if new_thread:
147 if new_thread:
117 Thread.objects.process_oldest_threads()
148 Thread.objects.process_oldest_threads()
118 self.connect_replies(post)
149 else:
150 thread.bump()
151 thread.last_edit_time = posting_time
152 thread.save()
119
153
120 logger.info('Created post #%d with title %s'
154 self.connect_replies(post)
121 % (post.id, post.get_title()))
122
155
123 return post
156 return post
124
157
125 def delete_post(self, post):
126 """
127 Deletes post and update or delete its thread
128 """
129
130 post_id = post.id
131
132 thread = post.get_thread()
133
134 if post.is_opening():
135 thread.delete()
136 else:
137 thread.last_edit_time = timezone.now()
138 thread.save()
139
140 post.delete()
141
142 logger.info('Deleted post #%d (%s)' % (post_id, post.get_title()))
143
144 def delete_posts_by_ip(self, ip):
158 def delete_posts_by_ip(self, ip):
145 """
159 """
146 Deletes all posts of the author with same IP
160 Deletes all posts of the author with same IP
@@ -148,7 +162,7 b' class PostManager(models.Manager):'
148
162
149 posts = self.filter(poster_ip=ip)
163 posts = self.filter(poster_ip=ip)
150 for post in posts:
164 for post in posts:
151 self.delete_post(post)
165 post.delete()
152
166
153 # TODO This can be moved into a post
167 # TODO This can be moved into a post
154 def connect_replies(self, post):
168 def connect_replies(self, post):
@@ -156,8 +170,9 b' class PostManager(models.Manager):'
156 Connects replies to a post to show them as a reflink map
170 Connects replies to a post to show them as a reflink map
157 """
171 """
158
172
159 for reply_number in post.get_replied_ids():
173 for reply_number in re.finditer(REGEX_REPLY, post.get_raw_text()):
160 ref_post = self.filter(id=reply_number)
174 post_id = reply_number.group(1)
175 ref_post = self.filter(id=post_id)
161 if ref_post.count() > 0:
176 if ref_post.count() > 0:
162 referenced_post = ref_post[0]
177 referenced_post = ref_post[0]
163 referenced_post.referenced_posts.add(post)
178 referenced_post.referenced_posts.add(post)
@@ -280,6 +295,17 b' class PostManager(models.Manager):'
280 # TODO Throw an exception?
295 # TODO Throw an exception?
281 pass
296 pass
282
297
298 def _preparse_text(self, text):
299 """
300 Preparses text to change patterns like '>>' to a proper bbcode
301 tags.
302 """
303
304 for key, value in PREPARSE_PATTERNS.items():
305 text = re.sub(key, value, text, flags=re.MULTILINE)
306
307 return text
308
283
309
284 class Post(models.Model, Viewable):
310 class Post(models.Model, Viewable):
285 """A post is a message."""
311 """A post is a message."""
@@ -292,8 +318,8 b' class Post(models.Model, Viewable):'
292
318
293 title = models.CharField(max_length=TITLE_MAX_LENGTH)
319 title = models.CharField(max_length=TITLE_MAX_LENGTH)
294 pub_time = models.DateTimeField()
320 pub_time = models.DateTimeField()
295 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
321 text = TextField(blank=True, null=True)
296 escape_html=False)
322 _text_rendered = TextField(blank=True, null=True, editable=False)
297
323
298 images = models.ManyToManyField(PostImage, null=True, blank=True,
324 images = models.ManyToManyField(PostImage, null=True, blank=True,
299 related_name='ip+', db_index=True)
325 related_name='ip+', db_index=True)
@@ -322,14 +348,21 b' class Post(models.Model, Viewable):'
322 # One post can be signed by many nodes that give their trust to it
348 # One post can be signed by many nodes that give their trust to it
323 signature = models.ManyToManyField('Signature', null=True, blank=True)
349 signature = models.ManyToManyField('Signature', null=True, blank=True)
324
350
325 def __unicode__(self):
351 def __str__(self):
326 return '#' + str(self.id) + ' ' + self.title + ' (' + \
352 return 'P#{}/{}'.format(self.id, self.title)
327 self.text.raw[:50] + ')'
353
354 def get_title(self) -> str:
355 """
356 Gets original post title or part of its text.
357 """
328
358
329 def get_title(self):
359 title = self.title
330 return self.title
360 if not title:
361 title = self.get_text()
331
362
332 def build_refmap(self):
363 return title
364
365 def build_refmap(self) -> None:
333 """
366 """
334 Builds a replies map string from replies list. This is a cache to stop
367 Builds a replies map string from replies list. This is a cache to stop
335 the server from recalculating the map on every post show.
368 the server from recalculating the map on every post show.
@@ -349,10 +382,13 b' class Post(models.Model, Viewable):'
349 def get_sorted_referenced_posts(self):
382 def get_sorted_referenced_posts(self):
350 return self.refmap
383 return self.refmap
351
384
352 def is_referenced(self):
385 def is_referenced(self) -> bool:
353 return len(self.refmap) > 0
386 if not self.refmap:
387 return False
388 else:
389 return len(self.refmap) > 0
354
390
355 def is_opening(self):
391 def is_opening(self) -> bool:
356 """
392 """
357 Checks if this is an opening post or just a reply.
393 Checks if this is an opening post or just a reply.
358 """
394 """
@@ -371,18 +407,6 b' class Post(models.Model, Viewable):'
371 thread.last_edit_time = edit_time
407 thread.last_edit_time = edit_time
372 thread.save(update_fields=['last_edit_time'])
408 thread.save(update_fields=['last_edit_time'])
373
409
374 @transaction.atomic
375 def remove_tag(self, tag):
376 edit_time = timezone.now()
377
378 thread = self.get_thread()
379 thread.remove_tag(tag)
380 self.last_edit_time = edit_time
381 self.save(update_fields=['last_edit_time'])
382
383 thread.last_edit_time = edit_time
384 thread.save(update_fields=['last_edit_time'])
385
386 def get_url(self, thread=None):
410 def get_url(self, thread=None):
387 """
411 """
388 Gets full url to the post.
412 Gets full url to the post.
@@ -407,7 +431,7 b' class Post(models.Model, Viewable):'
407
431
408 return link
432 return link
409
433
410 def get_thread(self):
434 def get_thread(self) -> Thread:
411 """
435 """
412 Gets post's thread.
436 Gets post's thread.
413 """
437 """
@@ -417,25 +441,17 b' class Post(models.Model, Viewable):'
417 def get_referenced_posts(self):
441 def get_referenced_posts(self):
418 return self.referenced_posts.only('id', 'thread_new')
442 return self.referenced_posts.only('id', 'thread_new')
419
443
420 def get_text(self):
421 return self.text
422
423 def get_view(self, moderator=False, need_open_link=False,
444 def get_view(self, moderator=False, need_open_link=False,
424 truncated=False, *args, **kwargs):
445 truncated=False, *args, **kwargs):
425 if 'is_opening' in kwargs:
446 """
426 is_opening = kwargs['is_opening']
447 Renders post's HTML view. Some of the post params can be passed over
427 else:
448 kwargs for the means of caching (if we view the thread, some params
428 is_opening = self.is_opening()
449 are same for every post and don't need to be computed over and over.
450 """
429
451
430 if 'thread' in kwargs:
452 is_opening = kwargs.get(PARAMETER_IS_OPENING, self.is_opening())
431 thread = kwargs['thread']
453 thread = kwargs.get(PARAMETER_THREAD, self.get_thread())
432 else:
454 can_bump = kwargs.get(PARAMETER_BUMPABLE, thread.can_bump())
433 thread = self.get_thread()
434
435 if 'can_bump' in kwargs:
436 can_bump = kwargs['can_bump']
437 else:
438 can_bump = thread.can_bump()
439
455
440 if is_opening:
456 if is_opening:
441 opening_post_id = self.id
457 opening_post_id = self.id
@@ -443,22 +459,26 b' class Post(models.Model, Viewable):'
443 opening_post_id = thread.get_opening_post_id()
459 opening_post_id = thread.get_opening_post_id()
444
460
445 return render_to_string('boards/post.html', {
461 return render_to_string('boards/post.html', {
446 'post': self,
462 PARAMETER_POST: self,
447 'moderator': moderator,
463 PARAMETER_MODERATOR: moderator,
448 'is_opening': is_opening,
464 PARAMETER_IS_OPENING: is_opening,
449 'thread': thread,
465 PARAMETER_THREAD: thread,
450 'bumpable': can_bump,
466 PARAMETER_BUMPABLE: can_bump,
451 'need_open_link': need_open_link,
467 PARAMETER_NEED_OPEN_LINK: need_open_link,
452 'truncated': truncated,
468 PARAMETER_TRUNCATED: truncated,
453 'opening_post_id': opening_post_id,
469 PARAMETER_OP_ID: opening_post_id,
454 })
470 })
455
471
456 def get_first_image(self):
472 def get_search_view(self, *args, **kwargs):
473 return self.get_view(args, kwargs)
474
475 def get_first_image(self) -> PostImage:
457 return self.images.earliest('id')
476 return self.images.earliest('id')
458
477
459 def delete(self, using=None):
478 def delete(self, using=None):
460 """
479 """
461 Deletes all post images and the post itself.
480 Deletes all post images and the post itself. If the post is opening,
481 thread with all posts is deleted.
462 """
482 """
463
483
464 self.images.all().delete()
484 self.images.all().delete()
@@ -466,7 +486,16 b' class Post(models.Model, Viewable):'
466 if self.global_id:
486 if self.global_id:
467 self.global_id.delete()
487 self.global_id.delete()
468
488
489 if self.is_opening():
490 self.get_thread().delete()
491 else:
492 thread = self.get_thread()
493 thread.last_edit_time = timezone.now()
494 thread.save()
495
469 super(Post, self).delete(using)
496 super(Post, self).delete(using)
497 logging.getLogger('boards.post.delete').info(
498 'Deleted post {}'.format(self))
470
499
471 def set_global_id(self, key_pair=None):
500 def set_global_id(self, key_pair=None):
472 """
501 """
@@ -494,6 +523,7 b' class Post(models.Model, Viewable):'
494 def get_pub_time_epoch(self):
523 def get_pub_time_epoch(self):
495 return utils.datetime_to_epoch(self.pub_time)
524 return utils.datetime_to_epoch(self.pub_time)
496
525
526 # TODO Use this to connect replies
497 def get_replied_ids(self):
527 def get_replied_ids(self):
498 """
528 """
499 Gets ID list of the posts that this post replies.
529 Gets ID list of the posts that this post replies.
@@ -516,3 +546,78 b' class Post(models.Model, Viewable):'
516 except GlobalId.DoesNotExist:
546 except GlobalId.DoesNotExist:
517 pass
547 pass
518 return local_replied + global_replied
548 return local_replied + global_replied
549
550
551 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
552 include_last_update=False):
553 """
554 Gets post HTML or JSON data that can be rendered on a page or used by
555 API.
556 """
557
558 if format_type == DIFF_TYPE_HTML:
559 params = dict()
560 params['post'] = self
561 if PARAMETER_TRUNCATED in request.GET:
562 params[PARAMETER_TRUNCATED] = True
563
564 return render_to_string('boards/api_post.html', params)
565 elif format_type == DIFF_TYPE_JSON:
566 post_json = {
567 'id': self.id,
568 'title': self.title,
569 'text': self._text_rendered,
570 }
571 if self.images.exists():
572 post_image = self.get_first_image()
573 post_json['image'] = post_image.image.url
574 post_json['image_preview'] = post_image.image.url_200x150
575 if include_last_update:
576 post_json['bump_time'] = datetime_to_epoch(
577 self.thread_new.bump_time)
578 return post_json
579
580 def send_to_websocket(self, request, recursive=True):
581 """
582 Sends post HTML data to the thread web socket.
583 """
584
585 if not settings.WEBSOCKETS_ENABLED:
586 return
587
588 client = Client()
589
590 thread = self.get_thread()
591 thread_id = thread.id
592 channel_name = WS_CHANNEL_THREAD + str(thread.get_opening_post_id())
593 client.publish(channel_name, {
594 WS_NOTIFICATION_TYPE: WS_NOTIFICATION_TYPE_NEW_POST,
595 })
596 client.send()
597
598 logger = logging.getLogger('boards.post.websocket')
599
600 logger.info('Sent notification from post #{} to channel {}'.format(
601 self.id, channel_name))
602
603 if recursive:
604 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
605 post_id = reply_number.group(1)
606 ref_post = Post.objects.filter(id=post_id)[0]
607
608 # If post is in this thread, its thread was already notified.
609 # Otherwise, notify its thread separately.
610 if ref_post.thread_new_id != thread_id:
611 ref_post.send_to_websocket(request, recursive=False)
612
613 def save(self, force_insert=False, force_update=False, using=None,
614 update_fields=None):
615 self._text_rendered = bbcode_extended(self.get_raw_text())
616
617 super().save(force_insert, force_update, using, update_fields)
618
619 def get_text(self) -> str:
620 return self._text_rendered
621
622 def get_raw_text(self) -> str:
623 return self.text
@@ -1,9 +1,8 b''
1 from django.template.loader import render_to_string
1 from django.template.loader import render_to_string
2 from django.db import models
2 from django.db import models
3 from django.db.models import Count, Sum
3 from django.db.models import Count
4 from django.core.urlresolvers import reverse
4 from django.core.urlresolvers import reverse
5
5
6 from boards.models import Thread
7 from boards.models.base import Viewable
6 from boards.models.base import Viewable
8
7
9
8
@@ -17,10 +16,9 b' class TagManager(models.Manager):'
17 Gets tags that have non-archived threads.
16 Gets tags that have non-archived threads.
18 """
17 """
19
18
20 tags = self.annotate(Count('threads')) \
19 return self.filter(thread__archived=False)\
21 .filter(threads__count__gt=0).order_by('name')
20 .annotate(num_threads=Count('thread')).filter(num_threads__gt=0)\
22
21 .order_by('-required', 'name')
23 return tags
24
22
25
23
26 class Tag(models.Model, Viewable):
24 class Tag(models.Model, Viewable):
@@ -36,43 +34,38 b' class Tag(models.Model, Viewable):'
36 ordering = ('name',)
34 ordering = ('name',)
37
35
38 name = models.CharField(max_length=100, db_index=True)
36 name = models.CharField(max_length=100, db_index=True)
39 threads = models.ManyToManyField(Thread, null=True,
37 required = models.BooleanField(default=False)
40 blank=True, related_name='tag+')
41
38
42 def __unicode__(self):
39 def __str__(self):
43 return self.name
40 return self.name
44
41
45 def is_empty(self):
42 def is_empty(self) -> bool:
46 """
43 """
47 Checks if the tag has some threads.
44 Checks if the tag has some threads.
48 """
45 """
49
46
50 return self.get_thread_count() == 0
47 return self.get_thread_count() == 0
51
48
52 def get_thread_count(self):
49 def get_thread_count(self) -> int:
53 return self.threads.count()
50 return self.get_threads().count()
54
55 def get_post_count(self, archived=False):
56 """
57 Gets posts count for the tag's threads.
58 """
59
60 posts_count = 0
61
62 threads = self.threads.filter(archived=archived)
63 if threads.exists():
64 posts_count = threads.annotate(posts_count=Count('replies')) \
65 .aggregate(posts_sum=Sum('posts_count'))['posts_sum']
66
67 if not posts_count:
68 posts_count = 0
69
70 return posts_count
71
51
72 def get_url(self):
52 def get_url(self):
73 return reverse('tag', kwargs={'tag_name': self.name})
53 return reverse('tag', kwargs={'tag_name': self.name})
74
54
75 def get_view(self, *args, **kwargs):
55 def get_threads(self):
56 return self.thread_set.order_by('-bump_time')
57
58 def is_required(self):
59 return self.required
60
61 def get_view(self):
62 link = '<a class="tag" href="{}">{}</a>'.format(
63 self.get_url(), self.name)
64 if self.is_required():
65 link = '<b>{}</b>'.format(link)
66 return link
67
68 def get_search_view(self, *args, **kwargs):
76 return render_to_string('boards/tag.html', {
69 return render_to_string('boards/tag.html', {
77 'tag': self,
70 'tag': self,
78 })
71 })
@@ -1,5 +1,5 b''
1 import logging
1 import logging
2 from django.db.models import Count
2 from django.db.models import Count, Sum
3 from django.utils import timezone
3 from django.utils import timezone
4 from django.core.cache import cache
4 from django.core.cache import cache
5 from django.db import models
5 from django.db import models
@@ -38,8 +38,9 b' class ThreadManager(models.Manager):'
38
38
39 def _archive_thread(self, thread):
39 def _archive_thread(self, thread):
40 thread.archived = True
40 thread.archived = True
41 thread.bumpable = False
41 thread.last_edit_time = timezone.now()
42 thread.last_edit_time = timezone.now()
42 thread.save(update_fields=['archived', 'last_edit_time'])
43 thread.save(update_fields=['archived', 'last_edit_time', 'bumpable'])
43
44
44
45
45 class Thread(models.Model):
46 class Thread(models.Model):
@@ -54,6 +55,7 b' class Thread(models.Model):'
54 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
55 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
55 blank=True, related_name='tre+')
56 blank=True, related_name='tre+')
56 archived = models.BooleanField(default=False)
57 archived = models.BooleanField(default=False)
58 bumpable = models.BooleanField(default=True)
57
59
58 def get_tags(self):
60 def get_tags(self):
59 """
61 """
@@ -70,30 +72,24 b' class Thread(models.Model):'
70 if self.can_bump():
72 if self.can_bump():
71 self.bump_time = timezone.now()
73 self.bump_time = timezone.now()
72
74
75 if self.get_reply_count() >= settings.MAX_POSTS_PER_THREAD:
76 self.bumpable = False
77
73 logger.info('Bumped thread %d' % self.id)
78 logger.info('Bumped thread %d' % self.id)
74
79
75 def get_reply_count(self):
80 def get_reply_count(self):
76 return self.replies.count()
81 return self.replies.count()
77
82
78 def get_images_count(self):
83 def get_images_count(self):
79 # TODO Use sum
84 return self.replies.annotate(images_count=Count(
80 total_count = 0
85 'images')).aggregate(Sum('images_count'))['images_count__sum']
81 for post_with_image in self.replies.annotate(images_count=Count(
82 'images')):
83 total_count += post_with_image.images_count
84 return total_count
85
86
86 def can_bump(self):
87 def can_bump(self):
87 """
88 """
88 Checks if the thread can be bumped by replying to it.
89 Checks if the thread can be bumped by replying to it.
89 """
90 """
90
91
91 if self.archived:
92 return self.bumpable
92 return False
93
94 post_count = self.get_reply_count()
95
96 return post_count < settings.MAX_POSTS_PER_THREAD
97
93
98 def get_last_replies(self):
94 def get_last_replies(self):
99 """
95 """
@@ -127,7 +123,7 b' class Thread(models.Model):'
127
123
128 query = self.replies.order_by('pub_time').prefetch_related('images')
124 query = self.replies.order_by('pub_time').prefetch_related('images')
129 if view_fields_only:
125 if view_fields_only:
130 query = query.defer('poster_user_agent', 'text_markup_type')
126 query = query.defer('poster_user_agent')
131 return query.all()
127 return query.all()
132
128
133 def get_replies_with_images(self, view_fields_only=False):
129 def get_replies_with_images(self, view_fields_only=False):
@@ -140,11 +136,6 b' class Thread(models.Model):'
140 """
136 """
141
137
142 self.tags.add(tag)
138 self.tags.add(tag)
143 tag.threads.add(self)
144
145 def remove_tag(self, tag):
146 self.tags.remove(tag)
147 tag.threads.remove(self)
148
139
149 def get_opening_post(self, only_id=False):
140 def get_opening_post(self, only_id=False):
150 """
141 """
@@ -185,4 +176,7 b' class Thread(models.Model):'
185 if self.replies.exists():
176 if self.replies.exists():
186 self.replies.all().delete()
177 self.replies.all().delete()
187
178
188 super(Thread, self).delete(using) No newline at end of file
179 super(Thread, self).delete(using)
180
181 def __str__(self):
182 return 'T#{}/{}'.format(self.id, self.get_opening_post_id()) No newline at end of file
@@ -21,4 +21,4 b' class TagIndex(indexes.SearchIndex, inde'
21 return Tag
21 return Tag
22
22
23 def index_queryset(self, using=None):
23 def index_queryset(self, using=None):
24 return self.get_model().objects.get_not_empty_tags()
24 return self.get_model().objects.all()
@@ -1,5 +1,5 b''
1 VERSION = '2.1 Aya'
1 VERSION = '2.2.3 Miyu'
2 SITE_NAME = 'n3b0a2d'
2 SITE_NAME = 'Neboard'
3
3
4 CACHE_TIMEOUT = 600 # Timeout for caching, if cache is used
4 CACHE_TIMEOUT = 600 # Timeout for caching, if cache is used
5 LOGIN_TIMEOUT = 3600 # Timeout between login tries
5 LOGIN_TIMEOUT = 3600 # Timeout between login tries
@@ -18,3 +18,5 b' LAST_REPLIES_COUNT = 3'
18 ARCHIVE_THREADS = True
18 ARCHIVE_THREADS = True
19 # Limit posting speed
19 # Limit posting speed
20 LIMIT_POSTING_SPEED = False
20 LIMIT_POSTING_SPEED = False
21 # Thread update
22 WEBSOCKETS_ENABLED = True
@@ -1,3 +1,12 b''
1 * {
2 text-decoration: none;
3 font-weight: inherit;
4 }
5
6 b {
7 font-weight: bold;
8 }
9
1 html {
10 html {
2 background: #555;
11 background: #555;
3 color: #ffffff;
12 color: #ffffff;
@@ -157,6 +166,10 b' p, .br {'
157 width: 100%;
166 width: 100%;
158 }
167 }
159
168
169 .post-form textarea {
170 resize: vertical;
171 }
172
160 .form-submit {
173 .form-submit {
161 display: table;
174 display: table;
162 margin-bottom: 1ex;
175 margin-bottom: 1ex;
@@ -3,7 +3,7 b''
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-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
@@ -23,12 +23,140 b''
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 wsUser = '';
27
27
28 var loading = false;
28 var loading = false;
29 var lastUpdateTime = null;
30 var unreadPosts = 0;
29 var unreadPosts = 0;
30 var documentOriginalTitle = '';
31
31
32 // Thread ID does not change, can be stored one time
33 var threadId = $('div.thread').children('.post').first().attr('id');
34
35 /**
36 * Connect to websocket server and subscribe to thread updates. On any update we
37 * request a thread diff.
38 *
39 * @returns {boolean} true if connected, false otherwise
40 */
41 function connectWebsocket() {
42 var metapanel = $('.metapanel')[0];
43
44 var wsHost = metapanel.getAttribute('data-ws-host');
45 var wsPort = metapanel.getAttribute('data-ws-port');
46
47 if (wsHost.length > 0 && wsPort.length > 0)
48 var centrifuge = new Centrifuge({
49 "url": 'ws://' + wsHost + ':' + wsPort + "/connection/websocket",
50 "project": metapanel.getAttribute('data-ws-project'),
51 "user": wsUser,
52 "timestamp": metapanel.getAttribute('data-last-update'),
53 "token": metapanel.getAttribute('data-ws-token'),
54 "debug": false
55 });
56
57 centrifuge.on('error', function(error_message) {
58 console.log("Error connecting to websocket server.");
59 return false;
60 });
61
62 centrifuge.on('connect', function() {
63 var channelName = 'thread:' + threadId;
64 centrifuge.subscribe(channelName, function(message) {
65 getThreadDiff();
66 });
67
68 // For the case we closed the browser and missed some updates
69 getThreadDiff();
70 $('#autoupdate').text('[+]');
71 });
72
73 centrifuge.connect();
74
75 return true;
76 }
77
78 /**
79 * Get diff of the posts from the current thread timestamp.
80 * This is required if the browser was closed and some post updates were
81 * missed.
82 */
83 function getThreadDiff() {
84 var lastUpdateTime = $('.metapanel').attr('data-last-update');
85
86 var diffUrl = '/api/diff_thread/' + threadId + '/' + lastUpdateTime + '/';
87
88 $.getJSON(diffUrl)
89 .success(function(data) {
90 var addedPosts = data.added;
91
92 for (var i = 0; i < addedPosts.length; i++) {
93 var postText = addedPosts[i];
94 var post = $(postText);
95
96 updatePost(post)
97
98 lastPost = post;
99 }
100
101 var updatedPosts = data.updated;
102
103 for (var i = 0; i < updatedPosts.length; i++) {
104 var postText = updatedPosts[i];
105 var post = $(postText);
106
107 updatePost(post)
108 }
109
110 // TODO Process removed posts if any
111 $('.metapanel').attr('data-last-update', data.last_update);
112 })
113 }
114
115 /**
116 * Add or update the post on html page.
117 */
118 function updatePost(postHtml) {
119 // This needs to be set on start because the page is scrolled after posts
120 // are added or updated
121 var bottom = isPageBottom();
122
123 var post = $(postHtml);
124
125 var threadBlock = $('div.thread');
126
127 var lastUpdate = '';
128
129 var postId = post.attr('id');
130
131 // If the post already exists, replace it. Otherwise add as a new one.
132 var existingPosts = threadBlock.children('.post[id=' + postId + ']');
133
134 if (existingPosts.size() > 0) {
135 existingPosts.replaceWith(post);
136 } else {
137 var threadPosts = threadBlock.children('.post');
138 var lastPost = threadPosts.last();
139
140 post.appendTo(lastPost.parent());
141
142 updateBumplimitProgress(1);
143 showNewPostsTitle(1);
144
145 lastUpdate = post.children('.post-info').first()
146 .children('.pub_time').first().text();
147
148 if (bottom) {
149 scrollToBottom();
150 }
151 }
152
153 processNewPost(post);
154 updateMetadataPanel(lastUpdate)
155 }
156
157 /**
158 * Initiate a blinking animation on a node to show it was updated.
159 */
32 function blink(node) {
160 function blink(node) {
33 var blinkCount = 2;
161 var blinkCount = 2;
34
162
@@ -38,103 +166,15 b' function blink(node) {'
38 }
166 }
39 }
167 }
40
168
41 function updateThread() {
42 if (loading) {
43 return;
44 }
45
46 loading = true;
47
48 var threadPosts = $('div.thread').children('.post');
49
50 var lastPost = threadPosts.last();
51 var threadId = threadPosts.first().attr('id');
52
53 var diffUrl = '/api/diff_thread/' + threadId + '/' + lastUpdateTime + '/';
54 $.getJSON(diffUrl)
55 .success(function(data) {
56 var bottom = isPageBottom();
57
58 var lastUpdate = '';
59
60 var addedPosts = data.added;
61 for (var i = 0; i < addedPosts.length; i++) {
62 var postText = addedPosts[i];
63
64 var post = $(postText);
65
66 if (lastUpdate === '') {
67 lastUpdate = post.find('.pub_time').text();
68 }
69
70 post.appendTo(lastPost.parent());
71 processNewPost(post);
72
73 lastPost = post;
74 blink(post);
75 }
76
77 var updatedPosts = data.updated;
78 for (var i = 0; i < updatedPosts.length; i++) {
79 var postText = updatedPosts[i];
80
81 var post = $(postText);
82
83 if (lastUpdate === '') {
84 lastUpdate = post.find('.pub_time').text();
85 }
86
87 var postId = post.attr('id');
88
89 var oldPost = $('div.thread').children('.post[id=' + postId + ']');
90
91 oldPost.replaceWith(post);
92 processNewPost(post);
93
94 blink(post);
95 }
96
97 // TODO Process deleted posts
98
99 lastUpdateTime = data.last_update;
100 loading = false;
101
102 if (bottom) {
103 scrollToBottom();
104 }
105
106 var hasPostChanges = (updatedPosts.length > 0)
107 || (addedPosts.length > 0);
108 if (hasPostChanges) {
109 updateMetadataPanel(lastUpdate);
110 }
111
112 updateBumplimitProgress(data.added.length);
113
114 if (data.added.length + data.updated.length > 0) {
115 showNewPostsTitle(data.added.length);
116 }
117 })
118 .error(function(data) {
119 // TODO Show error message that server is unavailable?
120
121 loading = false;
122 });
123 }
124
125 function isPageBottom() {
169 function isPageBottom() {
126 var scroll = $(window).scrollTop() / ($(document).height()
170 var scroll = $(window).scrollTop() / ($(document).height()
127 - $(window).height())
171 - $(window).height());
128
172
129 return scroll == 1
173 return scroll == 1
130 }
174 }
131
175
132 function initAutoupdate() {
176 function initAutoupdate() {
133 loading = false;
177 return connectWebsocket();
134
135 lastUpdateTime = $('.metapanel').attr('data-last-update');
136
137 setInterval(updateThread, THREAD_UPDATE_DELAY);
138 }
178 }
139
179
140 function getReplyCount() {
180 function getReplyCount() {
@@ -145,6 +185,10 b' function getImageCount() {'
145 return $('.thread').find('img').length
185 return $('.thread').find('img').length
146 }
186 }
147
187
188 /**
189 * Update post count, images count and last update time in the metadata
190 * panel.
191 */
148 function updateMetadataPanel(lastUpdate) {
192 function updateMetadataPanel(lastUpdate) {
149 var replyCountField = $('#reply-count');
193 var replyCountField = $('#reply-count');
150 var imageCountField = $('#image-count');
194 var imageCountField = $('#image-count');
@@ -185,7 +229,6 b' function updateBumplimitProgress(postDel'
185 }
229 }
186 }
230 }
187
231
188 var documentOriginalTitle = '';
189 /**
232 /**
190 * Show 'new posts' text in the title if the document is not visible to a user
233 * Show 'new posts' text in the title if the document is not visible to a user
191 */
234 */
@@ -230,7 +273,7 b' function updateOnPost(response, statusTe'
230
273
231 if (status === 'ok') {
274 if (status === 'ok') {
232 resetForm(form);
275 resetForm(form);
233 updateThread();
276 getThreadDiff();
234 } else {
277 } else {
235 var errors = json.errors;
278 var errors = json.errors;
236 for (var i = 0; i < errors.length; i++) {
279 for (var i = 0; i < errors.length; i++) {
@@ -241,6 +284,8 b' function updateOnPost(response, statusTe'
241 showAsErrors(form, error);
284 showAsErrors(form, error);
242 }
285 }
243 }
286 }
287
288 scrollToBottom();
244 }
289 }
245
290
246 /**
291 /**
@@ -264,25 +309,26 b' function showAsErrors(form, text) {'
264 function processNewPost(post) {
309 function processNewPost(post) {
265 addRefLinkPreview(post[0]);
310 addRefLinkPreview(post[0]);
266 highlightCode(post);
311 highlightCode(post);
312 blink(post);
267 }
313 }
268
314
269 $(document).ready(function(){
315 $(document).ready(function(){
270 initAutoupdate();
316 if (initAutoupdate()) {
317 // Post form data over AJAX
318 var threadId = $('div.thread').children('.post').first().attr('id');
271
319
272 // Post form data over AJAX
320 var form = $('#form');
273 var threadId = $('div.thread').children('.post').first().attr('id');
274
275 var form = $('#form');
276
321
277 var options = {
322 var options = {
278 beforeSubmit: function(arr, $form, options) {
323 beforeSubmit: function(arr, $form, options) {
279 showAsErrors($('form'), gettext('Sending message...'));
324 showAsErrors($('form'), gettext('Sending message...'));
280 },
325 },
281 success: updateOnPost,
326 success: updateOnPost,
282 url: '/api/add_post/' + threadId + '/'
327 url: '/api/add_post/' + threadId + '/'
283 };
328 };
284
329
285 form.ajaxForm(options);
330 form.ajaxForm(options);
286
331
287 resetForm(form);
332 resetForm(form);
333 }
288 });
334 });
@@ -9,6 +9,7 b''
9 <link rel="stylesheet" type="text/css" href="{% static 'css/base.css' %}" media="all"/>
9 <link rel="stylesheet" type="text/css" href="{% static 'css/base.css' %}" media="all"/>
10 <link rel="stylesheet" type="text/css" href="{% static 'css/3party/highlight.css' %}" media="all"/>
10 <link rel="stylesheet" type="text/css" href="{% static 'css/3party/highlight.css' %}" media="all"/>
11 <link rel="stylesheet" type="text/css" href="{% static theme_css %}" media="all"/>
11 <link rel="stylesheet" type="text/css" href="{% static theme_css %}" media="all"/>
12
12 <link rel="alternate" type="application/rss+xml" href="rss/" title="{% trans 'Feed' %}"/>
13 <link rel="alternate" type="application/rss+xml" href="rss/" title="{% trans 'Feed' %}"/>
13
14
14 <link rel="icon" type="image/png"
15 <link rel="icon" type="image/png"
@@ -28,8 +29,9 b''
28 <div class="navigation_panel header">
29 <div class="navigation_panel header">
29 <a class="link" href="{% url 'index' %}">{% trans "All threads" %}</a>
30 <a class="link" href="{% url 'index' %}">{% trans "All threads" %}</a>
30 {% for tag in tags %}
31 {% for tag in tags %}
31 <a class="tag" href="{% url 'tag' tag_name=tag.name %}"
32 {% autoescape off %}
32 >#{{ tag.name }}</a>,
33 {{ tag.get_view }}{% if not forloop.last %},{% endif %}
34 {% endautoescape %}
33 {% endfor %}
35 {% endfor %}
34 <a href="{% url 'tags' %}" title="{% trans 'Tag management' %}"
36 <a href="{% url 'tags' %}" title="{% trans 'Tag management' %}"
35 >[...]</a>,
37 >[...]</a>,
@@ -39,9 +41,9 b''
39
41
40 {% block content %}{% endblock %}
42 {% block content %}{% endblock %}
41
43
44 <script src="{% static 'js/3party/highlight.min.js' %}"></script>
42 <script src="{% static 'js/popup.js' %}"></script>
45 <script src="{% static 'js/popup.js' %}"></script>
43 <script src="{% static 'js/image.js' %}"></script>
46 <script src="{% static 'js/image.js' %}"></script>
44 <script src="{% static 'js/3party/highlight.min.js' %}"></script>
45 <script src="{% static 'js/refpopup.js' %}"></script>
47 <script src="{% static 'js/refpopup.js' %}"></script>
46 <script src="{% static 'js/main.js' %}"></script>
48 <script src="{% static 'js/main.js' %}"></script>
47
49
@@ -4,101 +4,99 b''
4
4
5 {% get_current_language as LANGUAGE_CODE %}
5 {% get_current_language as LANGUAGE_CODE %}
6
6
7 {% spaceless %}
7 {% if thread.archived %}
8 {% cache 600 post post.id post.last_edit_time thread.archived bumpable truncated moderator LANGUAGE_CODE need_open_link %}
8 <div class="post archive_post" id="{{ post.id }}">
9 {% if thread.archived %}
9 {% elif bumpable %}
10 <div class="post archive_post" id="{{ post.id }}">
10 <div class="post" id="{{ post.id }}">
11 {% elif bumpable %}
11 {% else %}
12 <div class="post" id="{{ post.id }}">
12 <div class="post dead_post" id="{{ post.id }}">
13 {% else %}
13 {% endif %}
14 <div class="post dead_post" id="{{ post.id }}">
15 {% endif %}
16
14
17 <div class="post-info">
15 <div class="post-info">
18 <a class="post_id" href="{% post_object_url post thread=thread %}"
16 <a class="post_id" href="{% post_object_url post thread=thread %}"
19 {% if not truncated and not thread.archived %}
17 {% if not truncated and not thread.archived %}
20 onclick="javascript:addQuickReply('{{ post.id }}'); return false;"
18 onclick="javascript:addQuickReply('{{ post.id }}'); return false;"
21 title="{% trans 'Quote' %}"
19 title="{% trans 'Quote' %}" {% endif %}>({{ post.id }})</a>
22 {% endif %}
20 <span class="title">{{ post.title }}</span>
23 >({{ post.id }}) </a>
21 <span class="pub_time">{{ post.pub_time }}</span>
24 <span class="title">{{ post.title }} </span>
22 {% comment %}
25 <span class="pub_time">{{ post.pub_time }}</span>
23 Thread death time needs to be shown only if the thread is alredy archived
26 {% if thread.archived %}
24 and this is an opening post (thread death time) or a post for popup
27 — {{ thread.bump_time }}
25 (we don't see OP here so we show the death time in the post itself).
26 {% endcomment %}
27 {% if thread.archived %}
28 {% if is_opening %}
29 — {{ thread.bump_time }}
30 {% endif %}
31 {% endif %}
32 {% if is_opening and need_open_link %}
33 {% if thread.archived %}
34 [<a class="link" href="{% url 'thread' post.id %}">{% trans "Open" %}</a>]
35 {% else %}
36 [<a class="link" href="{% url 'thread' post.id %}#form">{% trans "Reply" %}</a>]
37 {% endif %}
38 {% endif %}
39
40 {% if post.global_id %}
41 <a class="global-id" href="
42 {% url 'post_sync_data' post.id %}"> [RAW] </a>
43 {% endif %}
44
45 {% if moderator %}
46 <span class="moderator_info">
47 [<a href="{% url 'admin:boards_post_change' post.id %}">{% trans 'Edit' %}</a>]
48 {% if is_opening %}
49 [<a href="{% url 'admin:boards_thread_change' thread.id %}">{% trans 'Edit thread' %}</a>]
28 {% endif %}
50 {% endif %}
29 {% if is_opening and need_open_link %}
51 </span>
30 {% if thread.archived %}
52 {% endif %}
31 [<a class="link" href="{% url 'thread' post.id %}">{% trans "Open" %}</a>]
53 </div>
32 {% else %}
54 {% comment %}
33 [<a class="link" href="{% url 'thread' post.id %}#form">{% trans "Reply" %}</a>]
55 Post images. Currently only 1 image can be posted and shown, but post model
34 {% endif %}
56 supports multiple.
35 {% endif %}
57 {% endcomment %}
36
58 {% if post.images.exists %}
37 {% if post.global_id %}
59 {% with post.images.all.0 as image %}
38 <a class="global-id" href="
60 {% autoescape off %}
39 {% url 'post_sync_data' post.id %}"> [RAW] </a>
61 {{ image.get_view }}
40 {% endif %}
62 {% endautoescape %}
41
63 {% endwith %}
42 {% if moderator %}
64 {% endif %}
43 <span class="moderator_info">
65 {% comment %}
44 [<a href="{% url 'post_admin' post_id=post.id %}"
66 Post message (text)
45 >{% trans 'Edit' %}</a>]
67 {% endcomment %}
46 [<a href="{% url 'delete' post_id=post.id %}"
68 <div class="message">
47 >{% trans 'Delete' %}</a>]
69 {% autoescape off %}
48 ({{ post.poster_ip }})
70 {% if truncated %}
49 [<a href="{% url 'ban' post_id=post.id %}?next={{ request.path }}"
71 {{ post.get_text|truncatewords_html:50 }}
50 >{% trans 'Ban IP' %}</a>]
72 {% else %}
51 </span>
73 {{ post.get_text }}
52 {% endif %}
74 {% endif %}
75 {% endautoescape %}
76 {% if post.is_referenced %}
77 <div class="refmap">
78 {% autoescape off %}
79 {% trans "Replies" %}: {{ post.refmap }}
80 {% endautoescape %}
53 </div>
81 </div>
54 {% if post.images.exists %}
82 {% endif %}
55 {% with post.images.all.0 as image %}
83 </div>
56 <div class="image">
84 {% comment %}
57 <a
85 Thread metadata: counters, tags etc
58 class="thumb"
86 {% endcomment %}
59 href="{{ image.image.url }}"><img
87 {% if is_opening %}
60 src="{{ image.image.url_200x150 }}"
88 <div class="metadata">
61 alt="{{ post.id }}"
89 {% if is_opening and need_open_link %}
62 width="{{ image.pre_width }}"
90 {{ thread.get_reply_count }} {% trans 'messages' %},
63 height="{{ image.pre_height }}"
91 {{ thread.get_images_count }} {% trans 'images' %}.
64 data-width="{{ image.width }}"
65 data-height="{{ image.height }}"/>
66 </a>
67 </div>
68 {% endwith %}
69 {% endif %}
92 {% endif %}
70 <div class="message">
93 <span class="tags">
71 {% autoescape off %}
94 {% for tag in thread.get_tags %}
72 {% if truncated %}
95 {% autoescape off %}
73 {{ post.text.rendered|truncatewords_html:50 }}
96 {{ tag.get_view }}{% if not forloop.last %},{% endif %}
74 {% else %}
97 {% endautoescape %}
75 {{ post.text.rendered }}
98 {% endfor %}
76 {% endif %}
99 </span>
77 {% endautoescape %}
100 </div>
78 {% if post.is_referenced %}
101 {% endif %}
79 <div class="refmap">
102 </div>
80 {% autoescape off %}
81 {% trans "Replies" %}: {{ post.refmap }}
82 {% endautoescape %}
83 </div>
84 {% endif %}
85 </div>
86 {% endcache %}
87 {% if is_opening %}
88 {% cache 600 post_thread thread.id thread.last_edit_time LANGUAGE_CODE need_open_link %}
89 <div class="metadata">
90 {% if is_opening and need_open_link %}
91 {{ thread.get_reply_count }} {% trans 'messages' %},
92 {{ thread.get_images_count }} {% trans 'images' %}.
93 {% endif %}
94 <span class="tags">
95 {% for tag in thread.get_tags %}
96 <a class="tag" href="{% url 'tag' tag.name %}">
97 #{{ tag.name }}</a>{% if not forloop.last %},{% endif %}
98 {% endfor %}
99 </span>
100 </div>
101 {% endcache %}
102 {% endif %}
103 </div>
104 {% endspaceless %}
@@ -15,7 +15,7 b''
15 {% if current_page.has_previous %}
15 {% if current_page.has_previous %}
16 <link rel="prev" href="
16 <link rel="prev" href="
17 {% if tag %}
17 {% if tag %}
18 {% url "tag" tag_name=tag page=current_page.previous_page_number %}
18 {% url "tag" tag_name=tag.name page=current_page.previous_page_number %}
19 {% elif archived %}
19 {% elif archived %}
20 {% url "archive" page=current_page.previous_page_number %}
20 {% url "archive" page=current_page.previous_page_number %}
21 {% else %}
21 {% else %}
@@ -26,7 +26,7 b''
26 {% if current_page.has_next %}
26 {% if current_page.has_next %}
27 <link rel="next" href="
27 <link rel="next" href="
28 {% if tag %}
28 {% if tag %}
29 {% url "tag" tag_name=tag page=current_page.next_page_number %}
29 {% url "tag" tag_name=tag.name page=current_page.next_page_number %}
30 {% elif archived %}
30 {% elif archived %}
31 {% url "archive" page=current_page.next_page_number %}
31 {% url "archive" page=current_page.next_page_number %}
32 {% else %}
32 {% else %}
@@ -46,21 +46,26 b''
46 <h2>
46 <h2>
47 {% if tag in fav_tags %}
47 {% if tag in fav_tags %}
48 <a href="{% url 'tag' tag.name %}?method=unsubscribe&next={{ request.path }}"
48 <a href="{% url 'tag' tag.name %}?method=unsubscribe&next={{ request.path }}"
49 class="fav"></a>
49 class="fav" rel="nofollow"></a>
50 {% else %}
50 {% else %}
51 <a href="{% url 'tag' tag.name %}?method=subscribe&next={{ request.path }}"
51 <a href="{% url 'tag' tag.name %}?method=subscribe&next={{ request.path }}"
52 class="not_fav"></a>
52 class="not_fav" rel="nofollow"></a>
53 {% endif %}
53 {% endif %}
54 {% if tag in hidden_tags %}
54 {% if tag in hidden_tags %}
55 <a href="{% url 'tag' tag.name %}?method=unhide&next={{ request.path }}"
55 <a href="{% url 'tag' tag.name %}?method=unhide&next={{ request.path }}"
56 title="{% trans 'Show tag' %}"
56 title="{% trans 'Show tag' %}"
57 class="fav">H</a>
57 class="fav" rel="nofollow">H</a>
58 {% else %}
58 {% else %}
59 <a href="{% url 'tag' tag.name %}?method=hide&next={{ request.path }}"
59 <a href="{% url 'tag' tag.name %}?method=hide&next={{ request.path }}"
60 title="{% trans 'Hide tag' %}"
60 title="{% trans 'Hide tag' %}"
61 class="not_fav">H</a>
61 class="not_fav" rel="nofollow">H</a>
62 {% endif %}
62 {% endif %}
63 #{{ tag.name }}
63 {% autoescape off %}
64 {{ tag.get_view }}
65 {% endautoescape %}
66 {% if moderator %}
67 [<a href="{% url 'admin:boards_tag_change' tag.id %}"$>{% trans 'Edit tag' %}</a>]
68 {% endif %}
64 </h2>
69 </h2>
65 </div>
70 </div>
66 {% endif %}
71 {% endif %}
@@ -70,7 +75,7 b''
70 <div class="page_link">
75 <div class="page_link">
71 <a href="
76 <a href="
72 {% if tag %}
77 {% if tag %}
73 {% url "tag" tag_name=tag page=current_page.previous_page_number %}
78 {% url "tag" tag_name=tag.name page=current_page.previous_page_number %}
74 {% elif archived %}
79 {% elif archived %}
75 {% url "archive" page=current_page.previous_page_number %}
80 {% url "archive" page=current_page.previous_page_number %}
76 {% else %}
81 {% else %}
@@ -112,7 +117,7 b''
112 <div class="page_link">
117 <div class="page_link">
113 <a href="
118 <a href="
114 {% if tag %}
119 {% if tag %}
115 {% url "tag" tag_name=tag page=current_page.next_page_number %}
120 {% url "tag" tag_name=tag.name page=current_page.next_page_number %}
116 {% elif archived %}
121 {% elif archived %}
117 {% url "archive" page=current_page.next_page_number %}
122 {% url "archive" page=current_page.next_page_number %}
118 {% else %}
123 {% else %}
@@ -157,7 +162,7 b''
157 {% trans "Pages:" %}
162 {% trans "Pages:" %}
158 <a href="
163 <a href="
159 {% if tag %}
164 {% if tag %}
160 {% url "tag" tag_name=tag page=paginator.page_range|first %}
165 {% url "tag" tag_name=tag.name page=paginator.page_range|first %}
161 {% elif archived %}
166 {% elif archived %}
162 {% url "archive" page=paginator.page_range|first %}
167 {% url "archive" page=paginator.page_range|first %}
163 {% else %}
168 {% else %}
@@ -172,7 +177,7 b''
172 {% endifequal %}
177 {% endifequal %}
173 href="
178 href="
174 {% if tag %}
179 {% if tag %}
175 {% url "tag" tag_name=tag page=page %}
180 {% url "tag" tag_name=tag.name page=page %}
176 {% elif archived %}
181 {% elif archived %}
177 {% url "archive" page=page %}
182 {% url "archive" page=page %}
178 {% else %}
183 {% else %}
@@ -184,7 +189,7 b''
184 ]
189 ]
185 <a href="
190 <a href="
186 {% if tag %}
191 {% if tag %}
187 {% url "tag" tag_name=tag page=paginator.page_range|last %}
192 {% url "tag" tag_name=tag.name page=paginator.page_range|last %}
188 {% elif archived %}
193 {% elif archived %}
189 {% url "archive" page=paginator.page_range|last %}
194 {% url "archive" page=paginator.page_range|last %}
190 {% else %}
195 {% else %}
@@ -4,7 +4,7 b''
4 <img src="{{ obj.get_first_image.image.url_200x150 }}"
4 <img src="{{ obj.get_first_image.image.url_200x150 }}"
5 alt="{% trans 'Post image' %}" />
5 alt="{% trans 'Post image' %}" />
6 {% endif %}
6 {% endif %}
7 {{ obj.text.rendered|safe }}
7 {{ obj.get_text|safe }}
8 {% if obj.tags.all %}
8 {% if obj.tags.all %}
9 <p>
9 <p>
10 {% trans 'Tags' %}:
10 {% trans 'Tags' %}:
@@ -1,3 +1,5 b''
1 <div class="post">
1 <div class="post">
2 <a class="tag" href="{% url 'tag' tag_name=tag.name %}">#{{ tag.name }}</a>
2 {% autoescape off %}
3 </div> No newline at end of file
3 {{ tag.get_view }}
4 {% endautoescape %}
5 </div>
@@ -14,8 +14,9 b''
14 {% if all_tags %}
14 {% if all_tags %}
15 {% for tag in all_tags %}
15 {% for tag in all_tags %}
16 <div class="tag_item">
16 <div class="tag_item">
17 <a class="tag" href="{% url 'tag' tag.name %}">
17 {% autoescape off %}
18 #{{ tag.name }}</a>
18 {{ tag.get_view }}
19 {% endautoescape %}
19 </div>
20 </div>
20 {% endfor %}
21 {% endfor %}
21 {% else %}
22 {% else %}
@@ -11,7 +11,6 b''
11 {% endblock %}
11 {% endblock %}
12
12
13 {% block content %}
13 {% block content %}
14 {% spaceless %}
15 {% get_current_language as LANGUAGE_CODE %}
14 {% get_current_language as LANGUAGE_CODE %}
16
15
17 {% cache 600 thread_view thread.id thread.last_edit_time moderator LANGUAGE_CODE %}
16 {% cache 600 thread_view thread.id thread.last_edit_time moderator LANGUAGE_CODE %}
@@ -34,57 +33,59 b''
34 <div class="thread">
33 <div class="thread">
35 {% with can_bump=thread.can_bump %}
34 {% with can_bump=thread.can_bump %}
36 {% for post in thread.get_replies %}
35 {% for post in thread.get_replies %}
37 {% if forloop.first %}
36 {% with is_opening=forloop.first %}
38 {% post_view post moderator=moderator is_opening=True thread=thread can_bump=can_bump opening_post_id=opening_post.id %}
37 {% post_view post moderator=moderator is_opening=is_opening thread=thread bumpable=can_bump opening_post_id=opening_post.id %}
39 {% else %}
38 {% endwith %}
40 {% post_view post moderator=moderator is_opening=False thread=thread can_bump=can_bump opening_post_id=opening_post.id %}
41 {% endif %}
42 {% endfor %}
39 {% endfor %}
43 {% endwith %}
40 {% endwith %}
44 </div>
41 </div>
45
42
46 {% if not thread.archived %}
43 {% if not thread.archived %}
47
44 <div class="post-form-w" id="form">
48 <div class="post-form-w" id="form">
45 <script src="{% static 'js/panel.js' %}"></script>
49 <script src="{% static 'js/panel.js' %}"></script>
46 <div class="form-title">{% trans "Reply to thread" %} #{{ opening_post.id }}</div>
50 <div class="form-title">{% trans "Reply to thread" %} #{{ opening_post.id }}</div>
47 <div class="post-form" id="compact-form">
51 <div class="post-form" id="compact-form">
48 <div class="swappable-form-full">
52 <div class="swappable-form-full">
49 <form enctype="multipart/form-data" method="post"
53 <form enctype="multipart/form-data" method="post"
50 >{% csrf_token %}
54 >{% csrf_token %}
55 <div class="compact-form-text"></div>
51 <div class="compact-form-text"></div>
56 {{ form.as_div }}
52 {{ form.as_div }}
57 <div class="form-submit">
53 <div class="form-submit">
58 <input type="submit" value="{% trans "Post" %}"/>
54 <input type="submit" value="{% trans "Post" %}"/>
59 </div>
55 </div>
60 </form>
56 </form>
57 </div>
58 <a onclick="swapForm(); return false;" href="#">
59 {% trans 'Switch mode' %}
60 </a>
61 <div><a href="{% url "staticpage" name="help" %}">
62 {% trans 'Text syntax' %}</a></div>
61 </div>
63 </div>
62 <a onclick="swapForm(); return false;" href="#">
63 {% trans 'Switch mode' %}
64 </a>
65 <div><a href="{% url "staticpage" name="help" %}">
66 {% trans 'Text syntax' %}</a></div>
67 </div>
64 </div>
68 </div>
69
65
70 <script src="{% static 'js/jquery.form.min.js' %}"></script>
66 <script src="{% static 'js/jquery.form.min.js' %}"></script>
71 <script src="{% static 'js/thread_update.js' %}"></script>
67 <script src="{% static 'js/thread_update.js' %}"></script>
68 <script src="{% static 'js/3party/centrifuge.js' %}"></script>
72 {% endif %}
69 {% endif %}
73
70
74 <script src="{% static 'js/form.js' %}"></script>
71 <script src="{% static 'js/form.js' %}"></script>
75 <script src="{% static 'js/thread.js' %}"></script>
72 <script src="{% static 'js/thread.js' %}"></script>
76
73
77 {% endcache %}
74 {% endcache %}
78
79 {% endspaceless %}
80 {% endblock %}
75 {% endblock %}
81
76
82 {% block metapanel %}
77 {% block metapanel %}
83
78
84 {% get_current_language as LANGUAGE_CODE %}
79 {% get_current_language as LANGUAGE_CODE %}
85
80
86 <span class="metapanel" data-last-update="{{ last_update }}">
81 <span class="metapanel"
82 data-last-update="{{ last_update }}"
83 data-ws-token="{{ ws_token }}"
84 data-ws-project="{{ ws_project }}"
85 data-ws-host="{{ ws_host }}"
86 data-ws-port="{{ ws_port }}">
87 {% cache 600 thread_meta thread.last_edit_time moderator LANGUAGE_CODE %}
87 {% cache 600 thread_meta thread.last_edit_time moderator LANGUAGE_CODE %}
88 <span id="autoupdate">[-]</span>
88 <span id="reply-count">{{ thread.get_reply_count }}</span>/{{ max_replies }} {% trans 'messages' %},
89 <span id="reply-count">{{ thread.get_reply_count }}</span>/{{ max_replies }} {% trans 'messages' %},
89 <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}.
90 <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}.
90 {% trans 'Last update: ' %}<span id="last-update">{{ thread.last_edit_time }}</span>
91 {% trans 'Last update: ' %}<span id="last-update">{{ thread.last_edit_time }}</span>
@@ -25,7 +25,7 b''
25 {% endif %}
25 {% endif %}
26
26
27 {% for result in page.object_list %}
27 {% for result in page.object_list %}
28 {{ result.object.get_view }}
28 {{ result.object.get_search_view }}
29 {% endfor %}
29 {% endfor %}
30
30
31 {% if page.has_next %}
31 {% if page.has_next %}
@@ -35,4 +35,4 b''
35 </div>
35 </div>
36 {% endif %}
36 {% endif %}
37 {% endif %}
37 {% endif %}
38 {% endblock %} No newline at end of file
38 {% endblock %}
@@ -1,7 +1,7 b''
1 from django.test import TestCase, Client
1 from django.test import TestCase, Client
2 import time
2 import time
3 from boards import settings
3 from boards import settings
4 from boards.models import Post
4 from boards.models import Post, Tag
5 import neboard
5 import neboard
6
6
7
7
@@ -22,6 +22,7 b' class FormTest(TestCase):'
22
22
23 valid_tags = 'tag1 tag_2 тег_3'
23 valid_tags = 'tag1 tag_2 тег_3'
24 invalid_tags = '$%_356 ---'
24 invalid_tags = '$%_356 ---'
25 Tag.objects.create(name='tag1', required=True)
25
26
26 response = client.post(NEW_THREAD_PAGE, {'title': 'test title',
27 response = client.post(NEW_THREAD_PAGE, {'title': 'test title',
27 'text': TEST_TEXT,
28 'text': TEST_TEXT,
@@ -7,12 +7,10 b" NEW_THREAD_PAGE = '/'"
7 THREAD_PAGE_ONE = '/thread/1/'
7 THREAD_PAGE_ONE = '/thread/1/'
8 THREAD_PAGE = '/thread/'
8 THREAD_PAGE = '/thread/'
9 TAG_PAGE = '/tag/'
9 TAG_PAGE = '/tag/'
10 HTTP_CODE_REDIRECT = 302
10 HTTP_CODE_REDIRECT = 301
11 HTTP_CODE_OK = 200
11 HTTP_CODE_OK = 200
12 HTTP_CODE_NOT_FOUND = 404
12 HTTP_CODE_NOT_FOUND = 404
13
13
14 PAGE_404 = 'boards/404.html'
15
16
14
17 class PagesTest(TestCase):
15 class PagesTest(TestCase):
18
16
@@ -33,7 +31,7 b' class PagesTest(TestCase):'
33
31
34 response_not_existing = client.get(THREAD_PAGE + str(
32 response_not_existing = client.get(THREAD_PAGE + str(
35 existing_post_id + 1) + '/')
33 existing_post_id + 1) + '/')
36 self.assertEqual(PAGE_404, response_not_existing.templates[0].name,
34 self.assertEqual(HTTP_CODE_NOT_FOUND, response_not_existing.status_code,
37 'Not existing thread is opened')
35 'Not existing thread is opened')
38
36
39 response_existing = client.get(TAG_PAGE + tag_name + '/')
37 response_existing = client.get(TAG_PAGE + tag_name + '/')
@@ -42,8 +40,7 b' class PagesTest(TestCase):'
42 'Cannot open existing tag')
40 'Cannot open existing tag')
43
41
44 response_not_existing = client.get(TAG_PAGE + 'not_tag' + '/')
42 response_not_existing = client.get(TAG_PAGE + 'not_tag' + '/')
45 self.assertEqual(PAGE_404,
43 self.assertEqual(HTTP_CODE_NOT_FOUND, response_not_existing.status_code,
46 response_not_existing.templates[0].name,
47 'Not existing tag is opened')
44 'Not existing tag is opened')
48
45
49 reply_id = Post.objects.create_post('', TEST_TEXT,
46 reply_id = Post.objects.create_post('', TEST_TEXT,
@@ -51,6 +48,5 b' class PagesTest(TestCase):'
51 .get_thread())
48 .get_thread())
52 response_not_existing = client.get(THREAD_PAGE + str(
49 response_not_existing = client.get(THREAD_PAGE + str(
53 reply_id) + '/')
50 reply_id) + '/')
54 self.assertEqual(PAGE_404,
51 self.assertEqual(HTTP_CODE_REDIRECT, response_not_existing.status_code,
55 response_not_existing.templates[0].name,
56 'Reply is opened as a thread')
52 'Reply is opened as a thread')
@@ -26,7 +26,7 b' class PostTests(TestCase):'
26 post = self._create_post()
26 post = self._create_post()
27 post_id = post.id
27 post_id = post.id
28
28
29 Post.objects.delete_post(post)
29 post.delete()
30
30
31 self.assertFalse(Post.objects.filter(id=post_id).exists())
31 self.assertFalse(Post.objects.filter(id=post_id).exists())
32
32
@@ -37,9 +37,12 b' class PostTests(TestCase):'
37 thread = opening_post.get_thread()
37 thread = opening_post.get_thread()
38 reply = Post.objects.create_post("", "", thread=thread)
38 reply = Post.objects.create_post("", "", thread=thread)
39
39
40 thread.delete()
40 opening_post.delete()
41
41
42 self.assertFalse(Post.objects.filter(id=reply.id).exists())
42 self.assertFalse(Post.objects.filter(id=reply.id).exists(),
43 'Reply was not deleted with the thread.')
44 self.assertFalse(Post.objects.filter(id=opening_post.id).exists(),
45 'Opening post was not deleted with the thread.')
43
46
44 def test_post_to_thread(self):
47 def test_post_to_thread(self):
45 """Test adding post to a thread"""
48 """Test adding post to a thread"""
@@ -84,7 +87,6 b' class PostTests(TestCase):'
84 thread = post.get_thread()
87 thread = post.get_thread()
85 self.assertIsNotNone(post, 'Post not created')
88 self.assertIsNotNone(post, 'Post not created')
86 self.assertTrue(tag in thread.tags.all(), 'Tag not added to thread')
89 self.assertTrue(tag in thread.tags.all(), 'Tag not added to thread')
87 self.assertTrue(thread in tag.threads.all(), 'Thread not added to tag')
88
90
89 def test_thread_max_count(self):
91 def test_thread_max_count(self):
90 """Test deletion of old posts when the max thread count is reached"""
92 """Test deletion of old posts when the max thread count is reached"""
@@ -139,4 +141,23 b' class PostTests(TestCase):'
139 self.assertTrue(post_global_reflink in post.referenced_posts.all(),
141 self.assertTrue(post_global_reflink in post.referenced_posts.all(),
140 'Global reflink not connecting posts.')
142 'Global reflink not connecting posts.')
141
143
142 # TODO Check that links are parsed into the rendered text
144 def test_thread_replies(self):
145 """
146 Tests that the replies can be queried from a thread in all possible
147 ways.
148 """
149
150 tag = Tag.objects.create(name='test_tag')
151 opening_post = Post.objects.create_post(title='title', text='text',
152 tags=[tag])
153 thread = opening_post.get_thread()
154
155 reply1 = Post.objects.create_post(title='title', text='text', thread=thread)
156 reply2 = Post.objects.create_post(title='title', text='text', thread=thread)
157
158 replies = thread.get_replies()
159 self.assertTrue(len(replies) > 0, 'No replies found for thread.')
160
161 replies = thread.get_replies(view_fields_only=True)
162 self.assertTrue(len(replies) > 0,
163 'No replies found for thread with view fields only.')
@@ -8,6 +8,10 b' logger = logging.getLogger(__name__)'
8
8
9 HTTP_CODE_OK = 200
9 HTTP_CODE_OK = 200
10
10
11 EXCLUDED_VIEWS = {
12 'banned',
13 }
14
11
15
12 class ViewTest(TestCase):
16 class ViewTest(TestCase):
13
17
@@ -21,13 +25,17 b' class ViewTest(TestCase):'
21 for url in urls.urlpatterns:
25 for url in urls.urlpatterns:
22 try:
26 try:
23 view_name = url.name
27 view_name = url.name
28 if view_name in EXCLUDED_VIEWS:
29 logger.debug('View {} is excluded.'.format(view_name))
30 continue
31
24 logger.debug('Testing view %s' % view_name)
32 logger.debug('Testing view %s' % view_name)
25
33
26 try:
34 try:
27 response = client.get(reverse(view_name))
35 response = client.get(reverse(view_name))
28
36
29 self.assertEqual(HTTP_CODE_OK, response.status_code,
37 self.assertEqual(HTTP_CODE_OK, response.status_code,
30 '%s view not opened' % view_name)
38 'View not opened: {}'.format(view_name))
31 except NoReverseMatch:
39 except NoReverseMatch:
32 # This view just needs additional arguments
40 # This view just needs additional arguments
33 pass
41 pass
@@ -212,8 +212,4 b' class ImageWithThumbsField(ImageField):'
212 thumb_height_ratio = int(original_height * scale_ratio)
212 thumb_height_ratio = int(original_height * scale_ratio)
213
213
214 setattr(instance, thumb_width_field, thumb_width_ratio)
214 setattr(instance, thumb_width_field, thumb_width_ratio)
215 setattr(instance, thumb_height_field, thumb_height_ratio)
215 setattr(instance, thumb_height_field, thumb_height_ratio) No newline at end of file
216
217
218 from south.modelsinspector import add_introspection_rules
219 add_introspection_rules([], ["^boards\.thumbs\.ImageWithThumbsField"])
@@ -5,11 +5,9 b' from boards.rss import AllThreadsFeed, T'
5 from boards.views import api, tag_threads, all_threads, \
5 from boards.views import api, tag_threads, all_threads, \
6 settings, all_tags
6 settings, all_tags
7 from boards.views.authors import AuthorsView
7 from boards.views.authors import AuthorsView
8 from boards.views.delete_post import DeletePostView
9 from boards.views.ban import BanUserView
8 from boards.views.ban import BanUserView
10 from boards.views.search import BoardSearchView
9 from boards.views.search import BoardSearchView
11 from boards.views.static import StaticPageView
10 from boards.views.static import StaticPageView
12 from boards.views.post_admin import PostAdminView
13 from boards.views.preview import PostPreviewView
11 from boards.views.preview import PostPreviewView
14 from boards.views.sync import get_post_sync_data
12 from boards.views.sync import get_post_sync_data
15
13
@@ -37,15 +35,9 b" urlpatterns = patterns('',"
37 url(r'^thread/(?P<post_id>\w+)/mode/(?P<mode>\w+)/$', views.thread.ThreadView
35 url(r'^thread/(?P<post_id>\w+)/mode/(?P<mode>\w+)/$', views.thread.ThreadView
38 .as_view(), name='thread_mode'),
36 .as_view(), name='thread_mode'),
39
37
40 # /boards/post_admin/
41 url(r'^post_admin/(?P<post_id>\w+)/$', PostAdminView.as_view(),
42 name='post_admin'),
43
44 url(r'^settings/$', settings.SettingsView.as_view(), name='settings'),
38 url(r'^settings/$', settings.SettingsView.as_view(), name='settings'),
45 url(r'^tags/$', all_tags.AllTagsView.as_view(), name='tags'),
39 url(r'^tags/$', all_tags.AllTagsView.as_view(), name='tags'),
46 url(r'^authors/$', AuthorsView.as_view(), name='authors'),
40 url(r'^authors/$', AuthorsView.as_view(), name='authors'),
47 url(r'^delete/(?P<post_id>\w+)/$', DeletePostView.as_view(),
48 name='delete'),
49 url(r'^ban/(?P<post_id>\w+)/$', BanUserView.as_view(), name='ban'),
41 url(r'^ban/(?P<post_id>\w+)/$', BanUserView.as_view(), name='ban'),
50
42
51 url(r'^banned/$', views.banned.BannedView.as_view(), name='banned'),
43 url(r'^banned/$', views.banned.BannedView.as_view(), name='banned'),
@@ -1,8 +1,8 b''
1 """
1 """
2 This module contains helper functions and helper classes.
2 This module contains helper functions and helper classes.
3 """
3 """
4 import hashlib
5 import time
4 import time
5 import hmac
6
6
7 from django.utils import timezone
7 from django.utils import timezone
8
8
@@ -14,55 +14,6 b" KEY_CAPTCHA_DELAY_TIME = 'key_captcha_de"
14 KEY_CAPTCHA_LAST_ACTIVITY = 'key_captcha_last_activity'
14 KEY_CAPTCHA_LAST_ACTIVITY = 'key_captcha_last_activity'
15
15
16
16
17 def need_include_captcha(request):
18 """
19 Check if request is made by a user.
20 It contains rules which check for bots.
21 """
22
23 if not settings.ENABLE_CAPTCHA:
24 return False
25
26 enable_captcha = False
27
28 #newcomer
29 if KEY_CAPTCHA_LAST_ACTIVITY not in request.session:
30 return settings.ENABLE_CAPTCHA
31
32 last_activity = request.session[KEY_CAPTCHA_LAST_ACTIVITY]
33 current_delay = int(time.time()) - last_activity
34
35 delay_time = (request.session[KEY_CAPTCHA_DELAY_TIME]
36 if KEY_CAPTCHA_DELAY_TIME in request.session
37 else settings.CAPTCHA_DEFAULT_SAFE_TIME)
38
39 if current_delay < delay_time:
40 enable_captcha = True
41
42 return enable_captcha
43
44
45 def update_captcha_access(request, passed):
46 """
47 Update captcha fields.
48 It will reduce delay time if user passed captcha verification and
49 it will increase it otherwise.
50 """
51 session = request.session
52
53 delay_time = (request.session[KEY_CAPTCHA_DELAY_TIME]
54 if KEY_CAPTCHA_DELAY_TIME in request.session
55 else settings.CAPTCHA_DEFAULT_SAFE_TIME)
56
57 if passed:
58 delay_time -= 2 if delay_time >= 7 else 5
59 else:
60 delay_time += 10
61
62 session[KEY_CAPTCHA_LAST_ACTIVITY] = int(time.time())
63 session[KEY_CAPTCHA_DELAY_TIME] = delay_time
64
65
66 def get_client_ip(request):
17 def get_client_ip(request):
67 x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
18 x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
68 if x_forwarded_for:
19 if x_forwarded_for:
@@ -76,3 +27,17 b' def datetime_to_epoch(datetime):'
76 return int(time.mktime(timezone.localtime(
27 return int(time.mktime(timezone.localtime(
77 datetime,timezone.get_current_timezone()).timetuple())
28 datetime,timezone.get_current_timezone()).timetuple())
78 * 1000000 + datetime.microsecond)
29 * 1000000 + datetime.microsecond)
30
31
32 def get_websocket_token(user_id='', timestamp=''):
33 """
34 Create token to validate information provided by new connection.
35 """
36
37 sign = hmac.new(settings.CENTRIFUGE_PROJECT_SECRET.encode())
38 sign.update(settings.CENTRIFUGE_PROJECT_ID.encode())
39 sign.update(user_id.encode())
40 sign.update(timestamp.encode())
41 token = sign.hexdigest()
42
43 return token No newline at end of file
@@ -7,7 +7,8 b' from boards.models.tag import Tag'
7 class AllTagsView(BaseBoardView):
7 class AllTagsView(BaseBoardView):
8
8
9 def get(self, request):
9 def get(self, request):
10 context = self.get_context_data(request=request)
10 params = dict()
11 context['all_tags'] = Tag.objects.get_not_empty_tags()
12
11
13 return render(request, 'boards/tags.html', context)
12 params['all_tags'] = Tag.objects.get_not_empty_tags()
13
14 return render(request, 'boards/tags.html', params)
@@ -1,5 +1,3 b''
1 import string
2
3 from django.db import transaction
1 from django.db import transaction
4 from django.shortcuts import render, redirect
2 from django.shortcuts import render, redirect
5
3
@@ -12,6 +10,7 b' from boards.views.banned import BannedVi'
12 from boards.views.base import BaseBoardView, CONTEXT_FORM
10 from boards.views.base import BaseBoardView, CONTEXT_FORM
13 from boards.views.posting_mixin import PostMixin
11 from boards.views.posting_mixin import PostMixin
14
12
13
15 FORM_TAGS = 'tags'
14 FORM_TAGS = 'tags'
16 FORM_TEXT = 'text'
15 FORM_TEXT = 'text'
17 FORM_TITLE = 'title'
16 FORM_TITLE = 'title'
@@ -34,7 +33,7 b' class AllThreadsView(PostMixin, BaseBoar'
34 super(AllThreadsView, self).__init__()
33 super(AllThreadsView, self).__init__()
35
34
36 def get(self, request, page=DEFAULT_PAGE, form=None):
35 def get(self, request, page=DEFAULT_PAGE, form=None):
37 context = self.get_context_data(request=request)
36 params = self.get_context_data(request=request)
38
37
39 if not form:
38 if not form:
40 form = ThreadForm(error_class=PlainErrorList)
39 form = ThreadForm(error_class=PlainErrorList)
@@ -46,12 +45,12 b' class AllThreadsView(PostMixin, BaseBoar'
46
45
47 threads = paginator.page(page).object_list
46 threads = paginator.page(page).object_list
48
47
49 context[PARAMETER_THREADS] = threads
48 params[PARAMETER_THREADS] = threads
50 context[CONTEXT_FORM] = form
49 params[CONTEXT_FORM] = form
51
50
52 self._get_page_context(paginator, context, page)
51 self._get_page_context(paginator, params, page)
53
52
54 return render(request, TEMPLATE, context)
53 return render(request, TEMPLATE, params)
55
54
56 def post(self, request, page=DEFAULT_PAGE):
55 def post(self, request, page=DEFAULT_PAGE):
57 form = ThreadForm(request.POST, request.FILES,
56 form = ThreadForm(request.POST, request.FILES,
@@ -66,14 +65,13 b' class AllThreadsView(PostMixin, BaseBoar'
66
65
67 return self.get(request, page, form)
66 return self.get(request, page, form)
68
67
69 @staticmethod
68 def _get_page_context(self, paginator, params, page):
70 def _get_page_context(paginator, context, page):
71 """
69 """
72 Get pagination context variables
70 Get pagination context variables
73 """
71 """
74
72
75 context[PARAMETER_PAGINATOR] = paginator
73 params[PARAMETER_PAGINATOR] = paginator
76 context[PARAMETER_CURRENT_PAGE] = paginator.page(int(page))
74 params[PARAMETER_CURRENT_PAGE] = paginator.page(int(page))
77
75
78 @staticmethod
76 @staticmethod
79 def parse_tags_string(tag_strings):
77 def parse_tags_string(tag_strings):
@@ -112,14 +110,10 b' class AllThreadsView(PostMixin, BaseBoar'
112
110
113 title = data[FORM_TITLE]
111 title = data[FORM_TITLE]
114 text = data[FORM_TEXT]
112 text = data[FORM_TEXT]
113 image = data.get(FORM_IMAGE)
115
114
116 text = self._remove_invalid_links(text)
115 text = self._remove_invalid_links(text)
117
116
118 if FORM_IMAGE in list(data.keys()):
119 image = data[FORM_IMAGE]
120 else:
121 image = None
122
123 tag_strings = data[FORM_TAGS]
117 tag_strings = data[FORM_TAGS]
124
118
125 tags = self.parse_tags_string(tag_strings)
119 tags = self.parse_tags_string(tag_strings)
@@ -127,6 +121,10 b' class AllThreadsView(PostMixin, BaseBoar'
127 post = Post.objects.create_post(title=title, text=text, image=image,
121 post = Post.objects.create_post(title=title, text=text, image=image,
128 ip=ip, tags=tags)
122 ip=ip, tags=tags)
129
123
124 # This is required to update the threads to which posts we have replied
125 # when creating this one
126 post.send_to_websocket(request)
127
130 if html_response:
128 if html_response:
131 return redirect(post.get_url())
129 return redirect(post.get_url())
132
130
@@ -7,7 +7,6 b' from django.shortcuts import get_object_'
7 from django.template import RequestContext
7 from django.template import RequestContext
8 from django.utils import timezone
8 from django.utils import timezone
9 from django.core import serializers
9 from django.core import serializers
10 from django.template.loader import render_to_string
11
10
12 from boards.forms import PostForm, PlainErrorList
11 from boards.forms import PostForm, PlainErrorList
13 from boards.models import Post, Thread, Tag
12 from boards.models import Post, Thread, Tag
@@ -56,14 +55,12 b' def api_get_threaddiff(request, thread_i'
56 pub_time__lte=filter_time,
55 pub_time__lte=filter_time,
57 last_edit_time__gt=filter_time)
56 last_edit_time__gt=filter_time)
58
57
59 diff_type = DIFF_TYPE_HTML
58 diff_type = request.GET.get(PARAMETER_DIFF_TYPE, DIFF_TYPE_HTML)
60 if PARAMETER_DIFF_TYPE in request.GET:
61 diff_type = request.GET[PARAMETER_DIFF_TYPE]
62
59
63 for post in added_posts:
60 for post in added_posts:
64 json_data['added'].append(_get_post_data(post.id, diff_type, request))
61 json_data['added'].append(get_post_data(post.id, diff_type, request))
65 for post in updated_posts:
62 for post in updated_posts:
66 json_data['updated'].append(_get_post_data(post.id, diff_type, request))
63 json_data['updated'].append(get_post_data(post.id, diff_type, request))
67 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
64 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
68
65
69 return HttpResponse(content=json.dumps(json_data))
66 return HttpResponse(content=json.dumps(json_data))
@@ -114,8 +111,6 b' def get_post(request, post_id):'
114 in threads list with 'truncated' get parameter.
111 in threads list with 'truncated' get parameter.
115 """
112 """
116
113
117 logger.info('Getting post #%s' % post_id)
118
119 post = get_object_or_404(Post, id=post_id)
114 post = get_object_or_404(Post, id=post_id)
120
115
121 context = RequestContext(request)
116 context = RequestContext(request)
@@ -123,7 +118,8 b' def get_post(request, post_id):'
123 if PARAMETER_TRUNCATED in request.GET:
118 if PARAMETER_TRUNCATED in request.GET:
124 context[PARAMETER_TRUNCATED] = True
119 context[PARAMETER_TRUNCATED] = True
125
120
126 return render(request, 'boards/api_post.html', context)
121 # TODO Use dict here
122 return render(request, 'boards/api_post.html', context_instance=context)
127
123
128
124
129 # TODO Test this
125 # TODO Test this
@@ -138,7 +134,7 b' def api_get_threads(request, count):'
138 tag_name = request.GET[PARAMETER_TAG]
134 tag_name = request.GET[PARAMETER_TAG]
139 if tag_name is not None:
135 if tag_name is not None:
140 tag = get_object_or_404(Tag, name=tag_name)
136 tag = get_object_or_404(Tag, name=tag_name)
141 threads = tag.threads.filter(archived=False)
137 threads = tag.get_threads().filter(archived=False)
142 else:
138 else:
143 threads = Thread.objects.filter(archived=False)
139 threads = Thread.objects.filter(archived=False)
144
140
@@ -156,7 +152,7 b' def api_get_threads(request, count):'
156 opening_post = thread.get_opening_post()
152 opening_post = thread.get_opening_post()
157
153
158 # TODO Add tags, replies and images count
154 # TODO Add tags, replies and images count
159 opening_posts.append(_get_post_data(opening_post.id,
155 opening_posts.append(get_post_data(opening_post.id,
160 include_last_update=True))
156 include_last_update=True))
161
157
162 return HttpResponse(content=json.dumps(opening_posts))
158 return HttpResponse(content=json.dumps(opening_posts))
@@ -196,7 +192,7 b' def api_get_thread_posts(request, openin'
196 json_post_list = []
192 json_post_list = []
197
193
198 for post in posts:
194 for post in posts:
199 json_post_list.append(_get_post_data(post.id))
195 json_post_list.append(get_post_data(post.id))
200 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
196 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
201 json_data['posts'] = json_post_list
197 json_data['posts'] = json_post_list
202
198
@@ -219,30 +215,9 b' def api_get_post(request, post_id):'
219 return HttpResponse(content=json)
215 return HttpResponse(content=json)
220
216
221
217
222 # TODO Add pub time and replies
218 # TODO Remove this method and use post method directly
223 def _get_post_data(post_id, format_type=DIFF_TYPE_JSON, request=None,
219 def get_post_data(post_id, format_type=DIFF_TYPE_JSON, request=None,
224 include_last_update=False):
220 include_last_update=False):
225 if format_type == DIFF_TYPE_HTML:
221 post = get_object_or_404(Post, id=post_id)
226 post = get_object_or_404(Post, id=post_id)
222 return post.get_post_data(format_type=format_type, request=request,
227
223 include_last_update=include_last_update)
228 context = RequestContext(request)
229 context['post'] = post
230 if PARAMETER_TRUNCATED in request.GET:
231 context[PARAMETER_TRUNCATED] = True
232
233 return render_to_string('boards/api_post.html', context)
234 elif format_type == DIFF_TYPE_JSON:
235 post = get_object_or_404(Post, id=post_id)
236 post_json = {
237 'id': post.id,
238 'title': post.title,
239 'text': post.text.rendered,
240 }
241 if post.images.exists():
242 post_image = post.get_first_image()
243 post_json['image'] = post_image.image.url
244 post_json['image_preview'] = post_image.image.url_200x150
245 if include_last_update:
246 post_json['bump_time'] = datetime_to_epoch(
247 post.thread_new.bump_time)
248 return post_json
@@ -7,7 +7,7 b' from boards.views.base import BaseBoardV'
7 class AuthorsView(BaseBoardView):
7 class AuthorsView(BaseBoardView):
8
8
9 def get(self, request):
9 def get(self, request):
10 context = self.get_context_data(request=request)
10 params = dict()
11 context['authors'] = authors
11 params['authors'] = authors
12
12
13 return render(request, 'boards/authors.html', context)
13 return render(request, 'boards/authors.html', params)
@@ -9,8 +9,9 b' class BannedView(BaseBoardView):'
9 def get(self, request):
9 def get(self, request):
10 """Show the page that notifies that user is banned"""
10 """Show the page that notifies that user is banned"""
11
11
12 context = self.get_context_data(request=request)
12 params = dict()
13
13
14 ban = get_object_or_404(Ban, ip=utils.get_client_ip(request))
14 ban = get_object_or_404(Ban, ip=utils.get_client_ip(request))
15 context['ban_reason'] = ban.reason
15 params['ban_reason'] = ban.reason
16 return render(request, 'boards/staticpages/banned.html', context)
16
17 return render(request, 'boards/staticpages/banned.html', params)
@@ -14,11 +14,7 b" CONTEXT_FORM = 'form'"
14 class BaseBoardView(View):
14 class BaseBoardView(View):
15
15
16 def get_context_data(self, **kwargs):
16 def get_context_data(self, **kwargs):
17 request = kwargs['request']
17 return dict()
18 # context = self._default_context(request)
19 context = RequestContext(request)
20
21 return context
22
18
23 @transaction.atomic
19 @transaction.atomic
24 def _ban_current_user(self, request):
20 def _ban_current_user(self, request):
@@ -1,3 +1,4 b''
1 PARAM_NEXT = 'next'
1 PARAMETER_METHOD = 'method'
2 PARAMETER_METHOD = 'method'
2
3
3 from django.shortcuts import redirect
4 from django.shortcuts import redirect
@@ -13,8 +14,8 b' class RedirectNextMixin:'
13 current view has finished its work.
14 current view has finished its work.
14 """
15 """
15
16
16 if 'next' in request.GET:
17 if PARAM_NEXT in request.GET:
17 next_page = request.GET['next']
18 next_page = request.GET[PARAM_NEXT]
18 return HttpResponseRedirect(next_page)
19 return HttpResponseRedirect(next_page)
19 else:
20 else:
20 return redirect('index')
21 return redirect('index')
@@ -9,5 +9,9 b' class NotFoundView(BaseBoardView):'
9 """
9 """
10
10
11 def get(self, request):
11 def get(self, request):
12 context = self.get_context_data(request=request)
12 params = self.get_context_data()
13 return render(request, 'boards/404.html', context)
13
14 response = render(request, 'boards/404.html', params)
15 response.status_code = 404
16
17 return response
@@ -18,7 +18,8 b' class PostPreviewView(View):'
18 def get(self, request):
18 def get(self, request):
19 context = RequestContext(request)
19 context = RequestContext(request)
20
20
21 return render(request, TEMPLATE, context)
21 # TODO Use dict here
22 return render(request, TEMPLATE, context_instance=context)
22
23
23 def post(self, request):
24 def post(self, request):
24 context = RequestContext(request)
25 context = RequestContext(request)
@@ -32,4 +33,5 b' class PostPreviewView(View):'
32 context[CONTEXT_RESULT] = rendered_text
33 context[CONTEXT_RESULT] = rendered_text
33 context[CONTEXT_QUERY] = raw_text
34 context[CONTEXT_QUERY] = raw_text
34
35
35 return render(request, TEMPLATE, context)
36 # TODO Use dict here
37 return render(request, TEMPLATE, context_instance=context)
@@ -1,10 +1,14 b''
1 from django.shortcuts import render
1 from django.shortcuts import render
2 from django.template import RequestContext
3 from django.views.generic import View
2 from django.views.generic import View
4 from haystack.query import SearchQuerySet
3 from haystack.query import SearchQuerySet
4
5 from boards.abstracts.paginator import get_paginator
5 from boards.abstracts.paginator import get_paginator
6 from boards.forms import SearchForm, PlainErrorList
6 from boards.forms import SearchForm, PlainErrorList
7
7
8
9 MIN_QUERY_LENGTH = 3
10 RESULTS_PER_PAGE = 10
11
8 FORM_QUERY = 'query'
12 FORM_QUERY = 'query'
9
13
10 CONTEXT_QUERY = 'query'
14 CONTEXT_QUERY = 'query'
@@ -20,21 +24,20 b" TEMPLATE = 'search/search.html'"
20
24
21 class BoardSearchView(View):
25 class BoardSearchView(View):
22 def get(self, request):
26 def get(self, request):
23 context = RequestContext(request)
27 params = dict()
28
24 form = SearchForm(request.GET, error_class=PlainErrorList)
29 form = SearchForm(request.GET, error_class=PlainErrorList)
25 context[CONTEXT_FORM] = form
30 params[CONTEXT_FORM] = form
26
31
27 if form.is_valid():
32 if form.is_valid():
28 query = form.cleaned_data[FORM_QUERY]
33 query = form.cleaned_data[FORM_QUERY]
29 if len(query) >= 3:
34 if len(query) >= MIN_QUERY_LENGTH:
30 results = SearchQuerySet().auto_query(query).order_by('-id').load_all()
35 results = SearchQuerySet().auto_query(query).order_by('-id')
31 paginator = get_paginator(results, 10)
36 paginator = get_paginator(results, RESULTS_PER_PAGE)
32
37
33 if REQUEST_PAGE in request.GET:
38 page = int(request.GET.get(REQUEST_PAGE, '1'))
34 page = int(request.GET[REQUEST_PAGE])
35 else:
36 page = 1
37 context[CONTEXT_PAGE] = paginator.page(page)
38 context[CONTEXT_QUERY] = query
39
39
40 return render(request, TEMPLATE, context)
40 params[CONTEXT_PAGE] = paginator.page(page)
41 params[CONTEXT_QUERY] = query
42
43 return render(request, TEMPLATE, params)
@@ -5,24 +5,27 b' from boards.abstracts.settingsmanager im'
5 from boards.views.base import BaseBoardView, CONTEXT_FORM
5 from boards.views.base import BaseBoardView, CONTEXT_FORM
6 from boards.forms import SettingsForm, PlainErrorList
6 from boards.forms import SettingsForm, PlainErrorList
7
7
8 FORM_THEME = 'theme'
9
8 CONTEXT_HIDDEN_TAGS = 'hidden_tags'
10 CONTEXT_HIDDEN_TAGS = 'hidden_tags'
9
11
10
12
11 class SettingsView(BaseBoardView):
13 class SettingsView(BaseBoardView):
12
14
13 def get(self, request):
15 def get(self, request):
14 context = self.get_context_data(request=request)
16 params = self.get_context_data()
15 settings_manager = get_settings_manager(request)
17 settings_manager = get_settings_manager(request)
16
18
17 selected_theme = settings_manager.get_theme()
19 selected_theme = settings_manager.get_theme()
18
20
19 form = SettingsForm(initial={'theme': selected_theme},
21 form = SettingsForm(initial={FORM_THEME: selected_theme},
20 error_class=PlainErrorList)
22 error_class=PlainErrorList)
21
23
22 context[CONTEXT_FORM] = form
24 params[CONTEXT_FORM] = form
23 context[CONTEXT_HIDDEN_TAGS] = settings_manager.get_hidden_tags()
25 params[CONTEXT_HIDDEN_TAGS] = settings_manager.get_hidden_tags()
24
26
25 return render(request, 'boards/settings.html', context)
27 # TODO Use dict here
28 return render(request, 'boards/settings.html', params)
26
29
27 def post(self, request):
30 def post(self, request):
28 settings_manager = get_settings_manager(request)
31 settings_manager = get_settings_manager(request)
@@ -31,7 +34,7 b' class SettingsView(BaseBoardView):'
31 form = SettingsForm(request.POST, error_class=PlainErrorList)
34 form = SettingsForm(request.POST, error_class=PlainErrorList)
32
35
33 if form.is_valid():
36 if form.is_valid():
34 selected_theme = form.cleaned_data['theme']
37 selected_theme = form.cleaned_data[FORM_THEME]
35
38
36 settings_manager.set_theme(selected_theme)
39 settings_manager.set_theme(selected_theme)
37
40
@@ -10,5 +10,4 b' class StaticPageView(BaseBoardView):'
10 Show a static page that needs only tags list and a CSS
10 Show a static page that needs only tags list and a CSS
11 """
11 """
12
12
13 context = self.get_context_data(request=request)
13 return render(request, 'boards/staticpages/' + name + '.html')
14 return render(request, 'boards/staticpages/' + name + '.html', context)
@@ -1,11 +1,14 b''
1 from django.shortcuts import get_object_or_404
1 from django.shortcuts import get_object_or_404
2
2
3 from boards.abstracts.settingsmanager import get_settings_manager
3 from boards.abstracts.settingsmanager import get_settings_manager
4 from boards.models import Tag
4 from boards.models import Tag, Thread
5 from boards.views.all_threads import AllThreadsView, DEFAULT_PAGE
5 from boards.views.all_threads import AllThreadsView, DEFAULT_PAGE
6 from boards.views.mixins import DispatcherMixin, RedirectNextMixin
6 from boards.views.mixins import DispatcherMixin, RedirectNextMixin
7 from boards.forms import ThreadForm, PlainErrorList
7 from boards.forms import ThreadForm, PlainErrorList
8
8
9 PARAM_HIDDEN_TAGS = 'hidden_tags'
10 PARAM_FAV_TAGS = 'fav_tags'
11 PARAM_TAG = 'tag'
9
12
10 __author__ = 'neko259'
13 __author__ = 'neko259'
11
14
@@ -17,20 +20,20 b' class TagView(AllThreadsView, Dispatcher'
17 def get_threads(self):
20 def get_threads(self):
18 tag = get_object_or_404(Tag, name=self.tag_name)
21 tag = get_object_or_404(Tag, name=self.tag_name)
19
22
20 return tag.threads.all().order_by('-bump_time')
23 return tag.get_threads()
21
24
22 def get_context_data(self, **kwargs):
25 def get_context_data(self, **kwargs):
23 context = super(TagView, self).get_context_data(**kwargs)
26 params = super(TagView, self).get_context_data(**kwargs)
24
27
25 settings_manager = get_settings_manager(kwargs['request'])
28 settings_manager = get_settings_manager(kwargs['request'])
26
29
27 tag = get_object_or_404(Tag, name=self.tag_name)
30 tag = get_object_or_404(Tag, name=self.tag_name)
28 context['tag'] = tag
31 params[PARAM_TAG] = tag
29
32
30 context['fav_tags'] = settings_manager.get_fav_tags()
33 params[PARAM_FAV_TAGS] = settings_manager.get_fav_tags()
31 context['hidden_tags'] = settings_manager.get_hidden_tags()
34 params[PARAM_HIDDEN_TAGS] = settings_manager.get_hidden_tags()
32
35
33 return context
36 return params
34
37
35 def get(self, request, tag_name, page=DEFAULT_PAGE, form=None):
38 def get(self, request, tag_name, page=DEFAULT_PAGE, form=None):
36 self.tag_name = tag_name
39 self.tag_name = tag_name
@@ -10,6 +10,7 b' from boards.models import Post, Ban'
10 from boards.views.banned import BannedView
10 from boards.views.banned import BannedView
11 from boards.views.base import BaseBoardView, CONTEXT_FORM
11 from boards.views.base import BaseBoardView, CONTEXT_FORM
12 from boards.views.posting_mixin import PostMixin
12 from boards.views.posting_mixin import PostMixin
13 import neboard
13
14
14 TEMPLATE_GALLERY = 'boards/thread_gallery.html'
15 TEMPLATE_GALLERY = 'boards/thread_gallery.html'
15 TEMPLATE_NORMAL = 'boards/thread.html'
16 TEMPLATE_NORMAL = 'boards/thread.html'
@@ -22,6 +23,10 b' CONTEXT_LASTUPDATE = "last_update"'
22 CONTEXT_MAX_REPLIES = 'max_replies'
23 CONTEXT_MAX_REPLIES = 'max_replies'
23 CONTEXT_THREAD = 'thread'
24 CONTEXT_THREAD = 'thread'
24 CONTEXT_BUMPABLE = 'bumpable'
25 CONTEXT_BUMPABLE = 'bumpable'
26 CONTEXT_WS_TOKEN = 'ws_token'
27 CONTEXT_WS_PROJECT = 'ws_project'
28 CONTEXT_WS_HOST = 'ws_host'
29 CONTEXT_WS_PORT = 'ws_port'
25
30
26 FORM_TITLE = 'title'
31 FORM_TITLE = 'title'
27 FORM_TEXT = 'text'
32 FORM_TEXT = 'text'
@@ -48,36 +53,44 b' class ThreadView(BaseBoardView, PostMixi'
48
53
49 thread_to_show = opening_post.get_thread()
54 thread_to_show = opening_post.get_thread()
50
55
51 context = self.get_context_data(request=request)
56 params = dict()
57
58 params[CONTEXT_FORM] = form
59 params[CONTEXT_LASTUPDATE] = str(utils.datetime_to_epoch(
60 thread_to_show.last_edit_time))
61 params[CONTEXT_THREAD] = thread_to_show
62 params[CONTEXT_MAX_REPLIES] = settings.MAX_POSTS_PER_THREAD
52
63
53 context[CONTEXT_FORM] = form
64 if settings.WEBSOCKETS_ENABLED:
54 context[CONTEXT_LASTUPDATE] = utils.datetime_to_epoch(
65 params[CONTEXT_WS_TOKEN] = utils.get_websocket_token(
55 thread_to_show.last_edit_time)
66 timestamp=params[CONTEXT_LASTUPDATE])
56 context[CONTEXT_THREAD] = thread_to_show
67 params[CONTEXT_WS_PROJECT] = neboard.settings.CENTRIFUGE_PROJECT_ID
57 context[CONTEXT_MAX_REPLIES] = settings.MAX_POSTS_PER_THREAD
68 params[CONTEXT_WS_HOST] = request.get_host().split(':')[0]
69 params[CONTEXT_WS_PORT] = neboard.settings.CENTRIFUGE_PORT
58
70
71 # TODO Move this to subclasses: NormalThreadView, GalleryThreadView etc
59 if MODE_NORMAL == mode:
72 if MODE_NORMAL == mode:
60 bumpable = thread_to_show.can_bump()
73 bumpable = thread_to_show.can_bump()
61 context[CONTEXT_BUMPABLE] = bumpable
74 params[CONTEXT_BUMPABLE] = bumpable
62 if bumpable:
75 if bumpable:
63 left_posts = settings.MAX_POSTS_PER_THREAD \
76 left_posts = settings.MAX_POSTS_PER_THREAD \
64 - thread_to_show.get_reply_count()
77 - thread_to_show.get_reply_count()
65 context[CONTEXT_POSTS_LEFT] = left_posts
78 params[CONTEXT_POSTS_LEFT] = left_posts
66 context[CONTEXT_BUMPLIMIT_PRG] = str(
79 params[CONTEXT_BUMPLIMIT_PRG] = str(
67 float(left_posts) / settings.MAX_POSTS_PER_THREAD * 100)
80 float(left_posts) / settings.MAX_POSTS_PER_THREAD * 100)
68
81
69 context[CONTEXT_OP] = opening_post
82 params[CONTEXT_OP] = opening_post
70
83
71 document = TEMPLATE_NORMAL
84 document = TEMPLATE_NORMAL
72 elif MODE_GALLERY == mode:
85 elif MODE_GALLERY == mode:
73 context[CONTEXT_POSTS] = thread_to_show.get_replies_with_images(
86 params[CONTEXT_POSTS] = thread_to_show.get_replies_with_images(
74 view_fields_only=True)
87 view_fields_only=True)
75
88
76 document = TEMPLATE_GALLERY
89 document = TEMPLATE_GALLERY
77 else:
90 else:
78 raise Http404
91 raise Http404
79
92
80 return render(request, document, context)
93 return render(request, document, params)
81
94
82 def post(self, request, post_id, mode=MODE_NORMAL):
95 def post(self, request, post_id, mode=MODE_NORMAL):
83 opening_post = get_object_or_404(Post, id=post_id)
96 opening_post = get_object_or_404(Post, id=post_id)
@@ -99,37 +112,24 b' class ThreadView(BaseBoardView, PostMixi'
99
112
100 return self.get(request, post_id, mode, form)
113 return self.get(request, post_id, mode, form)
101
114
102 @transaction.atomic
103 def new_post(self, request, form, opening_post=None, html_response=True):
115 def new_post(self, request, form, opening_post=None, html_response=True):
104 """Add a new post (in thread or as a reply)."""
116 """Add a new post (in thread or as a reply)."""
105
117
106 ip = utils.get_client_ip(request)
118 ip = utils.get_client_ip(request)
107 is_banned = Ban.objects.filter(ip=ip).exists()
108
109 if is_banned:
110 if html_response:
111 return redirect(BannedView().as_view())
112 else:
113 return None
114
119
115 data = form.cleaned_data
120 data = form.cleaned_data
116
121
117 title = data[FORM_TITLE]
122 title = data[FORM_TITLE]
118 text = data[FORM_TEXT]
123 text = data[FORM_TEXT]
124 image = data.get(FORM_IMAGE)
119
125
120 text = self._remove_invalid_links(text)
126 text = self._remove_invalid_links(text)
121
127
122 if FORM_IMAGE in list(data.keys()):
123 image = data[FORM_IMAGE]
124 else:
125 image = None
126
127 tags = []
128
129 post_thread = opening_post.get_thread()
128 post_thread = opening_post.get_thread()
130
129
131 post = Post.objects.create_post(title=title, text=text, image=image,
130 post = Post.objects.create_post(title=title, text=text, image=image,
132 thread=post_thread, ip=ip, tags=tags)
131 thread=post_thread, ip=ip)
132 post.send_to_websocket(request)
133
133
134 thread_to_show = (opening_post.id if opening_post else post.id)
134 thread_to_show = (opening_post.id if opening_post else post.id)
135
135
@@ -1,4 +1,4 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python3
2 import os
2 import os
3 import sys
3 import sys
4
4
@@ -82,6 +82,7 b' STATICFILES_DIRS = ('
82 STATICFILES_FINDERS = (
82 STATICFILES_FINDERS = (
83 'django.contrib.staticfiles.finders.FileSystemFinder',
83 'django.contrib.staticfiles.finders.FileSystemFinder',
84 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
84 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
85 'compressor.finders.CompressorFinder',
85 )
86 )
86
87
87 if DEBUG:
88 if DEBUG:
@@ -115,7 +116,6 b' MIDDLEWARE_CLASSES = ('
115 'django.contrib.auth.middleware.AuthenticationMiddleware',
116 'django.contrib.auth.middleware.AuthenticationMiddleware',
116 'django.contrib.messages.middleware.MessageMiddleware',
117 'django.contrib.messages.middleware.MessageMiddleware',
117 'boards.middlewares.BanMiddleware',
118 'boards.middlewares.BanMiddleware',
118 'boards.middlewares.MinifyHTMLMiddleware',
119 )
119 )
120
120
121 ROOT_URLCONF = 'neboard.urls'
121 ROOT_URLCONF = 'neboard.urls'
@@ -144,8 +144,6 b' INSTALLED_APPS = ('
144 'django.contrib.humanize',
144 'django.contrib.humanize',
145 'django_cleanup',
145 'django_cleanup',
146
146
147 # Migrations
148 'south',
149 'debug_toolbar',
147 'debug_toolbar',
150
148
151 # Search
149 # Search
@@ -164,10 +162,10 b' LOGGING = {'
164 'disable_existing_loggers': False,
162 'disable_existing_loggers': False,
165 'formatters': {
163 'formatters': {
166 'verbose': {
164 'verbose': {
167 'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s'
165 'format': '%(levelname)s %(asctime)s %(name)s %(process)d %(thread)d %(message)s'
168 },
166 },
169 'simple': {
167 'simple': {
170 'format': '%(levelname)s %(asctime)s [%(module)s] %(message)s'
168 'format': '%(levelname)s %(asctime)s [%(name)s] %(message)s'
171 },
169 },
172 },
170 },
173 'filters': {
171 'filters': {
@@ -197,10 +195,6 b' HAYSTACK_CONNECTIONS = {'
197 },
195 },
198 }
196 }
199
197
200 MARKUP_FIELD_TYPES = (
201 ('bbcode', bbcode_extended),
202 )
203
204 THEMES = [
198 THEMES = [
205 ('md', 'Mystic Dark'),
199 ('md', 'Mystic Dark'),
206 ('md_centered', 'Mystic Dark (centered)'),
200 ('md_centered', 'Mystic Dark (centered)'),
@@ -208,11 +202,16 b' THEMES = ['
208 ('pg', 'Photon Gray'),
202 ('pg', 'Photon Gray'),
209 ]
203 ]
210
204
211 POPULAR_TAGS = 10
212
213 POSTING_DELAY = 20 # seconds
205 POSTING_DELAY = 20 # seconds
214
206
215 COMPRESS_HTML = True
207 # Websocket settins
208 CENTRIFUGE_HOST = 'localhost'
209 CENTRIFUGE_PORT = '9090'
210
211 CENTRIFUGE_ADDRESS = 'http://{}:{}'.format(CENTRIFUGE_HOST, CENTRIFUGE_PORT)
212 CENTRIFUGE_PROJECT_ID = '<project id here>'
213 CENTRIFUGE_PROJECT_SECRET = '<project secret here>'
214 CENTRIFUGE_TIMEOUT = 5
216
215
217 # Debug mode middlewares
216 # Debug mode middlewares
218 if DEBUG:
217 if DEBUG:
@@ -221,7 +220,7 b' if DEBUG:'
221 )
220 )
222
221
223 def custom_show_toolbar(request):
222 def custom_show_toolbar(request):
224 return False
223 return True
225
224
226 DEBUG_TOOLBAR_CONFIG = {
225 DEBUG_TOOLBAR_CONFIG = {
227 'ENABLE_STACKTRACES': True,
226 'ENABLE_STACKTRACES': True,
@@ -232,4 +231,3 b' if DEBUG:'
232 #DEBUG_TOOLBAR_PANELS += (
231 #DEBUG_TOOLBAR_PANELS += (
233 # 'debug_toolbar.panels.profiling.ProfilingDebugPanel',
232 # 'debug_toolbar.panels.profiling.ProfilingDebugPanel',
234 #)
233 #)
235
@@ -7,23 +7,6 b' Main repository: https://bitbucket.org/n'
7
7
8 Site: http://neboard.me/
8 Site: http://neboard.me/
9
9
10 # DEPENDENCIES #
11
12 ## REQUIRED ##
13
14 * pillow
15 * django >= 1.6
16 * django_cleanup
17 * django-markupfield
18 * markdown
19 * python-markdown
20 * django-simple-captcha
21 * line-profiler
22
23 ## OPTIONAL ##
24
25 * django-debug-toolbar
26
27 # INSTALLATION #
10 # INSTALLATION #
28
11
29 1. Install all dependencies over pip or system-wide
12 1. Install all dependencies over pip or system-wide
@@ -1,10 +1,9 b''
1 httplib2
1 httplib2
2 simplejson
2 simplejson
3 south>=0.8.4
3 adjacent
4 haystack
4 haystack
5 pillow
5 pillow
6 django>=1.6
6 django>=1.7
7 django_cleanup
7 django_cleanup
8 django-markupfield
9 bbcode
8 bbcode
10 ecdsa
9 ecdsa
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now