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 | bc8fce57a613175450b8b6d933cdd85f22c04658 1.1 |
|
1 | bc8fce57a613175450b8b6d933cdd85f22c04658 1.1 | |
2 | 784258eb652c563c288ca7652c33f52cd4733d83 1.1-stable |
|
2 | 784258eb652c563c288ca7652c33f52cd4733d83 1.1-stable | |
3 | 1b53a22467a8fccc798935d7a26efe78e4bc7b25 1.2-stable |
|
3 | 1b53a22467a8fccc798935d7a26efe78e4bc7b25 1.2-stable | |
4 | 1713fb7543386089e364c39703b79e57d3d851f0 1.3 |
|
4 | 1713fb7543386089e364c39703b79e57d3d851f0 1.3 | |
5 | 80f183ebbe132ea8433eacae9431360f31fe7083 1.4 |
|
5 | 80f183ebbe132ea8433eacae9431360f31fe7083 1.4 | |
6 | 4330ff5a2bf6c543d8aaae8a43de1dc062f3bd13 1.4.1 |
|
6 | 4330ff5a2bf6c543d8aaae8a43de1dc062f3bd13 1.4.1 | |
7 | 8531d7b001392289a6b761f38c73a257606552ad 1.5 |
|
7 | 8531d7b001392289a6b761f38c73a257606552ad 1.5 | |
8 | 78e843c8b04b5a81cee5aa24601e305fae75da24 1.5.1 |
|
8 | 78e843c8b04b5a81cee5aa24601e305fae75da24 1.5.1 | |
9 | 4f92838730ed9aa1d17651bbcdca19a097fd0c37 1.6 |
|
9 | 4f92838730ed9aa1d17651bbcdca19a097fd0c37 1.6 | |
10 | 4bac2f37ea463337ddd27f98e7985407a74de504 1.7 |
|
10 | 4bac2f37ea463337ddd27f98e7985407a74de504 1.7 | |
11 | 1c4febea92c6503ae557fba73b2768659ae90d24 1.7.1 |
|
11 | 1c4febea92c6503ae557fba73b2768659ae90d24 1.7.1 | |
12 | 56a4a4578fc454ee455e33dd74a2cc82234bcb59 1.7.2 |
|
12 | 56a4a4578fc454ee455e33dd74a2cc82234bcb59 1.7.2 | |
13 | 34d6f3d5deb22be56b6c1512ec654bd7f6e03bcc 1.7.3 |
|
13 | 34d6f3d5deb22be56b6c1512ec654bd7f6e03bcc 1.7.3 | |
14 | f5cca33d29c673b67d43f310bebc4e3a21c6d04c 1.7.4 |
|
14 | f5cca33d29c673b67d43f310bebc4e3a21c6d04c 1.7.4 | |
15 | 7f7c33ba6e3f3797ca866c5ed5d358a2393f1371 1.8 |
|
15 | 7f7c33ba6e3f3797ca866c5ed5d358a2393f1371 1.8 | |
16 | a6b9dd9547bdc17b681502efcceb17aa5c09adf4 1.8.1 |
|
16 | a6b9dd9547bdc17b681502efcceb17aa5c09adf4 1.8.1 | |
17 | 8318fa1615d1946e4519f5735ae880909521990d 2.0 |
|
17 | 8318fa1615d1946e4519f5735ae880909521990d 2.0 | |
18 | e23590ee7e2067a3f0fc3cbcfd66404b47127feb 2.1 |
|
18 | e23590ee7e2067a3f0fc3cbcfd66404b47127feb 2.1 | |
|
19 | 4d998aba79e4abf0a2e78e93baaa2c2800b1c49c 2.2 | |||
|
20 | 07fdef4ac33a859250d03f17c594089792bca615 2.2.1 | |||
|
21 | bcc74d45f060ecd3ff06ff448165aea0d026cb3e 2.2.2 | |||
|
22 | b0e629ff24eb47a449ecfb455dc6cc600d18c9e2 2.2.3 |
@@ -1,146 +1,147 b'' | |||||
1 | from django.shortcuts import get_object_or_404 |
|
1 | from django.shortcuts import get_object_or_404 | |
2 | from boards.models import Tag |
|
2 | from boards.models import Tag | |
3 |
|
3 | |||
4 | __author__ = 'neko259' |
|
4 | __author__ = 'neko259' | |
5 |
|
5 | |||
6 | SESSION_SETTING = 'setting' |
|
6 | SESSION_SETTING = 'setting' | |
7 |
|
7 | |||
|
8 | # Remove this, it is not used any more cause there is a user's permission | |||
8 | PERMISSION_MODERATE = 'moderator' |
|
9 | PERMISSION_MODERATE = 'moderator' | |
9 |
|
10 | |||
10 | SETTING_THEME = 'theme' |
|
11 | SETTING_THEME = 'theme' | |
11 | SETTING_FAVORITE_TAGS = 'favorite_tags' |
|
12 | SETTING_FAVORITE_TAGS = 'favorite_tags' | |
12 | SETTING_HIDDEN_TAGS = 'hidden_tags' |
|
13 | SETTING_HIDDEN_TAGS = 'hidden_tags' | |
13 | SETTING_PERMISSIONS = 'permissions' |
|
14 | SETTING_PERMISSIONS = 'permissions' | |
14 |
|
15 | |||
15 | DEFAULT_THEME = 'md' |
|
16 | DEFAULT_THEME = 'md' | |
16 |
|
17 | |||
17 |
|
18 | |||
18 | def get_settings_manager(request): |
|
19 | def get_settings_manager(request): | |
19 | """ |
|
20 | """ | |
20 | Get settings manager based on the request object. Currently only |
|
21 | Get settings manager based on the request object. Currently only | |
21 | session-based manager is supported. In the future, cookie-based or |
|
22 | session-based manager is supported. In the future, cookie-based or | |
22 | database-based managers could be implemented. |
|
23 | database-based managers could be implemented. | |
23 | """ |
|
24 | """ | |
24 | return SessionSettingsManager(request.session) |
|
25 | return SessionSettingsManager(request.session) | |
25 |
|
26 | |||
26 |
|
27 | |||
27 | class SettingsManager: |
|
28 | class SettingsManager: | |
28 | """ |
|
29 | """ | |
29 | Base settings manager class. get_setting and set_setting methods should |
|
30 | Base settings manager class. get_setting and set_setting methods should | |
30 | be overriden. |
|
31 | be overriden. | |
31 | """ |
|
32 | """ | |
32 | def __init__(self): |
|
33 | def __init__(self): | |
33 | pass |
|
34 | pass | |
34 |
|
35 | |||
35 | def get_theme(self): |
|
36 | def get_theme(self): | |
36 | theme = self.get_setting(SETTING_THEME) |
|
37 | theme = self.get_setting(SETTING_THEME) | |
37 | if not theme: |
|
38 | if not theme: | |
38 | theme = DEFAULT_THEME |
|
39 | theme = DEFAULT_THEME | |
39 | self.set_setting(SETTING_THEME, theme) |
|
40 | self.set_setting(SETTING_THEME, theme) | |
40 |
|
41 | |||
41 | return theme |
|
42 | return theme | |
42 |
|
43 | |||
43 | def set_theme(self, theme): |
|
44 | def set_theme(self, theme): | |
44 | self.set_setting(SETTING_THEME, theme) |
|
45 | self.set_setting(SETTING_THEME, theme) | |
45 |
|
46 | |||
46 | def has_permission(self, permission): |
|
47 | def has_permission(self, permission): | |
47 | permissions = self.get_setting(SETTING_PERMISSIONS) |
|
48 | permissions = self.get_setting(SETTING_PERMISSIONS) | |
48 | if permissions: |
|
49 | if permissions: | |
49 | return permission in permissions |
|
50 | return permission in permissions | |
50 | else: |
|
51 | else: | |
51 | return False |
|
52 | return False | |
52 |
|
53 | |||
53 | def get_setting(self, setting): |
|
54 | def get_setting(self, setting): | |
54 | pass |
|
55 | pass | |
55 |
|
56 | |||
56 | def set_setting(self, setting, value): |
|
57 | def set_setting(self, setting, value): | |
57 | pass |
|
58 | pass | |
58 |
|
59 | |||
59 | def add_permission(self, permission): |
|
60 | def add_permission(self, permission): | |
60 | permissions = self.get_setting(SETTING_PERMISSIONS) |
|
61 | permissions = self.get_setting(SETTING_PERMISSIONS) | |
61 | if not permissions: |
|
62 | if not permissions: | |
62 | permissions = [permission] |
|
63 | permissions = [permission] | |
63 | else: |
|
64 | else: | |
64 | permissions.append(permission) |
|
65 | permissions.append(permission) | |
65 | self.set_setting(SETTING_PERMISSIONS, permissions) |
|
66 | self.set_setting(SETTING_PERMISSIONS, permissions) | |
66 |
|
67 | |||
67 | def del_permission(self, permission): |
|
68 | def del_permission(self, permission): | |
68 | permissions = self.get_setting(SETTING_PERMISSIONS) |
|
69 | permissions = self.get_setting(SETTING_PERMISSIONS) | |
69 | if not permissions: |
|
70 | if not permissions: | |
70 | permissions = [] |
|
71 | permissions = [] | |
71 | else: |
|
72 | else: | |
72 | permissions.remove(permission) |
|
73 | permissions.remove(permission) | |
73 | self.set_setting(SETTING_PERMISSIONS, permissions) |
|
74 | self.set_setting(SETTING_PERMISSIONS, permissions) | |
74 |
|
75 | |||
75 | def get_fav_tags(self): |
|
76 | def get_fav_tags(self): | |
76 | tag_names = self.get_setting(SETTING_FAVORITE_TAGS) |
|
77 | tag_names = self.get_setting(SETTING_FAVORITE_TAGS) | |
77 | tags = [] |
|
78 | tags = [] | |
78 | if tag_names: |
|
79 | if tag_names: | |
79 | for tag_name in tag_names: |
|
80 | for tag_name in tag_names: | |
80 | tag = get_object_or_404(Tag, name=tag_name) |
|
81 | tag = get_object_or_404(Tag, name=tag_name) | |
81 | tags.append(tag) |
|
82 | tags.append(tag) | |
82 | return tags |
|
83 | return tags | |
83 |
|
84 | |||
84 | def add_fav_tag(self, tag): |
|
85 | def add_fav_tag(self, tag): | |
85 | tags = self.get_setting(SETTING_FAVORITE_TAGS) |
|
86 | tags = self.get_setting(SETTING_FAVORITE_TAGS) | |
86 | if not tags: |
|
87 | if not tags: | |
87 | tags = [tag.name] |
|
88 | tags = [tag.name] | |
88 | else: |
|
89 | else: | |
89 | if not tag.name in tags: |
|
90 | if not tag.name in tags: | |
90 | tags.append(tag.name) |
|
91 | tags.append(tag.name) | |
91 |
|
92 | |||
92 | tags.sort() |
|
93 | tags.sort() | |
93 | self.set_setting(SETTING_FAVORITE_TAGS, tags) |
|
94 | self.set_setting(SETTING_FAVORITE_TAGS, tags) | |
94 |
|
95 | |||
95 | def del_fav_tag(self, tag): |
|
96 | def del_fav_tag(self, tag): | |
96 | tags = self.get_setting(SETTING_FAVORITE_TAGS) |
|
97 | tags = self.get_setting(SETTING_FAVORITE_TAGS) | |
97 | if tag.name in tags: |
|
98 | if tag.name in tags: | |
98 | tags.remove(tag.name) |
|
99 | tags.remove(tag.name) | |
99 | self.set_setting(SETTING_FAVORITE_TAGS, tags) |
|
100 | self.set_setting(SETTING_FAVORITE_TAGS, tags) | |
100 |
|
101 | |||
101 | def get_hidden_tags(self): |
|
102 | def get_hidden_tags(self): | |
102 | tag_names = self.get_setting(SETTING_HIDDEN_TAGS) |
|
103 | tag_names = self.get_setting(SETTING_HIDDEN_TAGS) | |
103 | tags = [] |
|
104 | tags = [] | |
104 | if tag_names: |
|
105 | if tag_names: | |
105 | for tag_name in tag_names: |
|
106 | for tag_name in tag_names: | |
106 | tag = get_object_or_404(Tag, name=tag_name) |
|
107 | tag = get_object_or_404(Tag, name=tag_name) | |
107 | tags.append(tag) |
|
108 | tags.append(tag) | |
108 |
|
109 | |||
109 | return tags |
|
110 | return tags | |
110 |
|
111 | |||
111 | def add_hidden_tag(self, tag): |
|
112 | def add_hidden_tag(self, tag): | |
112 | tags = self.get_setting(SETTING_HIDDEN_TAGS) |
|
113 | tags = self.get_setting(SETTING_HIDDEN_TAGS) | |
113 | if not tags: |
|
114 | if not tags: | |
114 | tags = [tag.name] |
|
115 | tags = [tag.name] | |
115 | else: |
|
116 | else: | |
116 | if not tag.name in tags: |
|
117 | if not tag.name in tags: | |
117 | tags.append(tag.name) |
|
118 | tags.append(tag.name) | |
118 |
|
119 | |||
119 | tags.sort() |
|
120 | tags.sort() | |
120 | self.set_setting(SETTING_HIDDEN_TAGS, tags) |
|
121 | self.set_setting(SETTING_HIDDEN_TAGS, tags) | |
121 |
|
122 | |||
122 | def del_hidden_tag(self, tag): |
|
123 | def del_hidden_tag(self, tag): | |
123 | tags = self.get_setting(SETTING_HIDDEN_TAGS) |
|
124 | tags = self.get_setting(SETTING_HIDDEN_TAGS) | |
124 | if tag.name in tags: |
|
125 | if tag.name in tags: | |
125 | tags.remove(tag.name) |
|
126 | tags.remove(tag.name) | |
126 | self.set_setting(SETTING_HIDDEN_TAGS, tags) |
|
127 | self.set_setting(SETTING_HIDDEN_TAGS, tags) | |
127 |
|
128 | |||
128 |
|
129 | |||
129 | class SessionSettingsManager(SettingsManager): |
|
130 | class SessionSettingsManager(SettingsManager): | |
130 | """ |
|
131 | """ | |
131 | Session-based settings manager. All settings are saved to the user's |
|
132 | Session-based settings manager. All settings are saved to the user's | |
132 | session. |
|
133 | session. | |
133 | """ |
|
134 | """ | |
134 | def __init__(self, session): |
|
135 | def __init__(self, session): | |
135 | SettingsManager.__init__(self) |
|
136 | SettingsManager.__init__(self) | |
136 | self.session = session |
|
137 | self.session = session | |
137 |
|
138 | |||
138 | def get_setting(self, setting): |
|
139 | def get_setting(self, setting): | |
139 | if setting in self.session: |
|
140 | if setting in self.session: | |
140 | return self.session[setting] |
|
141 | return self.session[setting] | |
141 | else: |
|
142 | else: | |
142 | return None |
|
143 | return None | |
143 |
|
144 | |||
144 | def set_setting(self, setting, value): |
|
145 | def set_setting(self, setting, value): | |
145 | self.session[setting] = value |
|
146 | self.session[setting] = value | |
146 |
|
147 |
@@ -1,43 +1,54 b'' | |||||
1 | from django.contrib import admin |
|
1 | from django.contrib import admin | |
2 | from boards.models import Post, Tag, Ban, Thread, KeyPair |
|
2 | from boards.models import Post, Tag, Ban, Thread, KeyPair | |
3 |
|
3 | |||
4 |
|
4 | |||
|
5 | @admin.register(Post) | |||
5 | class PostAdmin(admin.ModelAdmin): |
|
6 | class PostAdmin(admin.ModelAdmin): | |
6 |
|
7 | |||
7 | list_display = ('id', 'title', 'text') |
|
8 | list_display = ('id', 'title', 'text') | |
8 | list_filter = ('pub_time', 'thread_new') |
|
9 | list_filter = ('pub_time', 'thread_new') | |
9 | search_fields = ('id', 'title', 'text') |
|
10 | search_fields = ('id', 'title', 'text') | |
|
11 | exclude = ('referenced_posts', 'refmap') | |||
|
12 | readonly_fields = ('poster_ip', 'thread_new') | |||
10 |
|
13 | |||
11 |
|
14 | |||
|
15 | @admin.register(Tag) | |||
12 | class TagAdmin(admin.ModelAdmin): |
|
16 | class TagAdmin(admin.ModelAdmin): | |
13 |
|
17 | |||
14 | list_display = ('name',) |
|
18 | def thread_count(self, obj: Tag) -> int: | |
|
19 | return obj.get_thread_count() | |||
15 |
|
20 | |||
|
21 | list_display = ('name', 'thread_count') | |||
|
22 | search_fields = ('name',) | |||
|
23 | ||||
|
24 | ||||
|
25 | @admin.register(Thread) | |||
16 | class ThreadAdmin(admin.ModelAdmin): |
|
26 | class ThreadAdmin(admin.ModelAdmin): | |
17 |
|
27 | |||
18 | def title(self, obj): |
|
28 | def title(self, obj: Thread) -> str: | |
19 | return obj.get_opening_post().title |
|
29 | return obj.get_opening_post().get_title() | |
20 |
|
30 | |||
21 | def reply_count(self, obj): |
|
31 | def reply_count(self, obj: Thread) -> int: | |
22 | return obj.get_reply_count() |
|
32 | return obj.get_reply_count() | |
23 |
|
33 | |||
24 | list_display = ('id', 'title', 'reply_count', 'archived') |
|
34 | def ip(self, obj: Thread): | |
25 | list_filter = ('bump_time', 'archived') |
|
35 | return obj.get_opening_post().poster_ip | |
|
36 | ||||
|
37 | list_display = ('id', 'title', 'reply_count', 'archived', 'ip') | |||
|
38 | list_filter = ('bump_time', 'archived', 'bumpable') | |||
26 | search_fields = ('id', 'title') |
|
39 | search_fields = ('id', 'title') | |
|
40 | filter_horizontal = ('tags',) | |||
27 |
|
41 | |||
28 |
|
42 | |||
|
43 | @admin.register(KeyPair) | |||
29 | class KeyPairAdmin(admin.ModelAdmin): |
|
44 | class KeyPairAdmin(admin.ModelAdmin): | |
30 | list_display = ('public_key', 'primary') |
|
45 | list_display = ('public_key', 'primary') | |
31 | list_filter = ('primary',) |
|
46 | list_filter = ('primary',) | |
32 | search_fields = ('public_key',) |
|
47 | search_fields = ('public_key',) | |
33 |
|
48 | |||
|
49 | ||||
|
50 | @admin.register(Ban) | |||
34 | class BanAdmin(admin.ModelAdmin): |
|
51 | class BanAdmin(admin.ModelAdmin): | |
35 | list_display = ('ip', 'can_read') |
|
52 | list_display = ('ip', 'can_read') | |
36 | list_filter = ('can_read',) |
|
53 | list_filter = ('can_read',) | |
37 | search_fields = ('ip',) |
|
54 | search_fields = ('ip',) | |
38 |
|
||||
39 | admin.site.register(Post, PostAdmin) |
|
|||
40 | admin.site.register(Tag, TagAdmin) |
|
|||
41 | admin.site.register(Ban, BanAdmin) |
|
|||
42 | admin.site.register(Thread, ThreadAdmin) |
|
|||
43 | admin.site.register(KeyPair, KeyPairAdmin) |
|
@@ -1,42 +1,41 b'' | |||||
1 |
from boards.abstracts.settingsmanager import |
|
1 | from boards.abstracts.settingsmanager import get_settings_manager | |
2 | get_settings_manager |
|
|||
3 |
|
2 | |||
4 | __author__ = 'neko259' |
|
3 | __author__ = 'neko259' | |
5 |
|
4 | |||
6 | from boards import settings |
|
5 | from boards import settings | |
7 | from boards.models import Post |
|
6 | from boards.models import Post | |
8 |
|
7 | |||
9 | CONTEXT_SITE_NAME = 'site_name' |
|
8 | CONTEXT_SITE_NAME = 'site_name' | |
10 | CONTEXT_VERSION = 'version' |
|
9 | CONTEXT_VERSION = 'version' | |
11 | CONTEXT_MODERATOR = 'moderator' |
|
10 | CONTEXT_MODERATOR = 'moderator' | |
12 | CONTEXT_THEME_CSS = 'theme_css' |
|
11 | CONTEXT_THEME_CSS = 'theme_css' | |
13 | CONTEXT_THEME = 'theme' |
|
12 | CONTEXT_THEME = 'theme' | |
14 | CONTEXT_PPD = 'posts_per_day' |
|
13 | CONTEXT_PPD = 'posts_per_day' | |
15 | CONTEXT_TAGS = 'tags' |
|
14 | CONTEXT_TAGS = 'tags' | |
16 | CONTEXT_USER = 'user' |
|
15 | CONTEXT_USER = 'user' | |
17 |
|
16 | |||
18 | PERMISSION_MODERATE = 'moderation' |
|
17 | PERMISSION_MODERATE = 'moderation' | |
19 |
|
18 | |||
20 |
|
19 | |||
21 | def user_and_ui_processor(request): |
|
20 | def user_and_ui_processor(request): | |
22 |
context = |
|
21 | context = dict() | |
23 |
|
22 | |||
24 | context[CONTEXT_PPD] = float(Post.objects.get_posts_per_day()) |
|
23 | context[CONTEXT_PPD] = float(Post.objects.get_posts_per_day()) | |
25 |
|
24 | |||
26 | settings_manager = get_settings_manager(request) |
|
25 | settings_manager = get_settings_manager(request) | |
27 | context[CONTEXT_TAGS] = settings_manager.get_fav_tags() |
|
26 | context[CONTEXT_TAGS] = settings_manager.get_fav_tags() | |
28 | theme = settings_manager.get_theme() |
|
27 | theme = settings_manager.get_theme() | |
29 | context[CONTEXT_THEME] = theme |
|
28 | context[CONTEXT_THEME] = theme | |
30 | context[CONTEXT_THEME_CSS] = 'css/' + theme + '/base_page.css' |
|
29 | context[CONTEXT_THEME_CSS] = 'css/' + theme + '/base_page.css' | |
31 |
|
30 | |||
32 | # This shows the moderator panel |
|
31 | # This shows the moderator panel | |
33 | try: |
|
32 | try: | |
34 |
moderate = request.user.has_perm( |
|
33 | moderate = request.user.has_perm(PERMISSION_MODERATE) | |
35 | except AttributeError: |
|
34 | except AttributeError: | |
36 | moderate = False |
|
35 | moderate = False | |
37 | context[CONTEXT_MODERATOR] = moderate |
|
36 | context[CONTEXT_MODERATOR] = moderate | |
38 |
|
37 | |||
39 | context[CONTEXT_VERSION] = settings.VERSION |
|
38 | context[CONTEXT_VERSION] = settings.VERSION | |
40 | context[CONTEXT_SITE_NAME] = settings.SITE_NAME |
|
39 | context[CONTEXT_SITE_NAME] = settings.SITE_NAME | |
41 |
|
40 | |||
42 | return context |
|
41 | return context |
@@ -1,294 +1,305 b'' | |||||
1 | import re |
|
1 | import re | |
2 | import time |
|
2 | import time | |
3 | import hashlib |
|
3 | import hashlib | |
4 |
|
4 | |||
5 | from django import forms |
|
5 | from django import forms | |
6 | from django.forms.util import ErrorList |
|
6 | from django.forms.util import ErrorList | |
7 | from django.utils.translation import ugettext_lazy as _ |
|
7 | from django.utils.translation import ugettext_lazy as _ | |
8 |
|
8 | |||
9 | from boards.mdx_neboard import formatters |
|
9 | from boards.mdx_neboard import formatters | |
10 | from boards.models.post import TITLE_MAX_LENGTH |
|
10 | from boards.models.post import TITLE_MAX_LENGTH | |
11 | from boards.models import PostImage |
|
11 | from boards.models import PostImage, Tag | |
12 | from neboard import settings |
|
12 | from neboard import settings | |
13 | from boards import utils |
|
13 | from boards import utils | |
14 | import boards.settings as board_settings |
|
14 | import boards.settings as board_settings | |
15 |
|
15 | |||
16 | VETERAN_POSTING_DELAY = 5 |
|
16 | VETERAN_POSTING_DELAY = 5 | |
17 |
|
17 | |||
18 | ATTRIBUTE_PLACEHOLDER = 'placeholder' |
|
18 | ATTRIBUTE_PLACEHOLDER = 'placeholder' | |
19 |
|
19 | |||
20 | LAST_POST_TIME = 'last_post_time' |
|
20 | LAST_POST_TIME = 'last_post_time' | |
21 | LAST_LOGIN_TIME = 'last_login_time' |
|
21 | LAST_LOGIN_TIME = 'last_login_time' | |
22 | TEXT_PLACEHOLDER = _('''Type message here. Use formatting panel for more advanced usage.''') |
|
22 | TEXT_PLACEHOLDER = _('''Type message here. Use formatting panel for more advanced usage.''') | |
23 | TAGS_PLACEHOLDER = _('tag1 several_words_tag') |
|
23 | TAGS_PLACEHOLDER = _('tag1 several_words_tag') | |
24 |
|
24 | |||
25 | ERROR_IMAGE_DUPLICATE = _('Such image was already posted') |
|
25 | ERROR_IMAGE_DUPLICATE = _('Such image was already posted') | |
26 |
|
26 | |||
27 | LABEL_TITLE = _('Title') |
|
27 | LABEL_TITLE = _('Title') | |
28 | LABEL_TEXT = _('Text') |
|
28 | LABEL_TEXT = _('Text') | |
29 | LABEL_TAG = _('Tag') |
|
29 | LABEL_TAG = _('Tag') | |
30 | LABEL_SEARCH = _('Search') |
|
30 | LABEL_SEARCH = _('Search') | |
31 |
|
31 | |||
32 | TAG_MAX_LENGTH = 20 |
|
32 | TAG_MAX_LENGTH = 20 | |
33 |
|
33 | |||
34 | REGEX_TAG = r'^[\w\d]+$' |
|
34 | REGEX_TAG = r'^[\w\d]+$' | |
35 |
|
35 | |||
36 |
|
36 | |||
37 | class FormatPanel(forms.Textarea): |
|
37 | class FormatPanel(forms.Textarea): | |
38 | def render(self, name, value, attrs=None): |
|
38 | def render(self, name, value, attrs=None): | |
39 | output = '<div id="mark-panel">' |
|
39 | output = '<div id="mark-panel">' | |
40 | for formatter in formatters: |
|
40 | for formatter in formatters: | |
41 | output += '<span class="mark_btn"' + \ |
|
41 | output += '<span class="mark_btn"' + \ | |
42 | ' onClick="addMarkToMsg(\'' + formatter.format_left + \ |
|
42 | ' onClick="addMarkToMsg(\'' + formatter.format_left + \ | |
43 | '\', \'' + formatter.format_right + '\')">' + \ |
|
43 | '\', \'' + formatter.format_right + '\')">' + \ | |
44 | formatter.preview_left + formatter.name + \ |
|
44 | formatter.preview_left + formatter.name + \ | |
45 | formatter.preview_right + '</span>' |
|
45 | formatter.preview_right + '</span>' | |
46 |
|
46 | |||
47 | output += '</div>' |
|
47 | output += '</div>' | |
48 | output += super(FormatPanel, self).render(name, value, attrs=None) |
|
48 | output += super(FormatPanel, self).render(name, value, attrs=None) | |
49 |
|
49 | |||
50 | return output |
|
50 | return output | |
51 |
|
51 | |||
52 |
|
52 | |||
53 | class PlainErrorList(ErrorList): |
|
53 | class PlainErrorList(ErrorList): | |
54 | def __unicode__(self): |
|
54 | def __unicode__(self): | |
55 | return self.as_text() |
|
55 | return self.as_text() | |
56 |
|
56 | |||
57 | def as_text(self): |
|
57 | def as_text(self): | |
58 | return ''.join(['(!) %s ' % e for e in self]) |
|
58 | return ''.join(['(!) %s ' % e for e in self]) | |
59 |
|
59 | |||
60 |
|
60 | |||
61 | class NeboardForm(forms.Form): |
|
61 | class NeboardForm(forms.Form): | |
62 |
|
62 | |||
63 | def as_div(self): |
|
63 | def as_div(self): | |
64 | """ |
|
64 | """ | |
65 | Returns this form rendered as HTML <as_div>s. |
|
65 | Returns this form rendered as HTML <as_div>s. | |
66 | """ |
|
66 | """ | |
67 |
|
67 | |||
68 | return self._html_output( |
|
68 | return self._html_output( | |
69 | # TODO Do not show hidden rows in the list here |
|
69 | # TODO Do not show hidden rows in the list here | |
70 | normal_row='<div class="form-row"><div class="form-label">' |
|
70 | normal_row='<div class="form-row"><div class="form-label">' | |
71 | '%(label)s' |
|
71 | '%(label)s' | |
72 | '</div></div>' |
|
72 | '</div></div>' | |
73 | '<div class="form-row"><div class="form-input">' |
|
73 | '<div class="form-row"><div class="form-input">' | |
74 | '%(field)s' |
|
74 | '%(field)s' | |
75 | '</div></div>' |
|
75 | '</div></div>' | |
76 | '<div class="form-row">' |
|
76 | '<div class="form-row">' | |
77 | '%(help_text)s' |
|
77 | '%(help_text)s' | |
78 | '</div>', |
|
78 | '</div>', | |
79 | error_row='<div class="form-row">' |
|
79 | error_row='<div class="form-row">' | |
80 | '<div class="form-label"></div>' |
|
80 | '<div class="form-label"></div>' | |
81 | '<div class="form-errors">%s</div>' |
|
81 | '<div class="form-errors">%s</div>' | |
82 | '</div>', |
|
82 | '</div>', | |
83 | row_ender='</div>', |
|
83 | row_ender='</div>', | |
84 | help_text_html='%s', |
|
84 | help_text_html='%s', | |
85 | errors_on_separate_row=True) |
|
85 | errors_on_separate_row=True) | |
86 |
|
86 | |||
87 | def as_json_errors(self): |
|
87 | def as_json_errors(self): | |
88 | errors = [] |
|
88 | errors = [] | |
89 |
|
89 | |||
90 | for name, field in list(self.fields.items()): |
|
90 | for name, field in list(self.fields.items()): | |
91 | if self[name].errors: |
|
91 | if self[name].errors: | |
92 | errors.append({ |
|
92 | errors.append({ | |
93 | 'field': name, |
|
93 | 'field': name, | |
94 | 'errors': self[name].errors.as_text(), |
|
94 | 'errors': self[name].errors.as_text(), | |
95 | }) |
|
95 | }) | |
96 |
|
96 | |||
97 | return errors |
|
97 | return errors | |
98 |
|
98 | |||
99 |
|
99 | |||
100 | class PostForm(NeboardForm): |
|
100 | class PostForm(NeboardForm): | |
101 |
|
101 | |||
102 | title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False, |
|
102 | title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False, | |
103 | label=LABEL_TITLE) |
|
103 | label=LABEL_TITLE) | |
104 | text = forms.CharField( |
|
104 | text = forms.CharField( | |
105 | widget=FormatPanel(attrs={ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER}), |
|
105 | widget=FormatPanel(attrs={ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER}), | |
106 | required=False, label=LABEL_TEXT) |
|
106 | required=False, label=LABEL_TEXT) | |
107 | image = forms.ImageField(required=False, label=_('Image'), |
|
107 | image = forms.ImageField(required=False, label=_('Image'), | |
108 | widget=forms.ClearableFileInput( |
|
108 | widget=forms.ClearableFileInput( | |
109 | attrs={'accept': 'image/*'})) |
|
109 | attrs={'accept': 'image/*'})) | |
110 |
|
110 | |||
111 | # This field is for spam prevention only |
|
111 | # This field is for spam prevention only | |
112 | email = forms.CharField(max_length=100, required=False, label=_('e-mail'), |
|
112 | email = forms.CharField(max_length=100, required=False, label=_('e-mail'), | |
113 | widget=forms.TextInput(attrs={ |
|
113 | widget=forms.TextInput(attrs={ | |
114 | 'class': 'form-email'})) |
|
114 | 'class': 'form-email'})) | |
115 |
|
115 | |||
116 | session = None |
|
116 | session = None | |
117 | need_to_ban = False |
|
117 | need_to_ban = False | |
118 |
|
118 | |||
119 | def clean_title(self): |
|
119 | def clean_title(self): | |
120 | title = self.cleaned_data['title'] |
|
120 | title = self.cleaned_data['title'] | |
121 | if title: |
|
121 | if title: | |
122 | if len(title) > TITLE_MAX_LENGTH: |
|
122 | if len(title) > TITLE_MAX_LENGTH: | |
123 | raise forms.ValidationError(_('Title must have less than %s ' |
|
123 | raise forms.ValidationError(_('Title must have less than %s ' | |
124 | 'characters') % |
|
124 | 'characters') % | |
125 | str(TITLE_MAX_LENGTH)) |
|
125 | str(TITLE_MAX_LENGTH)) | |
126 | return title |
|
126 | return title | |
127 |
|
127 | |||
128 | def clean_text(self): |
|
128 | def clean_text(self): | |
129 | text = self.cleaned_data['text'].strip() |
|
129 | text = self.cleaned_data['text'].strip() | |
130 | if text: |
|
130 | if text: | |
131 | if len(text) > board_settings.MAX_TEXT_LENGTH: |
|
131 | if len(text) > board_settings.MAX_TEXT_LENGTH: | |
132 | raise forms.ValidationError(_('Text must have less than %s ' |
|
132 | raise forms.ValidationError(_('Text must have less than %s ' | |
133 | 'characters') % |
|
133 | 'characters') % | |
134 | str(board_settings |
|
134 | str(board_settings | |
135 | .MAX_TEXT_LENGTH)) |
|
135 | .MAX_TEXT_LENGTH)) | |
136 | return text |
|
136 | return text | |
137 |
|
137 | |||
138 | def clean_image(self): |
|
138 | def clean_image(self): | |
139 | image = self.cleaned_data['image'] |
|
139 | image = self.cleaned_data['image'] | |
140 | if image: |
|
140 | if image: | |
141 | if image.size > board_settings.MAX_IMAGE_SIZE: |
|
141 | if image.size > board_settings.MAX_IMAGE_SIZE: | |
142 | raise forms.ValidationError( |
|
142 | raise forms.ValidationError( | |
143 | _('Image must be less than %s bytes') |
|
143 | _('Image must be less than %s bytes') | |
144 | % str(board_settings.MAX_IMAGE_SIZE)) |
|
144 | % str(board_settings.MAX_IMAGE_SIZE)) | |
145 |
|
145 | |||
146 | md5 = hashlib.md5() |
|
146 | md5 = hashlib.md5() | |
147 | for chunk in image.chunks(): |
|
147 | for chunk in image.chunks(): | |
148 | md5.update(chunk) |
|
148 | md5.update(chunk) | |
149 | image_hash = md5.hexdigest() |
|
149 | image_hash = md5.hexdigest() | |
150 | if PostImage.objects.filter(hash=image_hash).exists(): |
|
150 | if PostImage.objects.filter(hash=image_hash).exists(): | |
151 | raise forms.ValidationError(ERROR_IMAGE_DUPLICATE) |
|
151 | raise forms.ValidationError(ERROR_IMAGE_DUPLICATE) | |
152 |
|
152 | |||
153 | return image |
|
153 | return image | |
154 |
|
154 | |||
155 | def clean(self): |
|
155 | def clean(self): | |
156 | cleaned_data = super(PostForm, self).clean() |
|
156 | cleaned_data = super(PostForm, self).clean() | |
157 |
|
157 | |||
158 | if not self.session: |
|
158 | if not self.session: | |
159 | raise forms.ValidationError('Humans have sessions') |
|
159 | raise forms.ValidationError('Humans have sessions') | |
160 |
|
160 | |||
161 | if cleaned_data['email']: |
|
161 | if cleaned_data['email']: | |
162 | self.need_to_ban = True |
|
162 | self.need_to_ban = True | |
163 | raise forms.ValidationError('A human cannot enter a hidden field') |
|
163 | raise forms.ValidationError('A human cannot enter a hidden field') | |
164 |
|
164 | |||
165 | if not self.errors: |
|
165 | if not self.errors: | |
166 | self._clean_text_image() |
|
166 | self._clean_text_image() | |
167 |
|
167 | |||
168 | if not self.errors and self.session: |
|
168 | if not self.errors and self.session: | |
169 | self._validate_posting_speed() |
|
169 | self._validate_posting_speed() | |
170 |
|
170 | |||
171 | return cleaned_data |
|
171 | return cleaned_data | |
172 |
|
172 | |||
173 | def _clean_text_image(self): |
|
173 | def _clean_text_image(self): | |
174 | text = self.cleaned_data.get('text') |
|
174 | text = self.cleaned_data.get('text') | |
175 | image = self.cleaned_data.get('image') |
|
175 | image = self.cleaned_data.get('image') | |
176 |
|
176 | |||
177 | if (not text) and (not image): |
|
177 | if (not text) and (not image): | |
178 | error_message = _('Either text or image must be entered.') |
|
178 | error_message = _('Either text or image must be entered.') | |
179 | self._errors['text'] = self.error_class([error_message]) |
|
179 | self._errors['text'] = self.error_class([error_message]) | |
180 |
|
180 | |||
181 | def _validate_posting_speed(self): |
|
181 | def _validate_posting_speed(self): | |
182 | can_post = True |
|
182 | can_post = True | |
183 |
|
183 | |||
184 | posting_delay = settings.POSTING_DELAY |
|
184 | posting_delay = settings.POSTING_DELAY | |
185 |
|
185 | |||
186 | if board_settings.LIMIT_POSTING_SPEED and LAST_POST_TIME in \ |
|
186 | if board_settings.LIMIT_POSTING_SPEED and LAST_POST_TIME in \ | |
187 | self.session: |
|
187 | self.session: | |
188 | now = time.time() |
|
188 | now = time.time() | |
189 | last_post_time = self.session[LAST_POST_TIME] |
|
189 | last_post_time = self.session[LAST_POST_TIME] | |
190 |
|
190 | |||
191 | current_delay = int(now - last_post_time) |
|
191 | current_delay = int(now - last_post_time) | |
192 |
|
192 | |||
193 | if current_delay < posting_delay: |
|
193 | if current_delay < posting_delay: | |
194 | error_message = _('Wait %s seconds after last posting') % str( |
|
194 | error_message = _('Wait %s seconds after last posting') % str( | |
195 | posting_delay - current_delay) |
|
195 | posting_delay - current_delay) | |
196 | self._errors['text'] = self.error_class([error_message]) |
|
196 | self._errors['text'] = self.error_class([error_message]) | |
197 |
|
197 | |||
198 | can_post = False |
|
198 | can_post = False | |
199 |
|
199 | |||
200 | if can_post: |
|
200 | if can_post: | |
201 | self.session[LAST_POST_TIME] = time.time() |
|
201 | self.session[LAST_POST_TIME] = time.time() | |
202 |
|
202 | |||
203 |
|
203 | |||
204 | class ThreadForm(PostForm): |
|
204 | class ThreadForm(PostForm): | |
205 |
|
205 | |||
206 | regex_tags = re.compile(r'^[\w\s\d]+$', re.UNICODE) |
|
206 | regex_tags = re.compile(r'^[\w\s\d]+$', re.UNICODE) | |
207 |
|
207 | |||
208 | tags = forms.CharField( |
|
208 | tags = forms.CharField( | |
209 | widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}), |
|
209 | widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}), | |
210 | max_length=100, label=_('Tags'), required=True) |
|
210 | max_length=100, label=_('Tags'), required=True) | |
211 |
|
211 | |||
212 | def clean_tags(self): |
|
212 | def clean_tags(self): | |
213 | tags = self.cleaned_data['tags'].strip() |
|
213 | tags = self.cleaned_data['tags'].strip() | |
214 |
|
214 | |||
215 | if not tags or not self.regex_tags.match(tags): |
|
215 | if not tags or not self.regex_tags.match(tags): | |
216 | raise forms.ValidationError( |
|
216 | raise forms.ValidationError( | |
217 | _('Inappropriate characters in tags.')) |
|
217 | _('Inappropriate characters in tags.')) | |
218 |
|
218 | |||
|
219 | tag_models = [] | |||
|
220 | required_tag_exists = False | |||
|
221 | for tag in tags.split(): | |||
|
222 | tag_model = Tag.objects.filter(name=tag.strip().lower(), | |||
|
223 | required=True) | |||
|
224 | if tag_model.exists(): | |||
|
225 | required_tag_exists = True | |||
|
226 | ||||
|
227 | if not required_tag_exists: | |||
|
228 | raise forms.ValidationError(_('Need at least 1 required tag.')) | |||
|
229 | ||||
219 | return tags |
|
230 | return tags | |
220 |
|
231 | |||
221 | def clean(self): |
|
232 | def clean(self): | |
222 | cleaned_data = super(ThreadForm, self).clean() |
|
233 | cleaned_data = super(ThreadForm, self).clean() | |
223 |
|
234 | |||
224 | return cleaned_data |
|
235 | return cleaned_data | |
225 |
|
236 | |||
226 |
|
237 | |||
227 | class SettingsForm(NeboardForm): |
|
238 | class SettingsForm(NeboardForm): | |
228 |
|
239 | |||
229 | theme = forms.ChoiceField(choices=settings.THEMES, |
|
240 | theme = forms.ChoiceField(choices=settings.THEMES, | |
230 | label=_('Theme')) |
|
241 | label=_('Theme')) | |
231 |
|
242 | |||
232 |
|
243 | |||
233 | class AddTagForm(NeboardForm): |
|
244 | class AddTagForm(NeboardForm): | |
234 |
|
245 | |||
235 | tag = forms.CharField(max_length=TAG_MAX_LENGTH, label=LABEL_TAG) |
|
246 | tag = forms.CharField(max_length=TAG_MAX_LENGTH, label=LABEL_TAG) | |
236 | method = forms.CharField(widget=forms.HiddenInput(), initial='add_tag') |
|
247 | method = forms.CharField(widget=forms.HiddenInput(), initial='add_tag') | |
237 |
|
248 | |||
238 | def clean_tag(self): |
|
249 | def clean_tag(self): | |
239 | tag = self.cleaned_data['tag'] |
|
250 | tag = self.cleaned_data['tag'] | |
240 |
|
251 | |||
241 | regex_tag = re.compile(REGEX_TAG, re.UNICODE) |
|
252 | regex_tag = re.compile(REGEX_TAG, re.UNICODE) | |
242 | if not regex_tag.match(tag): |
|
253 | if not regex_tag.match(tag): | |
243 | raise forms.ValidationError(_('Inappropriate characters in tags.')) |
|
254 | raise forms.ValidationError(_('Inappropriate characters in tags.')) | |
244 |
|
255 | |||
245 | return tag |
|
256 | return tag | |
246 |
|
257 | |||
247 | def clean(self): |
|
258 | def clean(self): | |
248 | cleaned_data = super(AddTagForm, self).clean() |
|
259 | cleaned_data = super(AddTagForm, self).clean() | |
249 |
|
260 | |||
250 | return cleaned_data |
|
261 | return cleaned_data | |
251 |
|
262 | |||
252 |
|
263 | |||
253 | class SearchForm(NeboardForm): |
|
264 | class SearchForm(NeboardForm): | |
254 | query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False) |
|
265 | query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False) | |
255 |
|
266 | |||
256 |
|
267 | |||
257 | class LoginForm(NeboardForm): |
|
268 | class LoginForm(NeboardForm): | |
258 |
|
269 | |||
259 | password = forms.CharField() |
|
270 | password = forms.CharField() | |
260 |
|
271 | |||
261 | session = None |
|
272 | session = None | |
262 |
|
273 | |||
263 | def clean_password(self): |
|
274 | def clean_password(self): | |
264 | password = self.cleaned_data['password'] |
|
275 | password = self.cleaned_data['password'] | |
265 | if board_settings.MASTER_PASSWORD != password: |
|
276 | if board_settings.MASTER_PASSWORD != password: | |
266 | raise forms.ValidationError(_('Invalid master password')) |
|
277 | raise forms.ValidationError(_('Invalid master password')) | |
267 |
|
278 | |||
268 | return password |
|
279 | return password | |
269 |
|
280 | |||
270 | def _validate_login_speed(self): |
|
281 | def _validate_login_speed(self): | |
271 | can_post = True |
|
282 | can_post = True | |
272 |
|
283 | |||
273 | if LAST_LOGIN_TIME in self.session: |
|
284 | if LAST_LOGIN_TIME in self.session: | |
274 | now = time.time() |
|
285 | now = time.time() | |
275 | last_login_time = self.session[LAST_LOGIN_TIME] |
|
286 | last_login_time = self.session[LAST_LOGIN_TIME] | |
276 |
|
287 | |||
277 | current_delay = int(now - last_login_time) |
|
288 | current_delay = int(now - last_login_time) | |
278 |
|
289 | |||
279 | if current_delay < board_settings.LOGIN_TIMEOUT: |
|
290 | if current_delay < board_settings.LOGIN_TIMEOUT: | |
280 | error_message = _('Wait %s minutes after last login') % str( |
|
291 | error_message = _('Wait %s minutes after last login') % str( | |
281 | (board_settings.LOGIN_TIMEOUT - current_delay) / 60) |
|
292 | (board_settings.LOGIN_TIMEOUT - current_delay) / 60) | |
282 | self._errors['password'] = self.error_class([error_message]) |
|
293 | self._errors['password'] = self.error_class([error_message]) | |
283 |
|
294 | |||
284 | can_post = False |
|
295 | can_post = False | |
285 |
|
296 | |||
286 | if can_post: |
|
297 | if can_post: | |
287 | self.session[LAST_LOGIN_TIME] = time.time() |
|
298 | self.session[LAST_LOGIN_TIME] = time.time() | |
288 |
|
299 | |||
289 | def clean(self): |
|
300 | def clean(self): | |
290 | self._validate_login_speed() |
|
301 | self._validate_login_speed() | |
291 |
|
302 | |||
292 | cleaned_data = super(LoginForm, self).clean() |
|
303 | cleaned_data = super(LoginForm, self).clean() | |
293 |
|
304 | |||
294 | return cleaned_data |
|
305 | return cleaned_data |
1 | NO CONTENT: modified file, binary diff hidden |
|
NO CONTENT: modified file, binary diff hidden |
@@ -1,365 +1,370 b'' | |||||
1 | # SOME DESCRIPTIVE TITLE. |
|
1 | # SOME DESCRIPTIVE TITLE. | |
2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER |
|
2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER | |
3 | # This file is distributed under the same license as the PACKAGE package. |
|
3 | # This file is distributed under the same license as the PACKAGE package. | |
4 | # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. |
|
4 | # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. | |
5 | # |
|
5 | # | |
6 | msgid "" |
|
6 | msgid "" | |
7 | msgstr "" |
|
7 | msgstr "" | |
8 | "Project-Id-Version: PACKAGE VERSION\n" |
|
8 | "Project-Id-Version: PACKAGE VERSION\n" | |
9 | "Report-Msgid-Bugs-To: \n" |
|
9 | "Report-Msgid-Bugs-To: \n" | |
10 |
"POT-Creation-Date: 201 |
|
10 | "POT-Creation-Date: 2015-01-08 16:36+0200\n" | |
11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" |
|
11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" | |
12 | "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" |
|
12 | "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" | |
13 | "Language-Team: LANGUAGE <LL@li.org>\n" |
|
13 | "Language-Team: LANGUAGE <LL@li.org>\n" | |
14 | "Language: ru\n" |
|
14 | "Language: ru\n" | |
15 | "MIME-Version: 1.0\n" |
|
15 | "MIME-Version: 1.0\n" | |
16 | "Content-Type: text/plain; charset=UTF-8\n" |
|
16 | "Content-Type: text/plain; charset=UTF-8\n" | |
17 | "Content-Transfer-Encoding: 8bit\n" |
|
17 | "Content-Transfer-Encoding: 8bit\n" | |
18 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" |
|
18 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" | |
19 | "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" |
|
19 | "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" | |
20 |
|
20 | |||
21 | #: authors.py:9 |
|
21 | #: authors.py:9 | |
22 | msgid "author" |
|
22 | msgid "author" | |
23 | msgstr "автор" |
|
23 | msgstr "автор" | |
24 |
|
24 | |||
25 | #: authors.py:10 |
|
25 | #: authors.py:10 | |
26 | msgid "developer" |
|
26 | msgid "developer" | |
27 | msgstr "разработчик" |
|
27 | msgstr "разработчик" | |
28 |
|
28 | |||
29 | #: authors.py:11 |
|
29 | #: authors.py:11 | |
30 | msgid "javascript developer" |
|
30 | msgid "javascript developer" | |
31 | msgstr "разработчик javascript" |
|
31 | msgstr "разработчик javascript" | |
32 |
|
32 | |||
33 | #: authors.py:12 |
|
33 | #: authors.py:12 | |
34 | msgid "designer" |
|
34 | msgid "designer" | |
35 | msgstr "дизайнер" |
|
35 | msgstr "дизайнер" | |
36 |
|
36 | |||
37 | #: forms.py:22 |
|
37 | #: forms.py:22 | |
38 | msgid "Type message here. Use formatting panel for more advanced usage." |
|
38 | msgid "Type message here. Use formatting panel for more advanced usage." | |
39 | msgstr "" |
|
39 | msgstr "" | |
40 | "Вводите сообщение сюда. Используйте панель для более сложного форматирования." |
|
40 | "Вводите сообщение сюда. Используйте панель для более сложного форматирования." | |
41 |
|
41 | |||
42 | #: forms.py:23 |
|
42 | #: forms.py:23 | |
43 | msgid "tag1 several_words_tag" |
|
43 | msgid "tag1 several_words_tag" | |
44 |
msgstr " |
|
44 | msgstr "метка1 метка_из_нескольких_слов" | |
45 |
|
45 | |||
46 | #: forms.py:25 |
|
46 | #: forms.py:25 | |
47 | msgid "Such image was already posted" |
|
47 | msgid "Such image was already posted" | |
48 | msgstr "Такое изображение уже было загружено" |
|
48 | msgstr "Такое изображение уже было загружено" | |
49 |
|
49 | |||
50 | #: forms.py:27 |
|
50 | #: forms.py:27 | |
51 | msgid "Title" |
|
51 | msgid "Title" | |
52 | msgstr "Заголовок" |
|
52 | msgstr "Заголовок" | |
53 |
|
53 | |||
54 | #: forms.py:28 |
|
54 | #: forms.py:28 | |
55 | msgid "Text" |
|
55 | msgid "Text" | |
56 | msgstr "Текст" |
|
56 | msgstr "Текст" | |
57 |
|
57 | |||
58 | #: forms.py:29 |
|
58 | #: forms.py:29 | |
59 | msgid "Tag" |
|
59 | msgid "Tag" | |
60 |
msgstr " |
|
60 | msgstr "Метка" | |
61 |
|
61 | |||
62 |
#: forms.py:30 templates/boards/base.html:3 |
|
62 | #: forms.py:30 templates/boards/base.html:38 templates/search/search.html:9 | |
63 | #: templates/search/search.html.py:13 |
|
63 | #: templates/search/search.html.py:13 | |
64 | msgid "Search" |
|
64 | msgid "Search" | |
65 | msgstr "Поиск" |
|
65 | msgstr "Поиск" | |
66 |
|
66 | |||
67 | #: forms.py:107 |
|
67 | #: forms.py:107 | |
68 | msgid "Image" |
|
68 | msgid "Image" | |
69 | msgstr "Изображение" |
|
69 | msgstr "Изображение" | |
70 |
|
70 | |||
71 | #: forms.py:112 |
|
71 | #: forms.py:112 | |
72 | msgid "e-mail" |
|
72 | msgid "e-mail" | |
73 | msgstr "" |
|
73 | msgstr "" | |
74 |
|
74 | |||
75 | #: forms.py:123 |
|
75 | #: forms.py:123 | |
76 | #, python-format |
|
76 | #, python-format | |
77 | msgid "Title must have less than %s characters" |
|
77 | msgid "Title must have less than %s characters" | |
78 | msgstr "Заголовок должен иметь меньше %s символов" |
|
78 | msgstr "Заголовок должен иметь меньше %s символов" | |
79 |
|
79 | |||
80 | #: forms.py:132 |
|
80 | #: forms.py:132 | |
81 | #, python-format |
|
81 | #, python-format | |
82 | msgid "Text must have less than %s characters" |
|
82 | msgid "Text must have less than %s characters" | |
83 | msgstr "Текст должен быть короче %s символов" |
|
83 | msgstr "Текст должен быть короче %s символов" | |
84 |
|
84 | |||
85 | #: forms.py:143 |
|
85 | #: forms.py:143 | |
86 | #, python-format |
|
86 | #, python-format | |
87 | msgid "Image must be less than %s bytes" |
|
87 | msgid "Image must be less than %s bytes" | |
88 | msgstr "Изображение должно быть менее %s байт" |
|
88 | msgstr "Изображение должно быть менее %s байт" | |
89 |
|
89 | |||
90 | #: forms.py:178 |
|
90 | #: forms.py:178 | |
91 | msgid "Either text or image must be entered." |
|
91 | msgid "Either text or image must be entered." | |
92 | msgstr "Текст или картинка должны быть введены." |
|
92 | msgstr "Текст или картинка должны быть введены." | |
93 |
|
93 | |||
94 |
#: forms.py:19 |
|
94 | #: forms.py:194 | |
95 | #, python-format |
|
95 | #, python-format | |
96 | msgid "Wait %s seconds after last posting" |
|
96 | msgid "Wait %s seconds after last posting" | |
97 | msgstr "Подождите %s секунд после последнего постинга" |
|
97 | msgstr "Подождите %s секунд после последнего постинга" | |
98 |
|
98 | |||
99 |
#: forms.py:21 |
|
99 | #: forms.py:210 templates/boards/rss/post.html:10 templates/boards/tags.html:7 | |
100 | msgid "Tags" |
|
100 | msgid "Tags" | |
101 |
msgstr " |
|
101 | msgstr "Метки" | |
102 |
|
102 | |||
103 |
#: forms.py:2 |
|
103 | #: forms.py:217 forms.py:254 | |
104 | msgid "Inappropriate characters in tags." |
|
104 | msgid "Inappropriate characters in tags." | |
105 |
msgstr "Недопустимые символы в |
|
105 | msgstr "Недопустимые символы в метках." | |
106 |
|
106 | |||
107 |
#: forms.py:2 |
|
107 | #: forms.py:228 | |
|
108 | msgid "Need at least 1 required tag." | |||
|
109 | msgstr "Нужна хотя бы 1 обязательная метка." | |||
|
110 | ||||
|
111 | #: forms.py:241 | |||
108 | msgid "Theme" |
|
112 | msgid "Theme" | |
109 | msgstr "Тема" |
|
113 | msgstr "Тема" | |
110 |
|
114 | |||
111 |
#: forms.py:27 |
|
115 | #: forms.py:277 | |
112 | msgid "Invalid master password" |
|
116 | msgid "Invalid master password" | |
113 | msgstr "Неверный мастер-пароль" |
|
117 | msgstr "Неверный мастер-пароль" | |
114 |
|
118 | |||
115 |
#: forms.py:2 |
|
119 | #: forms.py:291 | |
116 | #, python-format |
|
120 | #, python-format | |
117 | msgid "Wait %s minutes after last login" |
|
121 | msgid "Wait %s minutes after last login" | |
118 | msgstr "Подождите %s минут после последнего входа" |
|
122 | msgstr "Подождите %s минут после последнего входа" | |
119 |
|
123 | |||
120 | #: templates/boards/404.html:6 |
|
124 | #: templates/boards/404.html:6 | |
121 | msgid "Not found" |
|
125 | msgid "Not found" | |
122 | msgstr "Не найдено" |
|
126 | msgstr "Не найдено" | |
123 |
|
127 | |||
124 | #: templates/boards/404.html:12 |
|
128 | #: templates/boards/404.html:12 | |
125 | msgid "This page does not exist" |
|
129 | msgid "This page does not exist" | |
126 | msgstr "Этой страницы не существует" |
|
130 | msgstr "Этой страницы не существует" | |
127 |
|
131 | |||
128 | #: templates/boards/authors.html:6 templates/boards/authors.html.py:12 |
|
132 | #: templates/boards/authors.html:6 templates/boards/authors.html.py:12 | |
129 | msgid "Authors" |
|
133 | msgid "Authors" | |
130 | msgstr "Авторы" |
|
134 | msgstr "Авторы" | |
131 |
|
135 | |||
132 | #: templates/boards/authors.html:26 |
|
136 | #: templates/boards/authors.html:26 | |
133 | msgid "Distributed under the" |
|
137 | msgid "Distributed under the" | |
134 | msgstr "Распространяется под" |
|
138 | msgstr "Распространяется под" | |
135 |
|
139 | |||
136 | #: templates/boards/authors.html:28 |
|
140 | #: templates/boards/authors.html:28 | |
137 | msgid "license" |
|
141 | msgid "license" | |
138 | msgstr "лицензией" |
|
142 | msgstr "лицензией" | |
139 |
|
143 | |||
140 | #: templates/boards/authors.html:30 |
|
144 | #: templates/boards/authors.html:30 | |
141 | msgid "Repository" |
|
145 | msgid "Repository" | |
142 | msgstr "Репозиторий" |
|
146 | msgstr "Репозиторий" | |
143 |
|
147 | |||
144 |
#: templates/boards/base.html:1 |
|
148 | #: templates/boards/base.html:13 | |
145 | msgid "Feed" |
|
149 | msgid "Feed" | |
146 | msgstr "Лента" |
|
150 | msgstr "Лента" | |
147 |
|
151 | |||
148 |
#: templates/boards/base.html: |
|
152 | #: templates/boards/base.html:30 | |
149 | msgid "All threads" |
|
153 | msgid "All threads" | |
150 | msgstr "Все темы" |
|
154 | msgstr "Все темы" | |
151 |
|
155 | |||
152 |
#: templates/boards/base.html:3 |
|
156 | #: templates/boards/base.html:36 | |
153 | msgid "Tag management" |
|
157 | msgid "Tag management" | |
154 |
msgstr "Управление |
|
158 | msgstr "Управление метками" | |
155 |
|
159 | |||
156 |
#: templates/boards/base.html:3 |
|
160 | #: templates/boards/base.html:39 templates/boards/settings.html:7 | |
157 | msgid "Settings" |
|
161 | msgid "Settings" | |
158 | msgstr "Настройки" |
|
162 | msgstr "Настройки" | |
159 |
|
163 | |||
160 |
#: templates/boards/base.html:5 |
|
164 | #: templates/boards/base.html:52 | |
161 | msgid "Admin" |
|
165 | msgid "Admin" | |
162 | msgstr "" |
|
166 | msgstr "" | |
163 |
|
167 | |||
164 |
#: templates/boards/base.html:5 |
|
168 | #: templates/boards/base.html:54 | |
165 | #, python-format |
|
169 | #, python-format | |
166 | msgid "Speed: %(ppd)s posts per day" |
|
170 | msgid "Speed: %(ppd)s posts per day" | |
167 | msgstr "Скорость: %(ppd)s сообщений в день" |
|
171 | msgstr "Скорость: %(ppd)s сообщений в день" | |
168 |
|
172 | |||
169 |
#: templates/boards/base.html:5 |
|
173 | #: templates/boards/base.html:56 | |
170 | msgid "Up" |
|
174 | msgid "Up" | |
171 | msgstr "Вверх" |
|
175 | msgstr "Вверх" | |
172 |
|
176 | |||
173 | #: templates/boards/login.html:6 templates/boards/login.html.py:16 |
|
177 | #: templates/boards/login.html:6 templates/boards/login.html.py:16 | |
174 | msgid "Login" |
|
178 | msgid "Login" | |
175 | msgstr "Вход" |
|
179 | msgstr "Вход" | |
176 |
|
180 | |||
177 | #: templates/boards/login.html:19 |
|
181 | #: templates/boards/login.html:19 | |
178 | msgid "Insert your user id above" |
|
182 | msgid "Insert your user id above" | |
179 | msgstr "Вставьте свой ID пользователя выше" |
|
183 | msgstr "Вставьте свой ID пользователя выше" | |
180 |
|
184 | |||
181 |
#: templates/boards/post.html: |
|
185 | #: templates/boards/post.html:19 templates/boards/staticpages/help.html:17 | |
182 | msgid "Quote" |
|
186 | msgid "Quote" | |
183 | msgstr "Цитата" |
|
187 | msgstr "Цитата" | |
184 |
|
188 | |||
185 |
#: templates/boards/post.html: |
|
189 | #: templates/boards/post.html:27 | |
186 | msgid "Open" |
|
190 | msgid "Open" | |
187 | msgstr "Открыть" |
|
191 | msgstr "Открыть" | |
188 |
|
192 | |||
189 |
#: templates/boards/post.html: |
|
193 | #: templates/boards/post.html:29 | |
190 | msgid "Reply" |
|
194 | msgid "Reply" | |
191 | msgstr "Ответ" |
|
195 | msgstr "Ответ" | |
192 |
|
196 | |||
193 |
#: templates/boards/post.html: |
|
197 | #: templates/boards/post.html:36 | |
194 | msgid "Edit" |
|
198 | msgid "Edit" | |
195 | msgstr "Изменить" |
|
199 | msgstr "Изменить" | |
196 |
|
200 | |||
197 |
#: templates/boards/post.html: |
|
201 | #: templates/boards/post.html:39 | |
198 | msgid "Delete" |
|
202 | msgid "Edit thread" | |
199 |
msgstr " |
|
203 | msgstr "Изменить тему" | |
200 |
|
204 | |||
201 |
#: templates/boards/post.html: |
|
205 | #: templates/boards/post.html:71 | |
202 | msgid "Ban IP" |
|
|||
203 | msgstr "Заблокировать IP" |
|
|||
204 |
|
||||
205 | #: templates/boards/post.html:76 |
|
|||
206 | msgid "Replies" |
|
206 | msgid "Replies" | |
207 | msgstr "Ответы" |
|
207 | msgstr "Ответы" | |
208 |
|
208 | |||
209 |
#: templates/boards/post.html: |
|
209 | #: templates/boards/post.html:79 templates/boards/thread.html:89 | |
210 | #: templates/boards/thread_gallery.html:59 |
|
210 | #: templates/boards/thread_gallery.html:59 | |
211 | msgid "messages" |
|
211 | msgid "messages" | |
212 | msgstr "сообщений" |
|
212 | msgstr "сообщений" | |
213 |
|
213 | |||
214 |
#: templates/boards/post.html:8 |
|
214 | #: templates/boards/post.html:80 templates/boards/thread.html:90 | |
215 | #: templates/boards/thread_gallery.html:60 |
|
215 | #: templates/boards/thread_gallery.html:60 | |
216 | msgid "images" |
|
216 | msgid "images" | |
217 | msgstr "изображений" |
|
217 | msgstr "изображений" | |
218 |
|
218 | |||
219 | #: templates/boards/post_admin.html:19 |
|
219 | #: templates/boards/post_admin.html:19 | |
220 | msgid "Tags:" |
|
220 | msgid "Tags:" | |
221 |
msgstr " |
|
221 | msgstr "Метки:" | |
222 |
|
222 | |||
223 | #: templates/boards/post_admin.html:30 |
|
223 | #: templates/boards/post_admin.html:30 | |
224 | msgid "Add tag" |
|
224 | msgid "Add tag" | |
225 |
msgstr "Добавить |
|
225 | msgstr "Добавить метку" | |
226 |
|
226 | |||
227 | #: templates/boards/posting_general.html:56 |
|
227 | #: templates/boards/posting_general.html:56 | |
228 | msgid "Show tag" |
|
228 | msgid "Show tag" | |
229 |
msgstr "Показывать |
|
229 | msgstr "Показывать метку" | |
230 |
|
230 | |||
231 | #: templates/boards/posting_general.html:60 |
|
231 | #: templates/boards/posting_general.html:60 | |
232 | msgid "Hide tag" |
|
232 | msgid "Hide tag" | |
233 |
msgstr "Скрывать |
|
233 | msgstr "Скрывать метку" | |
234 |
|
234 | |||
235 |
#: templates/boards/posting_general.html: |
|
235 | #: templates/boards/posting_general.html:66 | |
|
236 | msgid "Edit tag" | |||
|
237 | msgstr "Изменить метку" | |||
|
238 | ||||
|
239 | #: templates/boards/posting_general.html:82 templates/search/search.html:22 | |||
236 | msgid "Previous page" |
|
240 | msgid "Previous page" | |
237 | msgstr "Предыдущая страница" |
|
241 | msgstr "Предыдущая страница" | |
238 |
|
242 | |||
239 |
#: templates/boards/posting_general.html:9 |
|
243 | #: templates/boards/posting_general.html:97 | |
240 | #, python-format |
|
244 | #, python-format | |
241 | msgid "Skipped %(count)s replies. Open thread to see all replies." |
|
245 | msgid "Skipped %(count)s replies. Open thread to see all replies." | |
242 | msgstr "Пропущено %(count)s ответов. Откройте тред, чтобы увидеть все ответы." |
|
246 | msgstr "Пропущено %(count)s ответов. Откройте тред, чтобы увидеть все ответы." | |
243 |
|
247 | |||
244 |
#: templates/boards/posting_general.html:12 |
|
248 | #: templates/boards/posting_general.html:124 templates/search/search.html:33 | |
245 | msgid "Next page" |
|
249 | msgid "Next page" | |
246 | msgstr "Следующая страница" |
|
250 | msgstr "Следующая страница" | |
247 |
|
251 | |||
248 |
#: templates/boards/posting_general.html:12 |
|
252 | #: templates/boards/posting_general.html:129 | |
249 | msgid "No threads exist. Create the first one!" |
|
253 | msgid "No threads exist. Create the first one!" | |
250 | msgstr "Нет тем. Создайте первую!" |
|
254 | msgstr "Нет тем. Создайте первую!" | |
251 |
|
255 | |||
252 |
#: templates/boards/posting_general.html:13 |
|
256 | #: templates/boards/posting_general.html:135 | |
253 | msgid "Create new thread" |
|
257 | msgid "Create new thread" | |
254 | msgstr "Создать новую тему" |
|
258 | msgstr "Создать новую тему" | |
255 |
|
259 | |||
256 |
#: templates/boards/posting_general.html:1 |
|
260 | #: templates/boards/posting_general.html:140 templates/boards/preview.html:16 | |
257 |
#: templates/boards/thread.html:5 |
|
261 | #: templates/boards/thread.html:54 | |
258 | msgid "Post" |
|
262 | msgid "Post" | |
259 | msgstr "Отправить" |
|
263 | msgstr "Отправить" | |
260 |
|
264 | |||
261 |
#: templates/boards/posting_general.html:14 |
|
265 | #: templates/boards/posting_general.html:145 | |
262 | msgid "Tags must be delimited by spaces. Text or image is required." |
|
266 | msgid "Tags must be delimited by spaces. Text or image is required." | |
263 | msgstr "" |
|
267 | msgstr "" | |
264 |
" |
|
268 | "Метки должны быть разделены пробелами. Текст или изображение обязательны." | |
265 |
|
269 | |||
266 |
#: templates/boards/posting_general.html:14 |
|
270 | #: templates/boards/posting_general.html:148 templates/boards/thread.html:62 | |
267 | msgid "Text syntax" |
|
271 | msgid "Text syntax" | |
268 | msgstr "Синтаксис текста" |
|
272 | msgstr "Синтаксис текста" | |
269 |
|
273 | |||
270 |
#: templates/boards/posting_general.html:1 |
|
274 | #: templates/boards/posting_general.html:160 | |
271 | msgid "Pages:" |
|
275 | msgid "Pages:" | |
272 | msgstr "Страницы: " |
|
276 | msgstr "Страницы: " | |
273 |
|
277 | |||
274 | #: templates/boards/preview.html:6 templates/boards/staticpages/help.html:19 |
|
278 | #: templates/boards/preview.html:6 templates/boards/staticpages/help.html:19 | |
275 | msgid "Preview" |
|
279 | msgid "Preview" | |
276 | msgstr "Предпросмотр" |
|
280 | msgstr "Предпросмотр" | |
277 |
|
281 | |||
|
282 | #: templates/boards/rss/post.html:5 | |||
|
283 | msgid "Post image" | |||
|
284 | msgstr "Изображение сообщения" | |||
|
285 | ||||
278 | #: templates/boards/settings.html:15 |
|
286 | #: templates/boards/settings.html:15 | |
279 | msgid "You are moderator." |
|
287 | msgid "You are moderator." | |
280 | msgstr "Вы модератор." |
|
288 | msgstr "Вы модератор." | |
281 |
|
289 | |||
282 | #: templates/boards/settings.html:19 |
|
290 | #: templates/boards/settings.html:19 | |
283 | msgid "Hidden tags:" |
|
291 | msgid "Hidden tags:" | |
284 |
msgstr "Скрытые |
|
292 | msgstr "Скрытые метки:" | |
285 |
|
293 | |||
286 | #: templates/boards/settings.html:26 |
|
294 | #: templates/boards/settings.html:26 | |
287 | msgid "No hidden tags." |
|
295 | msgid "No hidden tags." | |
288 |
msgstr "Нет скрытых |
|
296 | msgstr "Нет скрытых меток." | |
289 |
|
297 | |||
290 | #: templates/boards/settings.html:35 |
|
298 | #: templates/boards/settings.html:35 | |
291 | msgid "Save" |
|
299 | msgid "Save" | |
292 | msgstr "Сохранить" |
|
300 | msgstr "Сохранить" | |
293 |
|
301 | |||
294 | #: templates/boards/tags.html:22 |
|
|||
295 | msgid "No tags found." |
|
|||
296 | msgstr "Теги не найдены." |
|
|||
297 |
|
||||
298 | #: templates/boards/thread.html:20 templates/boards/thread_gallery.html:19 |
|
|||
299 | msgid "Normal mode" |
|
|||
300 | msgstr "Нормальный режим" |
|
|||
301 |
|
||||
302 | #: templates/boards/thread.html:21 templates/boards/thread_gallery.html:20 |
|
|||
303 | msgid "Gallery mode" |
|
|||
304 | msgstr "Режим галереи" |
|
|||
305 |
|
||||
306 | #: templates/boards/thread.html:29 |
|
|||
307 | msgid "posts to bumplimit" |
|
|||
308 | msgstr "сообщений до бамплимита" |
|
|||
309 |
|
||||
310 | #: templates/boards/thread.html:50 |
|
|||
311 | msgid "Reply to thread" |
|
|||
312 | msgstr "Ответить в тему" |
|
|||
313 |
|
||||
314 | #: templates/boards/thread.html:63 |
|
|||
315 | msgid "Switch mode" |
|
|||
316 | msgstr "Переключить режим" |
|
|||
317 |
|
||||
318 | #: templates/boards/thread.html:90 templates/boards/thread_gallery.html:61 |
|
|||
319 | msgid "Last update: " |
|
|||
320 | msgstr "Последнее обновление: " |
|
|||
321 |
|
||||
322 | #: templates/boards/rss/post.html:5 |
|
|||
323 | msgid "Post image" |
|
|||
324 | msgstr "Изображение сообщения" |
|
|||
325 |
|
||||
326 | #: templates/boards/staticpages/banned.html:6 |
|
302 | #: templates/boards/staticpages/banned.html:6 | |
327 | msgid "Banned" |
|
303 | msgid "Banned" | |
328 | msgstr "Заблокирован" |
|
304 | msgstr "Заблокирован" | |
329 |
|
305 | |||
330 | #: templates/boards/staticpages/banned.html:11 |
|
306 | #: templates/boards/staticpages/banned.html:11 | |
331 | msgid "Your IP address has been banned. Contact the administrator" |
|
307 | msgid "Your IP address has been banned. Contact the administrator" | |
332 | msgstr "Ваш IP адрес был заблокирован. Свяжитесь с администратором" |
|
308 | msgstr "Ваш IP адрес был заблокирован. Свяжитесь с администратором" | |
333 |
|
309 | |||
334 | #: templates/boards/staticpages/help.html:6 |
|
310 | #: templates/boards/staticpages/help.html:6 | |
335 | #: templates/boards/staticpages/help.html:10 |
|
311 | #: templates/boards/staticpages/help.html:10 | |
336 | msgid "Syntax" |
|
312 | msgid "Syntax" | |
337 | msgstr "Синтаксис" |
|
313 | msgstr "Синтаксис" | |
338 |
|
314 | |||
339 | #: templates/boards/staticpages/help.html:11 |
|
315 | #: templates/boards/staticpages/help.html:11 | |
340 | msgid "Italic text" |
|
316 | msgid "Italic text" | |
341 | msgstr "Курсивный текст" |
|
317 | msgstr "Курсивный текст" | |
342 |
|
318 | |||
343 | #: templates/boards/staticpages/help.html:12 |
|
319 | #: templates/boards/staticpages/help.html:12 | |
344 | msgid "Bold text" |
|
320 | msgid "Bold text" | |
345 | msgstr "Полужирный текст" |
|
321 | msgstr "Полужирный текст" | |
346 |
|
322 | |||
347 | #: templates/boards/staticpages/help.html:13 |
|
323 | #: templates/boards/staticpages/help.html:13 | |
348 | msgid "Spoiler" |
|
324 | msgid "Spoiler" | |
349 | msgstr "Спойлер" |
|
325 | msgstr "Спойлер" | |
350 |
|
326 | |||
351 | #: templates/boards/staticpages/help.html:14 |
|
327 | #: templates/boards/staticpages/help.html:14 | |
352 | msgid "Link to a post" |
|
328 | msgid "Link to a post" | |
353 | msgstr "Ссылка на сообщение" |
|
329 | msgstr "Ссылка на сообщение" | |
354 |
|
330 | |||
355 | #: templates/boards/staticpages/help.html:15 |
|
331 | #: templates/boards/staticpages/help.html:15 | |
356 | msgid "Strikethrough text" |
|
332 | msgid "Strikethrough text" | |
357 | msgstr "Зачеркнутый текст" |
|
333 | msgstr "Зачеркнутый текст" | |
358 |
|
334 | |||
359 | #: templates/boards/staticpages/help.html:16 |
|
335 | #: templates/boards/staticpages/help.html:16 | |
360 | msgid "Comment" |
|
336 | msgid "Comment" | |
361 | msgstr "Комментарий" |
|
337 | msgstr "Комментарий" | |
362 |
|
338 | |||
363 | #: templates/boards/staticpages/help.html:19 |
|
339 | #: templates/boards/staticpages/help.html:19 | |
364 | msgid "You can try pasting the text and previewing the result here:" |
|
340 | msgid "You can try pasting the text and previewing the result here:" | |
365 | msgstr "Вы можете попробовать вставить текст и проверить результат здесь:" |
|
341 | msgstr "Вы можете попробовать вставить текст и проверить результат здесь:" | |
|
342 | ||||
|
343 | #: templates/boards/tags.html:23 | |||
|
344 | msgid "No tags found." | |||
|
345 | msgstr "Метки не найдены." | |||
|
346 | ||||
|
347 | #: templates/boards/thread.html:19 templates/boards/thread_gallery.html:19 | |||
|
348 | msgid "Normal mode" | |||
|
349 | msgstr "Нормальный режим" | |||
|
350 | ||||
|
351 | #: templates/boards/thread.html:20 templates/boards/thread_gallery.html:20 | |||
|
352 | msgid "Gallery mode" | |||
|
353 | msgstr "Режим галереи" | |||
|
354 | ||||
|
355 | #: templates/boards/thread.html:28 | |||
|
356 | msgid "posts to bumplimit" | |||
|
357 | msgstr "сообщений до бамплимита" | |||
|
358 | ||||
|
359 | #: templates/boards/thread.html:46 | |||
|
360 | msgid "Reply to thread" | |||
|
361 | msgstr "Ответить в тему" | |||
|
362 | ||||
|
363 | #: templates/boards/thread.html:59 | |||
|
364 | msgid "Switch mode" | |||
|
365 | msgstr "Переключить режим" | |||
|
366 | ||||
|
367 | #: templates/boards/thread.html:91 templates/boards/thread_gallery.html:61 | |||
|
368 | msgid "Last update: " | |||
|
369 | msgstr "Последнее обновление: " | |||
|
370 |
@@ -1,201 +1,202 b'' | |||||
1 | # coding=utf-8 |
|
1 | # coding=utf-8 | |
2 |
|
2 | |||
3 | import re |
|
3 | import re | |
4 | import bbcode |
|
4 | import bbcode | |
5 |
|
5 | |||
6 | import boards |
|
6 | import boards | |
7 |
|
7 | |||
8 |
|
8 | |||
9 | __author__ = 'neko259' |
|
9 | __author__ = 'neko259' | |
10 |
|
10 | |||
11 |
|
11 | |||
12 | REFLINK_PATTERN = re.compile(r'^\d+$') |
|
12 | REFLINK_PATTERN = re.compile(r'^\d+$') | |
13 | GLOBAL_REFLINK_PATTERN = re.compile(r'^(\w+)::([^:]+)::(\d+)$') |
|
13 | GLOBAL_REFLINK_PATTERN = re.compile(r'^(\w+)::([^:]+)::(\d+)$') | |
14 | MULTI_NEWLINES_PATTERN = re.compile(r'(\r?\n){2,}') |
|
14 | MULTI_NEWLINES_PATTERN = re.compile(r'(\r?\n){2,}') | |
15 | ONE_NEWLINE = '\n' |
|
15 | ONE_NEWLINE = '\n' | |
16 |
|
16 | |||
17 |
|
17 | |||
18 | class TextFormatter(): |
|
18 | class TextFormatter(): | |
19 | """ |
|
19 | """ | |
20 | An interface for formatter that can be used in the text format panel |
|
20 | An interface for formatter that can be used in the text format panel | |
21 | """ |
|
21 | """ | |
22 |
|
22 | |||
23 | def __init__(self): |
|
23 | def __init__(self): | |
24 | pass |
|
24 | pass | |
25 |
|
25 | |||
26 | name = '' |
|
26 | name = '' | |
27 |
|
27 | |||
28 | # Left and right tags for the button preview |
|
28 | # Left and right tags for the button preview | |
29 | preview_left = '' |
|
29 | preview_left = '' | |
30 | preview_right = '' |
|
30 | preview_right = '' | |
31 |
|
31 | |||
32 | # Left and right characters for the textarea input |
|
32 | # Left and right characters for the textarea input | |
33 | format_left = '' |
|
33 | format_left = '' | |
34 | format_right = '' |
|
34 | format_right = '' | |
35 |
|
35 | |||
36 |
|
36 | |||
37 | class AutolinkPattern(): |
|
37 | class AutolinkPattern(): | |
38 | def handleMatch(self, m): |
|
38 | def handleMatch(self, m): | |
39 | link_element = etree.Element('a') |
|
39 | link_element = etree.Element('a') | |
40 | href = m.group(2) |
|
40 | href = m.group(2) | |
41 | link_element.set('href', href) |
|
41 | link_element.set('href', href) | |
42 | link_element.text = href |
|
42 | link_element.text = href | |
43 |
|
43 | |||
44 | return link_element |
|
44 | return link_element | |
45 |
|
45 | |||
46 |
|
46 | |||
47 | class QuotePattern(TextFormatter): |
|
47 | class QuotePattern(TextFormatter): | |
48 | name = 'q' |
|
48 | name = 'q' | |
49 | preview_left = '<span class="multiquote">' |
|
49 | preview_left = '<span class="multiquote">' | |
50 | preview_right = '</span>' |
|
50 | preview_right = '</span>' | |
51 |
|
51 | |||
52 | format_left = '[quote]' |
|
52 | format_left = '[quote]' | |
53 | format_right = '[/quote]' |
|
53 | format_right = '[/quote]' | |
54 |
|
54 | |||
55 |
|
55 | |||
56 | class SpoilerPattern(TextFormatter): |
|
56 | class SpoilerPattern(TextFormatter): | |
57 | name = 'spoiler' |
|
57 | name = 'spoiler' | |
58 | preview_left = '<span class="spoiler">' |
|
58 | preview_left = '<span class="spoiler">' | |
59 | preview_right = '</span>' |
|
59 | preview_right = '</span>' | |
60 |
|
60 | |||
61 | format_left = '[spoiler]' |
|
61 | format_left = '[spoiler]' | |
62 | format_right = '[/spoiler]' |
|
62 | format_right = '[/spoiler]' | |
63 |
|
63 | |||
64 | def handleMatch(self, m): |
|
64 | def handleMatch(self, m): | |
65 | quote_element = etree.Element('span') |
|
65 | quote_element = etree.Element('span') | |
66 | quote_element.set('class', 'spoiler') |
|
66 | quote_element.set('class', 'spoiler') | |
67 | quote_element.text = m.group(2) |
|
67 | quote_element.text = m.group(2) | |
68 |
|
68 | |||
69 | return quote_element |
|
69 | return quote_element | |
70 |
|
70 | |||
71 |
|
71 | |||
72 | class CommentPattern(TextFormatter): |
|
72 | class CommentPattern(TextFormatter): | |
73 | name = '' |
|
73 | name = '' | |
74 | preview_left = '<span class="comment">// ' |
|
74 | preview_left = '<span class="comment">// ' | |
75 | preview_right = '</span>' |
|
75 | preview_right = '</span>' | |
76 |
|
76 | |||
77 | format_left = '[comment]' |
|
77 | format_left = '[comment]' | |
78 | format_right = '[/comment]' |
|
78 | format_right = '[/comment]' | |
79 |
|
79 | |||
80 |
|
80 | |||
81 | # TODO Use <s> tag here |
|
81 | # TODO Use <s> tag here | |
82 | class StrikeThroughPattern(TextFormatter): |
|
82 | class StrikeThroughPattern(TextFormatter): | |
83 | name = 's' |
|
83 | name = 's' | |
84 | preview_left = '<span class="strikethrough">' |
|
84 | preview_left = '<span class="strikethrough">' | |
85 | preview_right = '</span>' |
|
85 | preview_right = '</span>' | |
86 |
|
86 | |||
87 | format_left = '[s]' |
|
87 | format_left = '[s]' | |
88 | format_right = '[/s]' |
|
88 | format_right = '[/s]' | |
89 |
|
89 | |||
90 |
|
90 | |||
91 | class ItalicPattern(TextFormatter): |
|
91 | class ItalicPattern(TextFormatter): | |
92 | name = 'i' |
|
92 | name = 'i' | |
93 | preview_left = '<i>' |
|
93 | preview_left = '<i>' | |
94 | preview_right = '</i>' |
|
94 | preview_right = '</i>' | |
95 |
|
95 | |||
96 | format_left = '[i]' |
|
96 | format_left = '[i]' | |
97 | format_right = '[/i]' |
|
97 | format_right = '[/i]' | |
98 |
|
98 | |||
99 |
|
99 | |||
100 | class BoldPattern(TextFormatter): |
|
100 | class BoldPattern(TextFormatter): | |
101 | name = 'b' |
|
101 | name = 'b' | |
102 | preview_left = '<b>' |
|
102 | preview_left = '<b>' | |
103 | preview_right = '</b>' |
|
103 | preview_right = '</b>' | |
104 |
|
104 | |||
105 | format_left = '[b]' |
|
105 | format_left = '[b]' | |
106 | format_right = '[/b]' |
|
106 | format_right = '[/b]' | |
107 |
|
107 | |||
108 |
|
108 | |||
109 | class CodePattern(TextFormatter): |
|
109 | class CodePattern(TextFormatter): | |
110 | name = 'code' |
|
110 | name = 'code' | |
111 | preview_left = '<code>' |
|
111 | preview_left = '<code>' | |
112 | preview_right = '</code>' |
|
112 | preview_right = '</code>' | |
113 |
|
113 | |||
114 | format_left = '[code]' |
|
114 | format_left = '[code]' | |
115 | format_right = '[/code]' |
|
115 | format_right = '[/code]' | |
116 |
|
116 | |||
117 |
|
117 | |||
118 | def render_reflink(tag_name, value, options, parent, context): |
|
118 | def render_reflink(tag_name, value, options, parent, context): | |
119 | post_id = None |
|
119 | post_id = None | |
120 |
|
120 | |||
121 | matches = REFLINK_PATTERN.findall(value) |
|
121 | matches = REFLINK_PATTERN.findall(value) | |
122 | if matches: |
|
122 | if matches: | |
123 | post_id = int(matches[0][0]) |
|
123 | post_id = int(matches[0][0]) | |
124 | else: |
|
124 | else: | |
125 | match = GLOBAL_REFLINK_PATTERN.match(value) |
|
125 | match = GLOBAL_REFLINK_PATTERN.match(value) | |
126 | if match: |
|
126 | if match: | |
127 | key_type = match.group(1) |
|
127 | key_type = match.group(1) | |
128 | key = match.group(2) |
|
128 | key = match.group(2) | |
129 | local_id = match.group(3) |
|
129 | local_id = match.group(3) | |
130 |
|
130 | |||
131 | try: |
|
131 | try: | |
132 | global_id = boards.models.GlobalId.objects.get(key_type=key_type, |
|
132 | global_id = boards.models.GlobalId.objects.get(key_type=key_type, | |
133 | key=key, local_id=local_id) |
|
133 | key=key, local_id=local_id) | |
134 | for post in boards.models.Post.objects.filter(global_id=global_id).only('id'): |
|
134 | for post in boards.models.Post.objects.filter(global_id=global_id).only('id'): | |
135 | post_id = post.id |
|
135 | post_id = post.id | |
136 | except boards.models.GlobalId.DoesNotExist: |
|
136 | except boards.models.GlobalId.DoesNotExist: | |
137 | pass |
|
137 | pass | |
138 |
|
138 | |||
139 | if not post_id: |
|
139 | if not post_id: | |
140 | return value |
|
140 | return value | |
141 |
|
141 | |||
142 | posts = boards.models.Post.objects.filter(id=post_id) |
|
142 | posts = boards.models.Post.objects.filter(id=post_id) | |
143 | if posts.exists(): |
|
143 | if posts.exists(): | |
144 | post = posts[0] |
|
144 | post = posts[0] | |
145 |
|
145 | |||
146 | return '<a href="%s">>>%s</a>' % (post.get_url(), post_id) |
|
146 | return '<a href="%s">>>%s</a>' % (post.get_url(), post_id) | |
147 | else: |
|
147 | else: | |
148 | return '>>%s' % value |
|
148 | return '>>%s' % value | |
149 |
|
149 | |||
150 |
|
150 | |||
151 | def render_quote(tag_name, value, options, parent, context): |
|
151 | def render_quote(tag_name, value, options, parent, context): | |
152 | source = '' |
|
152 | source = '' | |
153 | if 'source' in options: |
|
153 | if 'source' in options: | |
154 | source = options['source'] |
|
154 | source = options['source'] | |
155 |
|
155 | |||
156 | result = '' |
|
156 | result = '' | |
157 | if source: |
|
157 | if source: | |
158 | result = '<div class="multiquote"><div class="quote-header">%s</div><div class="quote-text">%s</div></div>' % (source, value) |
|
158 | result = '<div class="multiquote"><div class="quote-header">%s</div><div class="quote-text">%s</div></div>' % (source, value) | |
159 | else: |
|
159 | else: | |
160 | result = '<div class="multiquote"><div class="quote-text">%s</div></div>' % value |
|
160 | result = '<div class="multiquote"><div class="quote-text">%s</div></div>' % value | |
161 |
|
161 | |||
162 | return result |
|
162 | return result | |
163 |
|
163 | |||
164 |
|
164 | |||
165 | def preparse_text(text): |
|
165 | def preparse_text(text): | |
166 | """ |
|
166 | """ | |
167 | Performs manual parsing before the bbcode parser is used. |
|
167 | Performs manual parsing before the bbcode parser is used. | |
168 | """ |
|
168 | """ | |
169 |
|
169 | |||
170 | return MULTI_NEWLINES_PATTERN.sub(ONE_NEWLINE, text) |
|
170 | return MULTI_NEWLINES_PATTERN.sub(ONE_NEWLINE, text) | |
171 |
|
171 | |||
172 |
|
172 | |||
173 | def bbcode_extended(markup): |
|
173 | def bbcode_extended(markup): | |
174 | # The newline hack is added because br's margin does not work in all |
|
174 | # The newline hack is added because br's margin does not work in all | |
175 | # browsers except firefox, when the div's does. |
|
175 | # browsers except firefox, when the div's does. | |
176 | parser = bbcode.Parser(newline='<div class="br"></div>') |
|
176 | parser = bbcode.Parser(newline='<div class="br"></div>') | |
177 | parser.add_formatter('post', render_reflink, strip=True) |
|
177 | parser.add_formatter('post', render_reflink, strip=True) | |
178 | parser.add_formatter('quote', render_quote, strip=True) |
|
178 | parser.add_formatter('quote', render_quote, strip=True) | |
179 | parser.add_simple_formatter('comment', |
|
179 | parser.add_simple_formatter('comment', | |
180 |
|
|
180 | '<span class="comment">//%(value)s</span>') | |
181 | parser.add_simple_formatter('spoiler', |
|
181 | parser.add_simple_formatter('spoiler', | |
182 |
|
|
182 | '<span class="spoiler">%(value)s</span>') | |
183 | # TODO Use <s> here |
|
183 | # TODO Use <s> here | |
184 | parser.add_simple_formatter('s', |
|
184 | parser.add_simple_formatter('s', | |
185 |
|
|
185 | '<span class="strikethrough">%(value)s</span>') | |
186 | # TODO Why not use built-in tag? |
|
186 | # TODO Why not use built-in tag? | |
187 | parser.add_simple_formatter('code', |
|
187 | parser.add_simple_formatter('code', | |
188 |
|
|
188 | '<pre><code>%(value)s</pre></code>', | |
|
189 | render_embedded=False) | |||
189 |
|
190 | |||
190 | text = preparse_text(markup) |
|
191 | text = preparse_text(markup) | |
191 | return parser.format(text) |
|
192 | return parser.format(text) | |
192 |
|
193 | |||
193 | formatters = [ |
|
194 | formatters = [ | |
194 | QuotePattern, |
|
195 | QuotePattern, | |
195 | SpoilerPattern, |
|
196 | SpoilerPattern, | |
196 | ItalicPattern, |
|
197 | ItalicPattern, | |
197 | BoldPattern, |
|
198 | BoldPattern, | |
198 | CommentPattern, |
|
199 | CommentPattern, | |
199 | StrikeThroughPattern, |
|
200 | StrikeThroughPattern, | |
200 | CodePattern, |
|
201 | CodePattern, | |
201 | ] |
|
202 | ] |
@@ -1,46 +1,28 b'' | |||||
1 | from django.shortcuts import redirect |
|
1 | from django.shortcuts import redirect | |
2 | from boards import utils |
|
2 | from boards import utils | |
3 | from boards.models import Ban |
|
3 | from boards.models import Ban | |
4 | from django.utils.html import strip_spaces_between_tags |
|
|||
5 | from django.conf import settings |
|
|||
6 | from boards.views.banned import BannedView |
|
|||
7 |
|
4 | |||
8 | RESPONSE_CONTENT_TYPE = 'Content-Type' |
|
5 | RESPONSE_CONTENT_TYPE = 'Content-Type' | |
9 |
|
6 | |||
10 | TYPE_HTML = 'text/html' |
|
7 | TYPE_HTML = 'text/html' | |
11 |
|
8 | |||
12 |
|
9 | |||
13 | class BanMiddleware: |
|
10 | class BanMiddleware: | |
14 | """ |
|
11 | """ | |
15 | This is run before showing the thread. Banned users don't need to see |
|
12 | This is run before showing the thread. Banned users don't need to see | |
16 | anything |
|
13 | anything | |
17 | """ |
|
14 | """ | |
18 |
|
15 | |||
19 | def __init__(self): |
|
16 | def __init__(self): | |
20 | pass |
|
17 | pass | |
21 |
|
18 | |||
22 | def process_view(self, request, view_func, view_args, view_kwargs): |
|
19 | def process_view(self, request, view_func, view_args, view_kwargs): | |
23 |
|
20 | |||
24 | if view_func != BannedView.as_view: |
|
21 | if request.path != '/banned/': | |
25 | ip = utils.get_client_ip(request) |
|
22 | ip = utils.get_client_ip(request) | |
26 | bans = Ban.objects.filter(ip=ip) |
|
23 | bans = Ban.objects.filter(ip=ip) | |
27 |
|
24 | |||
28 | if bans.exists(): |
|
25 | if bans.exists(): | |
29 | ban = bans[0] |
|
26 | ban = bans[0] | |
30 | if not ban.can_read: |
|
27 | if not ban.can_read: | |
31 | return redirect('banned') |
|
28 | return redirect('banned') | |
32 |
|
||||
33 |
|
||||
34 | class MinifyHTMLMiddleware(object): |
|
|||
35 | def process_response(self, request, response): |
|
|||
36 | try: |
|
|||
37 | compress_html = settings.COMPRESS_HTML |
|
|||
38 | except AttributeError: |
|
|||
39 | compress_html = False |
|
|||
40 |
|
||||
41 | if RESPONSE_CONTENT_TYPE in response\ |
|
|||
42 | and TYPE_HTML in response[RESPONSE_CONTENT_TYPE]\ |
|
|||
43 | and compress_html: |
|
|||
44 | response.content = strip_spaces_between_tags( |
|
|||
45 | response.content.strip()) |
|
|||
46 | return response No newline at end of file |
|
@@ -1,95 +1,113 b'' | |||||
1 | # -*- coding: utf-8 -*- |
|
1 | # -*- coding: utf-8 -*- | |
2 | import datetime |
|
2 | from __future__ import unicode_literals | |
3 | from south.db import db |
|
3 | ||
4 |
from |
|
4 | from django.db import models, migrations | |
5 | from django.db import models |
|
5 | import boards.models.image | |
|
6 | import boards.models.base | |||
|
7 | import boards.thumbs | |||
6 |
|
8 | |||
7 |
|
9 | |||
8 |
class Migration( |
|
10 | class Migration(migrations.Migration): | |
9 |
|
||||
10 | def forwards(self, orm): |
|
|||
11 | # Adding model 'Tag' |
|
|||
12 | db.create_table(u'boards_tag', ( |
|
|||
13 | (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), |
|
|||
14 | ('name', self.gf('django.db.models.fields.CharField')(max_length=100)), |
|
|||
15 | )) |
|
|||
16 | db.send_create_signal(u'boards', ['Tag']) |
|
|||
17 |
|
11 | |||
18 | # Adding model 'Post' |
|
12 | dependencies = [ | |
19 | db.create_table(u'boards_post', ( |
|
13 | ] | |
20 | (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), |
|
|||
21 | ('title', self.gf('django.db.models.fields.CharField')(max_length=50)), |
|
|||
22 | ('pub_time', self.gf('django.db.models.fields.DateTimeField')()), |
|
|||
23 | ('text', self.gf('markupfield.fields.MarkupField')(rendered_field=True)), |
|
|||
24 | ('text_markup_type', self.gf('django.db.models.fields.CharField')(default='markdown', max_length=30)), |
|
|||
25 | ('image', self.gf('boards.thumbs.ImageWithThumbsField')(max_length=100, blank=True)), |
|
|||
26 | ('poster_ip', self.gf('django.db.models.fields.IPAddressField')(max_length=15)), |
|
|||
27 | ('_text_rendered', self.gf('django.db.models.fields.TextField')()), |
|
|||
28 | ('poster_user_agent', self.gf('django.db.models.fields.TextField')()), |
|
|||
29 | ('parent', self.gf('django.db.models.fields.BigIntegerField')()), |
|
|||
30 | ('last_edit_time', self.gf('django.db.models.fields.DateTimeField')()), |
|
|||
31 | )) |
|
|||
32 | db.send_create_signal(u'boards', ['Post']) |
|
|||
33 |
|
||||
34 | # Adding M2M table for field tags on 'Post' |
|
|||
35 | m2m_table_name = db.shorten_name(u'boards_post_tags') |
|
|||
36 | db.create_table(m2m_table_name, ( |
|
|||
37 | ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), |
|
|||
38 | ('post', models.ForeignKey(orm[u'boards.post'], null=False)), |
|
|||
39 | ('tag', models.ForeignKey(orm[u'boards.tag'], null=False)) |
|
|||
40 | )) |
|
|||
41 | db.create_unique(m2m_table_name, ['post_id', 'tag_id']) |
|
|||
42 |
|
||||
43 | # Adding model 'Admin' |
|
|||
44 | db.create_table(u'boards_admin', ( |
|
|||
45 | (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), |
|
|||
46 | ('name', self.gf('django.db.models.fields.CharField')(max_length=100)), |
|
|||
47 | ('password', self.gf('django.db.models.fields.CharField')(max_length=100)), |
|
|||
48 | )) |
|
|||
49 | db.send_create_signal(u'boards', ['Admin']) |
|
|||
50 |
|
||||
51 |
|
14 | |||
52 | def backwards(self, orm): |
|
15 | operations = [ | |
53 | # Deleting model 'Tag' |
|
16 | migrations.CreateModel( | |
54 | db.delete_table(u'boards_tag') |
|
17 | name='Ban', | |
55 |
|
18 | fields=[ | ||
56 | # Deleting model 'Post' |
|
19 | ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')), | |
57 | db.delete_table(u'boards_post') |
|
20 | ('ip', models.GenericIPAddressField()), | |
58 |
|
21 | ('reason', models.CharField(max_length=200, default='Auto')), | ||
59 | # Removing M2M table for field tags on 'Post' |
|
22 | ('can_read', models.BooleanField(default=True)), | |
60 | db.delete_table(db.shorten_name(u'boards_post_tags')) |
|
23 | ], | |
61 |
|
24 | options={ | ||
62 | # Deleting model 'Admin' |
|
25 | }, | |
63 | db.delete_table(u'boards_admin') |
|
26 | bases=(models.Model,), | |
64 |
|
27 | ), | ||
65 |
|
28 | migrations.CreateModel( | ||
66 | models = { |
|
29 | name='Post', | |
67 | u'boards.admin': { |
|
30 | fields=[ | |
68 | 'Meta': {'object_name': 'Admin'}, |
|
31 | ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')), | |
69 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
|
32 | ('title', models.CharField(max_length=200)), | |
70 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), |
|
33 | ('pub_time', models.DateTimeField()), | |
71 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '100'}) |
|
34 | ('text', models.TextField(null=True, blank=True)), | |
|
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',), | |||
72 | }, |
|
60 | }, | |
73 | u'boards.post': { |
|
61 | bases=(models.Model,), | |
74 | 'Meta': {'object_name': 'Post'}, |
|
62 | ), | |
75 | '_text_rendered': ('django.db.models.fields.TextField', [], {}), |
|
63 | migrations.CreateModel( | |
76 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
|
64 | name='Tag', | |
77 | 'image': ('boards.thumbs.ImageWithThumbsField', [], {'max_length': '100', 'blank': 'True'}), |
|
65 | fields=[ | |
78 | 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}), |
|
66 | ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')), | |
79 | 'parent': ('django.db.models.fields.BigIntegerField', [], {}), |
|
67 | ('name', models.CharField(db_index=True, max_length=100)), | |
80 | 'poster_ip': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}), |
|
68 | ], | |
81 | 'poster_user_agent': ('django.db.models.fields.TextField', [], {}), |
|
69 | options={ | |
82 | 'pub_time': ('django.db.models.fields.DateTimeField', [], {}), |
|
70 | 'ordering': ('name',), | |
83 | 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['boards.Tag']", 'symmetrical': 'False'}), |
|
71 | }, | |
84 | 'text': ('markupfield.fields.MarkupField', [], {'rendered_field': 'True'}), |
|
72 | bases=(models.Model, boards.models.base.Viewable), | |
85 | 'text_markup_type': ('django.db.models.fields.CharField', [], {'default': "'markdown'", 'max_length': '30'}), |
|
73 | ), | |
86 | 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}) |
|
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={ | |||
87 | }, |
|
86 | }, | |
88 | u'boards.tag': { |
|
87 | bases=(models.Model,), | |
89 | 'Meta': {'object_name': 'Tag'}, |
|
88 | ), | |
90 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
|
89 | migrations.AddField( | |
91 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) |
|
90 | model_name='tag', | |
92 | } |
|
91 | name='threads', | |
93 | } |
|
92 | field=models.ManyToManyField(null=True, related_name='tag+', to='boards.Thread', blank=True), | |
94 |
|
93 | preserve_default=True, | ||
95 | complete_apps = ['boards'] No newline at end of file |
|
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 | __author__ = 'neko259' |
|
1 | __author__ = 'neko259' | |
2 |
|
2 | |||
3 |
|
3 | |||
4 | class Viewable(): |
|
4 | class Viewable(): | |
5 | def __init__(self): |
|
5 | def __init__(self): | |
6 | pass |
|
6 | pass | |
7 |
|
7 | |||
8 | def get_view(self, *args, **kwargs): |
|
8 | def get_view(self, *args, **kwargs): | |
9 | """Get an HTML view for a model""" |
|
9 | """ | |
10 | pass No newline at end of file |
|
10 | Gets an HTML view for a model | |
|
11 | """ | |||
|
12 | pass | |||
|
13 | ||||
|
14 | def get_search_view(self, *args, **kwargs): | |||
|
15 | """ | |||
|
16 | Gets an HTML view for search. | |||
|
17 | """ | |||
|
18 | pass |
@@ -1,62 +1,83 b'' | |||||
1 | import hashlib |
|
1 | import hashlib | |
2 | import os |
|
2 | import os | |
3 | from random import random |
|
3 | from random import random | |
4 | import time |
|
4 | import time | |
5 | from django.db import models |
|
5 | from django.db import models | |
6 | from boards import thumbs |
|
6 | from boards import thumbs | |
|
7 | from boards.models.base import Viewable | |||
7 |
|
8 | |||
8 | __author__ = 'neko259' |
|
9 | __author__ = 'neko259' | |
9 |
|
10 | |||
10 |
|
11 | |||
11 | IMAGE_THUMB_SIZE = (200, 150) |
|
12 | IMAGE_THUMB_SIZE = (200, 150) | |
12 | IMAGES_DIRECTORY = 'images/' |
|
13 | IMAGES_DIRECTORY = 'images/' | |
13 | FILE_EXTENSION_DELIMITER = '.' |
|
14 | FILE_EXTENSION_DELIMITER = '.' | |
|
15 | HASH_LENGTH = 36 | |||
|
16 | ||||
|
17 | CSS_CLASS_IMAGE = 'image' | |||
|
18 | CSS_CLASS_THUMB = 'thumb' | |||
14 |
|
19 | |||
15 |
|
20 | |||
16 | class PostImage(models.Model): |
|
21 | class PostImage(models.Model, Viewable): | |
17 | class Meta: |
|
22 | class Meta: | |
18 | app_label = 'boards' |
|
23 | app_label = 'boards' | |
19 | ordering = ('id',) |
|
24 | ordering = ('id',) | |
20 |
|
25 | |||
21 | def _update_image_filename(self, filename): |
|
26 | def _update_image_filename(self, filename): | |
22 | """ |
|
27 | """ | |
23 | Gets unique image filename |
|
28 | Gets unique image filename | |
24 | """ |
|
29 | """ | |
25 |
|
30 | |||
26 | path = IMAGES_DIRECTORY |
|
31 | path = IMAGES_DIRECTORY | |
27 | new_name = str(int(time.mktime(time.gmtime()))) |
|
32 | new_name = str(int(time.mktime(time.gmtime()))) | |
28 | new_name += str(int(random() * 1000)) |
|
33 | new_name += str(int(random() * 1000)) | |
29 | new_name += FILE_EXTENSION_DELIMITER |
|
34 | new_name += FILE_EXTENSION_DELIMITER | |
30 | new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0] |
|
35 | new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0] | |
31 |
|
36 | |||
32 | return os.path.join(path, new_name) |
|
37 | return os.path.join(path, new_name) | |
33 |
|
38 | |||
34 | width = models.IntegerField(default=0) |
|
39 | width = models.IntegerField(default=0) | |
35 | height = models.IntegerField(default=0) |
|
40 | height = models.IntegerField(default=0) | |
36 |
|
41 | |||
37 | pre_width = models.IntegerField(default=0) |
|
42 | pre_width = models.IntegerField(default=0) | |
38 | pre_height = models.IntegerField(default=0) |
|
43 | pre_height = models.IntegerField(default=0) | |
39 |
|
44 | |||
40 | image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename, |
|
45 | image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename, | |
41 | blank=True, sizes=(IMAGE_THUMB_SIZE,), |
|
46 | blank=True, sizes=(IMAGE_THUMB_SIZE,), | |
42 | width_field='width', |
|
47 | width_field='width', | |
43 | height_field='height', |
|
48 | height_field='height', | |
44 | preview_width_field='pre_width', |
|
49 | preview_width_field='pre_width', | |
45 | preview_height_field='pre_height') |
|
50 | preview_height_field='pre_height') | |
46 |
hash = models.CharField(max_length= |
|
51 | hash = models.CharField(max_length=HASH_LENGTH) | |
47 |
|
52 | |||
48 | def save(self, *args, **kwargs): |
|
53 | def save(self, *args, **kwargs): | |
49 | """ |
|
54 | """ | |
50 | Saves the model and computes the image hash for deduplication purposes. |
|
55 | Saves the model and computes the image hash for deduplication purposes. | |
51 | """ |
|
56 | """ | |
52 |
|
57 | |||
53 | if not self.pk and self.image: |
|
58 | if not self.pk and self.image: | |
54 | md5 = hashlib.md5() |
|
59 | md5 = hashlib.md5() | |
55 | for chunk in self.image.chunks(): |
|
60 | for chunk in self.image.chunks(): | |
56 | md5.update(chunk) |
|
61 | md5.update(chunk) | |
57 | self.hash = md5.hexdigest() |
|
62 | self.hash = md5.hexdigest() | |
58 | super(PostImage, self).save(*args, **kwargs) |
|
63 | super(PostImage, self).save(*args, **kwargs) | |
59 |
|
64 | |||
60 | def __str__(self): |
|
65 | def __str__(self): | |
61 | return self.image.url |
|
66 | return self.image.url | |
62 |
|
67 | |||
|
68 | def get_view(self): | |||
|
69 | return '<div class="{}">' \ | |||
|
70 | '<a class="{}" href="{}">' \ | |||
|
71 | '<img' \ | |||
|
72 | ' src="{}"' \ | |||
|
73 | ' alt="{}"' \ | |||
|
74 | ' width="{}"' \ | |||
|
75 | ' height="{}"' \ | |||
|
76 | ' data-width="{}"' \ | |||
|
77 | ' data-height="{}" />' \ | |||
|
78 | '</a>' \ | |||
|
79 | '</div>'\ | |||
|
80 | .format(CSS_CLASS_IMAGE, CSS_CLASS_THUMB, self.image.url, | |||
|
81 | self.image.url_200x150, | |||
|
82 | str(self.hash), str(self.pre_width), | |||
|
83 | str(self.pre_height), str(self.width), str(self.height)) |
@@ -1,518 +1,623 b'' | |||||
1 | from datetime import datetime, timedelta, date |
|
1 | from datetime import datetime, timedelta, date | |
2 | from datetime import time as dtime |
|
2 | from datetime import time as dtime | |
3 | import logging |
|
3 | import logging | |
4 | import re |
|
4 | import re | |
5 | import xml.etree.ElementTree as et |
|
5 | import xml.etree.ElementTree as et | |
6 |
|
6 | |||
|
7 | from adjacent import Client | |||
7 | from django.core.cache import cache |
|
8 | from django.core.cache import cache | |
8 | from django.core.urlresolvers import reverse |
|
9 | from django.core.urlresolvers import reverse | |
9 | from django.db import models, transaction |
|
10 | from django.db import models, transaction | |
|
11 | from django.db.models import TextField | |||
10 | from django.template.loader import render_to_string |
|
12 | from django.template.loader import render_to_string | |
11 | from django.utils import timezone |
|
13 | from django.utils import timezone | |
12 |
|
14 | |||
13 | from markupfield.fields import MarkupField |
|
|||
14 |
|
||||
15 | from boards.models import PostImage, KeyPair, GlobalId, Signature |
|
15 | from boards.models import PostImage, KeyPair, GlobalId, Signature | |
|
16 | from boards import settings | |||
|
17 | from boards.mdx_neboard import bbcode_extended | |||
|
18 | from boards.models import PostImage | |||
16 | from boards.models.base import Viewable |
|
19 | from boards.models.base import Viewable | |
17 | from boards.models.thread import Thread |
|
20 | from boards.models.thread import Thread | |
18 | from boards import utils |
|
21 | from boards import utils | |
|
22 | from boards.utils import datetime_to_epoch | |||
19 |
|
23 | |||
20 | ENCODING_UNICODE = 'unicode' |
|
24 | ENCODING_UNICODE = 'unicode' | |
21 |
|
25 | |||
|
26 | WS_NOTIFICATION_TYPE_NEW_POST = 'new_post' | |||
|
27 | WS_NOTIFICATION_TYPE = 'notification_type' | |||
|
28 | ||||
|
29 | WS_CHANNEL_THREAD = "thread:" | |||
|
30 | ||||
22 | APP_LABEL_BOARDS = 'boards' |
|
31 | APP_LABEL_BOARDS = 'boards' | |
23 |
|
32 | |||
24 | CACHE_KEY_PPD = 'ppd' |
|
33 | CACHE_KEY_PPD = 'ppd' | |
25 | CACHE_KEY_POST_URL = 'post_url' |
|
34 | CACHE_KEY_POST_URL = 'post_url' | |
26 |
|
35 | |||
27 | POSTS_PER_DAY_RANGE = 7 |
|
36 | POSTS_PER_DAY_RANGE = 7 | |
28 |
|
37 | |||
29 | BAN_REASON_AUTO = 'Auto' |
|
38 | BAN_REASON_AUTO = 'Auto' | |
30 |
|
39 | |||
31 | IMAGE_THUMB_SIZE = (200, 150) |
|
40 | IMAGE_THUMB_SIZE = (200, 150) | |
32 |
|
41 | |||
33 | TITLE_MAX_LENGTH = 200 |
|
42 | TITLE_MAX_LENGTH = 200 | |
34 |
|
43 | |||
35 | DEFAULT_MARKUP_TYPE = 'bbcode' |
|
|||
36 |
|
||||
37 | # TODO This should be removed |
|
44 | # TODO This should be removed | |
38 | NO_IP = '0.0.0.0' |
|
45 | NO_IP = '0.0.0.0' | |
39 |
|
46 | |||
40 | # TODO Real user agent should be saved instead of this |
|
47 | # TODO Real user agent should be saved instead of this | |
41 | UNKNOWN_UA = '' |
|
48 | UNKNOWN_UA = '' | |
42 |
|
49 | |||
43 | REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]') |
|
50 | REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]') | |
44 | REGEX_GLOBAL_REPLY = re.compile(r'\[post\](\w+)::([^:]+)::(\d+)\[/post\]') |
|
51 | REGEX_GLOBAL_REPLY = re.compile(r'\[post\](\w+)::([^:]+)::(\d+)\[/post\]') | |
45 |
|
52 | |||
46 | TAG_MODEL = 'model' |
|
53 | TAG_MODEL = 'model' | |
47 | TAG_REQUEST = 'request' |
|
54 | TAG_REQUEST = 'request' | |
48 | TAG_RESPONSE = 'response' |
|
55 | TAG_RESPONSE = 'response' | |
49 | TAG_ID = 'id' |
|
56 | TAG_ID = 'id' | |
50 | TAG_STATUS = 'status' |
|
57 | TAG_STATUS = 'status' | |
51 | TAG_MODELS = 'models' |
|
58 | TAG_MODELS = 'models' | |
52 | TAG_TITLE = 'title' |
|
59 | TAG_TITLE = 'title' | |
53 | TAG_TEXT = 'text' |
|
60 | TAG_TEXT = 'text' | |
54 | TAG_THREAD = 'thread' |
|
61 | TAG_THREAD = 'thread' | |
55 | TAG_PUB_TIME = 'pub-time' |
|
62 | TAG_PUB_TIME = 'pub-time' | |
56 | TAG_SIGNATURES = 'signatures' |
|
63 | TAG_SIGNATURES = 'signatures' | |
57 | TAG_SIGNATURE = 'signature' |
|
64 | TAG_SIGNATURE = 'signature' | |
58 | TAG_CONTENT = 'content' |
|
65 | TAG_CONTENT = 'content' | |
59 | TAG_ATTACHMENTS = 'attachments' |
|
66 | TAG_ATTACHMENTS = 'attachments' | |
60 | TAG_ATTACHMENT = 'attachment' |
|
67 | TAG_ATTACHMENT = 'attachment' | |
61 |
|
68 | |||
62 | TYPE_GET = 'get' |
|
69 | TYPE_GET = 'get' | |
63 |
|
70 | |||
64 | ATTR_VERSION = 'version' |
|
71 | ATTR_VERSION = 'version' | |
65 | ATTR_TYPE = 'type' |
|
72 | ATTR_TYPE = 'type' | |
66 | ATTR_NAME = 'name' |
|
73 | ATTR_NAME = 'name' | |
67 | ATTR_VALUE = 'value' |
|
74 | ATTR_VALUE = 'value' | |
68 | ATTR_MIMETYPE = 'mimetype' |
|
75 | ATTR_MIMETYPE = 'mimetype' | |
69 |
|
76 | |||
70 | STATUS_SUCCESS = 'success' |
|
77 | STATUS_SUCCESS = 'success' | |
71 |
|
78 | |||
72 | logger = logging.getLogger(__name__) |
|
79 | PARAMETER_TRUNCATED = 'truncated' | |
|
80 | PARAMETER_TAG = 'tag' | |||
|
81 | PARAMETER_OFFSET = 'offset' | |||
|
82 | PARAMETER_DIFF_TYPE = 'type' | |||
|
83 | PARAMETER_BUMPABLE = 'bumpable' | |||
|
84 | PARAMETER_THREAD = 'thread' | |||
|
85 | PARAMETER_IS_OPENING = 'is_opening' | |||
|
86 | PARAMETER_MODERATOR = 'moderator' | |||
|
87 | PARAMETER_POST = 'post' | |||
|
88 | PARAMETER_OP_ID = 'opening_post_id' | |||
|
89 | PARAMETER_NEED_OPEN_LINK = 'need_open_link' | |||
|
90 | ||||
|
91 | DIFF_TYPE_HTML = 'html' | |||
|
92 | DIFF_TYPE_JSON = 'json' | |||
|
93 | ||||
|
94 | PREPARSE_PATTERNS = { | |||
|
95 | r'>>(\d+)': r'[post]\1[/post]', # Reflink ">>123" | |||
|
96 | r'^>([^>].+)': r'[quote]\1[/quote]', # Quote ">text" | |||
|
97 | r'^//(.+)': r'[comment]\1[/comment]', # Comment "//text" | |||
|
98 | } | |||
73 |
|
99 | |||
74 |
|
100 | |||
75 | class PostManager(models.Manager): |
|
101 | class PostManager(models.Manager): | |
76 | def create_post(self, title, text, image=None, thread=None, ip=NO_IP, |
|
102 | @transaction.atomic | |
77 | tags=None): |
|
103 | def create_post(self, title: str, text: str, image=None, thread=None, | |
|
104 | ip=NO_IP, tags: list=None): | |||
78 | """ |
|
105 | """ | |
79 | Creates new post |
|
106 | Creates new post | |
80 | """ |
|
107 | """ | |
81 |
|
108 | |||
82 | if not tags: |
|
109 | if not tags: | |
83 | tags = [] |
|
110 | tags = [] | |
84 |
|
111 | |||
85 | posting_time = timezone.now() |
|
112 | posting_time = timezone.now() | |
86 | if not thread: |
|
113 | if not thread: | |
87 | thread = Thread.objects.create(bump_time=posting_time, |
|
114 | thread = Thread.objects.create(bump_time=posting_time, | |
88 | last_edit_time=posting_time) |
|
115 | last_edit_time=posting_time) | |
89 | new_thread = True |
|
116 | new_thread = True | |
90 | else: |
|
117 | else: | |
91 | thread.bump() |
|
|||
92 | thread.last_edit_time = posting_time |
|
|||
93 | thread.save() |
|
|||
94 | new_thread = False |
|
118 | new_thread = False | |
95 |
|
119 | |||
|
120 | pre_text = self._preparse_text(text) | |||
|
121 | ||||
96 | post = self.create(title=title, |
|
122 | post = self.create(title=title, | |
97 | text=text, |
|
123 | text=pre_text, | |
98 | pub_time=posting_time, |
|
124 | pub_time=posting_time, | |
99 | thread_new=thread, |
|
125 | thread_new=thread, | |
100 | poster_ip=ip, |
|
126 | poster_ip=ip, | |
101 | poster_user_agent=UNKNOWN_UA, # TODO Get UA at |
|
127 | poster_user_agent=UNKNOWN_UA, # TODO Get UA at | |
102 | # last! |
|
128 | # last! | |
103 | last_edit_time=posting_time) |
|
129 | last_edit_time=posting_time) | |
104 |
|
130 | |||
105 | post.set_global_id() |
|
131 | post.set_global_id() | |
106 |
|
132 | |||
|
133 | logger = logging.getLogger('boards.post.create') | |||
|
134 | ||||
|
135 | logger.info('Created post {} by {}'.format( | |||
|
136 | post, post.poster_ip)) | |||
|
137 | ||||
107 | if image: |
|
138 | if image: | |
108 | post_image = PostImage.objects.create(image=image) |
|
139 | post_image = PostImage.objects.create(image=image) | |
109 | post.images.add(post_image) |
|
140 | post.images.add(post_image) | |
110 |
logger.info('Created image # |
|
141 | logger.info('Created image #{} for post #{}'.format( | |
111 | post.id)) |
|
142 | post_image.id, post.id)) | |
112 |
|
143 | |||
113 | thread.replies.add(post) |
|
144 | thread.replies.add(post) | |
114 | list(map(thread.add_tag, tags)) |
|
145 | list(map(thread.add_tag, tags)) | |
115 |
|
146 | |||
116 | if new_thread: |
|
147 | if new_thread: | |
117 | Thread.objects.process_oldest_threads() |
|
148 | Thread.objects.process_oldest_threads() | |
118 | self.connect_replies(post) |
|
149 | else: | |
|
150 | thread.bump() | |||
|
151 | thread.last_edit_time = posting_time | |||
|
152 | thread.save() | |||
119 |
|
153 | |||
120 | logger.info('Created post #%d with title %s' |
|
154 | self.connect_replies(post) | |
121 | % (post.id, post.get_title())) |
|
|||
122 |
|
155 | |||
123 | return post |
|
156 | return post | |
124 |
|
157 | |||
125 | def delete_post(self, post): |
|
|||
126 | """ |
|
|||
127 | Deletes post and update or delete its thread |
|
|||
128 | """ |
|
|||
129 |
|
||||
130 | post_id = post.id |
|
|||
131 |
|
||||
132 | thread = post.get_thread() |
|
|||
133 |
|
||||
134 | if post.is_opening(): |
|
|||
135 | thread.delete() |
|
|||
136 | else: |
|
|||
137 | thread.last_edit_time = timezone.now() |
|
|||
138 | thread.save() |
|
|||
139 |
|
||||
140 | post.delete() |
|
|||
141 |
|
||||
142 | logger.info('Deleted post #%d (%s)' % (post_id, post.get_title())) |
|
|||
143 |
|
||||
144 | def delete_posts_by_ip(self, ip): |
|
158 | def delete_posts_by_ip(self, ip): | |
145 | """ |
|
159 | """ | |
146 | Deletes all posts of the author with same IP |
|
160 | Deletes all posts of the author with same IP | |
147 | """ |
|
161 | """ | |
148 |
|
162 | |||
149 | posts = self.filter(poster_ip=ip) |
|
163 | posts = self.filter(poster_ip=ip) | |
150 | for post in posts: |
|
164 | for post in posts: | |
151 |
|
|
165 | post.delete() | |
152 |
|
166 | |||
153 | # TODO This can be moved into a post |
|
167 | # TODO This can be moved into a post | |
154 | def connect_replies(self, post): |
|
168 | def connect_replies(self, post): | |
155 | """ |
|
169 | """ | |
156 | Connects replies to a post to show them as a reflink map |
|
170 | Connects replies to a post to show them as a reflink map | |
157 | """ |
|
171 | """ | |
158 |
|
172 | |||
159 |
for reply_number in post.get_r |
|
173 | for reply_number in re.finditer(REGEX_REPLY, post.get_raw_text()): | |
160 |
|
|
174 | post_id = reply_number.group(1) | |
|
175 | ref_post = self.filter(id=post_id) | |||
161 | if ref_post.count() > 0: |
|
176 | if ref_post.count() > 0: | |
162 | referenced_post = ref_post[0] |
|
177 | referenced_post = ref_post[0] | |
163 | referenced_post.referenced_posts.add(post) |
|
178 | referenced_post.referenced_posts.add(post) | |
164 | referenced_post.last_edit_time = post.pub_time |
|
179 | referenced_post.last_edit_time = post.pub_time | |
165 | referenced_post.build_refmap() |
|
180 | referenced_post.build_refmap() | |
166 | referenced_post.save(update_fields=['refmap', 'last_edit_time']) |
|
181 | referenced_post.save(update_fields=['refmap', 'last_edit_time']) | |
167 |
|
182 | |||
168 | referenced_thread = referenced_post.get_thread() |
|
183 | referenced_thread = referenced_post.get_thread() | |
169 | referenced_thread.last_edit_time = post.pub_time |
|
184 | referenced_thread.last_edit_time = post.pub_time | |
170 | referenced_thread.save(update_fields=['last_edit_time']) |
|
185 | referenced_thread.save(update_fields=['last_edit_time']) | |
171 |
|
186 | |||
172 | def get_posts_per_day(self): |
|
187 | def get_posts_per_day(self): | |
173 | """ |
|
188 | """ | |
174 | Gets average count of posts per day for the last 7 days |
|
189 | Gets average count of posts per day for the last 7 days | |
175 | """ |
|
190 | """ | |
176 |
|
191 | |||
177 | day_end = date.today() |
|
192 | day_end = date.today() | |
178 | day_start = day_end - timedelta(POSTS_PER_DAY_RANGE) |
|
193 | day_start = day_end - timedelta(POSTS_PER_DAY_RANGE) | |
179 |
|
194 | |||
180 | cache_key = CACHE_KEY_PPD + str(day_end) |
|
195 | cache_key = CACHE_KEY_PPD + str(day_end) | |
181 | ppd = cache.get(cache_key) |
|
196 | ppd = cache.get(cache_key) | |
182 | if ppd: |
|
197 | if ppd: | |
183 | return ppd |
|
198 | return ppd | |
184 |
|
199 | |||
185 | day_time_start = timezone.make_aware(datetime.combine( |
|
200 | day_time_start = timezone.make_aware(datetime.combine( | |
186 | day_start, dtime()), timezone.get_current_timezone()) |
|
201 | day_start, dtime()), timezone.get_current_timezone()) | |
187 | day_time_end = timezone.make_aware(datetime.combine( |
|
202 | day_time_end = timezone.make_aware(datetime.combine( | |
188 | day_end, dtime()), timezone.get_current_timezone()) |
|
203 | day_end, dtime()), timezone.get_current_timezone()) | |
189 |
|
204 | |||
190 | posts_per_period = float(self.filter( |
|
205 | posts_per_period = float(self.filter( | |
191 | pub_time__lte=day_time_end, |
|
206 | pub_time__lte=day_time_end, | |
192 | pub_time__gte=day_time_start).count()) |
|
207 | pub_time__gte=day_time_start).count()) | |
193 |
|
208 | |||
194 | ppd = posts_per_period / POSTS_PER_DAY_RANGE |
|
209 | ppd = posts_per_period / POSTS_PER_DAY_RANGE | |
195 |
|
210 | |||
196 | cache.set(cache_key, ppd) |
|
211 | cache.set(cache_key, ppd) | |
197 | return ppd |
|
212 | return ppd | |
198 |
|
213 | |||
199 | # TODO Make a separate sync facade? |
|
214 | # TODO Make a separate sync facade? | |
200 | def generate_response_get(self, model_list: list): |
|
215 | def generate_response_get(self, model_list: list): | |
201 | response = et.Element(TAG_RESPONSE) |
|
216 | response = et.Element(TAG_RESPONSE) | |
202 |
|
217 | |||
203 | status = et.SubElement(response, TAG_STATUS) |
|
218 | status = et.SubElement(response, TAG_STATUS) | |
204 | status.text = STATUS_SUCCESS |
|
219 | status.text = STATUS_SUCCESS | |
205 |
|
220 | |||
206 | models = et.SubElement(response, TAG_MODELS) |
|
221 | models = et.SubElement(response, TAG_MODELS) | |
207 |
|
222 | |||
208 | for post in model_list: |
|
223 | for post in model_list: | |
209 | model = et.SubElement(models, TAG_MODEL) |
|
224 | model = et.SubElement(models, TAG_MODEL) | |
210 | model.set(ATTR_NAME, 'post') |
|
225 | model.set(ATTR_NAME, 'post') | |
211 |
|
226 | |||
212 | content_tag = et.SubElement(model, TAG_CONTENT) |
|
227 | content_tag = et.SubElement(model, TAG_CONTENT) | |
213 |
|
228 | |||
214 | tag_id = et.SubElement(content_tag, TAG_ID) |
|
229 | tag_id = et.SubElement(content_tag, TAG_ID) | |
215 | post.global_id.to_xml_element(tag_id) |
|
230 | post.global_id.to_xml_element(tag_id) | |
216 |
|
231 | |||
217 | title = et.SubElement(content_tag, TAG_TITLE) |
|
232 | title = et.SubElement(content_tag, TAG_TITLE) | |
218 | title.text = post.title |
|
233 | title.text = post.title | |
219 |
|
234 | |||
220 | text = et.SubElement(content_tag, TAG_TEXT) |
|
235 | text = et.SubElement(content_tag, TAG_TEXT) | |
221 | # TODO Replace local links by global ones in the text |
|
236 | # TODO Replace local links by global ones in the text | |
222 | text.text = post.text.raw |
|
237 | text.text = post.text.raw | |
223 |
|
238 | |||
224 | if not post.is_opening(): |
|
239 | if not post.is_opening(): | |
225 | thread = et.SubElement(content_tag, TAG_THREAD) |
|
240 | thread = et.SubElement(content_tag, TAG_THREAD) | |
226 | thread.text = str(post.get_thread().get_opening_post_id()) |
|
241 | thread.text = str(post.get_thread().get_opening_post_id()) | |
227 | else: |
|
242 | else: | |
228 | # TODO Output tags here |
|
243 | # TODO Output tags here | |
229 | pass |
|
244 | pass | |
230 |
|
245 | |||
231 | pub_time = et.SubElement(content_tag, TAG_PUB_TIME) |
|
246 | pub_time = et.SubElement(content_tag, TAG_PUB_TIME) | |
232 | pub_time.text = str(post.get_pub_time_epoch()) |
|
247 | pub_time.text = str(post.get_pub_time_epoch()) | |
233 |
|
248 | |||
234 | signatures_tag = et.SubElement(model, TAG_SIGNATURES) |
|
249 | signatures_tag = et.SubElement(model, TAG_SIGNATURES) | |
235 | post_signatures = post.signature.all() |
|
250 | post_signatures = post.signature.all() | |
236 | if post_signatures: |
|
251 | if post_signatures: | |
237 | signatures = post.signatures |
|
252 | signatures = post.signatures | |
238 | else: |
|
253 | else: | |
239 | # TODO Maybe the signature can be computed only once after |
|
254 | # TODO Maybe the signature can be computed only once after | |
240 | # the post is added? Need to add some on_save signal queue |
|
255 | # the post is added? Need to add some on_save signal queue | |
241 | # and add this there. |
|
256 | # and add this there. | |
242 | key = KeyPair.objects.get(public_key=post.global_id.key) |
|
257 | key = KeyPair.objects.get(public_key=post.global_id.key) | |
243 | signatures = [Signature( |
|
258 | signatures = [Signature( | |
244 | key_type=key.key_type, |
|
259 | key_type=key.key_type, | |
245 | key=key.public_key, |
|
260 | key=key.public_key, | |
246 | signature=key.sign(et.tostring(model, ENCODING_UNICODE)), |
|
261 | signature=key.sign(et.tostring(model, ENCODING_UNICODE)), | |
247 | )] |
|
262 | )] | |
248 | for signature in signatures: |
|
263 | for signature in signatures: | |
249 | signature_tag = et.SubElement(signatures_tag, TAG_SIGNATURE) |
|
264 | signature_tag = et.SubElement(signatures_tag, TAG_SIGNATURE) | |
250 | signature_tag.set(ATTR_TYPE, signature.key_type) |
|
265 | signature_tag.set(ATTR_TYPE, signature.key_type) | |
251 | signature_tag.set(ATTR_VALUE, signature.signature) |
|
266 | signature_tag.set(ATTR_VALUE, signature.signature) | |
252 |
|
267 | |||
253 | return et.tostring(response, ENCODING_UNICODE) |
|
268 | return et.tostring(response, ENCODING_UNICODE) | |
254 |
|
269 | |||
255 | def parse_response_get(self, response_xml): |
|
270 | def parse_response_get(self, response_xml): | |
256 | tag_root = et.fromstring(response_xml) |
|
271 | tag_root = et.fromstring(response_xml) | |
257 | tag_status = tag_root[0] |
|
272 | tag_status = tag_root[0] | |
258 | if 'success' == tag_status.text: |
|
273 | if 'success' == tag_status.text: | |
259 | tag_models = tag_root[1] |
|
274 | tag_models = tag_root[1] | |
260 | for tag_model in tag_models: |
|
275 | for tag_model in tag_models: | |
261 | tag_content = tag_model[0] |
|
276 | tag_content = tag_model[0] | |
262 | tag_id = tag_content[1] |
|
277 | tag_id = tag_content[1] | |
263 | try: |
|
278 | try: | |
264 | GlobalId.from_xml_element(tag_id, existing=True) |
|
279 | GlobalId.from_xml_element(tag_id, existing=True) | |
265 | # If this post already exists, just continue |
|
280 | # If this post already exists, just continue | |
266 | # TODO Compare post content and update the post if necessary |
|
281 | # TODO Compare post content and update the post if necessary | |
267 | pass |
|
282 | pass | |
268 | except GlobalId.DoesNotExist: |
|
283 | except GlobalId.DoesNotExist: | |
269 | global_id = GlobalId.from_xml_element(tag_id) |
|
284 | global_id = GlobalId.from_xml_element(tag_id) | |
270 |
|
285 | |||
271 | title = tag_content.find(TAG_TITLE).text |
|
286 | title = tag_content.find(TAG_TITLE).text | |
272 | text = tag_content.find(TAG_TEXT).text |
|
287 | text = tag_content.find(TAG_TEXT).text | |
273 | # TODO Check that the replied posts are already present |
|
288 | # TODO Check that the replied posts are already present | |
274 | # before adding new ones |
|
289 | # before adding new ones | |
275 |
|
290 | |||
276 | # TODO Pub time, thread, tags |
|
291 | # TODO Pub time, thread, tags | |
277 |
|
292 | |||
278 | post = Post.objects.create(title=title, text=text) |
|
293 | post = Post.objects.create(title=title, text=text) | |
279 | else: |
|
294 | else: | |
280 | # TODO Throw an exception? |
|
295 | # TODO Throw an exception? | |
281 | pass |
|
296 | pass | |
282 |
|
297 | |||
|
298 | def _preparse_text(self, text): | |||
|
299 | """ | |||
|
300 | Preparses text to change patterns like '>>' to a proper bbcode | |||
|
301 | tags. | |||
|
302 | """ | |||
|
303 | ||||
|
304 | for key, value in PREPARSE_PATTERNS.items(): | |||
|
305 | text = re.sub(key, value, text, flags=re.MULTILINE) | |||
|
306 | ||||
|
307 | return text | |||
|
308 | ||||
283 |
|
309 | |||
284 | class Post(models.Model, Viewable): |
|
310 | class Post(models.Model, Viewable): | |
285 | """A post is a message.""" |
|
311 | """A post is a message.""" | |
286 |
|
312 | |||
287 | objects = PostManager() |
|
313 | objects = PostManager() | |
288 |
|
314 | |||
289 | class Meta: |
|
315 | class Meta: | |
290 | app_label = APP_LABEL_BOARDS |
|
316 | app_label = APP_LABEL_BOARDS | |
291 | ordering = ('id',) |
|
317 | ordering = ('id',) | |
292 |
|
318 | |||
293 | title = models.CharField(max_length=TITLE_MAX_LENGTH) |
|
319 | title = models.CharField(max_length=TITLE_MAX_LENGTH) | |
294 | pub_time = models.DateTimeField() |
|
320 | pub_time = models.DateTimeField() | |
295 | text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE, |
|
321 | text = TextField(blank=True, null=True) | |
296 | escape_html=False) |
|
322 | _text_rendered = TextField(blank=True, null=True, editable=False) | |
297 |
|
323 | |||
298 | images = models.ManyToManyField(PostImage, null=True, blank=True, |
|
324 | images = models.ManyToManyField(PostImage, null=True, blank=True, | |
299 | related_name='ip+', db_index=True) |
|
325 | related_name='ip+', db_index=True) | |
300 |
|
326 | |||
301 | poster_ip = models.GenericIPAddressField() |
|
327 | poster_ip = models.GenericIPAddressField() | |
302 | poster_user_agent = models.TextField() |
|
328 | poster_user_agent = models.TextField() | |
303 |
|
329 | |||
304 | thread_new = models.ForeignKey('Thread', null=True, default=None, |
|
330 | thread_new = models.ForeignKey('Thread', null=True, default=None, | |
305 | db_index=True) |
|
331 | db_index=True) | |
306 | last_edit_time = models.DateTimeField() |
|
332 | last_edit_time = models.DateTimeField() | |
307 |
|
333 | |||
308 | # Replies to the post |
|
334 | # Replies to the post | |
309 | referenced_posts = models.ManyToManyField('Post', symmetrical=False, |
|
335 | referenced_posts = models.ManyToManyField('Post', symmetrical=False, | |
310 | null=True, |
|
336 | null=True, | |
311 | blank=True, related_name='rfp+', |
|
337 | blank=True, related_name='rfp+', | |
312 | db_index=True) |
|
338 | db_index=True) | |
313 |
|
339 | |||
314 | # Replies map. This is built from the referenced posts list to speed up |
|
340 | # Replies map. This is built from the referenced posts list to speed up | |
315 | # page loading (no need to get all the referenced posts from the database). |
|
341 | # page loading (no need to get all the referenced posts from the database). | |
316 | refmap = models.TextField(null=True, blank=True) |
|
342 | refmap = models.TextField(null=True, blank=True) | |
317 |
|
343 | |||
318 | # Global ID with author key. If the message was downloaded from another |
|
344 | # Global ID with author key. If the message was downloaded from another | |
319 | # server, this indicates the server. |
|
345 | # server, this indicates the server. | |
320 | global_id = models.OneToOneField('GlobalId', null=True, blank=True) |
|
346 | global_id = models.OneToOneField('GlobalId', null=True, blank=True) | |
321 |
|
347 | |||
322 | # One post can be signed by many nodes that give their trust to it |
|
348 | # One post can be signed by many nodes that give their trust to it | |
323 | signature = models.ManyToManyField('Signature', null=True, blank=True) |
|
349 | signature = models.ManyToManyField('Signature', null=True, blank=True) | |
324 |
|
350 | |||
325 |
def __ |
|
351 | def __str__(self): | |
326 |
return '#' |
|
352 | return 'P#{}/{}'.format(self.id, self.title) | |
327 | self.text.raw[:50] + ')' |
|
353 | ||
|
354 | def get_title(self) -> str: | |||
|
355 | """ | |||
|
356 | Gets original post title or part of its text. | |||
|
357 | """ | |||
328 |
|
358 | |||
329 | def get_title(self): |
|
359 | title = self.title | |
330 |
|
|
360 | if not title: | |
|
361 | title = self.get_text() | |||
331 |
|
362 | |||
332 | def build_refmap(self): |
|
363 | return title | |
|
364 | ||||
|
365 | def build_refmap(self) -> None: | |||
333 | """ |
|
366 | """ | |
334 | Builds a replies map string from replies list. This is a cache to stop |
|
367 | Builds a replies map string from replies list. This is a cache to stop | |
335 | the server from recalculating the map on every post show. |
|
368 | the server from recalculating the map on every post show. | |
336 | """ |
|
369 | """ | |
337 | map_string = '' |
|
370 | map_string = '' | |
338 |
|
371 | |||
339 | first = True |
|
372 | first = True | |
340 | for refpost in self.referenced_posts.all(): |
|
373 | for refpost in self.referenced_posts.all(): | |
341 | if not first: |
|
374 | if not first: | |
342 | map_string += ', ' |
|
375 | map_string += ', ' | |
343 | map_string += '<a href="%s">>>%s</a>' % (refpost.get_url(), |
|
376 | map_string += '<a href="%s">>>%s</a>' % (refpost.get_url(), | |
344 | refpost.id) |
|
377 | refpost.id) | |
345 | first = False |
|
378 | first = False | |
346 |
|
379 | |||
347 | self.refmap = map_string |
|
380 | self.refmap = map_string | |
348 |
|
381 | |||
349 | def get_sorted_referenced_posts(self): |
|
382 | def get_sorted_referenced_posts(self): | |
350 | return self.refmap |
|
383 | return self.refmap | |
351 |
|
384 | |||
352 | def is_referenced(self): |
|
385 | def is_referenced(self) -> bool: | |
|
386 | if not self.refmap: | |||
|
387 | return False | |||
|
388 | else: | |||
353 | return len(self.refmap) > 0 |
|
389 | return len(self.refmap) > 0 | |
354 |
|
390 | |||
355 | def is_opening(self): |
|
391 | def is_opening(self) -> bool: | |
356 | """ |
|
392 | """ | |
357 | Checks if this is an opening post or just a reply. |
|
393 | Checks if this is an opening post or just a reply. | |
358 | """ |
|
394 | """ | |
359 |
|
395 | |||
360 | return self.get_thread().get_opening_post_id() == self.id |
|
396 | return self.get_thread().get_opening_post_id() == self.id | |
361 |
|
397 | |||
362 | @transaction.atomic |
|
398 | @transaction.atomic | |
363 | def add_tag(self, tag): |
|
399 | def add_tag(self, tag): | |
364 | edit_time = timezone.now() |
|
400 | edit_time = timezone.now() | |
365 |
|
401 | |||
366 | thread = self.get_thread() |
|
402 | thread = self.get_thread() | |
367 | thread.add_tag(tag) |
|
403 | thread.add_tag(tag) | |
368 | self.last_edit_time = edit_time |
|
404 | self.last_edit_time = edit_time | |
369 | self.save(update_fields=['last_edit_time']) |
|
405 | self.save(update_fields=['last_edit_time']) | |
370 |
|
406 | |||
371 | thread.last_edit_time = edit_time |
|
407 | thread.last_edit_time = edit_time | |
372 | thread.save(update_fields=['last_edit_time']) |
|
408 | thread.save(update_fields=['last_edit_time']) | |
373 |
|
409 | |||
374 | @transaction.atomic |
|
|||
375 | def remove_tag(self, tag): |
|
|||
376 | edit_time = timezone.now() |
|
|||
377 |
|
||||
378 | thread = self.get_thread() |
|
|||
379 | thread.remove_tag(tag) |
|
|||
380 | self.last_edit_time = edit_time |
|
|||
381 | self.save(update_fields=['last_edit_time']) |
|
|||
382 |
|
||||
383 | thread.last_edit_time = edit_time |
|
|||
384 | thread.save(update_fields=['last_edit_time']) |
|
|||
385 |
|
||||
386 | def get_url(self, thread=None): |
|
410 | def get_url(self, thread=None): | |
387 | """ |
|
411 | """ | |
388 | Gets full url to the post. |
|
412 | Gets full url to the post. | |
389 | """ |
|
413 | """ | |
390 |
|
414 | |||
391 | cache_key = CACHE_KEY_POST_URL + str(self.id) |
|
415 | cache_key = CACHE_KEY_POST_URL + str(self.id) | |
392 | link = cache.get(cache_key) |
|
416 | link = cache.get(cache_key) | |
393 |
|
417 | |||
394 | if not link: |
|
418 | if not link: | |
395 | if not thread: |
|
419 | if not thread: | |
396 | thread = self.get_thread() |
|
420 | thread = self.get_thread() | |
397 |
|
421 | |||
398 | opening_id = thread.get_opening_post_id() |
|
422 | opening_id = thread.get_opening_post_id() | |
399 |
|
423 | |||
400 | if self.id != opening_id: |
|
424 | if self.id != opening_id: | |
401 | link = reverse('thread', kwargs={ |
|
425 | link = reverse('thread', kwargs={ | |
402 | 'post_id': opening_id}) + '#' + str(self.id) |
|
426 | 'post_id': opening_id}) + '#' + str(self.id) | |
403 | else: |
|
427 | else: | |
404 | link = reverse('thread', kwargs={'post_id': self.id}) |
|
428 | link = reverse('thread', kwargs={'post_id': self.id}) | |
405 |
|
429 | |||
406 | cache.set(cache_key, link) |
|
430 | cache.set(cache_key, link) | |
407 |
|
431 | |||
408 | return link |
|
432 | return link | |
409 |
|
433 | |||
410 | def get_thread(self): |
|
434 | def get_thread(self) -> Thread: | |
411 | """ |
|
435 | """ | |
412 | Gets post's thread. |
|
436 | Gets post's thread. | |
413 | """ |
|
437 | """ | |
414 |
|
438 | |||
415 | return self.thread_new |
|
439 | return self.thread_new | |
416 |
|
440 | |||
417 | def get_referenced_posts(self): |
|
441 | def get_referenced_posts(self): | |
418 | return self.referenced_posts.only('id', 'thread_new') |
|
442 | return self.referenced_posts.only('id', 'thread_new') | |
419 |
|
443 | |||
420 | def get_text(self): |
|
|||
421 | return self.text |
|
|||
422 |
|
||||
423 | def get_view(self, moderator=False, need_open_link=False, |
|
444 | def get_view(self, moderator=False, need_open_link=False, | |
424 | truncated=False, *args, **kwargs): |
|
445 | truncated=False, *args, **kwargs): | |
425 | if 'is_opening' in kwargs: |
|
446 | """ | |
426 | is_opening = kwargs['is_opening'] |
|
447 | Renders post's HTML view. Some of the post params can be passed over | |
427 | else: |
|
448 | kwargs for the means of caching (if we view the thread, some params | |
428 | is_opening = self.is_opening() |
|
449 | are same for every post and don't need to be computed over and over. | |
|
450 | """ | |||
429 |
|
451 | |||
430 | if 'thread' in kwargs: |
|
452 | is_opening = kwargs.get(PARAMETER_IS_OPENING, self.is_opening()) | |
431 | thread = kwargs['thread'] |
|
453 | thread = kwargs.get(PARAMETER_THREAD, self.get_thread()) | |
432 | else: |
|
454 | can_bump = kwargs.get(PARAMETER_BUMPABLE, thread.can_bump()) | |
433 | thread = self.get_thread() |
|
|||
434 |
|
||||
435 | if 'can_bump' in kwargs: |
|
|||
436 | can_bump = kwargs['can_bump'] |
|
|||
437 | else: |
|
|||
438 | can_bump = thread.can_bump() |
|
|||
439 |
|
455 | |||
440 | if is_opening: |
|
456 | if is_opening: | |
441 | opening_post_id = self.id |
|
457 | opening_post_id = self.id | |
442 | else: |
|
458 | else: | |
443 | opening_post_id = thread.get_opening_post_id() |
|
459 | opening_post_id = thread.get_opening_post_id() | |
444 |
|
460 | |||
445 | return render_to_string('boards/post.html', { |
|
461 | return render_to_string('boards/post.html', { | |
446 |
|
|
462 | PARAMETER_POST: self, | |
447 |
|
|
463 | PARAMETER_MODERATOR: moderator, | |
448 |
|
|
464 | PARAMETER_IS_OPENING: is_opening, | |
449 |
|
|
465 | PARAMETER_THREAD: thread, | |
450 |
|
|
466 | PARAMETER_BUMPABLE: can_bump, | |
451 |
|
|
467 | PARAMETER_NEED_OPEN_LINK: need_open_link, | |
452 |
|
|
468 | PARAMETER_TRUNCATED: truncated, | |
453 |
|
|
469 | PARAMETER_OP_ID: opening_post_id, | |
454 | }) |
|
470 | }) | |
455 |
|
471 | |||
456 | def get_first_image(self): |
|
472 | def get_search_view(self, *args, **kwargs): | |
|
473 | return self.get_view(args, kwargs) | |||
|
474 | ||||
|
475 | def get_first_image(self) -> PostImage: | |||
457 | return self.images.earliest('id') |
|
476 | return self.images.earliest('id') | |
458 |
|
477 | |||
459 | def delete(self, using=None): |
|
478 | def delete(self, using=None): | |
460 | """ |
|
479 | """ | |
461 | Deletes all post images and the post itself. |
|
480 | Deletes all post images and the post itself. If the post is opening, | |
|
481 | thread with all posts is deleted. | |||
462 | """ |
|
482 | """ | |
463 |
|
483 | |||
464 | self.images.all().delete() |
|
484 | self.images.all().delete() | |
465 | self.signature.all().delete() |
|
485 | self.signature.all().delete() | |
466 | if self.global_id: |
|
486 | if self.global_id: | |
467 | self.global_id.delete() |
|
487 | self.global_id.delete() | |
468 |
|
488 | |||
|
489 | if self.is_opening(): | |||
|
490 | self.get_thread().delete() | |||
|
491 | else: | |||
|
492 | thread = self.get_thread() | |||
|
493 | thread.last_edit_time = timezone.now() | |||
|
494 | thread.save() | |||
|
495 | ||||
469 | super(Post, self).delete(using) |
|
496 | super(Post, self).delete(using) | |
|
497 | logging.getLogger('boards.post.delete').info( | |||
|
498 | 'Deleted post {}'.format(self)) | |||
470 |
|
499 | |||
471 | def set_global_id(self, key_pair=None): |
|
500 | def set_global_id(self, key_pair=None): | |
472 | """ |
|
501 | """ | |
473 | Sets global id based on the given key pair. If no key pair is given, |
|
502 | Sets global id based on the given key pair. If no key pair is given, | |
474 | default one is used. |
|
503 | default one is used. | |
475 | """ |
|
504 | """ | |
476 |
|
505 | |||
477 | if key_pair: |
|
506 | if key_pair: | |
478 | key = key_pair |
|
507 | key = key_pair | |
479 | else: |
|
508 | else: | |
480 | try: |
|
509 | try: | |
481 | key = KeyPair.objects.get(primary=True) |
|
510 | key = KeyPair.objects.get(primary=True) | |
482 | except KeyPair.DoesNotExist: |
|
511 | except KeyPair.DoesNotExist: | |
483 | # Do not update the global id because there is no key defined |
|
512 | # Do not update the global id because there is no key defined | |
484 | return |
|
513 | return | |
485 | global_id = GlobalId(key_type=key.key_type, |
|
514 | global_id = GlobalId(key_type=key.key_type, | |
486 | key=key.public_key, |
|
515 | key=key.public_key, | |
487 | local_id = self.id) |
|
516 | local_id = self.id) | |
488 | global_id.save() |
|
517 | global_id.save() | |
489 |
|
518 | |||
490 | self.global_id = global_id |
|
519 | self.global_id = global_id | |
491 |
|
520 | |||
492 | self.save(update_fields=['global_id']) |
|
521 | self.save(update_fields=['global_id']) | |
493 |
|
522 | |||
494 | def get_pub_time_epoch(self): |
|
523 | def get_pub_time_epoch(self): | |
495 | return utils.datetime_to_epoch(self.pub_time) |
|
524 | return utils.datetime_to_epoch(self.pub_time) | |
496 |
|
525 | |||
|
526 | # TODO Use this to connect replies | |||
497 | def get_replied_ids(self): |
|
527 | def get_replied_ids(self): | |
498 | """ |
|
528 | """ | |
499 | Gets ID list of the posts that this post replies. |
|
529 | Gets ID list of the posts that this post replies. | |
500 | """ |
|
530 | """ | |
501 |
|
531 | |||
502 | local_replied = REGEX_REPLY.findall(self.text.raw) |
|
532 | local_replied = REGEX_REPLY.findall(self.text.raw) | |
503 | global_replied = [] |
|
533 | global_replied = [] | |
504 | # TODO Similar code is used in mdx_neboard, maybe it can be extracted |
|
534 | # TODO Similar code is used in mdx_neboard, maybe it can be extracted | |
505 | # into a method? |
|
535 | # into a method? | |
506 | for match in REGEX_GLOBAL_REPLY.findall(self.text.raw): |
|
536 | for match in REGEX_GLOBAL_REPLY.findall(self.text.raw): | |
507 | key_type = match[0] |
|
537 | key_type = match[0] | |
508 | key = match[1] |
|
538 | key = match[1] | |
509 | local_id = match[2] |
|
539 | local_id = match[2] | |
510 |
|
540 | |||
511 | try: |
|
541 | try: | |
512 | global_id = GlobalId.objects.get(key_type=key_type, |
|
542 | global_id = GlobalId.objects.get(key_type=key_type, | |
513 | key=key, local_id=local_id) |
|
543 | key=key, local_id=local_id) | |
514 | for post in Post.objects.filter(global_id=global_id).only('id'): |
|
544 | for post in Post.objects.filter(global_id=global_id).only('id'): | |
515 | global_replied.append(post.id) |
|
545 | global_replied.append(post.id) | |
516 | except GlobalId.DoesNotExist: |
|
546 | except GlobalId.DoesNotExist: | |
517 | pass |
|
547 | pass | |
518 | return local_replied + global_replied |
|
548 | return local_replied + global_replied | |
|
549 | ||||
|
550 | ||||
|
551 | def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None, | |||
|
552 | include_last_update=False): | |||
|
553 | """ | |||
|
554 | Gets post HTML or JSON data that can be rendered on a page or used by | |||
|
555 | API. | |||
|
556 | """ | |||
|
557 | ||||
|
558 | if format_type == DIFF_TYPE_HTML: | |||
|
559 | params = dict() | |||
|
560 | params['post'] = self | |||
|
561 | if PARAMETER_TRUNCATED in request.GET: | |||
|
562 | params[PARAMETER_TRUNCATED] = True | |||
|
563 | ||||
|
564 | return render_to_string('boards/api_post.html', params) | |||
|
565 | elif format_type == DIFF_TYPE_JSON: | |||
|
566 | post_json = { | |||
|
567 | 'id': self.id, | |||
|
568 | 'title': self.title, | |||
|
569 | 'text': self._text_rendered, | |||
|
570 | } | |||
|
571 | if self.images.exists(): | |||
|
572 | post_image = self.get_first_image() | |||
|
573 | post_json['image'] = post_image.image.url | |||
|
574 | post_json['image_preview'] = post_image.image.url_200x150 | |||
|
575 | if include_last_update: | |||
|
576 | post_json['bump_time'] = datetime_to_epoch( | |||
|
577 | self.thread_new.bump_time) | |||
|
578 | return post_json | |||
|
579 | ||||
|
580 | def send_to_websocket(self, request, recursive=True): | |||
|
581 | """ | |||
|
582 | Sends post HTML data to the thread web socket. | |||
|
583 | """ | |||
|
584 | ||||
|
585 | if not settings.WEBSOCKETS_ENABLED: | |||
|
586 | return | |||
|
587 | ||||
|
588 | client = Client() | |||
|
589 | ||||
|
590 | thread = self.get_thread() | |||
|
591 | thread_id = thread.id | |||
|
592 | channel_name = WS_CHANNEL_THREAD + str(thread.get_opening_post_id()) | |||
|
593 | client.publish(channel_name, { | |||
|
594 | WS_NOTIFICATION_TYPE: WS_NOTIFICATION_TYPE_NEW_POST, | |||
|
595 | }) | |||
|
596 | client.send() | |||
|
597 | ||||
|
598 | logger = logging.getLogger('boards.post.websocket') | |||
|
599 | ||||
|
600 | logger.info('Sent notification from post #{} to channel {}'.format( | |||
|
601 | self.id, channel_name)) | |||
|
602 | ||||
|
603 | if recursive: | |||
|
604 | for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()): | |||
|
605 | post_id = reply_number.group(1) | |||
|
606 | ref_post = Post.objects.filter(id=post_id)[0] | |||
|
607 | ||||
|
608 | # If post is in this thread, its thread was already notified. | |||
|
609 | # Otherwise, notify its thread separately. | |||
|
610 | if ref_post.thread_new_id != thread_id: | |||
|
611 | ref_post.send_to_websocket(request, recursive=False) | |||
|
612 | ||||
|
613 | def save(self, force_insert=False, force_update=False, using=None, | |||
|
614 | update_fields=None): | |||
|
615 | self._text_rendered = bbcode_extended(self.get_raw_text()) | |||
|
616 | ||||
|
617 | super().save(force_insert, force_update, using, update_fields) | |||
|
618 | ||||
|
619 | def get_text(self) -> str: | |||
|
620 | return self._text_rendered | |||
|
621 | ||||
|
622 | def get_raw_text(self) -> str: | |||
|
623 | return self.text |
@@ -1,78 +1,71 b'' | |||||
1 | from django.template.loader import render_to_string |
|
1 | from django.template.loader import render_to_string | |
2 | from django.db import models |
|
2 | from django.db import models | |
3 |
from django.db.models import Count |
|
3 | from django.db.models import Count | |
4 | from django.core.urlresolvers import reverse |
|
4 | from django.core.urlresolvers import reverse | |
5 |
|
5 | |||
6 | from boards.models import Thread |
|
|||
7 | from boards.models.base import Viewable |
|
6 | from boards.models.base import Viewable | |
8 |
|
7 | |||
9 |
|
8 | |||
10 | __author__ = 'neko259' |
|
9 | __author__ = 'neko259' | |
11 |
|
10 | |||
12 |
|
11 | |||
13 | class TagManager(models.Manager): |
|
12 | class TagManager(models.Manager): | |
14 |
|
13 | |||
15 | def get_not_empty_tags(self): |
|
14 | def get_not_empty_tags(self): | |
16 | """ |
|
15 | """ | |
17 | Gets tags that have non-archived threads. |
|
16 | Gets tags that have non-archived threads. | |
18 | """ |
|
17 | """ | |
19 |
|
18 | |||
20 | tags = self.annotate(Count('threads')) \ |
|
19 | return self.filter(thread__archived=False)\ | |
21 | .filter(threads__count__gt=0).order_by('name') |
|
20 | .annotate(num_threads=Count('thread')).filter(num_threads__gt=0)\ | |
22 |
|
21 | .order_by('-required', 'name') | ||
23 | return tags |
|
|||
24 |
|
22 | |||
25 |
|
23 | |||
26 | class Tag(models.Model, Viewable): |
|
24 | class Tag(models.Model, Viewable): | |
27 | """ |
|
25 | """ | |
28 | A tag is a text node assigned to the thread. The tag serves as a board |
|
26 | A tag is a text node assigned to the thread. The tag serves as a board | |
29 | section. There can be multiple tags for each thread |
|
27 | section. There can be multiple tags for each thread | |
30 | """ |
|
28 | """ | |
31 |
|
29 | |||
32 | objects = TagManager() |
|
30 | objects = TagManager() | |
33 |
|
31 | |||
34 | class Meta: |
|
32 | class Meta: | |
35 | app_label = 'boards' |
|
33 | app_label = 'boards' | |
36 | ordering = ('name',) |
|
34 | ordering = ('name',) | |
37 |
|
35 | |||
38 | name = models.CharField(max_length=100, db_index=True) |
|
36 | name = models.CharField(max_length=100, db_index=True) | |
39 | threads = models.ManyToManyField(Thread, null=True, |
|
37 | required = models.BooleanField(default=False) | |
40 | blank=True, related_name='tag+') |
|
|||
41 |
|
38 | |||
42 |
def __ |
|
39 | def __str__(self): | |
43 | return self.name |
|
40 | return self.name | |
44 |
|
41 | |||
45 | def is_empty(self): |
|
42 | def is_empty(self) -> bool: | |
46 | """ |
|
43 | """ | |
47 | Checks if the tag has some threads. |
|
44 | Checks if the tag has some threads. | |
48 | """ |
|
45 | """ | |
49 |
|
46 | |||
50 | return self.get_thread_count() == 0 |
|
47 | return self.get_thread_count() == 0 | |
51 |
|
48 | |||
52 | def get_thread_count(self): |
|
49 | def get_thread_count(self) -> int: | |
53 | return self.threads.count() |
|
50 | return self.get_threads().count() | |
54 |
|
||||
55 | def get_post_count(self, archived=False): |
|
|||
56 | """ |
|
|||
57 | Gets posts count for the tag's threads. |
|
|||
58 | """ |
|
|||
59 |
|
||||
60 | posts_count = 0 |
|
|||
61 |
|
||||
62 | threads = self.threads.filter(archived=archived) |
|
|||
63 | if threads.exists(): |
|
|||
64 | posts_count = threads.annotate(posts_count=Count('replies')) \ |
|
|||
65 | .aggregate(posts_sum=Sum('posts_count'))['posts_sum'] |
|
|||
66 |
|
||||
67 | if not posts_count: |
|
|||
68 | posts_count = 0 |
|
|||
69 |
|
||||
70 | return posts_count |
|
|||
71 |
|
51 | |||
72 | def get_url(self): |
|
52 | def get_url(self): | |
73 | return reverse('tag', kwargs={'tag_name': self.name}) |
|
53 | return reverse('tag', kwargs={'tag_name': self.name}) | |
74 |
|
54 | |||
75 |
def get_ |
|
55 | def get_threads(self): | |
|
56 | return self.thread_set.order_by('-bump_time') | |||
|
57 | ||||
|
58 | def is_required(self): | |||
|
59 | return self.required | |||
|
60 | ||||
|
61 | def get_view(self): | |||
|
62 | link = '<a class="tag" href="{}">{}</a>'.format( | |||
|
63 | self.get_url(), self.name) | |||
|
64 | if self.is_required(): | |||
|
65 | link = '<b>{}</b>'.format(link) | |||
|
66 | return link | |||
|
67 | ||||
|
68 | def get_search_view(self, *args, **kwargs): | |||
76 | return render_to_string('boards/tag.html', { |
|
69 | return render_to_string('boards/tag.html', { | |
77 | 'tag': self, |
|
70 | 'tag': self, | |
78 | }) |
|
71 | }) |
@@ -1,188 +1,182 b'' | |||||
1 | import logging |
|
1 | import logging | |
2 | from django.db.models import Count |
|
2 | from django.db.models import Count, Sum | |
3 | from django.utils import timezone |
|
3 | from django.utils import timezone | |
4 | from django.core.cache import cache |
|
4 | from django.core.cache import cache | |
5 | from django.db import models |
|
5 | from django.db import models | |
6 | from boards import settings |
|
6 | from boards import settings | |
7 |
|
7 | |||
8 | __author__ = 'neko259' |
|
8 | __author__ = 'neko259' | |
9 |
|
9 | |||
10 |
|
10 | |||
11 | logger = logging.getLogger(__name__) |
|
11 | logger = logging.getLogger(__name__) | |
12 |
|
12 | |||
13 |
|
13 | |||
14 | CACHE_KEY_OPENING_POST = 'opening_post_id' |
|
14 | CACHE_KEY_OPENING_POST = 'opening_post_id' | |
15 |
|
15 | |||
16 |
|
16 | |||
17 | class ThreadManager(models.Manager): |
|
17 | class ThreadManager(models.Manager): | |
18 | def process_oldest_threads(self): |
|
18 | def process_oldest_threads(self): | |
19 | """ |
|
19 | """ | |
20 | Preserves maximum thread count. If there are too many threads, |
|
20 | Preserves maximum thread count. If there are too many threads, | |
21 | archive or delete the old ones. |
|
21 | archive or delete the old ones. | |
22 | """ |
|
22 | """ | |
23 |
|
23 | |||
24 | threads = Thread.objects.filter(archived=False).order_by('-bump_time') |
|
24 | threads = Thread.objects.filter(archived=False).order_by('-bump_time') | |
25 | thread_count = threads.count() |
|
25 | thread_count = threads.count() | |
26 |
|
26 | |||
27 | if thread_count > settings.MAX_THREAD_COUNT: |
|
27 | if thread_count > settings.MAX_THREAD_COUNT: | |
28 | num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT |
|
28 | num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT | |
29 | old_threads = threads[thread_count - num_threads_to_delete:] |
|
29 | old_threads = threads[thread_count - num_threads_to_delete:] | |
30 |
|
30 | |||
31 | for thread in old_threads: |
|
31 | for thread in old_threads: | |
32 | if settings.ARCHIVE_THREADS: |
|
32 | if settings.ARCHIVE_THREADS: | |
33 | self._archive_thread(thread) |
|
33 | self._archive_thread(thread) | |
34 | else: |
|
34 | else: | |
35 | thread.delete() |
|
35 | thread.delete() | |
36 |
|
36 | |||
37 | logger.info('Processed %d old threads' % num_threads_to_delete) |
|
37 | logger.info('Processed %d old threads' % num_threads_to_delete) | |
38 |
|
38 | |||
39 | def _archive_thread(self, thread): |
|
39 | def _archive_thread(self, thread): | |
40 | thread.archived = True |
|
40 | thread.archived = True | |
|
41 | thread.bumpable = False | |||
41 | thread.last_edit_time = timezone.now() |
|
42 | thread.last_edit_time = timezone.now() | |
42 | thread.save(update_fields=['archived', 'last_edit_time']) |
|
43 | thread.save(update_fields=['archived', 'last_edit_time', 'bumpable']) | |
43 |
|
44 | |||
44 |
|
45 | |||
45 | class Thread(models.Model): |
|
46 | class Thread(models.Model): | |
46 | objects = ThreadManager() |
|
47 | objects = ThreadManager() | |
47 |
|
48 | |||
48 | class Meta: |
|
49 | class Meta: | |
49 | app_label = 'boards' |
|
50 | app_label = 'boards' | |
50 |
|
51 | |||
51 | tags = models.ManyToManyField('Tag') |
|
52 | tags = models.ManyToManyField('Tag') | |
52 | bump_time = models.DateTimeField() |
|
53 | bump_time = models.DateTimeField() | |
53 | last_edit_time = models.DateTimeField() |
|
54 | last_edit_time = models.DateTimeField() | |
54 | replies = models.ManyToManyField('Post', symmetrical=False, null=True, |
|
55 | replies = models.ManyToManyField('Post', symmetrical=False, null=True, | |
55 | blank=True, related_name='tre+') |
|
56 | blank=True, related_name='tre+') | |
56 | archived = models.BooleanField(default=False) |
|
57 | archived = models.BooleanField(default=False) | |
|
58 | bumpable = models.BooleanField(default=True) | |||
57 |
|
59 | |||
58 | def get_tags(self): |
|
60 | def get_tags(self): | |
59 | """ |
|
61 | """ | |
60 | Gets a sorted tag list. |
|
62 | Gets a sorted tag list. | |
61 | """ |
|
63 | """ | |
62 |
|
64 | |||
63 | return self.tags.order_by('name') |
|
65 | return self.tags.order_by('name') | |
64 |
|
66 | |||
65 | def bump(self): |
|
67 | def bump(self): | |
66 | """ |
|
68 | """ | |
67 | Bumps (moves to up) thread if possible. |
|
69 | Bumps (moves to up) thread if possible. | |
68 | """ |
|
70 | """ | |
69 |
|
71 | |||
70 | if self.can_bump(): |
|
72 | if self.can_bump(): | |
71 | self.bump_time = timezone.now() |
|
73 | self.bump_time = timezone.now() | |
72 |
|
74 | |||
|
75 | if self.get_reply_count() >= settings.MAX_POSTS_PER_THREAD: | |||
|
76 | self.bumpable = False | |||
|
77 | ||||
73 | logger.info('Bumped thread %d' % self.id) |
|
78 | logger.info('Bumped thread %d' % self.id) | |
74 |
|
79 | |||
75 | def get_reply_count(self): |
|
80 | def get_reply_count(self): | |
76 | return self.replies.count() |
|
81 | return self.replies.count() | |
77 |
|
82 | |||
78 | def get_images_count(self): |
|
83 | def get_images_count(self): | |
79 | # TODO Use sum |
|
84 | return self.replies.annotate(images_count=Count( | |
80 | total_count = 0 |
|
85 | 'images')).aggregate(Sum('images_count'))['images_count__sum'] | |
81 | for post_with_image in self.replies.annotate(images_count=Count( |
|
|||
82 | 'images')): |
|
|||
83 | total_count += post_with_image.images_count |
|
|||
84 | return total_count |
|
|||
85 |
|
86 | |||
86 | def can_bump(self): |
|
87 | def can_bump(self): | |
87 | """ |
|
88 | """ | |
88 | Checks if the thread can be bumped by replying to it. |
|
89 | Checks if the thread can be bumped by replying to it. | |
89 | """ |
|
90 | """ | |
90 |
|
91 | |||
91 |
|
|
92 | return self.bumpable | |
92 | return False |
|
|||
93 |
|
||||
94 | post_count = self.get_reply_count() |
|
|||
95 |
|
||||
96 | return post_count < settings.MAX_POSTS_PER_THREAD |
|
|||
97 |
|
93 | |||
98 | def get_last_replies(self): |
|
94 | def get_last_replies(self): | |
99 | """ |
|
95 | """ | |
100 | Gets several last replies, not including opening post |
|
96 | Gets several last replies, not including opening post | |
101 | """ |
|
97 | """ | |
102 |
|
98 | |||
103 | if settings.LAST_REPLIES_COUNT > 0: |
|
99 | if settings.LAST_REPLIES_COUNT > 0: | |
104 | reply_count = self.get_reply_count() |
|
100 | reply_count = self.get_reply_count() | |
105 |
|
101 | |||
106 | if reply_count > 0: |
|
102 | if reply_count > 0: | |
107 | reply_count_to_show = min(settings.LAST_REPLIES_COUNT, |
|
103 | reply_count_to_show = min(settings.LAST_REPLIES_COUNT, | |
108 | reply_count - 1) |
|
104 | reply_count - 1) | |
109 | replies = self.get_replies() |
|
105 | replies = self.get_replies() | |
110 | last_replies = replies[reply_count - reply_count_to_show:] |
|
106 | last_replies = replies[reply_count - reply_count_to_show:] | |
111 |
|
107 | |||
112 | return last_replies |
|
108 | return last_replies | |
113 |
|
109 | |||
114 | def get_skipped_replies_count(self): |
|
110 | def get_skipped_replies_count(self): | |
115 | """ |
|
111 | """ | |
116 | Gets number of posts between opening post and last replies. |
|
112 | Gets number of posts between opening post and last replies. | |
117 | """ |
|
113 | """ | |
118 | reply_count = self.get_reply_count() |
|
114 | reply_count = self.get_reply_count() | |
119 | last_replies_count = min(settings.LAST_REPLIES_COUNT, |
|
115 | last_replies_count = min(settings.LAST_REPLIES_COUNT, | |
120 | reply_count - 1) |
|
116 | reply_count - 1) | |
121 | return reply_count - last_replies_count - 1 |
|
117 | return reply_count - last_replies_count - 1 | |
122 |
|
118 | |||
123 | def get_replies(self, view_fields_only=False): |
|
119 | def get_replies(self, view_fields_only=False): | |
124 | """ |
|
120 | """ | |
125 | Gets sorted thread posts |
|
121 | Gets sorted thread posts | |
126 | """ |
|
122 | """ | |
127 |
|
123 | |||
128 | query = self.replies.order_by('pub_time').prefetch_related('images') |
|
124 | query = self.replies.order_by('pub_time').prefetch_related('images') | |
129 | if view_fields_only: |
|
125 | if view_fields_only: | |
130 |
query = query.defer('poster_user_agent' |
|
126 | query = query.defer('poster_user_agent') | |
131 | return query.all() |
|
127 | return query.all() | |
132 |
|
128 | |||
133 | def get_replies_with_images(self, view_fields_only=False): |
|
129 | def get_replies_with_images(self, view_fields_only=False): | |
134 | return self.get_replies(view_fields_only).annotate(images_count=Count( |
|
130 | return self.get_replies(view_fields_only).annotate(images_count=Count( | |
135 | 'images')).filter(images_count__gt=0) |
|
131 | 'images')).filter(images_count__gt=0) | |
136 |
|
132 | |||
137 | def add_tag(self, tag): |
|
133 | def add_tag(self, tag): | |
138 | """ |
|
134 | """ | |
139 | Connects thread to a tag and tag to a thread |
|
135 | Connects thread to a tag and tag to a thread | |
140 | """ |
|
136 | """ | |
141 |
|
137 | |||
142 | self.tags.add(tag) |
|
138 | self.tags.add(tag) | |
143 | tag.threads.add(self) |
|
|||
144 |
|
||||
145 | def remove_tag(self, tag): |
|
|||
146 | self.tags.remove(tag) |
|
|||
147 | tag.threads.remove(self) |
|
|||
148 |
|
139 | |||
149 | def get_opening_post(self, only_id=False): |
|
140 | def get_opening_post(self, only_id=False): | |
150 | """ |
|
141 | """ | |
151 | Gets the first post of the thread |
|
142 | Gets the first post of the thread | |
152 | """ |
|
143 | """ | |
153 |
|
144 | |||
154 | query = self.replies.order_by('pub_time') |
|
145 | query = self.replies.order_by('pub_time') | |
155 | if only_id: |
|
146 | if only_id: | |
156 | query = query.only('id') |
|
147 | query = query.only('id') | |
157 | opening_post = query.first() |
|
148 | opening_post = query.first() | |
158 |
|
149 | |||
159 | return opening_post |
|
150 | return opening_post | |
160 |
|
151 | |||
161 | def get_opening_post_id(self): |
|
152 | def get_opening_post_id(self): | |
162 | """ |
|
153 | """ | |
163 | Gets ID of the first thread post. |
|
154 | Gets ID of the first thread post. | |
164 | """ |
|
155 | """ | |
165 |
|
156 | |||
166 | cache_key = CACHE_KEY_OPENING_POST + str(self.id) |
|
157 | cache_key = CACHE_KEY_OPENING_POST + str(self.id) | |
167 | opening_post_id = cache.get(cache_key) |
|
158 | opening_post_id = cache.get(cache_key) | |
168 | if not opening_post_id: |
|
159 | if not opening_post_id: | |
169 | opening_post_id = self.get_opening_post(only_id=True).id |
|
160 | opening_post_id = self.get_opening_post(only_id=True).id | |
170 | cache.set(cache_key, opening_post_id) |
|
161 | cache.set(cache_key, opening_post_id) | |
171 |
|
162 | |||
172 | return opening_post_id |
|
163 | return opening_post_id | |
173 |
|
164 | |||
174 | def __unicode__(self): |
|
165 | def __unicode__(self): | |
175 | return str(self.id) |
|
166 | return str(self.id) | |
176 |
|
167 | |||
177 | def get_pub_time(self): |
|
168 | def get_pub_time(self): | |
178 | """ |
|
169 | """ | |
179 | Gets opening post's pub time because thread does not have its own one. |
|
170 | Gets opening post's pub time because thread does not have its own one. | |
180 | """ |
|
171 | """ | |
181 |
|
172 | |||
182 | return self.get_opening_post().pub_time |
|
173 | return self.get_opening_post().pub_time | |
183 |
|
174 | |||
184 | def delete(self, using=None): |
|
175 | def delete(self, using=None): | |
185 | if self.replies.exists(): |
|
176 | if self.replies.exists(): | |
186 | self.replies.all().delete() |
|
177 | self.replies.all().delete() | |
187 |
|
178 | |||
188 | super(Thread, self).delete(using) No newline at end of file |
|
179 | super(Thread, self).delete(using) | |
|
180 | ||||
|
181 | def __str__(self): | |||
|
182 | return 'T#{}/{}'.format(self.id, self.get_opening_post_id()) No newline at end of file |
@@ -1,24 +1,24 b'' | |||||
1 | from haystack import indexes |
|
1 | from haystack import indexes | |
2 | from boards.models import Post, Tag |
|
2 | from boards.models import Post, Tag | |
3 |
|
3 | |||
4 | __author__ = 'neko259' |
|
4 | __author__ = 'neko259' | |
5 |
|
5 | |||
6 |
|
6 | |||
7 | class PostIndex(indexes.SearchIndex, indexes.Indexable): |
|
7 | class PostIndex(indexes.SearchIndex, indexes.Indexable): | |
8 | text = indexes.CharField(document=True, use_template=True) |
|
8 | text = indexes.CharField(document=True, use_template=True) | |
9 |
|
9 | |||
10 | def get_model(self): |
|
10 | def get_model(self): | |
11 | return Post |
|
11 | return Post | |
12 |
|
12 | |||
13 | def index_queryset(self, using=None): |
|
13 | def index_queryset(self, using=None): | |
14 | return self.get_model().objects.all() |
|
14 | return self.get_model().objects.all() | |
15 |
|
15 | |||
16 |
|
16 | |||
17 | class TagIndex(indexes.SearchIndex, indexes.Indexable): |
|
17 | class TagIndex(indexes.SearchIndex, indexes.Indexable): | |
18 | text = indexes.CharField(document=True, use_template=True) |
|
18 | text = indexes.CharField(document=True, use_template=True) | |
19 |
|
19 | |||
20 | def get_model(self): |
|
20 | def get_model(self): | |
21 | return Tag |
|
21 | return Tag | |
22 |
|
22 | |||
23 | def index_queryset(self, using=None): |
|
23 | def index_queryset(self, using=None): | |
24 |
return self.get_model().objects. |
|
24 | return self.get_model().objects.all() |
@@ -1,20 +1,22 b'' | |||||
1 |
VERSION = '2. |
|
1 | VERSION = '2.2.3 Miyu' | |
2 |
SITE_NAME = ' |
|
2 | SITE_NAME = 'Neboard' | |
3 |
|
3 | |||
4 | CACHE_TIMEOUT = 600 # Timeout for caching, if cache is used |
|
4 | CACHE_TIMEOUT = 600 # Timeout for caching, if cache is used | |
5 | LOGIN_TIMEOUT = 3600 # Timeout between login tries |
|
5 | LOGIN_TIMEOUT = 3600 # Timeout between login tries | |
6 | MAX_TEXT_LENGTH = 30000 # Max post length in characters |
|
6 | MAX_TEXT_LENGTH = 30000 # Max post length in characters | |
7 | MAX_IMAGE_SIZE = 8 * 1024 * 1024 # Max image size |
|
7 | MAX_IMAGE_SIZE = 8 * 1024 * 1024 # Max image size | |
8 |
|
8 | |||
9 | # Thread bumplimit |
|
9 | # Thread bumplimit | |
10 | MAX_POSTS_PER_THREAD = 10 |
|
10 | MAX_POSTS_PER_THREAD = 10 | |
11 | # Old posts will be archived or deleted if this value is reached |
|
11 | # Old posts will be archived or deleted if this value is reached | |
12 | MAX_THREAD_COUNT = 5 |
|
12 | MAX_THREAD_COUNT = 5 | |
13 | THREADS_PER_PAGE = 3 |
|
13 | THREADS_PER_PAGE = 3 | |
14 | DEFAULT_THEME = 'md' |
|
14 | DEFAULT_THEME = 'md' | |
15 | LAST_REPLIES_COUNT = 3 |
|
15 | LAST_REPLIES_COUNT = 3 | |
16 |
|
16 | |||
17 | # Enable archiving threads instead of deletion when the thread limit is reached |
|
17 | # Enable archiving threads instead of deletion when the thread limit is reached | |
18 | ARCHIVE_THREADS = True |
|
18 | ARCHIVE_THREADS = True | |
19 | # Limit posting speed |
|
19 | # Limit posting speed | |
20 | LIMIT_POSTING_SPEED = False |
|
20 | LIMIT_POSTING_SPEED = False | |
|
21 | # Thread update | |||
|
22 | WEBSOCKETS_ENABLED = True |
@@ -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 | html { |
|
10 | html { | |
2 | background: #555; |
|
11 | background: #555; | |
3 | color: #ffffff; |
|
12 | color: #ffffff; | |
4 | } |
|
13 | } | |
5 |
|
14 | |||
6 | body { |
|
15 | body { | |
7 | margin: 0; |
|
16 | margin: 0; | |
8 | } |
|
17 | } | |
9 |
|
18 | |||
10 | #admin_panel { |
|
19 | #admin_panel { | |
11 | background: #FF0000; |
|
20 | background: #FF0000; | |
12 | color: #00FF00 |
|
21 | color: #00FF00 | |
13 | } |
|
22 | } | |
14 |
|
23 | |||
15 | .input_field_error { |
|
24 | .input_field_error { | |
16 | color: #FF0000; |
|
25 | color: #FF0000; | |
17 | } |
|
26 | } | |
18 |
|
27 | |||
19 | .title { |
|
28 | .title { | |
20 | font-weight: bold; |
|
29 | font-weight: bold; | |
21 | color: #ffcc00; |
|
30 | color: #ffcc00; | |
22 | } |
|
31 | } | |
23 |
|
32 | |||
24 | .link, a { |
|
33 | .link, a { | |
25 | color: #afdcec; |
|
34 | color: #afdcec; | |
26 | } |
|
35 | } | |
27 |
|
36 | |||
28 | .block { |
|
37 | .block { | |
29 | display: inline-block; |
|
38 | display: inline-block; | |
30 | vertical-align: top; |
|
39 | vertical-align: top; | |
31 | } |
|
40 | } | |
32 |
|
41 | |||
33 | .tag { |
|
42 | .tag { | |
34 | color: #FFD37D; |
|
43 | color: #FFD37D; | |
35 | } |
|
44 | } | |
36 |
|
45 | |||
37 | .post_id { |
|
46 | .post_id { | |
38 | color: #fff380; |
|
47 | color: #fff380; | |
39 | } |
|
48 | } | |
40 |
|
49 | |||
41 | .post, .dead_post, .archive_post, #posts-table { |
|
50 | .post, .dead_post, .archive_post, #posts-table { | |
42 | background: #333; |
|
51 | background: #333; | |
43 | padding: 10px; |
|
52 | padding: 10px; | |
44 | clear: left; |
|
53 | clear: left; | |
45 | word-wrap: break-word; |
|
54 | word-wrap: break-word; | |
46 | border-top: 1px solid #777; |
|
55 | border-top: 1px solid #777; | |
47 | border-bottom: 1px solid #777; |
|
56 | border-bottom: 1px solid #777; | |
48 | } |
|
57 | } | |
49 |
|
58 | |||
50 | .post + .post { |
|
59 | .post + .post { | |
51 | border-top: none; |
|
60 | border-top: none; | |
52 | } |
|
61 | } | |
53 |
|
62 | |||
54 | .dead_post + .dead_post { |
|
63 | .dead_post + .dead_post { | |
55 | border-top: none; |
|
64 | border-top: none; | |
56 | } |
|
65 | } | |
57 |
|
66 | |||
58 | .archive_post + .archive_post { |
|
67 | .archive_post + .archive_post { | |
59 | border-top: none; |
|
68 | border-top: none; | |
60 | } |
|
69 | } | |
61 |
|
70 | |||
62 | .metadata { |
|
71 | .metadata { | |
63 | padding-top: 5px; |
|
72 | padding-top: 5px; | |
64 | margin-top: 10px; |
|
73 | margin-top: 10px; | |
65 | border-top: solid 1px #666; |
|
74 | border-top: solid 1px #666; | |
66 | color: #ddd; |
|
75 | color: #ddd; | |
67 | } |
|
76 | } | |
68 |
|
77 | |||
69 | .navigation_panel, .tag_info { |
|
78 | .navigation_panel, .tag_info { | |
70 | background: #222; |
|
79 | background: #222; | |
71 | margin-bottom: 5px; |
|
80 | margin-bottom: 5px; | |
72 | margin-top: 5px; |
|
81 | margin-top: 5px; | |
73 | padding: 10px; |
|
82 | padding: 10px; | |
74 | border-bottom: solid 1px #888; |
|
83 | border-bottom: solid 1px #888; | |
75 | border-top: solid 1px #888; |
|
84 | border-top: solid 1px #888; | |
76 | color: #eee; |
|
85 | color: #eee; | |
77 | } |
|
86 | } | |
78 |
|
87 | |||
79 | .navigation_panel .link { |
|
88 | .navigation_panel .link { | |
80 | border-right: 1px solid #fff; |
|
89 | border-right: 1px solid #fff; | |
81 | font-weight: bold; |
|
90 | font-weight: bold; | |
82 | margin-right: 1ex; |
|
91 | margin-right: 1ex; | |
83 | padding-right: 1ex; |
|
92 | padding-right: 1ex; | |
84 | } |
|
93 | } | |
85 | .navigation_panel .link:last-child { |
|
94 | .navigation_panel .link:last-child { | |
86 | border-left: 1px solid #fff; |
|
95 | border-left: 1px solid #fff; | |
87 | border-right: none; |
|
96 | border-right: none; | |
88 | float: right; |
|
97 | float: right; | |
89 | margin-left: 1ex; |
|
98 | margin-left: 1ex; | |
90 | margin-right: 0; |
|
99 | margin-right: 0; | |
91 | padding-left: 1ex; |
|
100 | padding-left: 1ex; | |
92 | padding-right: 0; |
|
101 | padding-right: 0; | |
93 | } |
|
102 | } | |
94 |
|
103 | |||
95 | .navigation_panel::after, .post::after { |
|
104 | .navigation_panel::after, .post::after { | |
96 | clear: both; |
|
105 | clear: both; | |
97 | content: "."; |
|
106 | content: "."; | |
98 | display: block; |
|
107 | display: block; | |
99 | height: 0; |
|
108 | height: 0; | |
100 | line-height: 0; |
|
109 | line-height: 0; | |
101 | visibility: hidden; |
|
110 | visibility: hidden; | |
102 | } |
|
111 | } | |
103 |
|
112 | |||
104 | .header { |
|
113 | .header { | |
105 | border-bottom: solid 2px #ccc; |
|
114 | border-bottom: solid 2px #ccc; | |
106 | margin-bottom: 5px; |
|
115 | margin-bottom: 5px; | |
107 | border-top: none; |
|
116 | border-top: none; | |
108 | margin-top: 0; |
|
117 | margin-top: 0; | |
109 | } |
|
118 | } | |
110 |
|
119 | |||
111 | .footer { |
|
120 | .footer { | |
112 | border-top: solid 2px #ccc; |
|
121 | border-top: solid 2px #ccc; | |
113 | margin-top: 5px; |
|
122 | margin-top: 5px; | |
114 | border-bottom: none; |
|
123 | border-bottom: none; | |
115 | margin-bottom: 0; |
|
124 | margin-bottom: 0; | |
116 | } |
|
125 | } | |
117 |
|
126 | |||
118 | p, .br { |
|
127 | p, .br { | |
119 | margin-top: .5em; |
|
128 | margin-top: .5em; | |
120 | margin-bottom: .5em; |
|
129 | margin-bottom: .5em; | |
121 | } |
|
130 | } | |
122 |
|
131 | |||
123 | .post-form-w { |
|
132 | .post-form-w { | |
124 | background: #333344; |
|
133 | background: #333344; | |
125 | border-top: solid 1px #888; |
|
134 | border-top: solid 1px #888; | |
126 | border-bottom: solid 1px #888; |
|
135 | border-bottom: solid 1px #888; | |
127 | color: #fff; |
|
136 | color: #fff; | |
128 | padding: 10px; |
|
137 | padding: 10px; | |
129 | margin-bottom: 5px; |
|
138 | margin-bottom: 5px; | |
130 | margin-top: 5px; |
|
139 | margin-top: 5px; | |
131 | } |
|
140 | } | |
132 |
|
141 | |||
133 | .form-row { |
|
142 | .form-row { | |
134 | width: 100%; |
|
143 | width: 100%; | |
135 | } |
|
144 | } | |
136 |
|
145 | |||
137 | .form-label { |
|
146 | .form-label { | |
138 | padding: .25em 1ex .25em 0; |
|
147 | padding: .25em 1ex .25em 0; | |
139 | vertical-align: top; |
|
148 | vertical-align: top; | |
140 | } |
|
149 | } | |
141 |
|
150 | |||
142 | .form-input { |
|
151 | .form-input { | |
143 | padding: .25em 0; |
|
152 | padding: .25em 0; | |
144 | } |
|
153 | } | |
145 |
|
154 | |||
146 | .form-errors { |
|
155 | .form-errors { | |
147 | font-weight: bolder; |
|
156 | font-weight: bolder; | |
148 | vertical-align: middle; |
|
157 | vertical-align: middle; | |
149 | } |
|
158 | } | |
150 |
|
159 | |||
151 | .post-form input:not([name="image"]), .post-form textarea { |
|
160 | .post-form input:not([name="image"]), .post-form textarea { | |
152 | background: #333; |
|
161 | background: #333; | |
153 | color: #fff; |
|
162 | color: #fff; | |
154 | border: solid 1px; |
|
163 | border: solid 1px; | |
155 | padding: 0; |
|
164 | padding: 0; | |
156 | font: medium sans-serif; |
|
165 | font: medium sans-serif; | |
157 | width: 100%; |
|
166 | width: 100%; | |
158 | } |
|
167 | } | |
159 |
|
168 | |||
|
169 | .post-form textarea { | |||
|
170 | resize: vertical; | |||
|
171 | } | |||
|
172 | ||||
160 | .form-submit { |
|
173 | .form-submit { | |
161 | display: table; |
|
174 | display: table; | |
162 | margin-bottom: 1ex; |
|
175 | margin-bottom: 1ex; | |
163 | margin-top: 1ex; |
|
176 | margin-top: 1ex; | |
164 | } |
|
177 | } | |
165 |
|
178 | |||
166 | .form-title { |
|
179 | .form-title { | |
167 | font-weight: bold; |
|
180 | font-weight: bold; | |
168 | font-size: 2ex; |
|
181 | font-size: 2ex; | |
169 | margin-bottom: 0.5ex; |
|
182 | margin-bottom: 0.5ex; | |
170 | } |
|
183 | } | |
171 |
|
184 | |||
172 | .post-form input[type="submit"], input[type="submit"] { |
|
185 | .post-form input[type="submit"], input[type="submit"] { | |
173 | background: #222; |
|
186 | background: #222; | |
174 | border: solid 2px #fff; |
|
187 | border: solid 2px #fff; | |
175 | color: #fff; |
|
188 | color: #fff; | |
176 | padding: 0.5ex; |
|
189 | padding: 0.5ex; | |
177 | } |
|
190 | } | |
178 |
|
191 | |||
179 | input[type="submit"]:hover { |
|
192 | input[type="submit"]:hover { | |
180 | background: #060; |
|
193 | background: #060; | |
181 | } |
|
194 | } | |
182 |
|
195 | |||
183 | blockquote { |
|
196 | blockquote { | |
184 | border-left: solid 2px; |
|
197 | border-left: solid 2px; | |
185 | padding-left: 5px; |
|
198 | padding-left: 5px; | |
186 | color: #B1FB17; |
|
199 | color: #B1FB17; | |
187 | margin: 0; |
|
200 | margin: 0; | |
188 | } |
|
201 | } | |
189 |
|
202 | |||
190 | .post > .image { |
|
203 | .post > .image { | |
191 | float: left; |
|
204 | float: left; | |
192 | margin: 0 1ex .5ex 0; |
|
205 | margin: 0 1ex .5ex 0; | |
193 | min-width: 1px; |
|
206 | min-width: 1px; | |
194 | text-align: center; |
|
207 | text-align: center; | |
195 | display: table-row; |
|
208 | display: table-row; | |
196 | } |
|
209 | } | |
197 |
|
210 | |||
198 | .post > .metadata { |
|
211 | .post > .metadata { | |
199 | clear: left; |
|
212 | clear: left; | |
200 | } |
|
213 | } | |
201 |
|
214 | |||
202 | .get { |
|
215 | .get { | |
203 | font-weight: bold; |
|
216 | font-weight: bold; | |
204 | color: #d55; |
|
217 | color: #d55; | |
205 | } |
|
218 | } | |
206 |
|
219 | |||
207 | * { |
|
220 | * { | |
208 | text-decoration: none; |
|
221 | text-decoration: none; | |
209 | } |
|
222 | } | |
210 |
|
223 | |||
211 | .dead_post { |
|
224 | .dead_post { | |
212 | background-color: #442222; |
|
225 | background-color: #442222; | |
213 | } |
|
226 | } | |
214 |
|
227 | |||
215 | .archive_post { |
|
228 | .archive_post { | |
216 | background-color: #000; |
|
229 | background-color: #000; | |
217 | } |
|
230 | } | |
218 |
|
231 | |||
219 | .mark_btn { |
|
232 | .mark_btn { | |
220 | border: 1px solid; |
|
233 | border: 1px solid; | |
221 | min-width: 2ex; |
|
234 | min-width: 2ex; | |
222 | padding: 2px 2ex; |
|
235 | padding: 2px 2ex; | |
223 | } |
|
236 | } | |
224 |
|
237 | |||
225 | .mark_btn:hover { |
|
238 | .mark_btn:hover { | |
226 | background: #555; |
|
239 | background: #555; | |
227 | } |
|
240 | } | |
228 |
|
241 | |||
229 | .quote { |
|
242 | .quote { | |
230 | color: #92cf38; |
|
243 | color: #92cf38; | |
231 | font-style: italic; |
|
244 | font-style: italic; | |
232 | } |
|
245 | } | |
233 |
|
246 | |||
234 | .multiquote { |
|
247 | .multiquote { | |
235 | padding: 3px; |
|
248 | padding: 3px; | |
236 | display: inline-block; |
|
249 | display: inline-block; | |
237 | background: #222; |
|
250 | background: #222; | |
238 | border-style: solid; |
|
251 | border-style: solid; | |
239 | border-width: 1px 1px 1px 4px; |
|
252 | border-width: 1px 1px 1px 4px; | |
240 | font-size: 0.9em; |
|
253 | font-size: 0.9em; | |
241 | } |
|
254 | } | |
242 |
|
255 | |||
243 | .spoiler { |
|
256 | .spoiler { | |
244 | background: white; |
|
257 | background: white; | |
245 | color: white; |
|
258 | color: white; | |
246 | } |
|
259 | } | |
247 |
|
260 | |||
248 | .spoiler:hover { |
|
261 | .spoiler:hover { | |
249 | color: black; |
|
262 | color: black; | |
250 | } |
|
263 | } | |
251 |
|
264 | |||
252 | .comment { |
|
265 | .comment { | |
253 | color: #eb2; |
|
266 | color: #eb2; | |
254 | } |
|
267 | } | |
255 |
|
268 | |||
256 | a:hover { |
|
269 | a:hover { | |
257 | text-decoration: underline; |
|
270 | text-decoration: underline; | |
258 | } |
|
271 | } | |
259 |
|
272 | |||
260 | .last-replies { |
|
273 | .last-replies { | |
261 | margin-left: 3ex; |
|
274 | margin-left: 3ex; | |
262 | margin-right: 3ex; |
|
275 | margin-right: 3ex; | |
263 | border-left: solid 1px #777; |
|
276 | border-left: solid 1px #777; | |
264 | border-right: solid 1px #777; |
|
277 | border-right: solid 1px #777; | |
265 | } |
|
278 | } | |
266 |
|
279 | |||
267 | .last-replies > .post:first-child { |
|
280 | .last-replies > .post:first-child { | |
268 | border-top: none; |
|
281 | border-top: none; | |
269 | } |
|
282 | } | |
270 |
|
283 | |||
271 | .thread { |
|
284 | .thread { | |
272 | margin-bottom: 3ex; |
|
285 | margin-bottom: 3ex; | |
273 | margin-top: 1ex; |
|
286 | margin-top: 1ex; | |
274 | } |
|
287 | } | |
275 |
|
288 | |||
276 | .post:target { |
|
289 | .post:target { | |
277 | border: solid 2px white; |
|
290 | border: solid 2px white; | |
278 | } |
|
291 | } | |
279 |
|
292 | |||
280 | pre{ |
|
293 | pre{ | |
281 | white-space:pre-wrap |
|
294 | white-space:pre-wrap | |
282 | } |
|
295 | } | |
283 |
|
296 | |||
284 | li { |
|
297 | li { | |
285 | list-style-position: inside; |
|
298 | list-style-position: inside; | |
286 | } |
|
299 | } | |
287 |
|
300 | |||
288 | .fancybox-skin { |
|
301 | .fancybox-skin { | |
289 | position: relative; |
|
302 | position: relative; | |
290 | background-color: #fff; |
|
303 | background-color: #fff; | |
291 | color: #ddd; |
|
304 | color: #ddd; | |
292 | text-shadow: none; |
|
305 | text-shadow: none; | |
293 | } |
|
306 | } | |
294 |
|
307 | |||
295 | .fancybox-image { |
|
308 | .fancybox-image { | |
296 | border: 1px solid black; |
|
309 | border: 1px solid black; | |
297 | } |
|
310 | } | |
298 |
|
311 | |||
299 | .image-mode-tab { |
|
312 | .image-mode-tab { | |
300 | background: #444; |
|
313 | background: #444; | |
301 | color: #eee; |
|
314 | color: #eee; | |
302 | margin-top: 5px; |
|
315 | margin-top: 5px; | |
303 | padding: 5px; |
|
316 | padding: 5px; | |
304 | border-top: 1px solid #888; |
|
317 | border-top: 1px solid #888; | |
305 | border-bottom: 1px solid #888; |
|
318 | border-bottom: 1px solid #888; | |
306 | } |
|
319 | } | |
307 |
|
320 | |||
308 | .image-mode-tab > label { |
|
321 | .image-mode-tab > label { | |
309 | margin: 0 1ex; |
|
322 | margin: 0 1ex; | |
310 | } |
|
323 | } | |
311 |
|
324 | |||
312 | .image-mode-tab > label > input { |
|
325 | .image-mode-tab > label > input { | |
313 | margin-right: .5ex; |
|
326 | margin-right: .5ex; | |
314 | } |
|
327 | } | |
315 |
|
328 | |||
316 | #posts-table { |
|
329 | #posts-table { | |
317 | margin-top: 5px; |
|
330 | margin-top: 5px; | |
318 | margin-bottom: 5px; |
|
331 | margin-bottom: 5px; | |
319 | } |
|
332 | } | |
320 |
|
333 | |||
321 | .tag_info > h2 { |
|
334 | .tag_info > h2 { | |
322 | margin: 0; |
|
335 | margin: 0; | |
323 | } |
|
336 | } | |
324 |
|
337 | |||
325 | .post-info { |
|
338 | .post-info { | |
326 | color: #ddd; |
|
339 | color: #ddd; | |
327 | margin-bottom: 1ex; |
|
340 | margin-bottom: 1ex; | |
328 | } |
|
341 | } | |
329 |
|
342 | |||
330 | .moderator_info { |
|
343 | .moderator_info { | |
331 | color: #e99d41; |
|
344 | color: #e99d41; | |
332 | float: right; |
|
345 | float: right; | |
333 | font-weight: bold; |
|
346 | font-weight: bold; | |
334 | } |
|
347 | } | |
335 |
|
348 | |||
336 | .refmap { |
|
349 | .refmap { | |
337 | font-size: 0.9em; |
|
350 | font-size: 0.9em; | |
338 | color: #ccc; |
|
351 | color: #ccc; | |
339 | margin-top: 1em; |
|
352 | margin-top: 1em; | |
340 | } |
|
353 | } | |
341 |
|
354 | |||
342 | .fav { |
|
355 | .fav { | |
343 | color: yellow; |
|
356 | color: yellow; | |
344 | } |
|
357 | } | |
345 |
|
358 | |||
346 | .not_fav { |
|
359 | .not_fav { | |
347 | color: #ccc; |
|
360 | color: #ccc; | |
348 | } |
|
361 | } | |
349 |
|
362 | |||
350 | .role { |
|
363 | .role { | |
351 | text-decoration: underline; |
|
364 | text-decoration: underline; | |
352 | } |
|
365 | } | |
353 |
|
366 | |||
354 | .form-email { |
|
367 | .form-email { | |
355 | display: none; |
|
368 | display: none; | |
356 | } |
|
369 | } | |
357 |
|
370 | |||
358 | .bar-value { |
|
371 | .bar-value { | |
359 | background: rgba(50, 55, 164, 0.45); |
|
372 | background: rgba(50, 55, 164, 0.45); | |
360 | font-size: 0.9em; |
|
373 | font-size: 0.9em; | |
361 | height: 1.5em; |
|
374 | height: 1.5em; | |
362 | } |
|
375 | } | |
363 |
|
376 | |||
364 | .bar-bg { |
|
377 | .bar-bg { | |
365 | position: relative; |
|
378 | position: relative; | |
366 | border-top: solid 1px #888; |
|
379 | border-top: solid 1px #888; | |
367 | border-bottom: solid 1px #888; |
|
380 | border-bottom: solid 1px #888; | |
368 | margin-top: 5px; |
|
381 | margin-top: 5px; | |
369 | overflow: hidden; |
|
382 | overflow: hidden; | |
370 | } |
|
383 | } | |
371 |
|
384 | |||
372 | .bar-text { |
|
385 | .bar-text { | |
373 | padding: 2px; |
|
386 | padding: 2px; | |
374 | position: absolute; |
|
387 | position: absolute; | |
375 | left: 0; |
|
388 | left: 0; | |
376 | top: 0; |
|
389 | top: 0; | |
377 | } |
|
390 | } | |
378 |
|
391 | |||
379 | .page_link { |
|
392 | .page_link { | |
380 | background: #444; |
|
393 | background: #444; | |
381 | border-top: solid 1px #888; |
|
394 | border-top: solid 1px #888; | |
382 | border-bottom: solid 1px #888; |
|
395 | border-bottom: solid 1px #888; | |
383 | padding: 5px; |
|
396 | padding: 5px; | |
384 | color: #eee; |
|
397 | color: #eee; | |
385 | font-size: 2ex; |
|
398 | font-size: 2ex; | |
386 | } |
|
399 | } | |
387 |
|
400 | |||
388 | .skipped_replies { |
|
401 | .skipped_replies { | |
389 | padding: 5px; |
|
402 | padding: 5px; | |
390 | margin-left: 3ex; |
|
403 | margin-left: 3ex; | |
391 | margin-right: 3ex; |
|
404 | margin-right: 3ex; | |
392 | border-left: solid 1px #888; |
|
405 | border-left: solid 1px #888; | |
393 | border-right: solid 1px #888; |
|
406 | border-right: solid 1px #888; | |
394 | border-bottom: solid 1px #888; |
|
407 | border-bottom: solid 1px #888; | |
395 | background: #000; |
|
408 | background: #000; | |
396 | } |
|
409 | } | |
397 |
|
410 | |||
398 | .current_page { |
|
411 | .current_page { | |
399 | padding: 2px; |
|
412 | padding: 2px; | |
400 | background-color: #afdcec; |
|
413 | background-color: #afdcec; | |
401 | color: #000; |
|
414 | color: #000; | |
402 | } |
|
415 | } | |
403 |
|
416 | |||
404 | .current_mode { |
|
417 | .current_mode { | |
405 | font-weight: bold; |
|
418 | font-weight: bold; | |
406 | } |
|
419 | } | |
407 |
|
420 | |||
408 | .gallery_image { |
|
421 | .gallery_image { | |
409 | border: solid 1px; |
|
422 | border: solid 1px; | |
410 | padding: 0.5ex; |
|
423 | padding: 0.5ex; | |
411 | margin: 0.5ex; |
|
424 | margin: 0.5ex; | |
412 | text-align: center; |
|
425 | text-align: center; | |
413 | } |
|
426 | } | |
414 |
|
427 | |||
415 | code { |
|
428 | code { | |
416 | border: dashed 1px #ccc; |
|
429 | border: dashed 1px #ccc; | |
417 | background: #111; |
|
430 | background: #111; | |
418 | padding: 2px; |
|
431 | padding: 2px; | |
419 | font-size: 1.2em; |
|
432 | font-size: 1.2em; | |
420 | display: inline-block; |
|
433 | display: inline-block; | |
421 | } |
|
434 | } | |
422 |
|
435 | |||
423 | pre { |
|
436 | pre { | |
424 | overflow: auto; |
|
437 | overflow: auto; | |
425 | } |
|
438 | } | |
426 |
|
439 | |||
427 | .img-full { |
|
440 | .img-full { | |
428 | background: #222; |
|
441 | background: #222; | |
429 | border: solid 1px white; |
|
442 | border: solid 1px white; | |
430 | } |
|
443 | } | |
431 |
|
444 | |||
432 | .tag_item { |
|
445 | .tag_item { | |
433 | display: inline-block; |
|
446 | display: inline-block; | |
434 | border: 1px dashed #666; |
|
447 | border: 1px dashed #666; | |
435 | margin: 0.2ex; |
|
448 | margin: 0.2ex; | |
436 | padding: 0.1ex; |
|
449 | padding: 0.1ex; | |
437 | } |
|
450 | } | |
438 |
|
451 | |||
439 | #id_models li { |
|
452 | #id_models li { | |
440 | list-style: none; |
|
453 | list-style: none; | |
441 | } |
|
454 | } | |
442 |
|
455 | |||
443 | #id_q { |
|
456 | #id_q { | |
444 | margin-left: 1ex; |
|
457 | margin-left: 1ex; | |
445 | } |
|
458 | } | |
446 |
|
459 | |||
447 | ul { |
|
460 | ul { | |
448 | padding-left: 0px; |
|
461 | padding-left: 0px; | |
449 | } |
|
462 | } | |
450 |
|
463 | |||
451 | .quote-header { |
|
464 | .quote-header { | |
452 | border-bottom: 2px solid #ddd; |
|
465 | border-bottom: 2px solid #ddd; | |
453 | margin-bottom: 1ex; |
|
466 | margin-bottom: 1ex; | |
454 | padding-bottom: .5ex; |
|
467 | padding-bottom: .5ex; | |
455 | color: #ddd; |
|
468 | color: #ddd; | |
456 | font-size: 1.2em; |
|
469 | font-size: 1.2em; | |
457 | } |
|
470 | } | |
458 |
|
471 | |||
459 | .global-id { |
|
472 | .global-id { | |
460 | font-weight: bolder; |
|
473 | font-weight: bolder; | |
461 | opacity: .5; |
|
474 | opacity: .5; | |
462 | } |
|
475 | } | |
463 |
|
476 | |||
464 | /* Post */ |
|
477 | /* Post */ | |
465 | .post > .message, .post > .image { |
|
478 | .post > .message, .post > .image { | |
466 | padding-left: 1em; |
|
479 | padding-left: 1em; | |
467 | } |
|
480 | } | |
468 |
|
481 | |||
469 | /* Reflink preview */ |
|
482 | /* Reflink preview */ | |
470 | .post_preview { |
|
483 | .post_preview { | |
471 | border-left: 1px solid #777; |
|
484 | border-left: 1px solid #777; | |
472 | border-right: 1px solid #777; |
|
485 | border-right: 1px solid #777; | |
473 | } |
|
486 | } | |
474 |
|
487 | |||
475 | /* Code highlighter */ |
|
488 | /* Code highlighter */ | |
476 | .hljs { |
|
489 | .hljs { | |
477 | color: #fff; |
|
490 | color: #fff; | |
478 | background: #000; |
|
491 | background: #000; | |
479 | display: inline-block; |
|
492 | display: inline-block; | |
480 | } |
|
493 | } | |
481 |
|
494 | |||
482 | .hljs, .hljs-subst, .hljs-tag .hljs-title, .lisp .hljs-title, .clojure .hljs-built_in, .nginx .hljs-title { |
|
495 | .hljs, .hljs-subst, .hljs-tag .hljs-title, .lisp .hljs-title, .clojure .hljs-built_in, .nginx .hljs-title { | |
483 | color: #fff; |
|
496 | color: #fff; | |
484 | } |
|
497 | } |
@@ -1,288 +1,334 b'' | |||||
1 | /* |
|
1 | /* | |
2 | @licstart The following is the entire license notice for the |
|
2 | @licstart The following is the entire license notice for the | |
3 | JavaScript code in this page. |
|
3 | JavaScript code in this page. | |
4 |
|
4 | |||
5 |
|
5 | |||
6 | Copyright (C) 2013 neko259 |
|
6 | Copyright (C) 2013-2014 neko259 | |
7 |
|
7 | |||
8 | The JavaScript code in this page is free software: you can |
|
8 | The JavaScript code in this page is free software: you can | |
9 | redistribute it and/or modify it under the terms of the GNU |
|
9 | redistribute it and/or modify it under the terms of the GNU | |
10 | General Public License (GNU GPL) as published by the Free Software |
|
10 | General Public License (GNU GPL) as published by the Free Software | |
11 | Foundation, either version 3 of the License, or (at your option) |
|
11 | Foundation, either version 3 of the License, or (at your option) | |
12 | any later version. The code is distributed WITHOUT ANY WARRANTY; |
|
12 | any later version. The code is distributed WITHOUT ANY WARRANTY; | |
13 | without even the implied warranty of MERCHANTABILITY or FITNESS |
|
13 | without even the implied warranty of MERCHANTABILITY or FITNESS | |
14 | FOR A PARTICULAR PURPOSE. See the GNU GPL for more details. |
|
14 | FOR A PARTICULAR PURPOSE. See the GNU GPL for more details. | |
15 |
|
15 | |||
16 | As additional permission under GNU GPL version 3 section 7, you |
|
16 | As additional permission under GNU GPL version 3 section 7, you | |
17 | may distribute non-source (e.g., minimized or compacted) forms of |
|
17 | may distribute non-source (e.g., minimized or compacted) forms of | |
18 | that code without the copy of the GNU GPL normally required by |
|
18 | that code without the copy of the GNU GPL normally required by | |
19 | section 4, provided you include this license notice and a URL |
|
19 | section 4, provided you include this license notice and a URL | |
20 | through which recipients can access the Corresponding Source. |
|
20 | through which recipients can access the Corresponding Source. | |
21 |
|
21 | |||
22 | @licend The above is the entire license notice |
|
22 | @licend The above is the entire license notice | |
23 | for the JavaScript code in this page. |
|
23 | for the JavaScript code in this page. | |
24 | */ |
|
24 | */ | |
25 |
|
25 | |||
26 | var THREAD_UPDATE_DELAY = 10000; |
|
26 | var wsUser = ''; | |
27 |
|
27 | |||
28 | var loading = false; |
|
28 | var loading = false; | |
29 | var lastUpdateTime = null; |
|
|||
30 | var unreadPosts = 0; |
|
29 | var unreadPosts = 0; | |
|
30 | var documentOriginalTitle = ''; | |||
31 |
|
31 | |||
|
32 | // Thread ID does not change, can be stored one time | |||
|
33 | var threadId = $('div.thread').children('.post').first().attr('id'); | |||
|
34 | ||||
|
35 | /** | |||
|
36 | * Connect to websocket server and subscribe to thread updates. On any update we | |||
|
37 | * request a thread diff. | |||
|
38 | * | |||
|
39 | * @returns {boolean} true if connected, false otherwise | |||
|
40 | */ | |||
|
41 | function connectWebsocket() { | |||
|
42 | var metapanel = $('.metapanel')[0]; | |||
|
43 | ||||
|
44 | var wsHost = metapanel.getAttribute('data-ws-host'); | |||
|
45 | var wsPort = metapanel.getAttribute('data-ws-port'); | |||
|
46 | ||||
|
47 | if (wsHost.length > 0 && wsPort.length > 0) | |||
|
48 | var centrifuge = new Centrifuge({ | |||
|
49 | "url": 'ws://' + wsHost + ':' + wsPort + "/connection/websocket", | |||
|
50 | "project": metapanel.getAttribute('data-ws-project'), | |||
|
51 | "user": wsUser, | |||
|
52 | "timestamp": metapanel.getAttribute('data-last-update'), | |||
|
53 | "token": metapanel.getAttribute('data-ws-token'), | |||
|
54 | "debug": false | |||
|
55 | }); | |||
|
56 | ||||
|
57 | centrifuge.on('error', function(error_message) { | |||
|
58 | console.log("Error connecting to websocket server."); | |||
|
59 | return false; | |||
|
60 | }); | |||
|
61 | ||||
|
62 | centrifuge.on('connect', function() { | |||
|
63 | var channelName = 'thread:' + threadId; | |||
|
64 | centrifuge.subscribe(channelName, function(message) { | |||
|
65 | getThreadDiff(); | |||
|
66 | }); | |||
|
67 | ||||
|
68 | // For the case we closed the browser and missed some updates | |||
|
69 | getThreadDiff(); | |||
|
70 | $('#autoupdate').text('[+]'); | |||
|
71 | }); | |||
|
72 | ||||
|
73 | centrifuge.connect(); | |||
|
74 | ||||
|
75 | return true; | |||
|
76 | } | |||
|
77 | ||||
|
78 | /** | |||
|
79 | * Get diff of the posts from the current thread timestamp. | |||
|
80 | * This is required if the browser was closed and some post updates were | |||
|
81 | * missed. | |||
|
82 | */ | |||
|
83 | function getThreadDiff() { | |||
|
84 | var lastUpdateTime = $('.metapanel').attr('data-last-update'); | |||
|
85 | ||||
|
86 | var diffUrl = '/api/diff_thread/' + threadId + '/' + lastUpdateTime + '/'; | |||
|
87 | ||||
|
88 | $.getJSON(diffUrl) | |||
|
89 | .success(function(data) { | |||
|
90 | var addedPosts = data.added; | |||
|
91 | ||||
|
92 | for (var i = 0; i < addedPosts.length; i++) { | |||
|
93 | var postText = addedPosts[i]; | |||
|
94 | var post = $(postText); | |||
|
95 | ||||
|
96 | updatePost(post) | |||
|
97 | ||||
|
98 | lastPost = post; | |||
|
99 | } | |||
|
100 | ||||
|
101 | var updatedPosts = data.updated; | |||
|
102 | ||||
|
103 | for (var i = 0; i < updatedPosts.length; i++) { | |||
|
104 | var postText = updatedPosts[i]; | |||
|
105 | var post = $(postText); | |||
|
106 | ||||
|
107 | updatePost(post) | |||
|
108 | } | |||
|
109 | ||||
|
110 | // TODO Process removed posts if any | |||
|
111 | $('.metapanel').attr('data-last-update', data.last_update); | |||
|
112 | }) | |||
|
113 | } | |||
|
114 | ||||
|
115 | /** | |||
|
116 | * Add or update the post on html page. | |||
|
117 | */ | |||
|
118 | function updatePost(postHtml) { | |||
|
119 | // This needs to be set on start because the page is scrolled after posts | |||
|
120 | // are added or updated | |||
|
121 | var bottom = isPageBottom(); | |||
|
122 | ||||
|
123 | var post = $(postHtml); | |||
|
124 | ||||
|
125 | var threadBlock = $('div.thread'); | |||
|
126 | ||||
|
127 | var lastUpdate = ''; | |||
|
128 | ||||
|
129 | var postId = post.attr('id'); | |||
|
130 | ||||
|
131 | // If the post already exists, replace it. Otherwise add as a new one. | |||
|
132 | var existingPosts = threadBlock.children('.post[id=' + postId + ']'); | |||
|
133 | ||||
|
134 | if (existingPosts.size() > 0) { | |||
|
135 | existingPosts.replaceWith(post); | |||
|
136 | } else { | |||
|
137 | var threadPosts = threadBlock.children('.post'); | |||
|
138 | var lastPost = threadPosts.last(); | |||
|
139 | ||||
|
140 | post.appendTo(lastPost.parent()); | |||
|
141 | ||||
|
142 | updateBumplimitProgress(1); | |||
|
143 | showNewPostsTitle(1); | |||
|
144 | ||||
|
145 | lastUpdate = post.children('.post-info').first() | |||
|
146 | .children('.pub_time').first().text(); | |||
|
147 | ||||
|
148 | if (bottom) { | |||
|
149 | scrollToBottom(); | |||
|
150 | } | |||
|
151 | } | |||
|
152 | ||||
|
153 | processNewPost(post); | |||
|
154 | updateMetadataPanel(lastUpdate) | |||
|
155 | } | |||
|
156 | ||||
|
157 | /** | |||
|
158 | * Initiate a blinking animation on a node to show it was updated. | |||
|
159 | */ | |||
32 | function blink(node) { |
|
160 | function blink(node) { | |
33 | var blinkCount = 2; |
|
161 | var blinkCount = 2; | |
34 |
|
162 | |||
35 | var nodeToAnimate = node; |
|
163 | var nodeToAnimate = node; | |
36 | for (var i = 0; i < blinkCount; i++) { |
|
164 | for (var i = 0; i < blinkCount; i++) { | |
37 | nodeToAnimate = nodeToAnimate.fadeTo('fast', 0.5).fadeTo('fast', 1.0); |
|
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 | function isPageBottom() { |
|
169 | function isPageBottom() { | |
126 | var scroll = $(window).scrollTop() / ($(document).height() |
|
170 | var scroll = $(window).scrollTop() / ($(document).height() | |
127 | - $(window).height()) |
|
171 | - $(window).height()); | |
128 |
|
172 | |||
129 | return scroll == 1 |
|
173 | return scroll == 1 | |
130 | } |
|
174 | } | |
131 |
|
175 | |||
132 | function initAutoupdate() { |
|
176 | function initAutoupdate() { | |
133 | loading = false; |
|
177 | return connectWebsocket(); | |
134 |
|
||||
135 | lastUpdateTime = $('.metapanel').attr('data-last-update'); |
|
|||
136 |
|
||||
137 | setInterval(updateThread, THREAD_UPDATE_DELAY); |
|
|||
138 | } |
|
178 | } | |
139 |
|
179 | |||
140 | function getReplyCount() { |
|
180 | function getReplyCount() { | |
141 | return $('.thread').children('.post').length |
|
181 | return $('.thread').children('.post').length | |
142 | } |
|
182 | } | |
143 |
|
183 | |||
144 | function getImageCount() { |
|
184 | function getImageCount() { | |
145 | return $('.thread').find('img').length |
|
185 | return $('.thread').find('img').length | |
146 | } |
|
186 | } | |
147 |
|
187 | |||
|
188 | /** | |||
|
189 | * Update post count, images count and last update time in the metadata | |||
|
190 | * panel. | |||
|
191 | */ | |||
148 | function updateMetadataPanel(lastUpdate) { |
|
192 | function updateMetadataPanel(lastUpdate) { | |
149 | var replyCountField = $('#reply-count'); |
|
193 | var replyCountField = $('#reply-count'); | |
150 | var imageCountField = $('#image-count'); |
|
194 | var imageCountField = $('#image-count'); | |
151 |
|
195 | |||
152 | replyCountField.text(getReplyCount()); |
|
196 | replyCountField.text(getReplyCount()); | |
153 | imageCountField.text(getImageCount()); |
|
197 | imageCountField.text(getImageCount()); | |
154 |
|
198 | |||
155 | if (lastUpdate !== '') { |
|
199 | if (lastUpdate !== '') { | |
156 | var lastUpdateField = $('#last-update'); |
|
200 | var lastUpdateField = $('#last-update'); | |
157 | lastUpdateField.text(lastUpdate); |
|
201 | lastUpdateField.text(lastUpdate); | |
158 | blink(lastUpdateField); |
|
202 | blink(lastUpdateField); | |
159 | } |
|
203 | } | |
160 |
|
204 | |||
161 | blink(replyCountField); |
|
205 | blink(replyCountField); | |
162 | blink(imageCountField); |
|
206 | blink(imageCountField); | |
163 | } |
|
207 | } | |
164 |
|
208 | |||
165 | /** |
|
209 | /** | |
166 | * Update bumplimit progress bar |
|
210 | * Update bumplimit progress bar | |
167 | */ |
|
211 | */ | |
168 | function updateBumplimitProgress(postDelta) { |
|
212 | function updateBumplimitProgress(postDelta) { | |
169 | var progressBar = $('#bumplimit_progress'); |
|
213 | var progressBar = $('#bumplimit_progress'); | |
170 | if (progressBar) { |
|
214 | if (progressBar) { | |
171 | var postsToLimitElement = $('#left_to_limit'); |
|
215 | var postsToLimitElement = $('#left_to_limit'); | |
172 |
|
216 | |||
173 | var oldPostsToLimit = parseInt(postsToLimitElement.text()); |
|
217 | var oldPostsToLimit = parseInt(postsToLimitElement.text()); | |
174 | var postCount = getReplyCount(); |
|
218 | var postCount = getReplyCount(); | |
175 | var bumplimit = postCount - postDelta + oldPostsToLimit; |
|
219 | var bumplimit = postCount - postDelta + oldPostsToLimit; | |
176 |
|
220 | |||
177 | var newPostsToLimit = bumplimit - postCount; |
|
221 | var newPostsToLimit = bumplimit - postCount; | |
178 | if (newPostsToLimit <= 0) { |
|
222 | if (newPostsToLimit <= 0) { | |
179 | $('.bar-bg').remove(); |
|
223 | $('.bar-bg').remove(); | |
180 | $('.thread').children('.post').addClass('dead_post'); |
|
224 | $('.thread').children('.post').addClass('dead_post'); | |
181 | } else { |
|
225 | } else { | |
182 | postsToLimitElement.text(newPostsToLimit); |
|
226 | postsToLimitElement.text(newPostsToLimit); | |
183 | progressBar.width((100 - postCount / bumplimit * 100.0) + '%'); |
|
227 | progressBar.width((100 - postCount / bumplimit * 100.0) + '%'); | |
184 | } |
|
228 | } | |
185 | } |
|
229 | } | |
186 | } |
|
230 | } | |
187 |
|
231 | |||
188 | var documentOriginalTitle = ''; |
|
|||
189 | /** |
|
232 | /** | |
190 | * Show 'new posts' text in the title if the document is not visible to a user |
|
233 | * Show 'new posts' text in the title if the document is not visible to a user | |
191 | */ |
|
234 | */ | |
192 | function showNewPostsTitle(newPostCount) { |
|
235 | function showNewPostsTitle(newPostCount) { | |
193 | if (document.hidden) { |
|
236 | if (document.hidden) { | |
194 | if (documentOriginalTitle === '') { |
|
237 | if (documentOriginalTitle === '') { | |
195 | documentOriginalTitle = document.title; |
|
238 | documentOriginalTitle = document.title; | |
196 | } |
|
239 | } | |
197 | unreadPosts = unreadPosts + newPostCount; |
|
240 | unreadPosts = unreadPosts + newPostCount; | |
198 | document.title = '[' + unreadPosts + '] ' + documentOriginalTitle; |
|
241 | document.title = '[' + unreadPosts + '] ' + documentOriginalTitle; | |
199 |
|
242 | |||
200 | document.addEventListener('visibilitychange', function() { |
|
243 | document.addEventListener('visibilitychange', function() { | |
201 | if (documentOriginalTitle !== '') { |
|
244 | if (documentOriginalTitle !== '') { | |
202 | document.title = documentOriginalTitle; |
|
245 | document.title = documentOriginalTitle; | |
203 | documentOriginalTitle = ''; |
|
246 | documentOriginalTitle = ''; | |
204 | unreadPosts = 0; |
|
247 | unreadPosts = 0; | |
205 | } |
|
248 | } | |
206 |
|
249 | |||
207 | document.removeEventListener('visibilitychange', null); |
|
250 | document.removeEventListener('visibilitychange', null); | |
208 | }); |
|
251 | }); | |
209 | } |
|
252 | } | |
210 | } |
|
253 | } | |
211 |
|
254 | |||
212 | /** |
|
255 | /** | |
213 | * Clear all entered values in the form fields |
|
256 | * Clear all entered values in the form fields | |
214 | */ |
|
257 | */ | |
215 | function resetForm(form) { |
|
258 | function resetForm(form) { | |
216 | form.find('input:text, input:password, input:file, select, textarea').val(''); |
|
259 | form.find('input:text, input:password, input:file, select, textarea').val(''); | |
217 | form.find('input:radio, input:checkbox') |
|
260 | form.find('input:radio, input:checkbox') | |
218 | .removeAttr('checked').removeAttr('selected'); |
|
261 | .removeAttr('checked').removeAttr('selected'); | |
219 | $('.file_wrap').find('.file-thumb').remove(); |
|
262 | $('.file_wrap').find('.file-thumb').remove(); | |
220 | } |
|
263 | } | |
221 |
|
264 | |||
222 | /** |
|
265 | /** | |
223 | * When the form is posted, this method will be run as a callback |
|
266 | * When the form is posted, this method will be run as a callback | |
224 | */ |
|
267 | */ | |
225 | function updateOnPost(response, statusText, xhr, form) { |
|
268 | function updateOnPost(response, statusText, xhr, form) { | |
226 | var json = $.parseJSON(response); |
|
269 | var json = $.parseJSON(response); | |
227 | var status = json.status; |
|
270 | var status = json.status; | |
228 |
|
271 | |||
229 | showAsErrors(form, ''); |
|
272 | showAsErrors(form, ''); | |
230 |
|
273 | |||
231 | if (status === 'ok') { |
|
274 | if (status === 'ok') { | |
232 | resetForm(form); |
|
275 | resetForm(form); | |
233 |
|
|
276 | getThreadDiff(); | |
234 | } else { |
|
277 | } else { | |
235 | var errors = json.errors; |
|
278 | var errors = json.errors; | |
236 | for (var i = 0; i < errors.length; i++) { |
|
279 | for (var i = 0; i < errors.length; i++) { | |
237 | var fieldErrors = errors[i]; |
|
280 | var fieldErrors = errors[i]; | |
238 |
|
281 | |||
239 | var error = fieldErrors.errors; |
|
282 | var error = fieldErrors.errors; | |
240 |
|
283 | |||
241 | showAsErrors(form, error); |
|
284 | showAsErrors(form, error); | |
242 | } |
|
285 | } | |
243 | } |
|
286 | } | |
|
287 | ||||
|
288 | scrollToBottom(); | |||
244 | } |
|
289 | } | |
245 |
|
290 | |||
246 | /** |
|
291 | /** | |
247 | * Show text in the errors row of the form. |
|
292 | * Show text in the errors row of the form. | |
248 | * @param form |
|
293 | * @param form | |
249 | * @param text |
|
294 | * @param text | |
250 | */ |
|
295 | */ | |
251 | function showAsErrors(form, text) { |
|
296 | function showAsErrors(form, text) { | |
252 | form.children('.form-errors').remove(); |
|
297 | form.children('.form-errors').remove(); | |
253 |
|
298 | |||
254 | if (text.length > 0) { |
|
299 | if (text.length > 0) { | |
255 | var errorList = $('<div class="form-errors">' + text |
|
300 | var errorList = $('<div class="form-errors">' + text | |
256 | + '<div>'); |
|
301 | + '<div>'); | |
257 | errorList.appendTo(form); |
|
302 | errorList.appendTo(form); | |
258 | } |
|
303 | } | |
259 | } |
|
304 | } | |
260 |
|
305 | |||
261 | /** |
|
306 | /** | |
262 | * Run js methods that are usually run on the document, on the new post |
|
307 | * Run js methods that are usually run on the document, on the new post | |
263 | */ |
|
308 | */ | |
264 | function processNewPost(post) { |
|
309 | function processNewPost(post) { | |
265 | addRefLinkPreview(post[0]); |
|
310 | addRefLinkPreview(post[0]); | |
266 | highlightCode(post); |
|
311 | highlightCode(post); | |
|
312 | blink(post); | |||
267 | } |
|
313 | } | |
268 |
|
314 | |||
269 | $(document).ready(function(){ |
|
315 | $(document).ready(function(){ | |
270 |
initAutoupdate() |
|
316 | if (initAutoupdate()) { | |
271 |
|
||||
272 | // Post form data over AJAX |
|
317 | // Post form data over AJAX | |
273 | var threadId = $('div.thread').children('.post').first().attr('id'); |
|
318 | var threadId = $('div.thread').children('.post').first().attr('id'); | |
274 |
|
319 | |||
275 | var form = $('#form'); |
|
320 | var form = $('#form'); | |
276 |
|
321 | |||
277 | var options = { |
|
322 | var options = { | |
278 | beforeSubmit: function(arr, $form, options) { |
|
323 | beforeSubmit: function(arr, $form, options) { | |
279 | showAsErrors($('form'), gettext('Sending message...')); |
|
324 | showAsErrors($('form'), gettext('Sending message...')); | |
280 | }, |
|
325 | }, | |
281 | success: updateOnPost, |
|
326 | success: updateOnPost, | |
282 | url: '/api/add_post/' + threadId + '/' |
|
327 | url: '/api/add_post/' + threadId + '/' | |
283 | }; |
|
328 | }; | |
284 |
|
329 | |||
285 | form.ajaxForm(options); |
|
330 | form.ajaxForm(options); | |
286 |
|
331 | |||
287 | resetForm(form); |
|
332 | resetForm(form); | |
|
333 | } | |||
288 | }); |
|
334 | }); |
@@ -1,58 +1,60 b'' | |||||
1 | {% load staticfiles %} |
|
1 | {% load staticfiles %} | |
2 | {% load i18n %} |
|
2 | {% load i18n %} | |
3 | {% load l10n %} |
|
3 | {% load l10n %} | |
4 | {% load static from staticfiles %} |
|
4 | {% load static from staticfiles %} | |
5 |
|
5 | |||
6 | <!DOCTYPE html> |
|
6 | <!DOCTYPE html> | |
7 | <html> |
|
7 | <html> | |
8 | <head> |
|
8 | <head> | |
9 | <link rel="stylesheet" type="text/css" href="{% static 'css/base.css' %}" media="all"/> |
|
9 | <link rel="stylesheet" type="text/css" href="{% static 'css/base.css' %}" media="all"/> | |
10 | <link rel="stylesheet" type="text/css" href="{% static 'css/3party/highlight.css' %}" media="all"/> |
|
10 | <link rel="stylesheet" type="text/css" href="{% static 'css/3party/highlight.css' %}" media="all"/> | |
11 | <link rel="stylesheet" type="text/css" href="{% static theme_css %}" media="all"/> |
|
11 | <link rel="stylesheet" type="text/css" href="{% static theme_css %}" media="all"/> | |
|
12 | ||||
12 | <link rel="alternate" type="application/rss+xml" href="rss/" title="{% trans 'Feed' %}"/> |
|
13 | <link rel="alternate" type="application/rss+xml" href="rss/" title="{% trans 'Feed' %}"/> | |
13 |
|
14 | |||
14 | <link rel="icon" type="image/png" |
|
15 | <link rel="icon" type="image/png" | |
15 | href="{% static 'favicon.png' %}"> |
|
16 | href="{% static 'favicon.png' %}"> | |
16 |
|
17 | |||
17 | <meta name="viewport" content="width=device-width, initial-scale=1"/> |
|
18 | <meta name="viewport" content="width=device-width, initial-scale=1"/> | |
18 | <meta charset="utf-8"/> |
|
19 | <meta charset="utf-8"/> | |
19 |
|
20 | |||
20 | {% block head %}{% endblock %} |
|
21 | {% block head %}{% endblock %} | |
21 | </head> |
|
22 | </head> | |
22 | <body> |
|
23 | <body> | |
23 | <script src="{% static 'js/jquery-2.0.1.min.js' %}"></script> |
|
24 | <script src="{% static 'js/jquery-2.0.1.min.js' %}"></script> | |
24 | <script src="{% static 'js/jquery-ui-1.10.3.custom.min.js' %}"></script> |
|
25 | <script src="{% static 'js/jquery-ui-1.10.3.custom.min.js' %}"></script> | |
25 | <script src="{% static 'js/jquery.mousewheel.js' %}"></script> |
|
26 | <script src="{% static 'js/jquery.mousewheel.js' %}"></script> | |
26 | <script src="{% url 'js_info_dict' %}"></script> |
|
27 | <script src="{% url 'js_info_dict' %}"></script> | |
27 |
|
28 | |||
28 | <div class="navigation_panel header"> |
|
29 | <div class="navigation_panel header"> | |
29 | <a class="link" href="{% url 'index' %}">{% trans "All threads" %}</a> |
|
30 | <a class="link" href="{% url 'index' %}">{% trans "All threads" %}</a> | |
30 | {% for tag in tags %} |
|
31 | {% for tag in tags %} | |
31 | <a class="tag" href="{% url 'tag' tag_name=tag.name %}" |
|
32 | {% autoescape off %} | |
32 | >#{{ tag.name }}</a>, |
|
33 | {{ tag.get_view }}{% if not forloop.last %},{% endif %} | |
|
34 | {% endautoescape %} | |||
33 | {% endfor %} |
|
35 | {% endfor %} | |
34 | <a href="{% url 'tags' %}" title="{% trans 'Tag management' %}" |
|
36 | <a href="{% url 'tags' %}" title="{% trans 'Tag management' %}" | |
35 | >[...]</a>, |
|
37 | >[...]</a>, | |
36 | <a href="{% url 'search' %}" title="{% trans 'Search' %}">[S]</a> |
|
38 | <a href="{% url 'search' %}" title="{% trans 'Search' %}">[S]</a> | |
37 | <a class="link" href="{% url 'settings' %}">{% trans 'Settings' %}</a> |
|
39 | <a class="link" href="{% url 'settings' %}">{% trans 'Settings' %}</a> | |
38 | </div> |
|
40 | </div> | |
39 |
|
41 | |||
40 | {% block content %}{% endblock %} |
|
42 | {% block content %}{% endblock %} | |
41 |
|
43 | |||
|
44 | <script src="{% static 'js/3party/highlight.min.js' %}"></script> | |||
42 | <script src="{% static 'js/popup.js' %}"></script> |
|
45 | <script src="{% static 'js/popup.js' %}"></script> | |
43 | <script src="{% static 'js/image.js' %}"></script> |
|
46 | <script src="{% static 'js/image.js' %}"></script> | |
44 | <script src="{% static 'js/3party/highlight.min.js' %}"></script> |
|
|||
45 | <script src="{% static 'js/refpopup.js' %}"></script> |
|
47 | <script src="{% static 'js/refpopup.js' %}"></script> | |
46 | <script src="{% static 'js/main.js' %}"></script> |
|
48 | <script src="{% static 'js/main.js' %}"></script> | |
47 |
|
49 | |||
48 | <div class="navigation_panel footer"> |
|
50 | <div class="navigation_panel footer"> | |
49 | {% block metapanel %}{% endblock %} |
|
51 | {% block metapanel %}{% endblock %} | |
50 | [<a href="{% url 'admin:index' %}">{% trans 'Admin' %}</a>] |
|
52 | [<a href="{% url 'admin:index' %}">{% trans 'Admin' %}</a>] | |
51 | {% with ppd=posts_per_day|floatformat:2 %} |
|
53 | {% with ppd=posts_per_day|floatformat:2 %} | |
52 | {% blocktrans %}Speed: {{ ppd }} posts per day{% endblocktrans %} |
|
54 | {% blocktrans %}Speed: {{ ppd }} posts per day{% endblocktrans %} | |
53 | {% endwith %} |
|
55 | {% endwith %} | |
54 | <a class="link" href="#top">{% trans 'Up' %}</a> |
|
56 | <a class="link" href="#top">{% trans 'Up' %}</a> | |
55 | </div> |
|
57 | </div> | |
56 |
|
58 | |||
57 | </body> |
|
59 | </body> | |
58 | </html> |
|
60 | </html> |
@@ -1,104 +1,102 b'' | |||||
1 | {% load i18n %} |
|
1 | {% load i18n %} | |
2 | {% load board %} |
|
2 | {% load board %} | |
3 | {% load cache %} |
|
3 | {% load cache %} | |
4 |
|
4 | |||
5 | {% get_current_language as LANGUAGE_CODE %} |
|
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 |
|
|
7 | {% if thread.archived %} | |
10 |
|
|
8 | <div class="post archive_post" id="{{ post.id }}"> | |
11 |
|
|
9 | {% elif bumpable %} | |
12 |
|
|
10 | <div class="post" id="{{ post.id }}"> | |
13 |
|
|
11 | {% else %} | |
14 |
|
|
12 | <div class="post dead_post" id="{{ post.id }}"> | |
15 |
|
|
13 | {% endif %} | |
16 |
|
14 | |||
17 |
|
|
15 | <div class="post-info"> | |
18 |
|
|
16 | <a class="post_id" href="{% post_object_url post thread=thread %}" | |
19 |
|
|
17 | {% if not truncated and not thread.archived %} | |
20 |
|
|
18 | onclick="javascript:addQuickReply('{{ post.id }}'); return false;" | |
21 |
|
|
19 | title="{% trans 'Quote' %}" {% endif %}>({{ post.id }})</a> | |
22 | {% endif %} |
|
|||
23 | >({{ post.id }}) </a> |
|
|||
24 |
|
|
20 | <span class="title">{{ post.title }}</span> | |
25 |
|
|
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 %} | |||
26 |
|
|
27 | {% if thread.archived %} | |
|
28 | {% if is_opening %} | |||
27 |
|
|
29 | — {{ thread.bump_time }} | |
28 |
|
|
30 | {% endif %} | |
|
31 | {% endif %} | |||
29 |
|
|
32 | {% if is_opening and need_open_link %} | |
30 |
|
|
33 | {% if thread.archived %} | |
31 |
|
|
34 | [<a class="link" href="{% url 'thread' post.id %}">{% trans "Open" %}</a>] | |
32 |
|
|
35 | {% else %} | |
33 |
|
|
36 | [<a class="link" href="{% url 'thread' post.id %}#form">{% trans "Reply" %}</a>] | |
34 |
|
|
37 | {% endif %} | |
35 |
|
|
38 | {% endif %} | |
36 |
|
39 | |||
37 |
|
|
40 | {% if post.global_id %} | |
38 |
|
|
41 | <a class="global-id" href=" | |
39 |
|
|
42 | {% url 'post_sync_data' post.id %}"> [RAW] </a> | |
40 |
|
|
43 | {% endif %} | |
41 |
|
44 | |||
42 |
|
|
45 | {% if moderator %} | |
43 |
|
|
46 | <span class="moderator_info"> | |
44 |
|
|
47 | [<a href="{% url 'admin:boards_post_change' post.id %}">{% trans 'Edit' %}</a>] | |
45 | >{% trans 'Edit' %}</a>] |
|
48 | {% if is_opening %} | |
46 | [<a href="{% url 'delete' post_id=post.id %}" |
|
49 | [<a href="{% url 'admin:boards_thread_change' thread.id %}">{% trans 'Edit thread' %}</a>] | |
47 | >{% trans 'Delete' %}</a>] |
|
50 | {% endif %} | |
48 | ({{ post.poster_ip }}) |
|
|||
49 | [<a href="{% url 'ban' post_id=post.id %}?next={{ request.path }}" |
|
|||
50 | >{% trans 'Ban IP' %}</a>] |
|
|||
51 |
|
|
51 | </span> | |
52 |
|
|
52 | {% endif %} | |
53 |
|
|
53 | </div> | |
|
54 | {% comment %} | |||
|
55 | Post images. Currently only 1 image can be posted and shown, but post model | |||
|
56 | supports multiple. | |||
|
57 | {% endcomment %} | |||
54 |
|
|
58 | {% if post.images.exists %} | |
55 |
|
|
59 | {% with post.images.all.0 as image %} | |
56 | <div class="image"> |
|
60 | {% autoescape off %} | |
57 | <a |
|
61 | {{ image.get_view }} | |
58 | class="thumb" |
|
62 | {% endautoescape %} | |
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 |
|
|
63 | {% endwith %} | |
69 |
|
|
64 | {% endif %} | |
|
65 | {% comment %} | |||
|
66 | Post message (text) | |||
|
67 | {% endcomment %} | |||
70 |
|
|
68 | <div class="message"> | |
71 |
|
|
69 | {% autoescape off %} | |
72 |
|
|
70 | {% if truncated %} | |
73 |
|
|
71 | {{ post.get_text|truncatewords_html:50 }} | |
74 |
|
|
72 | {% else %} | |
75 |
|
|
73 | {{ post.get_text }} | |
76 |
|
|
74 | {% endif %} | |
77 |
|
|
75 | {% endautoescape %} | |
78 |
|
|
76 | {% if post.is_referenced %} | |
79 |
|
|
77 | <div class="refmap"> | |
80 |
|
|
78 | {% autoescape off %} | |
81 |
|
|
79 | {% trans "Replies" %}: {{ post.refmap }} | |
82 |
|
|
80 | {% endautoescape %} | |
83 |
|
|
81 | </div> | |
84 |
|
|
82 | {% endif %} | |
85 |
|
|
83 | </div> | |
86 | {% endcache %} |
|
84 | {% comment %} | |
|
85 | Thread metadata: counters, tags etc | |||
|
86 | {% endcomment %} | |||
87 |
|
|
87 | {% if is_opening %} | |
88 | {% cache 600 post_thread thread.id thread.last_edit_time LANGUAGE_CODE need_open_link %} |
|
|||
89 |
|
|
88 | <div class="metadata"> | |
90 |
|
|
89 | {% if is_opening and need_open_link %} | |
91 |
|
|
90 | {{ thread.get_reply_count }} {% trans 'messages' %}, | |
92 |
|
|
91 | {{ thread.get_images_count }} {% trans 'images' %}. | |
93 |
|
|
92 | {% endif %} | |
94 |
|
|
93 | <span class="tags"> | |
95 |
|
|
94 | {% for tag in thread.get_tags %} | |
96 | <a class="tag" href="{% url 'tag' tag.name %}"> |
|
95 | {% autoescape off %} | |
97 |
|
|
96 | {{ tag.get_view }}{% if not forloop.last %},{% endif %} | |
|
97 | {% endautoescape %} | |||
98 |
|
|
98 | {% endfor %} | |
99 |
|
|
99 | </span> | |
100 |
|
|
100 | </div> | |
101 | {% endcache %} |
|
|||
102 |
|
|
101 | {% endif %} | |
103 |
|
|
102 | </div> | |
104 | {% endspaceless %} |
|
@@ -1,197 +1,202 b'' | |||||
1 | {% extends "boards/base.html" %} |
|
1 | {% extends "boards/base.html" %} | |
2 |
|
2 | |||
3 | {% load i18n %} |
|
3 | {% load i18n %} | |
4 | {% load cache %} |
|
4 | {% load cache %} | |
5 | {% load board %} |
|
5 | {% load board %} | |
6 | {% load static %} |
|
6 | {% load static %} | |
7 |
|
7 | |||
8 | {% block head %} |
|
8 | {% block head %} | |
9 | {% if tag %} |
|
9 | {% if tag %} | |
10 | <title>{{ tag.name }} - {{ site_name }}</title> |
|
10 | <title>{{ tag.name }} - {{ site_name }}</title> | |
11 | {% else %} |
|
11 | {% else %} | |
12 | <title>{{ site_name }}</title> |
|
12 | <title>{{ site_name }}</title> | |
13 | {% endif %} |
|
13 | {% endif %} | |
14 |
|
14 | |||
15 | {% if current_page.has_previous %} |
|
15 | {% if current_page.has_previous %} | |
16 | <link rel="prev" href=" |
|
16 | <link rel="prev" href=" | |
17 | {% if tag %} |
|
17 | {% if tag %} | |
18 | {% url "tag" tag_name=tag page=current_page.previous_page_number %} |
|
18 | {% url "tag" tag_name=tag.name page=current_page.previous_page_number %} | |
19 | {% elif archived %} |
|
19 | {% elif archived %} | |
20 | {% url "archive" page=current_page.previous_page_number %} |
|
20 | {% url "archive" page=current_page.previous_page_number %} | |
21 | {% else %} |
|
21 | {% else %} | |
22 | {% url "index" page=current_page.previous_page_number %} |
|
22 | {% url "index" page=current_page.previous_page_number %} | |
23 | {% endif %} |
|
23 | {% endif %} | |
24 | " /> |
|
24 | " /> | |
25 | {% endif %} |
|
25 | {% endif %} | |
26 | {% if current_page.has_next %} |
|
26 | {% if current_page.has_next %} | |
27 | <link rel="next" href=" |
|
27 | <link rel="next" href=" | |
28 | {% if tag %} |
|
28 | {% if tag %} | |
29 | {% url "tag" tag_name=tag page=current_page.next_page_number %} |
|
29 | {% url "tag" tag_name=tag.name page=current_page.next_page_number %} | |
30 | {% elif archived %} |
|
30 | {% elif archived %} | |
31 | {% url "archive" page=current_page.next_page_number %} |
|
31 | {% url "archive" page=current_page.next_page_number %} | |
32 | {% else %} |
|
32 | {% else %} | |
33 | {% url "index" page=current_page.next_page_number %} |
|
33 | {% url "index" page=current_page.next_page_number %} | |
34 | {% endif %} |
|
34 | {% endif %} | |
35 | " /> |
|
35 | " /> | |
36 | {% endif %} |
|
36 | {% endif %} | |
37 |
|
37 | |||
38 | {% endblock %} |
|
38 | {% endblock %} | |
39 |
|
39 | |||
40 | {% block content %} |
|
40 | {% block content %} | |
41 |
|
41 | |||
42 | {% get_current_language as LANGUAGE_CODE %} |
|
42 | {% get_current_language as LANGUAGE_CODE %} | |
43 |
|
43 | |||
44 | {% if tag %} |
|
44 | {% if tag %} | |
45 | <div class="tag_info"> |
|
45 | <div class="tag_info"> | |
46 | <h2> |
|
46 | <h2> | |
47 | {% if tag in fav_tags %} |
|
47 | {% if tag in fav_tags %} | |
48 | <a href="{% url 'tag' tag.name %}?method=unsubscribe&next={{ request.path }}" |
|
48 | <a href="{% url 'tag' tag.name %}?method=unsubscribe&next={{ request.path }}" | |
49 | class="fav">★</a> |
|
49 | class="fav" rel="nofollow">★</a> | |
50 | {% else %} |
|
50 | {% else %} | |
51 | <a href="{% url 'tag' tag.name %}?method=subscribe&next={{ request.path }}" |
|
51 | <a href="{% url 'tag' tag.name %}?method=subscribe&next={{ request.path }}" | |
52 | class="not_fav">★</a> |
|
52 | class="not_fav" rel="nofollow">★</a> | |
53 | {% endif %} |
|
53 | {% endif %} | |
54 | {% if tag in hidden_tags %} |
|
54 | {% if tag in hidden_tags %} | |
55 | <a href="{% url 'tag' tag.name %}?method=unhide&next={{ request.path }}" |
|
55 | <a href="{% url 'tag' tag.name %}?method=unhide&next={{ request.path }}" | |
56 | title="{% trans 'Show tag' %}" |
|
56 | title="{% trans 'Show tag' %}" | |
57 | class="fav">H</a> |
|
57 | class="fav" rel="nofollow">H</a> | |
58 | {% else %} |
|
58 | {% else %} | |
59 | <a href="{% url 'tag' tag.name %}?method=hide&next={{ request.path }}" |
|
59 | <a href="{% url 'tag' tag.name %}?method=hide&next={{ request.path }}" | |
60 | title="{% trans 'Hide tag' %}" |
|
60 | title="{% trans 'Hide tag' %}" | |
61 | class="not_fav">H</a> |
|
61 | class="not_fav" rel="nofollow">H</a> | |
62 | {% endif %} |
|
62 | {% endif %} | |
63 |
|
|
63 | {% autoescape off %} | |
|
64 | {{ tag.get_view }} | |||
|
65 | {% endautoescape %} | |||
|
66 | {% if moderator %} | |||
|
67 | [<a href="{% url 'admin:boards_tag_change' tag.id %}"$>{% trans 'Edit tag' %}</a>] | |||
|
68 | {% endif %} | |||
64 | </h2> |
|
69 | </h2> | |
65 | </div> |
|
70 | </div> | |
66 | {% endif %} |
|
71 | {% endif %} | |
67 |
|
72 | |||
68 | {% if threads %} |
|
73 | {% if threads %} | |
69 | {% if current_page.has_previous %} |
|
74 | {% if current_page.has_previous %} | |
70 | <div class="page_link"> |
|
75 | <div class="page_link"> | |
71 | <a href=" |
|
76 | <a href=" | |
72 | {% if tag %} |
|
77 | {% if tag %} | |
73 | {% url "tag" tag_name=tag page=current_page.previous_page_number %} |
|
78 | {% url "tag" tag_name=tag.name page=current_page.previous_page_number %} | |
74 | {% elif archived %} |
|
79 | {% elif archived %} | |
75 | {% url "archive" page=current_page.previous_page_number %} |
|
80 | {% url "archive" page=current_page.previous_page_number %} | |
76 | {% else %} |
|
81 | {% else %} | |
77 | {% url "index" page=current_page.previous_page_number %} |
|
82 | {% url "index" page=current_page.previous_page_number %} | |
78 | {% endif %} |
|
83 | {% endif %} | |
79 | ">{% trans "Previous page" %}</a> |
|
84 | ">{% trans "Previous page" %}</a> | |
80 | </div> |
|
85 | </div> | |
81 | {% endif %} |
|
86 | {% endif %} | |
82 |
|
87 | |||
83 | {% for thread in threads %} |
|
88 | {% for thread in threads %} | |
84 | {% cache 600 thread_short thread.id thread.last_edit_time moderator LANGUAGE_CODE %} |
|
89 | {% cache 600 thread_short thread.id thread.last_edit_time moderator LANGUAGE_CODE %} | |
85 | <div class="thread"> |
|
90 | <div class="thread"> | |
86 | {% with can_bump=thread.can_bump %} |
|
91 | {% with can_bump=thread.can_bump %} | |
87 | {% post_view thread.get_opening_post moderator is_opening=True thread=thread can_bump=can_bump truncated=True need_open_link=True %} |
|
92 | {% post_view thread.get_opening_post moderator is_opening=True thread=thread can_bump=can_bump truncated=True need_open_link=True %} | |
88 | {% if not thread.archived %} |
|
93 | {% if not thread.archived %} | |
89 | {% with last_replies=thread.get_last_replies %} |
|
94 | {% with last_replies=thread.get_last_replies %} | |
90 | {% if last_replies %} |
|
95 | {% if last_replies %} | |
91 | {% if thread.get_skipped_replies_count %} |
|
96 | {% if thread.get_skipped_replies_count %} | |
92 | <div class="skipped_replies"> |
|
97 | <div class="skipped_replies"> | |
93 | <a href="{% url 'thread' thread.get_opening_post.id %}"> |
|
98 | <a href="{% url 'thread' thread.get_opening_post.id %}"> | |
94 | {% blocktrans with count=thread.get_skipped_replies_count %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %} |
|
99 | {% blocktrans with count=thread.get_skipped_replies_count %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %} | |
95 | </a> |
|
100 | </a> | |
96 | </div> |
|
101 | </div> | |
97 | {% endif %} |
|
102 | {% endif %} | |
98 | <div class="last-replies"> |
|
103 | <div class="last-replies"> | |
99 | {% for post in last_replies %} |
|
104 | {% for post in last_replies %} | |
100 | {% post_view post moderator=moderator is_opening=False thread=thread can_bump=can_bump truncated=True %} |
|
105 | {% post_view post moderator=moderator is_opening=False thread=thread can_bump=can_bump truncated=True %} | |
101 | {% endfor %} |
|
106 | {% endfor %} | |
102 | </div> |
|
107 | </div> | |
103 | {% endif %} |
|
108 | {% endif %} | |
104 | {% endwith %} |
|
109 | {% endwith %} | |
105 | {% endif %} |
|
110 | {% endif %} | |
106 | {% endwith %} |
|
111 | {% endwith %} | |
107 | </div> |
|
112 | </div> | |
108 | {% endcache %} |
|
113 | {% endcache %} | |
109 | {% endfor %} |
|
114 | {% endfor %} | |
110 |
|
115 | |||
111 | {% if current_page.has_next %} |
|
116 | {% if current_page.has_next %} | |
112 | <div class="page_link"> |
|
117 | <div class="page_link"> | |
113 | <a href=" |
|
118 | <a href=" | |
114 | {% if tag %} |
|
119 | {% if tag %} | |
115 | {% url "tag" tag_name=tag page=current_page.next_page_number %} |
|
120 | {% url "tag" tag_name=tag.name page=current_page.next_page_number %} | |
116 | {% elif archived %} |
|
121 | {% elif archived %} | |
117 | {% url "archive" page=current_page.next_page_number %} |
|
122 | {% url "archive" page=current_page.next_page_number %} | |
118 | {% else %} |
|
123 | {% else %} | |
119 | {% url "index" page=current_page.next_page_number %} |
|
124 | {% url "index" page=current_page.next_page_number %} | |
120 | {% endif %} |
|
125 | {% endif %} | |
121 | ">{% trans "Next page" %}</a> |
|
126 | ">{% trans "Next page" %}</a> | |
122 | </div> |
|
127 | </div> | |
123 | {% endif %} |
|
128 | {% endif %} | |
124 | {% else %} |
|
129 | {% else %} | |
125 | <div class="post"> |
|
130 | <div class="post"> | |
126 | {% trans 'No threads exist. Create the first one!' %}</div> |
|
131 | {% trans 'No threads exist. Create the first one!' %}</div> | |
127 | {% endif %} |
|
132 | {% endif %} | |
128 |
|
133 | |||
129 | <div class="post-form-w"> |
|
134 | <div class="post-form-w"> | |
130 | <script src="{% static 'js/panel.js' %}"></script> |
|
135 | <script src="{% static 'js/panel.js' %}"></script> | |
131 | <div class="post-form"> |
|
136 | <div class="post-form"> | |
132 | <div class="form-title">{% trans "Create new thread" %}</div> |
|
137 | <div class="form-title">{% trans "Create new thread" %}</div> | |
133 | <div class="swappable-form-full"> |
|
138 | <div class="swappable-form-full"> | |
134 | <form enctype="multipart/form-data" method="post">{% csrf_token %} |
|
139 | <form enctype="multipart/form-data" method="post">{% csrf_token %} | |
135 | {{ form.as_div }} |
|
140 | {{ form.as_div }} | |
136 | <div class="form-submit"> |
|
141 | <div class="form-submit"> | |
137 | <input type="submit" value="{% trans "Post" %}"/> |
|
142 | <input type="submit" value="{% trans "Post" %}"/> | |
138 | </div> |
|
143 | </div> | |
139 | </form> |
|
144 | </form> | |
140 | </div> |
|
145 | </div> | |
141 | <div> |
|
146 | <div> | |
142 | {% trans 'Tags must be delimited by spaces. Text or image is required.' %} |
|
147 | {% trans 'Tags must be delimited by spaces. Text or image is required.' %} | |
143 | </div> |
|
148 | </div> | |
144 | <div><a href="{% url "staticpage" name="help" %}"> |
|
149 | <div><a href="{% url "staticpage" name="help" %}"> | |
145 | {% trans 'Text syntax' %}</a></div> |
|
150 | {% trans 'Text syntax' %}</a></div> | |
146 | </div> |
|
151 | </div> | |
147 | </div> |
|
152 | </div> | |
148 |
|
153 | |||
149 | <script src="{% static 'js/form.js' %}"></script> |
|
154 | <script src="{% static 'js/form.js' %}"></script> | |
150 |
|
155 | |||
151 | {% endblock %} |
|
156 | {% endblock %} | |
152 |
|
157 | |||
153 | {% block metapanel %} |
|
158 | {% block metapanel %} | |
154 |
|
159 | |||
155 | <span class="metapanel"> |
|
160 | <span class="metapanel"> | |
156 | <b><a href="{% url "authors" %}">{{ site_name }}</a> {{ version }}</b> |
|
161 | <b><a href="{% url "authors" %}">{{ site_name }}</a> {{ version }}</b> | |
157 | {% trans "Pages:" %} |
|
162 | {% trans "Pages:" %} | |
158 | <a href=" |
|
163 | <a href=" | |
159 | {% if tag %} |
|
164 | {% if tag %} | |
160 | {% url "tag" tag_name=tag page=paginator.page_range|first %} |
|
165 | {% url "tag" tag_name=tag.name page=paginator.page_range|first %} | |
161 | {% elif archived %} |
|
166 | {% elif archived %} | |
162 | {% url "archive" page=paginator.page_range|first %} |
|
167 | {% url "archive" page=paginator.page_range|first %} | |
163 | {% else %} |
|
168 | {% else %} | |
164 | {% url "index" page=paginator.page_range|first %} |
|
169 | {% url "index" page=paginator.page_range|first %} | |
165 | {% endif %} |
|
170 | {% endif %} | |
166 | "><<</a> |
|
171 | "><<</a> | |
167 | [ |
|
172 | [ | |
168 | {% for page in paginator.center_range %} |
|
173 | {% for page in paginator.center_range %} | |
169 | <a |
|
174 | <a | |
170 | {% ifequal page current_page.number %} |
|
175 | {% ifequal page current_page.number %} | |
171 | class="current_page" |
|
176 | class="current_page" | |
172 | {% endifequal %} |
|
177 | {% endifequal %} | |
173 | href=" |
|
178 | href=" | |
174 | {% if tag %} |
|
179 | {% if tag %} | |
175 | {% url "tag" tag_name=tag page=page %} |
|
180 | {% url "tag" tag_name=tag.name page=page %} | |
176 | {% elif archived %} |
|
181 | {% elif archived %} | |
177 | {% url "archive" page=page %} |
|
182 | {% url "archive" page=page %} | |
178 | {% else %} |
|
183 | {% else %} | |
179 | {% url "index" page=page %} |
|
184 | {% url "index" page=page %} | |
180 | {% endif %} |
|
185 | {% endif %} | |
181 | ">{{ page }}</a> |
|
186 | ">{{ page }}</a> | |
182 | {% if not forloop.last %},{% endif %} |
|
187 | {% if not forloop.last %},{% endif %} | |
183 | {% endfor %} |
|
188 | {% endfor %} | |
184 | ] |
|
189 | ] | |
185 | <a href=" |
|
190 | <a href=" | |
186 | {% if tag %} |
|
191 | {% if tag %} | |
187 | {% url "tag" tag_name=tag page=paginator.page_range|last %} |
|
192 | {% url "tag" tag_name=tag.name page=paginator.page_range|last %} | |
188 | {% elif archived %} |
|
193 | {% elif archived %} | |
189 | {% url "archive" page=paginator.page_range|last %} |
|
194 | {% url "archive" page=paginator.page_range|last %} | |
190 | {% else %} |
|
195 | {% else %} | |
191 | {% url "index" page=paginator.page_range|last %} |
|
196 | {% url "index" page=paginator.page_range|last %} | |
192 | {% endif %} |
|
197 | {% endif %} | |
193 | ">>></a> |
|
198 | ">>></a> | |
194 | [<a href="rss/">RSS</a>] |
|
199 | [<a href="rss/">RSS</a>] | |
195 | </span> |
|
200 | </span> | |
196 |
|
201 | |||
197 | {% endblock %} |
|
202 | {% endblock %} |
@@ -1,15 +1,15 b'' | |||||
1 | {% load i18n %} |
|
1 | {% load i18n %} | |
2 |
|
2 | |||
3 | {% if obj.images.exists %} |
|
3 | {% if obj.images.exists %} | |
4 | <img src="{{ obj.get_first_image.image.url_200x150 }}" |
|
4 | <img src="{{ obj.get_first_image.image.url_200x150 }}" | |
5 | alt="{% trans 'Post image' %}" /> |
|
5 | alt="{% trans 'Post image' %}" /> | |
6 | {% endif %} |
|
6 | {% endif %} | |
7 |
{{ obj.text |
|
7 | {{ obj.get_text|safe }} | |
8 | {% if obj.tags.all %} |
|
8 | {% if obj.tags.all %} | |
9 | <p> |
|
9 | <p> | |
10 | {% trans 'Tags' %}: |
|
10 | {% trans 'Tags' %}: | |
11 | {% for tag in obj.tags.all %} |
|
11 | {% for tag in obj.tags.all %} | |
12 | {{ tag.name }} |
|
12 | {{ tag.name }} | |
13 | {% endfor %} |
|
13 | {% endfor %} | |
14 | </p> |
|
14 | </p> | |
15 | {% endif %} No newline at end of file |
|
15 | {% endif %} |
@@ -1,3 +1,5 b'' | |||||
1 | <div class="post"> |
|
1 | <div class="post"> | |
2 | <a class="tag" href="{% url 'tag' tag_name=tag.name %}">#{{ tag.name }}</a> |
|
2 | {% autoescape off %} | |
3 | </div> No newline at end of file |
|
3 | {{ tag.get_view }} | |
|
4 | {% endautoescape %} | |||
|
5 | </div> |
@@ -1,27 +1,28 b'' | |||||
1 | {% extends "boards/base.html" %} |
|
1 | {% extends "boards/base.html" %} | |
2 |
|
2 | |||
3 | {% load i18n %} |
|
3 | {% load i18n %} | |
4 | {% load cache %} |
|
4 | {% load cache %} | |
5 |
|
5 | |||
6 | {% block head %} |
|
6 | {% block head %} | |
7 | <title>Neboard - {% trans "Tags" %}</title> |
|
7 | <title>Neboard - {% trans "Tags" %}</title> | |
8 | {% endblock %} |
|
8 | {% endblock %} | |
9 |
|
9 | |||
10 | {% block content %} |
|
10 | {% block content %} | |
11 |
|
11 | |||
12 | {% cache 600 all_tags_list %} |
|
12 | {% cache 600 all_tags_list %} | |
13 | <div class="post"> |
|
13 | <div class="post"> | |
14 | {% if all_tags %} |
|
14 | {% if all_tags %} | |
15 | {% for tag in all_tags %} |
|
15 | {% for tag in all_tags %} | |
16 | <div class="tag_item"> |
|
16 | <div class="tag_item"> | |
17 | <a class="tag" href="{% url 'tag' tag.name %}"> |
|
17 | {% autoescape off %} | |
18 |
|
|
18 | {{ tag.get_view }} | |
|
19 | {% endautoescape %} | |||
19 | </div> |
|
20 | </div> | |
20 | {% endfor %} |
|
21 | {% endfor %} | |
21 | {% else %} |
|
22 | {% else %} | |
22 | {% trans 'No tags found.' %} |
|
23 | {% trans 'No tags found.' %} | |
23 | {% endif %} |
|
24 | {% endif %} | |
24 | </div> |
|
25 | </div> | |
25 | {% endcache %} |
|
26 | {% endcache %} | |
26 |
|
27 | |||
27 | {% endblock %} |
|
28 | {% endblock %} |
@@ -1,95 +1,96 b'' | |||||
1 | {% extends "boards/base.html" %} |
|
1 | {% extends "boards/base.html" %} | |
2 |
|
2 | |||
3 | {% load i18n %} |
|
3 | {% load i18n %} | |
4 | {% load cache %} |
|
4 | {% load cache %} | |
5 | {% load static from staticfiles %} |
|
5 | {% load static from staticfiles %} | |
6 | {% load board %} |
|
6 | {% load board %} | |
7 |
|
7 | |||
8 | {% block head %} |
|
8 | {% block head %} | |
9 | <title>{{ opening_post.get_title|striptags|truncatewords:10 }} |
|
9 | <title>{{ opening_post.get_title|striptags|truncatewords:10 }} | |
10 | - {{ site_name }}</title> |
|
10 | - {{ site_name }}</title> | |
11 | {% endblock %} |
|
11 | {% endblock %} | |
12 |
|
12 | |||
13 | {% block content %} |
|
13 | {% block content %} | |
14 | {% spaceless %} |
|
|||
15 | {% get_current_language as LANGUAGE_CODE %} |
|
14 | {% get_current_language as LANGUAGE_CODE %} | |
16 |
|
15 | |||
17 | {% cache 600 thread_view thread.id thread.last_edit_time moderator LANGUAGE_CODE %} |
|
16 | {% cache 600 thread_view thread.id thread.last_edit_time moderator LANGUAGE_CODE %} | |
18 |
|
17 | |||
19 | <div class="image-mode-tab"> |
|
18 | <div class="image-mode-tab"> | |
20 | <a class="current_mode" href="{% url 'thread' opening_post.id %}">{% trans 'Normal mode' %}</a>, |
|
19 | <a class="current_mode" href="{% url 'thread' opening_post.id %}">{% trans 'Normal mode' %}</a>, | |
21 | <a href="{% url 'thread_mode' opening_post.id 'gallery' %}">{% trans 'Gallery mode' %}</a> |
|
20 | <a href="{% url 'thread_mode' opening_post.id 'gallery' %}">{% trans 'Gallery mode' %}</a> | |
22 | </div> |
|
21 | </div> | |
23 |
|
22 | |||
24 | {% if bumpable %} |
|
23 | {% if bumpable %} | |
25 | <div class="bar-bg"> |
|
24 | <div class="bar-bg"> | |
26 | <div class="bar-value" style="width:{{ bumplimit_progress }}%" id="bumplimit_progress"> |
|
25 | <div class="bar-value" style="width:{{ bumplimit_progress }}%" id="bumplimit_progress"> | |
27 | </div> |
|
26 | </div> | |
28 | <div class="bar-text"> |
|
27 | <div class="bar-text"> | |
29 | <span id="left_to_limit">{{ posts_left }}</span> {% trans 'posts to bumplimit' %} |
|
28 | <span id="left_to_limit">{{ posts_left }}</span> {% trans 'posts to bumplimit' %} | |
30 | </div> |
|
29 | </div> | |
31 | </div> |
|
30 | </div> | |
32 | {% endif %} |
|
31 | {% endif %} | |
33 |
|
32 | |||
34 | <div class="thread"> |
|
33 | <div class="thread"> | |
35 | {% with can_bump=thread.can_bump %} |
|
34 | {% with can_bump=thread.can_bump %} | |
36 | {% for post in thread.get_replies %} |
|
35 | {% for post in thread.get_replies %} | |
37 |
{% |
|
36 | {% with is_opening=forloop.first %} | |
38 |
{% post_view post moderator=moderator is_opening= |
|
37 | {% post_view post moderator=moderator is_opening=is_opening thread=thread bumpable=can_bump opening_post_id=opening_post.id %} | |
39 |
{% e |
|
38 | {% endwith %} | |
40 | {% post_view post moderator=moderator is_opening=False thread=thread can_bump=can_bump opening_post_id=opening_post.id %} |
|
|||
41 | {% endif %} |
|
|||
42 | {% endfor %} |
|
39 | {% endfor %} | |
43 | {% endwith %} |
|
40 | {% endwith %} | |
44 | </div> |
|
41 | </div> | |
45 |
|
42 | |||
46 | {% if not thread.archived %} |
|
43 | {% if not thread.archived %} | |
47 |
|
||||
48 | <div class="post-form-w" id="form"> |
|
44 | <div class="post-form-w" id="form"> | |
49 | <script src="{% static 'js/panel.js' %}"></script> |
|
45 | <script src="{% static 'js/panel.js' %}"></script> | |
50 | <div class="form-title">{% trans "Reply to thread" %} #{{ opening_post.id }}</div> |
|
46 | <div class="form-title">{% trans "Reply to thread" %} #{{ opening_post.id }}</div> | |
51 | <div class="post-form" id="compact-form"> |
|
47 | <div class="post-form" id="compact-form"> | |
52 | <div class="swappable-form-full"> |
|
48 | <div class="swappable-form-full"> | |
53 | <form enctype="multipart/form-data" method="post" |
|
49 | <form enctype="multipart/form-data" method="post" | |
54 | >{% csrf_token %} |
|
50 | >{% csrf_token %} | |
55 | <div class="compact-form-text"></div> |
|
51 | <div class="compact-form-text"></div> | |
56 | {{ form.as_div }} |
|
52 | {{ form.as_div }} | |
57 | <div class="form-submit"> |
|
53 | <div class="form-submit"> | |
58 | <input type="submit" value="{% trans "Post" %}"/> |
|
54 | <input type="submit" value="{% trans "Post" %}"/> | |
59 | </div> |
|
55 | </div> | |
60 | </form> |
|
56 | </form> | |
61 | </div> |
|
57 | </div> | |
62 | <a onclick="swapForm(); return false;" href="#"> |
|
58 | <a onclick="swapForm(); return false;" href="#"> | |
63 | {% trans 'Switch mode' %} |
|
59 | {% trans 'Switch mode' %} | |
64 | </a> |
|
60 | </a> | |
65 | <div><a href="{% url "staticpage" name="help" %}"> |
|
61 | <div><a href="{% url "staticpage" name="help" %}"> | |
66 | {% trans 'Text syntax' %}</a></div> |
|
62 | {% trans 'Text syntax' %}</a></div> | |
67 | </div> |
|
63 | </div> | |
68 | </div> |
|
64 | </div> | |
69 |
|
65 | |||
70 | <script src="{% static 'js/jquery.form.min.js' %}"></script> |
|
66 | <script src="{% static 'js/jquery.form.min.js' %}"></script> | |
71 | <script src="{% static 'js/thread_update.js' %}"></script> |
|
67 | <script src="{% static 'js/thread_update.js' %}"></script> | |
|
68 | <script src="{% static 'js/3party/centrifuge.js' %}"></script> | |||
72 | {% endif %} |
|
69 | {% endif %} | |
73 |
|
70 | |||
74 | <script src="{% static 'js/form.js' %}"></script> |
|
71 | <script src="{% static 'js/form.js' %}"></script> | |
75 | <script src="{% static 'js/thread.js' %}"></script> |
|
72 | <script src="{% static 'js/thread.js' %}"></script> | |
76 |
|
73 | |||
77 | {% endcache %} |
|
74 | {% endcache %} | |
78 |
|
||||
79 | {% endspaceless %} |
|
|||
80 | {% endblock %} |
|
75 | {% endblock %} | |
81 |
|
76 | |||
82 | {% block metapanel %} |
|
77 | {% block metapanel %} | |
83 |
|
78 | |||
84 | {% get_current_language as LANGUAGE_CODE %} |
|
79 | {% get_current_language as LANGUAGE_CODE %} | |
85 |
|
80 | |||
86 | <span class="metapanel" data-last-update="{{ last_update }}"> |
|
81 | <span class="metapanel" | |
|
82 | data-last-update="{{ last_update }}" | |||
|
83 | data-ws-token="{{ ws_token }}" | |||
|
84 | data-ws-project="{{ ws_project }}" | |||
|
85 | data-ws-host="{{ ws_host }}" | |||
|
86 | data-ws-port="{{ ws_port }}"> | |||
87 | {% cache 600 thread_meta thread.last_edit_time moderator LANGUAGE_CODE %} |
|
87 | {% cache 600 thread_meta thread.last_edit_time moderator LANGUAGE_CODE %} | |
|
88 | <span id="autoupdate">[-]</span> | |||
88 | <span id="reply-count">{{ thread.get_reply_count }}</span>/{{ max_replies }} {% trans 'messages' %}, |
|
89 | <span id="reply-count">{{ thread.get_reply_count }}</span>/{{ max_replies }} {% trans 'messages' %}, | |
89 | <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}. |
|
90 | <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}. | |
90 | {% trans 'Last update: ' %}<span id="last-update">{{ thread.last_edit_time }}</span> |
|
91 | {% trans 'Last update: ' %}<span id="last-update">{{ thread.last_edit_time }}</span> | |
91 | [<a href="rss/">RSS</a>] |
|
92 | [<a href="rss/">RSS</a>] | |
92 | {% endcache %} |
|
93 | {% endcache %} | |
93 | </span> |
|
94 | </span> | |
94 |
|
95 | |||
95 | {% endblock %} |
|
96 | {% endblock %} |
@@ -1,38 +1,38 b'' | |||||
1 | {% extends 'boards/base.html' %} |
|
1 | {% extends 'boards/base.html' %} | |
2 |
|
2 | |||
3 | {% load board %} |
|
3 | {% load board %} | |
4 | {% load i18n %} |
|
4 | {% load i18n %} | |
5 |
|
5 | |||
6 | {% block content %} |
|
6 | {% block content %} | |
7 | <div class="post-form-w"> |
|
7 | <div class="post-form-w"> | |
8 | <div class="post-form"> |
|
8 | <div class="post-form"> | |
9 | <h3>{% trans 'Search' %}</h3> |
|
9 | <h3>{% trans 'Search' %}</h3> | |
10 | <form method="get" action=""> |
|
10 | <form method="get" action=""> | |
11 | {{ form.as_div }} |
|
11 | {{ form.as_div }} | |
12 | <div class="form-submit"> |
|
12 | <div class="form-submit"> | |
13 | <input type="submit" value="{% trans 'Search' %}"> |
|
13 | <input type="submit" value="{% trans 'Search' %}"> | |
14 | </div> |
|
14 | </div> | |
15 | </form> |
|
15 | </form> | |
16 | </div> |
|
16 | </div> | |
17 | </div> |
|
17 | </div> | |
18 |
|
18 | |||
19 | {% if page %} |
|
19 | {% if page %} | |
20 | {% if page.has_previous %} |
|
20 | {% if page.has_previous %} | |
21 | <div class="page_link"> |
|
21 | <div class="page_link"> | |
22 | <a href="?query={{ query }}&page={{ page.previous_page_number }}">{% trans "Previous page" %} |
|
22 | <a href="?query={{ query }}&page={{ page.previous_page_number }}">{% trans "Previous page" %} | |
23 | </a> |
|
23 | </a> | |
24 | </div> |
|
24 | </div> | |
25 | {% endif %} |
|
25 | {% endif %} | |
26 |
|
26 | |||
27 | {% for result in page.object_list %} |
|
27 | {% for result in page.object_list %} | |
28 | {{ result.object.get_view }} |
|
28 | {{ result.object.get_search_view }} | |
29 | {% endfor %} |
|
29 | {% endfor %} | |
30 |
|
30 | |||
31 | {% if page.has_next %} |
|
31 | {% if page.has_next %} | |
32 | <div class="page_link"> |
|
32 | <div class="page_link"> | |
33 | <a href="?query={{ query }}&page={{ page.next_page_number }}">{% trans "Next page" %} |
|
33 | <a href="?query={{ query }}&page={{ page.next_page_number }}">{% trans "Next page" %} | |
34 | </a> |
|
34 | </a> | |
35 | </div> |
|
35 | </div> | |
36 | {% endif %} |
|
36 | {% endif %} | |
37 | {% endif %} |
|
37 | {% endif %} | |
38 | {% endblock %} No newline at end of file |
|
38 | {% endblock %} |
@@ -1,55 +1,56 b'' | |||||
1 | from django.test import TestCase, Client |
|
1 | from django.test import TestCase, Client | |
2 | import time |
|
2 | import time | |
3 | from boards import settings |
|
3 | from boards import settings | |
4 | from boards.models import Post |
|
4 | from boards.models import Post, Tag | |
5 | import neboard |
|
5 | import neboard | |
6 |
|
6 | |||
7 |
|
7 | |||
8 | TEST_TAG = 'test_tag' |
|
8 | TEST_TAG = 'test_tag' | |
9 |
|
9 | |||
10 | PAGE_404 = 'boards/404.html' |
|
10 | PAGE_404 = 'boards/404.html' | |
11 |
|
11 | |||
12 | TEST_TEXT = 'test text' |
|
12 | TEST_TEXT = 'test text' | |
13 |
|
13 | |||
14 | NEW_THREAD_PAGE = '/' |
|
14 | NEW_THREAD_PAGE = '/' | |
15 | THREAD_PAGE_ONE = '/thread/1/' |
|
15 | THREAD_PAGE_ONE = '/thread/1/' | |
16 | HTTP_CODE_REDIRECT = 302 |
|
16 | HTTP_CODE_REDIRECT = 302 | |
17 |
|
17 | |||
18 |
|
18 | |||
19 | class FormTest(TestCase): |
|
19 | class FormTest(TestCase): | |
20 | def test_post_validation(self): |
|
20 | def test_post_validation(self): | |
21 | client = Client() |
|
21 | client = Client() | |
22 |
|
22 | |||
23 | valid_tags = 'tag1 tag_2 тег_3' |
|
23 | valid_tags = 'tag1 tag_2 тег_3' | |
24 | invalid_tags = '$%_356 ---' |
|
24 | invalid_tags = '$%_356 ---' | |
|
25 | Tag.objects.create(name='tag1', required=True) | |||
25 |
|
26 | |||
26 | response = client.post(NEW_THREAD_PAGE, {'title': 'test title', |
|
27 | response = client.post(NEW_THREAD_PAGE, {'title': 'test title', | |
27 | 'text': TEST_TEXT, |
|
28 | 'text': TEST_TEXT, | |
28 | 'tags': valid_tags}) |
|
29 | 'tags': valid_tags}) | |
29 | self.assertEqual(response.status_code, HTTP_CODE_REDIRECT, |
|
30 | self.assertEqual(response.status_code, HTTP_CODE_REDIRECT, | |
30 | msg='Posting new message failed: got code ' + |
|
31 | msg='Posting new message failed: got code ' + | |
31 | str(response.status_code)) |
|
32 | str(response.status_code)) | |
32 |
|
33 | |||
33 | self.assertEqual(1, Post.objects.count(), |
|
34 | self.assertEqual(1, Post.objects.count(), | |
34 | msg='No posts were created') |
|
35 | msg='No posts were created') | |
35 |
|
36 | |||
36 | client.post(NEW_THREAD_PAGE, {'text': TEST_TEXT, |
|
37 | client.post(NEW_THREAD_PAGE, {'text': TEST_TEXT, | |
37 | 'tags': invalid_tags}) |
|
38 | 'tags': invalid_tags}) | |
38 | self.assertEqual(1, Post.objects.count(), msg='The validation passed ' |
|
39 | self.assertEqual(1, Post.objects.count(), msg='The validation passed ' | |
39 | 'where it should fail') |
|
40 | 'where it should fail') | |
40 |
|
41 | |||
41 | # Change posting delay so we don't have to wait for 30 seconds or more |
|
42 | # Change posting delay so we don't have to wait for 30 seconds or more | |
42 | old_posting_delay = neboard.settings.POSTING_DELAY |
|
43 | old_posting_delay = neboard.settings.POSTING_DELAY | |
43 | # Wait fot the posting delay or we won't be able to post |
|
44 | # Wait fot the posting delay or we won't be able to post | |
44 | neboard.settings.POSTING_DELAY = 1 |
|
45 | neboard.settings.POSTING_DELAY = 1 | |
45 | time.sleep(neboard.settings.POSTING_DELAY + 1) |
|
46 | time.sleep(neboard.settings.POSTING_DELAY + 1) | |
46 | response = client.post(THREAD_PAGE_ONE, {'text': TEST_TEXT, |
|
47 | response = client.post(THREAD_PAGE_ONE, {'text': TEST_TEXT, | |
47 | 'tags': valid_tags}) |
|
48 | 'tags': valid_tags}) | |
48 | self.assertEqual(HTTP_CODE_REDIRECT, response.status_code, |
|
49 | self.assertEqual(HTTP_CODE_REDIRECT, response.status_code, | |
49 | msg='Posting new message failed: got code ' + |
|
50 | msg='Posting new message failed: got code ' + | |
50 | str(response.status_code)) |
|
51 | str(response.status_code)) | |
51 | # Restore posting delay |
|
52 | # Restore posting delay | |
52 | settings.POSTING_DELAY = old_posting_delay |
|
53 | settings.POSTING_DELAY = old_posting_delay | |
53 |
|
54 | |||
54 | self.assertEqual(2, Post.objects.count(), |
|
55 | self.assertEqual(2, Post.objects.count(), | |
55 | msg='No posts were created') |
|
56 | msg='No posts were created') |
@@ -1,56 +1,52 b'' | |||||
1 | from django.test import TestCase, Client |
|
1 | from django.test import TestCase, Client | |
2 | from boards.models import Tag, Post |
|
2 | from boards.models import Tag, Post | |
3 |
|
3 | |||
4 | TEST_TEXT = 'test' |
|
4 | TEST_TEXT = 'test' | |
5 |
|
5 | |||
6 | NEW_THREAD_PAGE = '/' |
|
6 | NEW_THREAD_PAGE = '/' | |
7 | THREAD_PAGE_ONE = '/thread/1/' |
|
7 | THREAD_PAGE_ONE = '/thread/1/' | |
8 | THREAD_PAGE = '/thread/' |
|
8 | THREAD_PAGE = '/thread/' | |
9 | TAG_PAGE = '/tag/' |
|
9 | TAG_PAGE = '/tag/' | |
10 |
HTTP_CODE_REDIRECT = 30 |
|
10 | HTTP_CODE_REDIRECT = 301 | |
11 | HTTP_CODE_OK = 200 |
|
11 | HTTP_CODE_OK = 200 | |
12 | HTTP_CODE_NOT_FOUND = 404 |
|
12 | HTTP_CODE_NOT_FOUND = 404 | |
13 |
|
13 | |||
14 | PAGE_404 = 'boards/404.html' |
|
|||
15 |
|
||||
16 |
|
14 | |||
17 | class PagesTest(TestCase): |
|
15 | class PagesTest(TestCase): | |
18 |
|
16 | |||
19 | def test_404(self): |
|
17 | def test_404(self): | |
20 | """Test receiving error 404 when opening a non-existent page""" |
|
18 | """Test receiving error 404 when opening a non-existent page""" | |
21 |
|
19 | |||
22 | tag_name = 'test_tag' |
|
20 | tag_name = 'test_tag' | |
23 | tag = Tag.objects.create(name=tag_name) |
|
21 | tag = Tag.objects.create(name=tag_name) | |
24 | client = Client() |
|
22 | client = Client() | |
25 |
|
23 | |||
26 | Post.objects.create_post('title', TEST_TEXT, tags=[tag]) |
|
24 | Post.objects.create_post('title', TEST_TEXT, tags=[tag]) | |
27 |
|
25 | |||
28 | existing_post_id = Post.objects.all()[0].id |
|
26 | existing_post_id = Post.objects.all()[0].id | |
29 | response_existing = client.get(THREAD_PAGE + str(existing_post_id) + |
|
27 | response_existing = client.get(THREAD_PAGE + str(existing_post_id) + | |
30 | '/') |
|
28 | '/') | |
31 | self.assertEqual(HTTP_CODE_OK, response_existing.status_code, |
|
29 | self.assertEqual(HTTP_CODE_OK, response_existing.status_code, | |
32 | 'Cannot open existing thread') |
|
30 | 'Cannot open existing thread') | |
33 |
|
31 | |||
34 | response_not_existing = client.get(THREAD_PAGE + str( |
|
32 | response_not_existing = client.get(THREAD_PAGE + str( | |
35 | existing_post_id + 1) + '/') |
|
33 | existing_post_id + 1) + '/') | |
36 |
self.assertEqual( |
|
34 | self.assertEqual(HTTP_CODE_NOT_FOUND, response_not_existing.status_code, | |
37 | 'Not existing thread is opened') |
|
35 | 'Not existing thread is opened') | |
38 |
|
36 | |||
39 | response_existing = client.get(TAG_PAGE + tag_name + '/') |
|
37 | response_existing = client.get(TAG_PAGE + tag_name + '/') | |
40 | self.assertEqual(HTTP_CODE_OK, |
|
38 | self.assertEqual(HTTP_CODE_OK, | |
41 | response_existing.status_code, |
|
39 | response_existing.status_code, | |
42 | 'Cannot open existing tag') |
|
40 | 'Cannot open existing tag') | |
43 |
|
41 | |||
44 | response_not_existing = client.get(TAG_PAGE + 'not_tag' + '/') |
|
42 | response_not_existing = client.get(TAG_PAGE + 'not_tag' + '/') | |
45 | self.assertEqual(PAGE_404, |
|
43 | self.assertEqual(HTTP_CODE_NOT_FOUND, response_not_existing.status_code, | |
46 | response_not_existing.templates[0].name, |
|
|||
47 | 'Not existing tag is opened') |
|
44 | 'Not existing tag is opened') | |
48 |
|
45 | |||
49 | reply_id = Post.objects.create_post('', TEST_TEXT, |
|
46 | reply_id = Post.objects.create_post('', TEST_TEXT, | |
50 | thread=Post.objects.all()[0] |
|
47 | thread=Post.objects.all()[0] | |
51 | .get_thread()) |
|
48 | .get_thread()) | |
52 | response_not_existing = client.get(THREAD_PAGE + str( |
|
49 | response_not_existing = client.get(THREAD_PAGE + str( | |
53 | reply_id) + '/') |
|
50 | reply_id) + '/') | |
54 | self.assertEqual(PAGE_404, |
|
51 | self.assertEqual(HTTP_CODE_REDIRECT, response_not_existing.status_code, | |
55 | response_not_existing.templates[0].name, |
|
|||
56 | 'Reply is opened as a thread') |
|
52 | 'Reply is opened as a thread') |
@@ -1,142 +1,163 b'' | |||||
1 | from django.core.paginator import Paginator |
|
1 | from django.core.paginator import Paginator | |
2 | from django.test import TestCase |
|
2 | from django.test import TestCase | |
3 | from boards import settings |
|
3 | from boards import settings | |
4 | from boards.models import Tag, Post, Thread, KeyPair |
|
4 | from boards.models import Tag, Post, Thread, KeyPair | |
5 |
|
5 | |||
6 |
|
6 | |||
7 | class PostTests(TestCase): |
|
7 | class PostTests(TestCase): | |
8 |
|
8 | |||
9 | def _create_post(self): |
|
9 | def _create_post(self): | |
10 | tag = Tag.objects.create(name='test_tag') |
|
10 | tag = Tag.objects.create(name='test_tag') | |
11 | return Post.objects.create_post(title='title', text='text', |
|
11 | return Post.objects.create_post(title='title', text='text', | |
12 | tags=[tag]) |
|
12 | tags=[tag]) | |
13 |
|
13 | |||
14 | def test_post_add(self): |
|
14 | def test_post_add(self): | |
15 | """Test adding post""" |
|
15 | """Test adding post""" | |
16 |
|
16 | |||
17 | post = self._create_post() |
|
17 | post = self._create_post() | |
18 |
|
18 | |||
19 | self.assertIsNotNone(post, 'No post was created.') |
|
19 | self.assertIsNotNone(post, 'No post was created.') | |
20 | self.assertEqual('test_tag', post.get_thread().tags.all()[0].name, |
|
20 | self.assertEqual('test_tag', post.get_thread().tags.all()[0].name, | |
21 | 'No tags were added to the post.') |
|
21 | 'No tags were added to the post.') | |
22 |
|
22 | |||
23 | def test_delete_post(self): |
|
23 | def test_delete_post(self): | |
24 | """Test post deletion""" |
|
24 | """Test post deletion""" | |
25 |
|
25 | |||
26 | post = self._create_post() |
|
26 | post = self._create_post() | |
27 | post_id = post.id |
|
27 | post_id = post.id | |
28 |
|
28 | |||
29 |
|
|
29 | post.delete() | |
30 |
|
30 | |||
31 | self.assertFalse(Post.objects.filter(id=post_id).exists()) |
|
31 | self.assertFalse(Post.objects.filter(id=post_id).exists()) | |
32 |
|
32 | |||
33 | def test_delete_thread(self): |
|
33 | def test_delete_thread(self): | |
34 | """Test thread deletion""" |
|
34 | """Test thread deletion""" | |
35 |
|
35 | |||
36 | opening_post = self._create_post() |
|
36 | opening_post = self._create_post() | |
37 | thread = opening_post.get_thread() |
|
37 | thread = opening_post.get_thread() | |
38 | reply = Post.objects.create_post("", "", thread=thread) |
|
38 | reply = Post.objects.create_post("", "", thread=thread) | |
39 |
|
39 | |||
40 |
|
|
40 | opening_post.delete() | |
41 |
|
41 | |||
42 |
self.assertFalse(Post.objects.filter(id=reply.id).exists() |
|
42 | self.assertFalse(Post.objects.filter(id=reply.id).exists(), | |
|
43 | 'Reply was not deleted with the thread.') | |||
|
44 | self.assertFalse(Post.objects.filter(id=opening_post.id).exists(), | |||
|
45 | 'Opening post was not deleted with the thread.') | |||
43 |
|
46 | |||
44 | def test_post_to_thread(self): |
|
47 | def test_post_to_thread(self): | |
45 | """Test adding post to a thread""" |
|
48 | """Test adding post to a thread""" | |
46 |
|
49 | |||
47 | op = self._create_post() |
|
50 | op = self._create_post() | |
48 | post = Post.objects.create_post("", "", thread=op.get_thread()) |
|
51 | post = Post.objects.create_post("", "", thread=op.get_thread()) | |
49 |
|
52 | |||
50 | self.assertIsNotNone(post, 'Reply to thread wasn\'t created') |
|
53 | self.assertIsNotNone(post, 'Reply to thread wasn\'t created') | |
51 | self.assertEqual(op.get_thread().last_edit_time, post.pub_time, |
|
54 | self.assertEqual(op.get_thread().last_edit_time, post.pub_time, | |
52 | 'Post\'s create time doesn\'t match thread last edit' |
|
55 | 'Post\'s create time doesn\'t match thread last edit' | |
53 | ' time') |
|
56 | ' time') | |
54 |
|
57 | |||
55 | def test_delete_posts_by_ip(self): |
|
58 | def test_delete_posts_by_ip(self): | |
56 | """Test deleting posts with the given ip""" |
|
59 | """Test deleting posts with the given ip""" | |
57 |
|
60 | |||
58 | post = self._create_post() |
|
61 | post = self._create_post() | |
59 | post_id = post.id |
|
62 | post_id = post.id | |
60 |
|
63 | |||
61 | Post.objects.delete_posts_by_ip('0.0.0.0') |
|
64 | Post.objects.delete_posts_by_ip('0.0.0.0') | |
62 |
|
65 | |||
63 | self.assertFalse(Post.objects.filter(id=post_id).exists()) |
|
66 | self.assertFalse(Post.objects.filter(id=post_id).exists()) | |
64 |
|
67 | |||
65 | def test_get_thread(self): |
|
68 | def test_get_thread(self): | |
66 | """Test getting all posts of a thread""" |
|
69 | """Test getting all posts of a thread""" | |
67 |
|
70 | |||
68 | opening_post = self._create_post() |
|
71 | opening_post = self._create_post() | |
69 |
|
72 | |||
70 | for i in range(2): |
|
73 | for i in range(2): | |
71 | Post.objects.create_post('title', 'text', |
|
74 | Post.objects.create_post('title', 'text', | |
72 | thread=opening_post.get_thread()) |
|
75 | thread=opening_post.get_thread()) | |
73 |
|
76 | |||
74 | thread = opening_post.get_thread() |
|
77 | thread = opening_post.get_thread() | |
75 |
|
78 | |||
76 | self.assertEqual(3, thread.replies.count()) |
|
79 | self.assertEqual(3, thread.replies.count()) | |
77 |
|
80 | |||
78 | def test_create_post_with_tag(self): |
|
81 | def test_create_post_with_tag(self): | |
79 | """Test adding tag to post""" |
|
82 | """Test adding tag to post""" | |
80 |
|
83 | |||
81 | tag = Tag.objects.create(name='test_tag') |
|
84 | tag = Tag.objects.create(name='test_tag') | |
82 | post = Post.objects.create_post(title='title', text='text', tags=[tag]) |
|
85 | post = Post.objects.create_post(title='title', text='text', tags=[tag]) | |
83 |
|
86 | |||
84 | thread = post.get_thread() |
|
87 | thread = post.get_thread() | |
85 | self.assertIsNotNone(post, 'Post not created') |
|
88 | self.assertIsNotNone(post, 'Post not created') | |
86 | self.assertTrue(tag in thread.tags.all(), 'Tag not added to thread') |
|
89 | self.assertTrue(tag in thread.tags.all(), 'Tag not added to thread') | |
87 | self.assertTrue(thread in tag.threads.all(), 'Thread not added to tag') |
|
|||
88 |
|
90 | |||
89 | def test_thread_max_count(self): |
|
91 | def test_thread_max_count(self): | |
90 | """Test deletion of old posts when the max thread count is reached""" |
|
92 | """Test deletion of old posts when the max thread count is reached""" | |
91 |
|
93 | |||
92 | for i in range(settings.MAX_THREAD_COUNT + 1): |
|
94 | for i in range(settings.MAX_THREAD_COUNT + 1): | |
93 | self._create_post() |
|
95 | self._create_post() | |
94 |
|
96 | |||
95 | self.assertEqual(settings.MAX_THREAD_COUNT, |
|
97 | self.assertEqual(settings.MAX_THREAD_COUNT, | |
96 | len(Thread.objects.filter(archived=False))) |
|
98 | len(Thread.objects.filter(archived=False))) | |
97 |
|
99 | |||
98 | def test_pages(self): |
|
100 | def test_pages(self): | |
99 | """Test that the thread list is properly split into pages""" |
|
101 | """Test that the thread list is properly split into pages""" | |
100 |
|
102 | |||
101 | for i in range(settings.MAX_THREAD_COUNT): |
|
103 | for i in range(settings.MAX_THREAD_COUNT): | |
102 | self._create_post() |
|
104 | self._create_post() | |
103 |
|
105 | |||
104 | all_threads = Thread.objects.filter(archived=False) |
|
106 | all_threads = Thread.objects.filter(archived=False) | |
105 |
|
107 | |||
106 | paginator = Paginator(Thread.objects.filter(archived=False), |
|
108 | paginator = Paginator(Thread.objects.filter(archived=False), | |
107 | settings.THREADS_PER_PAGE) |
|
109 | settings.THREADS_PER_PAGE) | |
108 | posts_in_second_page = paginator.page(2).object_list |
|
110 | posts_in_second_page = paginator.page(2).object_list | |
109 | first_post = posts_in_second_page[0] |
|
111 | first_post = posts_in_second_page[0] | |
110 |
|
112 | |||
111 | self.assertEqual(all_threads[settings.THREADS_PER_PAGE].id, |
|
113 | self.assertEqual(all_threads[settings.THREADS_PER_PAGE].id, | |
112 | first_post.id) |
|
114 | first_post.id) | |
113 |
|
115 | |||
114 | def test_reflinks(self): |
|
116 | def test_reflinks(self): | |
115 | """ |
|
117 | """ | |
116 | Tests that reflinks are parsed within post and connecting replies |
|
118 | Tests that reflinks are parsed within post and connecting replies | |
117 | to the replied posts. |
|
119 | to the replied posts. | |
118 |
|
120 | |||
119 | Local reflink example: [post]123[/post] |
|
121 | Local reflink example: [post]123[/post] | |
120 | Global reflink example: [post]key_type::key::123[/post] |
|
122 | Global reflink example: [post]key_type::key::123[/post] | |
121 | """ |
|
123 | """ | |
122 |
|
124 | |||
123 | key = KeyPair.objects.generate_key(primary=True) |
|
125 | key = KeyPair.objects.generate_key(primary=True) | |
124 |
|
126 | |||
125 | tag = Tag.objects.create(name='test_tag') |
|
127 | tag = Tag.objects.create(name='test_tag') | |
126 |
|
128 | |||
127 | post = Post.objects.create_post(title='', text='', tags=[tag]) |
|
129 | post = Post.objects.create_post(title='', text='', tags=[tag]) | |
128 | post_local_reflink = Post.objects.create_post(title='', |
|
130 | post_local_reflink = Post.objects.create_post(title='', | |
129 | text='[post]%d[/post]' % post.id, thread=post.get_thread()) |
|
131 | text='[post]%d[/post]' % post.id, thread=post.get_thread()) | |
130 |
|
132 | |||
131 | self.assertTrue(post_local_reflink in post.referenced_posts.all(), |
|
133 | self.assertTrue(post_local_reflink in post.referenced_posts.all(), | |
132 | 'Local reflink not connecting posts.') |
|
134 | 'Local reflink not connecting posts.') | |
133 |
|
135 | |||
134 | post_global_reflink = Post.objects.create_post(title='', |
|
136 | post_global_reflink = Post.objects.create_post(title='', | |
135 | text='[post]%s::%s::%d[/post]' % ( |
|
137 | text='[post]%s::%s::%d[/post]' % ( | |
136 | post.global_id.key_type, post.global_id.key, post.id), |
|
138 | post.global_id.key_type, post.global_id.key, post.id), | |
137 | thread=post.get_thread()) |
|
139 | thread=post.get_thread()) | |
138 |
|
140 | |||
139 | self.assertTrue(post_global_reflink in post.referenced_posts.all(), |
|
141 | self.assertTrue(post_global_reflink in post.referenced_posts.all(), | |
140 | 'Global reflink not connecting posts.') |
|
142 | 'Global reflink not connecting posts.') | |
141 |
|
143 | |||
142 | # TODO Check that links are parsed into the rendered text |
|
144 | def test_thread_replies(self): | |
|
145 | """ | |||
|
146 | Tests that the replies can be queried from a thread in all possible | |||
|
147 | ways. | |||
|
148 | """ | |||
|
149 | ||||
|
150 | tag = Tag.objects.create(name='test_tag') | |||
|
151 | opening_post = Post.objects.create_post(title='title', text='text', | |||
|
152 | tags=[tag]) | |||
|
153 | thread = opening_post.get_thread() | |||
|
154 | ||||
|
155 | reply1 = Post.objects.create_post(title='title', text='text', thread=thread) | |||
|
156 | reply2 = Post.objects.create_post(title='title', text='text', thread=thread) | |||
|
157 | ||||
|
158 | replies = thread.get_replies() | |||
|
159 | self.assertTrue(len(replies) > 0, 'No replies found for thread.') | |||
|
160 | ||||
|
161 | replies = thread.get_replies(view_fields_only=True) | |||
|
162 | self.assertTrue(len(replies) > 0, | |||
|
163 | 'No replies found for thread with view fields only.') |
@@ -1,38 +1,46 b'' | |||||
1 | import logging |
|
1 | import logging | |
2 | from django.core.urlresolvers import reverse, NoReverseMatch |
|
2 | from django.core.urlresolvers import reverse, NoReverseMatch | |
3 | from django.test import TestCase, Client |
|
3 | from django.test import TestCase, Client | |
4 | from boards import urls |
|
4 | from boards import urls | |
5 |
|
5 | |||
6 |
|
6 | |||
7 | logger = logging.getLogger(__name__) |
|
7 | logger = logging.getLogger(__name__) | |
8 |
|
8 | |||
9 | HTTP_CODE_OK = 200 |
|
9 | HTTP_CODE_OK = 200 | |
10 |
|
10 | |||
|
11 | EXCLUDED_VIEWS = { | |||
|
12 | 'banned', | |||
|
13 | } | |||
|
14 | ||||
11 |
|
15 | |||
12 | class ViewTest(TestCase): |
|
16 | class ViewTest(TestCase): | |
13 |
|
17 | |||
14 | def test_all_views(self): |
|
18 | def test_all_views(self): | |
15 | """ |
|
19 | """ | |
16 | Try opening all views defined in ulrs.py that don't need additional |
|
20 | Try opening all views defined in ulrs.py that don't need additional | |
17 | parameters |
|
21 | parameters | |
18 | """ |
|
22 | """ | |
19 |
|
23 | |||
20 | client = Client() |
|
24 | client = Client() | |
21 | for url in urls.urlpatterns: |
|
25 | for url in urls.urlpatterns: | |
22 | try: |
|
26 | try: | |
23 | view_name = url.name |
|
27 | view_name = url.name | |
|
28 | if view_name in EXCLUDED_VIEWS: | |||
|
29 | logger.debug('View {} is excluded.'.format(view_name)) | |||
|
30 | continue | |||
|
31 | ||||
24 | logger.debug('Testing view %s' % view_name) |
|
32 | logger.debug('Testing view %s' % view_name) | |
25 |
|
33 | |||
26 | try: |
|
34 | try: | |
27 | response = client.get(reverse(view_name)) |
|
35 | response = client.get(reverse(view_name)) | |
28 |
|
36 | |||
29 | self.assertEqual(HTTP_CODE_OK, response.status_code, |
|
37 | self.assertEqual(HTTP_CODE_OK, response.status_code, | |
30 |
|
|
38 | 'View not opened: {}'.format(view_name)) | |
31 | except NoReverseMatch: |
|
39 | except NoReverseMatch: | |
32 | # This view just needs additional arguments |
|
40 | # This view just needs additional arguments | |
33 | pass |
|
41 | pass | |
34 | except Exception as e: |
|
42 | except Exception as e: | |
35 | self.fail('Got exception %s at %s view' % (e, view_name)) |
|
43 | self.fail('Got exception %s at %s view' % (e, view_name)) | |
36 | except AttributeError: |
|
44 | except AttributeError: | |
37 | # This is normal, some views do not have names |
|
45 | # This is normal, some views do not have names | |
38 | pass |
|
46 | pass |
@@ -1,219 +1,215 b'' | |||||
1 | # -*- encoding: utf-8 -*- |
|
1 | # -*- encoding: utf-8 -*- | |
2 | """ |
|
2 | """ | |
3 | django-thumbs by Antonio Melé |
|
3 | django-thumbs by Antonio Melé | |
4 | http://django.es |
|
4 | http://django.es | |
5 | """ |
|
5 | """ | |
6 | from django.core.files.images import ImageFile |
|
6 | from django.core.files.images import ImageFile | |
7 | from django.db.models import ImageField |
|
7 | from django.db.models import ImageField | |
8 | from django.db.models.fields.files import ImageFieldFile |
|
8 | from django.db.models.fields.files import ImageFieldFile | |
9 | from PIL import Image |
|
9 | from PIL import Image | |
10 | from django.core.files.base import ContentFile |
|
10 | from django.core.files.base import ContentFile | |
11 | import io |
|
11 | import io | |
12 |
|
12 | |||
13 |
|
13 | |||
14 | def generate_thumb(img, thumb_size, format): |
|
14 | def generate_thumb(img, thumb_size, format): | |
15 | """ |
|
15 | """ | |
16 | Generates a thumbnail image and returns a ContentFile object with the thumbnail |
|
16 | Generates a thumbnail image and returns a ContentFile object with the thumbnail | |
17 |
|
17 | |||
18 | Parameters: |
|
18 | Parameters: | |
19 | =========== |
|
19 | =========== | |
20 | img File object |
|
20 | img File object | |
21 |
|
21 | |||
22 | thumb_size desired thumbnail size, ie: (200,120) |
|
22 | thumb_size desired thumbnail size, ie: (200,120) | |
23 |
|
23 | |||
24 | format format of the original image ('jpeg','gif','png',...) |
|
24 | format format of the original image ('jpeg','gif','png',...) | |
25 | (this format will be used for the generated thumbnail, too) |
|
25 | (this format will be used for the generated thumbnail, too) | |
26 | """ |
|
26 | """ | |
27 |
|
27 | |||
28 | img.seek(0) # see http://code.djangoproject.com/ticket/8222 for details |
|
28 | img.seek(0) # see http://code.djangoproject.com/ticket/8222 for details | |
29 | image = Image.open(img) |
|
29 | image = Image.open(img) | |
30 |
|
30 | |||
31 | # get size |
|
31 | # get size | |
32 | thumb_w, thumb_h = thumb_size |
|
32 | thumb_w, thumb_h = thumb_size | |
33 | # If you want to generate a square thumbnail |
|
33 | # If you want to generate a square thumbnail | |
34 | if thumb_w == thumb_h: |
|
34 | if thumb_w == thumb_h: | |
35 | # quad |
|
35 | # quad | |
36 | xsize, ysize = image.size |
|
36 | xsize, ysize = image.size | |
37 | # get minimum size |
|
37 | # get minimum size | |
38 | minsize = min(xsize, ysize) |
|
38 | minsize = min(xsize, ysize) | |
39 | # largest square possible in the image |
|
39 | # largest square possible in the image | |
40 | xnewsize = (xsize - minsize) / 2 |
|
40 | xnewsize = (xsize - minsize) / 2 | |
41 | ynewsize = (ysize - minsize) / 2 |
|
41 | ynewsize = (ysize - minsize) / 2 | |
42 | # crop it |
|
42 | # crop it | |
43 | image2 = image.crop( |
|
43 | image2 = image.crop( | |
44 | (xnewsize, ynewsize, xsize - xnewsize, ysize - ynewsize)) |
|
44 | (xnewsize, ynewsize, xsize - xnewsize, ysize - ynewsize)) | |
45 | # load is necessary after crop |
|
45 | # load is necessary after crop | |
46 | image2.load() |
|
46 | image2.load() | |
47 | # thumbnail of the cropped image (with ANTIALIAS to make it look better) |
|
47 | # thumbnail of the cropped image (with ANTIALIAS to make it look better) | |
48 | image2.thumbnail(thumb_size, Image.ANTIALIAS) |
|
48 | image2.thumbnail(thumb_size, Image.ANTIALIAS) | |
49 | else: |
|
49 | else: | |
50 | # not quad |
|
50 | # not quad | |
51 | image2 = image |
|
51 | image2 = image | |
52 | image2.thumbnail(thumb_size, Image.ANTIALIAS) |
|
52 | image2.thumbnail(thumb_size, Image.ANTIALIAS) | |
53 |
|
53 | |||
54 | output = io.BytesIO() |
|
54 | output = io.BytesIO() | |
55 | # PNG and GIF are the same, JPG is JPEG |
|
55 | # PNG and GIF are the same, JPG is JPEG | |
56 | if format.upper() == 'JPG': |
|
56 | if format.upper() == 'JPG': | |
57 | format = 'JPEG' |
|
57 | format = 'JPEG' | |
58 |
|
58 | |||
59 | image2.save(output, format) |
|
59 | image2.save(output, format) | |
60 | return ContentFile(output.getvalue()) |
|
60 | return ContentFile(output.getvalue()) | |
61 |
|
61 | |||
62 |
|
62 | |||
63 | class ImageWithThumbsFieldFile(ImageFieldFile): |
|
63 | class ImageWithThumbsFieldFile(ImageFieldFile): | |
64 | """ |
|
64 | """ | |
65 | See ImageWithThumbsField for usage example |
|
65 | See ImageWithThumbsField for usage example | |
66 | """ |
|
66 | """ | |
67 |
|
67 | |||
68 | def __init__(self, *args, **kwargs): |
|
68 | def __init__(self, *args, **kwargs): | |
69 | super(ImageWithThumbsFieldFile, self).__init__(*args, **kwargs) |
|
69 | super(ImageWithThumbsFieldFile, self).__init__(*args, **kwargs) | |
70 | self.sizes = self.field.sizes |
|
70 | self.sizes = self.field.sizes | |
71 |
|
71 | |||
72 | if self.sizes: |
|
72 | if self.sizes: | |
73 | def get_size(self, size): |
|
73 | def get_size(self, size): | |
74 | if not self: |
|
74 | if not self: | |
75 | return '' |
|
75 | return '' | |
76 | else: |
|
76 | else: | |
77 | split = self.url.rsplit('.', 1) |
|
77 | split = self.url.rsplit('.', 1) | |
78 | thumb_url = '%s.%sx%s.%s' % (split[0], w, h, split[1]) |
|
78 | thumb_url = '%s.%sx%s.%s' % (split[0], w, h, split[1]) | |
79 | return thumb_url |
|
79 | return thumb_url | |
80 |
|
80 | |||
81 | for size in self.sizes: |
|
81 | for size in self.sizes: | |
82 | (w, h) = size |
|
82 | (w, h) = size | |
83 | setattr(self, 'url_%sx%s' % (w, h), get_size(self, size)) |
|
83 | setattr(self, 'url_%sx%s' % (w, h), get_size(self, size)) | |
84 |
|
84 | |||
85 | def save(self, name, content, save=True): |
|
85 | def save(self, name, content, save=True): | |
86 | super(ImageWithThumbsFieldFile, self).save(name, content, save) |
|
86 | super(ImageWithThumbsFieldFile, self).save(name, content, save) | |
87 |
|
87 | |||
88 | if self.sizes: |
|
88 | if self.sizes: | |
89 | for size in self.sizes: |
|
89 | for size in self.sizes: | |
90 | (w, h) = size |
|
90 | (w, h) = size | |
91 | split = self.name.rsplit('.', 1) |
|
91 | split = self.name.rsplit('.', 1) | |
92 | thumb_name = '%s.%sx%s.%s' % (split[0], w, h, split[1]) |
|
92 | thumb_name = '%s.%sx%s.%s' % (split[0], w, h, split[1]) | |
93 |
|
93 | |||
94 | # you can use another thumbnailing function if you like |
|
94 | # you can use another thumbnailing function if you like | |
95 | thumb_content = generate_thumb(content, size, split[1]) |
|
95 | thumb_content = generate_thumb(content, size, split[1]) | |
96 |
|
96 | |||
97 | thumb_name_ = self.storage.save(thumb_name, thumb_content) |
|
97 | thumb_name_ = self.storage.save(thumb_name, thumb_content) | |
98 |
|
98 | |||
99 | if not thumb_name == thumb_name_: |
|
99 | if not thumb_name == thumb_name_: | |
100 | raise ValueError( |
|
100 | raise ValueError( | |
101 | 'There is already a file named %s' % thumb_name) |
|
101 | 'There is already a file named %s' % thumb_name) | |
102 |
|
102 | |||
103 | def delete(self, save=True): |
|
103 | def delete(self, save=True): | |
104 | name = self.name |
|
104 | name = self.name | |
105 | super(ImageWithThumbsFieldFile, self).delete(save) |
|
105 | super(ImageWithThumbsFieldFile, self).delete(save) | |
106 | if self.sizes: |
|
106 | if self.sizes: | |
107 | for size in self.sizes: |
|
107 | for size in self.sizes: | |
108 | (w, h) = size |
|
108 | (w, h) = size | |
109 | split = name.rsplit('.', 1) |
|
109 | split = name.rsplit('.', 1) | |
110 | thumb_name = '%s.%sx%s.%s' % (split[0], w, h, split[1]) |
|
110 | thumb_name = '%s.%sx%s.%s' % (split[0], w, h, split[1]) | |
111 | try: |
|
111 | try: | |
112 | self.storage.delete(thumb_name) |
|
112 | self.storage.delete(thumb_name) | |
113 | except: |
|
113 | except: | |
114 | pass |
|
114 | pass | |
115 |
|
115 | |||
116 |
|
116 | |||
117 | class ImageWithThumbsField(ImageField): |
|
117 | class ImageWithThumbsField(ImageField): | |
118 | attr_class = ImageWithThumbsFieldFile |
|
118 | attr_class = ImageWithThumbsFieldFile | |
119 | """ |
|
119 | """ | |
120 | Usage example: |
|
120 | Usage example: | |
121 | ============== |
|
121 | ============== | |
122 | photo = ImageWithThumbsField(upload_to='images', sizes=((125,125),(300,200),) |
|
122 | photo = ImageWithThumbsField(upload_to='images', sizes=((125,125),(300,200),) | |
123 |
|
123 | |||
124 | To retrieve image URL, exactly the same way as with ImageField: |
|
124 | To retrieve image URL, exactly the same way as with ImageField: | |
125 | my_object.photo.url |
|
125 | my_object.photo.url | |
126 | To retrieve thumbnails URL's just add the size to it: |
|
126 | To retrieve thumbnails URL's just add the size to it: | |
127 | my_object.photo.url_125x125 |
|
127 | my_object.photo.url_125x125 | |
128 | my_object.photo.url_300x200 |
|
128 | my_object.photo.url_300x200 | |
129 |
|
129 | |||
130 | Note: The 'sizes' attribute is not required. If you don't provide it, |
|
130 | Note: The 'sizes' attribute is not required. If you don't provide it, | |
131 | ImageWithThumbsField will act as a normal ImageField |
|
131 | ImageWithThumbsField will act as a normal ImageField | |
132 |
|
132 | |||
133 | How it works: |
|
133 | How it works: | |
134 | ============= |
|
134 | ============= | |
135 | For each size in the 'sizes' atribute of the field it generates a |
|
135 | For each size in the 'sizes' atribute of the field it generates a | |
136 | thumbnail with that size and stores it following this format: |
|
136 | thumbnail with that size and stores it following this format: | |
137 |
|
137 | |||
138 | available_filename.[width]x[height].extension |
|
138 | available_filename.[width]x[height].extension | |
139 |
|
139 | |||
140 | Where 'available_filename' is the available filename returned by the storage |
|
140 | Where 'available_filename' is the available filename returned by the storage | |
141 | backend for saving the original file. |
|
141 | backend for saving the original file. | |
142 |
|
142 | |||
143 | Following the usage example above: For storing a file called "photo.jpg" it saves: |
|
143 | Following the usage example above: For storing a file called "photo.jpg" it saves: | |
144 | photo.jpg (original file) |
|
144 | photo.jpg (original file) | |
145 | photo.125x125.jpg (first thumbnail) |
|
145 | photo.125x125.jpg (first thumbnail) | |
146 | photo.300x200.jpg (second thumbnail) |
|
146 | photo.300x200.jpg (second thumbnail) | |
147 |
|
147 | |||
148 | With the default storage backend if photo.jpg already exists it will use these filenames: |
|
148 | With the default storage backend if photo.jpg already exists it will use these filenames: | |
149 | photo_.jpg |
|
149 | photo_.jpg | |
150 | photo_.125x125.jpg |
|
150 | photo_.125x125.jpg | |
151 | photo_.300x200.jpg |
|
151 | photo_.300x200.jpg | |
152 |
|
152 | |||
153 | Note: django-thumbs assumes that if filename "any_filename.jpg" is available |
|
153 | Note: django-thumbs assumes that if filename "any_filename.jpg" is available | |
154 | filenames with this format "any_filename.[widht]x[height].jpg" will be available, too. |
|
154 | filenames with this format "any_filename.[widht]x[height].jpg" will be available, too. | |
155 |
|
155 | |||
156 | To do: |
|
156 | To do: | |
157 | ====== |
|
157 | ====== | |
158 | Add method to regenerate thubmnails |
|
158 | Add method to regenerate thubmnails | |
159 |
|
159 | |||
160 |
|
160 | |||
161 | """ |
|
161 | """ | |
162 |
|
162 | |||
163 | preview_width_field = None |
|
163 | preview_width_field = None | |
164 | preview_height_field = None |
|
164 | preview_height_field = None | |
165 |
|
165 | |||
166 | def __init__(self, verbose_name=None, name=None, width_field=None, |
|
166 | def __init__(self, verbose_name=None, name=None, width_field=None, | |
167 | height_field=None, sizes=None, |
|
167 | height_field=None, sizes=None, | |
168 | preview_width_field=None, preview_height_field=None, |
|
168 | preview_width_field=None, preview_height_field=None, | |
169 | **kwargs): |
|
169 | **kwargs): | |
170 | self.verbose_name = verbose_name |
|
170 | self.verbose_name = verbose_name | |
171 | self.name = name |
|
171 | self.name = name | |
172 | self.width_field = width_field |
|
172 | self.width_field = width_field | |
173 | self.height_field = height_field |
|
173 | self.height_field = height_field | |
174 | self.sizes = sizes |
|
174 | self.sizes = sizes | |
175 | super(ImageField, self).__init__(**kwargs) |
|
175 | super(ImageField, self).__init__(**kwargs) | |
176 |
|
176 | |||
177 | if sizes is not None and len(sizes) == 1: |
|
177 | if sizes is not None and len(sizes) == 1: | |
178 | self.preview_width_field = preview_width_field |
|
178 | self.preview_width_field = preview_width_field | |
179 | self.preview_height_field = preview_height_field |
|
179 | self.preview_height_field = preview_height_field | |
180 |
|
180 | |||
181 | def update_dimension_fields(self, instance, force=False, *args, **kwargs): |
|
181 | def update_dimension_fields(self, instance, force=False, *args, **kwargs): | |
182 | """ |
|
182 | """ | |
183 | Update original image dimension fields and thumb dimension fields |
|
183 | Update original image dimension fields and thumb dimension fields | |
184 | (only if 1 thumb size is defined) |
|
184 | (only if 1 thumb size is defined) | |
185 | """ |
|
185 | """ | |
186 |
|
186 | |||
187 | super(ImageWithThumbsField, self).update_dimension_fields(instance, |
|
187 | super(ImageWithThumbsField, self).update_dimension_fields(instance, | |
188 | force, *args, |
|
188 | force, *args, | |
189 | **kwargs) |
|
189 | **kwargs) | |
190 | thumb_width_field = self.preview_width_field |
|
190 | thumb_width_field = self.preview_width_field | |
191 | thumb_height_field = self.preview_height_field |
|
191 | thumb_height_field = self.preview_height_field | |
192 |
|
192 | |||
193 | if thumb_width_field is None or thumb_height_field is None \ |
|
193 | if thumb_width_field is None or thumb_height_field is None \ | |
194 | or len(self.sizes) != 1: |
|
194 | or len(self.sizes) != 1: | |
195 | return |
|
195 | return | |
196 |
|
196 | |||
197 | original_width = getattr(instance, self.width_field) |
|
197 | original_width = getattr(instance, self.width_field) | |
198 | original_height = getattr(instance, self.height_field) |
|
198 | original_height = getattr(instance, self.height_field) | |
199 |
|
199 | |||
200 | if original_width > 0 and original_height > 0: |
|
200 | if original_width > 0 and original_height > 0: | |
201 | thumb_width, thumb_height = self.sizes[0] |
|
201 | thumb_width, thumb_height = self.sizes[0] | |
202 |
|
202 | |||
203 | w_scale = float(thumb_width) / original_width |
|
203 | w_scale = float(thumb_width) / original_width | |
204 | h_scale = float(thumb_height) / original_height |
|
204 | h_scale = float(thumb_height) / original_height | |
205 | scale_ratio = min(w_scale, h_scale) |
|
205 | scale_ratio = min(w_scale, h_scale) | |
206 |
|
206 | |||
207 | if scale_ratio >= 1: |
|
207 | if scale_ratio >= 1: | |
208 | thumb_width_ratio = original_width |
|
208 | thumb_width_ratio = original_width | |
209 | thumb_height_ratio = original_height |
|
209 | thumb_height_ratio = original_height | |
210 | else: |
|
210 | else: | |
211 | thumb_width_ratio = int(original_width * scale_ratio) |
|
211 | thumb_width_ratio = int(original_width * scale_ratio) | |
212 | thumb_height_ratio = int(original_height * scale_ratio) |
|
212 | thumb_height_ratio = int(original_height * scale_ratio) | |
213 |
|
213 | |||
214 | setattr(instance, thumb_width_field, thumb_width_ratio) |
|
214 | setattr(instance, thumb_width_field, thumb_width_ratio) | |
215 |
setattr(instance, thumb_height_field, thumb_height_ratio) |
|
215 | setattr(instance, thumb_height_field, thumb_height_ratio) No newline at end of file | |
216 |
|
||||
217 |
|
||||
218 | from south.modelsinspector import add_introspection_rules |
|
|||
219 | add_introspection_rules([], ["^boards\.thumbs\.ImageWithThumbsField"]) |
|
@@ -1,87 +1,79 b'' | |||||
1 | from django.conf.urls import patterns, url, include |
|
1 | from django.conf.urls import patterns, url, include | |
2 | from django.contrib import admin |
|
2 | from django.contrib import admin | |
3 | from boards import views |
|
3 | from boards import views | |
4 | from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed |
|
4 | from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed | |
5 | from boards.views import api, tag_threads, all_threads, \ |
|
5 | from boards.views import api, tag_threads, all_threads, \ | |
6 | settings, all_tags |
|
6 | settings, all_tags | |
7 | from boards.views.authors import AuthorsView |
|
7 | from boards.views.authors import AuthorsView | |
8 | from boards.views.delete_post import DeletePostView |
|
|||
9 | from boards.views.ban import BanUserView |
|
8 | from boards.views.ban import BanUserView | |
10 | from boards.views.search import BoardSearchView |
|
9 | from boards.views.search import BoardSearchView | |
11 | from boards.views.static import StaticPageView |
|
10 | from boards.views.static import StaticPageView | |
12 | from boards.views.post_admin import PostAdminView |
|
|||
13 | from boards.views.preview import PostPreviewView |
|
11 | from boards.views.preview import PostPreviewView | |
14 | from boards.views.sync import get_post_sync_data |
|
12 | from boards.views.sync import get_post_sync_data | |
15 |
|
13 | |||
16 | js_info_dict = { |
|
14 | js_info_dict = { | |
17 | 'packages': ('boards',), |
|
15 | 'packages': ('boards',), | |
18 | } |
|
16 | } | |
19 |
|
17 | |||
20 | urlpatterns = patterns('', |
|
18 | urlpatterns = patterns('', | |
21 | # /boards/ |
|
19 | # /boards/ | |
22 | url(r'^$', all_threads.AllThreadsView.as_view(), name='index'), |
|
20 | url(r'^$', all_threads.AllThreadsView.as_view(), name='index'), | |
23 | # /boards/page/ |
|
21 | # /boards/page/ | |
24 | url(r'^page/(?P<page>\w+)/$', all_threads.AllThreadsView.as_view(), |
|
22 | url(r'^page/(?P<page>\w+)/$', all_threads.AllThreadsView.as_view(), | |
25 | name='index'), |
|
23 | name='index'), | |
26 |
|
24 | |||
27 | # /boards/tag/tag_name/ |
|
25 | # /boards/tag/tag_name/ | |
28 | url(r'^tag/(?P<tag_name>\w+)/$', tag_threads.TagView.as_view(), |
|
26 | url(r'^tag/(?P<tag_name>\w+)/$', tag_threads.TagView.as_view(), | |
29 | name='tag'), |
|
27 | name='tag'), | |
30 | # /boards/tag/tag_id/page/ |
|
28 | # /boards/tag/tag_id/page/ | |
31 | url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/$', |
|
29 | url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/$', | |
32 | tag_threads.TagView.as_view(), name='tag'), |
|
30 | tag_threads.TagView.as_view(), name='tag'), | |
33 |
|
31 | |||
34 | # /boards/thread/ |
|
32 | # /boards/thread/ | |
35 | url(r'^thread/(?P<post_id>\w+)/$', views.thread.ThreadView.as_view(), |
|
33 | url(r'^thread/(?P<post_id>\w+)/$', views.thread.ThreadView.as_view(), | |
36 | name='thread'), |
|
34 | name='thread'), | |
37 | url(r'^thread/(?P<post_id>\w+)/mode/(?P<mode>\w+)/$', views.thread.ThreadView |
|
35 | url(r'^thread/(?P<post_id>\w+)/mode/(?P<mode>\w+)/$', views.thread.ThreadView | |
38 | .as_view(), name='thread_mode'), |
|
36 | .as_view(), name='thread_mode'), | |
39 |
|
37 | |||
40 | # /boards/post_admin/ |
|
|||
41 | url(r'^post_admin/(?P<post_id>\w+)/$', PostAdminView.as_view(), |
|
|||
42 | name='post_admin'), |
|
|||
43 |
|
||||
44 | url(r'^settings/$', settings.SettingsView.as_view(), name='settings'), |
|
38 | url(r'^settings/$', settings.SettingsView.as_view(), name='settings'), | |
45 | url(r'^tags/$', all_tags.AllTagsView.as_view(), name='tags'), |
|
39 | url(r'^tags/$', all_tags.AllTagsView.as_view(), name='tags'), | |
46 | url(r'^authors/$', AuthorsView.as_view(), name='authors'), |
|
40 | url(r'^authors/$', AuthorsView.as_view(), name='authors'), | |
47 | url(r'^delete/(?P<post_id>\w+)/$', DeletePostView.as_view(), |
|
|||
48 | name='delete'), |
|
|||
49 | url(r'^ban/(?P<post_id>\w+)/$', BanUserView.as_view(), name='ban'), |
|
41 | url(r'^ban/(?P<post_id>\w+)/$', BanUserView.as_view(), name='ban'), | |
50 |
|
42 | |||
51 | url(r'^banned/$', views.banned.BannedView.as_view(), name='banned'), |
|
43 | url(r'^banned/$', views.banned.BannedView.as_view(), name='banned'), | |
52 | url(r'^staticpage/(?P<name>\w+)/$', StaticPageView.as_view(), |
|
44 | url(r'^staticpage/(?P<name>\w+)/$', StaticPageView.as_view(), | |
53 | name='staticpage'), |
|
45 | name='staticpage'), | |
54 |
|
46 | |||
55 | # RSS feeds |
|
47 | # RSS feeds | |
56 | url(r'^rss/$', AllThreadsFeed()), |
|
48 | url(r'^rss/$', AllThreadsFeed()), | |
57 | url(r'^page/(?P<page>\w+)/rss/$', AllThreadsFeed()), |
|
49 | url(r'^page/(?P<page>\w+)/rss/$', AllThreadsFeed()), | |
58 | url(r'^tag/(?P<tag_name>\w+)/rss/$', TagThreadsFeed()), |
|
50 | url(r'^tag/(?P<tag_name>\w+)/rss/$', TagThreadsFeed()), | |
59 | url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/rss/$', TagThreadsFeed()), |
|
51 | url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/rss/$', TagThreadsFeed()), | |
60 | url(r'^thread/(?P<post_id>\w+)/rss/$', ThreadPostsFeed()), |
|
52 | url(r'^thread/(?P<post_id>\w+)/rss/$', ThreadPostsFeed()), | |
61 |
|
53 | |||
62 | # i18n |
|
54 | # i18n | |
63 | url(r'^jsi18n/$', 'boards.views.cached_js_catalog', js_info_dict, |
|
55 | url(r'^jsi18n/$', 'boards.views.cached_js_catalog', js_info_dict, | |
64 | name='js_info_dict'), |
|
56 | name='js_info_dict'), | |
65 |
|
57 | |||
66 | # API |
|
58 | # API | |
67 | url(r'^api/post/(?P<post_id>\w+)/$', api.get_post, name="get_post"), |
|
59 | url(r'^api/post/(?P<post_id>\w+)/$', api.get_post, name="get_post"), | |
68 | url(r'^api/diff_thread/(?P<thread_id>\w+)/(?P<last_update_time>\w+)/$', |
|
60 | url(r'^api/diff_thread/(?P<thread_id>\w+)/(?P<last_update_time>\w+)/$', | |
69 | api.api_get_threaddiff, name="get_thread_diff"), |
|
61 | api.api_get_threaddiff, name="get_thread_diff"), | |
70 | url(r'^api/threads/(?P<count>\w+)/$', api.api_get_threads, |
|
62 | url(r'^api/threads/(?P<count>\w+)/$', api.api_get_threads, | |
71 | name='get_threads'), |
|
63 | name='get_threads'), | |
72 | url(r'^api/tags/$', api.api_get_tags, name='get_tags'), |
|
64 | url(r'^api/tags/$', api.api_get_tags, name='get_tags'), | |
73 | url(r'^api/thread/(?P<opening_post_id>\w+)/$', api.api_get_thread_posts, |
|
65 | url(r'^api/thread/(?P<opening_post_id>\w+)/$', api.api_get_thread_posts, | |
74 | name='get_thread'), |
|
66 | name='get_thread'), | |
75 | url(r'^api/add_post/(?P<opening_post_id>\w+)/$', api.api_add_post, |
|
67 | url(r'^api/add_post/(?P<opening_post_id>\w+)/$', api.api_add_post, | |
76 | name='add_post'), |
|
68 | name='add_post'), | |
77 |
|
69 | |||
78 | # Search |
|
70 | # Search | |
79 | url(r'^search/$', BoardSearchView.as_view(), name='search'), |
|
71 | url(r'^search/$', BoardSearchView.as_view(), name='search'), | |
80 |
|
72 | |||
81 | # Post preview |
|
73 | # Post preview | |
82 | url(r'^preview/$', PostPreviewView.as_view(), name='preview'), |
|
74 | url(r'^preview/$', PostPreviewView.as_view(), name='preview'), | |
83 |
|
75 | |||
84 | url(r'^post_xml/(?P<post_id>\d+)$', get_post_sync_data, |
|
76 | url(r'^post_xml/(?P<post_id>\d+)$', get_post_sync_data, | |
85 | name='post_sync_data'), |
|
77 | name='post_sync_data'), | |
86 |
|
78 | |||
87 | ) |
|
79 | ) |
@@ -1,78 +1,43 b'' | |||||
1 | """ |
|
1 | """ | |
2 | This module contains helper functions and helper classes. |
|
2 | This module contains helper functions and helper classes. | |
3 | """ |
|
3 | """ | |
4 | import hashlib |
|
|||
5 | import time |
|
4 | import time | |
|
5 | import hmac | |||
6 |
|
6 | |||
7 | from django.utils import timezone |
|
7 | from django.utils import timezone | |
8 |
|
8 | |||
9 | from neboard import settings |
|
9 | from neboard import settings | |
10 |
|
10 | |||
11 |
|
11 | |||
12 | KEY_CAPTCHA_FAILS = 'key_captcha_fails' |
|
12 | KEY_CAPTCHA_FAILS = 'key_captcha_fails' | |
13 | KEY_CAPTCHA_DELAY_TIME = 'key_captcha_delay_time' |
|
13 | KEY_CAPTCHA_DELAY_TIME = 'key_captcha_delay_time' | |
14 | KEY_CAPTCHA_LAST_ACTIVITY = 'key_captcha_last_activity' |
|
14 | KEY_CAPTCHA_LAST_ACTIVITY = 'key_captcha_last_activity' | |
15 |
|
15 | |||
16 |
|
16 | |||
17 | def need_include_captcha(request): |
|
|||
18 | """ |
|
|||
19 | Check if request is made by a user. |
|
|||
20 | It contains rules which check for bots. |
|
|||
21 | """ |
|
|||
22 |
|
||||
23 | if not settings.ENABLE_CAPTCHA: |
|
|||
24 | return False |
|
|||
25 |
|
||||
26 | enable_captcha = False |
|
|||
27 |
|
||||
28 | #newcomer |
|
|||
29 | if KEY_CAPTCHA_LAST_ACTIVITY not in request.session: |
|
|||
30 | return settings.ENABLE_CAPTCHA |
|
|||
31 |
|
||||
32 | last_activity = request.session[KEY_CAPTCHA_LAST_ACTIVITY] |
|
|||
33 | current_delay = int(time.time()) - last_activity |
|
|||
34 |
|
||||
35 | delay_time = (request.session[KEY_CAPTCHA_DELAY_TIME] |
|
|||
36 | if KEY_CAPTCHA_DELAY_TIME in request.session |
|
|||
37 | else settings.CAPTCHA_DEFAULT_SAFE_TIME) |
|
|||
38 |
|
||||
39 | if current_delay < delay_time: |
|
|||
40 | enable_captcha = True |
|
|||
41 |
|
||||
42 | return enable_captcha |
|
|||
43 |
|
||||
44 |
|
||||
45 | def update_captcha_access(request, passed): |
|
|||
46 | """ |
|
|||
47 | Update captcha fields. |
|
|||
48 | It will reduce delay time if user passed captcha verification and |
|
|||
49 | it will increase it otherwise. |
|
|||
50 | """ |
|
|||
51 | session = request.session |
|
|||
52 |
|
||||
53 | delay_time = (request.session[KEY_CAPTCHA_DELAY_TIME] |
|
|||
54 | if KEY_CAPTCHA_DELAY_TIME in request.session |
|
|||
55 | else settings.CAPTCHA_DEFAULT_SAFE_TIME) |
|
|||
56 |
|
||||
57 | if passed: |
|
|||
58 | delay_time -= 2 if delay_time >= 7 else 5 |
|
|||
59 | else: |
|
|||
60 | delay_time += 10 |
|
|||
61 |
|
||||
62 | session[KEY_CAPTCHA_LAST_ACTIVITY] = int(time.time()) |
|
|||
63 | session[KEY_CAPTCHA_DELAY_TIME] = delay_time |
|
|||
64 |
|
||||
65 |
|
||||
66 | def get_client_ip(request): |
|
17 | def get_client_ip(request): | |
67 | x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') |
|
18 | x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') | |
68 | if x_forwarded_for: |
|
19 | if x_forwarded_for: | |
69 | ip = x_forwarded_for.split(',')[-1].strip() |
|
20 | ip = x_forwarded_for.split(',')[-1].strip() | |
70 | else: |
|
21 | else: | |
71 | ip = request.META.get('REMOTE_ADDR') |
|
22 | ip = request.META.get('REMOTE_ADDR') | |
72 | return ip |
|
23 | return ip | |
73 |
|
24 | |||
74 |
|
25 | |||
75 | def datetime_to_epoch(datetime): |
|
26 | def datetime_to_epoch(datetime): | |
76 | return int(time.mktime(timezone.localtime( |
|
27 | return int(time.mktime(timezone.localtime( | |
77 | datetime,timezone.get_current_timezone()).timetuple()) |
|
28 | datetime,timezone.get_current_timezone()).timetuple()) | |
78 | * 1000000 + datetime.microsecond) |
|
29 | * 1000000 + datetime.microsecond) | |
|
30 | ||||
|
31 | ||||
|
32 | def get_websocket_token(user_id='', timestamp=''): | |||
|
33 | """ | |||
|
34 | Create token to validate information provided by new connection. | |||
|
35 | """ | |||
|
36 | ||||
|
37 | sign = hmac.new(settings.CENTRIFUGE_PROJECT_SECRET.encode()) | |||
|
38 | sign.update(settings.CENTRIFUGE_PROJECT_ID.encode()) | |||
|
39 | sign.update(user_id.encode()) | |||
|
40 | sign.update(timestamp.encode()) | |||
|
41 | token = sign.hexdigest() | |||
|
42 | ||||
|
43 | return token No newline at end of file |
@@ -1,13 +1,14 b'' | |||||
1 | from django.shortcuts import render |
|
1 | from django.shortcuts import render | |
2 |
|
2 | |||
3 | from boards.views.base import BaseBoardView |
|
3 | from boards.views.base import BaseBoardView | |
4 | from boards.models.tag import Tag |
|
4 | from boards.models.tag import Tag | |
5 |
|
5 | |||
6 |
|
6 | |||
7 | class AllTagsView(BaseBoardView): |
|
7 | class AllTagsView(BaseBoardView): | |
8 |
|
8 | |||
9 | def get(self, request): |
|
9 | def get(self, request): | |
10 | context = self.get_context_data(request=request) |
|
10 | params = dict() | |
11 | context['all_tags'] = Tag.objects.get_not_empty_tags() |
|
|||
12 |
|
11 | |||
13 | return render(request, 'boards/tags.html', context) |
|
12 | params['all_tags'] = Tag.objects.get_not_empty_tags() | |
|
13 | ||||
|
14 | return render(request, 'boards/tags.html', params) |
@@ -1,139 +1,137 b'' | |||||
1 | import string |
|
|||
2 |
|
||||
3 |
|
|
1 | from django.db import transaction | |
4 | from django.shortcuts import render, redirect |
|
2 | from django.shortcuts import render, redirect | |
5 |
|
3 | |||
6 | from boards import utils, settings |
|
4 | from boards import utils, settings | |
7 | from boards.abstracts.paginator import get_paginator |
|
5 | from boards.abstracts.paginator import get_paginator | |
8 | from boards.abstracts.settingsmanager import get_settings_manager |
|
6 | from boards.abstracts.settingsmanager import get_settings_manager | |
9 | from boards.forms import ThreadForm, PlainErrorList |
|
7 | from boards.forms import ThreadForm, PlainErrorList | |
10 | from boards.models import Post, Thread, Ban, Tag |
|
8 | from boards.models import Post, Thread, Ban, Tag | |
11 | from boards.views.banned import BannedView |
|
9 | from boards.views.banned import BannedView | |
12 | from boards.views.base import BaseBoardView, CONTEXT_FORM |
|
10 | from boards.views.base import BaseBoardView, CONTEXT_FORM | |
13 | from boards.views.posting_mixin import PostMixin |
|
11 | from boards.views.posting_mixin import PostMixin | |
14 |
|
12 | |||
|
13 | ||||
15 | FORM_TAGS = 'tags' |
|
14 | FORM_TAGS = 'tags' | |
16 | FORM_TEXT = 'text' |
|
15 | FORM_TEXT = 'text' | |
17 | FORM_TITLE = 'title' |
|
16 | FORM_TITLE = 'title' | |
18 | FORM_IMAGE = 'image' |
|
17 | FORM_IMAGE = 'image' | |
19 |
|
18 | |||
20 | TAG_DELIMITER = ' ' |
|
19 | TAG_DELIMITER = ' ' | |
21 |
|
20 | |||
22 | PARAMETER_CURRENT_PAGE = 'current_page' |
|
21 | PARAMETER_CURRENT_PAGE = 'current_page' | |
23 | PARAMETER_PAGINATOR = 'paginator' |
|
22 | PARAMETER_PAGINATOR = 'paginator' | |
24 | PARAMETER_THREADS = 'threads' |
|
23 | PARAMETER_THREADS = 'threads' | |
25 |
|
24 | |||
26 | TEMPLATE = 'boards/posting_general.html' |
|
25 | TEMPLATE = 'boards/posting_general.html' | |
27 | DEFAULT_PAGE = 1 |
|
26 | DEFAULT_PAGE = 1 | |
28 |
|
27 | |||
29 |
|
28 | |||
30 | class AllThreadsView(PostMixin, BaseBoardView): |
|
29 | class AllThreadsView(PostMixin, BaseBoardView): | |
31 |
|
30 | |||
32 | def __init__(self): |
|
31 | def __init__(self): | |
33 | self.settings_manager = None |
|
32 | self.settings_manager = None | |
34 | super(AllThreadsView, self).__init__() |
|
33 | super(AllThreadsView, self).__init__() | |
35 |
|
34 | |||
36 | def get(self, request, page=DEFAULT_PAGE, form=None): |
|
35 | def get(self, request, page=DEFAULT_PAGE, form=None): | |
37 |
|
|
36 | params = self.get_context_data(request=request) | |
38 |
|
37 | |||
39 | if not form: |
|
38 | if not form: | |
40 | form = ThreadForm(error_class=PlainErrorList) |
|
39 | form = ThreadForm(error_class=PlainErrorList) | |
41 |
|
40 | |||
42 | self.settings_manager = get_settings_manager(request) |
|
41 | self.settings_manager = get_settings_manager(request) | |
43 | paginator = get_paginator(self.get_threads(), |
|
42 | paginator = get_paginator(self.get_threads(), | |
44 | settings.THREADS_PER_PAGE) |
|
43 | settings.THREADS_PER_PAGE) | |
45 | paginator.current_page = int(page) |
|
44 | paginator.current_page = int(page) | |
46 |
|
45 | |||
47 | threads = paginator.page(page).object_list |
|
46 | threads = paginator.page(page).object_list | |
48 |
|
47 | |||
49 |
|
|
48 | params[PARAMETER_THREADS] = threads | |
50 |
|
|
49 | params[CONTEXT_FORM] = form | |
51 |
|
50 | |||
52 |
self._get_page_context(paginator, |
|
51 | self._get_page_context(paginator, params, page) | |
53 |
|
52 | |||
54 |
return render(request, TEMPLATE, |
|
53 | return render(request, TEMPLATE, params) | |
55 |
|
54 | |||
56 | def post(self, request, page=DEFAULT_PAGE): |
|
55 | def post(self, request, page=DEFAULT_PAGE): | |
57 | form = ThreadForm(request.POST, request.FILES, |
|
56 | form = ThreadForm(request.POST, request.FILES, | |
58 | error_class=PlainErrorList) |
|
57 | error_class=PlainErrorList) | |
59 | form.session = request.session |
|
58 | form.session = request.session | |
60 |
|
59 | |||
61 | if form.is_valid(): |
|
60 | if form.is_valid(): | |
62 | return self.create_thread(request, form) |
|
61 | return self.create_thread(request, form) | |
63 | if form.need_to_ban: |
|
62 | if form.need_to_ban: | |
64 | # Ban user because he is suspected to be a bot |
|
63 | # Ban user because he is suspected to be a bot | |
65 | self._ban_current_user(request) |
|
64 | self._ban_current_user(request) | |
66 |
|
65 | |||
67 | return self.get(request, page, form) |
|
66 | return self.get(request, page, form) | |
68 |
|
67 | |||
69 | @staticmethod |
|
68 | def _get_page_context(self, paginator, params, page): | |
70 | def _get_page_context(paginator, context, page): |
|
|||
71 | """ |
|
69 | """ | |
72 | Get pagination context variables |
|
70 | Get pagination context variables | |
73 | """ |
|
71 | """ | |
74 |
|
72 | |||
75 |
|
|
73 | params[PARAMETER_PAGINATOR] = paginator | |
76 |
|
|
74 | params[PARAMETER_CURRENT_PAGE] = paginator.page(int(page)) | |
77 |
|
75 | |||
78 | @staticmethod |
|
76 | @staticmethod | |
79 | def parse_tags_string(tag_strings): |
|
77 | def parse_tags_string(tag_strings): | |
80 | """ |
|
78 | """ | |
81 | Parses tag list string and returns tag object list. |
|
79 | Parses tag list string and returns tag object list. | |
82 | """ |
|
80 | """ | |
83 |
|
81 | |||
84 | tags = [] |
|
82 | tags = [] | |
85 |
|
83 | |||
86 | if tag_strings: |
|
84 | if tag_strings: | |
87 | tag_strings = tag_strings.split(TAG_DELIMITER) |
|
85 | tag_strings = tag_strings.split(TAG_DELIMITER) | |
88 | for tag_name in tag_strings: |
|
86 | for tag_name in tag_strings: | |
89 | tag_name = tag_name.strip().lower() |
|
87 | tag_name = tag_name.strip().lower() | |
90 | if len(tag_name) > 0: |
|
88 | if len(tag_name) > 0: | |
91 | tag, created = Tag.objects.get_or_create(name=tag_name) |
|
89 | tag, created = Tag.objects.get_or_create(name=tag_name) | |
92 | tags.append(tag) |
|
90 | tags.append(tag) | |
93 |
|
91 | |||
94 | return tags |
|
92 | return tags | |
95 |
|
93 | |||
96 | @transaction.atomic |
|
94 | @transaction.atomic | |
97 | def create_thread(self, request, form, html_response=True): |
|
95 | def create_thread(self, request, form, html_response=True): | |
98 | """ |
|
96 | """ | |
99 | Creates a new thread with an opening post. |
|
97 | Creates a new thread with an opening post. | |
100 | """ |
|
98 | """ | |
101 |
|
99 | |||
102 | ip = utils.get_client_ip(request) |
|
100 | ip = utils.get_client_ip(request) | |
103 | is_banned = Ban.objects.filter(ip=ip).exists() |
|
101 | is_banned = Ban.objects.filter(ip=ip).exists() | |
104 |
|
102 | |||
105 | if is_banned: |
|
103 | if is_banned: | |
106 | if html_response: |
|
104 | if html_response: | |
107 | return redirect(BannedView().as_view()) |
|
105 | return redirect(BannedView().as_view()) | |
108 | else: |
|
106 | else: | |
109 | return |
|
107 | return | |
110 |
|
108 | |||
111 | data = form.cleaned_data |
|
109 | data = form.cleaned_data | |
112 |
|
110 | |||
113 | title = data[FORM_TITLE] |
|
111 | title = data[FORM_TITLE] | |
114 | text = data[FORM_TEXT] |
|
112 | text = data[FORM_TEXT] | |
|
113 | image = data.get(FORM_IMAGE) | |||
115 |
|
114 | |||
116 | text = self._remove_invalid_links(text) |
|
115 | text = self._remove_invalid_links(text) | |
117 |
|
116 | |||
118 | if FORM_IMAGE in list(data.keys()): |
|
|||
119 | image = data[FORM_IMAGE] |
|
|||
120 | else: |
|
|||
121 | image = None |
|
|||
122 |
|
||||
123 | tag_strings = data[FORM_TAGS] |
|
117 | tag_strings = data[FORM_TAGS] | |
124 |
|
118 | |||
125 | tags = self.parse_tags_string(tag_strings) |
|
119 | tags = self.parse_tags_string(tag_strings) | |
126 |
|
120 | |||
127 | post = Post.objects.create_post(title=title, text=text, image=image, |
|
121 | post = Post.objects.create_post(title=title, text=text, image=image, | |
128 | ip=ip, tags=tags) |
|
122 | ip=ip, tags=tags) | |
129 |
|
123 | |||
|
124 | # This is required to update the threads to which posts we have replied | |||
|
125 | # when creating this one | |||
|
126 | post.send_to_websocket(request) | |||
|
127 | ||||
130 | if html_response: |
|
128 | if html_response: | |
131 | return redirect(post.get_url()) |
|
129 | return redirect(post.get_url()) | |
132 |
|
130 | |||
133 | def get_threads(self): |
|
131 | def get_threads(self): | |
134 | """ |
|
132 | """ | |
135 | Gets list of threads that will be shown on a page. |
|
133 | Gets list of threads that will be shown on a page. | |
136 | """ |
|
134 | """ | |
137 |
|
135 | |||
138 | return Thread.objects.all().order_by('-bump_time')\ |
|
136 | return Thread.objects.all().order_by('-bump_time')\ | |
139 | .exclude(tags__in=self.settings_manager.get_hidden_tags()) |
|
137 | .exclude(tags__in=self.settings_manager.get_hidden_tags()) |
@@ -1,248 +1,223 b'' | |||||
1 | from datetime import datetime |
|
1 | from datetime import datetime | |
2 | import json |
|
2 | import json | |
3 | import logging |
|
3 | import logging | |
4 | from django.db import transaction |
|
4 | from django.db import transaction | |
5 | from django.http import HttpResponse |
|
5 | from django.http import HttpResponse | |
6 | from django.shortcuts import get_object_or_404, render |
|
6 | from django.shortcuts import get_object_or_404, render | |
7 | from django.template import RequestContext |
|
7 | from django.template import RequestContext | |
8 | from django.utils import timezone |
|
8 | from django.utils import timezone | |
9 | from django.core import serializers |
|
9 | from django.core import serializers | |
10 | from django.template.loader import render_to_string |
|
|||
11 |
|
10 | |||
12 | from boards.forms import PostForm, PlainErrorList |
|
11 | from boards.forms import PostForm, PlainErrorList | |
13 | from boards.models import Post, Thread, Tag |
|
12 | from boards.models import Post, Thread, Tag | |
14 | from boards.utils import datetime_to_epoch |
|
13 | from boards.utils import datetime_to_epoch | |
15 | from boards.views.thread import ThreadView |
|
14 | from boards.views.thread import ThreadView | |
16 |
|
15 | |||
17 | __author__ = 'neko259' |
|
16 | __author__ = 'neko259' | |
18 |
|
17 | |||
19 | PARAMETER_TRUNCATED = 'truncated' |
|
18 | PARAMETER_TRUNCATED = 'truncated' | |
20 | PARAMETER_TAG = 'tag' |
|
19 | PARAMETER_TAG = 'tag' | |
21 | PARAMETER_OFFSET = 'offset' |
|
20 | PARAMETER_OFFSET = 'offset' | |
22 | PARAMETER_DIFF_TYPE = 'type' |
|
21 | PARAMETER_DIFF_TYPE = 'type' | |
23 |
|
22 | |||
24 | DIFF_TYPE_HTML = 'html' |
|
23 | DIFF_TYPE_HTML = 'html' | |
25 | DIFF_TYPE_JSON = 'json' |
|
24 | DIFF_TYPE_JSON = 'json' | |
26 |
|
25 | |||
27 | STATUS_OK = 'ok' |
|
26 | STATUS_OK = 'ok' | |
28 | STATUS_ERROR = 'error' |
|
27 | STATUS_ERROR = 'error' | |
29 |
|
28 | |||
30 | logger = logging.getLogger(__name__) |
|
29 | logger = logging.getLogger(__name__) | |
31 |
|
30 | |||
32 |
|
31 | |||
33 | @transaction.atomic |
|
32 | @transaction.atomic | |
34 | def api_get_threaddiff(request, thread_id, last_update_time): |
|
33 | def api_get_threaddiff(request, thread_id, last_update_time): | |
35 | """ |
|
34 | """ | |
36 | Gets posts that were changed or added since time |
|
35 | Gets posts that were changed or added since time | |
37 | """ |
|
36 | """ | |
38 |
|
37 | |||
39 | thread = get_object_or_404(Post, id=thread_id).get_thread() |
|
38 | thread = get_object_or_404(Post, id=thread_id).get_thread() | |
40 |
|
39 | |||
41 | # Add 1 to ensure we don't load the same post over and over |
|
40 | # Add 1 to ensure we don't load the same post over and over | |
42 | last_update_timestamp = float(last_update_time) + 1 |
|
41 | last_update_timestamp = float(last_update_time) + 1 | |
43 |
|
42 | |||
44 | filter_time = datetime.fromtimestamp(last_update_timestamp / 1000000, |
|
43 | filter_time = datetime.fromtimestamp(last_update_timestamp / 1000000, | |
45 | timezone.get_current_timezone()) |
|
44 | timezone.get_current_timezone()) | |
46 |
|
45 | |||
47 | json_data = { |
|
46 | json_data = { | |
48 | 'added': [], |
|
47 | 'added': [], | |
49 | 'updated': [], |
|
48 | 'updated': [], | |
50 | 'last_update': None, |
|
49 | 'last_update': None, | |
51 | } |
|
50 | } | |
52 | added_posts = Post.objects.filter(thread_new=thread, |
|
51 | added_posts = Post.objects.filter(thread_new=thread, | |
53 | pub_time__gt=filter_time) \ |
|
52 | pub_time__gt=filter_time) \ | |
54 | .order_by('pub_time') |
|
53 | .order_by('pub_time') | |
55 | updated_posts = Post.objects.filter(thread_new=thread, |
|
54 | updated_posts = Post.objects.filter(thread_new=thread, | |
56 | pub_time__lte=filter_time, |
|
55 | pub_time__lte=filter_time, | |
57 | last_edit_time__gt=filter_time) |
|
56 | last_edit_time__gt=filter_time) | |
58 |
|
57 | |||
59 | diff_type = DIFF_TYPE_HTML |
|
58 | diff_type = request.GET.get(PARAMETER_DIFF_TYPE, DIFF_TYPE_HTML) | |
60 | if PARAMETER_DIFF_TYPE in request.GET: |
|
|||
61 | diff_type = request.GET[PARAMETER_DIFF_TYPE] |
|
|||
62 |
|
59 | |||
63 | for post in added_posts: |
|
60 | for post in added_posts: | |
64 |
json_data['added'].append( |
|
61 | json_data['added'].append(get_post_data(post.id, diff_type, request)) | |
65 | for post in updated_posts: |
|
62 | for post in updated_posts: | |
66 |
json_data['updated'].append( |
|
63 | json_data['updated'].append(get_post_data(post.id, diff_type, request)) | |
67 | json_data['last_update'] = datetime_to_epoch(thread.last_edit_time) |
|
64 | json_data['last_update'] = datetime_to_epoch(thread.last_edit_time) | |
68 |
|
65 | |||
69 | return HttpResponse(content=json.dumps(json_data)) |
|
66 | return HttpResponse(content=json.dumps(json_data)) | |
70 |
|
67 | |||
71 |
|
68 | |||
72 | def api_add_post(request, opening_post_id): |
|
69 | def api_add_post(request, opening_post_id): | |
73 | """ |
|
70 | """ | |
74 | Adds a post and return the JSON response for it |
|
71 | Adds a post and return the JSON response for it | |
75 | """ |
|
72 | """ | |
76 |
|
73 | |||
77 | opening_post = get_object_or_404(Post, id=opening_post_id) |
|
74 | opening_post = get_object_or_404(Post, id=opening_post_id) | |
78 |
|
75 | |||
79 | logger.info('Adding post via api...') |
|
76 | logger.info('Adding post via api...') | |
80 |
|
77 | |||
81 | status = STATUS_OK |
|
78 | status = STATUS_OK | |
82 | errors = [] |
|
79 | errors = [] | |
83 |
|
80 | |||
84 | if request.method == 'POST': |
|
81 | if request.method == 'POST': | |
85 | form = PostForm(request.POST, request.FILES, error_class=PlainErrorList) |
|
82 | form = PostForm(request.POST, request.FILES, error_class=PlainErrorList) | |
86 | form.session = request.session |
|
83 | form.session = request.session | |
87 |
|
84 | |||
88 | if form.need_to_ban: |
|
85 | if form.need_to_ban: | |
89 | # Ban user because he is suspected to be a bot |
|
86 | # Ban user because he is suspected to be a bot | |
90 | # _ban_current_user(request) |
|
87 | # _ban_current_user(request) | |
91 | status = STATUS_ERROR |
|
88 | status = STATUS_ERROR | |
92 | if form.is_valid(): |
|
89 | if form.is_valid(): | |
93 | post = ThreadView().new_post(request, form, opening_post, |
|
90 | post = ThreadView().new_post(request, form, opening_post, | |
94 | html_response=False) |
|
91 | html_response=False) | |
95 | if not post: |
|
92 | if not post: | |
96 | status = STATUS_ERROR |
|
93 | status = STATUS_ERROR | |
97 | else: |
|
94 | else: | |
98 | logger.info('Added post #%d via api.' % post.id) |
|
95 | logger.info('Added post #%d via api.' % post.id) | |
99 | else: |
|
96 | else: | |
100 | status = STATUS_ERROR |
|
97 | status = STATUS_ERROR | |
101 | errors = form.as_json_errors() |
|
98 | errors = form.as_json_errors() | |
102 |
|
99 | |||
103 | response = { |
|
100 | response = { | |
104 | 'status': status, |
|
101 | 'status': status, | |
105 | 'errors': errors, |
|
102 | 'errors': errors, | |
106 | } |
|
103 | } | |
107 |
|
104 | |||
108 | return HttpResponse(content=json.dumps(response)) |
|
105 | return HttpResponse(content=json.dumps(response)) | |
109 |
|
106 | |||
110 |
|
107 | |||
111 | def get_post(request, post_id): |
|
108 | def get_post(request, post_id): | |
112 | """ |
|
109 | """ | |
113 | Gets the html of a post. Used for popups. Post can be truncated if used |
|
110 | Gets the html of a post. Used for popups. Post can be truncated if used | |
114 | in threads list with 'truncated' get parameter. |
|
111 | in threads list with 'truncated' get parameter. | |
115 | """ |
|
112 | """ | |
116 |
|
113 | |||
117 | logger.info('Getting post #%s' % post_id) |
|
|||
118 |
|
||||
119 | post = get_object_or_404(Post, id=post_id) |
|
114 | post = get_object_or_404(Post, id=post_id) | |
120 |
|
115 | |||
121 | context = RequestContext(request) |
|
116 | context = RequestContext(request) | |
122 | context['post'] = post |
|
117 | context['post'] = post | |
123 | if PARAMETER_TRUNCATED in request.GET: |
|
118 | if PARAMETER_TRUNCATED in request.GET: | |
124 | context[PARAMETER_TRUNCATED] = True |
|
119 | context[PARAMETER_TRUNCATED] = True | |
125 |
|
120 | |||
126 | return render(request, 'boards/api_post.html', context) |
|
121 | # TODO Use dict here | |
|
122 | return render(request, 'boards/api_post.html', context_instance=context) | |||
127 |
|
123 | |||
128 |
|
124 | |||
129 | # TODO Test this |
|
125 | # TODO Test this | |
130 | def api_get_threads(request, count): |
|
126 | def api_get_threads(request, count): | |
131 | """ |
|
127 | """ | |
132 | Gets the JSON thread opening posts list. |
|
128 | Gets the JSON thread opening posts list. | |
133 | Parameters that can be used for filtering: |
|
129 | Parameters that can be used for filtering: | |
134 | tag, offset (from which thread to get results) |
|
130 | tag, offset (from which thread to get results) | |
135 | """ |
|
131 | """ | |
136 |
|
132 | |||
137 | if PARAMETER_TAG in request.GET: |
|
133 | if PARAMETER_TAG in request.GET: | |
138 | tag_name = request.GET[PARAMETER_TAG] |
|
134 | tag_name = request.GET[PARAMETER_TAG] | |
139 | if tag_name is not None: |
|
135 | if tag_name is not None: | |
140 | tag = get_object_or_404(Tag, name=tag_name) |
|
136 | tag = get_object_or_404(Tag, name=tag_name) | |
141 | threads = tag.threads.filter(archived=False) |
|
137 | threads = tag.get_threads().filter(archived=False) | |
142 | else: |
|
138 | else: | |
143 | threads = Thread.objects.filter(archived=False) |
|
139 | threads = Thread.objects.filter(archived=False) | |
144 |
|
140 | |||
145 | if PARAMETER_OFFSET in request.GET: |
|
141 | if PARAMETER_OFFSET in request.GET: | |
146 | offset = request.GET[PARAMETER_OFFSET] |
|
142 | offset = request.GET[PARAMETER_OFFSET] | |
147 | offset = int(offset) if offset is not None else 0 |
|
143 | offset = int(offset) if offset is not None else 0 | |
148 | else: |
|
144 | else: | |
149 | offset = 0 |
|
145 | offset = 0 | |
150 |
|
146 | |||
151 | threads = threads.order_by('-bump_time') |
|
147 | threads = threads.order_by('-bump_time') | |
152 | threads = threads[offset:offset + int(count)] |
|
148 | threads = threads[offset:offset + int(count)] | |
153 |
|
149 | |||
154 | opening_posts = [] |
|
150 | opening_posts = [] | |
155 | for thread in threads: |
|
151 | for thread in threads: | |
156 | opening_post = thread.get_opening_post() |
|
152 | opening_post = thread.get_opening_post() | |
157 |
|
153 | |||
158 | # TODO Add tags, replies and images count |
|
154 | # TODO Add tags, replies and images count | |
159 |
opening_posts.append( |
|
155 | opening_posts.append(get_post_data(opening_post.id, | |
160 | include_last_update=True)) |
|
156 | include_last_update=True)) | |
161 |
|
157 | |||
162 | return HttpResponse(content=json.dumps(opening_posts)) |
|
158 | return HttpResponse(content=json.dumps(opening_posts)) | |
163 |
|
159 | |||
164 |
|
160 | |||
165 | # TODO Test this |
|
161 | # TODO Test this | |
166 | def api_get_tags(request): |
|
162 | def api_get_tags(request): | |
167 | """ |
|
163 | """ | |
168 | Gets all tags or user tags. |
|
164 | Gets all tags or user tags. | |
169 | """ |
|
165 | """ | |
170 |
|
166 | |||
171 | # TODO Get favorite tags for the given user ID |
|
167 | # TODO Get favorite tags for the given user ID | |
172 |
|
168 | |||
173 | tags = Tag.objects.get_not_empty_tags() |
|
169 | tags = Tag.objects.get_not_empty_tags() | |
174 | tag_names = [] |
|
170 | tag_names = [] | |
175 | for tag in tags: |
|
171 | for tag in tags: | |
176 | tag_names.append(tag.name) |
|
172 | tag_names.append(tag.name) | |
177 |
|
173 | |||
178 | return HttpResponse(content=json.dumps(tag_names)) |
|
174 | return HttpResponse(content=json.dumps(tag_names)) | |
179 |
|
175 | |||
180 |
|
176 | |||
181 | # TODO The result can be cached by the thread last update time |
|
177 | # TODO The result can be cached by the thread last update time | |
182 | # TODO Test this |
|
178 | # TODO Test this | |
183 | def api_get_thread_posts(request, opening_post_id): |
|
179 | def api_get_thread_posts(request, opening_post_id): | |
184 | """ |
|
180 | """ | |
185 | Gets the JSON array of thread posts |
|
181 | Gets the JSON array of thread posts | |
186 | """ |
|
182 | """ | |
187 |
|
183 | |||
188 | opening_post = get_object_or_404(Post, id=opening_post_id) |
|
184 | opening_post = get_object_or_404(Post, id=opening_post_id) | |
189 | thread = opening_post.get_thread() |
|
185 | thread = opening_post.get_thread() | |
190 | posts = thread.get_replies() |
|
186 | posts = thread.get_replies() | |
191 |
|
187 | |||
192 | json_data = { |
|
188 | json_data = { | |
193 | 'posts': [], |
|
189 | 'posts': [], | |
194 | 'last_update': None, |
|
190 | 'last_update': None, | |
195 | } |
|
191 | } | |
196 | json_post_list = [] |
|
192 | json_post_list = [] | |
197 |
|
193 | |||
198 | for post in posts: |
|
194 | for post in posts: | |
199 |
json_post_list.append( |
|
195 | json_post_list.append(get_post_data(post.id)) | |
200 | json_data['last_update'] = datetime_to_epoch(thread.last_edit_time) |
|
196 | json_data['last_update'] = datetime_to_epoch(thread.last_edit_time) | |
201 | json_data['posts'] = json_post_list |
|
197 | json_data['posts'] = json_post_list | |
202 |
|
198 | |||
203 | return HttpResponse(content=json.dumps(json_data)) |
|
199 | return HttpResponse(content=json.dumps(json_data)) | |
204 |
|
200 | |||
205 |
|
201 | |||
206 | def api_get_post(request, post_id): |
|
202 | def api_get_post(request, post_id): | |
207 | """ |
|
203 | """ | |
208 | Gets the JSON of a post. This can be |
|
204 | Gets the JSON of a post. This can be | |
209 | used as and API for external clients. |
|
205 | used as and API for external clients. | |
210 | """ |
|
206 | """ | |
211 |
|
207 | |||
212 | post = get_object_or_404(Post, id=post_id) |
|
208 | post = get_object_or_404(Post, id=post_id) | |
213 |
|
209 | |||
214 | json = serializers.serialize("json", [post], fields=( |
|
210 | json = serializers.serialize("json", [post], fields=( | |
215 | "pub_time", "_text_rendered", "title", "text", "image", |
|
211 | "pub_time", "_text_rendered", "title", "text", "image", | |
216 | "image_width", "image_height", "replies", "tags" |
|
212 | "image_width", "image_height", "replies", "tags" | |
217 | )) |
|
213 | )) | |
218 |
|
214 | |||
219 | return HttpResponse(content=json) |
|
215 | return HttpResponse(content=json) | |
220 |
|
216 | |||
221 |
|
217 | |||
222 | # TODO Add pub time and replies |
|
218 | # TODO Remove this method and use post method directly | |
223 |
def |
|
219 | def get_post_data(post_id, format_type=DIFF_TYPE_JSON, request=None, | |
224 | include_last_update=False): |
|
220 | include_last_update=False): | |
225 | if format_type == DIFF_TYPE_HTML: |
|
|||
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 |
|
|
221 | post = get_object_or_404(Post, id=post_id) | |
236 | post_json = { |
|
222 | return post.get_post_data(format_type=format_type, request=request, | |
237 | 'id': post.id, |
|
223 | include_last_update=include_last_update) | |
238 | 'title': post.title, |
|
|||
239 | 'text': post.text.rendered, |
|
|||
240 | } |
|
|||
241 | if post.images.exists(): |
|
|||
242 | post_image = post.get_first_image() |
|
|||
243 | post_json['image'] = post_image.image.url |
|
|||
244 | post_json['image_preview'] = post_image.image.url_200x150 |
|
|||
245 | if include_last_update: |
|
|||
246 | post_json['bump_time'] = datetime_to_epoch( |
|
|||
247 | post.thread_new.bump_time) |
|
|||
248 | return post_json |
|
@@ -1,13 +1,13 b'' | |||||
1 | from django.shortcuts import render |
|
1 | from django.shortcuts import render | |
2 |
|
2 | |||
3 | from boards.authors import authors |
|
3 | from boards.authors import authors | |
4 | from boards.views.base import BaseBoardView |
|
4 | from boards.views.base import BaseBoardView | |
5 |
|
5 | |||
6 |
|
6 | |||
7 | class AuthorsView(BaseBoardView): |
|
7 | class AuthorsView(BaseBoardView): | |
8 |
|
8 | |||
9 | def get(self, request): |
|
9 | def get(self, request): | |
10 | context = self.get_context_data(request=request) |
|
10 | params = dict() | |
11 |
|
|
11 | params['authors'] = authors | |
12 |
|
12 | |||
13 |
return render(request, 'boards/authors.html', |
|
13 | return render(request, 'boards/authors.html', params) |
@@ -1,16 +1,17 b'' | |||||
1 | from django.shortcuts import get_object_or_404, render |
|
1 | from django.shortcuts import get_object_or_404, render | |
2 | from boards import utils |
|
2 | from boards import utils | |
3 | from boards.models import Ban |
|
3 | from boards.models import Ban | |
4 | from boards.views.base import BaseBoardView |
|
4 | from boards.views.base import BaseBoardView | |
5 |
|
5 | |||
6 |
|
6 | |||
7 | class BannedView(BaseBoardView): |
|
7 | class BannedView(BaseBoardView): | |
8 |
|
8 | |||
9 | def get(self, request): |
|
9 | def get(self, request): | |
10 | """Show the page that notifies that user is banned""" |
|
10 | """Show the page that notifies that user is banned""" | |
11 |
|
11 | |||
12 | context = self.get_context_data(request=request) |
|
12 | params = dict() | |
13 |
|
13 | |||
14 | ban = get_object_or_404(Ban, ip=utils.get_client_ip(request)) |
|
14 | ban = get_object_or_404(Ban, ip=utils.get_client_ip(request)) | |
15 |
|
|
15 | params['ban_reason'] = ban.reason | |
16 | return render(request, 'boards/staticpages/banned.html', context) |
|
16 | ||
|
17 | return render(request, 'boards/staticpages/banned.html', params) |
@@ -1,35 +1,31 b'' | |||||
1 | from django.db import transaction |
|
1 | from django.db import transaction | |
2 | from django.template import RequestContext |
|
2 | from django.template import RequestContext | |
3 | from django.views.generic import View |
|
3 | from django.views.generic import View | |
4 |
|
4 | |||
5 | from boards import utils |
|
5 | from boards import utils | |
6 | from boards.models.user import Ban |
|
6 | from boards.models.user import Ban | |
7 |
|
7 | |||
8 |
|
8 | |||
9 | BAN_REASON_SPAM = 'Autoban: spam bot' |
|
9 | BAN_REASON_SPAM = 'Autoban: spam bot' | |
10 |
|
10 | |||
11 | CONTEXT_FORM = 'form' |
|
11 | CONTEXT_FORM = 'form' | |
12 |
|
12 | |||
13 |
|
13 | |||
14 | class BaseBoardView(View): |
|
14 | class BaseBoardView(View): | |
15 |
|
15 | |||
16 | def get_context_data(self, **kwargs): |
|
16 | def get_context_data(self, **kwargs): | |
17 | request = kwargs['request'] |
|
17 | return dict() | |
18 | # context = self._default_context(request) |
|
|||
19 | context = RequestContext(request) |
|
|||
20 |
|
||||
21 | return context |
|
|||
22 |
|
18 | |||
23 | @transaction.atomic |
|
19 | @transaction.atomic | |
24 | def _ban_current_user(self, request): |
|
20 | def _ban_current_user(self, request): | |
25 | """ |
|
21 | """ | |
26 | Add current user to the IP ban list |
|
22 | Add current user to the IP ban list | |
27 | """ |
|
23 | """ | |
28 |
|
24 | |||
29 | ip = utils.get_client_ip(request) |
|
25 | ip = utils.get_client_ip(request) | |
30 | ban, created = Ban.objects.get_or_create(ip=ip) |
|
26 | ban, created = Ban.objects.get_or_create(ip=ip) | |
31 | if created: |
|
27 | if created: | |
32 | ban.can_read = False |
|
28 | ban.can_read = False | |
33 | ban.reason = BAN_REASON_SPAM |
|
29 | ban.reason = BAN_REASON_SPAM | |
34 | ban.save() |
|
30 | ban.save() | |
35 |
|
31 |
@@ -1,39 +1,40 b'' | |||||
|
1 | PARAM_NEXT = 'next' | |||
1 | PARAMETER_METHOD = 'method' |
|
2 | PARAMETER_METHOD = 'method' | |
2 |
|
3 | |||
3 | from django.shortcuts import redirect |
|
4 | from django.shortcuts import redirect | |
4 | from django.http import HttpResponseRedirect |
|
5 | from django.http import HttpResponseRedirect | |
5 |
|
6 | |||
6 |
|
7 | |||
7 | class RedirectNextMixin: |
|
8 | class RedirectNextMixin: | |
8 |
|
9 | |||
9 | def redirect_to_next(self, request): |
|
10 | def redirect_to_next(self, request): | |
10 | """ |
|
11 | """ | |
11 | If a 'next' parameter was specified, redirect to the next page. This |
|
12 | If a 'next' parameter was specified, redirect to the next page. This | |
12 | is used when the user is required to return to some page after the |
|
13 | is used when the user is required to return to some page after the | |
13 | current view has finished its work. |
|
14 | current view has finished its work. | |
14 | """ |
|
15 | """ | |
15 |
|
16 | |||
16 |
if |
|
17 | if PARAM_NEXT in request.GET: | |
17 |
next_page = request.GET[ |
|
18 | next_page = request.GET[PARAM_NEXT] | |
18 | return HttpResponseRedirect(next_page) |
|
19 | return HttpResponseRedirect(next_page) | |
19 | else: |
|
20 | else: | |
20 | return redirect('index') |
|
21 | return redirect('index') | |
21 |
|
22 | |||
22 |
|
23 | |||
23 | class DispatcherMixin: |
|
24 | class DispatcherMixin: | |
24 | """ |
|
25 | """ | |
25 | This class contains a dispather method that can run a method specified by |
|
26 | This class contains a dispather method that can run a method specified by | |
26 | 'method' request parameter. |
|
27 | 'method' request parameter. | |
27 | """ |
|
28 | """ | |
28 |
|
29 | |||
29 | def dispatch_method(self, *args, **kwargs): |
|
30 | def dispatch_method(self, *args, **kwargs): | |
30 | request = args[0] |
|
31 | request = args[0] | |
31 |
|
32 | |||
32 | method_name = None |
|
33 | method_name = None | |
33 | if PARAMETER_METHOD in request.GET: |
|
34 | if PARAMETER_METHOD in request.GET: | |
34 | method_name = request.GET[PARAMETER_METHOD] |
|
35 | method_name = request.GET[PARAMETER_METHOD] | |
35 | elif PARAMETER_METHOD in request.POST: |
|
36 | elif PARAMETER_METHOD in request.POST: | |
36 | method_name = request.POST[PARAMETER_METHOD] |
|
37 | method_name = request.POST[PARAMETER_METHOD] | |
37 |
|
38 | |||
38 | if method_name: |
|
39 | if method_name: | |
39 | return getattr(self, method_name)(*args, **kwargs) |
|
40 | return getattr(self, method_name)(*args, **kwargs) |
@@ -1,13 +1,17 b'' | |||||
1 | from django.shortcuts import render |
|
1 | from django.shortcuts import render | |
2 |
|
2 | |||
3 | from boards.views.base import BaseBoardView |
|
3 | from boards.views.base import BaseBoardView | |
4 |
|
4 | |||
5 |
|
5 | |||
6 | class NotFoundView(BaseBoardView): |
|
6 | class NotFoundView(BaseBoardView): | |
7 | """ |
|
7 | """ | |
8 | Page 404 (not found) |
|
8 | Page 404 (not found) | |
9 | """ |
|
9 | """ | |
10 |
|
10 | |||
11 | def get(self, request): |
|
11 | def get(self, request): | |
12 |
|
|
12 | params = self.get_context_data() | |
13 | return render(request, 'boards/404.html', context) |
|
13 | ||
|
14 | response = render(request, 'boards/404.html', params) | |||
|
15 | response.status_code = 404 | |||
|
16 | ||||
|
17 | return response |
@@ -1,35 +1,37 b'' | |||||
1 | from django.shortcuts import render |
|
1 | from django.shortcuts import render | |
2 | from django.template import RequestContext |
|
2 | from django.template import RequestContext | |
3 | from django.views.generic import View |
|
3 | from django.views.generic import View | |
4 |
|
4 | |||
5 | from boards.mdx_neboard import bbcode_extended |
|
5 | from boards.mdx_neboard import bbcode_extended | |
6 |
|
6 | |||
7 | FORM_QUERY = 'query' |
|
7 | FORM_QUERY = 'query' | |
8 |
|
8 | |||
9 | CONTEXT_RESULT = 'result' |
|
9 | CONTEXT_RESULT = 'result' | |
10 | CONTEXT_QUERY = 'query' |
|
10 | CONTEXT_QUERY = 'query' | |
11 |
|
11 | |||
12 | __author__ = 'neko259' |
|
12 | __author__ = 'neko259' | |
13 |
|
13 | |||
14 | TEMPLATE = 'boards/preview.html' |
|
14 | TEMPLATE = 'boards/preview.html' | |
15 |
|
15 | |||
16 |
|
16 | |||
17 | class PostPreviewView(View): |
|
17 | class PostPreviewView(View): | |
18 | def get(self, request): |
|
18 | def get(self, request): | |
19 | context = RequestContext(request) |
|
19 | context = RequestContext(request) | |
20 |
|
20 | |||
21 | return render(request, TEMPLATE, context) |
|
21 | # TODO Use dict here | |
|
22 | return render(request, TEMPLATE, context_instance=context) | |||
22 |
|
23 | |||
23 | def post(self, request): |
|
24 | def post(self, request): | |
24 | context = RequestContext(request) |
|
25 | context = RequestContext(request) | |
25 |
|
26 | |||
26 | if FORM_QUERY in request.POST: |
|
27 | if FORM_QUERY in request.POST: | |
27 | raw_text = request.POST[FORM_QUERY] |
|
28 | raw_text = request.POST[FORM_QUERY] | |
28 |
|
29 | |||
29 | if len(raw_text) >= 0: |
|
30 | if len(raw_text) >= 0: | |
30 | rendered_text = bbcode_extended(raw_text) |
|
31 | rendered_text = bbcode_extended(raw_text) | |
31 |
|
32 | |||
32 | context[CONTEXT_RESULT] = rendered_text |
|
33 | context[CONTEXT_RESULT] = rendered_text | |
33 | context[CONTEXT_QUERY] = raw_text |
|
34 | context[CONTEXT_QUERY] = raw_text | |
34 |
|
35 | |||
35 | return render(request, TEMPLATE, context) |
|
36 | # TODO Use dict here | |
|
37 | return render(request, TEMPLATE, context_instance=context) |
@@ -1,40 +1,43 b'' | |||||
1 | from django.shortcuts import render |
|
1 | from django.shortcuts import render | |
2 | from django.template import RequestContext |
|
|||
3 | from django.views.generic import View |
|
2 | from django.views.generic import View | |
4 | from haystack.query import SearchQuerySet |
|
3 | from haystack.query import SearchQuerySet | |
|
4 | ||||
5 | from boards.abstracts.paginator import get_paginator |
|
5 | from boards.abstracts.paginator import get_paginator | |
6 | from boards.forms import SearchForm, PlainErrorList |
|
6 | from boards.forms import SearchForm, PlainErrorList | |
7 |
|
7 | |||
|
8 | ||||
|
9 | MIN_QUERY_LENGTH = 3 | |||
|
10 | RESULTS_PER_PAGE = 10 | |||
|
11 | ||||
8 | FORM_QUERY = 'query' |
|
12 | FORM_QUERY = 'query' | |
9 |
|
13 | |||
10 | CONTEXT_QUERY = 'query' |
|
14 | CONTEXT_QUERY = 'query' | |
11 | CONTEXT_FORM = 'form' |
|
15 | CONTEXT_FORM = 'form' | |
12 | CONTEXT_PAGE = 'page' |
|
16 | CONTEXT_PAGE = 'page' | |
13 |
|
17 | |||
14 | REQUEST_PAGE = 'page' |
|
18 | REQUEST_PAGE = 'page' | |
15 |
|
19 | |||
16 | __author__ = 'neko259' |
|
20 | __author__ = 'neko259' | |
17 |
|
21 | |||
18 | TEMPLATE = 'search/search.html' |
|
22 | TEMPLATE = 'search/search.html' | |
19 |
|
23 | |||
20 |
|
24 | |||
21 | class BoardSearchView(View): |
|
25 | class BoardSearchView(View): | |
22 | def get(self, request): |
|
26 | def get(self, request): | |
23 | context = RequestContext(request) |
|
27 | params = dict() | |
|
28 | ||||
24 | form = SearchForm(request.GET, error_class=PlainErrorList) |
|
29 | form = SearchForm(request.GET, error_class=PlainErrorList) | |
25 |
|
|
30 | params[CONTEXT_FORM] = form | |
26 |
|
31 | |||
27 | if form.is_valid(): |
|
32 | if form.is_valid(): | |
28 | query = form.cleaned_data[FORM_QUERY] |
|
33 | query = form.cleaned_data[FORM_QUERY] | |
29 |
if len(query) >= |
|
34 | if len(query) >= MIN_QUERY_LENGTH: | |
30 |
results = SearchQuerySet().auto_query(query).order_by('-id') |
|
35 | results = SearchQuerySet().auto_query(query).order_by('-id') | |
31 |
paginator = get_paginator(results, |
|
36 | paginator = get_paginator(results, RESULTS_PER_PAGE) | |
32 |
|
37 | |||
33 | if REQUEST_PAGE in request.GET: |
|
38 | page = int(request.GET.get(REQUEST_PAGE, '1')) | |
34 | page = int(request.GET[REQUEST_PAGE]) |
|
|||
35 | else: |
|
|||
36 | page = 1 |
|
|||
37 | context[CONTEXT_PAGE] = paginator.page(page) |
|
|||
38 | context[CONTEXT_QUERY] = query |
|
|||
39 |
|
39 | |||
40 | return render(request, TEMPLATE, context) |
|
40 | params[CONTEXT_PAGE] = paginator.page(page) | |
|
41 | params[CONTEXT_QUERY] = query | |||
|
42 | ||||
|
43 | return render(request, TEMPLATE, params) |
@@ -1,38 +1,41 b'' | |||||
1 | from django.db import transaction |
|
1 | from django.db import transaction | |
2 | from django.shortcuts import render, redirect |
|
2 | from django.shortcuts import render, redirect | |
3 |
|
3 | |||
4 | from boards.abstracts.settingsmanager import get_settings_manager |
|
4 | from boards.abstracts.settingsmanager import get_settings_manager | |
5 | from boards.views.base import BaseBoardView, CONTEXT_FORM |
|
5 | from boards.views.base import BaseBoardView, CONTEXT_FORM | |
6 | from boards.forms import SettingsForm, PlainErrorList |
|
6 | from boards.forms import SettingsForm, PlainErrorList | |
7 |
|
7 | |||
|
8 | FORM_THEME = 'theme' | |||
|
9 | ||||
8 | CONTEXT_HIDDEN_TAGS = 'hidden_tags' |
|
10 | CONTEXT_HIDDEN_TAGS = 'hidden_tags' | |
9 |
|
11 | |||
10 |
|
12 | |||
11 | class SettingsView(BaseBoardView): |
|
13 | class SettingsView(BaseBoardView): | |
12 |
|
14 | |||
13 | def get(self, request): |
|
15 | def get(self, request): | |
14 |
|
|
16 | params = self.get_context_data() | |
15 | settings_manager = get_settings_manager(request) |
|
17 | settings_manager = get_settings_manager(request) | |
16 |
|
18 | |||
17 | selected_theme = settings_manager.get_theme() |
|
19 | selected_theme = settings_manager.get_theme() | |
18 |
|
20 | |||
19 |
form = SettingsForm(initial={ |
|
21 | form = SettingsForm(initial={FORM_THEME: selected_theme}, | |
20 | error_class=PlainErrorList) |
|
22 | error_class=PlainErrorList) | |
21 |
|
23 | |||
22 |
|
|
24 | params[CONTEXT_FORM] = form | |
23 |
|
|
25 | params[CONTEXT_HIDDEN_TAGS] = settings_manager.get_hidden_tags() | |
24 |
|
26 | |||
25 | return render(request, 'boards/settings.html', context) |
|
27 | # TODO Use dict here | |
|
28 | return render(request, 'boards/settings.html', params) | |||
26 |
|
29 | |||
27 | def post(self, request): |
|
30 | def post(self, request): | |
28 | settings_manager = get_settings_manager(request) |
|
31 | settings_manager = get_settings_manager(request) | |
29 |
|
32 | |||
30 | with transaction.atomic(): |
|
33 | with transaction.atomic(): | |
31 | form = SettingsForm(request.POST, error_class=PlainErrorList) |
|
34 | form = SettingsForm(request.POST, error_class=PlainErrorList) | |
32 |
|
35 | |||
33 | if form.is_valid(): |
|
36 | if form.is_valid(): | |
34 |
selected_theme = form.cleaned_data[ |
|
37 | selected_theme = form.cleaned_data[FORM_THEME] | |
35 |
|
38 | |||
36 | settings_manager.set_theme(selected_theme) |
|
39 | settings_manager.set_theme(selected_theme) | |
37 |
|
40 | |||
38 | return redirect('settings') |
|
41 | return redirect('settings') |
@@ -1,14 +1,13 b'' | |||||
1 | from django.shortcuts import render |
|
1 | from django.shortcuts import render | |
2 |
|
2 | |||
3 | from boards.views.base import BaseBoardView |
|
3 | from boards.views.base import BaseBoardView | |
4 |
|
4 | |||
5 |
|
5 | |||
6 | class StaticPageView(BaseBoardView): |
|
6 | class StaticPageView(BaseBoardView): | |
7 |
|
7 | |||
8 | def get(self, request, name): |
|
8 | def get(self, request, name): | |
9 | """ |
|
9 | """ | |
10 | Show a static page that needs only tags list and a CSS |
|
10 | Show a static page that needs only tags list and a CSS | |
11 | """ |
|
11 | """ | |
12 |
|
12 | |||
13 | context = self.get_context_data(request=request) |
|
13 | return render(request, 'boards/staticpages/' + name + '.html') | |
14 | return render(request, 'boards/staticpages/' + name + '.html', context) |
|
@@ -1,92 +1,95 b'' | |||||
1 | from django.shortcuts import get_object_or_404 |
|
1 | from django.shortcuts import get_object_or_404 | |
2 |
|
2 | |||
3 | from boards.abstracts.settingsmanager import get_settings_manager |
|
3 | from boards.abstracts.settingsmanager import get_settings_manager | |
4 | from boards.models import Tag |
|
4 | from boards.models import Tag, Thread | |
5 | from boards.views.all_threads import AllThreadsView, DEFAULT_PAGE |
|
5 | from boards.views.all_threads import AllThreadsView, DEFAULT_PAGE | |
6 | from boards.views.mixins import DispatcherMixin, RedirectNextMixin |
|
6 | from boards.views.mixins import DispatcherMixin, RedirectNextMixin | |
7 | from boards.forms import ThreadForm, PlainErrorList |
|
7 | from boards.forms import ThreadForm, PlainErrorList | |
8 |
|
8 | |||
|
9 | PARAM_HIDDEN_TAGS = 'hidden_tags' | |||
|
10 | PARAM_FAV_TAGS = 'fav_tags' | |||
|
11 | PARAM_TAG = 'tag' | |||
9 |
|
12 | |||
10 | __author__ = 'neko259' |
|
13 | __author__ = 'neko259' | |
11 |
|
14 | |||
12 |
|
15 | |||
13 | class TagView(AllThreadsView, DispatcherMixin, RedirectNextMixin): |
|
16 | class TagView(AllThreadsView, DispatcherMixin, RedirectNextMixin): | |
14 |
|
17 | |||
15 | tag_name = None |
|
18 | tag_name = None | |
16 |
|
19 | |||
17 | def get_threads(self): |
|
20 | def get_threads(self): | |
18 | tag = get_object_or_404(Tag, name=self.tag_name) |
|
21 | tag = get_object_or_404(Tag, name=self.tag_name) | |
19 |
|
22 | |||
20 |
return tag.threads |
|
23 | return tag.get_threads() | |
21 |
|
24 | |||
22 | def get_context_data(self, **kwargs): |
|
25 | def get_context_data(self, **kwargs): | |
23 |
|
|
26 | params = super(TagView, self).get_context_data(**kwargs) | |
24 |
|
27 | |||
25 | settings_manager = get_settings_manager(kwargs['request']) |
|
28 | settings_manager = get_settings_manager(kwargs['request']) | |
26 |
|
29 | |||
27 | tag = get_object_or_404(Tag, name=self.tag_name) |
|
30 | tag = get_object_or_404(Tag, name=self.tag_name) | |
28 | context['tag'] = tag |
|
31 | params[PARAM_TAG] = tag | |
29 |
|
32 | |||
30 |
|
|
33 | params[PARAM_FAV_TAGS] = settings_manager.get_fav_tags() | |
31 |
|
|
34 | params[PARAM_HIDDEN_TAGS] = settings_manager.get_hidden_tags() | |
32 |
|
35 | |||
33 |
return |
|
36 | return params | |
34 |
|
37 | |||
35 | def get(self, request, tag_name, page=DEFAULT_PAGE, form=None): |
|
38 | def get(self, request, tag_name, page=DEFAULT_PAGE, form=None): | |
36 | self.tag_name = tag_name |
|
39 | self.tag_name = tag_name | |
37 |
|
40 | |||
38 | dispatch_result = self.dispatch_method(request) |
|
41 | dispatch_result = self.dispatch_method(request) | |
39 | if dispatch_result: |
|
42 | if dispatch_result: | |
40 | return dispatch_result |
|
43 | return dispatch_result | |
41 | else: |
|
44 | else: | |
42 | return super(TagView, self).get(request, page, form) |
|
45 | return super(TagView, self).get(request, page, form) | |
43 |
|
46 | |||
44 | def post(self, request, tag_name, page=DEFAULT_PAGE): |
|
47 | def post(self, request, tag_name, page=DEFAULT_PAGE): | |
45 | form = ThreadForm(request.POST, request.FILES, |
|
48 | form = ThreadForm(request.POST, request.FILES, | |
46 | error_class=PlainErrorList) |
|
49 | error_class=PlainErrorList) | |
47 | form.session = request.session |
|
50 | form.session = request.session | |
48 |
|
51 | |||
49 | if form.is_valid(): |
|
52 | if form.is_valid(): | |
50 | return self.create_thread(request, form) |
|
53 | return self.create_thread(request, form) | |
51 | if form.need_to_ban: |
|
54 | if form.need_to_ban: | |
52 | # Ban user because he is suspected to be a bot |
|
55 | # Ban user because he is suspected to be a bot | |
53 | self._ban_current_user(request) |
|
56 | self._ban_current_user(request) | |
54 |
|
57 | |||
55 | return self.get(request, tag_name, page, form) |
|
58 | return self.get(request, tag_name, page, form) | |
56 |
|
59 | |||
57 | def subscribe(self, request): |
|
60 | def subscribe(self, request): | |
58 | tag = get_object_or_404(Tag, name=self.tag_name) |
|
61 | tag = get_object_or_404(Tag, name=self.tag_name) | |
59 |
|
62 | |||
60 | settings_manager = get_settings_manager(request) |
|
63 | settings_manager = get_settings_manager(request) | |
61 | settings_manager.add_fav_tag(tag) |
|
64 | settings_manager.add_fav_tag(tag) | |
62 |
|
65 | |||
63 | return self.redirect_to_next(request) |
|
66 | return self.redirect_to_next(request) | |
64 |
|
67 | |||
65 | def unsubscribe(self, request): |
|
68 | def unsubscribe(self, request): | |
66 | tag = get_object_or_404(Tag, name=self.tag_name) |
|
69 | tag = get_object_or_404(Tag, name=self.tag_name) | |
67 |
|
70 | |||
68 | settings_manager = get_settings_manager(request) |
|
71 | settings_manager = get_settings_manager(request) | |
69 | settings_manager.del_fav_tag(tag) |
|
72 | settings_manager.del_fav_tag(tag) | |
70 |
|
73 | |||
71 | return self.redirect_to_next(request) |
|
74 | return self.redirect_to_next(request) | |
72 |
|
75 | |||
73 | def hide(self, request): |
|
76 | def hide(self, request): | |
74 | """ |
|
77 | """ | |
75 | Adds tag to user's hidden tags. Threads with this tag will not be |
|
78 | Adds tag to user's hidden tags. Threads with this tag will not be | |
76 | shown. |
|
79 | shown. | |
77 | """ |
|
80 | """ | |
78 |
|
81 | |||
79 | tag = get_object_or_404(Tag, name=self.tag_name) |
|
82 | tag = get_object_or_404(Tag, name=self.tag_name) | |
80 |
|
83 | |||
81 | settings_manager = get_settings_manager(request) |
|
84 | settings_manager = get_settings_manager(request) | |
82 | settings_manager.add_hidden_tag(tag) |
|
85 | settings_manager.add_hidden_tag(tag) | |
83 |
|
86 | |||
84 | def unhide(self, request): |
|
87 | def unhide(self, request): | |
85 | """ |
|
88 | """ | |
86 | Removed tag from user's hidden tags. |
|
89 | Removed tag from user's hidden tags. | |
87 | """ |
|
90 | """ | |
88 |
|
91 | |||
89 | tag = get_object_or_404(Tag, name=self.tag_name) |
|
92 | tag = get_object_or_404(Tag, name=self.tag_name) | |
90 |
|
93 | |||
91 | settings_manager = get_settings_manager(request) |
|
94 | settings_manager = get_settings_manager(request) | |
92 | settings_manager.del_hidden_tag(tag) |
|
95 | settings_manager.del_hidden_tag(tag) |
@@ -1,142 +1,142 b'' | |||||
1 | from django.core.urlresolvers import reverse |
|
1 | from django.core.urlresolvers import reverse | |
2 | from django.db import transaction |
|
2 | from django.db import transaction | |
3 | from django.http import Http404 |
|
3 | from django.http import Http404 | |
4 | from django.shortcuts import get_object_or_404, render, redirect |
|
4 | from django.shortcuts import get_object_or_404, render, redirect | |
5 | from django.views.generic.edit import FormMixin |
|
5 | from django.views.generic.edit import FormMixin | |
6 |
|
6 | |||
7 | from boards import utils, settings |
|
7 | from boards import utils, settings | |
8 | from boards.forms import PostForm, PlainErrorList |
|
8 | from boards.forms import PostForm, PlainErrorList | |
9 | from boards.models import Post, Ban |
|
9 | from boards.models import Post, Ban | |
10 | from boards.views.banned import BannedView |
|
10 | from boards.views.banned import BannedView | |
11 | from boards.views.base import BaseBoardView, CONTEXT_FORM |
|
11 | from boards.views.base import BaseBoardView, CONTEXT_FORM | |
12 | from boards.views.posting_mixin import PostMixin |
|
12 | from boards.views.posting_mixin import PostMixin | |
|
13 | import neboard | |||
13 |
|
14 | |||
14 | TEMPLATE_GALLERY = 'boards/thread_gallery.html' |
|
15 | TEMPLATE_GALLERY = 'boards/thread_gallery.html' | |
15 | TEMPLATE_NORMAL = 'boards/thread.html' |
|
16 | TEMPLATE_NORMAL = 'boards/thread.html' | |
16 |
|
17 | |||
17 | CONTEXT_POSTS = 'posts' |
|
18 | CONTEXT_POSTS = 'posts' | |
18 | CONTEXT_OP = 'opening_post' |
|
19 | CONTEXT_OP = 'opening_post' | |
19 | CONTEXT_BUMPLIMIT_PRG = 'bumplimit_progress' |
|
20 | CONTEXT_BUMPLIMIT_PRG = 'bumplimit_progress' | |
20 | CONTEXT_POSTS_LEFT = 'posts_left' |
|
21 | CONTEXT_POSTS_LEFT = 'posts_left' | |
21 | CONTEXT_LASTUPDATE = "last_update" |
|
22 | CONTEXT_LASTUPDATE = "last_update" | |
22 | CONTEXT_MAX_REPLIES = 'max_replies' |
|
23 | CONTEXT_MAX_REPLIES = 'max_replies' | |
23 | CONTEXT_THREAD = 'thread' |
|
24 | CONTEXT_THREAD = 'thread' | |
24 | CONTEXT_BUMPABLE = 'bumpable' |
|
25 | CONTEXT_BUMPABLE = 'bumpable' | |
|
26 | CONTEXT_WS_TOKEN = 'ws_token' | |||
|
27 | CONTEXT_WS_PROJECT = 'ws_project' | |||
|
28 | CONTEXT_WS_HOST = 'ws_host' | |||
|
29 | CONTEXT_WS_PORT = 'ws_port' | |||
25 |
|
30 | |||
26 | FORM_TITLE = 'title' |
|
31 | FORM_TITLE = 'title' | |
27 | FORM_TEXT = 'text' |
|
32 | FORM_TEXT = 'text' | |
28 | FORM_IMAGE = 'image' |
|
33 | FORM_IMAGE = 'image' | |
29 |
|
34 | |||
30 | MODE_GALLERY = 'gallery' |
|
35 | MODE_GALLERY = 'gallery' | |
31 | MODE_NORMAL = 'normal' |
|
36 | MODE_NORMAL = 'normal' | |
32 |
|
37 | |||
33 |
|
38 | |||
34 | class ThreadView(BaseBoardView, PostMixin, FormMixin): |
|
39 | class ThreadView(BaseBoardView, PostMixin, FormMixin): | |
35 |
|
40 | |||
36 | def get(self, request, post_id, mode=MODE_NORMAL, form=None): |
|
41 | def get(self, request, post_id, mode=MODE_NORMAL, form=None): | |
37 | try: |
|
42 | try: | |
38 | opening_post = Post.objects.filter(id=post_id).only('thread_new')[0] |
|
43 | opening_post = Post.objects.filter(id=post_id).only('thread_new')[0] | |
39 | except IndexError: |
|
44 | except IndexError: | |
40 | raise Http404 |
|
45 | raise Http404 | |
41 |
|
46 | |||
42 | # If this is not OP, don't show it as it is |
|
47 | # If this is not OP, don't show it as it is | |
43 | if not opening_post or not opening_post.is_opening(): |
|
48 | if not opening_post or not opening_post.is_opening(): | |
44 | raise Http404 |
|
49 | raise Http404 | |
45 |
|
50 | |||
46 | if not form: |
|
51 | if not form: | |
47 | form = PostForm(error_class=PlainErrorList) |
|
52 | form = PostForm(error_class=PlainErrorList) | |
48 |
|
53 | |||
49 | thread_to_show = opening_post.get_thread() |
|
54 | thread_to_show = opening_post.get_thread() | |
50 |
|
55 | |||
51 | context = self.get_context_data(request=request) |
|
56 | params = dict() | |
|
57 | ||||
|
58 | params[CONTEXT_FORM] = form | |||
|
59 | params[CONTEXT_LASTUPDATE] = str(utils.datetime_to_epoch( | |||
|
60 | thread_to_show.last_edit_time)) | |||
|
61 | params[CONTEXT_THREAD] = thread_to_show | |||
|
62 | params[CONTEXT_MAX_REPLIES] = settings.MAX_POSTS_PER_THREAD | |||
52 |
|
63 | |||
53 | context[CONTEXT_FORM] = form |
|
64 | if settings.WEBSOCKETS_ENABLED: | |
54 | context[CONTEXT_LASTUPDATE] = utils.datetime_to_epoch( |
|
65 | params[CONTEXT_WS_TOKEN] = utils.get_websocket_token( | |
55 | thread_to_show.last_edit_time) |
|
66 | timestamp=params[CONTEXT_LASTUPDATE]) | |
56 | context[CONTEXT_THREAD] = thread_to_show |
|
67 | params[CONTEXT_WS_PROJECT] = neboard.settings.CENTRIFUGE_PROJECT_ID | |
57 | context[CONTEXT_MAX_REPLIES] = settings.MAX_POSTS_PER_THREAD |
|
68 | params[CONTEXT_WS_HOST] = request.get_host().split(':')[0] | |
|
69 | params[CONTEXT_WS_PORT] = neboard.settings.CENTRIFUGE_PORT | |||
58 |
|
70 | |||
|
71 | # TODO Move this to subclasses: NormalThreadView, GalleryThreadView etc | |||
59 | if MODE_NORMAL == mode: |
|
72 | if MODE_NORMAL == mode: | |
60 | bumpable = thread_to_show.can_bump() |
|
73 | bumpable = thread_to_show.can_bump() | |
61 |
|
|
74 | params[CONTEXT_BUMPABLE] = bumpable | |
62 | if bumpable: |
|
75 | if bumpable: | |
63 | left_posts = settings.MAX_POSTS_PER_THREAD \ |
|
76 | left_posts = settings.MAX_POSTS_PER_THREAD \ | |
64 | - thread_to_show.get_reply_count() |
|
77 | - thread_to_show.get_reply_count() | |
65 |
|
|
78 | params[CONTEXT_POSTS_LEFT] = left_posts | |
66 |
|
|
79 | params[CONTEXT_BUMPLIMIT_PRG] = str( | |
67 | float(left_posts) / settings.MAX_POSTS_PER_THREAD * 100) |
|
80 | float(left_posts) / settings.MAX_POSTS_PER_THREAD * 100) | |
68 |
|
81 | |||
69 |
|
|
82 | params[CONTEXT_OP] = opening_post | |
70 |
|
83 | |||
71 | document = TEMPLATE_NORMAL |
|
84 | document = TEMPLATE_NORMAL | |
72 | elif MODE_GALLERY == mode: |
|
85 | elif MODE_GALLERY == mode: | |
73 |
|
|
86 | params[CONTEXT_POSTS] = thread_to_show.get_replies_with_images( | |
74 | view_fields_only=True) |
|
87 | view_fields_only=True) | |
75 |
|
88 | |||
76 | document = TEMPLATE_GALLERY |
|
89 | document = TEMPLATE_GALLERY | |
77 | else: |
|
90 | else: | |
78 | raise Http404 |
|
91 | raise Http404 | |
79 |
|
92 | |||
80 |
return render(request, document, |
|
93 | return render(request, document, params) | |
81 |
|
94 | |||
82 | def post(self, request, post_id, mode=MODE_NORMAL): |
|
95 | def post(self, request, post_id, mode=MODE_NORMAL): | |
83 | opening_post = get_object_or_404(Post, id=post_id) |
|
96 | opening_post = get_object_or_404(Post, id=post_id) | |
84 |
|
97 | |||
85 | # If this is not OP, don't show it as it is |
|
98 | # If this is not OP, don't show it as it is | |
86 | if not opening_post.is_opening(): |
|
99 | if not opening_post.is_opening(): | |
87 | raise Http404 |
|
100 | raise Http404 | |
88 |
|
101 | |||
89 | if not opening_post.get_thread().archived: |
|
102 | if not opening_post.get_thread().archived: | |
90 | form = PostForm(request.POST, request.FILES, |
|
103 | form = PostForm(request.POST, request.FILES, | |
91 | error_class=PlainErrorList) |
|
104 | error_class=PlainErrorList) | |
92 | form.session = request.session |
|
105 | form.session = request.session | |
93 |
|
106 | |||
94 | if form.is_valid(): |
|
107 | if form.is_valid(): | |
95 | return self.new_post(request, form, opening_post) |
|
108 | return self.new_post(request, form, opening_post) | |
96 | if form.need_to_ban: |
|
109 | if form.need_to_ban: | |
97 | # Ban user because he is suspected to be a bot |
|
110 | # Ban user because he is suspected to be a bot | |
98 | self._ban_current_user(request) |
|
111 | self._ban_current_user(request) | |
99 |
|
112 | |||
100 | return self.get(request, post_id, mode, form) |
|
113 | return self.get(request, post_id, mode, form) | |
101 |
|
114 | |||
102 | @transaction.atomic |
|
|||
103 | def new_post(self, request, form, opening_post=None, html_response=True): |
|
115 | def new_post(self, request, form, opening_post=None, html_response=True): | |
104 | """Add a new post (in thread or as a reply).""" |
|
116 | """Add a new post (in thread or as a reply).""" | |
105 |
|
117 | |||
106 | ip = utils.get_client_ip(request) |
|
118 | ip = utils.get_client_ip(request) | |
107 | is_banned = Ban.objects.filter(ip=ip).exists() |
|
|||
108 |
|
||||
109 | if is_banned: |
|
|||
110 | if html_response: |
|
|||
111 | return redirect(BannedView().as_view()) |
|
|||
112 | else: |
|
|||
113 | return None |
|
|||
114 |
|
119 | |||
115 | data = form.cleaned_data |
|
120 | data = form.cleaned_data | |
116 |
|
121 | |||
117 | title = data[FORM_TITLE] |
|
122 | title = data[FORM_TITLE] | |
118 | text = data[FORM_TEXT] |
|
123 | text = data[FORM_TEXT] | |
|
124 | image = data.get(FORM_IMAGE) | |||
119 |
|
125 | |||
120 | text = self._remove_invalid_links(text) |
|
126 | text = self._remove_invalid_links(text) | |
121 |
|
127 | |||
122 | if FORM_IMAGE in list(data.keys()): |
|
|||
123 | image = data[FORM_IMAGE] |
|
|||
124 | else: |
|
|||
125 | image = None |
|
|||
126 |
|
||||
127 | tags = [] |
|
|||
128 |
|
||||
129 | post_thread = opening_post.get_thread() |
|
128 | post_thread = opening_post.get_thread() | |
130 |
|
129 | |||
131 | post = Post.objects.create_post(title=title, text=text, image=image, |
|
130 | post = Post.objects.create_post(title=title, text=text, image=image, | |
132 |
thread=post_thread, ip=ip |
|
131 | thread=post_thread, ip=ip) | |
|
132 | post.send_to_websocket(request) | |||
133 |
|
133 | |||
134 | thread_to_show = (opening_post.id if opening_post else post.id) |
|
134 | thread_to_show = (opening_post.id if opening_post else post.id) | |
135 |
|
135 | |||
136 | if html_response: |
|
136 | if html_response: | |
137 | if opening_post: |
|
137 | if opening_post: | |
138 | return redirect( |
|
138 | return redirect( | |
139 | reverse('thread', kwargs={'post_id': thread_to_show}) |
|
139 | reverse('thread', kwargs={'post_id': thread_to_show}) | |
140 | + '#' + str(post.id)) |
|
140 | + '#' + str(post.id)) | |
141 | else: |
|
141 | else: | |
142 | return post |
|
142 | return post |
@@ -1,10 +1,10 b'' | |||||
1 | #!/usr/bin/env python |
|
1 | #!/usr/bin/env python3 | |
2 | import os |
|
2 | import os | |
3 | import sys |
|
3 | import sys | |
4 |
|
4 | |||
5 | if __name__ == "__main__": |
|
5 | if __name__ == "__main__": | |
6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "neboard.settings") |
|
6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "neboard.settings") | |
7 |
|
7 | |||
8 | from django.core.management import execute_from_command_line |
|
8 | from django.core.management import execute_from_command_line | |
9 |
|
9 | |||
10 | execute_from_command_line(sys.argv) |
|
10 | execute_from_command_line(sys.argv) |
@@ -1,235 +1,233 b'' | |||||
1 | # Django settings for neboard project. |
|
1 | # Django settings for neboard project. | |
2 | import os |
|
2 | import os | |
3 | from boards.mdx_neboard import bbcode_extended |
|
3 | from boards.mdx_neboard import bbcode_extended | |
4 |
|
4 | |||
5 | DEBUG = True |
|
5 | DEBUG = True | |
6 | TEMPLATE_DEBUG = DEBUG |
|
6 | TEMPLATE_DEBUG = DEBUG | |
7 |
|
7 | |||
8 | ADMINS = ( |
|
8 | ADMINS = ( | |
9 | # ('Your Name', 'your_email@example.com'), |
|
9 | # ('Your Name', 'your_email@example.com'), | |
10 | ('admin', 'admin@example.com') |
|
10 | ('admin', 'admin@example.com') | |
11 | ) |
|
11 | ) | |
12 |
|
12 | |||
13 | MANAGERS = ADMINS |
|
13 | MANAGERS = ADMINS | |
14 |
|
14 | |||
15 | DATABASES = { |
|
15 | DATABASES = { | |
16 | 'default': { |
|
16 | 'default': { | |
17 | 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. |
|
17 | 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. | |
18 | 'NAME': 'database.db', # Or path to database file if using sqlite3. |
|
18 | 'NAME': 'database.db', # Or path to database file if using sqlite3. | |
19 | 'USER': '', # Not used with sqlite3. |
|
19 | 'USER': '', # Not used with sqlite3. | |
20 | 'PASSWORD': '', # Not used with sqlite3. |
|
20 | 'PASSWORD': '', # Not used with sqlite3. | |
21 | 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. |
|
21 | 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. | |
22 | 'PORT': '', # Set to empty string for default. Not used with sqlite3. |
|
22 | 'PORT': '', # Set to empty string for default. Not used with sqlite3. | |
23 | 'CONN_MAX_AGE': None, |
|
23 | 'CONN_MAX_AGE': None, | |
24 | } |
|
24 | } | |
25 | } |
|
25 | } | |
26 |
|
26 | |||
27 | # Local time zone for this installation. Choices can be found here: |
|
27 | # Local time zone for this installation. Choices can be found here: | |
28 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name |
|
28 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name | |
29 | # although not all choices may be available on all operating systems. |
|
29 | # although not all choices may be available on all operating systems. | |
30 | # In a Windows environment this must be set to your system time zone. |
|
30 | # In a Windows environment this must be set to your system time zone. | |
31 | TIME_ZONE = 'Europe/Kiev' |
|
31 | TIME_ZONE = 'Europe/Kiev' | |
32 |
|
32 | |||
33 | # Language code for this installation. All choices can be found here: |
|
33 | # Language code for this installation. All choices can be found here: | |
34 | # http://www.i18nguy.com/unicode/language-identifiers.html |
|
34 | # http://www.i18nguy.com/unicode/language-identifiers.html | |
35 | LANGUAGE_CODE = 'en' |
|
35 | LANGUAGE_CODE = 'en' | |
36 |
|
36 | |||
37 | SITE_ID = 1 |
|
37 | SITE_ID = 1 | |
38 |
|
38 | |||
39 | # If you set this to False, Django will make some optimizations so as not |
|
39 | # If you set this to False, Django will make some optimizations so as not | |
40 | # to load the internationalization machinery. |
|
40 | # to load the internationalization machinery. | |
41 | USE_I18N = True |
|
41 | USE_I18N = True | |
42 |
|
42 | |||
43 | # If you set this to False, Django will not format dates, numbers and |
|
43 | # If you set this to False, Django will not format dates, numbers and | |
44 | # calendars according to the current locale. |
|
44 | # calendars according to the current locale. | |
45 | USE_L10N = True |
|
45 | USE_L10N = True | |
46 |
|
46 | |||
47 | # If you set this to False, Django will not use timezone-aware datetimes. |
|
47 | # If you set this to False, Django will not use timezone-aware datetimes. | |
48 | USE_TZ = True |
|
48 | USE_TZ = True | |
49 |
|
49 | |||
50 | # Absolute filesystem path to the directory that will hold user-uploaded files. |
|
50 | # Absolute filesystem path to the directory that will hold user-uploaded files. | |
51 | # Example: "/home/media/media.lawrence.com/media/" |
|
51 | # Example: "/home/media/media.lawrence.com/media/" | |
52 | MEDIA_ROOT = './media/' |
|
52 | MEDIA_ROOT = './media/' | |
53 |
|
53 | |||
54 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a |
|
54 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a | |
55 | # trailing slash. |
|
55 | # trailing slash. | |
56 | # Examples: "http://media.lawrence.com/media/", "http://example.com/media/" |
|
56 | # Examples: "http://media.lawrence.com/media/", "http://example.com/media/" | |
57 | MEDIA_URL = '/media/' |
|
57 | MEDIA_URL = '/media/' | |
58 |
|
58 | |||
59 | # Absolute path to the directory static files should be collected to. |
|
59 | # Absolute path to the directory static files should be collected to. | |
60 | # Don't put anything in this directory yourself; store your static files |
|
60 | # Don't put anything in this directory yourself; store your static files | |
61 | # in apps' "static/" subdirectories and in STATICFILES_DIRS. |
|
61 | # in apps' "static/" subdirectories and in STATICFILES_DIRS. | |
62 | # Example: "/home/media/media.lawrence.com/static/" |
|
62 | # Example: "/home/media/media.lawrence.com/static/" | |
63 | STATIC_ROOT = '' |
|
63 | STATIC_ROOT = '' | |
64 |
|
64 | |||
65 | # URL prefix for static files. |
|
65 | # URL prefix for static files. | |
66 | # Example: "http://media.lawrence.com/static/" |
|
66 | # Example: "http://media.lawrence.com/static/" | |
67 | STATIC_URL = '/static/' |
|
67 | STATIC_URL = '/static/' | |
68 |
|
68 | |||
69 | # Additional locations of static files |
|
69 | # Additional locations of static files | |
70 | # It is really a hack, put real paths, not related |
|
70 | # It is really a hack, put real paths, not related | |
71 | STATICFILES_DIRS = ( |
|
71 | STATICFILES_DIRS = ( | |
72 | os.path.dirname(__file__) + '/boards/static', |
|
72 | os.path.dirname(__file__) + '/boards/static', | |
73 |
|
73 | |||
74 | # '/d/work/python/django/neboard/neboard/boards/static', |
|
74 | # '/d/work/python/django/neboard/neboard/boards/static', | |
75 | # Put strings here, like "/home/html/static" or "C:/www/django/static". |
|
75 | # Put strings here, like "/home/html/static" or "C:/www/django/static". | |
76 | # Always use forward slashes, even on Windows. |
|
76 | # Always use forward slashes, even on Windows. | |
77 | # Don't forget to use absolute paths, not relative paths. |
|
77 | # Don't forget to use absolute paths, not relative paths. | |
78 | ) |
|
78 | ) | |
79 |
|
79 | |||
80 | # List of finder classes that know how to find static files in |
|
80 | # List of finder classes that know how to find static files in | |
81 | # various locations. |
|
81 | # various locations. | |
82 | STATICFILES_FINDERS = ( |
|
82 | STATICFILES_FINDERS = ( | |
83 | 'django.contrib.staticfiles.finders.FileSystemFinder', |
|
83 | 'django.contrib.staticfiles.finders.FileSystemFinder', | |
84 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', |
|
84 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', | |
|
85 | 'compressor.finders.CompressorFinder', | |||
85 | ) |
|
86 | ) | |
86 |
|
87 | |||
87 | if DEBUG: |
|
88 | if DEBUG: | |
88 | STATICFILES_STORAGE = \ |
|
89 | STATICFILES_STORAGE = \ | |
89 | 'django.contrib.staticfiles.storage.StaticFilesStorage' |
|
90 | 'django.contrib.staticfiles.storage.StaticFilesStorage' | |
90 | else: |
|
91 | else: | |
91 | STATICFILES_STORAGE = \ |
|
92 | STATICFILES_STORAGE = \ | |
92 | 'django.contrib.staticfiles.storage.CachedStaticFilesStorage' |
|
93 | 'django.contrib.staticfiles.storage.CachedStaticFilesStorage' | |
93 |
|
94 | |||
94 | # Make this unique, and don't share it with anybody. |
|
95 | # Make this unique, and don't share it with anybody. | |
95 | SECRET_KEY = '@1rc$o(7=tt#kd+4s$u6wchm**z^)4x90)7f6z(i&55@o11*8o' |
|
96 | SECRET_KEY = '@1rc$o(7=tt#kd+4s$u6wchm**z^)4x90)7f6z(i&55@o11*8o' | |
96 |
|
97 | |||
97 | # List of callables that know how to import templates from various sources. |
|
98 | # List of callables that know how to import templates from various sources. | |
98 | TEMPLATE_LOADERS = ( |
|
99 | TEMPLATE_LOADERS = ( | |
99 | 'django.template.loaders.filesystem.Loader', |
|
100 | 'django.template.loaders.filesystem.Loader', | |
100 | 'django.template.loaders.app_directories.Loader', |
|
101 | 'django.template.loaders.app_directories.Loader', | |
101 | ) |
|
102 | ) | |
102 |
|
103 | |||
103 | TEMPLATE_CONTEXT_PROCESSORS = ( |
|
104 | TEMPLATE_CONTEXT_PROCESSORS = ( | |
104 | 'django.core.context_processors.media', |
|
105 | 'django.core.context_processors.media', | |
105 | 'django.core.context_processors.static', |
|
106 | 'django.core.context_processors.static', | |
106 | 'django.core.context_processors.request', |
|
107 | 'django.core.context_processors.request', | |
107 | 'django.contrib.auth.context_processors.auth', |
|
108 | 'django.contrib.auth.context_processors.auth', | |
108 | 'boards.context_processors.user_and_ui_processor', |
|
109 | 'boards.context_processors.user_and_ui_processor', | |
109 | ) |
|
110 | ) | |
110 |
|
111 | |||
111 | MIDDLEWARE_CLASSES = ( |
|
112 | MIDDLEWARE_CLASSES = ( | |
112 | 'django.contrib.sessions.middleware.SessionMiddleware', |
|
113 | 'django.contrib.sessions.middleware.SessionMiddleware', | |
113 | 'django.middleware.locale.LocaleMiddleware', |
|
114 | 'django.middleware.locale.LocaleMiddleware', | |
114 | 'django.middleware.common.CommonMiddleware', |
|
115 | 'django.middleware.common.CommonMiddleware', | |
115 | 'django.contrib.auth.middleware.AuthenticationMiddleware', |
|
116 | 'django.contrib.auth.middleware.AuthenticationMiddleware', | |
116 | 'django.contrib.messages.middleware.MessageMiddleware', |
|
117 | 'django.contrib.messages.middleware.MessageMiddleware', | |
117 | 'boards.middlewares.BanMiddleware', |
|
118 | 'boards.middlewares.BanMiddleware', | |
118 | 'boards.middlewares.MinifyHTMLMiddleware', |
|
|||
119 | ) |
|
119 | ) | |
120 |
|
120 | |||
121 | ROOT_URLCONF = 'neboard.urls' |
|
121 | ROOT_URLCONF = 'neboard.urls' | |
122 |
|
122 | |||
123 | # Python dotted path to the WSGI application used by Django's runserver. |
|
123 | # Python dotted path to the WSGI application used by Django's runserver. | |
124 | WSGI_APPLICATION = 'neboard.wsgi.application' |
|
124 | WSGI_APPLICATION = 'neboard.wsgi.application' | |
125 |
|
125 | |||
126 | TEMPLATE_DIRS = ( |
|
126 | TEMPLATE_DIRS = ( | |
127 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". |
|
127 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". | |
128 | # Always use forward slashes, even on Windows. |
|
128 | # Always use forward slashes, even on Windows. | |
129 | # Don't forget to use absolute paths, not relative paths. |
|
129 | # Don't forget to use absolute paths, not relative paths. | |
130 | 'templates', |
|
130 | 'templates', | |
131 | ) |
|
131 | ) | |
132 |
|
132 | |||
133 | INSTALLED_APPS = ( |
|
133 | INSTALLED_APPS = ( | |
134 | 'django.contrib.auth', |
|
134 | 'django.contrib.auth', | |
135 | 'django.contrib.contenttypes', |
|
135 | 'django.contrib.contenttypes', | |
136 | 'django.contrib.sessions', |
|
136 | 'django.contrib.sessions', | |
137 | # 'django.contrib.sites', |
|
137 | # 'django.contrib.sites', | |
138 | 'django.contrib.messages', |
|
138 | 'django.contrib.messages', | |
139 | 'django.contrib.staticfiles', |
|
139 | 'django.contrib.staticfiles', | |
140 | # Uncomment the next line to enable the admin: |
|
140 | # Uncomment the next line to enable the admin: | |
141 | 'django.contrib.admin', |
|
141 | 'django.contrib.admin', | |
142 | # Uncomment the next line to enable admin documentation: |
|
142 | # Uncomment the next line to enable admin documentation: | |
143 | # 'django.contrib.admindocs', |
|
143 | # 'django.contrib.admindocs', | |
144 | 'django.contrib.humanize', |
|
144 | 'django.contrib.humanize', | |
145 | 'django_cleanup', |
|
145 | 'django_cleanup', | |
146 |
|
146 | |||
147 | # Migrations |
|
|||
148 | 'south', |
|
|||
149 | 'debug_toolbar', |
|
147 | 'debug_toolbar', | |
150 |
|
148 | |||
151 | # Search |
|
149 | # Search | |
152 | 'haystack', |
|
150 | 'haystack', | |
153 |
|
151 | |||
154 | 'boards', |
|
152 | 'boards', | |
155 | ) |
|
153 | ) | |
156 |
|
154 | |||
157 | # A sample logging configuration. The only tangible logging |
|
155 | # A sample logging configuration. The only tangible logging | |
158 | # performed by this configuration is to send an email to |
|
156 | # performed by this configuration is to send an email to | |
159 | # the site admins on every HTTP 500 error when DEBUG=False. |
|
157 | # the site admins on every HTTP 500 error when DEBUG=False. | |
160 | # See http://docs.djangoproject.com/en/dev/topics/logging for |
|
158 | # See http://docs.djangoproject.com/en/dev/topics/logging for | |
161 | # more details on how to customize your logging configuration. |
|
159 | # more details on how to customize your logging configuration. | |
162 | LOGGING = { |
|
160 | LOGGING = { | |
163 | 'version': 1, |
|
161 | 'version': 1, | |
164 | 'disable_existing_loggers': False, |
|
162 | 'disable_existing_loggers': False, | |
165 | 'formatters': { |
|
163 | 'formatters': { | |
166 | 'verbose': { |
|
164 | 'verbose': { | |
167 |
'format': '%(levelname)s %(asctime)s %(m |
|
165 | 'format': '%(levelname)s %(asctime)s %(name)s %(process)d %(thread)d %(message)s' | |
168 | }, |
|
166 | }, | |
169 | 'simple': { |
|
167 | 'simple': { | |
170 |
'format': '%(levelname)s %(asctime)s [%(m |
|
168 | 'format': '%(levelname)s %(asctime)s [%(name)s] %(message)s' | |
171 | }, |
|
169 | }, | |
172 | }, |
|
170 | }, | |
173 | 'filters': { |
|
171 | 'filters': { | |
174 | 'require_debug_false': { |
|
172 | 'require_debug_false': { | |
175 | '()': 'django.utils.log.RequireDebugFalse' |
|
173 | '()': 'django.utils.log.RequireDebugFalse' | |
176 | } |
|
174 | } | |
177 | }, |
|
175 | }, | |
178 | 'handlers': { |
|
176 | 'handlers': { | |
179 | 'console': { |
|
177 | 'console': { | |
180 | 'level': 'DEBUG', |
|
178 | 'level': 'DEBUG', | |
181 | 'class': 'logging.StreamHandler', |
|
179 | 'class': 'logging.StreamHandler', | |
182 | 'formatter': 'simple' |
|
180 | 'formatter': 'simple' | |
183 | }, |
|
181 | }, | |
184 | }, |
|
182 | }, | |
185 | 'loggers': { |
|
183 | 'loggers': { | |
186 | 'boards': { |
|
184 | 'boards': { | |
187 | 'handlers': ['console'], |
|
185 | 'handlers': ['console'], | |
188 | 'level': 'DEBUG', |
|
186 | 'level': 'DEBUG', | |
189 | } |
|
187 | } | |
190 | }, |
|
188 | }, | |
191 | } |
|
189 | } | |
192 |
|
190 | |||
193 | HAYSTACK_CONNECTIONS = { |
|
191 | HAYSTACK_CONNECTIONS = { | |
194 | 'default': { |
|
192 | 'default': { | |
195 | 'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine', |
|
193 | 'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine', | |
196 | 'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'), |
|
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 | THEMES = [ |
|
198 | THEMES = [ | |
205 | ('md', 'Mystic Dark'), |
|
199 | ('md', 'Mystic Dark'), | |
206 | ('md_centered', 'Mystic Dark (centered)'), |
|
200 | ('md_centered', 'Mystic Dark (centered)'), | |
207 | ('sw', 'Snow White'), |
|
201 | ('sw', 'Snow White'), | |
208 | ('pg', 'Photon Gray'), |
|
202 | ('pg', 'Photon Gray'), | |
209 | ] |
|
203 | ] | |
210 |
|
204 | |||
211 | POPULAR_TAGS = 10 |
|
|||
212 |
|
||||
213 | POSTING_DELAY = 20 # seconds |
|
205 | POSTING_DELAY = 20 # seconds | |
214 |
|
206 | |||
215 | COMPRESS_HTML = True |
|
207 | # Websocket settins | |
|
208 | CENTRIFUGE_HOST = 'localhost' | |||
|
209 | CENTRIFUGE_PORT = '9090' | |||
|
210 | ||||
|
211 | CENTRIFUGE_ADDRESS = 'http://{}:{}'.format(CENTRIFUGE_HOST, CENTRIFUGE_PORT) | |||
|
212 | CENTRIFUGE_PROJECT_ID = '<project id here>' | |||
|
213 | CENTRIFUGE_PROJECT_SECRET = '<project secret here>' | |||
|
214 | CENTRIFUGE_TIMEOUT = 5 | |||
216 |
|
215 | |||
217 | # Debug mode middlewares |
|
216 | # Debug mode middlewares | |
218 | if DEBUG: |
|
217 | if DEBUG: | |
219 | MIDDLEWARE_CLASSES += ( |
|
218 | MIDDLEWARE_CLASSES += ( | |
220 | 'debug_toolbar.middleware.DebugToolbarMiddleware', |
|
219 | 'debug_toolbar.middleware.DebugToolbarMiddleware', | |
221 | ) |
|
220 | ) | |
222 |
|
221 | |||
223 | def custom_show_toolbar(request): |
|
222 | def custom_show_toolbar(request): | |
224 |
return |
|
223 | return True | |
225 |
|
224 | |||
226 | DEBUG_TOOLBAR_CONFIG = { |
|
225 | DEBUG_TOOLBAR_CONFIG = { | |
227 | 'ENABLE_STACKTRACES': True, |
|
226 | 'ENABLE_STACKTRACES': True, | |
228 | 'SHOW_TOOLBAR_CALLBACK': 'neboard.settings.custom_show_toolbar', |
|
227 | 'SHOW_TOOLBAR_CALLBACK': 'neboard.settings.custom_show_toolbar', | |
229 | } |
|
228 | } | |
230 |
|
229 | |||
231 | # FIXME Uncommenting this fails somehow. Need to investigate this |
|
230 | # FIXME Uncommenting this fails somehow. Need to investigate this | |
232 | #DEBUG_TOOLBAR_PANELS += ( |
|
231 | #DEBUG_TOOLBAR_PANELS += ( | |
233 | # 'debug_toolbar.panels.profiling.ProfilingDebugPanel', |
|
232 | # 'debug_toolbar.panels.profiling.ProfilingDebugPanel', | |
234 | #) |
|
233 | #) | |
235 |
|
@@ -1,55 +1,38 b'' | |||||
1 | # INTRO # |
|
1 | # INTRO # | |
2 |
|
2 | |||
3 | This project aims to create centralized forum-like discussion platform with |
|
3 | This project aims to create centralized forum-like discussion platform with | |
4 | anonymity in mind. |
|
4 | anonymity in mind. | |
5 |
|
5 | |||
6 | Main repository: https://bitbucket.org/neko259/neboard/ |
|
6 | Main repository: https://bitbucket.org/neko259/neboard/ | |
7 |
|
7 | |||
8 | Site: http://neboard.me/ |
|
8 | Site: http://neboard.me/ | |
9 |
|
9 | |||
10 | # DEPENDENCIES # |
|
|||
11 |
|
||||
12 | ## REQUIRED ## |
|
|||
13 |
|
||||
14 | * pillow |
|
|||
15 | * django >= 1.6 |
|
|||
16 | * django_cleanup |
|
|||
17 | * django-markupfield |
|
|||
18 | * markdown |
|
|||
19 | * python-markdown |
|
|||
20 | * django-simple-captcha |
|
|||
21 | * line-profiler |
|
|||
22 |
|
||||
23 | ## OPTIONAL ## |
|
|||
24 |
|
||||
25 | * django-debug-toolbar |
|
|||
26 |
|
||||
27 | # INSTALLATION # |
|
10 | # INSTALLATION # | |
28 |
|
11 | |||
29 | 1. Install all dependencies over pip or system-wide |
|
12 | 1. Install all dependencies over pip or system-wide | |
30 | 2. Setup a database in `neboard/settings.py` |
|
13 | 2. Setup a database in `neboard/settings.py` | |
31 | 3. Run `./manage.py syncdb` and ensure the database was created |
|
14 | 3. Run `./manage.py syncdb` and ensure the database was created | |
32 | 4. Run `./manage.py migrate boards` to apply all south migrations |
|
15 | 4. Run `./manage.py migrate boards` to apply all south migrations | |
33 |
|
16 | |||
34 | # RUNNING # |
|
17 | # RUNNING # | |
35 |
|
18 | |||
36 | You can run the server using django default embedded webserver by running |
|
19 | You can run the server using django default embedded webserver by running | |
37 |
|
20 | |||
38 | ./manage.py runserver <address>:<port> |
|
21 | ./manage.py runserver <address>:<port> | |
39 |
|
22 | |||
40 | See django-admin command help for details |
|
23 | See django-admin command help for details | |
41 |
|
24 | |||
42 | Also consider using wsgi or fcgi interfaces on production servers. |
|
25 | Also consider using wsgi or fcgi interfaces on production servers. | |
43 |
|
26 | |||
44 | # UPGRADE # |
|
27 | # UPGRADE # | |
45 |
|
28 | |||
46 | 1. Backup your project data. |
|
29 | 1. Backup your project data. | |
47 | 2. Save the settings in `neboard/settings.py` and `boards/settings.py` |
|
30 | 2. Save the settings in `neboard/settings.py` and `boards/settings.py` | |
48 | 3. Copy the project contents over the old project directory |
|
31 | 3. Copy the project contents over the old project directory | |
49 | 4. Run migrations by `./manage.py migrate boards` |
|
32 | 4. Run migrations by `./manage.py migrate boards` | |
50 |
|
33 | |||
51 | You can also just clone the mercurial project and pull it to update |
|
34 | You can also just clone the mercurial project and pull it to update | |
52 |
|
35 | |||
53 | # CONCLUSION # |
|
36 | # CONCLUSION # | |
54 |
|
37 | |||
55 | Enjoy our software and thank you! |
|
38 | Enjoy our software and thank you! |
@@ -1,10 +1,9 b'' | |||||
1 | httplib2 |
|
1 | httplib2 | |
2 | simplejson |
|
2 | simplejson | |
3 | south>=0.8.4 |
|
3 | adjacent | |
4 | haystack |
|
4 | haystack | |
5 | pillow |
|
5 | pillow | |
6 |
django>=1. |
|
6 | django>=1.7 | |
7 | django_cleanup |
|
7 | django_cleanup | |
8 | django-markupfield |
|
|||
9 | bbcode |
|
8 | bbcode | |
10 | ecdsa |
|
9 | ecdsa |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
General Comments 0
You need to be logged in to leave comments.
Login now