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