##// END OF EJS Templates
Added centrifuge (websocket) support for thread autoupdate. Only websocket version is supported for now
neko259 -
r853:ea46532a default
parent child Browse files
Show More
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));
@@ -1,46 +1,45 b''
1 1 from django.shortcuts import redirect
2 2 from boards import utils
3 3 from boards.models import Ban
4 4 from django.utils.html import strip_spaces_between_tags
5 5 from django.conf import settings
6 from boards.views.banned import BannedView
7 6
8 7 RESPONSE_CONTENT_TYPE = 'Content-Type'
9 8
10 9 TYPE_HTML = 'text/html'
11 10
12 11
13 12 class BanMiddleware:
14 13 """
15 14 This is run before showing the thread. Banned users don't need to see
16 15 anything
17 16 """
18 17
19 18 def __init__(self):
20 19 pass
21 20
22 21 def process_view(self, request, view_func, view_args, view_kwargs):
23 22
24 if view_func != BannedView.as_view:
23 if request.path != '/banned/':
25 24 ip = utils.get_client_ip(request)
26 25 bans = Ban.objects.filter(ip=ip)
27 26
28 27 if bans.exists():
29 28 ban = bans[0]
30 29 if not ban.can_read:
31 30 return redirect('banned')
32 31
33 32
34 33 class MinifyHTMLMiddleware(object):
35 34 def process_response(self, request, response):
36 35 try:
37 36 compress_html = settings.COMPRESS_HTML
38 37 except AttributeError:
39 38 compress_html = False
40 39
41 40 if RESPONSE_CONTENT_TYPE in response\
42 41 and TYPE_HTML in response[RESPONSE_CONTENT_TYPE]\
43 42 and compress_html:
44 43 response.content = strip_spaces_between_tags(
45 44 response.content.strip())
46 45 return response No newline at end of file
@@ -1,346 +1,417 b''
1 1 from datetime import datetime, timedelta, date
2 2 from datetime import time as dtime
3 from adjacent import Client
3 4 import logging
4 5 import re
5 6
6 7 from django.core.cache import cache
7 8 from django.core.urlresolvers import reverse
8 9 from django.db import models, transaction
10 from django.shortcuts import get_object_or_404
11 from django.template import RequestContext
9 12 from django.template.loader import render_to_string
10 13 from django.utils import timezone
11 14 from markupfield.fields import MarkupField
15 from boards import settings
12 16
13 17 from boards.models import PostImage
14 18 from boards.models.base import Viewable
15 19 from boards.models.thread import Thread
20 from boards.utils import datetime_to_epoch
16 21
22 WS_CHANNEL_THREAD = "thread:"
17 23
18 24 APP_LABEL_BOARDS = 'boards'
19 25
20 26 CACHE_KEY_PPD = 'ppd'
21 27 CACHE_KEY_POST_URL = 'post_url'
22 28
23 29 POSTS_PER_DAY_RANGE = 7
24 30
25 31 BAN_REASON_AUTO = 'Auto'
26 32
27 33 IMAGE_THUMB_SIZE = (200, 150)
28 34
29 35 TITLE_MAX_LENGTH = 200
30 36
31 37 DEFAULT_MARKUP_TYPE = 'bbcode'
32 38
33 39 # TODO This should be removed
34 40 NO_IP = '0.0.0.0'
35 41
36 42 # TODO Real user agent should be saved instead of this
37 43 UNKNOWN_UA = ''
38 44
39 45 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
40 46
47 PARAMETER_TRUNCATED = 'truncated'
48 PARAMETER_TAG = 'tag'
49 PARAMETER_OFFSET = 'offset'
50 PARAMETER_DIFF_TYPE = 'type'
51
52 DIFF_TYPE_HTML = 'html'
53 DIFF_TYPE_JSON = 'json'
54
41 55 logger = logging.getLogger(__name__)
42 56
43 57
44 58 class PostManager(models.Manager):
45 59 def create_post(self, title, text, image=None, thread=None, ip=NO_IP,
46 60 tags=None):
47 61 """
48 62 Creates new post
49 63 """
50 64
51 65 if not tags:
52 66 tags = []
53 67
54 68 posting_time = timezone.now()
55 69 if not thread:
56 70 thread = Thread.objects.create(bump_time=posting_time,
57 71 last_edit_time=posting_time)
58 72 new_thread = True
59 73 else:
60 74 thread.bump()
61 75 thread.last_edit_time = posting_time
62 76 thread.save()
63 77 new_thread = False
64 78
65 79 post = self.create(title=title,
66 80 text=text,
67 81 pub_time=posting_time,
68 82 thread_new=thread,
69 83 poster_ip=ip,
70 84 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
71 85 # last!
72 86 last_edit_time=posting_time)
73 87
74 88 logger.info('Created post #%d with title "%s"' % (post.id,
75 89 post.title))
76 90
77 91 if image:
78 92 post_image = PostImage.objects.create(image=image)
79 93 post.images.add(post_image)
80 94 logger.info('Created image #%d for post #%d' % (post_image.id,
81 95 post.id))
82 96
83 97 thread.replies.add(post)
84 98 list(map(thread.add_tag, tags))
85 99
86 100 if new_thread:
87 101 Thread.objects.process_oldest_threads()
88 102 self.connect_replies(post)
89 103
90 104 return post
91 105
92 106 def delete_post(self, post):
93 107 """
94 108 Deletes post and update or delete its thread
95 109 """
96 110
97 111 post_id = post.id
98 112
99 113 thread = post.get_thread()
100 114
101 115 if post.is_opening():
102 116 thread.delete()
103 117 else:
104 118 thread.last_edit_time = timezone.now()
105 119 thread.save()
106 120
107 121 post.delete()
108 122
109 123 logger.info('Deleted post #%d (%s)' % (post_id, post.get_title()))
110 124
111 125 def delete_posts_by_ip(self, ip):
112 126 """
113 127 Deletes all posts of the author with same IP
114 128 """
115 129
116 130 posts = self.filter(poster_ip=ip)
117 131 for post in posts:
118 132 self.delete_post(post)
119 133
120 def connect_replies(self, post):
134 def connect_replies(self, post):
121 135 """
122 136 Connects replies to a post to show them as a reflink map
123 137 """
124 138
125 139 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
126 140 post_id = reply_number.group(1)
127 141 ref_post = self.filter(id=post_id)
128 142 if ref_post.count() > 0:
129 143 referenced_post = ref_post[0]
130 144 referenced_post.referenced_posts.add(post)
131 145 referenced_post.last_edit_time = post.pub_time
132 146 referenced_post.build_refmap()
133 147 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
134 148
135 149 referenced_thread = referenced_post.get_thread()
136 150 referenced_thread.last_edit_time = post.pub_time
137 151 referenced_thread.save(update_fields=['last_edit_time'])
138 152
139 153 def get_posts_per_day(self):
140 154 """
141 155 Gets average count of posts per day for the last 7 days
142 156 """
143 157
144 158 day_end = date.today()
145 159 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
146 160
147 161 cache_key = CACHE_KEY_PPD + str(day_end)
148 162 ppd = cache.get(cache_key)
149 163 if ppd:
150 164 return ppd
151 165
152 166 day_time_start = timezone.make_aware(datetime.combine(
153 167 day_start, dtime()), timezone.get_current_timezone())
154 168 day_time_end = timezone.make_aware(datetime.combine(
155 169 day_end, dtime()), timezone.get_current_timezone())
156 170
157 171 posts_per_period = float(self.filter(
158 172 pub_time__lte=day_time_end,
159 173 pub_time__gte=day_time_start).count())
160 174
161 175 ppd = posts_per_period / POSTS_PER_DAY_RANGE
162 176
163 177 cache.set(cache_key, ppd)
164 178 return ppd
165 179
166 180
167 181 class Post(models.Model, Viewable):
168 182 """A post is a message."""
169 183
170 184 objects = PostManager()
171 185
172 186 class Meta:
173 187 app_label = APP_LABEL_BOARDS
174 188 ordering = ('id',)
175 189
176 190 title = models.CharField(max_length=TITLE_MAX_LENGTH)
177 191 pub_time = models.DateTimeField()
178 192 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
179 193 escape_html=False)
180 194
181 195 images = models.ManyToManyField(PostImage, null=True, blank=True,
182 196 related_name='ip+', db_index=True)
183 197
184 198 poster_ip = models.GenericIPAddressField()
185 199 poster_user_agent = models.TextField()
186 200
187 201 thread_new = models.ForeignKey('Thread', null=True, default=None,
188 202 db_index=True)
189 203 last_edit_time = models.DateTimeField()
190 204
191 205 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
192 206 null=True,
193 207 blank=True, related_name='rfp+',
194 208 db_index=True)
195 209 refmap = models.TextField(null=True, blank=True)
196 210
197 211 def __unicode__(self):
198 212 return '#' + str(self.id) + ' ' + self.title + ' (' + \
199 213 self.text.raw[:50] + ')'
200 214
201 215 def get_title(self):
202 216 """
203 217 Gets original post title or part of its text.
204 218 """
205 219
206 220 title = self.title
207 221 if not title:
208 222 title = self.text.rendered
209 223
210 224 return title
211 225
212 226 def build_refmap(self):
213 227 """
214 228 Builds a replies map string from replies list. This is a cache to stop
215 229 the server from recalculating the map on every post show.
216 230 """
217 231 map_string = ''
218 232
219 233 first = True
220 234 for refpost in self.referenced_posts.all():
221 235 if not first:
222 236 map_string += ', '
223 237 map_string += '<a href="%s">&gt;&gt;%s</a>' % (refpost.get_url(),
224 238 refpost.id)
225 239 first = False
226 240
227 241 self.refmap = map_string
228 242
229 243 def get_sorted_referenced_posts(self):
230 244 return self.refmap
231 245
232 246 def is_referenced(self):
233 247 return len(self.refmap) > 0
234 248
235 249 def is_opening(self):
236 250 """
237 251 Checks if this is an opening post or just a reply.
238 252 """
239 253
240 254 return self.get_thread().get_opening_post_id() == self.id
241 255
242 256 @transaction.atomic
243 257 def add_tag(self, tag):
244 258 edit_time = timezone.now()
245 259
246 260 thread = self.get_thread()
247 261 thread.add_tag(tag)
248 262 self.last_edit_time = edit_time
249 263 self.save(update_fields=['last_edit_time'])
250 264
251 265 thread.last_edit_time = edit_time
252 266 thread.save(update_fields=['last_edit_time'])
253 267
254 268 @transaction.atomic
255 269 def remove_tag(self, tag):
256 270 edit_time = timezone.now()
257 271
258 272 thread = self.get_thread()
259 273 thread.remove_tag(tag)
260 274 self.last_edit_time = edit_time
261 275 self.save(update_fields=['last_edit_time'])
262 276
263 277 thread.last_edit_time = edit_time
264 278 thread.save(update_fields=['last_edit_time'])
265 279
266 280 def get_url(self, thread=None):
267 281 """
268 282 Gets full url to the post.
269 283 """
270 284
271 285 cache_key = CACHE_KEY_POST_URL + str(self.id)
272 286 link = cache.get(cache_key)
273 287
274 288 if not link:
275 289 if not thread:
276 290 thread = self.get_thread()
277 291
278 292 opening_id = thread.get_opening_post_id()
279 293
280 294 if self.id != opening_id:
281 295 link = reverse('thread', kwargs={
282 296 'post_id': opening_id}) + '#' + str(self.id)
283 297 else:
284 298 link = reverse('thread', kwargs={'post_id': self.id})
285 299
286 300 cache.set(cache_key, link)
287 301
288 302 return link
289 303
290 304 def get_thread(self):
291 305 """
292 306 Gets post's thread.
293 307 """
294 308
295 309 return self.thread_new
296 310
297 311 def get_referenced_posts(self):
298 312 return self.referenced_posts.only('id', 'thread_new')
299 313
300 314 def get_text(self):
301 315 return self.text
302 316
303 317 def get_view(self, moderator=False, need_open_link=False,
304 318 truncated=False, *args, **kwargs):
305 319 if 'is_opening' in kwargs:
306 320 is_opening = kwargs['is_opening']
307 321 else:
308 322 is_opening = self.is_opening()
309 323
310 324 if 'thread' in kwargs:
311 325 thread = kwargs['thread']
312 326 else:
313 327 thread = self.get_thread()
314 328
315 329 if 'can_bump' in kwargs:
316 330 can_bump = kwargs['can_bump']
317 331 else:
318 332 can_bump = thread.can_bump()
319 333
320 334 if is_opening:
321 335 opening_post_id = self.id
322 336 else:
323 337 opening_post_id = thread.get_opening_post_id()
324 338
325 339 return render_to_string('boards/post.html', {
326 340 'post': self,
327 341 'moderator': moderator,
328 342 'is_opening': is_opening,
329 343 'thread': thread,
330 344 'bumpable': can_bump,
331 345 'need_open_link': need_open_link,
332 346 'truncated': truncated,
333 347 'opening_post_id': opening_post_id,
334 348 })
335 349
336 350 def get_first_image(self):
337 351 return self.images.earliest('id')
338 352
339 353 def delete(self, using=None):
340 354 """
341 355 Deletes all post images and the post itself.
342 356 """
343 357
344 358 self.images.all().delete()
345 359
346 360 super(Post, self).delete(using)
361
362 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
363 include_last_update=False):
364 """
365 Gets post HTML or JSON data that can be rendered on a page or used by
366 API.
367 """
368
369 if format_type == DIFF_TYPE_HTML:
370 context = RequestContext(request)
371 context['post'] = self
372 if PARAMETER_TRUNCATED in request.GET:
373 context[PARAMETER_TRUNCATED] = True
374
375 return render_to_string('boards/api_post.html', context)
376 elif format_type == DIFF_TYPE_JSON:
377 post_json = {
378 'id': self.id,
379 'title': self.title,
380 'text': self.text.rendered,
381 }
382 if self.images.exists():
383 post_image = self.get_first_image()
384 post_json['image'] = post_image.image.url
385 post_json['image_preview'] = post_image.image.url_200x150
386 if include_last_update:
387 post_json['bump_time'] = datetime_to_epoch(
388 self.thread_new.bump_time)
389 return post_json
390
391 def send_to_websocket(self, request, recursive=True):
392 """
393 Sends post HTML data to the thread web socket.
394 """
395
396 if not settings.WEBSOCKETS_ENABLED:
397 return
398
399 client = Client()
400
401 channel_name = WS_CHANNEL_THREAD + str(self.get_thread().get_opening_post_id())
402 client.publish(channel_name, {
403 'html': self.get_post_data(
404 format_type=DIFF_TYPE_HTML,
405 request=request),
406 'diff_type': 'added' if recursive else 'updated',
407 })
408 client.send()
409
410 logger.info('Sent post #{} to channel {}'.format(self.id, channel_name))
411
412 if recursive:
413 for reply_number in re.finditer(REGEX_REPLY, self.text.raw):
414 post_id = reply_number.group(1)
415 ref_post = Post.objects.filter(id=post_id)[0]
416
417 ref_post.send_to_websocket(request, recursive=False) No newline at end of file
@@ -1,20 +1,22 b''
1 1 VERSION = '2.1 Aya'
2 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
6 6 MAX_TEXT_LENGTH = 30000 # Max post length in characters
7 7 MAX_IMAGE_SIZE = 8 * 1024 * 1024 # Max image size
8 8
9 9 # Thread bumplimit
10 10 MAX_POSTS_PER_THREAD = 10
11 11 # Old posts will be archived or deleted if this value is reached
12 12 MAX_THREAD_COUNT = 5
13 13 THREADS_PER_PAGE = 3
14 14 DEFAULT_THEME = 'md'
15 15 LAST_REPLIES_COUNT = 3
16 16
17 17 # Enable archiving threads instead of deletion when the thread limit is reached
18 18 ARCHIVE_THREADS = True
19 19 # Limit posting speed
20 20 LIMIT_POSTING_SPEED = False
21 # Thread update
22 WEBSOCKETS_ENABLED = True No newline at end of file
@@ -1,288 +1,278 b''
1 1 /*
2 2 @licstart The following is the entire license notice for the
3 3 JavaScript code in this page.
4 4
5 5
6 6 Copyright (C) 2013 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
10 10 General Public License (GNU GPL) as published by the Free Software
11 11 Foundation, either version 3 of the License, or (at your option)
12 12 any later version. The code is distributed WITHOUT ANY WARRANTY;
13 13 without even the implied warranty of MERCHANTABILITY or FITNESS
14 14 FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
15 15
16 16 As additional permission under GNU GPL version 3 section 7, you
17 17 may distribute non-source (e.g., minimized or compacted) forms of
18 18 that code without the copy of the GNU GPL normally required by
19 19 section 4, provided you include this license notice and a URL
20 20 through which recipients can access the Corresponding Source.
21 21
22 22 @licend The above is the entire license notice
23 23 for the JavaScript code in this page.
24 24 */
25 25
26 var THREAD_UPDATE_DELAY = 10000;
26 var wsUrl = 'ws://localhost:9090/connection/websocket';
27 var wsUser = '';
27 28
28 29 var loading = false;
29 30 var lastUpdateTime = null;
30 31 var unreadPosts = 0;
31 32
33 // Thread ID does not change, can be stored one time
34 var threadId = $('div.thread').children('.post').first().attr('id');
35
36 function connectWebsocket() {
37 var metapanel = $('.metapanel')[0];
38
39 var wsHost = metapanel.getAttribute('data-ws-host');
40 var wsPort = metapanel.getAttribute('data-ws-port');
41
42 if (wsHost.length > 0 && wsPort.length > 0)
43 var centrifuge = new Centrifuge({
44 "url": 'ws://' + wsHost + ':' + wsPort + "/connection/websocket",
45 "project": metapanel.getAttribute('data-ws-project'),
46 "user": wsUser,
47 "timestamp": metapanel.getAttribute('data-last-update'),
48 "token": metapanel.getAttribute('data-ws-token'),
49 "debug": true
50 });
51
52 centrifuge.on('error', function(error_message) {
53 alert("Error connecting to websocket server.");
54 });
55
56 centrifuge.on('connect', function() {
57 var channelName = 'thread:' + threadId;
58 centrifuge.subscribe(channelName, function(message) {
59 var postHtml = message.data['html'];
60 var isAdded = (message.data['diff_type'] === 'added');
61
62 if (postHtml) {
63 updatePost(postHtml, isAdded);
64 }
65 });
66
67 $('#autoupdate').text('[+]');
68 });
69
70 centrifuge.connect();
71 }
72
73 function updatePost(postHtml, isAdded) {
74 // This needs to be set on start because the page is scrolled after posts
75 // are added or updated
76 var bottom = isPageBottom();
77
78 var post = $(postHtml);
79
80 var threadPosts = $('div.thread').children('.post');
81
82 var lastUpdate = '';
83
84 if (isAdded) {
85 var lastPost = threadPosts.last();
86
87 post.appendTo(lastPost.parent());
88
89 updateBumplimitProgress(1);
90 showNewPostsTitle(1);
91
92 lastUpdate = post.children('.post-info').first()
93 .children('.pub_time').first().text();
94
95 if (bottom) {
96 scrollToBottom();
97 }
98 } else {
99 var postId = post.attr('id');
100
101 var oldPost = $('div.thread').children('.post[id=' + postId + ']');
102
103 oldPost.replaceWith(post);
104 }
105
106 processNewPost(post);
107 updateMetadataPanel(lastUpdate)
108 }
109
32 110 function blink(node) {
33 111 var blinkCount = 2;
34 112
35 113 var nodeToAnimate = node;
36 114 for (var i = 0; i < blinkCount; i++) {
37 115 nodeToAnimate = nodeToAnimate.fadeTo('fast', 0.5).fadeTo('fast', 1.0);
38 116 }
39 117 }
40 118
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 119 function isPageBottom() {
126 120 var scroll = $(window).scrollTop() / ($(document).height()
127 121 - $(window).height())
128 122
129 123 return scroll == 1
130 124 }
131 125
132 126 function initAutoupdate() {
133 loading = false;
134
135 lastUpdateTime = $('.metapanel').attr('data-last-update');
136
137 setInterval(updateThread, THREAD_UPDATE_DELAY);
127 connectWebsocket()
138 128 }
139 129
140 130 function getReplyCount() {
141 131 return $('.thread').children('.post').length
142 132 }
143 133
144 134 function getImageCount() {
145 135 return $('.thread').find('img').length
146 136 }
147 137
148 138 function updateMetadataPanel(lastUpdate) {
149 139 var replyCountField = $('#reply-count');
150 140 var imageCountField = $('#image-count');
151 141
152 142 replyCountField.text(getReplyCount());
153 143 imageCountField.text(getImageCount());
154 144
155 145 if (lastUpdate !== '') {
156 146 var lastUpdateField = $('#last-update');
157 147 lastUpdateField.text(lastUpdate);
158 148 blink(lastUpdateField);
159 149 }
160 150
161 151 blink(replyCountField);
162 152 blink(imageCountField);
163 153 }
164 154
165 155 /**
166 156 * Update bumplimit progress bar
167 157 */
168 158 function updateBumplimitProgress(postDelta) {
169 159 var progressBar = $('#bumplimit_progress');
170 160 if (progressBar) {
171 161 var postsToLimitElement = $('#left_to_limit');
172 162
173 163 var oldPostsToLimit = parseInt(postsToLimitElement.text());
174 164 var postCount = getReplyCount();
175 165 var bumplimit = postCount - postDelta + oldPostsToLimit;
176 166
177 167 var newPostsToLimit = bumplimit - postCount;
178 168 if (newPostsToLimit <= 0) {
179 169 $('.bar-bg').remove();
180 170 $('.thread').children('.post').addClass('dead_post');
181 171 } else {
182 172 postsToLimitElement.text(newPostsToLimit);
183 173 progressBar.width((100 - postCount / bumplimit * 100.0) + '%');
184 174 }
185 175 }
186 176 }
187 177
188 178 var documentOriginalTitle = '';
189 179 /**
190 180 * Show 'new posts' text in the title if the document is not visible to a user
191 181 */
192 182 function showNewPostsTitle(newPostCount) {
193 183 if (document.hidden) {
194 184 if (documentOriginalTitle === '') {
195 185 documentOriginalTitle = document.title;
196 186 }
197 187 unreadPosts = unreadPosts + newPostCount;
198 188 document.title = '[' + unreadPosts + '] ' + documentOriginalTitle;
199 189
200 190 document.addEventListener('visibilitychange', function() {
201 191 if (documentOriginalTitle !== '') {
202 192 document.title = documentOriginalTitle;
203 193 documentOriginalTitle = '';
204 194 unreadPosts = 0;
205 195 }
206 196
207 197 document.removeEventListener('visibilitychange', null);
208 198 });
209 199 }
210 200 }
211 201
212 202 /**
213 203 * Clear all entered values in the form fields
214 204 */
215 205 function resetForm(form) {
216 206 form.find('input:text, input:password, input:file, select, textarea').val('');
217 207 form.find('input:radio, input:checkbox')
218 208 .removeAttr('checked').removeAttr('selected');
219 209 $('.file_wrap').find('.file-thumb').remove();
220 210 }
221 211
222 212 /**
223 213 * When the form is posted, this method will be run as a callback
224 214 */
225 215 function updateOnPost(response, statusText, xhr, form) {
226 216 var json = $.parseJSON(response);
227 217 var status = json.status;
228 218
229 219 showAsErrors(form, '');
230 220
231 221 if (status === 'ok') {
232 222 resetForm(form);
233 updateThread();
234 223 } else {
235 224 var errors = json.errors;
236 225 for (var i = 0; i < errors.length; i++) {
237 226 var fieldErrors = errors[i];
238 227
239 228 var error = fieldErrors.errors;
240 229
241 230 showAsErrors(form, error);
242 231 }
243 232 }
244 233 }
245 234
246 235 /**
247 236 * Show text in the errors row of the form.
248 237 * @param form
249 238 * @param text
250 239 */
251 240 function showAsErrors(form, text) {
252 241 form.children('.form-errors').remove();
253 242
254 243 if (text.length > 0) {
255 244 var errorList = $('<div class="form-errors">' + text
256 245 + '<div>');
257 246 errorList.appendTo(form);
258 247 }
259 248 }
260 249
261 250 /**
262 251 * Run js methods that are usually run on the document, on the new post
263 252 */
264 253 function processNewPost(post) {
265 254 addRefLinkPreview(post[0]);
266 255 highlightCode(post);
256 blink(post);
267 257 }
268 258
269 259 $(document).ready(function(){
270 260 initAutoupdate();
271 261
272 262 // Post form data over AJAX
273 263 var threadId = $('div.thread').children('.post').first().attr('id');
274 264
275 265 var form = $('#form');
276 266
277 267 var options = {
278 268 beforeSubmit: function(arr, $form, options) {
279 269 showAsErrors($('form'), gettext('Sending message...'));
280 270 },
281 271 success: updateOnPost,
282 272 url: '/api/add_post/' + threadId + '/'
283 273 };
284 274
285 275 form.ajaxForm(options);
286 276
287 277 resetForm(form);
288 278 });
@@ -1,98 +1,107 b''
1 1 {% extends "boards/base.html" %}
2 2
3 3 {% load i18n %}
4 4 {% load cache %}
5 5 {% load static from staticfiles %}
6 6 {% load board %}
7 7 {% load compress %}
8 8
9 9 {% block head %}
10 10 <title>{{ opening_post.get_title|striptags|truncatewords:10 }}
11 11 - {{ site_name }}</title>
12 12 {% endblock %}
13 13
14 14 {% block content %}
15 15 {% spaceless %}
16 16 {% get_current_language as LANGUAGE_CODE %}
17 17
18 18 {% cache 600 thread_view thread.id thread.last_edit_time moderator LANGUAGE_CODE %}
19 19
20 20 <div class="image-mode-tab">
21 21 <a class="current_mode" href="{% url 'thread' opening_post.id %}">{% trans 'Normal mode' %}</a>,
22 22 <a href="{% url 'thread_mode' opening_post.id 'gallery' %}">{% trans 'Gallery mode' %}</a>
23 23 </div>
24 24
25 25 {% if bumpable %}
26 26 <div class="bar-bg">
27 27 <div class="bar-value" style="width:{{ bumplimit_progress }}%" id="bumplimit_progress">
28 28 </div>
29 29 <div class="bar-text">
30 30 <span id="left_to_limit">{{ posts_left }}</span> {% trans 'posts to bumplimit' %}
31 31 </div>
32 32 </div>
33 33 {% endif %}
34 34
35 35 <div class="thread">
36 36 {% with can_bump=thread.can_bump %}
37 37 {% for post in thread.get_replies %}
38 38 {% if forloop.first %}
39 39 {% post_view post moderator=moderator is_opening=True thread=thread can_bump=can_bump opening_post_id=opening_post.id %}
40 40 {% else %}
41 41 {% post_view post moderator=moderator is_opening=False thread=thread can_bump=can_bump opening_post_id=opening_post.id %}
42 42 {% endif %}
43 43 {% endfor %}
44 44 {% endwith %}
45 45 </div>
46 46
47 47 {% if not thread.archived %}
48 48
49 49 <div class="post-form-w" id="form">
50 50 <script src="{% static 'js/panel.js' %}"></script>
51 51 <div class="form-title">{% trans "Reply to thread" %} #{{ opening_post.id }}</div>
52 52 <div class="post-form" id="compact-form">
53 53 <div class="swappable-form-full">
54 54 <form enctype="multipart/form-data" method="post"
55 55 >{% csrf_token %}
56 56 <div class="compact-form-text"></div>
57 57 {{ form.as_div }}
58 58 <div class="form-submit">
59 59 <input type="submit" value="{% trans "Post" %}"/>
60 60 </div>
61 61 </form>
62 62 </div>
63 63 <a onclick="swapForm(); return false;" href="#">
64 64 {% trans 'Switch mode' %}
65 65 </a>
66 66 <div><a href="{% url "staticpage" name="help" %}">
67 67 {% trans 'Text syntax' %}</a></div>
68 68 </div>
69 69 </div>
70 70
71 <script src="{% static 'js/jquery.form.min.js' %}"></script>
72 <script src="{% static 'js/thread_update.js' %}"></script>
71 <script src="{% static 'js/jquery.form.min.js' %}"></script>
72 {% compress js %}
73 <script src="{% static 'js/thread_update.js' %}"></script>
74 <script src="{% static 'js/3party/centrifuge.js' %}"></script>
75 {% endcompress %}
73 76 {% endif %}
74 77
75 78 {% compress js %}
76 79 <script src="{% static 'js/form.js' %}"></script>
77 80 <script src="{% static 'js/thread.js' %}"></script>
78 81 {% endcompress %}
79 82
80 83 {% endcache %}
81 84
82 85 {% endspaceless %}
83 86 {% endblock %}
84 87
85 88 {% block metapanel %}
86 89
87 90 {% get_current_language as LANGUAGE_CODE %}
88 91
89 <span class="metapanel" data-last-update="{{ last_update }}">
92 <span class="metapanel"
93 data-last-update="{{ last_update }}"
94 data-ws-token="{{ ws_token }}"
95 data-ws-project="{{ ws_project }}"
96 data-ws-host="{{ ws_host }}"
97 data-ws-port="{{ ws_port }}">
90 98 {% cache 600 thread_meta thread.last_edit_time moderator LANGUAGE_CODE %}
99 <span id="autoupdate">[-]</span>
91 100 <span id="reply-count">{{ thread.get_reply_count }}</span>/{{ max_replies }} {% trans 'messages' %},
92 101 <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}.
93 102 {% trans 'Last update: ' %}<span id="last-update">{{ thread.last_edit_time }}</span>
94 103 [<a href="rss/">RSS</a>]
95 104 {% endcache %}
96 105 </span>
97 106
98 107 {% endblock %}
@@ -1,78 +1,92 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
9 9 from neboard import settings
10 10
11 11
12 12 KEY_CAPTCHA_FAILS = 'key_captcha_fails'
13 13 KEY_CAPTCHA_DELAY_TIME = 'key_captcha_delay_time'
14 14 KEY_CAPTCHA_LAST_ACTIVITY = 'key_captcha_last_activity'
15 15
16 16
17 17 def need_include_captcha(request):
18 18 """
19 19 Check if request is made by a user.
20 20 It contains rules which check for bots.
21 21 """
22 22
23 23 if not settings.ENABLE_CAPTCHA:
24 24 return False
25 25
26 26 enable_captcha = False
27 27
28 28 #newcomer
29 29 if KEY_CAPTCHA_LAST_ACTIVITY not in request.session:
30 30 return settings.ENABLE_CAPTCHA
31 31
32 32 last_activity = request.session[KEY_CAPTCHA_LAST_ACTIVITY]
33 33 current_delay = int(time.time()) - last_activity
34 34
35 35 delay_time = (request.session[KEY_CAPTCHA_DELAY_TIME]
36 36 if KEY_CAPTCHA_DELAY_TIME in request.session
37 37 else settings.CAPTCHA_DEFAULT_SAFE_TIME)
38 38
39 39 if current_delay < delay_time:
40 40 enable_captcha = True
41 41
42 42 return enable_captcha
43 43
44 44
45 45 def update_captcha_access(request, passed):
46 46 """
47 47 Update captcha fields.
48 48 It will reduce delay time if user passed captcha verification and
49 49 it will increase it otherwise.
50 50 """
51 51 session = request.session
52 52
53 53 delay_time = (request.session[KEY_CAPTCHA_DELAY_TIME]
54 54 if KEY_CAPTCHA_DELAY_TIME in request.session
55 55 else settings.CAPTCHA_DEFAULT_SAFE_TIME)
56 56
57 57 if passed:
58 58 delay_time -= 2 if delay_time >= 7 else 5
59 59 else:
60 60 delay_time += 10
61 61
62 62 session[KEY_CAPTCHA_LAST_ACTIVITY] = int(time.time())
63 63 session[KEY_CAPTCHA_DELAY_TIME] = delay_time
64 64
65 65
66 66 def get_client_ip(request):
67 67 x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
68 68 if x_forwarded_for:
69 69 ip = x_forwarded_for.split(',')[-1].strip()
70 70 else:
71 71 ip = request.META.get('REMOTE_ADDR')
72 72 return ip
73 73
74 74
75 75 def datetime_to_epoch(datetime):
76 76 return int(time.mktime(timezone.localtime(
77 77 datetime,timezone.get_current_timezone()).timetuple())
78 78 * 1000000 + datetime.microsecond)
79
80
81 def get_websocket_token(user_id='', timestamp=''):
82 """
83 Create token to validate information provided by new connection.
84 """
85
86 sign = hmac.new(settings.CENTRIFUGE_PROJECT_SECRET.encode())
87 sign.update(settings.CENTRIFUGE_PROJECT_ID.encode())
88 sign.update(user_id.encode())
89 sign.update(timestamp.encode())
90 token = sign.hexdigest()
91
92 return token No newline at end of file
@@ -1,139 +1,141 b''
1 1 import string
2 2
3 3 from django.db import transaction
4 4 from django.shortcuts import render, redirect
5 5
6 6 from boards import utils, settings
7 7 from boards.abstracts.paginator import get_paginator
8 8 from boards.abstracts.settingsmanager import get_settings_manager
9 9 from boards.forms import ThreadForm, PlainErrorList
10 10 from boards.models import Post, Thread, Ban, Tag
11 11 from boards.views.banned import BannedView
12 12 from boards.views.base import BaseBoardView, CONTEXT_FORM
13 13 from boards.views.posting_mixin import PostMixin
14 14
15 15 FORM_TAGS = 'tags'
16 16 FORM_TEXT = 'text'
17 17 FORM_TITLE = 'title'
18 18 FORM_IMAGE = 'image'
19 19
20 20 TAG_DELIMITER = ' '
21 21
22 22 PARAMETER_CURRENT_PAGE = 'current_page'
23 23 PARAMETER_PAGINATOR = 'paginator'
24 24 PARAMETER_THREADS = 'threads'
25 25
26 26 TEMPLATE = 'boards/posting_general.html'
27 27 DEFAULT_PAGE = 1
28 28
29 29
30 30 class AllThreadsView(PostMixin, BaseBoardView):
31 31
32 32 def __init__(self):
33 33 self.settings_manager = None
34 34 super(AllThreadsView, self).__init__()
35 35
36 36 def get(self, request, page=DEFAULT_PAGE, form=None):
37 37 context = self.get_context_data(request=request)
38 38
39 39 if not form:
40 40 form = ThreadForm(error_class=PlainErrorList)
41 41
42 42 self.settings_manager = get_settings_manager(request)
43 43 paginator = get_paginator(self.get_threads(),
44 44 settings.THREADS_PER_PAGE)
45 45 paginator.current_page = int(page)
46 46
47 47 threads = paginator.page(page).object_list
48 48
49 49 context[PARAMETER_THREADS] = threads
50 50 context[CONTEXT_FORM] = form
51 51
52 52 self._get_page_context(paginator, context, page)
53 53
54 54 return render(request, TEMPLATE, context)
55 55
56 56 def post(self, request, page=DEFAULT_PAGE):
57 57 form = ThreadForm(request.POST, request.FILES,
58 58 error_class=PlainErrorList)
59 59 form.session = request.session
60 60
61 61 if form.is_valid():
62 62 return self.create_thread(request, form)
63 63 if form.need_to_ban:
64 64 # Ban user because he is suspected to be a bot
65 65 self._ban_current_user(request)
66 66
67 67 return self.get(request, page, form)
68 68
69 69 @staticmethod
70 70 def _get_page_context(paginator, context, page):
71 71 """
72 72 Get pagination context variables
73 73 """
74 74
75 75 context[PARAMETER_PAGINATOR] = paginator
76 76 context[PARAMETER_CURRENT_PAGE] = paginator.page(int(page))
77 77
78 78 @staticmethod
79 79 def parse_tags_string(tag_strings):
80 80 """
81 81 Parses tag list string and returns tag object list.
82 82 """
83 83
84 84 tags = []
85 85
86 86 if tag_strings:
87 87 tag_strings = tag_strings.split(TAG_DELIMITER)
88 88 for tag_name in tag_strings:
89 89 tag_name = tag_name.strip().lower()
90 90 if len(tag_name) > 0:
91 91 tag, created = Tag.objects.get_or_create(name=tag_name)
92 92 tags.append(tag)
93 93
94 94 return tags
95 95
96 96 @transaction.atomic
97 97 def create_thread(self, request, form, html_response=True):
98 98 """
99 99 Creates a new thread with an opening post.
100 100 """
101 101
102 102 ip = utils.get_client_ip(request)
103 103 is_banned = Ban.objects.filter(ip=ip).exists()
104 104
105 105 if is_banned:
106 106 if html_response:
107 107 return redirect(BannedView().as_view())
108 108 else:
109 109 return
110 110
111 111 data = form.cleaned_data
112 112
113 113 title = data[FORM_TITLE]
114 114 text = data[FORM_TEXT]
115 115
116 116 text = self._remove_invalid_links(text)
117 117
118 118 if FORM_IMAGE in list(data.keys()):
119 119 image = data[FORM_IMAGE]
120 120 else:
121 121 image = None
122 122
123 123 tag_strings = data[FORM_TAGS]
124 124
125 125 tags = self.parse_tags_string(tag_strings)
126 126
127 127 post = Post.objects.create_post(title=title, text=text, image=image,
128 128 ip=ip, tags=tags)
129 # FIXME
130 post.send_to_websocket(request)
129 131
130 132 if html_response:
131 133 return redirect(post.get_url())
132 134
133 135 def get_threads(self):
134 136 """
135 137 Gets list of threads that will be shown on a page.
136 138 """
137 139
138 140 return Thread.objects.all().order_by('-bump_time')\
139 141 .exclude(tags__in=self.settings_manager.get_hidden_tags())
@@ -1,248 +1,227 b''
1 1 from datetime import datetime
2 2 import json
3 3 import logging
4 4 from django.db import transaction
5 5 from django.http import HttpResponse
6 6 from django.shortcuts import get_object_or_404, render
7 7 from django.template import RequestContext
8 8 from django.utils import timezone
9 9 from django.core import serializers
10 10 from django.template.loader import render_to_string
11 11
12 12 from boards.forms import PostForm, PlainErrorList
13 13 from boards.models import Post, Thread, Tag
14 14 from boards.utils import datetime_to_epoch
15 15 from boards.views.thread import ThreadView
16 16
17 17 __author__ = 'neko259'
18 18
19 19 PARAMETER_TRUNCATED = 'truncated'
20 20 PARAMETER_TAG = 'tag'
21 21 PARAMETER_OFFSET = 'offset'
22 22 PARAMETER_DIFF_TYPE = 'type'
23 23
24 24 DIFF_TYPE_HTML = 'html'
25 25 DIFF_TYPE_JSON = 'json'
26 26
27 27 STATUS_OK = 'ok'
28 28 STATUS_ERROR = 'error'
29 29
30 30 logger = logging.getLogger(__name__)
31 31
32 32
33 33 @transaction.atomic
34 34 def api_get_threaddiff(request, thread_id, last_update_time):
35 35 """
36 36 Gets posts that were changed or added since time
37 37 """
38 38
39 39 thread = get_object_or_404(Post, id=thread_id).get_thread()
40 40
41 41 # Add 1 to ensure we don't load the same post over and over
42 42 last_update_timestamp = float(last_update_time) + 1
43 43
44 44 filter_time = datetime.fromtimestamp(last_update_timestamp / 1000000,
45 45 timezone.get_current_timezone())
46 46
47 47 json_data = {
48 48 'added': [],
49 49 'updated': [],
50 50 'last_update': None,
51 51 }
52 52 added_posts = Post.objects.filter(thread_new=thread,
53 53 pub_time__gt=filter_time) \
54 54 .order_by('pub_time')
55 55 updated_posts = Post.objects.filter(thread_new=thread,
56 56 pub_time__lte=filter_time,
57 57 last_edit_time__gt=filter_time)
58 58
59 59 diff_type = DIFF_TYPE_HTML
60 60 if PARAMETER_DIFF_TYPE in request.GET:
61 61 diff_type = request.GET[PARAMETER_DIFF_TYPE]
62 62
63 63 for post in added_posts:
64 json_data['added'].append(_get_post_data(post.id, diff_type, request))
64 json_data['added'].append(get_post_data(post.id, diff_type, request))
65 65 for post in updated_posts:
66 json_data['updated'].append(_get_post_data(post.id, diff_type, request))
66 json_data['updated'].append(get_post_data(post.id, diff_type, request))
67 67 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
68 68
69 69 return HttpResponse(content=json.dumps(json_data))
70 70
71 71
72 72 def api_add_post(request, opening_post_id):
73 73 """
74 74 Adds a post and return the JSON response for it
75 75 """
76 76
77 77 opening_post = get_object_or_404(Post, id=opening_post_id)
78 78
79 79 logger.info('Adding post via api...')
80 80
81 81 status = STATUS_OK
82 82 errors = []
83 83
84 84 if request.method == 'POST':
85 85 form = PostForm(request.POST, request.FILES, error_class=PlainErrorList)
86 86 form.session = request.session
87 87
88 88 if form.need_to_ban:
89 89 # Ban user because he is suspected to be a bot
90 90 # _ban_current_user(request)
91 91 status = STATUS_ERROR
92 92 if form.is_valid():
93 93 post = ThreadView().new_post(request, form, opening_post,
94 94 html_response=False)
95 95 if not post:
96 96 status = STATUS_ERROR
97 97 else:
98 98 logger.info('Added post #%d via api.' % post.id)
99 99 else:
100 100 status = STATUS_ERROR
101 101 errors = form.as_json_errors()
102 102
103 103 response = {
104 104 'status': status,
105 105 'errors': errors,
106 106 }
107 107
108 108 return HttpResponse(content=json.dumps(response))
109 109
110 110
111 111 def get_post(request, post_id):
112 112 """
113 113 Gets the html of a post. Used for popups. Post can be truncated if used
114 114 in threads list with 'truncated' get parameter.
115 115 """
116 116
117 117 logger.info('Getting post #%s' % post_id)
118 118
119 119 post = get_object_or_404(Post, id=post_id)
120 120
121 121 context = RequestContext(request)
122 122 context['post'] = post
123 123 if PARAMETER_TRUNCATED in request.GET:
124 124 context[PARAMETER_TRUNCATED] = True
125 125
126 126 return render(request, 'boards/api_post.html', context)
127 127
128 128
129 129 # TODO Test this
130 130 def api_get_threads(request, count):
131 131 """
132 132 Gets the JSON thread opening posts list.
133 133 Parameters that can be used for filtering:
134 134 tag, offset (from which thread to get results)
135 135 """
136 136
137 137 if PARAMETER_TAG in request.GET:
138 138 tag_name = request.GET[PARAMETER_TAG]
139 139 if tag_name is not None:
140 140 tag = get_object_or_404(Tag, name=tag_name)
141 141 threads = tag.threads.filter(archived=False)
142 142 else:
143 143 threads = Thread.objects.filter(archived=False)
144 144
145 145 if PARAMETER_OFFSET in request.GET:
146 146 offset = request.GET[PARAMETER_OFFSET]
147 147 offset = int(offset) if offset is not None else 0
148 148 else:
149 149 offset = 0
150 150
151 151 threads = threads.order_by('-bump_time')
152 152 threads = threads[offset:offset + int(count)]
153 153
154 154 opening_posts = []
155 155 for thread in threads:
156 156 opening_post = thread.get_opening_post()
157 157
158 158 # TODO Add tags, replies and images count
159 opening_posts.append(_get_post_data(opening_post.id,
159 opening_posts.append(get_post_data(opening_post.id,
160 160 include_last_update=True))
161 161
162 162 return HttpResponse(content=json.dumps(opening_posts))
163 163
164 164
165 165 # TODO Test this
166 166 def api_get_tags(request):
167 167 """
168 168 Gets all tags or user tags.
169 169 """
170 170
171 171 # TODO Get favorite tags for the given user ID
172 172
173 173 tags = Tag.objects.get_not_empty_tags()
174 174 tag_names = []
175 175 for tag in tags:
176 176 tag_names.append(tag.name)
177 177
178 178 return HttpResponse(content=json.dumps(tag_names))
179 179
180 180
181 181 # TODO The result can be cached by the thread last update time
182 182 # TODO Test this
183 183 def api_get_thread_posts(request, opening_post_id):
184 184 """
185 185 Gets the JSON array of thread posts
186 186 """
187 187
188 188 opening_post = get_object_or_404(Post, id=opening_post_id)
189 189 thread = opening_post.get_thread()
190 190 posts = thread.get_replies()
191 191
192 192 json_data = {
193 193 'posts': [],
194 194 'last_update': None,
195 195 }
196 196 json_post_list = []
197 197
198 198 for post in posts:
199 json_post_list.append(_get_post_data(post.id))
199 json_post_list.append(get_post_data(post.id))
200 200 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
201 201 json_data['posts'] = json_post_list
202 202
203 203 return HttpResponse(content=json.dumps(json_data))
204 204
205 205
206 206 def api_get_post(request, post_id):
207 207 """
208 208 Gets the JSON of a post. This can be
209 209 used as and API for external clients.
210 210 """
211 211
212 212 post = get_object_or_404(Post, id=post_id)
213 213
214 214 json = serializers.serialize("json", [post], fields=(
215 215 "pub_time", "_text_rendered", "title", "text", "image",
216 216 "image_width", "image_height", "replies", "tags"
217 217 ))
218 218
219 219 return HttpResponse(content=json)
220 220
221 221
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
222 # TODO Remove this method and use post method directly
223 def get_post_data(post_id, format_type=DIFF_TYPE_JSON, request=None,
224 include_last_update=False):
225 post = get_object_or_404(Post, id=post_id)
226 return post.get_post_data(format_type=format_type, request=request,
227 include_last_update=include_last_update)
@@ -1,142 +1,155 b''
1 1 from django.core.urlresolvers import reverse
2 2 from django.db import transaction
3 3 from django.http import Http404
4 4 from django.shortcuts import get_object_or_404, render, redirect
5 5 from django.views.generic.edit import FormMixin
6 6
7 7 from boards import utils, settings
8 8 from boards.forms import PostForm, PlainErrorList
9 9 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'
16 17
17 18 CONTEXT_POSTS = 'posts'
18 19 CONTEXT_OP = 'opening_post'
19 20 CONTEXT_BUMPLIMIT_PRG = 'bumplimit_progress'
20 21 CONTEXT_POSTS_LEFT = 'posts_left'
21 22 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'
28 33 FORM_IMAGE = 'image'
29 34
30 35 MODE_GALLERY = 'gallery'
31 36 MODE_NORMAL = 'normal'
32 37
33 38
34 39 class ThreadView(BaseBoardView, PostMixin, FormMixin):
35 40
36 41 def get(self, request, post_id, mode=MODE_NORMAL, form=None):
37 42 try:
38 43 opening_post = Post.objects.filter(id=post_id).only('thread_new')[0]
39 44 except IndexError:
40 45 raise Http404
41 46
42 47 # If this is not OP, don't show it as it is
43 48 if not opening_post or not opening_post.is_opening():
44 49 raise Http404
45 50
46 51 if not form:
47 52 form = PostForm(error_class=PlainErrorList)
48 53
49 54 thread_to_show = opening_post.get_thread()
50 55
51 56 context = self.get_context_data(request=request)
52 57
53 58 context[CONTEXT_FORM] = form
54 context[CONTEXT_LASTUPDATE] = utils.datetime_to_epoch(
55 thread_to_show.last_edit_time)
59 context[CONTEXT_LASTUPDATE] = str(utils.datetime_to_epoch(
60 thread_to_show.last_edit_time))
56 61 context[CONTEXT_THREAD] = thread_to_show
57 62 context[CONTEXT_MAX_REPLIES] = settings.MAX_POSTS_PER_THREAD
58 63
64 if settings.WEBSOCKETS_ENABLED:
65 context[CONTEXT_WS_TOKEN] = utils.get_websocket_token(
66 timestamp=context[CONTEXT_LASTUPDATE])
67 context[CONTEXT_WS_PROJECT] = neboard.settings.CENTRIFUGE_PROJECT_ID
68 context[CONTEXT_WS_HOST] = request.get_host().split(':')[0]
69 context[CONTEXT_WS_PORT] = neboard.settings.CENTRIFUGE_PORT
70
59 71 if MODE_NORMAL == mode:
60 72 bumpable = thread_to_show.can_bump()
61 73 context[CONTEXT_BUMPABLE] = bumpable
62 74 if bumpable:
63 75 left_posts = settings.MAX_POSTS_PER_THREAD \
64 76 - thread_to_show.get_reply_count()
65 77 context[CONTEXT_POSTS_LEFT] = left_posts
66 78 context[CONTEXT_BUMPLIMIT_PRG] = str(
67 79 float(left_posts) / settings.MAX_POSTS_PER_THREAD * 100)
68 80
69 81 context[CONTEXT_OP] = opening_post
70 82
71 83 document = TEMPLATE_NORMAL
72 84 elif MODE_GALLERY == mode:
73 85 context[CONTEXT_POSTS] = thread_to_show.get_replies_with_images(
74 86 view_fields_only=True)
75 87
76 88 document = TEMPLATE_GALLERY
77 89 else:
78 90 raise Http404
79 91
80 92 return render(request, document, context)
81 93
82 94 def post(self, request, post_id, mode=MODE_NORMAL):
83 95 opening_post = get_object_or_404(Post, id=post_id)
84 96
85 97 # If this is not OP, don't show it as it is
86 98 if not opening_post.is_opening():
87 99 raise Http404
88 100
89 101 if not opening_post.get_thread().archived:
90 102 form = PostForm(request.POST, request.FILES,
91 103 error_class=PlainErrorList)
92 104 form.session = request.session
93 105
94 106 if form.is_valid():
95 107 return self.new_post(request, form, opening_post)
96 108 if form.need_to_ban:
97 109 # Ban user because he is suspected to be a bot
98 110 self._ban_current_user(request)
99 111
100 112 return self.get(request, post_id, mode, form)
101 113
102 114 @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 119 is_banned = Ban.objects.filter(ip=ip).exists()
108 120
109 121 if is_banned:
110 122 if html_response:
111 123 return redirect(BannedView().as_view())
112 124 else:
113 125 return None
114 126
115 127 data = form.cleaned_data
116 128
117 129 title = data[FORM_TITLE]
118 130 text = data[FORM_TEXT]
119 131
120 132 text = self._remove_invalid_links(text)
121 133
122 134 if FORM_IMAGE in list(data.keys()):
123 135 image = data[FORM_IMAGE]
124 136 else:
125 137 image = None
126 138
127 139 tags = []
128 140
129 141 post_thread = opening_post.get_thread()
130 142
131 143 post = Post.objects.create_post(title=title, text=text, image=image,
132 144 thread=post_thread, ip=ip, tags=tags)
145 post.send_to_websocket(request)
133 146
134 147 thread_to_show = (opening_post.id if opening_post else post.id)
135 148
136 149 if html_response:
137 150 if opening_post:
138 151 return redirect(
139 152 reverse('thread', kwargs={'post_id': thread_to_show})
140 153 + '#' + str(post.id))
141 154 else:
142 155 return post
@@ -1,239 +1,245 b''
1 1 # Django settings for neboard project.
2 2 import os
3 3 from boards.mdx_neboard import bbcode_extended
4 4
5 5 DEBUG = True
6 6 TEMPLATE_DEBUG = DEBUG
7 7
8 8 ADMINS = (
9 9 # ('Your Name', 'your_email@example.com'),
10 10 ('admin', 'admin@example.com')
11 11 )
12 12
13 13 MANAGERS = ADMINS
14 14
15 15 DATABASES = {
16 16 'default': {
17 17 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
18 18 'NAME': 'database.db', # Or path to database file if using sqlite3.
19 19 'USER': '', # Not used with sqlite3.
20 20 'PASSWORD': '', # Not used with sqlite3.
21 21 'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
22 22 'PORT': '', # Set to empty string for default. Not used with sqlite3.
23 23 'CONN_MAX_AGE': None,
24 24 }
25 25 }
26 26
27 27 # Local time zone for this installation. Choices can be found here:
28 28 # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
29 29 # although not all choices may be available on all operating systems.
30 30 # In a Windows environment this must be set to your system time zone.
31 31 TIME_ZONE = 'Europe/Kiev'
32 32
33 33 # Language code for this installation. All choices can be found here:
34 34 # http://www.i18nguy.com/unicode/language-identifiers.html
35 35 LANGUAGE_CODE = 'en'
36 36
37 37 SITE_ID = 1
38 38
39 39 # If you set this to False, Django will make some optimizations so as not
40 40 # to load the internationalization machinery.
41 41 USE_I18N = True
42 42
43 43 # If you set this to False, Django will not format dates, numbers and
44 44 # calendars according to the current locale.
45 45 USE_L10N = True
46 46
47 47 # If you set this to False, Django will not use timezone-aware datetimes.
48 48 USE_TZ = True
49 49
50 50 # Absolute filesystem path to the directory that will hold user-uploaded files.
51 51 # Example: "/home/media/media.lawrence.com/media/"
52 52 MEDIA_ROOT = './media/'
53 53
54 54 # URL that handles the media served from MEDIA_ROOT. Make sure to use a
55 55 # trailing slash.
56 56 # Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
57 57 MEDIA_URL = '/media/'
58 58
59 59 # Absolute path to the directory static files should be collected to.
60 60 # Don't put anything in this directory yourself; store your static files
61 61 # in apps' "static/" subdirectories and in STATICFILES_DIRS.
62 62 # Example: "/home/media/media.lawrence.com/static/"
63 63 STATIC_ROOT = ''
64 64
65 65 # URL prefix for static files.
66 66 # Example: "http://media.lawrence.com/static/"
67 67 STATIC_URL = '/static/'
68 68
69 69 # Additional locations of static files
70 70 # It is really a hack, put real paths, not related
71 71 STATICFILES_DIRS = (
72 72 os.path.dirname(__file__) + '/boards/static',
73 73
74 74 # '/d/work/python/django/neboard/neboard/boards/static',
75 75 # Put strings here, like "/home/html/static" or "C:/www/django/static".
76 76 # Always use forward slashes, even on Windows.
77 77 # Don't forget to use absolute paths, not relative paths.
78 78 )
79 79
80 80 # List of finder classes that know how to find static files in
81 81 # various locations.
82 82 STATICFILES_FINDERS = (
83 83 'django.contrib.staticfiles.finders.FileSystemFinder',
84 84 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
85 85 'compressor.finders.CompressorFinder',
86 86 )
87 87
88 88 if DEBUG:
89 89 STATICFILES_STORAGE = \
90 90 'django.contrib.staticfiles.storage.StaticFilesStorage'
91 91 else:
92 92 STATICFILES_STORAGE = \
93 93 'django.contrib.staticfiles.storage.CachedStaticFilesStorage'
94 94
95 95 # Make this unique, and don't share it with anybody.
96 96 SECRET_KEY = '@1rc$o(7=tt#kd+4s$u6wchm**z^)4x90)7f6z(i&amp;55@o11*8o'
97 97
98 98 # List of callables that know how to import templates from various sources.
99 99 TEMPLATE_LOADERS = (
100 100 'django.template.loaders.filesystem.Loader',
101 101 'django.template.loaders.app_directories.Loader',
102 102 )
103 103
104 104 TEMPLATE_CONTEXT_PROCESSORS = (
105 105 'django.core.context_processors.media',
106 106 'django.core.context_processors.static',
107 107 'django.core.context_processors.request',
108 108 'django.contrib.auth.context_processors.auth',
109 109 'boards.context_processors.user_and_ui_processor',
110 110 )
111 111
112 112 MIDDLEWARE_CLASSES = (
113 113 'django.contrib.sessions.middleware.SessionMiddleware',
114 114 'django.middleware.locale.LocaleMiddleware',
115 115 'django.middleware.common.CommonMiddleware',
116 116 'django.contrib.auth.middleware.AuthenticationMiddleware',
117 117 'django.contrib.messages.middleware.MessageMiddleware',
118 118 'boards.middlewares.BanMiddleware',
119 119 'boards.middlewares.MinifyHTMLMiddleware',
120 120 )
121 121
122 122 ROOT_URLCONF = 'neboard.urls'
123 123
124 124 # Python dotted path to the WSGI application used by Django's runserver.
125 125 WSGI_APPLICATION = 'neboard.wsgi.application'
126 126
127 127 TEMPLATE_DIRS = (
128 128 # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
129 129 # Always use forward slashes, even on Windows.
130 130 # Don't forget to use absolute paths, not relative paths.
131 131 'templates',
132 132 )
133 133
134 134 INSTALLED_APPS = (
135 135 'django.contrib.auth',
136 136 'django.contrib.contenttypes',
137 137 'django.contrib.sessions',
138 138 # 'django.contrib.sites',
139 139 'django.contrib.messages',
140 140 'django.contrib.staticfiles',
141 141 # Uncomment the next line to enable the admin:
142 142 'django.contrib.admin',
143 143 # Uncomment the next line to enable admin documentation:
144 144 # 'django.contrib.admindocs',
145 145 'django.contrib.humanize',
146 146 'django_cleanup',
147 147
148 148 # Migrations
149 149 'south',
150 150 'debug_toolbar',
151 151
152 152 # Search
153 153 'haystack',
154 154
155 155 # Static files compressor
156 156 'compressor',
157 157
158 158 'boards',
159 159 )
160 160
161 161 # A sample logging configuration. The only tangible logging
162 162 # performed by this configuration is to send an email to
163 163 # the site admins on every HTTP 500 error when DEBUG=False.
164 164 # See http://docs.djangoproject.com/en/dev/topics/logging for
165 165 # more details on how to customize your logging configuration.
166 166 LOGGING = {
167 167 'version': 1,
168 168 'disable_existing_loggers': False,
169 169 'formatters': {
170 170 'verbose': {
171 171 'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s'
172 172 },
173 173 'simple': {
174 174 'format': '%(levelname)s %(asctime)s [%(module)s] %(message)s'
175 175 },
176 176 },
177 177 'filters': {
178 178 'require_debug_false': {
179 179 '()': 'django.utils.log.RequireDebugFalse'
180 180 }
181 181 },
182 182 'handlers': {
183 183 'console': {
184 184 'level': 'DEBUG',
185 185 'class': 'logging.StreamHandler',
186 186 'formatter': 'simple'
187 187 },
188 188 },
189 189 'loggers': {
190 190 'boards': {
191 191 'handlers': ['console'],
192 192 'level': 'DEBUG',
193 193 }
194 194 },
195 195 }
196 196
197 197 HAYSTACK_CONNECTIONS = {
198 198 'default': {
199 199 'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine',
200 200 'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'),
201 201 },
202 202 }
203 203
204 204 MARKUP_FIELD_TYPES = (
205 205 ('bbcode', bbcode_extended),
206 206 )
207 207
208 208 THEMES = [
209 209 ('md', 'Mystic Dark'),
210 210 ('md_centered', 'Mystic Dark (centered)'),
211 211 ('sw', 'Snow White'),
212 212 ('pg', 'Photon Gray'),
213 213 ]
214 214
215 POPULAR_TAGS = 10
216
217 215 POSTING_DELAY = 20 # seconds
218 216
219 COMPRESS_HTML = True
217 COMPRESS_HTML = False
218
219 # Websocket settins
220 CENTRIFUGE_HOST = 'localhost'
221 CENTRIFUGE_PORT = '9090'
222
223 CENTRIFUGE_ADDRESS = 'http://{}:{}'.format(CENTRIFUGE_HOST, CENTRIFUGE_PORT)
224 CENTRIFUGE_PROJECT_ID = '<project id here>'
225 CENTRIFUGE_PROJECT_SECRET = '<project secret here>'
226 CENTRIFUGE_TIMEOUT = 5
220 227
221 228 # Debug mode middlewares
222 229 if DEBUG:
223 230 MIDDLEWARE_CLASSES += (
224 231 'debug_toolbar.middleware.DebugToolbarMiddleware',
225 232 )
226 233
227 234 def custom_show_toolbar(request):
228 235 return False
229 236
230 237 DEBUG_TOOLBAR_CONFIG = {
231 238 'ENABLE_STACKTRACES': True,
232 239 'SHOW_TOOLBAR_CALLBACK': 'neboard.settings.custom_show_toolbar',
233 240 }
234 241
235 242 # FIXME Uncommenting this fails somehow. Need to investigate this
236 243 #DEBUG_TOOLBAR_PANELS += (
237 244 # 'debug_toolbar.panels.profiling.ProfilingDebugPanel',
238 #)
239
245 #) No newline at end of file
@@ -1,8 +1,9 b''
1 adjacent
1 2 south>=0.8.4
2 3 haystack
3 4 pillow
4 5 django>=1.6
5 6 django_cleanup
6 7 django-markupfield
7 8 bbcode
8 9 django_compressor No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now