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