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 |
@@ -1,18 +1,22 b'' | |||
|
1 | 1 | bc8fce57a613175450b8b6d933cdd85f22c04658 1.1 |
|
2 | 2 | 784258eb652c563c288ca7652c33f52cd4733d83 1.1-stable |
|
3 | 3 | 1b53a22467a8fccc798935d7a26efe78e4bc7b25 1.2-stable |
|
4 | 4 | 1713fb7543386089e364c39703b79e57d3d851f0 1.3 |
|
5 | 5 | 80f183ebbe132ea8433eacae9431360f31fe7083 1.4 |
|
6 | 6 | 4330ff5a2bf6c543d8aaae8a43de1dc062f3bd13 1.4.1 |
|
7 | 7 | 8531d7b001392289a6b761f38c73a257606552ad 1.5 |
|
8 | 8 | 78e843c8b04b5a81cee5aa24601e305fae75da24 1.5.1 |
|
9 | 9 | 4f92838730ed9aa1d17651bbcdca19a097fd0c37 1.6 |
|
10 | 10 | 4bac2f37ea463337ddd27f98e7985407a74de504 1.7 |
|
11 | 11 | 1c4febea92c6503ae557fba73b2768659ae90d24 1.7.1 |
|
12 | 12 | 56a4a4578fc454ee455e33dd74a2cc82234bcb59 1.7.2 |
|
13 | 13 | 34d6f3d5deb22be56b6c1512ec654bd7f6e03bcc 1.7.3 |
|
14 | 14 | f5cca33d29c673b67d43f310bebc4e3a21c6d04c 1.7.4 |
|
15 | 15 | 7f7c33ba6e3f3797ca866c5ed5d358a2393f1371 1.8 |
|
16 | 16 | a6b9dd9547bdc17b681502efcceb17aa5c09adf4 1.8.1 |
|
17 | 17 | 8318fa1615d1946e4519f5735ae880909521990d 2.0 |
|
18 | 18 | e23590ee7e2067a3f0fc3cbcfd66404b47127feb 2.1 |
|
19 | 4d998aba79e4abf0a2e78e93baaa2c2800b1c49c 2.2 | |
|
20 | 07fdef4ac33a859250d03f17c594089792bca615 2.2.1 | |
|
21 | bcc74d45f060ecd3ff06ff448165aea0d026cb3e 2.2.2 | |
|
22 | b0e629ff24eb47a449ecfb455dc6cc600d18c9e2 2.2.3 |
@@ -1,146 +1,147 b'' | |||
|
1 | 1 | from django.shortcuts import get_object_or_404 |
|
2 | 2 | from boards.models import Tag |
|
3 | 3 | |
|
4 | 4 | __author__ = 'neko259' |
|
5 | 5 | |
|
6 | 6 | SESSION_SETTING = 'setting' |
|
7 | 7 | |
|
8 | # Remove this, it is not used any more cause there is a user's permission | |
|
8 | 9 | PERMISSION_MODERATE = 'moderator' |
|
9 | 10 | |
|
10 | 11 | SETTING_THEME = 'theme' |
|
11 | 12 | SETTING_FAVORITE_TAGS = 'favorite_tags' |
|
12 | 13 | SETTING_HIDDEN_TAGS = 'hidden_tags' |
|
13 | 14 | SETTING_PERMISSIONS = 'permissions' |
|
14 | 15 | |
|
15 | 16 | DEFAULT_THEME = 'md' |
|
16 | 17 | |
|
17 | 18 | |
|
18 | 19 | def get_settings_manager(request): |
|
19 | 20 | """ |
|
20 | 21 | Get settings manager based on the request object. Currently only |
|
21 | 22 | session-based manager is supported. In the future, cookie-based or |
|
22 | 23 | database-based managers could be implemented. |
|
23 | 24 | """ |
|
24 | 25 | return SessionSettingsManager(request.session) |
|
25 | 26 | |
|
26 | 27 | |
|
27 | 28 | class SettingsManager: |
|
28 | 29 | """ |
|
29 | 30 | Base settings manager class. get_setting and set_setting methods should |
|
30 | 31 | be overriden. |
|
31 | 32 | """ |
|
32 | 33 | def __init__(self): |
|
33 | 34 | pass |
|
34 | 35 | |
|
35 | 36 | def get_theme(self): |
|
36 | 37 | theme = self.get_setting(SETTING_THEME) |
|
37 | 38 | if not theme: |
|
38 | 39 | theme = DEFAULT_THEME |
|
39 | 40 | self.set_setting(SETTING_THEME, theme) |
|
40 | 41 | |
|
41 | 42 | return theme |
|
42 | 43 | |
|
43 | 44 | def set_theme(self, theme): |
|
44 | 45 | self.set_setting(SETTING_THEME, theme) |
|
45 | 46 | |
|
46 | 47 | def has_permission(self, permission): |
|
47 | 48 | permissions = self.get_setting(SETTING_PERMISSIONS) |
|
48 | 49 | if permissions: |
|
49 | 50 | return permission in permissions |
|
50 | 51 | else: |
|
51 | 52 | return False |
|
52 | 53 | |
|
53 | 54 | def get_setting(self, setting): |
|
54 | 55 | pass |
|
55 | 56 | |
|
56 | 57 | def set_setting(self, setting, value): |
|
57 | 58 | pass |
|
58 | 59 | |
|
59 | 60 | def add_permission(self, permission): |
|
60 | 61 | permissions = self.get_setting(SETTING_PERMISSIONS) |
|
61 | 62 | if not permissions: |
|
62 | 63 | permissions = [permission] |
|
63 | 64 | else: |
|
64 | 65 | permissions.append(permission) |
|
65 | 66 | self.set_setting(SETTING_PERMISSIONS, permissions) |
|
66 | 67 | |
|
67 | 68 | def del_permission(self, permission): |
|
68 | 69 | permissions = self.get_setting(SETTING_PERMISSIONS) |
|
69 | 70 | if not permissions: |
|
70 | 71 | permissions = [] |
|
71 | 72 | else: |
|
72 | 73 | permissions.remove(permission) |
|
73 | 74 | self.set_setting(SETTING_PERMISSIONS, permissions) |
|
74 | 75 | |
|
75 | 76 | def get_fav_tags(self): |
|
76 | 77 | tag_names = self.get_setting(SETTING_FAVORITE_TAGS) |
|
77 | 78 | tags = [] |
|
78 | 79 | if tag_names: |
|
79 | 80 | for tag_name in tag_names: |
|
80 | 81 | tag = get_object_or_404(Tag, name=tag_name) |
|
81 | 82 | tags.append(tag) |
|
82 | 83 | return tags |
|
83 | 84 | |
|
84 | 85 | def add_fav_tag(self, tag): |
|
85 | 86 | tags = self.get_setting(SETTING_FAVORITE_TAGS) |
|
86 | 87 | if not tags: |
|
87 | 88 | tags = [tag.name] |
|
88 | 89 | else: |
|
89 | 90 | if not tag.name in tags: |
|
90 | 91 | tags.append(tag.name) |
|
91 | 92 | |
|
92 | 93 | tags.sort() |
|
93 | 94 | self.set_setting(SETTING_FAVORITE_TAGS, tags) |
|
94 | 95 | |
|
95 | 96 | def del_fav_tag(self, tag): |
|
96 | 97 | tags = self.get_setting(SETTING_FAVORITE_TAGS) |
|
97 | 98 | if tag.name in tags: |
|
98 | 99 | tags.remove(tag.name) |
|
99 | 100 | self.set_setting(SETTING_FAVORITE_TAGS, tags) |
|
100 | 101 | |
|
101 | 102 | def get_hidden_tags(self): |
|
102 | 103 | tag_names = self.get_setting(SETTING_HIDDEN_TAGS) |
|
103 | 104 | tags = [] |
|
104 | 105 | if tag_names: |
|
105 | 106 | for tag_name in tag_names: |
|
106 | 107 | tag = get_object_or_404(Tag, name=tag_name) |
|
107 | 108 | tags.append(tag) |
|
108 | 109 | |
|
109 | 110 | return tags |
|
110 | 111 | |
|
111 | 112 | def add_hidden_tag(self, tag): |
|
112 | 113 | tags = self.get_setting(SETTING_HIDDEN_TAGS) |
|
113 | 114 | if not tags: |
|
114 | 115 | tags = [tag.name] |
|
115 | 116 | else: |
|
116 | 117 | if not tag.name in tags: |
|
117 | 118 | tags.append(tag.name) |
|
118 | 119 | |
|
119 | 120 | tags.sort() |
|
120 | 121 | self.set_setting(SETTING_HIDDEN_TAGS, tags) |
|
121 | 122 | |
|
122 | 123 | def del_hidden_tag(self, tag): |
|
123 | 124 | tags = self.get_setting(SETTING_HIDDEN_TAGS) |
|
124 | 125 | if tag.name in tags: |
|
125 | 126 | tags.remove(tag.name) |
|
126 | 127 | self.set_setting(SETTING_HIDDEN_TAGS, tags) |
|
127 | 128 | |
|
128 | 129 | |
|
129 | 130 | class SessionSettingsManager(SettingsManager): |
|
130 | 131 | """ |
|
131 | 132 | Session-based settings manager. All settings are saved to the user's |
|
132 | 133 | session. |
|
133 | 134 | """ |
|
134 | 135 | def __init__(self, session): |
|
135 | 136 | SettingsManager.__init__(self) |
|
136 | 137 | self.session = session |
|
137 | 138 | |
|
138 | 139 | def get_setting(self, setting): |
|
139 | 140 | if setting in self.session: |
|
140 | 141 | return self.session[setting] |
|
141 | 142 | else: |
|
142 | 143 | return None |
|
143 | 144 | |
|
144 | 145 | def set_setting(self, setting, value): |
|
145 | 146 | self.session[setting] = value |
|
146 | 147 |
@@ -1,43 +1,54 b'' | |||
|
1 | 1 | from django.contrib import admin |
|
2 | 2 | from boards.models import Post, Tag, Ban, Thread, KeyPair |
|
3 | 3 | |
|
4 | 4 | |
|
5 | @admin.register(Post) | |
|
5 | 6 | class PostAdmin(admin.ModelAdmin): |
|
6 | 7 | |
|
7 | 8 | list_display = ('id', 'title', 'text') |
|
8 | 9 | list_filter = ('pub_time', 'thread_new') |
|
9 | 10 | search_fields = ('id', 'title', 'text') |
|
11 | exclude = ('referenced_posts', 'refmap') | |
|
12 | readonly_fields = ('poster_ip', 'thread_new') | |
|
10 | 13 | |
|
11 | 14 | |
|
15 | @admin.register(Tag) | |
|
12 | 16 | class TagAdmin(admin.ModelAdmin): |
|
13 | 17 | |
|
14 | list_display = ('name',) | |
|
18 | def thread_count(self, obj: Tag) -> int: | |
|
19 | return obj.get_thread_count() | |
|
15 | 20 | |
|
21 | list_display = ('name', 'thread_count') | |
|
22 | search_fields = ('name',) | |
|
23 | ||
|
24 | ||
|
25 | @admin.register(Thread) | |
|
16 | 26 | class ThreadAdmin(admin.ModelAdmin): |
|
17 | 27 | |
|
18 | def title(self, obj): | |
|
19 | return obj.get_opening_post().title | |
|
28 | def title(self, obj: Thread) -> str: | |
|
29 | return obj.get_opening_post().get_title() | |
|
20 | 30 | |
|
21 | def reply_count(self, obj): | |
|
31 | def reply_count(self, obj: Thread) -> int: | |
|
22 | 32 | return obj.get_reply_count() |
|
23 | 33 | |
|
24 | list_display = ('id', 'title', 'reply_count', 'archived') | |
|
25 | list_filter = ('bump_time', 'archived') | |
|
34 | def ip(self, obj: Thread): | |
|
35 | return obj.get_opening_post().poster_ip | |
|
36 | ||
|
37 | list_display = ('id', 'title', 'reply_count', 'archived', 'ip') | |
|
38 | list_filter = ('bump_time', 'archived', 'bumpable') | |
|
26 | 39 | search_fields = ('id', 'title') |
|
40 | filter_horizontal = ('tags',) | |
|
27 | 41 | |
|
28 | 42 | |
|
43 | @admin.register(KeyPair) | |
|
29 | 44 | class KeyPairAdmin(admin.ModelAdmin): |
|
30 | 45 | list_display = ('public_key', 'primary') |
|
31 | 46 | list_filter = ('primary',) |
|
32 | 47 | search_fields = ('public_key',) |
|
33 | 48 | |
|
49 | ||
|
50 | @admin.register(Ban) | |
|
34 | 51 | class BanAdmin(admin.ModelAdmin): |
|
35 | 52 | list_display = ('ip', 'can_read') |
|
36 | 53 | list_filter = ('can_read',) |
|
37 | 54 | search_fields = ('ip',) |
|
38 | ||
|
39 | admin.site.register(Post, PostAdmin) | |
|
40 | admin.site.register(Tag, TagAdmin) | |
|
41 | admin.site.register(Ban, BanAdmin) | |
|
42 | admin.site.register(Thread, ThreadAdmin) | |
|
43 | admin.site.register(KeyPair, KeyPairAdmin) |
@@ -1,42 +1,41 b'' | |||
|
1 |
from boards.abstracts.settingsmanager import |
|
|
2 | get_settings_manager | |
|
1 | from boards.abstracts.settingsmanager import get_settings_manager | |
|
3 | 2 | |
|
4 | 3 | __author__ = 'neko259' |
|
5 | 4 | |
|
6 | 5 | from boards import settings |
|
7 | 6 | from boards.models import Post |
|
8 | 7 | |
|
9 | 8 | CONTEXT_SITE_NAME = 'site_name' |
|
10 | 9 | CONTEXT_VERSION = 'version' |
|
11 | 10 | CONTEXT_MODERATOR = 'moderator' |
|
12 | 11 | CONTEXT_THEME_CSS = 'theme_css' |
|
13 | 12 | CONTEXT_THEME = 'theme' |
|
14 | 13 | CONTEXT_PPD = 'posts_per_day' |
|
15 | 14 | CONTEXT_TAGS = 'tags' |
|
16 | 15 | CONTEXT_USER = 'user' |
|
17 | 16 | |
|
18 | 17 | PERMISSION_MODERATE = 'moderation' |
|
19 | 18 | |
|
20 | 19 | |
|
21 | 20 | def user_and_ui_processor(request): |
|
22 |
context = |
|
|
21 | context = dict() | |
|
23 | 22 | |
|
24 | 23 | context[CONTEXT_PPD] = float(Post.objects.get_posts_per_day()) |
|
25 | 24 | |
|
26 | 25 | settings_manager = get_settings_manager(request) |
|
27 | 26 | context[CONTEXT_TAGS] = settings_manager.get_fav_tags() |
|
28 | 27 | theme = settings_manager.get_theme() |
|
29 | 28 | context[CONTEXT_THEME] = theme |
|
30 | 29 | context[CONTEXT_THEME_CSS] = 'css/' + theme + '/base_page.css' |
|
31 | 30 | |
|
32 | 31 | # This shows the moderator panel |
|
33 | 32 | try: |
|
34 |
moderate = request.user.has_perm( |
|
|
33 | moderate = request.user.has_perm(PERMISSION_MODERATE) | |
|
35 | 34 | except AttributeError: |
|
36 | 35 | moderate = False |
|
37 | 36 | context[CONTEXT_MODERATOR] = moderate |
|
38 | 37 | |
|
39 | 38 | context[CONTEXT_VERSION] = settings.VERSION |
|
40 | 39 | context[CONTEXT_SITE_NAME] = settings.SITE_NAME |
|
41 | 40 | |
|
42 | 41 | return context |
@@ -1,294 +1,305 b'' | |||
|
1 | 1 | import re |
|
2 | 2 | import time |
|
3 | 3 | import hashlib |
|
4 | 4 | |
|
5 | 5 | from django import forms |
|
6 | 6 | from django.forms.util import ErrorList |
|
7 | 7 | from django.utils.translation import ugettext_lazy as _ |
|
8 | 8 | |
|
9 | 9 | from boards.mdx_neboard import formatters |
|
10 | 10 | from boards.models.post import TITLE_MAX_LENGTH |
|
11 | from boards.models import PostImage | |
|
11 | from boards.models import PostImage, Tag | |
|
12 | 12 | from neboard import settings |
|
13 | 13 | from boards import utils |
|
14 | 14 | import boards.settings as board_settings |
|
15 | 15 | |
|
16 | 16 | VETERAN_POSTING_DELAY = 5 |
|
17 | 17 | |
|
18 | 18 | ATTRIBUTE_PLACEHOLDER = 'placeholder' |
|
19 | 19 | |
|
20 | 20 | LAST_POST_TIME = 'last_post_time' |
|
21 | 21 | LAST_LOGIN_TIME = 'last_login_time' |
|
22 | 22 | TEXT_PLACEHOLDER = _('''Type message here. Use formatting panel for more advanced usage.''') |
|
23 | 23 | TAGS_PLACEHOLDER = _('tag1 several_words_tag') |
|
24 | 24 | |
|
25 | 25 | ERROR_IMAGE_DUPLICATE = _('Such image was already posted') |
|
26 | 26 | |
|
27 | 27 | LABEL_TITLE = _('Title') |
|
28 | 28 | LABEL_TEXT = _('Text') |
|
29 | 29 | LABEL_TAG = _('Tag') |
|
30 | 30 | LABEL_SEARCH = _('Search') |
|
31 | 31 | |
|
32 | 32 | TAG_MAX_LENGTH = 20 |
|
33 | 33 | |
|
34 | 34 | REGEX_TAG = r'^[\w\d]+$' |
|
35 | 35 | |
|
36 | 36 | |
|
37 | 37 | class FormatPanel(forms.Textarea): |
|
38 | 38 | def render(self, name, value, attrs=None): |
|
39 | 39 | output = '<div id="mark-panel">' |
|
40 | 40 | for formatter in formatters: |
|
41 | 41 | output += '<span class="mark_btn"' + \ |
|
42 | 42 | ' onClick="addMarkToMsg(\'' + formatter.format_left + \ |
|
43 | 43 | '\', \'' + formatter.format_right + '\')">' + \ |
|
44 | 44 | formatter.preview_left + formatter.name + \ |
|
45 | 45 | formatter.preview_right + '</span>' |
|
46 | 46 | |
|
47 | 47 | output += '</div>' |
|
48 | 48 | output += super(FormatPanel, self).render(name, value, attrs=None) |
|
49 | 49 | |
|
50 | 50 | return output |
|
51 | 51 | |
|
52 | 52 | |
|
53 | 53 | class PlainErrorList(ErrorList): |
|
54 | 54 | def __unicode__(self): |
|
55 | 55 | return self.as_text() |
|
56 | 56 | |
|
57 | 57 | def as_text(self): |
|
58 | 58 | return ''.join(['(!) %s ' % e for e in self]) |
|
59 | 59 | |
|
60 | 60 | |
|
61 | 61 | class NeboardForm(forms.Form): |
|
62 | 62 | |
|
63 | 63 | def as_div(self): |
|
64 | 64 | """ |
|
65 | 65 | Returns this form rendered as HTML <as_div>s. |
|
66 | 66 | """ |
|
67 | 67 | |
|
68 | 68 | return self._html_output( |
|
69 | 69 | # TODO Do not show hidden rows in the list here |
|
70 | 70 | normal_row='<div class="form-row"><div class="form-label">' |
|
71 | 71 | '%(label)s' |
|
72 | 72 | '</div></div>' |
|
73 | 73 | '<div class="form-row"><div class="form-input">' |
|
74 | 74 | '%(field)s' |
|
75 | 75 | '</div></div>' |
|
76 | 76 | '<div class="form-row">' |
|
77 | 77 | '%(help_text)s' |
|
78 | 78 | '</div>', |
|
79 | 79 | error_row='<div class="form-row">' |
|
80 | 80 | '<div class="form-label"></div>' |
|
81 | 81 | '<div class="form-errors">%s</div>' |
|
82 | 82 | '</div>', |
|
83 | 83 | row_ender='</div>', |
|
84 | 84 | help_text_html='%s', |
|
85 | 85 | errors_on_separate_row=True) |
|
86 | 86 | |
|
87 | 87 | def as_json_errors(self): |
|
88 | 88 | errors = [] |
|
89 | 89 | |
|
90 | 90 | for name, field in list(self.fields.items()): |
|
91 | 91 | if self[name].errors: |
|
92 | 92 | errors.append({ |
|
93 | 93 | 'field': name, |
|
94 | 94 | 'errors': self[name].errors.as_text(), |
|
95 | 95 | }) |
|
96 | 96 | |
|
97 | 97 | return errors |
|
98 | 98 | |
|
99 | 99 | |
|
100 | 100 | class PostForm(NeboardForm): |
|
101 | 101 | |
|
102 | 102 | title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False, |
|
103 | 103 | label=LABEL_TITLE) |
|
104 | 104 | text = forms.CharField( |
|
105 | 105 | widget=FormatPanel(attrs={ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER}), |
|
106 | 106 | required=False, label=LABEL_TEXT) |
|
107 | 107 | image = forms.ImageField(required=False, label=_('Image'), |
|
108 | 108 | widget=forms.ClearableFileInput( |
|
109 | 109 | attrs={'accept': 'image/*'})) |
|
110 | 110 | |
|
111 | 111 | # This field is for spam prevention only |
|
112 | 112 | email = forms.CharField(max_length=100, required=False, label=_('e-mail'), |
|
113 | 113 | widget=forms.TextInput(attrs={ |
|
114 | 114 | 'class': 'form-email'})) |
|
115 | 115 | |
|
116 | 116 | session = None |
|
117 | 117 | need_to_ban = False |
|
118 | 118 | |
|
119 | 119 | def clean_title(self): |
|
120 | 120 | title = self.cleaned_data['title'] |
|
121 | 121 | if title: |
|
122 | 122 | if len(title) > TITLE_MAX_LENGTH: |
|
123 | 123 | raise forms.ValidationError(_('Title must have less than %s ' |
|
124 | 124 | 'characters') % |
|
125 | 125 | str(TITLE_MAX_LENGTH)) |
|
126 | 126 | return title |
|
127 | 127 | |
|
128 | 128 | def clean_text(self): |
|
129 | 129 | text = self.cleaned_data['text'].strip() |
|
130 | 130 | if text: |
|
131 | 131 | if len(text) > board_settings.MAX_TEXT_LENGTH: |
|
132 | 132 | raise forms.ValidationError(_('Text must have less than %s ' |
|
133 | 133 | 'characters') % |
|
134 | 134 | str(board_settings |
|
135 | 135 | .MAX_TEXT_LENGTH)) |
|
136 | 136 | return text |
|
137 | 137 | |
|
138 | 138 | def clean_image(self): |
|
139 | 139 | image = self.cleaned_data['image'] |
|
140 | 140 | if image: |
|
141 | 141 | if image.size > board_settings.MAX_IMAGE_SIZE: |
|
142 | 142 | raise forms.ValidationError( |
|
143 | 143 | _('Image must be less than %s bytes') |
|
144 | 144 | % str(board_settings.MAX_IMAGE_SIZE)) |
|
145 | 145 | |
|
146 | 146 | md5 = hashlib.md5() |
|
147 | 147 | for chunk in image.chunks(): |
|
148 | 148 | md5.update(chunk) |
|
149 | 149 | image_hash = md5.hexdigest() |
|
150 | 150 | if PostImage.objects.filter(hash=image_hash).exists(): |
|
151 | 151 | raise forms.ValidationError(ERROR_IMAGE_DUPLICATE) |
|
152 | 152 | |
|
153 | 153 | return image |
|
154 | 154 | |
|
155 | 155 | def clean(self): |
|
156 | 156 | cleaned_data = super(PostForm, self).clean() |
|
157 | 157 | |
|
158 | 158 | if not self.session: |
|
159 | 159 | raise forms.ValidationError('Humans have sessions') |
|
160 | 160 | |
|
161 | 161 | if cleaned_data['email']: |
|
162 | 162 | self.need_to_ban = True |
|
163 | 163 | raise forms.ValidationError('A human cannot enter a hidden field') |
|
164 | 164 | |
|
165 | 165 | if not self.errors: |
|
166 | 166 | self._clean_text_image() |
|
167 | 167 | |
|
168 | 168 | if not self.errors and self.session: |
|
169 | 169 | self._validate_posting_speed() |
|
170 | 170 | |
|
171 | 171 | return cleaned_data |
|
172 | 172 | |
|
173 | 173 | def _clean_text_image(self): |
|
174 | 174 | text = self.cleaned_data.get('text') |
|
175 | 175 | image = self.cleaned_data.get('image') |
|
176 | 176 | |
|
177 | 177 | if (not text) and (not image): |
|
178 | 178 | error_message = _('Either text or image must be entered.') |
|
179 | 179 | self._errors['text'] = self.error_class([error_message]) |
|
180 | 180 | |
|
181 | 181 | def _validate_posting_speed(self): |
|
182 | 182 | can_post = True |
|
183 | 183 | |
|
184 | 184 | posting_delay = settings.POSTING_DELAY |
|
185 | 185 | |
|
186 | 186 | if board_settings.LIMIT_POSTING_SPEED and LAST_POST_TIME in \ |
|
187 | 187 | self.session: |
|
188 | 188 | now = time.time() |
|
189 | 189 | last_post_time = self.session[LAST_POST_TIME] |
|
190 | 190 | |
|
191 | 191 | current_delay = int(now - last_post_time) |
|
192 | 192 | |
|
193 | 193 | if current_delay < posting_delay: |
|
194 | 194 | error_message = _('Wait %s seconds after last posting') % str( |
|
195 | 195 | posting_delay - current_delay) |
|
196 | 196 | self._errors['text'] = self.error_class([error_message]) |
|
197 | 197 | |
|
198 | 198 | can_post = False |
|
199 | 199 | |
|
200 | 200 | if can_post: |
|
201 | 201 | self.session[LAST_POST_TIME] = time.time() |
|
202 | 202 | |
|
203 | 203 | |
|
204 | 204 | class ThreadForm(PostForm): |
|
205 | 205 | |
|
206 | 206 | regex_tags = re.compile(r'^[\w\s\d]+$', re.UNICODE) |
|
207 | 207 | |
|
208 | 208 | tags = forms.CharField( |
|
209 | 209 | widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}), |
|
210 | 210 | max_length=100, label=_('Tags'), required=True) |
|
211 | 211 | |
|
212 | 212 | def clean_tags(self): |
|
213 | 213 | tags = self.cleaned_data['tags'].strip() |
|
214 | 214 | |
|
215 | 215 | if not tags or not self.regex_tags.match(tags): |
|
216 | 216 | raise forms.ValidationError( |
|
217 | 217 | _('Inappropriate characters in tags.')) |
|
218 | 218 | |
|
219 | tag_models = [] | |
|
220 | required_tag_exists = False | |
|
221 | for tag in tags.split(): | |
|
222 | tag_model = Tag.objects.filter(name=tag.strip().lower(), | |
|
223 | required=True) | |
|
224 | if tag_model.exists(): | |
|
225 | required_tag_exists = True | |
|
226 | ||
|
227 | if not required_tag_exists: | |
|
228 | raise forms.ValidationError(_('Need at least 1 required tag.')) | |
|
229 | ||
|
219 | 230 | return tags |
|
220 | 231 | |
|
221 | 232 | def clean(self): |
|
222 | 233 | cleaned_data = super(ThreadForm, self).clean() |
|
223 | 234 | |
|
224 | 235 | return cleaned_data |
|
225 | 236 | |
|
226 | 237 | |
|
227 | 238 | class SettingsForm(NeboardForm): |
|
228 | 239 | |
|
229 | 240 | theme = forms.ChoiceField(choices=settings.THEMES, |
|
230 | 241 | label=_('Theme')) |
|
231 | 242 | |
|
232 | 243 | |
|
233 | 244 | class AddTagForm(NeboardForm): |
|
234 | 245 | |
|
235 | 246 | tag = forms.CharField(max_length=TAG_MAX_LENGTH, label=LABEL_TAG) |
|
236 | 247 | method = forms.CharField(widget=forms.HiddenInput(), initial='add_tag') |
|
237 | 248 | |
|
238 | 249 | def clean_tag(self): |
|
239 | 250 | tag = self.cleaned_data['tag'] |
|
240 | 251 | |
|
241 | 252 | regex_tag = re.compile(REGEX_TAG, re.UNICODE) |
|
242 | 253 | if not regex_tag.match(tag): |
|
243 | 254 | raise forms.ValidationError(_('Inappropriate characters in tags.')) |
|
244 | 255 | |
|
245 | 256 | return tag |
|
246 | 257 | |
|
247 | 258 | def clean(self): |
|
248 | 259 | cleaned_data = super(AddTagForm, self).clean() |
|
249 | 260 | |
|
250 | 261 | return cleaned_data |
|
251 | 262 | |
|
252 | 263 | |
|
253 | 264 | class SearchForm(NeboardForm): |
|
254 | 265 | query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False) |
|
255 | 266 | |
|
256 | 267 | |
|
257 | 268 | class LoginForm(NeboardForm): |
|
258 | 269 | |
|
259 | 270 | password = forms.CharField() |
|
260 | 271 | |
|
261 | 272 | session = None |
|
262 | 273 | |
|
263 | 274 | def clean_password(self): |
|
264 | 275 | password = self.cleaned_data['password'] |
|
265 | 276 | if board_settings.MASTER_PASSWORD != password: |
|
266 | 277 | raise forms.ValidationError(_('Invalid master password')) |
|
267 | 278 | |
|
268 | 279 | return password |
|
269 | 280 | |
|
270 | 281 | def _validate_login_speed(self): |
|
271 | 282 | can_post = True |
|
272 | 283 | |
|
273 | 284 | if LAST_LOGIN_TIME in self.session: |
|
274 | 285 | now = time.time() |
|
275 | 286 | last_login_time = self.session[LAST_LOGIN_TIME] |
|
276 | 287 | |
|
277 | 288 | current_delay = int(now - last_login_time) |
|
278 | 289 | |
|
279 | 290 | if current_delay < board_settings.LOGIN_TIMEOUT: |
|
280 | 291 | error_message = _('Wait %s minutes after last login') % str( |
|
281 | 292 | (board_settings.LOGIN_TIMEOUT - current_delay) / 60) |
|
282 | 293 | self._errors['password'] = self.error_class([error_message]) |
|
283 | 294 | |
|
284 | 295 | can_post = False |
|
285 | 296 | |
|
286 | 297 | if can_post: |
|
287 | 298 | self.session[LAST_LOGIN_TIME] = time.time() |
|
288 | 299 | |
|
289 | 300 | def clean(self): |
|
290 | 301 | self._validate_login_speed() |
|
291 | 302 | |
|
292 | 303 | cleaned_data = super(LoginForm, self).clean() |
|
293 | 304 | |
|
294 | 305 | return cleaned_data |
|
1 | NO CONTENT: modified file, binary diff hidden |
@@ -1,365 +1,370 b'' | |||
|
1 | 1 | # SOME DESCRIPTIVE TITLE. |
|
2 | 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER |
|
3 | 3 | # This file is distributed under the same license as the PACKAGE package. |
|
4 | 4 | # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. |
|
5 | 5 | # |
|
6 | 6 | msgid "" |
|
7 | 7 | msgstr "" |
|
8 | 8 | "Project-Id-Version: PACKAGE VERSION\n" |
|
9 | 9 | "Report-Msgid-Bugs-To: \n" |
|
10 |
"POT-Creation-Date: 201 |
|
|
10 | "POT-Creation-Date: 2015-01-08 16:36+0200\n" | |
|
11 | 11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" |
|
12 | 12 | "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" |
|
13 | 13 | "Language-Team: LANGUAGE <LL@li.org>\n" |
|
14 | 14 | "Language: ru\n" |
|
15 | 15 | "MIME-Version: 1.0\n" |
|
16 | 16 | "Content-Type: text/plain; charset=UTF-8\n" |
|
17 | 17 | "Content-Transfer-Encoding: 8bit\n" |
|
18 | 18 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" |
|
19 | 19 | "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" |
|
20 | 20 | |
|
21 | 21 | #: authors.py:9 |
|
22 | 22 | msgid "author" |
|
23 | 23 | msgstr "автор" |
|
24 | 24 | |
|
25 | 25 | #: authors.py:10 |
|
26 | 26 | msgid "developer" |
|
27 | 27 | msgstr "разработчик" |
|
28 | 28 | |
|
29 | 29 | #: authors.py:11 |
|
30 | 30 | msgid "javascript developer" |
|
31 | 31 | msgstr "разработчик javascript" |
|
32 | 32 | |
|
33 | 33 | #: authors.py:12 |
|
34 | 34 | msgid "designer" |
|
35 | 35 | msgstr "дизайнер" |
|
36 | 36 | |
|
37 | 37 | #: forms.py:22 |
|
38 | 38 | msgid "Type message here. Use formatting panel for more advanced usage." |
|
39 | 39 | msgstr "" |
|
40 | 40 | "Вводите сообщение сюда. Используйте панель для более сложного форматирования." |
|
41 | 41 | |
|
42 | 42 | #: forms.py:23 |
|
43 | 43 | msgid "tag1 several_words_tag" |
|
44 |
msgstr " |
|
|
44 | msgstr "метка1 метка_из_нескольких_слов" | |
|
45 | 45 | |
|
46 | 46 | #: forms.py:25 |
|
47 | 47 | msgid "Such image was already posted" |
|
48 | 48 | msgstr "Такое изображение уже было загружено" |
|
49 | 49 | |
|
50 | 50 | #: forms.py:27 |
|
51 | 51 | msgid "Title" |
|
52 | 52 | msgstr "Заголовок" |
|
53 | 53 | |
|
54 | 54 | #: forms.py:28 |
|
55 | 55 | msgid "Text" |
|
56 | 56 | msgstr "Текст" |
|
57 | 57 | |
|
58 | 58 | #: forms.py:29 |
|
59 | 59 | msgid "Tag" |
|
60 |
msgstr " |
|
|
60 | msgstr "Метка" | |
|
61 | 61 | |
|
62 |
#: forms.py:30 templates/boards/base.html:3 |
|
|
62 | #: forms.py:30 templates/boards/base.html:38 templates/search/search.html:9 | |
|
63 | 63 | #: templates/search/search.html.py:13 |
|
64 | 64 | msgid "Search" |
|
65 | 65 | msgstr "Поиск" |
|
66 | 66 | |
|
67 | 67 | #: forms.py:107 |
|
68 | 68 | msgid "Image" |
|
69 | 69 | msgstr "Изображение" |
|
70 | 70 | |
|
71 | 71 | #: forms.py:112 |
|
72 | 72 | msgid "e-mail" |
|
73 | 73 | msgstr "" |
|
74 | 74 | |
|
75 | 75 | #: forms.py:123 |
|
76 | 76 | #, python-format |
|
77 | 77 | msgid "Title must have less than %s characters" |
|
78 | 78 | msgstr "Заголовок должен иметь меньше %s символов" |
|
79 | 79 | |
|
80 | 80 | #: forms.py:132 |
|
81 | 81 | #, python-format |
|
82 | 82 | msgid "Text must have less than %s characters" |
|
83 | 83 | msgstr "Текст должен быть короче %s символов" |
|
84 | 84 | |
|
85 | 85 | #: forms.py:143 |
|
86 | 86 | #, python-format |
|
87 | 87 | msgid "Image must be less than %s bytes" |
|
88 | 88 | msgstr "Изображение должно быть менее %s байт" |
|
89 | 89 | |
|
90 | 90 | #: forms.py:178 |
|
91 | 91 | msgid "Either text or image must be entered." |
|
92 | 92 | msgstr "Текст или картинка должны быть введены." |
|
93 | 93 | |
|
94 |
#: forms.py:19 |
|
|
94 | #: forms.py:194 | |
|
95 | 95 | #, python-format |
|
96 | 96 | msgid "Wait %s seconds after last posting" |
|
97 | 97 | msgstr "Подождите %s секунд после последнего постинга" |
|
98 | 98 | |
|
99 |
#: forms.py:21 |
|
|
99 | #: forms.py:210 templates/boards/rss/post.html:10 templates/boards/tags.html:7 | |
|
100 | 100 | msgid "Tags" |
|
101 |
msgstr " |
|
|
101 | msgstr "Метки" | |
|
102 | 102 | |
|
103 |
#: forms.py:2 |
|
|
103 | #: forms.py:217 forms.py:254 | |
|
104 | 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 | 112 | msgid "Theme" |
|
109 | 113 | msgstr "Тема" |
|
110 | 114 | |
|
111 |
#: forms.py:27 |
|
|
115 | #: forms.py:277 | |
|
112 | 116 | msgid "Invalid master password" |
|
113 | 117 | msgstr "Неверный мастер-пароль" |
|
114 | 118 | |
|
115 |
#: forms.py:2 |
|
|
119 | #: forms.py:291 | |
|
116 | 120 | #, python-format |
|
117 | 121 | msgid "Wait %s minutes after last login" |
|
118 | 122 | msgstr "Подождите %s минут после последнего входа" |
|
119 | 123 | |
|
120 | 124 | #: templates/boards/404.html:6 |
|
121 | 125 | msgid "Not found" |
|
122 | 126 | msgstr "Не найдено" |
|
123 | 127 | |
|
124 | 128 | #: templates/boards/404.html:12 |
|
125 | 129 | msgid "This page does not exist" |
|
126 | 130 | msgstr "Этой страницы не существует" |
|
127 | 131 | |
|
128 | 132 | #: templates/boards/authors.html:6 templates/boards/authors.html.py:12 |
|
129 | 133 | msgid "Authors" |
|
130 | 134 | msgstr "Авторы" |
|
131 | 135 | |
|
132 | 136 | #: templates/boards/authors.html:26 |
|
133 | 137 | msgid "Distributed under the" |
|
134 | 138 | msgstr "Распространяется под" |
|
135 | 139 | |
|
136 | 140 | #: templates/boards/authors.html:28 |
|
137 | 141 | msgid "license" |
|
138 | 142 | msgstr "лицензией" |
|
139 | 143 | |
|
140 | 144 | #: templates/boards/authors.html:30 |
|
141 | 145 | msgid "Repository" |
|
142 | 146 | msgstr "Репозиторий" |
|
143 | 147 | |
|
144 |
#: templates/boards/base.html:1 |
|
|
148 | #: templates/boards/base.html:13 | |
|
145 | 149 | msgid "Feed" |
|
146 | 150 | msgstr "Лента" |
|
147 | 151 | |
|
148 |
#: templates/boards/base.html: |
|
|
152 | #: templates/boards/base.html:30 | |
|
149 | 153 | msgid "All threads" |
|
150 | 154 | msgstr "Все темы" |
|
151 | 155 | |
|
152 |
#: templates/boards/base.html:3 |
|
|
156 | #: templates/boards/base.html:36 | |
|
153 | 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 | 161 | msgid "Settings" |
|
158 | 162 | msgstr "Настройки" |
|
159 | 163 | |
|
160 |
#: templates/boards/base.html:5 |
|
|
164 | #: templates/boards/base.html:52 | |
|
161 | 165 | msgid "Admin" |
|
162 | 166 | msgstr "" |
|
163 | 167 | |
|
164 |
#: templates/boards/base.html:5 |
|
|
168 | #: templates/boards/base.html:54 | |
|
165 | 169 | #, python-format |
|
166 | 170 | msgid "Speed: %(ppd)s posts per day" |
|
167 | 171 | msgstr "Скорость: %(ppd)s сообщений в день" |
|
168 | 172 | |
|
169 |
#: templates/boards/base.html:5 |
|
|
173 | #: templates/boards/base.html:56 | |
|
170 | 174 | msgid "Up" |
|
171 | 175 | msgstr "Вверх" |
|
172 | 176 | |
|
173 | 177 | #: templates/boards/login.html:6 templates/boards/login.html.py:16 |
|
174 | 178 | msgid "Login" |
|
175 | 179 | msgstr "Вход" |
|
176 | 180 | |
|
177 | 181 | #: templates/boards/login.html:19 |
|
178 | 182 | msgid "Insert your user id above" |
|
179 | 183 | msgstr "Вставьте свой ID пользователя выше" |
|
180 | 184 | |
|
181 |
#: templates/boards/post.html: |
|
|
185 | #: templates/boards/post.html:19 templates/boards/staticpages/help.html:17 | |
|
182 | 186 | msgid "Quote" |
|
183 | 187 | msgstr "Цитата" |
|
184 | 188 | |
|
185 |
#: templates/boards/post.html: |
|
|
189 | #: templates/boards/post.html:27 | |
|
186 | 190 | msgid "Open" |
|
187 | 191 | msgstr "Открыть" |
|
188 | 192 | |
|
189 |
#: templates/boards/post.html: |
|
|
193 | #: templates/boards/post.html:29 | |
|
190 | 194 | msgid "Reply" |
|
191 | 195 | msgstr "Ответ" |
|
192 | 196 | |
|
193 |
#: templates/boards/post.html: |
|
|
197 | #: templates/boards/post.html:36 | |
|
194 | 198 | msgid "Edit" |
|
195 | 199 | msgstr "Изменить" |
|
196 | 200 | |
|
197 |
#: templates/boards/post.html: |
|
|
198 | msgid "Delete" | |
|
199 |
msgstr " |
|
|
201 | #: templates/boards/post.html:39 | |
|
202 | msgid "Edit thread" | |
|
203 | msgstr "Изменить тему" | |
|
200 | 204 | |
|
201 |
#: templates/boards/post.html: |
|
|
202 | msgid "Ban IP" | |
|
203 | msgstr "Заблокировать IP" | |
|
204 | ||
|
205 | #: templates/boards/post.html:76 | |
|
205 | #: templates/boards/post.html:71 | |
|
206 | 206 | msgid "Replies" |
|
207 | 207 | msgstr "Ответы" |
|
208 | 208 | |
|
209 |
#: templates/boards/post.html: |
|
|
209 | #: templates/boards/post.html:79 templates/boards/thread.html:89 | |
|
210 | 210 | #: templates/boards/thread_gallery.html:59 |
|
211 | 211 | msgid "messages" |
|
212 | 212 | msgstr "сообщений" |
|
213 | 213 | |
|
214 |
#: templates/boards/post.html:8 |
|
|
214 | #: templates/boards/post.html:80 templates/boards/thread.html:90 | |
|
215 | 215 | #: templates/boards/thread_gallery.html:60 |
|
216 | 216 | msgid "images" |
|
217 | 217 | msgstr "изображений" |
|
218 | 218 | |
|
219 | 219 | #: templates/boards/post_admin.html:19 |
|
220 | 220 | msgid "Tags:" |
|
221 |
msgstr " |
|
|
221 | msgstr "Метки:" | |
|
222 | 222 | |
|
223 | 223 | #: templates/boards/post_admin.html:30 |
|
224 | 224 | msgid "Add tag" |
|
225 |
msgstr "Добавить |
|
|
225 | msgstr "Добавить метку" | |
|
226 | 226 | |
|
227 | 227 | #: templates/boards/posting_general.html:56 |
|
228 | 228 | msgid "Show tag" |
|
229 |
msgstr "Показывать |
|
|
229 | msgstr "Показывать метку" | |
|
230 | 230 | |
|
231 | 231 | #: templates/boards/posting_general.html:60 |
|
232 | 232 | msgid "Hide tag" |
|
233 |
msgstr "Скрывать |
|
|
233 | msgstr "Скрывать метку" | |
|
234 | 234 | |
|
235 |
#: templates/boards/posting_general.html: |
|
|
235 | #: templates/boards/posting_general.html:66 | |
|
236 | msgid "Edit tag" | |
|
237 | msgstr "Изменить метку" | |
|
238 | ||
|
239 | #: templates/boards/posting_general.html:82 templates/search/search.html:22 | |
|
236 | 240 | msgid "Previous page" |
|
237 | 241 | msgstr "Предыдущая страница" |
|
238 | 242 | |
|
239 |
#: templates/boards/posting_general.html:9 |
|
|
243 | #: templates/boards/posting_general.html:97 | |
|
240 | 244 | #, python-format |
|
241 | 245 | msgid "Skipped %(count)s replies. Open thread to see all replies." |
|
242 | 246 | msgstr "Пропущено %(count)s ответов. Откройте тред, чтобы увидеть все ответы." |
|
243 | 247 | |
|
244 |
#: templates/boards/posting_general.html:12 |
|
|
248 | #: templates/boards/posting_general.html:124 templates/search/search.html:33 | |
|
245 | 249 | msgid "Next page" |
|
246 | 250 | msgstr "Следующая страница" |
|
247 | 251 | |
|
248 |
#: templates/boards/posting_general.html:12 |
|
|
252 | #: templates/boards/posting_general.html:129 | |
|
249 | 253 | msgid "No threads exist. Create the first one!" |
|
250 | 254 | msgstr "Нет тем. Создайте первую!" |
|
251 | 255 | |
|
252 |
#: templates/boards/posting_general.html:13 |
|
|
256 | #: templates/boards/posting_general.html:135 | |
|
253 | 257 | msgid "Create new thread" |
|
254 | 258 | msgstr "Создать новую тему" |
|
255 | 259 | |
|
256 |
#: templates/boards/posting_general.html:1 |
|
|
257 |
#: templates/boards/thread.html:5 |
|
|
260 | #: templates/boards/posting_general.html:140 templates/boards/preview.html:16 | |
|
261 | #: templates/boards/thread.html:54 | |
|
258 | 262 | msgid "Post" |
|
259 | 263 | msgstr "Отправить" |
|
260 | 264 | |
|
261 |
#: templates/boards/posting_general.html:14 |
|
|
265 | #: templates/boards/posting_general.html:145 | |
|
262 | 266 | msgid "Tags must be delimited by spaces. Text or image is required." |
|
263 | 267 | msgstr "" |
|
264 |
" |
|
|
268 | "Метки должны быть разделены пробелами. Текст или изображение обязательны." | |
|
265 | 269 | |
|
266 |
#: templates/boards/posting_general.html:14 |
|
|
270 | #: templates/boards/posting_general.html:148 templates/boards/thread.html:62 | |
|
267 | 271 | msgid "Text syntax" |
|
268 | 272 | msgstr "Синтаксис текста" |
|
269 | 273 | |
|
270 |
#: templates/boards/posting_general.html:1 |
|
|
274 | #: templates/boards/posting_general.html:160 | |
|
271 | 275 | msgid "Pages:" |
|
272 | 276 | msgstr "Страницы: " |
|
273 | 277 | |
|
274 | 278 | #: templates/boards/preview.html:6 templates/boards/staticpages/help.html:19 |
|
275 | 279 | msgid "Preview" |
|
276 | 280 | msgstr "Предпросмотр" |
|
277 | 281 | |
|
282 | #: templates/boards/rss/post.html:5 | |
|
283 | msgid "Post image" | |
|
284 | msgstr "Изображение сообщения" | |
|
285 | ||
|
278 | 286 | #: templates/boards/settings.html:15 |
|
279 | 287 | msgid "You are moderator." |
|
280 | 288 | msgstr "Вы модератор." |
|
281 | 289 | |
|
282 | 290 | #: templates/boards/settings.html:19 |
|
283 | 291 | msgid "Hidden tags:" |
|
284 |
msgstr "Скрытые |
|
|
292 | msgstr "Скрытые метки:" | |
|
285 | 293 | |
|
286 | 294 | #: templates/boards/settings.html:26 |
|
287 | 295 | msgid "No hidden tags." |
|
288 |
msgstr "Нет скрытых |
|
|
296 | msgstr "Нет скрытых меток." | |
|
289 | 297 | |
|
290 | 298 | #: templates/boards/settings.html:35 |
|
291 | 299 | msgid "Save" |
|
292 | 300 | msgstr "Сохранить" |
|
293 | 301 | |
|
294 | #: templates/boards/tags.html:22 | |
|
295 | msgid "No tags found." | |
|
296 | msgstr "Теги не найдены." | |
|
297 | ||
|
298 | #: templates/boards/thread.html:20 templates/boards/thread_gallery.html:19 | |
|
299 | msgid "Normal mode" | |
|
300 | msgstr "Нормальный режим" | |
|
301 | ||
|
302 | #: templates/boards/thread.html:21 templates/boards/thread_gallery.html:20 | |
|
303 | msgid "Gallery mode" | |
|
304 | msgstr "Режим галереи" | |
|
305 | ||
|
306 | #: templates/boards/thread.html:29 | |
|
307 | msgid "posts to bumplimit" | |
|
308 | msgstr "сообщений до бамплимита" | |
|
309 | ||
|
310 | #: templates/boards/thread.html:50 | |
|
311 | msgid "Reply to thread" | |
|
312 | msgstr "Ответить в тему" | |
|
313 | ||
|
314 | #: templates/boards/thread.html:63 | |
|
315 | msgid "Switch mode" | |
|
316 | msgstr "Переключить режим" | |
|
317 | ||
|
318 | #: templates/boards/thread.html:90 templates/boards/thread_gallery.html:61 | |
|
319 | msgid "Last update: " | |
|
320 | msgstr "Последнее обновление: " | |
|
321 | ||
|
322 | #: templates/boards/rss/post.html:5 | |
|
323 | msgid "Post image" | |
|
324 | msgstr "Изображение сообщения" | |
|
325 | ||
|
326 | 302 | #: templates/boards/staticpages/banned.html:6 |
|
327 | 303 | msgid "Banned" |
|
328 | 304 | msgstr "Заблокирован" |
|
329 | 305 | |
|
330 | 306 | #: templates/boards/staticpages/banned.html:11 |
|
331 | 307 | msgid "Your IP address has been banned. Contact the administrator" |
|
332 | 308 | msgstr "Ваш IP адрес был заблокирован. Свяжитесь с администратором" |
|
333 | 309 | |
|
334 | 310 | #: templates/boards/staticpages/help.html:6 |
|
335 | 311 | #: templates/boards/staticpages/help.html:10 |
|
336 | 312 | msgid "Syntax" |
|
337 | 313 | msgstr "Синтаксис" |
|
338 | 314 | |
|
339 | 315 | #: templates/boards/staticpages/help.html:11 |
|
340 | 316 | msgid "Italic text" |
|
341 | 317 | msgstr "Курсивный текст" |
|
342 | 318 | |
|
343 | 319 | #: templates/boards/staticpages/help.html:12 |
|
344 | 320 | msgid "Bold text" |
|
345 | 321 | msgstr "Полужирный текст" |
|
346 | 322 | |
|
347 | 323 | #: templates/boards/staticpages/help.html:13 |
|
348 | 324 | msgid "Spoiler" |
|
349 | 325 | msgstr "Спойлер" |
|
350 | 326 | |
|
351 | 327 | #: templates/boards/staticpages/help.html:14 |
|
352 | 328 | msgid "Link to a post" |
|
353 | 329 | msgstr "Ссылка на сообщение" |
|
354 | 330 | |
|
355 | 331 | #: templates/boards/staticpages/help.html:15 |
|
356 | 332 | msgid "Strikethrough text" |
|
357 | 333 | msgstr "Зачеркнутый текст" |
|
358 | 334 | |
|
359 | 335 | #: templates/boards/staticpages/help.html:16 |
|
360 | 336 | msgid "Comment" |
|
361 | 337 | msgstr "Комментарий" |
|
362 | 338 | |
|
363 | 339 | #: templates/boards/staticpages/help.html:19 |
|
364 | 340 | msgid "You can try pasting the text and previewing the result here:" |
|
365 | 341 | msgstr "Вы можете попробовать вставить текст и проверить результат здесь:" |
|
342 | ||
|
343 | #: templates/boards/tags.html:23 | |
|
344 | msgid "No tags found." | |
|
345 | msgstr "Метки не найдены." | |
|
346 | ||
|
347 | #: templates/boards/thread.html:19 templates/boards/thread_gallery.html:19 | |
|
348 | msgid "Normal mode" | |
|
349 | msgstr "Нормальный режим" | |
|
350 | ||
|
351 | #: templates/boards/thread.html:20 templates/boards/thread_gallery.html:20 | |
|
352 | msgid "Gallery mode" | |
|
353 | msgstr "Режим галереи" | |
|
354 | ||
|
355 | #: templates/boards/thread.html:28 | |
|
356 | msgid "posts to bumplimit" | |
|
357 | msgstr "сообщений до бамплимита" | |
|
358 | ||
|
359 | #: templates/boards/thread.html:46 | |
|
360 | msgid "Reply to thread" | |
|
361 | msgstr "Ответить в тему" | |
|
362 | ||
|
363 | #: templates/boards/thread.html:59 | |
|
364 | msgid "Switch mode" | |
|
365 | msgstr "Переключить режим" | |
|
366 | ||
|
367 | #: templates/boards/thread.html:91 templates/boards/thread_gallery.html:61 | |
|
368 | msgid "Last update: " | |
|
369 | msgstr "Последнее обновление: " | |
|
370 |
@@ -1,201 +1,202 b'' | |||
|
1 | 1 | # coding=utf-8 |
|
2 | 2 | |
|
3 | 3 | import re |
|
4 | 4 | import bbcode |
|
5 | 5 | |
|
6 | 6 | import boards |
|
7 | 7 | |
|
8 | 8 | |
|
9 | 9 | __author__ = 'neko259' |
|
10 | 10 | |
|
11 | 11 | |
|
12 | 12 | REFLINK_PATTERN = re.compile(r'^\d+$') |
|
13 | 13 | GLOBAL_REFLINK_PATTERN = re.compile(r'^(\w+)::([^:]+)::(\d+)$') |
|
14 | 14 | MULTI_NEWLINES_PATTERN = re.compile(r'(\r?\n){2,}') |
|
15 | 15 | ONE_NEWLINE = '\n' |
|
16 | 16 | |
|
17 | 17 | |
|
18 | 18 | class TextFormatter(): |
|
19 | 19 | """ |
|
20 | 20 | An interface for formatter that can be used in the text format panel |
|
21 | 21 | """ |
|
22 | 22 | |
|
23 | 23 | def __init__(self): |
|
24 | 24 | pass |
|
25 | 25 | |
|
26 | 26 | name = '' |
|
27 | 27 | |
|
28 | 28 | # Left and right tags for the button preview |
|
29 | 29 | preview_left = '' |
|
30 | 30 | preview_right = '' |
|
31 | 31 | |
|
32 | 32 | # Left and right characters for the textarea input |
|
33 | 33 | format_left = '' |
|
34 | 34 | format_right = '' |
|
35 | 35 | |
|
36 | 36 | |
|
37 | 37 | class AutolinkPattern(): |
|
38 | 38 | def handleMatch(self, m): |
|
39 | 39 | link_element = etree.Element('a') |
|
40 | 40 | href = m.group(2) |
|
41 | 41 | link_element.set('href', href) |
|
42 | 42 | link_element.text = href |
|
43 | 43 | |
|
44 | 44 | return link_element |
|
45 | 45 | |
|
46 | 46 | |
|
47 | 47 | class QuotePattern(TextFormatter): |
|
48 | 48 | name = 'q' |
|
49 | 49 | preview_left = '<span class="multiquote">' |
|
50 | 50 | preview_right = '</span>' |
|
51 | 51 | |
|
52 | 52 | format_left = '[quote]' |
|
53 | 53 | format_right = '[/quote]' |
|
54 | 54 | |
|
55 | 55 | |
|
56 | 56 | class SpoilerPattern(TextFormatter): |
|
57 | 57 | name = 'spoiler' |
|
58 | 58 | preview_left = '<span class="spoiler">' |
|
59 | 59 | preview_right = '</span>' |
|
60 | 60 | |
|
61 | 61 | format_left = '[spoiler]' |
|
62 | 62 | format_right = '[/spoiler]' |
|
63 | 63 | |
|
64 | 64 | def handleMatch(self, m): |
|
65 | 65 | quote_element = etree.Element('span') |
|
66 | 66 | quote_element.set('class', 'spoiler') |
|
67 | 67 | quote_element.text = m.group(2) |
|
68 | 68 | |
|
69 | 69 | return quote_element |
|
70 | 70 | |
|
71 | 71 | |
|
72 | 72 | class CommentPattern(TextFormatter): |
|
73 | 73 | name = '' |
|
74 | 74 | preview_left = '<span class="comment">// ' |
|
75 | 75 | preview_right = '</span>' |
|
76 | 76 | |
|
77 | 77 | format_left = '[comment]' |
|
78 | 78 | format_right = '[/comment]' |
|
79 | 79 | |
|
80 | 80 | |
|
81 | 81 | # TODO Use <s> tag here |
|
82 | 82 | class StrikeThroughPattern(TextFormatter): |
|
83 | 83 | name = 's' |
|
84 | 84 | preview_left = '<span class="strikethrough">' |
|
85 | 85 | preview_right = '</span>' |
|
86 | 86 | |
|
87 | 87 | format_left = '[s]' |
|
88 | 88 | format_right = '[/s]' |
|
89 | 89 | |
|
90 | 90 | |
|
91 | 91 | class ItalicPattern(TextFormatter): |
|
92 | 92 | name = 'i' |
|
93 | 93 | preview_left = '<i>' |
|
94 | 94 | preview_right = '</i>' |
|
95 | 95 | |
|
96 | 96 | format_left = '[i]' |
|
97 | 97 | format_right = '[/i]' |
|
98 | 98 | |
|
99 | 99 | |
|
100 | 100 | class BoldPattern(TextFormatter): |
|
101 | 101 | name = 'b' |
|
102 | 102 | preview_left = '<b>' |
|
103 | 103 | preview_right = '</b>' |
|
104 | 104 | |
|
105 | 105 | format_left = '[b]' |
|
106 | 106 | format_right = '[/b]' |
|
107 | 107 | |
|
108 | 108 | |
|
109 | 109 | class CodePattern(TextFormatter): |
|
110 | 110 | name = 'code' |
|
111 | 111 | preview_left = '<code>' |
|
112 | 112 | preview_right = '</code>' |
|
113 | 113 | |
|
114 | 114 | format_left = '[code]' |
|
115 | 115 | format_right = '[/code]' |
|
116 | 116 | |
|
117 | 117 | |
|
118 | 118 | def render_reflink(tag_name, value, options, parent, context): |
|
119 | 119 | post_id = None |
|
120 | 120 | |
|
121 | 121 | matches = REFLINK_PATTERN.findall(value) |
|
122 | 122 | if matches: |
|
123 | 123 | post_id = int(matches[0][0]) |
|
124 | 124 | else: |
|
125 | 125 | match = GLOBAL_REFLINK_PATTERN.match(value) |
|
126 | 126 | if match: |
|
127 | 127 | key_type = match.group(1) |
|
128 | 128 | key = match.group(2) |
|
129 | 129 | local_id = match.group(3) |
|
130 | 130 | |
|
131 | 131 | try: |
|
132 | 132 | global_id = boards.models.GlobalId.objects.get(key_type=key_type, |
|
133 | 133 | key=key, local_id=local_id) |
|
134 | 134 | for post in boards.models.Post.objects.filter(global_id=global_id).only('id'): |
|
135 | 135 | post_id = post.id |
|
136 | 136 | except boards.models.GlobalId.DoesNotExist: |
|
137 | 137 | pass |
|
138 | 138 | |
|
139 | 139 | if not post_id: |
|
140 | 140 | return value |
|
141 | 141 | |
|
142 | 142 | posts = boards.models.Post.objects.filter(id=post_id) |
|
143 | 143 | if posts.exists(): |
|
144 | 144 | post = posts[0] |
|
145 | 145 | |
|
146 | 146 | return '<a href="%s">>>%s</a>' % (post.get_url(), post_id) |
|
147 | 147 | else: |
|
148 | 148 | return '>>%s' % value |
|
149 | 149 | |
|
150 | 150 | |
|
151 | 151 | def render_quote(tag_name, value, options, parent, context): |
|
152 | 152 | source = '' |
|
153 | 153 | if 'source' in options: |
|
154 | 154 | source = options['source'] |
|
155 | 155 | |
|
156 | 156 | result = '' |
|
157 | 157 | if source: |
|
158 | 158 | result = '<div class="multiquote"><div class="quote-header">%s</div><div class="quote-text">%s</div></div>' % (source, value) |
|
159 | 159 | else: |
|
160 | 160 | result = '<div class="multiquote"><div class="quote-text">%s</div></div>' % value |
|
161 | 161 | |
|
162 | 162 | return result |
|
163 | 163 | |
|
164 | 164 | |
|
165 | 165 | def preparse_text(text): |
|
166 | 166 | """ |
|
167 | 167 | Performs manual parsing before the bbcode parser is used. |
|
168 | 168 | """ |
|
169 | 169 | |
|
170 | 170 | return MULTI_NEWLINES_PATTERN.sub(ONE_NEWLINE, text) |
|
171 | 171 | |
|
172 | 172 | |
|
173 | 173 | def bbcode_extended(markup): |
|
174 | 174 | # The newline hack is added because br's margin does not work in all |
|
175 | 175 | # browsers except firefox, when the div's does. |
|
176 | 176 | parser = bbcode.Parser(newline='<div class="br"></div>') |
|
177 | 177 | parser.add_formatter('post', render_reflink, strip=True) |
|
178 | 178 | parser.add_formatter('quote', render_quote, strip=True) |
|
179 | 179 | parser.add_simple_formatter('comment', |
|
180 |
|
|
|
180 | '<span class="comment">//%(value)s</span>') | |
|
181 | 181 | parser.add_simple_formatter('spoiler', |
|
182 |
|
|
|
182 | '<span class="spoiler">%(value)s</span>') | |
|
183 | 183 | # TODO Use <s> here |
|
184 | 184 | parser.add_simple_formatter('s', |
|
185 |
|
|
|
185 | '<span class="strikethrough">%(value)s</span>') | |
|
186 | 186 | # TODO Why not use built-in tag? |
|
187 | 187 | parser.add_simple_formatter('code', |
|
188 |
|
|
|
188 | '<pre><code>%(value)s</pre></code>', | |
|
189 | render_embedded=False) | |
|
189 | 190 | |
|
190 | 191 | text = preparse_text(markup) |
|
191 | 192 | return parser.format(text) |
|
192 | 193 | |
|
193 | 194 | formatters = [ |
|
194 | 195 | QuotePattern, |
|
195 | 196 | SpoilerPattern, |
|
196 | 197 | ItalicPattern, |
|
197 | 198 | BoldPattern, |
|
198 | 199 | CommentPattern, |
|
199 | 200 | StrikeThroughPattern, |
|
200 | 201 | CodePattern, |
|
201 | 202 | ] |
@@ -1,46 +1,28 b'' | |||
|
1 | 1 | from django.shortcuts import redirect |
|
2 | 2 | from boards import utils |
|
3 | 3 | from boards.models import Ban |
|
4 | from django.utils.html import strip_spaces_between_tags | |
|
5 | from django.conf import settings | |
|
6 | from boards.views.banned import BannedView | |
|
7 | 4 | |
|
8 | 5 | RESPONSE_CONTENT_TYPE = 'Content-Type' |
|
9 | 6 | |
|
10 | 7 | TYPE_HTML = 'text/html' |
|
11 | 8 | |
|
12 | 9 | |
|
13 | 10 | class BanMiddleware: |
|
14 | 11 | """ |
|
15 | 12 | This is run before showing the thread. Banned users don't need to see |
|
16 | 13 | anything |
|
17 | 14 | """ |
|
18 | 15 | |
|
19 | 16 | def __init__(self): |
|
20 | 17 | pass |
|
21 | 18 | |
|
22 | 19 | def process_view(self, request, view_func, view_args, view_kwargs): |
|
23 | 20 | |
|
24 | if view_func != BannedView.as_view: | |
|
21 | if request.path != '/banned/': | |
|
25 | 22 | ip = utils.get_client_ip(request) |
|
26 | 23 | bans = Ban.objects.filter(ip=ip) |
|
27 | 24 | |
|
28 | 25 | if bans.exists(): |
|
29 | 26 | ban = bans[0] |
|
30 | 27 | if not ban.can_read: |
|
31 | 28 | return redirect('banned') |
|
32 | ||
|
33 | ||
|
34 | class MinifyHTMLMiddleware(object): | |
|
35 | def process_response(self, request, response): | |
|
36 | try: | |
|
37 | compress_html = settings.COMPRESS_HTML | |
|
38 | except AttributeError: | |
|
39 | compress_html = False | |
|
40 | ||
|
41 | if RESPONSE_CONTENT_TYPE in response\ | |
|
42 | and TYPE_HTML in response[RESPONSE_CONTENT_TYPE]\ | |
|
43 | and compress_html: | |
|
44 | response.content = strip_spaces_between_tags( | |
|
45 | response.content.strip()) | |
|
46 | return response No newline at end of file |
@@ -1,95 +1,113 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | import datetime | |
|
3 | from south.db import db | |
|
4 |
from |
|
|
5 | from django.db import models | |
|
2 | from __future__ import unicode_literals | |
|
3 | ||
|
4 | from django.db import models, migrations | |
|
5 | import boards.models.image | |
|
6 | import boards.models.base | |
|
7 | import boards.thumbs | |
|
6 | 8 | |
|
7 | 9 | |
|
8 |
class Migration( |
|
|
9 | ||
|
10 | def forwards(self, orm): | |
|
11 | # Adding model 'Tag' | |
|
12 | db.create_table(u'boards_tag', ( | |
|
13 | (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), | |
|
14 | ('name', self.gf('django.db.models.fields.CharField')(max_length=100)), | |
|
15 | )) | |
|
16 | db.send_create_signal(u'boards', ['Tag']) | |
|
10 | class Migration(migrations.Migration): | |
|
17 | 11 | |
|
18 | # Adding model 'Post' | |
|
19 | db.create_table(u'boards_post', ( | |
|
20 | (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), | |
|
21 | ('title', self.gf('django.db.models.fields.CharField')(max_length=50)), | |
|
22 | ('pub_time', self.gf('django.db.models.fields.DateTimeField')()), | |
|
23 | ('text', self.gf('markupfield.fields.MarkupField')(rendered_field=True)), | |
|
24 | ('text_markup_type', self.gf('django.db.models.fields.CharField')(default='markdown', max_length=30)), | |
|
25 | ('image', self.gf('boards.thumbs.ImageWithThumbsField')(max_length=100, blank=True)), | |
|
26 | ('poster_ip', self.gf('django.db.models.fields.IPAddressField')(max_length=15)), | |
|
27 | ('_text_rendered', self.gf('django.db.models.fields.TextField')()), | |
|
28 | ('poster_user_agent', self.gf('django.db.models.fields.TextField')()), | |
|
29 | ('parent', self.gf('django.db.models.fields.BigIntegerField')()), | |
|
30 | ('last_edit_time', self.gf('django.db.models.fields.DateTimeField')()), | |
|
31 | )) | |
|
32 | db.send_create_signal(u'boards', ['Post']) | |
|
33 | ||
|
34 | # Adding M2M table for field tags on 'Post' | |
|
35 | m2m_table_name = db.shorten_name(u'boards_post_tags') | |
|
36 | db.create_table(m2m_table_name, ( | |
|
37 | ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), | |
|
38 | ('post', models.ForeignKey(orm[u'boards.post'], null=False)), | |
|
39 | ('tag', models.ForeignKey(orm[u'boards.tag'], null=False)) | |
|
40 | )) | |
|
41 | db.create_unique(m2m_table_name, ['post_id', 'tag_id']) | |
|
42 | ||
|
43 | # Adding model 'Admin' | |
|
44 | db.create_table(u'boards_admin', ( | |
|
45 | (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), | |
|
46 | ('name', self.gf('django.db.models.fields.CharField')(max_length=100)), | |
|
47 | ('password', self.gf('django.db.models.fields.CharField')(max_length=100)), | |
|
48 | )) | |
|
49 | db.send_create_signal(u'boards', ['Admin']) | |
|
50 | ||
|
12 | dependencies = [ | |
|
13 | ] | |
|
51 | 14 | |
|
52 | def backwards(self, orm): | |
|
53 | # Deleting model 'Tag' | |
|
54 | db.delete_table(u'boards_tag') | |
|
55 | ||
|
56 | # Deleting model 'Post' | |
|
57 | db.delete_table(u'boards_post') | |
|
58 | ||
|
59 | # Removing M2M table for field tags on 'Post' | |
|
60 | db.delete_table(db.shorten_name(u'boards_post_tags')) | |
|
61 | ||
|
62 | # Deleting model 'Admin' | |
|
63 | db.delete_table(u'boards_admin') | |
|
64 | ||
|
65 | ||
|
66 | models = { | |
|
67 | u'boards.admin': { | |
|
68 | 'Meta': {'object_name': 'Admin'}, | |
|
69 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), | |
|
70 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), | |
|
71 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '100'}) | |
|
72 | }, | |
|
73 | u'boards.post': { | |
|
74 | 'Meta': {'object_name': 'Post'}, | |
|
75 | '_text_rendered': ('django.db.models.fields.TextField', [], {}), | |
|
76 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), | |
|
77 | 'image': ('boards.thumbs.ImageWithThumbsField', [], {'max_length': '100', 'blank': 'True'}), | |
|
78 | 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}), | |
|
79 | 'parent': ('django.db.models.fields.BigIntegerField', [], {}), | |
|
80 | 'poster_ip': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}), | |
|
81 | 'poster_user_agent': ('django.db.models.fields.TextField', [], {}), | |
|
82 | 'pub_time': ('django.db.models.fields.DateTimeField', [], {}), | |
|
83 | 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['boards.Tag']", 'symmetrical': 'False'}), | |
|
84 | 'text': ('markupfield.fields.MarkupField', [], {'rendered_field': 'True'}), | |
|
85 | 'text_markup_type': ('django.db.models.fields.CharField', [], {'default': "'markdown'", 'max_length': '30'}), | |
|
86 | 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}) | |
|
87 | }, | |
|
88 | u'boards.tag': { | |
|
89 | 'Meta': {'object_name': 'Tag'}, | |
|
90 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), | |
|
91 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) | |
|
92 | } | |
|
93 | } | |
|
94 | ||
|
95 | complete_apps = ['boards'] No newline at end of file | |
|
15 | operations = [ | |
|
16 | migrations.CreateModel( | |
|
17 | name='Ban', | |
|
18 | fields=[ | |
|
19 | ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')), | |
|
20 | ('ip', models.GenericIPAddressField()), | |
|
21 | ('reason', models.CharField(max_length=200, default='Auto')), | |
|
22 | ('can_read', models.BooleanField(default=True)), | |
|
23 | ], | |
|
24 | options={ | |
|
25 | }, | |
|
26 | bases=(models.Model,), | |
|
27 | ), | |
|
28 | migrations.CreateModel( | |
|
29 | name='Post', | |
|
30 | fields=[ | |
|
31 | ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')), | |
|
32 | ('title', models.CharField(max_length=200)), | |
|
33 | ('pub_time', models.DateTimeField()), | |
|
34 | ('text', models.TextField(null=True, blank=True)), | |
|
35 | ('text_markup_type', models.CharField(choices=[('', '--'), ('bbcode', 'bbcode')], max_length=30, default='bbcode')), | |
|
36 | ('poster_ip', models.GenericIPAddressField()), | |
|
37 | ('_text_rendered', models.TextField(editable=False)), | |
|
38 | ('poster_user_agent', models.TextField()), | |
|
39 | ('last_edit_time', models.DateTimeField()), | |
|
40 | ('refmap', models.TextField(null=True, blank=True)), | |
|
41 | ], | |
|
42 | options={ | |
|
43 | 'ordering': ('id',), | |
|
44 | }, | |
|
45 | bases=(models.Model, boards.models.base.Viewable), | |
|
46 | ), | |
|
47 | migrations.CreateModel( | |
|
48 | name='PostImage', | |
|
49 | fields=[ | |
|
50 | ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')), | |
|
51 | ('width', models.IntegerField(default=0)), | |
|
52 | ('height', models.IntegerField(default=0)), | |
|
53 | ('pre_width', models.IntegerField(default=0)), | |
|
54 | ('pre_height', models.IntegerField(default=0)), | |
|
55 | ('image', boards.thumbs.ImageWithThumbsField(height_field='height', width_field='width', upload_to=boards.models.image.PostImage._update_image_filename, blank=True)), | |
|
56 | ('hash', models.CharField(max_length=36)), | |
|
57 | ], | |
|
58 | options={ | |
|
59 | 'ordering': ('id',), | |
|
60 | }, | |
|
61 | bases=(models.Model,), | |
|
62 | ), | |
|
63 | migrations.CreateModel( | |
|
64 | name='Tag', | |
|
65 | fields=[ | |
|
66 | ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')), | |
|
67 | ('name', models.CharField(db_index=True, max_length=100)), | |
|
68 | ], | |
|
69 | options={ | |
|
70 | 'ordering': ('name',), | |
|
71 | }, | |
|
72 | bases=(models.Model, boards.models.base.Viewable), | |
|
73 | ), | |
|
74 | migrations.CreateModel( | |
|
75 | name='Thread', | |
|
76 | fields=[ | |
|
77 | ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')), | |
|
78 | ('bump_time', models.DateTimeField()), | |
|
79 | ('last_edit_time', models.DateTimeField()), | |
|
80 | ('archived', models.BooleanField(default=False)), | |
|
81 | ('bumpable', models.BooleanField(default=True)), | |
|
82 | ('replies', models.ManyToManyField(null=True, related_name='tre+', to='boards.Post', blank=True)), | |
|
83 | ('tags', models.ManyToManyField(to='boards.Tag')), | |
|
84 | ], | |
|
85 | options={ | |
|
86 | }, | |
|
87 | bases=(models.Model,), | |
|
88 | ), | |
|
89 | migrations.AddField( | |
|
90 | model_name='tag', | |
|
91 | name='threads', | |
|
92 | field=models.ManyToManyField(null=True, related_name='tag+', to='boards.Thread', blank=True), | |
|
93 | preserve_default=True, | |
|
94 | ), | |
|
95 | migrations.AddField( | |
|
96 | model_name='post', | |
|
97 | name='images', | |
|
98 | field=models.ManyToManyField(null=True, db_index=True, related_name='ip+', to='boards.PostImage', blank=True), | |
|
99 | preserve_default=True, | |
|
100 | ), | |
|
101 | migrations.AddField( | |
|
102 | model_name='post', | |
|
103 | name='referenced_posts', | |
|
104 | field=models.ManyToManyField(null=True, db_index=True, related_name='rfp+', to='boards.Post', blank=True), | |
|
105 | preserve_default=True, | |
|
106 | ), | |
|
107 | migrations.AddField( | |
|
108 | model_name='post', | |
|
109 | name='thread_new', | |
|
110 | field=models.ForeignKey(null=True, default=None, to='boards.Thread'), | |
|
111 | preserve_default=True, | |
|
112 | ), | |
|
113 | ] |
@@ -1,10 +1,18 b'' | |||
|
1 | 1 | __author__ = 'neko259' |
|
2 | 2 | |
|
3 | 3 | |
|
4 | 4 | class Viewable(): |
|
5 | 5 | def __init__(self): |
|
6 | 6 | pass |
|
7 | 7 | |
|
8 | 8 | def get_view(self, *args, **kwargs): |
|
9 | """Get an HTML view for a model""" | |
|
10 | pass No newline at end of file | |
|
9 | """ | |
|
10 | Gets an HTML view for a model | |
|
11 | """ | |
|
12 | pass | |
|
13 | ||
|
14 | def get_search_view(self, *args, **kwargs): | |
|
15 | """ | |
|
16 | Gets an HTML view for search. | |
|
17 | """ | |
|
18 | pass |
@@ -1,62 +1,83 b'' | |||
|
1 | 1 | import hashlib |
|
2 | 2 | import os |
|
3 | 3 | from random import random |
|
4 | 4 | import time |
|
5 | 5 | from django.db import models |
|
6 | 6 | from boards import thumbs |
|
7 | from boards.models.base import Viewable | |
|
7 | 8 | |
|
8 | 9 | __author__ = 'neko259' |
|
9 | 10 | |
|
10 | 11 | |
|
11 | 12 | IMAGE_THUMB_SIZE = (200, 150) |
|
12 | 13 | IMAGES_DIRECTORY = 'images/' |
|
13 | 14 | FILE_EXTENSION_DELIMITER = '.' |
|
15 | HASH_LENGTH = 36 | |
|
16 | ||
|
17 | CSS_CLASS_IMAGE = 'image' | |
|
18 | CSS_CLASS_THUMB = 'thumb' | |
|
14 | 19 | |
|
15 | 20 | |
|
16 | class PostImage(models.Model): | |
|
21 | class PostImage(models.Model, Viewable): | |
|
17 | 22 | class Meta: |
|
18 | 23 | app_label = 'boards' |
|
19 | 24 | ordering = ('id',) |
|
20 | 25 | |
|
21 | 26 | def _update_image_filename(self, filename): |
|
22 | 27 | """ |
|
23 | 28 | Gets unique image filename |
|
24 | 29 | """ |
|
25 | 30 | |
|
26 | 31 | path = IMAGES_DIRECTORY |
|
27 | 32 | new_name = str(int(time.mktime(time.gmtime()))) |
|
28 | 33 | new_name += str(int(random() * 1000)) |
|
29 | 34 | new_name += FILE_EXTENSION_DELIMITER |
|
30 | 35 | new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0] |
|
31 | 36 | |
|
32 | 37 | return os.path.join(path, new_name) |
|
33 | 38 | |
|
34 | 39 | width = models.IntegerField(default=0) |
|
35 | 40 | height = models.IntegerField(default=0) |
|
36 | 41 | |
|
37 | 42 | pre_width = models.IntegerField(default=0) |
|
38 | 43 | pre_height = models.IntegerField(default=0) |
|
39 | 44 | |
|
40 | 45 | image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename, |
|
41 | 46 | blank=True, sizes=(IMAGE_THUMB_SIZE,), |
|
42 | 47 | width_field='width', |
|
43 | 48 | height_field='height', |
|
44 | 49 | preview_width_field='pre_width', |
|
45 | 50 | preview_height_field='pre_height') |
|
46 |
hash = models.CharField(max_length= |
|
|
51 | hash = models.CharField(max_length=HASH_LENGTH) | |
|
47 | 52 | |
|
48 | 53 | def save(self, *args, **kwargs): |
|
49 | 54 | """ |
|
50 | 55 | Saves the model and computes the image hash for deduplication purposes. |
|
51 | 56 | """ |
|
52 | 57 | |
|
53 | 58 | if not self.pk and self.image: |
|
54 | 59 | md5 = hashlib.md5() |
|
55 | 60 | for chunk in self.image.chunks(): |
|
56 | 61 | md5.update(chunk) |
|
57 | 62 | self.hash = md5.hexdigest() |
|
58 | 63 | super(PostImage, self).save(*args, **kwargs) |
|
59 | 64 | |
|
60 | 65 | def __str__(self): |
|
61 | 66 | return self.image.url |
|
62 | 67 | |
|
68 | def get_view(self): | |
|
69 | return '<div class="{}">' \ | |
|
70 | '<a class="{}" href="{}">' \ | |
|
71 | '<img' \ | |
|
72 | ' src="{}"' \ | |
|
73 | ' alt="{}"' \ | |
|
74 | ' width="{}"' \ | |
|
75 | ' height="{}"' \ | |
|
76 | ' data-width="{}"' \ | |
|
77 | ' data-height="{}" />' \ | |
|
78 | '</a>' \ | |
|
79 | '</div>'\ | |
|
80 | .format(CSS_CLASS_IMAGE, CSS_CLASS_THUMB, self.image.url, | |
|
81 | self.image.url_200x150, | |
|
82 | str(self.hash), str(self.pre_width), | |
|
83 | str(self.pre_height), str(self.width), str(self.height)) |
@@ -1,518 +1,623 b'' | |||
|
1 | 1 | from datetime import datetime, timedelta, date |
|
2 | 2 | from datetime import time as dtime |
|
3 | 3 | import logging |
|
4 | 4 | import re |
|
5 | 5 | import xml.etree.ElementTree as et |
|
6 | 6 | |
|
7 | from adjacent import Client | |
|
7 | 8 | from django.core.cache import cache |
|
8 | 9 | from django.core.urlresolvers import reverse |
|
9 | 10 | from django.db import models, transaction |
|
11 | from django.db.models import TextField | |
|
10 | 12 | from django.template.loader import render_to_string |
|
11 | 13 | from django.utils import timezone |
|
12 | 14 | |
|
13 | from markupfield.fields import MarkupField | |
|
14 | ||
|
15 | 15 | from boards.models import PostImage, KeyPair, GlobalId, Signature |
|
16 | from boards import settings | |
|
17 | from boards.mdx_neboard import bbcode_extended | |
|
18 | from boards.models import PostImage | |
|
16 | 19 | from boards.models.base import Viewable |
|
17 | 20 | from boards.models.thread import Thread |
|
18 | 21 | from boards import utils |
|
22 | from boards.utils import datetime_to_epoch | |
|
19 | 23 | |
|
20 | 24 | ENCODING_UNICODE = 'unicode' |
|
21 | 25 | |
|
26 | WS_NOTIFICATION_TYPE_NEW_POST = 'new_post' | |
|
27 | WS_NOTIFICATION_TYPE = 'notification_type' | |
|
28 | ||
|
29 | WS_CHANNEL_THREAD = "thread:" | |
|
30 | ||
|
22 | 31 | APP_LABEL_BOARDS = 'boards' |
|
23 | 32 | |
|
24 | 33 | CACHE_KEY_PPD = 'ppd' |
|
25 | 34 | CACHE_KEY_POST_URL = 'post_url' |
|
26 | 35 | |
|
27 | 36 | POSTS_PER_DAY_RANGE = 7 |
|
28 | 37 | |
|
29 | 38 | BAN_REASON_AUTO = 'Auto' |
|
30 | 39 | |
|
31 | 40 | IMAGE_THUMB_SIZE = (200, 150) |
|
32 | 41 | |
|
33 | 42 | TITLE_MAX_LENGTH = 200 |
|
34 | 43 | |
|
35 | DEFAULT_MARKUP_TYPE = 'bbcode' | |
|
36 | ||
|
37 | 44 | # TODO This should be removed |
|
38 | 45 | NO_IP = '0.0.0.0' |
|
39 | 46 | |
|
40 | 47 | # TODO Real user agent should be saved instead of this |
|
41 | 48 | UNKNOWN_UA = '' |
|
42 | 49 | |
|
43 | 50 | REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]') |
|
44 | 51 | REGEX_GLOBAL_REPLY = re.compile(r'\[post\](\w+)::([^:]+)::(\d+)\[/post\]') |
|
45 | 52 | |
|
46 | 53 | TAG_MODEL = 'model' |
|
47 | 54 | TAG_REQUEST = 'request' |
|
48 | 55 | TAG_RESPONSE = 'response' |
|
49 | 56 | TAG_ID = 'id' |
|
50 | 57 | TAG_STATUS = 'status' |
|
51 | 58 | TAG_MODELS = 'models' |
|
52 | 59 | TAG_TITLE = 'title' |
|
53 | 60 | TAG_TEXT = 'text' |
|
54 | 61 | TAG_THREAD = 'thread' |
|
55 | 62 | TAG_PUB_TIME = 'pub-time' |
|
56 | 63 | TAG_SIGNATURES = 'signatures' |
|
57 | 64 | TAG_SIGNATURE = 'signature' |
|
58 | 65 | TAG_CONTENT = 'content' |
|
59 | 66 | TAG_ATTACHMENTS = 'attachments' |
|
60 | 67 | TAG_ATTACHMENT = 'attachment' |
|
61 | 68 | |
|
62 | 69 | TYPE_GET = 'get' |
|
63 | 70 | |
|
64 | 71 | ATTR_VERSION = 'version' |
|
65 | 72 | ATTR_TYPE = 'type' |
|
66 | 73 | ATTR_NAME = 'name' |
|
67 | 74 | ATTR_VALUE = 'value' |
|
68 | 75 | ATTR_MIMETYPE = 'mimetype' |
|
69 | 76 | |
|
70 | 77 | STATUS_SUCCESS = 'success' |
|
71 | 78 | |
|
72 | logger = logging.getLogger(__name__) | |
|
79 | PARAMETER_TRUNCATED = 'truncated' | |
|
80 | PARAMETER_TAG = 'tag' | |
|
81 | PARAMETER_OFFSET = 'offset' | |
|
82 | PARAMETER_DIFF_TYPE = 'type' | |
|
83 | PARAMETER_BUMPABLE = 'bumpable' | |
|
84 | PARAMETER_THREAD = 'thread' | |
|
85 | PARAMETER_IS_OPENING = 'is_opening' | |
|
86 | PARAMETER_MODERATOR = 'moderator' | |
|
87 | PARAMETER_POST = 'post' | |
|
88 | PARAMETER_OP_ID = 'opening_post_id' | |
|
89 | PARAMETER_NEED_OPEN_LINK = 'need_open_link' | |
|
90 | ||
|
91 | DIFF_TYPE_HTML = 'html' | |
|
92 | DIFF_TYPE_JSON = 'json' | |
|
93 | ||
|
94 | PREPARSE_PATTERNS = { | |
|
95 | r'>>(\d+)': r'[post]\1[/post]', # Reflink ">>123" | |
|
96 | r'^>([^>].+)': r'[quote]\1[/quote]', # Quote ">text" | |
|
97 | r'^//(.+)': r'[comment]\1[/comment]', # Comment "//text" | |
|
98 | } | |
|
73 | 99 | |
|
74 | 100 | |
|
75 | 101 | class PostManager(models.Manager): |
|
76 | def create_post(self, title, text, image=None, thread=None, ip=NO_IP, | |
|
77 | tags=None): | |
|
102 | @transaction.atomic | |
|
103 | def create_post(self, title: str, text: str, image=None, thread=None, | |
|
104 | ip=NO_IP, tags: list=None): | |
|
78 | 105 | """ |
|
79 | 106 | Creates new post |
|
80 | 107 | """ |
|
81 | 108 | |
|
82 | 109 | if not tags: |
|
83 | 110 | tags = [] |
|
84 | 111 | |
|
85 | 112 | posting_time = timezone.now() |
|
86 | 113 | if not thread: |
|
87 | 114 | thread = Thread.objects.create(bump_time=posting_time, |
|
88 | 115 | last_edit_time=posting_time) |
|
89 | 116 | new_thread = True |
|
90 | 117 | else: |
|
91 | thread.bump() | |
|
92 | thread.last_edit_time = posting_time | |
|
93 | thread.save() | |
|
94 | 118 | new_thread = False |
|
95 | 119 | |
|
120 | pre_text = self._preparse_text(text) | |
|
121 | ||
|
96 | 122 | post = self.create(title=title, |
|
97 | text=text, | |
|
123 | text=pre_text, | |
|
98 | 124 | pub_time=posting_time, |
|
99 | 125 | thread_new=thread, |
|
100 | 126 | poster_ip=ip, |
|
101 | 127 | poster_user_agent=UNKNOWN_UA, # TODO Get UA at |
|
102 | 128 | # last! |
|
103 | 129 | last_edit_time=posting_time) |
|
104 | 130 | |
|
105 | 131 | post.set_global_id() |
|
106 | 132 | |
|
133 | logger = logging.getLogger('boards.post.create') | |
|
134 | ||
|
135 | logger.info('Created post {} by {}'.format( | |
|
136 | post, post.poster_ip)) | |
|
137 | ||
|
107 | 138 | if image: |
|
108 | 139 | post_image = PostImage.objects.create(image=image) |
|
109 | 140 | post.images.add(post_image) |
|
110 |
logger.info('Created image # |
|
|
111 | post.id)) | |
|
141 | logger.info('Created image #{} for post #{}'.format( | |
|
142 | post_image.id, post.id)) | |
|
112 | 143 | |
|
113 | 144 | thread.replies.add(post) |
|
114 | 145 | list(map(thread.add_tag, tags)) |
|
115 | 146 | |
|
116 | 147 | if new_thread: |
|
117 | 148 | Thread.objects.process_oldest_threads() |
|
118 | self.connect_replies(post) | |
|
149 | else: | |
|
150 | thread.bump() | |
|
151 | thread.last_edit_time = posting_time | |
|
152 | thread.save() | |
|
119 | 153 | |
|
120 | logger.info('Created post #%d with title %s' | |
|
121 | % (post.id, post.get_title())) | |
|
154 | self.connect_replies(post) | |
|
122 | 155 | |
|
123 | 156 | return post |
|
124 | 157 | |
|
125 | def delete_post(self, post): | |
|
126 | """ | |
|
127 | Deletes post and update or delete its thread | |
|
128 | """ | |
|
129 | ||
|
130 | post_id = post.id | |
|
131 | ||
|
132 | thread = post.get_thread() | |
|
133 | ||
|
134 | if post.is_opening(): | |
|
135 | thread.delete() | |
|
136 | else: | |
|
137 | thread.last_edit_time = timezone.now() | |
|
138 | thread.save() | |
|
139 | ||
|
140 | post.delete() | |
|
141 | ||
|
142 | logger.info('Deleted post #%d (%s)' % (post_id, post.get_title())) | |
|
143 | ||
|
144 | 158 | def delete_posts_by_ip(self, ip): |
|
145 | 159 | """ |
|
146 | 160 | Deletes all posts of the author with same IP |
|
147 | 161 | """ |
|
148 | 162 | |
|
149 | 163 | posts = self.filter(poster_ip=ip) |
|
150 | 164 | for post in posts: |
|
151 |
|
|
|
165 | post.delete() | |
|
152 | 166 | |
|
153 | 167 | # TODO This can be moved into a post |
|
154 | 168 | def connect_replies(self, post): |
|
155 | 169 | """ |
|
156 | 170 | Connects replies to a post to show them as a reflink map |
|
157 | 171 | """ |
|
158 | 172 | |
|
159 |
for reply_number in post.get_r |
|
|
160 |
|
|
|
173 | for reply_number in re.finditer(REGEX_REPLY, post.get_raw_text()): | |
|
174 | post_id = reply_number.group(1) | |
|
175 | ref_post = self.filter(id=post_id) | |
|
161 | 176 | if ref_post.count() > 0: |
|
162 | 177 | referenced_post = ref_post[0] |
|
163 | 178 | referenced_post.referenced_posts.add(post) |
|
164 | 179 | referenced_post.last_edit_time = post.pub_time |
|
165 | 180 | referenced_post.build_refmap() |
|
166 | 181 | referenced_post.save(update_fields=['refmap', 'last_edit_time']) |
|
167 | 182 | |
|
168 | 183 | referenced_thread = referenced_post.get_thread() |
|
169 | 184 | referenced_thread.last_edit_time = post.pub_time |
|
170 | 185 | referenced_thread.save(update_fields=['last_edit_time']) |
|
171 | 186 | |
|
172 | 187 | def get_posts_per_day(self): |
|
173 | 188 | """ |
|
174 | 189 | Gets average count of posts per day for the last 7 days |
|
175 | 190 | """ |
|
176 | 191 | |
|
177 | 192 | day_end = date.today() |
|
178 | 193 | day_start = day_end - timedelta(POSTS_PER_DAY_RANGE) |
|
179 | 194 | |
|
180 | 195 | cache_key = CACHE_KEY_PPD + str(day_end) |
|
181 | 196 | ppd = cache.get(cache_key) |
|
182 | 197 | if ppd: |
|
183 | 198 | return ppd |
|
184 | 199 | |
|
185 | 200 | day_time_start = timezone.make_aware(datetime.combine( |
|
186 | 201 | day_start, dtime()), timezone.get_current_timezone()) |
|
187 | 202 | day_time_end = timezone.make_aware(datetime.combine( |
|
188 | 203 | day_end, dtime()), timezone.get_current_timezone()) |
|
189 | 204 | |
|
190 | 205 | posts_per_period = float(self.filter( |
|
191 | 206 | pub_time__lte=day_time_end, |
|
192 | 207 | pub_time__gte=day_time_start).count()) |
|
193 | 208 | |
|
194 | 209 | ppd = posts_per_period / POSTS_PER_DAY_RANGE |
|
195 | 210 | |
|
196 | 211 | cache.set(cache_key, ppd) |
|
197 | 212 | return ppd |
|
198 | 213 | |
|
199 | 214 | # TODO Make a separate sync facade? |
|
200 | 215 | def generate_response_get(self, model_list: list): |
|
201 | 216 | response = et.Element(TAG_RESPONSE) |
|
202 | 217 | |
|
203 | 218 | status = et.SubElement(response, TAG_STATUS) |
|
204 | 219 | status.text = STATUS_SUCCESS |
|
205 | 220 | |
|
206 | 221 | models = et.SubElement(response, TAG_MODELS) |
|
207 | 222 | |
|
208 | 223 | for post in model_list: |
|
209 | 224 | model = et.SubElement(models, TAG_MODEL) |
|
210 | 225 | model.set(ATTR_NAME, 'post') |
|
211 | 226 | |
|
212 | 227 | content_tag = et.SubElement(model, TAG_CONTENT) |
|
213 | 228 | |
|
214 | 229 | tag_id = et.SubElement(content_tag, TAG_ID) |
|
215 | 230 | post.global_id.to_xml_element(tag_id) |
|
216 | 231 | |
|
217 | 232 | title = et.SubElement(content_tag, TAG_TITLE) |
|
218 | 233 | title.text = post.title |
|
219 | 234 | |
|
220 | 235 | text = et.SubElement(content_tag, TAG_TEXT) |
|
221 | 236 | # TODO Replace local links by global ones in the text |
|
222 | 237 | text.text = post.text.raw |
|
223 | 238 | |
|
224 | 239 | if not post.is_opening(): |
|
225 | 240 | thread = et.SubElement(content_tag, TAG_THREAD) |
|
226 | 241 | thread.text = str(post.get_thread().get_opening_post_id()) |
|
227 | 242 | else: |
|
228 | 243 | # TODO Output tags here |
|
229 | 244 | pass |
|
230 | 245 | |
|
231 | 246 | pub_time = et.SubElement(content_tag, TAG_PUB_TIME) |
|
232 | 247 | pub_time.text = str(post.get_pub_time_epoch()) |
|
233 | 248 | |
|
234 | 249 | signatures_tag = et.SubElement(model, TAG_SIGNATURES) |
|
235 | 250 | post_signatures = post.signature.all() |
|
236 | 251 | if post_signatures: |
|
237 | 252 | signatures = post.signatures |
|
238 | 253 | else: |
|
239 | 254 | # TODO Maybe the signature can be computed only once after |
|
240 | 255 | # the post is added? Need to add some on_save signal queue |
|
241 | 256 | # and add this there. |
|
242 | 257 | key = KeyPair.objects.get(public_key=post.global_id.key) |
|
243 | 258 | signatures = [Signature( |
|
244 | 259 | key_type=key.key_type, |
|
245 | 260 | key=key.public_key, |
|
246 | 261 | signature=key.sign(et.tostring(model, ENCODING_UNICODE)), |
|
247 | 262 | )] |
|
248 | 263 | for signature in signatures: |
|
249 | 264 | signature_tag = et.SubElement(signatures_tag, TAG_SIGNATURE) |
|
250 | 265 | signature_tag.set(ATTR_TYPE, signature.key_type) |
|
251 | 266 | signature_tag.set(ATTR_VALUE, signature.signature) |
|
252 | 267 | |
|
253 | 268 | return et.tostring(response, ENCODING_UNICODE) |
|
254 | 269 | |
|
255 | 270 | def parse_response_get(self, response_xml): |
|
256 | 271 | tag_root = et.fromstring(response_xml) |
|
257 | 272 | tag_status = tag_root[0] |
|
258 | 273 | if 'success' == tag_status.text: |
|
259 | 274 | tag_models = tag_root[1] |
|
260 | 275 | for tag_model in tag_models: |
|
261 | 276 | tag_content = tag_model[0] |
|
262 | 277 | tag_id = tag_content[1] |
|
263 | 278 | try: |
|
264 | 279 | GlobalId.from_xml_element(tag_id, existing=True) |
|
265 | 280 | # If this post already exists, just continue |
|
266 | 281 | # TODO Compare post content and update the post if necessary |
|
267 | 282 | pass |
|
268 | 283 | except GlobalId.DoesNotExist: |
|
269 | 284 | global_id = GlobalId.from_xml_element(tag_id) |
|
270 | 285 | |
|
271 | 286 | title = tag_content.find(TAG_TITLE).text |
|
272 | 287 | text = tag_content.find(TAG_TEXT).text |
|
273 | 288 | # TODO Check that the replied posts are already present |
|
274 | 289 | # before adding new ones |
|
275 | 290 | |
|
276 | 291 | # TODO Pub time, thread, tags |
|
277 | 292 | |
|
278 | 293 | post = Post.objects.create(title=title, text=text) |
|
279 | 294 | else: |
|
280 | 295 | # TODO Throw an exception? |
|
281 | 296 | pass |
|
282 | 297 | |
|
298 | def _preparse_text(self, text): | |
|
299 | """ | |
|
300 | Preparses text to change patterns like '>>' to a proper bbcode | |
|
301 | tags. | |
|
302 | """ | |
|
303 | ||
|
304 | for key, value in PREPARSE_PATTERNS.items(): | |
|
305 | text = re.sub(key, value, text, flags=re.MULTILINE) | |
|
306 | ||
|
307 | return text | |
|
308 | ||
|
283 | 309 | |
|
284 | 310 | class Post(models.Model, Viewable): |
|
285 | 311 | """A post is a message.""" |
|
286 | 312 | |
|
287 | 313 | objects = PostManager() |
|
288 | 314 | |
|
289 | 315 | class Meta: |
|
290 | 316 | app_label = APP_LABEL_BOARDS |
|
291 | 317 | ordering = ('id',) |
|
292 | 318 | |
|
293 | 319 | title = models.CharField(max_length=TITLE_MAX_LENGTH) |
|
294 | 320 | pub_time = models.DateTimeField() |
|
295 | text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE, | |
|
296 | escape_html=False) | |
|
321 | text = TextField(blank=True, null=True) | |
|
322 | _text_rendered = TextField(blank=True, null=True, editable=False) | |
|
297 | 323 | |
|
298 | 324 | images = models.ManyToManyField(PostImage, null=True, blank=True, |
|
299 | 325 | related_name='ip+', db_index=True) |
|
300 | 326 | |
|
301 | 327 | poster_ip = models.GenericIPAddressField() |
|
302 | 328 | poster_user_agent = models.TextField() |
|
303 | 329 | |
|
304 | 330 | thread_new = models.ForeignKey('Thread', null=True, default=None, |
|
305 | 331 | db_index=True) |
|
306 | 332 | last_edit_time = models.DateTimeField() |
|
307 | 333 | |
|
308 | 334 | # Replies to the post |
|
309 | 335 | referenced_posts = models.ManyToManyField('Post', symmetrical=False, |
|
310 | 336 | null=True, |
|
311 | 337 | blank=True, related_name='rfp+', |
|
312 | 338 | db_index=True) |
|
313 | 339 | |
|
314 | 340 | # Replies map. This is built from the referenced posts list to speed up |
|
315 | 341 | # page loading (no need to get all the referenced posts from the database). |
|
316 | 342 | refmap = models.TextField(null=True, blank=True) |
|
317 | 343 | |
|
318 | 344 | # Global ID with author key. If the message was downloaded from another |
|
319 | 345 | # server, this indicates the server. |
|
320 | 346 | global_id = models.OneToOneField('GlobalId', null=True, blank=True) |
|
321 | 347 | |
|
322 | 348 | # One post can be signed by many nodes that give their trust to it |
|
323 | 349 | signature = models.ManyToManyField('Signature', null=True, blank=True) |
|
324 | 350 | |
|
325 |
def __ |
|
|
326 |
return '#' |
|
|
327 | self.text.raw[:50] + ')' | |
|
351 | def __str__(self): | |
|
352 | return 'P#{}/{}'.format(self.id, self.title) | |
|
353 | ||
|
354 | def get_title(self) -> str: | |
|
355 | """ | |
|
356 | Gets original post title or part of its text. | |
|
357 | """ | |
|
328 | 358 | |
|
329 | def get_title(self): | |
|
330 |
|
|
|
359 | title = self.title | |
|
360 | if not title: | |
|
361 | title = self.get_text() | |
|
331 | 362 | |
|
332 | def build_refmap(self): | |
|
363 | return title | |
|
364 | ||
|
365 | def build_refmap(self) -> None: | |
|
333 | 366 | """ |
|
334 | 367 | Builds a replies map string from replies list. This is a cache to stop |
|
335 | 368 | the server from recalculating the map on every post show. |
|
336 | 369 | """ |
|
337 | 370 | map_string = '' |
|
338 | 371 | |
|
339 | 372 | first = True |
|
340 | 373 | for refpost in self.referenced_posts.all(): |
|
341 | 374 | if not first: |
|
342 | 375 | map_string += ', ' |
|
343 | 376 | map_string += '<a href="%s">>>%s</a>' % (refpost.get_url(), |
|
344 | 377 | refpost.id) |
|
345 | 378 | first = False |
|
346 | 379 | |
|
347 | 380 | self.refmap = map_string |
|
348 | 381 | |
|
349 | 382 | def get_sorted_referenced_posts(self): |
|
350 | 383 | return self.refmap |
|
351 | 384 | |
|
352 | def is_referenced(self): | |
|
353 |
|
|
|
385 | def is_referenced(self) -> bool: | |
|
386 | if not self.refmap: | |
|
387 | return False | |
|
388 | else: | |
|
389 | return len(self.refmap) > 0 | |
|
354 | 390 | |
|
355 | def is_opening(self): | |
|
391 | def is_opening(self) -> bool: | |
|
356 | 392 | """ |
|
357 | 393 | Checks if this is an opening post or just a reply. |
|
358 | 394 | """ |
|
359 | 395 | |
|
360 | 396 | return self.get_thread().get_opening_post_id() == self.id |
|
361 | 397 | |
|
362 | 398 | @transaction.atomic |
|
363 | 399 | def add_tag(self, tag): |
|
364 | 400 | edit_time = timezone.now() |
|
365 | 401 | |
|
366 | 402 | thread = self.get_thread() |
|
367 | 403 | thread.add_tag(tag) |
|
368 | 404 | self.last_edit_time = edit_time |
|
369 | 405 | self.save(update_fields=['last_edit_time']) |
|
370 | 406 | |
|
371 | 407 | thread.last_edit_time = edit_time |
|
372 | 408 | thread.save(update_fields=['last_edit_time']) |
|
373 | 409 | |
|
374 | @transaction.atomic | |
|
375 | def remove_tag(self, tag): | |
|
376 | edit_time = timezone.now() | |
|
377 | ||
|
378 | thread = self.get_thread() | |
|
379 | thread.remove_tag(tag) | |
|
380 | self.last_edit_time = edit_time | |
|
381 | self.save(update_fields=['last_edit_time']) | |
|
382 | ||
|
383 | thread.last_edit_time = edit_time | |
|
384 | thread.save(update_fields=['last_edit_time']) | |
|
385 | ||
|
386 | 410 | def get_url(self, thread=None): |
|
387 | 411 | """ |
|
388 | 412 | Gets full url to the post. |
|
389 | 413 | """ |
|
390 | 414 | |
|
391 | 415 | cache_key = CACHE_KEY_POST_URL + str(self.id) |
|
392 | 416 | link = cache.get(cache_key) |
|
393 | 417 | |
|
394 | 418 | if not link: |
|
395 | 419 | if not thread: |
|
396 | 420 | thread = self.get_thread() |
|
397 | 421 | |
|
398 | 422 | opening_id = thread.get_opening_post_id() |
|
399 | 423 | |
|
400 | 424 | if self.id != opening_id: |
|
401 | 425 | link = reverse('thread', kwargs={ |
|
402 | 426 | 'post_id': opening_id}) + '#' + str(self.id) |
|
403 | 427 | else: |
|
404 | 428 | link = reverse('thread', kwargs={'post_id': self.id}) |
|
405 | 429 | |
|
406 | 430 | cache.set(cache_key, link) |
|
407 | 431 | |
|
408 | 432 | return link |
|
409 | 433 | |
|
410 | def get_thread(self): | |
|
434 | def get_thread(self) -> Thread: | |
|
411 | 435 | """ |
|
412 | 436 | Gets post's thread. |
|
413 | 437 | """ |
|
414 | 438 | |
|
415 | 439 | return self.thread_new |
|
416 | 440 | |
|
417 | 441 | def get_referenced_posts(self): |
|
418 | 442 | return self.referenced_posts.only('id', 'thread_new') |
|
419 | 443 | |
|
420 | def get_text(self): | |
|
421 | return self.text | |
|
422 | ||
|
423 | 444 | def get_view(self, moderator=False, need_open_link=False, |
|
424 | 445 | truncated=False, *args, **kwargs): |
|
425 | if 'is_opening' in kwargs: | |
|
426 | is_opening = kwargs['is_opening'] | |
|
427 | else: | |
|
428 | is_opening = self.is_opening() | |
|
446 | """ | |
|
447 | Renders post's HTML view. Some of the post params can be passed over | |
|
448 | kwargs for the means of caching (if we view the thread, some params | |
|
449 | are same for every post and don't need to be computed over and over. | |
|
450 | """ | |
|
429 | 451 | |
|
430 | if 'thread' in kwargs: | |
|
431 | thread = kwargs['thread'] | |
|
432 | else: | |
|
433 | thread = self.get_thread() | |
|
434 | ||
|
435 | if 'can_bump' in kwargs: | |
|
436 | can_bump = kwargs['can_bump'] | |
|
437 | else: | |
|
438 | can_bump = thread.can_bump() | |
|
452 | is_opening = kwargs.get(PARAMETER_IS_OPENING, self.is_opening()) | |
|
453 | thread = kwargs.get(PARAMETER_THREAD, self.get_thread()) | |
|
454 | can_bump = kwargs.get(PARAMETER_BUMPABLE, thread.can_bump()) | |
|
439 | 455 | |
|
440 | 456 | if is_opening: |
|
441 | 457 | opening_post_id = self.id |
|
442 | 458 | else: |
|
443 | 459 | opening_post_id = thread.get_opening_post_id() |
|
444 | 460 | |
|
445 | 461 | return render_to_string('boards/post.html', { |
|
446 |
|
|
|
447 |
|
|
|
448 |
|
|
|
449 |
|
|
|
450 |
|
|
|
451 |
|
|
|
452 |
|
|
|
453 |
|
|
|
462 | PARAMETER_POST: self, | |
|
463 | PARAMETER_MODERATOR: moderator, | |
|
464 | PARAMETER_IS_OPENING: is_opening, | |
|
465 | PARAMETER_THREAD: thread, | |
|
466 | PARAMETER_BUMPABLE: can_bump, | |
|
467 | PARAMETER_NEED_OPEN_LINK: need_open_link, | |
|
468 | PARAMETER_TRUNCATED: truncated, | |
|
469 | PARAMETER_OP_ID: opening_post_id, | |
|
454 | 470 | }) |
|
455 | 471 | |
|
456 | def get_first_image(self): | |
|
472 | def get_search_view(self, *args, **kwargs): | |
|
473 | return self.get_view(args, kwargs) | |
|
474 | ||
|
475 | def get_first_image(self) -> PostImage: | |
|
457 | 476 | return self.images.earliest('id') |
|
458 | 477 | |
|
459 | 478 | def delete(self, using=None): |
|
460 | 479 | """ |
|
461 | Deletes all post images and the post itself. | |
|
480 | Deletes all post images and the post itself. If the post is opening, | |
|
481 | thread with all posts is deleted. | |
|
462 | 482 | """ |
|
463 | 483 | |
|
464 | 484 | self.images.all().delete() |
|
465 | 485 | self.signature.all().delete() |
|
466 | 486 | if self.global_id: |
|
467 | 487 | self.global_id.delete() |
|
468 | 488 | |
|
489 | if self.is_opening(): | |
|
490 | self.get_thread().delete() | |
|
491 | else: | |
|
492 | thread = self.get_thread() | |
|
493 | thread.last_edit_time = timezone.now() | |
|
494 | thread.save() | |
|
495 | ||
|
469 | 496 | super(Post, self).delete(using) |
|
497 | logging.getLogger('boards.post.delete').info( | |
|
498 | 'Deleted post {}'.format(self)) | |
|
470 | 499 | |
|
471 | 500 | def set_global_id(self, key_pair=None): |
|
472 | 501 | """ |
|
473 | 502 | Sets global id based on the given key pair. If no key pair is given, |
|
474 | 503 | default one is used. |
|
475 | 504 | """ |
|
476 | 505 | |
|
477 | 506 | if key_pair: |
|
478 | 507 | key = key_pair |
|
479 | 508 | else: |
|
480 | 509 | try: |
|
481 | 510 | key = KeyPair.objects.get(primary=True) |
|
482 | 511 | except KeyPair.DoesNotExist: |
|
483 | 512 | # Do not update the global id because there is no key defined |
|
484 | 513 | return |
|
485 | 514 | global_id = GlobalId(key_type=key.key_type, |
|
486 | 515 | key=key.public_key, |
|
487 | 516 | local_id = self.id) |
|
488 | 517 | global_id.save() |
|
489 | 518 | |
|
490 | 519 | self.global_id = global_id |
|
491 | 520 | |
|
492 | 521 | self.save(update_fields=['global_id']) |
|
493 | 522 | |
|
494 | 523 | def get_pub_time_epoch(self): |
|
495 | 524 | return utils.datetime_to_epoch(self.pub_time) |
|
496 | 525 | |
|
526 | # TODO Use this to connect replies | |
|
497 | 527 | def get_replied_ids(self): |
|
498 | 528 | """ |
|
499 | 529 | Gets ID list of the posts that this post replies. |
|
500 | 530 | """ |
|
501 | 531 | |
|
502 | 532 | local_replied = REGEX_REPLY.findall(self.text.raw) |
|
503 | 533 | global_replied = [] |
|
504 | 534 | # TODO Similar code is used in mdx_neboard, maybe it can be extracted |
|
505 | 535 | # into a method? |
|
506 | 536 | for match in REGEX_GLOBAL_REPLY.findall(self.text.raw): |
|
507 | 537 | key_type = match[0] |
|
508 | 538 | key = match[1] |
|
509 | 539 | local_id = match[2] |
|
510 | 540 | |
|
511 | 541 | try: |
|
512 | 542 | global_id = GlobalId.objects.get(key_type=key_type, |
|
513 | 543 | key=key, local_id=local_id) |
|
514 | 544 | for post in Post.objects.filter(global_id=global_id).only('id'): |
|
515 | 545 | global_replied.append(post.id) |
|
516 | 546 | except GlobalId.DoesNotExist: |
|
517 | 547 | pass |
|
518 | 548 | return local_replied + global_replied |
|
549 | ||
|
550 | ||
|
551 | def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None, | |
|
552 | include_last_update=False): | |
|
553 | """ | |
|
554 | Gets post HTML or JSON data that can be rendered on a page or used by | |
|
555 | API. | |
|
556 | """ | |
|
557 | ||
|
558 | if format_type == DIFF_TYPE_HTML: | |
|
559 | params = dict() | |
|
560 | params['post'] = self | |
|
561 | if PARAMETER_TRUNCATED in request.GET: | |
|
562 | params[PARAMETER_TRUNCATED] = True | |
|
563 | ||
|
564 | return render_to_string('boards/api_post.html', params) | |
|
565 | elif format_type == DIFF_TYPE_JSON: | |
|
566 | post_json = { | |
|
567 | 'id': self.id, | |
|
568 | 'title': self.title, | |
|
569 | 'text': self._text_rendered, | |
|
570 | } | |
|
571 | if self.images.exists(): | |
|
572 | post_image = self.get_first_image() | |
|
573 | post_json['image'] = post_image.image.url | |
|
574 | post_json['image_preview'] = post_image.image.url_200x150 | |
|
575 | if include_last_update: | |
|
576 | post_json['bump_time'] = datetime_to_epoch( | |
|
577 | self.thread_new.bump_time) | |
|
578 | return post_json | |
|
579 | ||
|
580 | def send_to_websocket(self, request, recursive=True): | |
|
581 | """ | |
|
582 | Sends post HTML data to the thread web socket. | |
|
583 | """ | |
|
584 | ||
|
585 | if not settings.WEBSOCKETS_ENABLED: | |
|
586 | return | |
|
587 | ||
|
588 | client = Client() | |
|
589 | ||
|
590 | thread = self.get_thread() | |
|
591 | thread_id = thread.id | |
|
592 | channel_name = WS_CHANNEL_THREAD + str(thread.get_opening_post_id()) | |
|
593 | client.publish(channel_name, { | |
|
594 | WS_NOTIFICATION_TYPE: WS_NOTIFICATION_TYPE_NEW_POST, | |
|
595 | }) | |
|
596 | client.send() | |
|
597 | ||
|
598 | logger = logging.getLogger('boards.post.websocket') | |
|
599 | ||
|
600 | logger.info('Sent notification from post #{} to channel {}'.format( | |
|
601 | self.id, channel_name)) | |
|
602 | ||
|
603 | if recursive: | |
|
604 | for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()): | |
|
605 | post_id = reply_number.group(1) | |
|
606 | ref_post = Post.objects.filter(id=post_id)[0] | |
|
607 | ||
|
608 | # If post is in this thread, its thread was already notified. | |
|
609 | # Otherwise, notify its thread separately. | |
|
610 | if ref_post.thread_new_id != thread_id: | |
|
611 | ref_post.send_to_websocket(request, recursive=False) | |
|
612 | ||
|
613 | def save(self, force_insert=False, force_update=False, using=None, | |
|
614 | update_fields=None): | |
|
615 | self._text_rendered = bbcode_extended(self.get_raw_text()) | |
|
616 | ||
|
617 | super().save(force_insert, force_update, using, update_fields) | |
|
618 | ||
|
619 | def get_text(self) -> str: | |
|
620 | return self._text_rendered | |
|
621 | ||
|
622 | def get_raw_text(self) -> str: | |
|
623 | return self.text |
@@ -1,78 +1,71 b'' | |||
|
1 | 1 | from django.template.loader import render_to_string |
|
2 | 2 | from django.db import models |
|
3 |
from django.db.models import Count |
|
|
3 | from django.db.models import Count | |
|
4 | 4 | from django.core.urlresolvers import reverse |
|
5 | 5 | |
|
6 | from boards.models import Thread | |
|
7 | 6 | from boards.models.base import Viewable |
|
8 | 7 | |
|
9 | 8 | |
|
10 | 9 | __author__ = 'neko259' |
|
11 | 10 | |
|
12 | 11 | |
|
13 | 12 | class TagManager(models.Manager): |
|
14 | 13 | |
|
15 | 14 | def get_not_empty_tags(self): |
|
16 | 15 | """ |
|
17 | 16 | Gets tags that have non-archived threads. |
|
18 | 17 | """ |
|
19 | 18 | |
|
20 | tags = self.annotate(Count('threads')) \ | |
|
21 | .filter(threads__count__gt=0).order_by('name') | |
|
22 | ||
|
23 | return tags | |
|
19 | return self.filter(thread__archived=False)\ | |
|
20 | .annotate(num_threads=Count('thread')).filter(num_threads__gt=0)\ | |
|
21 | .order_by('-required', 'name') | |
|
24 | 22 | |
|
25 | 23 | |
|
26 | 24 | class Tag(models.Model, Viewable): |
|
27 | 25 | """ |
|
28 | 26 | A tag is a text node assigned to the thread. The tag serves as a board |
|
29 | 27 | section. There can be multiple tags for each thread |
|
30 | 28 | """ |
|
31 | 29 | |
|
32 | 30 | objects = TagManager() |
|
33 | 31 | |
|
34 | 32 | class Meta: |
|
35 | 33 | app_label = 'boards' |
|
36 | 34 | ordering = ('name',) |
|
37 | 35 | |
|
38 | 36 | name = models.CharField(max_length=100, db_index=True) |
|
39 | threads = models.ManyToManyField(Thread, null=True, | |
|
40 | blank=True, related_name='tag+') | |
|
37 | required = models.BooleanField(default=False) | |
|
41 | 38 | |
|
42 |
def __ |
|
|
39 | def __str__(self): | |
|
43 | 40 | return self.name |
|
44 | 41 | |
|
45 | def is_empty(self): | |
|
42 | def is_empty(self) -> bool: | |
|
46 | 43 | """ |
|
47 | 44 | Checks if the tag has some threads. |
|
48 | 45 | """ |
|
49 | 46 | |
|
50 | 47 | return self.get_thread_count() == 0 |
|
51 | 48 | |
|
52 | def get_thread_count(self): | |
|
53 | return self.threads.count() | |
|
54 | ||
|
55 | def get_post_count(self, archived=False): | |
|
56 | """ | |
|
57 | Gets posts count for the tag's threads. | |
|
58 | """ | |
|
59 | ||
|
60 | posts_count = 0 | |
|
61 | ||
|
62 | threads = self.threads.filter(archived=archived) | |
|
63 | if threads.exists(): | |
|
64 | posts_count = threads.annotate(posts_count=Count('replies')) \ | |
|
65 | .aggregate(posts_sum=Sum('posts_count'))['posts_sum'] | |
|
66 | ||
|
67 | if not posts_count: | |
|
68 | posts_count = 0 | |
|
69 | ||
|
70 | return posts_count | |
|
49 | def get_thread_count(self) -> int: | |
|
50 | return self.get_threads().count() | |
|
71 | 51 | |
|
72 | 52 | def get_url(self): |
|
73 | 53 | return reverse('tag', kwargs={'tag_name': self.name}) |
|
74 | 54 | |
|
75 |
def get_ |
|
|
55 | def get_threads(self): | |
|
56 | return self.thread_set.order_by('-bump_time') | |
|
57 | ||
|
58 | def is_required(self): | |
|
59 | return self.required | |
|
60 | ||
|
61 | def get_view(self): | |
|
62 | link = '<a class="tag" href="{}">{}</a>'.format( | |
|
63 | self.get_url(), self.name) | |
|
64 | if self.is_required(): | |
|
65 | link = '<b>{}</b>'.format(link) | |
|
66 | return link | |
|
67 | ||
|
68 | def get_search_view(self, *args, **kwargs): | |
|
76 | 69 | return render_to_string('boards/tag.html', { |
|
77 | 70 | 'tag': self, |
|
78 | 71 | }) |
@@ -1,188 +1,182 b'' | |||
|
1 | 1 | import logging |
|
2 | from django.db.models import Count | |
|
2 | from django.db.models import Count, Sum | |
|
3 | 3 | from django.utils import timezone |
|
4 | 4 | from django.core.cache import cache |
|
5 | 5 | from django.db import models |
|
6 | 6 | from boards import settings |
|
7 | 7 | |
|
8 | 8 | __author__ = 'neko259' |
|
9 | 9 | |
|
10 | 10 | |
|
11 | 11 | logger = logging.getLogger(__name__) |
|
12 | 12 | |
|
13 | 13 | |
|
14 | 14 | CACHE_KEY_OPENING_POST = 'opening_post_id' |
|
15 | 15 | |
|
16 | 16 | |
|
17 | 17 | class ThreadManager(models.Manager): |
|
18 | 18 | def process_oldest_threads(self): |
|
19 | 19 | """ |
|
20 | 20 | Preserves maximum thread count. If there are too many threads, |
|
21 | 21 | archive or delete the old ones. |
|
22 | 22 | """ |
|
23 | 23 | |
|
24 | 24 | threads = Thread.objects.filter(archived=False).order_by('-bump_time') |
|
25 | 25 | thread_count = threads.count() |
|
26 | 26 | |
|
27 | 27 | if thread_count > settings.MAX_THREAD_COUNT: |
|
28 | 28 | num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT |
|
29 | 29 | old_threads = threads[thread_count - num_threads_to_delete:] |
|
30 | 30 | |
|
31 | 31 | for thread in old_threads: |
|
32 | 32 | if settings.ARCHIVE_THREADS: |
|
33 | 33 | self._archive_thread(thread) |
|
34 | 34 | else: |
|
35 | 35 | thread.delete() |
|
36 | 36 | |
|
37 | 37 | logger.info('Processed %d old threads' % num_threads_to_delete) |
|
38 | 38 | |
|
39 | 39 | def _archive_thread(self, thread): |
|
40 | 40 | thread.archived = True |
|
41 | thread.bumpable = False | |
|
41 | 42 | thread.last_edit_time = timezone.now() |
|
42 | thread.save(update_fields=['archived', 'last_edit_time']) | |
|
43 | thread.save(update_fields=['archived', 'last_edit_time', 'bumpable']) | |
|
43 | 44 | |
|
44 | 45 | |
|
45 | 46 | class Thread(models.Model): |
|
46 | 47 | objects = ThreadManager() |
|
47 | 48 | |
|
48 | 49 | class Meta: |
|
49 | 50 | app_label = 'boards' |
|
50 | 51 | |
|
51 | 52 | tags = models.ManyToManyField('Tag') |
|
52 | 53 | bump_time = models.DateTimeField() |
|
53 | 54 | last_edit_time = models.DateTimeField() |
|
54 | 55 | replies = models.ManyToManyField('Post', symmetrical=False, null=True, |
|
55 | 56 | blank=True, related_name='tre+') |
|
56 | 57 | archived = models.BooleanField(default=False) |
|
58 | bumpable = models.BooleanField(default=True) | |
|
57 | 59 | |
|
58 | 60 | def get_tags(self): |
|
59 | 61 | """ |
|
60 | 62 | Gets a sorted tag list. |
|
61 | 63 | """ |
|
62 | 64 | |
|
63 | 65 | return self.tags.order_by('name') |
|
64 | 66 | |
|
65 | 67 | def bump(self): |
|
66 | 68 | """ |
|
67 | 69 | Bumps (moves to up) thread if possible. |
|
68 | 70 | """ |
|
69 | 71 | |
|
70 | 72 | if self.can_bump(): |
|
71 | 73 | self.bump_time = timezone.now() |
|
72 | 74 | |
|
75 | if self.get_reply_count() >= settings.MAX_POSTS_PER_THREAD: | |
|
76 | self.bumpable = False | |
|
77 | ||
|
73 | 78 | logger.info('Bumped thread %d' % self.id) |
|
74 | 79 | |
|
75 | 80 | def get_reply_count(self): |
|
76 | 81 | return self.replies.count() |
|
77 | 82 | |
|
78 | 83 | def get_images_count(self): |
|
79 | # TODO Use sum | |
|
80 | total_count = 0 | |
|
81 | for post_with_image in self.replies.annotate(images_count=Count( | |
|
82 | 'images')): | |
|
83 | total_count += post_with_image.images_count | |
|
84 | return total_count | |
|
84 | return self.replies.annotate(images_count=Count( | |
|
85 | 'images')).aggregate(Sum('images_count'))['images_count__sum'] | |
|
85 | 86 | |
|
86 | 87 | def can_bump(self): |
|
87 | 88 | """ |
|
88 | 89 | Checks if the thread can be bumped by replying to it. |
|
89 | 90 | """ |
|
90 | 91 | |
|
91 |
|
|
|
92 | return False | |
|
93 | ||
|
94 | post_count = self.get_reply_count() | |
|
95 | ||
|
96 | return post_count < settings.MAX_POSTS_PER_THREAD | |
|
92 | return self.bumpable | |
|
97 | 93 | |
|
98 | 94 | def get_last_replies(self): |
|
99 | 95 | """ |
|
100 | 96 | Gets several last replies, not including opening post |
|
101 | 97 | """ |
|
102 | 98 | |
|
103 | 99 | if settings.LAST_REPLIES_COUNT > 0: |
|
104 | 100 | reply_count = self.get_reply_count() |
|
105 | 101 | |
|
106 | 102 | if reply_count > 0: |
|
107 | 103 | reply_count_to_show = min(settings.LAST_REPLIES_COUNT, |
|
108 | 104 | reply_count - 1) |
|
109 | 105 | replies = self.get_replies() |
|
110 | 106 | last_replies = replies[reply_count - reply_count_to_show:] |
|
111 | 107 | |
|
112 | 108 | return last_replies |
|
113 | 109 | |
|
114 | 110 | def get_skipped_replies_count(self): |
|
115 | 111 | """ |
|
116 | 112 | Gets number of posts between opening post and last replies. |
|
117 | 113 | """ |
|
118 | 114 | reply_count = self.get_reply_count() |
|
119 | 115 | last_replies_count = min(settings.LAST_REPLIES_COUNT, |
|
120 | 116 | reply_count - 1) |
|
121 | 117 | return reply_count - last_replies_count - 1 |
|
122 | 118 | |
|
123 | 119 | def get_replies(self, view_fields_only=False): |
|
124 | 120 | """ |
|
125 | 121 | Gets sorted thread posts |
|
126 | 122 | """ |
|
127 | 123 | |
|
128 | 124 | query = self.replies.order_by('pub_time').prefetch_related('images') |
|
129 | 125 | if view_fields_only: |
|
130 |
query = query.defer('poster_user_agent' |
|
|
126 | query = query.defer('poster_user_agent') | |
|
131 | 127 | return query.all() |
|
132 | 128 | |
|
133 | 129 | def get_replies_with_images(self, view_fields_only=False): |
|
134 | 130 | return self.get_replies(view_fields_only).annotate(images_count=Count( |
|
135 | 131 | 'images')).filter(images_count__gt=0) |
|
136 | 132 | |
|
137 | 133 | def add_tag(self, tag): |
|
138 | 134 | """ |
|
139 | 135 | Connects thread to a tag and tag to a thread |
|
140 | 136 | """ |
|
141 | 137 | |
|
142 | 138 | self.tags.add(tag) |
|
143 | tag.threads.add(self) | |
|
144 | ||
|
145 | def remove_tag(self, tag): | |
|
146 | self.tags.remove(tag) | |
|
147 | tag.threads.remove(self) | |
|
148 | 139 | |
|
149 | 140 | def get_opening_post(self, only_id=False): |
|
150 | 141 | """ |
|
151 | 142 | Gets the first post of the thread |
|
152 | 143 | """ |
|
153 | 144 | |
|
154 | 145 | query = self.replies.order_by('pub_time') |
|
155 | 146 | if only_id: |
|
156 | 147 | query = query.only('id') |
|
157 | 148 | opening_post = query.first() |
|
158 | 149 | |
|
159 | 150 | return opening_post |
|
160 | 151 | |
|
161 | 152 | def get_opening_post_id(self): |
|
162 | 153 | """ |
|
163 | 154 | Gets ID of the first thread post. |
|
164 | 155 | """ |
|
165 | 156 | |
|
166 | 157 | cache_key = CACHE_KEY_OPENING_POST + str(self.id) |
|
167 | 158 | opening_post_id = cache.get(cache_key) |
|
168 | 159 | if not opening_post_id: |
|
169 | 160 | opening_post_id = self.get_opening_post(only_id=True).id |
|
170 | 161 | cache.set(cache_key, opening_post_id) |
|
171 | 162 | |
|
172 | 163 | return opening_post_id |
|
173 | 164 | |
|
174 | 165 | def __unicode__(self): |
|
175 | 166 | return str(self.id) |
|
176 | 167 | |
|
177 | 168 | def get_pub_time(self): |
|
178 | 169 | """ |
|
179 | 170 | Gets opening post's pub time because thread does not have its own one. |
|
180 | 171 | """ |
|
181 | 172 | |
|
182 | 173 | return self.get_opening_post().pub_time |
|
183 | 174 | |
|
184 | 175 | def delete(self, using=None): |
|
185 | 176 | if self.replies.exists(): |
|
186 | 177 | self.replies.all().delete() |
|
187 | 178 | |
|
188 | super(Thread, self).delete(using) No newline at end of file | |
|
179 | super(Thread, self).delete(using) | |
|
180 | ||
|
181 | def __str__(self): | |
|
182 | return 'T#{}/{}'.format(self.id, self.get_opening_post_id()) No newline at end of file |
@@ -1,24 +1,24 b'' | |||
|
1 | 1 | from haystack import indexes |
|
2 | 2 | from boards.models import Post, Tag |
|
3 | 3 | |
|
4 | 4 | __author__ = 'neko259' |
|
5 | 5 | |
|
6 | 6 | |
|
7 | 7 | class PostIndex(indexes.SearchIndex, indexes.Indexable): |
|
8 | 8 | text = indexes.CharField(document=True, use_template=True) |
|
9 | 9 | |
|
10 | 10 | def get_model(self): |
|
11 | 11 | return Post |
|
12 | 12 | |
|
13 | 13 | def index_queryset(self, using=None): |
|
14 | 14 | return self.get_model().objects.all() |
|
15 | 15 | |
|
16 | 16 | |
|
17 | 17 | class TagIndex(indexes.SearchIndex, indexes.Indexable): |
|
18 | 18 | text = indexes.CharField(document=True, use_template=True) |
|
19 | 19 | |
|
20 | 20 | def get_model(self): |
|
21 | 21 | return Tag |
|
22 | 22 | |
|
23 | 23 | def index_queryset(self, using=None): |
|
24 |
return self.get_model().objects. |
|
|
24 | return self.get_model().objects.all() |
@@ -1,20 +1,22 b'' | |||
|
1 |
VERSION = '2. |
|
|
2 |
SITE_NAME = ' |
|
|
1 | VERSION = '2.2.3 Miyu' | |
|
2 | SITE_NAME = 'Neboard' | |
|
3 | 3 | |
|
4 | 4 | CACHE_TIMEOUT = 600 # Timeout for caching, if cache is used |
|
5 | 5 | LOGIN_TIMEOUT = 3600 # Timeout between login tries |
|
6 | 6 | MAX_TEXT_LENGTH = 30000 # Max post length in characters |
|
7 | 7 | MAX_IMAGE_SIZE = 8 * 1024 * 1024 # Max image size |
|
8 | 8 | |
|
9 | 9 | # Thread bumplimit |
|
10 | 10 | MAX_POSTS_PER_THREAD = 10 |
|
11 | 11 | # Old posts will be archived or deleted if this value is reached |
|
12 | 12 | MAX_THREAD_COUNT = 5 |
|
13 | 13 | THREADS_PER_PAGE = 3 |
|
14 | 14 | DEFAULT_THEME = 'md' |
|
15 | 15 | LAST_REPLIES_COUNT = 3 |
|
16 | 16 | |
|
17 | 17 | # Enable archiving threads instead of deletion when the thread limit is reached |
|
18 | 18 | ARCHIVE_THREADS = True |
|
19 | 19 | # Limit posting speed |
|
20 | 20 | LIMIT_POSTING_SPEED = False |
|
21 | # Thread update | |
|
22 | WEBSOCKETS_ENABLED = True |
@@ -1,484 +1,497 b'' | |||
|
1 | * { | |
|
2 | text-decoration: none; | |
|
3 | font-weight: inherit; | |
|
4 | } | |
|
5 | ||
|
6 | b { | |
|
7 | font-weight: bold; | |
|
8 | } | |
|
9 | ||
|
1 | 10 | html { |
|
2 | 11 | background: #555; |
|
3 | 12 | color: #ffffff; |
|
4 | 13 | } |
|
5 | 14 | |
|
6 | 15 | body { |
|
7 | 16 | margin: 0; |
|
8 | 17 | } |
|
9 | 18 | |
|
10 | 19 | #admin_panel { |
|
11 | 20 | background: #FF0000; |
|
12 | 21 | color: #00FF00 |
|
13 | 22 | } |
|
14 | 23 | |
|
15 | 24 | .input_field_error { |
|
16 | 25 | color: #FF0000; |
|
17 | 26 | } |
|
18 | 27 | |
|
19 | 28 | .title { |
|
20 | 29 | font-weight: bold; |
|
21 | 30 | color: #ffcc00; |
|
22 | 31 | } |
|
23 | 32 | |
|
24 | 33 | .link, a { |
|
25 | 34 | color: #afdcec; |
|
26 | 35 | } |
|
27 | 36 | |
|
28 | 37 | .block { |
|
29 | 38 | display: inline-block; |
|
30 | 39 | vertical-align: top; |
|
31 | 40 | } |
|
32 | 41 | |
|
33 | 42 | .tag { |
|
34 | 43 | color: #FFD37D; |
|
35 | 44 | } |
|
36 | 45 | |
|
37 | 46 | .post_id { |
|
38 | 47 | color: #fff380; |
|
39 | 48 | } |
|
40 | 49 | |
|
41 | 50 | .post, .dead_post, .archive_post, #posts-table { |
|
42 | 51 | background: #333; |
|
43 | 52 | padding: 10px; |
|
44 | 53 | clear: left; |
|
45 | 54 | word-wrap: break-word; |
|
46 | 55 | border-top: 1px solid #777; |
|
47 | 56 | border-bottom: 1px solid #777; |
|
48 | 57 | } |
|
49 | 58 | |
|
50 | 59 | .post + .post { |
|
51 | 60 | border-top: none; |
|
52 | 61 | } |
|
53 | 62 | |
|
54 | 63 | .dead_post + .dead_post { |
|
55 | 64 | border-top: none; |
|
56 | 65 | } |
|
57 | 66 | |
|
58 | 67 | .archive_post + .archive_post { |
|
59 | 68 | border-top: none; |
|
60 | 69 | } |
|
61 | 70 | |
|
62 | 71 | .metadata { |
|
63 | 72 | padding-top: 5px; |
|
64 | 73 | margin-top: 10px; |
|
65 | 74 | border-top: solid 1px #666; |
|
66 | 75 | color: #ddd; |
|
67 | 76 | } |
|
68 | 77 | |
|
69 | 78 | .navigation_panel, .tag_info { |
|
70 | 79 | background: #222; |
|
71 | 80 | margin-bottom: 5px; |
|
72 | 81 | margin-top: 5px; |
|
73 | 82 | padding: 10px; |
|
74 | 83 | border-bottom: solid 1px #888; |
|
75 | 84 | border-top: solid 1px #888; |
|
76 | 85 | color: #eee; |
|
77 | 86 | } |
|
78 | 87 | |
|
79 | 88 | .navigation_panel .link { |
|
80 | 89 | border-right: 1px solid #fff; |
|
81 | 90 | font-weight: bold; |
|
82 | 91 | margin-right: 1ex; |
|
83 | 92 | padding-right: 1ex; |
|
84 | 93 | } |
|
85 | 94 | .navigation_panel .link:last-child { |
|
86 | 95 | border-left: 1px solid #fff; |
|
87 | 96 | border-right: none; |
|
88 | 97 | float: right; |
|
89 | 98 | margin-left: 1ex; |
|
90 | 99 | margin-right: 0; |
|
91 | 100 | padding-left: 1ex; |
|
92 | 101 | padding-right: 0; |
|
93 | 102 | } |
|
94 | 103 | |
|
95 | 104 | .navigation_panel::after, .post::after { |
|
96 | 105 | clear: both; |
|
97 | 106 | content: "."; |
|
98 | 107 | display: block; |
|
99 | 108 | height: 0; |
|
100 | 109 | line-height: 0; |
|
101 | 110 | visibility: hidden; |
|
102 | 111 | } |
|
103 | 112 | |
|
104 | 113 | .header { |
|
105 | 114 | border-bottom: solid 2px #ccc; |
|
106 | 115 | margin-bottom: 5px; |
|
107 | 116 | border-top: none; |
|
108 | 117 | margin-top: 0; |
|
109 | 118 | } |
|
110 | 119 | |
|
111 | 120 | .footer { |
|
112 | 121 | border-top: solid 2px #ccc; |
|
113 | 122 | margin-top: 5px; |
|
114 | 123 | border-bottom: none; |
|
115 | 124 | margin-bottom: 0; |
|
116 | 125 | } |
|
117 | 126 | |
|
118 | 127 | p, .br { |
|
119 | 128 | margin-top: .5em; |
|
120 | 129 | margin-bottom: .5em; |
|
121 | 130 | } |
|
122 | 131 | |
|
123 | 132 | .post-form-w { |
|
124 | 133 | background: #333344; |
|
125 | 134 | border-top: solid 1px #888; |
|
126 | 135 | border-bottom: solid 1px #888; |
|
127 | 136 | color: #fff; |
|
128 | 137 | padding: 10px; |
|
129 | 138 | margin-bottom: 5px; |
|
130 | 139 | margin-top: 5px; |
|
131 | 140 | } |
|
132 | 141 | |
|
133 | 142 | .form-row { |
|
134 | 143 | width: 100%; |
|
135 | 144 | } |
|
136 | 145 | |
|
137 | 146 | .form-label { |
|
138 | 147 | padding: .25em 1ex .25em 0; |
|
139 | 148 | vertical-align: top; |
|
140 | 149 | } |
|
141 | 150 | |
|
142 | 151 | .form-input { |
|
143 | 152 | padding: .25em 0; |
|
144 | 153 | } |
|
145 | 154 | |
|
146 | 155 | .form-errors { |
|
147 | 156 | font-weight: bolder; |
|
148 | 157 | vertical-align: middle; |
|
149 | 158 | } |
|
150 | 159 | |
|
151 | 160 | .post-form input:not([name="image"]), .post-form textarea { |
|
152 | 161 | background: #333; |
|
153 | 162 | color: #fff; |
|
154 | 163 | border: solid 1px; |
|
155 | 164 | padding: 0; |
|
156 | 165 | font: medium sans-serif; |
|
157 | 166 | width: 100%; |
|
158 | 167 | } |
|
159 | 168 | |
|
169 | .post-form textarea { | |
|
170 | resize: vertical; | |
|
171 | } | |
|
172 | ||
|
160 | 173 | .form-submit { |
|
161 | 174 | display: table; |
|
162 | 175 | margin-bottom: 1ex; |
|
163 | 176 | margin-top: 1ex; |
|
164 | 177 | } |
|
165 | 178 | |
|
166 | 179 | .form-title { |
|
167 | 180 | font-weight: bold; |
|
168 | 181 | font-size: 2ex; |
|
169 | 182 | margin-bottom: 0.5ex; |
|
170 | 183 | } |
|
171 | 184 | |
|
172 | 185 | .post-form input[type="submit"], input[type="submit"] { |
|
173 | 186 | background: #222; |
|
174 | 187 | border: solid 2px #fff; |
|
175 | 188 | color: #fff; |
|
176 | 189 | padding: 0.5ex; |
|
177 | 190 | } |
|
178 | 191 | |
|
179 | 192 | input[type="submit"]:hover { |
|
180 | 193 | background: #060; |
|
181 | 194 | } |
|
182 | 195 | |
|
183 | 196 | blockquote { |
|
184 | 197 | border-left: solid 2px; |
|
185 | 198 | padding-left: 5px; |
|
186 | 199 | color: #B1FB17; |
|
187 | 200 | margin: 0; |
|
188 | 201 | } |
|
189 | 202 | |
|
190 | 203 | .post > .image { |
|
191 | 204 | float: left; |
|
192 | 205 | margin: 0 1ex .5ex 0; |
|
193 | 206 | min-width: 1px; |
|
194 | 207 | text-align: center; |
|
195 | 208 | display: table-row; |
|
196 | 209 | } |
|
197 | 210 | |
|
198 | 211 | .post > .metadata { |
|
199 | 212 | clear: left; |
|
200 | 213 | } |
|
201 | 214 | |
|
202 | 215 | .get { |
|
203 | 216 | font-weight: bold; |
|
204 | 217 | color: #d55; |
|
205 | 218 | } |
|
206 | 219 | |
|
207 | 220 | * { |
|
208 | 221 | text-decoration: none; |
|
209 | 222 | } |
|
210 | 223 | |
|
211 | 224 | .dead_post { |
|
212 | 225 | background-color: #442222; |
|
213 | 226 | } |
|
214 | 227 | |
|
215 | 228 | .archive_post { |
|
216 | 229 | background-color: #000; |
|
217 | 230 | } |
|
218 | 231 | |
|
219 | 232 | .mark_btn { |
|
220 | 233 | border: 1px solid; |
|
221 | 234 | min-width: 2ex; |
|
222 | 235 | padding: 2px 2ex; |
|
223 | 236 | } |
|
224 | 237 | |
|
225 | 238 | .mark_btn:hover { |
|
226 | 239 | background: #555; |
|
227 | 240 | } |
|
228 | 241 | |
|
229 | 242 | .quote { |
|
230 | 243 | color: #92cf38; |
|
231 | 244 | font-style: italic; |
|
232 | 245 | } |
|
233 | 246 | |
|
234 | 247 | .multiquote { |
|
235 | 248 | padding: 3px; |
|
236 | 249 | display: inline-block; |
|
237 | 250 | background: #222; |
|
238 | 251 | border-style: solid; |
|
239 | 252 | border-width: 1px 1px 1px 4px; |
|
240 | 253 | font-size: 0.9em; |
|
241 | 254 | } |
|
242 | 255 | |
|
243 | 256 | .spoiler { |
|
244 | 257 | background: white; |
|
245 | 258 | color: white; |
|
246 | 259 | } |
|
247 | 260 | |
|
248 | 261 | .spoiler:hover { |
|
249 | 262 | color: black; |
|
250 | 263 | } |
|
251 | 264 | |
|
252 | 265 | .comment { |
|
253 | 266 | color: #eb2; |
|
254 | 267 | } |
|
255 | 268 | |
|
256 | 269 | a:hover { |
|
257 | 270 | text-decoration: underline; |
|
258 | 271 | } |
|
259 | 272 | |
|
260 | 273 | .last-replies { |
|
261 | 274 | margin-left: 3ex; |
|
262 | 275 | margin-right: 3ex; |
|
263 | 276 | border-left: solid 1px #777; |
|
264 | 277 | border-right: solid 1px #777; |
|
265 | 278 | } |
|
266 | 279 | |
|
267 | 280 | .last-replies > .post:first-child { |
|
268 | 281 | border-top: none; |
|
269 | 282 | } |
|
270 | 283 | |
|
271 | 284 | .thread { |
|
272 | 285 | margin-bottom: 3ex; |
|
273 | 286 | margin-top: 1ex; |
|
274 | 287 | } |
|
275 | 288 | |
|
276 | 289 | .post:target { |
|
277 | 290 | border: solid 2px white; |
|
278 | 291 | } |
|
279 | 292 | |
|
280 | 293 | pre{ |
|
281 | 294 | white-space:pre-wrap |
|
282 | 295 | } |
|
283 | 296 | |
|
284 | 297 | li { |
|
285 | 298 | list-style-position: inside; |
|
286 | 299 | } |
|
287 | 300 | |
|
288 | 301 | .fancybox-skin { |
|
289 | 302 | position: relative; |
|
290 | 303 | background-color: #fff; |
|
291 | 304 | color: #ddd; |
|
292 | 305 | text-shadow: none; |
|
293 | 306 | } |
|
294 | 307 | |
|
295 | 308 | .fancybox-image { |
|
296 | 309 | border: 1px solid black; |
|
297 | 310 | } |
|
298 | 311 | |
|
299 | 312 | .image-mode-tab { |
|
300 | 313 | background: #444; |
|
301 | 314 | color: #eee; |
|
302 | 315 | margin-top: 5px; |
|
303 | 316 | padding: 5px; |
|
304 | 317 | border-top: 1px solid #888; |
|
305 | 318 | border-bottom: 1px solid #888; |
|
306 | 319 | } |
|
307 | 320 | |
|
308 | 321 | .image-mode-tab > label { |
|
309 | 322 | margin: 0 1ex; |
|
310 | 323 | } |
|
311 | 324 | |
|
312 | 325 | .image-mode-tab > label > input { |
|
313 | 326 | margin-right: .5ex; |
|
314 | 327 | } |
|
315 | 328 | |
|
316 | 329 | #posts-table { |
|
317 | 330 | margin-top: 5px; |
|
318 | 331 | margin-bottom: 5px; |
|
319 | 332 | } |
|
320 | 333 | |
|
321 | 334 | .tag_info > h2 { |
|
322 | 335 | margin: 0; |
|
323 | 336 | } |
|
324 | 337 | |
|
325 | 338 | .post-info { |
|
326 | 339 | color: #ddd; |
|
327 | 340 | margin-bottom: 1ex; |
|
328 | 341 | } |
|
329 | 342 | |
|
330 | 343 | .moderator_info { |
|
331 | 344 | color: #e99d41; |
|
332 | 345 | float: right; |
|
333 | 346 | font-weight: bold; |
|
334 | 347 | } |
|
335 | 348 | |
|
336 | 349 | .refmap { |
|
337 | 350 | font-size: 0.9em; |
|
338 | 351 | color: #ccc; |
|
339 | 352 | margin-top: 1em; |
|
340 | 353 | } |
|
341 | 354 | |
|
342 | 355 | .fav { |
|
343 | 356 | color: yellow; |
|
344 | 357 | } |
|
345 | 358 | |
|
346 | 359 | .not_fav { |
|
347 | 360 | color: #ccc; |
|
348 | 361 | } |
|
349 | 362 | |
|
350 | 363 | .role { |
|
351 | 364 | text-decoration: underline; |
|
352 | 365 | } |
|
353 | 366 | |
|
354 | 367 | .form-email { |
|
355 | 368 | display: none; |
|
356 | 369 | } |
|
357 | 370 | |
|
358 | 371 | .bar-value { |
|
359 | 372 | background: rgba(50, 55, 164, 0.45); |
|
360 | 373 | font-size: 0.9em; |
|
361 | 374 | height: 1.5em; |
|
362 | 375 | } |
|
363 | 376 | |
|
364 | 377 | .bar-bg { |
|
365 | 378 | position: relative; |
|
366 | 379 | border-top: solid 1px #888; |
|
367 | 380 | border-bottom: solid 1px #888; |
|
368 | 381 | margin-top: 5px; |
|
369 | 382 | overflow: hidden; |
|
370 | 383 | } |
|
371 | 384 | |
|
372 | 385 | .bar-text { |
|
373 | 386 | padding: 2px; |
|
374 | 387 | position: absolute; |
|
375 | 388 | left: 0; |
|
376 | 389 | top: 0; |
|
377 | 390 | } |
|
378 | 391 | |
|
379 | 392 | .page_link { |
|
380 | 393 | background: #444; |
|
381 | 394 | border-top: solid 1px #888; |
|
382 | 395 | border-bottom: solid 1px #888; |
|
383 | 396 | padding: 5px; |
|
384 | 397 | color: #eee; |
|
385 | 398 | font-size: 2ex; |
|
386 | 399 | } |
|
387 | 400 | |
|
388 | 401 | .skipped_replies { |
|
389 | 402 | padding: 5px; |
|
390 | 403 | margin-left: 3ex; |
|
391 | 404 | margin-right: 3ex; |
|
392 | 405 | border-left: solid 1px #888; |
|
393 | 406 | border-right: solid 1px #888; |
|
394 | 407 | border-bottom: solid 1px #888; |
|
395 | 408 | background: #000; |
|
396 | 409 | } |
|
397 | 410 | |
|
398 | 411 | .current_page { |
|
399 | 412 | padding: 2px; |
|
400 | 413 | background-color: #afdcec; |
|
401 | 414 | color: #000; |
|
402 | 415 | } |
|
403 | 416 | |
|
404 | 417 | .current_mode { |
|
405 | 418 | font-weight: bold; |
|
406 | 419 | } |
|
407 | 420 | |
|
408 | 421 | .gallery_image { |
|
409 | 422 | border: solid 1px; |
|
410 | 423 | padding: 0.5ex; |
|
411 | 424 | margin: 0.5ex; |
|
412 | 425 | text-align: center; |
|
413 | 426 | } |
|
414 | 427 | |
|
415 | 428 | code { |
|
416 | 429 | border: dashed 1px #ccc; |
|
417 | 430 | background: #111; |
|
418 | 431 | padding: 2px; |
|
419 | 432 | font-size: 1.2em; |
|
420 | 433 | display: inline-block; |
|
421 | 434 | } |
|
422 | 435 | |
|
423 | 436 | pre { |
|
424 | 437 | overflow: auto; |
|
425 | 438 | } |
|
426 | 439 | |
|
427 | 440 | .img-full { |
|
428 | 441 | background: #222; |
|
429 | 442 | border: solid 1px white; |
|
430 | 443 | } |
|
431 | 444 | |
|
432 | 445 | .tag_item { |
|
433 | 446 | display: inline-block; |
|
434 | 447 | border: 1px dashed #666; |
|
435 | 448 | margin: 0.2ex; |
|
436 | 449 | padding: 0.1ex; |
|
437 | 450 | } |
|
438 | 451 | |
|
439 | 452 | #id_models li { |
|
440 | 453 | list-style: none; |
|
441 | 454 | } |
|
442 | 455 | |
|
443 | 456 | #id_q { |
|
444 | 457 | margin-left: 1ex; |
|
445 | 458 | } |
|
446 | 459 | |
|
447 | 460 | ul { |
|
448 | 461 | padding-left: 0px; |
|
449 | 462 | } |
|
450 | 463 | |
|
451 | 464 | .quote-header { |
|
452 | 465 | border-bottom: 2px solid #ddd; |
|
453 | 466 | margin-bottom: 1ex; |
|
454 | 467 | padding-bottom: .5ex; |
|
455 | 468 | color: #ddd; |
|
456 | 469 | font-size: 1.2em; |
|
457 | 470 | } |
|
458 | 471 | |
|
459 | 472 | .global-id { |
|
460 | 473 | font-weight: bolder; |
|
461 | 474 | opacity: .5; |
|
462 | 475 | } |
|
463 | 476 | |
|
464 | 477 | /* Post */ |
|
465 | 478 | .post > .message, .post > .image { |
|
466 | 479 | padding-left: 1em; |
|
467 | 480 | } |
|
468 | 481 | |
|
469 | 482 | /* Reflink preview */ |
|
470 | 483 | .post_preview { |
|
471 | 484 | border-left: 1px solid #777; |
|
472 | 485 | border-right: 1px solid #777; |
|
473 | 486 | } |
|
474 | 487 | |
|
475 | 488 | /* Code highlighter */ |
|
476 | 489 | .hljs { |
|
477 | 490 | color: #fff; |
|
478 | 491 | background: #000; |
|
479 | 492 | display: inline-block; |
|
480 | 493 | } |
|
481 | 494 | |
|
482 | 495 | .hljs, .hljs-subst, .hljs-tag .hljs-title, .lisp .hljs-title, .clojure .hljs-built_in, .nginx .hljs-title { |
|
483 | 496 | color: #fff; |
|
484 | 497 | } |
@@ -1,288 +1,334 b'' | |||
|
1 | 1 | /* |
|
2 | 2 | @licstart The following is the entire license notice for the |
|
3 | 3 | JavaScript code in this page. |
|
4 | 4 | |
|
5 | 5 | |
|
6 | Copyright (C) 2013 neko259 | |
|
6 | Copyright (C) 2013-2014 neko259 | |
|
7 | 7 | |
|
8 | 8 | The JavaScript code in this page is free software: you can |
|
9 | 9 | redistribute it and/or modify it under the terms of the GNU |
|
10 | 10 | General Public License (GNU GPL) as published by the Free Software |
|
11 | 11 | Foundation, either version 3 of the License, or (at your option) |
|
12 | 12 | any later version. The code is distributed WITHOUT ANY WARRANTY; |
|
13 | 13 | without even the implied warranty of MERCHANTABILITY or FITNESS |
|
14 | 14 | FOR A PARTICULAR PURPOSE. See the GNU GPL for more details. |
|
15 | 15 | |
|
16 | 16 | As additional permission under GNU GPL version 3 section 7, you |
|
17 | 17 | may distribute non-source (e.g., minimized or compacted) forms of |
|
18 | 18 | that code without the copy of the GNU GPL normally required by |
|
19 | 19 | section 4, provided you include this license notice and a URL |
|
20 | 20 | through which recipients can access the Corresponding Source. |
|
21 | 21 | |
|
22 | 22 | @licend The above is the entire license notice |
|
23 | 23 | for the JavaScript code in this page. |
|
24 | 24 | */ |
|
25 | 25 | |
|
26 | var THREAD_UPDATE_DELAY = 10000; | |
|
26 | var wsUser = ''; | |
|
27 | 27 | |
|
28 | 28 | var loading = false; |
|
29 | var lastUpdateTime = null; | |
|
30 | 29 | var unreadPosts = 0; |
|
30 | var documentOriginalTitle = ''; | |
|
31 | 31 | |
|
32 | // Thread ID does not change, can be stored one time | |
|
33 | var threadId = $('div.thread').children('.post').first().attr('id'); | |
|
34 | ||
|
35 | /** | |
|
36 | * Connect to websocket server and subscribe to thread updates. On any update we | |
|
37 | * request a thread diff. | |
|
38 | * | |
|
39 | * @returns {boolean} true if connected, false otherwise | |
|
40 | */ | |
|
41 | function connectWebsocket() { | |
|
42 | var metapanel = $('.metapanel')[0]; | |
|
43 | ||
|
44 | var wsHost = metapanel.getAttribute('data-ws-host'); | |
|
45 | var wsPort = metapanel.getAttribute('data-ws-port'); | |
|
46 | ||
|
47 | if (wsHost.length > 0 && wsPort.length > 0) | |
|
48 | var centrifuge = new Centrifuge({ | |
|
49 | "url": 'ws://' + wsHost + ':' + wsPort + "/connection/websocket", | |
|
50 | "project": metapanel.getAttribute('data-ws-project'), | |
|
51 | "user": wsUser, | |
|
52 | "timestamp": metapanel.getAttribute('data-last-update'), | |
|
53 | "token": metapanel.getAttribute('data-ws-token'), | |
|
54 | "debug": false | |
|
55 | }); | |
|
56 | ||
|
57 | centrifuge.on('error', function(error_message) { | |
|
58 | console.log("Error connecting to websocket server."); | |
|
59 | return false; | |
|
60 | }); | |
|
61 | ||
|
62 | centrifuge.on('connect', function() { | |
|
63 | var channelName = 'thread:' + threadId; | |
|
64 | centrifuge.subscribe(channelName, function(message) { | |
|
65 | getThreadDiff(); | |
|
66 | }); | |
|
67 | ||
|
68 | // For the case we closed the browser and missed some updates | |
|
69 | getThreadDiff(); | |
|
70 | $('#autoupdate').text('[+]'); | |
|
71 | }); | |
|
72 | ||
|
73 | centrifuge.connect(); | |
|
74 | ||
|
75 | return true; | |
|
76 | } | |
|
77 | ||
|
78 | /** | |
|
79 | * Get diff of the posts from the current thread timestamp. | |
|
80 | * This is required if the browser was closed and some post updates were | |
|
81 | * missed. | |
|
82 | */ | |
|
83 | function getThreadDiff() { | |
|
84 | var lastUpdateTime = $('.metapanel').attr('data-last-update'); | |
|
85 | ||
|
86 | var diffUrl = '/api/diff_thread/' + threadId + '/' + lastUpdateTime + '/'; | |
|
87 | ||
|
88 | $.getJSON(diffUrl) | |
|
89 | .success(function(data) { | |
|
90 | var addedPosts = data.added; | |
|
91 | ||
|
92 | for (var i = 0; i < addedPosts.length; i++) { | |
|
93 | var postText = addedPosts[i]; | |
|
94 | var post = $(postText); | |
|
95 | ||
|
96 | updatePost(post) | |
|
97 | ||
|
98 | lastPost = post; | |
|
99 | } | |
|
100 | ||
|
101 | var updatedPosts = data.updated; | |
|
102 | ||
|
103 | for (var i = 0; i < updatedPosts.length; i++) { | |
|
104 | var postText = updatedPosts[i]; | |
|
105 | var post = $(postText); | |
|
106 | ||
|
107 | updatePost(post) | |
|
108 | } | |
|
109 | ||
|
110 | // TODO Process removed posts if any | |
|
111 | $('.metapanel').attr('data-last-update', data.last_update); | |
|
112 | }) | |
|
113 | } | |
|
114 | ||
|
115 | /** | |
|
116 | * Add or update the post on html page. | |
|
117 | */ | |
|
118 | function updatePost(postHtml) { | |
|
119 | // This needs to be set on start because the page is scrolled after posts | |
|
120 | // are added or updated | |
|
121 | var bottom = isPageBottom(); | |
|
122 | ||
|
123 | var post = $(postHtml); | |
|
124 | ||
|
125 | var threadBlock = $('div.thread'); | |
|
126 | ||
|
127 | var lastUpdate = ''; | |
|
128 | ||
|
129 | var postId = post.attr('id'); | |
|
130 | ||
|
131 | // If the post already exists, replace it. Otherwise add as a new one. | |
|
132 | var existingPosts = threadBlock.children('.post[id=' + postId + ']'); | |
|
133 | ||
|
134 | if (existingPosts.size() > 0) { | |
|
135 | existingPosts.replaceWith(post); | |
|
136 | } else { | |
|
137 | var threadPosts = threadBlock.children('.post'); | |
|
138 | var lastPost = threadPosts.last(); | |
|
139 | ||
|
140 | post.appendTo(lastPost.parent()); | |
|
141 | ||
|
142 | updateBumplimitProgress(1); | |
|
143 | showNewPostsTitle(1); | |
|
144 | ||
|
145 | lastUpdate = post.children('.post-info').first() | |
|
146 | .children('.pub_time').first().text(); | |
|
147 | ||
|
148 | if (bottom) { | |
|
149 | scrollToBottom(); | |
|
150 | } | |
|
151 | } | |
|
152 | ||
|
153 | processNewPost(post); | |
|
154 | updateMetadataPanel(lastUpdate) | |
|
155 | } | |
|
156 | ||
|
157 | /** | |
|
158 | * Initiate a blinking animation on a node to show it was updated. | |
|
159 | */ | |
|
32 | 160 | function blink(node) { |
|
33 | 161 | var blinkCount = 2; |
|
34 | 162 | |
|
35 | 163 | var nodeToAnimate = node; |
|
36 | 164 | for (var i = 0; i < blinkCount; i++) { |
|
37 | 165 | nodeToAnimate = nodeToAnimate.fadeTo('fast', 0.5).fadeTo('fast', 1.0); |
|
38 | 166 | } |
|
39 | 167 | } |
|
40 | 168 | |
|
41 | function updateThread() { | |
|
42 | if (loading) { | |
|
43 | return; | |
|
44 | } | |
|
45 | ||
|
46 | loading = true; | |
|
47 | ||
|
48 | var threadPosts = $('div.thread').children('.post'); | |
|
49 | ||
|
50 | var lastPost = threadPosts.last(); | |
|
51 | var threadId = threadPosts.first().attr('id'); | |
|
52 | ||
|
53 | var diffUrl = '/api/diff_thread/' + threadId + '/' + lastUpdateTime + '/'; | |
|
54 | $.getJSON(diffUrl) | |
|
55 | .success(function(data) { | |
|
56 | var bottom = isPageBottom(); | |
|
57 | ||
|
58 | var lastUpdate = ''; | |
|
59 | ||
|
60 | var addedPosts = data.added; | |
|
61 | for (var i = 0; i < addedPosts.length; i++) { | |
|
62 | var postText = addedPosts[i]; | |
|
63 | ||
|
64 | var post = $(postText); | |
|
65 | ||
|
66 | if (lastUpdate === '') { | |
|
67 | lastUpdate = post.find('.pub_time').text(); | |
|
68 | } | |
|
69 | ||
|
70 | post.appendTo(lastPost.parent()); | |
|
71 | processNewPost(post); | |
|
72 | ||
|
73 | lastPost = post; | |
|
74 | blink(post); | |
|
75 | } | |
|
76 | ||
|
77 | var updatedPosts = data.updated; | |
|
78 | for (var i = 0; i < updatedPosts.length; i++) { | |
|
79 | var postText = updatedPosts[i]; | |
|
80 | ||
|
81 | var post = $(postText); | |
|
82 | ||
|
83 | if (lastUpdate === '') { | |
|
84 | lastUpdate = post.find('.pub_time').text(); | |
|
85 | } | |
|
86 | ||
|
87 | var postId = post.attr('id'); | |
|
88 | ||
|
89 | var oldPost = $('div.thread').children('.post[id=' + postId + ']'); | |
|
90 | ||
|
91 | oldPost.replaceWith(post); | |
|
92 | processNewPost(post); | |
|
93 | ||
|
94 | blink(post); | |
|
95 | } | |
|
96 | ||
|
97 | // TODO Process deleted posts | |
|
98 | ||
|
99 | lastUpdateTime = data.last_update; | |
|
100 | loading = false; | |
|
101 | ||
|
102 | if (bottom) { | |
|
103 | scrollToBottom(); | |
|
104 | } | |
|
105 | ||
|
106 | var hasPostChanges = (updatedPosts.length > 0) | |
|
107 | || (addedPosts.length > 0); | |
|
108 | if (hasPostChanges) { | |
|
109 | updateMetadataPanel(lastUpdate); | |
|
110 | } | |
|
111 | ||
|
112 | updateBumplimitProgress(data.added.length); | |
|
113 | ||
|
114 | if (data.added.length + data.updated.length > 0) { | |
|
115 | showNewPostsTitle(data.added.length); | |
|
116 | } | |
|
117 | }) | |
|
118 | .error(function(data) { | |
|
119 | // TODO Show error message that server is unavailable? | |
|
120 | ||
|
121 | loading = false; | |
|
122 | }); | |
|
123 | } | |
|
124 | ||
|
125 | 169 | function isPageBottom() { |
|
126 | 170 | var scroll = $(window).scrollTop() / ($(document).height() |
|
127 | - $(window).height()) | |
|
171 | - $(window).height()); | |
|
128 | 172 | |
|
129 | 173 | return scroll == 1 |
|
130 | 174 | } |
|
131 | 175 | |
|
132 | 176 | function initAutoupdate() { |
|
133 | loading = false; | |
|
134 | ||
|
135 | lastUpdateTime = $('.metapanel').attr('data-last-update'); | |
|
136 | ||
|
137 | setInterval(updateThread, THREAD_UPDATE_DELAY); | |
|
177 | return connectWebsocket(); | |
|
138 | 178 | } |
|
139 | 179 | |
|
140 | 180 | function getReplyCount() { |
|
141 | 181 | return $('.thread').children('.post').length |
|
142 | 182 | } |
|
143 | 183 | |
|
144 | 184 | function getImageCount() { |
|
145 | 185 | return $('.thread').find('img').length |
|
146 | 186 | } |
|
147 | 187 | |
|
188 | /** | |
|
189 | * Update post count, images count and last update time in the metadata | |
|
190 | * panel. | |
|
191 | */ | |
|
148 | 192 | function updateMetadataPanel(lastUpdate) { |
|
149 | 193 | var replyCountField = $('#reply-count'); |
|
150 | 194 | var imageCountField = $('#image-count'); |
|
151 | 195 | |
|
152 | 196 | replyCountField.text(getReplyCount()); |
|
153 | 197 | imageCountField.text(getImageCount()); |
|
154 | 198 | |
|
155 | 199 | if (lastUpdate !== '') { |
|
156 | 200 | var lastUpdateField = $('#last-update'); |
|
157 | 201 | lastUpdateField.text(lastUpdate); |
|
158 | 202 | blink(lastUpdateField); |
|
159 | 203 | } |
|
160 | 204 | |
|
161 | 205 | blink(replyCountField); |
|
162 | 206 | blink(imageCountField); |
|
163 | 207 | } |
|
164 | 208 | |
|
165 | 209 | /** |
|
166 | 210 | * Update bumplimit progress bar |
|
167 | 211 | */ |
|
168 | 212 | function updateBumplimitProgress(postDelta) { |
|
169 | 213 | var progressBar = $('#bumplimit_progress'); |
|
170 | 214 | if (progressBar) { |
|
171 | 215 | var postsToLimitElement = $('#left_to_limit'); |
|
172 | 216 | |
|
173 | 217 | var oldPostsToLimit = parseInt(postsToLimitElement.text()); |
|
174 | 218 | var postCount = getReplyCount(); |
|
175 | 219 | var bumplimit = postCount - postDelta + oldPostsToLimit; |
|
176 | 220 | |
|
177 | 221 | var newPostsToLimit = bumplimit - postCount; |
|
178 | 222 | if (newPostsToLimit <= 0) { |
|
179 | 223 | $('.bar-bg').remove(); |
|
180 | 224 | $('.thread').children('.post').addClass('dead_post'); |
|
181 | 225 | } else { |
|
182 | 226 | postsToLimitElement.text(newPostsToLimit); |
|
183 | 227 | progressBar.width((100 - postCount / bumplimit * 100.0) + '%'); |
|
184 | 228 | } |
|
185 | 229 | } |
|
186 | 230 | } |
|
187 | 231 | |
|
188 | var documentOriginalTitle = ''; | |
|
189 | 232 | /** |
|
190 | 233 | * Show 'new posts' text in the title if the document is not visible to a user |
|
191 | 234 | */ |
|
192 | 235 | function showNewPostsTitle(newPostCount) { |
|
193 | 236 | if (document.hidden) { |
|
194 | 237 | if (documentOriginalTitle === '') { |
|
195 | 238 | documentOriginalTitle = document.title; |
|
196 | 239 | } |
|
197 | 240 | unreadPosts = unreadPosts + newPostCount; |
|
198 | 241 | document.title = '[' + unreadPosts + '] ' + documentOriginalTitle; |
|
199 | 242 | |
|
200 | 243 | document.addEventListener('visibilitychange', function() { |
|
201 | 244 | if (documentOriginalTitle !== '') { |
|
202 | 245 | document.title = documentOriginalTitle; |
|
203 | 246 | documentOriginalTitle = ''; |
|
204 | 247 | unreadPosts = 0; |
|
205 | 248 | } |
|
206 | 249 | |
|
207 | 250 | document.removeEventListener('visibilitychange', null); |
|
208 | 251 | }); |
|
209 | 252 | } |
|
210 | 253 | } |
|
211 | 254 | |
|
212 | 255 | /** |
|
213 | 256 | * Clear all entered values in the form fields |
|
214 | 257 | */ |
|
215 | 258 | function resetForm(form) { |
|
216 | 259 | form.find('input:text, input:password, input:file, select, textarea').val(''); |
|
217 | 260 | form.find('input:radio, input:checkbox') |
|
218 | 261 | .removeAttr('checked').removeAttr('selected'); |
|
219 | 262 | $('.file_wrap').find('.file-thumb').remove(); |
|
220 | 263 | } |
|
221 | 264 | |
|
222 | 265 | /** |
|
223 | 266 | * When the form is posted, this method will be run as a callback |
|
224 | 267 | */ |
|
225 | 268 | function updateOnPost(response, statusText, xhr, form) { |
|
226 | 269 | var json = $.parseJSON(response); |
|
227 | 270 | var status = json.status; |
|
228 | 271 | |
|
229 | 272 | showAsErrors(form, ''); |
|
230 | 273 | |
|
231 | 274 | if (status === 'ok') { |
|
232 | 275 | resetForm(form); |
|
233 |
|
|
|
276 | getThreadDiff(); | |
|
234 | 277 | } else { |
|
235 | 278 | var errors = json.errors; |
|
236 | 279 | for (var i = 0; i < errors.length; i++) { |
|
237 | 280 | var fieldErrors = errors[i]; |
|
238 | 281 | |
|
239 | 282 | var error = fieldErrors.errors; |
|
240 | 283 | |
|
241 | 284 | showAsErrors(form, error); |
|
242 | 285 | } |
|
243 | 286 | } |
|
287 | ||
|
288 | scrollToBottom(); | |
|
244 | 289 | } |
|
245 | 290 | |
|
246 | 291 | /** |
|
247 | 292 | * Show text in the errors row of the form. |
|
248 | 293 | * @param form |
|
249 | 294 | * @param text |
|
250 | 295 | */ |
|
251 | 296 | function showAsErrors(form, text) { |
|
252 | 297 | form.children('.form-errors').remove(); |
|
253 | 298 | |
|
254 | 299 | if (text.length > 0) { |
|
255 | 300 | var errorList = $('<div class="form-errors">' + text |
|
256 | 301 | + '<div>'); |
|
257 | 302 | errorList.appendTo(form); |
|
258 | 303 | } |
|
259 | 304 | } |
|
260 | 305 | |
|
261 | 306 | /** |
|
262 | 307 | * Run js methods that are usually run on the document, on the new post |
|
263 | 308 | */ |
|
264 | 309 | function processNewPost(post) { |
|
265 | 310 | addRefLinkPreview(post[0]); |
|
266 | 311 | highlightCode(post); |
|
312 | blink(post); | |
|
267 | 313 | } |
|
268 | 314 | |
|
269 | 315 | $(document).ready(function(){ |
|
270 |
initAutoupdate() |
|
|
316 | if (initAutoupdate()) { | |
|
317 | // Post form data over AJAX | |
|
318 | var threadId = $('div.thread').children('.post').first().attr('id'); | |
|
271 | 319 | |
|
272 | // Post form data over AJAX | |
|
273 | var threadId = $('div.thread').children('.post').first().attr('id'); | |
|
274 | ||
|
275 | var form = $('#form'); | |
|
320 | var form = $('#form'); | |
|
276 | 321 | |
|
277 | var options = { | |
|
278 | beforeSubmit: function(arr, $form, options) { | |
|
279 | showAsErrors($('form'), gettext('Sending message...')); | |
|
280 | }, | |
|
281 | success: updateOnPost, | |
|
282 | url: '/api/add_post/' + threadId + '/' | |
|
283 | }; | |
|
322 | var options = { | |
|
323 | beforeSubmit: function(arr, $form, options) { | |
|
324 | showAsErrors($('form'), gettext('Sending message...')); | |
|
325 | }, | |
|
326 | success: updateOnPost, | |
|
327 | url: '/api/add_post/' + threadId + '/' | |
|
328 | }; | |
|
284 | 329 | |
|
285 | form.ajaxForm(options); | |
|
330 | form.ajaxForm(options); | |
|
286 | 331 | |
|
287 | resetForm(form); | |
|
332 | resetForm(form); | |
|
333 | } | |
|
288 | 334 | }); |
@@ -1,58 +1,60 b'' | |||
|
1 | 1 | {% load staticfiles %} |
|
2 | 2 | {% load i18n %} |
|
3 | 3 | {% load l10n %} |
|
4 | 4 | {% load static from staticfiles %} |
|
5 | 5 | |
|
6 | 6 | <!DOCTYPE html> |
|
7 | 7 | <html> |
|
8 | 8 | <head> |
|
9 | 9 | <link rel="stylesheet" type="text/css" href="{% static 'css/base.css' %}" media="all"/> |
|
10 | 10 | <link rel="stylesheet" type="text/css" href="{% static 'css/3party/highlight.css' %}" media="all"/> |
|
11 | 11 | <link rel="stylesheet" type="text/css" href="{% static theme_css %}" media="all"/> |
|
12 | ||
|
12 | 13 | <link rel="alternate" type="application/rss+xml" href="rss/" title="{% trans 'Feed' %}"/> |
|
13 | 14 | |
|
14 | 15 | <link rel="icon" type="image/png" |
|
15 | 16 | href="{% static 'favicon.png' %}"> |
|
16 | 17 | |
|
17 | 18 | <meta name="viewport" content="width=device-width, initial-scale=1"/> |
|
18 | 19 | <meta charset="utf-8"/> |
|
19 | 20 | |
|
20 | 21 | {% block head %}{% endblock %} |
|
21 | 22 | </head> |
|
22 | 23 | <body> |
|
23 | 24 | <script src="{% static 'js/jquery-2.0.1.min.js' %}"></script> |
|
24 | 25 | <script src="{% static 'js/jquery-ui-1.10.3.custom.min.js' %}"></script> |
|
25 | 26 | <script src="{% static 'js/jquery.mousewheel.js' %}"></script> |
|
26 | 27 | <script src="{% url 'js_info_dict' %}"></script> |
|
27 | 28 | |
|
28 | 29 | <div class="navigation_panel header"> |
|
29 | 30 | <a class="link" href="{% url 'index' %}">{% trans "All threads" %}</a> |
|
30 | 31 | {% for tag in tags %} |
|
31 | <a class="tag" href="{% url 'tag' tag_name=tag.name %}" | |
|
32 | >#{{ tag.name }}</a>, | |
|
32 | {% autoescape off %} | |
|
33 | {{ tag.get_view }}{% if not forloop.last %},{% endif %} | |
|
34 | {% endautoescape %} | |
|
33 | 35 | {% endfor %} |
|
34 | 36 | <a href="{% url 'tags' %}" title="{% trans 'Tag management' %}" |
|
35 | 37 | >[...]</a>, |
|
36 | 38 | <a href="{% url 'search' %}" title="{% trans 'Search' %}">[S]</a> |
|
37 | 39 | <a class="link" href="{% url 'settings' %}">{% trans 'Settings' %}</a> |
|
38 | 40 | </div> |
|
39 | 41 | |
|
40 | 42 | {% block content %}{% endblock %} |
|
41 | 43 | |
|
44 | <script src="{% static 'js/3party/highlight.min.js' %}"></script> | |
|
42 | 45 | <script src="{% static 'js/popup.js' %}"></script> |
|
43 | 46 | <script src="{% static 'js/image.js' %}"></script> |
|
44 | <script src="{% static 'js/3party/highlight.min.js' %}"></script> | |
|
45 | 47 | <script src="{% static 'js/refpopup.js' %}"></script> |
|
46 | 48 | <script src="{% static 'js/main.js' %}"></script> |
|
47 | 49 | |
|
48 | 50 | <div class="navigation_panel footer"> |
|
49 | 51 | {% block metapanel %}{% endblock %} |
|
50 | 52 | [<a href="{% url 'admin:index' %}">{% trans 'Admin' %}</a>] |
|
51 | 53 | {% with ppd=posts_per_day|floatformat:2 %} |
|
52 | 54 | {% blocktrans %}Speed: {{ ppd }} posts per day{% endblocktrans %} |
|
53 | 55 | {% endwith %} |
|
54 | 56 | <a class="link" href="#top">{% trans 'Up' %}</a> |
|
55 | 57 | </div> |
|
56 | 58 | |
|
57 | 59 | </body> |
|
58 | 60 | </html> |
@@ -1,104 +1,102 b'' | |||
|
1 | 1 | {% load i18n %} |
|
2 | 2 | {% load board %} |
|
3 | 3 | {% load cache %} |
|
4 | 4 | |
|
5 | 5 | {% get_current_language as LANGUAGE_CODE %} |
|
6 | 6 | |
|
7 | {% spaceless %} | |
|
8 | {% cache 600 post post.id post.last_edit_time thread.archived bumpable truncated moderator LANGUAGE_CODE need_open_link %} | |
|
9 | {% if thread.archived %} | |
|
10 |
|
|
|
11 | {% elif bumpable %} | |
|
12 |
|
|
|
13 | {% else %} | |
|
14 | <div class="post dead_post" id="{{ post.id }}"> | |
|
15 | {% endif %} | |
|
7 | {% if thread.archived %} | |
|
8 | <div class="post archive_post" id="{{ post.id }}"> | |
|
9 | {% elif bumpable %} | |
|
10 | <div class="post" id="{{ post.id }}"> | |
|
11 | {% else %} | |
|
12 | <div class="post dead_post" id="{{ post.id }}"> | |
|
13 | {% endif %} | |
|
16 | 14 | |
|
17 |
|
|
|
18 |
|
|
|
19 |
|
|
|
20 |
|
|
|
21 |
|
|
|
22 | {% endif %} | |
|
23 | >({{ post.id }}) </a> | |
|
24 | <span class="title">{{ post.title }} </span> | |
|
25 | <span class="pub_time">{{ post.pub_time }}</span> | |
|
26 | {% if thread.archived %} | |
|
27 | — {{ thread.bump_time }} | |
|
15 | <div class="post-info"> | |
|
16 | <a class="post_id" href="{% post_object_url post thread=thread %}" | |
|
17 | {% if not truncated and not thread.archived %} | |
|
18 | onclick="javascript:addQuickReply('{{ post.id }}'); return false;" | |
|
19 | title="{% trans 'Quote' %}" {% endif %}>({{ post.id }})</a> | |
|
20 | <span class="title">{{ post.title }}</span> | |
|
21 | <span class="pub_time">{{ post.pub_time }}</span> | |
|
22 | {% comment %} | |
|
23 | Thread death time needs to be shown only if the thread is alredy archived | |
|
24 | and this is an opening post (thread death time) or a post for popup | |
|
25 | (we don't see OP here so we show the death time in the post itself). | |
|
26 | {% endcomment %} | |
|
27 | {% if thread.archived %} | |
|
28 | {% if is_opening %} | |
|
29 | — {{ thread.bump_time }} | |
|
30 | {% endif %} | |
|
31 | {% endif %} | |
|
32 | {% if is_opening and need_open_link %} | |
|
33 | {% if thread.archived %} | |
|
34 | [<a class="link" href="{% url 'thread' post.id %}">{% trans "Open" %}</a>] | |
|
35 | {% else %} | |
|
36 | [<a class="link" href="{% url 'thread' post.id %}#form">{% trans "Reply" %}</a>] | |
|
37 | {% endif %} | |
|
38 | {% endif %} | |
|
39 | ||
|
40 | {% if post.global_id %} | |
|
41 | <a class="global-id" href=" | |
|
42 | {% url 'post_sync_data' post.id %}"> [RAW] </a> | |
|
43 | {% endif %} | |
|
44 | ||
|
45 | {% if moderator %} | |
|
46 | <span class="moderator_info"> | |
|
47 | [<a href="{% url 'admin:boards_post_change' post.id %}">{% trans 'Edit' %}</a>] | |
|
48 | {% if is_opening %} | |
|
49 | [<a href="{% url 'admin:boards_thread_change' thread.id %}">{% trans 'Edit thread' %}</a>] | |
|
28 | 50 | {% endif %} |
|
29 | {% if is_opening and need_open_link %} | |
|
30 | {% if thread.archived %} | |
|
31 | [<a class="link" href="{% url 'thread' post.id %}">{% trans "Open" %}</a>] | |
|
32 | {% else %} | |
|
33 | [<a class="link" href="{% url 'thread' post.id %}#form">{% trans "Reply" %}</a>] | |
|
34 | {% endif %} | |
|
35 | {% endif %} | |
|
36 | ||
|
37 | {% if post.global_id %} | |
|
38 | <a class="global-id" href=" | |
|
39 | {% url 'post_sync_data' post.id %}"> [RAW] </a> | |
|
40 |
|
|
|
41 | ||
|
42 | {% if moderator %} | |
|
43 | <span class="moderator_info"> | |
|
44 | [<a href="{% url 'post_admin' post_id=post.id %}" | |
|
45 | >{% trans 'Edit' %}</a>] | |
|
46 | [<a href="{% url 'delete' post_id=post.id %}" | |
|
47 | >{% trans 'Delete' %}</a>] | |
|
48 | ({{ post.poster_ip }}) | |
|
49 | [<a href="{% url 'ban' post_id=post.id %}?next={{ request.path }}" | |
|
50 | >{% trans 'Ban IP' %}</a>] | |
|
51 | </span> | |
|
52 |
|
|
|
51 | </span> | |
|
52 | {% endif %} | |
|
53 | </div> | |
|
54 | {% comment %} | |
|
55 | Post images. Currently only 1 image can be posted and shown, but post model | |
|
56 | supports multiple. | |
|
57 | {% endcomment %} | |
|
58 | {% if post.images.exists %} | |
|
59 | {% with post.images.all.0 as image %} | |
|
60 | {% autoescape off %} | |
|
61 | {{ image.get_view }} | |
|
62 | {% endautoescape %} | |
|
63 | {% endwith %} | |
|
64 | {% endif %} | |
|
65 | {% comment %} | |
|
66 | Post message (text) | |
|
67 | {% endcomment %} | |
|
68 | <div class="message"> | |
|
69 | {% autoescape off %} | |
|
70 | {% if truncated %} | |
|
71 | {{ post.get_text|truncatewords_html:50 }} | |
|
72 | {% else %} | |
|
73 | {{ post.get_text }} | |
|
74 | {% endif %} | |
|
75 | {% endautoescape %} | |
|
76 | {% if post.is_referenced %} | |
|
77 | <div class="refmap"> | |
|
78 | {% autoescape off %} | |
|
79 | {% trans "Replies" %}: {{ post.refmap }} | |
|
80 | {% endautoescape %} | |
|
53 | 81 | </div> |
|
54 | {% if post.images.exists %} | |
|
55 | {% with post.images.all.0 as image %} | |
|
56 | <div class="image"> | |
|
57 | <a | |
|
58 | class="thumb" | |
|
59 | href="{{ image.image.url }}"><img | |
|
60 | src="{{ image.image.url_200x150 }}" | |
|
61 | alt="{{ post.id }}" | |
|
62 | width="{{ image.pre_width }}" | |
|
63 | height="{{ image.pre_height }}" | |
|
64 | data-width="{{ image.width }}" | |
|
65 | data-height="{{ image.height }}"/> | |
|
66 | </a> | |
|
67 | </div> | |
|
68 | {% endwith %} | |
|
82 | {% endif %} | |
|
83 | </div> | |
|
84 | {% comment %} | |
|
85 | Thread metadata: counters, tags etc | |
|
86 | {% endcomment %} | |
|
87 | {% if is_opening %} | |
|
88 | <div class="metadata"> | |
|
89 | {% if is_opening and need_open_link %} | |
|
90 | {{ thread.get_reply_count }} {% trans 'messages' %}, | |
|
91 | {{ thread.get_images_count }} {% trans 'images' %}. | |
|
69 | 92 | {% endif %} |
|
70 |
< |
|
|
71 | {% autoescape off %} | |
|
72 |
{% |
|
|
73 | {{ post.text.rendered|truncatewords_html:50 }} | |
|
74 |
{% e |
|
|
75 | {{ post.text.rendered }} | |
|
76 | {% endif %} | |
|
77 | {% endautoescape %} | |
|
78 | {% if post.is_referenced %} | |
|
79 | <div class="refmap"> | |
|
80 | {% autoescape off %} | |
|
81 | {% trans "Replies" %}: {{ post.refmap }} | |
|
82 | {% endautoescape %} | |
|
83 | </div> | |
|
84 | {% endif %} | |
|
85 | </div> | |
|
86 | {% endcache %} | |
|
87 | {% if is_opening %} | |
|
88 | {% cache 600 post_thread thread.id thread.last_edit_time LANGUAGE_CODE need_open_link %} | |
|
89 | <div class="metadata"> | |
|
90 | {% if is_opening and need_open_link %} | |
|
91 | {{ thread.get_reply_count }} {% trans 'messages' %}, | |
|
92 | {{ thread.get_images_count }} {% trans 'images' %}. | |
|
93 | {% endif %} | |
|
94 | <span class="tags"> | |
|
95 | {% for tag in thread.get_tags %} | |
|
96 | <a class="tag" href="{% url 'tag' tag.name %}"> | |
|
97 | #{{ tag.name }}</a>{% if not forloop.last %},{% endif %} | |
|
98 | {% endfor %} | |
|
99 | </span> | |
|
100 | </div> | |
|
101 | {% endcache %} | |
|
102 | {% endif %} | |
|
103 | </div> | |
|
104 | {% endspaceless %} | |
|
93 | <span class="tags"> | |
|
94 | {% for tag in thread.get_tags %} | |
|
95 | {% autoescape off %} | |
|
96 | {{ tag.get_view }}{% if not forloop.last %},{% endif %} | |
|
97 | {% endautoescape %} | |
|
98 | {% endfor %} | |
|
99 | </span> | |
|
100 | </div> | |
|
101 | {% endif %} | |
|
102 | </div> |
@@ -1,197 +1,202 b'' | |||
|
1 | 1 | {% extends "boards/base.html" %} |
|
2 | 2 | |
|
3 | 3 | {% load i18n %} |
|
4 | 4 | {% load cache %} |
|
5 | 5 | {% load board %} |
|
6 | 6 | {% load static %} |
|
7 | 7 | |
|
8 | 8 | {% block head %} |
|
9 | 9 | {% if tag %} |
|
10 | 10 | <title>{{ tag.name }} - {{ site_name }}</title> |
|
11 | 11 | {% else %} |
|
12 | 12 | <title>{{ site_name }}</title> |
|
13 | 13 | {% endif %} |
|
14 | 14 | |
|
15 | 15 | {% if current_page.has_previous %} |
|
16 | 16 | <link rel="prev" href=" |
|
17 | 17 | {% if tag %} |
|
18 | {% url "tag" tag_name=tag page=current_page.previous_page_number %} | |
|
18 | {% url "tag" tag_name=tag.name page=current_page.previous_page_number %} | |
|
19 | 19 | {% elif archived %} |
|
20 | 20 | {% url "archive" page=current_page.previous_page_number %} |
|
21 | 21 | {% else %} |
|
22 | 22 | {% url "index" page=current_page.previous_page_number %} |
|
23 | 23 | {% endif %} |
|
24 | 24 | " /> |
|
25 | 25 | {% endif %} |
|
26 | 26 | {% if current_page.has_next %} |
|
27 | 27 | <link rel="next" href=" |
|
28 | 28 | {% if tag %} |
|
29 | {% url "tag" tag_name=tag page=current_page.next_page_number %} | |
|
29 | {% url "tag" tag_name=tag.name page=current_page.next_page_number %} | |
|
30 | 30 | {% elif archived %} |
|
31 | 31 | {% url "archive" page=current_page.next_page_number %} |
|
32 | 32 | {% else %} |
|
33 | 33 | {% url "index" page=current_page.next_page_number %} |
|
34 | 34 | {% endif %} |
|
35 | 35 | " /> |
|
36 | 36 | {% endif %} |
|
37 | 37 | |
|
38 | 38 | {% endblock %} |
|
39 | 39 | |
|
40 | 40 | {% block content %} |
|
41 | 41 | |
|
42 | 42 | {% get_current_language as LANGUAGE_CODE %} |
|
43 | 43 | |
|
44 | 44 | {% if tag %} |
|
45 | 45 | <div class="tag_info"> |
|
46 | 46 | <h2> |
|
47 | 47 | {% if tag in fav_tags %} |
|
48 | 48 | <a href="{% url 'tag' tag.name %}?method=unsubscribe&next={{ request.path }}" |
|
49 | class="fav">★</a> | |
|
49 | class="fav" rel="nofollow">★</a> | |
|
50 | 50 | {% else %} |
|
51 | 51 | <a href="{% url 'tag' tag.name %}?method=subscribe&next={{ request.path }}" |
|
52 | class="not_fav">★</a> | |
|
52 | class="not_fav" rel="nofollow">★</a> | |
|
53 | 53 | {% endif %} |
|
54 | 54 | {% if tag in hidden_tags %} |
|
55 | 55 | <a href="{% url 'tag' tag.name %}?method=unhide&next={{ request.path }}" |
|
56 | 56 | title="{% trans 'Show tag' %}" |
|
57 | class="fav">H</a> | |
|
57 | class="fav" rel="nofollow">H</a> | |
|
58 | 58 | {% else %} |
|
59 | 59 | <a href="{% url 'tag' tag.name %}?method=hide&next={{ request.path }}" |
|
60 | 60 | title="{% trans 'Hide tag' %}" |
|
61 | class="not_fav">H</a> | |
|
61 | class="not_fav" rel="nofollow">H</a> | |
|
62 | 62 | {% endif %} |
|
63 |
|
|
|
63 | {% autoescape off %} | |
|
64 | {{ tag.get_view }} | |
|
65 | {% endautoescape %} | |
|
66 | {% if moderator %} | |
|
67 | [<a href="{% url 'admin:boards_tag_change' tag.id %}"$>{% trans 'Edit tag' %}</a>] | |
|
68 | {% endif %} | |
|
64 | 69 | </h2> |
|
65 | 70 | </div> |
|
66 | 71 | {% endif %} |
|
67 | 72 | |
|
68 | 73 | {% if threads %} |
|
69 | 74 | {% if current_page.has_previous %} |
|
70 | 75 | <div class="page_link"> |
|
71 | 76 | <a href=" |
|
72 | 77 | {% if tag %} |
|
73 | {% url "tag" tag_name=tag page=current_page.previous_page_number %} | |
|
78 | {% url "tag" tag_name=tag.name page=current_page.previous_page_number %} | |
|
74 | 79 | {% elif archived %} |
|
75 | 80 | {% url "archive" page=current_page.previous_page_number %} |
|
76 | 81 | {% else %} |
|
77 | 82 | {% url "index" page=current_page.previous_page_number %} |
|
78 | 83 | {% endif %} |
|
79 | 84 | ">{% trans "Previous page" %}</a> |
|
80 | 85 | </div> |
|
81 | 86 | {% endif %} |
|
82 | 87 | |
|
83 | 88 | {% for thread in threads %} |
|
84 | 89 | {% cache 600 thread_short thread.id thread.last_edit_time moderator LANGUAGE_CODE %} |
|
85 | 90 | <div class="thread"> |
|
86 | 91 | {% with can_bump=thread.can_bump %} |
|
87 | 92 | {% post_view thread.get_opening_post moderator is_opening=True thread=thread can_bump=can_bump truncated=True need_open_link=True %} |
|
88 | 93 | {% if not thread.archived %} |
|
89 | 94 | {% with last_replies=thread.get_last_replies %} |
|
90 | 95 | {% if last_replies %} |
|
91 | 96 | {% if thread.get_skipped_replies_count %} |
|
92 | 97 | <div class="skipped_replies"> |
|
93 | 98 | <a href="{% url 'thread' thread.get_opening_post.id %}"> |
|
94 | 99 | {% blocktrans with count=thread.get_skipped_replies_count %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %} |
|
95 | 100 | </a> |
|
96 | 101 | </div> |
|
97 | 102 | {% endif %} |
|
98 | 103 | <div class="last-replies"> |
|
99 | 104 | {% for post in last_replies %} |
|
100 | 105 | {% post_view post moderator=moderator is_opening=False thread=thread can_bump=can_bump truncated=True %} |
|
101 | 106 | {% endfor %} |
|
102 | 107 | </div> |
|
103 | 108 | {% endif %} |
|
104 | 109 | {% endwith %} |
|
105 | 110 | {% endif %} |
|
106 | 111 | {% endwith %} |
|
107 | 112 | </div> |
|
108 | 113 | {% endcache %} |
|
109 | 114 | {% endfor %} |
|
110 | 115 | |
|
111 | 116 | {% if current_page.has_next %} |
|
112 | 117 | <div class="page_link"> |
|
113 | 118 | <a href=" |
|
114 | 119 | {% if tag %} |
|
115 | {% url "tag" tag_name=tag page=current_page.next_page_number %} | |
|
120 | {% url "tag" tag_name=tag.name page=current_page.next_page_number %} | |
|
116 | 121 | {% elif archived %} |
|
117 | 122 | {% url "archive" page=current_page.next_page_number %} |
|
118 | 123 | {% else %} |
|
119 | 124 | {% url "index" page=current_page.next_page_number %} |
|
120 | 125 | {% endif %} |
|
121 | 126 | ">{% trans "Next page" %}</a> |
|
122 | 127 | </div> |
|
123 | 128 | {% endif %} |
|
124 | 129 | {% else %} |
|
125 | 130 | <div class="post"> |
|
126 | 131 | {% trans 'No threads exist. Create the first one!' %}</div> |
|
127 | 132 | {% endif %} |
|
128 | 133 | |
|
129 | 134 | <div class="post-form-w"> |
|
130 | 135 | <script src="{% static 'js/panel.js' %}"></script> |
|
131 | 136 | <div class="post-form"> |
|
132 | 137 | <div class="form-title">{% trans "Create new thread" %}</div> |
|
133 | 138 | <div class="swappable-form-full"> |
|
134 | 139 | <form enctype="multipart/form-data" method="post">{% csrf_token %} |
|
135 | 140 | {{ form.as_div }} |
|
136 | 141 | <div class="form-submit"> |
|
137 | 142 | <input type="submit" value="{% trans "Post" %}"/> |
|
138 | 143 | </div> |
|
139 | 144 | </form> |
|
140 | 145 | </div> |
|
141 | 146 | <div> |
|
142 | 147 | {% trans 'Tags must be delimited by spaces. Text or image is required.' %} |
|
143 | 148 | </div> |
|
144 | 149 | <div><a href="{% url "staticpage" name="help" %}"> |
|
145 | 150 | {% trans 'Text syntax' %}</a></div> |
|
146 | 151 | </div> |
|
147 | 152 | </div> |
|
148 | 153 | |
|
149 | 154 | <script src="{% static 'js/form.js' %}"></script> |
|
150 | 155 | |
|
151 | 156 | {% endblock %} |
|
152 | 157 | |
|
153 | 158 | {% block metapanel %} |
|
154 | 159 | |
|
155 | 160 | <span class="metapanel"> |
|
156 | 161 | <b><a href="{% url "authors" %}">{{ site_name }}</a> {{ version }}</b> |
|
157 | 162 | {% trans "Pages:" %} |
|
158 | 163 | <a href=" |
|
159 | 164 | {% if tag %} |
|
160 | {% url "tag" tag_name=tag page=paginator.page_range|first %} | |
|
165 | {% url "tag" tag_name=tag.name page=paginator.page_range|first %} | |
|
161 | 166 | {% elif archived %} |
|
162 | 167 | {% url "archive" page=paginator.page_range|first %} |
|
163 | 168 | {% else %} |
|
164 | 169 | {% url "index" page=paginator.page_range|first %} |
|
165 | 170 | {% endif %} |
|
166 | 171 | "><<</a> |
|
167 | 172 | [ |
|
168 | 173 | {% for page in paginator.center_range %} |
|
169 | 174 | <a |
|
170 | 175 | {% ifequal page current_page.number %} |
|
171 | 176 | class="current_page" |
|
172 | 177 | {% endifequal %} |
|
173 | 178 | href=" |
|
174 | 179 | {% if tag %} |
|
175 | {% url "tag" tag_name=tag page=page %} | |
|
180 | {% url "tag" tag_name=tag.name page=page %} | |
|
176 | 181 | {% elif archived %} |
|
177 | 182 | {% url "archive" page=page %} |
|
178 | 183 | {% else %} |
|
179 | 184 | {% url "index" page=page %} |
|
180 | 185 | {% endif %} |
|
181 | 186 | ">{{ page }}</a> |
|
182 | 187 | {% if not forloop.last %},{% endif %} |
|
183 | 188 | {% endfor %} |
|
184 | 189 | ] |
|
185 | 190 | <a href=" |
|
186 | 191 | {% if tag %} |
|
187 | {% url "tag" tag_name=tag page=paginator.page_range|last %} | |
|
192 | {% url "tag" tag_name=tag.name page=paginator.page_range|last %} | |
|
188 | 193 | {% elif archived %} |
|
189 | 194 | {% url "archive" page=paginator.page_range|last %} |
|
190 | 195 | {% else %} |
|
191 | 196 | {% url "index" page=paginator.page_range|last %} |
|
192 | 197 | {% endif %} |
|
193 | 198 | ">>></a> |
|
194 | 199 | [<a href="rss/">RSS</a>] |
|
195 | 200 | </span> |
|
196 | 201 | |
|
197 | 202 | {% endblock %} |
@@ -1,15 +1,15 b'' | |||
|
1 | 1 | {% load i18n %} |
|
2 | 2 | |
|
3 | 3 | {% if obj.images.exists %} |
|
4 | 4 | <img src="{{ obj.get_first_image.image.url_200x150 }}" |
|
5 | 5 | alt="{% trans 'Post image' %}" /> |
|
6 | 6 | {% endif %} |
|
7 |
{{ obj.text |
|
|
7 | {{ obj.get_text|safe }} | |
|
8 | 8 | {% if obj.tags.all %} |
|
9 | 9 | <p> |
|
10 | 10 | {% trans 'Tags' %}: |
|
11 | 11 | {% for tag in obj.tags.all %} |
|
12 | 12 | {{ tag.name }} |
|
13 | 13 | {% endfor %} |
|
14 | 14 | </p> |
|
15 | 15 | {% endif %} No newline at end of file |
@@ -1,3 +1,5 b'' | |||
|
1 | 1 | <div class="post"> |
|
2 | <a class="tag" href="{% url 'tag' tag_name=tag.name %}">#{{ tag.name }}</a> | |
|
3 | </div> No newline at end of file | |
|
2 | {% autoescape off %} | |
|
3 | {{ tag.get_view }} | |
|
4 | {% endautoescape %} | |
|
5 | </div> |
@@ -1,27 +1,28 b'' | |||
|
1 | 1 | {% extends "boards/base.html" %} |
|
2 | 2 | |
|
3 | 3 | {% load i18n %} |
|
4 | 4 | {% load cache %} |
|
5 | 5 | |
|
6 | 6 | {% block head %} |
|
7 | 7 | <title>Neboard - {% trans "Tags" %}</title> |
|
8 | 8 | {% endblock %} |
|
9 | 9 | |
|
10 | 10 | {% block content %} |
|
11 | 11 | |
|
12 | 12 | {% cache 600 all_tags_list %} |
|
13 | 13 | <div class="post"> |
|
14 | 14 | {% if all_tags %} |
|
15 | 15 | {% for tag in all_tags %} |
|
16 | 16 | <div class="tag_item"> |
|
17 | <a class="tag" href="{% url 'tag' tag.name %}"> | |
|
18 |
|
|
|
17 | {% autoescape off %} | |
|
18 | {{ tag.get_view }} | |
|
19 | {% endautoescape %} | |
|
19 | 20 | </div> |
|
20 | 21 | {% endfor %} |
|
21 | 22 | {% else %} |
|
22 | 23 | {% trans 'No tags found.' %} |
|
23 | 24 | {% endif %} |
|
24 | 25 | </div> |
|
25 | 26 | {% endcache %} |
|
26 | 27 | |
|
27 | 28 | {% endblock %} |
@@ -1,95 +1,96 b'' | |||
|
1 | 1 | {% extends "boards/base.html" %} |
|
2 | 2 | |
|
3 | 3 | {% load i18n %} |
|
4 | 4 | {% load cache %} |
|
5 | 5 | {% load static from staticfiles %} |
|
6 | 6 | {% load board %} |
|
7 | 7 | |
|
8 | 8 | {% block head %} |
|
9 | 9 | <title>{{ opening_post.get_title|striptags|truncatewords:10 }} |
|
10 | 10 | - {{ site_name }}</title> |
|
11 | 11 | {% endblock %} |
|
12 | 12 | |
|
13 | 13 | {% block content %} |
|
14 | {% spaceless %} | |
|
15 | 14 | {% get_current_language as LANGUAGE_CODE %} |
|
16 | 15 | |
|
17 | 16 | {% cache 600 thread_view thread.id thread.last_edit_time moderator LANGUAGE_CODE %} |
|
18 | 17 | |
|
19 | 18 | <div class="image-mode-tab"> |
|
20 | 19 | <a class="current_mode" href="{% url 'thread' opening_post.id %}">{% trans 'Normal mode' %}</a>, |
|
21 | 20 | <a href="{% url 'thread_mode' opening_post.id 'gallery' %}">{% trans 'Gallery mode' %}</a> |
|
22 | 21 | </div> |
|
23 | 22 | |
|
24 | 23 | {% if bumpable %} |
|
25 | 24 | <div class="bar-bg"> |
|
26 | 25 | <div class="bar-value" style="width:{{ bumplimit_progress }}%" id="bumplimit_progress"> |
|
27 | 26 | </div> |
|
28 | 27 | <div class="bar-text"> |
|
29 | 28 | <span id="left_to_limit">{{ posts_left }}</span> {% trans 'posts to bumplimit' %} |
|
30 | 29 | </div> |
|
31 | 30 | </div> |
|
32 | 31 | {% endif %} |
|
33 | 32 | |
|
34 | 33 | <div class="thread"> |
|
35 | 34 | {% with can_bump=thread.can_bump %} |
|
36 | 35 | {% for post in thread.get_replies %} |
|
37 |
{% |
|
|
38 |
{% post_view post moderator=moderator is_opening= |
|
|
39 |
{% e |
|
|
40 | {% post_view post moderator=moderator is_opening=False thread=thread can_bump=can_bump opening_post_id=opening_post.id %} | |
|
41 | {% endif %} | |
|
36 | {% with is_opening=forloop.first %} | |
|
37 | {% post_view post moderator=moderator is_opening=is_opening thread=thread bumpable=can_bump opening_post_id=opening_post.id %} | |
|
38 | {% endwith %} | |
|
42 | 39 | {% endfor %} |
|
43 | 40 | {% endwith %} |
|
44 | 41 | </div> |
|
45 | 42 | |
|
46 | 43 | {% if not thread.archived %} |
|
47 | ||
|
48 | <div class="post-form-w" id="form"> | |
|
49 | <script src="{% static 'js/panel.js' %}"></script> | |
|
50 | <div class="form-title">{% trans "Reply to thread" %} #{{ opening_post.id }}</div> | |
|
51 |
<div class=" |
|
|
52 | <div class="swappable-form-full"> | |
|
53 | <form enctype="multipart/form-data" method="post" | |
|
54 | >{% csrf_token %} | |
|
44 | <div class="post-form-w" id="form"> | |
|
45 | <script src="{% static 'js/panel.js' %}"></script> | |
|
46 | <div class="form-title">{% trans "Reply to thread" %} #{{ opening_post.id }}</div> | |
|
47 | <div class="post-form" id="compact-form"> | |
|
48 | <div class="swappable-form-full"> | |
|
49 | <form enctype="multipart/form-data" method="post" | |
|
50 | >{% csrf_token %} | |
|
55 | 51 | <div class="compact-form-text"></div> |
|
56 | {{ form.as_div }} | |
|
57 | <div class="form-submit"> | |
|
58 | <input type="submit" value="{% trans "Post" %}"/> | |
|
59 | </div> | |
|
60 | </form> | |
|
52 | {{ form.as_div }} | |
|
53 | <div class="form-submit"> | |
|
54 | <input type="submit" value="{% trans "Post" %}"/> | |
|
55 | </div> | |
|
56 | </form> | |
|
57 | </div> | |
|
58 | <a onclick="swapForm(); return false;" href="#"> | |
|
59 | {% trans 'Switch mode' %} | |
|
60 | </a> | |
|
61 | <div><a href="{% url "staticpage" name="help" %}"> | |
|
62 | {% trans 'Text syntax' %}</a></div> | |
|
61 | 63 | </div> |
|
62 | <a onclick="swapForm(); return false;" href="#"> | |
|
63 | {% trans 'Switch mode' %} | |
|
64 | </a> | |
|
65 | <div><a href="{% url "staticpage" name="help" %}"> | |
|
66 | {% trans 'Text syntax' %}</a></div> | |
|
67 | 64 | </div> |
|
68 | </div> | |
|
69 | 65 | |
|
70 | <script src="{% static 'js/jquery.form.min.js' %}"></script> | |
|
71 | <script src="{% static 'js/thread_update.js' %}"></script> | |
|
66 | <script src="{% static 'js/jquery.form.min.js' %}"></script> | |
|
67 | <script src="{% static 'js/thread_update.js' %}"></script> | |
|
68 | <script src="{% static 'js/3party/centrifuge.js' %}"></script> | |
|
72 | 69 | {% endif %} |
|
73 | 70 | |
|
74 | 71 | <script src="{% static 'js/form.js' %}"></script> |
|
75 | 72 | <script src="{% static 'js/thread.js' %}"></script> |
|
76 | 73 | |
|
77 | 74 | {% endcache %} |
|
78 | ||
|
79 | {% endspaceless %} | |
|
80 | 75 | {% endblock %} |
|
81 | 76 | |
|
82 | 77 | {% block metapanel %} |
|
83 | 78 | |
|
84 | 79 | {% get_current_language as LANGUAGE_CODE %} |
|
85 | 80 | |
|
86 | <span class="metapanel" data-last-update="{{ last_update }}"> | |
|
81 | <span class="metapanel" | |
|
82 | data-last-update="{{ last_update }}" | |
|
83 | data-ws-token="{{ ws_token }}" | |
|
84 | data-ws-project="{{ ws_project }}" | |
|
85 | data-ws-host="{{ ws_host }}" | |
|
86 | data-ws-port="{{ ws_port }}"> | |
|
87 | 87 | {% cache 600 thread_meta thread.last_edit_time moderator LANGUAGE_CODE %} |
|
88 | <span id="autoupdate">[-]</span> | |
|
88 | 89 | <span id="reply-count">{{ thread.get_reply_count }}</span>/{{ max_replies }} {% trans 'messages' %}, |
|
89 | 90 | <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}. |
|
90 | 91 | {% trans 'Last update: ' %}<span id="last-update">{{ thread.last_edit_time }}</span> |
|
91 | 92 | [<a href="rss/">RSS</a>] |
|
92 | 93 | {% endcache %} |
|
93 | 94 | </span> |
|
94 | 95 | |
|
95 | 96 | {% endblock %} |
@@ -1,38 +1,38 b'' | |||
|
1 | 1 | {% extends 'boards/base.html' %} |
|
2 | 2 | |
|
3 | 3 | {% load board %} |
|
4 | 4 | {% load i18n %} |
|
5 | 5 | |
|
6 | 6 | {% block content %} |
|
7 | 7 | <div class="post-form-w"> |
|
8 | 8 | <div class="post-form"> |
|
9 | 9 | <h3>{% trans 'Search' %}</h3> |
|
10 | 10 | <form method="get" action=""> |
|
11 | 11 | {{ form.as_div }} |
|
12 | 12 | <div class="form-submit"> |
|
13 | 13 | <input type="submit" value="{% trans 'Search' %}"> |
|
14 | 14 | </div> |
|
15 | 15 | </form> |
|
16 | 16 | </div> |
|
17 | 17 | </div> |
|
18 | 18 | |
|
19 | 19 | {% if page %} |
|
20 | 20 | {% if page.has_previous %} |
|
21 | 21 | <div class="page_link"> |
|
22 | 22 | <a href="?query={{ query }}&page={{ page.previous_page_number }}">{% trans "Previous page" %} |
|
23 | 23 | </a> |
|
24 | 24 | </div> |
|
25 | 25 | {% endif %} |
|
26 | 26 | |
|
27 | 27 | {% for result in page.object_list %} |
|
28 | {{ result.object.get_view }} | |
|
28 | {{ result.object.get_search_view }} | |
|
29 | 29 | {% endfor %} |
|
30 | 30 | |
|
31 | 31 | {% if page.has_next %} |
|
32 | 32 | <div class="page_link"> |
|
33 | 33 | <a href="?query={{ query }}&page={{ page.next_page_number }}">{% trans "Next page" %} |
|
34 | 34 | </a> |
|
35 | 35 | </div> |
|
36 | 36 | {% endif %} |
|
37 | 37 | {% endif %} |
|
38 | {% endblock %} No newline at end of file | |
|
38 | {% endblock %} |
@@ -1,55 +1,56 b'' | |||
|
1 | 1 | from django.test import TestCase, Client |
|
2 | 2 | import time |
|
3 | 3 | from boards import settings |
|
4 | from boards.models import Post | |
|
4 | from boards.models import Post, Tag | |
|
5 | 5 | import neboard |
|
6 | 6 | |
|
7 | 7 | |
|
8 | 8 | TEST_TAG = 'test_tag' |
|
9 | 9 | |
|
10 | 10 | PAGE_404 = 'boards/404.html' |
|
11 | 11 | |
|
12 | 12 | TEST_TEXT = 'test text' |
|
13 | 13 | |
|
14 | 14 | NEW_THREAD_PAGE = '/' |
|
15 | 15 | THREAD_PAGE_ONE = '/thread/1/' |
|
16 | 16 | HTTP_CODE_REDIRECT = 302 |
|
17 | 17 | |
|
18 | 18 | |
|
19 | 19 | class FormTest(TestCase): |
|
20 | 20 | def test_post_validation(self): |
|
21 | 21 | client = Client() |
|
22 | 22 | |
|
23 | 23 | valid_tags = 'tag1 tag_2 тег_3' |
|
24 | 24 | invalid_tags = '$%_356 ---' |
|
25 | Tag.objects.create(name='tag1', required=True) | |
|
25 | 26 | |
|
26 | 27 | response = client.post(NEW_THREAD_PAGE, {'title': 'test title', |
|
27 | 28 | 'text': TEST_TEXT, |
|
28 | 29 | 'tags': valid_tags}) |
|
29 | 30 | self.assertEqual(response.status_code, HTTP_CODE_REDIRECT, |
|
30 | 31 | msg='Posting new message failed: got code ' + |
|
31 | 32 | str(response.status_code)) |
|
32 | 33 | |
|
33 | 34 | self.assertEqual(1, Post.objects.count(), |
|
34 | 35 | msg='No posts were created') |
|
35 | 36 | |
|
36 | 37 | client.post(NEW_THREAD_PAGE, {'text': TEST_TEXT, |
|
37 | 38 | 'tags': invalid_tags}) |
|
38 | 39 | self.assertEqual(1, Post.objects.count(), msg='The validation passed ' |
|
39 | 40 | 'where it should fail') |
|
40 | 41 | |
|
41 | 42 | # Change posting delay so we don't have to wait for 30 seconds or more |
|
42 | 43 | old_posting_delay = neboard.settings.POSTING_DELAY |
|
43 | 44 | # Wait fot the posting delay or we won't be able to post |
|
44 | 45 | neboard.settings.POSTING_DELAY = 1 |
|
45 | 46 | time.sleep(neboard.settings.POSTING_DELAY + 1) |
|
46 | 47 | response = client.post(THREAD_PAGE_ONE, {'text': TEST_TEXT, |
|
47 | 48 | 'tags': valid_tags}) |
|
48 | 49 | self.assertEqual(HTTP_CODE_REDIRECT, response.status_code, |
|
49 | 50 | msg='Posting new message failed: got code ' + |
|
50 | 51 | str(response.status_code)) |
|
51 | 52 | # Restore posting delay |
|
52 | 53 | settings.POSTING_DELAY = old_posting_delay |
|
53 | 54 | |
|
54 | 55 | self.assertEqual(2, Post.objects.count(), |
|
55 | 56 | msg='No posts were created') |
@@ -1,56 +1,52 b'' | |||
|
1 | 1 | from django.test import TestCase, Client |
|
2 | 2 | from boards.models import Tag, Post |
|
3 | 3 | |
|
4 | 4 | TEST_TEXT = 'test' |
|
5 | 5 | |
|
6 | 6 | NEW_THREAD_PAGE = '/' |
|
7 | 7 | THREAD_PAGE_ONE = '/thread/1/' |
|
8 | 8 | THREAD_PAGE = '/thread/' |
|
9 | 9 | TAG_PAGE = '/tag/' |
|
10 |
HTTP_CODE_REDIRECT = 30 |
|
|
10 | HTTP_CODE_REDIRECT = 301 | |
|
11 | 11 | HTTP_CODE_OK = 200 |
|
12 | 12 | HTTP_CODE_NOT_FOUND = 404 |
|
13 | 13 | |
|
14 | PAGE_404 = 'boards/404.html' | |
|
15 | ||
|
16 | 14 | |
|
17 | 15 | class PagesTest(TestCase): |
|
18 | 16 | |
|
19 | 17 | def test_404(self): |
|
20 | 18 | """Test receiving error 404 when opening a non-existent page""" |
|
21 | 19 | |
|
22 | 20 | tag_name = 'test_tag' |
|
23 | 21 | tag = Tag.objects.create(name=tag_name) |
|
24 | 22 | client = Client() |
|
25 | 23 | |
|
26 | 24 | Post.objects.create_post('title', TEST_TEXT, tags=[tag]) |
|
27 | 25 | |
|
28 | 26 | existing_post_id = Post.objects.all()[0].id |
|
29 | 27 | response_existing = client.get(THREAD_PAGE + str(existing_post_id) + |
|
30 | 28 | '/') |
|
31 | 29 | self.assertEqual(HTTP_CODE_OK, response_existing.status_code, |
|
32 | 30 | 'Cannot open existing thread') |
|
33 | 31 | |
|
34 | 32 | response_not_existing = client.get(THREAD_PAGE + str( |
|
35 | 33 | existing_post_id + 1) + '/') |
|
36 |
self.assertEqual( |
|
|
34 | self.assertEqual(HTTP_CODE_NOT_FOUND, response_not_existing.status_code, | |
|
37 | 35 | 'Not existing thread is opened') |
|
38 | 36 | |
|
39 | 37 | response_existing = client.get(TAG_PAGE + tag_name + '/') |
|
40 | 38 | self.assertEqual(HTTP_CODE_OK, |
|
41 | 39 | response_existing.status_code, |
|
42 | 40 | 'Cannot open existing tag') |
|
43 | 41 | |
|
44 | 42 | response_not_existing = client.get(TAG_PAGE + 'not_tag' + '/') |
|
45 | self.assertEqual(PAGE_404, | |
|
46 | response_not_existing.templates[0].name, | |
|
43 | self.assertEqual(HTTP_CODE_NOT_FOUND, response_not_existing.status_code, | |
|
47 | 44 | 'Not existing tag is opened') |
|
48 | 45 | |
|
49 | 46 | reply_id = Post.objects.create_post('', TEST_TEXT, |
|
50 | 47 | thread=Post.objects.all()[0] |
|
51 | 48 | .get_thread()) |
|
52 | 49 | response_not_existing = client.get(THREAD_PAGE + str( |
|
53 | 50 | reply_id) + '/') |
|
54 | self.assertEqual(PAGE_404, | |
|
55 | response_not_existing.templates[0].name, | |
|
51 | self.assertEqual(HTTP_CODE_REDIRECT, response_not_existing.status_code, | |
|
56 | 52 | 'Reply is opened as a thread') |
@@ -1,142 +1,163 b'' | |||
|
1 | 1 | from django.core.paginator import Paginator |
|
2 | 2 | from django.test import TestCase |
|
3 | 3 | from boards import settings |
|
4 | 4 | from boards.models import Tag, Post, Thread, KeyPair |
|
5 | 5 | |
|
6 | 6 | |
|
7 | 7 | class PostTests(TestCase): |
|
8 | 8 | |
|
9 | 9 | def _create_post(self): |
|
10 | 10 | tag = Tag.objects.create(name='test_tag') |
|
11 | 11 | return Post.objects.create_post(title='title', text='text', |
|
12 | 12 | tags=[tag]) |
|
13 | 13 | |
|
14 | 14 | def test_post_add(self): |
|
15 | 15 | """Test adding post""" |
|
16 | 16 | |
|
17 | 17 | post = self._create_post() |
|
18 | 18 | |
|
19 | 19 | self.assertIsNotNone(post, 'No post was created.') |
|
20 | 20 | self.assertEqual('test_tag', post.get_thread().tags.all()[0].name, |
|
21 | 21 | 'No tags were added to the post.') |
|
22 | 22 | |
|
23 | 23 | def test_delete_post(self): |
|
24 | 24 | """Test post deletion""" |
|
25 | 25 | |
|
26 | 26 | post = self._create_post() |
|
27 | 27 | post_id = post.id |
|
28 | 28 | |
|
29 |
|
|
|
29 | post.delete() | |
|
30 | 30 | |
|
31 | 31 | self.assertFalse(Post.objects.filter(id=post_id).exists()) |
|
32 | 32 | |
|
33 | 33 | def test_delete_thread(self): |
|
34 | 34 | """Test thread deletion""" |
|
35 | 35 | |
|
36 | 36 | opening_post = self._create_post() |
|
37 | 37 | thread = opening_post.get_thread() |
|
38 | 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 | 47 | def test_post_to_thread(self): |
|
45 | 48 | """Test adding post to a thread""" |
|
46 | 49 | |
|
47 | 50 | op = self._create_post() |
|
48 | 51 | post = Post.objects.create_post("", "", thread=op.get_thread()) |
|
49 | 52 | |
|
50 | 53 | self.assertIsNotNone(post, 'Reply to thread wasn\'t created') |
|
51 | 54 | self.assertEqual(op.get_thread().last_edit_time, post.pub_time, |
|
52 | 55 | 'Post\'s create time doesn\'t match thread last edit' |
|
53 | 56 | ' time') |
|
54 | 57 | |
|
55 | 58 | def test_delete_posts_by_ip(self): |
|
56 | 59 | """Test deleting posts with the given ip""" |
|
57 | 60 | |
|
58 | 61 | post = self._create_post() |
|
59 | 62 | post_id = post.id |
|
60 | 63 | |
|
61 | 64 | Post.objects.delete_posts_by_ip('0.0.0.0') |
|
62 | 65 | |
|
63 | 66 | self.assertFalse(Post.objects.filter(id=post_id).exists()) |
|
64 | 67 | |
|
65 | 68 | def test_get_thread(self): |
|
66 | 69 | """Test getting all posts of a thread""" |
|
67 | 70 | |
|
68 | 71 | opening_post = self._create_post() |
|
69 | 72 | |
|
70 | 73 | for i in range(2): |
|
71 | 74 | Post.objects.create_post('title', 'text', |
|
72 | 75 | thread=opening_post.get_thread()) |
|
73 | 76 | |
|
74 | 77 | thread = opening_post.get_thread() |
|
75 | 78 | |
|
76 | 79 | self.assertEqual(3, thread.replies.count()) |
|
77 | 80 | |
|
78 | 81 | def test_create_post_with_tag(self): |
|
79 | 82 | """Test adding tag to post""" |
|
80 | 83 | |
|
81 | 84 | tag = Tag.objects.create(name='test_tag') |
|
82 | 85 | post = Post.objects.create_post(title='title', text='text', tags=[tag]) |
|
83 | 86 | |
|
84 | 87 | thread = post.get_thread() |
|
85 | 88 | self.assertIsNotNone(post, 'Post not created') |
|
86 | 89 | self.assertTrue(tag in thread.tags.all(), 'Tag not added to thread') |
|
87 | self.assertTrue(thread in tag.threads.all(), 'Thread not added to tag') | |
|
88 | 90 | |
|
89 | 91 | def test_thread_max_count(self): |
|
90 | 92 | """Test deletion of old posts when the max thread count is reached""" |
|
91 | 93 | |
|
92 | 94 | for i in range(settings.MAX_THREAD_COUNT + 1): |
|
93 | 95 | self._create_post() |
|
94 | 96 | |
|
95 | 97 | self.assertEqual(settings.MAX_THREAD_COUNT, |
|
96 | 98 | len(Thread.objects.filter(archived=False))) |
|
97 | 99 | |
|
98 | 100 | def test_pages(self): |
|
99 | 101 | """Test that the thread list is properly split into pages""" |
|
100 | 102 | |
|
101 | 103 | for i in range(settings.MAX_THREAD_COUNT): |
|
102 | 104 | self._create_post() |
|
103 | 105 | |
|
104 | 106 | all_threads = Thread.objects.filter(archived=False) |
|
105 | 107 | |
|
106 | 108 | paginator = Paginator(Thread.objects.filter(archived=False), |
|
107 | 109 | settings.THREADS_PER_PAGE) |
|
108 | 110 | posts_in_second_page = paginator.page(2).object_list |
|
109 | 111 | first_post = posts_in_second_page[0] |
|
110 | 112 | |
|
111 | 113 | self.assertEqual(all_threads[settings.THREADS_PER_PAGE].id, |
|
112 | 114 | first_post.id) |
|
113 | 115 | |
|
114 | 116 | def test_reflinks(self): |
|
115 | 117 | """ |
|
116 | 118 | Tests that reflinks are parsed within post and connecting replies |
|
117 | 119 | to the replied posts. |
|
118 | 120 | |
|
119 | 121 | Local reflink example: [post]123[/post] |
|
120 | 122 | Global reflink example: [post]key_type::key::123[/post] |
|
121 | 123 | """ |
|
122 | 124 | |
|
123 | 125 | key = KeyPair.objects.generate_key(primary=True) |
|
124 | 126 | |
|
125 | 127 | tag = Tag.objects.create(name='test_tag') |
|
126 | 128 | |
|
127 | 129 | post = Post.objects.create_post(title='', text='', tags=[tag]) |
|
128 | 130 | post_local_reflink = Post.objects.create_post(title='', |
|
129 | 131 | text='[post]%d[/post]' % post.id, thread=post.get_thread()) |
|
130 | 132 | |
|
131 | 133 | self.assertTrue(post_local_reflink in post.referenced_posts.all(), |
|
132 | 134 | 'Local reflink not connecting posts.') |
|
133 | 135 | |
|
134 | 136 | post_global_reflink = Post.objects.create_post(title='', |
|
135 | 137 | text='[post]%s::%s::%d[/post]' % ( |
|
136 | 138 | post.global_id.key_type, post.global_id.key, post.id), |
|
137 | 139 | thread=post.get_thread()) |
|
138 | 140 | |
|
139 | 141 | self.assertTrue(post_global_reflink in post.referenced_posts.all(), |
|
140 | 142 | 'Global reflink not connecting posts.') |
|
141 | 143 | |
|
142 | # TODO Check that links are parsed into the rendered text | |
|
144 | def test_thread_replies(self): | |
|
145 | """ | |
|
146 | Tests that the replies can be queried from a thread in all possible | |
|
147 | ways. | |
|
148 | """ | |
|
149 | ||
|
150 | tag = Tag.objects.create(name='test_tag') | |
|
151 | opening_post = Post.objects.create_post(title='title', text='text', | |
|
152 | tags=[tag]) | |
|
153 | thread = opening_post.get_thread() | |
|
154 | ||
|
155 | reply1 = Post.objects.create_post(title='title', text='text', thread=thread) | |
|
156 | reply2 = Post.objects.create_post(title='title', text='text', thread=thread) | |
|
157 | ||
|
158 | replies = thread.get_replies() | |
|
159 | self.assertTrue(len(replies) > 0, 'No replies found for thread.') | |
|
160 | ||
|
161 | replies = thread.get_replies(view_fields_only=True) | |
|
162 | self.assertTrue(len(replies) > 0, | |
|
163 | 'No replies found for thread with view fields only.') |
@@ -1,38 +1,46 b'' | |||
|
1 | 1 | import logging |
|
2 | 2 | from django.core.urlresolvers import reverse, NoReverseMatch |
|
3 | 3 | from django.test import TestCase, Client |
|
4 | 4 | from boards import urls |
|
5 | 5 | |
|
6 | 6 | |
|
7 | 7 | logger = logging.getLogger(__name__) |
|
8 | 8 | |
|
9 | 9 | HTTP_CODE_OK = 200 |
|
10 | 10 | |
|
11 | EXCLUDED_VIEWS = { | |
|
12 | 'banned', | |
|
13 | } | |
|
14 | ||
|
11 | 15 | |
|
12 | 16 | class ViewTest(TestCase): |
|
13 | 17 | |
|
14 | 18 | def test_all_views(self): |
|
15 | 19 | """ |
|
16 | 20 | Try opening all views defined in ulrs.py that don't need additional |
|
17 | 21 | parameters |
|
18 | 22 | """ |
|
19 | 23 | |
|
20 | 24 | client = Client() |
|
21 | 25 | for url in urls.urlpatterns: |
|
22 | 26 | try: |
|
23 | 27 | view_name = url.name |
|
28 | if view_name in EXCLUDED_VIEWS: | |
|
29 | logger.debug('View {} is excluded.'.format(view_name)) | |
|
30 | continue | |
|
31 | ||
|
24 | 32 | logger.debug('Testing view %s' % view_name) |
|
25 | 33 | |
|
26 | 34 | try: |
|
27 | 35 | response = client.get(reverse(view_name)) |
|
28 | 36 | |
|
29 | 37 | self.assertEqual(HTTP_CODE_OK, response.status_code, |
|
30 |
|
|
|
38 | 'View not opened: {}'.format(view_name)) | |
|
31 | 39 | except NoReverseMatch: |
|
32 | 40 | # This view just needs additional arguments |
|
33 | 41 | pass |
|
34 | 42 | except Exception as e: |
|
35 | 43 | self.fail('Got exception %s at %s view' % (e, view_name)) |
|
36 | 44 | except AttributeError: |
|
37 | 45 | # This is normal, some views do not have names |
|
38 | 46 | pass |
@@ -1,219 +1,215 b'' | |||
|
1 | 1 | # -*- encoding: utf-8 -*- |
|
2 | 2 | """ |
|
3 | 3 | django-thumbs by Antonio Melé |
|
4 | 4 | http://django.es |
|
5 | 5 | """ |
|
6 | 6 | from django.core.files.images import ImageFile |
|
7 | 7 | from django.db.models import ImageField |
|
8 | 8 | from django.db.models.fields.files import ImageFieldFile |
|
9 | 9 | from PIL import Image |
|
10 | 10 | from django.core.files.base import ContentFile |
|
11 | 11 | import io |
|
12 | 12 | |
|
13 | 13 | |
|
14 | 14 | def generate_thumb(img, thumb_size, format): |
|
15 | 15 | """ |
|
16 | 16 | Generates a thumbnail image and returns a ContentFile object with the thumbnail |
|
17 | 17 | |
|
18 | 18 | Parameters: |
|
19 | 19 | =========== |
|
20 | 20 | img File object |
|
21 | 21 | |
|
22 | 22 | thumb_size desired thumbnail size, ie: (200,120) |
|
23 | 23 | |
|
24 | 24 | format format of the original image ('jpeg','gif','png',...) |
|
25 | 25 | (this format will be used for the generated thumbnail, too) |
|
26 | 26 | """ |
|
27 | 27 | |
|
28 | 28 | img.seek(0) # see http://code.djangoproject.com/ticket/8222 for details |
|
29 | 29 | image = Image.open(img) |
|
30 | 30 | |
|
31 | 31 | # get size |
|
32 | 32 | thumb_w, thumb_h = thumb_size |
|
33 | 33 | # If you want to generate a square thumbnail |
|
34 | 34 | if thumb_w == thumb_h: |
|
35 | 35 | # quad |
|
36 | 36 | xsize, ysize = image.size |
|
37 | 37 | # get minimum size |
|
38 | 38 | minsize = min(xsize, ysize) |
|
39 | 39 | # largest square possible in the image |
|
40 | 40 | xnewsize = (xsize - minsize) / 2 |
|
41 | 41 | ynewsize = (ysize - minsize) / 2 |
|
42 | 42 | # crop it |
|
43 | 43 | image2 = image.crop( |
|
44 | 44 | (xnewsize, ynewsize, xsize - xnewsize, ysize - ynewsize)) |
|
45 | 45 | # load is necessary after crop |
|
46 | 46 | image2.load() |
|
47 | 47 | # thumbnail of the cropped image (with ANTIALIAS to make it look better) |
|
48 | 48 | image2.thumbnail(thumb_size, Image.ANTIALIAS) |
|
49 | 49 | else: |
|
50 | 50 | # not quad |
|
51 | 51 | image2 = image |
|
52 | 52 | image2.thumbnail(thumb_size, Image.ANTIALIAS) |
|
53 | 53 | |
|
54 | 54 | output = io.BytesIO() |
|
55 | 55 | # PNG and GIF are the same, JPG is JPEG |
|
56 | 56 | if format.upper() == 'JPG': |
|
57 | 57 | format = 'JPEG' |
|
58 | 58 | |
|
59 | 59 | image2.save(output, format) |
|
60 | 60 | return ContentFile(output.getvalue()) |
|
61 | 61 | |
|
62 | 62 | |
|
63 | 63 | class ImageWithThumbsFieldFile(ImageFieldFile): |
|
64 | 64 | """ |
|
65 | 65 | See ImageWithThumbsField for usage example |
|
66 | 66 | """ |
|
67 | 67 | |
|
68 | 68 | def __init__(self, *args, **kwargs): |
|
69 | 69 | super(ImageWithThumbsFieldFile, self).__init__(*args, **kwargs) |
|
70 | 70 | self.sizes = self.field.sizes |
|
71 | 71 | |
|
72 | 72 | if self.sizes: |
|
73 | 73 | def get_size(self, size): |
|
74 | 74 | if not self: |
|
75 | 75 | return '' |
|
76 | 76 | else: |
|
77 | 77 | split = self.url.rsplit('.', 1) |
|
78 | 78 | thumb_url = '%s.%sx%s.%s' % (split[0], w, h, split[1]) |
|
79 | 79 | return thumb_url |
|
80 | 80 | |
|
81 | 81 | for size in self.sizes: |
|
82 | 82 | (w, h) = size |
|
83 | 83 | setattr(self, 'url_%sx%s' % (w, h), get_size(self, size)) |
|
84 | 84 | |
|
85 | 85 | def save(self, name, content, save=True): |
|
86 | 86 | super(ImageWithThumbsFieldFile, self).save(name, content, save) |
|
87 | 87 | |
|
88 | 88 | if self.sizes: |
|
89 | 89 | for size in self.sizes: |
|
90 | 90 | (w, h) = size |
|
91 | 91 | split = self.name.rsplit('.', 1) |
|
92 | 92 | thumb_name = '%s.%sx%s.%s' % (split[0], w, h, split[1]) |
|
93 | 93 | |
|
94 | 94 | # you can use another thumbnailing function if you like |
|
95 | 95 | thumb_content = generate_thumb(content, size, split[1]) |
|
96 | 96 | |
|
97 | 97 | thumb_name_ = self.storage.save(thumb_name, thumb_content) |
|
98 | 98 | |
|
99 | 99 | if not thumb_name == thumb_name_: |
|
100 | 100 | raise ValueError( |
|
101 | 101 | 'There is already a file named %s' % thumb_name) |
|
102 | 102 | |
|
103 | 103 | def delete(self, save=True): |
|
104 | 104 | name = self.name |
|
105 | 105 | super(ImageWithThumbsFieldFile, self).delete(save) |
|
106 | 106 | if self.sizes: |
|
107 | 107 | for size in self.sizes: |
|
108 | 108 | (w, h) = size |
|
109 | 109 | split = name.rsplit('.', 1) |
|
110 | 110 | thumb_name = '%s.%sx%s.%s' % (split[0], w, h, split[1]) |
|
111 | 111 | try: |
|
112 | 112 | self.storage.delete(thumb_name) |
|
113 | 113 | except: |
|
114 | 114 | pass |
|
115 | 115 | |
|
116 | 116 | |
|
117 | 117 | class ImageWithThumbsField(ImageField): |
|
118 | 118 | attr_class = ImageWithThumbsFieldFile |
|
119 | 119 | """ |
|
120 | 120 | Usage example: |
|
121 | 121 | ============== |
|
122 | 122 | photo = ImageWithThumbsField(upload_to='images', sizes=((125,125),(300,200),) |
|
123 | 123 | |
|
124 | 124 | To retrieve image URL, exactly the same way as with ImageField: |
|
125 | 125 | my_object.photo.url |
|
126 | 126 | To retrieve thumbnails URL's just add the size to it: |
|
127 | 127 | my_object.photo.url_125x125 |
|
128 | 128 | my_object.photo.url_300x200 |
|
129 | 129 | |
|
130 | 130 | Note: The 'sizes' attribute is not required. If you don't provide it, |
|
131 | 131 | ImageWithThumbsField will act as a normal ImageField |
|
132 | 132 | |
|
133 | 133 | How it works: |
|
134 | 134 | ============= |
|
135 | 135 | For each size in the 'sizes' atribute of the field it generates a |
|
136 | 136 | thumbnail with that size and stores it following this format: |
|
137 | 137 | |
|
138 | 138 | available_filename.[width]x[height].extension |
|
139 | 139 | |
|
140 | 140 | Where 'available_filename' is the available filename returned by the storage |
|
141 | 141 | backend for saving the original file. |
|
142 | 142 | |
|
143 | 143 | Following the usage example above: For storing a file called "photo.jpg" it saves: |
|
144 | 144 | photo.jpg (original file) |
|
145 | 145 | photo.125x125.jpg (first thumbnail) |
|
146 | 146 | photo.300x200.jpg (second thumbnail) |
|
147 | 147 | |
|
148 | 148 | With the default storage backend if photo.jpg already exists it will use these filenames: |
|
149 | 149 | photo_.jpg |
|
150 | 150 | photo_.125x125.jpg |
|
151 | 151 | photo_.300x200.jpg |
|
152 | 152 | |
|
153 | 153 | Note: django-thumbs assumes that if filename "any_filename.jpg" is available |
|
154 | 154 | filenames with this format "any_filename.[widht]x[height].jpg" will be available, too. |
|
155 | 155 | |
|
156 | 156 | To do: |
|
157 | 157 | ====== |
|
158 | 158 | Add method to regenerate thubmnails |
|
159 | 159 | |
|
160 | 160 | |
|
161 | 161 | """ |
|
162 | 162 | |
|
163 | 163 | preview_width_field = None |
|
164 | 164 | preview_height_field = None |
|
165 | 165 | |
|
166 | 166 | def __init__(self, verbose_name=None, name=None, width_field=None, |
|
167 | 167 | height_field=None, sizes=None, |
|
168 | 168 | preview_width_field=None, preview_height_field=None, |
|
169 | 169 | **kwargs): |
|
170 | 170 | self.verbose_name = verbose_name |
|
171 | 171 | self.name = name |
|
172 | 172 | self.width_field = width_field |
|
173 | 173 | self.height_field = height_field |
|
174 | 174 | self.sizes = sizes |
|
175 | 175 | super(ImageField, self).__init__(**kwargs) |
|
176 | 176 | |
|
177 | 177 | if sizes is not None and len(sizes) == 1: |
|
178 | 178 | self.preview_width_field = preview_width_field |
|
179 | 179 | self.preview_height_field = preview_height_field |
|
180 | 180 | |
|
181 | 181 | def update_dimension_fields(self, instance, force=False, *args, **kwargs): |
|
182 | 182 | """ |
|
183 | 183 | Update original image dimension fields and thumb dimension fields |
|
184 | 184 | (only if 1 thumb size is defined) |
|
185 | 185 | """ |
|
186 | 186 | |
|
187 | 187 | super(ImageWithThumbsField, self).update_dimension_fields(instance, |
|
188 | 188 | force, *args, |
|
189 | 189 | **kwargs) |
|
190 | 190 | thumb_width_field = self.preview_width_field |
|
191 | 191 | thumb_height_field = self.preview_height_field |
|
192 | 192 | |
|
193 | 193 | if thumb_width_field is None or thumb_height_field is None \ |
|
194 | 194 | or len(self.sizes) != 1: |
|
195 | 195 | return |
|
196 | 196 | |
|
197 | 197 | original_width = getattr(instance, self.width_field) |
|
198 | 198 | original_height = getattr(instance, self.height_field) |
|
199 | 199 | |
|
200 | 200 | if original_width > 0 and original_height > 0: |
|
201 | 201 | thumb_width, thumb_height = self.sizes[0] |
|
202 | 202 | |
|
203 | 203 | w_scale = float(thumb_width) / original_width |
|
204 | 204 | h_scale = float(thumb_height) / original_height |
|
205 | 205 | scale_ratio = min(w_scale, h_scale) |
|
206 | 206 | |
|
207 | 207 | if scale_ratio >= 1: |
|
208 | 208 | thumb_width_ratio = original_width |
|
209 | 209 | thumb_height_ratio = original_height |
|
210 | 210 | else: |
|
211 | 211 | thumb_width_ratio = int(original_width * scale_ratio) |
|
212 | 212 | thumb_height_ratio = int(original_height * scale_ratio) |
|
213 | 213 | |
|
214 | 214 | setattr(instance, thumb_width_field, thumb_width_ratio) |
|
215 |
setattr(instance, thumb_height_field, thumb_height_ratio) |
|
|
216 | ||
|
217 | ||
|
218 | from south.modelsinspector import add_introspection_rules | |
|
219 | add_introspection_rules([], ["^boards\.thumbs\.ImageWithThumbsField"]) | |
|
215 | setattr(instance, thumb_height_field, thumb_height_ratio) No newline at end of file |
@@ -1,87 +1,79 b'' | |||
|
1 | 1 | from django.conf.urls import patterns, url, include |
|
2 | 2 | from django.contrib import admin |
|
3 | 3 | from boards import views |
|
4 | 4 | from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed |
|
5 | 5 | from boards.views import api, tag_threads, all_threads, \ |
|
6 | 6 | settings, all_tags |
|
7 | 7 | from boards.views.authors import AuthorsView |
|
8 | from boards.views.delete_post import DeletePostView | |
|
9 | 8 | from boards.views.ban import BanUserView |
|
10 | 9 | from boards.views.search import BoardSearchView |
|
11 | 10 | from boards.views.static import StaticPageView |
|
12 | from boards.views.post_admin import PostAdminView | |
|
13 | 11 | from boards.views.preview import PostPreviewView |
|
14 | 12 | from boards.views.sync import get_post_sync_data |
|
15 | 13 | |
|
16 | 14 | js_info_dict = { |
|
17 | 15 | 'packages': ('boards',), |
|
18 | 16 | } |
|
19 | 17 | |
|
20 | 18 | urlpatterns = patterns('', |
|
21 | 19 | # /boards/ |
|
22 | 20 | url(r'^$', all_threads.AllThreadsView.as_view(), name='index'), |
|
23 | 21 | # /boards/page/ |
|
24 | 22 | url(r'^page/(?P<page>\w+)/$', all_threads.AllThreadsView.as_view(), |
|
25 | 23 | name='index'), |
|
26 | 24 | |
|
27 | 25 | # /boards/tag/tag_name/ |
|
28 | 26 | url(r'^tag/(?P<tag_name>\w+)/$', tag_threads.TagView.as_view(), |
|
29 | 27 | name='tag'), |
|
30 | 28 | # /boards/tag/tag_id/page/ |
|
31 | 29 | url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/$', |
|
32 | 30 | tag_threads.TagView.as_view(), name='tag'), |
|
33 | 31 | |
|
34 | 32 | # /boards/thread/ |
|
35 | 33 | url(r'^thread/(?P<post_id>\w+)/$', views.thread.ThreadView.as_view(), |
|
36 | 34 | name='thread'), |
|
37 | 35 | url(r'^thread/(?P<post_id>\w+)/mode/(?P<mode>\w+)/$', views.thread.ThreadView |
|
38 | 36 | .as_view(), name='thread_mode'), |
|
39 | 37 | |
|
40 | # /boards/post_admin/ | |
|
41 | url(r'^post_admin/(?P<post_id>\w+)/$', PostAdminView.as_view(), | |
|
42 | name='post_admin'), | |
|
43 | ||
|
44 | 38 | url(r'^settings/$', settings.SettingsView.as_view(), name='settings'), |
|
45 | 39 | url(r'^tags/$', all_tags.AllTagsView.as_view(), name='tags'), |
|
46 | 40 | url(r'^authors/$', AuthorsView.as_view(), name='authors'), |
|
47 | url(r'^delete/(?P<post_id>\w+)/$', DeletePostView.as_view(), | |
|
48 | name='delete'), | |
|
49 | 41 | url(r'^ban/(?P<post_id>\w+)/$', BanUserView.as_view(), name='ban'), |
|
50 | 42 | |
|
51 | 43 | url(r'^banned/$', views.banned.BannedView.as_view(), name='banned'), |
|
52 | 44 | url(r'^staticpage/(?P<name>\w+)/$', StaticPageView.as_view(), |
|
53 | 45 | name='staticpage'), |
|
54 | 46 | |
|
55 | 47 | # RSS feeds |
|
56 | 48 | url(r'^rss/$', AllThreadsFeed()), |
|
57 | 49 | url(r'^page/(?P<page>\w+)/rss/$', AllThreadsFeed()), |
|
58 | 50 | url(r'^tag/(?P<tag_name>\w+)/rss/$', TagThreadsFeed()), |
|
59 | 51 | url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/rss/$', TagThreadsFeed()), |
|
60 | 52 | url(r'^thread/(?P<post_id>\w+)/rss/$', ThreadPostsFeed()), |
|
61 | 53 | |
|
62 | 54 | # i18n |
|
63 | 55 | url(r'^jsi18n/$', 'boards.views.cached_js_catalog', js_info_dict, |
|
64 | 56 | name='js_info_dict'), |
|
65 | 57 | |
|
66 | 58 | # API |
|
67 | 59 | url(r'^api/post/(?P<post_id>\w+)/$', api.get_post, name="get_post"), |
|
68 | 60 | url(r'^api/diff_thread/(?P<thread_id>\w+)/(?P<last_update_time>\w+)/$', |
|
69 | 61 | api.api_get_threaddiff, name="get_thread_diff"), |
|
70 | 62 | url(r'^api/threads/(?P<count>\w+)/$', api.api_get_threads, |
|
71 | 63 | name='get_threads'), |
|
72 | 64 | url(r'^api/tags/$', api.api_get_tags, name='get_tags'), |
|
73 | 65 | url(r'^api/thread/(?P<opening_post_id>\w+)/$', api.api_get_thread_posts, |
|
74 | 66 | name='get_thread'), |
|
75 | 67 | url(r'^api/add_post/(?P<opening_post_id>\w+)/$', api.api_add_post, |
|
76 | 68 | name='add_post'), |
|
77 | 69 | |
|
78 | 70 | # Search |
|
79 | 71 | url(r'^search/$', BoardSearchView.as_view(), name='search'), |
|
80 | 72 | |
|
81 | 73 | # Post preview |
|
82 | 74 | url(r'^preview/$', PostPreviewView.as_view(), name='preview'), |
|
83 | 75 | |
|
84 | 76 | url(r'^post_xml/(?P<post_id>\d+)$', get_post_sync_data, |
|
85 | 77 | name='post_sync_data'), |
|
86 | 78 | |
|
87 | 79 | ) |
@@ -1,78 +1,43 b'' | |||
|
1 | 1 | """ |
|
2 | 2 | This module contains helper functions and helper classes. |
|
3 | 3 | """ |
|
4 | import hashlib | |
|
5 | 4 | import time |
|
5 | import hmac | |
|
6 | 6 | |
|
7 | 7 | from django.utils import timezone |
|
8 | 8 | |
|
9 | 9 | from neboard import settings |
|
10 | 10 | |
|
11 | 11 | |
|
12 | 12 | KEY_CAPTCHA_FAILS = 'key_captcha_fails' |
|
13 | 13 | KEY_CAPTCHA_DELAY_TIME = 'key_captcha_delay_time' |
|
14 | 14 | KEY_CAPTCHA_LAST_ACTIVITY = 'key_captcha_last_activity' |
|
15 | 15 | |
|
16 | 16 | |
|
17 | def need_include_captcha(request): | |
|
18 | """ | |
|
19 | Check if request is made by a user. | |
|
20 | It contains rules which check for bots. | |
|
21 | """ | |
|
22 | ||
|
23 | if not settings.ENABLE_CAPTCHA: | |
|
24 | return False | |
|
25 | ||
|
26 | enable_captcha = False | |
|
27 | ||
|
28 | #newcomer | |
|
29 | if KEY_CAPTCHA_LAST_ACTIVITY not in request.session: | |
|
30 | return settings.ENABLE_CAPTCHA | |
|
31 | ||
|
32 | last_activity = request.session[KEY_CAPTCHA_LAST_ACTIVITY] | |
|
33 | current_delay = int(time.time()) - last_activity | |
|
34 | ||
|
35 | delay_time = (request.session[KEY_CAPTCHA_DELAY_TIME] | |
|
36 | if KEY_CAPTCHA_DELAY_TIME in request.session | |
|
37 | else settings.CAPTCHA_DEFAULT_SAFE_TIME) | |
|
38 | ||
|
39 | if current_delay < delay_time: | |
|
40 | enable_captcha = True | |
|
41 | ||
|
42 | return enable_captcha | |
|
43 | ||
|
44 | ||
|
45 | def update_captcha_access(request, passed): | |
|
46 | """ | |
|
47 | Update captcha fields. | |
|
48 | It will reduce delay time if user passed captcha verification and | |
|
49 | it will increase it otherwise. | |
|
50 | """ | |
|
51 | session = request.session | |
|
52 | ||
|
53 | delay_time = (request.session[KEY_CAPTCHA_DELAY_TIME] | |
|
54 | if KEY_CAPTCHA_DELAY_TIME in request.session | |
|
55 | else settings.CAPTCHA_DEFAULT_SAFE_TIME) | |
|
56 | ||
|
57 | if passed: | |
|
58 | delay_time -= 2 if delay_time >= 7 else 5 | |
|
59 | else: | |
|
60 | delay_time += 10 | |
|
61 | ||
|
62 | session[KEY_CAPTCHA_LAST_ACTIVITY] = int(time.time()) | |
|
63 | session[KEY_CAPTCHA_DELAY_TIME] = delay_time | |
|
64 | ||
|
65 | ||
|
66 | 17 | def get_client_ip(request): |
|
67 | 18 | x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') |
|
68 | 19 | if x_forwarded_for: |
|
69 | 20 | ip = x_forwarded_for.split(',')[-1].strip() |
|
70 | 21 | else: |
|
71 | 22 | ip = request.META.get('REMOTE_ADDR') |
|
72 | 23 | return ip |
|
73 | 24 | |
|
74 | 25 | |
|
75 | 26 | def datetime_to_epoch(datetime): |
|
76 | 27 | return int(time.mktime(timezone.localtime( |
|
77 | 28 | datetime,timezone.get_current_timezone()).timetuple()) |
|
78 | 29 | * 1000000 + datetime.microsecond) |
|
30 | ||
|
31 | ||
|
32 | def get_websocket_token(user_id='', timestamp=''): | |
|
33 | """ | |
|
34 | Create token to validate information provided by new connection. | |
|
35 | """ | |
|
36 | ||
|
37 | sign = hmac.new(settings.CENTRIFUGE_PROJECT_SECRET.encode()) | |
|
38 | sign.update(settings.CENTRIFUGE_PROJECT_ID.encode()) | |
|
39 | sign.update(user_id.encode()) | |
|
40 | sign.update(timestamp.encode()) | |
|
41 | token = sign.hexdigest() | |
|
42 | ||
|
43 | return token No newline at end of file |
@@ -1,13 +1,14 b'' | |||
|
1 | 1 | from django.shortcuts import render |
|
2 | 2 | |
|
3 | 3 | from boards.views.base import BaseBoardView |
|
4 | 4 | from boards.models.tag import Tag |
|
5 | 5 | |
|
6 | 6 | |
|
7 | 7 | class AllTagsView(BaseBoardView): |
|
8 | 8 | |
|
9 | 9 | def get(self, request): |
|
10 | context = self.get_context_data(request=request) | |
|
11 | context['all_tags'] = Tag.objects.get_not_empty_tags() | |
|
10 | params = dict() | |
|
12 | 11 | |
|
13 | return render(request, 'boards/tags.html', context) | |
|
12 | params['all_tags'] = Tag.objects.get_not_empty_tags() | |
|
13 | ||
|
14 | return render(request, 'boards/tags.html', params) |
@@ -1,139 +1,137 b'' | |||
|
1 | import string | |
|
2 | ||
|
3 | 1 |
|
|
4 | 2 | from django.shortcuts import render, redirect |
|
5 | 3 | |
|
6 | 4 | from boards import utils, settings |
|
7 | 5 | from boards.abstracts.paginator import get_paginator |
|
8 | 6 | from boards.abstracts.settingsmanager import get_settings_manager |
|
9 | 7 | from boards.forms import ThreadForm, PlainErrorList |
|
10 | 8 | from boards.models import Post, Thread, Ban, Tag |
|
11 | 9 | from boards.views.banned import BannedView |
|
12 | 10 | from boards.views.base import BaseBoardView, CONTEXT_FORM |
|
13 | 11 | from boards.views.posting_mixin import PostMixin |
|
14 | 12 | |
|
13 | ||
|
15 | 14 | FORM_TAGS = 'tags' |
|
16 | 15 | FORM_TEXT = 'text' |
|
17 | 16 | FORM_TITLE = 'title' |
|
18 | 17 | FORM_IMAGE = 'image' |
|
19 | 18 | |
|
20 | 19 | TAG_DELIMITER = ' ' |
|
21 | 20 | |
|
22 | 21 | PARAMETER_CURRENT_PAGE = 'current_page' |
|
23 | 22 | PARAMETER_PAGINATOR = 'paginator' |
|
24 | 23 | PARAMETER_THREADS = 'threads' |
|
25 | 24 | |
|
26 | 25 | TEMPLATE = 'boards/posting_general.html' |
|
27 | 26 | DEFAULT_PAGE = 1 |
|
28 | 27 | |
|
29 | 28 | |
|
30 | 29 | class AllThreadsView(PostMixin, BaseBoardView): |
|
31 | 30 | |
|
32 | 31 | def __init__(self): |
|
33 | 32 | self.settings_manager = None |
|
34 | 33 | super(AllThreadsView, self).__init__() |
|
35 | 34 | |
|
36 | 35 | def get(self, request, page=DEFAULT_PAGE, form=None): |
|
37 |
|
|
|
36 | params = self.get_context_data(request=request) | |
|
38 | 37 | |
|
39 | 38 | if not form: |
|
40 | 39 | form = ThreadForm(error_class=PlainErrorList) |
|
41 | 40 | |
|
42 | 41 | self.settings_manager = get_settings_manager(request) |
|
43 | 42 | paginator = get_paginator(self.get_threads(), |
|
44 | 43 | settings.THREADS_PER_PAGE) |
|
45 | 44 | paginator.current_page = int(page) |
|
46 | 45 | |
|
47 | 46 | threads = paginator.page(page).object_list |
|
48 | 47 | |
|
49 |
|
|
|
50 |
|
|
|
48 | params[PARAMETER_THREADS] = threads | |
|
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 | 55 | def post(self, request, page=DEFAULT_PAGE): |
|
57 | 56 | form = ThreadForm(request.POST, request.FILES, |
|
58 | 57 | error_class=PlainErrorList) |
|
59 | 58 | form.session = request.session |
|
60 | 59 | |
|
61 | 60 | if form.is_valid(): |
|
62 | 61 | return self.create_thread(request, form) |
|
63 | 62 | if form.need_to_ban: |
|
64 | 63 | # Ban user because he is suspected to be a bot |
|
65 | 64 | self._ban_current_user(request) |
|
66 | 65 | |
|
67 | 66 | return self.get(request, page, form) |
|
68 | 67 | |
|
69 | @staticmethod | |
|
70 | def _get_page_context(paginator, context, page): | |
|
68 | def _get_page_context(self, paginator, params, page): | |
|
71 | 69 | """ |
|
72 | 70 | Get pagination context variables |
|
73 | 71 | """ |
|
74 | 72 | |
|
75 |
|
|
|
76 |
|
|
|
73 | params[PARAMETER_PAGINATOR] = paginator | |
|
74 | params[PARAMETER_CURRENT_PAGE] = paginator.page(int(page)) | |
|
77 | 75 | |
|
78 | 76 | @staticmethod |
|
79 | 77 | def parse_tags_string(tag_strings): |
|
80 | 78 | """ |
|
81 | 79 | Parses tag list string and returns tag object list. |
|
82 | 80 | """ |
|
83 | 81 | |
|
84 | 82 | tags = [] |
|
85 | 83 | |
|
86 | 84 | if tag_strings: |
|
87 | 85 | tag_strings = tag_strings.split(TAG_DELIMITER) |
|
88 | 86 | for tag_name in tag_strings: |
|
89 | 87 | tag_name = tag_name.strip().lower() |
|
90 | 88 | if len(tag_name) > 0: |
|
91 | 89 | tag, created = Tag.objects.get_or_create(name=tag_name) |
|
92 | 90 | tags.append(tag) |
|
93 | 91 | |
|
94 | 92 | return tags |
|
95 | 93 | |
|
96 | 94 | @transaction.atomic |
|
97 | 95 | def create_thread(self, request, form, html_response=True): |
|
98 | 96 | """ |
|
99 | 97 | Creates a new thread with an opening post. |
|
100 | 98 | """ |
|
101 | 99 | |
|
102 | 100 | ip = utils.get_client_ip(request) |
|
103 | 101 | is_banned = Ban.objects.filter(ip=ip).exists() |
|
104 | 102 | |
|
105 | 103 | if is_banned: |
|
106 | 104 | if html_response: |
|
107 | 105 | return redirect(BannedView().as_view()) |
|
108 | 106 | else: |
|
109 | 107 | return |
|
110 | 108 | |
|
111 | 109 | data = form.cleaned_data |
|
112 | 110 | |
|
113 | 111 | title = data[FORM_TITLE] |
|
114 | 112 | text = data[FORM_TEXT] |
|
113 | image = data.get(FORM_IMAGE) | |
|
115 | 114 | |
|
116 | 115 | text = self._remove_invalid_links(text) |
|
117 | 116 | |
|
118 | if FORM_IMAGE in list(data.keys()): | |
|
119 | image = data[FORM_IMAGE] | |
|
120 | else: | |
|
121 | image = None | |
|
122 | ||
|
123 | 117 | tag_strings = data[FORM_TAGS] |
|
124 | 118 | |
|
125 | 119 | tags = self.parse_tags_string(tag_strings) |
|
126 | 120 | |
|
127 | 121 | post = Post.objects.create_post(title=title, text=text, image=image, |
|
128 | 122 | ip=ip, tags=tags) |
|
129 | 123 | |
|
124 | # This is required to update the threads to which posts we have replied | |
|
125 | # when creating this one | |
|
126 | post.send_to_websocket(request) | |
|
127 | ||
|
130 | 128 | if html_response: |
|
131 | 129 | return redirect(post.get_url()) |
|
132 | 130 | |
|
133 | 131 | def get_threads(self): |
|
134 | 132 | """ |
|
135 | 133 | Gets list of threads that will be shown on a page. |
|
136 | 134 | """ |
|
137 | 135 | |
|
138 | 136 | return Thread.objects.all().order_by('-bump_time')\ |
|
139 | 137 | .exclude(tags__in=self.settings_manager.get_hidden_tags()) |
@@ -1,248 +1,223 b'' | |||
|
1 | 1 | from datetime import datetime |
|
2 | 2 | import json |
|
3 | 3 | import logging |
|
4 | 4 | from django.db import transaction |
|
5 | 5 | from django.http import HttpResponse |
|
6 | 6 | from django.shortcuts import get_object_or_404, render |
|
7 | 7 | from django.template import RequestContext |
|
8 | 8 | from django.utils import timezone |
|
9 | 9 | from django.core import serializers |
|
10 | from django.template.loader import render_to_string | |
|
11 | 10 | |
|
12 | 11 | from boards.forms import PostForm, PlainErrorList |
|
13 | 12 | from boards.models import Post, Thread, Tag |
|
14 | 13 | from boards.utils import datetime_to_epoch |
|
15 | 14 | from boards.views.thread import ThreadView |
|
16 | 15 | |
|
17 | 16 | __author__ = 'neko259' |
|
18 | 17 | |
|
19 | 18 | PARAMETER_TRUNCATED = 'truncated' |
|
20 | 19 | PARAMETER_TAG = 'tag' |
|
21 | 20 | PARAMETER_OFFSET = 'offset' |
|
22 | 21 | PARAMETER_DIFF_TYPE = 'type' |
|
23 | 22 | |
|
24 | 23 | DIFF_TYPE_HTML = 'html' |
|
25 | 24 | DIFF_TYPE_JSON = 'json' |
|
26 | 25 | |
|
27 | 26 | STATUS_OK = 'ok' |
|
28 | 27 | STATUS_ERROR = 'error' |
|
29 | 28 | |
|
30 | 29 | logger = logging.getLogger(__name__) |
|
31 | 30 | |
|
32 | 31 | |
|
33 | 32 | @transaction.atomic |
|
34 | 33 | def api_get_threaddiff(request, thread_id, last_update_time): |
|
35 | 34 | """ |
|
36 | 35 | Gets posts that were changed or added since time |
|
37 | 36 | """ |
|
38 | 37 | |
|
39 | 38 | thread = get_object_or_404(Post, id=thread_id).get_thread() |
|
40 | 39 | |
|
41 | 40 | # Add 1 to ensure we don't load the same post over and over |
|
42 | 41 | last_update_timestamp = float(last_update_time) + 1 |
|
43 | 42 | |
|
44 | 43 | filter_time = datetime.fromtimestamp(last_update_timestamp / 1000000, |
|
45 | 44 | timezone.get_current_timezone()) |
|
46 | 45 | |
|
47 | 46 | json_data = { |
|
48 | 47 | 'added': [], |
|
49 | 48 | 'updated': [], |
|
50 | 49 | 'last_update': None, |
|
51 | 50 | } |
|
52 | 51 | added_posts = Post.objects.filter(thread_new=thread, |
|
53 | 52 | pub_time__gt=filter_time) \ |
|
54 | 53 | .order_by('pub_time') |
|
55 | 54 | updated_posts = Post.objects.filter(thread_new=thread, |
|
56 | 55 | pub_time__lte=filter_time, |
|
57 | 56 | last_edit_time__gt=filter_time) |
|
58 | 57 | |
|
59 | diff_type = DIFF_TYPE_HTML | |
|
60 | if PARAMETER_DIFF_TYPE in request.GET: | |
|
61 | diff_type = request.GET[PARAMETER_DIFF_TYPE] | |
|
58 | diff_type = request.GET.get(PARAMETER_DIFF_TYPE, DIFF_TYPE_HTML) | |
|
62 | 59 | |
|
63 | 60 | for post in added_posts: |
|
64 |
json_data['added'].append( |
|
|
61 | json_data['added'].append(get_post_data(post.id, diff_type, request)) | |
|
65 | 62 | for post in updated_posts: |
|
66 |
json_data['updated'].append( |
|
|
63 | json_data['updated'].append(get_post_data(post.id, diff_type, request)) | |
|
67 | 64 | json_data['last_update'] = datetime_to_epoch(thread.last_edit_time) |
|
68 | 65 | |
|
69 | 66 | return HttpResponse(content=json.dumps(json_data)) |
|
70 | 67 | |
|
71 | 68 | |
|
72 | 69 | def api_add_post(request, opening_post_id): |
|
73 | 70 | """ |
|
74 | 71 | Adds a post and return the JSON response for it |
|
75 | 72 | """ |
|
76 | 73 | |
|
77 | 74 | opening_post = get_object_or_404(Post, id=opening_post_id) |
|
78 | 75 | |
|
79 | 76 | logger.info('Adding post via api...') |
|
80 | 77 | |
|
81 | 78 | status = STATUS_OK |
|
82 | 79 | errors = [] |
|
83 | 80 | |
|
84 | 81 | if request.method == 'POST': |
|
85 | 82 | form = PostForm(request.POST, request.FILES, error_class=PlainErrorList) |
|
86 | 83 | form.session = request.session |
|
87 | 84 | |
|
88 | 85 | if form.need_to_ban: |
|
89 | 86 | # Ban user because he is suspected to be a bot |
|
90 | 87 | # _ban_current_user(request) |
|
91 | 88 | status = STATUS_ERROR |
|
92 | 89 | if form.is_valid(): |
|
93 | 90 | post = ThreadView().new_post(request, form, opening_post, |
|
94 | 91 | html_response=False) |
|
95 | 92 | if not post: |
|
96 | 93 | status = STATUS_ERROR |
|
97 | 94 | else: |
|
98 | 95 | logger.info('Added post #%d via api.' % post.id) |
|
99 | 96 | else: |
|
100 | 97 | status = STATUS_ERROR |
|
101 | 98 | errors = form.as_json_errors() |
|
102 | 99 | |
|
103 | 100 | response = { |
|
104 | 101 | 'status': status, |
|
105 | 102 | 'errors': errors, |
|
106 | 103 | } |
|
107 | 104 | |
|
108 | 105 | return HttpResponse(content=json.dumps(response)) |
|
109 | 106 | |
|
110 | 107 | |
|
111 | 108 | def get_post(request, post_id): |
|
112 | 109 | """ |
|
113 | 110 | Gets the html of a post. Used for popups. Post can be truncated if used |
|
114 | 111 | in threads list with 'truncated' get parameter. |
|
115 | 112 | """ |
|
116 | 113 | |
|
117 | logger.info('Getting post #%s' % post_id) | |
|
118 | ||
|
119 | 114 | post = get_object_or_404(Post, id=post_id) |
|
120 | 115 | |
|
121 | 116 | context = RequestContext(request) |
|
122 | 117 | context['post'] = post |
|
123 | 118 | if PARAMETER_TRUNCATED in request.GET: |
|
124 | 119 | context[PARAMETER_TRUNCATED] = True |
|
125 | 120 | |
|
126 | return render(request, 'boards/api_post.html', context) | |
|
121 | # TODO Use dict here | |
|
122 | return render(request, 'boards/api_post.html', context_instance=context) | |
|
127 | 123 | |
|
128 | 124 | |
|
129 | 125 | # TODO Test this |
|
130 | 126 | def api_get_threads(request, count): |
|
131 | 127 | """ |
|
132 | 128 | Gets the JSON thread opening posts list. |
|
133 | 129 | Parameters that can be used for filtering: |
|
134 | 130 | tag, offset (from which thread to get results) |
|
135 | 131 | """ |
|
136 | 132 | |
|
137 | 133 | if PARAMETER_TAG in request.GET: |
|
138 | 134 | tag_name = request.GET[PARAMETER_TAG] |
|
139 | 135 | if tag_name is not None: |
|
140 | 136 | tag = get_object_or_404(Tag, name=tag_name) |
|
141 | threads = tag.threads.filter(archived=False) | |
|
137 | threads = tag.get_threads().filter(archived=False) | |
|
142 | 138 | else: |
|
143 | 139 | threads = Thread.objects.filter(archived=False) |
|
144 | 140 | |
|
145 | 141 | if PARAMETER_OFFSET in request.GET: |
|
146 | 142 | offset = request.GET[PARAMETER_OFFSET] |
|
147 | 143 | offset = int(offset) if offset is not None else 0 |
|
148 | 144 | else: |
|
149 | 145 | offset = 0 |
|
150 | 146 | |
|
151 | 147 | threads = threads.order_by('-bump_time') |
|
152 | 148 | threads = threads[offset:offset + int(count)] |
|
153 | 149 | |
|
154 | 150 | opening_posts = [] |
|
155 | 151 | for thread in threads: |
|
156 | 152 | opening_post = thread.get_opening_post() |
|
157 | 153 | |
|
158 | 154 | # TODO Add tags, replies and images count |
|
159 |
opening_posts.append( |
|
|
155 | opening_posts.append(get_post_data(opening_post.id, | |
|
160 | 156 | include_last_update=True)) |
|
161 | 157 | |
|
162 | 158 | return HttpResponse(content=json.dumps(opening_posts)) |
|
163 | 159 | |
|
164 | 160 | |
|
165 | 161 | # TODO Test this |
|
166 | 162 | def api_get_tags(request): |
|
167 | 163 | """ |
|
168 | 164 | Gets all tags or user tags. |
|
169 | 165 | """ |
|
170 | 166 | |
|
171 | 167 | # TODO Get favorite tags for the given user ID |
|
172 | 168 | |
|
173 | 169 | tags = Tag.objects.get_not_empty_tags() |
|
174 | 170 | tag_names = [] |
|
175 | 171 | for tag in tags: |
|
176 | 172 | tag_names.append(tag.name) |
|
177 | 173 | |
|
178 | 174 | return HttpResponse(content=json.dumps(tag_names)) |
|
179 | 175 | |
|
180 | 176 | |
|
181 | 177 | # TODO The result can be cached by the thread last update time |
|
182 | 178 | # TODO Test this |
|
183 | 179 | def api_get_thread_posts(request, opening_post_id): |
|
184 | 180 | """ |
|
185 | 181 | Gets the JSON array of thread posts |
|
186 | 182 | """ |
|
187 | 183 | |
|
188 | 184 | opening_post = get_object_or_404(Post, id=opening_post_id) |
|
189 | 185 | thread = opening_post.get_thread() |
|
190 | 186 | posts = thread.get_replies() |
|
191 | 187 | |
|
192 | 188 | json_data = { |
|
193 | 189 | 'posts': [], |
|
194 | 190 | 'last_update': None, |
|
195 | 191 | } |
|
196 | 192 | json_post_list = [] |
|
197 | 193 | |
|
198 | 194 | for post in posts: |
|
199 |
json_post_list.append( |
|
|
195 | json_post_list.append(get_post_data(post.id)) | |
|
200 | 196 | json_data['last_update'] = datetime_to_epoch(thread.last_edit_time) |
|
201 | 197 | json_data['posts'] = json_post_list |
|
202 | 198 | |
|
203 | 199 | return HttpResponse(content=json.dumps(json_data)) |
|
204 | 200 | |
|
205 | 201 | |
|
206 | 202 | def api_get_post(request, post_id): |
|
207 | 203 | """ |
|
208 | 204 | Gets the JSON of a post. This can be |
|
209 | 205 | used as and API for external clients. |
|
210 | 206 | """ |
|
211 | 207 | |
|
212 | 208 | post = get_object_or_404(Post, id=post_id) |
|
213 | 209 | |
|
214 | 210 | json = serializers.serialize("json", [post], fields=( |
|
215 | 211 | "pub_time", "_text_rendered", "title", "text", "image", |
|
216 | 212 | "image_width", "image_height", "replies", "tags" |
|
217 | 213 | )) |
|
218 | 214 | |
|
219 | 215 | return HttpResponse(content=json) |
|
220 | 216 | |
|
221 | 217 | |
|
222 | # TODO Add pub time and replies | |
|
223 |
def |
|
|
224 | include_last_update=False): | |
|
225 | if format_type == DIFF_TYPE_HTML: | |
|
226 | post = get_object_or_404(Post, id=post_id) | |
|
227 | ||
|
228 | context = RequestContext(request) | |
|
229 | context['post'] = post | |
|
230 | if PARAMETER_TRUNCATED in request.GET: | |
|
231 | context[PARAMETER_TRUNCATED] = True | |
|
232 | ||
|
233 | return render_to_string('boards/api_post.html', context) | |
|
234 | elif format_type == DIFF_TYPE_JSON: | |
|
235 | post = get_object_or_404(Post, id=post_id) | |
|
236 | post_json = { | |
|
237 | 'id': post.id, | |
|
238 | 'title': post.title, | |
|
239 | 'text': post.text.rendered, | |
|
240 | } | |
|
241 | if post.images.exists(): | |
|
242 | post_image = post.get_first_image() | |
|
243 | post_json['image'] = post_image.image.url | |
|
244 | post_json['image_preview'] = post_image.image.url_200x150 | |
|
245 | if include_last_update: | |
|
246 | post_json['bump_time'] = datetime_to_epoch( | |
|
247 | post.thread_new.bump_time) | |
|
248 | return post_json | |
|
218 | # TODO Remove this method and use post method directly | |
|
219 | def get_post_data(post_id, format_type=DIFF_TYPE_JSON, request=None, | |
|
220 | include_last_update=False): | |
|
221 | post = get_object_or_404(Post, id=post_id) | |
|
222 | return post.get_post_data(format_type=format_type, request=request, | |
|
223 | include_last_update=include_last_update) |
@@ -1,13 +1,13 b'' | |||
|
1 | 1 | from django.shortcuts import render |
|
2 | 2 | |
|
3 | 3 | from boards.authors import authors |
|
4 | 4 | from boards.views.base import BaseBoardView |
|
5 | 5 | |
|
6 | 6 | |
|
7 | 7 | class AuthorsView(BaseBoardView): |
|
8 | 8 | |
|
9 | 9 | def get(self, request): |
|
10 | context = self.get_context_data(request=request) | |
|
11 |
|
|
|
10 | params = dict() | |
|
11 | params['authors'] = authors | |
|
12 | 12 | |
|
13 |
return render(request, 'boards/authors.html', |
|
|
13 | return render(request, 'boards/authors.html', params) |
@@ -1,16 +1,17 b'' | |||
|
1 | 1 | from django.shortcuts import get_object_or_404, render |
|
2 | 2 | from boards import utils |
|
3 | 3 | from boards.models import Ban |
|
4 | 4 | from boards.views.base import BaseBoardView |
|
5 | 5 | |
|
6 | 6 | |
|
7 | 7 | class BannedView(BaseBoardView): |
|
8 | 8 | |
|
9 | 9 | def get(self, request): |
|
10 | 10 | """Show the page that notifies that user is banned""" |
|
11 | 11 | |
|
12 | context = self.get_context_data(request=request) | |
|
12 | params = dict() | |
|
13 | 13 | |
|
14 | 14 | ban = get_object_or_404(Ban, ip=utils.get_client_ip(request)) |
|
15 |
|
|
|
16 | return render(request, 'boards/staticpages/banned.html', context) | |
|
15 | params['ban_reason'] = ban.reason | |
|
16 | ||
|
17 | return render(request, 'boards/staticpages/banned.html', params) |
@@ -1,35 +1,31 b'' | |||
|
1 | 1 | from django.db import transaction |
|
2 | 2 | from django.template import RequestContext |
|
3 | 3 | from django.views.generic import View |
|
4 | 4 | |
|
5 | 5 | from boards import utils |
|
6 | 6 | from boards.models.user import Ban |
|
7 | 7 | |
|
8 | 8 | |
|
9 | 9 | BAN_REASON_SPAM = 'Autoban: spam bot' |
|
10 | 10 | |
|
11 | 11 | CONTEXT_FORM = 'form' |
|
12 | 12 | |
|
13 | 13 | |
|
14 | 14 | class BaseBoardView(View): |
|
15 | 15 | |
|
16 | 16 | def get_context_data(self, **kwargs): |
|
17 | request = kwargs['request'] | |
|
18 | # context = self._default_context(request) | |
|
19 | context = RequestContext(request) | |
|
20 | ||
|
21 | return context | |
|
17 | return dict() | |
|
22 | 18 | |
|
23 | 19 | @transaction.atomic |
|
24 | 20 | def _ban_current_user(self, request): |
|
25 | 21 | """ |
|
26 | 22 | Add current user to the IP ban list |
|
27 | 23 | """ |
|
28 | 24 | |
|
29 | 25 | ip = utils.get_client_ip(request) |
|
30 | 26 | ban, created = Ban.objects.get_or_create(ip=ip) |
|
31 | 27 | if created: |
|
32 | 28 | ban.can_read = False |
|
33 | 29 | ban.reason = BAN_REASON_SPAM |
|
34 | 30 | ban.save() |
|
35 | 31 |
@@ -1,39 +1,40 b'' | |||
|
1 | PARAM_NEXT = 'next' | |
|
1 | 2 | PARAMETER_METHOD = 'method' |
|
2 | 3 | |
|
3 | 4 | from django.shortcuts import redirect |
|
4 | 5 | from django.http import HttpResponseRedirect |
|
5 | 6 | |
|
6 | 7 | |
|
7 | 8 | class RedirectNextMixin: |
|
8 | 9 | |
|
9 | 10 | def redirect_to_next(self, request): |
|
10 | 11 | """ |
|
11 | 12 | If a 'next' parameter was specified, redirect to the next page. This |
|
12 | 13 | is used when the user is required to return to some page after the |
|
13 | 14 | current view has finished its work. |
|
14 | 15 | """ |
|
15 | 16 | |
|
16 |
if |
|
|
17 |
next_page = request.GET[ |
|
|
17 | if PARAM_NEXT in request.GET: | |
|
18 | next_page = request.GET[PARAM_NEXT] | |
|
18 | 19 | return HttpResponseRedirect(next_page) |
|
19 | 20 | else: |
|
20 | 21 | return redirect('index') |
|
21 | 22 | |
|
22 | 23 | |
|
23 | 24 | class DispatcherMixin: |
|
24 | 25 | """ |
|
25 | 26 | This class contains a dispather method that can run a method specified by |
|
26 | 27 | 'method' request parameter. |
|
27 | 28 | """ |
|
28 | 29 | |
|
29 | 30 | def dispatch_method(self, *args, **kwargs): |
|
30 | 31 | request = args[0] |
|
31 | 32 | |
|
32 | 33 | method_name = None |
|
33 | 34 | if PARAMETER_METHOD in request.GET: |
|
34 | 35 | method_name = request.GET[PARAMETER_METHOD] |
|
35 | 36 | elif PARAMETER_METHOD in request.POST: |
|
36 | 37 | method_name = request.POST[PARAMETER_METHOD] |
|
37 | 38 | |
|
38 | 39 | if method_name: |
|
39 | 40 | return getattr(self, method_name)(*args, **kwargs) |
@@ -1,13 +1,17 b'' | |||
|
1 | 1 | from django.shortcuts import render |
|
2 | 2 | |
|
3 | 3 | from boards.views.base import BaseBoardView |
|
4 | 4 | |
|
5 | 5 | |
|
6 | 6 | class NotFoundView(BaseBoardView): |
|
7 | 7 | """ |
|
8 | 8 | Page 404 (not found) |
|
9 | 9 | """ |
|
10 | 10 | |
|
11 | 11 | def get(self, request): |
|
12 |
|
|
|
13 | return render(request, 'boards/404.html', context) | |
|
12 | params = self.get_context_data() | |
|
13 | ||
|
14 | response = render(request, 'boards/404.html', params) | |
|
15 | response.status_code = 404 | |
|
16 | ||
|
17 | return response |
@@ -1,35 +1,37 b'' | |||
|
1 | 1 | from django.shortcuts import render |
|
2 | 2 | from django.template import RequestContext |
|
3 | 3 | from django.views.generic import View |
|
4 | 4 | |
|
5 | 5 | from boards.mdx_neboard import bbcode_extended |
|
6 | 6 | |
|
7 | 7 | FORM_QUERY = 'query' |
|
8 | 8 | |
|
9 | 9 | CONTEXT_RESULT = 'result' |
|
10 | 10 | CONTEXT_QUERY = 'query' |
|
11 | 11 | |
|
12 | 12 | __author__ = 'neko259' |
|
13 | 13 | |
|
14 | 14 | TEMPLATE = 'boards/preview.html' |
|
15 | 15 | |
|
16 | 16 | |
|
17 | 17 | class PostPreviewView(View): |
|
18 | 18 | def get(self, request): |
|
19 | 19 | context = RequestContext(request) |
|
20 | 20 | |
|
21 | return render(request, TEMPLATE, context) | |
|
21 | # TODO Use dict here | |
|
22 | return render(request, TEMPLATE, context_instance=context) | |
|
22 | 23 | |
|
23 | 24 | def post(self, request): |
|
24 | 25 | context = RequestContext(request) |
|
25 | 26 | |
|
26 | 27 | if FORM_QUERY in request.POST: |
|
27 | 28 | raw_text = request.POST[FORM_QUERY] |
|
28 | 29 | |
|
29 | 30 | if len(raw_text) >= 0: |
|
30 | 31 | rendered_text = bbcode_extended(raw_text) |
|
31 | 32 | |
|
32 | 33 | context[CONTEXT_RESULT] = rendered_text |
|
33 | 34 | context[CONTEXT_QUERY] = raw_text |
|
34 | 35 | |
|
35 | return render(request, TEMPLATE, context) | |
|
36 | # TODO Use dict here | |
|
37 | return render(request, TEMPLATE, context_instance=context) |
@@ -1,40 +1,43 b'' | |||
|
1 | 1 | from django.shortcuts import render |
|
2 | from django.template import RequestContext | |
|
3 | 2 | from django.views.generic import View |
|
4 | 3 | from haystack.query import SearchQuerySet |
|
4 | ||
|
5 | 5 | from boards.abstracts.paginator import get_paginator |
|
6 | 6 | from boards.forms import SearchForm, PlainErrorList |
|
7 | 7 | |
|
8 | ||
|
9 | MIN_QUERY_LENGTH = 3 | |
|
10 | RESULTS_PER_PAGE = 10 | |
|
11 | ||
|
8 | 12 | FORM_QUERY = 'query' |
|
9 | 13 | |
|
10 | 14 | CONTEXT_QUERY = 'query' |
|
11 | 15 | CONTEXT_FORM = 'form' |
|
12 | 16 | CONTEXT_PAGE = 'page' |
|
13 | 17 | |
|
14 | 18 | REQUEST_PAGE = 'page' |
|
15 | 19 | |
|
16 | 20 | __author__ = 'neko259' |
|
17 | 21 | |
|
18 | 22 | TEMPLATE = 'search/search.html' |
|
19 | 23 | |
|
20 | 24 | |
|
21 | 25 | class BoardSearchView(View): |
|
22 | 26 | def get(self, request): |
|
23 | context = RequestContext(request) | |
|
27 | params = dict() | |
|
28 | ||
|
24 | 29 | form = SearchForm(request.GET, error_class=PlainErrorList) |
|
25 |
|
|
|
30 | params[CONTEXT_FORM] = form | |
|
26 | 31 | |
|
27 | 32 | if form.is_valid(): |
|
28 | 33 | query = form.cleaned_data[FORM_QUERY] |
|
29 |
if len(query) >= |
|
|
30 |
results = SearchQuerySet().auto_query(query).order_by('-id') |
|
|
31 |
paginator = get_paginator(results, |
|
|
34 | if len(query) >= MIN_QUERY_LENGTH: | |
|
35 | results = SearchQuerySet().auto_query(query).order_by('-id') | |
|
36 | paginator = get_paginator(results, RESULTS_PER_PAGE) | |
|
32 | 37 | |
|
33 | if REQUEST_PAGE in request.GET: | |
|
34 | page = int(request.GET[REQUEST_PAGE]) | |
|
35 | else: | |
|
36 | page = 1 | |
|
37 | context[CONTEXT_PAGE] = paginator.page(page) | |
|
38 | context[CONTEXT_QUERY] = query | |
|
38 | page = int(request.GET.get(REQUEST_PAGE, '1')) | |
|
39 | 39 | |
|
40 | return render(request, TEMPLATE, context) | |
|
40 | params[CONTEXT_PAGE] = paginator.page(page) | |
|
41 | params[CONTEXT_QUERY] = query | |
|
42 | ||
|
43 | return render(request, TEMPLATE, params) |
@@ -1,38 +1,41 b'' | |||
|
1 | 1 | from django.db import transaction |
|
2 | 2 | from django.shortcuts import render, redirect |
|
3 | 3 | |
|
4 | 4 | from boards.abstracts.settingsmanager import get_settings_manager |
|
5 | 5 | from boards.views.base import BaseBoardView, CONTEXT_FORM |
|
6 | 6 | from boards.forms import SettingsForm, PlainErrorList |
|
7 | 7 | |
|
8 | FORM_THEME = 'theme' | |
|
9 | ||
|
8 | 10 | CONTEXT_HIDDEN_TAGS = 'hidden_tags' |
|
9 | 11 | |
|
10 | 12 | |
|
11 | 13 | class SettingsView(BaseBoardView): |
|
12 | 14 | |
|
13 | 15 | def get(self, request): |
|
14 |
|
|
|
16 | params = self.get_context_data() | |
|
15 | 17 | settings_manager = get_settings_manager(request) |
|
16 | 18 | |
|
17 | 19 | selected_theme = settings_manager.get_theme() |
|
18 | 20 | |
|
19 |
form = SettingsForm(initial={ |
|
|
21 | form = SettingsForm(initial={FORM_THEME: selected_theme}, | |
|
20 | 22 | error_class=PlainErrorList) |
|
21 | 23 | |
|
22 |
|
|
|
23 |
|
|
|
24 | params[CONTEXT_FORM] = form | |
|
25 | params[CONTEXT_HIDDEN_TAGS] = settings_manager.get_hidden_tags() | |
|
24 | 26 | |
|
25 | return render(request, 'boards/settings.html', context) | |
|
27 | # TODO Use dict here | |
|
28 | return render(request, 'boards/settings.html', params) | |
|
26 | 29 | |
|
27 | 30 | def post(self, request): |
|
28 | 31 | settings_manager = get_settings_manager(request) |
|
29 | 32 | |
|
30 | 33 | with transaction.atomic(): |
|
31 | 34 | form = SettingsForm(request.POST, error_class=PlainErrorList) |
|
32 | 35 | |
|
33 | 36 | if form.is_valid(): |
|
34 |
selected_theme = form.cleaned_data[ |
|
|
37 | selected_theme = form.cleaned_data[FORM_THEME] | |
|
35 | 38 | |
|
36 | 39 | settings_manager.set_theme(selected_theme) |
|
37 | 40 | |
|
38 | 41 | return redirect('settings') |
@@ -1,14 +1,13 b'' | |||
|
1 | 1 | from django.shortcuts import render |
|
2 | 2 | |
|
3 | 3 | from boards.views.base import BaseBoardView |
|
4 | 4 | |
|
5 | 5 | |
|
6 | 6 | class StaticPageView(BaseBoardView): |
|
7 | 7 | |
|
8 | 8 | def get(self, request, name): |
|
9 | 9 | """ |
|
10 | 10 | Show a static page that needs only tags list and a CSS |
|
11 | 11 | """ |
|
12 | 12 | |
|
13 | context = self.get_context_data(request=request) | |
|
14 | return render(request, 'boards/staticpages/' + name + '.html', context) | |
|
13 | return render(request, 'boards/staticpages/' + name + '.html') |
@@ -1,92 +1,95 b'' | |||
|
1 | 1 | from django.shortcuts import get_object_or_404 |
|
2 | 2 | |
|
3 | 3 | from boards.abstracts.settingsmanager import get_settings_manager |
|
4 | from boards.models import Tag | |
|
4 | from boards.models import Tag, Thread | |
|
5 | 5 | from boards.views.all_threads import AllThreadsView, DEFAULT_PAGE |
|
6 | 6 | from boards.views.mixins import DispatcherMixin, RedirectNextMixin |
|
7 | 7 | from boards.forms import ThreadForm, PlainErrorList |
|
8 | 8 | |
|
9 | PARAM_HIDDEN_TAGS = 'hidden_tags' | |
|
10 | PARAM_FAV_TAGS = 'fav_tags' | |
|
11 | PARAM_TAG = 'tag' | |
|
9 | 12 | |
|
10 | 13 | __author__ = 'neko259' |
|
11 | 14 | |
|
12 | 15 | |
|
13 | 16 | class TagView(AllThreadsView, DispatcherMixin, RedirectNextMixin): |
|
14 | 17 | |
|
15 | 18 | tag_name = None |
|
16 | 19 | |
|
17 | 20 | def get_threads(self): |
|
18 | 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 | 25 | def get_context_data(self, **kwargs): |
|
23 |
|
|
|
26 | params = super(TagView, self).get_context_data(**kwargs) | |
|
24 | 27 | |
|
25 | 28 | settings_manager = get_settings_manager(kwargs['request']) |
|
26 | 29 | |
|
27 | 30 | tag = get_object_or_404(Tag, name=self.tag_name) |
|
28 | context['tag'] = tag | |
|
31 | params[PARAM_TAG] = tag | |
|
29 | 32 | |
|
30 |
|
|
|
31 |
|
|
|
33 | params[PARAM_FAV_TAGS] = settings_manager.get_fav_tags() | |
|
34 | params[PARAM_HIDDEN_TAGS] = settings_manager.get_hidden_tags() | |
|
32 | 35 | |
|
33 |
return |
|
|
36 | return params | |
|
34 | 37 | |
|
35 | 38 | def get(self, request, tag_name, page=DEFAULT_PAGE, form=None): |
|
36 | 39 | self.tag_name = tag_name |
|
37 | 40 | |
|
38 | 41 | dispatch_result = self.dispatch_method(request) |
|
39 | 42 | if dispatch_result: |
|
40 | 43 | return dispatch_result |
|
41 | 44 | else: |
|
42 | 45 | return super(TagView, self).get(request, page, form) |
|
43 | 46 | |
|
44 | 47 | def post(self, request, tag_name, page=DEFAULT_PAGE): |
|
45 | 48 | form = ThreadForm(request.POST, request.FILES, |
|
46 | 49 | error_class=PlainErrorList) |
|
47 | 50 | form.session = request.session |
|
48 | 51 | |
|
49 | 52 | if form.is_valid(): |
|
50 | 53 | return self.create_thread(request, form) |
|
51 | 54 | if form.need_to_ban: |
|
52 | 55 | # Ban user because he is suspected to be a bot |
|
53 | 56 | self._ban_current_user(request) |
|
54 | 57 | |
|
55 | 58 | return self.get(request, tag_name, page, form) |
|
56 | 59 | |
|
57 | 60 | def subscribe(self, request): |
|
58 | 61 | tag = get_object_or_404(Tag, name=self.tag_name) |
|
59 | 62 | |
|
60 | 63 | settings_manager = get_settings_manager(request) |
|
61 | 64 | settings_manager.add_fav_tag(tag) |
|
62 | 65 | |
|
63 | 66 | return self.redirect_to_next(request) |
|
64 | 67 | |
|
65 | 68 | def unsubscribe(self, request): |
|
66 | 69 | tag = get_object_or_404(Tag, name=self.tag_name) |
|
67 | 70 | |
|
68 | 71 | settings_manager = get_settings_manager(request) |
|
69 | 72 | settings_manager.del_fav_tag(tag) |
|
70 | 73 | |
|
71 | 74 | return self.redirect_to_next(request) |
|
72 | 75 | |
|
73 | 76 | def hide(self, request): |
|
74 | 77 | """ |
|
75 | 78 | Adds tag to user's hidden tags. Threads with this tag will not be |
|
76 | 79 | shown. |
|
77 | 80 | """ |
|
78 | 81 | |
|
79 | 82 | tag = get_object_or_404(Tag, name=self.tag_name) |
|
80 | 83 | |
|
81 | 84 | settings_manager = get_settings_manager(request) |
|
82 | 85 | settings_manager.add_hidden_tag(tag) |
|
83 | 86 | |
|
84 | 87 | def unhide(self, request): |
|
85 | 88 | """ |
|
86 | 89 | Removed tag from user's hidden tags. |
|
87 | 90 | """ |
|
88 | 91 | |
|
89 | 92 | tag = get_object_or_404(Tag, name=self.tag_name) |
|
90 | 93 | |
|
91 | 94 | settings_manager = get_settings_manager(request) |
|
92 | 95 | settings_manager.del_hidden_tag(tag) |
@@ -1,142 +1,142 b'' | |||
|
1 | 1 | from django.core.urlresolvers import reverse |
|
2 | 2 | from django.db import transaction |
|
3 | 3 | from django.http import Http404 |
|
4 | 4 | from django.shortcuts import get_object_or_404, render, redirect |
|
5 | 5 | from django.views.generic.edit import FormMixin |
|
6 | 6 | |
|
7 | 7 | from boards import utils, settings |
|
8 | 8 | from boards.forms import PostForm, PlainErrorList |
|
9 | 9 | from boards.models import Post, Ban |
|
10 | 10 | from boards.views.banned import BannedView |
|
11 | 11 | from boards.views.base import BaseBoardView, CONTEXT_FORM |
|
12 | 12 | from boards.views.posting_mixin import PostMixin |
|
13 | import neboard | |
|
13 | 14 | |
|
14 | 15 | TEMPLATE_GALLERY = 'boards/thread_gallery.html' |
|
15 | 16 | TEMPLATE_NORMAL = 'boards/thread.html' |
|
16 | 17 | |
|
17 | 18 | CONTEXT_POSTS = 'posts' |
|
18 | 19 | CONTEXT_OP = 'opening_post' |
|
19 | 20 | CONTEXT_BUMPLIMIT_PRG = 'bumplimit_progress' |
|
20 | 21 | CONTEXT_POSTS_LEFT = 'posts_left' |
|
21 | 22 | CONTEXT_LASTUPDATE = "last_update" |
|
22 | 23 | CONTEXT_MAX_REPLIES = 'max_replies' |
|
23 | 24 | CONTEXT_THREAD = 'thread' |
|
24 | 25 | CONTEXT_BUMPABLE = 'bumpable' |
|
26 | CONTEXT_WS_TOKEN = 'ws_token' | |
|
27 | CONTEXT_WS_PROJECT = 'ws_project' | |
|
28 | CONTEXT_WS_HOST = 'ws_host' | |
|
29 | CONTEXT_WS_PORT = 'ws_port' | |
|
25 | 30 | |
|
26 | 31 | FORM_TITLE = 'title' |
|
27 | 32 | FORM_TEXT = 'text' |
|
28 | 33 | FORM_IMAGE = 'image' |
|
29 | 34 | |
|
30 | 35 | MODE_GALLERY = 'gallery' |
|
31 | 36 | MODE_NORMAL = 'normal' |
|
32 | 37 | |
|
33 | 38 | |
|
34 | 39 | class ThreadView(BaseBoardView, PostMixin, FormMixin): |
|
35 | 40 | |
|
36 | 41 | def get(self, request, post_id, mode=MODE_NORMAL, form=None): |
|
37 | 42 | try: |
|
38 | 43 | opening_post = Post.objects.filter(id=post_id).only('thread_new')[0] |
|
39 | 44 | except IndexError: |
|
40 | 45 | raise Http404 |
|
41 | 46 | |
|
42 | 47 | # If this is not OP, don't show it as it is |
|
43 | 48 | if not opening_post or not opening_post.is_opening(): |
|
44 | 49 | raise Http404 |
|
45 | 50 | |
|
46 | 51 | if not form: |
|
47 | 52 | form = PostForm(error_class=PlainErrorList) |
|
48 | 53 | |
|
49 | 54 | thread_to_show = opening_post.get_thread() |
|
50 | 55 | |
|
51 | context = self.get_context_data(request=request) | |
|
56 | params = dict() | |
|
57 | ||
|
58 | params[CONTEXT_FORM] = form | |
|
59 | params[CONTEXT_LASTUPDATE] = str(utils.datetime_to_epoch( | |
|
60 | thread_to_show.last_edit_time)) | |
|
61 | params[CONTEXT_THREAD] = thread_to_show | |
|
62 | params[CONTEXT_MAX_REPLIES] = settings.MAX_POSTS_PER_THREAD | |
|
52 | 63 | |
|
53 | context[CONTEXT_FORM] = form | |
|
54 | context[CONTEXT_LASTUPDATE] = utils.datetime_to_epoch( | |
|
55 | thread_to_show.last_edit_time) | |
|
56 | context[CONTEXT_THREAD] = thread_to_show | |
|
57 | context[CONTEXT_MAX_REPLIES] = settings.MAX_POSTS_PER_THREAD | |
|
64 | if settings.WEBSOCKETS_ENABLED: | |
|
65 | params[CONTEXT_WS_TOKEN] = utils.get_websocket_token( | |
|
66 | timestamp=params[CONTEXT_LASTUPDATE]) | |
|
67 | params[CONTEXT_WS_PROJECT] = neboard.settings.CENTRIFUGE_PROJECT_ID | |
|
68 | params[CONTEXT_WS_HOST] = request.get_host().split(':')[0] | |
|
69 | params[CONTEXT_WS_PORT] = neboard.settings.CENTRIFUGE_PORT | |
|
58 | 70 | |
|
71 | # TODO Move this to subclasses: NormalThreadView, GalleryThreadView etc | |
|
59 | 72 | if MODE_NORMAL == mode: |
|
60 | 73 | bumpable = thread_to_show.can_bump() |
|
61 |
|
|
|
74 | params[CONTEXT_BUMPABLE] = bumpable | |
|
62 | 75 | if bumpable: |
|
63 | 76 | left_posts = settings.MAX_POSTS_PER_THREAD \ |
|
64 | 77 | - thread_to_show.get_reply_count() |
|
65 |
|
|
|
66 |
|
|
|
78 | params[CONTEXT_POSTS_LEFT] = left_posts | |
|
79 | params[CONTEXT_BUMPLIMIT_PRG] = str( | |
|
67 | 80 | float(left_posts) / settings.MAX_POSTS_PER_THREAD * 100) |
|
68 | 81 | |
|
69 |
|
|
|
82 | params[CONTEXT_OP] = opening_post | |
|
70 | 83 | |
|
71 | 84 | document = TEMPLATE_NORMAL |
|
72 | 85 | elif MODE_GALLERY == mode: |
|
73 |
|
|
|
86 | params[CONTEXT_POSTS] = thread_to_show.get_replies_with_images( | |
|
74 | 87 | view_fields_only=True) |
|
75 | 88 | |
|
76 | 89 | document = TEMPLATE_GALLERY |
|
77 | 90 | else: |
|
78 | 91 | raise Http404 |
|
79 | 92 | |
|
80 |
return render(request, document, |
|
|
93 | return render(request, document, params) | |
|
81 | 94 | |
|
82 | 95 | def post(self, request, post_id, mode=MODE_NORMAL): |
|
83 | 96 | opening_post = get_object_or_404(Post, id=post_id) |
|
84 | 97 | |
|
85 | 98 | # If this is not OP, don't show it as it is |
|
86 | 99 | if not opening_post.is_opening(): |
|
87 | 100 | raise Http404 |
|
88 | 101 | |
|
89 | 102 | if not opening_post.get_thread().archived: |
|
90 | 103 | form = PostForm(request.POST, request.FILES, |
|
91 | 104 | error_class=PlainErrorList) |
|
92 | 105 | form.session = request.session |
|
93 | 106 | |
|
94 | 107 | if form.is_valid(): |
|
95 | 108 | return self.new_post(request, form, opening_post) |
|
96 | 109 | if form.need_to_ban: |
|
97 | 110 | # Ban user because he is suspected to be a bot |
|
98 | 111 | self._ban_current_user(request) |
|
99 | 112 | |
|
100 | 113 | return self.get(request, post_id, mode, form) |
|
101 | 114 | |
|
102 | @transaction.atomic | |
|
103 | 115 | def new_post(self, request, form, opening_post=None, html_response=True): |
|
104 | 116 | """Add a new post (in thread or as a reply).""" |
|
105 | 117 | |
|
106 | 118 | ip = utils.get_client_ip(request) |
|
107 | is_banned = Ban.objects.filter(ip=ip).exists() | |
|
108 | ||
|
109 | if is_banned: | |
|
110 | if html_response: | |
|
111 | return redirect(BannedView().as_view()) | |
|
112 | else: | |
|
113 | return None | |
|
114 | 119 | |
|
115 | 120 | data = form.cleaned_data |
|
116 | 121 | |
|
117 | 122 | title = data[FORM_TITLE] |
|
118 | 123 | text = data[FORM_TEXT] |
|
124 | image = data.get(FORM_IMAGE) | |
|
119 | 125 | |
|
120 | 126 | text = self._remove_invalid_links(text) |
|
121 | 127 | |
|
122 | if FORM_IMAGE in list(data.keys()): | |
|
123 | image = data[FORM_IMAGE] | |
|
124 | else: | |
|
125 | image = None | |
|
126 | ||
|
127 | tags = [] | |
|
128 | ||
|
129 | 128 | post_thread = opening_post.get_thread() |
|
130 | 129 | |
|
131 | 130 | post = Post.objects.create_post(title=title, text=text, image=image, |
|
132 |
thread=post_thread, ip=ip |
|
|
131 | thread=post_thread, ip=ip) | |
|
132 | post.send_to_websocket(request) | |
|
133 | 133 | |
|
134 | 134 | thread_to_show = (opening_post.id if opening_post else post.id) |
|
135 | 135 | |
|
136 | 136 | if html_response: |
|
137 | 137 | if opening_post: |
|
138 | 138 | return redirect( |
|
139 | 139 | reverse('thread', kwargs={'post_id': thread_to_show}) |
|
140 | 140 | + '#' + str(post.id)) |
|
141 | 141 | else: |
|
142 | 142 | return post |
@@ -1,10 +1,10 b'' | |||
|
1 | #!/usr/bin/env python | |
|
1 | #!/usr/bin/env python3 | |
|
2 | 2 | import os |
|
3 | 3 | import sys |
|
4 | 4 | |
|
5 | 5 | if __name__ == "__main__": |
|
6 | 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "neboard.settings") |
|
7 | 7 | |
|
8 | 8 | from django.core.management import execute_from_command_line |
|
9 | 9 | |
|
10 | 10 | execute_from_command_line(sys.argv) |
@@ -1,235 +1,233 b'' | |||
|
1 | 1 | # Django settings for neboard project. |
|
2 | 2 | import os |
|
3 | 3 | from boards.mdx_neboard import bbcode_extended |
|
4 | 4 | |
|
5 | 5 | DEBUG = True |
|
6 | 6 | TEMPLATE_DEBUG = DEBUG |
|
7 | 7 | |
|
8 | 8 | ADMINS = ( |
|
9 | 9 | # ('Your Name', 'your_email@example.com'), |
|
10 | 10 | ('admin', 'admin@example.com') |
|
11 | 11 | ) |
|
12 | 12 | |
|
13 | 13 | MANAGERS = ADMINS |
|
14 | 14 | |
|
15 | 15 | DATABASES = { |
|
16 | 16 | 'default': { |
|
17 | 17 | 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. |
|
18 | 18 | 'NAME': 'database.db', # Or path to database file if using sqlite3. |
|
19 | 19 | 'USER': '', # Not used with sqlite3. |
|
20 | 20 | 'PASSWORD': '', # Not used with sqlite3. |
|
21 | 21 | 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. |
|
22 | 22 | 'PORT': '', # Set to empty string for default. Not used with sqlite3. |
|
23 | 23 | 'CONN_MAX_AGE': None, |
|
24 | 24 | } |
|
25 | 25 | } |
|
26 | 26 | |
|
27 | 27 | # Local time zone for this installation. Choices can be found here: |
|
28 | 28 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name |
|
29 | 29 | # although not all choices may be available on all operating systems. |
|
30 | 30 | # In a Windows environment this must be set to your system time zone. |
|
31 | 31 | TIME_ZONE = 'Europe/Kiev' |
|
32 | 32 | |
|
33 | 33 | # Language code for this installation. All choices can be found here: |
|
34 | 34 | # http://www.i18nguy.com/unicode/language-identifiers.html |
|
35 | 35 | LANGUAGE_CODE = 'en' |
|
36 | 36 | |
|
37 | 37 | SITE_ID = 1 |
|
38 | 38 | |
|
39 | 39 | # If you set this to False, Django will make some optimizations so as not |
|
40 | 40 | # to load the internationalization machinery. |
|
41 | 41 | USE_I18N = True |
|
42 | 42 | |
|
43 | 43 | # If you set this to False, Django will not format dates, numbers and |
|
44 | 44 | # calendars according to the current locale. |
|
45 | 45 | USE_L10N = True |
|
46 | 46 | |
|
47 | 47 | # If you set this to False, Django will not use timezone-aware datetimes. |
|
48 | 48 | USE_TZ = True |
|
49 | 49 | |
|
50 | 50 | # Absolute filesystem path to the directory that will hold user-uploaded files. |
|
51 | 51 | # Example: "/home/media/media.lawrence.com/media/" |
|
52 | 52 | MEDIA_ROOT = './media/' |
|
53 | 53 | |
|
54 | 54 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a |
|
55 | 55 | # trailing slash. |
|
56 | 56 | # Examples: "http://media.lawrence.com/media/", "http://example.com/media/" |
|
57 | 57 | MEDIA_URL = '/media/' |
|
58 | 58 | |
|
59 | 59 | # Absolute path to the directory static files should be collected to. |
|
60 | 60 | # Don't put anything in this directory yourself; store your static files |
|
61 | 61 | # in apps' "static/" subdirectories and in STATICFILES_DIRS. |
|
62 | 62 | # Example: "/home/media/media.lawrence.com/static/" |
|
63 | 63 | STATIC_ROOT = '' |
|
64 | 64 | |
|
65 | 65 | # URL prefix for static files. |
|
66 | 66 | # Example: "http://media.lawrence.com/static/" |
|
67 | 67 | STATIC_URL = '/static/' |
|
68 | 68 | |
|
69 | 69 | # Additional locations of static files |
|
70 | 70 | # It is really a hack, put real paths, not related |
|
71 | 71 | STATICFILES_DIRS = ( |
|
72 | 72 | os.path.dirname(__file__) + '/boards/static', |
|
73 | 73 | |
|
74 | 74 | # '/d/work/python/django/neboard/neboard/boards/static', |
|
75 | 75 | # Put strings here, like "/home/html/static" or "C:/www/django/static". |
|
76 | 76 | # Always use forward slashes, even on Windows. |
|
77 | 77 | # Don't forget to use absolute paths, not relative paths. |
|
78 | 78 | ) |
|
79 | 79 | |
|
80 | 80 | # List of finder classes that know how to find static files in |
|
81 | 81 | # various locations. |
|
82 | 82 | STATICFILES_FINDERS = ( |
|
83 | 83 | 'django.contrib.staticfiles.finders.FileSystemFinder', |
|
84 | 84 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', |
|
85 | 'compressor.finders.CompressorFinder', | |
|
85 | 86 | ) |
|
86 | 87 | |
|
87 | 88 | if DEBUG: |
|
88 | 89 | STATICFILES_STORAGE = \ |
|
89 | 90 | 'django.contrib.staticfiles.storage.StaticFilesStorage' |
|
90 | 91 | else: |
|
91 | 92 | STATICFILES_STORAGE = \ |
|
92 | 93 | 'django.contrib.staticfiles.storage.CachedStaticFilesStorage' |
|
93 | 94 | |
|
94 | 95 | # Make this unique, and don't share it with anybody. |
|
95 | 96 | SECRET_KEY = '@1rc$o(7=tt#kd+4s$u6wchm**z^)4x90)7f6z(i&55@o11*8o' |
|
96 | 97 | |
|
97 | 98 | # List of callables that know how to import templates from various sources. |
|
98 | 99 | TEMPLATE_LOADERS = ( |
|
99 | 100 | 'django.template.loaders.filesystem.Loader', |
|
100 | 101 | 'django.template.loaders.app_directories.Loader', |
|
101 | 102 | ) |
|
102 | 103 | |
|
103 | 104 | TEMPLATE_CONTEXT_PROCESSORS = ( |
|
104 | 105 | 'django.core.context_processors.media', |
|
105 | 106 | 'django.core.context_processors.static', |
|
106 | 107 | 'django.core.context_processors.request', |
|
107 | 108 | 'django.contrib.auth.context_processors.auth', |
|
108 | 109 | 'boards.context_processors.user_and_ui_processor', |
|
109 | 110 | ) |
|
110 | 111 | |
|
111 | 112 | MIDDLEWARE_CLASSES = ( |
|
112 | 113 | 'django.contrib.sessions.middleware.SessionMiddleware', |
|
113 | 114 | 'django.middleware.locale.LocaleMiddleware', |
|
114 | 115 | 'django.middleware.common.CommonMiddleware', |
|
115 | 116 | 'django.contrib.auth.middleware.AuthenticationMiddleware', |
|
116 | 117 | 'django.contrib.messages.middleware.MessageMiddleware', |
|
117 | 118 | 'boards.middlewares.BanMiddleware', |
|
118 | 'boards.middlewares.MinifyHTMLMiddleware', | |
|
119 | 119 | ) |
|
120 | 120 | |
|
121 | 121 | ROOT_URLCONF = 'neboard.urls' |
|
122 | 122 | |
|
123 | 123 | # Python dotted path to the WSGI application used by Django's runserver. |
|
124 | 124 | WSGI_APPLICATION = 'neboard.wsgi.application' |
|
125 | 125 | |
|
126 | 126 | TEMPLATE_DIRS = ( |
|
127 | 127 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". |
|
128 | 128 | # Always use forward slashes, even on Windows. |
|
129 | 129 | # Don't forget to use absolute paths, not relative paths. |
|
130 | 130 | 'templates', |
|
131 | 131 | ) |
|
132 | 132 | |
|
133 | 133 | INSTALLED_APPS = ( |
|
134 | 134 | 'django.contrib.auth', |
|
135 | 135 | 'django.contrib.contenttypes', |
|
136 | 136 | 'django.contrib.sessions', |
|
137 | 137 | # 'django.contrib.sites', |
|
138 | 138 | 'django.contrib.messages', |
|
139 | 139 | 'django.contrib.staticfiles', |
|
140 | 140 | # Uncomment the next line to enable the admin: |
|
141 | 141 | 'django.contrib.admin', |
|
142 | 142 | # Uncomment the next line to enable admin documentation: |
|
143 | 143 | # 'django.contrib.admindocs', |
|
144 | 144 | 'django.contrib.humanize', |
|
145 | 145 | 'django_cleanup', |
|
146 | 146 | |
|
147 | # Migrations | |
|
148 | 'south', | |
|
149 | 147 | 'debug_toolbar', |
|
150 | 148 | |
|
151 | 149 | # Search |
|
152 | 150 | 'haystack', |
|
153 | 151 | |
|
154 | 152 | 'boards', |
|
155 | 153 | ) |
|
156 | 154 | |
|
157 | 155 | # A sample logging configuration. The only tangible logging |
|
158 | 156 | # performed by this configuration is to send an email to |
|
159 | 157 | # the site admins on every HTTP 500 error when DEBUG=False. |
|
160 | 158 | # See http://docs.djangoproject.com/en/dev/topics/logging for |
|
161 | 159 | # more details on how to customize your logging configuration. |
|
162 | 160 | LOGGING = { |
|
163 | 161 | 'version': 1, |
|
164 | 162 | 'disable_existing_loggers': False, |
|
165 | 163 | 'formatters': { |
|
166 | 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 | 167 | 'simple': { |
|
170 |
'format': '%(levelname)s %(asctime)s [%(m |
|
|
168 | 'format': '%(levelname)s %(asctime)s [%(name)s] %(message)s' | |
|
171 | 169 | }, |
|
172 | 170 | }, |
|
173 | 171 | 'filters': { |
|
174 | 172 | 'require_debug_false': { |
|
175 | 173 | '()': 'django.utils.log.RequireDebugFalse' |
|
176 | 174 | } |
|
177 | 175 | }, |
|
178 | 176 | 'handlers': { |
|
179 | 177 | 'console': { |
|
180 | 178 | 'level': 'DEBUG', |
|
181 | 179 | 'class': 'logging.StreamHandler', |
|
182 | 180 | 'formatter': 'simple' |
|
183 | 181 | }, |
|
184 | 182 | }, |
|
185 | 183 | 'loggers': { |
|
186 | 184 | 'boards': { |
|
187 | 185 | 'handlers': ['console'], |
|
188 | 186 | 'level': 'DEBUG', |
|
189 | 187 | } |
|
190 | 188 | }, |
|
191 | 189 | } |
|
192 | 190 | |
|
193 | 191 | HAYSTACK_CONNECTIONS = { |
|
194 | 192 | 'default': { |
|
195 | 193 | 'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine', |
|
196 | 194 | 'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'), |
|
197 | 195 | }, |
|
198 | 196 | } |
|
199 | 197 | |
|
200 | MARKUP_FIELD_TYPES = ( | |
|
201 | ('bbcode', bbcode_extended), | |
|
202 | ) | |
|
203 | ||
|
204 | 198 | THEMES = [ |
|
205 | 199 | ('md', 'Mystic Dark'), |
|
206 | 200 | ('md_centered', 'Mystic Dark (centered)'), |
|
207 | 201 | ('sw', 'Snow White'), |
|
208 | 202 | ('pg', 'Photon Gray'), |
|
209 | 203 | ] |
|
210 | 204 | |
|
211 | POPULAR_TAGS = 10 | |
|
212 | ||
|
213 | 205 | POSTING_DELAY = 20 # seconds |
|
214 | 206 | |
|
215 | COMPRESS_HTML = True | |
|
207 | # Websocket settins | |
|
208 | CENTRIFUGE_HOST = 'localhost' | |
|
209 | CENTRIFUGE_PORT = '9090' | |
|
210 | ||
|
211 | CENTRIFUGE_ADDRESS = 'http://{}:{}'.format(CENTRIFUGE_HOST, CENTRIFUGE_PORT) | |
|
212 | CENTRIFUGE_PROJECT_ID = '<project id here>' | |
|
213 | CENTRIFUGE_PROJECT_SECRET = '<project secret here>' | |
|
214 | CENTRIFUGE_TIMEOUT = 5 | |
|
216 | 215 | |
|
217 | 216 | # Debug mode middlewares |
|
218 | 217 | if DEBUG: |
|
219 | 218 | MIDDLEWARE_CLASSES += ( |
|
220 | 219 | 'debug_toolbar.middleware.DebugToolbarMiddleware', |
|
221 | 220 | ) |
|
222 | 221 | |
|
223 | 222 | def custom_show_toolbar(request): |
|
224 |
return |
|
|
223 | return True | |
|
225 | 224 | |
|
226 | 225 | DEBUG_TOOLBAR_CONFIG = { |
|
227 | 226 | 'ENABLE_STACKTRACES': True, |
|
228 | 227 | 'SHOW_TOOLBAR_CALLBACK': 'neboard.settings.custom_show_toolbar', |
|
229 | 228 | } |
|
230 | 229 | |
|
231 | 230 | # FIXME Uncommenting this fails somehow. Need to investigate this |
|
232 | 231 | #DEBUG_TOOLBAR_PANELS += ( |
|
233 | 232 | # 'debug_toolbar.panels.profiling.ProfilingDebugPanel', |
|
234 | 233 | #) |
|
235 |
@@ -1,55 +1,38 b'' | |||
|
1 | 1 | # INTRO # |
|
2 | 2 | |
|
3 | 3 | This project aims to create centralized forum-like discussion platform with |
|
4 | 4 | anonymity in mind. |
|
5 | 5 | |
|
6 | 6 | Main repository: https://bitbucket.org/neko259/neboard/ |
|
7 | 7 | |
|
8 | 8 | Site: http://neboard.me/ |
|
9 | 9 | |
|
10 | # DEPENDENCIES # | |
|
11 | ||
|
12 | ## REQUIRED ## | |
|
13 | ||
|
14 | * pillow | |
|
15 | * django >= 1.6 | |
|
16 | * django_cleanup | |
|
17 | * django-markupfield | |
|
18 | * markdown | |
|
19 | * python-markdown | |
|
20 | * django-simple-captcha | |
|
21 | * line-profiler | |
|
22 | ||
|
23 | ## OPTIONAL ## | |
|
24 | ||
|
25 | * django-debug-toolbar | |
|
26 | ||
|
27 | 10 | # INSTALLATION # |
|
28 | 11 | |
|
29 | 12 | 1. Install all dependencies over pip or system-wide |
|
30 | 13 | 2. Setup a database in `neboard/settings.py` |
|
31 | 14 | 3. Run `./manage.py syncdb` and ensure the database was created |
|
32 | 15 | 4. Run `./manage.py migrate boards` to apply all south migrations |
|
33 | 16 | |
|
34 | 17 | # RUNNING # |
|
35 | 18 | |
|
36 | 19 | You can run the server using django default embedded webserver by running |
|
37 | 20 | |
|
38 | 21 | ./manage.py runserver <address>:<port> |
|
39 | 22 | |
|
40 | 23 | See django-admin command help for details |
|
41 | 24 | |
|
42 | 25 | Also consider using wsgi or fcgi interfaces on production servers. |
|
43 | 26 | |
|
44 | 27 | # UPGRADE # |
|
45 | 28 | |
|
46 | 29 | 1. Backup your project data. |
|
47 | 30 | 2. Save the settings in `neboard/settings.py` and `boards/settings.py` |
|
48 | 31 | 3. Copy the project contents over the old project directory |
|
49 | 32 | 4. Run migrations by `./manage.py migrate boards` |
|
50 | 33 | |
|
51 | 34 | You can also just clone the mercurial project and pull it to update |
|
52 | 35 | |
|
53 | 36 | # CONCLUSION # |
|
54 | 37 | |
|
55 | 38 | Enjoy our software and thank you! |
@@ -1,10 +1,9 b'' | |||
|
1 | 1 | httplib2 |
|
2 | 2 | simplejson |
|
3 | south>=0.8.4 | |
|
3 | adjacent | |
|
4 | 4 | haystack |
|
5 | 5 | pillow |
|
6 |
django>=1. |
|
|
6 | django>=1.7 | |
|
7 | 7 | django_cleanup |
|
8 | django-markupfield | |
|
9 | 8 | bbcode |
|
10 | 9 | ecdsa |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
General Comments 0
You need to be logged in to leave comments.
Login now