##// 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
@@ -1,18 +1,22 b''
1 bc8fce57a613175450b8b6d933cdd85f22c04658 1.1
1 bc8fce57a613175450b8b6d933cdd85f22c04658 1.1
2 784258eb652c563c288ca7652c33f52cd4733d83 1.1-stable
2 784258eb652c563c288ca7652c33f52cd4733d83 1.1-stable
3 1b53a22467a8fccc798935d7a26efe78e4bc7b25 1.2-stable
3 1b53a22467a8fccc798935d7a26efe78e4bc7b25 1.2-stable
4 1713fb7543386089e364c39703b79e57d3d851f0 1.3
4 1713fb7543386089e364c39703b79e57d3d851f0 1.3
5 80f183ebbe132ea8433eacae9431360f31fe7083 1.4
5 80f183ebbe132ea8433eacae9431360f31fe7083 1.4
6 4330ff5a2bf6c543d8aaae8a43de1dc062f3bd13 1.4.1
6 4330ff5a2bf6c543d8aaae8a43de1dc062f3bd13 1.4.1
7 8531d7b001392289a6b761f38c73a257606552ad 1.5
7 8531d7b001392289a6b761f38c73a257606552ad 1.5
8 78e843c8b04b5a81cee5aa24601e305fae75da24 1.5.1
8 78e843c8b04b5a81cee5aa24601e305fae75da24 1.5.1
9 4f92838730ed9aa1d17651bbcdca19a097fd0c37 1.6
9 4f92838730ed9aa1d17651bbcdca19a097fd0c37 1.6
10 4bac2f37ea463337ddd27f98e7985407a74de504 1.7
10 4bac2f37ea463337ddd27f98e7985407a74de504 1.7
11 1c4febea92c6503ae557fba73b2768659ae90d24 1.7.1
11 1c4febea92c6503ae557fba73b2768659ae90d24 1.7.1
12 56a4a4578fc454ee455e33dd74a2cc82234bcb59 1.7.2
12 56a4a4578fc454ee455e33dd74a2cc82234bcb59 1.7.2
13 34d6f3d5deb22be56b6c1512ec654bd7f6e03bcc 1.7.3
13 34d6f3d5deb22be56b6c1512ec654bd7f6e03bcc 1.7.3
14 f5cca33d29c673b67d43f310bebc4e3a21c6d04c 1.7.4
14 f5cca33d29c673b67d43f310bebc4e3a21c6d04c 1.7.4
15 7f7c33ba6e3f3797ca866c5ed5d358a2393f1371 1.8
15 7f7c33ba6e3f3797ca866c5ed5d358a2393f1371 1.8
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
@@ -1,146 +1,147 b''
1 from django.shortcuts import get_object_or_404
1 from django.shortcuts import get_object_or_404
2 from boards.models import Tag
2 from boards.models import Tag
3
3
4 __author__ = 'neko259'
4 __author__ = 'neko259'
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'
11 SETTING_FAVORITE_TAGS = 'favorite_tags'
12 SETTING_FAVORITE_TAGS = 'favorite_tags'
12 SETTING_HIDDEN_TAGS = 'hidden_tags'
13 SETTING_HIDDEN_TAGS = 'hidden_tags'
13 SETTING_PERMISSIONS = 'permissions'
14 SETTING_PERMISSIONS = 'permissions'
14
15
15 DEFAULT_THEME = 'md'
16 DEFAULT_THEME = 'md'
16
17
17
18
18 def get_settings_manager(request):
19 def get_settings_manager(request):
19 """
20 """
20 Get settings manager based on the request object. Currently only
21 Get settings manager based on the request object. Currently only
21 session-based manager is supported. In the future, cookie-based or
22 session-based manager is supported. In the future, cookie-based or
22 database-based managers could be implemented.
23 database-based managers could be implemented.
23 """
24 """
24 return SessionSettingsManager(request.session)
25 return SessionSettingsManager(request.session)
25
26
26
27
27 class SettingsManager:
28 class SettingsManager:
28 """
29 """
29 Base settings manager class. get_setting and set_setting methods should
30 Base settings manager class. get_setting and set_setting methods should
30 be overriden.
31 be overriden.
31 """
32 """
32 def __init__(self):
33 def __init__(self):
33 pass
34 pass
34
35
35 def get_theme(self):
36 def get_theme(self):
36 theme = self.get_setting(SETTING_THEME)
37 theme = self.get_setting(SETTING_THEME)
37 if not theme:
38 if not theme:
38 theme = DEFAULT_THEME
39 theme = DEFAULT_THEME
39 self.set_setting(SETTING_THEME, theme)
40 self.set_setting(SETTING_THEME, theme)
40
41
41 return theme
42 return theme
42
43
43 def set_theme(self, theme):
44 def set_theme(self, theme):
44 self.set_setting(SETTING_THEME, theme)
45 self.set_setting(SETTING_THEME, theme)
45
46
46 def has_permission(self, permission):
47 def has_permission(self, permission):
47 permissions = self.get_setting(SETTING_PERMISSIONS)
48 permissions = self.get_setting(SETTING_PERMISSIONS)
48 if permissions:
49 if permissions:
49 return permission in permissions
50 return permission in permissions
50 else:
51 else:
51 return False
52 return False
52
53
53 def get_setting(self, setting):
54 def get_setting(self, setting):
54 pass
55 pass
55
56
56 def set_setting(self, setting, value):
57 def set_setting(self, setting, value):
57 pass
58 pass
58
59
59 def add_permission(self, permission):
60 def add_permission(self, permission):
60 permissions = self.get_setting(SETTING_PERMISSIONS)
61 permissions = self.get_setting(SETTING_PERMISSIONS)
61 if not permissions:
62 if not permissions:
62 permissions = [permission]
63 permissions = [permission]
63 else:
64 else:
64 permissions.append(permission)
65 permissions.append(permission)
65 self.set_setting(SETTING_PERMISSIONS, permissions)
66 self.set_setting(SETTING_PERMISSIONS, permissions)
66
67
67 def del_permission(self, permission):
68 def del_permission(self, permission):
68 permissions = self.get_setting(SETTING_PERMISSIONS)
69 permissions = self.get_setting(SETTING_PERMISSIONS)
69 if not permissions:
70 if not permissions:
70 permissions = []
71 permissions = []
71 else:
72 else:
72 permissions.remove(permission)
73 permissions.remove(permission)
73 self.set_setting(SETTING_PERMISSIONS, permissions)
74 self.set_setting(SETTING_PERMISSIONS, permissions)
74
75
75 def get_fav_tags(self):
76 def get_fav_tags(self):
76 tag_names = self.get_setting(SETTING_FAVORITE_TAGS)
77 tag_names = self.get_setting(SETTING_FAVORITE_TAGS)
77 tags = []
78 tags = []
78 if tag_names:
79 if tag_names:
79 for tag_name in tag_names:
80 for tag_name in tag_names:
80 tag = get_object_or_404(Tag, name=tag_name)
81 tag = get_object_or_404(Tag, name=tag_name)
81 tags.append(tag)
82 tags.append(tag)
82 return tags
83 return tags
83
84
84 def add_fav_tag(self, tag):
85 def add_fav_tag(self, tag):
85 tags = self.get_setting(SETTING_FAVORITE_TAGS)
86 tags = self.get_setting(SETTING_FAVORITE_TAGS)
86 if not tags:
87 if not tags:
87 tags = [tag.name]
88 tags = [tag.name]
88 else:
89 else:
89 if not tag.name in tags:
90 if not tag.name in tags:
90 tags.append(tag.name)
91 tags.append(tag.name)
91
92
92 tags.sort()
93 tags.sort()
93 self.set_setting(SETTING_FAVORITE_TAGS, tags)
94 self.set_setting(SETTING_FAVORITE_TAGS, tags)
94
95
95 def del_fav_tag(self, tag):
96 def del_fav_tag(self, tag):
96 tags = self.get_setting(SETTING_FAVORITE_TAGS)
97 tags = self.get_setting(SETTING_FAVORITE_TAGS)
97 if tag.name in tags:
98 if tag.name in tags:
98 tags.remove(tag.name)
99 tags.remove(tag.name)
99 self.set_setting(SETTING_FAVORITE_TAGS, tags)
100 self.set_setting(SETTING_FAVORITE_TAGS, tags)
100
101
101 def get_hidden_tags(self):
102 def get_hidden_tags(self):
102 tag_names = self.get_setting(SETTING_HIDDEN_TAGS)
103 tag_names = self.get_setting(SETTING_HIDDEN_TAGS)
103 tags = []
104 tags = []
104 if tag_names:
105 if tag_names:
105 for tag_name in tag_names:
106 for tag_name in tag_names:
106 tag = get_object_or_404(Tag, name=tag_name)
107 tag = get_object_or_404(Tag, name=tag_name)
107 tags.append(tag)
108 tags.append(tag)
108
109
109 return tags
110 return tags
110
111
111 def add_hidden_tag(self, tag):
112 def add_hidden_tag(self, tag):
112 tags = self.get_setting(SETTING_HIDDEN_TAGS)
113 tags = self.get_setting(SETTING_HIDDEN_TAGS)
113 if not tags:
114 if not tags:
114 tags = [tag.name]
115 tags = [tag.name]
115 else:
116 else:
116 if not tag.name in tags:
117 if not tag.name in tags:
117 tags.append(tag.name)
118 tags.append(tag.name)
118
119
119 tags.sort()
120 tags.sort()
120 self.set_setting(SETTING_HIDDEN_TAGS, tags)
121 self.set_setting(SETTING_HIDDEN_TAGS, tags)
121
122
122 def del_hidden_tag(self, tag):
123 def del_hidden_tag(self, tag):
123 tags = self.get_setting(SETTING_HIDDEN_TAGS)
124 tags = self.get_setting(SETTING_HIDDEN_TAGS)
124 if tag.name in tags:
125 if tag.name in tags:
125 tags.remove(tag.name)
126 tags.remove(tag.name)
126 self.set_setting(SETTING_HIDDEN_TAGS, tags)
127 self.set_setting(SETTING_HIDDEN_TAGS, tags)
127
128
128
129
129 class SessionSettingsManager(SettingsManager):
130 class SessionSettingsManager(SettingsManager):
130 """
131 """
131 Session-based settings manager. All settings are saved to the user's
132 Session-based settings manager. All settings are saved to the user's
132 session.
133 session.
133 """
134 """
134 def __init__(self, session):
135 def __init__(self, session):
135 SettingsManager.__init__(self)
136 SettingsManager.__init__(self)
136 self.session = session
137 self.session = session
137
138
138 def get_setting(self, setting):
139 def get_setting(self, setting):
139 if setting in self.session:
140 if setting in self.session:
140 return self.session[setting]
141 return self.session[setting]
141 else:
142 else:
142 return None
143 return None
143
144
144 def set_setting(self, setting, value):
145 def set_setting(self, setting, value):
145 self.session[setting] = value
146 self.session[setting] = value
146
147
@@ -1,43 +1,54 b''
1 from django.contrib import admin
1 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,42 +1,41 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
6 from boards import settings
5 from boards import settings
7 from boards.models import Post
6 from boards.models import Post
8
7
9 CONTEXT_SITE_NAME = 'site_name'
8 CONTEXT_SITE_NAME = 'site_name'
10 CONTEXT_VERSION = 'version'
9 CONTEXT_VERSION = 'version'
11 CONTEXT_MODERATOR = 'moderator'
10 CONTEXT_MODERATOR = 'moderator'
12 CONTEXT_THEME_CSS = 'theme_css'
11 CONTEXT_THEME_CSS = 'theme_css'
13 CONTEXT_THEME = 'theme'
12 CONTEXT_THEME = 'theme'
14 CONTEXT_PPD = 'posts_per_day'
13 CONTEXT_PPD = 'posts_per_day'
15 CONTEXT_TAGS = 'tags'
14 CONTEXT_TAGS = 'tags'
16 CONTEXT_USER = 'user'
15 CONTEXT_USER = 'user'
17
16
18 PERMISSION_MODERATE = 'moderation'
17 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
26 settings_manager = get_settings_manager(request)
25 settings_manager = get_settings_manager(request)
27 context[CONTEXT_TAGS] = settings_manager.get_fav_tags()
26 context[CONTEXT_TAGS] = settings_manager.get_fav_tags()
28 theme = settings_manager.get_theme()
27 theme = settings_manager.get_theme()
29 context[CONTEXT_THEME] = theme
28 context[CONTEXT_THEME] = theme
30 context[CONTEXT_THEME_CSS] = 'css/' + theme + '/base_page.css'
29 context[CONTEXT_THEME_CSS] = 'css/' + theme + '/base_page.css'
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
38
37
39 context[CONTEXT_VERSION] = settings.VERSION
38 context[CONTEXT_VERSION] = settings.VERSION
40 context[CONTEXT_SITE_NAME] = settings.SITE_NAME
39 context[CONTEXT_SITE_NAME] = settings.SITE_NAME
41
40
42 return context
41 return context
@@ -1,294 +1,305 b''
1 import re
1 import re
2 import time
2 import time
3 import hashlib
3 import hashlib
4
4
5 from django import forms
5 from django import forms
6 from django.forms.util import ErrorList
6 from django.forms.util import ErrorList
7 from django.utils.translation import ugettext_lazy as _
7 from django.utils.translation import ugettext_lazy as _
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
15
15
16 VETERAN_POSTING_DELAY = 5
16 VETERAN_POSTING_DELAY = 5
17
17
18 ATTRIBUTE_PLACEHOLDER = 'placeholder'
18 ATTRIBUTE_PLACEHOLDER = 'placeholder'
19
19
20 LAST_POST_TIME = 'last_post_time'
20 LAST_POST_TIME = 'last_post_time'
21 LAST_LOGIN_TIME = 'last_login_time'
21 LAST_LOGIN_TIME = 'last_login_time'
22 TEXT_PLACEHOLDER = _('''Type message here. Use formatting panel for more advanced usage.''')
22 TEXT_PLACEHOLDER = _('''Type message here. Use formatting panel for more advanced usage.''')
23 TAGS_PLACEHOLDER = _('tag1 several_words_tag')
23 TAGS_PLACEHOLDER = _('tag1 several_words_tag')
24
24
25 ERROR_IMAGE_DUPLICATE = _('Such image was already posted')
25 ERROR_IMAGE_DUPLICATE = _('Such image was already posted')
26
26
27 LABEL_TITLE = _('Title')
27 LABEL_TITLE = _('Title')
28 LABEL_TEXT = _('Text')
28 LABEL_TEXT = _('Text')
29 LABEL_TAG = _('Tag')
29 LABEL_TAG = _('Tag')
30 LABEL_SEARCH = _('Search')
30 LABEL_SEARCH = _('Search')
31
31
32 TAG_MAX_LENGTH = 20
32 TAG_MAX_LENGTH = 20
33
33
34 REGEX_TAG = r'^[\w\d]+$'
34 REGEX_TAG = r'^[\w\d]+$'
35
35
36
36
37 class FormatPanel(forms.Textarea):
37 class FormatPanel(forms.Textarea):
38 def render(self, name, value, attrs=None):
38 def render(self, name, value, attrs=None):
39 output = '<div id="mark-panel">'
39 output = '<div id="mark-panel">'
40 for formatter in formatters:
40 for formatter in formatters:
41 output += '<span class="mark_btn"' + \
41 output += '<span class="mark_btn"' + \
42 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
42 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
43 '\', \'' + formatter.format_right + '\')">' + \
43 '\', \'' + formatter.format_right + '\')">' + \
44 formatter.preview_left + formatter.name + \
44 formatter.preview_left + formatter.name + \
45 formatter.preview_right + '</span>'
45 formatter.preview_right + '</span>'
46
46
47 output += '</div>'
47 output += '</div>'
48 output += super(FormatPanel, self).render(name, value, attrs=None)
48 output += super(FormatPanel, self).render(name, value, attrs=None)
49
49
50 return output
50 return output
51
51
52
52
53 class PlainErrorList(ErrorList):
53 class PlainErrorList(ErrorList):
54 def __unicode__(self):
54 def __unicode__(self):
55 return self.as_text()
55 return self.as_text()
56
56
57 def as_text(self):
57 def as_text(self):
58 return ''.join(['(!) %s ' % e for e in self])
58 return ''.join(['(!) %s ' % e for e in self])
59
59
60
60
61 class NeboardForm(forms.Form):
61 class NeboardForm(forms.Form):
62
62
63 def as_div(self):
63 def as_div(self):
64 """
64 """
65 Returns this form rendered as HTML <as_div>s.
65 Returns this form rendered as HTML <as_div>s.
66 """
66 """
67
67
68 return self._html_output(
68 return self._html_output(
69 # TODO Do not show hidden rows in the list here
69 # TODO Do not show hidden rows in the list here
70 normal_row='<div class="form-row"><div class="form-label">'
70 normal_row='<div class="form-row"><div class="form-label">'
71 '%(label)s'
71 '%(label)s'
72 '</div></div>'
72 '</div></div>'
73 '<div class="form-row"><div class="form-input">'
73 '<div class="form-row"><div class="form-input">'
74 '%(field)s'
74 '%(field)s'
75 '</div></div>'
75 '</div></div>'
76 '<div class="form-row">'
76 '<div class="form-row">'
77 '%(help_text)s'
77 '%(help_text)s'
78 '</div>',
78 '</div>',
79 error_row='<div class="form-row">'
79 error_row='<div class="form-row">'
80 '<div class="form-label"></div>'
80 '<div class="form-label"></div>'
81 '<div class="form-errors">%s</div>'
81 '<div class="form-errors">%s</div>'
82 '</div>',
82 '</div>',
83 row_ender='</div>',
83 row_ender='</div>',
84 help_text_html='%s',
84 help_text_html='%s',
85 errors_on_separate_row=True)
85 errors_on_separate_row=True)
86
86
87 def as_json_errors(self):
87 def as_json_errors(self):
88 errors = []
88 errors = []
89
89
90 for name, field in list(self.fields.items()):
90 for name, field in list(self.fields.items()):
91 if self[name].errors:
91 if self[name].errors:
92 errors.append({
92 errors.append({
93 'field': name,
93 'field': name,
94 'errors': self[name].errors.as_text(),
94 'errors': self[name].errors.as_text(),
95 })
95 })
96
96
97 return errors
97 return errors
98
98
99
99
100 class PostForm(NeboardForm):
100 class PostForm(NeboardForm):
101
101
102 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
102 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
103 label=LABEL_TITLE)
103 label=LABEL_TITLE)
104 text = forms.CharField(
104 text = forms.CharField(
105 widget=FormatPanel(attrs={ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER}),
105 widget=FormatPanel(attrs={ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER}),
106 required=False, label=LABEL_TEXT)
106 required=False, label=LABEL_TEXT)
107 image = forms.ImageField(required=False, label=_('Image'),
107 image = forms.ImageField(required=False, label=_('Image'),
108 widget=forms.ClearableFileInput(
108 widget=forms.ClearableFileInput(
109 attrs={'accept': 'image/*'}))
109 attrs={'accept': 'image/*'}))
110
110
111 # This field is for spam prevention only
111 # This field is for spam prevention only
112 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
112 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
113 widget=forms.TextInput(attrs={
113 widget=forms.TextInput(attrs={
114 'class': 'form-email'}))
114 'class': 'form-email'}))
115
115
116 session = None
116 session = None
117 need_to_ban = False
117 need_to_ban = False
118
118
119 def clean_title(self):
119 def clean_title(self):
120 title = self.cleaned_data['title']
120 title = self.cleaned_data['title']
121 if title:
121 if title:
122 if len(title) > TITLE_MAX_LENGTH:
122 if len(title) > TITLE_MAX_LENGTH:
123 raise forms.ValidationError(_('Title must have less than %s '
123 raise forms.ValidationError(_('Title must have less than %s '
124 'characters') %
124 'characters') %
125 str(TITLE_MAX_LENGTH))
125 str(TITLE_MAX_LENGTH))
126 return title
126 return title
127
127
128 def clean_text(self):
128 def clean_text(self):
129 text = self.cleaned_data['text'].strip()
129 text = self.cleaned_data['text'].strip()
130 if text:
130 if text:
131 if len(text) > board_settings.MAX_TEXT_LENGTH:
131 if len(text) > board_settings.MAX_TEXT_LENGTH:
132 raise forms.ValidationError(_('Text must have less than %s '
132 raise forms.ValidationError(_('Text must have less than %s '
133 'characters') %
133 'characters') %
134 str(board_settings
134 str(board_settings
135 .MAX_TEXT_LENGTH))
135 .MAX_TEXT_LENGTH))
136 return text
136 return text
137
137
138 def clean_image(self):
138 def clean_image(self):
139 image = self.cleaned_data['image']
139 image = self.cleaned_data['image']
140 if image:
140 if image:
141 if image.size > board_settings.MAX_IMAGE_SIZE:
141 if image.size > board_settings.MAX_IMAGE_SIZE:
142 raise forms.ValidationError(
142 raise forms.ValidationError(
143 _('Image must be less than %s bytes')
143 _('Image must be less than %s bytes')
144 % str(board_settings.MAX_IMAGE_SIZE))
144 % str(board_settings.MAX_IMAGE_SIZE))
145
145
146 md5 = hashlib.md5()
146 md5 = hashlib.md5()
147 for chunk in image.chunks():
147 for chunk in image.chunks():
148 md5.update(chunk)
148 md5.update(chunk)
149 image_hash = md5.hexdigest()
149 image_hash = md5.hexdigest()
150 if PostImage.objects.filter(hash=image_hash).exists():
150 if PostImage.objects.filter(hash=image_hash).exists():
151 raise forms.ValidationError(ERROR_IMAGE_DUPLICATE)
151 raise forms.ValidationError(ERROR_IMAGE_DUPLICATE)
152
152
153 return image
153 return image
154
154
155 def clean(self):
155 def clean(self):
156 cleaned_data = super(PostForm, self).clean()
156 cleaned_data = super(PostForm, self).clean()
157
157
158 if not self.session:
158 if not self.session:
159 raise forms.ValidationError('Humans have sessions')
159 raise forms.ValidationError('Humans have sessions')
160
160
161 if cleaned_data['email']:
161 if cleaned_data['email']:
162 self.need_to_ban = True
162 self.need_to_ban = True
163 raise forms.ValidationError('A human cannot enter a hidden field')
163 raise forms.ValidationError('A human cannot enter a hidden field')
164
164
165 if not self.errors:
165 if not self.errors:
166 self._clean_text_image()
166 self._clean_text_image()
167
167
168 if not self.errors and self.session:
168 if not self.errors and self.session:
169 self._validate_posting_speed()
169 self._validate_posting_speed()
170
170
171 return cleaned_data
171 return cleaned_data
172
172
173 def _clean_text_image(self):
173 def _clean_text_image(self):
174 text = self.cleaned_data.get('text')
174 text = self.cleaned_data.get('text')
175 image = self.cleaned_data.get('image')
175 image = self.cleaned_data.get('image')
176
176
177 if (not text) and (not image):
177 if (not text) and (not image):
178 error_message = _('Either text or image must be entered.')
178 error_message = _('Either text or image must be entered.')
179 self._errors['text'] = self.error_class([error_message])
179 self._errors['text'] = self.error_class([error_message])
180
180
181 def _validate_posting_speed(self):
181 def _validate_posting_speed(self):
182 can_post = True
182 can_post = True
183
183
184 posting_delay = settings.POSTING_DELAY
184 posting_delay = settings.POSTING_DELAY
185
185
186 if board_settings.LIMIT_POSTING_SPEED and LAST_POST_TIME in \
186 if board_settings.LIMIT_POSTING_SPEED and LAST_POST_TIME in \
187 self.session:
187 self.session:
188 now = time.time()
188 now = time.time()
189 last_post_time = self.session[LAST_POST_TIME]
189 last_post_time = self.session[LAST_POST_TIME]
190
190
191 current_delay = int(now - last_post_time)
191 current_delay = int(now - last_post_time)
192
192
193 if current_delay < posting_delay:
193 if current_delay < posting_delay:
194 error_message = _('Wait %s seconds after last posting') % str(
194 error_message = _('Wait %s seconds after last posting') % str(
195 posting_delay - current_delay)
195 posting_delay - current_delay)
196 self._errors['text'] = self.error_class([error_message])
196 self._errors['text'] = self.error_class([error_message])
197
197
198 can_post = False
198 can_post = False
199
199
200 if can_post:
200 if can_post:
201 self.session[LAST_POST_TIME] = time.time()
201 self.session[LAST_POST_TIME] = time.time()
202
202
203
203
204 class ThreadForm(PostForm):
204 class ThreadForm(PostForm):
205
205
206 regex_tags = re.compile(r'^[\w\s\d]+$', re.UNICODE)
206 regex_tags = re.compile(r'^[\w\s\d]+$', re.UNICODE)
207
207
208 tags = forms.CharField(
208 tags = forms.CharField(
209 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
209 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
210 max_length=100, label=_('Tags'), required=True)
210 max_length=100, label=_('Tags'), required=True)
211
211
212 def clean_tags(self):
212 def clean_tags(self):
213 tags = self.cleaned_data['tags'].strip()
213 tags = self.cleaned_data['tags'].strip()
214
214
215 if not tags or not self.regex_tags.match(tags):
215 if not tags or not self.regex_tags.match(tags):
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):
222 cleaned_data = super(ThreadForm, self).clean()
233 cleaned_data = super(ThreadForm, self).clean()
223
234
224 return cleaned_data
235 return cleaned_data
225
236
226
237
227 class SettingsForm(NeboardForm):
238 class SettingsForm(NeboardForm):
228
239
229 theme = forms.ChoiceField(choices=settings.THEMES,
240 theme = forms.ChoiceField(choices=settings.THEMES,
230 label=_('Theme'))
241 label=_('Theme'))
231
242
232
243
233 class AddTagForm(NeboardForm):
244 class AddTagForm(NeboardForm):
234
245
235 tag = forms.CharField(max_length=TAG_MAX_LENGTH, label=LABEL_TAG)
246 tag = forms.CharField(max_length=TAG_MAX_LENGTH, label=LABEL_TAG)
236 method = forms.CharField(widget=forms.HiddenInput(), initial='add_tag')
247 method = forms.CharField(widget=forms.HiddenInput(), initial='add_tag')
237
248
238 def clean_tag(self):
249 def clean_tag(self):
239 tag = self.cleaned_data['tag']
250 tag = self.cleaned_data['tag']
240
251
241 regex_tag = re.compile(REGEX_TAG, re.UNICODE)
252 regex_tag = re.compile(REGEX_TAG, re.UNICODE)
242 if not regex_tag.match(tag):
253 if not regex_tag.match(tag):
243 raise forms.ValidationError(_('Inappropriate characters in tags.'))
254 raise forms.ValidationError(_('Inappropriate characters in tags.'))
244
255
245 return tag
256 return tag
246
257
247 def clean(self):
258 def clean(self):
248 cleaned_data = super(AddTagForm, self).clean()
259 cleaned_data = super(AddTagForm, self).clean()
249
260
250 return cleaned_data
261 return cleaned_data
251
262
252
263
253 class SearchForm(NeboardForm):
264 class SearchForm(NeboardForm):
254 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
265 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
255
266
256
267
257 class LoginForm(NeboardForm):
268 class LoginForm(NeboardForm):
258
269
259 password = forms.CharField()
270 password = forms.CharField()
260
271
261 session = None
272 session = None
262
273
263 def clean_password(self):
274 def clean_password(self):
264 password = self.cleaned_data['password']
275 password = self.cleaned_data['password']
265 if board_settings.MASTER_PASSWORD != password:
276 if board_settings.MASTER_PASSWORD != password:
266 raise forms.ValidationError(_('Invalid master password'))
277 raise forms.ValidationError(_('Invalid master password'))
267
278
268 return password
279 return password
269
280
270 def _validate_login_speed(self):
281 def _validate_login_speed(self):
271 can_post = True
282 can_post = True
272
283
273 if LAST_LOGIN_TIME in self.session:
284 if LAST_LOGIN_TIME in self.session:
274 now = time.time()
285 now = time.time()
275 last_login_time = self.session[LAST_LOGIN_TIME]
286 last_login_time = self.session[LAST_LOGIN_TIME]
276
287
277 current_delay = int(now - last_login_time)
288 current_delay = int(now - last_login_time)
278
289
279 if current_delay < board_settings.LOGIN_TIMEOUT:
290 if current_delay < board_settings.LOGIN_TIMEOUT:
280 error_message = _('Wait %s minutes after last login') % str(
291 error_message = _('Wait %s minutes after last login') % str(
281 (board_settings.LOGIN_TIMEOUT - current_delay) / 60)
292 (board_settings.LOGIN_TIMEOUT - current_delay) / 60)
282 self._errors['password'] = self.error_class([error_message])
293 self._errors['password'] = self.error_class([error_message])
283
294
284 can_post = False
295 can_post = False
285
296
286 if can_post:
297 if can_post:
287 self.session[LAST_LOGIN_TIME] = time.time()
298 self.session[LAST_LOGIN_TIME] = time.time()
288
299
289 def clean(self):
300 def clean(self):
290 self._validate_login_speed()
301 self._validate_login_speed()
291
302
292 cleaned_data = super(LoginForm, self).clean()
303 cleaned_data = super(LoginForm, self).clean()
293
304
294 return cleaned_data
305 return cleaned_data
1 NO CONTENT: modified file, binary diff hidden
NO CONTENT: modified file, binary diff hidden
@@ -1,365 +1,370 b''
1 # SOME DESCRIPTIVE TITLE.
1 # SOME DESCRIPTIVE TITLE.
2 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
2 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3 # This file is distributed under the same license as the PACKAGE package.
3 # This file is distributed under the same license as the PACKAGE package.
4 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
4 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
5 #
5 #
6 msgid ""
6 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"
14 "Language: ru\n"
14 "Language: ru\n"
15 "MIME-Version: 1.0\n"
15 "MIME-Version: 1.0\n"
16 "Content-Type: text/plain; charset=UTF-8\n"
16 "Content-Type: text/plain; charset=UTF-8\n"
17 "Content-Transfer-Encoding: 8bit\n"
17 "Content-Transfer-Encoding: 8bit\n"
18 "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
18 "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
19 "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
19 "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
20
20
21 #: authors.py:9
21 #: authors.py:9
22 msgid "author"
22 msgid "author"
23 msgstr "автор"
23 msgstr "автор"
24
24
25 #: authors.py:10
25 #: authors.py:10
26 msgid "developer"
26 msgid "developer"
27 msgstr "разработчик"
27 msgstr "разработчик"
28
28
29 #: authors.py:11
29 #: authors.py:11
30 msgid "javascript developer"
30 msgid "javascript developer"
31 msgstr "разработчик javascript"
31 msgstr "разработчик javascript"
32
32
33 #: authors.py:12
33 #: authors.py:12
34 msgid "designer"
34 msgid "designer"
35 msgstr "дизайнер"
35 msgstr "дизайнер"
36
36
37 #: forms.py:22
37 #: forms.py:22
38 msgid "Type message here. Use formatting panel for more advanced usage."
38 msgid "Type message here. Use formatting panel for more advanced usage."
39 msgstr ""
39 msgstr ""
40 "Вводите сообщение сюда. Используйте панель для более сложного форматирования."
40 "Вводите сообщение сюда. Используйте панель для более сложного форматирования."
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"
48 msgstr "Такое изображение уже было загружено"
48 msgstr "Такое изображение уже было загружено"
49
49
50 #: forms.py:27
50 #: forms.py:27
51 msgid "Title"
51 msgid "Title"
52 msgstr "Заголовок"
52 msgstr "Заголовок"
53
53
54 #: forms.py:28
54 #: forms.py:28
55 msgid "Text"
55 msgid "Text"
56 msgstr "Текст"
56 msgstr "Текст"
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 "Поиск"
66
66
67 #: forms.py:107
67 #: forms.py:107
68 msgid "Image"
68 msgid "Image"
69 msgstr "Изображение"
69 msgstr "Изображение"
70
70
71 #: forms.py:112
71 #: forms.py:112
72 msgid "e-mail"
72 msgid "e-mail"
73 msgstr ""
73 msgstr ""
74
74
75 #: forms.py:123
75 #: forms.py:123
76 #, python-format
76 #, python-format
77 msgid "Title must have less than %s characters"
77 msgid "Title must have less than %s characters"
78 msgstr "Заголовок должен иметь меньше %s символов"
78 msgstr "Заголовок должен иметь меньше %s символов"
79
79
80 #: forms.py:132
80 #: forms.py:132
81 #, python-format
81 #, python-format
82 msgid "Text must have less than %s characters"
82 msgid "Text must have less than %s characters"
83 msgstr "Текст должен быть короче %s символов"
83 msgstr "Текст должен быть короче %s символов"
84
84
85 #: forms.py:143
85 #: forms.py:143
86 #, python-format
86 #, python-format
87 msgid "Image must be less than %s bytes"
87 msgid "Image must be less than %s bytes"
88 msgstr "Изображение должно быть менее %s байт"
88 msgstr "Изображение должно быть менее %s байт"
89
89
90 #: forms.py:178
90 #: forms.py:178
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 минут после последнего входа"
119
123
120 #: templates/boards/404.html:6
124 #: templates/boards/404.html:6
121 msgid "Not found"
125 msgid "Not found"
122 msgstr "Не найдено"
126 msgstr "Не найдено"
123
127
124 #: templates/boards/404.html:12
128 #: templates/boards/404.html:12
125 msgid "This page does not exist"
129 msgid "This page does not exist"
126 msgstr "Этой страницы не существует"
130 msgstr "Этой страницы не существует"
127
131
128 #: templates/boards/authors.html:6 templates/boards/authors.html.py:12
132 #: templates/boards/authors.html:6 templates/boards/authors.html.py:12
129 msgid "Authors"
133 msgid "Authors"
130 msgstr "Авторы"
134 msgstr "Авторы"
131
135
132 #: templates/boards/authors.html:26
136 #: templates/boards/authors.html:26
133 msgid "Distributed under the"
137 msgid "Distributed under the"
134 msgstr "Распространяется под"
138 msgstr "Распространяется под"
135
139
136 #: templates/boards/authors.html:28
140 #: templates/boards/authors.html:28
137 msgid "license"
141 msgid "license"
138 msgstr "лицензией"
142 msgstr "лицензией"
139
143
140 #: templates/boards/authors.html:30
144 #: templates/boards/authors.html:30
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
173 #: templates/boards/login.html:6 templates/boards/login.html.py:16
177 #: templates/boards/login.html:6 templates/boards/login.html.py:16
174 msgid "Login"
178 msgid "Login"
175 msgstr "Вход"
179 msgstr "Вход"
176
180
177 #: templates/boards/login.html:19
181 #: templates/boards/login.html:19
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
274 #: templates/boards/preview.html:6 templates/boards/staticpages/help.html:19
278 #: templates/boards/preview.html:6 templates/boards/staticpages/help.html:19
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 "Заблокирован"
329
305
330 #: templates/boards/staticpages/banned.html:11
306 #: templates/boards/staticpages/banned.html:11
331 msgid "Your IP address has been banned. Contact the administrator"
307 msgid "Your IP address has been banned. Contact the administrator"
332 msgstr "Ваш IP адрес был заблокирован. Свяжитесь с администратором"
308 msgstr "Ваш IP адрес был заблокирован. Свяжитесь с администратором"
333
309
334 #: templates/boards/staticpages/help.html:6
310 #: templates/boards/staticpages/help.html:6
335 #: templates/boards/staticpages/help.html:10
311 #: templates/boards/staticpages/help.html:10
336 msgid "Syntax"
312 msgid "Syntax"
337 msgstr "Синтаксис"
313 msgstr "Синтаксис"
338
314
339 #: templates/boards/staticpages/help.html:11
315 #: templates/boards/staticpages/help.html:11
340 msgid "Italic text"
316 msgid "Italic text"
341 msgstr "Курсивный текст"
317 msgstr "Курсивный текст"
342
318
343 #: templates/boards/staticpages/help.html:12
319 #: templates/boards/staticpages/help.html:12
344 msgid "Bold text"
320 msgid "Bold text"
345 msgstr "Полужирный текст"
321 msgstr "Полужирный текст"
346
322
347 #: templates/boards/staticpages/help.html:13
323 #: templates/boards/staticpages/help.html:13
348 msgid "Spoiler"
324 msgid "Spoiler"
349 msgstr "Спойлер"
325 msgstr "Спойлер"
350
326
351 #: templates/boards/staticpages/help.html:14
327 #: templates/boards/staticpages/help.html:14
352 msgid "Link to a post"
328 msgid "Link to a post"
353 msgstr "Ссылка на сообщение"
329 msgstr "Ссылка на сообщение"
354
330
355 #: templates/boards/staticpages/help.html:15
331 #: templates/boards/staticpages/help.html:15
356 msgid "Strikethrough text"
332 msgid "Strikethrough text"
357 msgstr "Зачеркнутый текст"
333 msgstr "Зачеркнутый текст"
358
334
359 #: templates/boards/staticpages/help.html:16
335 #: templates/boards/staticpages/help.html:16
360 msgid "Comment"
336 msgid "Comment"
361 msgstr "Комментарий"
337 msgstr "Комментарий"
362
338
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
@@ -1,201 +1,202 b''
1 # coding=utf-8
1 # coding=utf-8
2
2
3 import re
3 import re
4 import bbcode
4 import bbcode
5
5
6 import boards
6 import boards
7
7
8
8
9 __author__ = 'neko259'
9 __author__ = 'neko259'
10
10
11
11
12 REFLINK_PATTERN = re.compile(r'^\d+$')
12 REFLINK_PATTERN = re.compile(r'^\d+$')
13 GLOBAL_REFLINK_PATTERN = re.compile(r'^(\w+)::([^:]+)::(\d+)$')
13 GLOBAL_REFLINK_PATTERN = re.compile(r'^(\w+)::([^:]+)::(\d+)$')
14 MULTI_NEWLINES_PATTERN = re.compile(r'(\r?\n){2,}')
14 MULTI_NEWLINES_PATTERN = re.compile(r'(\r?\n){2,}')
15 ONE_NEWLINE = '\n'
15 ONE_NEWLINE = '\n'
16
16
17
17
18 class TextFormatter():
18 class TextFormatter():
19 """
19 """
20 An interface for formatter that can be used in the text format panel
20 An interface for formatter that can be used in the text format panel
21 """
21 """
22
22
23 def __init__(self):
23 def __init__(self):
24 pass
24 pass
25
25
26 name = ''
26 name = ''
27
27
28 # Left and right tags for the button preview
28 # Left and right tags for the button preview
29 preview_left = ''
29 preview_left = ''
30 preview_right = ''
30 preview_right = ''
31
31
32 # Left and right characters for the textarea input
32 # Left and right characters for the textarea input
33 format_left = ''
33 format_left = ''
34 format_right = ''
34 format_right = ''
35
35
36
36
37 class AutolinkPattern():
37 class AutolinkPattern():
38 def handleMatch(self, m):
38 def handleMatch(self, m):
39 link_element = etree.Element('a')
39 link_element = etree.Element('a')
40 href = m.group(2)
40 href = m.group(2)
41 link_element.set('href', href)
41 link_element.set('href', href)
42 link_element.text = href
42 link_element.text = href
43
43
44 return link_element
44 return link_element
45
45
46
46
47 class QuotePattern(TextFormatter):
47 class QuotePattern(TextFormatter):
48 name = 'q'
48 name = 'q'
49 preview_left = '<span class="multiquote">'
49 preview_left = '<span class="multiquote">'
50 preview_right = '</span>'
50 preview_right = '</span>'
51
51
52 format_left = '[quote]'
52 format_left = '[quote]'
53 format_right = '[/quote]'
53 format_right = '[/quote]'
54
54
55
55
56 class SpoilerPattern(TextFormatter):
56 class SpoilerPattern(TextFormatter):
57 name = 'spoiler'
57 name = 'spoiler'
58 preview_left = '<span class="spoiler">'
58 preview_left = '<span class="spoiler">'
59 preview_right = '</span>'
59 preview_right = '</span>'
60
60
61 format_left = '[spoiler]'
61 format_left = '[spoiler]'
62 format_right = '[/spoiler]'
62 format_right = '[/spoiler]'
63
63
64 def handleMatch(self, m):
64 def handleMatch(self, m):
65 quote_element = etree.Element('span')
65 quote_element = etree.Element('span')
66 quote_element.set('class', 'spoiler')
66 quote_element.set('class', 'spoiler')
67 quote_element.text = m.group(2)
67 quote_element.text = m.group(2)
68
68
69 return quote_element
69 return quote_element
70
70
71
71
72 class CommentPattern(TextFormatter):
72 class CommentPattern(TextFormatter):
73 name = ''
73 name = ''
74 preview_left = '<span class="comment">// '
74 preview_left = '<span class="comment">// '
75 preview_right = '</span>'
75 preview_right = '</span>'
76
76
77 format_left = '[comment]'
77 format_left = '[comment]'
78 format_right = '[/comment]'
78 format_right = '[/comment]'
79
79
80
80
81 # TODO Use <s> tag here
81 # TODO Use <s> tag here
82 class StrikeThroughPattern(TextFormatter):
82 class StrikeThroughPattern(TextFormatter):
83 name = 's'
83 name = 's'
84 preview_left = '<span class="strikethrough">'
84 preview_left = '<span class="strikethrough">'
85 preview_right = '</span>'
85 preview_right = '</span>'
86
86
87 format_left = '[s]'
87 format_left = '[s]'
88 format_right = '[/s]'
88 format_right = '[/s]'
89
89
90
90
91 class ItalicPattern(TextFormatter):
91 class ItalicPattern(TextFormatter):
92 name = 'i'
92 name = 'i'
93 preview_left = '<i>'
93 preview_left = '<i>'
94 preview_right = '</i>'
94 preview_right = '</i>'
95
95
96 format_left = '[i]'
96 format_left = '[i]'
97 format_right = '[/i]'
97 format_right = '[/i]'
98
98
99
99
100 class BoldPattern(TextFormatter):
100 class BoldPattern(TextFormatter):
101 name = 'b'
101 name = 'b'
102 preview_left = '<b>'
102 preview_left = '<b>'
103 preview_right = '</b>'
103 preview_right = '</b>'
104
104
105 format_left = '[b]'
105 format_left = '[b]'
106 format_right = '[/b]'
106 format_right = '[/b]'
107
107
108
108
109 class CodePattern(TextFormatter):
109 class CodePattern(TextFormatter):
110 name = 'code'
110 name = 'code'
111 preview_left = '<code>'
111 preview_left = '<code>'
112 preview_right = '</code>'
112 preview_right = '</code>'
113
113
114 format_left = '[code]'
114 format_left = '[code]'
115 format_right = '[/code]'
115 format_right = '[/code]'
116
116
117
117
118 def render_reflink(tag_name, value, options, parent, context):
118 def render_reflink(tag_name, value, options, parent, context):
119 post_id = None
119 post_id = None
120
120
121 matches = REFLINK_PATTERN.findall(value)
121 matches = REFLINK_PATTERN.findall(value)
122 if matches:
122 if matches:
123 post_id = int(matches[0][0])
123 post_id = int(matches[0][0])
124 else:
124 else:
125 match = GLOBAL_REFLINK_PATTERN.match(value)
125 match = GLOBAL_REFLINK_PATTERN.match(value)
126 if match:
126 if match:
127 key_type = match.group(1)
127 key_type = match.group(1)
128 key = match.group(2)
128 key = match.group(2)
129 local_id = match.group(3)
129 local_id = match.group(3)
130
130
131 try:
131 try:
132 global_id = boards.models.GlobalId.objects.get(key_type=key_type,
132 global_id = boards.models.GlobalId.objects.get(key_type=key_type,
133 key=key, local_id=local_id)
133 key=key, local_id=local_id)
134 for post in boards.models.Post.objects.filter(global_id=global_id).only('id'):
134 for post in boards.models.Post.objects.filter(global_id=global_id).only('id'):
135 post_id = post.id
135 post_id = post.id
136 except boards.models.GlobalId.DoesNotExist:
136 except boards.models.GlobalId.DoesNotExist:
137 pass
137 pass
138
138
139 if not post_id:
139 if not post_id:
140 return value
140 return value
141
141
142 posts = boards.models.Post.objects.filter(id=post_id)
142 posts = boards.models.Post.objects.filter(id=post_id)
143 if posts.exists():
143 if posts.exists():
144 post = posts[0]
144 post = posts[0]
145
145
146 return '<a href="%s">&gt;&gt;%s</a>' % (post.get_url(), post_id)
146 return '<a href="%s">&gt;&gt;%s</a>' % (post.get_url(), post_id)
147 else:
147 else:
148 return '>>%s' % value
148 return '>>%s' % value
149
149
150
150
151 def render_quote(tag_name, value, options, parent, context):
151 def render_quote(tag_name, value, options, parent, context):
152 source = ''
152 source = ''
153 if 'source' in options:
153 if 'source' in options:
154 source = options['source']
154 source = options['source']
155
155
156 result = ''
156 result = ''
157 if source:
157 if source:
158 result = '<div class="multiquote"><div class="quote-header">%s</div><div class="quote-text">%s</div></div>' % (source, value)
158 result = '<div class="multiquote"><div class="quote-header">%s</div><div class="quote-text">%s</div></div>' % (source, value)
159 else:
159 else:
160 result = '<div class="multiquote"><div class="quote-text">%s</div></div>' % value
160 result = '<div class="multiquote"><div class="quote-text">%s</div></div>' % value
161
161
162 return result
162 return result
163
163
164
164
165 def preparse_text(text):
165 def preparse_text(text):
166 """
166 """
167 Performs manual parsing before the bbcode parser is used.
167 Performs manual parsing before the bbcode parser is used.
168 """
168 """
169
169
170 return MULTI_NEWLINES_PATTERN.sub(ONE_NEWLINE, text)
170 return MULTI_NEWLINES_PATTERN.sub(ONE_NEWLINE, text)
171
171
172
172
173 def bbcode_extended(markup):
173 def bbcode_extended(markup):
174 # The newline hack is added because br's margin does not work in all
174 # The newline hack is added because br's margin does not work in all
175 # browsers except firefox, when the div's does.
175 # browsers except firefox, when the div's does.
176 parser = bbcode.Parser(newline='<div class="br"></div>')
176 parser = bbcode.Parser(newline='<div class="br"></div>')
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)
192
193
193 formatters = [
194 formatters = [
194 QuotePattern,
195 QuotePattern,
195 SpoilerPattern,
196 SpoilerPattern,
196 ItalicPattern,
197 ItalicPattern,
197 BoldPattern,
198 BoldPattern,
198 CommentPattern,
199 CommentPattern,
199 StrikeThroughPattern,
200 StrikeThroughPattern,
200 CodePattern,
201 CodePattern,
201 ]
202 ]
@@ -1,46 +1,28 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
10 TYPE_HTML = 'text/html'
7 TYPE_HTML = 'text/html'
11
8
12
9
13 class BanMiddleware:
10 class BanMiddleware:
14 """
11 """
15 This is run before showing the thread. Banned users don't need to see
12 This is run before showing the thread. Banned users don't need to see
16 anything
13 anything
17 """
14 """
18
15
19 def __init__(self):
16 def __init__(self):
20 pass
17 pass
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
28 if bans.exists():
25 if bans.exists():
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)),
35 ('text_markup_type', models.CharField(choices=[('', '--'), ('bbcode', 'bbcode')], max_length=30, default='bbcode')),
36 ('poster_ip', models.GenericIPAddressField()),
37 ('_text_rendered', models.TextField(editable=False)),
38 ('poster_user_agent', models.TextField()),
39 ('last_edit_time', models.DateTimeField()),
40 ('refmap', models.TextField(null=True, blank=True)),
41 ],
42 options={
43 'ordering': ('id',),
44 },
45 bases=(models.Model, boards.models.base.Viewable),
46 ),
47 migrations.CreateModel(
48 name='PostImage',
49 fields=[
50 ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')),
51 ('width', models.IntegerField(default=0)),
52 ('height', models.IntegerField(default=0)),
53 ('pre_width', models.IntegerField(default=0)),
54 ('pre_height', models.IntegerField(default=0)),
55 ('image', boards.thumbs.ImageWithThumbsField(height_field='height', width_field='width', upload_to=boards.models.image.PostImage._update_image_filename, blank=True)),
56 ('hash', models.CharField(max_length=36)),
57 ],
58 options={
59 'ordering': ('id',),
72 },
60 },
73 u'boards.post': {
61 bases=(models.Model,),
74 'Meta': {'object_name': 'Post'},
62 ),
75 '_text_rendered': ('django.db.models.fields.TextField', [], {}),
63 migrations.CreateModel(
76 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
64 name='Tag',
77 'image': ('boards.thumbs.ImageWithThumbsField', [], {'max_length': '100', 'blank': 'True'}),
65 fields=[
78 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}),
66 ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')),
79 'parent': ('django.db.models.fields.BigIntegerField', [], {}),
67 ('name', models.CharField(db_index=True, max_length=100)),
80 'poster_ip': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}),
68 ],
81 'poster_user_agent': ('django.db.models.fields.TextField', [], {}),
69 options={
82 'pub_time': ('django.db.models.fields.DateTimeField', [], {}),
70 'ordering': ('name',),
83 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['boards.Tag']", 'symmetrical': 'False'}),
71 },
84 'text': ('markupfield.fields.MarkupField', [], {'rendered_field': 'True'}),
72 bases=(models.Model, boards.models.base.Viewable),
85 'text_markup_type': ('django.db.models.fields.CharField', [], {'default': "'markdown'", 'max_length': '30'}),
73 ),
86 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'})
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={
87 },
86 },
88 u'boards.tag': {
87 bases=(models.Model,),
89 'Meta': {'object_name': 'Tag'},
88 ),
90 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
89 migrations.AddField(
91 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
90 model_name='tag',
92 }
91 name='threads',
93 }
92 field=models.ManyToManyField(null=True, related_name='tag+', to='boards.Thread', blank=True),
94
93 preserve_default=True,
95 complete_apps = ['boards'] No newline at end of file
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 ]
@@ -1,10 +1,18 b''
1 __author__ = 'neko259'
1 __author__ = 'neko259'
2
2
3
3
4 class Viewable():
4 class Viewable():
5 def __init__(self):
5 def __init__(self):
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
@@ -1,62 +1,83 b''
1 import hashlib
1 import hashlib
2 import os
2 import os
3 from random import random
3 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
10
11
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',)
20
25
21 def _update_image_filename(self, filename):
26 def _update_image_filename(self, filename):
22 """
27 """
23 Gets unique image filename
28 Gets unique image filename
24 """
29 """
25
30
26 path = IMAGES_DIRECTORY
31 path = IMAGES_DIRECTORY
27 new_name = str(int(time.mktime(time.gmtime())))
32 new_name = str(int(time.mktime(time.gmtime())))
28 new_name += str(int(random() * 1000))
33 new_name += str(int(random() * 1000))
29 new_name += FILE_EXTENSION_DELIMITER
34 new_name += FILE_EXTENSION_DELIMITER
30 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
35 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
31
36
32 return os.path.join(path, new_name)
37 return os.path.join(path, new_name)
33
38
34 width = models.IntegerField(default=0)
39 width = models.IntegerField(default=0)
35 height = models.IntegerField(default=0)
40 height = models.IntegerField(default=0)
36
41
37 pre_width = models.IntegerField(default=0)
42 pre_width = models.IntegerField(default=0)
38 pre_height = models.IntegerField(default=0)
43 pre_height = models.IntegerField(default=0)
39
44
40 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
45 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
41 blank=True, sizes=(IMAGE_THUMB_SIZE,),
46 blank=True, sizes=(IMAGE_THUMB_SIZE,),
42 width_field='width',
47 width_field='width',
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 """
50 Saves the model and computes the image hash for deduplication purposes.
55 Saves the model and computes the image hash for deduplication purposes.
51 """
56 """
52
57
53 if not self.pk and self.image:
58 if not self.pk and self.image:
54 md5 = hashlib.md5()
59 md5 = hashlib.md5()
55 for chunk in self.image.chunks():
60 for chunk in self.image.chunks():
56 md5.update(chunk)
61 md5.update(chunk)
57 self.hash = md5.hexdigest()
62 self.hash = md5.hexdigest()
58 super(PostImage, self).save(*args, **kwargs)
63 super(PostImage, self).save(*args, **kwargs)
59
64
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))
@@ -1,518 +1,623 b''
1 from datetime import datetime, timedelta, date
1 from datetime import datetime, timedelta, date
2 from datetime import time as dtime
2 from datetime import time as dtime
3 import logging
3 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'
25 CACHE_KEY_POST_URL = 'post_url'
34 CACHE_KEY_POST_URL = 'post_url'
26
35
27 POSTS_PER_DAY_RANGE = 7
36 POSTS_PER_DAY_RANGE = 7
28
37
29 BAN_REASON_AUTO = 'Auto'
38 BAN_REASON_AUTO = 'Auto'
30
39
31 IMAGE_THUMB_SIZE = (200, 150)
40 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
40 # TODO Real user agent should be saved instead of this
47 # TODO Real user agent should be saved instead of this
41 UNKNOWN_UA = ''
48 UNKNOWN_UA = ''
42
49
43 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
50 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
44 REGEX_GLOBAL_REPLY = re.compile(r'\[post\](\w+)::([^:]+)::(\d+)\[/post\]')
51 REGEX_GLOBAL_REPLY = re.compile(r'\[post\](\w+)::([^:]+)::(\d+)\[/post\]')
45
52
46 TAG_MODEL = 'model'
53 TAG_MODEL = 'model'
47 TAG_REQUEST = 'request'
54 TAG_REQUEST = 'request'
48 TAG_RESPONSE = 'response'
55 TAG_RESPONSE = 'response'
49 TAG_ID = 'id'
56 TAG_ID = 'id'
50 TAG_STATUS = 'status'
57 TAG_STATUS = 'status'
51 TAG_MODELS = 'models'
58 TAG_MODELS = 'models'
52 TAG_TITLE = 'title'
59 TAG_TITLE = 'title'
53 TAG_TEXT = 'text'
60 TAG_TEXT = 'text'
54 TAG_THREAD = 'thread'
61 TAG_THREAD = 'thread'
55 TAG_PUB_TIME = 'pub-time'
62 TAG_PUB_TIME = 'pub-time'
56 TAG_SIGNATURES = 'signatures'
63 TAG_SIGNATURES = 'signatures'
57 TAG_SIGNATURE = 'signature'
64 TAG_SIGNATURE = 'signature'
58 TAG_CONTENT = 'content'
65 TAG_CONTENT = 'content'
59 TAG_ATTACHMENTS = 'attachments'
66 TAG_ATTACHMENTS = 'attachments'
60 TAG_ATTACHMENT = 'attachment'
67 TAG_ATTACHMENT = 'attachment'
61
68
62 TYPE_GET = 'get'
69 TYPE_GET = 'get'
63
70
64 ATTR_VERSION = 'version'
71 ATTR_VERSION = 'version'
65 ATTR_TYPE = 'type'
72 ATTR_TYPE = 'type'
66 ATTR_NAME = 'name'
73 ATTR_NAME = 'name'
67 ATTR_VALUE = 'value'
74 ATTR_VALUE = 'value'
68 ATTR_MIMETYPE = 'mimetype'
75 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 """
81
108
82 if not tags:
109 if not tags:
83 tags = []
110 tags = []
84
111
85 posting_time = timezone.now()
112 posting_time = timezone.now()
86 if not thread:
113 if not thread:
87 thread = Thread.objects.create(bump_time=posting_time,
114 thread = Thread.objects.create(bump_time=posting_time,
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,
101 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
127 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
102 # last!
128 # last!
103 last_edit_time=posting_time)
129 last_edit_time=posting_time)
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
147 """
161 """
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):
155 """
169 """
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)
164 referenced_post.last_edit_time = post.pub_time
179 referenced_post.last_edit_time = post.pub_time
165 referenced_post.build_refmap()
180 referenced_post.build_refmap()
166 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
181 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
167
182
168 referenced_thread = referenced_post.get_thread()
183 referenced_thread = referenced_post.get_thread()
169 referenced_thread.last_edit_time = post.pub_time
184 referenced_thread.last_edit_time = post.pub_time
170 referenced_thread.save(update_fields=['last_edit_time'])
185 referenced_thread.save(update_fields=['last_edit_time'])
171
186
172 def get_posts_per_day(self):
187 def get_posts_per_day(self):
173 """
188 """
174 Gets average count of posts per day for the last 7 days
189 Gets average count of posts per day for the last 7 days
175 """
190 """
176
191
177 day_end = date.today()
192 day_end = date.today()
178 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
193 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
179
194
180 cache_key = CACHE_KEY_PPD + str(day_end)
195 cache_key = CACHE_KEY_PPD + str(day_end)
181 ppd = cache.get(cache_key)
196 ppd = cache.get(cache_key)
182 if ppd:
197 if ppd:
183 return ppd
198 return ppd
184
199
185 day_time_start = timezone.make_aware(datetime.combine(
200 day_time_start = timezone.make_aware(datetime.combine(
186 day_start, dtime()), timezone.get_current_timezone())
201 day_start, dtime()), timezone.get_current_timezone())
187 day_time_end = timezone.make_aware(datetime.combine(
202 day_time_end = timezone.make_aware(datetime.combine(
188 day_end, dtime()), timezone.get_current_timezone())
203 day_end, dtime()), timezone.get_current_timezone())
189
204
190 posts_per_period = float(self.filter(
205 posts_per_period = float(self.filter(
191 pub_time__lte=day_time_end,
206 pub_time__lte=day_time_end,
192 pub_time__gte=day_time_start).count())
207 pub_time__gte=day_time_start).count())
193
208
194 ppd = posts_per_period / POSTS_PER_DAY_RANGE
209 ppd = posts_per_period / POSTS_PER_DAY_RANGE
195
210
196 cache.set(cache_key, ppd)
211 cache.set(cache_key, ppd)
197 return ppd
212 return ppd
198
213
199 # TODO Make a separate sync facade?
214 # TODO Make a separate sync facade?
200 def generate_response_get(self, model_list: list):
215 def generate_response_get(self, model_list: list):
201 response = et.Element(TAG_RESPONSE)
216 response = et.Element(TAG_RESPONSE)
202
217
203 status = et.SubElement(response, TAG_STATUS)
218 status = et.SubElement(response, TAG_STATUS)
204 status.text = STATUS_SUCCESS
219 status.text = STATUS_SUCCESS
205
220
206 models = et.SubElement(response, TAG_MODELS)
221 models = et.SubElement(response, TAG_MODELS)
207
222
208 for post in model_list:
223 for post in model_list:
209 model = et.SubElement(models, TAG_MODEL)
224 model = et.SubElement(models, TAG_MODEL)
210 model.set(ATTR_NAME, 'post')
225 model.set(ATTR_NAME, 'post')
211
226
212 content_tag = et.SubElement(model, TAG_CONTENT)
227 content_tag = et.SubElement(model, TAG_CONTENT)
213
228
214 tag_id = et.SubElement(content_tag, TAG_ID)
229 tag_id = et.SubElement(content_tag, TAG_ID)
215 post.global_id.to_xml_element(tag_id)
230 post.global_id.to_xml_element(tag_id)
216
231
217 title = et.SubElement(content_tag, TAG_TITLE)
232 title = et.SubElement(content_tag, TAG_TITLE)
218 title.text = post.title
233 title.text = post.title
219
234
220 text = et.SubElement(content_tag, TAG_TEXT)
235 text = et.SubElement(content_tag, TAG_TEXT)
221 # TODO Replace local links by global ones in the text
236 # TODO Replace local links by global ones in the text
222 text.text = post.text.raw
237 text.text = post.text.raw
223
238
224 if not post.is_opening():
239 if not post.is_opening():
225 thread = et.SubElement(content_tag, TAG_THREAD)
240 thread = et.SubElement(content_tag, TAG_THREAD)
226 thread.text = str(post.get_thread().get_opening_post_id())
241 thread.text = str(post.get_thread().get_opening_post_id())
227 else:
242 else:
228 # TODO Output tags here
243 # TODO Output tags here
229 pass
244 pass
230
245
231 pub_time = et.SubElement(content_tag, TAG_PUB_TIME)
246 pub_time = et.SubElement(content_tag, TAG_PUB_TIME)
232 pub_time.text = str(post.get_pub_time_epoch())
247 pub_time.text = str(post.get_pub_time_epoch())
233
248
234 signatures_tag = et.SubElement(model, TAG_SIGNATURES)
249 signatures_tag = et.SubElement(model, TAG_SIGNATURES)
235 post_signatures = post.signature.all()
250 post_signatures = post.signature.all()
236 if post_signatures:
251 if post_signatures:
237 signatures = post.signatures
252 signatures = post.signatures
238 else:
253 else:
239 # TODO Maybe the signature can be computed only once after
254 # TODO Maybe the signature can be computed only once after
240 # the post is added? Need to add some on_save signal queue
255 # the post is added? Need to add some on_save signal queue
241 # and add this there.
256 # and add this there.
242 key = KeyPair.objects.get(public_key=post.global_id.key)
257 key = KeyPair.objects.get(public_key=post.global_id.key)
243 signatures = [Signature(
258 signatures = [Signature(
244 key_type=key.key_type,
259 key_type=key.key_type,
245 key=key.public_key,
260 key=key.public_key,
246 signature=key.sign(et.tostring(model, ENCODING_UNICODE)),
261 signature=key.sign(et.tostring(model, ENCODING_UNICODE)),
247 )]
262 )]
248 for signature in signatures:
263 for signature in signatures:
249 signature_tag = et.SubElement(signatures_tag, TAG_SIGNATURE)
264 signature_tag = et.SubElement(signatures_tag, TAG_SIGNATURE)
250 signature_tag.set(ATTR_TYPE, signature.key_type)
265 signature_tag.set(ATTR_TYPE, signature.key_type)
251 signature_tag.set(ATTR_VALUE, signature.signature)
266 signature_tag.set(ATTR_VALUE, signature.signature)
252
267
253 return et.tostring(response, ENCODING_UNICODE)
268 return et.tostring(response, ENCODING_UNICODE)
254
269
255 def parse_response_get(self, response_xml):
270 def parse_response_get(self, response_xml):
256 tag_root = et.fromstring(response_xml)
271 tag_root = et.fromstring(response_xml)
257 tag_status = tag_root[0]
272 tag_status = tag_root[0]
258 if 'success' == tag_status.text:
273 if 'success' == tag_status.text:
259 tag_models = tag_root[1]
274 tag_models = tag_root[1]
260 for tag_model in tag_models:
275 for tag_model in tag_models:
261 tag_content = tag_model[0]
276 tag_content = tag_model[0]
262 tag_id = tag_content[1]
277 tag_id = tag_content[1]
263 try:
278 try:
264 GlobalId.from_xml_element(tag_id, existing=True)
279 GlobalId.from_xml_element(tag_id, existing=True)
265 # If this post already exists, just continue
280 # If this post already exists, just continue
266 # TODO Compare post content and update the post if necessary
281 # TODO Compare post content and update the post if necessary
267 pass
282 pass
268 except GlobalId.DoesNotExist:
283 except GlobalId.DoesNotExist:
269 global_id = GlobalId.from_xml_element(tag_id)
284 global_id = GlobalId.from_xml_element(tag_id)
270
285
271 title = tag_content.find(TAG_TITLE).text
286 title = tag_content.find(TAG_TITLE).text
272 text = tag_content.find(TAG_TEXT).text
287 text = tag_content.find(TAG_TEXT).text
273 # TODO Check that the replied posts are already present
288 # TODO Check that the replied posts are already present
274 # before adding new ones
289 # before adding new ones
275
290
276 # TODO Pub time, thread, tags
291 # TODO Pub time, thread, tags
277
292
278 post = Post.objects.create(title=title, text=text)
293 post = Post.objects.create(title=title, text=text)
279 else:
294 else:
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."""
286
312
287 objects = PostManager()
313 objects = PostManager()
288
314
289 class Meta:
315 class Meta:
290 app_label = APP_LABEL_BOARDS
316 app_label = APP_LABEL_BOARDS
291 ordering = ('id',)
317 ordering = ('id',)
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)
300
326
301 poster_ip = models.GenericIPAddressField()
327 poster_ip = models.GenericIPAddressField()
302 poster_user_agent = models.TextField()
328 poster_user_agent = models.TextField()
303
329
304 thread_new = models.ForeignKey('Thread', null=True, default=None,
330 thread_new = models.ForeignKey('Thread', null=True, default=None,
305 db_index=True)
331 db_index=True)
306 last_edit_time = models.DateTimeField()
332 last_edit_time = models.DateTimeField()
307
333
308 # Replies to the post
334 # Replies to the post
309 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
335 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
310 null=True,
336 null=True,
311 blank=True, related_name='rfp+',
337 blank=True, related_name='rfp+',
312 db_index=True)
338 db_index=True)
313
339
314 # Replies map. This is built from the referenced posts list to speed up
340 # Replies map. This is built from the referenced posts list to speed up
315 # page loading (no need to get all the referenced posts from the database).
341 # page loading (no need to get all the referenced posts from the database).
316 refmap = models.TextField(null=True, blank=True)
342 refmap = models.TextField(null=True, blank=True)
317
343
318 # Global ID with author key. If the message was downloaded from another
344 # Global ID with author key. If the message was downloaded from another
319 # server, this indicates the server.
345 # server, this indicates the server.
320 global_id = models.OneToOneField('GlobalId', null=True, blank=True)
346 global_id = models.OneToOneField('GlobalId', null=True, blank=True)
321
347
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.
336 """
369 """
337 map_string = ''
370 map_string = ''
338
371
339 first = True
372 first = True
340 for refpost in self.referenced_posts.all():
373 for refpost in self.referenced_posts.all():
341 if not first:
374 if not first:
342 map_string += ', '
375 map_string += ', '
343 map_string += '<a href="%s">&gt;&gt;%s</a>' % (refpost.get_url(),
376 map_string += '<a href="%s">&gt;&gt;%s</a>' % (refpost.get_url(),
344 refpost.id)
377 refpost.id)
345 first = False
378 first = False
346
379
347 self.refmap = map_string
380 self.refmap = map_string
348
381
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:
386 if not self.refmap:
387 return False
388 else:
353 return len(self.refmap) > 0
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 """
359
395
360 return self.get_thread().get_opening_post_id() == self.id
396 return self.get_thread().get_opening_post_id() == self.id
361
397
362 @transaction.atomic
398 @transaction.atomic
363 def add_tag(self, tag):
399 def add_tag(self, tag):
364 edit_time = timezone.now()
400 edit_time = timezone.now()
365
401
366 thread = self.get_thread()
402 thread = self.get_thread()
367 thread.add_tag(tag)
403 thread.add_tag(tag)
368 self.last_edit_time = edit_time
404 self.last_edit_time = edit_time
369 self.save(update_fields=['last_edit_time'])
405 self.save(update_fields=['last_edit_time'])
370
406
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.
389 """
413 """
390
414
391 cache_key = CACHE_KEY_POST_URL + str(self.id)
415 cache_key = CACHE_KEY_POST_URL + str(self.id)
392 link = cache.get(cache_key)
416 link = cache.get(cache_key)
393
417
394 if not link:
418 if not link:
395 if not thread:
419 if not thread:
396 thread = self.get_thread()
420 thread = self.get_thread()
397
421
398 opening_id = thread.get_opening_post_id()
422 opening_id = thread.get_opening_post_id()
399
423
400 if self.id != opening_id:
424 if self.id != opening_id:
401 link = reverse('thread', kwargs={
425 link = reverse('thread', kwargs={
402 'post_id': opening_id}) + '#' + str(self.id)
426 'post_id': opening_id}) + '#' + str(self.id)
403 else:
427 else:
404 link = reverse('thread', kwargs={'post_id': self.id})
428 link = reverse('thread', kwargs={'post_id': self.id})
405
429
406 cache.set(cache_key, link)
430 cache.set(cache_key, link)
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 """
414
438
415 return self.thread_new
439 return self.thread_new
416
440
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
442 else:
458 else:
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()
465 self.signature.all().delete()
485 self.signature.all().delete()
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 """
473 Sets global id based on the given key pair. If no key pair is given,
502 Sets global id based on the given key pair. If no key pair is given,
474 default one is used.
503 default one is used.
475 """
504 """
476
505
477 if key_pair:
506 if key_pair:
478 key = key_pair
507 key = key_pair
479 else:
508 else:
480 try:
509 try:
481 key = KeyPair.objects.get(primary=True)
510 key = KeyPair.objects.get(primary=True)
482 except KeyPair.DoesNotExist:
511 except KeyPair.DoesNotExist:
483 # Do not update the global id because there is no key defined
512 # Do not update the global id because there is no key defined
484 return
513 return
485 global_id = GlobalId(key_type=key.key_type,
514 global_id = GlobalId(key_type=key.key_type,
486 key=key.public_key,
515 key=key.public_key,
487 local_id = self.id)
516 local_id = self.id)
488 global_id.save()
517 global_id.save()
489
518
490 self.global_id = global_id
519 self.global_id = global_id
491
520
492 self.save(update_fields=['global_id'])
521 self.save(update_fields=['global_id'])
493
522
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.
500 """
530 """
501
531
502 local_replied = REGEX_REPLY.findall(self.text.raw)
532 local_replied = REGEX_REPLY.findall(self.text.raw)
503 global_replied = []
533 global_replied = []
504 # TODO Similar code is used in mdx_neboard, maybe it can be extracted
534 # TODO Similar code is used in mdx_neboard, maybe it can be extracted
505 # into a method?
535 # into a method?
506 for match in REGEX_GLOBAL_REPLY.findall(self.text.raw):
536 for match in REGEX_GLOBAL_REPLY.findall(self.text.raw):
507 key_type = match[0]
537 key_type = match[0]
508 key = match[1]
538 key = match[1]
509 local_id = match[2]
539 local_id = match[2]
510
540
511 try:
541 try:
512 global_id = GlobalId.objects.get(key_type=key_type,
542 global_id = GlobalId.objects.get(key_type=key_type,
513 key=key, local_id=local_id)
543 key=key, local_id=local_id)
514 for post in Post.objects.filter(global_id=global_id).only('id'):
544 for post in Post.objects.filter(global_id=global_id).only('id'):
515 global_replied.append(post.id)
545 global_replied.append(post.id)
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,78 +1,71 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
10 __author__ = 'neko259'
9 __author__ = 'neko259'
11
10
12
11
13 class TagManager(models.Manager):
12 class TagManager(models.Manager):
14
13
15 def get_not_empty_tags(self):
14 def get_not_empty_tags(self):
16 """
15 """
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):
27 """
25 """
28 A tag is a text node assigned to the thread. The tag serves as a board
26 A tag is a text node assigned to the thread. The tag serves as a board
29 section. There can be multiple tags for each thread
27 section. There can be multiple tags for each thread
30 """
28 """
31
29
32 objects = TagManager()
30 objects = TagManager()
33
31
34 class Meta:
32 class Meta:
35 app_label = 'boards'
33 app_label = 'boards'
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,188 +1,182 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
6 from boards import settings
6 from boards import settings
7
7
8 __author__ = 'neko259'
8 __author__ = 'neko259'
9
9
10
10
11 logger = logging.getLogger(__name__)
11 logger = logging.getLogger(__name__)
12
12
13
13
14 CACHE_KEY_OPENING_POST = 'opening_post_id'
14 CACHE_KEY_OPENING_POST = 'opening_post_id'
15
15
16
16
17 class ThreadManager(models.Manager):
17 class ThreadManager(models.Manager):
18 def process_oldest_threads(self):
18 def process_oldest_threads(self):
19 """
19 """
20 Preserves maximum thread count. If there are too many threads,
20 Preserves maximum thread count. If there are too many threads,
21 archive or delete the old ones.
21 archive or delete the old ones.
22 """
22 """
23
23
24 threads = Thread.objects.filter(archived=False).order_by('-bump_time')
24 threads = Thread.objects.filter(archived=False).order_by('-bump_time')
25 thread_count = threads.count()
25 thread_count = threads.count()
26
26
27 if thread_count > settings.MAX_THREAD_COUNT:
27 if thread_count > settings.MAX_THREAD_COUNT:
28 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
28 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
29 old_threads = threads[thread_count - num_threads_to_delete:]
29 old_threads = threads[thread_count - num_threads_to_delete:]
30
30
31 for thread in old_threads:
31 for thread in old_threads:
32 if settings.ARCHIVE_THREADS:
32 if settings.ARCHIVE_THREADS:
33 self._archive_thread(thread)
33 self._archive_thread(thread)
34 else:
34 else:
35 thread.delete()
35 thread.delete()
36
36
37 logger.info('Processed %d old threads' % num_threads_to_delete)
37 logger.info('Processed %d old threads' % num_threads_to_delete)
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):
46 objects = ThreadManager()
47 objects = ThreadManager()
47
48
48 class Meta:
49 class Meta:
49 app_label = 'boards'
50 app_label = 'boards'
50
51
51 tags = models.ManyToManyField('Tag')
52 tags = models.ManyToManyField('Tag')
52 bump_time = models.DateTimeField()
53 bump_time = models.DateTimeField()
53 last_edit_time = models.DateTimeField()
54 last_edit_time = models.DateTimeField()
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 """
60 Gets a sorted tag list.
62 Gets a sorted tag list.
61 """
63 """
62
64
63 return self.tags.order_by('name')
65 return self.tags.order_by('name')
64
66
65 def bump(self):
67 def bump(self):
66 """
68 """
67 Bumps (moves to up) thread if possible.
69 Bumps (moves to up) thread if possible.
68 """
70 """
69
71
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 """
100 Gets several last replies, not including opening post
96 Gets several last replies, not including opening post
101 """
97 """
102
98
103 if settings.LAST_REPLIES_COUNT > 0:
99 if settings.LAST_REPLIES_COUNT > 0:
104 reply_count = self.get_reply_count()
100 reply_count = self.get_reply_count()
105
101
106 if reply_count > 0:
102 if reply_count > 0:
107 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
103 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
108 reply_count - 1)
104 reply_count - 1)
109 replies = self.get_replies()
105 replies = self.get_replies()
110 last_replies = replies[reply_count - reply_count_to_show:]
106 last_replies = replies[reply_count - reply_count_to_show:]
111
107
112 return last_replies
108 return last_replies
113
109
114 def get_skipped_replies_count(self):
110 def get_skipped_replies_count(self):
115 """
111 """
116 Gets number of posts between opening post and last replies.
112 Gets number of posts between opening post and last replies.
117 """
113 """
118 reply_count = self.get_reply_count()
114 reply_count = self.get_reply_count()
119 last_replies_count = min(settings.LAST_REPLIES_COUNT,
115 last_replies_count = min(settings.LAST_REPLIES_COUNT,
120 reply_count - 1)
116 reply_count - 1)
121 return reply_count - last_replies_count - 1
117 return reply_count - last_replies_count - 1
122
118
123 def get_replies(self, view_fields_only=False):
119 def get_replies(self, view_fields_only=False):
124 """
120 """
125 Gets sorted thread posts
121 Gets sorted thread posts
126 """
122 """
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):
134 return self.get_replies(view_fields_only).annotate(images_count=Count(
130 return self.get_replies(view_fields_only).annotate(images_count=Count(
135 'images')).filter(images_count__gt=0)
131 'images')).filter(images_count__gt=0)
136
132
137 def add_tag(self, tag):
133 def add_tag(self, tag):
138 """
134 """
139 Connects thread to a tag and tag to a thread
135 Connects thread to a tag and tag to a thread
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 """
151 Gets the first post of the thread
142 Gets the first post of the thread
152 """
143 """
153
144
154 query = self.replies.order_by('pub_time')
145 query = self.replies.order_by('pub_time')
155 if only_id:
146 if only_id:
156 query = query.only('id')
147 query = query.only('id')
157 opening_post = query.first()
148 opening_post = query.first()
158
149
159 return opening_post
150 return opening_post
160
151
161 def get_opening_post_id(self):
152 def get_opening_post_id(self):
162 """
153 """
163 Gets ID of the first thread post.
154 Gets ID of the first thread post.
164 """
155 """
165
156
166 cache_key = CACHE_KEY_OPENING_POST + str(self.id)
157 cache_key = CACHE_KEY_OPENING_POST + str(self.id)
167 opening_post_id = cache.get(cache_key)
158 opening_post_id = cache.get(cache_key)
168 if not opening_post_id:
159 if not opening_post_id:
169 opening_post_id = self.get_opening_post(only_id=True).id
160 opening_post_id = self.get_opening_post(only_id=True).id
170 cache.set(cache_key, opening_post_id)
161 cache.set(cache_key, opening_post_id)
171
162
172 return opening_post_id
163 return opening_post_id
173
164
174 def __unicode__(self):
165 def __unicode__(self):
175 return str(self.id)
166 return str(self.id)
176
167
177 def get_pub_time(self):
168 def get_pub_time(self):
178 """
169 """
179 Gets opening post's pub time because thread does not have its own one.
170 Gets opening post's pub time because thread does not have its own one.
180 """
171 """
181
172
182 return self.get_opening_post().pub_time
173 return self.get_opening_post().pub_time
183
174
184 def delete(self, using=None):
175 def delete(self, using=None):
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
@@ -1,24 +1,24 b''
1 from haystack import indexes
1 from haystack import indexes
2 from boards.models import Post, Tag
2 from boards.models import Post, Tag
3
3
4 __author__ = 'neko259'
4 __author__ = 'neko259'
5
5
6
6
7 class PostIndex(indexes.SearchIndex, indexes.Indexable):
7 class PostIndex(indexes.SearchIndex, indexes.Indexable):
8 text = indexes.CharField(document=True, use_template=True)
8 text = indexes.CharField(document=True, use_template=True)
9
9
10 def get_model(self):
10 def get_model(self):
11 return Post
11 return Post
12
12
13 def index_queryset(self, using=None):
13 def index_queryset(self, using=None):
14 return self.get_model().objects.all()
14 return self.get_model().objects.all()
15
15
16
16
17 class TagIndex(indexes.SearchIndex, indexes.Indexable):
17 class TagIndex(indexes.SearchIndex, indexes.Indexable):
18 text = indexes.CharField(document=True, use_template=True)
18 text = indexes.CharField(document=True, use_template=True)
19
19
20 def get_model(self):
20 def get_model(self):
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,20 +1,22 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
6 MAX_TEXT_LENGTH = 30000 # Max post length in characters
6 MAX_TEXT_LENGTH = 30000 # Max post length in characters
7 MAX_IMAGE_SIZE = 8 * 1024 * 1024 # Max image size
7 MAX_IMAGE_SIZE = 8 * 1024 * 1024 # Max image size
8
8
9 # Thread bumplimit
9 # Thread bumplimit
10 MAX_POSTS_PER_THREAD = 10
10 MAX_POSTS_PER_THREAD = 10
11 # Old posts will be archived or deleted if this value is reached
11 # Old posts will be archived or deleted if this value is reached
12 MAX_THREAD_COUNT = 5
12 MAX_THREAD_COUNT = 5
13 THREADS_PER_PAGE = 3
13 THREADS_PER_PAGE = 3
14 DEFAULT_THEME = 'md'
14 DEFAULT_THEME = 'md'
15 LAST_REPLIES_COUNT = 3
15 LAST_REPLIES_COUNT = 3
16
16
17 # Enable archiving threads instead of deletion when the thread limit is reached
17 # Enable archiving threads instead of deletion when the thread limit is reached
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,484 +1,497 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;
4 }
13 }
5
14
6 body {
15 body {
7 margin: 0;
16 margin: 0;
8 }
17 }
9
18
10 #admin_panel {
19 #admin_panel {
11 background: #FF0000;
20 background: #FF0000;
12 color: #00FF00
21 color: #00FF00
13 }
22 }
14
23
15 .input_field_error {
24 .input_field_error {
16 color: #FF0000;
25 color: #FF0000;
17 }
26 }
18
27
19 .title {
28 .title {
20 font-weight: bold;
29 font-weight: bold;
21 color: #ffcc00;
30 color: #ffcc00;
22 }
31 }
23
32
24 .link, a {
33 .link, a {
25 color: #afdcec;
34 color: #afdcec;
26 }
35 }
27
36
28 .block {
37 .block {
29 display: inline-block;
38 display: inline-block;
30 vertical-align: top;
39 vertical-align: top;
31 }
40 }
32
41
33 .tag {
42 .tag {
34 color: #FFD37D;
43 color: #FFD37D;
35 }
44 }
36
45
37 .post_id {
46 .post_id {
38 color: #fff380;
47 color: #fff380;
39 }
48 }
40
49
41 .post, .dead_post, .archive_post, #posts-table {
50 .post, .dead_post, .archive_post, #posts-table {
42 background: #333;
51 background: #333;
43 padding: 10px;
52 padding: 10px;
44 clear: left;
53 clear: left;
45 word-wrap: break-word;
54 word-wrap: break-word;
46 border-top: 1px solid #777;
55 border-top: 1px solid #777;
47 border-bottom: 1px solid #777;
56 border-bottom: 1px solid #777;
48 }
57 }
49
58
50 .post + .post {
59 .post + .post {
51 border-top: none;
60 border-top: none;
52 }
61 }
53
62
54 .dead_post + .dead_post {
63 .dead_post + .dead_post {
55 border-top: none;
64 border-top: none;
56 }
65 }
57
66
58 .archive_post + .archive_post {
67 .archive_post + .archive_post {
59 border-top: none;
68 border-top: none;
60 }
69 }
61
70
62 .metadata {
71 .metadata {
63 padding-top: 5px;
72 padding-top: 5px;
64 margin-top: 10px;
73 margin-top: 10px;
65 border-top: solid 1px #666;
74 border-top: solid 1px #666;
66 color: #ddd;
75 color: #ddd;
67 }
76 }
68
77
69 .navigation_panel, .tag_info {
78 .navigation_panel, .tag_info {
70 background: #222;
79 background: #222;
71 margin-bottom: 5px;
80 margin-bottom: 5px;
72 margin-top: 5px;
81 margin-top: 5px;
73 padding: 10px;
82 padding: 10px;
74 border-bottom: solid 1px #888;
83 border-bottom: solid 1px #888;
75 border-top: solid 1px #888;
84 border-top: solid 1px #888;
76 color: #eee;
85 color: #eee;
77 }
86 }
78
87
79 .navigation_panel .link {
88 .navigation_panel .link {
80 border-right: 1px solid #fff;
89 border-right: 1px solid #fff;
81 font-weight: bold;
90 font-weight: bold;
82 margin-right: 1ex;
91 margin-right: 1ex;
83 padding-right: 1ex;
92 padding-right: 1ex;
84 }
93 }
85 .navigation_panel .link:last-child {
94 .navigation_panel .link:last-child {
86 border-left: 1px solid #fff;
95 border-left: 1px solid #fff;
87 border-right: none;
96 border-right: none;
88 float: right;
97 float: right;
89 margin-left: 1ex;
98 margin-left: 1ex;
90 margin-right: 0;
99 margin-right: 0;
91 padding-left: 1ex;
100 padding-left: 1ex;
92 padding-right: 0;
101 padding-right: 0;
93 }
102 }
94
103
95 .navigation_panel::after, .post::after {
104 .navigation_panel::after, .post::after {
96 clear: both;
105 clear: both;
97 content: ".";
106 content: ".";
98 display: block;
107 display: block;
99 height: 0;
108 height: 0;
100 line-height: 0;
109 line-height: 0;
101 visibility: hidden;
110 visibility: hidden;
102 }
111 }
103
112
104 .header {
113 .header {
105 border-bottom: solid 2px #ccc;
114 border-bottom: solid 2px #ccc;
106 margin-bottom: 5px;
115 margin-bottom: 5px;
107 border-top: none;
116 border-top: none;
108 margin-top: 0;
117 margin-top: 0;
109 }
118 }
110
119
111 .footer {
120 .footer {
112 border-top: solid 2px #ccc;
121 border-top: solid 2px #ccc;
113 margin-top: 5px;
122 margin-top: 5px;
114 border-bottom: none;
123 border-bottom: none;
115 margin-bottom: 0;
124 margin-bottom: 0;
116 }
125 }
117
126
118 p, .br {
127 p, .br {
119 margin-top: .5em;
128 margin-top: .5em;
120 margin-bottom: .5em;
129 margin-bottom: .5em;
121 }
130 }
122
131
123 .post-form-w {
132 .post-form-w {
124 background: #333344;
133 background: #333344;
125 border-top: solid 1px #888;
134 border-top: solid 1px #888;
126 border-bottom: solid 1px #888;
135 border-bottom: solid 1px #888;
127 color: #fff;
136 color: #fff;
128 padding: 10px;
137 padding: 10px;
129 margin-bottom: 5px;
138 margin-bottom: 5px;
130 margin-top: 5px;
139 margin-top: 5px;
131 }
140 }
132
141
133 .form-row {
142 .form-row {
134 width: 100%;
143 width: 100%;
135 }
144 }
136
145
137 .form-label {
146 .form-label {
138 padding: .25em 1ex .25em 0;
147 padding: .25em 1ex .25em 0;
139 vertical-align: top;
148 vertical-align: top;
140 }
149 }
141
150
142 .form-input {
151 .form-input {
143 padding: .25em 0;
152 padding: .25em 0;
144 }
153 }
145
154
146 .form-errors {
155 .form-errors {
147 font-weight: bolder;
156 font-weight: bolder;
148 vertical-align: middle;
157 vertical-align: middle;
149 }
158 }
150
159
151 .post-form input:not([name="image"]), .post-form textarea {
160 .post-form input:not([name="image"]), .post-form textarea {
152 background: #333;
161 background: #333;
153 color: #fff;
162 color: #fff;
154 border: solid 1px;
163 border: solid 1px;
155 padding: 0;
164 padding: 0;
156 font: medium sans-serif;
165 font: medium sans-serif;
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;
163 margin-top: 1ex;
176 margin-top: 1ex;
164 }
177 }
165
178
166 .form-title {
179 .form-title {
167 font-weight: bold;
180 font-weight: bold;
168 font-size: 2ex;
181 font-size: 2ex;
169 margin-bottom: 0.5ex;
182 margin-bottom: 0.5ex;
170 }
183 }
171
184
172 .post-form input[type="submit"], input[type="submit"] {
185 .post-form input[type="submit"], input[type="submit"] {
173 background: #222;
186 background: #222;
174 border: solid 2px #fff;
187 border: solid 2px #fff;
175 color: #fff;
188 color: #fff;
176 padding: 0.5ex;
189 padding: 0.5ex;
177 }
190 }
178
191
179 input[type="submit"]:hover {
192 input[type="submit"]:hover {
180 background: #060;
193 background: #060;
181 }
194 }
182
195
183 blockquote {
196 blockquote {
184 border-left: solid 2px;
197 border-left: solid 2px;
185 padding-left: 5px;
198 padding-left: 5px;
186 color: #B1FB17;
199 color: #B1FB17;
187 margin: 0;
200 margin: 0;
188 }
201 }
189
202
190 .post > .image {
203 .post > .image {
191 float: left;
204 float: left;
192 margin: 0 1ex .5ex 0;
205 margin: 0 1ex .5ex 0;
193 min-width: 1px;
206 min-width: 1px;
194 text-align: center;
207 text-align: center;
195 display: table-row;
208 display: table-row;
196 }
209 }
197
210
198 .post > .metadata {
211 .post > .metadata {
199 clear: left;
212 clear: left;
200 }
213 }
201
214
202 .get {
215 .get {
203 font-weight: bold;
216 font-weight: bold;
204 color: #d55;
217 color: #d55;
205 }
218 }
206
219
207 * {
220 * {
208 text-decoration: none;
221 text-decoration: none;
209 }
222 }
210
223
211 .dead_post {
224 .dead_post {
212 background-color: #442222;
225 background-color: #442222;
213 }
226 }
214
227
215 .archive_post {
228 .archive_post {
216 background-color: #000;
229 background-color: #000;
217 }
230 }
218
231
219 .mark_btn {
232 .mark_btn {
220 border: 1px solid;
233 border: 1px solid;
221 min-width: 2ex;
234 min-width: 2ex;
222 padding: 2px 2ex;
235 padding: 2px 2ex;
223 }
236 }
224
237
225 .mark_btn:hover {
238 .mark_btn:hover {
226 background: #555;
239 background: #555;
227 }
240 }
228
241
229 .quote {
242 .quote {
230 color: #92cf38;
243 color: #92cf38;
231 font-style: italic;
244 font-style: italic;
232 }
245 }
233
246
234 .multiquote {
247 .multiquote {
235 padding: 3px;
248 padding: 3px;
236 display: inline-block;
249 display: inline-block;
237 background: #222;
250 background: #222;
238 border-style: solid;
251 border-style: solid;
239 border-width: 1px 1px 1px 4px;
252 border-width: 1px 1px 1px 4px;
240 font-size: 0.9em;
253 font-size: 0.9em;
241 }
254 }
242
255
243 .spoiler {
256 .spoiler {
244 background: white;
257 background: white;
245 color: white;
258 color: white;
246 }
259 }
247
260
248 .spoiler:hover {
261 .spoiler:hover {
249 color: black;
262 color: black;
250 }
263 }
251
264
252 .comment {
265 .comment {
253 color: #eb2;
266 color: #eb2;
254 }
267 }
255
268
256 a:hover {
269 a:hover {
257 text-decoration: underline;
270 text-decoration: underline;
258 }
271 }
259
272
260 .last-replies {
273 .last-replies {
261 margin-left: 3ex;
274 margin-left: 3ex;
262 margin-right: 3ex;
275 margin-right: 3ex;
263 border-left: solid 1px #777;
276 border-left: solid 1px #777;
264 border-right: solid 1px #777;
277 border-right: solid 1px #777;
265 }
278 }
266
279
267 .last-replies > .post:first-child {
280 .last-replies > .post:first-child {
268 border-top: none;
281 border-top: none;
269 }
282 }
270
283
271 .thread {
284 .thread {
272 margin-bottom: 3ex;
285 margin-bottom: 3ex;
273 margin-top: 1ex;
286 margin-top: 1ex;
274 }
287 }
275
288
276 .post:target {
289 .post:target {
277 border: solid 2px white;
290 border: solid 2px white;
278 }
291 }
279
292
280 pre{
293 pre{
281 white-space:pre-wrap
294 white-space:pre-wrap
282 }
295 }
283
296
284 li {
297 li {
285 list-style-position: inside;
298 list-style-position: inside;
286 }
299 }
287
300
288 .fancybox-skin {
301 .fancybox-skin {
289 position: relative;
302 position: relative;
290 background-color: #fff;
303 background-color: #fff;
291 color: #ddd;
304 color: #ddd;
292 text-shadow: none;
305 text-shadow: none;
293 }
306 }
294
307
295 .fancybox-image {
308 .fancybox-image {
296 border: 1px solid black;
309 border: 1px solid black;
297 }
310 }
298
311
299 .image-mode-tab {
312 .image-mode-tab {
300 background: #444;
313 background: #444;
301 color: #eee;
314 color: #eee;
302 margin-top: 5px;
315 margin-top: 5px;
303 padding: 5px;
316 padding: 5px;
304 border-top: 1px solid #888;
317 border-top: 1px solid #888;
305 border-bottom: 1px solid #888;
318 border-bottom: 1px solid #888;
306 }
319 }
307
320
308 .image-mode-tab > label {
321 .image-mode-tab > label {
309 margin: 0 1ex;
322 margin: 0 1ex;
310 }
323 }
311
324
312 .image-mode-tab > label > input {
325 .image-mode-tab > label > input {
313 margin-right: .5ex;
326 margin-right: .5ex;
314 }
327 }
315
328
316 #posts-table {
329 #posts-table {
317 margin-top: 5px;
330 margin-top: 5px;
318 margin-bottom: 5px;
331 margin-bottom: 5px;
319 }
332 }
320
333
321 .tag_info > h2 {
334 .tag_info > h2 {
322 margin: 0;
335 margin: 0;
323 }
336 }
324
337
325 .post-info {
338 .post-info {
326 color: #ddd;
339 color: #ddd;
327 margin-bottom: 1ex;
340 margin-bottom: 1ex;
328 }
341 }
329
342
330 .moderator_info {
343 .moderator_info {
331 color: #e99d41;
344 color: #e99d41;
332 float: right;
345 float: right;
333 font-weight: bold;
346 font-weight: bold;
334 }
347 }
335
348
336 .refmap {
349 .refmap {
337 font-size: 0.9em;
350 font-size: 0.9em;
338 color: #ccc;
351 color: #ccc;
339 margin-top: 1em;
352 margin-top: 1em;
340 }
353 }
341
354
342 .fav {
355 .fav {
343 color: yellow;
356 color: yellow;
344 }
357 }
345
358
346 .not_fav {
359 .not_fav {
347 color: #ccc;
360 color: #ccc;
348 }
361 }
349
362
350 .role {
363 .role {
351 text-decoration: underline;
364 text-decoration: underline;
352 }
365 }
353
366
354 .form-email {
367 .form-email {
355 display: none;
368 display: none;
356 }
369 }
357
370
358 .bar-value {
371 .bar-value {
359 background: rgba(50, 55, 164, 0.45);
372 background: rgba(50, 55, 164, 0.45);
360 font-size: 0.9em;
373 font-size: 0.9em;
361 height: 1.5em;
374 height: 1.5em;
362 }
375 }
363
376
364 .bar-bg {
377 .bar-bg {
365 position: relative;
378 position: relative;
366 border-top: solid 1px #888;
379 border-top: solid 1px #888;
367 border-bottom: solid 1px #888;
380 border-bottom: solid 1px #888;
368 margin-top: 5px;
381 margin-top: 5px;
369 overflow: hidden;
382 overflow: hidden;
370 }
383 }
371
384
372 .bar-text {
385 .bar-text {
373 padding: 2px;
386 padding: 2px;
374 position: absolute;
387 position: absolute;
375 left: 0;
388 left: 0;
376 top: 0;
389 top: 0;
377 }
390 }
378
391
379 .page_link {
392 .page_link {
380 background: #444;
393 background: #444;
381 border-top: solid 1px #888;
394 border-top: solid 1px #888;
382 border-bottom: solid 1px #888;
395 border-bottom: solid 1px #888;
383 padding: 5px;
396 padding: 5px;
384 color: #eee;
397 color: #eee;
385 font-size: 2ex;
398 font-size: 2ex;
386 }
399 }
387
400
388 .skipped_replies {
401 .skipped_replies {
389 padding: 5px;
402 padding: 5px;
390 margin-left: 3ex;
403 margin-left: 3ex;
391 margin-right: 3ex;
404 margin-right: 3ex;
392 border-left: solid 1px #888;
405 border-left: solid 1px #888;
393 border-right: solid 1px #888;
406 border-right: solid 1px #888;
394 border-bottom: solid 1px #888;
407 border-bottom: solid 1px #888;
395 background: #000;
408 background: #000;
396 }
409 }
397
410
398 .current_page {
411 .current_page {
399 padding: 2px;
412 padding: 2px;
400 background-color: #afdcec;
413 background-color: #afdcec;
401 color: #000;
414 color: #000;
402 }
415 }
403
416
404 .current_mode {
417 .current_mode {
405 font-weight: bold;
418 font-weight: bold;
406 }
419 }
407
420
408 .gallery_image {
421 .gallery_image {
409 border: solid 1px;
422 border: solid 1px;
410 padding: 0.5ex;
423 padding: 0.5ex;
411 margin: 0.5ex;
424 margin: 0.5ex;
412 text-align: center;
425 text-align: center;
413 }
426 }
414
427
415 code {
428 code {
416 border: dashed 1px #ccc;
429 border: dashed 1px #ccc;
417 background: #111;
430 background: #111;
418 padding: 2px;
431 padding: 2px;
419 font-size: 1.2em;
432 font-size: 1.2em;
420 display: inline-block;
433 display: inline-block;
421 }
434 }
422
435
423 pre {
436 pre {
424 overflow: auto;
437 overflow: auto;
425 }
438 }
426
439
427 .img-full {
440 .img-full {
428 background: #222;
441 background: #222;
429 border: solid 1px white;
442 border: solid 1px white;
430 }
443 }
431
444
432 .tag_item {
445 .tag_item {
433 display: inline-block;
446 display: inline-block;
434 border: 1px dashed #666;
447 border: 1px dashed #666;
435 margin: 0.2ex;
448 margin: 0.2ex;
436 padding: 0.1ex;
449 padding: 0.1ex;
437 }
450 }
438
451
439 #id_models li {
452 #id_models li {
440 list-style: none;
453 list-style: none;
441 }
454 }
442
455
443 #id_q {
456 #id_q {
444 margin-left: 1ex;
457 margin-left: 1ex;
445 }
458 }
446
459
447 ul {
460 ul {
448 padding-left: 0px;
461 padding-left: 0px;
449 }
462 }
450
463
451 .quote-header {
464 .quote-header {
452 border-bottom: 2px solid #ddd;
465 border-bottom: 2px solid #ddd;
453 margin-bottom: 1ex;
466 margin-bottom: 1ex;
454 padding-bottom: .5ex;
467 padding-bottom: .5ex;
455 color: #ddd;
468 color: #ddd;
456 font-size: 1.2em;
469 font-size: 1.2em;
457 }
470 }
458
471
459 .global-id {
472 .global-id {
460 font-weight: bolder;
473 font-weight: bolder;
461 opacity: .5;
474 opacity: .5;
462 }
475 }
463
476
464 /* Post */
477 /* Post */
465 .post > .message, .post > .image {
478 .post > .message, .post > .image {
466 padding-left: 1em;
479 padding-left: 1em;
467 }
480 }
468
481
469 /* Reflink preview */
482 /* Reflink preview */
470 .post_preview {
483 .post_preview {
471 border-left: 1px solid #777;
484 border-left: 1px solid #777;
472 border-right: 1px solid #777;
485 border-right: 1px solid #777;
473 }
486 }
474
487
475 /* Code highlighter */
488 /* Code highlighter */
476 .hljs {
489 .hljs {
477 color: #fff;
490 color: #fff;
478 background: #000;
491 background: #000;
479 display: inline-block;
492 display: inline-block;
480 }
493 }
481
494
482 .hljs, .hljs-subst, .hljs-tag .hljs-title, .lisp .hljs-title, .clojure .hljs-built_in, .nginx .hljs-title {
495 .hljs, .hljs-subst, .hljs-tag .hljs-title, .lisp .hljs-title, .clojure .hljs-built_in, .nginx .hljs-title {
483 color: #fff;
496 color: #fff;
484 }
497 }
@@ -1,288 +1,334 b''
1 /*
1 /*
2 @licstart The following is the entire license notice for the
2 @licstart The following is the entire license notice for the
3 JavaScript code in this page.
3 JavaScript code in this page.
4
4
5
5
6 Copyright (C) 2013 neko259
6 Copyright (C) 2013-2014 neko259
7
7
8 The JavaScript code in this page is free software: you can
8 The JavaScript code in this page is free software: you can
9 redistribute it and/or modify it under the terms of the GNU
9 redistribute it and/or modify it under the terms of the GNU
10 General Public License (GNU GPL) as published by the Free Software
10 General Public License (GNU GPL) as published by the Free Software
11 Foundation, either version 3 of the License, or (at your option)
11 Foundation, either version 3 of the License, or (at your option)
12 any later version. The code is distributed WITHOUT ANY WARRANTY;
12 any later version. The code is distributed WITHOUT ANY WARRANTY;
13 without even the implied warranty of MERCHANTABILITY or FITNESS
13 without even the implied warranty of MERCHANTABILITY or FITNESS
14 FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
14 FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
15
15
16 As additional permission under GNU GPL version 3 section 7, you
16 As additional permission under GNU GPL version 3 section 7, you
17 may distribute non-source (e.g., minimized or compacted) forms of
17 may distribute non-source (e.g., minimized or compacted) forms of
18 that code without the copy of the GNU GPL normally required by
18 that code without the copy of the GNU GPL normally required by
19 section 4, provided you include this license notice and a URL
19 section 4, provided you include this license notice and a URL
20 through which recipients can access the Corresponding Source.
20 through which recipients can access the Corresponding Source.
21
21
22 @licend The above is the entire license notice
22 @licend The above is the entire license notice
23 for the JavaScript code in this page.
23 for the JavaScript code in this page.
24 */
24 */
25
25
26 var 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
35 var nodeToAnimate = node;
163 var nodeToAnimate = node;
36 for (var i = 0; i < blinkCount; i++) {
164 for (var i = 0; i < blinkCount; i++) {
37 nodeToAnimate = nodeToAnimate.fadeTo('fast', 0.5).fadeTo('fast', 1.0);
165 nodeToAnimate = nodeToAnimate.fadeTo('fast', 0.5).fadeTo('fast', 1.0);
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() {
141 return $('.thread').children('.post').length
181 return $('.thread').children('.post').length
142 }
182 }
143
183
144 function getImageCount() {
184 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');
151
195
152 replyCountField.text(getReplyCount());
196 replyCountField.text(getReplyCount());
153 imageCountField.text(getImageCount());
197 imageCountField.text(getImageCount());
154
198
155 if (lastUpdate !== '') {
199 if (lastUpdate !== '') {
156 var lastUpdateField = $('#last-update');
200 var lastUpdateField = $('#last-update');
157 lastUpdateField.text(lastUpdate);
201 lastUpdateField.text(lastUpdate);
158 blink(lastUpdateField);
202 blink(lastUpdateField);
159 }
203 }
160
204
161 blink(replyCountField);
205 blink(replyCountField);
162 blink(imageCountField);
206 blink(imageCountField);
163 }
207 }
164
208
165 /**
209 /**
166 * Update bumplimit progress bar
210 * Update bumplimit progress bar
167 */
211 */
168 function updateBumplimitProgress(postDelta) {
212 function updateBumplimitProgress(postDelta) {
169 var progressBar = $('#bumplimit_progress');
213 var progressBar = $('#bumplimit_progress');
170 if (progressBar) {
214 if (progressBar) {
171 var postsToLimitElement = $('#left_to_limit');
215 var postsToLimitElement = $('#left_to_limit');
172
216
173 var oldPostsToLimit = parseInt(postsToLimitElement.text());
217 var oldPostsToLimit = parseInt(postsToLimitElement.text());
174 var postCount = getReplyCount();
218 var postCount = getReplyCount();
175 var bumplimit = postCount - postDelta + oldPostsToLimit;
219 var bumplimit = postCount - postDelta + oldPostsToLimit;
176
220
177 var newPostsToLimit = bumplimit - postCount;
221 var newPostsToLimit = bumplimit - postCount;
178 if (newPostsToLimit <= 0) {
222 if (newPostsToLimit <= 0) {
179 $('.bar-bg').remove();
223 $('.bar-bg').remove();
180 $('.thread').children('.post').addClass('dead_post');
224 $('.thread').children('.post').addClass('dead_post');
181 } else {
225 } else {
182 postsToLimitElement.text(newPostsToLimit);
226 postsToLimitElement.text(newPostsToLimit);
183 progressBar.width((100 - postCount / bumplimit * 100.0) + '%');
227 progressBar.width((100 - postCount / bumplimit * 100.0) + '%');
184 }
228 }
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 */
192 function showNewPostsTitle(newPostCount) {
235 function showNewPostsTitle(newPostCount) {
193 if (document.hidden) {
236 if (document.hidden) {
194 if (documentOriginalTitle === '') {
237 if (documentOriginalTitle === '') {
195 documentOriginalTitle = document.title;
238 documentOriginalTitle = document.title;
196 }
239 }
197 unreadPosts = unreadPosts + newPostCount;
240 unreadPosts = unreadPosts + newPostCount;
198 document.title = '[' + unreadPosts + '] ' + documentOriginalTitle;
241 document.title = '[' + unreadPosts + '] ' + documentOriginalTitle;
199
242
200 document.addEventListener('visibilitychange', function() {
243 document.addEventListener('visibilitychange', function() {
201 if (documentOriginalTitle !== '') {
244 if (documentOriginalTitle !== '') {
202 document.title = documentOriginalTitle;
245 document.title = documentOriginalTitle;
203 documentOriginalTitle = '';
246 documentOriginalTitle = '';
204 unreadPosts = 0;
247 unreadPosts = 0;
205 }
248 }
206
249
207 document.removeEventListener('visibilitychange', null);
250 document.removeEventListener('visibilitychange', null);
208 });
251 });
209 }
252 }
210 }
253 }
211
254
212 /**
255 /**
213 * Clear all entered values in the form fields
256 * Clear all entered values in the form fields
214 */
257 */
215 function resetForm(form) {
258 function resetForm(form) {
216 form.find('input:text, input:password, input:file, select, textarea').val('');
259 form.find('input:text, input:password, input:file, select, textarea').val('');
217 form.find('input:radio, input:checkbox')
260 form.find('input:radio, input:checkbox')
218 .removeAttr('checked').removeAttr('selected');
261 .removeAttr('checked').removeAttr('selected');
219 $('.file_wrap').find('.file-thumb').remove();
262 $('.file_wrap').find('.file-thumb').remove();
220 }
263 }
221
264
222 /**
265 /**
223 * When the form is posted, this method will be run as a callback
266 * When the form is posted, this method will be run as a callback
224 */
267 */
225 function updateOnPost(response, statusText, xhr, form) {
268 function updateOnPost(response, statusText, xhr, form) {
226 var json = $.parseJSON(response);
269 var json = $.parseJSON(response);
227 var status = json.status;
270 var status = json.status;
228
271
229 showAsErrors(form, '');
272 showAsErrors(form, '');
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++) {
237 var fieldErrors = errors[i];
280 var fieldErrors = errors[i];
238
281
239 var error = fieldErrors.errors;
282 var error = fieldErrors.errors;
240
283
241 showAsErrors(form, error);
284 showAsErrors(form, error);
242 }
285 }
243 }
286 }
287
288 scrollToBottom();
244 }
289 }
245
290
246 /**
291 /**
247 * Show text in the errors row of the form.
292 * Show text in the errors row of the form.
248 * @param form
293 * @param form
249 * @param text
294 * @param text
250 */
295 */
251 function showAsErrors(form, text) {
296 function showAsErrors(form, text) {
252 form.children('.form-errors').remove();
297 form.children('.form-errors').remove();
253
298
254 if (text.length > 0) {
299 if (text.length > 0) {
255 var errorList = $('<div class="form-errors">' + text
300 var errorList = $('<div class="form-errors">' + text
256 + '<div>');
301 + '<div>');
257 errorList.appendTo(form);
302 errorList.appendTo(form);
258 }
303 }
259 }
304 }
260
305
261 /**
306 /**
262 * Run js methods that are usually run on the document, on the new post
307 * Run js methods that are usually run on the document, on the new post
263 */
308 */
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()) {
271
272 // Post form data over AJAX
317 // Post form data over AJAX
273 var threadId = $('div.thread').children('.post').first().attr('id');
318 var threadId = $('div.thread').children('.post').first().attr('id');
274
319
275 var form = $('#form');
320 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 });
@@ -1,58 +1,60 b''
1 {% load staticfiles %}
1 {% load staticfiles %}
2 {% load i18n %}
2 {% load i18n %}
3 {% load l10n %}
3 {% load l10n %}
4 {% load static from staticfiles %}
4 {% load static from staticfiles %}
5
5
6 <!DOCTYPE html>
6 <!DOCTYPE html>
7 <html>
7 <html>
8 <head>
8 <head>
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"
15 href="{% static 'favicon.png' %}">
16 href="{% static 'favicon.png' %}">
16
17
17 <meta name="viewport" content="width=device-width, initial-scale=1"/>
18 <meta name="viewport" content="width=device-width, initial-scale=1"/>
18 <meta charset="utf-8"/>
19 <meta charset="utf-8"/>
19
20
20 {% block head %}{% endblock %}
21 {% block head %}{% endblock %}
21 </head>
22 </head>
22 <body>
23 <body>
23 <script src="{% static 'js/jquery-2.0.1.min.js' %}"></script>
24 <script src="{% static 'js/jquery-2.0.1.min.js' %}"></script>
24 <script src="{% static 'js/jquery-ui-1.10.3.custom.min.js' %}"></script>
25 <script src="{% static 'js/jquery-ui-1.10.3.custom.min.js' %}"></script>
25 <script src="{% static 'js/jquery.mousewheel.js' %}"></script>
26 <script src="{% static 'js/jquery.mousewheel.js' %}"></script>
26 <script src="{% url 'js_info_dict' %}"></script>
27 <script src="{% url 'js_info_dict' %}"></script>
27
28
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>,
36 <a href="{% url 'search' %}" title="{% trans 'Search' %}">[S]</a>
38 <a href="{% url 'search' %}" title="{% trans 'Search' %}">[S]</a>
37 <a class="link" href="{% url 'settings' %}">{% trans 'Settings' %}</a>
39 <a class="link" href="{% url 'settings' %}">{% trans 'Settings' %}</a>
38 </div>
40 </div>
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
48 <div class="navigation_panel footer">
50 <div class="navigation_panel footer">
49 {% block metapanel %}{% endblock %}
51 {% block metapanel %}{% endblock %}
50 [<a href="{% url 'admin:index' %}">{% trans 'Admin' %}</a>]
52 [<a href="{% url 'admin:index' %}">{% trans 'Admin' %}</a>]
51 {% with ppd=posts_per_day|floatformat:2 %}
53 {% with ppd=posts_per_day|floatformat:2 %}
52 {% blocktrans %}Speed: {{ ppd }} posts per day{% endblocktrans %}
54 {% blocktrans %}Speed: {{ ppd }} posts per day{% endblocktrans %}
53 {% endwith %}
55 {% endwith %}
54 <a class="link" href="#top">{% trans 'Up' %}</a>
56 <a class="link" href="#top">{% trans 'Up' %}</a>
55 </div>
57 </div>
56
58
57 </body>
59 </body>
58 </html>
60 </html>
@@ -1,104 +1,102 b''
1 {% load i18n %}
1 {% load i18n %}
2 {% load board %}
2 {% load board %}
3 {% load cache %}
3 {% load cache %}
4
4
5 {% get_current_language as LANGUAGE_CODE %}
5 {% get_current_language as LANGUAGE_CODE %}
6
6
7 {% spaceless %}
8 {% cache 600 post post.id post.last_edit_time thread.archived bumpable truncated moderator LANGUAGE_CODE need_open_link %}
9 {% if thread.archived %}
7 {% if thread.archived %}
10 <div class="post archive_post" id="{{ post.id }}">
8 <div class="post archive_post" id="{{ post.id }}">
11 {% elif bumpable %}
9 {% elif bumpable %}
12 <div class="post" id="{{ post.id }}">
10 <div class="post" id="{{ post.id }}">
13 {% else %}
11 {% else %}
14 <div class="post dead_post" id="{{ post.id }}">
12 <div class="post dead_post" id="{{ post.id }}">
15 {% endif %}
13 {% 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 %}
23 >({{ post.id }}) </a>
24 <span class="title">{{ post.title }} </span>
20 <span class="title">{{ post.title }}</span>
25 <span class="pub_time">{{ post.pub_time }}</span>
21 <span class="pub_time">{{ post.pub_time }}</span>
22 {% comment %}
23 Thread death time needs to be shown only if the thread is alredy archived
24 and this is an opening post (thread death time) or a post for popup
25 (we don't see OP here so we show the death time in the post itself).
26 {% endcomment %}
26 {% if thread.archived %}
27 {% if thread.archived %}
28 {% if is_opening %}
27 — {{ thread.bump_time }}
29 — {{ thread.bump_time }}
28 {% endif %}
30 {% endif %}
31 {% endif %}
29 {% if is_opening and need_open_link %}
32 {% if is_opening and need_open_link %}
30 {% if thread.archived %}
33 {% if thread.archived %}
31 [<a class="link" href="{% url 'thread' post.id %}">{% trans "Open" %}</a>]
34 [<a class="link" href="{% url 'thread' post.id %}">{% trans "Open" %}</a>]
32 {% else %}
35 {% else %}
33 [<a class="link" href="{% url 'thread' post.id %}#form">{% trans "Reply" %}</a>]
36 [<a class="link" href="{% url 'thread' post.id %}#form">{% trans "Reply" %}</a>]
34 {% endif %}
37 {% endif %}
35 {% endif %}
38 {% endif %}
36
39
37 {% if post.global_id %}
40 {% if post.global_id %}
38 <a class="global-id" href="
41 <a class="global-id" href="
39 {% url 'post_sync_data' post.id %}"> [RAW] </a>
42 {% url 'post_sync_data' post.id %}"> [RAW] </a>
40 {% endif %}
43 {% endif %}
41
44
42 {% if moderator %}
45 {% if moderator %}
43 <span class="moderator_info">
46 <span class="moderator_info">
44 [<a href="{% url 'post_admin' post_id=post.id %}"
47 [<a href="{% url 'admin:boards_post_change' post.id %}">{% trans 'Edit' %}</a>]
45 >{% trans 'Edit' %}</a>]
48 {% if is_opening %}
46 [<a href="{% url 'delete' post_id=post.id %}"
49 [<a href="{% url 'admin:boards_thread_change' thread.id %}">{% trans 'Edit thread' %}</a>]
47 >{% trans 'Delete' %}</a>]
50 {% endif %}
48 ({{ post.poster_ip }})
49 [<a href="{% url 'ban' post_id=post.id %}?next={{ request.path }}"
50 >{% trans 'Ban IP' %}</a>]
51 </span>
51 </span>
52 {% endif %}
52 {% endif %}
53 </div>
53 </div>
54 {% comment %}
55 Post images. Currently only 1 image can be posted and shown, but post model
56 supports multiple.
57 {% endcomment %}
54 {% if post.images.exists %}
58 {% if post.images.exists %}
55 {% with post.images.all.0 as image %}
59 {% with post.images.all.0 as image %}
56 <div class="image">
60 {% autoescape off %}
57 <a
61 {{ image.get_view }}
58 class="thumb"
62 {% endautoescape %}
59 href="{{ image.image.url }}"><img
60 src="{{ image.image.url_200x150 }}"
61 alt="{{ post.id }}"
62 width="{{ image.pre_width }}"
63 height="{{ image.pre_height }}"
64 data-width="{{ image.width }}"
65 data-height="{{ image.height }}"/>
66 </a>
67 </div>
68 {% endwith %}
63 {% endwith %}
69 {% endif %}
64 {% endif %}
65 {% comment %}
66 Post message (text)
67 {% endcomment %}
70 <div class="message">
68 <div class="message">
71 {% autoescape off %}
69 {% autoescape off %}
72 {% if truncated %}
70 {% if truncated %}
73 {{ post.text.rendered|truncatewords_html:50 }}
71 {{ post.get_text|truncatewords_html:50 }}
74 {% else %}
72 {% else %}
75 {{ post.text.rendered }}
73 {{ post.get_text }}
76 {% endif %}
74 {% endif %}
77 {% endautoescape %}
75 {% endautoescape %}
78 {% if post.is_referenced %}
76 {% if post.is_referenced %}
79 <div class="refmap">
77 <div class="refmap">
80 {% autoescape off %}
78 {% autoescape off %}
81 {% trans "Replies" %}: {{ post.refmap }}
79 {% trans "Replies" %}: {{ post.refmap }}
82 {% endautoescape %}
80 {% endautoescape %}
83 </div>
81 </div>
84 {% endif %}
82 {% endif %}
85 </div>
83 </div>
86 {% endcache %}
84 {% comment %}
85 Thread metadata: counters, tags etc
86 {% endcomment %}
87 {% if is_opening %}
87 {% if is_opening %}
88 {% cache 600 post_thread thread.id thread.last_edit_time LANGUAGE_CODE need_open_link %}
89 <div class="metadata">
88 <div class="metadata">
90 {% if is_opening and need_open_link %}
89 {% if is_opening and need_open_link %}
91 {{ thread.get_reply_count }} {% trans 'messages' %},
90 {{ thread.get_reply_count }} {% trans 'messages' %},
92 {{ thread.get_images_count }} {% trans 'images' %}.
91 {{ thread.get_images_count }} {% trans 'images' %}.
93 {% endif %}
92 {% endif %}
94 <span class="tags">
93 <span class="tags">
95 {% for tag in thread.get_tags %}
94 {% for tag in thread.get_tags %}
96 <a class="tag" href="{% url 'tag' tag.name %}">
95 {% autoescape off %}
97 #{{ tag.name }}</a>{% if not forloop.last %},{% endif %}
96 {{ tag.get_view }}{% if not forloop.last %},{% endif %}
97 {% endautoescape %}
98 {% endfor %}
98 {% endfor %}
99 </span>
99 </span>
100 </div>
100 </div>
101 {% endcache %}
102 {% endif %}
101 {% endif %}
103 </div>
102 </div>
104 {% endspaceless %}
@@ -1,197 +1,202 b''
1 {% extends "boards/base.html" %}
1 {% extends "boards/base.html" %}
2
2
3 {% load i18n %}
3 {% load i18n %}
4 {% load cache %}
4 {% load cache %}
5 {% load board %}
5 {% load board %}
6 {% load static %}
6 {% load static %}
7
7
8 {% block head %}
8 {% block head %}
9 {% if tag %}
9 {% if tag %}
10 <title>{{ tag.name }} - {{ site_name }}</title>
10 <title>{{ tag.name }} - {{ site_name }}</title>
11 {% else %}
11 {% else %}
12 <title>{{ site_name }}</title>
12 <title>{{ site_name }}</title>
13 {% endif %}
13 {% endif %}
14
14
15 {% if current_page.has_previous %}
15 {% if current_page.has_previous %}
16 <link rel="prev" href="
16 <link rel="prev" href="
17 {% if tag %}
17 {% if tag %}
18 {% url "tag" tag_name=tag page=current_page.previous_page_number %}
18 {% url "tag" tag_name=tag.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 %}
22 {% url "index" page=current_page.previous_page_number %}
22 {% url "index" page=current_page.previous_page_number %}
23 {% endif %}
23 {% endif %}
24 " />
24 " />
25 {% endif %}
25 {% endif %}
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 %}
33 {% url "index" page=current_page.next_page_number %}
33 {% url "index" page=current_page.next_page_number %}
34 {% endif %}
34 {% endif %}
35 " />
35 " />
36 {% endif %}
36 {% endif %}
37
37
38 {% endblock %}
38 {% endblock %}
39
39
40 {% block content %}
40 {% block content %}
41
41
42 {% get_current_language as LANGUAGE_CODE %}
42 {% get_current_language as LANGUAGE_CODE %}
43
43
44 {% if tag %}
44 {% if tag %}
45 <div class="tag_info">
45 <div class="tag_info">
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 %}
67
72
68 {% if threads %}
73 {% if threads %}
69 {% if current_page.has_previous %}
74 {% if current_page.has_previous %}
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 %}
77 {% url "index" page=current_page.previous_page_number %}
82 {% url "index" page=current_page.previous_page_number %}
78 {% endif %}
83 {% endif %}
79 ">{% trans "Previous page" %}</a>
84 ">{% trans "Previous page" %}</a>
80 </div>
85 </div>
81 {% endif %}
86 {% endif %}
82
87
83 {% for thread in threads %}
88 {% for thread in threads %}
84 {% cache 600 thread_short thread.id thread.last_edit_time moderator LANGUAGE_CODE %}
89 {% cache 600 thread_short thread.id thread.last_edit_time moderator LANGUAGE_CODE %}
85 <div class="thread">
90 <div class="thread">
86 {% with can_bump=thread.can_bump %}
91 {% with can_bump=thread.can_bump %}
87 {% post_view thread.get_opening_post moderator is_opening=True thread=thread can_bump=can_bump truncated=True need_open_link=True %}
92 {% post_view thread.get_opening_post moderator is_opening=True thread=thread can_bump=can_bump truncated=True need_open_link=True %}
88 {% if not thread.archived %}
93 {% if not thread.archived %}
89 {% with last_replies=thread.get_last_replies %}
94 {% with last_replies=thread.get_last_replies %}
90 {% if last_replies %}
95 {% if last_replies %}
91 {% if thread.get_skipped_replies_count %}
96 {% if thread.get_skipped_replies_count %}
92 <div class="skipped_replies">
97 <div class="skipped_replies">
93 <a href="{% url 'thread' thread.get_opening_post.id %}">
98 <a href="{% url 'thread' thread.get_opening_post.id %}">
94 {% blocktrans with count=thread.get_skipped_replies_count %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %}
99 {% blocktrans with count=thread.get_skipped_replies_count %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %}
95 </a>
100 </a>
96 </div>
101 </div>
97 {% endif %}
102 {% endif %}
98 <div class="last-replies">
103 <div class="last-replies">
99 {% for post in last_replies %}
104 {% for post in last_replies %}
100 {% post_view post moderator=moderator is_opening=False thread=thread can_bump=can_bump truncated=True %}
105 {% post_view post moderator=moderator is_opening=False thread=thread can_bump=can_bump truncated=True %}
101 {% endfor %}
106 {% endfor %}
102 </div>
107 </div>
103 {% endif %}
108 {% endif %}
104 {% endwith %}
109 {% endwith %}
105 {% endif %}
110 {% endif %}
106 {% endwith %}
111 {% endwith %}
107 </div>
112 </div>
108 {% endcache %}
113 {% endcache %}
109 {% endfor %}
114 {% endfor %}
110
115
111 {% if current_page.has_next %}
116 {% if current_page.has_next %}
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 %}
119 {% url "index" page=current_page.next_page_number %}
124 {% url "index" page=current_page.next_page_number %}
120 {% endif %}
125 {% endif %}
121 ">{% trans "Next page" %}</a>
126 ">{% trans "Next page" %}</a>
122 </div>
127 </div>
123 {% endif %}
128 {% endif %}
124 {% else %}
129 {% else %}
125 <div class="post">
130 <div class="post">
126 {% trans 'No threads exist. Create the first one!' %}</div>
131 {% trans 'No threads exist. Create the first one!' %}</div>
127 {% endif %}
132 {% endif %}
128
133
129 <div class="post-form-w">
134 <div class="post-form-w">
130 <script src="{% static 'js/panel.js' %}"></script>
135 <script src="{% static 'js/panel.js' %}"></script>
131 <div class="post-form">
136 <div class="post-form">
132 <div class="form-title">{% trans "Create new thread" %}</div>
137 <div class="form-title">{% trans "Create new thread" %}</div>
133 <div class="swappable-form-full">
138 <div class="swappable-form-full">
134 <form enctype="multipart/form-data" method="post">{% csrf_token %}
139 <form enctype="multipart/form-data" method="post">{% csrf_token %}
135 {{ form.as_div }}
140 {{ form.as_div }}
136 <div class="form-submit">
141 <div class="form-submit">
137 <input type="submit" value="{% trans "Post" %}"/>
142 <input type="submit" value="{% trans "Post" %}"/>
138 </div>
143 </div>
139 </form>
144 </form>
140 </div>
145 </div>
141 <div>
146 <div>
142 {% trans 'Tags must be delimited by spaces. Text or image is required.' %}
147 {% trans 'Tags must be delimited by spaces. Text or image is required.' %}
143 </div>
148 </div>
144 <div><a href="{% url "staticpage" name="help" %}">
149 <div><a href="{% url "staticpage" name="help" %}">
145 {% trans 'Text syntax' %}</a></div>
150 {% trans 'Text syntax' %}</a></div>
146 </div>
151 </div>
147 </div>
152 </div>
148
153
149 <script src="{% static 'js/form.js' %}"></script>
154 <script src="{% static 'js/form.js' %}"></script>
150
155
151 {% endblock %}
156 {% endblock %}
152
157
153 {% block metapanel %}
158 {% block metapanel %}
154
159
155 <span class="metapanel">
160 <span class="metapanel">
156 <b><a href="{% url "authors" %}">{{ site_name }}</a> {{ version }}</b>
161 <b><a href="{% url "authors" %}">{{ site_name }}</a> {{ version }}</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 %}
164 {% url "index" page=paginator.page_range|first %}
169 {% url "index" page=paginator.page_range|first %}
165 {% endif %}
170 {% endif %}
166 ">&lt;&lt;</a>
171 ">&lt;&lt;</a>
167 [
172 [
168 {% for page in paginator.center_range %}
173 {% for page in paginator.center_range %}
169 <a
174 <a
170 {% ifequal page current_page.number %}
175 {% ifequal page current_page.number %}
171 class="current_page"
176 class="current_page"
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 %}
179 {% url "index" page=page %}
184 {% url "index" page=page %}
180 {% endif %}
185 {% endif %}
181 ">{{ page }}</a>
186 ">{{ page }}</a>
182 {% if not forloop.last %},{% endif %}
187 {% if not forloop.last %},{% endif %}
183 {% endfor %}
188 {% endfor %}
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 %}
191 {% url "index" page=paginator.page_range|last %}
196 {% url "index" page=paginator.page_range|last %}
192 {% endif %}
197 {% endif %}
193 ">&gt;&gt;</a>
198 ">&gt;&gt;</a>
194 [<a href="rss/">RSS</a>]
199 [<a href="rss/">RSS</a>]
195 </span>
200 </span>
196
201
197 {% endblock %}
202 {% endblock %}
@@ -1,15 +1,15 b''
1 {% load i18n %}
1 {% load i18n %}
2
2
3 {% if obj.images.exists %}
3 {% if obj.images.exists %}
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' %}:
11 {% for tag in obj.tags.all %}
11 {% for tag in obj.tags.all %}
12 {{ tag.name }}
12 {{ tag.name }}
13 {% endfor %}
13 {% endfor %}
14 </p>
14 </p>
15 {% endif %} No newline at end of file
15 {% endif %}
@@ -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>
@@ -1,27 +1,28 b''
1 {% extends "boards/base.html" %}
1 {% extends "boards/base.html" %}
2
2
3 {% load i18n %}
3 {% load i18n %}
4 {% load cache %}
4 {% load cache %}
5
5
6 {% block head %}
6 {% block head %}
7 <title>Neboard - {% trans "Tags" %}</title>
7 <title>Neboard - {% trans "Tags" %}</title>
8 {% endblock %}
8 {% endblock %}
9
9
10 {% block content %}
10 {% block content %}
11
11
12 {% cache 600 all_tags_list %}
12 {% cache 600 all_tags_list %}
13 <div class="post">
13 <div class="post">
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 %}
22 {% trans 'No tags found.' %}
23 {% trans 'No tags found.' %}
23 {% endif %}
24 {% endif %}
24 </div>
25 </div>
25 {% endcache %}
26 {% endcache %}
26
27
27 {% endblock %}
28 {% endblock %}
@@ -1,95 +1,96 b''
1 {% extends "boards/base.html" %}
1 {% extends "boards/base.html" %}
2
2
3 {% load i18n %}
3 {% load i18n %}
4 {% load cache %}
4 {% load cache %}
5 {% load static from staticfiles %}
5 {% load static from staticfiles %}
6 {% load board %}
6 {% load board %}
7
7
8 {% block head %}
8 {% block head %}
9 <title>{{ opening_post.get_title|striptags|truncatewords:10 }}
9 <title>{{ opening_post.get_title|striptags|truncatewords:10 }}
10 - {{ site_name }}</title>
10 - {{ site_name }}</title>
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 %}
18
17
19 <div class="image-mode-tab">
18 <div class="image-mode-tab">
20 <a class="current_mode" href="{% url 'thread' opening_post.id %}">{% trans 'Normal mode' %}</a>,
19 <a class="current_mode" href="{% url 'thread' opening_post.id %}">{% trans 'Normal mode' %}</a>,
21 <a href="{% url 'thread_mode' opening_post.id 'gallery' %}">{% trans 'Gallery mode' %}</a>
20 <a href="{% url 'thread_mode' opening_post.id 'gallery' %}">{% trans 'Gallery mode' %}</a>
22 </div>
21 </div>
23
22
24 {% if bumpable %}
23 {% if bumpable %}
25 <div class="bar-bg">
24 <div class="bar-bg">
26 <div class="bar-value" style="width:{{ bumplimit_progress }}%" id="bumplimit_progress">
25 <div class="bar-value" style="width:{{ bumplimit_progress }}%" id="bumplimit_progress">
27 </div>
26 </div>
28 <div class="bar-text">
27 <div class="bar-text">
29 <span id="left_to_limit">{{ posts_left }}</span> {% trans 'posts to bumplimit' %}
28 <span id="left_to_limit">{{ posts_left }}</span> {% trans 'posts to bumplimit' %}
30 </div>
29 </div>
31 </div>
30 </div>
32 {% endif %}
31 {% endif %}
33
32
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
48 <div class="post-form-w" id="form">
44 <div class="post-form-w" id="form">
49 <script src="{% static 'js/panel.js' %}"></script>
45 <script src="{% static 'js/panel.js' %}"></script>
50 <div class="form-title">{% trans "Reply to thread" %} #{{ opening_post.id }}</div>
46 <div class="form-title">{% trans "Reply to thread" %} #{{ opening_post.id }}</div>
51 <div class="post-form" id="compact-form">
47 <div class="post-form" id="compact-form">
52 <div class="swappable-form-full">
48 <div class="swappable-form-full">
53 <form enctype="multipart/form-data" method="post"
49 <form enctype="multipart/form-data" method="post"
54 >{% csrf_token %}
50 >{% 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>
61 </div>
57 </div>
62 <a onclick="swapForm(); return false;" href="#">
58 <a onclick="swapForm(); return false;" href="#">
63 {% trans 'Switch mode' %}
59 {% trans 'Switch mode' %}
64 </a>
60 </a>
65 <div><a href="{% url "staticpage" name="help" %}">
61 <div><a href="{% url "staticpage" name="help" %}">
66 {% trans 'Text syntax' %}</a></div>
62 {% trans 'Text syntax' %}</a></div>
67 </div>
63 </div>
68 </div>
64 </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>
91 [<a href="rss/">RSS</a>]
92 [<a href="rss/">RSS</a>]
92 {% endcache %}
93 {% endcache %}
93 </span>
94 </span>
94
95
95 {% endblock %}
96 {% endblock %}
@@ -1,38 +1,38 b''
1 {% extends 'boards/base.html' %}
1 {% extends 'boards/base.html' %}
2
2
3 {% load board %}
3 {% load board %}
4 {% load i18n %}
4 {% load i18n %}
5
5
6 {% block content %}
6 {% block content %}
7 <div class="post-form-w">
7 <div class="post-form-w">
8 <div class="post-form">
8 <div class="post-form">
9 <h3>{% trans 'Search' %}</h3>
9 <h3>{% trans 'Search' %}</h3>
10 <form method="get" action="">
10 <form method="get" action="">
11 {{ form.as_div }}
11 {{ form.as_div }}
12 <div class="form-submit">
12 <div class="form-submit">
13 <input type="submit" value="{% trans 'Search' %}">
13 <input type="submit" value="{% trans 'Search' %}">
14 </div>
14 </div>
15 </form>
15 </form>
16 </div>
16 </div>
17 </div>
17 </div>
18
18
19 {% if page %}
19 {% if page %}
20 {% if page.has_previous %}
20 {% if page.has_previous %}
21 <div class="page_link">
21 <div class="page_link">
22 <a href="?query={{ query }}&amp;page={{ page.previous_page_number }}">{% trans "Previous page" %}
22 <a href="?query={{ query }}&amp;page={{ page.previous_page_number }}">{% trans "Previous page" %}
23 </a>
23 </a>
24 </div>
24 </div>
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 %}
32 <div class="page_link">
32 <div class="page_link">
33 <a href="?query={{ query }}&amp;page={{ page.next_page_number }}">{% trans "Next page" %}
33 <a href="?query={{ query }}&amp;page={{ page.next_page_number }}">{% trans "Next page" %}
34 </a>
34 </a>
35 </div>
35 </div>
36 {% endif %}
36 {% endif %}
37 {% endif %}
37 {% endif %}
38 {% endblock %} No newline at end of file
38 {% endblock %}
@@ -1,55 +1,56 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
8 TEST_TAG = 'test_tag'
8 TEST_TAG = 'test_tag'
9
9
10 PAGE_404 = 'boards/404.html'
10 PAGE_404 = 'boards/404.html'
11
11
12 TEST_TEXT = 'test text'
12 TEST_TEXT = 'test text'
13
13
14 NEW_THREAD_PAGE = '/'
14 NEW_THREAD_PAGE = '/'
15 THREAD_PAGE_ONE = '/thread/1/'
15 THREAD_PAGE_ONE = '/thread/1/'
16 HTTP_CODE_REDIRECT = 302
16 HTTP_CODE_REDIRECT = 302
17
17
18
18
19 class FormTest(TestCase):
19 class FormTest(TestCase):
20 def test_post_validation(self):
20 def test_post_validation(self):
21 client = Client()
21 client = Client()
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,
28 'tags': valid_tags})
29 'tags': valid_tags})
29 self.assertEqual(response.status_code, HTTP_CODE_REDIRECT,
30 self.assertEqual(response.status_code, HTTP_CODE_REDIRECT,
30 msg='Posting new message failed: got code ' +
31 msg='Posting new message failed: got code ' +
31 str(response.status_code))
32 str(response.status_code))
32
33
33 self.assertEqual(1, Post.objects.count(),
34 self.assertEqual(1, Post.objects.count(),
34 msg='No posts were created')
35 msg='No posts were created')
35
36
36 client.post(NEW_THREAD_PAGE, {'text': TEST_TEXT,
37 client.post(NEW_THREAD_PAGE, {'text': TEST_TEXT,
37 'tags': invalid_tags})
38 'tags': invalid_tags})
38 self.assertEqual(1, Post.objects.count(), msg='The validation passed '
39 self.assertEqual(1, Post.objects.count(), msg='The validation passed '
39 'where it should fail')
40 'where it should fail')
40
41
41 # Change posting delay so we don't have to wait for 30 seconds or more
42 # Change posting delay so we don't have to wait for 30 seconds or more
42 old_posting_delay = neboard.settings.POSTING_DELAY
43 old_posting_delay = neboard.settings.POSTING_DELAY
43 # Wait fot the posting delay or we won't be able to post
44 # Wait fot the posting delay or we won't be able to post
44 neboard.settings.POSTING_DELAY = 1
45 neboard.settings.POSTING_DELAY = 1
45 time.sleep(neboard.settings.POSTING_DELAY + 1)
46 time.sleep(neboard.settings.POSTING_DELAY + 1)
46 response = client.post(THREAD_PAGE_ONE, {'text': TEST_TEXT,
47 response = client.post(THREAD_PAGE_ONE, {'text': TEST_TEXT,
47 'tags': valid_tags})
48 'tags': valid_tags})
48 self.assertEqual(HTTP_CODE_REDIRECT, response.status_code,
49 self.assertEqual(HTTP_CODE_REDIRECT, response.status_code,
49 msg='Posting new message failed: got code ' +
50 msg='Posting new message failed: got code ' +
50 str(response.status_code))
51 str(response.status_code))
51 # Restore posting delay
52 # Restore posting delay
52 settings.POSTING_DELAY = old_posting_delay
53 settings.POSTING_DELAY = old_posting_delay
53
54
54 self.assertEqual(2, Post.objects.count(),
55 self.assertEqual(2, Post.objects.count(),
55 msg='No posts were created')
56 msg='No posts were created')
@@ -1,56 +1,52 b''
1 from django.test import TestCase, Client
1 from django.test import TestCase, Client
2 from boards.models import Tag, Post
2 from boards.models import Tag, Post
3
3
4 TEST_TEXT = 'test'
4 TEST_TEXT = 'test'
5
5
6 NEW_THREAD_PAGE = '/'
6 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
19 def test_404(self):
17 def test_404(self):
20 """Test receiving error 404 when opening a non-existent page"""
18 """Test receiving error 404 when opening a non-existent page"""
21
19
22 tag_name = 'test_tag'
20 tag_name = 'test_tag'
23 tag = Tag.objects.create(name=tag_name)
21 tag = Tag.objects.create(name=tag_name)
24 client = Client()
22 client = Client()
25
23
26 Post.objects.create_post('title', TEST_TEXT, tags=[tag])
24 Post.objects.create_post('title', TEST_TEXT, tags=[tag])
27
25
28 existing_post_id = Post.objects.all()[0].id
26 existing_post_id = Post.objects.all()[0].id
29 response_existing = client.get(THREAD_PAGE + str(existing_post_id) +
27 response_existing = client.get(THREAD_PAGE + str(existing_post_id) +
30 '/')
28 '/')
31 self.assertEqual(HTTP_CODE_OK, response_existing.status_code,
29 self.assertEqual(HTTP_CODE_OK, response_existing.status_code,
32 'Cannot open existing thread')
30 'Cannot open existing thread')
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 + '/')
40 self.assertEqual(HTTP_CODE_OK,
38 self.assertEqual(HTTP_CODE_OK,
41 response_existing.status_code,
39 response_existing.status_code,
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,
50 thread=Post.objects.all()[0]
47 thread=Post.objects.all()[0]
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')
@@ -1,142 +1,163 b''
1 from django.core.paginator import Paginator
1 from django.core.paginator import Paginator
2 from django.test import TestCase
2 from django.test import TestCase
3 from boards import settings
3 from boards import settings
4 from boards.models import Tag, Post, Thread, KeyPair
4 from boards.models import Tag, Post, Thread, KeyPair
5
5
6
6
7 class PostTests(TestCase):
7 class PostTests(TestCase):
8
8
9 def _create_post(self):
9 def _create_post(self):
10 tag = Tag.objects.create(name='test_tag')
10 tag = Tag.objects.create(name='test_tag')
11 return Post.objects.create_post(title='title', text='text',
11 return Post.objects.create_post(title='title', text='text',
12 tags=[tag])
12 tags=[tag])
13
13
14 def test_post_add(self):
14 def test_post_add(self):
15 """Test adding post"""
15 """Test adding post"""
16
16
17 post = self._create_post()
17 post = self._create_post()
18
18
19 self.assertIsNotNone(post, 'No post was created.')
19 self.assertIsNotNone(post, 'No post was created.')
20 self.assertEqual('test_tag', post.get_thread().tags.all()[0].name,
20 self.assertEqual('test_tag', post.get_thread().tags.all()[0].name,
21 'No tags were added to the post.')
21 'No tags were added to the post.')
22
22
23 def test_delete_post(self):
23 def test_delete_post(self):
24 """Test post deletion"""
24 """Test post deletion"""
25
25
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
33 def test_delete_thread(self):
33 def test_delete_thread(self):
34 """Test thread deletion"""
34 """Test thread deletion"""
35
35
36 opening_post = self._create_post()
36 opening_post = self._create_post()
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"""
46
49
47 op = self._create_post()
50 op = self._create_post()
48 post = Post.objects.create_post("", "", thread=op.get_thread())
51 post = Post.objects.create_post("", "", thread=op.get_thread())
49
52
50 self.assertIsNotNone(post, 'Reply to thread wasn\'t created')
53 self.assertIsNotNone(post, 'Reply to thread wasn\'t created')
51 self.assertEqual(op.get_thread().last_edit_time, post.pub_time,
54 self.assertEqual(op.get_thread().last_edit_time, post.pub_time,
52 'Post\'s create time doesn\'t match thread last edit'
55 'Post\'s create time doesn\'t match thread last edit'
53 ' time')
56 ' time')
54
57
55 def test_delete_posts_by_ip(self):
58 def test_delete_posts_by_ip(self):
56 """Test deleting posts with the given ip"""
59 """Test deleting posts with the given ip"""
57
60
58 post = self._create_post()
61 post = self._create_post()
59 post_id = post.id
62 post_id = post.id
60
63
61 Post.objects.delete_posts_by_ip('0.0.0.0')
64 Post.objects.delete_posts_by_ip('0.0.0.0')
62
65
63 self.assertFalse(Post.objects.filter(id=post_id).exists())
66 self.assertFalse(Post.objects.filter(id=post_id).exists())
64
67
65 def test_get_thread(self):
68 def test_get_thread(self):
66 """Test getting all posts of a thread"""
69 """Test getting all posts of a thread"""
67
70
68 opening_post = self._create_post()
71 opening_post = self._create_post()
69
72
70 for i in range(2):
73 for i in range(2):
71 Post.objects.create_post('title', 'text',
74 Post.objects.create_post('title', 'text',
72 thread=opening_post.get_thread())
75 thread=opening_post.get_thread())
73
76
74 thread = opening_post.get_thread()
77 thread = opening_post.get_thread()
75
78
76 self.assertEqual(3, thread.replies.count())
79 self.assertEqual(3, thread.replies.count())
77
80
78 def test_create_post_with_tag(self):
81 def test_create_post_with_tag(self):
79 """Test adding tag to post"""
82 """Test adding tag to post"""
80
83
81 tag = Tag.objects.create(name='test_tag')
84 tag = Tag.objects.create(name='test_tag')
82 post = Post.objects.create_post(title='title', text='text', tags=[tag])
85 post = Post.objects.create_post(title='title', text='text', tags=[tag])
83
86
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"""
91
93
92 for i in range(settings.MAX_THREAD_COUNT + 1):
94 for i in range(settings.MAX_THREAD_COUNT + 1):
93 self._create_post()
95 self._create_post()
94
96
95 self.assertEqual(settings.MAX_THREAD_COUNT,
97 self.assertEqual(settings.MAX_THREAD_COUNT,
96 len(Thread.objects.filter(archived=False)))
98 len(Thread.objects.filter(archived=False)))
97
99
98 def test_pages(self):
100 def test_pages(self):
99 """Test that the thread list is properly split into pages"""
101 """Test that the thread list is properly split into pages"""
100
102
101 for i in range(settings.MAX_THREAD_COUNT):
103 for i in range(settings.MAX_THREAD_COUNT):
102 self._create_post()
104 self._create_post()
103
105
104 all_threads = Thread.objects.filter(archived=False)
106 all_threads = Thread.objects.filter(archived=False)
105
107
106 paginator = Paginator(Thread.objects.filter(archived=False),
108 paginator = Paginator(Thread.objects.filter(archived=False),
107 settings.THREADS_PER_PAGE)
109 settings.THREADS_PER_PAGE)
108 posts_in_second_page = paginator.page(2).object_list
110 posts_in_second_page = paginator.page(2).object_list
109 first_post = posts_in_second_page[0]
111 first_post = posts_in_second_page[0]
110
112
111 self.assertEqual(all_threads[settings.THREADS_PER_PAGE].id,
113 self.assertEqual(all_threads[settings.THREADS_PER_PAGE].id,
112 first_post.id)
114 first_post.id)
113
115
114 def test_reflinks(self):
116 def test_reflinks(self):
115 """
117 """
116 Tests that reflinks are parsed within post and connecting replies
118 Tests that reflinks are parsed within post and connecting replies
117 to the replied posts.
119 to the replied posts.
118
120
119 Local reflink example: [post]123[/post]
121 Local reflink example: [post]123[/post]
120 Global reflink example: [post]key_type::key::123[/post]
122 Global reflink example: [post]key_type::key::123[/post]
121 """
123 """
122
124
123 key = KeyPair.objects.generate_key(primary=True)
125 key = KeyPair.objects.generate_key(primary=True)
124
126
125 tag = Tag.objects.create(name='test_tag')
127 tag = Tag.objects.create(name='test_tag')
126
128
127 post = Post.objects.create_post(title='', text='', tags=[tag])
129 post = Post.objects.create_post(title='', text='', tags=[tag])
128 post_local_reflink = Post.objects.create_post(title='',
130 post_local_reflink = Post.objects.create_post(title='',
129 text='[post]%d[/post]' % post.id, thread=post.get_thread())
131 text='[post]%d[/post]' % post.id, thread=post.get_thread())
130
132
131 self.assertTrue(post_local_reflink in post.referenced_posts.all(),
133 self.assertTrue(post_local_reflink in post.referenced_posts.all(),
132 'Local reflink not connecting posts.')
134 'Local reflink not connecting posts.')
133
135
134 post_global_reflink = Post.objects.create_post(title='',
136 post_global_reflink = Post.objects.create_post(title='',
135 text='[post]%s::%s::%d[/post]' % (
137 text='[post]%s::%s::%d[/post]' % (
136 post.global_id.key_type, post.global_id.key, post.id),
138 post.global_id.key_type, post.global_id.key, post.id),
137 thread=post.get_thread())
139 thread=post.get_thread())
138
140
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.')
@@ -1,38 +1,46 b''
1 import logging
1 import logging
2 from django.core.urlresolvers import reverse, NoReverseMatch
2 from django.core.urlresolvers import reverse, NoReverseMatch
3 from django.test import TestCase, Client
3 from django.test import TestCase, Client
4 from boards import urls
4 from boards import urls
5
5
6
6
7 logger = logging.getLogger(__name__)
7 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
14 def test_all_views(self):
18 def test_all_views(self):
15 """
19 """
16 Try opening all views defined in ulrs.py that don't need additional
20 Try opening all views defined in ulrs.py that don't need additional
17 parameters
21 parameters
18 """
22 """
19
23
20 client = Client()
24 client = Client()
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
34 except Exception as e:
42 except Exception as e:
35 self.fail('Got exception %s at %s view' % (e, view_name))
43 self.fail('Got exception %s at %s view' % (e, view_name))
36 except AttributeError:
44 except AttributeError:
37 # This is normal, some views do not have names
45 # This is normal, some views do not have names
38 pass
46 pass
@@ -1,219 +1,215 b''
1 # -*- encoding: utf-8 -*-
1 # -*- encoding: utf-8 -*-
2 """
2 """
3 django-thumbs by Antonio Melé
3 django-thumbs by Antonio Melé
4 http://django.es
4 http://django.es
5 """
5 """
6 from django.core.files.images import ImageFile
6 from django.core.files.images import ImageFile
7 from django.db.models import ImageField
7 from django.db.models import ImageField
8 from django.db.models.fields.files import ImageFieldFile
8 from django.db.models.fields.files import ImageFieldFile
9 from PIL import Image
9 from PIL import Image
10 from django.core.files.base import ContentFile
10 from django.core.files.base import ContentFile
11 import io
11 import io
12
12
13
13
14 def generate_thumb(img, thumb_size, format):
14 def generate_thumb(img, thumb_size, format):
15 """
15 """
16 Generates a thumbnail image and returns a ContentFile object with the thumbnail
16 Generates a thumbnail image and returns a ContentFile object with the thumbnail
17
17
18 Parameters:
18 Parameters:
19 ===========
19 ===========
20 img File object
20 img File object
21
21
22 thumb_size desired thumbnail size, ie: (200,120)
22 thumb_size desired thumbnail size, ie: (200,120)
23
23
24 format format of the original image ('jpeg','gif','png',...)
24 format format of the original image ('jpeg','gif','png',...)
25 (this format will be used for the generated thumbnail, too)
25 (this format will be used for the generated thumbnail, too)
26 """
26 """
27
27
28 img.seek(0) # see http://code.djangoproject.com/ticket/8222 for details
28 img.seek(0) # see http://code.djangoproject.com/ticket/8222 for details
29 image = Image.open(img)
29 image = Image.open(img)
30
30
31 # get size
31 # get size
32 thumb_w, thumb_h = thumb_size
32 thumb_w, thumb_h = thumb_size
33 # If you want to generate a square thumbnail
33 # If you want to generate a square thumbnail
34 if thumb_w == thumb_h:
34 if thumb_w == thumb_h:
35 # quad
35 # quad
36 xsize, ysize = image.size
36 xsize, ysize = image.size
37 # get minimum size
37 # get minimum size
38 minsize = min(xsize, ysize)
38 minsize = min(xsize, ysize)
39 # largest square possible in the image
39 # largest square possible in the image
40 xnewsize = (xsize - minsize) / 2
40 xnewsize = (xsize - minsize) / 2
41 ynewsize = (ysize - minsize) / 2
41 ynewsize = (ysize - minsize) / 2
42 # crop it
42 # crop it
43 image2 = image.crop(
43 image2 = image.crop(
44 (xnewsize, ynewsize, xsize - xnewsize, ysize - ynewsize))
44 (xnewsize, ynewsize, xsize - xnewsize, ysize - ynewsize))
45 # load is necessary after crop
45 # load is necessary after crop
46 image2.load()
46 image2.load()
47 # thumbnail of the cropped image (with ANTIALIAS to make it look better)
47 # thumbnail of the cropped image (with ANTIALIAS to make it look better)
48 image2.thumbnail(thumb_size, Image.ANTIALIAS)
48 image2.thumbnail(thumb_size, Image.ANTIALIAS)
49 else:
49 else:
50 # not quad
50 # not quad
51 image2 = image
51 image2 = image
52 image2.thumbnail(thumb_size, Image.ANTIALIAS)
52 image2.thumbnail(thumb_size, Image.ANTIALIAS)
53
53
54 output = io.BytesIO()
54 output = io.BytesIO()
55 # PNG and GIF are the same, JPG is JPEG
55 # PNG and GIF are the same, JPG is JPEG
56 if format.upper() == 'JPG':
56 if format.upper() == 'JPG':
57 format = 'JPEG'
57 format = 'JPEG'
58
58
59 image2.save(output, format)
59 image2.save(output, format)
60 return ContentFile(output.getvalue())
60 return ContentFile(output.getvalue())
61
61
62
62
63 class ImageWithThumbsFieldFile(ImageFieldFile):
63 class ImageWithThumbsFieldFile(ImageFieldFile):
64 """
64 """
65 See ImageWithThumbsField for usage example
65 See ImageWithThumbsField for usage example
66 """
66 """
67
67
68 def __init__(self, *args, **kwargs):
68 def __init__(self, *args, **kwargs):
69 super(ImageWithThumbsFieldFile, self).__init__(*args, **kwargs)
69 super(ImageWithThumbsFieldFile, self).__init__(*args, **kwargs)
70 self.sizes = self.field.sizes
70 self.sizes = self.field.sizes
71
71
72 if self.sizes:
72 if self.sizes:
73 def get_size(self, size):
73 def get_size(self, size):
74 if not self:
74 if not self:
75 return ''
75 return ''
76 else:
76 else:
77 split = self.url.rsplit('.', 1)
77 split = self.url.rsplit('.', 1)
78 thumb_url = '%s.%sx%s.%s' % (split[0], w, h, split[1])
78 thumb_url = '%s.%sx%s.%s' % (split[0], w, h, split[1])
79 return thumb_url
79 return thumb_url
80
80
81 for size in self.sizes:
81 for size in self.sizes:
82 (w, h) = size
82 (w, h) = size
83 setattr(self, 'url_%sx%s' % (w, h), get_size(self, size))
83 setattr(self, 'url_%sx%s' % (w, h), get_size(self, size))
84
84
85 def save(self, name, content, save=True):
85 def save(self, name, content, save=True):
86 super(ImageWithThumbsFieldFile, self).save(name, content, save)
86 super(ImageWithThumbsFieldFile, self).save(name, content, save)
87
87
88 if self.sizes:
88 if self.sizes:
89 for size in self.sizes:
89 for size in self.sizes:
90 (w, h) = size
90 (w, h) = size
91 split = self.name.rsplit('.', 1)
91 split = self.name.rsplit('.', 1)
92 thumb_name = '%s.%sx%s.%s' % (split[0], w, h, split[1])
92 thumb_name = '%s.%sx%s.%s' % (split[0], w, h, split[1])
93
93
94 # you can use another thumbnailing function if you like
94 # you can use another thumbnailing function if you like
95 thumb_content = generate_thumb(content, size, split[1])
95 thumb_content = generate_thumb(content, size, split[1])
96
96
97 thumb_name_ = self.storage.save(thumb_name, thumb_content)
97 thumb_name_ = self.storage.save(thumb_name, thumb_content)
98
98
99 if not thumb_name == thumb_name_:
99 if not thumb_name == thumb_name_:
100 raise ValueError(
100 raise ValueError(
101 'There is already a file named %s' % thumb_name)
101 'There is already a file named %s' % thumb_name)
102
102
103 def delete(self, save=True):
103 def delete(self, save=True):
104 name = self.name
104 name = self.name
105 super(ImageWithThumbsFieldFile, self).delete(save)
105 super(ImageWithThumbsFieldFile, self).delete(save)
106 if self.sizes:
106 if self.sizes:
107 for size in self.sizes:
107 for size in self.sizes:
108 (w, h) = size
108 (w, h) = size
109 split = name.rsplit('.', 1)
109 split = name.rsplit('.', 1)
110 thumb_name = '%s.%sx%s.%s' % (split[0], w, h, split[1])
110 thumb_name = '%s.%sx%s.%s' % (split[0], w, h, split[1])
111 try:
111 try:
112 self.storage.delete(thumb_name)
112 self.storage.delete(thumb_name)
113 except:
113 except:
114 pass
114 pass
115
115
116
116
117 class ImageWithThumbsField(ImageField):
117 class ImageWithThumbsField(ImageField):
118 attr_class = ImageWithThumbsFieldFile
118 attr_class = ImageWithThumbsFieldFile
119 """
119 """
120 Usage example:
120 Usage example:
121 ==============
121 ==============
122 photo = ImageWithThumbsField(upload_to='images', sizes=((125,125),(300,200),)
122 photo = ImageWithThumbsField(upload_to='images', sizes=((125,125),(300,200),)
123
123
124 To retrieve image URL, exactly the same way as with ImageField:
124 To retrieve image URL, exactly the same way as with ImageField:
125 my_object.photo.url
125 my_object.photo.url
126 To retrieve thumbnails URL's just add the size to it:
126 To retrieve thumbnails URL's just add the size to it:
127 my_object.photo.url_125x125
127 my_object.photo.url_125x125
128 my_object.photo.url_300x200
128 my_object.photo.url_300x200
129
129
130 Note: The 'sizes' attribute is not required. If you don't provide it,
130 Note: The 'sizes' attribute is not required. If you don't provide it,
131 ImageWithThumbsField will act as a normal ImageField
131 ImageWithThumbsField will act as a normal ImageField
132
132
133 How it works:
133 How it works:
134 =============
134 =============
135 For each size in the 'sizes' atribute of the field it generates a
135 For each size in the 'sizes' atribute of the field it generates a
136 thumbnail with that size and stores it following this format:
136 thumbnail with that size and stores it following this format:
137
137
138 available_filename.[width]x[height].extension
138 available_filename.[width]x[height].extension
139
139
140 Where 'available_filename' is the available filename returned by the storage
140 Where 'available_filename' is the available filename returned by the storage
141 backend for saving the original file.
141 backend for saving the original file.
142
142
143 Following the usage example above: For storing a file called "photo.jpg" it saves:
143 Following the usage example above: For storing a file called "photo.jpg" it saves:
144 photo.jpg (original file)
144 photo.jpg (original file)
145 photo.125x125.jpg (first thumbnail)
145 photo.125x125.jpg (first thumbnail)
146 photo.300x200.jpg (second thumbnail)
146 photo.300x200.jpg (second thumbnail)
147
147
148 With the default storage backend if photo.jpg already exists it will use these filenames:
148 With the default storage backend if photo.jpg already exists it will use these filenames:
149 photo_.jpg
149 photo_.jpg
150 photo_.125x125.jpg
150 photo_.125x125.jpg
151 photo_.300x200.jpg
151 photo_.300x200.jpg
152
152
153 Note: django-thumbs assumes that if filename "any_filename.jpg" is available
153 Note: django-thumbs assumes that if filename "any_filename.jpg" is available
154 filenames with this format "any_filename.[widht]x[height].jpg" will be available, too.
154 filenames with this format "any_filename.[widht]x[height].jpg" will be available, too.
155
155
156 To do:
156 To do:
157 ======
157 ======
158 Add method to regenerate thubmnails
158 Add method to regenerate thubmnails
159
159
160
160
161 """
161 """
162
162
163 preview_width_field = None
163 preview_width_field = None
164 preview_height_field = None
164 preview_height_field = None
165
165
166 def __init__(self, verbose_name=None, name=None, width_field=None,
166 def __init__(self, verbose_name=None, name=None, width_field=None,
167 height_field=None, sizes=None,
167 height_field=None, sizes=None,
168 preview_width_field=None, preview_height_field=None,
168 preview_width_field=None, preview_height_field=None,
169 **kwargs):
169 **kwargs):
170 self.verbose_name = verbose_name
170 self.verbose_name = verbose_name
171 self.name = name
171 self.name = name
172 self.width_field = width_field
172 self.width_field = width_field
173 self.height_field = height_field
173 self.height_field = height_field
174 self.sizes = sizes
174 self.sizes = sizes
175 super(ImageField, self).__init__(**kwargs)
175 super(ImageField, self).__init__(**kwargs)
176
176
177 if sizes is not None and len(sizes) == 1:
177 if sizes is not None and len(sizes) == 1:
178 self.preview_width_field = preview_width_field
178 self.preview_width_field = preview_width_field
179 self.preview_height_field = preview_height_field
179 self.preview_height_field = preview_height_field
180
180
181 def update_dimension_fields(self, instance, force=False, *args, **kwargs):
181 def update_dimension_fields(self, instance, force=False, *args, **kwargs):
182 """
182 """
183 Update original image dimension fields and thumb dimension fields
183 Update original image dimension fields and thumb dimension fields
184 (only if 1 thumb size is defined)
184 (only if 1 thumb size is defined)
185 """
185 """
186
186
187 super(ImageWithThumbsField, self).update_dimension_fields(instance,
187 super(ImageWithThumbsField, self).update_dimension_fields(instance,
188 force, *args,
188 force, *args,
189 **kwargs)
189 **kwargs)
190 thumb_width_field = self.preview_width_field
190 thumb_width_field = self.preview_width_field
191 thumb_height_field = self.preview_height_field
191 thumb_height_field = self.preview_height_field
192
192
193 if thumb_width_field is None or thumb_height_field is None \
193 if thumb_width_field is None or thumb_height_field is None \
194 or len(self.sizes) != 1:
194 or len(self.sizes) != 1:
195 return
195 return
196
196
197 original_width = getattr(instance, self.width_field)
197 original_width = getattr(instance, self.width_field)
198 original_height = getattr(instance, self.height_field)
198 original_height = getattr(instance, self.height_field)
199
199
200 if original_width > 0 and original_height > 0:
200 if original_width > 0 and original_height > 0:
201 thumb_width, thumb_height = self.sizes[0]
201 thumb_width, thumb_height = self.sizes[0]
202
202
203 w_scale = float(thumb_width) / original_width
203 w_scale = float(thumb_width) / original_width
204 h_scale = float(thumb_height) / original_height
204 h_scale = float(thumb_height) / original_height
205 scale_ratio = min(w_scale, h_scale)
205 scale_ratio = min(w_scale, h_scale)
206
206
207 if scale_ratio >= 1:
207 if scale_ratio >= 1:
208 thumb_width_ratio = original_width
208 thumb_width_ratio = original_width
209 thumb_height_ratio = original_height
209 thumb_height_ratio = original_height
210 else:
210 else:
211 thumb_width_ratio = int(original_width * scale_ratio)
211 thumb_width_ratio = int(original_width * scale_ratio)
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"])
@@ -1,87 +1,79 b''
1 from django.conf.urls import patterns, url, include
1 from django.conf.urls import patterns, url, include
2 from django.contrib import admin
2 from django.contrib import admin
3 from boards import views
3 from boards import views
4 from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed
4 from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed
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
16 js_info_dict = {
14 js_info_dict = {
17 'packages': ('boards',),
15 'packages': ('boards',),
18 }
16 }
19
17
20 urlpatterns = patterns('',
18 urlpatterns = patterns('',
21 # /boards/
19 # /boards/
22 url(r'^$', all_threads.AllThreadsView.as_view(), name='index'),
20 url(r'^$', all_threads.AllThreadsView.as_view(), name='index'),
23 # /boards/page/
21 # /boards/page/
24 url(r'^page/(?P<page>\w+)/$', all_threads.AllThreadsView.as_view(),
22 url(r'^page/(?P<page>\w+)/$', all_threads.AllThreadsView.as_view(),
25 name='index'),
23 name='index'),
26
24
27 # /boards/tag/tag_name/
25 # /boards/tag/tag_name/
28 url(r'^tag/(?P<tag_name>\w+)/$', tag_threads.TagView.as_view(),
26 url(r'^tag/(?P<tag_name>\w+)/$', tag_threads.TagView.as_view(),
29 name='tag'),
27 name='tag'),
30 # /boards/tag/tag_id/page/
28 # /boards/tag/tag_id/page/
31 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/$',
29 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/$',
32 tag_threads.TagView.as_view(), name='tag'),
30 tag_threads.TagView.as_view(), name='tag'),
33
31
34 # /boards/thread/
32 # /boards/thread/
35 url(r'^thread/(?P<post_id>\w+)/$', views.thread.ThreadView.as_view(),
33 url(r'^thread/(?P<post_id>\w+)/$', views.thread.ThreadView.as_view(),
36 name='thread'),
34 name='thread'),
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'),
52 url(r'^staticpage/(?P<name>\w+)/$', StaticPageView.as_view(),
44 url(r'^staticpage/(?P<name>\w+)/$', StaticPageView.as_view(),
53 name='staticpage'),
45 name='staticpage'),
54
46
55 # RSS feeds
47 # RSS feeds
56 url(r'^rss/$', AllThreadsFeed()),
48 url(r'^rss/$', AllThreadsFeed()),
57 url(r'^page/(?P<page>\w+)/rss/$', AllThreadsFeed()),
49 url(r'^page/(?P<page>\w+)/rss/$', AllThreadsFeed()),
58 url(r'^tag/(?P<tag_name>\w+)/rss/$', TagThreadsFeed()),
50 url(r'^tag/(?P<tag_name>\w+)/rss/$', TagThreadsFeed()),
59 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/rss/$', TagThreadsFeed()),
51 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/rss/$', TagThreadsFeed()),
60 url(r'^thread/(?P<post_id>\w+)/rss/$', ThreadPostsFeed()),
52 url(r'^thread/(?P<post_id>\w+)/rss/$', ThreadPostsFeed()),
61
53
62 # i18n
54 # i18n
63 url(r'^jsi18n/$', 'boards.views.cached_js_catalog', js_info_dict,
55 url(r'^jsi18n/$', 'boards.views.cached_js_catalog', js_info_dict,
64 name='js_info_dict'),
56 name='js_info_dict'),
65
57
66 # API
58 # API
67 url(r'^api/post/(?P<post_id>\w+)/$', api.get_post, name="get_post"),
59 url(r'^api/post/(?P<post_id>\w+)/$', api.get_post, name="get_post"),
68 url(r'^api/diff_thread/(?P<thread_id>\w+)/(?P<last_update_time>\w+)/$',
60 url(r'^api/diff_thread/(?P<thread_id>\w+)/(?P<last_update_time>\w+)/$',
69 api.api_get_threaddiff, name="get_thread_diff"),
61 api.api_get_threaddiff, name="get_thread_diff"),
70 url(r'^api/threads/(?P<count>\w+)/$', api.api_get_threads,
62 url(r'^api/threads/(?P<count>\w+)/$', api.api_get_threads,
71 name='get_threads'),
63 name='get_threads'),
72 url(r'^api/tags/$', api.api_get_tags, name='get_tags'),
64 url(r'^api/tags/$', api.api_get_tags, name='get_tags'),
73 url(r'^api/thread/(?P<opening_post_id>\w+)/$', api.api_get_thread_posts,
65 url(r'^api/thread/(?P<opening_post_id>\w+)/$', api.api_get_thread_posts,
74 name='get_thread'),
66 name='get_thread'),
75 url(r'^api/add_post/(?P<opening_post_id>\w+)/$', api.api_add_post,
67 url(r'^api/add_post/(?P<opening_post_id>\w+)/$', api.api_add_post,
76 name='add_post'),
68 name='add_post'),
77
69
78 # Search
70 # Search
79 url(r'^search/$', BoardSearchView.as_view(), name='search'),
71 url(r'^search/$', BoardSearchView.as_view(), name='search'),
80
72
81 # Post preview
73 # Post preview
82 url(r'^preview/$', PostPreviewView.as_view(), name='preview'),
74 url(r'^preview/$', PostPreviewView.as_view(), name='preview'),
83
75
84 url(r'^post_xml/(?P<post_id>\d+)$', get_post_sync_data,
76 url(r'^post_xml/(?P<post_id>\d+)$', get_post_sync_data,
85 name='post_sync_data'),
77 name='post_sync_data'),
86
78
87 )
79 )
@@ -1,78 +1,43 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
9 from neboard import settings
9 from neboard import settings
10
10
11
11
12 KEY_CAPTCHA_FAILS = 'key_captcha_fails'
12 KEY_CAPTCHA_FAILS = 'key_captcha_fails'
13 KEY_CAPTCHA_DELAY_TIME = 'key_captcha_delay_time'
13 KEY_CAPTCHA_DELAY_TIME = 'key_captcha_delay_time'
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:
69 ip = x_forwarded_for.split(',')[-1].strip()
20 ip = x_forwarded_for.split(',')[-1].strip()
70 else:
21 else:
71 ip = request.META.get('REMOTE_ADDR')
22 ip = request.META.get('REMOTE_ADDR')
72 return ip
23 return ip
73
24
74
25
75 def datetime_to_epoch(datetime):
26 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
@@ -1,13 +1,14 b''
1 from django.shortcuts import render
1 from django.shortcuts import render
2
2
3 from boards.views.base import BaseBoardView
3 from boards.views.base import BaseBoardView
4 from boards.models.tag import Tag
4 from boards.models.tag import Tag
5
5
6
6
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,139 +1,137 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
6 from boards import utils, settings
4 from boards import utils, settings
7 from boards.abstracts.paginator import get_paginator
5 from boards.abstracts.paginator import get_paginator
8 from boards.abstracts.settingsmanager import get_settings_manager
6 from boards.abstracts.settingsmanager import get_settings_manager
9 from boards.forms import ThreadForm, PlainErrorList
7 from boards.forms import ThreadForm, PlainErrorList
10 from boards.models import Post, Thread, Ban, Tag
8 from boards.models import Post, Thread, Ban, Tag
11 from boards.views.banned import BannedView
9 from boards.views.banned import BannedView
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'
18 FORM_IMAGE = 'image'
17 FORM_IMAGE = 'image'
19
18
20 TAG_DELIMITER = ' '
19 TAG_DELIMITER = ' '
21
20
22 PARAMETER_CURRENT_PAGE = 'current_page'
21 PARAMETER_CURRENT_PAGE = 'current_page'
23 PARAMETER_PAGINATOR = 'paginator'
22 PARAMETER_PAGINATOR = 'paginator'
24 PARAMETER_THREADS = 'threads'
23 PARAMETER_THREADS = 'threads'
25
24
26 TEMPLATE = 'boards/posting_general.html'
25 TEMPLATE = 'boards/posting_general.html'
27 DEFAULT_PAGE = 1
26 DEFAULT_PAGE = 1
28
27
29
28
30 class AllThreadsView(PostMixin, BaseBoardView):
29 class AllThreadsView(PostMixin, BaseBoardView):
31
30
32 def __init__(self):
31 def __init__(self):
33 self.settings_manager = None
32 self.settings_manager = None
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)
41
40
42 self.settings_manager = get_settings_manager(request)
41 self.settings_manager = get_settings_manager(request)
43 paginator = get_paginator(self.get_threads(),
42 paginator = get_paginator(self.get_threads(),
44 settings.THREADS_PER_PAGE)
43 settings.THREADS_PER_PAGE)
45 paginator.current_page = int(page)
44 paginator.current_page = int(page)
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,
58 error_class=PlainErrorList)
57 error_class=PlainErrorList)
59 form.session = request.session
58 form.session = request.session
60
59
61 if form.is_valid():
60 if form.is_valid():
62 return self.create_thread(request, form)
61 return self.create_thread(request, form)
63 if form.need_to_ban:
62 if form.need_to_ban:
64 # Ban user because he is suspected to be a bot
63 # Ban user because he is suspected to be a bot
65 self._ban_current_user(request)
64 self._ban_current_user(request)
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):
80 """
78 """
81 Parses tag list string and returns tag object list.
79 Parses tag list string and returns tag object list.
82 """
80 """
83
81
84 tags = []
82 tags = []
85
83
86 if tag_strings:
84 if tag_strings:
87 tag_strings = tag_strings.split(TAG_DELIMITER)
85 tag_strings = tag_strings.split(TAG_DELIMITER)
88 for tag_name in tag_strings:
86 for tag_name in tag_strings:
89 tag_name = tag_name.strip().lower()
87 tag_name = tag_name.strip().lower()
90 if len(tag_name) > 0:
88 if len(tag_name) > 0:
91 tag, created = Tag.objects.get_or_create(name=tag_name)
89 tag, created = Tag.objects.get_or_create(name=tag_name)
92 tags.append(tag)
90 tags.append(tag)
93
91
94 return tags
92 return tags
95
93
96 @transaction.atomic
94 @transaction.atomic
97 def create_thread(self, request, form, html_response=True):
95 def create_thread(self, request, form, html_response=True):
98 """
96 """
99 Creates a new thread with an opening post.
97 Creates a new thread with an opening post.
100 """
98 """
101
99
102 ip = utils.get_client_ip(request)
100 ip = utils.get_client_ip(request)
103 is_banned = Ban.objects.filter(ip=ip).exists()
101 is_banned = Ban.objects.filter(ip=ip).exists()
104
102
105 if is_banned:
103 if is_banned:
106 if html_response:
104 if html_response:
107 return redirect(BannedView().as_view())
105 return redirect(BannedView().as_view())
108 else:
106 else:
109 return
107 return
110
108
111 data = form.cleaned_data
109 data = form.cleaned_data
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)
126
120
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
133 def get_threads(self):
131 def get_threads(self):
134 """
132 """
135 Gets list of threads that will be shown on a page.
133 Gets list of threads that will be shown on a page.
136 """
134 """
137
135
138 return Thread.objects.all().order_by('-bump_time')\
136 return Thread.objects.all().order_by('-bump_time')\
139 .exclude(tags__in=self.settings_manager.get_hidden_tags())
137 .exclude(tags__in=self.settings_manager.get_hidden_tags())
@@ -1,248 +1,223 b''
1 from datetime import datetime
1 from datetime import datetime
2 import json
2 import json
3 import logging
3 import logging
4 from django.db import transaction
4 from django.db import transaction
5 from django.http import HttpResponse
5 from django.http import HttpResponse
6 from django.shortcuts import get_object_or_404, render
6 from django.shortcuts import get_object_or_404, render
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
14 from boards.utils import datetime_to_epoch
13 from boards.utils import datetime_to_epoch
15 from boards.views.thread import ThreadView
14 from boards.views.thread import ThreadView
16
15
17 __author__ = 'neko259'
16 __author__ = 'neko259'
18
17
19 PARAMETER_TRUNCATED = 'truncated'
18 PARAMETER_TRUNCATED = 'truncated'
20 PARAMETER_TAG = 'tag'
19 PARAMETER_TAG = 'tag'
21 PARAMETER_OFFSET = 'offset'
20 PARAMETER_OFFSET = 'offset'
22 PARAMETER_DIFF_TYPE = 'type'
21 PARAMETER_DIFF_TYPE = 'type'
23
22
24 DIFF_TYPE_HTML = 'html'
23 DIFF_TYPE_HTML = 'html'
25 DIFF_TYPE_JSON = 'json'
24 DIFF_TYPE_JSON = 'json'
26
25
27 STATUS_OK = 'ok'
26 STATUS_OK = 'ok'
28 STATUS_ERROR = 'error'
27 STATUS_ERROR = 'error'
29
28
30 logger = logging.getLogger(__name__)
29 logger = logging.getLogger(__name__)
31
30
32
31
33 @transaction.atomic
32 @transaction.atomic
34 def api_get_threaddiff(request, thread_id, last_update_time):
33 def api_get_threaddiff(request, thread_id, last_update_time):
35 """
34 """
36 Gets posts that were changed or added since time
35 Gets posts that were changed or added since time
37 """
36 """
38
37
39 thread = get_object_or_404(Post, id=thread_id).get_thread()
38 thread = get_object_or_404(Post, id=thread_id).get_thread()
40
39
41 # Add 1 to ensure we don't load the same post over and over
40 # Add 1 to ensure we don't load the same post over and over
42 last_update_timestamp = float(last_update_time) + 1
41 last_update_timestamp = float(last_update_time) + 1
43
42
44 filter_time = datetime.fromtimestamp(last_update_timestamp / 1000000,
43 filter_time = datetime.fromtimestamp(last_update_timestamp / 1000000,
45 timezone.get_current_timezone())
44 timezone.get_current_timezone())
46
45
47 json_data = {
46 json_data = {
48 'added': [],
47 'added': [],
49 'updated': [],
48 'updated': [],
50 'last_update': None,
49 'last_update': None,
51 }
50 }
52 added_posts = Post.objects.filter(thread_new=thread,
51 added_posts = Post.objects.filter(thread_new=thread,
53 pub_time__gt=filter_time) \
52 pub_time__gt=filter_time) \
54 .order_by('pub_time')
53 .order_by('pub_time')
55 updated_posts = Post.objects.filter(thread_new=thread,
54 updated_posts = Post.objects.filter(thread_new=thread,
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))
70
67
71
68
72 def api_add_post(request, opening_post_id):
69 def api_add_post(request, opening_post_id):
73 """
70 """
74 Adds a post and return the JSON response for it
71 Adds a post and return the JSON response for it
75 """
72 """
76
73
77 opening_post = get_object_or_404(Post, id=opening_post_id)
74 opening_post = get_object_or_404(Post, id=opening_post_id)
78
75
79 logger.info('Adding post via api...')
76 logger.info('Adding post via api...')
80
77
81 status = STATUS_OK
78 status = STATUS_OK
82 errors = []
79 errors = []
83
80
84 if request.method == 'POST':
81 if request.method == 'POST':
85 form = PostForm(request.POST, request.FILES, error_class=PlainErrorList)
82 form = PostForm(request.POST, request.FILES, error_class=PlainErrorList)
86 form.session = request.session
83 form.session = request.session
87
84
88 if form.need_to_ban:
85 if form.need_to_ban:
89 # Ban user because he is suspected to be a bot
86 # Ban user because he is suspected to be a bot
90 # _ban_current_user(request)
87 # _ban_current_user(request)
91 status = STATUS_ERROR
88 status = STATUS_ERROR
92 if form.is_valid():
89 if form.is_valid():
93 post = ThreadView().new_post(request, form, opening_post,
90 post = ThreadView().new_post(request, form, opening_post,
94 html_response=False)
91 html_response=False)
95 if not post:
92 if not post:
96 status = STATUS_ERROR
93 status = STATUS_ERROR
97 else:
94 else:
98 logger.info('Added post #%d via api.' % post.id)
95 logger.info('Added post #%d via api.' % post.id)
99 else:
96 else:
100 status = STATUS_ERROR
97 status = STATUS_ERROR
101 errors = form.as_json_errors()
98 errors = form.as_json_errors()
102
99
103 response = {
100 response = {
104 'status': status,
101 'status': status,
105 'errors': errors,
102 'errors': errors,
106 }
103 }
107
104
108 return HttpResponse(content=json.dumps(response))
105 return HttpResponse(content=json.dumps(response))
109
106
110
107
111 def get_post(request, post_id):
108 def get_post(request, post_id):
112 """
109 """
113 Gets the html of a post. Used for popups. Post can be truncated if used
110 Gets the html of a post. Used for popups. Post can be truncated if used
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)
122 context['post'] = post
117 context['post'] = post
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
130 def api_get_threads(request, count):
126 def api_get_threads(request, count):
131 """
127 """
132 Gets the JSON thread opening posts list.
128 Gets the JSON thread opening posts list.
133 Parameters that can be used for filtering:
129 Parameters that can be used for filtering:
134 tag, offset (from which thread to get results)
130 tag, offset (from which thread to get results)
135 """
131 """
136
132
137 if PARAMETER_TAG in request.GET:
133 if PARAMETER_TAG in request.GET:
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
145 if PARAMETER_OFFSET in request.GET:
141 if PARAMETER_OFFSET in request.GET:
146 offset = request.GET[PARAMETER_OFFSET]
142 offset = request.GET[PARAMETER_OFFSET]
147 offset = int(offset) if offset is not None else 0
143 offset = int(offset) if offset is not None else 0
148 else:
144 else:
149 offset = 0
145 offset = 0
150
146
151 threads = threads.order_by('-bump_time')
147 threads = threads.order_by('-bump_time')
152 threads = threads[offset:offset + int(count)]
148 threads = threads[offset:offset + int(count)]
153
149
154 opening_posts = []
150 opening_posts = []
155 for thread in threads:
151 for thread in threads:
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))
163
159
164
160
165 # TODO Test this
161 # TODO Test this
166 def api_get_tags(request):
162 def api_get_tags(request):
167 """
163 """
168 Gets all tags or user tags.
164 Gets all tags or user tags.
169 """
165 """
170
166
171 # TODO Get favorite tags for the given user ID
167 # TODO Get favorite tags for the given user ID
172
168
173 tags = Tag.objects.get_not_empty_tags()
169 tags = Tag.objects.get_not_empty_tags()
174 tag_names = []
170 tag_names = []
175 for tag in tags:
171 for tag in tags:
176 tag_names.append(tag.name)
172 tag_names.append(tag.name)
177
173
178 return HttpResponse(content=json.dumps(tag_names))
174 return HttpResponse(content=json.dumps(tag_names))
179
175
180
176
181 # TODO The result can be cached by the thread last update time
177 # TODO The result can be cached by the thread last update time
182 # TODO Test this
178 # TODO Test this
183 def api_get_thread_posts(request, opening_post_id):
179 def api_get_thread_posts(request, opening_post_id):
184 """
180 """
185 Gets the JSON array of thread posts
181 Gets the JSON array of thread posts
186 """
182 """
187
183
188 opening_post = get_object_or_404(Post, id=opening_post_id)
184 opening_post = get_object_or_404(Post, id=opening_post_id)
189 thread = opening_post.get_thread()
185 thread = opening_post.get_thread()
190 posts = thread.get_replies()
186 posts = thread.get_replies()
191
187
192 json_data = {
188 json_data = {
193 'posts': [],
189 'posts': [],
194 'last_update': None,
190 'last_update': None,
195 }
191 }
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
203 return HttpResponse(content=json.dumps(json_data))
199 return HttpResponse(content=json.dumps(json_data))
204
200
205
201
206 def api_get_post(request, post_id):
202 def api_get_post(request, post_id):
207 """
203 """
208 Gets the JSON of a post. This can be
204 Gets the JSON of a post. This can be
209 used as and API for external clients.
205 used as and API for external clients.
210 """
206 """
211
207
212 post = get_object_or_404(Post, id=post_id)
208 post = get_object_or_404(Post, id=post_id)
213
209
214 json = serializers.serialize("json", [post], fields=(
210 json = serializers.serialize("json", [post], fields=(
215 "pub_time", "_text_rendered", "title", "text", "image",
211 "pub_time", "_text_rendered", "title", "text", "image",
216 "image_width", "image_height", "replies", "tags"
212 "image_width", "image_height", "replies", "tags"
217 ))
213 ))
218
214
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:
226 post = get_object_or_404(Post, id=post_id)
227
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)
221 post = get_object_or_404(Post, id=post_id)
236 post_json = {
222 return post.get_post_data(format_type=format_type, request=request,
237 'id': post.id,
223 include_last_update=include_last_update)
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
@@ -1,13 +1,13 b''
1 from django.shortcuts import render
1 from django.shortcuts import render
2
2
3 from boards.authors import authors
3 from boards.authors import authors
4 from boards.views.base import BaseBoardView
4 from boards.views.base import BaseBoardView
5
5
6
6
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)
@@ -1,16 +1,17 b''
1 from django.shortcuts import get_object_or_404, render
1 from django.shortcuts import get_object_or_404, render
2 from boards import utils
2 from boards import utils
3 from boards.models import Ban
3 from boards.models import Ban
4 from boards.views.base import BaseBoardView
4 from boards.views.base import BaseBoardView
5
5
6
6
7 class BannedView(BaseBoardView):
7 class BannedView(BaseBoardView):
8
8
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)
@@ -1,35 +1,31 b''
1 from django.db import transaction
1 from django.db import transaction
2 from django.template import RequestContext
2 from django.template import RequestContext
3 from django.views.generic import View
3 from django.views.generic import View
4
4
5 from boards import utils
5 from boards import utils
6 from boards.models.user import Ban
6 from boards.models.user import Ban
7
7
8
8
9 BAN_REASON_SPAM = 'Autoban: spam bot'
9 BAN_REASON_SPAM = 'Autoban: spam bot'
10
10
11 CONTEXT_FORM = 'form'
11 CONTEXT_FORM = 'form'
12
12
13
13
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):
25 """
21 """
26 Add current user to the IP ban list
22 Add current user to the IP ban list
27 """
23 """
28
24
29 ip = utils.get_client_ip(request)
25 ip = utils.get_client_ip(request)
30 ban, created = Ban.objects.get_or_create(ip=ip)
26 ban, created = Ban.objects.get_or_create(ip=ip)
31 if created:
27 if created:
32 ban.can_read = False
28 ban.can_read = False
33 ban.reason = BAN_REASON_SPAM
29 ban.reason = BAN_REASON_SPAM
34 ban.save()
30 ban.save()
35
31
@@ -1,39 +1,40 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
4 from django.http import HttpResponseRedirect
5 from django.http import HttpResponseRedirect
5
6
6
7
7 class RedirectNextMixin:
8 class RedirectNextMixin:
8
9
9 def redirect_to_next(self, request):
10 def redirect_to_next(self, request):
10 """
11 """
11 If a 'next' parameter was specified, redirect to the next page. This
12 If a 'next' parameter was specified, redirect to the next page. This
12 is used when the user is required to return to some page after the
13 is used when the user is required to return to some page after the
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')
21
22
22
23
23 class DispatcherMixin:
24 class DispatcherMixin:
24 """
25 """
25 This class contains a dispather method that can run a method specified by
26 This class contains a dispather method that can run a method specified by
26 'method' request parameter.
27 'method' request parameter.
27 """
28 """
28
29
29 def dispatch_method(self, *args, **kwargs):
30 def dispatch_method(self, *args, **kwargs):
30 request = args[0]
31 request = args[0]
31
32
32 method_name = None
33 method_name = None
33 if PARAMETER_METHOD in request.GET:
34 if PARAMETER_METHOD in request.GET:
34 method_name = request.GET[PARAMETER_METHOD]
35 method_name = request.GET[PARAMETER_METHOD]
35 elif PARAMETER_METHOD in request.POST:
36 elif PARAMETER_METHOD in request.POST:
36 method_name = request.POST[PARAMETER_METHOD]
37 method_name = request.POST[PARAMETER_METHOD]
37
38
38 if method_name:
39 if method_name:
39 return getattr(self, method_name)(*args, **kwargs)
40 return getattr(self, method_name)(*args, **kwargs)
@@ -1,13 +1,17 b''
1 from django.shortcuts import render
1 from django.shortcuts import render
2
2
3 from boards.views.base import BaseBoardView
3 from boards.views.base import BaseBoardView
4
4
5
5
6 class NotFoundView(BaseBoardView):
6 class NotFoundView(BaseBoardView):
7 """
7 """
8 Page 404 (not found)
8 Page 404 (not found)
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
@@ -1,35 +1,37 b''
1 from django.shortcuts import render
1 from django.shortcuts import render
2 from django.template import RequestContext
2 from django.template import RequestContext
3 from django.views.generic import View
3 from django.views.generic import View
4
4
5 from boards.mdx_neboard import bbcode_extended
5 from boards.mdx_neboard import bbcode_extended
6
6
7 FORM_QUERY = 'query'
7 FORM_QUERY = 'query'
8
8
9 CONTEXT_RESULT = 'result'
9 CONTEXT_RESULT = 'result'
10 CONTEXT_QUERY = 'query'
10 CONTEXT_QUERY = 'query'
11
11
12 __author__ = 'neko259'
12 __author__ = 'neko259'
13
13
14 TEMPLATE = 'boards/preview.html'
14 TEMPLATE = 'boards/preview.html'
15
15
16
16
17 class PostPreviewView(View):
17 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)
25
26
26 if FORM_QUERY in request.POST:
27 if FORM_QUERY in request.POST:
27 raw_text = request.POST[FORM_QUERY]
28 raw_text = request.POST[FORM_QUERY]
28
29
29 if len(raw_text) >= 0:
30 if len(raw_text) >= 0:
30 rendered_text = bbcode_extended(raw_text)
31 rendered_text = bbcode_extended(raw_text)
31
32
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,40 +1,43 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'
11 CONTEXT_FORM = 'form'
15 CONTEXT_FORM = 'form'
12 CONTEXT_PAGE = 'page'
16 CONTEXT_PAGE = 'page'
13
17
14 REQUEST_PAGE = 'page'
18 REQUEST_PAGE = 'page'
15
19
16 __author__ = 'neko259'
20 __author__ = 'neko259'
17
21
18 TEMPLATE = 'search/search.html'
22 TEMPLATE = 'search/search.html'
19
23
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)
@@ -1,38 +1,41 b''
1 from django.db import transaction
1 from django.db import transaction
2 from django.shortcuts import render, redirect
2 from django.shortcuts import render, redirect
3
3
4 from boards.abstracts.settingsmanager import get_settings_manager
4 from boards.abstracts.settingsmanager import get_settings_manager
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)
29
32
30 with transaction.atomic():
33 with transaction.atomic():
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
38 return redirect('settings')
41 return redirect('settings')
@@ -1,14 +1,13 b''
1 from django.shortcuts import render
1 from django.shortcuts import render
2
2
3 from boards.views.base import BaseBoardView
3 from boards.views.base import BaseBoardView
4
4
5
5
6 class StaticPageView(BaseBoardView):
6 class StaticPageView(BaseBoardView):
7
7
8 def get(self, request, name):
8 def get(self, request, name):
9 """
9 """
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,92 +1,95 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
12
15
13 class TagView(AllThreadsView, DispatcherMixin, RedirectNextMixin):
16 class TagView(AllThreadsView, DispatcherMixin, RedirectNextMixin):
14
17
15 tag_name = None
18 tag_name = None
16
19
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
37
40
38 dispatch_result = self.dispatch_method(request)
41 dispatch_result = self.dispatch_method(request)
39 if dispatch_result:
42 if dispatch_result:
40 return dispatch_result
43 return dispatch_result
41 else:
44 else:
42 return super(TagView, self).get(request, page, form)
45 return super(TagView, self).get(request, page, form)
43
46
44 def post(self, request, tag_name, page=DEFAULT_PAGE):
47 def post(self, request, tag_name, page=DEFAULT_PAGE):
45 form = ThreadForm(request.POST, request.FILES,
48 form = ThreadForm(request.POST, request.FILES,
46 error_class=PlainErrorList)
49 error_class=PlainErrorList)
47 form.session = request.session
50 form.session = request.session
48
51
49 if form.is_valid():
52 if form.is_valid():
50 return self.create_thread(request, form)
53 return self.create_thread(request, form)
51 if form.need_to_ban:
54 if form.need_to_ban:
52 # Ban user because he is suspected to be a bot
55 # Ban user because he is suspected to be a bot
53 self._ban_current_user(request)
56 self._ban_current_user(request)
54
57
55 return self.get(request, tag_name, page, form)
58 return self.get(request, tag_name, page, form)
56
59
57 def subscribe(self, request):
60 def subscribe(self, request):
58 tag = get_object_or_404(Tag, name=self.tag_name)
61 tag = get_object_or_404(Tag, name=self.tag_name)
59
62
60 settings_manager = get_settings_manager(request)
63 settings_manager = get_settings_manager(request)
61 settings_manager.add_fav_tag(tag)
64 settings_manager.add_fav_tag(tag)
62
65
63 return self.redirect_to_next(request)
66 return self.redirect_to_next(request)
64
67
65 def unsubscribe(self, request):
68 def unsubscribe(self, request):
66 tag = get_object_or_404(Tag, name=self.tag_name)
69 tag = get_object_or_404(Tag, name=self.tag_name)
67
70
68 settings_manager = get_settings_manager(request)
71 settings_manager = get_settings_manager(request)
69 settings_manager.del_fav_tag(tag)
72 settings_manager.del_fav_tag(tag)
70
73
71 return self.redirect_to_next(request)
74 return self.redirect_to_next(request)
72
75
73 def hide(self, request):
76 def hide(self, request):
74 """
77 """
75 Adds tag to user's hidden tags. Threads with this tag will not be
78 Adds tag to user's hidden tags. Threads with this tag will not be
76 shown.
79 shown.
77 """
80 """
78
81
79 tag = get_object_or_404(Tag, name=self.tag_name)
82 tag = get_object_or_404(Tag, name=self.tag_name)
80
83
81 settings_manager = get_settings_manager(request)
84 settings_manager = get_settings_manager(request)
82 settings_manager.add_hidden_tag(tag)
85 settings_manager.add_hidden_tag(tag)
83
86
84 def unhide(self, request):
87 def unhide(self, request):
85 """
88 """
86 Removed tag from user's hidden tags.
89 Removed tag from user's hidden tags.
87 """
90 """
88
91
89 tag = get_object_or_404(Tag, name=self.tag_name)
92 tag = get_object_or_404(Tag, name=self.tag_name)
90
93
91 settings_manager = get_settings_manager(request)
94 settings_manager = get_settings_manager(request)
92 settings_manager.del_hidden_tag(tag)
95 settings_manager.del_hidden_tag(tag)
@@ -1,142 +1,142 b''
1 from django.core.urlresolvers import reverse
1 from django.core.urlresolvers import reverse
2 from django.db import transaction
2 from django.db import transaction
3 from django.http import Http404
3 from django.http import Http404
4 from django.shortcuts import get_object_or_404, render, redirect
4 from django.shortcuts import get_object_or_404, render, redirect
5 from django.views.generic.edit import FormMixin
5 from django.views.generic.edit import FormMixin
6
6
7 from boards import utils, settings
7 from boards import utils, settings
8 from boards.forms import PostForm, PlainErrorList
8 from boards.forms import PostForm, PlainErrorList
9 from boards.models import Post, Ban
9 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'
16
17
17 CONTEXT_POSTS = 'posts'
18 CONTEXT_POSTS = 'posts'
18 CONTEXT_OP = 'opening_post'
19 CONTEXT_OP = 'opening_post'
19 CONTEXT_BUMPLIMIT_PRG = 'bumplimit_progress'
20 CONTEXT_BUMPLIMIT_PRG = 'bumplimit_progress'
20 CONTEXT_POSTS_LEFT = 'posts_left'
21 CONTEXT_POSTS_LEFT = 'posts_left'
21 CONTEXT_LASTUPDATE = "last_update"
22 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'
28 FORM_IMAGE = 'image'
33 FORM_IMAGE = 'image'
29
34
30 MODE_GALLERY = 'gallery'
35 MODE_GALLERY = 'gallery'
31 MODE_NORMAL = 'normal'
36 MODE_NORMAL = 'normal'
32
37
33
38
34 class ThreadView(BaseBoardView, PostMixin, FormMixin):
39 class ThreadView(BaseBoardView, PostMixin, FormMixin):
35
40
36 def get(self, request, post_id, mode=MODE_NORMAL, form=None):
41 def get(self, request, post_id, mode=MODE_NORMAL, form=None):
37 try:
42 try:
38 opening_post = Post.objects.filter(id=post_id).only('thread_new')[0]
43 opening_post = Post.objects.filter(id=post_id).only('thread_new')[0]
39 except IndexError:
44 except IndexError:
40 raise Http404
45 raise Http404
41
46
42 # If this is not OP, don't show it as it is
47 # If this is not OP, don't show it as it is
43 if not opening_post or not opening_post.is_opening():
48 if not opening_post or not opening_post.is_opening():
44 raise Http404
49 raise Http404
45
50
46 if not form:
51 if not form:
47 form = PostForm(error_class=PlainErrorList)
52 form = PostForm(error_class=PlainErrorList)
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)
84
97
85 # If this is not OP, don't show it as it is
98 # If this is not OP, don't show it as it is
86 if not opening_post.is_opening():
99 if not opening_post.is_opening():
87 raise Http404
100 raise Http404
88
101
89 if not opening_post.get_thread().archived:
102 if not opening_post.get_thread().archived:
90 form = PostForm(request.POST, request.FILES,
103 form = PostForm(request.POST, request.FILES,
91 error_class=PlainErrorList)
104 error_class=PlainErrorList)
92 form.session = request.session
105 form.session = request.session
93
106
94 if form.is_valid():
107 if form.is_valid():
95 return self.new_post(request, form, opening_post)
108 return self.new_post(request, form, opening_post)
96 if form.need_to_ban:
109 if form.need_to_ban:
97 # Ban user because he is suspected to be a bot
110 # Ban user because he is suspected to be a bot
98 self._ban_current_user(request)
111 self._ban_current_user(request)
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
136 if html_response:
136 if html_response:
137 if opening_post:
137 if opening_post:
138 return redirect(
138 return redirect(
139 reverse('thread', kwargs={'post_id': thread_to_show})
139 reverse('thread', kwargs={'post_id': thread_to_show})
140 + '#' + str(post.id))
140 + '#' + str(post.id))
141 else:
141 else:
142 return post
142 return post
@@ -1,10 +1,10 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
5 if __name__ == "__main__":
5 if __name__ == "__main__":
6 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "neboard.settings")
6 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "neboard.settings")
7
7
8 from django.core.management import execute_from_command_line
8 from django.core.management import execute_from_command_line
9
9
10 execute_from_command_line(sys.argv)
10 execute_from_command_line(sys.argv)
@@ -1,235 +1,233 b''
1 # Django settings for neboard project.
1 # Django settings for neboard project.
2 import os
2 import os
3 from boards.mdx_neboard import bbcode_extended
3 from boards.mdx_neboard import bbcode_extended
4
4
5 DEBUG = True
5 DEBUG = True
6 TEMPLATE_DEBUG = DEBUG
6 TEMPLATE_DEBUG = DEBUG
7
7
8 ADMINS = (
8 ADMINS = (
9 # ('Your Name', 'your_email@example.com'),
9 # ('Your Name', 'your_email@example.com'),
10 ('admin', 'admin@example.com')
10 ('admin', 'admin@example.com')
11 )
11 )
12
12
13 MANAGERS = ADMINS
13 MANAGERS = ADMINS
14
14
15 DATABASES = {
15 DATABASES = {
16 'default': {
16 'default': {
17 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
17 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
18 'NAME': 'database.db', # Or path to database file if using sqlite3.
18 'NAME': 'database.db', # Or path to database file if using sqlite3.
19 'USER': '', # Not used with sqlite3.
19 'USER': '', # Not used with sqlite3.
20 'PASSWORD': '', # Not used with sqlite3.
20 'PASSWORD': '', # Not used with sqlite3.
21 'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
21 'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
22 'PORT': '', # Set to empty string for default. Not used with sqlite3.
22 'PORT': '', # Set to empty string for default. Not used with sqlite3.
23 'CONN_MAX_AGE': None,
23 'CONN_MAX_AGE': None,
24 }
24 }
25 }
25 }
26
26
27 # Local time zone for this installation. Choices can be found here:
27 # Local time zone for this installation. Choices can be found here:
28 # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
28 # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
29 # although not all choices may be available on all operating systems.
29 # although not all choices may be available on all operating systems.
30 # In a Windows environment this must be set to your system time zone.
30 # In a Windows environment this must be set to your system time zone.
31 TIME_ZONE = 'Europe/Kiev'
31 TIME_ZONE = 'Europe/Kiev'
32
32
33 # Language code for this installation. All choices can be found here:
33 # Language code for this installation. All choices can be found here:
34 # http://www.i18nguy.com/unicode/language-identifiers.html
34 # http://www.i18nguy.com/unicode/language-identifiers.html
35 LANGUAGE_CODE = 'en'
35 LANGUAGE_CODE = 'en'
36
36
37 SITE_ID = 1
37 SITE_ID = 1
38
38
39 # If you set this to False, Django will make some optimizations so as not
39 # If you set this to False, Django will make some optimizations so as not
40 # to load the internationalization machinery.
40 # to load the internationalization machinery.
41 USE_I18N = True
41 USE_I18N = True
42
42
43 # If you set this to False, Django will not format dates, numbers and
43 # If you set this to False, Django will not format dates, numbers and
44 # calendars according to the current locale.
44 # calendars according to the current locale.
45 USE_L10N = True
45 USE_L10N = True
46
46
47 # If you set this to False, Django will not use timezone-aware datetimes.
47 # If you set this to False, Django will not use timezone-aware datetimes.
48 USE_TZ = True
48 USE_TZ = True
49
49
50 # Absolute filesystem path to the directory that will hold user-uploaded files.
50 # Absolute filesystem path to the directory that will hold user-uploaded files.
51 # Example: "/home/media/media.lawrence.com/media/"
51 # Example: "/home/media/media.lawrence.com/media/"
52 MEDIA_ROOT = './media/'
52 MEDIA_ROOT = './media/'
53
53
54 # URL that handles the media served from MEDIA_ROOT. Make sure to use a
54 # URL that handles the media served from MEDIA_ROOT. Make sure to use a
55 # trailing slash.
55 # trailing slash.
56 # Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
56 # Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
57 MEDIA_URL = '/media/'
57 MEDIA_URL = '/media/'
58
58
59 # Absolute path to the directory static files should be collected to.
59 # Absolute path to the directory static files should be collected to.
60 # Don't put anything in this directory yourself; store your static files
60 # Don't put anything in this directory yourself; store your static files
61 # in apps' "static/" subdirectories and in STATICFILES_DIRS.
61 # in apps' "static/" subdirectories and in STATICFILES_DIRS.
62 # Example: "/home/media/media.lawrence.com/static/"
62 # Example: "/home/media/media.lawrence.com/static/"
63 STATIC_ROOT = ''
63 STATIC_ROOT = ''
64
64
65 # URL prefix for static files.
65 # URL prefix for static files.
66 # Example: "http://media.lawrence.com/static/"
66 # Example: "http://media.lawrence.com/static/"
67 STATIC_URL = '/static/'
67 STATIC_URL = '/static/'
68
68
69 # Additional locations of static files
69 # Additional locations of static files
70 # It is really a hack, put real paths, not related
70 # It is really a hack, put real paths, not related
71 STATICFILES_DIRS = (
71 STATICFILES_DIRS = (
72 os.path.dirname(__file__) + '/boards/static',
72 os.path.dirname(__file__) + '/boards/static',
73
73
74 # '/d/work/python/django/neboard/neboard/boards/static',
74 # '/d/work/python/django/neboard/neboard/boards/static',
75 # Put strings here, like "/home/html/static" or "C:/www/django/static".
75 # Put strings here, like "/home/html/static" or "C:/www/django/static".
76 # Always use forward slashes, even on Windows.
76 # Always use forward slashes, even on Windows.
77 # Don't forget to use absolute paths, not relative paths.
77 # Don't forget to use absolute paths, not relative paths.
78 )
78 )
79
79
80 # List of finder classes that know how to find static files in
80 # List of finder classes that know how to find static files in
81 # various locations.
81 # various locations.
82 STATICFILES_FINDERS = (
82 STATICFILES_FINDERS = (
83 'django.contrib.staticfiles.finders.FileSystemFinder',
83 'django.contrib.staticfiles.finders.FileSystemFinder',
84 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
84 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
85 'compressor.finders.CompressorFinder',
85 )
86 )
86
87
87 if DEBUG:
88 if DEBUG:
88 STATICFILES_STORAGE = \
89 STATICFILES_STORAGE = \
89 'django.contrib.staticfiles.storage.StaticFilesStorage'
90 'django.contrib.staticfiles.storage.StaticFilesStorage'
90 else:
91 else:
91 STATICFILES_STORAGE = \
92 STATICFILES_STORAGE = \
92 'django.contrib.staticfiles.storage.CachedStaticFilesStorage'
93 'django.contrib.staticfiles.storage.CachedStaticFilesStorage'
93
94
94 # Make this unique, and don't share it with anybody.
95 # Make this unique, and don't share it with anybody.
95 SECRET_KEY = '@1rc$o(7=tt#kd+4s$u6wchm**z^)4x90)7f6z(i&amp;55@o11*8o'
96 SECRET_KEY = '@1rc$o(7=tt#kd+4s$u6wchm**z^)4x90)7f6z(i&amp;55@o11*8o'
96
97
97 # List of callables that know how to import templates from various sources.
98 # List of callables that know how to import templates from various sources.
98 TEMPLATE_LOADERS = (
99 TEMPLATE_LOADERS = (
99 'django.template.loaders.filesystem.Loader',
100 'django.template.loaders.filesystem.Loader',
100 'django.template.loaders.app_directories.Loader',
101 'django.template.loaders.app_directories.Loader',
101 )
102 )
102
103
103 TEMPLATE_CONTEXT_PROCESSORS = (
104 TEMPLATE_CONTEXT_PROCESSORS = (
104 'django.core.context_processors.media',
105 'django.core.context_processors.media',
105 'django.core.context_processors.static',
106 'django.core.context_processors.static',
106 'django.core.context_processors.request',
107 'django.core.context_processors.request',
107 'django.contrib.auth.context_processors.auth',
108 'django.contrib.auth.context_processors.auth',
108 'boards.context_processors.user_and_ui_processor',
109 'boards.context_processors.user_and_ui_processor',
109 )
110 )
110
111
111 MIDDLEWARE_CLASSES = (
112 MIDDLEWARE_CLASSES = (
112 'django.contrib.sessions.middleware.SessionMiddleware',
113 'django.contrib.sessions.middleware.SessionMiddleware',
113 'django.middleware.locale.LocaleMiddleware',
114 'django.middleware.locale.LocaleMiddleware',
114 'django.middleware.common.CommonMiddleware',
115 'django.middleware.common.CommonMiddleware',
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'
122
122
123 # Python dotted path to the WSGI application used by Django's runserver.
123 # Python dotted path to the WSGI application used by Django's runserver.
124 WSGI_APPLICATION = 'neboard.wsgi.application'
124 WSGI_APPLICATION = 'neboard.wsgi.application'
125
125
126 TEMPLATE_DIRS = (
126 TEMPLATE_DIRS = (
127 # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
127 # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
128 # Always use forward slashes, even on Windows.
128 # Always use forward slashes, even on Windows.
129 # Don't forget to use absolute paths, not relative paths.
129 # Don't forget to use absolute paths, not relative paths.
130 'templates',
130 'templates',
131 )
131 )
132
132
133 INSTALLED_APPS = (
133 INSTALLED_APPS = (
134 'django.contrib.auth',
134 'django.contrib.auth',
135 'django.contrib.contenttypes',
135 'django.contrib.contenttypes',
136 'django.contrib.sessions',
136 'django.contrib.sessions',
137 # 'django.contrib.sites',
137 # 'django.contrib.sites',
138 'django.contrib.messages',
138 'django.contrib.messages',
139 'django.contrib.staticfiles',
139 'django.contrib.staticfiles',
140 # Uncomment the next line to enable the admin:
140 # Uncomment the next line to enable the admin:
141 'django.contrib.admin',
141 'django.contrib.admin',
142 # Uncomment the next line to enable admin documentation:
142 # Uncomment the next line to enable admin documentation:
143 # 'django.contrib.admindocs',
143 # 'django.contrib.admindocs',
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
152 'haystack',
150 'haystack',
153
151
154 'boards',
152 'boards',
155 )
153 )
156
154
157 # A sample logging configuration. The only tangible logging
155 # A sample logging configuration. The only tangible logging
158 # performed by this configuration is to send an email to
156 # performed by this configuration is to send an email to
159 # the site admins on every HTTP 500 error when DEBUG=False.
157 # the site admins on every HTTP 500 error when DEBUG=False.
160 # See http://docs.djangoproject.com/en/dev/topics/logging for
158 # See http://docs.djangoproject.com/en/dev/topics/logging for
161 # more details on how to customize your logging configuration.
159 # more details on how to customize your logging configuration.
162 LOGGING = {
160 LOGGING = {
163 'version': 1,
161 'version': 1,
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': {
174 'require_debug_false': {
172 'require_debug_false': {
175 '()': 'django.utils.log.RequireDebugFalse'
173 '()': 'django.utils.log.RequireDebugFalse'
176 }
174 }
177 },
175 },
178 'handlers': {
176 'handlers': {
179 'console': {
177 'console': {
180 'level': 'DEBUG',
178 'level': 'DEBUG',
181 'class': 'logging.StreamHandler',
179 'class': 'logging.StreamHandler',
182 'formatter': 'simple'
180 'formatter': 'simple'
183 },
181 },
184 },
182 },
185 'loggers': {
183 'loggers': {
186 'boards': {
184 'boards': {
187 'handlers': ['console'],
185 'handlers': ['console'],
188 'level': 'DEBUG',
186 'level': 'DEBUG',
189 }
187 }
190 },
188 },
191 }
189 }
192
190
193 HAYSTACK_CONNECTIONS = {
191 HAYSTACK_CONNECTIONS = {
194 'default': {
192 'default': {
195 'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine',
193 'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine',
196 'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'),
194 'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'),
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)'),
207 ('sw', 'Snow White'),
201 ('sw', 'Snow White'),
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:
219 MIDDLEWARE_CLASSES += (
218 MIDDLEWARE_CLASSES += (
220 'debug_toolbar.middleware.DebugToolbarMiddleware',
219 'debug_toolbar.middleware.DebugToolbarMiddleware',
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,
228 'SHOW_TOOLBAR_CALLBACK': 'neboard.settings.custom_show_toolbar',
227 'SHOW_TOOLBAR_CALLBACK': 'neboard.settings.custom_show_toolbar',
229 }
228 }
230
229
231 # FIXME Uncommenting this fails somehow. Need to investigate this
230 # FIXME Uncommenting this fails somehow. Need to investigate this
232 #DEBUG_TOOLBAR_PANELS += (
231 #DEBUG_TOOLBAR_PANELS += (
233 # 'debug_toolbar.panels.profiling.ProfilingDebugPanel',
232 # 'debug_toolbar.panels.profiling.ProfilingDebugPanel',
234 #)
233 #)
235
@@ -1,55 +1,38 b''
1 # INTRO #
1 # INTRO #
2
2
3 This project aims to create centralized forum-like discussion platform with
3 This project aims to create centralized forum-like discussion platform with
4 anonymity in mind.
4 anonymity in mind.
5
5
6 Main repository: https://bitbucket.org/neko259/neboard/
6 Main repository: https://bitbucket.org/neko259/neboard/
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
30 2. Setup a database in `neboard/settings.py`
13 2. Setup a database in `neboard/settings.py`
31 3. Run `./manage.py syncdb` and ensure the database was created
14 3. Run `./manage.py syncdb` and ensure the database was created
32 4. Run `./manage.py migrate boards` to apply all south migrations
15 4. Run `./manage.py migrate boards` to apply all south migrations
33
16
34 # RUNNING #
17 # RUNNING #
35
18
36 You can run the server using django default embedded webserver by running
19 You can run the server using django default embedded webserver by running
37
20
38 ./manage.py runserver <address>:<port>
21 ./manage.py runserver <address>:<port>
39
22
40 See django-admin command help for details
23 See django-admin command help for details
41
24
42 Also consider using wsgi or fcgi interfaces on production servers.
25 Also consider using wsgi or fcgi interfaces on production servers.
43
26
44 # UPGRADE #
27 # UPGRADE #
45
28
46 1. Backup your project data.
29 1. Backup your project data.
47 2. Save the settings in `neboard/settings.py` and `boards/settings.py`
30 2. Save the settings in `neboard/settings.py` and `boards/settings.py`
48 3. Copy the project contents over the old project directory
31 3. Copy the project contents over the old project directory
49 4. Run migrations by `./manage.py migrate boards`
32 4. Run migrations by `./manage.py migrate boards`
50
33
51 You can also just clone the mercurial project and pull it to update
34 You can also just clone the mercurial project and pull it to update
52
35
53 # CONCLUSION #
36 # CONCLUSION #
54
37
55 Enjoy our software and thank you!
38 Enjoy our software and thank you!
@@ -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