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