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">>>%s</a>' % (refpost.get_url(), |
|
237 | map_string += '<a href="%s">>>%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( |
|
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( |
|
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( |
|
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( |
|
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 |
|
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&55@o11*8o' |
|
96 | SECRET_KEY = '@1rc$o(7=tt#kd+4s$u6wchm**z^)4x90)7f6z(i&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 = |
|
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 |
|
General Comments 0
You need to be logged in to leave comments.
Login now