Show More
@@ -0,0 +1,32 | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | from __future__ import unicode_literals | |||
|
3 | ||||
|
4 | from django.db import migrations, models | |||
|
5 | ||||
|
6 | ||||
|
7 | class Migration(migrations.Migration): | |||
|
8 | ||||
|
9 | def bumpable_and_opening_to_status(apps, schema_editor): | |||
|
10 | Thread = apps.get_model('boards', 'Thread') | |||
|
11 | for thread in Thread.objects.all(): | |||
|
12 | if thread.archived: | |||
|
13 | thread.status = 'archived' | |||
|
14 | elif not thread.bumpable: | |||
|
15 | thread.status = 'bumplimit' | |||
|
16 | else: | |||
|
17 | thread.status = 'active' | |||
|
18 | thread.save(update_fields=['status']) | |||
|
19 | ||||
|
20 | ||||
|
21 | dependencies = [ | |||
|
22 | ('boards', '0035_auto_20151021_1346'), | |||
|
23 | ] | |||
|
24 | ||||
|
25 | operations = [ | |||
|
26 | migrations.AddField( | |||
|
27 | model_name='thread', | |||
|
28 | name='status', | |||
|
29 | field=models.CharField(default='active', max_length=50), | |||
|
30 | ), | |||
|
31 | migrations.RunPython(bumpable_and_opening_to_status), | |||
|
32 | ] |
@@ -0,0 +1,22 | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | from __future__ import unicode_literals | |||
|
3 | ||||
|
4 | from django.db import migrations, models | |||
|
5 | ||||
|
6 | ||||
|
7 | class Migration(migrations.Migration): | |||
|
8 | ||||
|
9 | dependencies = [ | |||
|
10 | ('boards', '0036_thread_status'), | |||
|
11 | ] | |||
|
12 | ||||
|
13 | operations = [ | |||
|
14 | migrations.RemoveField( | |||
|
15 | model_name='thread', | |||
|
16 | name='archived', | |||
|
17 | ), | |||
|
18 | migrations.RemoveField( | |||
|
19 | model_name='thread', | |||
|
20 | name='bumpable', | |||
|
21 | ), | |||
|
22 | ] |
@@ -0,0 +1,19 | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | from __future__ import unicode_literals | |||
|
3 | ||||
|
4 | from django.db import migrations, models | |||
|
5 | ||||
|
6 | ||||
|
7 | class Migration(migrations.Migration): | |||
|
8 | ||||
|
9 | dependencies = [ | |||
|
10 | ('boards', '0037_auto_20151122_2155'), | |||
|
11 | ] | |||
|
12 | ||||
|
13 | operations = [ | |||
|
14 | migrations.AlterField( | |||
|
15 | model_name='banner', | |||
|
16 | name='text', | |||
|
17 | field=models.TextField(null=True, blank=True), | |||
|
18 | ), | |||
|
19 | ] |
@@ -0,0 +1,19 | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | from __future__ import unicode_literals | |||
|
3 | ||||
|
4 | from django.db import migrations, models | |||
|
5 | ||||
|
6 | ||||
|
7 | class Migration(migrations.Migration): | |||
|
8 | ||||
|
9 | dependencies = [ | |||
|
10 | ('boards', '0038_auto_20151123_1203'), | |||
|
11 | ] | |||
|
12 | ||||
|
13 | operations = [ | |||
|
14 | migrations.AlterField( | |||
|
15 | model_name='thread', | |||
|
16 | name='status', | |||
|
17 | field=models.CharField(max_length=50, choices=[('active', 'active'), ('bumplimit', 'bumplimit'), ('archived', 'archived')], default='active'), | |||
|
18 | ), | |||
|
19 | ] |
@@ -0,0 +1,19 | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | from __future__ import unicode_literals | |||
|
3 | ||||
|
4 | from django.db import migrations, models | |||
|
5 | ||||
|
6 | ||||
|
7 | class Migration(migrations.Migration): | |||
|
8 | ||||
|
9 | dependencies = [ | |||
|
10 | ('boards', '0039_auto_20151203_1841'), | |||
|
11 | ] | |||
|
12 | ||||
|
13 | operations = [ | |||
|
14 | migrations.AddField( | |||
|
15 | model_name='thread', | |||
|
16 | name='monochrome', | |||
|
17 | field=models.BooleanField(default=False), | |||
|
18 | ), | |||
|
19 | ] |
@@ -0,0 +1,16 | |||||
|
1 | /* | |||
|
2 | CryptoJS v3.1.2 | |||
|
3 | code.google.com/p/crypto-js | |||
|
4 | (c) 2009-2013 by Jeff Mott. All rights reserved. | |||
|
5 | code.google.com/p/crypto-js/wiki/License | |||
|
6 | */ | |||
|
7 | var CryptoJS=CryptoJS||function(h,s){var f={},t=f.lib={},g=function(){},j=t.Base={extend:function(a){g.prototype=this;var c=new g;a&&c.mixIn(a);c.hasOwnProperty("init")||(c.init=function(){c.$super.init.apply(this,arguments)});c.init.prototype=c;c.$super=this;return c},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var c in a)a.hasOwnProperty(c)&&(this[c]=a[c]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}}, | |||
|
8 | q=t.WordArray=j.extend({init:function(a,c){a=this.words=a||[];this.sigBytes=c!=s?c:4*a.length},toString:function(a){return(a||u).stringify(this)},concat:function(a){var c=this.words,d=a.words,b=this.sigBytes;a=a.sigBytes;this.clamp();if(b%4)for(var e=0;e<a;e++)c[b+e>>>2]|=(d[e>>>2]>>>24-8*(e%4)&255)<<24-8*((b+e)%4);else if(65535<d.length)for(e=0;e<a;e+=4)c[b+e>>>2]=d[e>>>2];else c.push.apply(c,d);this.sigBytes+=a;return this},clamp:function(){var a=this.words,c=this.sigBytes;a[c>>>2]&=4294967295<< | |||
|
9 | 32-8*(c%4);a.length=h.ceil(c/4)},clone:function(){var a=j.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var c=[],d=0;d<a;d+=4)c.push(4294967296*h.random()|0);return new q.init(c,a)}}),v=f.enc={},u=v.Hex={stringify:function(a){var c=a.words;a=a.sigBytes;for(var d=[],b=0;b<a;b++){var e=c[b>>>2]>>>24-8*(b%4)&255;d.push((e>>>4).toString(16));d.push((e&15).toString(16))}return d.join("")},parse:function(a){for(var c=a.length,d=[],b=0;b<c;b+=2)d[b>>>3]|=parseInt(a.substr(b, | |||
|
10 | 2),16)<<24-4*(b%8);return new q.init(d,c/2)}},k=v.Latin1={stringify:function(a){var c=a.words;a=a.sigBytes;for(var d=[],b=0;b<a;b++)d.push(String.fromCharCode(c[b>>>2]>>>24-8*(b%4)&255));return d.join("")},parse:function(a){for(var c=a.length,d=[],b=0;b<c;b++)d[b>>>2]|=(a.charCodeAt(b)&255)<<24-8*(b%4);return new q.init(d,c)}},l=v.Utf8={stringify:function(a){try{return decodeURIComponent(escape(k.stringify(a)))}catch(c){throw Error("Malformed UTF-8 data");}},parse:function(a){return k.parse(unescape(encodeURIComponent(a)))}}, | |||
|
11 | x=t.BufferedBlockAlgorithm=j.extend({reset:function(){this._data=new q.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=l.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var c=this._data,d=c.words,b=c.sigBytes,e=this.blockSize,f=b/(4*e),f=a?h.ceil(f):h.max((f|0)-this._minBufferSize,0);a=f*e;b=h.min(4*a,b);if(a){for(var m=0;m<a;m+=e)this._doProcessBlock(d,m);m=d.splice(0,a);c.sigBytes-=b}return new q.init(m,b)},clone:function(){var a=j.clone.call(this); | |||
|
12 | a._data=this._data.clone();return a},_minBufferSize:0});t.Hasher=x.extend({cfg:j.extend(),init:function(a){this.cfg=this.cfg.extend(a);this.reset()},reset:function(){x.reset.call(this);this._doReset()},update:function(a){this._append(a);this._process();return this},finalize:function(a){a&&this._append(a);return this._doFinalize()},blockSize:16,_createHelper:function(a){return function(c,d){return(new a.init(d)).finalize(c)}},_createHmacHelper:function(a){return function(c,d){return(new w.HMAC.init(a, | |||
|
13 | d)).finalize(c)}}});var w=f.algo={};return f}(Math); | |||
|
14 | (function(h){for(var s=CryptoJS,f=s.lib,t=f.WordArray,g=f.Hasher,f=s.algo,j=[],q=[],v=function(a){return 4294967296*(a-(a|0))|0},u=2,k=0;64>k;){var l;a:{l=u;for(var x=h.sqrt(l),w=2;w<=x;w++)if(!(l%w)){l=!1;break a}l=!0}l&&(8>k&&(j[k]=v(h.pow(u,0.5))),q[k]=v(h.pow(u,1/3)),k++);u++}var a=[],f=f.SHA256=g.extend({_doReset:function(){this._hash=new t.init(j.slice(0))},_doProcessBlock:function(c,d){for(var b=this._hash.words,e=b[0],f=b[1],m=b[2],h=b[3],p=b[4],j=b[5],k=b[6],l=b[7],n=0;64>n;n++){if(16>n)a[n]= | |||
|
15 | c[d+n]|0;else{var r=a[n-15],g=a[n-2];a[n]=((r<<25|r>>>7)^(r<<14|r>>>18)^r>>>3)+a[n-7]+((g<<15|g>>>17)^(g<<13|g>>>19)^g>>>10)+a[n-16]}r=l+((p<<26|p>>>6)^(p<<21|p>>>11)^(p<<7|p>>>25))+(p&j^~p&k)+q[n]+a[n];g=((e<<30|e>>>2)^(e<<19|e>>>13)^(e<<10|e>>>22))+(e&f^e&m^f&m);l=k;k=j;j=p;p=h+r|0;h=m;m=f;f=e;e=r+g|0}b[0]=b[0]+e|0;b[1]=b[1]+f|0;b[2]=b[2]+m|0;b[3]=b[3]+h|0;b[4]=b[4]+p|0;b[5]=b[5]+j|0;b[6]=b[6]+k|0;b[7]=b[7]+l|0},_doFinalize:function(){var a=this._data,d=a.words,b=8*this._nDataBytes,e=8*a.sigBytes; | |||
|
16 | d[e>>>5]|=128<<24-e%32;d[(e+64>>>9<<4)+14]=h.floor(b/4294967296);d[(e+64>>>9<<4)+15]=b;a.sigBytes=4*d.length;this._process();return this._hash},clone:function(){var a=g.clone.call(this);a._hash=this._hash.clone();return a}});s.SHA256=g._createHelper(f);s.HmacSHA256=g._createHmacHelper(f)})(Math); |
@@ -0,0 +1,54 | |||||
|
1 | var POW_COMPUTING_TIMEOUT = 2; | |||
|
2 | var POW_HASH_LENGTH = 16; | |||
|
3 | ||||
|
4 | ||||
|
5 | function computeHash(iteration, guess, target, payload, timestamp, hasher) { | |||
|
6 | iteration += 1; | |||
|
7 | var hash = hasher(payload + iteration).toString(); | |||
|
8 | guess = hash.substring(0, POW_HASH_LENGTH); | |||
|
9 | ||||
|
10 | if (guess <= target) { | |||
|
11 | //console.log("Iteration: ", iteration); | |||
|
12 | //console.log("Guess: ", guess); | |||
|
13 | //console.log("Target: ", target); | |||
|
14 | ||||
|
15 | var data = { | |||
|
16 | iteration: iteration, | |||
|
17 | timestamp: timestamp, | |||
|
18 | guess: guess | |||
|
19 | }; | |||
|
20 | self.postMessage(data); | |||
|
21 | } else { | |||
|
22 | //console.log("Iteration: ", iteration); | |||
|
23 | //console.log("Guess: ", guess); | |||
|
24 | //console.log("Target: ", target); | |||
|
25 | ||||
|
26 | setTimeout(function() { | |||
|
27 | computeHash(iteration, guess, target, payload, timestamp, hasher); | |||
|
28 | }, POW_COMPUTING_TIMEOUT); | |||
|
29 | } | |||
|
30 | } | |||
|
31 | ||||
|
32 | function doWork(message, hasher, difficulty) { | |||
|
33 | var timestamp = Date.now(); | |||
|
34 | var iteration = 0; | |||
|
35 | var payload = timestamp + message; | |||
|
36 | ||||
|
37 | var target = parseInt(Math.pow(2, POW_HASH_LENGTH * 3) / difficulty).toString(); | |||
|
38 | while (target.length < POW_HASH_LENGTH) { | |||
|
39 | target = '0' + target; | |||
|
40 | } | |||
|
41 | ||||
|
42 | var guess = target + '0'; | |||
|
43 | ||||
|
44 | setTimeout(function() { | |||
|
45 | computeHash(iteration, guess, target, payload, timestamp, hasher); | |||
|
46 | }, POW_COMPUTING_TIMEOUT); | |||
|
47 | } | |||
|
48 | ||||
|
49 | self.onmessage = function(e) { | |||
|
50 | var difficulty = e.data.difficulty; | |||
|
51 | importScripts(e.data.hasher); | |||
|
52 | var hasher = CryptoJS.SHA256; | |||
|
53 | self.doWork(e.data.msg, hasher, difficulty); | |||
|
54 | }; |
@@ -0,0 +1,31 | |||||
|
1 | from django.core.urlresolvers import reverse | |||
|
2 | from django.shortcuts import get_object_or_404, render | |||
|
3 | ||||
|
4 | from boards import settings | |||
|
5 | from boards.abstracts.paginator import get_paginator | |||
|
6 | from boards.models import Tag | |||
|
7 | from boards.views.base import BaseBoardView | |||
|
8 | from boards.views.mixins import PaginatedMixin | |||
|
9 | ||||
|
10 | IMAGES_PER_PAGE = settings.get_int('View', 'ImagesPerPageGallery') | |||
|
11 | ||||
|
12 | TEMPLATE = 'boards/tag_gallery.html' | |||
|
13 | ||||
|
14 | ||||
|
15 | class TagGalleryView(BaseBoardView, PaginatedMixin): | |||
|
16 | ||||
|
17 | def get(self, request, tag_name): | |||
|
18 | page = int(request.GET.get('page', 1)) | |||
|
19 | ||||
|
20 | params = dict() | |||
|
21 | tag = get_object_or_404(Tag, name=tag_name) | |||
|
22 | params['tag'] = tag | |||
|
23 | paginator = get_paginator(tag.get_images(), IMAGES_PER_PAGE, | |||
|
24 | current_page=page) | |||
|
25 | params['paginator'] = paginator | |||
|
26 | params['images'] = paginator.page(page).object_list | |||
|
27 | paginator.set_url(reverse('tag_gallery', kwargs={'tag_name': tag_name}), | |||
|
28 | request.GET.dict()) | |||
|
29 | self.set_page_urls(paginator, params) | |||
|
30 | ||||
|
31 | return render(request, TEMPLATE, params) No newline at end of file |
@@ -0,0 +1,7 | |||||
|
1 | from django.views.decorators.cache import cache_page | |||
|
2 | from django.views.i18n import javascript_catalog | |||
|
3 | ||||
|
4 | @cache_page(600) | |||
|
5 | def cached_javascript_catalog(request, domain='djangojs', packages=None): | |||
|
6 | return javascript_catalog(request, domain, packages) | |||
|
7 |
@@ -1,37 +1,39 | |||||
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 |
|
19 | 4d998aba79e4abf0a2e78e93baaa2c2800b1c49c 2.2 | |
20 | 07fdef4ac33a859250d03f17c594089792bca615 2.2.1 |
|
20 | 07fdef4ac33a859250d03f17c594089792bca615 2.2.1 | |
21 | bcc74d45f060ecd3ff06ff448165aea0d026cb3e 2.2.2 |
|
21 | bcc74d45f060ecd3ff06ff448165aea0d026cb3e 2.2.2 | |
22 | b0e629ff24eb47a449ecfb455dc6cc600d18c9e2 2.2.3 |
|
22 | b0e629ff24eb47a449ecfb455dc6cc600d18c9e2 2.2.3 | |
23 | 1b52ba60f17fd7c90912c14d9d17e880b7952d01 2.2.4 |
|
23 | 1b52ba60f17fd7c90912c14d9d17e880b7952d01 2.2.4 | |
24 | 957e2fec91468f739b0fc2b9936d564505048c68 2.3.0 |
|
24 | 957e2fec91468f739b0fc2b9936d564505048c68 2.3.0 | |
25 | bb91141c6ea5c822ccbe2d46c3c48bdab683b77d 2.4.0 |
|
25 | bb91141c6ea5c822ccbe2d46c3c48bdab683b77d 2.4.0 | |
26 | 97eb184637e5691b288eaf6b03e8971f3364c239 2.5.0 |
|
26 | 97eb184637e5691b288eaf6b03e8971f3364c239 2.5.0 | |
27 | 119fafc5381b933bf30d97be0b278349f6135075 2.5.1 |
|
27 | 119fafc5381b933bf30d97be0b278349f6135075 2.5.1 | |
28 | d528d76d3242cced614fa11bb63f3d342e4e1d09 2.5.2 |
|
28 | d528d76d3242cced614fa11bb63f3d342e4e1d09 2.5.2 | |
29 | 1b631781ced34fbdeec032e7674bc4e131724699 2.6.0 |
|
29 | 1b631781ced34fbdeec032e7674bc4e131724699 2.6.0 | |
30 | 0f2ef17dc0de678ada279bf7eedf6c5585f1fd7a 2.6.1 |
|
30 | 0f2ef17dc0de678ada279bf7eedf6c5585f1fd7a 2.6.1 | |
31 | d53fc814a424d7fd90f23025c87b87baa164450e 2.7.0 |
|
31 | d53fc814a424d7fd90f23025c87b87baa164450e 2.7.0 | |
32 | 836d8bb9fcd930b952b9a02029442c71c2441983 2.8.0 |
|
32 | 836d8bb9fcd930b952b9a02029442c71c2441983 2.8.0 | |
33 | dfb6c481b1a2c33705de9a9b5304bc924c46b202 2.8.1 |
|
33 | dfb6c481b1a2c33705de9a9b5304bc924c46b202 2.8.1 | |
34 | 4a5bec08ccfb47a27f9e98698f12dd5b7246623b 2.8.2 |
|
34 | 4a5bec08ccfb47a27f9e98698f12dd5b7246623b 2.8.2 | |
35 | 604935b98f5b5e4a5e903594f048046e1fbb3519 2.8.3 |
|
35 | 604935b98f5b5e4a5e903594f048046e1fbb3519 2.8.3 | |
36 | c48ffdc671566069ed0f33644da1229277f3cd18 2.9.0 |
|
36 | c48ffdc671566069ed0f33644da1229277f3cd18 2.9.0 | |
37 | d66dc192d4e089ba85325afeef5229b73cb0fde4 2.10.0 |
|
37 | d66dc192d4e089ba85325afeef5229b73cb0fde4 2.10.0 | |
|
38 | 1c22a38cca9ae3bee13d6f263792c0629d0061f6 2.10.1 | |||
|
39 | 3076e0d03339f3b41dcc71fb6af2b4169920846c 2.11.0 |
@@ -1,74 +1,91 | |||||
1 | __author__ = 'neko259' |
|
1 | __author__ = 'neko259' | |
2 |
|
2 | |||
3 | from django.core.paginator import Paginator |
|
3 | from django.core.paginator import Paginator | |
4 |
|
4 | |||
5 | PAGINATOR_LOOKAROUND_SIZE = 2 |
|
5 | PAGINATOR_LOOKAROUND_SIZE = 2 | |
6 |
|
6 | |||
7 |
|
7 | |||
8 | def get_paginator(*args, **kwargs): |
|
8 | def get_paginator(*args, **kwargs): | |
9 | return DividedPaginator(*args, **kwargs) |
|
9 | return DividedPaginator(*args, **kwargs) | |
10 |
|
10 | |||
11 |
|
11 | |||
12 | class DividedPaginator(Paginator): |
|
12 | class DividedPaginator(Paginator): | |
13 |
|
13 | |||
14 | lookaround_size = PAGINATOR_LOOKAROUND_SIZE |
|
14 | lookaround_size = PAGINATOR_LOOKAROUND_SIZE | |
15 | current_page = 0 |
|
15 | ||
|
16 | def __init__(self, object_list, per_page, orphans=0, | |||
|
17 | allow_empty_first_page=True, current_page=1): | |||
|
18 | super().__init__(object_list, per_page, orphans, allow_empty_first_page) | |||
|
19 | ||||
|
20 | self.link = None | |||
|
21 | self.params = None | |||
|
22 | self.current_page = current_page | |||
16 |
|
23 | |||
17 | def _left_range(self): |
|
24 | def _left_range(self): | |
18 | return self.page_range[:self.lookaround_size] |
|
25 | return self.page_range[:self.lookaround_size] | |
19 |
|
26 | |||
20 | def _right_range(self): |
|
27 | def _right_range(self): | |
21 | pages = self.num_pages-self.lookaround_size |
|
28 | pages = self.num_pages-self.lookaround_size | |
22 | if pages <= 0: |
|
29 | if pages <= 0: | |
23 | return [] |
|
30 | return [] | |
24 | else: |
|
31 | else: | |
25 | return self.page_range[pages:] |
|
32 | return self.page_range[pages:] | |
26 |
|
33 | |||
27 | def _center_range(self): |
|
34 | def _center_range(self): | |
28 | index = self.page_range.index(self.current_page) |
|
35 | index = self.page_range.index(self.current_page) | |
29 |
|
36 | |||
30 | start = max(self.lookaround_size, index - self.lookaround_size) |
|
37 | start = max(self.lookaround_size, index - self.lookaround_size) | |
31 | end = min(self.num_pages - self.lookaround_size, index + self.lookaround_size + 1) |
|
38 | end = min(self.num_pages - self.lookaround_size, index + self.lookaround_size + 1) | |
32 | return self.page_range[start:end] |
|
39 | return self.page_range[start:end] | |
33 |
|
40 | |||
34 | def get_divided_range(self): |
|
41 | def get_divided_range(self): | |
35 | dr = list() |
|
42 | dr = list() | |
36 |
|
43 | |||
37 | dr += self._left_range() |
|
44 | dr += self._left_range() | |
38 | dr += self._center_range() |
|
45 | dr += self._center_range() | |
39 | dr += self._right_range() |
|
46 | dr += self._right_range() | |
40 |
|
47 | |||
41 | # Remove duplicates |
|
48 | # Remove duplicates | |
42 | dr = list(set(dr)) |
|
49 | dr = list(set(dr)) | |
43 | dr.sort() |
|
50 | dr.sort() | |
44 |
|
51 | |||
45 | return dr |
|
52 | return dr | |
46 |
|
53 | |||
47 | def get_dividers(self): |
|
54 | def get_dividers(self): | |
48 | dividers = [] |
|
55 | dividers = [] | |
49 |
|
56 | |||
50 | prev_page = 1 |
|
57 | prev_page = 1 | |
51 | for page in self.get_divided_range(): |
|
58 | for page in self.get_divided_range(): | |
52 | if page - prev_page > 1: |
|
59 | if page - prev_page > 1: | |
53 | dividers.append(page) |
|
60 | dividers.append(page) | |
54 |
|
61 | |||
55 | # There can be no more than 2 dividers, so don't bother going |
|
62 | # There can be no more than 2 dividers, so don't bother going | |
56 | # further |
|
63 | # further | |
57 | if len(dividers) > 2: |
|
64 | if len(dividers) > 2: | |
58 | break |
|
65 | break | |
59 | prev_page = page |
|
66 | prev_page = page | |
60 |
|
67 | |||
61 | return dividers |
|
68 | return dividers | |
62 |
|
69 | |||
63 | def set_url(self, link, params): |
|
70 | def set_url(self, link, params): | |
64 | self.link = link |
|
71 | self.link = link | |
65 | self.params = params |
|
72 | self.params = params | |
66 |
|
73 | |||
67 | def get_page_url(self, page): |
|
74 | def get_page_url(self, page): | |
68 | self.params['page'] = page |
|
75 | self.params['page'] = page | |
69 | url_params = '?' + '&'.join(['{}={}'.format(key, self.params[key]) |
|
76 | url_params = '?' + '&'.join(['{}={}'.format(key, self.params[key]) | |
70 | for key in self.params.keys()]) |
|
77 | for key in self.params.keys()]) | |
71 | return self.link + url_params |
|
78 | return self.link + url_params | |
72 |
|
79 | |||
73 | def supports_urls(self): |
|
80 | def supports_urls(self): | |
74 | return self.link is not None and self.params is not None |
|
81 | return self.link is not None and self.params is not None | |
|
82 | ||||
|
83 | def get_next_page_url(self): | |||
|
84 | current = self.page(self.current_page) | |||
|
85 | if current.has_next(): | |||
|
86 | return self.get_page_url(current.next_page_number()) | |||
|
87 | ||||
|
88 | def get_prev_page_url(self): | |||
|
89 | current = self.page(self.current_page) | |||
|
90 | if current.has_previous(): | |||
|
91 | return self.get_page_url(current.previous_page_number()) No newline at end of file |
@@ -1,172 +1,180 | |||||
1 | from boards.models import Tag |
|
1 | from boards.models import Tag | |
2 | from boards.models.thread import FAV_THREAD_NO_UPDATES |
|
2 | from boards.models.thread import FAV_THREAD_NO_UPDATES | |
3 |
|
3 | |||
4 | MAX_TRIPCODE_COLLISIONS = 50 |
|
4 | MAX_TRIPCODE_COLLISIONS = 50 | |
5 |
|
5 | |||
6 | __author__ = 'neko259' |
|
6 | __author__ = 'neko259' | |
7 |
|
7 | |||
8 | SESSION_SETTING = 'setting' |
|
8 | SESSION_SETTING = 'setting' | |
9 |
|
9 | |||
10 | # Remove this, it is not used any more cause there is a user's permission |
|
10 | # Remove this, it is not used any more cause there is a user's permission | |
11 | PERMISSION_MODERATE = 'moderator' |
|
11 | PERMISSION_MODERATE = 'moderator' | |
12 |
|
12 | |||
13 | SETTING_THEME = 'theme' |
|
13 | SETTING_THEME = 'theme' | |
14 | SETTING_FAVORITE_TAGS = 'favorite_tags' |
|
14 | SETTING_FAVORITE_TAGS = 'favorite_tags' | |
15 | SETTING_FAVORITE_THREADS = 'favorite_threads' |
|
15 | SETTING_FAVORITE_THREADS = 'favorite_threads' | |
16 | SETTING_HIDDEN_TAGS = 'hidden_tags' |
|
16 | SETTING_HIDDEN_TAGS = 'hidden_tags' | |
17 | SETTING_PERMISSIONS = 'permissions' |
|
17 | SETTING_PERMISSIONS = 'permissions' | |
18 | SETTING_USERNAME = 'username' |
|
18 | SETTING_USERNAME = 'username' | |
19 | SETTING_LAST_NOTIFICATION_ID = 'last_notification' |
|
19 | SETTING_LAST_NOTIFICATION_ID = 'last_notification' | |
20 | SETTING_IMAGE_VIEWER = 'image_viewer' |
|
20 | SETTING_IMAGE_VIEWER = 'image_viewer' | |
21 | SETTING_TRIPCODE = 'tripcode' |
|
21 | SETTING_TRIPCODE = 'tripcode' | |
22 |
|
22 | |||
23 | DEFAULT_THEME = 'md' |
|
23 | DEFAULT_THEME = 'md' | |
24 |
|
24 | |||
25 |
|
25 | |||
26 | class SettingsManager: |
|
26 | class SettingsManager: | |
27 | """ |
|
27 | """ | |
28 | Base settings manager class. get_setting and set_setting methods should |
|
28 | Base settings manager class. get_setting and set_setting methods should | |
29 | be overriden. |
|
29 | be overriden. | |
30 | """ |
|
30 | """ | |
31 | def __init__(self): |
|
31 | def __init__(self): | |
32 | pass |
|
32 | pass | |
33 |
|
33 | |||
34 | def get_theme(self) -> str: |
|
34 | def get_theme(self) -> str: | |
35 | theme = self.get_setting(SETTING_THEME) |
|
35 | theme = self.get_setting(SETTING_THEME) | |
36 | if not theme: |
|
36 | if not theme: | |
37 | theme = DEFAULT_THEME |
|
37 | theme = DEFAULT_THEME | |
38 | self.set_setting(SETTING_THEME, theme) |
|
38 | self.set_setting(SETTING_THEME, theme) | |
39 |
|
39 | |||
40 | return theme |
|
40 | return theme | |
41 |
|
41 | |||
42 | def set_theme(self, theme): |
|
42 | def set_theme(self, theme): | |
43 | self.set_setting(SETTING_THEME, theme) |
|
43 | self.set_setting(SETTING_THEME, theme) | |
44 |
|
44 | |||
45 | def has_permission(self, permission): |
|
45 | def has_permission(self, permission): | |
46 | permissions = self.get_setting(SETTING_PERMISSIONS) |
|
46 | permissions = self.get_setting(SETTING_PERMISSIONS) | |
47 | if permissions: |
|
47 | if permissions: | |
48 | return permission in permissions |
|
48 | return permission in permissions | |
49 | else: |
|
49 | else: | |
50 | return False |
|
50 | return False | |
51 |
|
51 | |||
52 | def get_setting(self, setting, default=None): |
|
52 | def get_setting(self, setting, default=None): | |
53 | pass |
|
53 | pass | |
54 |
|
54 | |||
55 | def set_setting(self, setting, value): |
|
55 | def set_setting(self, setting, value): | |
56 | pass |
|
56 | pass | |
57 |
|
57 | |||
58 | def add_permission(self, permission): |
|
58 | def add_permission(self, permission): | |
59 | permissions = self.get_setting(SETTING_PERMISSIONS) |
|
59 | permissions = self.get_setting(SETTING_PERMISSIONS) | |
60 | if not permissions: |
|
60 | if not permissions: | |
61 | permissions = [permission] |
|
61 | permissions = [permission] | |
62 | else: |
|
62 | else: | |
63 | permissions.append(permission) |
|
63 | permissions.append(permission) | |
64 | self.set_setting(SETTING_PERMISSIONS, permissions) |
|
64 | self.set_setting(SETTING_PERMISSIONS, permissions) | |
65 |
|
65 | |||
66 | def del_permission(self, permission): |
|
66 | def del_permission(self, permission): | |
67 | permissions = self.get_setting(SETTING_PERMISSIONS) |
|
67 | permissions = self.get_setting(SETTING_PERMISSIONS) | |
68 | if not permissions: |
|
68 | if not permissions: | |
69 | permissions = [] |
|
69 | permissions = [] | |
70 | else: |
|
70 | else: | |
71 | permissions.remove(permission) |
|
71 | permissions.remove(permission) | |
72 | self.set_setting(SETTING_PERMISSIONS, permissions) |
|
72 | self.set_setting(SETTING_PERMISSIONS, permissions) | |
73 |
|
73 | |||
74 | def get_fav_tags(self) -> list: |
|
74 | def get_fav_tags(self) -> list: | |
75 | tag_names = self.get_setting(SETTING_FAVORITE_TAGS) |
|
75 | tag_names = self.get_setting(SETTING_FAVORITE_TAGS) | |
76 | tags = [] |
|
76 | tags = [] | |
77 | if tag_names: |
|
77 | if tag_names: | |
78 | tags = list(Tag.objects.filter(name__in=tag_names)) |
|
78 | tags = list(Tag.objects.filter(name__in=tag_names)) | |
79 | return tags |
|
79 | return tags | |
80 |
|
80 | |||
81 | def add_fav_tag(self, tag): |
|
81 | def add_fav_tag(self, tag): | |
82 | tags = self.get_setting(SETTING_FAVORITE_TAGS) |
|
82 | tags = self.get_setting(SETTING_FAVORITE_TAGS) | |
83 | if not tags: |
|
83 | if not tags: | |
84 | tags = [tag.name] |
|
84 | tags = [tag.name] | |
85 | else: |
|
85 | else: | |
86 | if not tag.name in tags: |
|
86 | if not tag.name in tags: | |
87 | tags.append(tag.name) |
|
87 | tags.append(tag.name) | |
88 |
|
88 | |||
89 | tags.sort() |
|
89 | tags.sort() | |
90 | self.set_setting(SETTING_FAVORITE_TAGS, tags) |
|
90 | self.set_setting(SETTING_FAVORITE_TAGS, tags) | |
91 |
|
91 | |||
92 | def del_fav_tag(self, tag): |
|
92 | def del_fav_tag(self, tag): | |
93 | tags = self.get_setting(SETTING_FAVORITE_TAGS) |
|
93 | tags = self.get_setting(SETTING_FAVORITE_TAGS) | |
94 | if tag.name in tags: |
|
94 | if tag.name in tags: | |
95 | tags.remove(tag.name) |
|
95 | tags.remove(tag.name) | |
96 | self.set_setting(SETTING_FAVORITE_TAGS, tags) |
|
96 | self.set_setting(SETTING_FAVORITE_TAGS, tags) | |
97 |
|
97 | |||
98 | def get_hidden_tags(self) -> list: |
|
98 | def get_hidden_tags(self) -> list: | |
99 | tag_names = self.get_setting(SETTING_HIDDEN_TAGS) |
|
99 | tag_names = self.get_setting(SETTING_HIDDEN_TAGS) | |
100 | tags = [] |
|
100 | tags = [] | |
101 | if tag_names: |
|
101 | if tag_names: | |
102 | tags = list(Tag.objects.filter(name__in=tag_names)) |
|
102 | tags = list(Tag.objects.filter(name__in=tag_names)) | |
103 |
|
103 | |||
104 | return tags |
|
104 | return tags | |
105 |
|
105 | |||
106 | def add_hidden_tag(self, tag): |
|
106 | def add_hidden_tag(self, tag): | |
107 | tags = self.get_setting(SETTING_HIDDEN_TAGS) |
|
107 | tags = self.get_setting(SETTING_HIDDEN_TAGS) | |
108 | if not tags: |
|
108 | if not tags: | |
109 | tags = [tag.name] |
|
109 | tags = [tag.name] | |
110 | else: |
|
110 | else: | |
111 | if not tag.name in tags: |
|
111 | if not tag.name in tags: | |
112 | tags.append(tag.name) |
|
112 | tags.append(tag.name) | |
113 |
|
113 | |||
114 | tags.sort() |
|
114 | tags.sort() | |
115 | self.set_setting(SETTING_HIDDEN_TAGS, tags) |
|
115 | self.set_setting(SETTING_HIDDEN_TAGS, tags) | |
116 |
|
116 | |||
117 | def del_hidden_tag(self, tag): |
|
117 | def del_hidden_tag(self, tag): | |
118 | tags = self.get_setting(SETTING_HIDDEN_TAGS) |
|
118 | tags = self.get_setting(SETTING_HIDDEN_TAGS) | |
119 | if tag.name in tags: |
|
119 | if tag.name in tags: | |
120 | tags.remove(tag.name) |
|
120 | tags.remove(tag.name) | |
121 | self.set_setting(SETTING_HIDDEN_TAGS, tags) |
|
121 | self.set_setting(SETTING_HIDDEN_TAGS, tags) | |
122 |
|
122 | |||
123 | def get_fav_threads(self) -> dict: |
|
123 | def get_fav_threads(self) -> dict: | |
124 | return self.get_setting(SETTING_FAVORITE_THREADS, default=dict()) |
|
124 | return self.get_setting(SETTING_FAVORITE_THREADS, default=dict()) | |
125 |
|
125 | |||
126 | def add_or_read_fav_thread(self, opening_post): |
|
126 | def add_or_read_fav_thread(self, opening_post): | |
127 | threads = self.get_fav_threads() |
|
127 | threads = self.get_fav_threads() | |
128 | thread = opening_post.get_thread() |
|
128 | thread = opening_post.get_thread() | |
129 | # Don't check for new posts if the thread is archived already |
|
129 | # Don't check for new posts if the thread is archived already | |
130 | if thread.is_archived(): |
|
130 | if thread.is_archived(): | |
131 | last_id = FAV_THREAD_NO_UPDATES |
|
131 | last_id = FAV_THREAD_NO_UPDATES | |
132 | else: |
|
132 | else: | |
133 | last_id = thread.get_replies().last().id |
|
133 | last_id = thread.get_replies().last().id | |
134 | threads[str(opening_post.id)] = last_id |
|
134 | threads[str(opening_post.id)] = last_id | |
135 | self.set_setting(SETTING_FAVORITE_THREADS, threads) |
|
135 | self.set_setting(SETTING_FAVORITE_THREADS, threads) | |
136 |
|
136 | |||
137 | def del_fav_thread(self, opening_post): |
|
137 | def del_fav_thread(self, opening_post): | |
138 | threads = self.get_fav_threads() |
|
138 | threads = self.get_fav_threads() | |
139 | if self.thread_is_fav(opening_post): |
|
139 | if self.thread_is_fav(opening_post): | |
140 | del threads[str(opening_post.id)] |
|
140 | del threads[str(opening_post.id)] | |
141 | self.set_setting(SETTING_FAVORITE_THREADS, threads) |
|
141 | self.set_setting(SETTING_FAVORITE_THREADS, threads) | |
142 |
|
142 | |||
143 | def thread_is_fav(self, opening_post): |
|
143 | def thread_is_fav(self, opening_post): | |
144 | return str(opening_post.id) in self.get_fav_threads() |
|
144 | return str(opening_post.id) in self.get_fav_threads() | |
145 |
|
145 | |||
|
146 | def get_notification_usernames(self): | |||
|
147 | name_list = self.get_setting(SETTING_USERNAME) | |||
|
148 | if name_list is not None and len(name_list) > 0: | |||
|
149 | return name_list.lower().split(',') | |||
|
150 | else: | |||
|
151 | return list() | |||
|
152 | ||||
|
153 | ||||
146 | class SessionSettingsManager(SettingsManager): |
|
154 | class SessionSettingsManager(SettingsManager): | |
147 | """ |
|
155 | """ | |
148 | Session-based settings manager. All settings are saved to the user's |
|
156 | Session-based settings manager. All settings are saved to the user's | |
149 | session. |
|
157 | session. | |
150 | """ |
|
158 | """ | |
151 | def __init__(self, session): |
|
159 | def __init__(self, session): | |
152 | SettingsManager.__init__(self) |
|
160 | SettingsManager.__init__(self) | |
153 | self.session = session |
|
161 | self.session = session | |
154 |
|
162 | |||
155 | def get_setting(self, setting, default=None): |
|
163 | def get_setting(self, setting, default=None): | |
156 | if setting in self.session: |
|
164 | if setting in self.session: | |
157 | return self.session[setting] |
|
165 | return self.session[setting] | |
158 | else: |
|
166 | else: | |
159 | self.set_setting(setting, default) |
|
167 | self.set_setting(setting, default) | |
160 | return default |
|
168 | return default | |
161 |
|
169 | |||
162 | def set_setting(self, setting, value): |
|
170 | def set_setting(self, setting, value): | |
163 | self.session[setting] = value |
|
171 | self.session[setting] = value | |
164 |
|
172 | |||
165 |
|
173 | |||
166 | def get_settings_manager(request) -> SettingsManager: |
|
174 | def get_settings_manager(request) -> SettingsManager: | |
167 | """ |
|
175 | """ | |
168 | Get settings manager based on the request object. Currently only |
|
176 | Get settings manager based on the request object. Currently only | |
169 | session-based manager is supported. In the future, cookie-based or |
|
177 | session-based manager is supported. In the future, cookie-based or | |
170 | database-based managers could be implemented. |
|
178 | database-based managers could be implemented. | |
171 | """ |
|
179 | """ | |
172 | return SessionSettingsManager(request.session) |
|
180 | return SessionSettingsManager(request.session) |
@@ -1,81 +1,82 | |||||
1 | from django.contrib import admin |
|
1 | from django.contrib import admin | |
2 | from boards.models import Post, Tag, Ban, Thread, KeyPair, Banner |
|
2 | from boards.models import Post, Tag, Ban, Thread, KeyPair, Banner | |
3 | from django.utils.translation import ugettext_lazy as _ |
|
3 | from django.utils.translation import ugettext_lazy as _ | |
4 |
|
4 | |||
5 |
|
5 | |||
6 | @admin.register(Post) |
|
6 | @admin.register(Post) | |
7 | class PostAdmin(admin.ModelAdmin): |
|
7 | class PostAdmin(admin.ModelAdmin): | |
8 |
|
8 | |||
9 | list_display = ('id', 'title', 'text', 'poster_ip') |
|
9 | list_display = ('id', 'title', 'text', 'poster_ip') | |
10 | list_filter = ('pub_time',) |
|
10 | list_filter = ('pub_time',) | |
11 | search_fields = ('id', 'title', 'text', 'poster_ip') |
|
11 | search_fields = ('id', 'title', 'text', 'poster_ip') | |
12 | exclude = ('referenced_posts', 'refmap') |
|
12 | exclude = ('referenced_posts', 'refmap') | |
13 |
readonly_fields = ('poster_ip', 'threads', 'thread', 'images', |
|
13 | readonly_fields = ('poster_ip', 'threads', 'thread', 'images', | |
|
14 | 'attachments', 'uid', 'url', 'pub_time', 'opening') | |||
14 |
|
15 | |||
15 | def ban_poster(self, request, queryset): |
|
16 | def ban_poster(self, request, queryset): | |
16 | bans = 0 |
|
17 | bans = 0 | |
17 | for post in queryset: |
|
18 | for post in queryset: | |
18 | poster_ip = post.poster_ip |
|
19 | poster_ip = post.poster_ip | |
19 | ban, created = Ban.objects.get_or_create(ip=poster_ip) |
|
20 | ban, created = Ban.objects.get_or_create(ip=poster_ip) | |
20 | if created: |
|
21 | if created: | |
21 | bans += 1 |
|
22 | bans += 1 | |
22 | self.message_user(request, _('{} posters were banned').format(bans)) |
|
23 | self.message_user(request, _('{} posters were banned').format(bans)) | |
23 |
|
24 | |||
24 | actions = ['ban_poster'] |
|
25 | actions = ['ban_poster'] | |
25 |
|
26 | |||
26 |
|
27 | |||
27 | @admin.register(Tag) |
|
28 | @admin.register(Tag) | |
28 | class TagAdmin(admin.ModelAdmin): |
|
29 | class TagAdmin(admin.ModelAdmin): | |
29 |
|
30 | |||
30 | def thread_count(self, obj: Tag) -> int: |
|
31 | def thread_count(self, obj: Tag) -> int: | |
31 | return obj.get_thread_count() |
|
32 | return obj.get_thread_count() | |
32 |
|
33 | |||
33 | def display_children(self, obj: Tag): |
|
34 | def display_children(self, obj: Tag): | |
34 | return ', '.join([str(child) for child in obj.get_children().all()]) |
|
35 | return ', '.join([str(child) for child in obj.get_children().all()]) | |
35 |
|
36 | |||
36 | list_display = ('name', 'thread_count', 'display_children') |
|
37 | list_display = ('name', 'thread_count', 'display_children') | |
37 | search_fields = ('name',) |
|
38 | search_fields = ('name',) | |
38 |
|
39 | |||
39 |
|
40 | |||
40 | @admin.register(Thread) |
|
41 | @admin.register(Thread) | |
41 | class ThreadAdmin(admin.ModelAdmin): |
|
42 | class ThreadAdmin(admin.ModelAdmin): | |
42 |
|
43 | |||
43 | def title(self, obj: Thread) -> str: |
|
44 | def title(self, obj: Thread) -> str: | |
44 | return obj.get_opening_post().get_title() |
|
45 | return obj.get_opening_post().get_title() | |
45 |
|
46 | |||
46 | def reply_count(self, obj: Thread) -> int: |
|
47 | def reply_count(self, obj: Thread) -> int: | |
47 | return obj.get_reply_count() |
|
48 | return obj.get_reply_count() | |
48 |
|
49 | |||
49 | def ip(self, obj: Thread): |
|
50 | def ip(self, obj: Thread): | |
50 | return obj.get_opening_post().poster_ip |
|
51 | return obj.get_opening_post().poster_ip | |
51 |
|
52 | |||
52 | def display_tags(self, obj: Thread): |
|
53 | def display_tags(self, obj: Thread): | |
53 | return ', '.join([str(tag) for tag in obj.get_tags().all()]) |
|
54 | return ', '.join([str(tag) for tag in obj.get_tags().all()]) | |
54 |
|
55 | |||
55 | def op(self, obj: Thread): |
|
56 | def op(self, obj: Thread): | |
56 | return obj.get_opening_post_id() |
|
57 | return obj.get_opening_post_id() | |
57 |
|
58 | |||
58 |
list_display = ('id', 'op', 'title', 'reply_count', ' |
|
59 | list_display = ('id', 'op', 'title', 'reply_count', 'status', 'ip', | |
59 | 'display_tags') |
|
60 | 'display_tags') | |
60 |
list_filter = ('bump_time', ' |
|
61 | list_filter = ('bump_time', 'status') | |
61 | search_fields = ('id', 'title') |
|
62 | search_fields = ('id', 'title') | |
62 | filter_horizontal = ('tags',) |
|
63 | filter_horizontal = ('tags',) | |
63 |
|
64 | |||
64 |
|
65 | |||
65 | @admin.register(KeyPair) |
|
66 | @admin.register(KeyPair) | |
66 | class KeyPairAdmin(admin.ModelAdmin): |
|
67 | class KeyPairAdmin(admin.ModelAdmin): | |
67 | list_display = ('public_key', 'primary') |
|
68 | list_display = ('public_key', 'primary') | |
68 | list_filter = ('primary',) |
|
69 | list_filter = ('primary',) | |
69 | search_fields = ('public_key',) |
|
70 | search_fields = ('public_key',) | |
70 |
|
71 | |||
71 |
|
72 | |||
72 | @admin.register(Ban) |
|
73 | @admin.register(Ban) | |
73 | class BanAdmin(admin.ModelAdmin): |
|
74 | class BanAdmin(admin.ModelAdmin): | |
74 | list_display = ('ip', 'can_read') |
|
75 | list_display = ('ip', 'can_read') | |
75 | list_filter = ('can_read',) |
|
76 | list_filter = ('can_read',) | |
76 | search_fields = ('ip',) |
|
77 | search_fields = ('ip',) | |
77 |
|
78 | |||
78 |
|
79 | |||
79 | @admin.register(Banner) |
|
80 | @admin.register(Banner) | |
80 | class BannerAdmin(admin.ModelAdmin): |
|
81 | class BannerAdmin(admin.ModelAdmin): | |
81 | list_display = ('title', 'text') |
|
82 | list_display = ('title', 'text') |
@@ -1,34 +1,39 | |||||
1 | [Version] |
|
1 | [Version] | |
2 |
Version = 2.1 |
|
2 | Version = 2.11.0 Yuko | |
3 | SiteName = Neboard DEV |
|
3 | SiteName = Neboard DEV | |
4 |
|
4 | |||
5 | [Cache] |
|
5 | [Cache] | |
6 | # Timeout for caching, if cache is used |
|
6 | # Timeout for caching, if cache is used | |
7 | CacheTimeout = 600 |
|
7 | CacheTimeout = 600 | |
8 |
|
8 | |||
9 | [Forms] |
|
9 | [Forms] | |
10 | # Max post length in characters |
|
10 | # Max post length in characters | |
11 | MaxTextLength = 30000 |
|
11 | MaxTextLength = 30000 | |
12 | MaxFileSize = 8000000 |
|
12 | MaxFileSize = 8000000 | |
13 |
LimitPostingSpeed = |
|
13 | LimitPostingSpeed = true | |
|
14 | PowDifficulty = 20 | |||
14 |
|
15 | |||
15 | [Messages] |
|
16 | [Messages] | |
16 | # Thread bumplimit |
|
17 | # Thread bumplimit | |
17 | MaxPostsPerThread = 10 |
|
18 | MaxPostsPerThread = 10 | |
18 | # Old posts will be archived or deleted if this value is reached |
|
19 | # Old posts will be archived or deleted if this value is reached | |
19 | MaxThreadCount = 5 |
|
20 | MaxThreadCount = 5 | |
20 | AnonymousMode = false |
|
21 | AnonymousMode = false | |
21 |
|
22 | |||
22 | [View] |
|
23 | [View] | |
23 | DefaultTheme = md |
|
24 | DefaultTheme = md | |
24 | DefaultImageViewer = simple |
|
25 | DefaultImageViewer = simple | |
25 | LastRepliesCount = 3 |
|
26 | LastRepliesCount = 3 | |
26 | ThreadsPerPage = 3 |
|
27 | ThreadsPerPage = 3 | |
|
28 | ImagesPerPageGallery = 20 | |||
27 |
|
29 | |||
28 | [Storage] |
|
30 | [Storage] | |
29 | # Enable archiving threads instead of deletion when the thread limit is reached |
|
31 | # Enable archiving threads instead of deletion when the thread limit is reached | |
30 | ArchiveThreads = true |
|
32 | ArchiveThreads = true | |
31 |
|
33 | |||
32 | [External] |
|
34 | [External] | |
33 | # Thread update |
|
35 | # Thread update | |
34 | WebsocketsEnabled = false |
|
36 | WebsocketsEnabled = false | |
|
37 | ||||
|
38 | [RSS] | |||
|
39 | MaxItems = 20 |
@@ -1,68 +1,68 | |||||
1 | from boards.abstracts.settingsmanager import get_settings_manager, \ |
|
1 | from boards.abstracts.settingsmanager import get_settings_manager, \ | |
2 |
|
|
2 | SETTING_LAST_NOTIFICATION_ID, SETTING_IMAGE_VIEWER | |
3 | from boards.models.user import Notification |
|
3 | from boards.models.user import Notification | |
4 |
|
4 | |||
5 | __author__ = 'neko259' |
|
5 | __author__ = 'neko259' | |
6 |
|
6 | |||
7 |
from boards import settings |
|
7 | from boards import settings | |
8 | from boards.models import Post, Tag |
|
8 | from boards.models import Post, Tag | |
9 |
|
9 | |||
10 | CONTEXT_SITE_NAME = 'site_name' |
|
10 | CONTEXT_SITE_NAME = 'site_name' | |
11 | CONTEXT_VERSION = 'version' |
|
11 | CONTEXT_VERSION = 'version' | |
12 | CONTEXT_MODERATOR = 'moderator' |
|
|||
13 | CONTEXT_THEME_CSS = 'theme_css' |
|
12 | CONTEXT_THEME_CSS = 'theme_css' | |
14 | CONTEXT_THEME = 'theme' |
|
13 | CONTEXT_THEME = 'theme' | |
15 | CONTEXT_PPD = 'posts_per_day' |
|
14 | CONTEXT_PPD = 'posts_per_day' | |
16 | CONTEXT_TAGS = 'tags' |
|
15 | CONTEXT_TAGS = 'tags' | |
17 | CONTEXT_USER = 'user' |
|
16 | CONTEXT_USER = 'user' | |
18 | CONTEXT_NEW_NOTIFICATIONS_COUNT = 'new_notifications_count' |
|
17 | CONTEXT_NEW_NOTIFICATIONS_COUNT = 'new_notifications_count' | |
19 | CONTEXT_USERNAME = 'username' |
|
18 | CONTEXT_USERNAMES = 'usernames' | |
20 | CONTEXT_TAGS_STR = 'tags_str' |
|
19 | CONTEXT_TAGS_STR = 'tags_str' | |
21 | CONTEXT_IMAGE_VIEWER = 'image_viewer' |
|
20 | CONTEXT_IMAGE_VIEWER = 'image_viewer' | |
22 | CONTEXT_HAS_FAV_THREADS = 'has_fav_threads' |
|
21 | CONTEXT_HAS_FAV_THREADS = 'has_fav_threads' | |
|
22 | CONTEXT_POW_DIFFICULTY = 'pow_difficulty' | |||
23 |
|
23 | |||
24 |
|
24 | |||
25 | def get_notifications(context, request): |
|
25 | def get_notifications(context, request): | |
26 | settings_manager = get_settings_manager(request) |
|
26 | settings_manager = get_settings_manager(request) | |
27 |
username = settings_manager.get_ |
|
27 | usernames = settings_manager.get_notification_usernames() | |
28 | new_notifications_count = 0 |
|
28 | new_notifications_count = 0 | |
29 |
if username is not None |
|
29 | if usernames is not None: | |
30 | last_notification_id = settings_manager.get_setting( |
|
30 | last_notification_id = settings_manager.get_setting( | |
31 | SETTING_LAST_NOTIFICATION_ID) |
|
31 | SETTING_LAST_NOTIFICATION_ID) | |
32 |
|
32 | |||
33 | new_notifications_count = Notification.objects.get_notification_posts( |
|
33 | new_notifications_count = Notification.objects.get_notification_posts( | |
34 | username=username, last=last_notification_id).count() |
|
34 | usernames=usernames, last=last_notification_id).count() | |
35 | context[CONTEXT_NEW_NOTIFICATIONS_COUNT] = new_notifications_count |
|
35 | context[CONTEXT_NEW_NOTIFICATIONS_COUNT] = new_notifications_count | |
36 | context[CONTEXT_USERNAME] = username |
|
36 | context[CONTEXT_USERNAMES] = usernames | |
37 |
|
37 | |||
38 |
|
38 | |||
39 | def user_and_ui_processor(request): |
|
39 | def user_and_ui_processor(request): | |
40 | context = dict() |
|
40 | context = dict() | |
41 |
|
41 | |||
42 | context[CONTEXT_PPD] = float(Post.objects.get_posts_per_day()) |
|
42 | context[CONTEXT_PPD] = float(Post.objects.get_posts_per_day()) | |
43 |
|
43 | |||
44 | settings_manager = get_settings_manager(request) |
|
44 | settings_manager = get_settings_manager(request) | |
45 | fav_tags = settings_manager.get_fav_tags() |
|
45 | fav_tags = settings_manager.get_fav_tags() | |
46 | context[CONTEXT_TAGS] = fav_tags |
|
46 | context[CONTEXT_TAGS] = fav_tags | |
47 |
|
47 | |||
48 | context[CONTEXT_TAGS_STR] = Tag.objects.get_tag_url_list(fav_tags) |
|
48 | context[CONTEXT_TAGS_STR] = Tag.objects.get_tag_url_list(fav_tags) | |
49 | theme = settings_manager.get_theme() |
|
49 | theme = settings_manager.get_theme() | |
50 | context[CONTEXT_THEME] = theme |
|
50 | context[CONTEXT_THEME] = theme | |
51 | context[CONTEXT_THEME_CSS] = 'css/' + theme + '/base_page.css' |
|
51 | context[CONTEXT_THEME_CSS] = 'css/' + theme + '/base_page.css' | |
52 |
|
52 | |||
53 | # This shows the moderator panel |
|
|||
54 | context[CONTEXT_MODERATOR] = utils.is_moderator(request) |
|
|||
55 |
|
||||
56 | context[CONTEXT_VERSION] = settings.get('Version', 'Version') |
|
53 | context[CONTEXT_VERSION] = settings.get('Version', 'Version') | |
57 | context[CONTEXT_SITE_NAME] = settings.get('Version', 'SiteName') |
|
54 | context[CONTEXT_SITE_NAME] = settings.get('Version', 'SiteName') | |
58 |
|
55 | |||
|
56 | if settings.get_bool('Forms', 'LimitPostingSpeed'): | |||
|
57 | context[CONTEXT_POW_DIFFICULTY] = settings.get_int('Forms', 'PowDifficulty') | |||
|
58 | ||||
59 | context[CONTEXT_IMAGE_VIEWER] = settings_manager.get_setting( |
|
59 | context[CONTEXT_IMAGE_VIEWER] = settings_manager.get_setting( | |
60 | SETTING_IMAGE_VIEWER, |
|
60 | SETTING_IMAGE_VIEWER, | |
61 | default=settings.get('View', 'DefaultImageViewer')) |
|
61 | default=settings.get('View', 'DefaultImageViewer')) | |
62 |
|
62 | |||
63 | context[CONTEXT_HAS_FAV_THREADS] =\ |
|
63 | context[CONTEXT_HAS_FAV_THREADS] =\ | |
64 | len(settings_manager.get_fav_threads()) > 0 |
|
64 | len(settings_manager.get_fav_threads()) > 0 | |
65 |
|
65 | |||
66 | get_notifications(context, request) |
|
66 | get_notifications(context, request) | |
67 |
|
67 | |||
68 | return context |
|
68 | return context |
@@ -1,406 +1,443 | |||||
1 | import hashlib |
|
1 | import hashlib | |
2 | import re |
|
2 | import re | |
3 | import time |
|
3 | import time | |
4 | import logging |
|
4 | import logging | |
|
5 | ||||
5 | import pytz |
|
6 | import pytz | |
6 |
|
7 | |||
7 | from django import forms |
|
8 | from django import forms | |
8 | from django.core.files.uploadedfile import SimpleUploadedFile |
|
9 | from django.core.files.uploadedfile import SimpleUploadedFile | |
9 | from django.core.exceptions import ObjectDoesNotExist |
|
10 | from django.core.exceptions import ObjectDoesNotExist | |
10 | from django.forms.util import ErrorList |
|
11 | from django.forms.util import ErrorList | |
11 | from django.utils.translation import ugettext_lazy as _, ungettext_lazy |
|
12 | from django.utils.translation import ugettext_lazy as _, ungettext_lazy | |
|
13 | from django.utils import timezone | |||
12 |
|
14 | |||
13 | from boards.mdx_neboard import formatters |
|
15 | from boards.mdx_neboard import formatters | |
14 | from boards.models.attachment.downloaders import Downloader |
|
16 | from boards.models.attachment.downloaders import Downloader | |
15 | from boards.models.post import TITLE_MAX_LENGTH |
|
17 | from boards.models.post import TITLE_MAX_LENGTH | |
16 | from boards.models import Tag, Post |
|
18 | from boards.models import Tag, Post | |
17 | from boards.utils import validate_file_size, get_file_mimetype, \ |
|
19 | from boards.utils import validate_file_size, get_file_mimetype, \ | |
18 | FILE_EXTENSION_DELIMITER |
|
20 | FILE_EXTENSION_DELIMITER | |
19 | from neboard import settings |
|
21 | from neboard import settings | |
20 | import boards.settings as board_settings |
|
22 | import boards.settings as board_settings | |
21 | import neboard |
|
23 | import neboard | |
22 |
|
24 | |||
|
25 | POW_HASH_LENGTH = 16 | |||
|
26 | POW_LIFE_MINUTES = 1 | |||
|
27 | ||||
23 | REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE) |
|
28 | REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE) | |
|
29 | REGEX_USERNAMES = re.compile(r'^[\w\s\d,]+$', re.UNICODE) | |||
24 |
|
30 | |||
25 | VETERAN_POSTING_DELAY = 5 |
|
31 | VETERAN_POSTING_DELAY = 5 | |
26 |
|
32 | |||
27 | ATTRIBUTE_PLACEHOLDER = 'placeholder' |
|
33 | ATTRIBUTE_PLACEHOLDER = 'placeholder' | |
28 | ATTRIBUTE_ROWS = 'rows' |
|
34 | ATTRIBUTE_ROWS = 'rows' | |
29 |
|
35 | |||
30 | LAST_POST_TIME = 'last_post_time' |
|
36 | LAST_POST_TIME = 'last_post_time' | |
31 | LAST_LOGIN_TIME = 'last_login_time' |
|
37 | LAST_LOGIN_TIME = 'last_login_time' | |
32 | TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.') |
|
38 | TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.') | |
33 | TAGS_PLACEHOLDER = _('music images i_dont_like_tags') |
|
39 | TAGS_PLACEHOLDER = _('music images i_dont_like_tags') | |
34 |
|
40 | |||
35 | LABEL_TITLE = _('Title') |
|
41 | LABEL_TITLE = _('Title') | |
36 | LABEL_TEXT = _('Text') |
|
42 | LABEL_TEXT = _('Text') | |
37 | LABEL_TAG = _('Tag') |
|
43 | LABEL_TAG = _('Tag') | |
38 | LABEL_SEARCH = _('Search') |
|
44 | LABEL_SEARCH = _('Search') | |
39 |
|
45 | |||
40 | ERROR_SPEED = 'Please wait %(delay)d second before sending message' |
|
46 | ERROR_SPEED = 'Please wait %(delay)d second before sending message' | |
41 | ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message' |
|
47 | ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message' | |
42 |
|
48 | |||
43 | TAG_MAX_LENGTH = 20 |
|
49 | TAG_MAX_LENGTH = 20 | |
44 |
|
50 | |||
45 | TEXTAREA_ROWS = 4 |
|
51 | TEXTAREA_ROWS = 4 | |
46 |
|
52 | |||
47 | TRIPCODE_DELIM = '#' |
|
53 | TRIPCODE_DELIM = '#' | |
48 |
|
54 | |||
49 | # TODO Maybe this may be converted into the database table? |
|
55 | # TODO Maybe this may be converted into the database table? | |
50 | MIMETYPE_EXTENSIONS = { |
|
56 | MIMETYPE_EXTENSIONS = { | |
51 | 'image/jpeg': 'jpeg', |
|
57 | 'image/jpeg': 'jpeg', | |
52 | 'image/png': 'png', |
|
58 | 'image/png': 'png', | |
53 | 'image/gif': 'gif', |
|
59 | 'image/gif': 'gif', | |
54 | 'video/webm': 'webm', |
|
60 | 'video/webm': 'webm', | |
55 | 'application/pdf': 'pdf', |
|
61 | 'application/pdf': 'pdf', | |
56 | 'x-diff': 'diff', |
|
62 | 'x-diff': 'diff', | |
57 | 'image/svg+xml': 'svg', |
|
63 | 'image/svg+xml': 'svg', | |
58 | 'application/x-shockwave-flash': 'swf', |
|
64 | 'application/x-shockwave-flash': 'swf', | |
59 | } |
|
65 | } | |
60 |
|
66 | |||
61 |
|
67 | |||
62 | def get_timezones(): |
|
68 | def get_timezones(): | |
63 | timezones = [] |
|
69 | timezones = [] | |
64 | for tz in pytz.common_timezones: |
|
70 | for tz in pytz.common_timezones: | |
65 | timezones.append((tz, tz),) |
|
71 | timezones.append((tz, tz),) | |
66 | return timezones |
|
72 | return timezones | |
67 |
|
73 | |||
68 |
|
74 | |||
69 | class FormatPanel(forms.Textarea): |
|
75 | class FormatPanel(forms.Textarea): | |
70 | """ |
|
76 | """ | |
71 | Panel for text formatting. Consists of buttons to add different tags to the |
|
77 | Panel for text formatting. Consists of buttons to add different tags to the | |
72 | form text area. |
|
78 | form text area. | |
73 | """ |
|
79 | """ | |
74 |
|
80 | |||
75 | def render(self, name, value, attrs=None): |
|
81 | def render(self, name, value, attrs=None): | |
76 | output = '<div id="mark-panel">' |
|
82 | output = '<div id="mark-panel">' | |
77 | for formatter in formatters: |
|
83 | for formatter in formatters: | |
78 | output += '<span class="mark_btn"' + \ |
|
84 | output += '<span class="mark_btn"' + \ | |
79 | ' onClick="addMarkToMsg(\'' + formatter.format_left + \ |
|
85 | ' onClick="addMarkToMsg(\'' + formatter.format_left + \ | |
80 | '\', \'' + formatter.format_right + '\')">' + \ |
|
86 | '\', \'' + formatter.format_right + '\')">' + \ | |
81 | formatter.preview_left + formatter.name + \ |
|
87 | formatter.preview_left + formatter.name + \ | |
82 | formatter.preview_right + '</span>' |
|
88 | formatter.preview_right + '</span>' | |
83 |
|
89 | |||
84 | output += '</div>' |
|
90 | output += '</div>' | |
85 |
output += super(FormatPanel, self).render(name, value, attrs= |
|
91 | output += super(FormatPanel, self).render(name, value, attrs=attrs) | |
86 |
|
92 | |||
87 | return output |
|
93 | return output | |
88 |
|
94 | |||
89 |
|
95 | |||
90 | class PlainErrorList(ErrorList): |
|
96 | class PlainErrorList(ErrorList): | |
91 | def __unicode__(self): |
|
97 | def __unicode__(self): | |
92 | return self.as_text() |
|
98 | return self.as_text() | |
93 |
|
99 | |||
94 | def as_text(self): |
|
100 | def as_text(self): | |
95 | return ''.join(['(!) %s ' % e for e in self]) |
|
101 | return ''.join(['(!) %s ' % e for e in self]) | |
96 |
|
102 | |||
97 |
|
103 | |||
98 | class NeboardForm(forms.Form): |
|
104 | class NeboardForm(forms.Form): | |
99 | """ |
|
105 | """ | |
100 | Form with neboard-specific formatting. |
|
106 | Form with neboard-specific formatting. | |
101 | """ |
|
107 | """ | |
102 |
|
108 | |||
103 | def as_div(self): |
|
109 | def as_div(self): | |
104 | """ |
|
110 | """ | |
105 | Returns this form rendered as HTML <as_div>s. |
|
111 | Returns this form rendered as HTML <as_div>s. | |
106 | """ |
|
112 | """ | |
107 |
|
113 | |||
108 | return self._html_output( |
|
114 | return self._html_output( | |
109 | # TODO Do not show hidden rows in the list here |
|
115 | # TODO Do not show hidden rows in the list here | |
110 | normal_row='<div class="form-row">' |
|
116 | normal_row='<div class="form-row">' | |
111 | '<div class="form-label">' |
|
117 | '<div class="form-label">' | |
112 | '%(label)s' |
|
118 | '%(label)s' | |
113 | '</div>' |
|
119 | '</div>' | |
114 | '<div class="form-input">' |
|
120 | '<div class="form-input">' | |
115 | '%(field)s' |
|
121 | '%(field)s' | |
116 | '</div>' |
|
122 | '</div>' | |
117 | '</div>' |
|
123 | '</div>' | |
118 | '<div class="form-row">' |
|
124 | '<div class="form-row">' | |
119 | '%(help_text)s' |
|
125 | '%(help_text)s' | |
120 | '</div>', |
|
126 | '</div>', | |
121 | error_row='<div class="form-row">' |
|
127 | error_row='<div class="form-row">' | |
122 | '<div class="form-label"></div>' |
|
128 | '<div class="form-label"></div>' | |
123 | '<div class="form-errors">%s</div>' |
|
129 | '<div class="form-errors">%s</div>' | |
124 | '</div>', |
|
130 | '</div>', | |
125 | row_ender='</div>', |
|
131 | row_ender='</div>', | |
126 | help_text_html='%s', |
|
132 | help_text_html='%s', | |
127 | errors_on_separate_row=True) |
|
133 | errors_on_separate_row=True) | |
128 |
|
134 | |||
129 | def as_json_errors(self): |
|
135 | def as_json_errors(self): | |
130 | errors = [] |
|
136 | errors = [] | |
131 |
|
137 | |||
132 | for name, field in list(self.fields.items()): |
|
138 | for name, field in list(self.fields.items()): | |
133 | if self[name].errors: |
|
139 | if self[name].errors: | |
134 | errors.append({ |
|
140 | errors.append({ | |
135 | 'field': name, |
|
141 | 'field': name, | |
136 | 'errors': self[name].errors.as_text(), |
|
142 | 'errors': self[name].errors.as_text(), | |
137 | }) |
|
143 | }) | |
138 |
|
144 | |||
139 | return errors |
|
145 | return errors | |
140 |
|
146 | |||
141 |
|
147 | |||
142 | class PostForm(NeboardForm): |
|
148 | class PostForm(NeboardForm): | |
143 |
|
149 | |||
144 | title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False, |
|
150 | title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False, | |
145 | label=LABEL_TITLE, |
|
151 | label=LABEL_TITLE, | |
146 | widget=forms.TextInput( |
|
152 | widget=forms.TextInput( | |
147 | attrs={ATTRIBUTE_PLACEHOLDER: |
|
153 | attrs={ATTRIBUTE_PLACEHOLDER: | |
148 | 'test#tripcode'})) |
|
154 | 'test#tripcode'})) | |
149 | text = forms.CharField( |
|
155 | text = forms.CharField( | |
150 | widget=FormatPanel(attrs={ |
|
156 | widget=FormatPanel(attrs={ | |
151 | ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER, |
|
157 | ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER, | |
152 | ATTRIBUTE_ROWS: TEXTAREA_ROWS, |
|
158 | ATTRIBUTE_ROWS: TEXTAREA_ROWS, | |
153 | }), |
|
159 | }), | |
154 | required=False, label=LABEL_TEXT) |
|
160 | required=False, label=LABEL_TEXT) | |
155 | file = forms.FileField(required=False, label=_('File'), |
|
161 | file = forms.FileField(required=False, label=_('File'), | |
156 | widget=forms.ClearableFileInput( |
|
162 | widget=forms.ClearableFileInput( | |
157 | attrs={'accept': 'file/*'})) |
|
163 | attrs={'accept': 'file/*'})) | |
158 | file_url = forms.CharField(required=False, label=_('File URL'), |
|
164 | file_url = forms.CharField(required=False, label=_('File URL'), | |
159 | widget=forms.TextInput( |
|
165 | widget=forms.TextInput( | |
160 | attrs={ATTRIBUTE_PLACEHOLDER: |
|
166 | attrs={ATTRIBUTE_PLACEHOLDER: | |
161 | 'http://example.com/image.png'})) |
|
167 | 'http://example.com/image.png'})) | |
162 |
|
168 | |||
163 | # This field is for spam prevention only |
|
169 | # This field is for spam prevention only | |
164 | email = forms.CharField(max_length=100, required=False, label=_('e-mail'), |
|
170 | email = forms.CharField(max_length=100, required=False, label=_('e-mail'), | |
165 | widget=forms.TextInput(attrs={ |
|
171 | widget=forms.TextInput(attrs={ | |
166 | 'class': 'form-email'})) |
|
172 | 'class': 'form-email'})) | |
167 | threads = forms.CharField(required=False, label=_('Additional threads'), |
|
173 | threads = forms.CharField(required=False, label=_('Additional threads'), | |
168 | widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: |
|
174 | widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: | |
169 | '123 456 789'})) |
|
175 | '123 456 789'})) | |
170 |
|
176 | |||
|
177 | guess = forms.CharField(widget=forms.HiddenInput(), required=False) | |||
|
178 | timestamp = forms.CharField(widget=forms.HiddenInput(), required=False) | |||
|
179 | iteration = forms.CharField(widget=forms.HiddenInput(), required=False) | |||
|
180 | ||||
171 | session = None |
|
181 | session = None | |
172 | need_to_ban = False |
|
182 | need_to_ban = False | |
173 |
|
183 | |||
174 | def _update_file_extension(self, file): |
|
184 | def _update_file_extension(self, file): | |
175 | if file: |
|
185 | if file: | |
176 | mimetype = get_file_mimetype(file) |
|
186 | mimetype = get_file_mimetype(file) | |
177 | extension = MIMETYPE_EXTENSIONS.get(mimetype) |
|
187 | extension = MIMETYPE_EXTENSIONS.get(mimetype) | |
178 | if extension: |
|
188 | if extension: | |
179 | filename = file.name.split(FILE_EXTENSION_DELIMITER, 1)[0] |
|
189 | filename = file.name.split(FILE_EXTENSION_DELIMITER, 1)[0] | |
180 | new_filename = filename + FILE_EXTENSION_DELIMITER + extension |
|
190 | new_filename = filename + FILE_EXTENSION_DELIMITER + extension | |
181 |
|
191 | |||
182 | file.name = new_filename |
|
192 | file.name = new_filename | |
183 | else: |
|
193 | else: | |
184 | logger = logging.getLogger('boards.forms.extension') |
|
194 | logger = logging.getLogger('boards.forms.extension') | |
185 |
|
195 | |||
186 | logger.info('Unrecognized file mimetype: {}'.format(mimetype)) |
|
196 | logger.info('Unrecognized file mimetype: {}'.format(mimetype)) | |
187 |
|
197 | |||
188 | def clean_title(self): |
|
198 | def clean_title(self): | |
189 | title = self.cleaned_data['title'] |
|
199 | title = self.cleaned_data['title'] | |
190 | if title: |
|
200 | if title: | |
191 | if len(title) > TITLE_MAX_LENGTH: |
|
201 | if len(title) > TITLE_MAX_LENGTH: | |
192 | raise forms.ValidationError(_('Title must have less than %s ' |
|
202 | raise forms.ValidationError(_('Title must have less than %s ' | |
193 | 'characters') % |
|
203 | 'characters') % | |
194 | str(TITLE_MAX_LENGTH)) |
|
204 | str(TITLE_MAX_LENGTH)) | |
195 | return title |
|
205 | return title | |
196 |
|
206 | |||
197 | def clean_text(self): |
|
207 | def clean_text(self): | |
198 | text = self.cleaned_data['text'].strip() |
|
208 | text = self.cleaned_data['text'].strip() | |
199 | if text: |
|
209 | if text: | |
200 | max_length = board_settings.get_int('Forms', 'MaxTextLength') |
|
210 | max_length = board_settings.get_int('Forms', 'MaxTextLength') | |
201 | if len(text) > max_length: |
|
211 | if len(text) > max_length: | |
202 | raise forms.ValidationError(_('Text must have less than %s ' |
|
212 | raise forms.ValidationError(_('Text must have less than %s ' | |
203 | 'characters') % str(max_length)) |
|
213 | 'characters') % str(max_length)) | |
204 | return text |
|
214 | return text | |
205 |
|
215 | |||
206 | def clean_file(self): |
|
216 | def clean_file(self): | |
207 | file = self.cleaned_data['file'] |
|
217 | file = self.cleaned_data['file'] | |
208 |
|
218 | |||
209 | if file: |
|
219 | if file: | |
210 | validate_file_size(file.size) |
|
220 | validate_file_size(file.size) | |
211 | self._update_file_extension(file) |
|
221 | self._update_file_extension(file) | |
212 |
|
222 | |||
213 | return file |
|
223 | return file | |
214 |
|
224 | |||
215 | def clean_file_url(self): |
|
225 | def clean_file_url(self): | |
216 | url = self.cleaned_data['file_url'] |
|
226 | url = self.cleaned_data['file_url'] | |
217 |
|
227 | |||
218 | file = None |
|
228 | file = None | |
219 | if url: |
|
229 | if url: | |
220 | file = self._get_file_from_url(url) |
|
230 | file = self._get_file_from_url(url) | |
221 |
|
231 | |||
222 | if not file: |
|
232 | if not file: | |
223 | raise forms.ValidationError(_('Invalid URL')) |
|
233 | raise forms.ValidationError(_('Invalid URL')) | |
224 | else: |
|
234 | else: | |
225 | validate_file_size(file.size) |
|
235 | validate_file_size(file.size) | |
226 | self._update_file_extension(file) |
|
236 | self._update_file_extension(file) | |
227 |
|
237 | |||
228 | return file |
|
238 | return file | |
229 |
|
239 | |||
230 | def clean_threads(self): |
|
240 | def clean_threads(self): | |
231 | threads_str = self.cleaned_data['threads'] |
|
241 | threads_str = self.cleaned_data['threads'] | |
232 |
|
242 | |||
233 | if len(threads_str) > 0: |
|
243 | if len(threads_str) > 0: | |
234 | threads_id_list = threads_str.split(' ') |
|
244 | threads_id_list = threads_str.split(' ') | |
235 |
|
245 | |||
236 | threads = list() |
|
246 | threads = list() | |
237 |
|
247 | |||
238 | for thread_id in threads_id_list: |
|
248 | for thread_id in threads_id_list: | |
239 | try: |
|
249 | try: | |
240 | thread = Post.objects.get(id=int(thread_id)) |
|
250 | thread = Post.objects.get(id=int(thread_id)) | |
241 | if not thread.is_opening() or thread.get_thread().archived: |
|
251 | if not thread.is_opening() or thread.get_thread().is_archived(): | |
242 | raise ObjectDoesNotExist() |
|
252 | raise ObjectDoesNotExist() | |
243 | threads.append(thread) |
|
253 | threads.append(thread) | |
244 | except (ObjectDoesNotExist, ValueError): |
|
254 | except (ObjectDoesNotExist, ValueError): | |
245 | raise forms.ValidationError(_('Invalid additional thread list')) |
|
255 | raise forms.ValidationError(_('Invalid additional thread list')) | |
246 |
|
256 | |||
247 | return threads |
|
257 | return threads | |
248 |
|
258 | |||
249 | def clean(self): |
|
259 | def clean(self): | |
250 | cleaned_data = super(PostForm, self).clean() |
|
260 | cleaned_data = super(PostForm, self).clean() | |
251 |
|
261 | |||
252 | if cleaned_data['email']: |
|
262 | if cleaned_data['email']: | |
253 | self.need_to_ban = True |
|
263 | self.need_to_ban = True | |
254 | raise forms.ValidationError('A human cannot enter a hidden field') |
|
264 | raise forms.ValidationError('A human cannot enter a hidden field') | |
255 |
|
265 | |||
256 | if not self.errors: |
|
266 | if not self.errors: | |
257 | self._clean_text_file() |
|
267 | self._clean_text_file() | |
258 |
|
268 | |||
259 | if not self.errors and self.session: |
|
269 | limit_speed = board_settings.get_bool('Forms', 'LimitPostingSpeed') | |
260 | self._validate_posting_speed() |
|
270 | if not self.errors and limit_speed: | |
|
271 | pow_difficulty = board_settings.get_int('Forms', 'PowDifficulty') | |||
|
272 | if pow_difficulty > 0 and cleaned_data['timestamp'] and cleaned_data['iteration'] and cleaned_data['guess']: | |||
|
273 | self._validate_hash(cleaned_data['timestamp'], cleaned_data['iteration'], cleaned_data['guess'], cleaned_data['text']) | |||
|
274 | else: | |||
|
275 | self._validate_posting_speed() | |||
261 |
|
276 | |||
262 | return cleaned_data |
|
277 | return cleaned_data | |
263 |
|
278 | |||
264 | def get_file(self): |
|
279 | def get_file(self): | |
265 | """ |
|
280 | """ | |
266 | Gets file from form or URL. |
|
281 | Gets file from form or URL. | |
267 | """ |
|
282 | """ | |
268 |
|
283 | |||
269 | file = self.cleaned_data['file'] |
|
284 | file = self.cleaned_data['file'] | |
270 | return file or self.cleaned_data['file_url'] |
|
285 | return file or self.cleaned_data['file_url'] | |
271 |
|
286 | |||
272 | def get_tripcode(self): |
|
287 | def get_tripcode(self): | |
273 | title = self.cleaned_data['title'] |
|
288 | title = self.cleaned_data['title'] | |
274 | if title is not None and TRIPCODE_DELIM in title: |
|
289 | if title is not None and TRIPCODE_DELIM in title: | |
275 | code = title.split(TRIPCODE_DELIM, maxsplit=1)[1] + neboard.settings.SECRET_KEY |
|
290 | code = title.split(TRIPCODE_DELIM, maxsplit=1)[1] + neboard.settings.SECRET_KEY | |
276 | tripcode = hashlib.md5(code.encode()).hexdigest() |
|
291 | tripcode = hashlib.md5(code.encode()).hexdigest() | |
277 | else: |
|
292 | else: | |
278 | tripcode = '' |
|
293 | tripcode = '' | |
279 | return tripcode |
|
294 | return tripcode | |
280 |
|
295 | |||
281 | def get_title(self): |
|
296 | def get_title(self): | |
282 | title = self.cleaned_data['title'] |
|
297 | title = self.cleaned_data['title'] | |
283 | if title is not None and TRIPCODE_DELIM in title: |
|
298 | if title is not None and TRIPCODE_DELIM in title: | |
284 | return title.split(TRIPCODE_DELIM, maxsplit=1)[0] |
|
299 | return title.split(TRIPCODE_DELIM, maxsplit=1)[0] | |
285 | else: |
|
300 | else: | |
286 | return title |
|
301 | return title | |
287 |
|
302 | |||
288 | def _clean_text_file(self): |
|
303 | def _clean_text_file(self): | |
289 | text = self.cleaned_data.get('text') |
|
304 | text = self.cleaned_data.get('text') | |
290 | file = self.get_file() |
|
305 | file = self.get_file() | |
291 |
|
306 | |||
292 | if (not text) and (not file): |
|
307 | if (not text) and (not file): | |
293 | error_message = _('Either text or file must be entered.') |
|
308 | error_message = _('Either text or file must be entered.') | |
294 | self._errors['text'] = self.error_class([error_message]) |
|
309 | self._errors['text'] = self.error_class([error_message]) | |
295 |
|
310 | |||
296 | def _validate_posting_speed(self): |
|
311 | def _validate_posting_speed(self): | |
297 | can_post = True |
|
312 | can_post = True | |
298 |
|
313 | |||
299 | posting_delay = settings.POSTING_DELAY |
|
314 | posting_delay = settings.POSTING_DELAY | |
300 |
|
315 | |||
301 | if board_settings.get_bool('Forms', 'LimitPostingSpeed'): |
|
316 | if board_settings.get_bool('Forms', 'LimitPostingSpeed'): | |
302 | now = time.time() |
|
317 | now = time.time() | |
303 |
|
318 | |||
304 | current_delay = 0 |
|
319 | current_delay = 0 | |
305 |
|
320 | |||
306 | if LAST_POST_TIME not in self.session: |
|
321 | if LAST_POST_TIME not in self.session: | |
307 | self.session[LAST_POST_TIME] = now |
|
322 | self.session[LAST_POST_TIME] = now | |
308 |
|
323 | |||
309 | need_delay = True |
|
324 | need_delay = True | |
310 | else: |
|
325 | else: | |
311 | last_post_time = self.session.get(LAST_POST_TIME) |
|
326 | last_post_time = self.session.get(LAST_POST_TIME) | |
312 | current_delay = int(now - last_post_time) |
|
327 | current_delay = int(now - last_post_time) | |
313 |
|
328 | |||
314 | need_delay = current_delay < posting_delay |
|
329 | need_delay = current_delay < posting_delay | |
315 |
|
330 | |||
316 | if need_delay: |
|
331 | if need_delay: | |
317 | delay = posting_delay - current_delay |
|
332 | delay = posting_delay - current_delay | |
318 | error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL, |
|
333 | error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL, | |
319 | delay) % {'delay': delay} |
|
334 | delay) % {'delay': delay} | |
320 | self._errors['text'] = self.error_class([error_message]) |
|
335 | self._errors['text'] = self.error_class([error_message]) | |
321 |
|
336 | |||
322 | can_post = False |
|
337 | can_post = False | |
323 |
|
338 | |||
324 | if can_post: |
|
339 | if can_post: | |
325 | self.session[LAST_POST_TIME] = now |
|
340 | self.session[LAST_POST_TIME] = now | |
326 |
|
341 | |||
327 | def _get_file_from_url(self, url: str) -> SimpleUploadedFile: |
|
342 | def _get_file_from_url(self, url: str) -> SimpleUploadedFile: | |
328 | """ |
|
343 | """ | |
329 | Gets an file file from URL. |
|
344 | Gets an file file from URL. | |
330 | """ |
|
345 | """ | |
331 |
|
346 | |||
332 | img_temp = None |
|
347 | img_temp = None | |
333 |
|
348 | |||
334 | try: |
|
349 | try: | |
335 | for downloader in Downloader.__subclasses__(): |
|
350 | for downloader in Downloader.__subclasses__(): | |
336 | if downloader.handles(url): |
|
351 | if downloader.handles(url): | |
337 | return downloader.download(url) |
|
352 | return downloader.download(url) | |
338 | # If nobody of the specific downloaders handles this, use generic |
|
353 | # If nobody of the specific downloaders handles this, use generic | |
339 | # one |
|
354 | # one | |
340 | return Downloader.download(url) |
|
355 | return Downloader.download(url) | |
341 | except forms.ValidationError as e: |
|
356 | except forms.ValidationError as e: | |
342 | raise e |
|
357 | raise e | |
343 | except Exception as e: |
|
358 | except Exception as e: | |
344 | # Just return no file |
|
359 | raise forms.ValidationError(e) | |
345 | pass |
|
360 | ||
|
361 | def _validate_hash(self, timestamp: str, iteration: str, guess: str, message: str): | |||
|
362 | post_time = timezone.datetime.fromtimestamp( | |||
|
363 | int(timestamp[:-3]), tz=timezone.get_current_timezone()) | |||
|
364 | timedelta = (timezone.now() - post_time).seconds / 60 | |||
|
365 | if timedelta > POW_LIFE_MINUTES: | |||
|
366 | self._errors['text'] = self.error_class([_('Stale PoW.')]) | |||
|
367 | ||||
|
368 | payload = timestamp + message.replace('\r\n', '\n') | |||
|
369 | difficulty = board_settings.get_int('Forms', 'PowDifficulty') | |||
|
370 | target = str(int(2 ** (POW_HASH_LENGTH * 3) / difficulty)) | |||
|
371 | if len(target) < POW_HASH_LENGTH: | |||
|
372 | target = '0' * (POW_HASH_LENGTH - len(target)) + target | |||
|
373 | ||||
|
374 | computed_guess = hashlib.sha256((payload + iteration).encode())\ | |||
|
375 | .hexdigest()[0:POW_HASH_LENGTH] | |||
|
376 | if guess != computed_guess or guess > target: | |||
|
377 | self._errors['text'] = self.error_class( | |||
|
378 | [_('Invalid PoW.')]) | |||
346 |
|
379 | |||
347 |
|
380 | |||
348 | class ThreadForm(PostForm): |
|
381 | class ThreadForm(PostForm): | |
349 |
|
382 | |||
350 | tags = forms.CharField( |
|
383 | tags = forms.CharField( | |
351 | widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}), |
|
384 | widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}), | |
352 | max_length=100, label=_('Tags'), required=True) |
|
385 | max_length=100, label=_('Tags'), required=True) | |
|
386 | monochrome = forms.BooleanField(label=_('Monochrome'), required=False) | |||
353 |
|
387 | |||
354 | def clean_tags(self): |
|
388 | def clean_tags(self): | |
355 | tags = self.cleaned_data['tags'].strip() |
|
389 | tags = self.cleaned_data['tags'].strip() | |
356 |
|
390 | |||
357 | if not tags or not REGEX_TAGS.match(tags): |
|
391 | if not tags or not REGEX_TAGS.match(tags): | |
358 | raise forms.ValidationError( |
|
392 | raise forms.ValidationError( | |
359 | _('Inappropriate characters in tags.')) |
|
393 | _('Inappropriate characters in tags.')) | |
360 |
|
394 | |||
361 | required_tag_exists = False |
|
395 | required_tag_exists = False | |
362 | tag_set = set() |
|
396 | tag_set = set() | |
363 | for tag_string in tags.split(): |
|
397 | for tag_string in tags.split(): | |
364 | tag, created = Tag.objects.get_or_create(name=tag_string.strip().lower()) |
|
398 | tag, created = Tag.objects.get_or_create(name=tag_string.strip().lower()) | |
365 | tag_set.add(tag) |
|
399 | tag_set.add(tag) | |
366 |
|
400 | |||
367 | # If this is a new tag, don't check for its parents because nobody |
|
401 | # If this is a new tag, don't check for its parents because nobody | |
368 | # added them yet |
|
402 | # added them yet | |
369 | if not created: |
|
403 | if not created: | |
370 | tag_set |= set(tag.get_all_parents()) |
|
404 | tag_set |= set(tag.get_all_parents()) | |
371 |
|
405 | |||
372 | for tag in tag_set: |
|
406 | for tag in tag_set: | |
373 | if tag.required: |
|
407 | if tag.required: | |
374 | required_tag_exists = True |
|
408 | required_tag_exists = True | |
375 | break |
|
409 | break | |
376 |
|
410 | |||
377 | if not required_tag_exists: |
|
411 | if not required_tag_exists: | |
378 | raise forms.ValidationError( |
|
412 | raise forms.ValidationError( | |
379 | _('Need at least one section.')) |
|
413 | _('Need at least one section.')) | |
380 |
|
414 | |||
381 | return tag_set |
|
415 | return tag_set | |
382 |
|
416 | |||
383 | def clean(self): |
|
417 | def clean(self): | |
384 | cleaned_data = super(ThreadForm, self).clean() |
|
418 | cleaned_data = super(ThreadForm, self).clean() | |
385 |
|
419 | |||
386 | return cleaned_data |
|
420 | return cleaned_data | |
387 |
|
421 | |||
|
422 | def is_monochrome(self): | |||
|
423 | return self.cleaned_data['monochrome'] | |||
|
424 | ||||
388 |
|
425 | |||
389 | class SettingsForm(NeboardForm): |
|
426 | class SettingsForm(NeboardForm): | |
390 |
|
427 | |||
391 | theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme')) |
|
428 | theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme')) | |
392 | image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('Image view mode')) |
|
429 | image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('Image view mode')) | |
393 | username = forms.CharField(label=_('User name'), required=False) |
|
430 | username = forms.CharField(label=_('User name'), required=False) | |
394 | timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone')) |
|
431 | timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone')) | |
395 |
|
432 | |||
396 | def clean_username(self): |
|
433 | def clean_username(self): | |
397 | username = self.cleaned_data['username'] |
|
434 | username = self.cleaned_data['username'] | |
398 |
|
435 | |||
399 |
if username and not REGEX_ |
|
436 | if username and not REGEX_USERNAMES.match(username): | |
400 | raise forms.ValidationError(_('Inappropriate characters.')) |
|
437 | raise forms.ValidationError(_('Inappropriate characters.')) | |
401 |
|
438 | |||
402 | return username |
|
439 | return username | |
403 |
|
440 | |||
404 |
|
441 | |||
405 | class SearchForm(NeboardForm): |
|
442 | class SearchForm(NeboardForm): | |
406 | query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False) |
|
443 | query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False) |
1 | NO CONTENT: modified file, binary diff hidden |
|
NO CONTENT: modified file, binary diff hidden |
@@ -1,501 +1,532 | |||||
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: 2015-10-09 23:21+0300\n" |
|
10 | "POT-Creation-Date: 2015-10-09 23:21+0300\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 | #: admin.py:22 |
|
21 | #: admin.py:22 | |
22 | msgid "{} posters were banned" |
|
22 | msgid "{} posters were banned" | |
23 | msgstr "" |
|
23 | msgstr "" | |
24 |
|
24 | |||
25 | #: authors.py:9 |
|
25 | #: authors.py:9 | |
26 | msgid "author" |
|
26 | msgid "author" | |
27 | msgstr "автор" |
|
27 | msgstr "автор" | |
28 |
|
28 | |||
29 | #: authors.py:10 |
|
29 | #: authors.py:10 | |
30 | msgid "developer" |
|
30 | msgid "developer" | |
31 | msgstr "разработчик" |
|
31 | msgstr "разработчик" | |
32 |
|
32 | |||
33 | #: authors.py:11 |
|
33 | #: authors.py:11 | |
34 | msgid "javascript developer" |
|
34 | msgid "javascript developer" | |
35 | msgstr "разработчик javascript" |
|
35 | msgstr "разработчик javascript" | |
36 |
|
36 | |||
37 | #: authors.py:12 |
|
37 | #: authors.py:12 | |
38 | msgid "designer" |
|
38 | msgid "designer" | |
39 | msgstr "дизайнер" |
|
39 | msgstr "дизайнер" | |
40 |
|
40 | |||
41 | #: forms.py:30 |
|
41 | #: forms.py:30 | |
42 | msgid "Type message here. Use formatting panel for more advanced usage." |
|
42 | msgid "Type message here. Use formatting panel for more advanced usage." | |
43 | msgstr "" |
|
43 | msgstr "" | |
44 | "Вводите сообщение сюда. Используйте панель для более сложного форматирования." |
|
44 | "Вводите сообщение сюда. Используйте панель для более сложного форматирования." | |
45 |
|
45 | |||
46 | #: forms.py:31 |
|
46 | #: forms.py:31 | |
47 | msgid "music images i_dont_like_tags" |
|
47 | msgid "music images i_dont_like_tags" | |
48 | msgstr "музыка картинки теги_не_нужны" |
|
48 | msgstr "музыка картинки теги_не_нужны" | |
49 |
|
49 | |||
50 | #: forms.py:33 |
|
50 | #: forms.py:33 | |
51 | msgid "Title" |
|
51 | msgid "Title" | |
52 | msgstr "Заголовок" |
|
52 | msgstr "Заголовок" | |
53 |
|
53 | |||
54 | #: forms.py:34 |
|
54 | #: forms.py:34 | |
55 | msgid "Text" |
|
55 | msgid "Text" | |
56 | msgstr "Текст" |
|
56 | msgstr "Текст" | |
57 |
|
57 | |||
58 | #: forms.py:35 |
|
58 | #: forms.py:35 | |
59 | msgid "Tag" |
|
59 | msgid "Tag" | |
60 | msgstr "Метка" |
|
60 | msgstr "Метка" | |
61 |
|
61 | |||
62 | #: forms.py:36 templates/boards/base.html:40 templates/search/search.html:7 |
|
62 | #: forms.py:36 templates/boards/base.html:40 templates/search/search.html:7 | |
63 | msgid "Search" |
|
63 | msgid "Search" | |
64 | msgstr "Поиск" |
|
64 | msgstr "Поиск" | |
65 |
|
65 | |||
66 | #: forms.py:139 |
|
66 | #: forms.py:139 | |
67 | msgid "File" |
|
67 | msgid "File" | |
68 | msgstr "Файл" |
|
68 | msgstr "Файл" | |
69 |
|
69 | |||
70 | #: forms.py:142 |
|
70 | #: forms.py:142 | |
71 | msgid "File URL" |
|
71 | msgid "File URL" | |
72 | msgstr "URL файла" |
|
72 | msgstr "URL файла" | |
73 |
|
73 | |||
74 | #: forms.py:148 |
|
74 | #: forms.py:148 | |
75 | msgid "e-mail" |
|
75 | msgid "e-mail" | |
76 | msgstr "" |
|
76 | msgstr "" | |
77 |
|
77 | |||
78 | #: forms.py:151 |
|
78 | #: forms.py:151 | |
79 | msgid "Additional threads" |
|
79 | msgid "Additional threads" | |
80 | msgstr "Дополнительные темы" |
|
80 | msgstr "Дополнительные темы" | |
81 |
|
81 | |||
82 | #: forms.py:162 |
|
82 | #: forms.py:162 | |
83 | #, python-format |
|
83 | #, python-format | |
84 | msgid "Title must have less than %s characters" |
|
84 | msgid "Title must have less than %s characters" | |
85 | msgstr "Заголовок должен иметь меньше %s символов" |
|
85 | msgstr "Заголовок должен иметь меньше %s символов" | |
86 |
|
86 | |||
87 | #: forms.py:172 |
|
87 | #: forms.py:172 | |
88 | #, python-format |
|
88 | #, python-format | |
89 | msgid "Text must have less than %s characters" |
|
89 | msgid "Text must have less than %s characters" | |
90 | msgstr "Текст должен быть короче %s символов" |
|
90 | msgstr "Текст должен быть короче %s символов" | |
91 |
|
91 | |||
92 | #: forms.py:192 |
|
92 | #: forms.py:192 | |
93 | msgid "Invalid URL" |
|
93 | msgid "Invalid URL" | |
94 | msgstr "Неверный URL" |
|
94 | msgstr "Неверный URL" | |
95 |
|
95 | |||
96 | #: forms.py:213 |
|
96 | #: forms.py:213 | |
97 | msgid "Invalid additional thread list" |
|
97 | msgid "Invalid additional thread list" | |
98 | msgstr "Неверный список дополнительных тем" |
|
98 | msgstr "Неверный список дополнительных тем" | |
99 |
|
99 | |||
100 | #: forms.py:258 |
|
100 | #: forms.py:258 | |
101 | msgid "Either text or file must be entered." |
|
101 | msgid "Either text or file must be entered." | |
102 | msgstr "Текст или файл должны быть введены." |
|
102 | msgstr "Текст или файл должны быть введены." | |
103 |
|
103 | |||
104 | #: forms.py:317 templates/boards/all_threads.html:153 |
|
104 | #: forms.py:317 templates/boards/all_threads.html:153 | |
105 | #: templates/boards/rss/post.html:10 templates/boards/tags.html:6 |
|
105 | #: templates/boards/rss/post.html:10 templates/boards/tags.html:6 | |
106 | msgid "Tags" |
|
106 | msgid "Tags" | |
107 | msgstr "Метки" |
|
107 | msgstr "Метки" | |
108 |
|
108 | |||
109 | #: forms.py:324 |
|
109 | #: forms.py:324 | |
110 | msgid "Inappropriate characters in tags." |
|
110 | msgid "Inappropriate characters in tags." | |
111 | msgstr "Недопустимые символы в метках." |
|
111 | msgstr "Недопустимые символы в метках." | |
112 |
|
112 | |||
113 | #: forms.py:344 |
|
113 | #: forms.py:344 | |
114 | msgid "Need at least one section." |
|
114 | msgid "Need at least one section." | |
115 | msgstr "Нужен хотя бы один раздел." |
|
115 | msgstr "Нужен хотя бы один раздел." | |
116 |
|
116 | |||
117 | #: forms.py:356 |
|
117 | #: forms.py:356 | |
118 | msgid "Theme" |
|
118 | msgid "Theme" | |
119 | msgstr "Тема" |
|
119 | msgstr "Тема" | |
120 |
|
120 | |||
121 | #: forms.py:357 |
|
121 | #: forms.py:357 | |
122 | msgid "Image view mode" |
|
122 | msgid "Image view mode" | |
123 | msgstr "Режим просмотра изображений" |
|
123 | msgstr "Режим просмотра изображений" | |
124 |
|
124 | |||
125 | #: forms.py:358 |
|
125 | #: forms.py:358 | |
126 | msgid "User name" |
|
126 | msgid "User name" | |
127 | msgstr "Имя пользователя" |
|
127 | msgstr "Имя пользователя" | |
128 |
|
128 | |||
129 | #: forms.py:359 |
|
129 | #: forms.py:359 | |
130 | msgid "Time zone" |
|
130 | msgid "Time zone" | |
131 | msgstr "Часовой пояс" |
|
131 | msgstr "Часовой пояс" | |
132 |
|
132 | |||
133 | #: forms.py:365 |
|
133 | #: forms.py:365 | |
134 | msgid "Inappropriate characters." |
|
134 | msgid "Inappropriate characters." | |
135 | msgstr "Недопустимые символы." |
|
135 | msgstr "Недопустимые символы." | |
136 |
|
136 | |||
137 | #: templates/boards/404.html:6 |
|
137 | #: templates/boards/404.html:6 | |
138 | msgid "Not found" |
|
138 | msgid "Not found" | |
139 | msgstr "Не найдено" |
|
139 | msgstr "Не найдено" | |
140 |
|
140 | |||
141 | #: templates/boards/404.html:12 |
|
141 | #: templates/boards/404.html:12 | |
142 | msgid "This page does not exist" |
|
142 | msgid "This page does not exist" | |
143 | msgstr "Этой страницы не существует" |
|
143 | msgstr "Этой страницы не существует" | |
144 |
|
144 | |||
145 | #: templates/boards/all_threads.html:35 |
|
145 | #: templates/boards/all_threads.html:35 | |
146 | msgid "Related message" |
|
146 | msgid "Details" | |
147 | msgstr "Связанное сообщение" |
|
147 | msgstr "Подробности" | |
148 |
|
148 | |||
149 | #: templates/boards/all_threads.html:69 |
|
149 | #: templates/boards/all_threads.html:69 | |
150 | msgid "Edit tag" |
|
150 | msgid "Edit tag" | |
151 | msgstr "Изменить метку" |
|
151 | msgstr "Изменить метку" | |
152 |
|
152 | |||
153 | #: templates/boards/all_threads.html:76 |
|
153 | #: templates/boards/all_threads.html:76 | |
154 | #, python-format |
|
154 | #, python-format | |
155 | msgid "%(count)s active thread" |
|
155 | msgid "%(count)s active thread" | |
156 | msgid_plural "%(count)s active threads" |
|
156 | msgid_plural "%(count)s active threads" | |
157 | msgstr[0] "%(count)s активная тема" |
|
157 | msgstr[0] "%(count)s активная тема" | |
158 | msgstr[1] "%(count)s активные темы" |
|
158 | msgstr[1] "%(count)s активные темы" | |
159 | msgstr[2] "%(count)s активных тем" |
|
159 | msgstr[2] "%(count)s активных тем" | |
160 |
|
160 | |||
161 | #: templates/boards/all_threads.html:76 |
|
161 | #: templates/boards/all_threads.html:76 | |
162 | #, python-format |
|
162 | #, python-format | |
163 | msgid "%(count)s thread in bumplimit" |
|
163 | msgid "%(count)s thread in bumplimit" | |
164 | msgid_plural "%(count)s threads in bumplimit" |
|
164 | msgid_plural "%(count)s threads in bumplimit" | |
165 | msgstr[0] "%(count)s тема в бамплимите" |
|
165 | msgstr[0] "%(count)s тема в бамплимите" | |
166 | msgstr[1] "%(count)s темы в бамплимите" |
|
166 | msgstr[1] "%(count)s темы в бамплимите" | |
167 | msgstr[2] "%(count)s тем в бамплимите" |
|
167 | msgstr[2] "%(count)s тем в бамплимите" | |
168 |
|
168 | |||
169 | #: templates/boards/all_threads.html:77 |
|
169 | #: templates/boards/all_threads.html:77 | |
170 | #, python-format |
|
170 | #, python-format | |
171 | msgid "%(count)s archived thread" |
|
171 | msgid "%(count)s archived thread" | |
172 | msgid_plural "%(count)s archived thread" |
|
172 | msgid_plural "%(count)s archived thread" | |
173 | msgstr[0] "%(count)s архивная тема" |
|
173 | msgstr[0] "%(count)s архивная тема" | |
174 | msgstr[1] "%(count)s архивные темы" |
|
174 | msgstr[1] "%(count)s архивные темы" | |
175 | msgstr[2] "%(count)s архивных тем" |
|
175 | msgstr[2] "%(count)s архивных тем" | |
176 |
|
176 | |||
177 | #: templates/boards/all_threads.html:78 templates/boards/post.html:102 |
|
177 | #: templates/boards/all_threads.html:78 templates/boards/post.html:102 | |
178 | #, python-format |
|
178 | #, python-format | |
179 | #| msgid "%(count)s message" |
|
179 | #| msgid "%(count)s message" | |
180 | #| msgid_plural "%(count)s messages" |
|
180 | #| msgid_plural "%(count)s messages" | |
181 | msgid "%(count)s message" |
|
181 | msgid "%(count)s message" | |
182 | msgid_plural "%(count)s messages" |
|
182 | msgid_plural "%(count)s messages" | |
183 | msgstr[0] "%(count)s сообщение" |
|
183 | msgstr[0] "%(count)s сообщение" | |
184 | msgstr[1] "%(count)s сообщения" |
|
184 | msgstr[1] "%(count)s сообщения" | |
185 | msgstr[2] "%(count)s сообщений" |
|
185 | msgstr[2] "%(count)s сообщений" | |
186 |
|
186 | |||
187 | #: templates/boards/all_threads.html:95 templates/boards/feed.html:30 |
|
187 | #: templates/boards/all_threads.html:95 templates/boards/feed.html:30 | |
188 | #: templates/boards/notifications.html:17 templates/search/search.html:26 |
|
188 | #: templates/boards/notifications.html:17 templates/search/search.html:26 | |
189 | msgid "Previous page" |
|
189 | msgid "Previous page" | |
190 | msgstr "Предыдущая страница" |
|
190 | msgstr "Предыдущая страница" | |
191 |
|
191 | |||
192 | #: templates/boards/all_threads.html:109 |
|
192 | #: templates/boards/all_threads.html:109 | |
193 | #, python-format |
|
193 | #, python-format | |
194 | msgid "Skipped %(count)s reply. Open thread to see all replies." |
|
194 | msgid "Skipped %(count)s reply. Open thread to see all replies." | |
195 | msgid_plural "Skipped %(count)s replies. Open thread to see all replies." |
|
195 | msgid_plural "Skipped %(count)s replies. Open thread to see all replies." | |
196 | msgstr[0] "Пропущен %(count)s ответ. Откройте тред, чтобы увидеть все ответы." |
|
196 | msgstr[0] "Пропущен %(count)s ответ. Откройте тред, чтобы увидеть все ответы." | |
197 | msgstr[1] "" |
|
197 | msgstr[1] "" | |
198 | "Пропущено %(count)s ответа. Откройте тред, чтобы увидеть все ответы." |
|
198 | "Пропущено %(count)s ответа. Откройте тред, чтобы увидеть все ответы." | |
199 | msgstr[2] "" |
|
199 | msgstr[2] "" | |
200 | "Пропущено %(count)s ответов. Откройте тред, чтобы увидеть все ответы." |
|
200 | "Пропущено %(count)s ответов. Откройте тред, чтобы увидеть все ответы." | |
201 |
|
201 | |||
202 | #: templates/boards/all_threads.html:127 templates/boards/feed.html:40 |
|
202 | #: templates/boards/all_threads.html:127 templates/boards/feed.html:40 | |
203 | #: templates/boards/notifications.html:27 templates/search/search.html:37 |
|
203 | #: templates/boards/notifications.html:27 templates/search/search.html:37 | |
204 | msgid "Next page" |
|
204 | msgid "Next page" | |
205 | msgstr "Следующая страница" |
|
205 | msgstr "Следующая страница" | |
206 |
|
206 | |||
207 | #: templates/boards/all_threads.html:132 |
|
207 | #: templates/boards/all_threads.html:132 | |
208 | msgid "No threads exist. Create the first one!" |
|
208 | msgid "No threads exist. Create the first one!" | |
209 | msgstr "Нет тем. Создайте первую!" |
|
209 | msgstr "Нет тем. Создайте первую!" | |
210 |
|
210 | |||
211 | #: templates/boards/all_threads.html:138 |
|
211 | #: templates/boards/all_threads.html:138 | |
212 | msgid "Create new thread" |
|
212 | msgid "Create new thread" | |
213 | msgstr "Создать новую тему" |
|
213 | msgstr "Создать новую тему" | |
214 |
|
214 | |||
215 | #: templates/boards/all_threads.html:143 templates/boards/preview.html:16 |
|
215 | #: templates/boards/all_threads.html:143 templates/boards/preview.html:16 | |
216 | #: templates/boards/thread_normal.html:51 |
|
216 | #: templates/boards/thread_normal.html:51 | |
217 | msgid "Post" |
|
217 | msgid "Post" | |
218 | msgstr "Отправить" |
|
218 | msgstr "Отправить" | |
219 |
|
219 | |||
220 | #: templates/boards/all_threads.html:144 templates/boards/preview.html:6 |
|
220 | #: templates/boards/all_threads.html:144 templates/boards/preview.html:6 | |
221 | #: templates/boards/staticpages/help.html:21 |
|
221 | #: templates/boards/staticpages/help.html:21 | |
222 | #: templates/boards/thread_normal.html:52 |
|
222 | #: templates/boards/thread_normal.html:52 | |
223 | msgid "Preview" |
|
223 | msgid "Preview" | |
224 | msgstr "Предпросмотр" |
|
224 | msgstr "Предпросмотр" | |
225 |
|
225 | |||
226 | #: templates/boards/all_threads.html:149 |
|
226 | #: templates/boards/all_threads.html:149 | |
227 | msgid "Tags must be delimited by spaces. Text or image is required." |
|
227 | msgid "Tags must be delimited by spaces. Text or image is required." | |
228 | msgstr "" |
|
228 | msgstr "" | |
229 | "Метки должны быть разделены пробелами. Текст или изображение обязательны." |
|
229 | "Метки должны быть разделены пробелами. Текст или изображение обязательны." | |
230 |
|
230 | |||
231 | #: templates/boards/all_threads.html:152 templates/boards/thread_normal.html:58 |
|
231 | #: templates/boards/all_threads.html:152 templates/boards/thread_normal.html:58 | |
232 | msgid "Text syntax" |
|
232 | msgid "Text syntax" | |
233 | msgstr "Синтаксис текста" |
|
233 | msgstr "Синтаксис текста" | |
234 |
|
234 | |||
235 | #: templates/boards/all_threads.html:166 templates/boards/feed.html:53 |
|
235 | #: templates/boards/all_threads.html:166 templates/boards/feed.html:53 | |
236 | msgid "Pages:" |
|
236 | msgid "Pages:" | |
237 | msgstr "Страницы: " |
|
237 | msgstr "Страницы: " | |
238 |
|
238 | |||
239 | #: templates/boards/authors.html:6 templates/boards/authors.html.py:12 |
|
239 | #: templates/boards/authors.html:6 templates/boards/authors.html.py:12 | |
240 | msgid "Authors" |
|
240 | msgid "Authors" | |
241 | msgstr "Авторы" |
|
241 | msgstr "Авторы" | |
242 |
|
242 | |||
243 | #: templates/boards/authors.html:26 |
|
243 | #: templates/boards/authors.html:26 | |
244 | msgid "Distributed under the" |
|
244 | msgid "Distributed under the" | |
245 | msgstr "Распространяется под" |
|
245 | msgstr "Распространяется под" | |
246 |
|
246 | |||
247 | #: templates/boards/authors.html:28 |
|
247 | #: templates/boards/authors.html:28 | |
248 | msgid "license" |
|
248 | msgid "license" | |
249 | msgstr "лицензией" |
|
249 | msgstr "лицензией" | |
250 |
|
250 | |||
251 | #: templates/boards/authors.html:30 |
|
251 | #: templates/boards/authors.html:30 | |
252 | msgid "Repository" |
|
252 | msgid "Repository" | |
253 | msgstr "Репозиторий" |
|
253 | msgstr "Репозиторий" | |
254 |
|
254 | |||
255 | #: templates/boards/base.html:14 templates/boards/base.html.py:41 |
|
255 | #: templates/boards/base.html:14 templates/boards/base.html.py:41 | |
256 | msgid "Feed" |
|
256 | msgid "Feed" | |
257 | msgstr "Лента" |
|
257 | msgstr "Лента" | |
258 |
|
258 | |||
259 | #: templates/boards/base.html:31 |
|
259 | #: templates/boards/base.html:31 | |
260 | msgid "All threads" |
|
260 | msgid "All threads" | |
261 | msgstr "Все темы" |
|
261 | msgstr "Все темы" | |
262 |
|
262 | |||
263 | #: templates/boards/base.html:37 |
|
263 | #: templates/boards/base.html:37 | |
264 | msgid "Add tags" |
|
264 | msgid "Add tags" | |
265 | msgstr "Добавить метки" |
|
265 | msgstr "Добавить метки" | |
266 |
|
266 | |||
267 | #: templates/boards/base.html:39 |
|
267 | #: templates/boards/base.html:39 | |
268 | msgid "Tag management" |
|
268 | msgid "Tag management" | |
269 | msgstr "Управление метками" |
|
269 | msgstr "Управление метками" | |
270 |
|
270 | |||
271 | #: templates/boards/base.html:39 |
|
271 | #: templates/boards/base.html:39 | |
272 | msgid "tags" |
|
272 | msgid "tags" | |
273 | msgstr "метки" |
|
273 | msgstr "метки" | |
274 |
|
274 | |||
275 | #: templates/boards/base.html:40 |
|
275 | #: templates/boards/base.html:40 | |
276 | msgid "search" |
|
276 | msgid "search" | |
277 | msgstr "поиск" |
|
277 | msgstr "поиск" | |
278 |
|
278 | |||
279 | #: templates/boards/base.html:41 templates/boards/feed.html:11 |
|
279 | #: templates/boards/base.html:41 templates/boards/feed.html:11 | |
280 | msgid "feed" |
|
280 | msgid "feed" | |
281 | msgstr "лента" |
|
281 | msgstr "лента" | |
282 |
|
282 | |||
283 | #: templates/boards/base.html:42 templates/boards/random.html:6 |
|
283 | #: templates/boards/base.html:42 templates/boards/random.html:6 | |
284 | msgid "Random images" |
|
284 | msgid "Random images" | |
285 | msgstr "Случайные изображения" |
|
285 | msgstr "Случайные изображения" | |
286 |
|
286 | |||
287 | #: templates/boards/base.html:42 |
|
287 | #: templates/boards/base.html:42 | |
288 | msgid "random" |
|
288 | msgid "random" | |
289 | msgstr "случайные" |
|
289 | msgstr "случайные" | |
290 |
|
290 | |||
291 | #: templates/boards/base.html:44 |
|
291 | #: templates/boards/base.html:44 | |
292 | msgid "favorites" |
|
292 | msgid "favorites" | |
293 | msgstr "избранное" |
|
293 | msgstr "избранное" | |
294 |
|
294 | |||
295 | #: templates/boards/base.html:48 templates/boards/base.html.py:49 |
|
295 | #: templates/boards/base.html:48 templates/boards/base.html.py:49 | |
296 | #: templates/boards/notifications.html:8 |
|
296 | #: templates/boards/notifications.html:8 | |
297 | msgid "Notifications" |
|
297 | msgid "Notifications" | |
298 | msgstr "Уведомления" |
|
298 | msgstr "Уведомления" | |
299 |
|
299 | |||
300 | #: templates/boards/base.html:56 templates/boards/settings.html:8 |
|
300 | #: templates/boards/base.html:56 templates/boards/settings.html:8 | |
301 | msgid "Settings" |
|
301 | msgid "Settings" | |
302 | msgstr "Настройки" |
|
302 | msgstr "Настройки" | |
303 |
|
303 | |||
304 | #: templates/boards/base.html:59 |
|
304 | #: templates/boards/base.html:59 | |
305 | msgid "Loading..." |
|
305 | msgid "Loading..." | |
306 | msgstr "Загрузка..." |
|
306 | msgstr "Загрузка..." | |
307 |
|
307 | |||
308 | #: templates/boards/base.html:71 |
|
308 | #: templates/boards/base.html:71 | |
309 | msgid "Admin" |
|
309 | msgid "Admin" | |
310 | msgstr "Администрирование" |
|
310 | msgstr "Администрирование" | |
311 |
|
311 | |||
312 | #: templates/boards/base.html:73 |
|
312 | #: templates/boards/base.html:73 | |
313 | #, python-format |
|
313 | #, python-format | |
314 | msgid "Speed: %(ppd)s posts per day" |
|
314 | msgid "Speed: %(ppd)s posts per day" | |
315 | msgstr "Скорость: %(ppd)s сообщений в день" |
|
315 | msgstr "Скорость: %(ppd)s сообщений в день" | |
316 |
|
316 | |||
317 | #: templates/boards/base.html:75 |
|
317 | #: templates/boards/base.html:75 | |
318 | msgid "Up" |
|
318 | msgid "Up" | |
319 | msgstr "Вверх" |
|
319 | msgstr "Вверх" | |
320 |
|
320 | |||
321 | #: templates/boards/feed.html:45 |
|
321 | #: templates/boards/feed.html:45 | |
322 | msgid "No posts exist. Create the first one!" |
|
322 | msgid "No posts exist. Create the first one!" | |
323 | msgstr "Нет сообщений. Создайте первое!" |
|
323 | msgstr "Нет сообщений. Создайте первое!" | |
324 |
|
324 | |||
325 | #: templates/boards/post.html:33 |
|
325 | #: templates/boards/post.html:33 | |
326 | msgid "Open" |
|
326 | msgid "Open" | |
327 | msgstr "Открыть" |
|
327 | msgstr "Открыть" | |
328 |
|
328 | |||
329 | #: templates/boards/post.html:35 templates/boards/post.html.py:46 |
|
329 | #: templates/boards/post.html:35 templates/boards/post.html.py:46 | |
330 | msgid "Reply" |
|
330 | msgid "Reply" | |
331 | msgstr "Ответить" |
|
331 | msgstr "Ответить" | |
332 |
|
332 | |||
333 | #: templates/boards/post.html:41 |
|
333 | #: templates/boards/post.html:41 | |
334 | msgid " in " |
|
334 | msgid " in " | |
335 | msgstr " в " |
|
335 | msgstr " в " | |
336 |
|
336 | |||
337 | #: templates/boards/post.html:51 |
|
337 | #: templates/boards/post.html:51 | |
338 | msgid "Edit" |
|
338 | msgid "Edit" | |
339 | msgstr "Изменить" |
|
339 | msgstr "Изменить" | |
340 |
|
340 | |||
341 | #: templates/boards/post.html:53 |
|
341 | #: templates/boards/post.html:53 | |
342 | msgid "Edit thread" |
|
342 | msgid "Edit thread" | |
343 | msgstr "Изменить тему" |
|
343 | msgstr "Изменить тему" | |
344 |
|
344 | |||
345 | #: templates/boards/post.html:91 |
|
345 | #: templates/boards/post.html:91 | |
346 | msgid "Replies" |
|
346 | msgid "Replies" | |
347 | msgstr "Ответы" |
|
347 | msgstr "Ответы" | |
348 |
|
348 | |||
349 | #: templates/boards/post.html:103 |
|
349 | #: templates/boards/post.html:103 | |
350 | #, python-format |
|
350 | #, python-format | |
351 | msgid "%(count)s image" |
|
351 | msgid "%(count)s image" | |
352 | msgid_plural "%(count)s images" |
|
352 | msgid_plural "%(count)s images" | |
353 | msgstr[0] "%(count)s изображение" |
|
353 | msgstr[0] "%(count)s изображение" | |
354 | msgstr[1] "%(count)s изображения" |
|
354 | msgstr[1] "%(count)s изображения" | |
355 | msgstr[2] "%(count)s изображений" |
|
355 | msgstr[2] "%(count)s изображений" | |
356 |
|
356 | |||
357 | #: templates/boards/rss/post.html:5 |
|
357 | #: templates/boards/rss/post.html:5 | |
358 | msgid "Post image" |
|
358 | msgid "Post image" | |
359 | msgstr "Изображение сообщения" |
|
359 | msgstr "Изображение сообщения" | |
360 |
|
360 | |||
361 | #: templates/boards/settings.html:15 |
|
361 | #: templates/boards/settings.html:15 | |
362 | msgid "You are moderator." |
|
362 | msgid "You are moderator." | |
363 | msgstr "Вы модератор." |
|
363 | msgstr "Вы модератор." | |
364 |
|
364 | |||
365 | #: templates/boards/settings.html:19 |
|
365 | #: templates/boards/settings.html:19 | |
366 | msgid "Hidden tags:" |
|
366 | msgid "Hidden tags:" | |
367 | msgstr "Скрытые метки:" |
|
367 | msgstr "Скрытые метки:" | |
368 |
|
368 | |||
369 | #: templates/boards/settings.html:25 |
|
369 | #: templates/boards/settings.html:25 | |
370 | msgid "No hidden tags." |
|
370 | msgid "No hidden tags." | |
371 | msgstr "Нет скрытых меток." |
|
371 | msgstr "Нет скрытых меток." | |
372 |
|
372 | |||
373 | #: templates/boards/settings.html:34 |
|
373 | #: templates/boards/settings.html:34 | |
374 | msgid "Save" |
|
374 | msgid "Save" | |
375 | msgstr "Сохранить" |
|
375 | msgstr "Сохранить" | |
376 |
|
376 | |||
377 | #: templates/boards/staticpages/banned.html:6 |
|
377 | #: templates/boards/staticpages/banned.html:6 | |
378 | msgid "Banned" |
|
378 | msgid "Banned" | |
379 | msgstr "Заблокирован" |
|
379 | msgstr "Заблокирован" | |
380 |
|
380 | |||
381 | #: templates/boards/staticpages/banned.html:11 |
|
381 | #: templates/boards/staticpages/banned.html:11 | |
382 | msgid "Your IP address has been banned. Contact the administrator" |
|
382 | msgid "Your IP address has been banned. Contact the administrator" | |
383 | msgstr "Ваш IP адрес был заблокирован. Свяжитесь с администратором" |
|
383 | msgstr "Ваш IP адрес был заблокирован. Свяжитесь с администратором" | |
384 |
|
384 | |||
385 | #: templates/boards/staticpages/help.html:6 |
|
385 | #: templates/boards/staticpages/help.html:6 | |
386 | #: templates/boards/staticpages/help.html:10 |
|
386 | #: templates/boards/staticpages/help.html:10 | |
387 | msgid "Syntax" |
|
387 | msgid "Syntax" | |
388 | msgstr "Синтаксис" |
|
388 | msgstr "Синтаксис" | |
389 |
|
389 | |||
390 | #: templates/boards/staticpages/help.html:11 |
|
390 | #: templates/boards/staticpages/help.html:11 | |
391 | msgid "Italic text" |
|
391 | msgid "Italic text" | |
392 | msgstr "Курсивный текст" |
|
392 | msgstr "Курсивный текст" | |
393 |
|
393 | |||
394 | #: templates/boards/staticpages/help.html:12 |
|
394 | #: templates/boards/staticpages/help.html:12 | |
395 | msgid "Bold text" |
|
395 | msgid "Bold text" | |
396 | msgstr "Полужирный текст" |
|
396 | msgstr "Полужирный текст" | |
397 |
|
397 | |||
398 | #: templates/boards/staticpages/help.html:13 |
|
398 | #: templates/boards/staticpages/help.html:13 | |
399 | msgid "Spoiler" |
|
399 | msgid "Spoiler" | |
400 | msgstr "Спойлер" |
|
400 | msgstr "Спойлер" | |
401 |
|
401 | |||
402 | #: templates/boards/staticpages/help.html:14 |
|
402 | #: templates/boards/staticpages/help.html:14 | |
403 | msgid "Link to a post" |
|
403 | msgid "Link to a post" | |
404 | msgstr "Ссылка на сообщение" |
|
404 | msgstr "Ссылка на сообщение" | |
405 |
|
405 | |||
406 | #: templates/boards/staticpages/help.html:15 |
|
406 | #: templates/boards/staticpages/help.html:15 | |
407 | msgid "Strikethrough text" |
|
407 | msgid "Strikethrough text" | |
408 | msgstr "Зачеркнутый текст" |
|
408 | msgstr "Зачеркнутый текст" | |
409 |
|
409 | |||
410 | #: templates/boards/staticpages/help.html:16 |
|
410 | #: templates/boards/staticpages/help.html:16 | |
411 | msgid "Comment" |
|
411 | msgid "Comment" | |
412 | msgstr "Комментарий" |
|
412 | msgstr "Комментарий" | |
413 |
|
413 | |||
414 | #: templates/boards/staticpages/help.html:17 |
|
414 | #: templates/boards/staticpages/help.html:17 | |
415 | #: templates/boards/staticpages/help.html:18 |
|
415 | #: templates/boards/staticpages/help.html:18 | |
416 | msgid "Quote" |
|
416 | msgid "Quote" | |
417 | msgstr "Цитата" |
|
417 | msgstr "Цитата" | |
418 |
|
418 | |||
419 | #: templates/boards/staticpages/help.html:21 |
|
419 | #: templates/boards/staticpages/help.html:21 | |
420 | msgid "You can try pasting the text and previewing the result here:" |
|
420 | msgid "You can try pasting the text and previewing the result here:" | |
421 | msgstr "Вы можете попробовать вставить текст и проверить результат здесь:" |
|
421 | msgstr "Вы можете попробовать вставить текст и проверить результат здесь:" | |
422 |
|
422 | |||
423 | #: templates/boards/tags.html:17 |
|
423 | #: templates/boards/tags.html:17 | |
424 | msgid "Sections:" |
|
424 | msgid "Sections:" | |
425 | msgstr "Разделы:" |
|
425 | msgstr "Разделы:" | |
426 |
|
426 | |||
427 | #: templates/boards/tags.html:30 |
|
427 | #: templates/boards/tags.html:30 | |
428 | msgid "Other tags:" |
|
428 | msgid "Other tags:" | |
429 | msgstr "Другие метки:" |
|
429 | msgstr "Другие метки:" | |
430 |
|
430 | |||
431 | #: templates/boards/tags.html:43 |
|
431 | #: templates/boards/tags.html:43 | |
432 | msgid "All tags..." |
|
432 | msgid "All tags..." | |
433 | msgstr "Все метки..." |
|
433 | msgstr "Все метки..." | |
434 |
|
434 | |||
435 | #: templates/boards/thread.html:14 |
|
435 | #: templates/boards/thread.html:14 | |
436 | msgid "Normal" |
|
436 | msgid "Normal" | |
437 | msgstr "Нормальный" |
|
437 | msgstr "Нормальный" | |
438 |
|
438 | |||
439 | #: templates/boards/thread.html:15 |
|
439 | #: templates/boards/thread.html:15 | |
440 | msgid "Gallery" |
|
440 | msgid "Gallery" | |
441 | msgstr "Галерея" |
|
441 | msgstr "Галерея" | |
442 |
|
442 | |||
443 | #: templates/boards/thread.html:16 |
|
443 | #: templates/boards/thread.html:16 | |
444 | msgid "Tree" |
|
444 | msgid "Tree" | |
445 | msgstr "Дерево" |
|
445 | msgstr "Дерево" | |
446 |
|
446 | |||
447 | #: templates/boards/thread.html:35 |
|
447 | #: templates/boards/thread.html:35 | |
448 | msgid "message" |
|
448 | msgid "message" | |
449 | msgid_plural "messages" |
|
449 | msgid_plural "messages" | |
450 | msgstr[0] "сообщение" |
|
450 | msgstr[0] "сообщение" | |
451 | msgstr[1] "сообщения" |
|
451 | msgstr[1] "сообщения" | |
452 | msgstr[2] "сообщений" |
|
452 | msgstr[2] "сообщений" | |
453 |
|
453 | |||
454 | #: templates/boards/thread.html:38 |
|
454 | #: templates/boards/thread.html:38 | |
455 | msgid "image" |
|
455 | msgid "image" | |
456 | msgid_plural "images" |
|
456 | msgid_plural "images" | |
457 | msgstr[0] "изображение" |
|
457 | msgstr[0] "изображение" | |
458 | msgstr[1] "изображения" |
|
458 | msgstr[1] "изображения" | |
459 | msgstr[2] "изображений" |
|
459 | msgstr[2] "изображений" | |
460 |
|
460 | |||
461 | #: templates/boards/thread.html:40 |
|
461 | #: templates/boards/thread.html:40 | |
462 | msgid "Last update: " |
|
462 | msgid "Last update: " | |
463 | msgstr "Последнее обновление: " |
|
463 | msgstr "Последнее обновление: " | |
464 |
|
464 | |||
465 | #: templates/boards/thread_gallery.html:36 |
|
465 | #: templates/boards/thread_gallery.html:36 | |
466 | msgid "No images." |
|
466 | msgid "No images." | |
467 | msgstr "Нет изображений." |
|
467 | msgstr "Нет изображений." | |
468 |
|
468 | |||
469 | #: templates/boards/thread_normal.html:30 |
|
469 | #: templates/boards/thread_normal.html:30 | |
470 | msgid "posts to bumplimit" |
|
470 | msgid "posts to bumplimit" | |
471 | msgstr "сообщений до бамплимита" |
|
471 | msgstr "сообщений до бамплимита" | |
472 |
|
472 | |||
473 | #: templates/boards/thread_normal.html:44 |
|
473 | #: templates/boards/thread_normal.html:44 | |
474 | msgid "Reply to thread" |
|
474 | msgid "Reply to thread" | |
475 | msgstr "Ответить в тему" |
|
475 | msgstr "Ответить в тему" | |
476 |
|
476 | |||
477 | #: templates/boards/thread_normal.html:44 |
|
477 | #: templates/boards/thread_normal.html:44 | |
478 | msgid "to message " |
|
478 | msgid "to message " | |
479 | msgstr "на сообщение" |
|
479 | msgstr "на сообщение" | |
480 |
|
480 | |||
481 | #: templates/boards/thread_normal.html:59 |
|
481 | #: templates/boards/thread_normal.html:59 | |
482 | msgid "Close form" |
|
482 | msgid "Close form" | |
483 | msgstr "Закрыть форму" |
|
483 | msgstr "Закрыть форму" | |
484 |
|
484 | |||
485 | #: templates/search/search.html:17 |
|
485 | #: templates/search/search.html:17 | |
486 | msgid "Ok" |
|
486 | msgid "Ok" | |
487 | msgstr "Ок" |
|
487 | msgstr "Ок" | |
488 |
|
488 | |||
489 | #: utils.py:120 |
|
489 | #: utils.py:120 | |
490 | #, python-format |
|
490 | #, python-format | |
491 |
msgid "File must be less than %s b |
|
491 | msgid "File must be less than %s but is %s." | |
492 |
msgstr "Файл должен быть менее %s |
|
492 | msgstr "Файл должен быть менее %s, но его размер %s." | |
493 |
|
493 | |||
494 | msgid "Please wait %(delay)d second before sending message" |
|
494 | msgid "Please wait %(delay)d second before sending message" | |
495 | msgid_plural "Please wait %(delay)d seconds before sending message" |
|
495 | msgid_plural "Please wait %(delay)d seconds before sending message" | |
496 | msgstr[0] "Пожалуйста подождите %(delay)d секунду перед отправкой сообщения" |
|
496 | msgstr[0] "Пожалуйста подождите %(delay)d секунду перед отправкой сообщения" | |
497 | msgstr[1] "Пожалуйста подождите %(delay)d секунды перед отправкой сообщения" |
|
497 | msgstr[1] "Пожалуйста подождите %(delay)d секунды перед отправкой сообщения" | |
498 | msgstr[2] "Пожалуйста подождите %(delay)d секунд перед отправкой сообщения" |
|
498 | msgstr[2] "Пожалуйста подождите %(delay)d секунд перед отправкой сообщения" | |
499 |
|
499 | |||
500 | msgid "New threads" |
|
500 | msgid "New threads" | |
501 | msgstr "Новые темы" |
|
501 | msgstr "Новые темы" | |
|
502 | ||||
|
503 | #, python-format | |||
|
504 | msgid "Max file size is %(size)s." | |||
|
505 | msgstr "Максимальный размер файла %(size)s." | |||
|
506 | ||||
|
507 | msgid "Size of media:" | |||
|
508 | msgstr "Размер медиа:" | |||
|
509 | ||||
|
510 | msgid "Statistics" | |||
|
511 | msgstr "Статистика" | |||
|
512 | ||||
|
513 | msgid "Invalid PoW." | |||
|
514 | msgstr "Неверный PoW." | |||
|
515 | ||||
|
516 | msgid "Stale PoW." | |||
|
517 | msgstr "PoW устарел." | |||
|
518 | ||||
|
519 | msgid "Show" | |||
|
520 | msgstr "Показывать" | |||
|
521 | ||||
|
522 | msgid "Hide" | |||
|
523 | msgstr "Скрывать" | |||
|
524 | ||||
|
525 | msgid "Add to favorites" | |||
|
526 | msgstr "Добавить в избранное" | |||
|
527 | ||||
|
528 | msgid "Remove from favorites" | |||
|
529 | msgstr "Убрать из избранного" | |||
|
530 | ||||
|
531 | msgid "Monochrome" | |||
|
532 | msgstr "Монохромный" No newline at end of file |
1 | NO CONTENT: modified file, binary diff hidden |
|
NO CONTENT: modified file, binary diff hidden |
@@ -1,55 +1,57 | |||||
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 | #, fuzzy |
|
6 | #, fuzzy | |
7 | msgid "" |
|
7 | msgid "" | |
8 | msgstr "" |
|
8 | msgstr "" | |
9 | "Project-Id-Version: PACKAGE VERSION\n" |
|
9 | "Project-Id-Version: PACKAGE VERSION\n" | |
10 | "Report-Msgid-Bugs-To: \n" |
|
10 | "Report-Msgid-Bugs-To: \n" | |
11 | "POT-Creation-Date: 2015-09-04 18:47+0300\n" |
|
11 | "POT-Creation-Date: 2015-09-04 18:47+0300\n" | |
12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" |
|
12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" | |
13 | "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" |
|
13 | "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" | |
14 | "Language-Team: LANGUAGE <LL@li.org>\n" |
|
14 | "Language-Team: LANGUAGE <LL@li.org>\n" | |
15 | "Language: \n" |
|
15 | "Language: \n" | |
16 | "MIME-Version: 1.0\n" |
|
16 | "MIME-Version: 1.0\n" | |
17 | "Content-Type: text/plain; charset=UTF-8\n" |
|
17 | "Content-Type: text/plain; charset=UTF-8\n" | |
18 | "Content-Transfer-Encoding: 8bit\n" |
|
18 | "Content-Transfer-Encoding: 8bit\n" | |
19 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" |
|
19 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" | |
20 | "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" |
|
20 | "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" | |
21 |
|
21 | |||
22 | #: static/js/3party/jquery-ui.min.js:8 |
|
22 | #: static/js/3party/jquery-ui.min.js:8 | |
23 | msgid "'" |
|
23 | msgid "'" | |
24 | msgstr "" |
|
24 | msgstr "" | |
25 |
|
25 | |||
26 | #: static/js/refpopup.js:72 |
|
26 | #: static/js/refpopup.js:72 | |
27 | msgid "Loading..." |
|
27 | msgid "Loading..." | |
28 | msgstr "Загрузка..." |
|
28 | msgstr "Загрузка..." | |
29 |
|
29 | |||
30 | #: static/js/refpopup.js:91 |
|
30 | #: static/js/refpopup.js:91 | |
31 | msgid "Post not found" |
|
31 | msgid "Post not found" | |
32 | msgstr "Сообщение не найдено" |
|
32 | msgstr "Сообщение не найдено" | |
33 |
|
33 | |||
34 | #: static/js/thread_update.js:261 |
|
34 | #: static/js/thread_update.js:261 | |
35 | msgid "message" |
|
35 | msgid "message" | |
36 | msgid_plural "messages" |
|
36 | msgid_plural "messages" | |
37 | msgstr[0] "сообщение" |
|
37 | msgstr[0] "сообщение" | |
38 | msgstr[1] "сообщения" |
|
38 | msgstr[1] "сообщения" | |
39 | msgstr[2] "сообщений" |
|
39 | msgstr[2] "сообщений" | |
40 |
|
40 | |||
41 | #: static/js/thread_update.js:262 |
|
41 | #: static/js/thread_update.js:262 | |
42 | msgid "image" |
|
42 | msgid "image" | |
43 | msgid_plural "images" |
|
43 | msgid_plural "images" | |
44 | msgstr[0] "изображение" |
|
44 | msgstr[0] "изображение" | |
45 | msgstr[1] "изображения" |
|
45 | msgstr[1] "изображения" | |
46 | msgstr[2] "изображений" |
|
46 | msgstr[2] "изображений" | |
47 |
|
47 | |||
48 | #: static/js/thread_update.js:445 |
|
48 | #: static/js/thread_update.js:445 | |
49 | msgid "Sending message..." |
|
49 | msgid "Sending message..." | |
50 | msgstr "Отправка сообщения..." |
|
50 | msgstr "Отправка сообщения..." | |
51 |
|
51 | |||
52 | #: static/js/thread_update.js:449 |
|
52 | #: static/js/thread_update.js:449 | |
53 | msgid "Server error!" |
|
53 | msgid "Server error!" | |
54 | msgstr "Ошибка сервера!" |
|
54 | msgstr "Ошибка сервера!" | |
55 |
|
55 | |||
|
56 | msgid "Computing PoW..." | |||
|
57 | msgstr "Расчёт PoW..." No newline at end of file |
@@ -1,19 +1,19 | |||||
1 | from django.core.management import BaseCommand |
|
1 | from django.core.management import BaseCommand | |
2 | from django.db import transaction |
|
2 | from django.db import transaction | |
3 | from django.db.models import Count |
|
3 | from django.db.models import Count | |
4 |
|
4 | |||
5 | from boards.models import Tag |
|
5 | from boards.models import Tag | |
6 |
|
6 | |||
7 |
|
7 | |||
8 | __author__ = 'neko259' |
|
8 | __author__ = 'neko259' | |
9 |
|
9 | |||
10 |
|
10 | |||
11 | class Command(BaseCommand): |
|
11 | class Command(BaseCommand): | |
12 | help = 'Removed tags that have no threads' |
|
12 | help = 'Removed tags that have no threads' | |
13 |
|
13 | |||
14 | @transaction.atomic |
|
14 | @transaction.atomic | |
15 | def handle(self, *args, **options): |
|
15 | def handle(self, *args, **options): | |
16 | empty = Tag.objects.annotate(num_threads=Count('thread'))\ |
|
16 | empty = Tag.objects.annotate(num_threads=Count('thread_tags'))\ | |
17 | .filter(num_threads=0).order_by('-required', 'name') |
|
17 | .filter(num_threads=0).order_by('-required', 'name') | |
18 | print('Removing {} empty tags'.format(empty.count())) |
|
18 | print('Removing {} empty tags'.format(empty.count())) | |
19 | empty.delete() |
|
19 | empty.delete() |
@@ -1,230 +1,232 | |||||
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 | from urllib.parse import unquote |
|
6 | from urllib.parse import unquote | |
7 |
|
7 | |||
8 | from django.core.exceptions import ObjectDoesNotExist |
|
8 | from django.core.exceptions import ObjectDoesNotExist | |
9 | from django.core.urlresolvers import reverse |
|
9 | from django.core.urlresolvers import reverse | |
10 |
|
10 | |||
11 | import boards |
|
11 | import boards | |
12 |
|
12 | |||
13 |
|
13 | |||
14 | __author__ = 'neko259' |
|
14 | __author__ = 'neko259' | |
15 |
|
15 | |||
16 |
|
16 | |||
17 | REFLINK_PATTERN = re.compile(r'^\d+$') |
|
17 | REFLINK_PATTERN = re.compile(r'^\d+$') | |
18 | MULTI_NEWLINES_PATTERN = re.compile(r'(\r?\n){2,}') |
|
18 | MULTI_NEWLINES_PATTERN = re.compile(r'(\r?\n){2,}') | |
19 | ONE_NEWLINE = '\n' |
|
19 | ONE_NEWLINE = '\n' | |
20 | REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?') |
|
20 | REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?') | |
21 | LINE_BREAK_HTML = '<div class="br"></div>' |
|
21 | LINE_BREAK_HTML = '<div class="br"></div>' | |
22 |
|
22 | |||
23 |
|
23 | |||
24 | class TextFormatter(): |
|
24 | class TextFormatter(): | |
25 | """ |
|
25 | """ | |
26 | An interface for formatter that can be used in the text format panel |
|
26 | An interface for formatter that can be used in the text format panel | |
27 | """ |
|
27 | """ | |
28 |
|
28 | |||
29 | def __init__(self): |
|
29 | def __init__(self): | |
30 | pass |
|
30 | pass | |
31 |
|
31 | |||
32 | name = '' |
|
32 | name = '' | |
33 |
|
33 | |||
34 | # Left and right tags for the button preview |
|
34 | # Left and right tags for the button preview | |
35 | preview_left = '' |
|
35 | preview_left = '' | |
36 | preview_right = '' |
|
36 | preview_right = '' | |
37 |
|
37 | |||
38 | # Left and right characters for the textarea input |
|
38 | # Left and right characters for the textarea input | |
39 | format_left = '' |
|
39 | format_left = '' | |
40 | format_right = '' |
|
40 | format_right = '' | |
41 |
|
41 | |||
42 |
|
42 | |||
43 | class AutolinkPattern(): |
|
43 | class AutolinkPattern(): | |
44 | def handleMatch(self, m): |
|
44 | def handleMatch(self, m): | |
45 | link_element = etree.Element('a') |
|
45 | link_element = etree.Element('a') | |
46 | href = m.group(2) |
|
46 | href = m.group(2) | |
47 | link_element.set('href', href) |
|
47 | link_element.set('href', href) | |
48 | link_element.text = href |
|
48 | link_element.text = href | |
49 |
|
49 | |||
50 | return link_element |
|
50 | return link_element | |
51 |
|
51 | |||
52 |
|
52 | |||
53 | class QuotePattern(TextFormatter): |
|
53 | class QuotePattern(TextFormatter): | |
54 | name = '>q' |
|
54 | name = '>q' | |
55 | preview_left = '<span class="quote">' |
|
55 | preview_left = '<span class="quote">' | |
56 | preview_right = '</span>' |
|
56 | preview_right = '</span>' | |
57 |
|
57 | |||
58 | format_left = '[quote]' |
|
58 | format_left = '[quote]' | |
59 | format_right = '[/quote]' |
|
59 | format_right = '[/quote]' | |
60 |
|
60 | |||
61 |
|
61 | |||
62 | class SpoilerPattern(TextFormatter): |
|
62 | class SpoilerPattern(TextFormatter): | |
63 | name = 'spoiler' |
|
63 | name = 'spoiler' | |
64 | preview_left = '<span class="spoiler">' |
|
64 | preview_left = '<span class="spoiler">' | |
65 | preview_right = '</span>' |
|
65 | preview_right = '</span>' | |
66 |
|
66 | |||
67 | format_left = '[spoiler]' |
|
67 | format_left = '[spoiler]' | |
68 | format_right = '[/spoiler]' |
|
68 | format_right = '[/spoiler]' | |
69 |
|
69 | |||
70 | def handleMatch(self, m): |
|
70 | def handleMatch(self, m): | |
71 | quote_element = etree.Element('span') |
|
71 | quote_element = etree.Element('span') | |
72 | quote_element.set('class', 'spoiler') |
|
72 | quote_element.set('class', 'spoiler') | |
73 | quote_element.text = m.group(2) |
|
73 | quote_element.text = m.group(2) | |
74 |
|
74 | |||
75 | return quote_element |
|
75 | return quote_element | |
76 |
|
76 | |||
77 |
|
77 | |||
78 | class CommentPattern(TextFormatter): |
|
78 | class CommentPattern(TextFormatter): | |
79 | name = '' |
|
79 | name = '' | |
80 | preview_left = '<span class="comment">// ' |
|
80 | preview_left = '<span class="comment">// ' | |
81 | preview_right = '</span>' |
|
81 | preview_right = '</span>' | |
82 |
|
82 | |||
83 | format_left = '[comment]' |
|
83 | format_left = '[comment]' | |
84 | format_right = '[/comment]' |
|
84 | format_right = '[/comment]' | |
85 |
|
85 | |||
86 |
|
86 | |||
87 | # TODO Use <s> tag here |
|
87 | # TODO Use <s> tag here | |
88 | class StrikeThroughPattern(TextFormatter): |
|
88 | class StrikeThroughPattern(TextFormatter): | |
89 | name = 's' |
|
89 | name = 's' | |
90 | preview_left = '<span class="strikethrough">' |
|
90 | preview_left = '<span class="strikethrough">' | |
91 | preview_right = '</span>' |
|
91 | preview_right = '</span>' | |
92 |
|
92 | |||
93 | format_left = '[s]' |
|
93 | format_left = '[s]' | |
94 | format_right = '[/s]' |
|
94 | format_right = '[/s]' | |
95 |
|
95 | |||
96 |
|
96 | |||
97 | class ItalicPattern(TextFormatter): |
|
97 | class ItalicPattern(TextFormatter): | |
98 | name = 'i' |
|
98 | name = 'i' | |
99 | preview_left = '<i>' |
|
99 | preview_left = '<i>' | |
100 | preview_right = '</i>' |
|
100 | preview_right = '</i>' | |
101 |
|
101 | |||
102 | format_left = '[i]' |
|
102 | format_left = '[i]' | |
103 | format_right = '[/i]' |
|
103 | format_right = '[/i]' | |
104 |
|
104 | |||
105 |
|
105 | |||
106 | class BoldPattern(TextFormatter): |
|
106 | class BoldPattern(TextFormatter): | |
107 | name = 'b' |
|
107 | name = 'b' | |
108 | preview_left = '<b>' |
|
108 | preview_left = '<b>' | |
109 | preview_right = '</b>' |
|
109 | preview_right = '</b>' | |
110 |
|
110 | |||
111 | format_left = '[b]' |
|
111 | format_left = '[b]' | |
112 | format_right = '[/b]' |
|
112 | format_right = '[/b]' | |
113 |
|
113 | |||
114 |
|
114 | |||
115 | class CodePattern(TextFormatter): |
|
115 | class CodePattern(TextFormatter): | |
116 | name = 'code' |
|
116 | name = 'code' | |
117 | preview_left = '<code>' |
|
117 | preview_left = '<code>' | |
118 | preview_right = '</code>' |
|
118 | preview_right = '</code>' | |
119 |
|
119 | |||
120 | format_left = '[code]' |
|
120 | format_left = '[code]' | |
121 | format_right = '[/code]' |
|
121 | format_right = '[/code]' | |
122 |
|
122 | |||
123 |
|
123 | |||
124 | def render_reflink(tag_name, value, options, parent, context): |
|
124 | def render_reflink(tag_name, value, options, parent, context): | |
125 | result = '>>%s' % value |
|
125 | result = '>>%s' % value | |
126 |
|
126 | |||
127 | if REFLINK_PATTERN.match(value): |
|
127 | if REFLINK_PATTERN.match(value): | |
128 | post_id = int(value) |
|
128 | post_id = int(value) | |
129 |
|
129 | |||
130 | try: |
|
130 | try: | |
131 | post = boards.models.Post.objects.get(id=post_id) |
|
131 | post = boards.models.Post.objects.get(id=post_id) | |
132 |
|
132 | |||
133 | result = post.get_link_view() |
|
133 | result = post.get_link_view() | |
134 | except ObjectDoesNotExist: |
|
134 | except ObjectDoesNotExist: | |
135 | pass |
|
135 | pass | |
136 |
|
136 | |||
137 | return result |
|
137 | return result | |
138 |
|
138 | |||
139 |
|
139 | |||
140 | def render_quote(tag_name, value, options, parent, context): |
|
140 | def render_quote(tag_name, value, options, parent, context): | |
141 | source = '' |
|
141 | source = '' | |
142 | if 'source' in options: |
|
142 | if 'source' in options: | |
143 | source = options['source'] |
|
143 | source = options['source'] | |
|
144 | elif 'quote' in options: | |||
|
145 | source = options['quote'] | |||
144 |
|
146 | |||
145 | if source: |
|
147 | if source: | |
146 | result = '<div class="multiquote"><div class="quote-header">%s</div><div class="quote-text">%s</div></div>' % (source, value) |
|
148 | result = '<div class="multiquote"><div class="quote-header">%s</div><div class="quote-text">%s</div></div>' % (source, value) | |
147 | else: |
|
149 | else: | |
148 | # Insert a ">" at the start of every line |
|
150 | # Insert a ">" at the start of every line | |
149 | result = '<span class="quote">>{}</span>'.format( |
|
151 | result = '<span class="quote">>{}</span>'.format( | |
150 | value.replace(LINE_BREAK_HTML, |
|
152 | value.replace(LINE_BREAK_HTML, | |
151 | '{}>'.format(LINE_BREAK_HTML))) |
|
153 | '{}>'.format(LINE_BREAK_HTML))) | |
152 |
|
154 | |||
153 | return result |
|
155 | return result | |
154 |
|
156 | |||
155 |
|
157 | |||
156 | def render_notification(tag_name, value, options, parent, content): |
|
158 | def render_notification(tag_name, value, options, parent, content): | |
157 | username = value.lower() |
|
159 | username = value.lower() | |
158 |
|
160 | |||
159 | return '<a href="{}" class="user-cast">@{}</a>'.format( |
|
161 | return '<a href="{}" class="user-cast">@{}</a>'.format( | |
160 | reverse('notifications', kwargs={'username': username}), username) |
|
162 | reverse('notifications', kwargs={'username': username}), username) | |
161 |
|
163 | |||
162 |
|
164 | |||
163 | def render_tag(tag_name, value, options, parent, context): |
|
165 | def render_tag(tag_name, value, options, parent, context): | |
164 | tag_name = value.lower() |
|
166 | tag_name = value.lower() | |
165 |
|
167 | |||
166 | try: |
|
168 | try: | |
167 | url = boards.models.Tag.objects.get(name=tag_name).get_view() |
|
169 | url = boards.models.Tag.objects.get(name=tag_name).get_view() | |
168 | except ObjectDoesNotExist: |
|
170 | except ObjectDoesNotExist: | |
169 | url = tag_name |
|
171 | url = tag_name | |
170 |
|
172 | |||
171 | return url |
|
173 | return url | |
172 |
|
174 | |||
173 |
|
175 | |||
174 | formatters = [ |
|
176 | formatters = [ | |
175 | QuotePattern, |
|
177 | QuotePattern, | |
176 | SpoilerPattern, |
|
178 | SpoilerPattern, | |
177 | ItalicPattern, |
|
179 | ItalicPattern, | |
178 | BoldPattern, |
|
180 | BoldPattern, | |
179 | CommentPattern, |
|
181 | CommentPattern, | |
180 | StrikeThroughPattern, |
|
182 | StrikeThroughPattern, | |
181 | CodePattern, |
|
183 | CodePattern, | |
182 | ] |
|
184 | ] | |
183 |
|
185 | |||
184 |
|
186 | |||
185 | PREPARSE_PATTERNS = { |
|
187 | PREPARSE_PATTERNS = { | |
186 | r'(?<!>)>>(\d+)': r'[post]\1[/post]', # Reflink ">>123" |
|
188 | r'(?<!>)>>(\d+)': r'[post]\1[/post]', # Reflink ">>123" | |
187 | r'^>([^>].+)': r'[quote]\1[/quote]', # Quote ">text" |
|
189 | r'^>([^>].+)': r'[quote]\1[/quote]', # Quote ">text" | |
188 | r'^//(.+)': r'[comment]\1[/comment]', # Comment "//text" |
|
190 | r'^//(.+)': r'[comment]\1[/comment]', # Comment "//text" | |
189 | r'\B@(\w+)': r'[user]\1[/user]', # User notification "@user" |
|
191 | r'\B@(\w+)': r'[user]\1[/user]', # User notification "@user" | |
190 | } |
|
192 | } | |
191 |
|
193 | |||
192 |
|
194 | |||
193 | class Parser: |
|
195 | class Parser: | |
194 | def __init__(self): |
|
196 | def __init__(self): | |
195 | # The newline hack is added because br's margin does not work in all |
|
197 | # The newline hack is added because br's margin does not work in all | |
196 | # browsers except firefox, when the div's does. |
|
198 | # browsers except firefox, when the div's does. | |
197 | self.parser = bbcode.Parser(newline=LINE_BREAK_HTML) |
|
199 | self.parser = bbcode.Parser(newline=LINE_BREAK_HTML) | |
198 |
|
200 | |||
199 | self.parser.add_formatter('post', render_reflink, strip=True) |
|
201 | self.parser.add_formatter('post', render_reflink, strip=True) | |
200 | self.parser.add_formatter('quote', render_quote, strip=True) |
|
202 | self.parser.add_formatter('quote', render_quote, strip=True) | |
201 | self.parser.add_formatter('user', render_notification, strip=True) |
|
203 | self.parser.add_formatter('user', render_notification, strip=True) | |
202 | self.parser.add_formatter('tag', render_tag, strip=True) |
|
204 | self.parser.add_formatter('tag', render_tag, strip=True) | |
203 | self.parser.add_simple_formatter( |
|
205 | self.parser.add_simple_formatter( | |
204 | 'comment', '<span class="comment">//%(value)s</span>') |
|
206 | 'comment', '<span class="comment">//%(value)s</span>') | |
205 | self.parser.add_simple_formatter( |
|
207 | self.parser.add_simple_formatter( | |
206 | 'spoiler', '<span class="spoiler">%(value)s</span>') |
|
208 | 'spoiler', '<span class="spoiler">%(value)s</span>') | |
207 | self.parser.add_simple_formatter( |
|
209 | self.parser.add_simple_formatter( | |
208 | 's', '<span class="strikethrough">%(value)s</span>') |
|
210 | 's', '<span class="strikethrough">%(value)s</span>') | |
209 | # TODO Why not use built-in tag? |
|
211 | # TODO Why not use built-in tag? | |
210 | self.parser.add_simple_formatter('code', |
|
212 | self.parser.add_simple_formatter('code', | |
211 | '<pre><code>%(value)s</pre></code>', |
|
213 | '<pre><code>%(value)s</pre></code>', | |
212 | render_embedded=False) |
|
214 | render_embedded=False) | |
213 |
|
215 | |||
214 | def preparse(self, text): |
|
216 | def preparse(self, text): | |
215 | """ |
|
217 | """ | |
216 | Performs manual parsing before the bbcode parser is used. |
|
218 | Performs manual parsing before the bbcode parser is used. | |
217 | Preparsed text is saved as raw and the text before preparsing is lost. |
|
219 | Preparsed text is saved as raw and the text before preparsing is lost. | |
218 | """ |
|
220 | """ | |
219 | new_text = MULTI_NEWLINES_PATTERN.sub(ONE_NEWLINE, text) |
|
221 | new_text = MULTI_NEWLINES_PATTERN.sub(ONE_NEWLINE, text) | |
220 |
|
222 | |||
221 | for key, value in PREPARSE_PATTERNS.items(): |
|
223 | for key, value in PREPARSE_PATTERNS.items(): | |
222 | new_text = re.sub(key, value, new_text, flags=re.MULTILINE) |
|
224 | new_text = re.sub(key, value, new_text, flags=re.MULTILINE) | |
223 |
|
225 | |||
224 | for link in REGEX_URL.findall(text): |
|
226 | for link in REGEX_URL.findall(text): | |
225 | new_text = new_text.replace(link, unquote(link)) |
|
227 | new_text = new_text.replace(link, unquote(link)) | |
226 |
|
228 | |||
227 | return new_text |
|
229 | return new_text | |
228 |
|
230 | |||
229 | def parse(self, text): |
|
231 | def parse(self, text): | |
230 | return self.parser.format(text) |
|
232 | return self.parser.format(text) |
@@ -1,11 +1,14 | |||||
1 | __author__ = 'neko259' |
|
1 | STATUS_ACTIVE = 'active' | |
|
2 | STATUS_BUMPLIMIT = 'bumplimit' | |||
|
3 | STATUS_ARCHIVE = 'archived' | |||
|
4 | ||||
2 |
|
5 | |||
3 | from boards.models.signature import GlobalId, Signature |
|
6 | from boards.models.signature import GlobalId, Signature | |
4 | from boards.models.sync_key import KeyPair |
|
7 | from boards.models.sync_key import KeyPair | |
5 | from boards.models.image import PostImage |
|
8 | from boards.models.image import PostImage | |
6 | from boards.models.attachment import Attachment |
|
9 | from boards.models.attachment import Attachment | |
7 | from boards.models.thread import Thread |
|
10 | from boards.models.thread import Thread | |
8 | from boards.models.post import Post |
|
11 | from boards.models.post import Post | |
9 | from boards.models.tag import Tag |
|
12 | from boards.models.tag import Tag | |
10 | from boards.models.user import Ban |
|
13 | from boards.models.user import Ban | |
11 | from boards.models.banner import Banner |
|
14 | from boards.models.banner import Banner |
@@ -1,41 +1,42 | |||||
1 | from django.db import models |
|
1 | from django.db import models | |
2 |
|
2 | |||
3 | from boards import utils |
|
3 | from boards import utils | |
4 | from boards.models.attachment.viewers import get_viewers, AbstractViewer |
|
4 | from boards.models.attachment.viewers import get_viewers, AbstractViewer | |
5 | from boards.utils import get_upload_filename, get_file_mimetype, get_extension |
|
5 | from boards.utils import get_upload_filename, get_file_mimetype, get_extension | |
6 |
|
6 | |||
7 |
|
7 | |||
8 | class AttachmentManager(models.Manager): |
|
8 | class AttachmentManager(models.Manager): | |
9 | def create_with_hash(self, file): |
|
9 | def create_with_hash(self, file): | |
10 | file_hash = utils.get_file_hash(file) |
|
10 | file_hash = utils.get_file_hash(file) | |
11 | existing = self.filter(hash=file_hash) |
|
11 | existing = self.filter(hash=file_hash) | |
12 | if len(existing) > 0: |
|
12 | if len(existing) > 0: | |
13 | attachment = existing[0] |
|
13 | attachment = existing[0] | |
14 | else: |
|
14 | else: | |
15 | # FIXME Use full mimetype here, need to modify viewers too |
|
15 | # FIXME Use full mimetype here, need to modify viewers too | |
16 | file_type = get_extension(file.name) |
|
16 | file_type = get_extension(file.name) | |
17 | attachment = self.create(file=file, mimetype=file_type, |
|
17 | attachment = self.create(file=file, mimetype=file_type, | |
18 | hash=file_hash) |
|
18 | hash=file_hash) | |
19 |
|
19 | |||
20 | return attachment |
|
20 | return attachment | |
21 |
|
21 | |||
22 |
|
22 | |||
23 | class Attachment(models.Model): |
|
23 | class Attachment(models.Model): | |
24 | objects = AttachmentManager() |
|
24 | objects = AttachmentManager() | |
25 |
|
25 | |||
26 | file = models.FileField(upload_to=get_upload_filename) |
|
26 | file = models.FileField(upload_to=get_upload_filename) | |
27 | mimetype = models.CharField(max_length=50) |
|
27 | mimetype = models.CharField(max_length=50) | |
28 | hash = models.CharField(max_length=36) |
|
28 | hash = models.CharField(max_length=36) | |
29 |
|
29 | |||
30 | def get_view(self): |
|
30 | def get_view(self): | |
31 | file_viewer = None |
|
31 | file_viewer = None | |
32 | for viewer in get_viewers(): |
|
32 | for viewer in get_viewers(): | |
33 | if viewer.supports(self.mimetype): |
|
33 | if viewer.supports(self.mimetype): | |
34 | file_viewer = viewer |
|
34 | file_viewer = viewer | |
35 | break |
|
35 | break | |
36 | if file_viewer is None: |
|
36 | if file_viewer is None: | |
37 | file_viewer = AbstractViewer |
|
37 | file_viewer = AbstractViewer | |
38 |
|
38 | |||
39 | return file_viewer(self.file, self.mimetype).get_view() |
|
39 | return file_viewer(self.file, self.mimetype).get_view() | |
40 |
|
40 | |||
41 |
|
41 | def __str__(self): | ||
|
42 | return self.file.url |
@@ -1,66 +1,69 | |||||
1 | import os |
|
1 | import os | |
2 | import re |
|
2 | import re | |
3 |
|
3 | |||
4 | from django.core.files.uploadedfile import SimpleUploadedFile |
|
4 | from django.core.files.uploadedfile import SimpleUploadedFile, \ | |
|
5 | TemporaryUploadedFile | |||
5 | from pytube import YouTube |
|
6 | from pytube import YouTube | |
6 | import requests |
|
7 | import requests | |
7 |
|
8 | |||
8 | from boards.utils import validate_file_size |
|
9 | from boards.utils import validate_file_size | |
9 |
|
10 | |||
10 | YOUTUBE_VIDEO_FORMAT = 'webm' |
|
11 | YOUTUBE_VIDEO_FORMAT = 'webm' | |
11 |
|
12 | |||
12 | HTTP_RESULT_OK = 200 |
|
13 | HTTP_RESULT_OK = 200 | |
13 |
|
14 | |||
14 | HEADER_CONTENT_LENGTH = 'content-length' |
|
15 | HEADER_CONTENT_LENGTH = 'content-length' | |
15 | HEADER_CONTENT_TYPE = 'content-type' |
|
16 | HEADER_CONTENT_TYPE = 'content-type' | |
16 |
|
17 | |||
17 |
FILE_DOWNLOAD_CHUNK_BYTES = |
|
18 | FILE_DOWNLOAD_CHUNK_BYTES = 200000 | |
18 |
|
19 | |||
19 | YOUTUBE_URL = re.compile(r'https?://www\.youtube\.com/watch\?v=\w+') |
|
20 | YOUTUBE_URL = re.compile(r'https?://(www\.youtube\.com/watch\?v=|youtu.be/)\w+') | |
20 |
|
21 | |||
21 |
|
22 | |||
22 | class Downloader: |
|
23 | class Downloader: | |
23 | @staticmethod |
|
24 | @staticmethod | |
24 | def handles(url: str) -> bool: |
|
25 | def handles(url: str) -> bool: | |
25 | return False |
|
26 | return False | |
26 |
|
27 | |||
27 | @staticmethod |
|
28 | @staticmethod | |
28 | def download(url: str): |
|
29 | def download(url: str): | |
29 | # Verify content headers |
|
30 | # Verify content headers | |
30 | response_head = requests.head(url, verify=False) |
|
31 | response_head = requests.head(url, verify=False) | |
31 | content_type = response_head.headers[HEADER_CONTENT_TYPE].split(';')[0] |
|
32 | content_type = response_head.headers[HEADER_CONTENT_TYPE].split(';')[0] | |
32 | length_header = response_head.headers.get(HEADER_CONTENT_LENGTH) |
|
33 | length_header = response_head.headers.get(HEADER_CONTENT_LENGTH) | |
33 | if length_header: |
|
34 | if length_header: | |
34 | length = int(length_header) |
|
35 | length = int(length_header) | |
35 | validate_file_size(length) |
|
36 | validate_file_size(length) | |
36 | # Get the actual content into memory |
|
37 | # Get the actual content into memory | |
37 | response = requests.get(url, verify=False, stream=True) |
|
38 | response = requests.get(url, verify=False, stream=True) | |
38 |
|
39 | |||
39 | # Download file, stop if the size exceeds limit |
|
40 | # Download file, stop if the size exceeds limit | |
40 | size = 0 |
|
41 | size = 0 | |
41 | content = b'' |
|
42 | ||
|
43 | # Set a dummy file name that will be replaced | |||
|
44 | # anyway, just keep the valid extension | |||
|
45 | filename = 'file.' + content_type.split('/')[1] | |||
|
46 | ||||
|
47 | file = TemporaryUploadedFile(filename, content_type, 0, None, None) | |||
42 | for chunk in response.iter_content(FILE_DOWNLOAD_CHUNK_BYTES): |
|
48 | for chunk in response.iter_content(FILE_DOWNLOAD_CHUNK_BYTES): | |
43 | size += len(chunk) |
|
49 | size += len(chunk) | |
44 | validate_file_size(size) |
|
50 | validate_file_size(size) | |
45 |
|
|
51 | file.write(chunk) | |
46 |
|
52 | |||
47 |
if response.status_code == HTTP_RESULT_OK |
|
53 | if response.status_code == HTTP_RESULT_OK: | |
48 | # Set a dummy file name that will be replaced |
|
54 | return file | |
49 | # anyway, just keep the valid extension |
|
|||
50 | filename = 'file.' + content_type.split('/')[1] |
|
|||
51 | return SimpleUploadedFile(filename, content, content_type) |
|
|||
52 |
|
55 | |||
53 |
|
56 | |||
54 | class YouTubeDownloader(Downloader): |
|
57 | class YouTubeDownloader(Downloader): | |
55 | @staticmethod |
|
58 | @staticmethod | |
56 | def download(url: str): |
|
59 | def download(url: str): | |
57 | yt = YouTube() |
|
60 | yt = YouTube() | |
58 | yt.from_url(url) |
|
61 | yt.from_url(url) | |
59 | videos = yt.filter(YOUTUBE_VIDEO_FORMAT) |
|
62 | videos = yt.filter(YOUTUBE_VIDEO_FORMAT) | |
60 | if len(videos) > 0: |
|
63 | if len(videos) > 0: | |
61 | video = videos[0] |
|
64 | video = videos[0] | |
62 | return Downloader.download(video.url) |
|
65 | return Downloader.download(video.url) | |
63 |
|
66 | |||
64 | @staticmethod |
|
67 | @staticmethod | |
65 | def handles(url: str) -> bool: |
|
68 | def handles(url: str) -> bool: | |
66 | return YOUTUBE_URL.match(url) |
|
69 | return YOUTUBE_URL.match(url) |
@@ -1,10 +1,13 | |||||
1 | from django.db import models |
|
1 | from django.db import models | |
2 |
|
2 | |||
3 |
|
3 | |||
4 | class Banner(models.Model): |
|
4 | class Banner(models.Model): | |
5 | title = models.TextField() |
|
5 | title = models.TextField() | |
6 | text = models.TextField() |
|
6 | text = models.TextField(blank=True, null=True) | |
7 | post = models.ForeignKey('Post') |
|
7 | post = models.ForeignKey('Post') | |
8 |
|
8 | |||
9 | def __str__(self): |
|
9 | def __str__(self): | |
10 | return self.title |
|
10 | return self.title | |
|
11 | ||||
|
12 | def get_text(self) -> str: | |||
|
13 | return self.text or self.post.get_text() |
@@ -1,95 +1,97 | |||||
1 | from django.db import models |
|
1 | from django.db import models | |
2 | from django.template.defaultfilters import filesizeformat |
|
2 | from django.template.defaultfilters import filesizeformat | |
3 |
|
3 | |||
4 | from boards import thumbs, utils |
|
4 | from boards import thumbs, utils | |
5 | import boards |
|
5 | import boards | |
6 | from boards.models.base import Viewable |
|
6 | from boards.models.base import Viewable | |
|
7 | from boards.models import STATUS_ARCHIVE | |||
7 | from boards.utils import get_upload_filename |
|
8 | from boards.utils import get_upload_filename | |
8 |
|
9 | |||
|
10 | ||||
9 | __author__ = 'neko259' |
|
11 | __author__ = 'neko259' | |
10 |
|
12 | |||
11 |
|
13 | |||
12 | IMAGE_THUMB_SIZE = (200, 150) |
|
14 | IMAGE_THUMB_SIZE = (200, 150) | |
13 | HASH_LENGTH = 36 |
|
15 | HASH_LENGTH = 36 | |
14 |
|
16 | |||
15 | CSS_CLASS_IMAGE = 'image' |
|
17 | CSS_CLASS_IMAGE = 'image' | |
16 | CSS_CLASS_THUMB = 'thumb' |
|
18 | CSS_CLASS_THUMB = 'thumb' | |
17 |
|
19 | |||
18 |
|
20 | |||
19 | class PostImageManager(models.Manager): |
|
21 | class PostImageManager(models.Manager): | |
20 | def create_with_hash(self, image): |
|
22 | def create_with_hash(self, image): | |
21 | image_hash = utils.get_file_hash(image) |
|
23 | image_hash = utils.get_file_hash(image) | |
22 | existing = self.filter(hash=image_hash) |
|
24 | existing = self.filter(hash=image_hash) | |
23 | if len(existing) > 0: |
|
25 | if len(existing) > 0: | |
24 | post_image = existing[0] |
|
26 | post_image = existing[0] | |
25 | else: |
|
27 | else: | |
26 | post_image = PostImage.objects.create(image=image) |
|
28 | post_image = PostImage.objects.create(image=image) | |
27 |
|
29 | |||
28 | return post_image |
|
30 | return post_image | |
29 |
|
31 | |||
30 |
def get_random_images(self, count, |
|
32 | def get_random_images(self, count, tags=None): | |
31 |
images = self. |
|
33 | images = self.exclude(post_images__thread__status=STATUS_ARCHIVE) | |
32 | if tags is not None: |
|
34 | if tags is not None: | |
33 | images = images.filter(post_images__threads__tags__in=tags) |
|
35 | images = images.filter(post_images__threads__tags__in=tags) | |
34 | return images.order_by('?')[:count] |
|
36 | return images.order_by('?')[:count] | |
35 |
|
37 | |||
36 |
|
38 | |||
37 | class PostImage(models.Model, Viewable): |
|
39 | class PostImage(models.Model, Viewable): | |
38 | objects = PostImageManager() |
|
40 | objects = PostImageManager() | |
39 |
|
41 | |||
40 | class Meta: |
|
42 | class Meta: | |
41 | app_label = 'boards' |
|
43 | app_label = 'boards' | |
42 | ordering = ('id',) |
|
44 | ordering = ('id',) | |
43 |
|
45 | |||
44 | width = models.IntegerField(default=0) |
|
46 | width = models.IntegerField(default=0) | |
45 | height = models.IntegerField(default=0) |
|
47 | height = models.IntegerField(default=0) | |
46 |
|
48 | |||
47 | pre_width = models.IntegerField(default=0) |
|
49 | pre_width = models.IntegerField(default=0) | |
48 | pre_height = models.IntegerField(default=0) |
|
50 | pre_height = models.IntegerField(default=0) | |
49 |
|
51 | |||
50 | image = thumbs.ImageWithThumbsField(upload_to=get_upload_filename, |
|
52 | image = thumbs.ImageWithThumbsField(upload_to=get_upload_filename, | |
51 | blank=True, sizes=(IMAGE_THUMB_SIZE,), |
|
53 | blank=True, sizes=(IMAGE_THUMB_SIZE,), | |
52 | width_field='width', |
|
54 | width_field='width', | |
53 | height_field='height', |
|
55 | height_field='height', | |
54 | preview_width_field='pre_width', |
|
56 | preview_width_field='pre_width', | |
55 | preview_height_field='pre_height') |
|
57 | preview_height_field='pre_height') | |
56 | hash = models.CharField(max_length=HASH_LENGTH) |
|
58 | hash = models.CharField(max_length=HASH_LENGTH) | |
57 |
|
59 | |||
58 | def save(self, *args, **kwargs): |
|
60 | def save(self, *args, **kwargs): | |
59 | """ |
|
61 | """ | |
60 | Saves the model and computes the image hash for deduplication purposes. |
|
62 | Saves the model and computes the image hash for deduplication purposes. | |
61 | """ |
|
63 | """ | |
62 |
|
64 | |||
63 | if not self.pk and self.image: |
|
65 | if not self.pk and self.image: | |
64 | self.hash = utils.get_file_hash(self.image) |
|
66 | self.hash = utils.get_file_hash(self.image) | |
65 | super(PostImage, self).save(*args, **kwargs) |
|
67 | super(PostImage, self).save(*args, **kwargs) | |
66 |
|
68 | |||
67 | def __str__(self): |
|
69 | def __str__(self): | |
68 | return self.image.url |
|
70 | return self.image.url | |
69 |
|
71 | |||
70 | def get_view(self): |
|
72 | def get_view(self): | |
71 | metadata = '{}, {}'.format(self.image.name.split('.')[-1], |
|
73 | metadata = '{}, {}'.format(self.image.name.split('.')[-1], | |
72 | filesizeformat(self.image.size)) |
|
74 | filesizeformat(self.image.size)) | |
73 | return '<div class="{}">' \ |
|
75 | return '<div class="{}">' \ | |
74 | '<a class="{}" href="{full}">' \ |
|
76 | '<a class="{}" href="{full}">' \ | |
75 | '<img class="post-image-preview"' \ |
|
77 | '<img class="post-image-preview"' \ | |
76 | ' src="{}"' \ |
|
78 | ' src="{}"' \ | |
77 | ' alt="{}"' \ |
|
79 | ' alt="{}"' \ | |
78 | ' width="{}"' \ |
|
80 | ' width="{}"' \ | |
79 | ' height="{}"' \ |
|
81 | ' height="{}"' \ | |
80 | ' data-width="{}"' \ |
|
82 | ' data-width="{}"' \ | |
81 | ' data-height="{}" />' \ |
|
83 | ' data-height="{}" />' \ | |
82 | '</a>' \ |
|
84 | '</a>' \ | |
83 | '<div class="image-metadata">'\ |
|
85 | '<div class="image-metadata">'\ | |
84 | '<a href="{full}" download>{image_meta}</a>'\ |
|
86 | '<a href="{full}" download>{image_meta}</a>'\ | |
85 | '</div>' \ |
|
87 | '</div>' \ | |
86 | '</div>'\ |
|
88 | '</div>'\ | |
87 | .format(CSS_CLASS_IMAGE, CSS_CLASS_THUMB, |
|
89 | .format(CSS_CLASS_IMAGE, CSS_CLASS_THUMB, | |
88 | self.image.url_200x150, |
|
90 | self.image.url_200x150, | |
89 | str(self.hash), str(self.pre_width), |
|
91 | str(self.hash), str(self.pre_width), | |
90 | str(self.pre_height), str(self.width), str(self.height), |
|
92 | str(self.pre_height), str(self.width), str(self.height), | |
91 | full=self.image.url, image_meta=metadata) |
|
93 | full=self.image.url, image_meta=metadata) | |
92 |
|
94 | |||
93 | def get_random_associated_post(self): |
|
95 | def get_random_associated_post(self): | |
94 | posts = boards.models.Post.objects.filter(images__in=[self]) |
|
96 | posts = boards.models.Post.objects.filter(images__in=[self]) | |
95 | return posts.order_by('?').first() |
|
97 | return posts.order_by('?').first() |
@@ -1,436 +1,447 | |||||
1 | import logging |
|
1 | import logging | |
2 | import re |
|
2 | import re | |
3 | import uuid |
|
3 | import uuid | |
4 |
|
4 | |||
5 | from django.core.exceptions import ObjectDoesNotExist |
|
5 | from django.core.exceptions import ObjectDoesNotExist | |
6 | from django.core.urlresolvers import reverse |
|
6 | from django.core.urlresolvers import reverse | |
7 | from django.db import models |
|
7 | from django.db import models | |
8 | from django.db.models import TextField, QuerySet |
|
8 | from django.db.models import TextField, QuerySet | |
9 | from django.template.defaultfilters import truncatewords, striptags |
|
9 | from django.template.defaultfilters import truncatewords, striptags | |
10 | from django.template.loader import render_to_string |
|
10 | from django.template.loader import render_to_string | |
11 | from django.utils import timezone |
|
11 | from django.utils import timezone | |
12 |
|
12 | |||
13 | from boards import settings |
|
13 | from boards import settings | |
14 | from boards.abstracts.tripcode import Tripcode |
|
14 | from boards.abstracts.tripcode import Tripcode | |
15 | from boards.mdx_neboard import Parser |
|
15 | from boards.mdx_neboard import Parser | |
16 | from boards.models import PostImage, Attachment, KeyPair, GlobalId |
|
16 | from boards.models import PostImage, Attachment, KeyPair, GlobalId | |
17 | from boards.models.base import Viewable |
|
17 | from boards.models.base import Viewable | |
18 | from boards.models.post.export import get_exporter, DIFF_TYPE_JSON |
|
18 | from boards.models.post.export import get_exporter, DIFF_TYPE_JSON | |
19 | from boards.models.post.manager import PostManager |
|
19 | from boards.models.post.manager import PostManager | |
20 | from boards.models.user import Notification |
|
20 | from boards.models.user import Notification | |
21 |
|
21 | |||
22 | CSS_CLS_HIDDEN_POST = 'hidden_post' |
|
22 | CSS_CLS_HIDDEN_POST = 'hidden_post' | |
23 | CSS_CLS_DEAD_POST = 'dead_post' |
|
23 | CSS_CLS_DEAD_POST = 'dead_post' | |
24 | CSS_CLS_ARCHIVE_POST = 'archive_post' |
|
24 | CSS_CLS_ARCHIVE_POST = 'archive_post' | |
25 | CSS_CLS_POST = 'post' |
|
25 | CSS_CLS_POST = 'post' | |
|
26 | CSS_CLS_MONOCHROME = 'monochrome' | |||
26 |
|
27 | |||
27 | TITLE_MAX_WORDS = 10 |
|
28 | TITLE_MAX_WORDS = 10 | |
28 |
|
29 | |||
29 | APP_LABEL_BOARDS = 'boards' |
|
30 | APP_LABEL_BOARDS = 'boards' | |
30 |
|
31 | |||
31 | BAN_REASON_AUTO = 'Auto' |
|
32 | BAN_REASON_AUTO = 'Auto' | |
32 |
|
33 | |||
33 | IMAGE_THUMB_SIZE = (200, 150) |
|
34 | IMAGE_THUMB_SIZE = (200, 150) | |
34 |
|
35 | |||
35 | TITLE_MAX_LENGTH = 200 |
|
36 | TITLE_MAX_LENGTH = 200 | |
36 |
|
37 | |||
37 | REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]') |
|
38 | REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]') | |
38 | REGEX_GLOBAL_REPLY = re.compile(r'\[post\](\w+)::([^:]+)::(\d+)\[/post\]') |
|
39 | REGEX_GLOBAL_REPLY = re.compile(r'\[post\](\w+)::([^:]+)::(\d+)\[/post\]') | |
39 | REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?') |
|
40 | REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?') | |
40 | REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]') |
|
41 | REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]') | |
41 |
|
42 | |||
42 | PARAMETER_TRUNCATED = 'truncated' |
|
43 | PARAMETER_TRUNCATED = 'truncated' | |
43 | PARAMETER_TAG = 'tag' |
|
44 | PARAMETER_TAG = 'tag' | |
44 | PARAMETER_OFFSET = 'offset' |
|
45 | PARAMETER_OFFSET = 'offset' | |
45 | PARAMETER_DIFF_TYPE = 'type' |
|
46 | PARAMETER_DIFF_TYPE = 'type' | |
46 | PARAMETER_CSS_CLASS = 'css_class' |
|
47 | PARAMETER_CSS_CLASS = 'css_class' | |
47 | PARAMETER_THREAD = 'thread' |
|
48 | PARAMETER_THREAD = 'thread' | |
48 | PARAMETER_IS_OPENING = 'is_opening' |
|
49 | PARAMETER_IS_OPENING = 'is_opening' | |
49 | PARAMETER_MODERATOR = 'moderator' |
|
|||
50 | PARAMETER_POST = 'post' |
|
50 | PARAMETER_POST = 'post' | |
51 | PARAMETER_OP_ID = 'opening_post_id' |
|
51 | PARAMETER_OP_ID = 'opening_post_id' | |
52 | PARAMETER_NEED_OPEN_LINK = 'need_open_link' |
|
52 | PARAMETER_NEED_OPEN_LINK = 'need_open_link' | |
53 | PARAMETER_REPLY_LINK = 'reply_link' |
|
53 | PARAMETER_REPLY_LINK = 'reply_link' | |
54 | PARAMETER_NEED_OP_DATA = 'need_op_data' |
|
54 | PARAMETER_NEED_OP_DATA = 'need_op_data' | |
55 |
|
55 | |||
56 | POST_VIEW_PARAMS = ( |
|
56 | POST_VIEW_PARAMS = ( | |
57 | 'need_op_data', |
|
57 | 'need_op_data', | |
58 | 'reply_link', |
|
58 | 'reply_link', | |
59 | 'moderator', |
|
|||
60 | 'need_open_link', |
|
59 | 'need_open_link', | |
61 | 'truncated', |
|
60 | 'truncated', | |
62 | 'mode_tree', |
|
61 | 'mode_tree', | |
|
62 | 'perms', | |||
63 | ) |
|
63 | ) | |
64 |
|
64 | |||
65 |
|
65 | |||
66 | class Post(models.Model, Viewable): |
|
66 | class Post(models.Model, Viewable): | |
67 | """A post is a message.""" |
|
67 | """A post is a message.""" | |
68 |
|
68 | |||
69 | objects = PostManager() |
|
69 | objects = PostManager() | |
70 |
|
70 | |||
71 | class Meta: |
|
71 | class Meta: | |
72 | app_label = APP_LABEL_BOARDS |
|
72 | app_label = APP_LABEL_BOARDS | |
73 | ordering = ('id',) |
|
73 | ordering = ('id',) | |
74 |
|
74 | |||
75 | title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True) |
|
75 | title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True) | |
76 | pub_time = models.DateTimeField() |
|
76 | pub_time = models.DateTimeField() | |
77 | text = TextField(blank=True, null=True) |
|
77 | text = TextField(blank=True, null=True) | |
78 | _text_rendered = TextField(blank=True, null=True, editable=False) |
|
78 | _text_rendered = TextField(blank=True, null=True, editable=False) | |
79 |
|
79 | |||
80 | images = models.ManyToManyField(PostImage, null=True, blank=True, |
|
80 | images = models.ManyToManyField(PostImage, null=True, blank=True, | |
81 | related_name='post_images', db_index=True) |
|
81 | related_name='post_images', db_index=True) | |
82 | attachments = models.ManyToManyField(Attachment, null=True, blank=True, |
|
82 | attachments = models.ManyToManyField(Attachment, null=True, blank=True, | |
83 | related_name='attachment_posts') |
|
83 | related_name='attachment_posts') | |
84 |
|
84 | |||
85 | poster_ip = models.GenericIPAddressField() |
|
85 | poster_ip = models.GenericIPAddressField() | |
86 |
|
86 | |||
87 | # TODO This field can be removed cause UID is used for update now |
|
87 | # TODO This field can be removed cause UID is used for update now | |
88 | last_edit_time = models.DateTimeField() |
|
88 | last_edit_time = models.DateTimeField() | |
89 |
|
89 | |||
90 | referenced_posts = models.ManyToManyField('Post', symmetrical=False, |
|
90 | referenced_posts = models.ManyToManyField('Post', symmetrical=False, | |
91 | null=True, |
|
91 | null=True, | |
92 | blank=True, related_name='refposts', |
|
92 | blank=True, related_name='refposts', | |
93 | db_index=True) |
|
93 | db_index=True) | |
94 | refmap = models.TextField(null=True, blank=True) |
|
94 | refmap = models.TextField(null=True, blank=True) | |
95 | threads = models.ManyToManyField('Thread', db_index=True, |
|
95 | threads = models.ManyToManyField('Thread', db_index=True, | |
96 | related_name='multi_replies') |
|
96 | related_name='multi_replies') | |
97 | thread = models.ForeignKey('Thread', db_index=True, related_name='pt+') |
|
97 | thread = models.ForeignKey('Thread', db_index=True, related_name='pt+') | |
98 |
|
98 | |||
99 | url = models.TextField() |
|
99 | url = models.TextField() | |
100 | uid = models.TextField(db_index=True) |
|
100 | uid = models.TextField(db_index=True) | |
101 |
|
101 | |||
102 | # Global ID with author key. If the message was downloaded from another |
|
102 | # Global ID with author key. If the message was downloaded from another | |
103 | # server, this indicates the server. |
|
103 | # server, this indicates the server. | |
104 | global_id = models.OneToOneField('GlobalId', null=True, blank=True) |
|
104 | global_id = models.OneToOneField('GlobalId', null=True, blank=True) | |
105 |
|
105 | |||
106 | tripcode = models.CharField(max_length=50, blank=True, default='') |
|
106 | tripcode = models.CharField(max_length=50, blank=True, default='') | |
107 | opening = models.BooleanField(db_index=True) |
|
107 | opening = models.BooleanField(db_index=True) | |
108 | hidden = models.BooleanField(default=False) |
|
108 | hidden = models.BooleanField(default=False) | |
109 |
|
109 | |||
110 | def __str__(self): |
|
110 | def __str__(self): | |
111 | return 'P#{}/{}'.format(self.id, self.get_title()) |
|
111 | return 'P#{}/{}'.format(self.id, self.get_title()) | |
112 |
|
112 | |||
113 | def get_referenced_posts(self): |
|
113 | def get_referenced_posts(self): | |
114 | threads = self.get_threads().all() |
|
114 | threads = self.get_threads().all() | |
115 | return self.referenced_posts.filter(threads__in=threads)\ |
|
115 | return self.referenced_posts.filter(threads__in=threads)\ | |
116 | .order_by('pub_time').distinct().all() |
|
116 | .order_by('pub_time').distinct().all() | |
117 |
|
117 | |||
118 | def get_title(self) -> str: |
|
118 | def get_title(self) -> str: | |
119 | return self.title |
|
119 | return self.title | |
120 |
|
120 | |||
121 | def get_title_or_text(self): |
|
121 | def get_title_or_text(self): | |
122 | title = self.get_title() |
|
122 | title = self.get_title() | |
123 | if not title: |
|
123 | if not title: | |
124 | title = truncatewords(striptags(self.get_text()), TITLE_MAX_WORDS) |
|
124 | title = truncatewords(striptags(self.get_text()), TITLE_MAX_WORDS) | |
125 |
|
125 | |||
126 | return title |
|
126 | return title | |
127 |
|
127 | |||
128 | def build_refmap(self) -> None: |
|
128 | def build_refmap(self) -> None: | |
129 | """ |
|
129 | """ | |
130 | Builds a replies map string from replies list. This is a cache to stop |
|
130 | Builds a replies map string from replies list. This is a cache to stop | |
131 | the server from recalculating the map on every post show. |
|
131 | the server from recalculating the map on every post show. | |
132 | """ |
|
132 | """ | |
133 |
|
133 | |||
134 | post_urls = [refpost.get_link_view() |
|
134 | post_urls = [refpost.get_link_view() | |
135 | for refpost in self.referenced_posts.all()] |
|
135 | for refpost in self.referenced_posts.all()] | |
136 |
|
136 | |||
137 | self.refmap = ', '.join(post_urls) |
|
137 | self.refmap = ', '.join(post_urls) | |
138 |
|
138 | |||
139 | def is_referenced(self) -> bool: |
|
139 | def is_referenced(self) -> bool: | |
140 | return self.refmap and len(self.refmap) > 0 |
|
140 | return self.refmap and len(self.refmap) > 0 | |
141 |
|
141 | |||
142 | def is_opening(self) -> bool: |
|
142 | def is_opening(self) -> bool: | |
143 | """ |
|
143 | """ | |
144 | Checks if this is an opening post or just a reply. |
|
144 | Checks if this is an opening post or just a reply. | |
145 | """ |
|
145 | """ | |
146 |
|
146 | |||
147 | return self.opening |
|
147 | return self.opening | |
148 |
|
148 | |||
149 | def get_absolute_url(self, thread=None): |
|
149 | def get_absolute_url(self, thread=None): | |
150 | url = None |
|
150 | url = None | |
151 |
|
151 | |||
152 | if thread is None: |
|
152 | if thread is None: | |
153 | thread = self.get_thread() |
|
153 | thread = self.get_thread() | |
154 |
|
154 | |||
155 | # Url is cached only for the "main" thread. When getting url |
|
155 | # Url is cached only for the "main" thread. When getting url | |
156 | # for other threads, do it manually. |
|
156 | # for other threads, do it manually. | |
157 | if self.url: |
|
157 | if self.url: | |
158 | url = self.url |
|
158 | url = self.url | |
159 |
|
159 | |||
160 | if url is None: |
|
160 | if url is None: | |
161 | opening_id = thread.get_opening_post_id() |
|
161 | opening_id = thread.get_opening_post_id() | |
162 | url = reverse('thread', kwargs={'post_id': opening_id}) |
|
162 | url = reverse('thread', kwargs={'post_id': opening_id}) | |
163 | if self.id != opening_id: |
|
163 | if self.id != opening_id: | |
164 | url += '#' + str(self.id) |
|
164 | url += '#' + str(self.id) | |
165 |
|
165 | |||
166 | return url |
|
166 | return url | |
167 |
|
167 | |||
168 | def get_thread(self): |
|
168 | def get_thread(self): | |
169 | return self.thread |
|
169 | return self.thread | |
170 |
|
170 | |||
171 | def get_threads(self) -> QuerySet: |
|
171 | def get_threads(self) -> QuerySet: | |
172 | """ |
|
172 | """ | |
173 | Gets post's thread. |
|
173 | Gets post's thread. | |
174 | """ |
|
174 | """ | |
175 |
|
175 | |||
176 | return self.threads |
|
176 | return self.threads | |
177 |
|
177 | |||
178 | def get_view(self, *args, **kwargs) -> str: |
|
178 | def get_view(self, *args, **kwargs) -> str: | |
179 | """ |
|
179 | """ | |
180 | Renders post's HTML view. Some of the post params can be passed over |
|
180 | Renders post's HTML view. Some of the post params can be passed over | |
181 | kwargs for the means of caching (if we view the thread, some params |
|
181 | kwargs for the means of caching (if we view the thread, some params | |
182 | are same for every post and don't need to be computed over and over. |
|
182 | are same for every post and don't need to be computed over and over. | |
183 | """ |
|
183 | """ | |
184 |
|
184 | |||
185 | thread = self.get_thread() |
|
185 | thread = self.get_thread() | |
186 |
|
186 | |||
187 | css_classes = [CSS_CLS_POST] |
|
187 | css_classes = [CSS_CLS_POST] | |
188 | if thread.archived: |
|
188 | if thread.is_archived(): | |
189 | css_classes.append(CSS_CLS_ARCHIVE_POST) |
|
189 | css_classes.append(CSS_CLS_ARCHIVE_POST) | |
190 | elif not thread.can_bump(): |
|
190 | elif not thread.can_bump(): | |
191 | css_classes.append(CSS_CLS_DEAD_POST) |
|
191 | css_classes.append(CSS_CLS_DEAD_POST) | |
192 | if self.is_hidden(): |
|
192 | if self.is_hidden(): | |
193 | css_classes.append(CSS_CLS_HIDDEN_POST) |
|
193 | css_classes.append(CSS_CLS_HIDDEN_POST) | |
|
194 | if thread.is_monochrome(): | |||
|
195 | css_classes.append(CSS_CLS_MONOCHROME) | |||
194 |
|
196 | |||
195 | params = dict() |
|
197 | params = dict() | |
196 | for param in POST_VIEW_PARAMS: |
|
198 | for param in POST_VIEW_PARAMS: | |
197 | if param in kwargs: |
|
199 | if param in kwargs: | |
198 | params[param] = kwargs[param] |
|
200 | params[param] = kwargs[param] | |
199 |
|
201 | |||
200 | params.update({ |
|
202 | params.update({ | |
201 | PARAMETER_POST: self, |
|
203 | PARAMETER_POST: self, | |
202 | PARAMETER_IS_OPENING: self.is_opening(), |
|
204 | PARAMETER_IS_OPENING: self.is_opening(), | |
203 | PARAMETER_THREAD: thread, |
|
205 | PARAMETER_THREAD: thread, | |
204 | PARAMETER_CSS_CLASS: ' '.join(css_classes), |
|
206 | PARAMETER_CSS_CLASS: ' '.join(css_classes), | |
205 | }) |
|
207 | }) | |
206 |
|
208 | |||
207 | return render_to_string('boards/post.html', params) |
|
209 | return render_to_string('boards/post.html', params) | |
208 |
|
210 | |||
209 | def get_search_view(self, *args, **kwargs): |
|
211 | def get_search_view(self, *args, **kwargs): | |
210 | return self.get_view(need_op_data=True, *args, **kwargs) |
|
212 | return self.get_view(need_op_data=True, *args, **kwargs) | |
211 |
|
213 | |||
212 | def get_first_image(self) -> PostImage: |
|
214 | def get_first_image(self) -> PostImage: | |
213 | return self.images.earliest('id') |
|
215 | return self.images.earliest('id') | |
214 |
|
216 | |||
215 | def delete(self, using=None): |
|
217 | def delete(self, using=None): | |
216 | """ |
|
218 | """ | |
217 | Deletes all post images and the post itself. |
|
219 | Deletes all post images and the post itself. | |
218 | """ |
|
220 | """ | |
219 |
|
221 | |||
220 | for image in self.images.all(): |
|
222 | for image in self.images.all(): | |
221 | image_refs_count = image.post_images.count() |
|
223 | image_refs_count = image.post_images.count() | |
222 | if image_refs_count == 1: |
|
224 | if image_refs_count == 1: | |
223 | image.delete() |
|
225 | image.delete() | |
224 |
|
226 | |||
225 | for attachment in self.attachments.all(): |
|
227 | for attachment in self.attachments.all(): | |
226 | attachment_refs_count = attachment.attachment_posts.count() |
|
228 | attachment_refs_count = attachment.attachment_posts.count() | |
227 | if attachment_refs_count == 1: |
|
229 | if attachment_refs_count == 1: | |
228 | attachment.delete() |
|
230 | attachment.delete() | |
229 |
|
231 | |||
230 | if self.global_id: |
|
232 | if self.global_id: | |
231 | self.global_id.delete() |
|
233 | self.global_id.delete() | |
232 |
|
234 | |||
233 | thread = self.get_thread() |
|
235 | thread = self.get_thread() | |
234 | thread.last_edit_time = timezone.now() |
|
236 | thread.last_edit_time = timezone.now() | |
235 | thread.save() |
|
237 | thread.save() | |
236 |
|
238 | |||
237 | super(Post, self).delete(using) |
|
239 | super(Post, self).delete(using) | |
238 |
|
240 | |||
239 | logging.getLogger('boards.post.delete').info( |
|
241 | logging.getLogger('boards.post.delete').info( | |
240 | 'Deleted post {}'.format(self)) |
|
242 | 'Deleted post {}'.format(self)) | |
241 |
|
243 | |||
242 | def set_global_id(self, key_pair=None): |
|
244 | def set_global_id(self, key_pair=None): | |
243 | """ |
|
245 | """ | |
244 | Sets global id based on the given key pair. If no key pair is given, |
|
246 | Sets global id based on the given key pair. If no key pair is given, | |
245 | default one is used. |
|
247 | default one is used. | |
246 | """ |
|
248 | """ | |
247 |
|
249 | |||
248 | if key_pair: |
|
250 | if key_pair: | |
249 | key = key_pair |
|
251 | key = key_pair | |
250 | else: |
|
252 | else: | |
251 | try: |
|
253 | try: | |
252 | key = KeyPair.objects.get(primary=True) |
|
254 | key = KeyPair.objects.get(primary=True) | |
253 | except KeyPair.DoesNotExist: |
|
255 | except KeyPair.DoesNotExist: | |
254 | # Do not update the global id because there is no key defined |
|
256 | # Do not update the global id because there is no key defined | |
255 | return |
|
257 | return | |
256 | global_id = GlobalId(key_type=key.key_type, |
|
258 | global_id = GlobalId(key_type=key.key_type, | |
257 | key=key.public_key, |
|
259 | key=key.public_key, | |
258 | local_id=self.id) |
|
260 | local_id=self.id) | |
259 | global_id.save() |
|
261 | global_id.save() | |
260 |
|
262 | |||
261 | self.global_id = global_id |
|
263 | self.global_id = global_id | |
262 |
|
264 | |||
263 | self.save(update_fields=['global_id']) |
|
265 | self.save(update_fields=['global_id']) | |
264 |
|
266 | |||
265 | def get_pub_time_str(self): |
|
267 | def get_pub_time_str(self): | |
266 | return str(self.pub_time) |
|
268 | return str(self.pub_time) | |
267 |
|
269 | |||
268 | def get_replied_ids(self): |
|
270 | def get_replied_ids(self): | |
269 | """ |
|
271 | """ | |
270 | Gets ID list of the posts that this post replies. |
|
272 | Gets ID list of the posts that this post replies. | |
271 | """ |
|
273 | """ | |
272 |
|
274 | |||
273 | raw_text = self.get_raw_text() |
|
275 | raw_text = self.get_raw_text() | |
274 |
|
276 | |||
275 | local_replied = REGEX_REPLY.findall(raw_text) |
|
277 | local_replied = REGEX_REPLY.findall(raw_text) | |
276 | global_replied = [] |
|
278 | global_replied = [] | |
277 | for match in REGEX_GLOBAL_REPLY.findall(raw_text): |
|
279 | for match in REGEX_GLOBAL_REPLY.findall(raw_text): | |
278 | key_type = match[0] |
|
280 | key_type = match[0] | |
279 | key = match[1] |
|
281 | key = match[1] | |
280 | local_id = match[2] |
|
282 | local_id = match[2] | |
281 |
|
283 | |||
282 | try: |
|
284 | try: | |
283 | global_id = GlobalId.objects.get(key_type=key_type, |
|
285 | global_id = GlobalId.objects.get(key_type=key_type, | |
284 | key=key, local_id=local_id) |
|
286 | key=key, local_id=local_id) | |
285 | for post in Post.objects.filter(global_id=global_id).only('id'): |
|
287 | for post in Post.objects.filter(global_id=global_id).only('id'): | |
286 | global_replied.append(post.id) |
|
288 | global_replied.append(post.id) | |
287 | except GlobalId.DoesNotExist: |
|
289 | except GlobalId.DoesNotExist: | |
288 | pass |
|
290 | pass | |
289 | return local_replied + global_replied |
|
291 | return local_replied + global_replied | |
290 |
|
292 | |||
291 | def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None, |
|
293 | def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None, | |
292 | include_last_update=False) -> str: |
|
294 | include_last_update=False) -> str: | |
293 | """ |
|
295 | """ | |
294 | Gets post HTML or JSON data that can be rendered on a page or used by |
|
296 | Gets post HTML or JSON data that can be rendered on a page or used by | |
295 | API. |
|
297 | API. | |
296 | """ |
|
298 | """ | |
297 |
|
299 | |||
298 | return get_exporter(format_type).export(self, request, |
|
300 | return get_exporter(format_type).export(self, request, | |
299 | include_last_update) |
|
301 | include_last_update) | |
300 |
|
302 | |||
301 | def notify_clients(self, recursive=True): |
|
303 | def notify_clients(self, recursive=True): | |
302 | """ |
|
304 | """ | |
303 | Sends post HTML data to the thread web socket. |
|
305 | Sends post HTML data to the thread web socket. | |
304 | """ |
|
306 | """ | |
305 |
|
307 | |||
306 | if not settings.get_bool('External', 'WebsocketsEnabled'): |
|
308 | if not settings.get_bool('External', 'WebsocketsEnabled'): | |
307 | return |
|
309 | return | |
308 |
|
310 | |||
309 | thread_ids = list() |
|
311 | thread_ids = list() | |
310 | for thread in self.get_threads().all(): |
|
312 | for thread in self.get_threads().all(): | |
311 | thread_ids.append(thread.id) |
|
313 | thread_ids.append(thread.id) | |
312 |
|
314 | |||
313 | thread.notify_clients() |
|
315 | thread.notify_clients() | |
314 |
|
316 | |||
315 | if recursive: |
|
317 | if recursive: | |
316 | for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()): |
|
318 | for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()): | |
317 | post_id = reply_number.group(1) |
|
319 | post_id = reply_number.group(1) | |
318 |
|
320 | |||
319 | try: |
|
321 | try: | |
320 | ref_post = Post.objects.get(id=post_id) |
|
322 | ref_post = Post.objects.get(id=post_id) | |
321 |
|
323 | |||
322 | if ref_post.get_threads().exclude(id__in=thread_ids).exists(): |
|
324 | if ref_post.get_threads().exclude(id__in=thread_ids).exists(): | |
323 | # If post is in this thread, its thread was already notified. |
|
325 | # If post is in this thread, its thread was already notified. | |
324 | # Otherwise, notify its thread separately. |
|
326 | # Otherwise, notify its thread separately. | |
325 | ref_post.notify_clients(recursive=False) |
|
327 | ref_post.notify_clients(recursive=False) | |
326 | except ObjectDoesNotExist: |
|
328 | except ObjectDoesNotExist: | |
327 | pass |
|
329 | pass | |
328 |
|
330 | |||
329 | def build_url(self): |
|
331 | def build_url(self): | |
330 | self.url = self.get_absolute_url() |
|
332 | self.url = self.get_absolute_url() | |
331 | self.save(update_fields=['url']) |
|
333 | self.save(update_fields=['url']) | |
332 |
|
334 | |||
333 | def save(self, force_insert=False, force_update=False, using=None, |
|
335 | def save(self, force_insert=False, force_update=False, using=None, | |
334 | update_fields=None): |
|
336 | update_fields=None): | |
|
337 | new_post = self.id is None | |||
|
338 | ||||
335 | self._text_rendered = Parser().parse(self.get_raw_text()) |
|
339 | self._text_rendered = Parser().parse(self.get_raw_text()) | |
336 |
|
340 | |||
337 | self.uid = str(uuid.uuid4()) |
|
341 | self.uid = str(uuid.uuid4()) | |
338 | if update_fields is not None and 'uid' not in update_fields: |
|
342 | if update_fields is not None and 'uid' not in update_fields: | |
339 | update_fields += ['uid'] |
|
343 | update_fields += ['uid'] | |
340 |
|
344 | |||
341 |
if |
|
345 | if not new_post: | |
342 | for thread in self.get_threads().all(): |
|
346 | for thread in self.get_threads().all(): | |
343 | thread.last_edit_time = self.last_edit_time |
|
347 | thread.last_edit_time = self.last_edit_time | |
344 |
|
348 | |||
345 |
thread.save(update_fields=['last_edit_time', ' |
|
349 | thread.save(update_fields=['last_edit_time', 'status']) | |
346 |
|
350 | |||
347 | super().save(force_insert, force_update, using, update_fields) |
|
351 | super().save(force_insert, force_update, using, update_fields) | |
348 |
|
352 | |||
|
353 | # Post save triggers | |||
|
354 | if new_post: | |||
|
355 | self.build_url() | |||
|
356 | ||||
|
357 | self._connect_replies() | |||
|
358 | self._connect_notifications() | |||
|
359 | ||||
349 | def get_text(self) -> str: |
|
360 | def get_text(self) -> str: | |
350 | return self._text_rendered |
|
361 | return self._text_rendered | |
351 |
|
362 | |||
352 | def get_raw_text(self) -> str: |
|
363 | def get_raw_text(self) -> str: | |
353 | return self.text |
|
364 | return self.text | |
354 |
|
365 | |||
355 | def get_sync_text(self) -> str: |
|
366 | def get_sync_text(self) -> str: | |
356 | """ |
|
367 | """ | |
357 | Returns text applicable for sync. It has absolute post reflinks. |
|
368 | Returns text applicable for sync. It has absolute post reflinks. | |
358 | """ |
|
369 | """ | |
359 |
|
370 | |||
360 | replacements = dict() |
|
371 | replacements = dict() | |
361 | for post_id in REGEX_REPLY.findall(self.get_raw_text()): |
|
372 | for post_id in REGEX_REPLY.findall(self.get_raw_text()): | |
362 | absolute_post_id = str(Post.objects.get(id=post_id).global_id) |
|
373 | absolute_post_id = str(Post.objects.get(id=post_id).global_id) | |
363 | replacements[post_id] = absolute_post_id |
|
374 | replacements[post_id] = absolute_post_id | |
364 |
|
375 | |||
365 | text = self.get_raw_text() |
|
376 | text = self.get_raw_text() | |
366 | for key in replacements: |
|
377 | for key in replacements: | |
367 | text = text.replace('[post]{}[/post]'.format(key), |
|
378 | text = text.replace('[post]{}[/post]'.format(key), | |
368 | '[post]{}[/post]'.format(replacements[key])) |
|
379 | '[post]{}[/post]'.format(replacements[key])) | |
369 |
|
380 | |||
370 | return text |
|
381 | return text | |
371 |
|
382 | |||
372 | def get_absolute_id(self) -> str: |
|
383 | def get_absolute_id(self) -> str: | |
373 | """ |
|
384 | """ | |
374 | If the post has many threads, shows its main thread OP id in the post |
|
385 | If the post has many threads, shows its main thread OP id in the post | |
375 | ID. |
|
386 | ID. | |
376 | """ |
|
387 | """ | |
377 |
|
388 | |||
378 | if self.get_threads().count() > 1: |
|
389 | if self.get_threads().count() > 1: | |
379 | return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id) |
|
390 | return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id) | |
380 | else: |
|
391 | else: | |
381 | return str(self.id) |
|
392 | return str(self.id) | |
382 |
|
393 | |||
383 | def connect_notifications(self): |
|
394 | def _connect_notifications(self): | |
384 | for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()): |
|
395 | for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()): | |
385 | user_name = reply_number.group(1).lower() |
|
396 | user_name = reply_number.group(1).lower() | |
386 | Notification.objects.get_or_create(name=user_name, post=self) |
|
397 | Notification.objects.get_or_create(name=user_name, post=self) | |
387 |
|
398 | |||
388 | def connect_replies(self): |
|
399 | def _connect_replies(self): | |
389 | """ |
|
400 | """ | |
390 | Connects replies to a post to show them as a reflink map |
|
401 | Connects replies to a post to show them as a reflink map | |
391 | """ |
|
402 | """ | |
392 |
|
403 | |||
393 | for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()): |
|
404 | for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()): | |
394 | post_id = reply_number.group(1) |
|
405 | post_id = reply_number.group(1) | |
395 |
|
406 | |||
396 | try: |
|
407 | try: | |
397 | referenced_post = Post.objects.get(id=post_id) |
|
408 | referenced_post = Post.objects.get(id=post_id) | |
398 |
|
409 | |||
399 | referenced_post.referenced_posts.add(self) |
|
410 | referenced_post.referenced_posts.add(self) | |
400 | referenced_post.last_edit_time = self.pub_time |
|
411 | referenced_post.last_edit_time = self.pub_time | |
401 | referenced_post.build_refmap() |
|
412 | referenced_post.build_refmap() | |
402 | referenced_post.save(update_fields=['refmap', 'last_edit_time']) |
|
413 | referenced_post.save(update_fields=['refmap', 'last_edit_time']) | |
403 | except ObjectDoesNotExist: |
|
414 | except ObjectDoesNotExist: | |
404 | pass |
|
415 | pass | |
405 |
|
416 | |||
406 | def connect_threads(self, opening_posts): |
|
417 | def connect_threads(self, opening_posts): | |
407 | for opening_post in opening_posts: |
|
418 | for opening_post in opening_posts: | |
408 | threads = opening_post.get_threads().all() |
|
419 | threads = opening_post.get_threads().all() | |
409 | for thread in threads: |
|
420 | for thread in threads: | |
410 | if thread.can_bump(): |
|
421 | if thread.can_bump(): | |
411 | thread.update_bump_status() |
|
422 | thread.update_bump_status() | |
412 |
|
423 | |||
413 | thread.last_edit_time = self.last_edit_time |
|
424 | thread.last_edit_time = self.last_edit_time | |
414 |
thread.save(update_fields=['last_edit_time', ' |
|
425 | thread.save(update_fields=['last_edit_time', 'status']) | |
415 | self.threads.add(opening_post.get_thread()) |
|
426 | self.threads.add(opening_post.get_thread()) | |
416 |
|
427 | |||
417 | def get_tripcode(self): |
|
428 | def get_tripcode(self): | |
418 | if self.tripcode: |
|
429 | if self.tripcode: | |
419 | return Tripcode(self.tripcode) |
|
430 | return Tripcode(self.tripcode) | |
420 |
|
431 | |||
421 | def get_link_view(self): |
|
432 | def get_link_view(self): | |
422 | """ |
|
433 | """ | |
423 | Gets view of a reflink to the post. |
|
434 | Gets view of a reflink to the post. | |
424 | """ |
|
435 | """ | |
425 | result = '<a href="{}">>>{}</a>'.format(self.get_absolute_url(), |
|
436 | result = '<a href="{}">>>{}</a>'.format(self.get_absolute_url(), | |
426 | self.id) |
|
437 | self.id) | |
427 | if self.is_opening(): |
|
438 | if self.is_opening(): | |
428 | result = '<b>{}</b>'.format(result) |
|
439 | result = '<b>{}</b>'.format(result) | |
429 |
|
440 | |||
430 | return result |
|
441 | return result | |
431 |
|
442 | |||
432 | def is_hidden(self) -> bool: |
|
443 | def is_hidden(self) -> bool: | |
433 | return self.hidden |
|
444 | return self.hidden | |
434 |
|
445 | |||
435 | def set_hidden(self, hidden): |
|
446 | def set_hidden(self, hidden): | |
436 | self.hidden = hidden |
|
447 | self.hidden = hidden |
@@ -1,55 +1,57 | |||||
|
1 | from django.contrib.auth.context_processors import PermWrapper | |||
|
2 | ||||
1 | from boards import utils |
|
3 | from boards import utils | |
2 |
|
4 | |||
3 |
|
5 | |||
4 | PARAMETER_TRUNCATED = 'truncated' |
|
6 | PARAMETER_TRUNCATED = 'truncated' | |
5 |
|
7 | |||
6 | DIFF_TYPE_HTML = 'html' |
|
8 | DIFF_TYPE_HTML = 'html' | |
7 | DIFF_TYPE_JSON = 'json' |
|
9 | DIFF_TYPE_JSON = 'json' | |
8 |
|
10 | |||
9 |
|
11 | |||
10 | class Exporter(): |
|
12 | class Exporter(): | |
11 | @staticmethod |
|
13 | @staticmethod | |
12 | def export(post, request, include_last_update) -> str: |
|
14 | def export(post, request, include_last_update) -> str: | |
13 | pass |
|
15 | pass | |
14 |
|
16 | |||
15 |
|
17 | |||
16 | class HtmlExporter(Exporter): |
|
18 | class HtmlExporter(Exporter): | |
17 | @staticmethod |
|
19 | @staticmethod | |
18 | def export(post, request, include_last_update): |
|
20 | def export(post, request, include_last_update): | |
19 | if request is not None and PARAMETER_TRUNCATED in request.GET: |
|
21 | if request is not None and PARAMETER_TRUNCATED in request.GET: | |
20 | truncated = True |
|
22 | truncated = True | |
21 | reply_link = False |
|
23 | reply_link = False | |
22 | else: |
|
24 | else: | |
23 | truncated = False |
|
25 | truncated = False | |
24 | reply_link = True |
|
26 | reply_link = True | |
25 |
|
27 | |||
26 | return post.get_view(truncated=truncated, reply_link=reply_link, |
|
28 | return post.get_view(truncated=truncated, reply_link=reply_link, | |
27 | moderator=utils.is_moderator(request)) |
|
29 | perms=PermWrapper(request.user)) | |
28 |
|
30 | |||
29 |
|
31 | |||
30 | class JsonExporter(Exporter): |
|
32 | class JsonExporter(Exporter): | |
31 | @staticmethod |
|
33 | @staticmethod | |
32 | def export(post, request, include_last_update): |
|
34 | def export(post, request, include_last_update): | |
33 | post_json = { |
|
35 | post_json = { | |
34 | 'id': post.id, |
|
36 | 'id': post.id, | |
35 | 'title': post.title, |
|
37 | 'title': post.title, | |
36 | 'text': post.get_raw_text(), |
|
38 | 'text': post.get_raw_text(), | |
37 | } |
|
39 | } | |
38 | if post.images.exists(): |
|
40 | if post.images.exists(): | |
39 | post_image = post.get_first_image() |
|
41 | post_image = post.get_first_image() | |
40 | post_json['image'] = post_image.image.url |
|
42 | post_json['image'] = post_image.image.url | |
41 | post_json['image_preview'] = post_image.image.url_200x150 |
|
43 | post_json['image_preview'] = post_image.image.url_200x150 | |
42 | if include_last_update: |
|
44 | if include_last_update: | |
43 | post_json['bump_time'] = utils.datetime_to_epoch( |
|
45 | post_json['bump_time'] = utils.datetime_to_epoch( | |
44 | post.get_thread().bump_time) |
|
46 | post.get_thread().bump_time) | |
45 | return post_json |
|
47 | return post_json | |
46 |
|
48 | |||
47 |
|
49 | |||
48 | EXPORTERS = { |
|
50 | EXPORTERS = { | |
49 | DIFF_TYPE_HTML: HtmlExporter, |
|
51 | DIFF_TYPE_HTML: HtmlExporter, | |
50 | DIFF_TYPE_JSON: JsonExporter, |
|
52 | DIFF_TYPE_JSON: JsonExporter, | |
51 | } |
|
53 | } | |
52 |
|
54 | |||
53 |
|
55 | |||
54 | def get_exporter(export_type: str) -> Exporter: |
|
56 | def get_exporter(export_type: str) -> Exporter: | |
55 | return EXPORTERS[export_type]() |
|
57 | return EXPORTERS[export_type]() |
@@ -1,152 +1,149 | |||||
1 | import logging |
|
1 | import logging | |
2 |
|
2 | |||
3 | from datetime import datetime, timedelta, date |
|
3 | from datetime import datetime, timedelta, date | |
4 | from datetime import time as dtime |
|
4 | from datetime import time as dtime | |
5 |
|
5 | |||
6 | from django.db import models, transaction |
|
6 | from django.db import models, transaction | |
7 | from django.utils import timezone |
|
7 | from django.utils import timezone | |
8 |
|
8 | |||
9 | import boards |
|
9 | import boards | |
10 |
|
10 | |||
11 | from boards.models.user import Ban |
|
11 | from boards.models.user import Ban | |
12 | from boards.mdx_neboard import Parser |
|
12 | from boards.mdx_neboard import Parser | |
13 | from boards.models import PostImage, Attachment |
|
13 | from boards.models import PostImage, Attachment | |
14 | from boards import utils |
|
14 | from boards import utils | |
15 |
|
15 | |||
16 | __author__ = 'neko259' |
|
16 | __author__ = 'neko259' | |
17 |
|
17 | |||
18 | IMAGE_TYPES = ( |
|
18 | IMAGE_TYPES = ( | |
19 | 'jpeg', |
|
19 | 'jpeg', | |
20 | 'jpg', |
|
20 | 'jpg', | |
21 | 'png', |
|
21 | 'png', | |
22 | 'bmp', |
|
22 | 'bmp', | |
23 | 'gif', |
|
23 | 'gif', | |
24 | ) |
|
24 | ) | |
25 |
|
25 | |||
26 | POSTS_PER_DAY_RANGE = 7 |
|
26 | POSTS_PER_DAY_RANGE = 7 | |
27 | NO_IP = '0.0.0.0' |
|
27 | NO_IP = '0.0.0.0' | |
28 |
|
28 | |||
29 |
|
29 | |||
30 | class PostManager(models.Manager): |
|
30 | class PostManager(models.Manager): | |
31 | @transaction.atomic |
|
31 | @transaction.atomic | |
32 | def create_post(self, title: str, text: str, file=None, thread=None, |
|
32 | def create_post(self, title: str, text: str, file=None, thread=None, | |
33 | ip=NO_IP, tags: list=None, opening_posts: list=None, |
|
33 | ip=NO_IP, tags: list=None, opening_posts: list=None, | |
34 | tripcode=''): |
|
34 | tripcode='', monochrome=False): | |
35 | """ |
|
35 | """ | |
36 | Creates new post |
|
36 | Creates new post | |
37 | """ |
|
37 | """ | |
38 |
|
38 | |||
39 | if not utils.is_anonymous_mode(): |
|
39 | if not utils.is_anonymous_mode(): | |
40 | is_banned = Ban.objects.filter(ip=ip).exists() |
|
40 | is_banned = Ban.objects.filter(ip=ip).exists() | |
|
41 | else: | |||
|
42 | is_banned = False | |||
41 |
|
43 | |||
42 | # TODO Raise specific exception and catch it in the views |
|
44 | # TODO Raise specific exception and catch it in the views | |
43 | if is_banned: |
|
45 | if is_banned: | |
44 | raise Exception("This user is banned") |
|
46 | raise Exception("This user is banned") | |
45 |
|
47 | |||
46 | if not tags: |
|
48 | if not tags: | |
47 | tags = [] |
|
49 | tags = [] | |
48 | if not opening_posts: |
|
50 | if not opening_posts: | |
49 | opening_posts = [] |
|
51 | opening_posts = [] | |
50 |
|
52 | |||
51 | posting_time = timezone.now() |
|
53 | posting_time = timezone.now() | |
52 | new_thread = False |
|
54 | new_thread = False | |
53 | if not thread: |
|
55 | if not thread: | |
54 | thread = boards.models.thread.Thread.objects.create( |
|
56 | thread = boards.models.thread.Thread.objects.create( | |
55 |
bump_time=posting_time, last_edit_time=posting_time |
|
57 | bump_time=posting_time, last_edit_time=posting_time, | |
|
58 | monochrome=monochrome) | |||
56 | list(map(thread.tags.add, tags)) |
|
59 | list(map(thread.tags.add, tags)) | |
57 | boards.models.thread.Thread.objects.process_oldest_threads() |
|
60 | boards.models.thread.Thread.objects.process_oldest_threads() | |
58 | new_thread = True |
|
61 | new_thread = True | |
59 |
|
62 | |||
60 | pre_text = Parser().preparse(text) |
|
63 | pre_text = Parser().preparse(text) | |
61 |
|
64 | |||
62 | post = self.create(title=title, |
|
65 | post = self.create(title=title, | |
63 | text=pre_text, |
|
66 | text=pre_text, | |
64 | pub_time=posting_time, |
|
67 | pub_time=posting_time, | |
65 | poster_ip=ip, |
|
68 | poster_ip=ip, | |
66 | thread=thread, |
|
69 | thread=thread, | |
67 | last_edit_time=posting_time, |
|
70 | last_edit_time=posting_time, | |
68 | tripcode=tripcode, |
|
71 | tripcode=tripcode, | |
69 | opening=new_thread) |
|
72 | opening=new_thread) | |
70 | post.threads.add(thread) |
|
73 | post.threads.add(thread) | |
71 |
|
74 | |||
72 | logger = logging.getLogger('boards.post.create') |
|
75 | logger = logging.getLogger('boards.post.create') | |
73 |
|
76 | |||
74 | logger.info('Created post [{}] with text [{}] by {}'.format(post, |
|
77 | logger.info('Created post [{}] with text [{}] by {}'.format(post, | |
75 |
|
|
78 | post.get_text(),post.poster_ip)) | |
76 |
|
79 | |||
77 | # TODO Move this to other place |
|
80 | # TODO Move this to other place | |
78 | if file: |
|
81 | if file: | |
79 | file_type = file.name.split('.')[-1].lower() |
|
82 | file_type = file.name.split('.')[-1].lower() | |
80 | if file_type in IMAGE_TYPES: |
|
83 | if file_type in IMAGE_TYPES: | |
81 | post.images.add(PostImage.objects.create_with_hash(file)) |
|
84 | post.images.add(PostImage.objects.create_with_hash(file)) | |
82 | else: |
|
85 | else: | |
83 | post.attachments.add(Attachment.objects.create_with_hash(file)) |
|
86 | post.attachments.add(Attachment.objects.create_with_hash(file)) | |
84 |
|
87 | |||
85 | post.build_url() |
|
|||
86 | post.connect_replies() |
|
|||
87 | post.connect_threads(opening_posts) |
|
88 | post.connect_threads(opening_posts) | |
88 | post.connect_notifications() |
|
|||
89 | post.set_global_id() |
|
89 | post.set_global_id() | |
90 |
|
90 | |||
91 | # Thread needs to be bumped only when the post is already created |
|
91 | # Thread needs to be bumped only when the post is already created | |
92 | if not new_thread: |
|
92 | if not new_thread: | |
93 | thread.last_edit_time = posting_time |
|
93 | thread.last_edit_time = posting_time | |
94 | thread.bump() |
|
94 | thread.bump() | |
95 | thread.save() |
|
95 | thread.save() | |
96 |
|
96 | |||
97 | return post |
|
97 | return post | |
98 |
|
98 | |||
99 | def delete_posts_by_ip(self, ip): |
|
99 | def delete_posts_by_ip(self, ip): | |
100 | """ |
|
100 | """ | |
101 | Deletes all posts of the author with same IP |
|
101 | Deletes all posts of the author with same IP | |
102 | """ |
|
102 | """ | |
103 |
|
103 | |||
104 | posts = self.filter(poster_ip=ip) |
|
104 | posts = self.filter(poster_ip=ip) | |
105 | for post in posts: |
|
105 | for post in posts: | |
106 | post.delete() |
|
106 | post.delete() | |
107 |
|
107 | |||
108 | @utils.cached_result() |
|
108 | @utils.cached_result() | |
109 | def get_posts_per_day(self) -> float: |
|
109 | def get_posts_per_day(self) -> float: | |
110 | """ |
|
110 | """ | |
111 | Gets average count of posts per day for the last 7 days |
|
111 | Gets average count of posts per day for the last 7 days | |
112 | """ |
|
112 | """ | |
113 |
|
113 | |||
114 | day_end = date.today() |
|
114 | day_end = date.today() | |
115 | day_start = day_end - timedelta(POSTS_PER_DAY_RANGE) |
|
115 | day_start = day_end - timedelta(POSTS_PER_DAY_RANGE) | |
116 |
|
116 | |||
117 | day_time_start = timezone.make_aware(datetime.combine( |
|
117 | day_time_start = timezone.make_aware(datetime.combine( | |
118 | day_start, dtime()), timezone.get_current_timezone()) |
|
118 | day_start, dtime()), timezone.get_current_timezone()) | |
119 | day_time_end = timezone.make_aware(datetime.combine( |
|
119 | day_time_end = timezone.make_aware(datetime.combine( | |
120 | day_end, dtime()), timezone.get_current_timezone()) |
|
120 | day_end, dtime()), timezone.get_current_timezone()) | |
121 |
|
121 | |||
122 | posts_per_period = float(self.filter( |
|
122 | posts_per_period = float(self.filter( | |
123 | pub_time__lte=day_time_end, |
|
123 | pub_time__lte=day_time_end, | |
124 | pub_time__gte=day_time_start).count()) |
|
124 | pub_time__gte=day_time_start).count()) | |
125 |
|
125 | |||
126 | ppd = posts_per_period / POSTS_PER_DAY_RANGE |
|
126 | ppd = posts_per_period / POSTS_PER_DAY_RANGE | |
127 |
|
127 | |||
128 | return ppd |
|
128 | return ppd | |
129 |
|
129 | |||
130 | @transaction.atomic |
|
130 | @transaction.atomic | |
131 | def import_post(self, title: str, text: str, pub_time: str, global_id, |
|
131 | def import_post(self, title: str, text: str, pub_time: str, global_id, | |
132 | opening_post=None, tags=list()): |
|
132 | opening_post=None, tags=list()): | |
133 | is_opening = opening_post is None |
|
133 | is_opening = opening_post is None | |
134 | if is_opening: |
|
134 | if is_opening: | |
135 | thread = boards.models.thread.Thread.objects.create( |
|
135 | thread = boards.models.thread.Thread.objects.create( | |
136 | bump_time=pub_time, last_edit_time=pub_time) |
|
136 | bump_time=pub_time, last_edit_time=pub_time) | |
137 | list(map(thread.tags.add, tags)) |
|
137 | list(map(thread.tags.add, tags)) | |
138 | else: |
|
138 | else: | |
139 | thread = opening_post.get_thread() |
|
139 | thread = opening_post.get_thread() | |
140 |
|
140 | |||
141 | post = self.create(title=title, text=text, |
|
141 | post = self.create(title=title, text=text, | |
142 | pub_time=pub_time, |
|
142 | pub_time=pub_time, | |
143 | poster_ip=NO_IP, |
|
143 | poster_ip=NO_IP, | |
144 | last_edit_time=pub_time, |
|
144 | last_edit_time=pub_time, | |
145 | global_id=global_id, |
|
145 | global_id=global_id, | |
146 | opening=is_opening, |
|
146 | opening=is_opening, | |
147 | thread=thread) |
|
147 | thread=thread) | |
148 |
|
148 | |||
149 | post.threads.add(thread) |
|
149 | post.threads.add(thread) | |
150 | post.build_url() |
|
|||
151 | post.connect_replies() |
|
|||
152 | post.connect_notifications() |
|
@@ -1,143 +1,147 | |||||
1 | import hashlib |
|
1 | import hashlib | |
2 | from django.template.loader import render_to_string |
|
2 | from django.template.loader import render_to_string | |
3 | from django.db import models |
|
3 | from django.db import models | |
4 | from django.db.models import Count |
|
4 | from django.db.models import Count | |
5 | from django.core.urlresolvers import reverse |
|
5 | from django.core.urlresolvers import reverse | |
6 |
|
6 | |||
|
7 | from boards.models import PostImage | |||
7 | from boards.models.base import Viewable |
|
8 | from boards.models.base import Viewable | |
|
9 | from boards.models.thread import STATUS_ACTIVE, STATUS_BUMPLIMIT, STATUS_ARCHIVE | |||
8 | from boards.utils import cached_result |
|
10 | from boards.utils import cached_result | |
9 | import boards |
|
11 | import boards | |
10 |
|
12 | |||
11 | __author__ = 'neko259' |
|
13 | __author__ = 'neko259' | |
12 |
|
14 | |||
13 |
|
15 | |||
14 | RELATED_TAGS_COUNT = 5 |
|
16 | RELATED_TAGS_COUNT = 5 | |
15 |
|
17 | |||
16 |
|
18 | |||
17 | class TagManager(models.Manager): |
|
19 | class TagManager(models.Manager): | |
18 |
|
20 | |||
19 | def get_not_empty_tags(self): |
|
21 | def get_not_empty_tags(self): | |
20 | """ |
|
22 | """ | |
21 | Gets tags that have non-archived threads. |
|
23 | Gets tags that have non-archived threads. | |
22 | """ |
|
24 | """ | |
23 |
|
25 | |||
24 | return self.annotate(num_threads=Count('thread_tags')).filter(num_threads__gt=0)\ |
|
26 | return self.annotate(num_threads=Count('thread_tags')).filter(num_threads__gt=0)\ | |
25 | .order_by('-required', 'name') |
|
27 | .order_by('-required', 'name') | |
26 |
|
28 | |||
27 | def get_tag_url_list(self, tags: list) -> str: |
|
29 | def get_tag_url_list(self, tags: list) -> str: | |
28 | """ |
|
30 | """ | |
29 | Gets a comma-separated list of tag links. |
|
31 | Gets a comma-separated list of tag links. | |
30 | """ |
|
32 | """ | |
31 |
|
33 | |||
32 | return ', '.join([tag.get_view() for tag in tags]) |
|
34 | return ', '.join([tag.get_view() for tag in tags]) | |
33 |
|
35 | |||
34 |
|
36 | |||
35 | class Tag(models.Model, Viewable): |
|
37 | class Tag(models.Model, Viewable): | |
36 | """ |
|
38 | """ | |
37 | A tag is a text node assigned to the thread. The tag serves as a board |
|
39 | A tag is a text node assigned to the thread. The tag serves as a board | |
38 | section. There can be multiple tags for each thread |
|
40 | section. There can be multiple tags for each thread | |
39 | """ |
|
41 | """ | |
40 |
|
42 | |||
41 | objects = TagManager() |
|
43 | objects = TagManager() | |
42 |
|
44 | |||
43 | class Meta: |
|
45 | class Meta: | |
44 | app_label = 'boards' |
|
46 | app_label = 'boards' | |
45 | ordering = ('name',) |
|
47 | ordering = ('name',) | |
46 |
|
48 | |||
47 | name = models.CharField(max_length=100, db_index=True, unique=True) |
|
49 | name = models.CharField(max_length=100, db_index=True, unique=True) | |
48 | required = models.BooleanField(default=False, db_index=True) |
|
50 | required = models.BooleanField(default=False, db_index=True) | |
49 | description = models.TextField(blank=True) |
|
51 | description = models.TextField(blank=True) | |
50 |
|
52 | |||
51 | parent = models.ForeignKey('Tag', null=True, blank=True, |
|
53 | parent = models.ForeignKey('Tag', null=True, blank=True, | |
52 | related_name='children') |
|
54 | related_name='children') | |
53 |
|
55 | |||
54 | def __str__(self): |
|
56 | def __str__(self): | |
55 | return self.name |
|
57 | return self.name | |
56 |
|
58 | |||
57 | def is_empty(self) -> bool: |
|
59 | def is_empty(self) -> bool: | |
58 | """ |
|
60 | """ | |
59 | Checks if the tag has some threads. |
|
61 | Checks if the tag has some threads. | |
60 | """ |
|
62 | """ | |
61 |
|
63 | |||
62 | return self.get_thread_count() == 0 |
|
64 | return self.get_thread_count() == 0 | |
63 |
|
65 | |||
64 |
def get_thread_count(self, |
|
66 | def get_thread_count(self, status=None) -> int: | |
65 | threads = self.get_threads() |
|
67 | threads = self.get_threads() | |
66 |
if |
|
68 | if status is not None: | |
67 |
threads = threads.filter( |
|
69 | threads = threads.filter(status=status) | |
68 | if bumpable is not None: |
|
|||
69 | threads = threads.filter(bumpable=bumpable) |
|
|||
70 | return threads.count() |
|
70 | return threads.count() | |
71 |
|
71 | |||
72 | def get_active_thread_count(self) -> int: |
|
72 | def get_active_thread_count(self) -> int: | |
73 |
return self.get_thread_count( |
|
73 | return self.get_thread_count(status=STATUS_ACTIVE) | |
74 |
|
74 | |||
75 | def get_bumplimit_thread_count(self) -> int: |
|
75 | def get_bumplimit_thread_count(self) -> int: | |
76 |
return self.get_thread_count( |
|
76 | return self.get_thread_count(status=STATUS_BUMPLIMIT) | |
77 |
|
77 | |||
78 | def get_archived_thread_count(self) -> int: |
|
78 | def get_archived_thread_count(self) -> int: | |
79 |
return self.get_thread_count( |
|
79 | return self.get_thread_count(status=STATUS_ARCHIVE) | |
80 |
|
80 | |||
81 | def get_absolute_url(self): |
|
81 | def get_absolute_url(self): | |
82 | return reverse('tag', kwargs={'tag_name': self.name}) |
|
82 | return reverse('tag', kwargs={'tag_name': self.name}) | |
83 |
|
83 | |||
84 | def get_threads(self): |
|
84 | def get_threads(self): | |
85 | return self.thread_tags.order_by('-bump_time') |
|
85 | return self.thread_tags.order_by('-bump_time') | |
86 |
|
86 | |||
87 | def is_required(self): |
|
87 | def is_required(self): | |
88 | return self.required |
|
88 | return self.required | |
89 |
|
89 | |||
90 | def get_view(self): |
|
90 | def get_view(self): | |
91 | link = '<a class="tag" href="{}">{}</a>'.format( |
|
91 | link = '<a class="tag" href="{}">{}</a>'.format( | |
92 | self.get_absolute_url(), self.name) |
|
92 | self.get_absolute_url(), self.name) | |
93 | if self.is_required(): |
|
93 | if self.is_required(): | |
94 | link = '<b>{}</b>'.format(link) |
|
94 | link = '<b>{}</b>'.format(link) | |
95 | return link |
|
95 | return link | |
96 |
|
96 | |||
97 | def get_search_view(self, *args, **kwargs): |
|
97 | def get_search_view(self, *args, **kwargs): | |
98 | return render_to_string('boards/tag.html', { |
|
98 | return render_to_string('boards/tag.html', { | |
99 | 'tag': self, |
|
99 | 'tag': self, | |
100 | }) |
|
100 | }) | |
101 |
|
101 | |||
102 | @cached_result() |
|
102 | @cached_result() | |
103 | def get_post_count(self): |
|
103 | def get_post_count(self): | |
104 | return self.get_threads().aggregate(num_posts=Count('multi_replies'))['num_posts'] |
|
104 | return self.get_threads().aggregate(num_posts=Count('multi_replies'))['num_posts'] | |
105 |
|
105 | |||
106 | def get_description(self): |
|
106 | def get_description(self): | |
107 | return self.description |
|
107 | return self.description | |
108 |
|
108 | |||
109 |
def get_random_image_post(self, |
|
109 | def get_random_image_post(self, status=[STATUS_ACTIVE, STATUS_BUMPLIMIT]): | |
110 | posts = boards.models.Post.objects.annotate(images_count=Count( |
|
110 | posts = boards.models.Post.objects.annotate(images_count=Count( | |
111 | 'images')).filter(images_count__gt=0, threads__tags__in=[self]) |
|
111 | 'images')).filter(images_count__gt=0, threads__tags__in=[self]) | |
112 |
if |
|
112 | if status is not None: | |
113 |
posts = posts.filter(thread__ |
|
113 | posts = posts.filter(thread__status__in=status) | |
114 | return posts.order_by('?').first() |
|
114 | return posts.order_by('?').first() | |
115 |
|
115 | |||
116 | def get_first_letter(self): |
|
116 | def get_first_letter(self): | |
117 | return self.name and self.name[0] or '' |
|
117 | return self.name and self.name[0] or '' | |
118 |
|
118 | |||
119 | def get_related_tags(self): |
|
119 | def get_related_tags(self): | |
120 | return set(Tag.objects.filter(thread_tags__in=self.get_threads()).exclude( |
|
120 | return set(Tag.objects.filter(thread_tags__in=self.get_threads()).exclude( | |
121 | id=self.id).order_by('?')[:RELATED_TAGS_COUNT]) |
|
121 | id=self.id).order_by('?')[:RELATED_TAGS_COUNT]) | |
122 |
|
122 | |||
123 | @cached_result() |
|
123 | @cached_result() | |
124 | def get_color(self): |
|
124 | def get_color(self): | |
125 | """ |
|
125 | """ | |
126 | Gets color hashed from the tag name. |
|
126 | Gets color hashed from the tag name. | |
127 | """ |
|
127 | """ | |
128 | return hashlib.md5(self.name.encode()).hexdigest()[:6] |
|
128 | return hashlib.md5(self.name.encode()).hexdigest()[:6] | |
129 |
|
129 | |||
130 | def get_parent(self): |
|
130 | def get_parent(self): | |
131 | return self.parent |
|
131 | return self.parent | |
132 |
|
132 | |||
133 | def get_all_parents(self): |
|
133 | def get_all_parents(self): | |
134 | parents = list() |
|
134 | parents = list() | |
135 | parent = self.get_parent() |
|
135 | parent = self.get_parent() | |
136 | if parent and parent not in parents: |
|
136 | if parent and parent not in parents: | |
137 | parents.insert(0, parent) |
|
137 | parents.insert(0, parent) | |
138 | parents = parent.get_all_parents() + parents |
|
138 | parents = parent.get_all_parents() + parents | |
139 |
|
139 | |||
140 | return parents |
|
140 | return parents | |
141 |
|
141 | |||
142 | def get_children(self): |
|
142 | def get_children(self): | |
143 | return self.children |
|
143 | return self.children | |
|
144 | ||||
|
145 | def get_images(self): | |||
|
146 | return PostImage.objects.filter(post_images__thread__tags__in=[self])\ | |||
|
147 | .order_by('-post_images__pub_time') No newline at end of file |
@@ -1,258 +1,272 | |||||
1 | import logging |
|
1 | import logging | |
2 | from adjacent import Client |
|
2 | from adjacent import Client | |
3 |
|
3 | |||
4 | from django.db.models import Count, Sum, QuerySet, Q |
|
4 | from django.db.models import Count, Sum, QuerySet, Q | |
5 | from django.utils import timezone |
|
5 | from django.utils import timezone | |
6 | from django.db import models |
|
6 | from django.db import models | |
7 |
|
7 | |||
|
8 | from boards.models import STATUS_BUMPLIMIT, STATUS_ACTIVE, STATUS_ARCHIVE | |||
|
9 | ||||
8 | from boards import settings |
|
10 | from boards import settings | |
9 | import boards |
|
11 | import boards | |
10 | from boards.utils import cached_result, datetime_to_epoch |
|
12 | from boards.utils import cached_result, datetime_to_epoch | |
11 | from boards.models.post import Post |
|
13 | from boards.models.post import Post | |
12 | from boards.models.tag import Tag |
|
14 | from boards.models.tag import Tag | |
13 |
|
15 | |||
14 | FAV_THREAD_NO_UPDATES = -1 |
|
16 | FAV_THREAD_NO_UPDATES = -1 | |
15 |
|
17 | |||
16 |
|
18 | |||
17 | __author__ = 'neko259' |
|
19 | __author__ = 'neko259' | |
18 |
|
20 | |||
19 |
|
21 | |||
20 | logger = logging.getLogger(__name__) |
|
22 | logger = logging.getLogger(__name__) | |
21 |
|
23 | |||
22 |
|
24 | |||
23 | WS_NOTIFICATION_TYPE_NEW_POST = 'new_post' |
|
25 | WS_NOTIFICATION_TYPE_NEW_POST = 'new_post' | |
24 | WS_NOTIFICATION_TYPE = 'notification_type' |
|
26 | WS_NOTIFICATION_TYPE = 'notification_type' | |
25 |
|
27 | |||
26 | WS_CHANNEL_THREAD = "thread:" |
|
28 | WS_CHANNEL_THREAD = "thread:" | |
27 |
|
29 | |||
|
30 | STATUS_CHOICES = ( | |||
|
31 | (STATUS_ACTIVE, STATUS_ACTIVE), | |||
|
32 | (STATUS_BUMPLIMIT, STATUS_BUMPLIMIT), | |||
|
33 | (STATUS_ARCHIVE, STATUS_ARCHIVE), | |||
|
34 | ) | |||
|
35 | ||||
28 |
|
36 | |||
29 | class ThreadManager(models.Manager): |
|
37 | class ThreadManager(models.Manager): | |
30 | def process_oldest_threads(self): |
|
38 | def process_oldest_threads(self): | |
31 | """ |
|
39 | """ | |
32 | Preserves maximum thread count. If there are too many threads, |
|
40 | Preserves maximum thread count. If there are too many threads, | |
33 | archive or delete the old ones. |
|
41 | archive or delete the old ones. | |
34 | """ |
|
42 | """ | |
35 |
|
43 | |||
36 |
threads = Thread.objects. |
|
44 | threads = Thread.objects.exclude(status=STATUS_ARCHIVE).order_by('-bump_time') | |
37 | thread_count = threads.count() |
|
45 | thread_count = threads.count() | |
38 |
|
46 | |||
39 | max_thread_count = settings.get_int('Messages', 'MaxThreadCount') |
|
47 | max_thread_count = settings.get_int('Messages', 'MaxThreadCount') | |
40 | if thread_count > max_thread_count: |
|
48 | if thread_count > max_thread_count: | |
41 | num_threads_to_delete = thread_count - max_thread_count |
|
49 | num_threads_to_delete = thread_count - max_thread_count | |
42 | old_threads = threads[thread_count - num_threads_to_delete:] |
|
50 | old_threads = threads[thread_count - num_threads_to_delete:] | |
43 |
|
51 | |||
44 | for thread in old_threads: |
|
52 | for thread in old_threads: | |
45 | if settings.get_bool('Storage', 'ArchiveThreads'): |
|
53 | if settings.get_bool('Storage', 'ArchiveThreads'): | |
46 | self._archive_thread(thread) |
|
54 | self._archive_thread(thread) | |
47 | else: |
|
55 | else: | |
48 | thread.delete() |
|
56 | thread.delete() | |
49 |
|
57 | |||
50 | logger.info('Processed %d old threads' % num_threads_to_delete) |
|
58 | logger.info('Processed %d old threads' % num_threads_to_delete) | |
51 |
|
59 | |||
52 | def _archive_thread(self, thread): |
|
60 | def _archive_thread(self, thread): | |
53 | thread.archived = True |
|
61 | thread.status = STATUS_ARCHIVE | |
54 | thread.bumpable = False |
|
|||
55 | thread.last_edit_time = timezone.now() |
|
62 | thread.last_edit_time = timezone.now() | |
56 | thread.update_posts_time() |
|
63 | thread.update_posts_time() | |
57 |
thread.save(update_fields=[' |
|
64 | thread.save(update_fields=['last_edit_time', 'status']) | |
58 |
|
65 | |||
59 | def get_new_posts(self, datas): |
|
66 | def get_new_posts(self, datas): | |
60 | query = None |
|
67 | query = None | |
61 | # TODO Use classes instead of dicts |
|
68 | # TODO Use classes instead of dicts | |
62 | for data in datas: |
|
69 | for data in datas: | |
63 | if data['last_id'] != FAV_THREAD_NO_UPDATES: |
|
70 | if data['last_id'] != FAV_THREAD_NO_UPDATES: | |
64 | q = (Q(id=data['op'].get_thread().id) |
|
71 | q = (Q(id=data['op'].get_thread().id) | |
65 | & Q(multi_replies__id__gt=data['last_id'])) |
|
72 | & Q(multi_replies__id__gt=data['last_id'])) | |
66 | if query is None: |
|
73 | if query is None: | |
67 | query = q |
|
74 | query = q | |
68 | else: |
|
75 | else: | |
69 | query = query | q |
|
76 | query = query | q | |
70 | if query is not None: |
|
77 | if query is not None: | |
71 | return self.filter(query).annotate( |
|
78 | return self.filter(query).annotate( | |
72 | new_post_count=Count('multi_replies')) |
|
79 | new_post_count=Count('multi_replies')) | |
73 |
|
80 | |||
74 | def get_new_post_count(self, datas): |
|
81 | def get_new_post_count(self, datas): | |
75 | new_posts = self.get_new_posts(datas) |
|
82 | new_posts = self.get_new_posts(datas) | |
76 | return new_posts.aggregate(total_count=Count('multi_replies'))\ |
|
83 | return new_posts.aggregate(total_count=Count('multi_replies'))\ | |
77 | ['total_count'] if new_posts else 0 |
|
84 | ['total_count'] if new_posts else 0 | |
78 |
|
85 | |||
79 |
|
86 | |||
80 | def get_thread_max_posts(): |
|
87 | def get_thread_max_posts(): | |
81 | return settings.get_int('Messages', 'MaxPostsPerThread') |
|
88 | return settings.get_int('Messages', 'MaxPostsPerThread') | |
82 |
|
89 | |||
83 |
|
90 | |||
84 | class Thread(models.Model): |
|
91 | class Thread(models.Model): | |
85 | objects = ThreadManager() |
|
92 | objects = ThreadManager() | |
86 |
|
93 | |||
87 | class Meta: |
|
94 | class Meta: | |
88 | app_label = 'boards' |
|
95 | app_label = 'boards' | |
89 |
|
96 | |||
90 | tags = models.ManyToManyField('Tag', related_name='thread_tags') |
|
97 | tags = models.ManyToManyField('Tag', related_name='thread_tags') | |
91 | bump_time = models.DateTimeField(db_index=True) |
|
98 | bump_time = models.DateTimeField(db_index=True) | |
92 | last_edit_time = models.DateTimeField() |
|
99 | last_edit_time = models.DateTimeField() | |
93 | archived = models.BooleanField(default=False) |
|
|||
94 | bumpable = models.BooleanField(default=True) |
|
|||
95 | max_posts = models.IntegerField(default=get_thread_max_posts) |
|
100 | max_posts = models.IntegerField(default=get_thread_max_posts) | |
|
101 | status = models.CharField(max_length=50, default=STATUS_ACTIVE, | |||
|
102 | choices=STATUS_CHOICES) | |||
|
103 | monochrome = models.BooleanField(default=False) | |||
96 |
|
104 | |||
97 | def get_tags(self) -> QuerySet: |
|
105 | def get_tags(self) -> QuerySet: | |
98 | """ |
|
106 | """ | |
99 | Gets a sorted tag list. |
|
107 | Gets a sorted tag list. | |
100 | """ |
|
108 | """ | |
101 |
|
109 | |||
102 | return self.tags.order_by('name') |
|
110 | return self.tags.order_by('name') | |
103 |
|
111 | |||
104 | def bump(self): |
|
112 | def bump(self): | |
105 | """ |
|
113 | """ | |
106 | Bumps (moves to up) thread if possible. |
|
114 | Bumps (moves to up) thread if possible. | |
107 | """ |
|
115 | """ | |
108 |
|
116 | |||
109 | if self.can_bump(): |
|
117 | if self.can_bump(): | |
110 | self.bump_time = self.last_edit_time |
|
118 | self.bump_time = self.last_edit_time | |
111 |
|
119 | |||
112 | self.update_bump_status() |
|
120 | self.update_bump_status() | |
113 |
|
121 | |||
114 | logger.info('Bumped thread %d' % self.id) |
|
122 | logger.info('Bumped thread %d' % self.id) | |
115 |
|
123 | |||
116 | def has_post_limit(self) -> bool: |
|
124 | def has_post_limit(self) -> bool: | |
117 | return self.max_posts > 0 |
|
125 | return self.max_posts > 0 | |
118 |
|
126 | |||
119 | def update_bump_status(self, exclude_posts=None): |
|
127 | def update_bump_status(self, exclude_posts=None): | |
120 | if self.has_post_limit() and self.get_reply_count() >= self.max_posts: |
|
128 | if self.has_post_limit() and self.get_reply_count() >= self.max_posts: | |
121 | self.bumpable = False |
|
129 | self.status = STATUS_BUMPLIMIT | |
122 | self.update_posts_time(exclude_posts=exclude_posts) |
|
130 | self.update_posts_time(exclude_posts=exclude_posts) | |
123 |
|
131 | |||
124 | def _get_cache_key(self): |
|
132 | def _get_cache_key(self): | |
125 | return [datetime_to_epoch(self.last_edit_time)] |
|
133 | return [datetime_to_epoch(self.last_edit_time)] | |
126 |
|
134 | |||
127 | @cached_result(key_method=_get_cache_key) |
|
135 | @cached_result(key_method=_get_cache_key) | |
128 | def get_reply_count(self) -> int: |
|
136 | def get_reply_count(self) -> int: | |
129 | return self.get_replies().count() |
|
137 | return self.get_replies().count() | |
130 |
|
138 | |||
131 | @cached_result(key_method=_get_cache_key) |
|
139 | @cached_result(key_method=_get_cache_key) | |
132 | def get_images_count(self) -> int: |
|
140 | def get_images_count(self) -> int: | |
133 | return self.get_replies().annotate(images_count=Count( |
|
141 | return self.get_replies().annotate(images_count=Count( | |
134 | 'images')).aggregate(Sum('images_count'))['images_count__sum'] |
|
142 | 'images')).aggregate(Sum('images_count'))['images_count__sum'] | |
135 |
|
143 | |||
136 | def can_bump(self) -> bool: |
|
144 | def can_bump(self) -> bool: | |
137 | """ |
|
145 | """ | |
138 | Checks if the thread can be bumped by replying to it. |
|
146 | Checks if the thread can be bumped by replying to it. | |
139 | """ |
|
147 | """ | |
140 |
|
148 | |||
141 | return self.bumpable and not self.is_archived() |
|
149 | return self.get_status() == STATUS_ACTIVE | |
142 |
|
150 | |||
143 | def get_last_replies(self) -> QuerySet: |
|
151 | def get_last_replies(self) -> QuerySet: | |
144 | """ |
|
152 | """ | |
145 | Gets several last replies, not including opening post |
|
153 | Gets several last replies, not including opening post | |
146 | """ |
|
154 | """ | |
147 |
|
155 | |||
148 | last_replies_count = settings.get_int('View', 'LastRepliesCount') |
|
156 | last_replies_count = settings.get_int('View', 'LastRepliesCount') | |
149 |
|
157 | |||
150 | if last_replies_count > 0: |
|
158 | if last_replies_count > 0: | |
151 | reply_count = self.get_reply_count() |
|
159 | reply_count = self.get_reply_count() | |
152 |
|
160 | |||
153 | if reply_count > 0: |
|
161 | if reply_count > 0: | |
154 | reply_count_to_show = min(last_replies_count, |
|
162 | reply_count_to_show = min(last_replies_count, | |
155 | reply_count - 1) |
|
163 | reply_count - 1) | |
156 | replies = self.get_replies() |
|
164 | replies = self.get_replies() | |
157 | last_replies = replies[reply_count - reply_count_to_show:] |
|
165 | last_replies = replies[reply_count - reply_count_to_show:] | |
158 |
|
166 | |||
159 | return last_replies |
|
167 | return last_replies | |
160 |
|
168 | |||
161 | def get_skipped_replies_count(self) -> int: |
|
169 | def get_skipped_replies_count(self) -> int: | |
162 | """ |
|
170 | """ | |
163 | Gets number of posts between opening post and last replies. |
|
171 | Gets number of posts between opening post and last replies. | |
164 | """ |
|
172 | """ | |
165 | reply_count = self.get_reply_count() |
|
173 | reply_count = self.get_reply_count() | |
166 | last_replies_count = min(settings.get_int('View', 'LastRepliesCount'), |
|
174 | last_replies_count = min(settings.get_int('View', 'LastRepliesCount'), | |
167 | reply_count - 1) |
|
175 | reply_count - 1) | |
168 | return reply_count - last_replies_count - 1 |
|
176 | return reply_count - last_replies_count - 1 | |
169 |
|
177 | |||
170 | def get_replies(self, view_fields_only=False) -> QuerySet: |
|
178 | def get_replies(self, view_fields_only=False) -> QuerySet: | |
171 | """ |
|
179 | """ | |
172 | Gets sorted thread posts |
|
180 | Gets sorted thread posts | |
173 | """ |
|
181 | """ | |
174 |
|
182 | |||
175 | query = self.multi_replies.order_by('pub_time').prefetch_related( |
|
183 | query = self.multi_replies.order_by('pub_time').prefetch_related( | |
176 | 'images', 'thread', 'threads', 'attachments') |
|
184 | 'images', 'thread', 'threads', 'attachments') | |
177 | if view_fields_only: |
|
185 | if view_fields_only: | |
178 | query = query.defer('poster_ip') |
|
186 | query = query.defer('poster_ip') | |
179 | return query.all() |
|
187 | return query.all() | |
180 |
|
188 | |||
181 | def get_top_level_replies(self) -> QuerySet: |
|
189 | def get_top_level_replies(self) -> QuerySet: | |
182 | return self.get_replies().exclude(refposts__threads__in=[self]) |
|
190 | return self.get_replies().exclude(refposts__threads__in=[self]) | |
183 |
|
191 | |||
184 | def get_replies_with_images(self, view_fields_only=False) -> QuerySet: |
|
192 | def get_replies_with_images(self, view_fields_only=False) -> QuerySet: | |
185 | """ |
|
193 | """ | |
186 | Gets replies that have at least one image attached |
|
194 | Gets replies that have at least one image attached | |
187 | """ |
|
195 | """ | |
188 |
|
196 | |||
189 | return self.get_replies(view_fields_only).annotate(images_count=Count( |
|
197 | return self.get_replies(view_fields_only).annotate(images_count=Count( | |
190 | 'images')).filter(images_count__gt=0) |
|
198 | 'images')).filter(images_count__gt=0) | |
191 |
|
199 | |||
192 | def get_opening_post(self, only_id=False) -> Post: |
|
200 | def get_opening_post(self, only_id=False) -> Post: | |
193 | """ |
|
201 | """ | |
194 | Gets the first post of the thread |
|
202 | Gets the first post of the thread | |
195 | """ |
|
203 | """ | |
196 |
|
204 | |||
197 | query = self.get_replies().filter(opening=True) |
|
205 | query = self.get_replies().filter(opening=True) | |
198 | if only_id: |
|
206 | if only_id: | |
199 | query = query.only('id') |
|
207 | query = query.only('id') | |
200 | opening_post = query.first() |
|
208 | opening_post = query.first() | |
201 |
|
209 | |||
202 | return opening_post |
|
210 | return opening_post | |
203 |
|
211 | |||
204 | @cached_result() |
|
212 | @cached_result() | |
205 | def get_opening_post_id(self) -> int: |
|
213 | def get_opening_post_id(self) -> int: | |
206 | """ |
|
214 | """ | |
207 | Gets ID of the first thread post. |
|
215 | Gets ID of the first thread post. | |
208 | """ |
|
216 | """ | |
209 |
|
217 | |||
210 | return self.get_opening_post(only_id=True).id |
|
218 | return self.get_opening_post(only_id=True).id | |
211 |
|
219 | |||
212 | def get_pub_time(self): |
|
220 | def get_pub_time(self): | |
213 | """ |
|
221 | """ | |
214 | Gets opening post's pub time because thread does not have its own one. |
|
222 | Gets opening post's pub time because thread does not have its own one. | |
215 | """ |
|
223 | """ | |
216 |
|
224 | |||
217 | return self.get_opening_post().pub_time |
|
225 | return self.get_opening_post().pub_time | |
218 |
|
226 | |||
219 | def __str__(self): |
|
227 | def __str__(self): | |
220 | return 'T#{}/{}'.format(self.id, self.get_opening_post_id()) |
|
228 | return 'T#{}/{}'.format(self.id, self.get_opening_post_id()) | |
221 |
|
229 | |||
222 | def get_tag_url_list(self) -> list: |
|
230 | def get_tag_url_list(self) -> list: | |
223 | return boards.models.Tag.objects.get_tag_url_list(self.get_tags()) |
|
231 | return boards.models.Tag.objects.get_tag_url_list(self.get_tags()) | |
224 |
|
232 | |||
225 | def update_posts_time(self, exclude_posts=None): |
|
233 | def update_posts_time(self, exclude_posts=None): | |
226 | last_edit_time = self.last_edit_time |
|
234 | last_edit_time = self.last_edit_time | |
227 |
|
235 | |||
228 | for post in self.multi_replies.all(): |
|
236 | for post in self.multi_replies.all(): | |
229 | if exclude_posts is None or post not in exclude_posts: |
|
237 | if exclude_posts is None or post not in exclude_posts: | |
230 | # Manual update is required because uids are generated on save |
|
238 | # Manual update is required because uids are generated on save | |
231 | post.last_edit_time = last_edit_time |
|
239 | post.last_edit_time = last_edit_time | |
232 | post.save(update_fields=['last_edit_time']) |
|
240 | post.save(update_fields=['last_edit_time']) | |
233 |
|
241 | |||
234 | post.get_threads().update(last_edit_time=last_edit_time) |
|
242 | post.get_threads().update(last_edit_time=last_edit_time) | |
235 |
|
243 | |||
236 | def notify_clients(self): |
|
244 | def notify_clients(self): | |
237 | if not settings.get_bool('External', 'WebsocketsEnabled'): |
|
245 | if not settings.get_bool('External', 'WebsocketsEnabled'): | |
238 | return |
|
246 | return | |
239 |
|
247 | |||
240 | client = Client() |
|
248 | client = Client() | |
241 |
|
249 | |||
242 | channel_name = WS_CHANNEL_THREAD + str(self.get_opening_post_id()) |
|
250 | channel_name = WS_CHANNEL_THREAD + str(self.get_opening_post_id()) | |
243 | client.publish(channel_name, { |
|
251 | client.publish(channel_name, { | |
244 | WS_NOTIFICATION_TYPE: WS_NOTIFICATION_TYPE_NEW_POST, |
|
252 | WS_NOTIFICATION_TYPE: WS_NOTIFICATION_TYPE_NEW_POST, | |
245 | }) |
|
253 | }) | |
246 | client.send() |
|
254 | client.send() | |
247 |
|
255 | |||
248 | def get_absolute_url(self): |
|
256 | def get_absolute_url(self): | |
249 | return self.get_opening_post().get_absolute_url() |
|
257 | return self.get_opening_post().get_absolute_url() | |
250 |
|
258 | |||
251 | def get_required_tags(self): |
|
259 | def get_required_tags(self): | |
252 | return self.get_tags().filter(required=True) |
|
260 | return self.get_tags().filter(required=True) | |
253 |
|
261 | |||
254 | def get_replies_newer(self, post_id): |
|
262 | def get_replies_newer(self, post_id): | |
255 | return self.get_replies().filter(id__gt=post_id) |
|
263 | return self.get_replies().filter(id__gt=post_id) | |
256 |
|
264 | |||
257 | def is_archived(self): |
|
265 | def is_archived(self): | |
258 | return self.archived |
|
266 | return self.get_status() == STATUS_ARCHIVE | |
|
267 | ||||
|
268 | def get_status(self): | |||
|
269 | return self.status | |||
|
270 | ||||
|
271 | def is_monochrome(self): | |||
|
272 | return self.monochrome |
@@ -1,45 +1,45 | |||||
1 | from django.db import models |
|
1 | from django.db import models | |
2 | import boards |
|
2 | import boards | |
3 |
|
3 | |||
4 | __author__ = 'neko259' |
|
4 | __author__ = 'neko259' | |
5 |
|
5 | |||
6 | BAN_REASON_AUTO = 'Auto' |
|
6 | BAN_REASON_AUTO = 'Auto' | |
7 | BAN_REASON_MAX_LENGTH = 200 |
|
7 | BAN_REASON_MAX_LENGTH = 200 | |
8 |
|
8 | |||
9 |
|
9 | |||
10 | class Ban(models.Model): |
|
10 | class Ban(models.Model): | |
11 |
|
11 | |||
12 | class Meta: |
|
12 | class Meta: | |
13 | app_label = 'boards' |
|
13 | app_label = 'boards' | |
14 |
|
14 | |||
15 | ip = models.GenericIPAddressField() |
|
15 | ip = models.GenericIPAddressField() | |
16 | reason = models.CharField(default=BAN_REASON_AUTO, |
|
16 | reason = models.CharField(default=BAN_REASON_AUTO, | |
17 | max_length=BAN_REASON_MAX_LENGTH) |
|
17 | max_length=BAN_REASON_MAX_LENGTH) | |
18 | can_read = models.BooleanField(default=True) |
|
18 | can_read = models.BooleanField(default=True) | |
19 |
|
19 | |||
20 | def __str__(self): |
|
20 | def __str__(self): | |
21 | return self.ip |
|
21 | return self.ip | |
22 |
|
22 | |||
23 |
|
23 | |||
24 | class NotificationManager(models.Manager): |
|
24 | class NotificationManager(models.Manager): | |
25 |
def get_notification_posts(self, username: st |
|
25 | def get_notification_posts(self, usernames: list, last: int = None): | |
26 |
|
|
26 | lower_names = [username.lower() for username in usernames] | |
27 |
|
27 | posts = boards.models.post.Post.objects.filter( | ||
28 | posts = boards.models.post.Post.objects.filter(notification__name=i_username) |
|
28 | notification__name__in=lower_names).distinct() | |
29 | if last is not None: |
|
29 | if last is not None: | |
30 | posts = posts.filter(id__gt=last) |
|
30 | posts = posts.filter(id__gt=last) | |
31 | posts = posts.order_by('-id') |
|
31 | posts = posts.order_by('-id') | |
32 |
|
32 | |||
33 | return posts |
|
33 | return posts | |
34 |
|
34 | |||
35 |
|
35 | |||
36 | class Notification(models.Model): |
|
36 | class Notification(models.Model): | |
37 |
|
37 | |||
38 | class Meta: |
|
38 | class Meta: | |
39 | app_label = 'boards' |
|
39 | app_label = 'boards' | |
40 |
|
40 | |||
41 | objects = NotificationManager() |
|
41 | objects = NotificationManager() | |
42 |
|
42 | |||
43 | post = models.ForeignKey('Post') |
|
43 | post = models.ForeignKey('Post') | |
44 | name = models.TextField() |
|
44 | name = models.TextField() | |
45 |
|
45 |
@@ -1,80 +1,84 | |||||
1 | from django.contrib.syndication.views import Feed |
|
1 | from django.contrib.syndication.views import Feed | |
2 | from django.core.urlresolvers import reverse |
|
2 | from django.core.urlresolvers import reverse | |
3 | from django.shortcuts import get_object_or_404 |
|
3 | from django.shortcuts import get_object_or_404 | |
4 | from boards.models import Post, Tag, Thread |
|
4 | from boards.models import Post, Tag, Thread | |
5 | from boards import settings |
|
5 | from boards import settings | |
|
6 | from boards.models.thread import STATUS_ARCHIVE | |||
6 |
|
7 | |||
7 |
__author__ = 'neko |
|
8 | __author__ = 'nekorin' | |
|
9 | ||||
|
10 | ||||
|
11 | MAX_ITEMS = settings.get_int('RSS', 'MaxItems') | |||
8 |
|
12 | |||
9 |
|
13 | |||
10 | # TODO Make tests for all of these |
|
14 | # TODO Make tests for all of these | |
11 | class AllThreadsFeed(Feed): |
|
15 | class AllThreadsFeed(Feed): | |
12 |
|
16 | |||
13 | title = settings.get('Version', 'SiteName') + ' - All threads' |
|
17 | title = settings.get('Version', 'SiteName') + ' - All threads' | |
14 | link = '/' |
|
18 | link = '/' | |
15 | description_template = 'boards/rss/post.html' |
|
19 | description_template = 'boards/rss/post.html' | |
16 |
|
20 | |||
17 | def items(self): |
|
21 | def items(self): | |
18 |
return Thread.objects. |
|
22 | return Thread.objects.exclude(status=STATUS_ARCHIVE).order_by('-id')[:MAX_ITEMS] | |
19 |
|
23 | |||
20 | def item_title(self, item): |
|
24 | def item_title(self, item): | |
21 | return item.get_opening_post().title |
|
25 | return item.get_opening_post().title | |
22 |
|
26 | |||
23 | def item_link(self, item): |
|
27 | def item_link(self, item): | |
24 | return reverse('thread', args={item.get_opening_post_id()}) |
|
28 | return reverse('thread', args={item.get_opening_post_id()}) | |
25 |
|
29 | |||
26 | def item_pubdate(self, item): |
|
30 | def item_pubdate(self, item): | |
27 | return item.get_pub_time() |
|
31 | return item.get_pub_time() | |
28 |
|
32 | |||
29 |
|
33 | |||
30 | class TagThreadsFeed(Feed): |
|
34 | class TagThreadsFeed(Feed): | |
31 |
|
35 | |||
32 | link = '/' |
|
36 | link = '/' | |
33 | description_template = 'boards/rss/post.html' |
|
37 | description_template = 'boards/rss/post.html' | |
34 |
|
38 | |||
35 | def items(self, obj): |
|
39 | def items(self, obj): | |
36 |
return obj.threads. |
|
40 | return obj.get_threads().exclude(status=STATUS_ARCHIVE).order_by('-id')[:MAX_ITEMS] | |
37 |
|
41 | |||
38 | def get_object(self, request, tag_name): |
|
42 | def get_object(self, request, tag_name): | |
39 | return get_object_or_404(Tag, name=tag_name) |
|
43 | return get_object_or_404(Tag, name=tag_name) | |
40 |
|
44 | |||
41 | def item_title(self, item): |
|
45 | def item_title(self, item): | |
42 | return item.get_opening_post().title |
|
46 | return item.get_opening_post().title | |
43 |
|
47 | |||
44 | def item_link(self, item): |
|
48 | def item_link(self, item): | |
45 | return reverse('thread', args={item.get_opening_post_id()}) |
|
49 | return reverse('thread', args={item.get_opening_post_id()}) | |
46 |
|
50 | |||
47 | def item_pubdate(self, item): |
|
51 | def item_pubdate(self, item): | |
48 | return item.get_pub_time() |
|
52 | return item.get_pub_time() | |
49 |
|
53 | |||
50 | def title(self, obj): |
|
54 | def title(self, obj): | |
51 | return obj.name |
|
55 | return obj.name | |
52 |
|
56 | |||
53 |
|
57 | |||
54 | class ThreadPostsFeed(Feed): |
|
58 | class ThreadPostsFeed(Feed): | |
55 |
|
59 | |||
56 | link = '/' |
|
60 | link = '/' | |
57 | description_template = 'boards/rss/post.html' |
|
61 | description_template = 'boards/rss/post.html' | |
58 |
|
62 | |||
59 | def items(self, obj): |
|
63 | def items(self, obj): | |
60 | return obj.get_thread().get_replies() |
|
64 | return obj.get_thread().get_replies().order_by('-pub_time')[:MAX_ITEMS] | |
61 |
|
65 | |||
62 | def get_object(self, request, post_id): |
|
66 | def get_object(self, request, post_id): | |
63 | return get_object_or_404(Post, id=post_id) |
|
67 | return get_object_or_404(Post, id=post_id) | |
64 |
|
68 | |||
65 | def item_title(self, item): |
|
69 | def item_title(self, item): | |
66 | return item.title |
|
70 | return item.title | |
67 |
|
71 | |||
68 | def item_link(self, item): |
|
72 | def item_link(self, item): | |
69 | if not item.is_opening(): |
|
73 | if not item.is_opening(): | |
70 | return reverse('thread', args={ |
|
74 | return reverse('thread', args={ | |
71 | item.get_thread().get_opening_post_id() |
|
75 | item.get_thread().get_opening_post_id() | |
72 | }) + "#" + str(item.id) |
|
76 | }) + "#" + str(item.id) | |
73 | else: |
|
77 | else: | |
74 | return reverse('thread', args={item.id}) |
|
78 | return reverse('thread', args={item.id}) | |
75 |
|
79 | |||
76 | def item_pubdate(self, item): |
|
80 | def item_pubdate(self, item): | |
77 | return item.pub_time |
|
81 | return item.pub_time | |
78 |
|
82 | |||
79 | def title(self, obj): |
|
83 | def title(self, obj): | |
80 | return obj.title |
|
84 | return obj.title |
@@ -1,153 +1,159 | |||||
1 | .ui-button { |
|
1 | .ui-button { | |
2 | display: none; |
|
2 | display: none; | |
3 | } |
|
3 | } | |
4 |
|
4 | |||
5 | .ui-dialog-content { |
|
5 | .ui-dialog-content { | |
6 | padding: 0; |
|
6 | padding: 0; | |
7 | min-height: 0; |
|
7 | min-height: 0; | |
8 | } |
|
8 | } | |
9 |
|
9 | |||
10 | .mark_btn { |
|
10 | .mark_btn { | |
11 | cursor: pointer; |
|
11 | cursor: pointer; | |
12 | } |
|
12 | } | |
13 |
|
13 | |||
14 | .img-full { |
|
14 | .img-full { | |
15 | position: fixed; |
|
15 | position: fixed; | |
16 | background-color: #CCC; |
|
16 | background-color: #CCC; | |
17 | border: 1px solid #000; |
|
17 | border: 1px solid #000; | |
18 | cursor: pointer; |
|
18 | cursor: pointer; | |
19 | } |
|
19 | } | |
20 |
|
20 | |||
21 | .strikethrough { |
|
21 | .strikethrough { | |
22 | text-decoration: line-through; |
|
22 | text-decoration: line-through; | |
23 | } |
|
23 | } | |
24 |
|
24 | |||
25 | .post_preview { |
|
25 | .post_preview { | |
26 | z-index: 300; |
|
26 | z-index: 300; | |
27 | position:absolute; |
|
27 | position:absolute; | |
28 | } |
|
28 | } | |
29 |
|
29 | |||
30 | .gallery_image { |
|
30 | .gallery_image { | |
31 | display: inline-block; |
|
31 | display: inline-block; | |
32 | } |
|
32 | } | |
33 |
|
33 | |||
34 | @media print { |
|
34 | @media print { | |
35 | .post-form-w { |
|
35 | .post-form-w { | |
36 | display: none; |
|
36 | display: none; | |
37 | } |
|
37 | } | |
38 | } |
|
38 | } | |
39 |
|
39 | |||
40 | input[name="image"] { |
|
40 | input[name="image"] { | |
41 | display: block; |
|
41 | display: block; | |
42 | width: 100px; |
|
42 | width: 100px; | |
43 | height: 100px; |
|
43 | height: 100px; | |
44 | cursor: pointer; |
|
44 | cursor: pointer; | |
45 | position: absolute; |
|
45 | position: absolute; | |
46 | opacity: 0; |
|
46 | opacity: 0; | |
47 | z-index: 1; |
|
47 | z-index: 1; | |
48 | } |
|
48 | } | |
49 |
|
49 | |||
50 | .file_wrap { |
|
50 | .file_wrap { | |
51 | width: 100px; |
|
51 | width: 100px; | |
52 | height: 100px; |
|
52 | height: 100px; | |
53 | border: solid 1px white; |
|
53 | border: solid 1px white; | |
54 | display: inline-block; |
|
54 | display: inline-block; | |
55 | } |
|
55 | } | |
56 |
|
56 | |||
57 | form > .file_wrap { |
|
57 | form > .file_wrap { | |
58 | float: left; |
|
58 | float: left; | |
59 | } |
|
59 | } | |
60 |
|
60 | |||
61 | .file-thumb { |
|
61 | .file-thumb { | |
62 | width: 100px; |
|
62 | width: 100px; | |
63 | height: 100px; |
|
63 | height: 100px; | |
64 | background-size: cover; |
|
64 | background-size: cover; | |
65 | background-position: center; |
|
65 | background-position: center; | |
66 | } |
|
66 | } | |
67 |
|
67 | |||
68 | .compact-form-text { |
|
68 | .compact-form-text { | |
69 | margin-left:110px; |
|
69 | margin-left:110px; | |
70 | } |
|
70 | } | |
71 |
|
71 | |||
72 | textarea, input { |
|
72 | textarea, input { | |
73 | -moz-box-sizing: border-box; |
|
73 | -moz-box-sizing: border-box; | |
74 | -webkit-box-sizing: border-box; |
|
74 | -webkit-box-sizing: border-box; | |
75 | box-sizing: border-box; |
|
75 | box-sizing: border-box; | |
76 | } |
|
76 | } | |
77 |
|
77 | |||
78 | .compact-form-text > textarea { |
|
78 | .compact-form-text > textarea { | |
79 | height: 100px; |
|
79 | height: 100px; | |
80 | width: 100%; |
|
80 | width: 100%; | |
81 | } |
|
81 | } | |
82 |
|
82 | |||
83 | .post-button-form { |
|
83 | .post-button-form { | |
84 | display: inline; |
|
84 | display: inline; | |
85 | } |
|
85 | } | |
86 |
|
86 | |||
87 | .post-button-form > button, #autoupdate { |
|
87 | .post-button-form > button, #autoupdate { | |
88 | border: none; |
|
88 | border: none; | |
89 | margin: inherit; |
|
89 | margin: inherit; | |
90 | padding: inherit; |
|
90 | padding: inherit; | |
91 | background: none; |
|
91 | background: none; | |
92 | font-size: inherit; |
|
92 | font-size: inherit; | |
|
93 | cursor: pointer; | |||
93 | } |
|
94 | } | |
94 |
|
95 | |||
95 | #form-close-button { |
|
96 | #form-close-button { | |
96 | display: none; |
|
97 | display: none; | |
97 | } |
|
98 | } | |
98 |
|
99 | |||
99 | .post-image-full { |
|
100 | .post-image-full { | |
100 | width: 100%; |
|
101 | width: 100%; | |
101 | height: auto; |
|
102 | height: auto; | |
102 | } |
|
103 | } | |
103 |
|
104 | |||
104 | #preview-text { |
|
105 | #preview-text { | |
105 | display: none; |
|
106 | display: none; | |
106 | } |
|
107 | } | |
107 |
|
108 | |||
108 | .random-images-table { |
|
109 | .random-images-table { | |
109 | text-align: center; |
|
110 | text-align: center; | |
110 | width: 100%; |
|
111 | width: 100%; | |
111 | } |
|
112 | } | |
112 |
|
113 | |||
113 | .random-images-table > div { |
|
114 | .random-images-table > div { | |
114 | margin-left: auto; |
|
115 | margin-left: auto; | |
115 | margin-right: auto; |
|
116 | margin-right: auto; | |
116 | } |
|
117 | } | |
117 |
|
118 | |||
118 | .tag-image, .tag-text-data { |
|
119 | .tag-image, .tag-text-data { | |
119 | display: inline-block; |
|
120 | display: inline-block; | |
120 | } |
|
121 | } | |
121 |
|
122 | |||
122 | .tag-text-data > h2 { |
|
123 | .tag-text-data > h2 { | |
123 | margin: 0; |
|
124 | margin: 0; | |
124 | } |
|
125 | } | |
125 |
|
126 | |||
126 | .tag-image { |
|
127 | .tag-image { | |
127 | margin-right: 5px; |
|
128 | margin-right: 5px; | |
128 | } |
|
129 | } | |
129 |
|
130 | |||
130 | .reply-to-message { |
|
131 | .reply-to-message { | |
131 | display: none; |
|
132 | display: none; | |
132 | } |
|
133 | } | |
133 |
|
134 | |||
134 | .tripcode { |
|
135 | .tripcode { | |
135 | padding: 2px; |
|
136 | padding: 2px; | |
136 | } |
|
137 | } | |
137 |
|
138 | |||
138 | #fav-panel { |
|
139 | #fav-panel { | |
139 | display: none; |
|
140 | display: none; | |
140 | margin: 1ex; |
|
141 | margin: 1ex; | |
141 | } |
|
142 | } | |
142 |
|
143 | |||
143 | #new-fav-post-count { |
|
144 | #new-fav-post-count { | |
144 | display: none; |
|
145 | display: none; | |
145 | } |
|
146 | } | |
146 |
|
147 | |||
147 | .hidden_post { |
|
148 | .hidden_post { | |
148 | opacity: 0.2; |
|
149 | opacity: 0.2; | |
149 | } |
|
150 | } | |
150 |
|
151 | |||
151 | .hidden_post:hover { |
|
152 | .hidden_post:hover { | |
152 | opacity: 1; |
|
153 | opacity: 1; | |
153 | } |
|
154 | } | |
|
155 | ||||
|
156 | .monochrome > .image > .thumb > img { | |||
|
157 | filter: grayscale(100%); | |||
|
158 | -webkit-filter: grayscale(100%); | |||
|
159 | } |
@@ -1,579 +1,578 | |||||
1 | * { |
|
1 | * { | |
2 | text-decoration: none; |
|
2 | text-decoration: none; | |
3 | font-weight: inherit; |
|
3 | font-weight: inherit; | |
4 | } |
|
4 | } | |
5 |
|
5 | |||
6 | b, strong { |
|
6 | b, strong { | |
7 | font-weight: bold; |
|
7 | font-weight: bold; | |
8 | } |
|
8 | } | |
9 |
|
9 | |||
10 | html { |
|
10 | html { | |
11 | background: #555; |
|
11 | background: #555; | |
12 | color: #ffffff; |
|
12 | color: #ffffff; | |
13 | } |
|
13 | } | |
14 |
|
14 | |||
15 | body { |
|
15 | body { | |
16 | margin: 0; |
|
16 | margin: 0; | |
17 | } |
|
17 | } | |
18 |
|
18 | |||
19 | #admin_panel { |
|
19 | #admin_panel { | |
20 | background: #FF0000; |
|
20 | background: #FF0000; | |
21 | color: #00FF00 |
|
21 | color: #00FF00 | |
22 | } |
|
22 | } | |
23 |
|
23 | |||
24 | .input_field_error { |
|
24 | .input_field_error { | |
25 | color: #FF0000; |
|
25 | color: #FF0000; | |
26 | } |
|
26 | } | |
27 |
|
27 | |||
28 | .title { |
|
28 | .title { | |
29 | font-weight: bold; |
|
29 | font-weight: bold; | |
30 | color: #ffcc00; |
|
30 | color: #ffcc00; | |
31 | } |
|
31 | } | |
32 |
|
32 | |||
33 | .link, a { |
|
33 | .link, a { | |
34 | color: #afdcec; |
|
34 | color: #afdcec; | |
35 | } |
|
35 | } | |
36 |
|
36 | |||
37 | .block { |
|
37 | .block { | |
38 | display: inline-block; |
|
38 | display: inline-block; | |
39 | vertical-align: top; |
|
39 | vertical-align: top; | |
40 | } |
|
40 | } | |
41 |
|
41 | |||
42 | .tag { |
|
42 | .tag { | |
43 | color: #FFD37D; |
|
43 | color: #FFD37D; | |
44 | } |
|
44 | } | |
45 |
|
45 | |||
46 | .post_id { |
|
46 | .post_id { | |
47 | color: #fff380; |
|
47 | color: #fff380; | |
48 | } |
|
48 | } | |
49 |
|
49 | |||
50 | .post, .dead_post, .archive_post, #posts-table { |
|
50 | .post, .dead_post, .archive_post, #posts-table { | |
51 | background: #333; |
|
51 | background: #333; | |
52 | padding: 10px; |
|
52 | padding: 10px; | |
53 | clear: left; |
|
53 | clear: left; | |
54 | word-wrap: break-word; |
|
54 | word-wrap: break-word; | |
55 | border-top: 1px solid #777; |
|
55 | border-top: 1px solid #777; | |
56 | border-bottom: 1px solid #777; |
|
56 | border-bottom: 1px solid #777; | |
57 | } |
|
57 | } | |
58 |
|
58 | |||
59 | .post + .post { |
|
59 | .post + .post { | |
60 | border-top: none; |
|
60 | border-top: none; | |
61 | } |
|
61 | } | |
62 |
|
62 | |||
63 | .dead_post + .dead_post { |
|
63 | .dead_post + .dead_post { | |
64 | border-top: none; |
|
64 | border-top: none; | |
65 | } |
|
65 | } | |
66 |
|
66 | |||
67 | .archive_post + .archive_post { |
|
67 | .archive_post + .archive_post { | |
68 | border-top: none; |
|
68 | border-top: none; | |
69 | } |
|
69 | } | |
70 |
|
70 | |||
71 | .metadata { |
|
71 | .metadata { | |
72 | padding-top: 5px; |
|
72 | padding-top: 5px; | |
73 | margin-top: 10px; |
|
73 | margin-top: 10px; | |
74 | border-top: solid 1px #666; |
|
74 | border-top: solid 1px #666; | |
75 | color: #ddd; |
|
75 | color: #ddd; | |
76 | } |
|
76 | } | |
77 |
|
77 | |||
78 | .navigation_panel, .tag_info { |
|
78 | .navigation_panel, .tag_info { | |
79 | background: #222; |
|
79 | background: #222; | |
80 | margin-bottom: 5px; |
|
80 | margin-bottom: 5px; | |
81 | margin-top: 5px; |
|
81 | margin-top: 5px; | |
82 | padding: 10px; |
|
82 | padding: 10px; | |
83 | border-bottom: solid 1px #888; |
|
83 | border-bottom: solid 1px #888; | |
84 | border-top: solid 1px #888; |
|
84 | border-top: solid 1px #888; | |
85 | color: #eee; |
|
85 | color: #eee; | |
86 | } |
|
86 | } | |
87 |
|
87 | |||
88 | .navigation_panel .link:first-child { |
|
88 | .navigation_panel .link:first-child { | |
89 | border-right: 1px solid #fff; |
|
89 | border-right: 1px solid #fff; | |
90 | font-weight: bold; |
|
90 | font-weight: bold; | |
91 | margin-right: 1ex; |
|
91 | margin-right: 1ex; | |
92 | padding-right: 1ex; |
|
92 | padding-right: 1ex; | |
93 | } |
|
93 | } | |
94 |
|
94 | |||
95 | .navigation_panel .right-link { |
|
95 | .navigation_panel .right-link { | |
96 | border-left: 1px solid #fff; |
|
96 | border-left: 1px solid #fff; | |
97 | border-right: none; |
|
97 | border-right: none; | |
98 | float: right; |
|
98 | float: right; | |
99 | margin-left: 1ex; |
|
99 | margin-left: 1ex; | |
100 | margin-right: 0; |
|
100 | margin-right: 0; | |
101 | padding-left: 1ex; |
|
101 | padding-left: 1ex; | |
102 | padding-right: 0; |
|
102 | padding-right: 0; | |
103 | } |
|
103 | } | |
104 |
|
104 | |||
105 | .navigation_panel .link { |
|
105 | .navigation_panel .link { | |
106 | font-weight: bold; |
|
106 | font-weight: bold; | |
107 | } |
|
107 | } | |
108 |
|
108 | |||
109 | .navigation_panel::after, .post::after { |
|
109 | .navigation_panel::after, .post::after { | |
110 | clear: both; |
|
110 | clear: both; | |
111 | content: "."; |
|
111 | content: "."; | |
112 | display: block; |
|
112 | display: block; | |
113 | height: 0; |
|
113 | height: 0; | |
114 | line-height: 0; |
|
114 | line-height: 0; | |
115 | visibility: hidden; |
|
115 | visibility: hidden; | |
116 | } |
|
116 | } | |
117 |
|
117 | |||
118 | .tag_info { |
|
118 | .tag_info { | |
119 | text-align: center; |
|
119 | text-align: center; | |
120 | } |
|
120 | } | |
121 |
|
121 | |||
122 | .tag_info > .tag-text-data { |
|
122 | .tag_info > .tag-text-data { | |
123 | text-align: left; |
|
123 | text-align: left; | |
124 | } |
|
124 | } | |
125 |
|
125 | |||
126 | .header { |
|
126 | .header { | |
127 | border-bottom: solid 2px #ccc; |
|
127 | border-bottom: solid 2px #ccc; | |
128 | margin-bottom: 5px; |
|
128 | margin-bottom: 5px; | |
129 | border-top: none; |
|
129 | border-top: none; | |
130 | margin-top: 0; |
|
130 | margin-top: 0; | |
131 | } |
|
131 | } | |
132 |
|
132 | |||
133 | .footer { |
|
133 | .footer { | |
134 | border-top: solid 2px #ccc; |
|
134 | border-top: solid 2px #ccc; | |
135 | margin-top: 5px; |
|
135 | margin-top: 5px; | |
136 | border-bottom: none; |
|
136 | border-bottom: none; | |
137 | margin-bottom: 0; |
|
137 | margin-bottom: 0; | |
138 | } |
|
138 | } | |
139 |
|
139 | |||
140 | p, .br { |
|
140 | p, .br { | |
141 | margin-top: .5em; |
|
141 | margin-top: .5em; | |
142 | margin-bottom: .5em; |
|
142 | margin-bottom: .5em; | |
143 | } |
|
143 | } | |
144 |
|
144 | |||
145 | .post-form-w { |
|
145 | .post-form-w { | |
146 | background: #333344; |
|
146 | background: #333344; | |
147 | border-top: solid 1px #888; |
|
147 | border-top: solid 1px #888; | |
148 | border-bottom: solid 1px #888; |
|
148 | border-bottom: solid 1px #888; | |
149 | color: #fff; |
|
149 | color: #fff; | |
150 | padding: 10px; |
|
150 | padding: 10px; | |
151 | margin-bottom: 5px; |
|
151 | margin-bottom: 5px; | |
152 | margin-top: 5px; |
|
152 | margin-top: 5px; | |
153 | } |
|
153 | } | |
154 |
|
154 | |||
155 | .form-row { |
|
155 | .form-row { | |
156 | width: 100%; |
|
156 | width: 100%; | |
157 | display: table-row; |
|
157 | display: table-row; | |
158 | } |
|
158 | } | |
159 |
|
159 | |||
160 | .form-label { |
|
160 | .form-label { | |
161 | padding: .25em 1ex .25em 0; |
|
161 | padding: .25em 1ex .25em 0; | |
162 | vertical-align: top; |
|
162 | vertical-align: top; | |
163 | display: table-cell; |
|
163 | display: table-cell; | |
164 | } |
|
164 | } | |
165 |
|
165 | |||
166 | .form-input { |
|
166 | .form-input { | |
167 | padding: .25em 0; |
|
167 | padding: .25em 0; | |
168 | width: 100%; |
|
168 | width: 100%; | |
169 | display: table-cell; |
|
169 | display: table-cell; | |
170 | } |
|
170 | } | |
171 |
|
171 | |||
172 | .form-errors { |
|
172 | .form-errors { | |
173 | font-weight: bolder; |
|
173 | font-weight: bolder; | |
174 | vertical-align: middle; |
|
174 | vertical-align: middle; | |
175 | display: table-cell; |
|
175 | display: table-cell; | |
176 | } |
|
176 | } | |
177 |
|
177 | |||
178 | .post-form input:not([name="image"]):not([type="checkbox"]):not([type="submit"]), .post-form textarea, .post-form select { |
|
178 | .post-form input:not([name="image"]):not([type="checkbox"]):not([type="submit"]), .post-form textarea, .post-form select { | |
179 | background: #333; |
|
179 | background: #333; | |
180 | color: #fff; |
|
180 | color: #fff; | |
181 | border: solid 1px; |
|
181 | border: solid 1px; | |
182 | padding: 0; |
|
182 | padding: 0; | |
183 | font: medium sans-serif; |
|
183 | font: medium sans-serif; | |
184 | width: 100%; |
|
184 | width: 100%; | |
185 | } |
|
185 | } | |
186 |
|
186 | |||
187 | .post-form textarea { |
|
187 | .post-form textarea { | |
188 | resize: vertical; |
|
188 | resize: vertical; | |
189 | } |
|
189 | } | |
190 |
|
190 | |||
191 | .form-submit { |
|
191 | .form-submit { | |
192 | display: table; |
|
192 | display: table; | |
193 | margin-bottom: 1ex; |
|
193 | margin-bottom: 1ex; | |
194 | margin-top: 1ex; |
|
194 | margin-top: 1ex; | |
195 | } |
|
195 | } | |
196 |
|
196 | |||
197 | .form-title { |
|
197 | .form-title { | |
198 | font-weight: bold; |
|
198 | font-weight: bold; | |
199 | font-size: 2ex; |
|
199 | font-size: 2ex; | |
200 | margin-bottom: 0.5ex; |
|
200 | margin-bottom: 0.5ex; | |
201 | } |
|
201 | } | |
202 |
|
202 | |||
203 | input[type="submit"], button { |
|
203 | input[type="submit"], button { | |
204 | background: #222; |
|
204 | background: #222; | |
205 | border: solid 2px #fff; |
|
205 | border: solid 2px #fff; | |
206 | color: #fff; |
|
206 | color: #fff; | |
207 | padding: 0.5ex; |
|
207 | padding: 0.5ex; | |
208 | margin-right: 0.5ex; |
|
208 | margin-right: 0.5ex; | |
209 | } |
|
209 | } | |
210 |
|
210 | |||
211 | input[type="submit"]:hover { |
|
211 | input[type="submit"]:hover { | |
212 | background: #060; |
|
212 | background: #060; | |
213 | } |
|
213 | } | |
214 |
|
214 | |||
215 | .form-submit > button:hover { |
|
215 | .form-submit > button:hover { | |
216 | background: #006; |
|
216 | background: #006; | |
217 | } |
|
217 | } | |
218 |
|
218 | |||
219 | blockquote { |
|
219 | blockquote { | |
220 | border-left: solid 2px; |
|
220 | border-left: solid 2px; | |
221 | padding-left: 5px; |
|
221 | padding-left: 5px; | |
222 | color: #B1FB17; |
|
222 | color: #B1FB17; | |
223 | margin: 0; |
|
223 | margin: 0; | |
224 | } |
|
224 | } | |
225 |
|
225 | |||
226 | .post > .image { |
|
226 | .post > .image { | |
227 | float: left; |
|
227 | float: left; | |
228 | margin: 0 1ex .5ex 0; |
|
228 | margin: 0 1ex .5ex 0; | |
229 | min-width: 1px; |
|
229 | min-width: 1px; | |
230 | text-align: center; |
|
230 | text-align: center; | |
231 | display: table-row; |
|
231 | display: table-row; | |
232 | } |
|
232 | } | |
233 |
|
233 | |||
234 | .post > .metadata { |
|
234 | .post > .metadata { | |
235 | clear: left; |
|
235 | clear: left; | |
236 | } |
|
236 | } | |
237 |
|
237 | |||
238 | .get { |
|
238 | .get { | |
239 | font-weight: bold; |
|
239 | font-weight: bold; | |
240 | color: #d55; |
|
240 | color: #d55; | |
241 | } |
|
241 | } | |
242 |
|
242 | |||
243 | * { |
|
243 | * { | |
244 | text-decoration: none; |
|
244 | text-decoration: none; | |
245 | } |
|
245 | } | |
246 |
|
246 | |||
247 | .dead_post > .post-info { |
|
247 | .dead_post > .post-info { | |
248 | font-style: italic; |
|
248 | font-style: italic; | |
249 | } |
|
249 | } | |
250 |
|
250 | |||
251 | .archive_post > .post-info { |
|
251 | .archive_post > .post-info { | |
252 | text-decoration: line-through; |
|
252 | text-decoration: line-through; | |
253 | } |
|
253 | } | |
254 |
|
254 | |||
255 | .mark_btn { |
|
255 | .mark_btn { | |
256 | border: 1px solid; |
|
256 | border: 1px solid; | |
257 | padding: 2px 2ex; |
|
257 | padding: 2px 2ex; | |
258 | display: inline-block; |
|
258 | display: inline-block; | |
259 | margin: 0 5px 4px 0; |
|
259 | margin: 0 5px 4px 0; | |
260 | } |
|
260 | } | |
261 |
|
261 | |||
262 | .mark_btn:hover { |
|
262 | .mark_btn:hover { | |
263 | background: #555; |
|
263 | background: #555; | |
264 | } |
|
264 | } | |
265 |
|
265 | |||
266 | .quote { |
|
266 | .quote { | |
267 | color: #92cf38; |
|
267 | color: #92cf38; | |
268 | font-style: italic; |
|
268 | font-style: italic; | |
269 | } |
|
269 | } | |
270 |
|
270 | |||
271 | .multiquote { |
|
271 | .multiquote { | |
272 | padding: 3px; |
|
272 | padding: 3px; | |
273 | display: inline-block; |
|
273 | display: inline-block; | |
274 | background: #222; |
|
274 | background: #222; | |
275 | border-style: solid; |
|
275 | border-style: solid; | |
276 | border-width: 1px 1px 1px 4px; |
|
276 | border-width: 1px 1px 1px 4px; | |
277 | font-size: 0.9em; |
|
277 | font-size: 0.9em; | |
278 | } |
|
278 | } | |
279 |
|
279 | |||
280 | .spoiler { |
|
280 | .spoiler { | |
281 | background: black; |
|
281 | background: black; | |
282 | color: black; |
|
282 | color: black; | |
283 | padding: 0 1ex 0 1ex; |
|
283 | padding: 0 1ex 0 1ex; | |
284 | } |
|
284 | } | |
285 |
|
285 | |||
286 | .spoiler:hover { |
|
286 | .spoiler:hover { | |
287 | color: #ddd; |
|
287 | color: #ddd; | |
288 | } |
|
288 | } | |
289 |
|
289 | |||
290 | .comment { |
|
290 | .comment { | |
291 | color: #eb2; |
|
291 | color: #eb2; | |
292 | } |
|
292 | } | |
293 |
|
293 | |||
294 | a:hover { |
|
294 | a:hover { | |
295 | text-decoration: underline; |
|
295 | text-decoration: underline; | |
296 | } |
|
296 | } | |
297 |
|
297 | |||
298 | .last-replies { |
|
298 | .last-replies { | |
299 | margin-left: 3ex; |
|
299 | margin-left: 3ex; | |
300 | margin-right: 3ex; |
|
300 | margin-right: 3ex; | |
301 | border-left: solid 1px #777; |
|
301 | border-left: solid 1px #777; | |
302 | border-right: solid 1px #777; |
|
302 | border-right: solid 1px #777; | |
303 | } |
|
303 | } | |
304 |
|
304 | |||
305 | .last-replies > .post:first-child { |
|
305 | .last-replies > .post:first-child { | |
306 | border-top: none; |
|
306 | border-top: none; | |
307 | } |
|
307 | } | |
308 |
|
308 | |||
309 | .thread { |
|
309 | .thread { | |
310 | margin-bottom: 3ex; |
|
310 | margin-bottom: 3ex; | |
311 | margin-top: 1ex; |
|
311 | margin-top: 1ex; | |
312 | } |
|
312 | } | |
313 |
|
313 | |||
314 | .post:target { |
|
314 | .post:target { | |
315 | border: solid 2px white; |
|
315 | border: solid 2px white; | |
316 | } |
|
316 | } | |
317 |
|
317 | |||
318 | pre{ |
|
318 | pre{ | |
319 | white-space:pre-wrap |
|
319 | white-space:pre-wrap | |
320 | } |
|
320 | } | |
321 |
|
321 | |||
322 | li { |
|
322 | li { | |
323 | list-style-position: inside; |
|
323 | list-style-position: inside; | |
324 | } |
|
324 | } | |
325 |
|
325 | |||
326 | .fancybox-skin { |
|
326 | .fancybox-skin { | |
327 | position: relative; |
|
327 | position: relative; | |
328 | background-color: #fff; |
|
328 | background-color: #fff; | |
329 | color: #ddd; |
|
329 | color: #ddd; | |
330 | text-shadow: none; |
|
330 | text-shadow: none; | |
331 | } |
|
331 | } | |
332 |
|
332 | |||
333 | .fancybox-image { |
|
333 | .fancybox-image { | |
334 | border: 1px solid black; |
|
334 | border: 1px solid black; | |
335 | } |
|
335 | } | |
336 |
|
336 | |||
337 | .image-mode-tab { |
|
337 | .image-mode-tab { | |
338 | background: #444; |
|
338 | background: #444; | |
339 | color: #eee; |
|
339 | color: #eee; | |
340 | margin-top: 5px; |
|
340 | margin-top: 5px; | |
341 | padding: 5px; |
|
341 | padding: 5px; | |
342 | border-top: 1px solid #888; |
|
342 | border-top: 1px solid #888; | |
343 | border-bottom: 1px solid #888; |
|
343 | border-bottom: 1px solid #888; | |
344 | } |
|
344 | } | |
345 |
|
345 | |||
346 | .image-mode-tab > label { |
|
346 | .image-mode-tab > label { | |
347 | margin: 0 1ex; |
|
347 | margin: 0 1ex; | |
348 | } |
|
348 | } | |
349 |
|
349 | |||
350 | .image-mode-tab > label > input { |
|
350 | .image-mode-tab > label > input { | |
351 | margin-right: .5ex; |
|
351 | margin-right: .5ex; | |
352 | } |
|
352 | } | |
353 |
|
353 | |||
354 | #posts-table { |
|
354 | #posts-table { | |
355 | margin-top: 5px; |
|
355 | margin-top: 5px; | |
356 | margin-bottom: 5px; |
|
356 | margin-bottom: 5px; | |
357 | } |
|
357 | } | |
358 |
|
358 | |||
359 | .tag_info > h2 { |
|
359 | .tag_info > h2 { | |
360 | margin: 0; |
|
360 | margin: 0; | |
361 | } |
|
361 | } | |
362 |
|
362 | |||
363 | .post-info { |
|
363 | .post-info { | |
364 | color: #ddd; |
|
364 | color: #ddd; | |
365 | margin-bottom: 1ex; |
|
365 | margin-bottom: 1ex; | |
366 | } |
|
366 | } | |
367 |
|
367 | |||
368 | .moderator_info { |
|
368 | .moderator_info { | |
369 | color: #e99d41; |
|
369 | color: #e99d41; | |
370 | opacity: 0.4; |
|
370 | opacity: 0.4; | |
371 | } |
|
371 | } | |
372 |
|
372 | |||
373 | .moderator_info:hover { |
|
373 | .moderator_info:hover { | |
374 | opacity: 1; |
|
374 | opacity: 1; | |
375 | } |
|
375 | } | |
376 |
|
376 | |||
377 | .refmap { |
|
377 | .refmap { | |
378 | font-size: 0.9em; |
|
378 | font-size: 0.9em; | |
379 | color: #ccc; |
|
379 | color: #ccc; | |
380 | margin-top: 1em; |
|
380 | margin-top: 1em; | |
381 | } |
|
381 | } | |
382 |
|
382 | |||
383 | .fav { |
|
383 | .fav { | |
384 | color: yellow; |
|
384 | color: yellow; | |
385 | } |
|
385 | } | |
386 |
|
386 | |||
387 | .not_fav { |
|
387 | .not_fav { | |
388 | color: #ccc; |
|
388 | color: #ccc; | |
389 | } |
|
389 | } | |
390 |
|
390 | |||
391 | .role { |
|
|||
392 | text-decoration: underline; |
|
|||
393 | } |
|
|||
394 |
|
||||
395 | .form-email { |
|
391 | .form-email { | |
396 | display: none; |
|
392 | display: none; | |
397 | } |
|
393 | } | |
398 |
|
394 | |||
399 | .bar-value { |
|
395 | .bar-value { | |
400 | background: rgba(50, 55, 164, 0.45); |
|
396 | background: rgba(50, 55, 164, 0.45); | |
401 | font-size: 0.9em; |
|
397 | font-size: 0.9em; | |
402 | height: 1.5em; |
|
398 | height: 1.5em; | |
403 | } |
|
399 | } | |
404 |
|
400 | |||
405 | .bar-bg { |
|
401 | .bar-bg { | |
406 | position: relative; |
|
402 | position: relative; | |
407 | border-top: solid 1px #888; |
|
403 | border-top: solid 1px #888; | |
408 | border-bottom: solid 1px #888; |
|
404 | border-bottom: solid 1px #888; | |
409 | margin-top: 5px; |
|
405 | margin-top: 5px; | |
410 | overflow: hidden; |
|
406 | overflow: hidden; | |
411 | } |
|
407 | } | |
412 |
|
408 | |||
413 | .bar-text { |
|
409 | .bar-text { | |
414 | padding: 2px; |
|
410 | padding: 2px; | |
415 | position: absolute; |
|
411 | position: absolute; | |
416 | left: 0; |
|
412 | left: 0; | |
417 | top: 0; |
|
413 | top: 0; | |
418 | } |
|
414 | } | |
419 |
|
415 | |||
420 | .page_link { |
|
416 | .page_link { | |
421 | background: #444; |
|
417 | background: #444; | |
422 | border-top: solid 1px #888; |
|
418 | border-top: solid 1px #888; | |
423 | border-bottom: solid 1px #888; |
|
419 | border-bottom: solid 1px #888; | |
424 | padding: 5px; |
|
420 | padding: 5px; | |
425 | color: #eee; |
|
421 | color: #eee; | |
426 | font-size: 2ex; |
|
422 | font-size: 2ex; | |
427 | margin-top: .5ex; |
|
423 | margin-top: .5ex; | |
428 | margin-bottom: .5ex; |
|
424 | margin-bottom: .5ex; | |
429 | } |
|
425 | } | |
430 |
|
426 | |||
431 | .skipped_replies { |
|
427 | .skipped_replies { | |
432 | padding: 5px; |
|
428 | padding: 5px; | |
433 | margin-left: 3ex; |
|
429 | margin-left: 3ex; | |
434 | margin-right: 3ex; |
|
430 | margin-right: 3ex; | |
435 | border-left: solid 1px #888; |
|
431 | border-left: solid 1px #888; | |
436 | border-right: solid 1px #888; |
|
432 | border-right: solid 1px #888; | |
437 | border-bottom: solid 1px #888; |
|
433 | border-bottom: solid 1px #888; | |
438 | background: #000; |
|
434 | background: #000; | |
439 | } |
|
435 | } | |
440 |
|
436 | |||
441 | .current_page { |
|
437 | .current_page { | |
442 | padding: 2px; |
|
438 | padding: 2px; | |
443 | background-color: #afdcec; |
|
439 | background-color: #afdcec; | |
444 | color: #000; |
|
440 | color: #000; | |
445 | } |
|
441 | } | |
446 |
|
442 | |||
447 | .current_mode { |
|
443 | .current_mode { | |
448 | font-weight: bold; |
|
444 | font-weight: bold; | |
449 | } |
|
445 | } | |
450 |
|
446 | |||
451 | .gallery_image { |
|
447 | .gallery_image { | |
452 | border: solid 1px; |
|
448 | border: solid 1px; | |
453 | margin: 0.5ex; |
|
449 | margin: 0.5ex; | |
454 | text-align: center; |
|
450 | text-align: center; | |
455 | padding: 1ex; |
|
451 | padding: 1ex; | |
456 | } |
|
452 | } | |
457 |
|
453 | |||
458 | code { |
|
454 | code { | |
459 | border: dashed 1px #ccc; |
|
455 | border: dashed 1px #ccc; | |
460 | background: #111; |
|
456 | background: #111; | |
461 | padding: 2px; |
|
457 | padding: 2px; | |
462 | font-size: 1.2em; |
|
458 | font-size: 1.2em; | |
463 | display: inline-block; |
|
459 | display: inline-block; | |
464 | } |
|
460 | } | |
465 |
|
461 | |||
466 | pre { |
|
462 | pre { | |
467 | overflow: auto; |
|
463 | overflow: auto; | |
468 | } |
|
464 | } | |
469 |
|
465 | |||
470 | .img-full { |
|
466 | .img-full { | |
471 | background: #222; |
|
467 | background: #222; | |
472 | border: solid 1px white; |
|
468 | border: solid 1px white; | |
473 | } |
|
469 | } | |
474 |
|
470 | |||
475 | .tag_item { |
|
471 | .tag_item { | |
476 | display: inline-block; |
|
472 | display: inline-block; | |
477 | } |
|
473 | } | |
478 |
|
474 | |||
479 | #id_models li { |
|
475 | #id_models li { | |
480 | list-style: none; |
|
476 | list-style: none; | |
481 | } |
|
477 | } | |
482 |
|
478 | |||
483 | #id_q { |
|
479 | #id_q { | |
484 | margin-left: 1ex; |
|
480 | margin-left: 1ex; | |
485 | } |
|
481 | } | |
486 |
|
482 | |||
487 | ul { |
|
483 | ul { | |
488 | padding-left: 0px; |
|
484 | padding-left: 0px; | |
489 | } |
|
485 | } | |
490 |
|
486 | |||
491 | .quote-header { |
|
487 | .quote-header { | |
492 | border-bottom: 2px solid #ddd; |
|
488 | border-bottom: 2px solid #ddd; | |
493 | margin-bottom: 1ex; |
|
489 | margin-bottom: 1ex; | |
494 | padding-bottom: .5ex; |
|
490 | padding-bottom: .5ex; | |
495 | color: #ddd; |
|
491 | color: #ddd; | |
496 | font-size: 1.2em; |
|
492 | font-size: 1.2em; | |
497 | } |
|
493 | } | |
498 |
|
494 | |||
499 | .global-id { |
|
495 | .global-id { | |
500 | font-weight: bolder; |
|
496 | font-weight: bolder; | |
501 | opacity: .5; |
|
497 | opacity: .5; | |
502 | } |
|
498 | } | |
503 |
|
499 | |||
504 | /* Post */ |
|
500 | /* Post */ | |
505 | .post > .message, .post > .image { |
|
501 | .post > .message, .post > .image { | |
506 | padding-left: 1em; |
|
502 | padding-left: 1em; | |
507 | } |
|
503 | } | |
508 |
|
504 | |||
509 | /* Reflink preview */ |
|
505 | /* Reflink preview */ | |
510 | .post_preview { |
|
506 | .post_preview { | |
511 | border-left: 1px solid #777; |
|
507 | border-left: 1px solid #777; | |
512 | border-right: 1px solid #777; |
|
508 | border-right: 1px solid #777; | |
513 | max-width: 600px; |
|
509 | max-width: 600px; | |
514 | } |
|
510 | } | |
515 |
|
511 | |||
516 | /* Code highlighter */ |
|
512 | /* Code highlighter */ | |
517 | .hljs { |
|
513 | .hljs { | |
518 | color: #fff; |
|
514 | color: #fff; | |
519 | background: #000; |
|
515 | background: #000; | |
520 | display: inline-block; |
|
516 | display: inline-block; | |
521 | } |
|
517 | } | |
522 |
|
518 | |||
523 | .hljs, .hljs-subst, .hljs-tag .hljs-title, .lisp .hljs-title, .clojure .hljs-built_in, .nginx .hljs-title { |
|
519 | .hljs, .hljs-subst, .hljs-tag .hljs-title, .lisp .hljs-title, .clojure .hljs-built_in, .nginx .hljs-title { | |
524 | color: #fff; |
|
520 | color: #fff; | |
525 | } |
|
521 | } | |
526 |
|
522 | |||
527 | #up { |
|
523 | #up { | |
528 | position: fixed; |
|
524 | position: fixed; | |
529 | bottom: 5px; |
|
525 | bottom: 5px; | |
530 | right: 5px; |
|
526 | right: 5px; | |
531 | border: 1px solid #777; |
|
527 | border: 1px solid #777; | |
532 | background: #000; |
|
528 | background: #000; | |
533 | padding: 4px; |
|
529 | padding: 4px; | |
534 | opacity: 0.3; |
|
530 | opacity: 0.3; | |
535 | } |
|
531 | } | |
536 |
|
532 | |||
537 | #up:hover { |
|
533 | #up:hover { | |
538 | opacity: 1; |
|
534 | opacity: 1; | |
539 | } |
|
535 | } | |
540 |
|
536 | |||
541 | .user-cast { |
|
537 | .user-cast { | |
542 | border: solid #ffffff 1px; |
|
538 | border: solid #ffffff 1px; | |
543 | padding: .2ex; |
|
539 | padding: .2ex; | |
544 | background: #152154; |
|
540 | background: #152154; | |
545 | color: #fff; |
|
541 | color: #fff; | |
546 | } |
|
542 | } | |
547 |
|
543 | |||
548 | .highlight { |
|
544 | .highlight { | |
549 | background: #222; |
|
545 | background: #222; | |
550 | } |
|
546 | } | |
551 |
|
547 | |||
552 | .post-button-form > button:hover { |
|
548 | .post-button-form > button:hover { | |
553 | text-decoration: underline; |
|
549 | text-decoration: underline; | |
554 | } |
|
550 | } | |
555 |
|
551 | |||
556 | .tree_reply > .post { |
|
552 | .tree_reply > .post { | |
557 | margin-top: 1ex; |
|
553 | margin-top: 1ex; | |
558 | border-left: solid 1px #777; |
|
554 | border-left: solid 1px #777; | |
559 | padding-right: 0; |
|
555 | padding-right: 0; | |
560 | } |
|
556 | } | |
561 |
|
557 | |||
562 | #preview-text { |
|
558 | #preview-text { | |
563 | border: solid 1px white; |
|
559 | border: solid 1px white; | |
564 | margin: 1ex 0 1ex 0; |
|
560 | margin: 1ex 0 1ex 0; | |
565 | padding: 1ex; |
|
561 | padding: 1ex; | |
566 | } |
|
562 | } | |
567 |
|
563 | |||
568 | .image-metadata { |
|
564 | .image-metadata { | |
569 | font-style: italic; |
|
|||
570 | font-size: 0.9em; |
|
565 | font-size: 0.9em; | |
571 | } |
|
566 | } | |
572 |
|
567 | |||
573 | .tripcode { |
|
568 | .tripcode { | |
574 | color: white; |
|
569 | color: white; | |
575 | } |
|
570 | } | |
576 |
|
571 | |||
577 | #fav-panel { |
|
572 | #fav-panel { | |
578 | border: 1px solid white; |
|
573 | border: 1px solid white; | |
579 | } |
|
574 | } | |
|
575 | ||||
|
576 | .post-blink { | |||
|
577 | background-color: #000; | |||
|
578 | } |
@@ -1,383 +1,383 | |||||
1 | html { |
|
1 | html { | |
2 | background: rgb(238, 238, 238); |
|
2 | background: rgb(238, 238, 238); | |
3 | color: rgb(51, 51, 51); |
|
3 | color: rgb(51, 51, 51); | |
4 | } |
|
4 | } | |
5 |
|
5 | |||
6 | #admin_panel { |
|
6 | #admin_panel { | |
7 | background: #FF0000; |
|
7 | background: #FF0000; | |
8 | color: #00FF00 |
|
8 | color: #00FF00 | |
9 | } |
|
9 | } | |
10 |
|
10 | |||
11 | .input_field { |
|
11 | .input_field { | |
12 |
|
12 | |||
13 | } |
|
13 | } | |
14 |
|
14 | |||
15 | .input_field_name { |
|
15 | .input_field_name { | |
16 |
|
16 | |||
17 | } |
|
17 | } | |
18 |
|
18 | |||
19 | .input_field_error { |
|
19 | .input_field_error { | |
20 | color: #FF0000; |
|
20 | color: #FF0000; | |
21 | } |
|
21 | } | |
22 |
|
22 | |||
23 |
|
23 | |||
24 | .title { |
|
24 | .title { | |
25 | font-weight: bold; |
|
25 | font-weight: bold; | |
26 | color: #333; |
|
26 | color: #333; | |
27 | font-size: 2ex; |
|
27 | font-size: 2ex; | |
28 | } |
|
28 | } | |
29 |
|
29 | |||
30 | .link, a { |
|
30 | .link, a { | |
31 | color: #ff7000; |
|
31 | color: #ff7000; | |
32 | } |
|
32 | } | |
33 |
|
33 | |||
34 | .block { |
|
34 | .block { | |
35 | display: inline-block; |
|
35 | display: inline-block; | |
36 | vertical-align: top; |
|
36 | vertical-align: top; | |
37 | } |
|
37 | } | |
38 |
|
38 | |||
39 | .tag { |
|
39 | .tag { | |
40 | color: #222; |
|
40 | color: #222; | |
41 | } |
|
41 | } | |
42 |
|
42 | |||
43 | .post_id:hover { |
|
43 | .post_id:hover { | |
44 | color: #11f; |
|
44 | color: #11f; | |
45 | } |
|
45 | } | |
46 |
|
46 | |||
47 | .post_id { |
|
47 | .post_id { | |
48 | color: #444; |
|
48 | color: #444; | |
49 | } |
|
49 | } | |
50 |
|
50 | |||
51 | .post, .dead_post, #posts-table { |
|
51 | .post, .dead_post, #posts-table { | |
52 | margin: 5px; |
|
52 | margin: 5px; | |
53 | padding: 10px; |
|
53 | padding: 10px; | |
54 | background: rgb(221, 221, 221); |
|
54 | background: rgb(221, 221, 221); | |
55 | border: 1px solid rgb(204, 204, 204); |
|
55 | border: 1px solid rgb(204, 204, 204); | |
56 | border-radius: 5px 5px 5px 5px; |
|
56 | border-radius: 5px 5px 5px 5px; | |
57 | clear: left; |
|
57 | clear: left; | |
58 | word-wrap: break-word; |
|
58 | word-wrap: break-word; | |
59 | display: table; |
|
59 | display: table; | |
60 | } |
|
60 | } | |
61 |
|
61 | |||
62 | .metadata { |
|
62 | .metadata { | |
63 | padding: 5px; |
|
63 | padding: 5px; | |
64 | margin-top: 10px; |
|
64 | margin-top: 10px; | |
65 | border: solid 1px #666; |
|
65 | border: solid 1px #666; | |
66 | font-size: 0.9em; |
|
66 | font-size: 0.9em; | |
67 | display: table; |
|
67 | display: table; | |
68 | } |
|
68 | } | |
69 |
|
69 | |||
70 | .navigation_panel, .tag_info, .page_link { |
|
70 | .navigation_panel, .tag_info, .page_link { | |
71 | margin: 5px; |
|
71 | margin: 5px; | |
72 | padding: 10px; |
|
72 | padding: 10px; | |
73 | border: 1px solid rgb(204, 204, 204); |
|
73 | border: 1px solid rgb(204, 204, 204); | |
74 | border-radius: 5px 5px 5px 5px; |
|
74 | border-radius: 5px 5px 5px 5px; | |
75 | } |
|
75 | } | |
76 |
|
76 | |||
77 | .navigation_panel .link { |
|
77 | .navigation_panel .link { | |
78 | border-right: 1px solid #000; |
|
78 | border-right: 1px solid #000; | |
79 | font-weight: bold; |
|
79 | font-weight: bold; | |
80 | margin-right: 1ex; |
|
80 | margin-right: 1ex; | |
81 | padding-right: 1ex; |
|
81 | padding-right: 1ex; | |
82 | } |
|
82 | } | |
83 | .navigation_panel .link:last-child { |
|
83 | .navigation_panel .link:last-child { | |
84 | border-left: 1px solid #000; |
|
84 | border-left: 1px solid #000; | |
85 | border-right: none; |
|
85 | border-right: none; | |
86 | float: right; |
|
86 | float: right; | |
87 | margin-left: 1ex; |
|
87 | margin-left: 1ex; | |
88 | margin-right: 0; |
|
88 | margin-right: 0; | |
89 | padding-left: 1ex; |
|
89 | padding-left: 1ex; | |
90 | padding-right: 0; |
|
90 | padding-right: 0; | |
91 | } |
|
91 | } | |
92 |
|
92 | |||
93 | .navigation_panel::after, .post::after { |
|
93 | .navigation_panel::after, .post::after { | |
94 | clear: both; |
|
94 | clear: both; | |
95 | content: "."; |
|
95 | content: "."; | |
96 | display: block; |
|
96 | display: block; | |
97 | height: 0; |
|
97 | height: 0; | |
98 | line-height: 0; |
|
98 | line-height: 0; | |
99 | visibility: hidden; |
|
99 | visibility: hidden; | |
100 | } |
|
100 | } | |
101 |
|
101 | |||
102 | p { |
|
102 | p { | |
103 | margin-top: .5em; |
|
103 | margin-top: .5em; | |
104 | margin-bottom: .5em; |
|
104 | margin-bottom: .5em; | |
105 | } |
|
105 | } | |
106 |
|
106 | |||
107 | .post-form-w { |
|
107 | .post-form-w { | |
108 | display: table; |
|
108 | display: table; | |
109 | padding: 10px; |
|
109 | padding: 10px; | |
110 | margin: 5px |
|
110 | margin: 5px | |
111 | } |
|
111 | } | |
112 |
|
112 | |||
113 | .form-row { |
|
113 | .form-row { | |
114 | display: table-row; |
|
114 | display: table-row; | |
115 | } |
|
115 | } | |
116 |
|
116 | |||
117 | .form-label, .form-input, .form-errors { |
|
117 | .form-label, .form-input, .form-errors { | |
118 | display: table-cell; |
|
118 | display: table-cell; | |
119 | } |
|
119 | } | |
120 |
|
120 | |||
121 | .form-label { |
|
121 | .form-label { | |
122 | padding: .25em 1ex .25em 0; |
|
122 | padding: .25em 1ex .25em 0; | |
123 | vertical-align: top; |
|
123 | vertical-align: top; | |
124 | } |
|
124 | } | |
125 |
|
125 | |||
126 | .form-input { |
|
126 | .form-input { | |
127 | padding: .25em 0; |
|
127 | padding: .25em 0; | |
128 | } |
|
128 | } | |
129 |
|
129 | |||
130 | .form-errors { |
|
130 | .form-errors { | |
131 | padding-left: 1ex; |
|
131 | padding-left: 1ex; | |
132 | font-weight: bold; |
|
132 | font-weight: bold; | |
133 | vertical-align: middle; |
|
133 | vertical-align: middle; | |
134 | } |
|
134 | } | |
135 |
|
135 | |||
136 | .post-form input:not([name="image"]), .post-form textarea { |
|
136 | .post-form input:not([name="image"]), .post-form textarea { | |
137 | background: #fff; |
|
137 | background: #fff; | |
138 | color: #000; |
|
138 | color: #000; | |
139 | border: solid 1px; |
|
139 | border: solid 1px; | |
140 | padding: 0; |
|
140 | padding: 0; | |
141 | width: 100%; |
|
141 | width: 100%; | |
142 | font: medium sans; |
|
142 | font: medium sans; | |
143 | } |
|
143 | } | |
144 |
|
144 | |||
145 | .form-submit { |
|
145 | .form-submit { | |
146 | border-bottom: 2px solid #ddd; |
|
146 | border-bottom: 2px solid #ddd; | |
147 | margin-bottom: .5em; |
|
147 | margin-bottom: .5em; | |
148 | padding-bottom: .5em; |
|
148 | padding-bottom: .5em; | |
149 | } |
|
149 | } | |
150 |
|
150 | |||
151 | .form-title { |
|
151 | .form-title { | |
152 | font-weight: bold; |
|
152 | font-weight: bold; | |
153 | } |
|
153 | } | |
154 |
|
154 | |||
155 | input[type="submit"] { |
|
155 | input[type="submit"] { | |
156 | background: #fff; |
|
156 | background: #fff; | |
157 | border: solid 1px #000; |
|
157 | border: solid 1px #000; | |
158 | color: #000; |
|
158 | color: #000; | |
159 | } |
|
159 | } | |
160 |
|
160 | |||
161 | blockquote { |
|
161 | blockquote { | |
162 | border-left: solid 2px; |
|
162 | border-left: solid 2px; | |
163 | padding-left: 5px; |
|
163 | padding-left: 5px; | |
164 | color: #B1FB17; |
|
164 | color: #B1FB17; | |
165 | margin: 0; |
|
165 | margin: 0; | |
166 | } |
|
166 | } | |
167 |
|
167 | |||
168 | .post > .image { |
|
168 | .post > .image { | |
169 | float: left; |
|
169 | float: left; | |
170 | margin: 0 1ex .5ex 0; |
|
170 | margin: 0 1ex .5ex 0; | |
171 | min-width: 1px; |
|
171 | min-width: 1px; | |
172 | text-align: center; |
|
172 | text-align: center; | |
173 | display: table-row; |
|
173 | display: table-row; | |
174 | } |
|
174 | } | |
175 |
|
175 | |||
176 | .post > .metadata { |
|
176 | .post > .metadata { | |
177 | clear: left; |
|
177 | clear: left; | |
178 | } |
|
178 | } | |
179 |
|
179 | |||
180 | .get { |
|
180 | .get { | |
181 | font-weight: bold; |
|
181 | font-weight: bold; | |
182 | color: #d55; |
|
182 | color: #d55; | |
183 | } |
|
183 | } | |
184 |
|
184 | |||
185 | * { |
|
185 | * { | |
186 | text-decoration: none; |
|
186 | text-decoration: none; | |
187 | } |
|
187 | } | |
188 |
|
188 | |||
189 | .dead_post { |
|
189 | .dead_post { | |
190 | border-top: solid #d5494f; |
|
190 | border-top: solid #d5494f; | |
191 | } |
|
191 | } | |
192 |
|
192 | |||
193 | .archive_post { |
|
193 | .archive_post { | |
194 | border-top: solid #575e9f; |
|
194 | border-top: solid #575e9f; | |
195 | } |
|
195 | } | |
196 |
|
196 | |||
197 | .quote { |
|
197 | .quote { | |
198 | color: #080; |
|
198 | color: #080; | |
199 | font-style: italic; |
|
199 | font-style: italic; | |
200 | } |
|
200 | } | |
201 |
|
201 | |||
202 | .spoiler { |
|
202 | .spoiler { | |
203 | background: white; |
|
203 | background: white; | |
204 | color: white; |
|
204 | color: white; | |
205 | } |
|
205 | } | |
206 |
|
206 | |||
207 | .spoiler:hover { |
|
207 | .spoiler:hover { | |
208 | color: black; |
|
208 | color: black; | |
209 | } |
|
209 | } | |
210 |
|
210 | |||
211 | .comment { |
|
211 | .comment { | |
212 | color: #8B6914; |
|
212 | color: #8B6914; | |
213 | font-style: italic; |
|
213 | font-style: italic; | |
214 | } |
|
214 | } | |
215 |
|
215 | |||
216 | a:hover { |
|
216 | a:hover { | |
217 | text-decoration: underline; |
|
217 | text-decoration: underline; | |
218 | } |
|
218 | } | |
219 |
|
219 | |||
220 | .last-replies { |
|
220 | .last-replies { | |
221 | margin-left: 3ex; |
|
221 | margin-left: 3ex; | |
222 | } |
|
222 | } | |
223 |
|
223 | |||
224 | .thread { |
|
224 | .thread { | |
225 | margin-bottom: 3ex; |
|
225 | margin-bottom: 3ex; | |
226 | } |
|
226 | } | |
227 |
|
227 | |||
228 | .post:target { |
|
228 | .post:target { | |
229 | border: solid 2px black; |
|
229 | border: solid 2px black; | |
230 | } |
|
230 | } | |
231 |
|
231 | |||
232 | pre{ |
|
232 | pre{ | |
233 | white-space:pre-wrap |
|
233 | white-space:pre-wrap | |
234 | } |
|
234 | } | |
235 |
|
235 | |||
236 | li { |
|
236 | li { | |
237 | list-style-position: inside; |
|
237 | list-style-position: inside; | |
238 | } |
|
238 | } | |
239 |
|
239 | |||
240 | .fancybox-skin { |
|
240 | .fancybox-skin { | |
241 | position: relative; |
|
241 | position: relative; | |
242 | background-color: #fff; |
|
242 | background-color: #fff; | |
243 | color: #ddd; |
|
243 | color: #ddd; | |
244 | text-shadow: none; |
|
244 | text-shadow: none; | |
245 | } |
|
245 | } | |
246 |
|
246 | |||
247 | .fancybox-image { |
|
247 | .fancybox-image { | |
248 | border: 1px solid black; |
|
248 | border: 1px solid black; | |
249 | } |
|
249 | } | |
250 |
|
250 | |||
251 | .image-mode-tab { |
|
251 | .image-mode-tab { | |
252 | display: table; |
|
252 | display: table; | |
253 | margin: 5px; |
|
253 | margin: 5px; | |
254 | padding: 5px; |
|
254 | padding: 5px; | |
255 | background: rgb(221, 221, 221); |
|
255 | background: rgb(221, 221, 221); | |
256 | border: 1px solid rgb(204, 204, 204); |
|
256 | border: 1px solid rgb(204, 204, 204); | |
257 | border-radius: 5px 5px 5px 5px; |
|
257 | border-radius: 5px 5px 5px 5px; | |
258 | } |
|
258 | } | |
259 |
|
259 | |||
260 | .image-mode-tab > label { |
|
260 | .image-mode-tab > label { | |
261 | margin: 0 1ex; |
|
261 | margin: 0 1ex; | |
262 | } |
|
262 | } | |
263 |
|
263 | |||
264 | .image-mode-tab > label > input { |
|
264 | .image-mode-tab > label > input { | |
265 | margin-right: .5ex; |
|
265 | margin-right: .5ex; | |
266 | } |
|
266 | } | |
267 |
|
267 | |||
268 | #posts-table { |
|
268 | #posts-table { | |
269 | margin: 5px; |
|
269 | margin: 5px; | |
270 | } |
|
270 | } | |
271 |
|
271 | |||
272 | .tag_info, .page_link { |
|
272 | .tag_info, .page_link { | |
273 | display: table; |
|
273 | display: table; | |
274 | } |
|
274 | } | |
275 |
|
275 | |||
276 | .tag_info > h2 { |
|
276 | .tag_info > h2 { | |
277 | margin: 0; |
|
277 | margin: 0; | |
278 | } |
|
278 | } | |
279 |
|
279 | |||
280 | .moderator_info { |
|
280 | .moderator_info { | |
281 | color: #e99d41; |
|
281 | color: #e99d41; | |
282 | border: dashed 1px; |
|
282 | border: dashed 1px; | |
283 | padding: 3px; |
|
283 | padding: 3px; | |
284 | } |
|
284 | } | |
285 |
|
285 | |||
286 | .refmap { |
|
286 | .refmap { | |
287 | font-size: 0.9em; |
|
287 | font-size: 0.9em; | |
288 | color: #444; |
|
288 | color: #444; | |
289 | margin-top: 1em; |
|
289 | margin-top: 1em; | |
290 | } |
|
290 | } | |
291 |
|
291 | |||
292 | input[type="submit"]:hover { |
|
292 | input[type="submit"]:hover { | |
293 | background: #ccc; |
|
293 | background: #ccc; | |
294 | } |
|
294 | } | |
295 |
|
295 | |||
296 |
|
296 | |||
297 | .fav { |
|
297 | .fav { | |
298 | color: rgb(255, 102, 0); |
|
298 | color: rgb(255, 102, 0); | |
299 | } |
|
299 | } | |
300 |
|
300 | |||
301 | .not_fav { |
|
301 | .not_fav { | |
302 | color: #555; |
|
302 | color: #555; | |
303 | } |
|
303 | } | |
304 |
|
304 | |||
305 | .role { |
|
|||
306 | text-decoration: underline; |
|
|||
307 | } |
|
|||
308 |
|
||||
309 | .form-email { |
|
305 | .form-email { | |
310 | display: none; |
|
306 | display: none; | |
311 | } |
|
307 | } | |
312 |
|
308 | |||
313 | .mark_btn { |
|
309 | .mark_btn { | |
314 | padding: 2px 2ex; |
|
310 | padding: 2px 2ex; | |
315 | border: 1px solid; |
|
311 | border: 1px solid; | |
316 | } |
|
312 | } | |
317 |
|
313 | |||
318 | .mark_btn:hover { |
|
314 | .mark_btn:hover { | |
319 | background: #ccc; |
|
315 | background: #ccc; | |
320 | } |
|
316 | } | |
321 |
|
317 | |||
322 | .bar-value { |
|
318 | .bar-value { | |
323 | background: rgba(251, 199, 16, 0.61); |
|
319 | background: rgba(251, 199, 16, 0.61); | |
324 | padding: 2px; |
|
320 | padding: 2px; | |
325 | font-size: 0.9em; |
|
321 | font-size: 0.9em; | |
326 | height: 1.5em; |
|
322 | height: 1.5em; | |
327 | } |
|
323 | } | |
328 |
|
324 | |||
329 | .bar-bg { |
|
325 | .bar-bg { | |
330 | position: relative; |
|
326 | position: relative; | |
331 | border: 1px solid rgb(204, 204, 204); |
|
327 | border: 1px solid rgb(204, 204, 204); | |
332 | border-radius: 5px 5px 5px 5px; |
|
328 | border-radius: 5px 5px 5px 5px; | |
333 | margin: 5px; |
|
329 | margin: 5px; | |
334 | overflow: hidden; |
|
330 | overflow: hidden; | |
335 | } |
|
331 | } | |
336 |
|
332 | |||
337 | .bar-text { |
|
333 | .bar-text { | |
338 | padding: 2px; |
|
334 | padding: 2px; | |
339 | position: absolute; |
|
335 | position: absolute; | |
340 | left: 0; |
|
336 | left: 0; | |
341 | top: 0; |
|
337 | top: 0; | |
342 | } |
|
338 | } | |
343 |
|
339 | |||
344 | .skipped_replies { |
|
340 | .skipped_replies { | |
345 | margin: 5px; |
|
341 | margin: 5px; | |
346 | } |
|
342 | } | |
347 |
|
343 | |||
348 | .current_page, .current_mode { |
|
344 | .current_page, .current_mode { | |
349 | border: solid 1px #000; |
|
345 | border: solid 1px #000; | |
350 | padding: 2px; |
|
346 | padding: 2px; | |
351 | } |
|
347 | } | |
352 |
|
348 | |||
353 | .tag_item { |
|
349 | .tag_item { | |
354 | display: inline-block; |
|
350 | display: inline-block; | |
355 | border: 1px solid #ccc; |
|
351 | border: 1px solid #ccc; | |
356 | margin: 0.3ex; |
|
352 | margin: 0.3ex; | |
357 | padding: 0.2ex; |
|
353 | padding: 0.2ex; | |
358 | } |
|
354 | } | |
359 |
|
355 | |||
360 | .multiquote { |
|
356 | .multiquote { | |
361 | padding: 3px; |
|
357 | padding: 3px; | |
362 | display: inline-block; |
|
358 | display: inline-block; | |
363 | background: #ddd; |
|
359 | background: #ddd; | |
364 | border-style: solid; |
|
360 | border-style: solid; | |
365 | border-width: 1px 1px 1px 4px; |
|
361 | border-width: 1px 1px 1px 4px; | |
366 | border-color: #222; |
|
362 | border-color: #222; | |
367 | font-size: 0.9em; |
|
363 | font-size: 0.9em; | |
368 | } |
|
364 | } | |
369 |
|
365 | |||
370 | .highlight { |
|
366 | .highlight { | |
371 | background-color: #F9E8A5; |
|
367 | background-color: #F9E8A5; | |
372 | } |
|
368 | } | |
373 |
|
369 | |||
374 | #preview-text { |
|
370 | #preview-text { | |
375 | border: solid 1px black; |
|
371 | border: solid 1px black; | |
376 | margin: 1ex 0 1ex 0; |
|
372 | margin: 1ex 0 1ex 0; | |
377 | padding: 1ex; |
|
373 | padding: 1ex; | |
378 | } |
|
374 | } | |
379 |
|
375 | |||
380 | .image-metadata { |
|
376 | .image-metadata { | |
381 | font-style: italic; |
|
377 | font-style: italic; | |
382 | font-size: 0.9em; |
|
378 | font-size: 0.9em; | |
383 | } No newline at end of file |
|
379 | } | |
|
380 | ||||
|
381 | .post-blink { | |||
|
382 | background-color: #333; | |||
|
383 | } |
@@ -1,418 +1,418 | |||||
1 | * { |
|
1 | * { | |
2 | font-size: inherit; |
|
2 | font-size: inherit; | |
3 | margin: 0; |
|
3 | margin: 0; | |
4 | padding: 0; |
|
4 | padding: 0; | |
5 | } |
|
5 | } | |
6 | html { |
|
6 | html { | |
7 | background: #fff; |
|
7 | background: #fff; | |
8 | color: #000; |
|
8 | color: #000; | |
9 | font: medium sans-serif; |
|
9 | font: medium sans-serif; | |
10 | } |
|
10 | } | |
11 | a { |
|
11 | a { | |
12 | color: inherit; |
|
12 | color: inherit; | |
13 | text-decoration: underline; |
|
13 | text-decoration: underline; | |
14 | } |
|
14 | } | |
15 | li { |
|
15 | li { | |
16 | list-style-position: inside; |
|
16 | list-style-position: inside; | |
17 | } |
|
17 | } | |
18 |
|
18 | |||
19 | #admin_panel { |
|
19 | #admin_panel { | |
20 | background: #182F6F; |
|
20 | background: #182F6F; | |
21 | color: #fff; |
|
21 | color: #fff; | |
22 | padding: .5ex 1ex .5ex 1ex; |
|
22 | padding: .5ex 1ex .5ex 1ex; | |
23 | } |
|
23 | } | |
24 |
|
24 | |||
25 | .navigation_panel { |
|
25 | .navigation_panel { | |
26 | background: #182F6F; |
|
26 | background: #182F6F; | |
27 | color: #B4CFEC; |
|
27 | color: #B4CFEC; | |
28 | margin-bottom: 1em; |
|
28 | margin-bottom: 1em; | |
29 | padding: .5ex 1ex 1ex 1ex; |
|
29 | padding: .5ex 1ex 1ex 1ex; | |
30 | } |
|
30 | } | |
31 | .navigation_panel::after { |
|
31 | .navigation_panel::after { | |
32 | clear: both; |
|
32 | clear: both; | |
33 | content: "."; |
|
33 | content: "."; | |
34 | display: block; |
|
34 | display: block; | |
35 | height: 0; |
|
35 | height: 0; | |
36 | line-height: 0; |
|
36 | line-height: 0; | |
37 | visibility: hidden; |
|
37 | visibility: hidden; | |
38 | } |
|
38 | } | |
39 |
|
39 | |||
40 | .navigation_panel a:link, .navigation_panel a:visited, .navigation_panel a:hover { |
|
40 | .navigation_panel a:link, .navigation_panel a:visited, .navigation_panel a:hover { | |
41 | text-decoration: none; |
|
41 | text-decoration: none; | |
42 | } |
|
42 | } | |
43 |
|
43 | |||
44 | .navigation_panel .link { |
|
44 | .navigation_panel .link { | |
45 | border-right: 1px solid #fff; |
|
45 | border-right: 1px solid #fff; | |
46 | color: #fff; |
|
46 | color: #fff; | |
47 | font-weight: bold; |
|
47 | font-weight: bold; | |
48 | margin-right: 1ex; |
|
48 | margin-right: 1ex; | |
49 | padding-right: 1ex; |
|
49 | padding-right: 1ex; | |
50 | } |
|
50 | } | |
51 | .navigation_panel .right-link { |
|
51 | .navigation_panel .right-link { | |
52 | border-left: 1px solid #fff; |
|
52 | border-left: 1px solid #fff; | |
53 | border-right: none; |
|
53 | border-right: none; | |
54 | float: right; |
|
54 | float: right; | |
55 | margin-left: 1ex; |
|
55 | margin-left: 1ex; | |
56 | margin-right: 0; |
|
56 | margin-right: 0; | |
57 | padding-left: 1ex; |
|
57 | padding-left: 1ex; | |
58 | padding-right: 0; |
|
58 | padding-right: 0; | |
59 | } |
|
59 | } | |
60 |
|
60 | |||
61 | .navigation_panel .tag { |
|
61 | .navigation_panel .tag { | |
62 | color: #fff; |
|
62 | color: #fff; | |
63 | } |
|
63 | } | |
64 |
|
64 | |||
65 | .input_field { |
|
65 | .input_field { | |
66 |
|
66 | |||
67 | } |
|
67 | } | |
68 |
|
68 | |||
69 | .input_field_name { |
|
69 | .input_field_name { | |
70 |
|
70 | |||
71 | } |
|
71 | } | |
72 |
|
72 | |||
73 | .input_field_error { |
|
73 | .input_field_error { | |
74 | color: #FF0000; |
|
74 | color: #FF0000; | |
75 | } |
|
75 | } | |
76 |
|
76 | |||
77 |
|
77 | |||
78 | .title { |
|
78 | .title { | |
79 | color: #182F6F; |
|
79 | color: #182F6F; | |
80 | font-weight: bold; |
|
80 | font-weight: bold; | |
81 | } |
|
81 | } | |
82 |
|
82 | |||
83 | .post-form-w { |
|
83 | .post-form-w { | |
84 | background: #182F6F; |
|
84 | background: #182F6F; | |
85 | border-radius: 1ex; |
|
85 | border-radius: 1ex; | |
86 | color: #fff; |
|
86 | color: #fff; | |
87 | margin: 1em 1ex; |
|
87 | margin: 1em 1ex; | |
88 | padding: 1ex; |
|
88 | padding: 1ex; | |
89 | } |
|
89 | } | |
90 |
|
90 | |||
91 | .form-row { |
|
91 | .form-row { | |
92 | display: table; |
|
92 | display: table; | |
93 | width: 100%; |
|
93 | width: 100%; | |
94 | } |
|
94 | } | |
95 | .form-label, .form-input { |
|
95 | .form-label, .form-input { | |
96 | display: table-cell; |
|
96 | display: table-cell; | |
97 | vertical-align: top; |
|
97 | vertical-align: top; | |
98 | } |
|
98 | } | |
99 | .form-label { |
|
99 | .form-label { | |
100 | padding: .25em 1ex .25em 0; |
|
100 | padding: .25em 1ex .25em 0; | |
101 | width: 14ex; |
|
101 | width: 14ex; | |
102 | } |
|
102 | } | |
103 | .form-input { |
|
103 | .form-input { | |
104 | padding: .25em 0; |
|
104 | padding: .25em 0; | |
105 | } |
|
105 | } | |
106 | .form-input > * { |
|
106 | .form-input > * { | |
107 | background: #fff; |
|
107 | background: #fff; | |
108 | color: #000; |
|
108 | color: #000; | |
109 | border: none; |
|
109 | border: none; | |
110 | padding: 0; |
|
110 | padding: 0; | |
111 | resize: vertical; |
|
111 | resize: vertical; | |
112 | } |
|
112 | } | |
113 |
|
113 | |||
114 | .form-input > :not(.file_wrap) { |
|
114 | .form-input > :not(.file_wrap) { | |
115 | width: 100%; |
|
115 | width: 100%; | |
116 | } |
|
116 | } | |
117 |
|
117 | |||
118 | .form-submit { |
|
118 | .form-submit { | |
119 | border-bottom: 1px solid #666; |
|
119 | border-bottom: 1px solid #666; | |
120 | margin-bottom: .5em; |
|
120 | margin-bottom: .5em; | |
121 | padding-bottom: .5em; |
|
121 | padding-bottom: .5em; | |
122 | } |
|
122 | } | |
123 | .form-title { |
|
123 | .form-title { | |
124 | font-weight: bold; |
|
124 | font-weight: bold; | |
125 | margin-bottom: .5em; |
|
125 | margin-bottom: .5em; | |
126 | } |
|
126 | } | |
127 | .post-form .settings_item { |
|
127 | .post-form .settings_item { | |
128 | margin: .5em 0; |
|
128 | margin: .5em 0; | |
129 | } |
|
129 | } | |
130 | .form-submit input { |
|
130 | .form-submit input { | |
131 | margin-top: .5em; |
|
131 | margin-top: .5em; | |
132 | padding: .2em 1ex; |
|
132 | padding: .2em 1ex; | |
133 | } |
|
133 | } | |
134 | .form-label { |
|
134 | .form-label { | |
135 | text-align: left; |
|
135 | text-align: left; | |
136 | } |
|
136 | } | |
137 |
|
137 | |||
138 | .block { |
|
138 | .block { | |
139 | display: inline-block; |
|
139 | display: inline-block; | |
140 | vertical-align: top; |
|
140 | vertical-align: top; | |
141 | } |
|
141 | } | |
142 |
|
142 | |||
143 | .post_id { |
|
143 | .post_id { | |
144 | color: #a00; |
|
144 | color: #a00; | |
145 | } |
|
145 | } | |
146 |
|
146 | |||
147 | .post { |
|
147 | .post { | |
148 | clear: left; |
|
148 | clear: left; | |
149 | margin: 0 1ex 1em 1ex; |
|
149 | margin: 0 1ex 1em 1ex; | |
150 | overflow-x: auto; |
|
150 | overflow-x: auto; | |
151 | word-wrap: break-word; |
|
151 | word-wrap: break-word; | |
152 | background: #FFF; |
|
152 | background: #FFF; | |
153 | padding: 1ex; |
|
153 | padding: 1ex; | |
154 | border: 1px solid #666; |
|
154 | border: 1px solid #666; | |
155 | box-shadow: 1px 1px 2px 1px #666; |
|
155 | box-shadow: 1px 1px 2px 1px #666; | |
156 | } |
|
156 | } | |
157 |
|
157 | |||
158 | #posts > .post:last-child { |
|
158 | #posts > .post:last-child { | |
159 | border-bottom: none; |
|
159 | border-bottom: none; | |
160 | padding-bottom: 0; |
|
160 | padding-bottom: 0; | |
161 | } |
|
161 | } | |
162 |
|
162 | |||
163 | .metadata { |
|
163 | .metadata { | |
164 | background: #C0E4E8; |
|
164 | background: #C0E4E8; | |
165 | border: 1px solid #7F9699; |
|
165 | border: 1px solid #7F9699; | |
166 | border-radius: .4ex; |
|
166 | border-radius: .4ex; | |
167 | display: table; |
|
167 | display: table; | |
168 | margin-top: .5em; |
|
168 | margin-top: .5em; | |
169 | padding: .4em; |
|
169 | padding: .4em; | |
170 | } |
|
170 | } | |
171 |
|
171 | |||
172 | .post ul, .post ol { |
|
172 | .post ul, .post ol { | |
173 | margin: .5em 0 .5em 3ex; |
|
173 | margin: .5em 0 .5em 3ex; | |
174 | } |
|
174 | } | |
175 | .post li { |
|
175 | .post li { | |
176 | margin: .2em 0; |
|
176 | margin: .2em 0; | |
177 | } |
|
177 | } | |
178 | .post p { |
|
178 | .post p { | |
179 | margin: .5em 0; |
|
179 | margin: .5em 0; | |
180 | } |
|
180 | } | |
181 | .post blockquote { |
|
181 | .post blockquote { | |
182 | border-left: 3px solid #182F6F; |
|
182 | border-left: 3px solid #182F6F; | |
183 | margin: .5em 0 .5em 3ex; |
|
183 | margin: .5em 0 .5em 3ex; | |
184 | padding-left: 1ex; |
|
184 | padding-left: 1ex; | |
185 | } |
|
185 | } | |
186 | .post blockquote > blockquote { |
|
186 | .post blockquote > blockquote { | |
187 | padding-top: .1em; |
|
187 | padding-top: .1em; | |
188 | } |
|
188 | } | |
189 |
|
189 | |||
190 | .post > .image { |
|
190 | .post > .image { | |
191 | float: left; |
|
191 | float: left; | |
192 | margin-right: 1ex; |
|
192 | margin-right: 1ex; | |
193 | } |
|
193 | } | |
194 | .post > .metadata { |
|
194 | .post > .metadata { | |
195 | clear: left; |
|
195 | clear: left; | |
196 | } |
|
196 | } | |
197 |
|
197 | |||
198 | .post > .message .get { |
|
198 | .post > .message .get { | |
199 | color: #182F6F; font-weight: bold; |
|
199 | color: #182F6F; font-weight: bold; | |
200 | } |
|
200 | } | |
201 |
|
201 | |||
202 | .dead_post > .metadata { |
|
202 | .dead_post > .metadata { | |
203 | background: #eee; |
|
203 | background: #eee; | |
204 | } |
|
204 | } | |
205 |
|
205 | |||
206 | .quote, .multiquote { |
|
206 | .quote, .multiquote { | |
207 | color: #182F6F; |
|
207 | color: #182F6F; | |
208 | } |
|
208 | } | |
209 |
|
209 | |||
210 | .spoiler { |
|
210 | .spoiler { | |
211 | background: black; |
|
211 | background: black; | |
212 | color: black; |
|
212 | color: black; | |
213 | } |
|
213 | } | |
214 |
|
214 | |||
215 | .spoiler:hover { |
|
215 | .spoiler:hover { | |
216 | background: #ffffff; |
|
216 | background: #ffffff; | |
217 | } |
|
217 | } | |
218 |
|
218 | |||
219 | .comment { |
|
219 | .comment { | |
220 | color: #557055; |
|
220 | color: #557055; | |
221 | } |
|
221 | } | |
222 |
|
222 | |||
223 | .last-replies { |
|
223 | .last-replies { | |
224 | margin-left: 6ex; |
|
224 | margin-left: 6ex; | |
225 | } |
|
225 | } | |
226 |
|
226 | |||
227 | .thread > .post > .message > .post-info { |
|
227 | .thread > .post > .message > .post-info { | |
228 | border-bottom: 1px solid #ccc; |
|
228 | border-bottom: 1px solid #ccc; | |
229 | padding-bottom: .5em; |
|
229 | padding-bottom: .5em; | |
230 | } |
|
230 | } | |
231 |
|
231 | |||
232 | :target .post_id { |
|
232 | :target .post_id { | |
233 | background: #182F6F; |
|
233 | background: #182F6F; | |
234 | color: #FFF; |
|
234 | color: #FFF; | |
235 | text-decoration: none; |
|
235 | text-decoration: none; | |
236 | } |
|
236 | } | |
237 |
|
237 | |||
238 | .image-mode-tab { |
|
238 | .image-mode-tab { | |
239 | background: #182F6F; |
|
239 | background: #182F6F; | |
240 | color: #FFF; |
|
240 | color: #FFF; | |
241 | display: table; |
|
241 | display: table; | |
242 | margin: 1em auto 1em 0; |
|
242 | margin: 1em auto 1em 0; | |
243 | padding: .2em .5ex; |
|
243 | padding: .2em .5ex; | |
244 | } |
|
244 | } | |
245 |
|
245 | |||
246 | .image-mode-tab > label { |
|
246 | .image-mode-tab > label { | |
247 | margin: 0 1ex; |
|
247 | margin: 0 1ex; | |
248 | } |
|
248 | } | |
249 |
|
249 | |||
250 | .image-mode-tab > label > input { |
|
250 | .image-mode-tab > label > input { | |
251 | margin-right: .5ex; |
|
251 | margin-right: .5ex; | |
252 | } |
|
252 | } | |
253 |
|
253 | |||
254 | .tag_info, .page_link { |
|
254 | .tag_info, .page_link { | |
255 | margin: 1em 0; |
|
255 | margin: 1em 0; | |
256 | text-align: center; |
|
256 | text-align: center; | |
257 | } |
|
257 | } | |
258 |
|
258 | |||
259 | .form-errors { |
|
259 | .form-errors { | |
260 | margin-left: 1ex; |
|
260 | margin-left: 1ex; | |
261 | } |
|
261 | } | |
262 |
|
262 | |||
263 | .moderator_info { |
|
263 | .moderator_info { | |
264 | font-weight: bold; |
|
264 | font-weight: bold; | |
265 | float: right; |
|
265 | float: right; | |
266 | } |
|
266 | } | |
267 |
|
267 | |||
268 | .refmap { |
|
268 | .refmap { | |
269 | border: 1px dashed #aaa; |
|
269 | border: 1px dashed #aaa; | |
270 | padding: 0.5em; |
|
270 | padding: 0.5em; | |
271 | display: table; |
|
271 | display: table; | |
272 | } |
|
272 | } | |
273 |
|
273 | |||
274 | .fav { |
|
274 | .fav { | |
275 | color: blue; |
|
275 | color: blue; | |
276 | } |
|
276 | } | |
277 |
|
277 | |||
278 | .not_fav { |
|
278 | .not_fav { | |
279 | color: #ccc; |
|
279 | color: #ccc; | |
280 | } |
|
280 | } | |
281 |
|
281 | |||
282 | .role { |
|
|||
283 | text-decoration: underline; |
|
|||
284 | } |
|
|||
285 |
|
||||
286 | .form-email { |
|
282 | .form-email { | |
287 | display: none; |
|
283 | display: none; | |
288 | } |
|
284 | } | |
289 |
|
285 | |||
290 | .bar-value { |
|
286 | .bar-value { | |
291 | background: #E3E7F2; |
|
287 | background: #E3E7F2; | |
292 | padding: .1em 1ex; |
|
288 | padding: .1em 1ex; | |
293 | moz-box-sizing: border-box; |
|
289 | moz-box-sizing: border-box; | |
294 | box-sizing: border-box; |
|
290 | box-sizing: border-box; | |
295 | height: 1.5em; |
|
291 | height: 1.5em; | |
296 | } |
|
292 | } | |
297 |
|
293 | |||
298 | .bar-bg { |
|
294 | .bar-bg { | |
299 | background: #EA4649; |
|
295 | background: #EA4649; | |
300 | border: 1px solid #666; |
|
296 | border: 1px solid #666; | |
301 | margin: 0 1ex 1em 1ex; |
|
297 | margin: 0 1ex 1em 1ex; | |
302 | position: relative; |
|
298 | position: relative; | |
303 | overflow: hidden; |
|
299 | overflow: hidden; | |
304 | } |
|
300 | } | |
305 |
|
301 | |||
306 | .bar-text { |
|
302 | .bar-text { | |
307 | padding: 2px; |
|
303 | padding: 2px; | |
308 | position: absolute; |
|
304 | position: absolute; | |
309 | left: 0; |
|
305 | left: 0; | |
310 | top: 0; |
|
306 | top: 0; | |
311 | } |
|
307 | } | |
312 |
|
308 | |||
313 | .skipped_replies { |
|
309 | .skipped_replies { | |
314 | margin: 1ex; |
|
310 | margin: 1ex; | |
315 | } |
|
311 | } | |
316 |
|
312 | |||
317 | #mark-panel { |
|
313 | #mark-panel { | |
318 | background: #eee; |
|
314 | background: #eee; | |
319 | border-bottom: 1px solid #182F6F; |
|
315 | border-bottom: 1px solid #182F6F; | |
320 | } |
|
316 | } | |
321 |
|
317 | |||
322 | .mark_btn { |
|
318 | .mark_btn { | |
323 | display: inline-block; |
|
319 | display: inline-block; | |
324 | padding: .2em 1ex; |
|
320 | padding: .2em 1ex; | |
325 | border-left: 1px solid #182F6F; |
|
321 | border-left: 1px solid #182F6F; | |
326 | } |
|
322 | } | |
327 |
|
323 | |||
328 | .mark_btn:first-child { |
|
324 | .mark_btn:first-child { | |
329 | border-left: none; |
|
325 | border-left: none; | |
330 | } |
|
326 | } | |
331 |
|
327 | |||
332 | .mark_btn:last-child { |
|
328 | .mark_btn:last-child { | |
333 | border-right: 1px solid #182F6F; |
|
329 | border-right: 1px solid #182F6F; | |
334 | } |
|
330 | } | |
335 |
|
331 | |||
336 | .current_page { |
|
332 | .current_page { | |
337 | border-bottom: 1px solid #FFF; |
|
333 | border-bottom: 1px solid #FFF; | |
338 | padding: 0px 0.5ex; |
|
334 | padding: 0px 0.5ex; | |
339 | } |
|
335 | } | |
340 |
|
336 | |||
341 | .image-mode-tab a { |
|
337 | .image-mode-tab a { | |
342 | text-decoration: none; |
|
338 | text-decoration: none; | |
343 | } |
|
339 | } | |
344 | .image-mode-tab .current_mode::before { |
|
340 | .image-mode-tab .current_mode::before { | |
345 | content: "✓ "; |
|
341 | content: "✓ "; | |
346 | padding: 0 0 0 .5ex; |
|
342 | padding: 0 0 0 .5ex; | |
347 | color: #182F6F; |
|
343 | color: #182F6F; | |
348 | background: #FFF; |
|
344 | background: #FFF; | |
349 | } |
|
345 | } | |
350 | .image-mode-tab .current_mode { |
|
346 | .image-mode-tab .current_mode { | |
351 | padding: 0 .5ex 0 0; |
|
347 | padding: 0 .5ex 0 0; | |
352 | color: #182F6F; |
|
348 | color: #182F6F; | |
353 | background: #FFF; |
|
349 | background: #FFF; | |
354 | } |
|
350 | } | |
355 |
|
351 | |||
356 | .gallery_image_metadata { |
|
352 | .gallery_image_metadata { | |
357 | margin-bottom: 1em; |
|
353 | margin-bottom: 1em; | |
358 | } |
|
354 | } | |
359 |
|
355 | |||
360 | .gallery_image { |
|
356 | .gallery_image { | |
361 | padding: 4px; |
|
357 | padding: 4px; | |
362 | margin: .5em 0 0 1ex; |
|
358 | margin: .5em 0 0 1ex; | |
363 | text-align: center; |
|
359 | text-align: center; | |
364 | vertical-align: top; |
|
360 | vertical-align: top; | |
365 | } |
|
361 | } | |
366 |
|
362 | |||
367 | .swappable-form-full > form { |
|
363 | .swappable-form-full > form { | |
368 | display: table; |
|
364 | display: table; | |
369 | width: 100%; |
|
365 | width: 100%; | |
370 | } |
|
366 | } | |
371 |
|
367 | |||
372 | #id_models li { |
|
368 | #id_models li { | |
373 | list-style: none; |
|
369 | list-style: none; | |
374 | } |
|
370 | } | |
375 |
|
371 | |||
376 | #id_q { |
|
372 | #id_q { | |
377 | margin-left: 1ex; |
|
373 | margin-left: 1ex; | |
378 | } |
|
374 | } | |
379 |
|
375 | |||
380 | .br { |
|
376 | .br { | |
381 | margin-top: 0.5em; |
|
377 | margin-top: 0.5em; | |
382 | margin-bottom: 0.5em; |
|
378 | margin-bottom: 0.5em; | |
383 | } |
|
379 | } | |
384 |
|
380 | |||
385 | .message, .refmap { |
|
381 | .message, .refmap { | |
386 | margin-top: .5em; |
|
382 | margin-top: .5em; | |
387 | } |
|
383 | } | |
388 |
|
384 | |||
389 | .user-cast { |
|
385 | .user-cast { | |
390 | padding: 0.2em .5ex; |
|
386 | padding: 0.2em .5ex; | |
391 | background: #008; |
|
387 | background: #008; | |
392 | color: #FFF; |
|
388 | color: #FFF; | |
393 | display: inline-block; |
|
389 | display: inline-block; | |
394 | text-decoration: none; |
|
390 | text-decoration: none; | |
395 | } |
|
391 | } | |
396 |
|
392 | |||
397 | .highlight { |
|
393 | .highlight { | |
398 | background-color: #D4F0F9; |
|
394 | background-color: #D4F0F9; | |
399 | } |
|
395 | } | |
400 |
|
396 | |||
401 | .dead_post { |
|
397 | .dead_post { | |
402 | border-right: 1ex solid #666; |
|
398 | border-right: 1ex solid #666; | |
403 | } |
|
399 | } | |
404 |
|
400 | |||
405 | #preview-text { |
|
401 | #preview-text { | |
406 | border: solid 1px white; |
|
402 | border: solid 1px white; | |
407 | margin: 1ex 0 1ex 0; |
|
403 | margin: 1ex 0 1ex 0; | |
408 | padding: 1ex; |
|
404 | padding: 1ex; | |
409 | } |
|
405 | } | |
410 |
|
406 | |||
411 | .image-metadata { |
|
407 | .image-metadata { | |
412 | font-style: italic; |
|
408 | font-style: italic; | |
413 | font-size: 0.9em; |
|
409 | font-size: 0.9em; | |
414 | } |
|
410 | } | |
415 |
|
411 | |||
416 | audio { |
|
412 | audio { | |
417 | margin-top: 1em; |
|
413 | margin-top: 1em; | |
418 | } |
|
414 | } | |
|
415 | ||||
|
416 | .post-blink { | |||
|
417 | background-color: #ccc; | |||
|
418 | } |
@@ -1,43 +1,95 | |||||
1 | $('input[name=image]').wrap($('<div class="file_wrap"></div>')); |
|
1 | $('input[name=image]').wrap($('<div class="file_wrap"></div>')); | |
2 |
|
2 | |||
3 | $('body').on('change', 'input[name=image]', function(event) { |
|
3 | $('body').on('change', 'input[name=image]', function(event) { | |
4 | var file = event.target.files[0]; |
|
4 | var file = event.target.files[0]; | |
5 |
|
5 | |||
6 | if(file.type.match('image.*')) { |
|
6 | if(file.type.match('image.*')) { | |
7 | var fileReader = new FileReader(); |
|
7 | var fileReader = new FileReader(); | |
8 |
|
8 | |||
9 | fileReader.addEventListener("load", function(event) { |
|
9 | fileReader.addEventListener("load", function(event) { | |
10 | var wrapper = $('.file_wrap'); |
|
10 | var wrapper = $('.file_wrap'); | |
11 |
|
11 | |||
12 | wrapper.find('.file-thumb').remove(); |
|
12 | wrapper.find('.file-thumb').remove(); | |
13 | wrapper.append( |
|
13 | wrapper.append( | |
14 | $('<div class="file-thumb" style="background-image: url('+event.target.result+')"></div>') |
|
14 | $('<div class="file-thumb" style="background-image: url('+event.target.result+')"></div>') | |
15 | ); |
|
15 | ); | |
16 | }); |
|
16 | }); | |
17 |
|
17 | |||
18 | fileReader.readAsDataURL(file); |
|
18 | fileReader.readAsDataURL(file); | |
19 | } |
|
19 | } | |
20 | }); |
|
20 | }); | |
21 |
|
21 | |||
22 | var form = $('#form'); |
|
22 | var form = $('#form'); | |
23 | $('textarea').keypress(function(event) { |
|
23 | $('textarea').keypress(function(event) { | |
24 | if (event.which == 13 && event.ctrlKey) { |
|
24 | if (event.which == 13 && event.ctrlKey) { | |
25 | form.submit(); |
|
25 | form.find('input[type=submit]').click(); | |
26 | } |
|
26 | } | |
27 | }); |
|
27 | }); | |
28 |
|
28 | |||
29 | $('#preview-button').click(function() { |
|
29 | $('#preview-button').click(function() { | |
30 | var data = { |
|
30 | var data = { | |
31 | raw_text: $('textarea').val() |
|
31 | raw_text: $('textarea').val() | |
32 | } |
|
32 | } | |
33 |
|
33 | |||
34 | var diffUrl = '/api/preview/'; |
|
34 | var diffUrl = '/api/preview/'; | |
35 |
|
35 | |||
36 | $.post(diffUrl, |
|
36 | $.post(diffUrl, | |
37 | data, |
|
37 | data, | |
38 | function(data) { |
|
38 | function(data) { | |
39 | var previewTextBlock = $('#preview-text'); |
|
39 | var previewTextBlock = $('#preview-text'); | |
40 | previewTextBlock.html(data); |
|
40 | previewTextBlock.html(data); | |
41 | previewTextBlock.show(); |
|
41 | previewTextBlock.show(); | |
42 | }) |
|
42 | }) | |
43 | }) |
|
43 | }); | |
|
44 | ||||
|
45 | /** | |||
|
46 | * Show text in the errors row of the form. | |||
|
47 | * @param form | |||
|
48 | * @param text | |||
|
49 | */ | |||
|
50 | function showAsErrors(form, text) { | |||
|
51 | form.children('.form-errors').remove(); | |||
|
52 | ||||
|
53 | if (text.length > 0) { | |||
|
54 | var errorList = $('<div class="form-errors">' + text + '<div>'); | |||
|
55 | errorList.appendTo(form); | |||
|
56 | } | |||
|
57 | } | |||
|
58 | ||||
|
59 | function addHiddenInput(form, name, value) { | |||
|
60 | form.find('input[name=' + name + ']').val(value); | |||
|
61 | } | |||
|
62 | ||||
|
63 | $(document).ready(function() { | |||
|
64 | var powDifficulty = parseInt($('body').attr('data-pow-difficulty')); | |||
|
65 | if (powDifficulty > 0) { | |||
|
66 | var worker = new Worker($('#powScript').attr('src')); | |||
|
67 | worker.onmessage = function(e) { | |||
|
68 | var form = $('#form'); | |||
|
69 | addHiddenInput(form, 'timestamp', e.data.timestamp); | |||
|
70 | addHiddenInput(form, 'iteration', e.data.iteration); | |||
|
71 | addHiddenInput(form, 'guess', e.data.guess); | |||
|
72 | ||||
|
73 | form.submit(); | |||
|
74 | form.find('input[type=submit]').toggle(); | |||
|
75 | }; | |||
|
76 | ||||
|
77 | var form = $('#form'); | |||
|
78 | var submitButton = form.find('input[type=submit]'); | |||
|
79 | submitButton.click(function() { | |||
|
80 | showAsErrors(form, gettext('Computing PoW...')); | |||
|
81 | submitButton.toggle(); | |||
|
82 | ||||
|
83 | var msg = $('textarea').val().trim(); | |||
|
84 | ||||
|
85 | var data = { | |||
|
86 | msg: msg, | |||
|
87 | difficulty: parseInt($('body').attr('data-pow-difficulty')), | |||
|
88 | hasher: $('#sha256Script').attr('src') | |||
|
89 | }; | |||
|
90 | worker.postMessage(data); | |||
|
91 | ||||
|
92 | return false; | |||
|
93 | }); | |||
|
94 | } | |||
|
95 | }); |
@@ -1,181 +1,202 | |||||
1 | /* |
|
1 | /* | |
2 | @licstart The following is the entire license notice for the |
|
2 | @licstart The following is the entire license notice for the | |
3 | JavaScript code in this page. |
|
3 | JavaScript code in this page. | |
4 |
|
4 | |||
5 |
|
5 | |||
6 | Copyright (C) 2013 neko259 |
|
6 | Copyright (C) 2013 neko259 | |
7 |
|
7 | |||
8 | The JavaScript code in this page is free software: you can |
|
8 | The JavaScript code in this page is free software: you can | |
9 | redistribute it and/or modify it under the terms of the GNU |
|
9 | redistribute it and/or modify it under the terms of the GNU | |
10 | General Public License (GNU GPL) as published by the Free Software |
|
10 | General Public License (GNU GPL) as published by the Free Software | |
11 | Foundation, either version 3 of the License, or (at your option) |
|
11 | Foundation, either version 3 of the License, or (at your option) | |
12 | any later version. The code is distributed WITHOUT ANY WARRANTY; |
|
12 | any later version. The code is distributed WITHOUT ANY WARRANTY; | |
13 | without even the implied warranty of MERCHANTABILITY or FITNESS |
|
13 | without even the implied warranty of MERCHANTABILITY or FITNESS | |
14 | FOR A PARTICULAR PURPOSE. See the GNU GPL for more details. |
|
14 | FOR A PARTICULAR PURPOSE. See the GNU GPL for more details. | |
15 |
|
15 | |||
16 | As additional permission under GNU GPL version 3 section 7, you |
|
16 | As additional permission under GNU GPL version 3 section 7, you | |
17 | may distribute non-source (e.g., minimized or compacted) forms of |
|
17 | may distribute non-source (e.g., minimized or compacted) forms of | |
18 | that code without the copy of the GNU GPL normally required by |
|
18 | that code without the copy of the GNU GPL normally required by | |
19 | section 4, provided you include this license notice and a URL |
|
19 | section 4, provided you include this license notice and a URL | |
20 | through which recipients can access the Corresponding Source. |
|
20 | through which recipients can access the Corresponding Source. | |
21 |
|
21 | |||
22 | @licend The above is the entire license notice |
|
22 | @licend The above is the entire license notice | |
23 | for the JavaScript code in this page. |
|
23 | for the JavaScript code in this page. | |
24 | */ |
|
24 | */ | |
25 |
|
25 | |||
26 | var IMAGE_POPUP_MARGIN = 10; |
|
26 | var IMAGE_POPUP_MARGIN = 10; | |
27 |
|
27 | |||
28 |
|
28 | |||
29 | var IMAGE_VIEWERS = [ |
|
29 | var IMAGE_VIEWERS = [ | |
30 | ['simple', new SimpleImageViewer()], |
|
30 | ['simple', new SimpleImageViewer()], | |
31 | ['popup', new PopupImageViewer()] |
|
31 | ['popup', new PopupImageViewer()] | |
32 | ]; |
|
32 | ]; | |
33 |
|
33 | |||
34 | var FULL_IMG_CLASS = 'post-image-full'; |
|
34 | var FULL_IMG_CLASS = 'post-image-full'; | |
35 |
|
35 | |||
36 | var ATTR_SCALE = 'scale'; |
|
36 | var ATTR_SCALE = 'scale'; | |
37 |
|
37 | |||
38 |
|
38 | |||
|
39 | // Init image viewer | |||
|
40 | var viewerName = $('body').attr('data-image-viewer'); | |||
|
41 | var viewer = ImageViewer(); | |||
|
42 | for (var i = 0; i < IMAGE_VIEWERS.length; i++) { | |||
|
43 | var item = IMAGE_VIEWERS[i]; | |||
|
44 | if (item[0] === viewerName) { | |||
|
45 | viewer = item[1]; | |||
|
46 | break; | |||
|
47 | } | |||
|
48 | } | |||
|
49 | ||||
|
50 | ||||
|
51 | function getFullImageWidth(previewImage) { | |||
|
52 | var full_img_w = previewImage.attr('data-width'); | |||
|
53 | if (full_img_w == null) { | |||
|
54 | full_img_w = previewImage[0].naturalWidth; | |||
|
55 | } | |||
|
56 | ||||
|
57 | return full_img_w; | |||
|
58 | } | |||
|
59 | ||||
|
60 | function getFullImageHeight(previewImage) { | |||
|
61 | var full_img_h = previewImage.attr('data-height'); | |||
|
62 | if (full_img_h == null) { | |||
|
63 | full_img_h = previewImage[0].naturalHeight; | |||
|
64 | } | |||
|
65 | ||||
|
66 | return full_img_h; | |||
|
67 | } | |||
|
68 | ||||
|
69 | ||||
39 | function ImageViewer() {} |
|
70 | function ImageViewer() {} | |
40 | ImageViewer.prototype.view = function (post) {}; |
|
71 | ImageViewer.prototype.view = function (post) {}; | |
41 |
|
72 | |||
42 | function SimpleImageViewer() {} |
|
73 | function SimpleImageViewer() {} | |
43 | SimpleImageViewer.prototype.view = function (post) { |
|
74 | SimpleImageViewer.prototype.view = function (post) { | |
44 | var images = post.find('img'); |
|
75 | var images = post.find('img'); | |
45 | images.toggle(); |
|
76 | images.toggle(); | |
46 |
|
77 | |||
47 | // When we first enlarge an image, a full image needs to be created |
|
78 | // When we first enlarge an image, a full image needs to be created | |
48 | if (images.length == 1) { |
|
79 | if (images.length == 1) { | |
49 | var thumb = images.first(); |
|
80 | var thumb = images.first(); | |
50 |
|
81 | |||
51 |
var width = thumb |
|
82 | var width = getFullImageWidth(thumb); | |
52 | var height = thumb.attr('data-height'); |
|
83 | var height = getFullImageHeight(thumb); | |
53 |
|
84 | |||
54 | if (width == null || height == null) { |
|
85 | if (width == null || height == null) { | |
55 | width = '100%'; |
|
86 | width = '100%'; | |
56 | height = '100%'; |
|
87 | height = '100%'; | |
57 | } |
|
88 | } | |
58 |
|
89 | |||
59 | var parent = images.first().parent(); |
|
90 | var parent = images.first().parent(); | |
60 | var link = parent.attr('href'); |
|
91 | var link = parent.attr('href'); | |
61 |
|
92 | |||
62 | var fullImg = $('<img />') |
|
93 | var fullImg = $('<img />') | |
63 | .addClass(FULL_IMG_CLASS) |
|
94 | .addClass(FULL_IMG_CLASS) | |
64 | .attr('src', link) |
|
95 | .attr('src', link) | |
65 | .attr('width', width) |
|
96 | .attr('width', width) | |
66 | .attr('height', height); |
|
97 | .attr('height', height); | |
67 |
|
98 | |||
68 | parent.append(fullImg); |
|
99 | parent.append(fullImg); | |
69 | } |
|
100 | } | |
70 | }; |
|
101 | }; | |
71 |
|
102 | |||
72 | function PopupImageViewer() {} |
|
103 | function PopupImageViewer() {} | |
73 | PopupImageViewer.prototype.view = function (post) { |
|
104 | PopupImageViewer.prototype.view = function (post) { | |
74 | var el = post; |
|
105 | var el = post; | |
75 | var thumb_id = 'full' + el.find('img').attr('alt'); |
|
106 | var thumb_id = 'full' + el.find('img').attr('alt'); | |
76 |
|
107 | |||
77 | var existingPopups = $('#' + thumb_id); |
|
108 | var existingPopups = $('#' + thumb_id); | |
78 | if (!existingPopups.length) { |
|
109 | if (!existingPopups.length) { | |
79 | var imgElement= el.find('img'); |
|
110 | var imgElement = el.find('img'); | |
80 |
|
111 | |||
81 |
var full_img_w = imgElement |
|
112 | var full_img_w = getFullImageWidth(imgElement); | |
82 |
var full_img_h = imgElement |
|
113 | var full_img_h = getFullImageHeight(imgElement); | |
83 |
|
114 | |||
84 | var win = $(window); |
|
115 | var win = $(window); | |
85 |
|
116 | |||
86 | var win_w = win.width(); |
|
117 | var win_w = win.width(); | |
87 | var win_h = win.height(); |
|
118 | var win_h = win.height(); | |
88 |
|
119 | |||
89 | // New image size |
|
120 | // New image size | |
90 | var w_scale = 1; |
|
121 | var w_scale = 1; | |
91 | var h_scale = 1; |
|
122 | var h_scale = 1; | |
92 |
|
123 | |||
93 | var freeWidth = win_w - 2 * IMAGE_POPUP_MARGIN; |
|
124 | var freeWidth = win_w - 2 * IMAGE_POPUP_MARGIN; | |
94 | var freeHeight = win_h - 2 * IMAGE_POPUP_MARGIN; |
|
125 | var freeHeight = win_h - 2 * IMAGE_POPUP_MARGIN; | |
95 |
|
126 | |||
96 | if (full_img_w > freeWidth) { |
|
127 | if (full_img_w > freeWidth) { | |
97 | w_scale = full_img_w / freeWidth; |
|
128 | w_scale = full_img_w / freeWidth; | |
98 | } |
|
129 | } | |
99 | if (full_img_h > freeHeight) { |
|
130 | if (full_img_h > freeHeight) { | |
100 | h_scale = full_img_h / freeHeight; |
|
131 | h_scale = full_img_h / freeHeight; | |
101 | } |
|
132 | } | |
102 |
|
133 | |||
103 | var scale = Math.max(w_scale, h_scale) |
|
134 | var scale = Math.max(w_scale, h_scale) | |
104 | var img_w = full_img_w / scale; |
|
135 | var img_w = full_img_w / scale; | |
105 | var img_h = full_img_h / scale; |
|
136 | var img_h = full_img_h / scale; | |
106 |
|
137 | |||
107 | var postNode = $(el); |
|
138 | var postNode = $(el); | |
108 |
|
139 | |||
109 | var img_pv = new Image(); |
|
140 | var img_pv = new Image(); | |
110 | var newImage = $(img_pv); |
|
141 | var newImage = $(img_pv); | |
111 | newImage.addClass('img-full') |
|
142 | newImage.addClass('img-full') | |
112 | .attr('id', thumb_id) |
|
143 | .attr('id', thumb_id) | |
113 | .attr('src', postNode.attr('href')) |
|
144 | .attr('src', postNode.attr('href')) | |
114 | .attr(ATTR_SCALE, scale) |
|
145 | .attr(ATTR_SCALE, scale) | |
115 | .css({ |
|
146 | .css({ | |
116 | 'width': img_w, |
|
147 | 'width': img_w, | |
117 | 'height': img_h, |
|
148 | 'height': img_h, | |
118 | 'left': (win_w - img_w) / 2, |
|
149 | 'left': (win_w - img_w) / 2, | |
119 | 'top': ((win_h - img_h) / 2) |
|
150 | 'top': ((win_h - img_h) / 2) | |
120 | }) |
|
151 | }) | |
121 | .appendTo(postNode) |
|
152 | .appendTo(postNode) | |
122 | //scaling preview |
|
153 | //scaling preview | |
123 | .mousewheel(function(event, delta) { |
|
154 | .mousewheel(function(event, delta) { | |
124 | var cx = event.originalEvent.clientX; |
|
155 | var cx = event.originalEvent.clientX; | |
125 | var cy = event.originalEvent.clientY; |
|
156 | var cy = event.originalEvent.clientY; | |
126 |
|
157 | |||
127 | var scale = newImage.attr(ATTR_SCALE) / (delta > 0 ? 1.25 : 0.8); |
|
158 | var scale = newImage.attr(ATTR_SCALE) / (delta > 0 ? 1.25 : 0.8); | |
128 |
|
159 | |||
129 | var oldWidth = newImage.width(); |
|
160 | var oldWidth = newImage.width(); | |
130 | var oldHeight = newImage.height(); |
|
161 | var oldHeight = newImage.height(); | |
131 |
|
162 | |||
132 | var newIW = full_img_w / scale; |
|
163 | var newIW = full_img_w / scale; | |
133 | var newIH = full_img_h / scale; |
|
164 | var newIH = full_img_h / scale; | |
134 |
|
165 | |||
135 | newImage.width(newIW); |
|
166 | newImage.width(newIW); | |
136 | newImage.height(newIH); |
|
167 | newImage.height(newIH); | |
137 | newImage.attr(ATTR_SCALE, scale); |
|
168 | newImage.attr(ATTR_SCALE, scale); | |
138 |
|
169 | |||
139 | // Set position |
|
170 | // Set position | |
140 | var oldPosition = newImage.position(); |
|
171 | var oldPosition = newImage.position(); | |
141 | newImage.css({ |
|
172 | newImage.css({ | |
142 | left: parseInt(cx - (newIW/oldWidth) * (cx - parseInt(oldPosition.left, 10)), 10), |
|
173 | left: parseInt(cx - (newIW/oldWidth) * (cx - parseInt(oldPosition.left, 10)), 10), | |
143 | top: parseInt(cy - (newIH/oldHeight) * (cy - parseInt(oldPosition.top, 10)), 10) |
|
174 | top: parseInt(cy - (newIH/oldHeight) * (cy - parseInt(oldPosition.top, 10)), 10) | |
144 | }); |
|
175 | }); | |
145 |
|
176 | |||
146 | return false; |
|
177 | return false; | |
147 | } |
|
178 | } | |
148 | ) |
|
179 | ) | |
149 | .draggable({ |
|
180 | .draggable({ | |
150 | addClasses: false, |
|
181 | addClasses: false, | |
151 | stack: '.img-full' |
|
182 | stack: '.img-full' | |
152 | }); |
|
183 | }); | |
153 | } else { |
|
184 | } else { | |
154 | existingPopups.remove(); |
|
185 | existingPopups.remove(); | |
155 | } |
|
186 | } | |
156 | }; |
|
187 | }; | |
157 |
|
188 | |||
158 | function addImgPreview() { |
|
189 | function addImgPreview() { | |
159 | var viewerName = $('body').attr('data-image-viewer'); |
|
|||
160 | var viewer = ImageViewer(); |
|
|||
161 | for (var i = 0; i < IMAGE_VIEWERS.length; i++) { |
|
|||
162 | var item = IMAGE_VIEWERS[i]; |
|
|||
163 | if (item[0] === viewerName) { |
|
|||
164 | viewer = item[1]; |
|
|||
165 | break; |
|
|||
166 | } |
|
|||
167 | } |
|
|||
168 |
|
||||
169 | //keybind |
|
190 | //keybind | |
170 | $(document).on('keyup.removepic', function(e) { |
|
191 | $(document).on('keyup.removepic', function(e) { | |
171 | if(e.which === 27) { |
|
192 | if(e.which === 27) { | |
172 | $('.img-full').remove(); |
|
193 | $('.img-full').remove(); | |
173 | } |
|
194 | } | |
174 | }); |
|
195 | }); | |
175 |
|
196 | |||
176 | $('body').on('click', '.thumb', function() { |
|
197 | $('body').on('click', '.thumb', function() { | |
177 | viewer.view($(this)); |
|
198 | viewer.view($(this)); | |
178 |
|
199 | |||
179 | return false; |
|
200 | return false; | |
180 | }); |
|
201 | }); | |
181 | } |
|
202 | } |
@@ -1,126 +1,160 | |||||
1 | /* |
|
1 | /* | |
2 | @licstart The following is the entire license notice for the |
|
2 | @licstart The following is the entire license notice for the | |
3 | JavaScript code in this page. |
|
3 | JavaScript code in this page. | |
4 |
|
4 | |||
5 |
|
5 | |||
6 | Copyright (C) 2013 neko259 |
|
6 | Copyright (C) 2013 neko259 | |
7 |
|
7 | |||
8 | The JavaScript code in this page is free software: you can |
|
8 | The JavaScript code in this page is free software: you can | |
9 | redistribute it and/or modify it under the terms of the GNU |
|
9 | redistribute it and/or modify it under the terms of the GNU | |
10 | General Public License (GNU GPL) as published by the Free Software |
|
10 | General Public License (GNU GPL) as published by the Free Software | |
11 | Foundation, either version 3 of the License, or (at your option) |
|
11 | Foundation, either version 3 of the License, or (at your option) | |
12 | any later version. The code is distributed WITHOUT ANY WARRANTY; |
|
12 | any later version. The code is distributed WITHOUT ANY WARRANTY; | |
13 | without even the implied warranty of MERCHANTABILITY or FITNESS |
|
13 | without even the implied warranty of MERCHANTABILITY or FITNESS | |
14 | FOR A PARTICULAR PURPOSE. See the GNU GPL for more details. |
|
14 | FOR A PARTICULAR PURPOSE. See the GNU GPL for more details. | |
15 |
|
15 | |||
16 | As additional permission under GNU GPL version 3 section 7, you |
|
16 | As additional permission under GNU GPL version 3 section 7, you | |
17 | may distribute non-source (e.g., minimized or compacted) forms of |
|
17 | may distribute non-source (e.g., minimized or compacted) forms of | |
18 | that code without the copy of the GNU GPL normally required by |
|
18 | that code without the copy of the GNU GPL normally required by | |
19 | section 4, provided you include this license notice and a URL |
|
19 | section 4, provided you include this license notice and a URL | |
20 | through which recipients can access the Corresponding Source. |
|
20 | through which recipients can access the Corresponding Source. | |
21 |
|
21 | |||
22 | @licend The above is the entire license notice |
|
22 | @licend The above is the entire license notice | |
23 | for the JavaScript code in this page. |
|
23 | for the JavaScript code in this page. | |
24 | */ |
|
24 | */ | |
25 |
|
25 | |||
26 | var FAV_POST_UPDATE_PERIOD = 10000; |
|
26 | var FAV_POST_UPDATE_PERIOD = 10000; | |
|
27 | var ITEM_VOLUME_LEVEL = 'volumeLevel'; | |||
27 |
|
28 | |||
28 | /** |
|
29 | /** | |
29 | * An email is a hidden file to prevent spam bots from posting. It has to be |
|
30 | * An email is a hidden file to prevent spam bots from posting. It has to be | |
30 | * hidden. |
|
31 | * hidden. | |
31 | */ |
|
32 | */ | |
32 | function hideEmailFromForm() { |
|
33 | function hideEmailFromForm() { | |
33 | $('.form-email').parent().parent().hide(); |
|
34 | $('.form-email').parent().parent().hide(); | |
34 | } |
|
35 | } | |
35 |
|
36 | |||
36 | /** |
|
37 | /** | |
37 | * Highlight code blocks with code highlighter |
|
38 | * Highlight code blocks with code highlighter | |
38 | */ |
|
39 | */ | |
39 | function highlightCode(node) { |
|
40 | function highlightCode(node) { | |
40 | node.find('pre code').each(function(i, e) { |
|
41 | node.find('pre code').each(function(i, e) { | |
41 | hljs.highlightBlock(e); |
|
42 | hljs.highlightBlock(e); | |
42 | }); |
|
43 | }); | |
43 | } |
|
44 | } | |
44 |
|
45 | |||
45 | function updateFavPosts() { |
|
46 | function updateFavPosts() { | |
46 | var includePostBody = $('#fav-panel').is(":visible"); |
|
47 | var includePostBody = $('#fav-panel').is(":visible"); | |
47 | var url = '/api/new_posts/'; |
|
48 | var url = '/api/new_posts/'; | |
48 | if (includePostBody) { |
|
49 | if (includePostBody) { | |
49 | url += '?include_posts' |
|
50 | url += '?include_posts' | |
50 | } |
|
51 | } | |
51 | $.getJSON(url, |
|
52 | $.getJSON(url, | |
52 | function(data) { |
|
53 | function(data) { | |
53 | var allNewPostCount = 0; |
|
54 | var allNewPostCount = 0; | |
54 |
|
55 | |||
55 | if (includePostBody) { |
|
56 | if (includePostBody) { | |
56 | var favoriteThreadPanel = $('#fav-panel'); |
|
57 | var favoriteThreadPanel = $('#fav-panel'); | |
57 | favoriteThreadPanel.empty(); |
|
58 | favoriteThreadPanel.empty(); | |
58 | } |
|
59 | } | |
59 |
|
60 | |||
60 | $.each(data, function (_, dict) { |
|
61 | $.each(data, function (_, dict) { | |
61 | var newPostCount = dict.new_post_count; |
|
62 | var newPostCount = dict.new_post_count; | |
62 | allNewPostCount += newPostCount; |
|
63 | allNewPostCount += newPostCount; | |
63 |
|
64 | |||
64 | if (includePostBody) { |
|
65 | if (includePostBody) { | |
65 | var favThreadNode = $('<div class="post"></div>'); |
|
66 | var favThreadNode = $('<div class="post"></div>'); | |
66 | favThreadNode.append($(dict.post_url)); |
|
67 | favThreadNode.append($(dict.post_url)); | |
67 | favThreadNode.append(' '); |
|
68 | favThreadNode.append(' '); | |
68 | favThreadNode.append($('<span class="title">' + dict.title + '</span>')); |
|
69 | favThreadNode.append($('<span class="title">' + dict.title + '</span>')); | |
69 |
|
70 | |||
70 | if (newPostCount > 0) { |
|
71 | if (newPostCount > 0) { | |
71 | favThreadNode.append(' (<a href="' + dict.newest_post_link + '">+' + newPostCount + "</a>)"); |
|
72 | favThreadNode.append(' (<a href="' + dict.newest_post_link + '">+' + newPostCount + "</a>)"); | |
72 | } |
|
73 | } | |
73 |
|
74 | |||
74 | favoriteThreadPanel.append(favThreadNode); |
|
75 | favoriteThreadPanel.append(favThreadNode); | |
75 |
|
76 | |||
76 | addRefLinkPreview(favThreadNode[0]); |
|
77 | addRefLinkPreview(favThreadNode[0]); | |
77 | } |
|
78 | } | |
78 | }); |
|
79 | }); | |
79 |
|
80 | |||
80 | var newPostCountNode = $('#new-fav-post-count'); |
|
81 | var newPostCountNode = $('#new-fav-post-count'); | |
81 | if (allNewPostCount > 0) { |
|
82 | if (allNewPostCount > 0) { | |
82 | newPostCountNode.text('(+' + allNewPostCount + ')'); |
|
83 | newPostCountNode.text('(+' + allNewPostCount + ')'); | |
83 | newPostCountNode.show(); |
|
84 | newPostCountNode.show(); | |
84 | } else { |
|
85 | } else { | |
85 | newPostCountNode.hide(); |
|
86 | newPostCountNode.hide(); | |
86 | } |
|
87 | } | |
87 | } |
|
88 | } | |
88 | ); |
|
89 | ); | |
89 | } |
|
90 | } | |
90 |
|
91 | |||
91 | function initFavPanel() { |
|
92 | function initFavPanel() { | |
92 | updateFavPosts(); |
|
93 | updateFavPosts(); | |
93 |
|
94 | |||
94 | if ($('#fav-panel-btn').length > 0) { |
|
95 | if ($('#fav-panel-btn').length > 0) { | |
95 | setInterval(updateFavPosts, FAV_POST_UPDATE_PERIOD); |
|
96 | setInterval(updateFavPosts, FAV_POST_UPDATE_PERIOD); | |
96 | $('#fav-panel-btn').click(function() { |
|
97 | $('#fav-panel-btn').click(function() { | |
97 | $('#fav-panel').toggle(); |
|
98 | $('#fav-panel').toggle(); | |
98 | updateFavPosts(); |
|
99 | updateFavPosts(); | |
99 |
|
100 | |||
100 | return false; |
|
101 | return false; | |
101 | }); |
|
102 | }); | |
102 |
|
103 | |||
103 | $(document).on('keyup.removepic', function(e) { |
|
104 | $(document).on('keyup.removepic', function(e) { | |
104 | if(e.which === 27) { |
|
105 | if(e.which === 27) { | |
105 | $('#fav-panel').hide(); |
|
106 | $('#fav-panel').hide(); | |
106 | } |
|
107 | } | |
107 | }); |
|
108 | }); | |
108 | } |
|
109 | } | |
109 | } |
|
110 | } | |
110 |
|
111 | |||
|
112 | function setVolumeLevel(level) { | |||
|
113 | localStorage.setItem(ITEM_VOLUME_LEVEL, level); | |||
|
114 | } | |||
|
115 | ||||
|
116 | function getVolumeLevel() { | |||
|
117 | var level = localStorage.getItem(ITEM_VOLUME_LEVEL); | |||
|
118 | if (level == null) { | |||
|
119 | level = 1.0; | |||
|
120 | } | |||
|
121 | return level | |||
|
122 | } | |||
|
123 | ||||
|
124 | function processVolumeUser(node) { | |||
|
125 | node.prop("volume", getVolumeLevel()); | |||
|
126 | node.on('volumechange', function(event) { | |||
|
127 | setVolumeLevel(event.target.volume); | |||
|
128 | $("video,audio").prop("volume", getVolumeLevel()); | |||
|
129 | }); | |||
|
130 | } | |||
|
131 | ||||
|
132 | /** | |||
|
133 | * Add all scripts than need to work on post, when the post is added to the | |||
|
134 | * document. | |||
|
135 | */ | |||
|
136 | function addScriptsToPost(post) { | |||
|
137 | addRefLinkPreview(post[0]); | |||
|
138 | highlightCode(post); | |||
|
139 | processVolumeUser(post.find("video,audio")); | |||
|
140 | } | |||
|
141 | ||||
111 | $( document ).ready(function() { |
|
142 | $( document ).ready(function() { | |
112 | hideEmailFromForm(); |
|
143 | hideEmailFromForm(); | |
113 |
|
144 | |||
114 | $("a[href='#top']").click(function() { |
|
145 | $("a[href='#top']").click(function() { | |
115 | $("html, body").animate({ scrollTop: 0 }, "slow"); |
|
146 | $("html, body").animate({ scrollTop: 0 }, "slow"); | |
116 | return false; |
|
147 | return false; | |
117 | }); |
|
148 | }); | |
118 |
|
149 | |||
119 | addImgPreview(); |
|
150 | addImgPreview(); | |
120 |
|
151 | |||
121 | addRefLinkPreview(); |
|
152 | addRefLinkPreview(); | |
122 |
|
153 | |||
123 | highlightCode($(document)); |
|
154 | highlightCode($(document)); | |
124 |
|
155 | |||
125 | initFavPanel(); |
|
156 | initFavPanel(); | |
|
157 | ||||
|
158 | var volumeUsers = $("video,audio"); | |||
|
159 | processVolumeUser(volumeUsers); | |||
126 | }); |
|
160 | }); |
@@ -1,120 +1,119 | |||||
1 | function $X(path, root) { |
|
1 | function $X(path, root) { | |
2 | return document.evaluate(path, root || document, null, 6, null); |
|
2 | return document.evaluate(path, root || document, null, 6, null); | |
3 | } |
|
3 | } | |
4 | function $x(path, root) { |
|
4 | function $x(path, root) { | |
5 | return document.evaluate(path, root || document, null, 8, null).singleNodeValue; |
|
5 | return document.evaluate(path, root || document, null, 8, null).singleNodeValue; | |
6 | } |
|
6 | } | |
7 |
|
7 | |||
8 | function $del(el) { |
|
8 | function $del(el) { | |
9 | if(el) el.parentNode.removeChild(el); |
|
9 | if(el) el.parentNode.removeChild(el); | |
10 | } |
|
10 | } | |
11 |
|
11 | |||
12 | function $each(list, fn) { |
|
12 | function $each(list, fn) { | |
13 | if(!list) return; |
|
13 | if(!list) return; | |
14 | var i = list.snapshotLength; |
|
14 | var i = list.snapshotLength; | |
15 | if(i > 0) while(i--) fn(list.snapshotItem(i), i); |
|
15 | if(i > 0) while(i--) fn(list.snapshotItem(i), i); | |
16 | } |
|
16 | } | |
17 |
|
17 | |||
18 | function mkPreview(cln, html) { |
|
18 | function mkPreview(cln, html) { | |
19 | cln.innerHTML = html; |
|
19 | cln.innerHTML = html; | |
20 |
|
20 | |||
21 | highlightCode($(cln)); |
|
21 | addScriptsToPost($(cln)); | |
22 | addRefLinkPreview(cln); |
|
22 | } | |
23 | }; |
|
|||
24 |
|
23 | |||
25 | function isElementInViewport (el) { |
|
24 | function isElementInViewport (el) { | |
26 | //special bonus for those using jQuery |
|
25 | //special bonus for those using jQuery | |
27 | if (typeof jQuery === "function" && el instanceof jQuery) { |
|
26 | if (typeof jQuery === "function" && el instanceof jQuery) { | |
28 | el = el[0]; |
|
27 | el = el[0]; | |
29 | } |
|
28 | } | |
30 |
|
29 | |||
31 | var rect = el.getBoundingClientRect(); |
|
30 | var rect = el.getBoundingClientRect(); | |
32 |
|
31 | |||
33 | return ( |
|
32 | return ( | |
34 | rect.top >= 0 && |
|
33 | rect.top >= 0 && | |
35 | rect.left >= 0 && |
|
34 | rect.left >= 0 && | |
36 | rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /*or $(window).height() */ |
|
35 | rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /*or $(window).height() */ | |
37 | rect.right <= (window.innerWidth || document.documentElement.clientWidth) /*or $(window).width() */ |
|
36 | rect.right <= (window.innerWidth || document.documentElement.clientWidth) /*or $(window).width() */ | |
38 | ); |
|
37 | ); | |
39 | } |
|
38 | } | |
40 |
|
39 | |||
41 | function addRefLinkPreview(node) { |
|
40 | function addRefLinkPreview(node) { | |
42 | $each($X('.//a[starts-with(text(),">>")]', node || document), function(link) { |
|
41 | $each($X('.//a[starts-with(text(),">>")]', node || document), function(link) { | |
43 | link.addEventListener('mouseover', showPostPreview, false); |
|
42 | link.addEventListener('mouseover', showPostPreview, false); | |
44 | link.addEventListener('mouseout', delPostPreview, false); |
|
43 | link.addEventListener('mouseout', delPostPreview, false); | |
45 | }); |
|
44 | }); | |
46 | } |
|
45 | } | |
47 |
|
46 | |||
48 | function showPostPreview(e) { |
|
47 | function showPostPreview(e) { | |
49 | var doc = document; |
|
48 | var doc = document; | |
50 | //ref id |
|
49 | //ref id | |
51 | var pNum = $(this).text().match(/\d+/); |
|
50 | var pNum = $(this).text().match(/\d+/); | |
52 |
|
51 | |||
53 | if (pNum == null || pNum.length == 0) { |
|
52 | if (pNum == null || pNum.length == 0) { | |
54 | return; |
|
53 | return; | |
55 | } |
|
54 | } | |
56 |
|
55 | |||
57 | var post = $('#' + pNum); |
|
56 | var post = $('#' + pNum); | |
58 | if (post.length > 0 && isElementInViewport(post)) { |
|
57 | if (post.length > 0 && isElementInViewport(post)) { | |
59 | post.addClass('highlight'); |
|
58 | post.addClass('highlight'); | |
60 | } else { |
|
59 | } else { | |
61 | var x = e.clientX + (doc.documentElement.scrollLeft || doc.body.scrollLeft) + 2; |
|
60 | var x = e.clientX + (doc.documentElement.scrollLeft || doc.body.scrollLeft) + 2; | |
62 | var y = e.clientY + (doc.documentElement.scrollTop || doc.body.scrollTop); |
|
61 | var y = e.clientY + (doc.documentElement.scrollTop || doc.body.scrollTop); | |
63 |
|
62 | |||
64 | var cln = doc.createElement('div'); |
|
63 | var cln = doc.createElement('div'); | |
65 | cln.id = 'pstprev_' + pNum; |
|
64 | cln.id = 'pstprev_' + pNum; | |
66 | cln.className = 'post_preview'; |
|
65 | cln.className = 'post_preview'; | |
67 |
|
66 | |||
68 | cln.style.cssText = 'top:' + y + 'px;' + (x < doc.body.clientWidth/2 ? 'left:' + x + 'px' : 'right:' + parseInt(doc.body.clientWidth - x + 1) + 'px'); |
|
67 | cln.style.cssText = 'top:' + y + 'px;' + (x < doc.body.clientWidth/2 ? 'left:' + x + 'px' : 'right:' + parseInt(doc.body.clientWidth - x + 1) + 'px'); | |
69 |
|
68 | |||
70 | cln.addEventListener('mouseout', delPostPreview, false); |
|
69 | cln.addEventListener('mouseout', delPostPreview, false); | |
71 |
|
70 | |||
72 | cln.innerHTML = "<div class=\"post\">" + gettext('Loading...') + "</div>"; |
|
71 | cln.innerHTML = "<div class=\"post\">" + gettext('Loading...') + "</div>"; | |
73 |
|
72 | |||
74 | if(post.length > 0) { |
|
73 | if(post.length > 0) { | |
75 | var postdata = post.clone().wrap("<div/>").parent().html(); |
|
74 | var postdata = post.clone().wrap("<div/>").parent().html(); | |
76 |
|
75 | |||
77 | mkPreview(cln, postdata); |
|
76 | mkPreview(cln, postdata); | |
78 | } else { |
|
77 | } else { | |
79 | $.ajax({ |
|
78 | $.ajax({ | |
80 | url: '/api/post/' + pNum + '/?truncated' |
|
79 | url: '/api/post/' + pNum + '/?truncated' | |
81 | }) |
|
80 | }) | |
82 | .success(function(data) { |
|
81 | .success(function(data) { | |
83 | var postdata = $(data).wrap("<div/>").parent().html(); |
|
82 | var postdata = $(data).wrap("<div/>").parent().html(); | |
84 |
|
83 | |||
85 | //make preview |
|
84 | //make preview | |
86 | mkPreview(cln, postdata); |
|
85 | mkPreview(cln, postdata); | |
87 |
|
86 | |||
88 | }) |
|
87 | }) | |
89 | .error(function() { |
|
88 | .error(function() { | |
90 | cln.innerHTML = "<div class=\"post\">" |
|
89 | cln.innerHTML = "<div class=\"post\">" | |
91 | + gettext('Post not found') + "</div>"; |
|
90 | + gettext('Post not found') + "</div>"; | |
92 | }); |
|
91 | }); | |
93 | } |
|
92 | } | |
94 |
|
93 | |||
95 | $del(doc.getElementById(cln.id)); |
|
94 | $del(doc.getElementById(cln.id)); | |
96 |
|
95 | |||
97 | //add preview |
|
96 | //add preview | |
98 | $(cln).fadeIn(200); |
|
97 | $(cln).fadeIn(200); | |
99 | $('body').append(cln); |
|
98 | $('body').append(cln); | |
100 | } |
|
99 | } | |
101 | } |
|
100 | } | |
102 |
|
101 | |||
103 | function delPostPreview(e) { |
|
102 | function delPostPreview(e) { | |
104 | var el = $x('ancestor-or-self::*[starts-with(@id,"pstprev")]', e.relatedTarget); |
|
103 | var el = $x('ancestor-or-self::*[starts-with(@id,"pstprev")]', e.relatedTarget); | |
105 | if(!el) { |
|
104 | if(!el) { | |
106 | $each($X('.//div[starts-with(@id,"pstprev")]'), function(clone) { |
|
105 | $each($X('.//div[starts-with(@id,"pstprev")]'), function(clone) { | |
107 | $del(clone) |
|
106 | $del(clone) | |
108 | }); |
|
107 | }); | |
109 | } else { |
|
108 | } else { | |
110 | while(el.nextSibling) $del(el.nextSibling); |
|
109 | while(el.nextSibling) $del(el.nextSibling); | |
111 | } |
|
110 | } | |
112 |
|
111 | |||
113 | $('.highlight').removeClass('highlight'); |
|
112 | $('.highlight').removeClass('highlight'); | |
114 | } |
|
113 | } | |
115 |
|
114 | |||
116 | function addPreview() { |
|
115 | function addPreview() { | |
117 | $('.post').find('a').each(function() { |
|
116 | $('.post').find('a').each(function() { | |
118 | showPostPreview($(this)); |
|
117 | showPostPreview($(this)); | |
119 | }); |
|
118 | }); | |
120 | } |
|
119 | } |
@@ -1,457 +1,444 | |||||
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-2014 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 CLASS_POST = '.post' |
|
26 | var CLASS_POST = '.post' | |
27 |
|
27 | |||
28 | var POST_ADDED = 0; |
|
28 | var POST_ADDED = 0; | |
29 | var POST_UPDATED = 1; |
|
29 | var POST_UPDATED = 1; | |
30 |
|
30 | |||
|
31 | // TODO These need to be syncronized with board settings. | |||
31 | var JS_AUTOUPDATE_PERIOD = 20000; |
|
32 | var JS_AUTOUPDATE_PERIOD = 20000; | |
|
33 | // TODO This needs to be the same for attachment download time limit. | |||
|
34 | var POST_AJAX_TIMEOUT = 30000; | |||
|
35 | var BLINK_SPEED = 500; | |||
32 |
|
36 | |||
33 | var ALLOWED_FOR_PARTIAL_UPDATE = [ |
|
37 | var ALLOWED_FOR_PARTIAL_UPDATE = [ | |
34 | 'refmap', |
|
38 | 'refmap', | |
35 | 'post-info' |
|
39 | 'post-info' | |
36 | ]; |
|
40 | ]; | |
37 |
|
41 | |||
38 | var ATTR_CLASS = 'class'; |
|
42 | var ATTR_CLASS = 'class'; | |
39 | var ATTR_UID = 'data-uid'; |
|
43 | var ATTR_UID = 'data-uid'; | |
40 |
|
44 | |||
41 | var wsUser = ''; |
|
45 | var wsUser = ''; | |
42 |
|
46 | |||
43 | var unreadPosts = 0; |
|
47 | var unreadPosts = 0; | |
44 | var documentOriginalTitle = ''; |
|
48 | var documentOriginalTitle = ''; | |
45 |
|
49 | |||
46 | // Thread ID does not change, can be stored one time |
|
50 | // Thread ID does not change, can be stored one time | |
47 | var threadId = $('div.thread').children(CLASS_POST).first().attr('id'); |
|
51 | var threadId = $('div.thread').children(CLASS_POST).first().attr('id'); | |
|
52 | var blinkColor = $('<div class="post-blink"></div>').css('background-color'); | |||
48 |
|
53 | |||
49 | /** |
|
54 | /** | |
50 | * Connect to websocket server and subscribe to thread updates. On any update we |
|
55 | * Connect to websocket server and subscribe to thread updates. On any update we | |
51 | * request a thread diff. |
|
56 | * request a thread diff. | |
52 | * |
|
57 | * | |
53 | * @returns {boolean} true if connected, false otherwise |
|
58 | * @returns {boolean} true if connected, false otherwise | |
54 | */ |
|
59 | */ | |
55 | function connectWebsocket() { |
|
60 | function connectWebsocket() { | |
56 | var metapanel = $('.metapanel')[0]; |
|
61 | var metapanel = $('.metapanel')[0]; | |
57 |
|
62 | |||
58 | var wsHost = metapanel.getAttribute('data-ws-host'); |
|
63 | var wsHost = metapanel.getAttribute('data-ws-host'); | |
59 | var wsPort = metapanel.getAttribute('data-ws-port'); |
|
64 | var wsPort = metapanel.getAttribute('data-ws-port'); | |
60 |
|
65 | |||
61 | if (wsHost.length > 0 && wsPort.length > 0) { |
|
66 | if (wsHost.length > 0 && wsPort.length > 0) { | |
62 | var centrifuge = new Centrifuge({ |
|
67 | var centrifuge = new Centrifuge({ | |
63 | "url": 'ws://' + wsHost + ':' + wsPort + "/connection/websocket", |
|
68 | "url": 'ws://' + wsHost + ':' + wsPort + "/connection/websocket", | |
64 | "project": metapanel.getAttribute('data-ws-project'), |
|
69 | "project": metapanel.getAttribute('data-ws-project'), | |
65 | "user": wsUser, |
|
70 | "user": wsUser, | |
66 | "timestamp": metapanel.getAttribute('data-ws-token-time'), |
|
71 | "timestamp": metapanel.getAttribute('data-ws-token-time'), | |
67 | "token": metapanel.getAttribute('data-ws-token'), |
|
72 | "token": metapanel.getAttribute('data-ws-token'), | |
68 | "debug": false |
|
73 | "debug": false | |
69 | }); |
|
74 | }); | |
70 |
|
75 | |||
71 | centrifuge.on('error', function(error_message) { |
|
76 | centrifuge.on('error', function(error_message) { | |
72 | console.log("Error connecting to websocket server."); |
|
77 | console.log("Error connecting to websocket server."); | |
73 | console.log(error_message); |
|
78 | console.log(error_message); | |
74 | console.log("Using javascript update instead."); |
|
79 | console.log("Using javascript update instead."); | |
75 |
|
80 | |||
76 | // If websockets don't work, enable JS update instead |
|
81 | // If websockets don't work, enable JS update instead | |
77 | enableJsUpdate() |
|
82 | enableJsUpdate() | |
78 | }); |
|
83 | }); | |
79 |
|
84 | |||
80 | centrifuge.on('connect', function() { |
|
85 | centrifuge.on('connect', function() { | |
81 | var channelName = 'thread:' + threadId; |
|
86 | var channelName = 'thread:' + threadId; | |
82 | centrifuge.subscribe(channelName, function(message) { |
|
87 | centrifuge.subscribe(channelName, function(message) { | |
83 | getThreadDiff(); |
|
88 | getThreadDiff(); | |
84 | }); |
|
89 | }); | |
85 |
|
90 | |||
86 | // For the case we closed the browser and missed some updates |
|
91 | // For the case we closed the browser and missed some updates | |
87 | getThreadDiff(); |
|
92 | getThreadDiff(); | |
88 | $('#autoupdate').hide(); |
|
93 | $('#autoupdate').hide(); | |
89 | }); |
|
94 | }); | |
90 |
|
95 | |||
91 | centrifuge.connect(); |
|
96 | centrifuge.connect(); | |
92 |
|
97 | |||
93 | return true; |
|
98 | return true; | |
94 | } else { |
|
99 | } else { | |
95 | return false; |
|
100 | return false; | |
96 | } |
|
101 | } | |
97 | } |
|
102 | } | |
98 |
|
103 | |||
99 | /** |
|
104 | /** | |
100 | * Get diff of the posts from the current thread timestamp. |
|
105 | * Get diff of the posts from the current thread timestamp. | |
101 | * This is required if the browser was closed and some post updates were |
|
106 | * This is required if the browser was closed and some post updates were | |
102 | * missed. |
|
107 | * missed. | |
103 | */ |
|
108 | */ | |
104 | function getThreadDiff() { |
|
109 | function getThreadDiff() { | |
105 | var all_posts = $('.post'); |
|
110 | var all_posts = $('.post'); | |
106 |
|
111 | |||
107 | var uids = ''; |
|
112 | var uids = ''; | |
108 | var posts = all_posts; |
|
113 | var posts = all_posts; | |
109 | for (var i = 0; i < posts.length; i++) { |
|
114 | for (var i = 0; i < posts.length; i++) { | |
110 | uids += posts[i].getAttribute('data-uid') + ' '; |
|
115 | uids += posts[i].getAttribute('data-uid') + ' '; | |
111 | } |
|
116 | } | |
112 |
|
117 | |||
113 | var data = { |
|
118 | var data = { | |
114 | uids: uids, |
|
119 | uids: uids, | |
115 | thread: threadId |
|
120 | thread: threadId | |
116 | }; |
|
121 | }; | |
117 |
|
122 | |||
118 | var diffUrl = '/api/diff_thread/'; |
|
123 | var diffUrl = '/api/diff_thread/'; | |
119 |
|
124 | |||
120 | $.post(diffUrl, |
|
125 | $.post(diffUrl, | |
121 | data, |
|
126 | data, | |
122 | function(data) { |
|
127 | function(data) { | |
123 | var updatedPosts = data.updated; |
|
128 | var updatedPosts = data.updated; | |
124 | var addedPostCount = 0; |
|
129 | var addedPostCount = 0; | |
125 |
|
130 | |||
126 | for (var i = 0; i < updatedPosts.length; i++) { |
|
131 | for (var i = 0; i < updatedPosts.length; i++) { | |
127 | var postText = updatedPosts[i]; |
|
132 | var postText = updatedPosts[i]; | |
128 | var post = $(postText); |
|
133 | var post = $(postText); | |
129 |
|
134 | |||
130 | if (updatePost(post) == POST_ADDED) { |
|
135 | if (updatePost(post) == POST_ADDED) { | |
131 | addedPostCount++; |
|
136 | addedPostCount++; | |
132 | } |
|
137 | } | |
133 | } |
|
138 | } | |
134 |
|
139 | |||
135 | var hasMetaUpdates = updatedPosts.length > 0; |
|
140 | var hasMetaUpdates = updatedPosts.length > 0; | |
136 | if (hasMetaUpdates) { |
|
141 | if (hasMetaUpdates) { | |
137 | updateMetadataPanel(); |
|
142 | updateMetadataPanel(); | |
138 | } |
|
143 | } | |
139 |
|
144 | |||
140 | if (addedPostCount > 0) { |
|
145 | if (addedPostCount > 0) { | |
141 | updateBumplimitProgress(addedPostCount); |
|
146 | updateBumplimitProgress(addedPostCount); | |
142 | } |
|
147 | } | |
143 |
|
148 | |||
144 | if (updatedPosts.length > 0) { |
|
149 | if (updatedPosts.length > 0) { | |
145 | showNewPostsTitle(addedPostCount); |
|
150 | showNewPostsTitle(addedPostCount); | |
146 | } |
|
151 | } | |
147 |
|
152 | |||
148 | // TODO Process removed posts if any |
|
153 | // TODO Process removed posts if any | |
149 | $('.metapanel').attr('data-last-update', data.last_update); |
|
154 | $('.metapanel').attr('data-last-update', data.last_update); | |
150 | }, |
|
155 | }, | |
151 | 'json' |
|
156 | 'json' | |
152 | ) |
|
157 | ) | |
153 | } |
|
158 | } | |
154 |
|
159 | |||
155 | /** |
|
160 | /** | |
156 | * Add or update the post on html page. |
|
161 | * Add or update the post on html page. | |
157 | */ |
|
162 | */ | |
158 | function updatePost(postHtml) { |
|
163 | function updatePost(postHtml) { | |
159 | // This needs to be set on start because the page is scrolled after posts |
|
164 | // This needs to be set on start because the page is scrolled after posts | |
160 | // are added or updated |
|
165 | // are added or updated | |
161 | var bottom = isPageBottom(); |
|
166 | var bottom = isPageBottom(); | |
162 |
|
167 | |||
163 | var post = $(postHtml); |
|
168 | var post = $(postHtml); | |
164 |
|
169 | |||
165 | var threadBlock = $('div.thread'); |
|
170 | var threadBlock = $('div.thread'); | |
166 |
|
171 | |||
167 | var postId = post.attr('id'); |
|
172 | var postId = post.attr('id'); | |
168 |
|
173 | |||
169 | // If the post already exists, replace it. Otherwise add as a new one. |
|
174 | // If the post already exists, replace it. Otherwise add as a new one. | |
170 | var existingPosts = threadBlock.children('.post[id=' + postId + ']'); |
|
175 | var existingPosts = threadBlock.children('.post[id=' + postId + ']'); | |
171 |
|
176 | |||
172 | var type; |
|
177 | var type; | |
173 |
|
178 | |||
174 | if (existingPosts.size() > 0) { |
|
179 | if (existingPosts.size() > 0) { | |
175 | replacePartial(existingPosts.first(), post, false); |
|
180 | replacePartial(existingPosts.first(), post, false); | |
176 | post = existingPosts.first(); |
|
181 | post = existingPosts.first(); | |
177 |
|
182 | |||
178 | type = POST_UPDATED; |
|
183 | type = POST_UPDATED; | |
179 | } else { |
|
184 | } else { | |
180 | post.appendTo(threadBlock); |
|
185 | post.appendTo(threadBlock); | |
181 |
|
186 | |||
182 | if (bottom) { |
|
187 | if (bottom) { | |
183 | scrollToBottom(); |
|
188 | scrollToBottom(); | |
184 | } |
|
189 | } | |
185 |
|
190 | |||
186 | type = POST_ADDED; |
|
191 | type = POST_ADDED; | |
187 | } |
|
192 | } | |
188 |
|
193 | |||
189 | processNewPost(post); |
|
194 | processNewPost(post); | |
190 |
|
195 | |||
191 | return type; |
|
196 | return type; | |
192 | } |
|
197 | } | |
193 |
|
198 | |||
194 | /** |
|
199 | /** | |
195 | * Initiate a blinking animation on a node to show it was updated. |
|
200 | * Initiate a blinking animation on a node to show it was updated. | |
196 | */ |
|
201 | */ | |
197 | function blink(node) { |
|
202 | function blink(node) { | |
198 | var blinkCount = 2; |
|
203 | node.effect('highlight', { color: blinkColor }, BLINK_SPEED); | |
199 |
|
||||
200 | var nodeToAnimate = node; |
|
|||
201 | for (var i = 0; i < blinkCount; i++) { |
|
|||
202 | nodeToAnimate = nodeToAnimate.fadeTo('fast', 0.5).fadeTo('fast', 1.0); |
|
|||
203 | } |
|
|||
204 | } |
|
204 | } | |
205 |
|
205 | |||
206 | function isPageBottom() { |
|
206 | function isPageBottom() { | |
207 | var scroll = $(window).scrollTop() / ($(document).height() |
|
207 | var scroll = $(window).scrollTop() / ($(document).height() | |
208 | - $(window).height()); |
|
208 | - $(window).height()); | |
209 |
|
209 | |||
210 | return scroll == 1 |
|
210 | return scroll == 1 | |
211 | } |
|
211 | } | |
212 |
|
212 | |||
213 | function enableJsUpdate() { |
|
213 | function enableJsUpdate() { | |
214 | setInterval(getThreadDiff, JS_AUTOUPDATE_PERIOD); |
|
214 | setInterval(getThreadDiff, JS_AUTOUPDATE_PERIOD); | |
215 | return true; |
|
215 | return true; | |
216 | } |
|
216 | } | |
217 |
|
217 | |||
218 | function initAutoupdate() { |
|
218 | function initAutoupdate() { | |
219 | if (location.protocol === 'https:') { |
|
219 | if (location.protocol === 'https:') { | |
220 | return enableJsUpdate(); |
|
220 | return enableJsUpdate(); | |
221 | } else { |
|
221 | } else { | |
222 | if (connectWebsocket()) { |
|
222 | if (connectWebsocket()) { | |
223 | return true; |
|
223 | return true; | |
224 | } else { |
|
224 | } else { | |
225 | return enableJsUpdate(); |
|
225 | return enableJsUpdate(); | |
226 | } |
|
226 | } | |
227 | } |
|
227 | } | |
228 | } |
|
228 | } | |
229 |
|
229 | |||
230 | function getReplyCount() { |
|
230 | function getReplyCount() { | |
231 | return $('.thread').children(CLASS_POST).length |
|
231 | return $('.thread').children(CLASS_POST).length | |
232 | } |
|
232 | } | |
233 |
|
233 | |||
234 | function getImageCount() { |
|
234 | function getImageCount() { | |
235 | return $('.thread').find('img').length |
|
235 | return $('.thread').find('img').length | |
236 | } |
|
236 | } | |
237 |
|
237 | |||
238 | /** |
|
238 | /** | |
239 | * Update post count, images count and last update time in the metadata |
|
239 | * Update post count, images count and last update time in the metadata | |
240 | * panel. |
|
240 | * panel. | |
241 | */ |
|
241 | */ | |
242 | function updateMetadataPanel() { |
|
242 | function updateMetadataPanel() { | |
243 | var replyCountField = $('#reply-count'); |
|
243 | var replyCountField = $('#reply-count'); | |
244 | var imageCountField = $('#image-count'); |
|
244 | var imageCountField = $('#image-count'); | |
245 |
|
245 | |||
246 | var replyCount = getReplyCount(); |
|
246 | var replyCount = getReplyCount(); | |
247 | replyCountField.text(replyCount); |
|
247 | replyCountField.text(replyCount); | |
248 | var imageCount = getImageCount(); |
|
248 | var imageCount = getImageCount(); | |
249 | imageCountField.text(imageCount); |
|
249 | imageCountField.text(imageCount); | |
250 |
|
250 | |||
251 | var lastUpdate = $('.post:last').children('.post-info').first() |
|
251 | var lastUpdate = $('.post:last').children('.post-info').first() | |
252 | .children('.pub_time').first().html(); |
|
252 | .children('.pub_time').first().html(); | |
253 | if (lastUpdate !== '') { |
|
253 | if (lastUpdate !== '') { | |
254 | var lastUpdateField = $('#last-update'); |
|
254 | var lastUpdateField = $('#last-update'); | |
255 | lastUpdateField.html(lastUpdate); |
|
255 | lastUpdateField.html(lastUpdate); | |
256 | blink(lastUpdateField); |
|
256 | blink(lastUpdateField); | |
257 | } |
|
257 | } | |
258 |
|
258 | |||
259 | blink(replyCountField); |
|
259 | blink(replyCountField); | |
260 | blink(imageCountField); |
|
260 | blink(imageCountField); | |
261 |
|
261 | |||
262 | $('#message-count-text').text(ngettext('message', 'messages', replyCount)); |
|
262 | $('#message-count-text').text(ngettext('message', 'messages', replyCount)); | |
263 | $('#image-count-text').text(ngettext('image', 'images', imageCount)); |
|
263 | $('#image-count-text').text(ngettext('image', 'images', imageCount)); | |
264 | } |
|
264 | } | |
265 |
|
265 | |||
266 | /** |
|
266 | /** | |
267 | * Update bumplimit progress bar |
|
267 | * Update bumplimit progress bar | |
268 | */ |
|
268 | */ | |
269 | function updateBumplimitProgress(postDelta) { |
|
269 | function updateBumplimitProgress(postDelta) { | |
270 | var progressBar = $('#bumplimit_progress'); |
|
270 | var progressBar = $('#bumplimit_progress'); | |
271 | if (progressBar) { |
|
271 | if (progressBar) { | |
272 | var postsToLimitElement = $('#left_to_limit'); |
|
272 | var postsToLimitElement = $('#left_to_limit'); | |
273 |
|
273 | |||
274 | var oldPostsToLimit = parseInt(postsToLimitElement.text()); |
|
274 | var oldPostsToLimit = parseInt(postsToLimitElement.text()); | |
275 | var postCount = getReplyCount(); |
|
275 | var postCount = getReplyCount(); | |
276 | var bumplimit = postCount - postDelta + oldPostsToLimit; |
|
276 | var bumplimit = postCount - postDelta + oldPostsToLimit; | |
277 |
|
277 | |||
278 | var newPostsToLimit = bumplimit - postCount; |
|
278 | var newPostsToLimit = bumplimit - postCount; | |
279 | if (newPostsToLimit <= 0) { |
|
279 | if (newPostsToLimit <= 0) { | |
280 | $('.bar-bg').remove(); |
|
280 | $('.bar-bg').remove(); | |
281 | } else { |
|
281 | } else { | |
282 | postsToLimitElement.text(newPostsToLimit); |
|
282 | postsToLimitElement.text(newPostsToLimit); | |
283 | progressBar.width((100 - postCount / bumplimit * 100.0) + '%'); |
|
283 | progressBar.width((100 - postCount / bumplimit * 100.0) + '%'); | |
284 | } |
|
284 | } | |
285 | } |
|
285 | } | |
286 | } |
|
286 | } | |
287 |
|
287 | |||
288 | /** |
|
288 | /** | |
289 | * Show 'new posts' text in the title if the document is not visible to a user |
|
289 | * Show 'new posts' text in the title if the document is not visible to a user | |
290 | */ |
|
290 | */ | |
291 | function showNewPostsTitle(newPostCount) { |
|
291 | function showNewPostsTitle(newPostCount) { | |
292 | if (document.hidden) { |
|
292 | if (document.hidden) { | |
293 | if (documentOriginalTitle === '') { |
|
293 | if (documentOriginalTitle === '') { | |
294 | documentOriginalTitle = document.title; |
|
294 | documentOriginalTitle = document.title; | |
295 | } |
|
295 | } | |
296 | unreadPosts = unreadPosts + newPostCount; |
|
296 | unreadPosts = unreadPosts + newPostCount; | |
297 |
|
297 | |||
298 | var newTitle = '* '; |
|
298 | var newTitle = '* '; | |
299 | if (unreadPosts > 0) { |
|
299 | if (unreadPosts > 0) { | |
300 | newTitle += '[' + unreadPosts + '] '; |
|
300 | newTitle += '[' + unreadPosts + '] '; | |
301 | } |
|
301 | } | |
302 | newTitle += documentOriginalTitle; |
|
302 | newTitle += documentOriginalTitle; | |
303 |
|
303 | |||
304 | document.title = newTitle; |
|
304 | document.title = newTitle; | |
305 |
|
305 | |||
306 | document.addEventListener('visibilitychange', function() { |
|
306 | document.addEventListener('visibilitychange', function() { | |
307 | if (documentOriginalTitle !== '') { |
|
307 | if (documentOriginalTitle !== '') { | |
308 | document.title = documentOriginalTitle; |
|
308 | document.title = documentOriginalTitle; | |
309 | documentOriginalTitle = ''; |
|
309 | documentOriginalTitle = ''; | |
310 | unreadPosts = 0; |
|
310 | unreadPosts = 0; | |
311 | } |
|
311 | } | |
312 |
|
312 | |||
313 | document.removeEventListener('visibilitychange', null); |
|
313 | document.removeEventListener('visibilitychange', null); | |
314 | }); |
|
314 | }); | |
315 | } |
|
315 | } | |
316 | } |
|
316 | } | |
317 |
|
317 | |||
318 | /** |
|
318 | /** | |
319 | * Clear all entered values in the form fields |
|
319 | * Clear all entered values in the form fields | |
320 | */ |
|
320 | */ | |
321 | function resetForm(form) { |
|
321 | function resetForm(form) { | |
322 | form.find('input:text, input:password, input:file, select, textarea').val(''); |
|
322 | form.find('input:text, input:password, input:file, select, textarea').val(''); | |
323 | form.find('input:radio, input:checkbox') |
|
323 | form.find('input:radio, input:checkbox') | |
324 | .removeAttr('checked').removeAttr('selected'); |
|
324 | .removeAttr('checked').removeAttr('selected'); | |
325 | $('.file_wrap').find('.file-thumb').remove(); |
|
325 | $('.file_wrap').find('.file-thumb').remove(); | |
326 | $('#preview-text').hide(); |
|
326 | $('#preview-text').hide(); | |
327 | } |
|
327 | } | |
328 |
|
328 | |||
329 | /** |
|
329 | /** | |
330 | * When the form is posted, this method will be run as a callback |
|
330 | * When the form is posted, this method will be run as a callback | |
331 | */ |
|
331 | */ | |
332 | function updateOnPost(response, statusText, xhr, form) { |
|
332 | function updateOnPost(response, statusText, xhr, form) { | |
333 | var json = $.parseJSON(response); |
|
333 | var json = $.parseJSON(response); | |
334 | var status = json.status; |
|
334 | var status = json.status; | |
335 |
|
335 | |||
336 | showAsErrors(form, ''); |
|
336 | showAsErrors(form, ''); | |
337 |
|
337 | |||
338 | if (status === 'ok') { |
|
338 | if (status === 'ok') { | |
339 | resetFormPosition(); |
|
339 | resetFormPosition(); | |
340 | resetForm(form); |
|
340 | resetForm(form); | |
341 | getThreadDiff(); |
|
341 | getThreadDiff(); | |
342 | scrollToBottom(); |
|
342 | scrollToBottom(); | |
343 | } else { |
|
343 | } else { | |
344 | var errors = json.errors; |
|
344 | var errors = json.errors; | |
345 | for (var i = 0; i < errors.length; i++) { |
|
345 | for (var i = 0; i < errors.length; i++) { | |
346 | var fieldErrors = errors[i]; |
|
346 | var fieldErrors = errors[i]; | |
347 |
|
347 | |||
348 | var error = fieldErrors.errors; |
|
348 | var error = fieldErrors.errors; | |
349 |
|
349 | |||
350 | showAsErrors(form, error); |
|
350 | showAsErrors(form, error); | |
351 | } |
|
351 | } | |
352 | } |
|
352 | } | |
353 | } |
|
353 | } | |
354 |
|
354 | |||
355 | /** |
|
|||
356 | * Show text in the errors row of the form. |
|
|||
357 | * @param form |
|
|||
358 | * @param text |
|
|||
359 | */ |
|
|||
360 | function showAsErrors(form, text) { |
|
|||
361 | form.children('.form-errors').remove(); |
|
|||
362 |
|
||||
363 | if (text.length > 0) { |
|
|||
364 | var errorList = $('<div class="form-errors">' + text + '<div>'); |
|
|||
365 | errorList.appendTo(form); |
|
|||
366 | } |
|
|||
367 | } |
|
|||
368 |
|
355 | |||
369 | /** |
|
356 | /** | |
370 | * Run js methods that are usually run on the document, on the new post |
|
357 | * Run js methods that are usually run on the document, on the new post | |
371 | */ |
|
358 | */ | |
372 | function processNewPost(post) { |
|
359 | function processNewPost(post) { | |
373 | addRefLinkPreview(post[0]); |
|
360 | addScriptsToPost(post); | |
374 | highlightCode(post); |
|
|||
375 | blink(post); |
|
361 | blink(post); | |
376 | } |
|
362 | } | |
377 |
|
363 | |||
378 | function replacePartial(oldNode, newNode, recursive) { |
|
364 | function replacePartial(oldNode, newNode, recursive) { | |
379 | if (!equalNodes(oldNode, newNode)) { |
|
365 | if (!equalNodes(oldNode, newNode)) { | |
380 | // Update parent node attributes |
|
366 | // Update parent node attributes | |
381 | updateNodeAttr(oldNode, newNode, ATTR_CLASS); |
|
367 | updateNodeAttr(oldNode, newNode, ATTR_CLASS); | |
382 | updateNodeAttr(oldNode, newNode, ATTR_UID); |
|
368 | updateNodeAttr(oldNode, newNode, ATTR_UID); | |
383 |
|
369 | |||
384 | // Replace children |
|
370 | // Replace children | |
385 | var children = oldNode.children(); |
|
371 | var children = oldNode.children(); | |
386 | if (children.length == 0) { |
|
372 | if (children.length == 0) { | |
387 | oldNode.replaceWith(newNode); |
|
373 | oldNode.replaceWith(newNode); | |
388 | } else { |
|
374 | } else { | |
389 | var newChildren = newNode.children(); |
|
375 | var newChildren = newNode.children(); | |
390 | newChildren.each(function(i) { |
|
376 | newChildren.each(function(i) { | |
391 | var newChild = newChildren.eq(i); |
|
377 | var newChild = newChildren.eq(i); | |
392 | var newChildClass = newChild.attr(ATTR_CLASS); |
|
378 | var newChildClass = newChild.attr(ATTR_CLASS); | |
393 |
|
379 | |||
394 | // Update only certain allowed blocks (e.g. not images) |
|
380 | // Update only certain allowed blocks (e.g. not images) | |
395 | if (ALLOWED_FOR_PARTIAL_UPDATE.indexOf(newChildClass) > -1) { |
|
381 | if (ALLOWED_FOR_PARTIAL_UPDATE.indexOf(newChildClass) > -1) { | |
396 | var oldChild = oldNode.children('.' + newChildClass); |
|
382 | var oldChild = oldNode.children('.' + newChildClass); | |
397 |
|
383 | |||
398 | if (oldChild.length == 0) { |
|
384 | if (oldChild.length == 0) { | |
399 | oldNode.append(newChild); |
|
385 | oldNode.append(newChild); | |
400 | } else { |
|
386 | } else { | |
401 | if (!equalNodes(oldChild, newChild)) { |
|
387 | if (!equalNodes(oldChild, newChild)) { | |
402 | if (recursive) { |
|
388 | if (recursive) { | |
403 | replacePartial(oldChild, newChild, false); |
|
389 | replacePartial(oldChild, newChild, false); | |
404 | } else { |
|
390 | } else { | |
405 | oldChild.replaceWith(newChild); |
|
391 | oldChild.replaceWith(newChild); | |
406 | } |
|
392 | } | |
407 | } |
|
393 | } | |
408 | } |
|
394 | } | |
409 | } |
|
395 | } | |
410 | }); |
|
396 | }); | |
411 | } |
|
397 | } | |
412 | } |
|
398 | } | |
413 | } |
|
399 | } | |
414 |
|
400 | |||
415 | /** |
|
401 | /** | |
416 | * Compare nodes by content |
|
402 | * Compare nodes by content | |
417 | */ |
|
403 | */ | |
418 | function equalNodes(node1, node2) { |
|
404 | function equalNodes(node1, node2) { | |
419 | return node1[0].outerHTML == node2[0].outerHTML; |
|
405 | return node1[0].outerHTML == node2[0].outerHTML; | |
420 | } |
|
406 | } | |
421 |
|
407 | |||
422 | /** |
|
408 | /** | |
423 | * Update attribute of a node if it has changed |
|
409 | * Update attribute of a node if it has changed | |
424 | */ |
|
410 | */ | |
425 | function updateNodeAttr(oldNode, newNode, attrName) { |
|
411 | function updateNodeAttr(oldNode, newNode, attrName) { | |
426 | var oldAttr = oldNode.attr(attrName); |
|
412 | var oldAttr = oldNode.attr(attrName); | |
427 | var newAttr = newNode.attr(attrName); |
|
413 | var newAttr = newNode.attr(attrName); | |
428 | if (oldAttr != newAttr) { |
|
414 | if (oldAttr != newAttr) { | |
429 | oldNode.attr(attrName, newAttr); |
|
415 | oldNode.attr(attrName, newAttr); | |
430 | } |
|
416 | } | |
431 | } |
|
417 | } | |
432 |
|
418 | |||
433 | $(document).ready(function(){ |
|
419 | $(document).ready(function() { | |
434 | if (initAutoupdate()) { |
|
420 | if (initAutoupdate()) { | |
435 | // Post form data over AJAX |
|
421 | // Post form data over AJAX | |
436 | var threadId = $('div.thread').children('.post').first().attr('id'); |
|
422 | var threadId = $('div.thread').children('.post').first().attr('id'); | |
437 |
|
423 | |||
438 | var form = $('#form'); |
|
424 | var form = $('#form'); | |
439 |
|
425 | |||
440 | if (form.length > 0) { |
|
426 | if (form.length > 0) { | |
441 | var options = { |
|
427 | var options = { | |
442 |
beforeSubmit: function(arr, |
|
428 | beforeSubmit: function(arr, form, options) { | |
443 |
showAsErrors( |
|
429 | showAsErrors(form, gettext('Sending message...')); | |
444 | }, |
|
430 | }, | |
445 | success: updateOnPost, |
|
431 | success: updateOnPost, | |
446 | error: function() { |
|
432 | error: function() { | |
447 |
showAsErrors( |
|
433 | showAsErrors(form, gettext('Server error!')); | |
448 | }, |
|
434 | }, | |
449 | url: '/api/add_post/' + threadId + '/' |
|
435 | url: '/api/add_post/' + threadId + '/', | |
|
436 | timeout: POST_AJAX_TIMEOUT | |||
450 | }; |
|
437 | }; | |
451 |
|
438 | |||
452 | form.ajaxForm(options); |
|
439 | form.ajaxForm(options); | |
453 |
|
440 | |||
454 | resetForm(form); |
|
441 | resetForm(form); | |
455 | } |
|
442 | } | |
456 | } |
|
443 | } | |
457 | }); |
|
444 | }); |
@@ -1,186 +1,202 | |||||
1 | {% extends "boards/base.html" %} |
|
1 | {% extends "boards/base.html" %} | |
2 |
|
2 | |||
3 | {% load i18n %} |
|
3 | {% load i18n %} | |
4 | {% load board %} |
|
4 | {% load board %} | |
5 | {% load static %} |
|
5 | {% load static %} | |
6 | {% load tz %} |
|
6 | {% load tz %} | |
7 |
|
7 | |||
8 | {% block head %} |
|
8 | {% block head %} | |
9 | <meta name="robots" content="noindex"> |
|
9 | <meta name="robots" content="noindex"> | |
10 |
|
10 | |||
11 | {% if tag %} |
|
11 | {% if tag %} | |
12 | <title>{{ tag.name }} - {{ site_name }}</title> |
|
12 | <title>{{ tag.name }} - {{ site_name }}</title> | |
13 | {% else %} |
|
13 | {% else %} | |
14 | <title>{{ site_name }}</title> |
|
14 | <title>{{ site_name }}</title> | |
15 | {% endif %} |
|
15 | {% endif %} | |
16 |
|
16 | |||
17 | {% if prev_page_link %} |
|
17 | {% if prev_page_link %} | |
18 | <link rel="prev" href="{{ prev_page_link }}" /> |
|
18 | <link rel="prev" href="{{ prev_page_link }}" /> | |
19 | {% endif %} |
|
19 | {% endif %} | |
20 | {% if next_page_link %} |
|
20 | {% if next_page_link %} | |
21 | <link rel="next" href="{{ next_page_link }}" /> |
|
21 | <link rel="next" href="{{ next_page_link }}" /> | |
22 | {% endif %} |
|
22 | {% endif %} | |
23 |
|
23 | |||
24 | {% endblock %} |
|
24 | {% endblock %} | |
25 |
|
25 | |||
26 | {% block content %} |
|
26 | {% block content %} | |
27 |
|
27 | |||
28 | {% get_current_language as LANGUAGE_CODE %} |
|
28 | {% get_current_language as LANGUAGE_CODE %} | |
29 | {% get_current_timezone as TIME_ZONE %} |
|
29 | {% get_current_timezone as TIME_ZONE %} | |
30 |
|
30 | |||
31 | {% for banner in banners %} |
|
31 | {% for banner in banners %} | |
32 | <div class="post"> |
|
32 | <div class="post"> | |
33 | <div class="title">{{ banner.title }}</div> |
|
33 | <div class="title">{{ banner.title }}</div> | |
34 | <div>{{ banner.text }}</div> |
|
34 | <div>{{ banner.get_text|safe }}</div> | |
35 |
<div>{% trans ' |
|
35 | <div>{% trans 'Details' %}: <a href="{{ banner.post.get_absolute_url }}">>>{{ banner.post.id }}</a></div> | |
36 | </div> |
|
36 | </div> | |
37 | {% endfor %} |
|
37 | {% endfor %} | |
38 |
|
38 | |||
39 | {% if tag %} |
|
39 | {% if tag %} | |
40 | <div class="tag_info" style="border-bottom: solid .5ex #{{ tag.get_color }}"> |
|
40 | <div class="tag_info" style="border-bottom: solid .5ex #{{ tag.get_color }}"> | |
41 | {% if random_image_post %} |
|
41 | {% if random_image_post %} | |
42 | <div class="tag-image"> |
|
42 | <div class="tag-image"> | |
43 | {% with image=random_image_post.images.first %} |
|
43 | {% with image=random_image_post.images.first %} | |
44 | <a href="{{ random_image_post.get_absolute_url }}"><img |
|
44 | <a href="{{ random_image_post.get_absolute_url }}"><img | |
45 | src="{{ image.image.url_200x150 }}" |
|
45 | src="{{ image.image.url_200x150 }}" | |
46 | width="{{ image.pre_width }}" |
|
46 | width="{{ image.pre_width }}" | |
47 |
height="{{ image.pre_height }}" |
|
47 | height="{{ image.pre_height }}" | |
|
48 | alt="{{ random_image_post.id }}"/></a> | |||
48 | {% endwith %} |
|
49 | {% endwith %} | |
49 | </div> |
|
50 | </div> | |
50 | {% endif %} |
|
51 | {% endif %} | |
51 | <div class="tag-text-data"> |
|
52 | <div class="tag-text-data"> | |
52 | <h2> |
|
53 | <h2> | |
|
54 | /{{ tag.get_view|safe }}/ | |||
|
55 | {% if perms.change_tag %} | |||
|
56 | <span class="moderator_info">| <a href="{% url 'admin:boards_tag_change' tag.id %}">{% trans 'Edit tag' %}</a></span> | |||
|
57 | {% endif %} | |||
|
58 | </h2> | |||
|
59 | <p> | |||
53 | <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form"> |
|
60 | <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form"> | |
54 | {% if is_favorite %} |
|
61 | {% if is_favorite %} | |
55 | <button name="method" value="unsubscribe" class="fav">★</button> |
|
62 | <button name="method" value="unsubscribe" class="fav">★ {% trans "Remove from favorites" %}</button> | |
56 | {% else %} |
|
63 | {% else %} | |
57 | <button name="method" value="subscribe" class="not_fav">★</button> |
|
64 | <button name="method" value="subscribe" class="not_fav">★ {% trans "Add to favorites" %}</button> | |
58 | {% endif %} |
|
65 | {% endif %} | |
59 | </form> |
|
66 | </form> | |
60 | <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form"> |
|
67 | <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form"> | |
61 | {% if is_hidden %} |
|
68 | {% if is_hidden %} | |
62 |
<button name="method" value="unhide" class="fav"> |
|
69 | <button name="method" value="unhide" class="fav">{% trans "Show" %}</button> | |
63 | {% else %} |
|
70 | {% else %} | |
64 | <button name="method" value="hide" class="not_fav">H</button> |
|
71 | <button name="method" value="hide" class="not_fav">{% trans "Hide" %}</button> | |
65 | {% endif %} |
|
72 | {% endif %} | |
66 | </form> |
|
73 | </form> | |
67 | {{ tag.get_view|safe }} |
|
74 | <a href="{% url 'tag_gallery' tag.name %}">{% trans 'Gallery' %}</a> | |
68 | {% if moderator %} |
|
75 | </p> | |
69 | <span class="moderator_info">| <a href="{% url 'admin:boards_tag_change' tag.id %}">{% trans 'Edit tag' %}</a></span> |
|
|||
70 | {% endif %} |
|
|||
71 | </h2> |
|
|||
72 | {% if tag.get_description %} |
|
76 | {% if tag.get_description %} | |
73 | <p>{{ tag.get_description|safe }}</p> |
|
77 | <p>{{ tag.get_description|safe }}</p> | |
74 | {% endif %} |
|
78 | {% endif %} | |
75 | <p> |
|
79 | <p> | |
76 | {% blocktrans count count=tag.get_active_thread_count %}{{ count }} active thread{% plural %}active threads{% endblocktrans %}, |
|
80 | {% with active_count=tag.get_active_thread_count bumplimit_count=tag.get_bumplimit_thread_count archived_count=tag.get_archived_thread_count %} | |
77 | {% blocktrans count count=tag.get_bumplimit_thread_count %}{{ count }} thread in bumplimit{% plural %} threads in bumplimit{% endblocktrans %}, |
|
81 | {% if active_count %} | |
78 |
{% blocktrans count count= |
|
82 | {% blocktrans count count=active_count %}{{ count }} active thread{% plural %}active threads{% endblocktrans %}, | |
|
83 | {% endif %} | |||
|
84 | {% if bumplimit_count %} | |||
|
85 | {% blocktrans count count=bumplimit_count %}{{ count }} thread in bumplimit{% plural %} threads in bumplimit{% endblocktrans %}, | |||
|
86 | {% endif %} | |||
|
87 | {% if archived_count %} | |||
|
88 | {% blocktrans count count=archived_count %}{{ count }} archived thread{% plural %}archived threads{% endblocktrans %}, | |||
|
89 | {% endif %} | |||
|
90 | {% endwith %} | |||
79 | {% blocktrans count count=tag.get_post_count %}{{ count }} message{% plural %}messages{% endblocktrans %}. |
|
91 | {% blocktrans count count=tag.get_post_count %}{{ count }} message{% plural %}messages{% endblocktrans %}. | |
80 | </p> |
|
92 | </p> | |
81 | {% if tag.get_all_parents %} |
|
93 | {% if tag.get_all_parents %} | |
82 | <p> |
|
94 | <p> | |
83 | {% for parent in tag.get_all_parents %} |
|
95 | {% for parent in tag.get_all_parents %} | |
84 | {{ parent.get_view|safe }} > |
|
96 | {{ parent.get_view|safe }} > | |
85 | {% endfor %} |
|
97 | {% endfor %} | |
86 | {{ tag.get_view|safe }} |
|
98 | {{ tag.get_view|safe }} | |
87 | </p> |
|
99 | </p> | |
88 | {% endif %} |
|
100 | {% endif %} | |
89 | </div> |
|
101 | </div> | |
90 | </div> |
|
102 | </div> | |
91 | {% endif %} |
|
103 | {% endif %} | |
92 |
|
104 | |||
93 | {% if threads %} |
|
105 | {% if threads %} | |
94 | {% if prev_page_link %} |
|
106 | {% if prev_page_link %} | |
95 | <div class="page_link"> |
|
107 | <div class="page_link"> | |
96 | <a href="{{ prev_page_link }}">{% trans "Previous page" %}</a> |
|
108 | <a href="{{ prev_page_link }}">{% trans "Previous page" %}</a> | |
97 | </div> |
|
109 | </div> | |
98 | {% endif %} |
|
110 | {% endif %} | |
99 |
|
111 | |||
100 | {% for thread in threads %} |
|
112 | {% for thread in threads %} | |
101 | <div class="thread"> |
|
113 | <div class="thread"> | |
102 |
{% post_view thread.get_opening_post |
|
114 | {% post_view thread.get_opening_post thread=thread truncated=True need_open_link=True %} | |
103 | {% if not thread.archived %} |
|
115 | {% if not thread.archived %} | |
104 | {% with last_replies=thread.get_last_replies %} |
|
116 | {% with last_replies=thread.get_last_replies %} | |
105 | {% if last_replies %} |
|
117 | {% if last_replies %} | |
106 | {% with skipped_replies_count=thread.get_skipped_replies_count %} |
|
118 | {% with skipped_replies_count=thread.get_skipped_replies_count %} | |
107 | {% if skipped_replies_count %} |
|
119 | {% if skipped_replies_count %} | |
108 | <div class="skipped_replies"> |
|
120 | <div class="skipped_replies"> | |
109 | <a href="{% url 'thread' thread.get_opening_post_id %}"> |
|
121 | <a href="{% url 'thread' thread.get_opening_post_id %}"> | |
110 | {% blocktrans count count=skipped_replies_count %}Skipped {{ count }} reply. Open thread to see all replies.{% plural %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %} |
|
122 | {% blocktrans count count=skipped_replies_count %}Skipped {{ count }} reply. Open thread to see all replies.{% plural %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %} | |
111 | </a> |
|
123 | </a> | |
112 | </div> |
|
124 | </div> | |
113 | {% endif %} |
|
125 | {% endif %} | |
114 | {% endwith %} |
|
126 | {% endwith %} | |
115 | <div class="last-replies"> |
|
127 | <div class="last-replies"> | |
116 | {% for post in last_replies %} |
|
128 | {% for post in last_replies %} | |
117 |
{% post_view post |
|
129 | {% post_view post truncated=True %} | |
118 | {% endfor %} |
|
130 | {% endfor %} | |
119 | </div> |
|
131 | </div> | |
120 | {% endif %} |
|
132 | {% endif %} | |
121 | {% endwith %} |
|
133 | {% endwith %} | |
122 | {% endif %} |
|
134 | {% endif %} | |
123 | </div> |
|
135 | </div> | |
124 | {% endfor %} |
|
136 | {% endfor %} | |
125 |
|
137 | |||
126 | {% if next_page_link %} |
|
138 | {% if next_page_link %} | |
127 | <div class="page_link"> |
|
139 | <div class="page_link"> | |
128 | <a href="{{ next_page_link }}">{% trans "Next page" %}</a> |
|
140 | <a href="{{ next_page_link }}">{% trans "Next page" %}</a> | |
129 | </div> |
|
141 | </div> | |
130 | {% endif %} |
|
142 | {% endif %} | |
131 | {% else %} |
|
143 | {% else %} | |
132 | <div class="post"> |
|
144 | <div class="post"> | |
133 | {% trans 'No threads exist. Create the first one!' %}</div> |
|
145 | {% trans 'No threads exist. Create the first one!' %}</div> | |
134 | {% endif %} |
|
146 | {% endif %} | |
135 |
|
147 | |||
136 | <div class="post-form-w"> |
|
148 | <div class="post-form-w"> | |
137 | <script src="{% static 'js/panel.js' %}"></script> |
|
149 | <script src="{% static 'js/panel.js' %}"></script> | |
138 | <div class="post-form"> |
|
150 | <div class="post-form"> | |
139 | <div class="form-title">{% trans "Create new thread" %}</div> |
|
151 | <div class="form-title">{% trans "Create new thread" %}</div> | |
140 | <div class="swappable-form-full"> |
|
152 | <div class="swappable-form-full"> | |
141 | <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %} |
|
153 | <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %} | |
142 | {{ form.as_div }} |
|
154 | {{ form.as_div }} | |
143 | <div class="form-submit"> |
|
155 | <div class="form-submit"> | |
144 | <input type="submit" value="{% trans "Post" %}"/> |
|
156 | <input type="submit" value="{% trans "Post" %}"/> | |
145 | <button id="preview-button" onclick="return false;">{% trans 'Preview' %}</button> |
|
157 | <button id="preview-button" onclick="return false;">{% trans 'Preview' %}</button> | |
146 | </div> |
|
158 | </div> | |
147 | </form> |
|
159 | </form> | |
148 | </div> |
|
160 | </div> | |
149 | <div> |
|
161 | <div> | |
150 | {% trans 'Tags must be delimited by spaces. Text or image is required.' %} |
|
162 | {% trans 'Tags must be delimited by spaces. Text or image is required.' %} | |
|
163 | {% with size=max_file_size|filesizeformat %} | |||
|
164 | {% blocktrans %}Max file size is {{ size }}.{% endblocktrans %} | |||
|
165 | {% endwith %} | |||
151 | </div> |
|
166 | </div> | |
152 | <div id="preview-text"></div> |
|
167 | <div id="preview-text"></div> | |
153 | <div><a href="{% url "staticpage" name="help" %}">{% trans 'Text syntax' %}</a></div> |
|
168 | <div><a href="{% url "staticpage" name="help" %}">{% trans 'Text syntax' %}</a></div> | |
154 | <div><a href="{% url "tags" "required" %}">{% trans 'Tags' %}</a></div> |
|
169 | <div><a href="{% url "tags" "required" %}">{% trans 'Tags' %}</a></div> | |
155 | </div> |
|
170 | </div> | |
156 | </div> |
|
171 | </div> | |
157 |
|
172 | |||
158 | <script src="{% static 'js/form.js' %}"></script> |
|
173 | <script src="{% static 'js/form.js' %}"></script> | |
|
174 | <script id="sha256Script" src="{% static 'js/3party/sha256.js' %}"></script> | |||
|
175 | <script id="powScript" src="{% static 'js/proof_of_work.js' %}"></script> | |||
159 | <script src="{% static 'js/thread_create.js' %}"></script> |
|
176 | <script src="{% static 'js/thread_create.js' %}"></script> | |
160 |
|
177 | |||
161 | {% endblock %} |
|
178 | {% endblock %} | |
162 |
|
179 | |||
163 | {% block metapanel %} |
|
180 | {% block metapanel %} | |
164 |
|
181 | |||
165 | <span class="metapanel"> |
|
182 | <span class="metapanel"> | |
166 | <b><a href="{% url "authors" %}">{{ site_name }}</a> {{ version }}</b> |
|
183 | <b><a href="{% url "authors" %}">{{ site_name }}</a> {{ version }}</b> | |
167 | {% trans "Pages:" %} |
|
184 | {% trans "Pages:" %} | |
168 | [ |
|
185 | [ | |
169 | {% with dividers=paginator.get_dividers %} |
|
186 | {% with dividers=paginator.get_dividers %} | |
170 | {% for page in paginator.get_divided_range %} |
|
187 | {% for page in paginator.get_divided_range %} | |
171 | {% if page in dividers %} |
|
188 | {% if page in dividers %} | |
172 | …, |
|
189 | …, | |
173 | {% endif %} |
|
190 | {% endif %} | |
174 | <a |
|
191 | <a | |
175 | {% ifequal page current_page.number %} |
|
192 | {% ifequal page current_page.number %} | |
176 | class="current_page" |
|
193 | class="current_page" | |
177 | {% endifequal %} |
|
194 | {% endifequal %} | |
178 | href="{% page_url paginator page %}">{{ page }}</a> |
|
195 | href="{% page_url paginator page %}">{{ page }}</a> | |
179 | {% if not forloop.last %},{% endif %} |
|
196 | {% if not forloop.last %},{% endif %} | |
180 | {% endfor %} |
|
197 | {% endfor %} | |
181 | {% endwith %} |
|
198 | {% endwith %} | |
182 | ] |
|
199 | ] | |
183 | [<a href="rss/">RSS</a>] |
|
|||
184 | </span> |
|
200 | </span> | |
185 |
|
201 | |||
186 | {% endblock %} |
|
202 | {% endblock %} |
@@ -1,34 +1,34 | |||||
1 | {% extends "boards/base.html" %} |
|
1 | {% extends "boards/base.html" %} | |
2 |
|
2 | |||
3 | {% load i18n %} |
|
3 | {% load i18n %} | |
4 |
|
4 | |||
5 | {% block head %} |
|
5 | {% block head %} | |
6 | <title>{% trans "Authors" %}</title> |
|
6 | <title>{% trans "Authors" %}</title> | |
7 | {% endblock %} |
|
7 | {% endblock %} | |
8 |
|
8 | |||
9 | {% block content %} |
|
9 | {% block content %} | |
10 | <div class="post"> |
|
10 | <div class="post"> | |
11 | <p><img src="{{ STATIC_URL }}favicon.png" width="200" /></p> |
|
11 | <p><img src="{{ STATIC_URL }}favicon.png" width="200" /></p> | |
|
12 | <h2>{% trans 'Statistics' %}</h2> | |||
|
13 | <p>{% trans 'Size of media:' %} {{ media_size|filesizeformat }}. | |||
|
14 | <p>{% blocktrans count count=post_count %}{{ count }} message{% plural %}messages{% endblocktrans %}.</p> | |||
12 | <h2>{% trans 'Authors' %}</h2> |
|
15 | <h2>{% trans 'Authors' %}</h2> | |
13 | {% for nick, values in authors.items %} |
|
16 | {% for nick, values in authors.items %} | |
14 | <p> |
|
17 | <p> | |
15 | <b>{{ nick }}</b> ({{ values.name }}): |
|
18 | <b>{{ nick }}</b> ({{ values.name }}): | |
16 | {% for value in values.contacts %} |
|
19 | {% for value in values.contacts %} | |
17 | <a href="mailto:{{ value }}">{{ value }}</a> |
|
20 | <a href="mailto:{{ value }}">{{ value }}</a> | |
18 | {% endfor %} - |
|
21 | {% endfor %} - | |
19 |
{ |
|
22 | {{ values.roles|join:', ' }} | |
20 | <span class="role">{% trans role %}</span> |
|
|||
21 | {% if not forloop.last %}, {% endif %} |
|
|||
22 | {% endfor %} |
|
|||
23 | </p> |
|
23 | </p> | |
24 | {% endfor %} |
|
24 | {% endfor %} | |
25 | <br /> |
|
25 | <br /> | |
26 | <p>{% trans "Distributed under the" %} |
|
26 | <p>{% trans "Distributed under the" %} | |
27 | <a href="http://www.gnu.org/licenses/gpl.html" >GNU GPLv3</a> |
|
27 | <a href="http://www.gnu.org/licenses/gpl.html" >GNU GPLv3</a> | |
28 | {% trans "license" %}</p> |
|
28 | {% trans "license" %}</p> | |
29 | <p><a href="https://bitbucket.org/neko259/neboard"> |
|
29 | <p><a href="https://bitbucket.org/neko259/neboard"> | |
30 | {% trans "Repository" %}</a></p> |
|
30 | {% trans "Repository" %}</a></p> | |
31 |
|
31 | |||
32 | <p>Bitcoin: <b>1A4dePg6CGfYcJ7SH1tbaVdjjj1VLv8X1H</b></p> |
|
32 | <p>Bitcoin: <b>1A4dePg6CGfYcJ7SH1tbaVdjjj1VLv8X1H</b></p> | |
33 | </div> |
|
33 | </div> | |
34 | {% endblock %} |
|
34 | {% endblock %} |
@@ -1,80 +1,87 | |||||
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 'css/3party/jquery-ui.min.css' %}" media="all"/> |
|
11 | <link rel="stylesheet" type="text/css" href="{% static 'css/3party/jquery-ui.min.css' %}" media="all"/> | |
12 | <link rel="stylesheet" type="text/css" href="{% static theme_css %}" media="all"/> |
|
12 | <link rel="stylesheet" type="text/css" href="{% static theme_css %}" media="all"/> | |
13 |
|
13 | |||
14 | <link rel="alternate" type="application/rss+xml" href="rss/" title="{% trans 'Feed' %}"/> |
|
14 | {% if rss_url %} | |
|
15 | <link rel="alternate" type="application/rss+xml" href="{{ rss_url }}" title="{% trans 'Feed' %}"/> | |||
|
16 | {% endif %} | |||
15 |
|
17 | |||
16 | <link rel="icon" type="image/png" |
|
18 | <link rel="icon" type="image/png" | |
17 | href="{% static 'favicon.png' %}"> |
|
19 | href="{% static 'favicon.png' %}"> | |
18 |
|
20 | |||
19 | <meta name="viewport" content="width=device-width, initial-scale=1"/> |
|
21 | <meta name="viewport" content="width=device-width, initial-scale=1"/> | |
20 | <meta charset="utf-8"/> |
|
22 | <meta charset="utf-8"/> | |
21 |
|
23 | |||
22 | {% block head %}{% endblock %} |
|
24 | {% block head %}{% endblock %} | |
23 | </head> |
|
25 | </head> | |
24 | <body data-image-viewer="{{ image_viewer }}"> |
|
26 | <body data-image-viewer="{{ image_viewer }}" data-pow-difficulty="{{ pow_difficulty }}"> | |
25 | <script src="{% static 'js/jquery-2.0.1.min.js' %}"></script> |
|
27 | <script src="{% static 'js/jquery-2.0.1.min.js' %}"></script> | |
26 | <script src="{% static 'js/3party/jquery-ui.min.js' %}"></script> |
|
|||
27 | <script src="{% static 'js/jquery.mousewheel.js' %}"></script> |
|
|||
28 | <script src="{% url 'js_info_dict' %}"></script> |
|
|||
29 |
|
28 | |||
30 | <div class="navigation_panel header"> |
|
29 | <div class="navigation_panel header"> | |
31 | <a class="link" href="{% url 'index' %}">{% trans "All threads" %}</a> |
|
30 | <a class="link" href="{% url 'index' %}">{% trans "All threads" %}</a> | |
32 | {% if tags_str %} |
|
31 | {% if tags_str %} | |
33 | {% autoescape off %} |
|
32 | {% autoescape off %} | |
34 | {{ tags_str }}, |
|
33 | {{ tags_str }}, | |
35 | {% endautoescape %} |
|
34 | {% endautoescape %} | |
36 | {% else %} |
|
35 | {% else %} | |
37 | {% trans 'Add tags' %} → |
|
36 | {% trans 'Add tags' %} → | |
38 | {% endif %} |
|
37 | {% endif %} | |
39 | <a href="{% url 'tags' 'required'%}" title="{% trans 'Tag management' %}">{% trans "tags" %}</a>, |
|
38 | <a href="{% url 'tags' 'required'%}" title="{% trans 'Tag management' %}">{% trans "tags" %}</a>, | |
40 | <a href="{% url 'search' %}" title="{% trans 'Search' %}">{% trans 'search' %}</a>, |
|
39 | <a href="{% url 'search' %}" title="{% trans 'Search' %}">{% trans 'search' %}</a>, | |
41 | <a href="{% url 'feed' %}" title="{% trans 'Feed' %}">{% trans 'feed' %}</a>, |
|
40 | <a href="{% url 'feed' %}" title="{% trans 'Feed' %}">{% trans 'feed' %}</a>, | |
42 | <a href="{% url 'random' %}" title="{% trans 'Random images' %}">{% trans 'random' %}</a>{% if has_fav_threads %}, |
|
41 | <a href="{% url 'random' %}" title="{% trans 'Random images' %}">{% trans 'random' %}</a>{% if has_fav_threads %}, | |
43 |
|
42 | |||
44 | <a href="#" id="fav-panel-btn">{% trans 'favorites' %} <span id="new-fav-post-count"></span></a> |
|
43 | <a href="#" id="fav-panel-btn">{% trans 'favorites' %} <span id="new-fav-post-count"></span></a> | |
45 | {% endif %} |
|
44 | {% endif %} | |
46 |
|
45 | |||
47 | {% if username %} |
|
46 | {% if usernames %} | |
48 |
<a class="right-link link" href="{% url 'notifications' |
|
47 | <a class="right-link link" href="{% url 'notifications' %}" title="{% trans 'Notifications' %}"> | |
49 | {% trans 'Notifications' %} |
|
48 | {% trans 'Notifications' %} | |
50 | {% ifnotequal new_notifications_count 0 %} |
|
49 | {% ifnotequal new_notifications_count 0 %} | |
51 | (<b>{{ new_notifications_count }}</b>) |
|
50 | (<b>{{ new_notifications_count }}</b>) | |
52 | {% endifnotequal %} |
|
51 | {% endifnotequal %} | |
53 | </a> |
|
52 | </a> | |
54 | {% endif %} |
|
53 | {% endif %} | |
55 |
|
54 | |||
56 | <a class="right-link link" href="{% url 'settings' %}">{% trans 'Settings' %}</a> |
|
55 | <a class="right-link link" href="{% url 'settings' %}">{% trans 'Settings' %}</a> | |
57 | </div> |
|
56 | </div> | |
58 |
|
57 | |||
59 | <div id="fav-panel"><div class="post">{% trans "Loading..." %}</div></div> |
|
58 | <div id="fav-panel"><div class="post">{% trans "Loading..." %}</div></div> | |
60 |
|
59 | |||
61 | {% block content %}{% endblock %} |
|
60 | {% block content %}{% endblock %} | |
62 |
|
61 | |||
|
62 | <script src="{% static 'js/3party/jquery-ui.min.js' %}"></script> | |||
|
63 | <script src="{% static 'js/jquery.mousewheel.js' %}"></script> | |||
63 | <script src="{% static 'js/3party/highlight.min.js' %}"></script> |
|
64 | <script src="{% static 'js/3party/highlight.min.js' %}"></script> | |
|
65 | ||||
|
66 | <script src="{% url 'js_info_dict' %}"></script> | |||
|
67 | ||||
64 | <script src="{% static 'js/popup.js' %}"></script> |
|
68 | <script src="{% static 'js/popup.js' %}"></script> | |
65 | <script src="{% static 'js/image.js' %}"></script> |
|
69 | <script src="{% static 'js/image.js' %}"></script> | |
66 | <script src="{% static 'js/refpopup.js' %}"></script> |
|
70 | <script src="{% static 'js/refpopup.js' %}"></script> | |
67 | <script src="{% static 'js/main.js' %}"></script> |
|
71 | <script src="{% static 'js/main.js' %}"></script> | |
68 |
|
72 | |||
69 | <div class="navigation_panel footer"> |
|
73 | <div class="navigation_panel footer"> | |
70 | {% block metapanel %}{% endblock %} |
|
74 | {% block metapanel %}{% endblock %} | |
|
75 | {% if rss_url %} | |||
|
76 | [<a href="{{ rss_url }}">RSS</a>] | |||
|
77 | {% endif %} | |||
71 | [<a href="{% url 'admin:index' %}">{% trans 'Admin' %}</a>] |
|
78 | [<a href="{% url 'admin:index' %}">{% trans 'Admin' %}</a>] | |
72 | [<a href="{% url 'index' %}?order=pub">{% trans 'New threads' %}</a>] |
|
79 | [<a href="{% url 'index' %}?order=pub">{% trans 'New threads' %}</a>] | |
73 | {% with ppd=posts_per_day|floatformat:2 %} |
|
80 | {% with ppd=posts_per_day|floatformat:2 %} | |
74 | {% blocktrans %}Speed: {{ ppd }} posts per day{% endblocktrans %} |
|
81 | {% blocktrans %}Speed: {{ ppd }} posts per day{% endblocktrans %} | |
75 | {% endwith %} |
|
82 | {% endwith %} | |
76 | <a class="link" href="#top" id="up">{% trans 'Up' %}</a> |
|
83 | <a class="link" href="#top" id="up">{% trans 'Up' %}</a> | |
77 | </div> |
|
84 | </div> | |
78 |
|
85 | |||
79 | </body> |
|
86 | </body> | |
80 | </html> |
|
87 | </html> |
@@ -1,31 +1,35 | |||||
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 head %} |
|
6 | {% block head %} | |
7 | <meta name="robots" content="noindex"> |
|
7 | <meta name="robots" content="noindex"> | |
8 | <title>{{ site_name }} - {% trans 'Notifications' %} - {{ notification_username }}</title> |
|
8 | <title>{{ site_name }} - {% trans 'Notifications' %} - {{ notification_usernames|join:', ' }}</title> | |
9 | {% endblock %} |
|
9 | {% endblock %} | |
10 |
|
10 | |||
11 | {% block content %} |
|
11 | {% block content %} | |
12 | <div class="tag_info"><a href="{% url 'notifications' notification_username %}" class="user-cast">@{{ notification_username }}</a></div> |
|
12 | <div class="tag_info"> | |
|
13 | {% for username in notification_usernames %} | |||
|
14 | <a href="{% url 'notifications' username %}" class="user-cast">@{{ username }}</a> | |||
|
15 | {% endfor %} | |||
|
16 | </div> | |||
13 |
|
17 | |||
14 | {% if page %} |
|
18 | {% if page %} | |
15 | {% if page.has_previous %} |
|
19 | {% if page.has_previous %} | |
16 | <div class="page_link"> |
|
20 | <div class="page_link"> | |
17 | <a href="?page={{ page.previous_page_number }}">{% trans "Previous page" %}</a> |
|
21 | <a href="?page={{ page.previous_page_number }}">{% trans "Previous page" %}</a> | |
18 | </div> |
|
22 | </div> | |
19 | {% endif %} |
|
23 | {% endif %} | |
20 |
|
24 | |||
21 | {% for post in page.object_list %} |
|
25 | {% for post in page.object_list %} | |
22 | {% post_view post need_op_data=True %} |
|
26 | {% post_view post need_op_data=True %} | |
23 | {% endfor %} |
|
27 | {% endfor %} | |
24 |
|
28 | |||
25 | {% if page.has_next %} |
|
29 | {% if page.has_next %} | |
26 | <div class="page_link"> |
|
30 | <div class="page_link"> | |
27 | <a href="?page={{ page.next_page_number }}">{% trans "Next page" %}</a> |
|
31 | <a href="?page={{ page.next_page_number }}">{% trans "Next page" %}</a> | |
28 | </div> |
|
32 | </div> | |
29 | {% endif %} |
|
33 | {% endif %} | |
30 | {% endif %} |
|
34 | {% endif %} | |
31 | {% endblock %} |
|
35 | {% endblock %} |
@@ -1,114 +1,115 | |||||
1 | {% load i18n %} |
|
1 | {% load i18n %} | |
2 | {% load board %} |
|
2 | {% load board %} | |
3 |
|
3 | |||
4 | {% get_current_language as LANGUAGE_CODE %} |
|
4 | {% get_current_language as LANGUAGE_CODE %} | |
5 |
|
5 | |||
6 | <div class="{{ css_class }}" id="{{ post.id }}" data-uid="{{ post.uid }}"> |
|
6 | <div class="{{ css_class }}" id="{{ post.id }}" data-uid="{{ post.uid }}"> | |
7 | <div class="post-info"> |
|
7 | <div class="post-info"> | |
8 | <a class="post_id" href="{{ post.get_absolute_url }}">#{{ post.get_absolute_id }}</a> |
|
8 | <a class="post_id" href="{{ post.get_absolute_url }}">#{{ post.get_absolute_id }}</a> | |
9 | <span class="title">{{ post.title }}</span> |
|
9 | <span class="title">{{ post.title }}</span> | |
10 | <span class="pub_time"><time datetime="{{ post.pub_time|date:'c' }}">{{ post.pub_time }}</time></span> |
|
10 | <span class="pub_time"><time datetime="{{ post.pub_time|date:'c' }}">{{ post.pub_time }}</time></span> | |
11 | {% if post.tripcode %} |
|
11 | {% if post.tripcode %} | |
12 | / |
|
12 | / | |
13 | {% with tripcode=post.get_tripcode %} |
|
13 | {% with tripcode=post.get_tripcode %} | |
14 | <a href="{% url 'feed' %}?tripcode={{ tripcode.get_full_text }}" |
|
14 | <a href="{% url 'feed' %}?tripcode={{ tripcode.get_full_text }}" | |
15 | class="tripcode" title="{{ tripcode.get_full_text }}" |
|
15 | class="tripcode" title="{{ tripcode.get_full_text }}" | |
16 | style="border: solid 2px #{{ tripcode.get_color }}; border-left: solid 1ex #{{ tripcode.get_color }};">{{ tripcode.get_short_text }}</a> |
|
16 | style="border: solid 2px #{{ tripcode.get_color }}; border-left: solid 1ex #{{ tripcode.get_color }};">{{ tripcode.get_short_text }}</a> | |
17 | {% endwith %} |
|
17 | {% endwith %} | |
18 | {% endif %} |
|
18 | {% endif %} | |
19 | {% comment %} |
|
19 | {% comment %} | |
20 | Thread death time needs to be shown only if the thread is alredy archived |
|
20 | Thread death time needs to be shown only if the thread is alredy archived | |
21 | and this is an opening post (thread death time) or a post for popup |
|
21 | and this is an opening post (thread death time) or a post for popup | |
22 | (we don't see OP here so we show the death time in the post itself). |
|
22 | (we don't see OP here so we show the death time in the post itself). | |
23 | {% endcomment %} |
|
23 | {% endcomment %} | |
24 | {% if thread.archived %} |
|
24 | {% if thread.is_archived %} | |
25 | {% if is_opening %} |
|
25 | {% if is_opening %} | |
26 | — <time datetime="{{ thread.bump_time|date:'c' }}">{{ thread.bump_time }}</time> |
|
26 | — <time datetime="{{ thread.bump_time|date:'c' }}">{{ thread.bump_time }}</time> | |
27 | {% endif %} |
|
27 | {% endif %} | |
28 | {% endif %} |
|
28 | {% endif %} | |
29 | {% if is_opening %} |
|
29 | {% if is_opening %} | |
30 | {% if need_open_link %} |
|
30 | {% if need_open_link %} | |
31 | {% if thread.archived %} |
|
31 | {% if thread.is_archived %} | |
32 | <a class="link" href="{% url 'thread' post.id %}">{% trans "Open" %}</a> |
|
32 | <a class="link" href="{% url 'thread' post.id %}">{% trans "Open" %}</a> | |
33 | {% else %} |
|
33 | {% else %} | |
34 | <a class="link" href="{% url 'thread' post.id %}#form">{% trans "Reply" %}</a> |
|
34 | <a class="link" href="{% url 'thread' post.id %}#form">{% trans "Reply" %}</a> | |
35 | {% endif %} |
|
35 | {% endif %} | |
36 | {% endif %} |
|
36 | {% endif %} | |
37 | {% else %} |
|
37 | {% else %} | |
38 | {% if need_op_data %} |
|
38 | {% if need_op_data %} | |
39 | {% with thread.get_opening_post as op %} |
|
39 | {% with thread.get_opening_post as op %} | |
40 | {% trans " in " %}{{ op.get_link_view|safe }} <span class="title">{{ op.get_title_or_text }}</span> |
|
40 | {% trans " in " %}{{ op.get_link_view|safe }} <span class="title">{{ op.get_title_or_text }}</span> | |
41 | {% endwith %} |
|
41 | {% endwith %} | |
42 | {% endif %} |
|
42 | {% endif %} | |
43 | {% endif %} |
|
43 | {% endif %} | |
44 | {% if reply_link and not thread.archived %} |
|
44 | {% if reply_link and not thread.is_archived %} | |
45 | <a href="#form" onclick="addQuickReply('{{ post.id }}'); return false;">{% trans 'Reply' %}</a> |
|
45 | <a href="#form" onclick="addQuickReply('{{ post.id }}'); return false;">{% trans 'Reply' %}</a> | |
46 | {% endif %} |
|
46 | {% endif %} | |
47 |
|
47 | |||
48 | {% if post.global_id %} |
|
48 | {% if post.global_id %} | |
49 | <a class="global-id" href="{% url 'post_sync_data' post.id %}"> [RAW] </a> |
|
49 | <a class="global-id" href="{% url 'post_sync_data' post.id %}"> [RAW] </a> | |
50 | {% endif %} |
|
50 | {% endif %} | |
51 |
|
51 | |||
52 | {% if moderator %} |
|
52 | {% if perms.boards.change_post or perms.boards.delete_post or perms.boards.change_thread or perms_boards.delete_thread %} | |
53 | <span class="moderator_info"> |
|
53 | <span class="moderator_info"> | |
54 | | <a href="{% url 'admin:boards_post_change' post.id %}">{% trans 'Edit' %}</a> |
|
54 | {% if perms.boards.change_post or perms.boards.delete_post %} | |
55 | {% if is_opening %} |
|
55 | | <a href="{% url 'admin:boards_post_change' post.id %}">{% trans 'Edit' %}</a> | |
56 | | <a href="{% url 'admin:boards_thread_change' thread.id %}">{% trans 'Edit thread' %}</a> |
|
|||
57 | {% endif %} |
|
56 | {% endif %} | |
58 | <form action="{% url 'thread' thread.get_opening_post_id %}?post_id={{ post.id }}" method="post" class="post-button-form"> |
|
57 | {% if perms.boards.change_thread or perms_boards.delete_thread %} | |
59 | | <button name="method" value="toggle_hide_post">H</button> |
|
58 | {% if is_opening %} | |
60 | </form> |
|
59 | | <a href="{% url 'admin:boards_thread_change' thread.id %}">{% trans 'Edit thread' %}</a> | |
|
60 | {% endif %} | |||
|
61 | {% endif %} | |||
61 | </form> |
|
62 | </form> | |
62 | </span> |
|
63 | </span> | |
63 | {% endif %} |
|
64 | {% endif %} | |
64 | </div> |
|
65 | </div> | |
65 | {% comment %} |
|
66 | {% comment %} | |
66 | Post images. Currently only 1 image can be posted and shown, but post model |
|
67 | Post images. Currently only 1 image can be posted and shown, but post model | |
67 | supports multiple. |
|
68 | supports multiple. | |
68 | {% endcomment %} |
|
69 | {% endcomment %} | |
69 | {% for image in post.images.all %} |
|
70 | {% for image in post.images.all %} | |
70 | {{ image.get_view|safe }} |
|
71 | {{ image.get_view|safe }} | |
71 | {% endfor %} |
|
72 | {% endfor %} | |
72 | {% for file in post.attachments.all %} |
|
73 | {% for file in post.attachments.all %} | |
73 | {{ file.get_view|safe }} |
|
74 | {{ file.get_view|safe }} | |
74 | {% endfor %} |
|
75 | {% endfor %} | |
75 | {% comment %} |
|
76 | {% comment %} | |
76 | Post message (text) |
|
77 | Post message (text) | |
77 | {% endcomment %} |
|
78 | {% endcomment %} | |
78 | <div class="message"> |
|
79 | <div class="message"> | |
79 | {% autoescape off %} |
|
80 | {% autoescape off %} | |
80 | {% if truncated %} |
|
81 | {% if truncated %} | |
81 | {{ post.get_text|truncatewords_html:50 }} |
|
82 | {{ post.get_text|truncatewords_html:50 }} | |
82 | {% else %} |
|
83 | {% else %} | |
83 | {{ post.get_text }} |
|
84 | {{ post.get_text }} | |
84 | {% endif %} |
|
85 | {% endif %} | |
85 | {% endautoescape %} |
|
86 | {% endautoescape %} | |
86 | </div> |
|
87 | </div> | |
87 | {% if post.is_referenced %} |
|
88 | {% if post.is_referenced %} | |
88 | {% if mode_tree %} |
|
89 | {% if mode_tree %} | |
89 | <div class="tree_reply"> |
|
90 | <div class="tree_reply"> | |
90 | {% for refpost in post.get_referenced_posts %} |
|
91 | {% for refpost in post.get_referenced_posts %} | |
91 | {% post_view refpost mode_tree=True %} |
|
92 | {% post_view refpost mode_tree=True %} | |
92 | {% endfor %} |
|
93 | {% endfor %} | |
93 | </div> |
|
94 | </div> | |
94 | {% else %} |
|
95 | {% else %} | |
95 | <div class="refmap"> |
|
96 | <div class="refmap"> | |
96 | {% trans "Replies" %}: {{ post.refmap|safe }} |
|
97 | {% trans "Replies" %}: {{ post.refmap|safe }} | |
97 | </div> |
|
98 | </div> | |
98 | {% endif %} |
|
99 | {% endif %} | |
99 | {% endif %} |
|
100 | {% endif %} | |
100 | {% comment %} |
|
101 | {% comment %} | |
101 | Thread metadata: counters, tags etc |
|
102 | Thread metadata: counters, tags etc | |
102 | {% endcomment %} |
|
103 | {% endcomment %} | |
103 | {% if is_opening %} |
|
104 | {% if is_opening %} | |
104 | <div class="metadata"> |
|
105 | <div class="metadata"> | |
105 | {% if is_opening and need_open_link %} |
|
106 | {% if is_opening and need_open_link %} | |
106 | {% blocktrans count count=thread.get_reply_count %}{{ count }} message{% plural %}{{ count }} messages{% endblocktrans %}, |
|
107 | {% blocktrans count count=thread.get_reply_count %}{{ count }} message{% plural %}{{ count }} messages{% endblocktrans %}, | |
107 | {% blocktrans count count=thread.get_images_count %}{{ count }} image{% plural %}{{ count }} images{% endblocktrans %}. |
|
108 | {% blocktrans count count=thread.get_images_count %}{{ count }} image{% plural %}{{ count }} images{% endblocktrans %}. | |
108 | {% endif %} |
|
109 | {% endif %} | |
109 | <span class="tags"> |
|
110 | <span class="tags"> | |
110 | {{ thread.get_tag_url_list|safe }} |
|
111 | {{ thread.get_tag_url_list|safe }} | |
111 | </span> |
|
112 | </span> | |
112 | </div> |
|
113 | </div> | |
113 | {% endif %} |
|
114 | {% endif %} | |
114 | </div> |
|
115 | </div> |
@@ -1,22 +1,22 | |||||
1 | {% extends "boards/static_base.html" %} |
|
1 | {% extends "boards/static_base.html" %} | |
2 |
|
2 | |||
3 | {% load i18n %} |
|
3 | {% load i18n %} | |
4 |
|
4 | |||
5 | {% block head %} |
|
5 | {% block head %} | |
6 | <title>{% trans "Syntax" %}</title> |
|
6 | <title>{% trans "Syntax" %}</title> | |
7 | {% endblock %} |
|
7 | {% endblock %} | |
8 |
|
8 | |||
9 | {% block staticcontent %} |
|
9 | {% block staticcontent %} | |
10 | <h2>{% trans 'Syntax' %}</h2> |
|
10 | <h2>{% trans 'Syntax' %}</h2> | |
11 | <p>[i]<i>{% trans 'Italic text' %}</i>[/i]</p> |
|
11 | <p>[i]<i>{% trans 'Italic text' %}</i>[/i]</p> | |
12 | <p>[b]<b>{% trans 'Bold text' %}</b>[/b]</p> |
|
12 | <p>[b]<b>{% trans 'Bold text' %}</b>[/b]</p> | |
13 | <p>[spoiler]<span class="spoiler">{% trans 'Spoiler' %}</span>[/spoiler]</p> |
|
13 | <p>[spoiler]<span class="spoiler">{% trans 'Spoiler' %}</span>[/spoiler]</p> | |
14 | <p>[post]123[/post] — {% trans 'Link to a post' %}</p> |
|
14 | <p>[post]123[/post] — {% trans 'Link to a post' %}</p> | |
15 | <p>[s]<span class="strikethrough">{% trans 'Strikethrough text' %}</span>[/s]</p> |
|
15 | <p>[s]<span class="strikethrough">{% trans 'Strikethrough text' %}</span>[/s]</p> | |
16 | <p>[comment]<span class="comment">{% trans 'Comment' %}</span>[/comment]</p> |
|
16 | <p>[comment]<span class="comment">{% trans 'Comment' %}</span>[/comment]</p> | |
17 | <p>[quote]<span class="quote">>{% trans 'Quote' %}</span>[/quote]</p> |
|
17 | <p>[quote]<span class="quote">>{% trans 'Quote' %}</span>[/quote]</p> | |
18 |
<p>[quote |
|
18 | <p>[quote=src]<div class="multiquote"><div class="quote-header">src</div><div class="quote-text">{% trans 'Quote' %}</div></div><br />[/quote]</p> | |
19 | <p>[tag]<a class="tag">tag</a>[/tag]</p> |
|
19 | <p>[tag]<a class="tag">tag</a>[/tag]</p> | |
20 | <br/> |
|
20 | <br/> | |
21 | <p>{% trans 'You can try pasting the text and previewing the result here:' %} <a href="{% url 'preview' %}">{% trans 'Preview' %}</a></p> |
|
21 | <p>{% trans 'You can try pasting the text and previewing the result here:' %} <a href="{% url 'preview' %}">{% trans 'Preview' %}</a></p> | |
22 | {% endblock %} |
|
22 | {% endblock %} |
@@ -1,186 +1,128 | |||||
1 | {% extends "boards/base.html" %} |
|
1 | {% extends "boards/base.html" %} | |
2 |
|
2 | |||
3 | {% load i18n %} |
|
3 | {% load i18n %} | |
4 | {% load board %} |
|
4 | {% load board %} | |
5 | {% load static %} |
|
5 | {% load static %} | |
6 | {% load tz %} |
|
6 | {% load tz %} | |
7 |
|
7 | |||
8 | {% block head %} |
|
8 | {% block head %} | |
9 | <meta name="robots" content="noindex"> |
|
9 | <meta name="robots" content="noindex"> | |
10 |
|
10 | |||
11 | {% if tag %} |
|
11 | <title>{{ tag.name }} - {% trans 'Gallery' %} - {{ site_name }}</title> | |
12 | <title>{{ tag.name }} - {{ site_name }}</title> |
|
|||
13 | {% else %} |
|
|||
14 | <title>{{ site_name }}</title> |
|
|||
15 | {% endif %} |
|
|||
16 |
|
12 | |||
17 | {% if prev_page_link %} |
|
13 | {% if prev_page_link %} | |
18 | <link rel="prev" href="{{ prev_page_link }}" /> |
|
14 | <link rel="prev" href="{{ prev_page_link }}" /> | |
19 | {% endif %} |
|
15 | {% endif %} | |
20 | {% if next_page_link %} |
|
16 | {% if next_page_link %} | |
21 | <link rel="next" href="{{ next_page_link }}" /> |
|
17 | <link rel="next" href="{{ next_page_link }}" /> | |
22 | {% endif %} |
|
18 | {% endif %} | |
23 |
|
19 | |||
24 | {% endblock %} |
|
20 | {% endblock %} | |
25 |
|
21 | |||
26 | {% block content %} |
|
22 | {% block content %} | |
27 |
|
23 | |||
28 | {% get_current_language as LANGUAGE_CODE %} |
|
24 | {% get_current_language as LANGUAGE_CODE %} | |
29 | {% get_current_timezone as TIME_ZONE %} |
|
25 | {% get_current_timezone as TIME_ZONE %} | |
30 |
|
26 | |||
31 | {% for banner in banners %} |
|
27 | {% for banner in banners %} | |
32 | <div class="post"> |
|
28 | <div class="post"> | |
33 | <div class="title">{{ banner.title }}</div> |
|
29 | <div class="title">{{ banner.title }}</div> | |
34 | <div>{{ banner.text }}</div> |
|
30 | <div>{{ banner.get_text }}</div> | |
35 |
<div>{% trans ' |
|
31 | <div>{% trans 'Details' %}: <a href="{{ banner.post.get_absolute_url }}">>>{{ banner.post.id }}</a></div> | |
36 | </div> |
|
32 | </div> | |
37 | {% endfor %} |
|
33 | {% endfor %} | |
38 |
|
34 | |||
39 | {% if tag %} |
|
|||
40 | <div class="tag_info" style="border-bottom: solid .5ex #{{ tag.get_color }}"> |
|
35 | <div class="tag_info" style="border-bottom: solid .5ex #{{ tag.get_color }}"> | |
41 | {% if random_image_post %} |
|
36 | {% if random_image_post %} | |
42 | <div class="tag-image"> |
|
37 | <div class="tag-image"> | |
43 | {% with image=random_image_post.images.first %} |
|
38 | {% with image=random_image_post.images.first %} | |
44 | <a href="{{ random_image_post.get_absolute_url }}"><img |
|
39 | <a href="{{ random_image_post.get_absolute_url }}"><img | |
45 | src="{{ image.image.url_200x150 }}" |
|
40 | src="{{ image.image.url_200x150 }}" | |
46 | width="{{ image.pre_width }}" |
|
41 | width="{{ image.pre_width }}" | |
47 |
height="{{ image.pre_height }}" |
|
42 | height="{{ image.pre_height }}" | |
|
43 | alt="{{ random_image_post.id }}"/></a> | |||
48 | {% endwith %} |
|
44 | {% endwith %} | |
49 | </div> |
|
45 | </div> | |
50 | {% endif %} |
|
46 | {% endif %} | |
51 | <div class="tag-text-data"> |
|
47 | <div class="tag-text-data"> | |
52 | <h2> |
|
48 | <h2> | |
53 | <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form"> |
|
49 | /{{ tag.get_view|safe }}/ | |
54 |
|
|
50 | {% if perms.change_tag %} | |
55 | <button name="method" value="unsubscribe" class="fav">★</button> |
|
|||
56 | {% else %} |
|
|||
57 | <button name="method" value="subscribe" class="not_fav">★</button> |
|
|||
58 | {% endif %} |
|
|||
59 | </form> |
|
|||
60 | <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form"> |
|
|||
61 | {% if is_hidden %} |
|
|||
62 | <button name="method" value="unhide" class="fav">H</button> |
|
|||
63 | {% else %} |
|
|||
64 | <button name="method" value="hide" class="not_fav">H</button> |
|
|||
65 | {% endif %} |
|
|||
66 | </form> |
|
|||
67 | {{ tag.get_view|safe }} |
|
|||
68 | {% if moderator %} |
|
|||
69 | <span class="moderator_info">| <a href="{% url 'admin:boards_tag_change' tag.id %}">{% trans 'Edit tag' %}</a></span> |
|
51 | <span class="moderator_info">| <a href="{% url 'admin:boards_tag_change' tag.id %}">{% trans 'Edit tag' %}</a></span> | |
70 | {% endif %} |
|
52 | {% endif %} | |
71 | </h2> |
|
53 | </h2> | |
72 | {% if tag.get_description %} |
|
54 | {% if tag.get_description %} | |
73 | <p>{{ tag.get_description|safe }}</p> |
|
55 | <p>{{ tag.get_description|safe }}</p> | |
74 | {% endif %} |
|
56 | {% endif %} | |
75 | <p> |
|
57 | <p> | |
76 | {% blocktrans count count=tag.get_active_thread_count %}{{ count }} active thread{% plural %}active threads{% endblocktrans %}, |
|
58 | {% with active_count=tag.get_active_thread_count bumplimit_count=tag.get_bumplimit_thread_count archived_count=tag.get_archived_thread_count %} | |
77 | {% blocktrans count count=tag.get_bumplimit_thread_count %}{{ count }} thread in bumplimit{% plural %} threads in bumplimit{% endblocktrans %}, |
|
59 | {% if active_count %} | |
78 |
{% blocktrans count count= |
|
60 | {% blocktrans count count=active_count %}{{ count }} active thread{% plural %}active threads{% endblocktrans %}, | |
|
61 | {% endif %} | |||
|
62 | {% if bumplimit_count %} | |||
|
63 | {% blocktrans count count=bumplimit_count %}{{ count }} thread in bumplimit{% plural %} threads in bumplimit{% endblocktrans %}, | |||
|
64 | {% endif %} | |||
|
65 | {% if archived_count %} | |||
|
66 | {% blocktrans count count=archived_count %}{{ count }} archived thread{% plural %}archived threads{% endblocktrans %}, | |||
|
67 | {% endif %} | |||
|
68 | {% endwith %} | |||
79 | {% blocktrans count count=tag.get_post_count %}{{ count }} message{% plural %}messages{% endblocktrans %}. |
|
69 | {% blocktrans count count=tag.get_post_count %}{{ count }} message{% plural %}messages{% endblocktrans %}. | |
80 | </p> |
|
70 | </p> | |
81 | {% if tag.get_all_parents %} |
|
71 | {% if tag.get_all_parents %} | |
82 | <p> |
|
72 | <p> | |
83 | {% for parent in tag.get_all_parents %} |
|
73 | {% for parent in tag.get_all_parents %} | |
84 | {{ parent.get_view|safe }} > |
|
74 | {{ parent.get_view|safe }} > | |
85 | {% endfor %} |
|
75 | {% endfor %} | |
86 | {{ tag.get_view|safe }} |
|
76 | {{ tag.get_view|safe }} | |
87 | </p> |
|
77 | </p> | |
88 | {% endif %} |
|
78 | {% endif %} | |
89 | </div> |
|
79 | </div> | |
90 | </div> |
|
80 | </div> | |
91 | {% endif %} |
|
|||
92 |
|
81 | |||
93 |
{% if |
|
82 | {% if prev_page_link %} | |
94 | {% if prev_page_link %} |
|
83 | <div class="page_link"> | |
95 | <div class="page_link"> |
|
84 | <a href="{{ prev_page_link }}">{% trans "Previous page" %}</a> | |
96 | <a href="{{ prev_page_link }}">{% trans "Previous page" %}</a> |
|
85 | </div> | |
97 | </div> |
|
|||
98 | {% endif %} |
|
|||
99 |
|
||||
100 | {% for thread in threads %} |
|
|||
101 | <div class="thread"> |
|
|||
102 | {% post_view thread.get_opening_post moderator=moderator thread=thread truncated=True need_open_link=True %} |
|
|||
103 | {% if not thread.archived %} |
|
|||
104 | {% with last_replies=thread.get_last_replies %} |
|
|||
105 | {% if last_replies %} |
|
|||
106 | {% with skipped_replies_count=thread.get_skipped_replies_count %} |
|
|||
107 | {% if skipped_replies_count %} |
|
|||
108 | <div class="skipped_replies"> |
|
|||
109 | <a href="{% url 'thread' thread.get_opening_post_id %}"> |
|
|||
110 | {% blocktrans count count=skipped_replies_count %}Skipped {{ count }} reply. Open thread to see all replies.{% plural %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %} |
|
|||
111 | </a> |
|
|||
112 | </div> |
|
|||
113 | {% endif %} |
|
|||
114 | {% endwith %} |
|
|||
115 | <div class="last-replies"> |
|
|||
116 | {% for post in last_replies %} |
|
|||
117 | {% post_view post moderator=moderator truncated=True %} |
|
|||
118 | {% endfor %} |
|
|||
119 | </div> |
|
|||
120 | {% endif %} |
|
|||
121 | {% endwith %} |
|
|||
122 | {% endif %} |
|
|||
123 | </div> |
|
|||
124 | {% endfor %} |
|
|||
125 |
|
||||
126 | {% if next_page_link %} |
|
|||
127 | <div class="page_link"> |
|
|||
128 | <a href="{{ next_page_link }}">{% trans "Next page" %}</a> |
|
|||
129 | </div> |
|
|||
130 | {% endif %} |
|
|||
131 | {% else %} |
|
|||
132 | <div class="post"> |
|
|||
133 | {% trans 'No threads exist. Create the first one!' %}</div> |
|
|||
134 | {% endif %} |
|
86 | {% endif %} | |
135 |
|
87 | |||
136 | <div class="post-form-w"> |
|
88 | {% for image in images %} | |
137 | <script src="{% static 'js/panel.js' %}"></script> |
|
89 | <div class="gallery_image"> | |
138 | <div class="post-form"> |
|
90 | {% autoescape off %} | |
139 | <div class="form-title">{% trans "Create new thread" %}</div> |
|
91 | {{ image.get_view }} | |
140 | <div class="swappable-form-full"> |
|
92 | {% endautoescape %} | |
141 | <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %} |
|
93 | <div class="gallery_image_metadata"> | |
142 | {{ form.as_div }} |
|
94 | {{ image.width }}x{{ image.height }} | |
143 | <div class="form-submit"> |
|
|||
144 | <input type="submit" value="{% trans "Post" %}"/> |
|
|||
145 | <button id="preview-button" onclick="return false;">{% trans 'Preview' %}</button> |
|
|||
146 | </div> |
|
|||
147 | </form> |
|
|||
148 | </div> |
|
95 | </div> | |
149 | <div> |
|
|||
150 | {% trans 'Tags must be delimited by spaces. Text or image is required.' %} |
|
|||
151 | </div> |
|
|||
152 | <div id="preview-text"></div> |
|
|||
153 | <div><a href="{% url "staticpage" name="help" %}">{% trans 'Text syntax' %}</a></div> |
|
|||
154 | <div><a href="{% url "tags" "required" %}">{% trans 'Tags' %}</a></div> |
|
|||
155 | </div> |
|
96 | </div> | |
156 | </div> |
|
97 | {% endfor %} | |
157 |
|
98 | |||
158 | <script src="{% static 'js/form.js' %}"></script> |
|
99 | {% if next_page_link %} | |
159 | <script src="{% static 'js/thread_create.js' %}"></script> |
|
100 | <div class="page_link"> | |
160 |
|
101 | <a href="{{ next_page_link }}">{% trans "Next page" %}</a> | ||
|
102 | </div> | |||
|
103 | {% endif %} | |||
161 | {% endblock %} |
|
104 | {% endblock %} | |
162 |
|
105 | |||
163 | {% block metapanel %} |
|
106 | {% block metapanel %} | |
164 |
|
107 | |||
165 | <span class="metapanel"> |
|
108 | <span class="metapanel"> | |
166 | <b><a href="{% url "authors" %}">{{ site_name }}</a> {{ version }}</b> |
|
109 | <b><a href="{% url "authors" %}">{{ site_name }}</a> {{ version }}</b> | |
167 | {% trans "Pages:" %} |
|
110 | {% trans "Pages:" %} | |
168 | [ |
|
111 | [ | |
169 | {% with dividers=paginator.get_dividers %} |
|
112 | {% with dividers=paginator.get_dividers %} | |
170 | {% for page in paginator.get_divided_range %} |
|
113 | {% for page in paginator.get_divided_range %} | |
171 | {% if page in dividers %} |
|
114 | {% if page in dividers %} | |
172 | …, |
|
115 | …, | |
173 | {% endif %} |
|
116 | {% endif %} | |
174 | <a |
|
117 | <a | |
175 |
{% ifequal page current_page |
|
118 | {% ifequal page paginator.current_page %} | |
176 | class="current_page" |
|
119 | class="current_page" | |
177 | {% endifequal %} |
|
120 | {% endifequal %} | |
178 | href="{% page_url paginator page %}">{{ page }}</a> |
|
121 | href="{% page_url paginator page %}">{{ page }}</a> | |
179 | {% if not forloop.last %},{% endif %} |
|
122 | {% if not forloop.last %},{% endif %} | |
180 | {% endfor %} |
|
123 | {% endfor %} | |
181 | {% endwith %} |
|
124 | {% endwith %} | |
182 | ] |
|
125 | ] | |
183 | [<a href="rss/">RSS</a>] |
|
|||
184 | </span> |
|
126 | </span> | |
185 |
|
127 | |||
186 | {% endblock %} |
|
128 | {% endblock %} |
@@ -1,44 +1,43 | |||||
1 | {% extends "boards/base.html" %} |
|
1 | {% extends "boards/base.html" %} | |
2 |
|
2 | |||
3 | {% load i18n %} |
|
3 | {% load i18n %} | |
4 | {% load static from staticfiles %} |
|
4 | {% load static from staticfiles %} | |
5 | {% load board %} |
|
5 | {% load board %} | |
6 | {% load tz %} |
|
6 | {% load tz %} | |
7 |
|
7 | |||
8 | {% block head %} |
|
8 | {% block head %} | |
9 | <title>{{ opening_post.get_title_or_text }} - {{ site_name }}</title> |
|
9 | <title>{{ opening_post.get_title_or_text }} - {{ site_name }}</title> | |
10 | {% endblock %} |
|
10 | {% endblock %} | |
11 |
|
11 | |||
12 | {% block content %} |
|
12 | {% block content %} | |
13 | <div class="image-mode-tab"> |
|
13 | <div class="image-mode-tab"> | |
14 | <a {% ifequal mode 'normal' %}class="current_mode"{% endifequal %} href="{% url 'thread' opening_post.id %}">{% trans 'Normal' %}</a>, |
|
14 | <a {% ifequal mode 'normal' %}class="current_mode"{% endifequal %} href="{% url 'thread' opening_post.id %}">{% trans 'Normal' %}</a>, | |
15 | <a {% ifequal mode 'gallery' %}class="current_mode"{% endifequal %} href="{% url 'thread_gallery' opening_post.id %}">{% trans 'Gallery' %}</a>, |
|
15 | <a {% ifequal mode 'gallery' %}class="current_mode"{% endifequal %} href="{% url 'thread_gallery' opening_post.id %}">{% trans 'Gallery' %}</a>, | |
16 | <a {% ifequal mode 'tree' %}class="current_mode"{% endifequal %} href="{% url 'thread_tree' opening_post.id %}">{% trans 'Tree' %}</a> |
|
16 | <a {% ifequal mode 'tree' %}class="current_mode"{% endifequal %} href="{% url 'thread_tree' opening_post.id %}">{% trans 'Tree' %}</a> | |
17 | </div> |
|
17 | </div> | |
18 |
|
18 | |||
19 | {% block thread_content %} |
|
19 | {% block thread_content %} | |
20 | {% endblock %} |
|
20 | {% endblock %} | |
21 | {% endblock %} |
|
21 | {% endblock %} | |
22 |
|
22 | |||
23 | {% block metapanel %} |
|
23 | {% block metapanel %} | |
24 |
|
24 | |||
25 | <span class="metapanel" |
|
25 | <span class="metapanel" | |
26 | data-last-update="{{ last_update }}" |
|
26 | data-last-update="{{ last_update }}" | |
27 | data-ws-token-time="{{ ws_token_time }}" |
|
27 | data-ws-token-time="{{ ws_token_time }}" | |
28 | data-ws-token="{{ ws_token }}" |
|
28 | data-ws-token="{{ ws_token }}" | |
29 | data-ws-project="{{ ws_project }}" |
|
29 | data-ws-project="{{ ws_project }}" | |
30 | data-ws-host="{{ ws_host }}" |
|
30 | data-ws-host="{{ ws_host }}" | |
31 | data-ws-port="{{ ws_port }}"> |
|
31 | data-ws-port="{{ ws_port }}"> | |
32 |
|
32 | |||
33 | {% with replies_count=thread.get_reply_count%} |
|
33 | {% with replies_count=thread.get_reply_count%} | |
34 | <span id="reply-count">{{ thread.get_reply_count }}</span>{% if thread.has_post_limit %}/{{ thread.max_posts }}{% endif %} |
|
34 | <span id="reply-count">{{ thread.get_reply_count }}</span>{% if thread.has_post_limit %}/{{ thread.max_posts }}{% endif %} | |
35 | <span id="message-count-text">{% blocktrans count repliess_count=replies_count %}message{% plural %}messages{% endblocktrans %}</span>, |
|
35 | <span id="message-count-text">{% blocktrans count repliess_count=replies_count %}message{% plural %}messages{% endblocktrans %}</span>, | |
36 | {% endwith %} |
|
36 | {% endwith %} | |
37 | {% with images_count=thread.get_images_count%} |
|
37 | {% with images_count=thread.get_images_count%} | |
38 | <span id="image-count">{{ images_count }}</span> <span id="image-count-text">{% blocktrans count count=images_count %}image{% plural %}images{% endblocktrans %}</span>. |
|
38 | <span id="image-count">{{ images_count }}</span> <span id="image-count-text">{% blocktrans count count=images_count %}image{% plural %}images{% endblocktrans %}</span>. | |
39 | {% endwith %} |
|
39 | {% endwith %} | |
40 |
|
|
40 | {% trans 'Last update: ' %}<span id="last-update"><time datetime="{{ thread.last_edit_time|date:'c' }}">{{ thread.last_edit_time }}</time></span> | |
41 | [<a href="rss/">RSS</a>] |
|
|||
42 | </span> |
|
41 | </span> | |
43 |
|
42 | |||
44 | {% endblock %} |
|
43 | {% endblock %} |
@@ -1,70 +1,78 | |||||
1 | {% extends "boards/thread.html" %} |
|
1 | {% extends "boards/thread.html" %} | |
2 |
|
2 | |||
3 | {% load i18n %} |
|
3 | {% load i18n %} | |
4 | {% load static from staticfiles %} |
|
4 | {% load static from staticfiles %} | |
5 | {% load board %} |
|
5 | {% load board %} | |
6 | {% load tz %} |
|
6 | {% load tz %} | |
7 |
|
7 | |||
8 | {% block thread_content %} |
|
8 | {% block thread_content %} | |
9 | {% get_current_language as LANGUAGE_CODE %} |
|
9 | {% get_current_language as LANGUAGE_CODE %} | |
10 | {% get_current_timezone as TIME_ZONE %} |
|
10 | {% get_current_timezone as TIME_ZONE %} | |
11 |
|
11 | |||
12 | <div class="tag_info"> |
|
12 | <div class="tag_info"> | |
13 | <h2> |
|
13 | <h2> | |
14 | <form action="{% url 'thread' opening_post.id %}" method="post" class="post-button-form"> |
|
14 | <form action="{% url 'thread' opening_post.id %}" method="post" class="post-button-form"> | |
|
15 | {% csrf_token %} | |||
15 | {% if is_favorite %} |
|
16 | {% if is_favorite %} | |
16 | <button name="method" value="unsubscribe" class="fav">★</button> |
|
17 | <button name="method" value="unsubscribe" class="fav">★</button> | |
17 | {% else %} |
|
18 | {% else %} | |
18 | <button name="method" value="subscribe" class="not_fav">★</button> |
|
19 | <button name="method" value="subscribe" class="not_fav">★</button> | |
19 | {% endif %} |
|
20 | {% endif %} | |
20 | </form> |
|
21 | </form> | |
21 | {{ opening_post.get_title_or_text }} |
|
22 | {{ opening_post.get_title_or_text }} | |
22 | </h2> |
|
23 | </h2> | |
23 | </div> |
|
24 | </div> | |
24 |
|
25 | |||
25 | {% if bumpable and thread.has_post_limit %} |
|
26 | {% if bumpable and thread.has_post_limit %} | |
26 | <div class="bar-bg"> |
|
27 | <div class="bar-bg"> | |
27 | <div class="bar-value" style="width:{{ bumplimit_progress }}%" id="bumplimit_progress"> |
|
28 | <div class="bar-value" style="width:{{ bumplimit_progress }}%" id="bumplimit_progress"> | |
28 | </div> |
|
29 | </div> | |
29 | <div class="bar-text"> |
|
30 | <div class="bar-text"> | |
30 | <span id="left_to_limit">{{ posts_left }}</span> {% trans 'posts to bumplimit' %} |
|
31 | <span id="left_to_limit">{{ posts_left }}</span> {% trans 'posts to bumplimit' %} | |
31 | </div> |
|
32 | </div> | |
32 | </div> |
|
33 | </div> | |
33 | {% endif %} |
|
34 | {% endif %} | |
34 |
|
35 | |||
35 | <div class="thread"> |
|
36 | <div class="thread"> | |
36 | {% for post in thread.get_replies %} |
|
37 | {% for post in thread.get_replies %} | |
37 |
{% post_view post |
|
38 | {% post_view post reply_link=True %} | |
38 | {% endfor %} |
|
39 | {% endfor %} | |
39 | </div> |
|
40 | </div> | |
40 |
|
41 | |||
41 | {% if not thread.archived %} |
|
42 | {% if not thread.is_archived %} | |
42 | <div class="post-form-w"> |
|
43 | <div class="post-form-w"> | |
43 | <script src="{% static 'js/panel.js' %}"></script> |
|
44 | <script src="{% static 'js/panel.js' %}"></script> | |
44 | <div class="form-title">{% trans "Reply to thread" %} #{{ opening_post.id }}<span class="reply-to-message"> {% trans "to message " %} #<span id="reply-to-message-id"></span></span></div> |
|
45 | <div class="form-title">{% trans "Reply to thread" %} #{{ opening_post.id }}<span class="reply-to-message"> {% trans "to message " %} #<span id="reply-to-message-id"></span></span></div> | |
45 | <div class="post-form" id="compact-form"> |
|
46 | <div class="post-form" id="compact-form"> | |
46 | <div class="swappable-form-full"> |
|
47 | <div class="swappable-form-full"> | |
47 | <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %} |
|
48 | <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %} | |
48 | <div class="compact-form-text"></div> |
|
49 | <div class="compact-form-text"></div> | |
49 | {{ form.as_div }} |
|
50 | {{ form.as_div }} | |
50 | <div class="form-submit"> |
|
51 | <div class="form-submit"> | |
51 | <input type="submit" value="{% trans "Post" %}"/> |
|
52 | <input type="submit" value="{% trans "Post" %}"/> | |
52 | <button id="preview-button" onclick="return false;">{% trans 'Preview' %}</button> |
|
53 | <button id="preview-button" onclick="return false;">{% trans 'Preview' %}</button> | |
53 | </div> |
|
54 | </div> | |
54 | </form> |
|
55 | </form> | |
55 | </div> |
|
56 | </div> | |
56 | <div id="preview-text"></div> |
|
57 | <div id="preview-text"></div> | |
|
58 | <div> | |||
|
59 | {% with size=max_file_size|filesizeformat %} | |||
|
60 | {% blocktrans %}Max file size is {{ size }}.{% endblocktrans %} | |||
|
61 | {% endwith %} | |||
|
62 | </div> | |||
57 | <div><a href="{% url "staticpage" name="help" %}"> |
|
63 | <div><a href="{% url "staticpage" name="help" %}"> | |
58 | {% trans 'Text syntax' %}</a></div> |
|
64 | {% trans 'Text syntax' %}</a></div> | |
59 | <div><a id="form-close-button" href="#" onClick="resetFormPosition(); return false;">{% trans 'Close form' %}</a></div> |
|
65 | <div><a id="form-close-button" href="#" onClick="resetFormPosition(); return false;">{% trans 'Close form' %}</a></div> | |
60 | </div> |
|
66 | </div> | |
61 | </div> |
|
67 | </div> | |
62 |
|
68 | |||
|
69 | <script src="{% static 'js/form.js' %}"></script> | |||
63 | <script src="{% static 'js/jquery.form.min.js' %}"></script> |
|
70 | <script src="{% static 'js/jquery.form.min.js' %}"></script> | |
|
71 | <script id="sha256Script" src="{% static 'js/3party/sha256.js' %}"></script> | |||
|
72 | <script id="powScript" src="{% static 'js/proof_of_work.js' %}"></script> | |||
|
73 | <script src="{% static 'js/thread.js' %}"></script> | |||
|
74 | <script src="{% static 'js/thread_update.js' %}"></script> | |||
64 | {% endif %} |
|
75 | {% endif %} | |
65 |
|
76 | |||
66 | <script src="{% static 'js/form.js' %}"></script> |
|
|||
67 | <script src="{% static 'js/thread.js' %}"></script> |
|
|||
68 | <script src="{% static 'js/thread_update.js' %}"></script> |
|
|||
69 | <script src="{% static 'js/3party/centrifuge.js' %}"></script> |
|
77 | <script src="{% static 'js/3party/centrifuge.js' %}"></script> | |
70 | {% endblock %} |
|
78 | {% endblock %} |
@@ -1,19 +1,19 | |||||
1 | {% extends "boards/thread.html" %} |
|
1 | {% extends "boards/thread.html" %} | |
2 |
|
2 | |||
3 | {% load i18n %} |
|
3 | {% load i18n %} | |
4 | {% load static from staticfiles %} |
|
4 | {% load static from staticfiles %} | |
5 | {% load board %} |
|
5 | {% load board %} | |
6 | {% load tz %} |
|
6 | {% load tz %} | |
7 |
|
7 | |||
8 | {% block thread_content %} |
|
8 | {% block thread_content %} | |
9 | {% get_current_language as LANGUAGE_CODE %} |
|
9 | {% get_current_language as LANGUAGE_CODE %} | |
10 | {% get_current_timezone as TIME_ZONE %} |
|
10 | {% get_current_timezone as TIME_ZONE %} | |
11 |
|
11 | |||
12 | <div class="thread"> |
|
12 | <div class="thread"> | |
13 | {% for post in thread.get_top_level_replies %} |
|
13 | {% for post in thread.get_top_level_replies %} | |
14 |
{% post_view post mode |
|
14 | {% post_view post mode_tree=True %} | |
15 | {% endfor %} |
|
15 | {% endfor %} | |
16 | </div> |
|
16 | </div> | |
17 |
|
17 | |||
18 | <script src="{% static 'js/thread.js' %}"></script> |
|
18 | <script src="{% static 'js/thread.js' %}"></script> | |
19 | {% endblock %} |
|
19 | {% endblock %} |
@@ -1,49 +1,50 | |||||
1 | import re |
|
1 | import re | |
2 | from django.shortcuts import get_object_or_404 |
|
2 | from django.shortcuts import get_object_or_404 | |
3 | from django import template |
|
3 | from django import template | |
4 |
|
4 | |||
5 |
|
5 | |||
6 | IMG_ACTION_URL = '[<a href="{}">{}</a>]' |
|
6 | IMG_ACTION_URL = '[<a href="{}">{}</a>]' | |
7 |
|
7 | |||
8 |
|
8 | |||
9 | register = template.Library() |
|
9 | register = template.Library() | |
10 |
|
10 | |||
11 | actions = [ |
|
11 | actions = [ | |
12 | { |
|
12 | { | |
13 | 'name': 'google', |
|
13 | 'name': 'google', | |
14 | 'link': 'http://google.com/searchbyimage?image_url=%s', |
|
14 | 'link': 'http://google.com/searchbyimage?image_url=%s', | |
15 | }, |
|
15 | }, | |
16 | { |
|
16 | { | |
17 | 'name': 'iqdb', |
|
17 | 'name': 'iqdb', | |
18 | 'link': 'http://iqdb.org/?url=%s', |
|
18 | 'link': 'http://iqdb.org/?url=%s', | |
19 | }, |
|
19 | }, | |
20 | ] |
|
20 | ] | |
21 |
|
21 | |||
22 |
|
22 | |||
23 | @register.simple_tag(name='post_url') |
|
23 | @register.simple_tag(name='post_url') | |
24 | def post_url(*args, **kwargs): |
|
24 | def post_url(*args, **kwargs): | |
25 | post_id = args[0] |
|
25 | post_id = args[0] | |
26 |
|
26 | |||
27 | post = get_object_or_404('Post', id=post_id) |
|
27 | post = get_object_or_404('Post', id=post_id) | |
28 |
|
28 | |||
29 | return post.get_absolute_url() |
|
29 | return post.get_absolute_url() | |
30 |
|
30 | |||
31 |
|
31 | |||
32 | @register.simple_tag(name='image_actions') |
|
32 | @register.simple_tag(name='image_actions') | |
33 | def image_actions(*args, **kwargs): |
|
33 | def image_actions(*args, **kwargs): | |
34 | image_link = args[0] |
|
34 | image_link = args[0] | |
35 | if len(args) > 1: |
|
35 | if len(args) > 1: | |
36 | image_link = 'http://' + args[1] + image_link # TODO https? |
|
36 | image_link = 'http://' + args[1] + image_link # TODO https? | |
37 |
|
37 | |||
38 | return ', '.join([IMG_ACTION_URL.format( |
|
38 | return ', '.join([IMG_ACTION_URL.format( | |
39 | action['link'] % image_link, action['name']) for action in actions]) |
|
39 | action['link'] % image_link, action['name']) for action in actions]) | |
40 |
|
40 | |||
41 |
|
41 | |||
42 | @register.simple_tag(name='post_view') |
|
42 | @register.simple_tag(name='post_view', takes_context=True) | |
43 | def post_view(post, *args, **kwargs): |
|
43 | def post_view(context, post, *args, **kwargs): | |
|
44 | kwargs['perms'] = context['perms'] | |||
44 | return post.get_view(*args, **kwargs) |
|
45 | return post.get_view(*args, **kwargs) | |
45 |
|
46 | |||
46 | @register.simple_tag(name='page_url') |
|
47 | @register.simple_tag(name='page_url') | |
47 | def page_url(paginator, page_number, *args, **kwargs): |
|
48 | def page_url(paginator, page_number, *args, **kwargs): | |
48 | if paginator.supports_urls(): |
|
49 | if paginator.supports_urls(): | |
49 | return paginator.get_page_url(page_number) |
|
50 | return paginator.get_page_url(page_number) |
@@ -1,69 +1,70 | |||||
1 | import simplejson |
|
1 | import simplejson | |
2 |
|
2 | |||
3 | from django.test import TestCase |
|
3 | from django.test import TestCase | |
4 | from boards.views import api |
|
4 | from boards.views import api | |
5 |
|
5 | |||
6 | from boards.models import Tag, Post |
|
6 | from boards.models import Tag, Post | |
7 | from boards.tests.mocks import MockRequest |
|
7 | from boards.tests.mocks import MockRequest | |
8 | from boards.utils import datetime_to_epoch |
|
8 | from boards.utils import datetime_to_epoch | |
9 |
|
9 | |||
10 |
|
10 | |||
11 | class ApiTest(TestCase): |
|
11 | class ApiTest(TestCase): | |
12 | def test_thread_diff(self): |
|
12 | def test_thread_diff(self): | |
13 | tag = Tag.objects.create(name='test_tag') |
|
13 | tag = Tag.objects.create(name='test_tag') | |
14 | opening_post = Post.objects.create_post(title='title', text='text', |
|
14 | opening_post = Post.objects.create_post(title='title', text='text', | |
15 | tags=[tag]) |
|
15 | tags=[tag]) | |
16 |
|
16 | |||
17 | req = MockRequest() |
|
17 | req = MockRequest() | |
18 | req.POST['thread'] = opening_post.id |
|
18 | req.POST['thread'] = opening_post.id | |
19 | req.POST['uids'] = opening_post.uid |
|
19 | req.POST['uids'] = opening_post.uid | |
20 | # Check the exact timestamp post was added |
|
20 | # Check the exact timestamp post was added | |
21 | empty_response = api.api_get_threaddiff(req) |
|
21 | empty_response = api.api_get_threaddiff(req) | |
22 | diff = simplejson.loads(empty_response.content) |
|
22 | diff = simplejson.loads(empty_response.content) | |
23 | self.assertEqual(0, len(diff['updated']), |
|
23 | self.assertEqual(0, len(diff['updated']), | |
24 | 'There must be no updated posts in the diff.') |
|
24 | 'There must be no updated posts in the diff.') | |
25 |
|
25 | |||
26 | uids = [opening_post.uid] |
|
26 | uids = [opening_post.uid] | |
27 |
|
27 | |||
28 | reply = Post.objects.create_post(title='', |
|
28 | reply = Post.objects.create_post(title='', | |
29 | text='[post]%d[/post]\ntext' % opening_post.id, |
|
29 | text='[post]%d[/post]\ntext' % opening_post.id, | |
30 | thread=opening_post.get_thread()) |
|
30 | thread=opening_post.get_thread()) | |
31 | req = MockRequest() |
|
31 | req = MockRequest() | |
32 | req.POST['thread'] = opening_post.id |
|
32 | req.POST['thread'] = opening_post.id | |
33 | req.POST['uids'] = ' '.join(uids) |
|
33 | req.POST['uids'] = ' '.join(uids) | |
|
34 | req.user = None | |||
34 | # Check the timestamp before post was added |
|
35 | # Check the timestamp before post was added | |
35 | response = api.api_get_threaddiff(req) |
|
36 | response = api.api_get_threaddiff(req) | |
36 | diff = simplejson.loads(response.content) |
|
37 | diff = simplejson.loads(response.content) | |
37 | self.assertEqual(2, len(diff['updated']), |
|
38 | self.assertEqual(2, len(diff['updated']), | |
38 | 'There must be 2 updated posts in the diff.') |
|
39 | 'There must be 2 updated posts in the diff.') | |
39 |
|
40 | |||
40 | # Reload post to get the new UID |
|
41 | # Reload post to get the new UID | |
41 | opening_post = Post.objects.get(id=opening_post.id) |
|
42 | opening_post = Post.objects.get(id=opening_post.id) | |
42 | req = MockRequest() |
|
43 | req = MockRequest() | |
43 | req.POST['thread'] = opening_post.id |
|
44 | req.POST['thread'] = opening_post.id | |
44 | req.POST['uids'] = ' '.join([opening_post.uid, reply.uid]) |
|
45 | req.POST['uids'] = ' '.join([opening_post.uid, reply.uid]) | |
45 | empty_response = api.api_get_threaddiff(req) |
|
46 | empty_response = api.api_get_threaddiff(req) | |
46 | diff = simplejson.loads(empty_response.content) |
|
47 | diff = simplejson.loads(empty_response.content) | |
47 | self.assertEqual(0, len(diff['updated']), |
|
48 | self.assertEqual(0, len(diff['updated']), | |
48 | 'There must be no updated posts in the diff.') |
|
49 | 'There must be no updated posts in the diff.') | |
49 |
|
50 | |||
50 | def test_get_threads(self): |
|
51 | def test_get_threads(self): | |
51 | # Create 10 threads |
|
52 | # Create 10 threads | |
52 | tag = Tag.objects.create(name='test_tag') |
|
53 | tag = Tag.objects.create(name='test_tag') | |
53 | for i in range(5): |
|
54 | for i in range(5): | |
54 | Post.objects.create_post(title='title', text='text', tags=[tag]) |
|
55 | Post.objects.create_post(title='title', text='text', tags=[tag]) | |
55 |
|
56 | |||
56 | # Get all threads |
|
57 | # Get all threads | |
57 | response = api.api_get_threads(MockRequest(), 5) |
|
58 | response = api.api_get_threads(MockRequest(), 5) | |
58 | diff = simplejson.loads(response.content) |
|
59 | diff = simplejson.loads(response.content) | |
59 | self.assertEqual(5, len(diff), 'Invalid thread list response.') |
|
60 | self.assertEqual(5, len(diff), 'Invalid thread list response.') | |
60 |
|
61 | |||
61 | # Get less threads then exist |
|
62 | # Get less threads then exist | |
62 | response = api.api_get_threads(MockRequest(), 3) |
|
63 | response = api.api_get_threads(MockRequest(), 3) | |
63 | diff = simplejson.loads(response.content) |
|
64 | diff = simplejson.loads(response.content) | |
64 | self.assertEqual(3, len(diff), 'Invalid thread list response.') |
|
65 | self.assertEqual(3, len(diff), 'Invalid thread list response.') | |
65 |
|
66 | |||
66 | # Get more threads then exist |
|
67 | # Get more threads then exist | |
67 | response = api.api_get_threads(MockRequest(), 10) |
|
68 | response = api.api_get_threads(MockRequest(), 10) | |
68 | diff = simplejson.loads(response.content) |
|
69 | diff = simplejson.loads(response.content) | |
69 | self.assertEqual(5, len(diff), 'Invalid thread list response.') |
|
70 | self.assertEqual(5, len(diff), 'Invalid thread list response.') |
@@ -1,199 +1,200 | |||||
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 |
|
3 | |||
4 | from boards import settings |
|
4 | from boards import settings | |
5 | from boards.models import Tag, Post, Thread, KeyPair |
|
5 | from boards.models import Tag, Post, Thread, KeyPair | |
|
6 | from boards.models.thread import STATUS_ARCHIVE | |||
6 |
|
7 | |||
7 |
|
8 | |||
8 | class PostTests(TestCase): |
|
9 | class PostTests(TestCase): | |
9 |
|
10 | |||
10 | def _create_post(self): |
|
11 | def _create_post(self): | |
11 | tag, created = Tag.objects.get_or_create(name='test_tag') |
|
12 | tag, created = Tag.objects.get_or_create(name='test_tag') | |
12 | return Post.objects.create_post(title='title', text='text', |
|
13 | return Post.objects.create_post(title='title', text='text', | |
13 | tags=[tag]) |
|
14 | tags=[tag]) | |
14 |
|
15 | |||
15 | def test_post_add(self): |
|
16 | def test_post_add(self): | |
16 | """Test adding post""" |
|
17 | """Test adding post""" | |
17 |
|
18 | |||
18 | post = self._create_post() |
|
19 | post = self._create_post() | |
19 |
|
20 | |||
20 | self.assertIsNotNone(post, 'No post was created.') |
|
21 | self.assertIsNotNone(post, 'No post was created.') | |
21 | self.assertEqual('test_tag', post.get_thread().tags.all()[0].name, |
|
22 | self.assertEqual('test_tag', post.get_thread().tags.all()[0].name, | |
22 | 'No tags were added to the post.') |
|
23 | 'No tags were added to the post.') | |
23 |
|
24 | |||
24 | def test_delete_post(self): |
|
25 | def test_delete_post(self): | |
25 | """Test post deletion""" |
|
26 | """Test post deletion""" | |
26 |
|
27 | |||
27 | post = self._create_post() |
|
28 | post = self._create_post() | |
28 | post_id = post.id |
|
29 | post_id = post.id | |
29 |
|
30 | |||
30 | post.delete() |
|
31 | post.delete() | |
31 |
|
32 | |||
32 | self.assertFalse(Post.objects.filter(id=post_id).exists()) |
|
33 | self.assertFalse(Post.objects.filter(id=post_id).exists()) | |
33 |
|
34 | |||
34 | def test_delete_thread(self): |
|
35 | def test_delete_thread(self): | |
35 | """Test thread deletion""" |
|
36 | """Test thread deletion""" | |
36 |
|
37 | |||
37 | opening_post = self._create_post() |
|
38 | opening_post = self._create_post() | |
38 | thread = opening_post.get_thread() |
|
39 | thread = opening_post.get_thread() | |
39 | reply = Post.objects.create_post("", "", thread=thread) |
|
40 | reply = Post.objects.create_post("", "", thread=thread) | |
40 |
|
41 | |||
41 | thread.delete() |
|
42 | thread.delete() | |
42 |
|
43 | |||
43 | self.assertFalse(Post.objects.filter(id=reply.id).exists(), |
|
44 | self.assertFalse(Post.objects.filter(id=reply.id).exists(), | |
44 | 'Reply was not deleted with the thread.') |
|
45 | 'Reply was not deleted with the thread.') | |
45 | self.assertFalse(Post.objects.filter(id=opening_post.id).exists(), |
|
46 | self.assertFalse(Post.objects.filter(id=opening_post.id).exists(), | |
46 | 'Opening post was not deleted with the thread.') |
|
47 | 'Opening post was not deleted with the thread.') | |
47 |
|
48 | |||
48 | def test_post_to_thread(self): |
|
49 | def test_post_to_thread(self): | |
49 | """Test adding post to a thread""" |
|
50 | """Test adding post to a thread""" | |
50 |
|
51 | |||
51 | op = self._create_post() |
|
52 | op = self._create_post() | |
52 | post = Post.objects.create_post("", "", thread=op.get_thread()) |
|
53 | post = Post.objects.create_post("", "", thread=op.get_thread()) | |
53 |
|
54 | |||
54 | self.assertIsNotNone(post, 'Reply to thread wasn\'t created') |
|
55 | self.assertIsNotNone(post, 'Reply to thread wasn\'t created') | |
55 | self.assertEqual(op.get_thread().last_edit_time, post.pub_time, |
|
56 | self.assertEqual(op.get_thread().last_edit_time, post.pub_time, | |
56 | 'Post\'s create time doesn\'t match thread last edit' |
|
57 | 'Post\'s create time doesn\'t match thread last edit' | |
57 | ' time') |
|
58 | ' time') | |
58 |
|
59 | |||
59 | def test_delete_posts_by_ip(self): |
|
60 | def test_delete_posts_by_ip(self): | |
60 | """Test deleting posts with the given ip""" |
|
61 | """Test deleting posts with the given ip""" | |
61 |
|
62 | |||
62 | post = self._create_post() |
|
63 | post = self._create_post() | |
63 | post_id = post.id |
|
64 | post_id = post.id | |
64 |
|
65 | |||
65 | Post.objects.delete_posts_by_ip('0.0.0.0') |
|
66 | Post.objects.delete_posts_by_ip('0.0.0.0') | |
66 |
|
67 | |||
67 | self.assertFalse(Post.objects.filter(id=post_id).exists()) |
|
68 | self.assertFalse(Post.objects.filter(id=post_id).exists()) | |
68 |
|
69 | |||
69 | def test_get_thread(self): |
|
70 | def test_get_thread(self): | |
70 | """Test getting all posts of a thread""" |
|
71 | """Test getting all posts of a thread""" | |
71 |
|
72 | |||
72 | opening_post = self._create_post() |
|
73 | opening_post = self._create_post() | |
73 |
|
74 | |||
74 | for i in range(2): |
|
75 | for i in range(2): | |
75 | Post.objects.create_post('title', 'text', |
|
76 | Post.objects.create_post('title', 'text', | |
76 | thread=opening_post.get_thread()) |
|
77 | thread=opening_post.get_thread()) | |
77 |
|
78 | |||
78 | thread = opening_post.get_thread() |
|
79 | thread = opening_post.get_thread() | |
79 |
|
80 | |||
80 | self.assertEqual(3, thread.get_replies().count()) |
|
81 | self.assertEqual(3, thread.get_replies().count()) | |
81 |
|
82 | |||
82 | def test_create_post_with_tag(self): |
|
83 | def test_create_post_with_tag(self): | |
83 | """Test adding tag to post""" |
|
84 | """Test adding tag to post""" | |
84 |
|
85 | |||
85 | tag = Tag.objects.create(name='test_tag') |
|
86 | tag = Tag.objects.create(name='test_tag') | |
86 | post = Post.objects.create_post(title='title', text='text', tags=[tag]) |
|
87 | post = Post.objects.create_post(title='title', text='text', tags=[tag]) | |
87 |
|
88 | |||
88 | thread = post.get_thread() |
|
89 | thread = post.get_thread() | |
89 | self.assertIsNotNone(post, 'Post not created') |
|
90 | self.assertIsNotNone(post, 'Post not created') | |
90 | self.assertTrue(tag in thread.tags.all(), 'Tag not added to thread') |
|
91 | self.assertTrue(tag in thread.tags.all(), 'Tag not added to thread') | |
91 |
|
92 | |||
92 | def test_thread_max_count(self): |
|
93 | def test_thread_max_count(self): | |
93 | """Test deletion of old posts when the max thread count is reached""" |
|
94 | """Test deletion of old posts when the max thread count is reached""" | |
94 |
|
95 | |||
95 | for i in range(settings.get_int('Messages', 'MaxThreadCount') + 1): |
|
96 | for i in range(settings.get_int('Messages', 'MaxThreadCount') + 1): | |
96 | self._create_post() |
|
97 | self._create_post() | |
97 |
|
98 | |||
98 | self.assertEqual(settings.get_int('Messages', 'MaxThreadCount'), |
|
99 | self.assertEqual(settings.get_int('Messages', 'MaxThreadCount'), | |
99 |
len(Thread.objects. |
|
100 | len(Thread.objects.exclude(status=STATUS_ARCHIVE))) | |
100 |
|
101 | |||
101 | def test_pages(self): |
|
102 | def test_pages(self): | |
102 | """Test that the thread list is properly split into pages""" |
|
103 | """Test that the thread list is properly split into pages""" | |
103 |
|
104 | |||
104 | for i in range(settings.get_int('Messages', 'MaxThreadCount')): |
|
105 | for i in range(settings.get_int('Messages', 'MaxThreadCount')): | |
105 | self._create_post() |
|
106 | self._create_post() | |
106 |
|
107 | |||
107 |
all_threads = Thread.objects. |
|
108 | all_threads = Thread.objects.exclude(status=STATUS_ARCHIVE) | |
108 |
|
109 | |||
109 |
paginator = Paginator(Thread.objects. |
|
110 | paginator = Paginator(Thread.objects.exclude(status=STATUS_ARCHIVE), | |
110 | settings.get_int('View', 'ThreadsPerPage')) |
|
111 | settings.get_int('View', 'ThreadsPerPage')) | |
111 | posts_in_second_page = paginator.page(2).object_list |
|
112 | posts_in_second_page = paginator.page(2).object_list | |
112 | first_post = posts_in_second_page[0] |
|
113 | first_post = posts_in_second_page[0] | |
113 |
|
114 | |||
114 | self.assertEqual(all_threads[settings.get_int('View', 'ThreadsPerPage')].id, |
|
115 | self.assertEqual(all_threads[settings.get_int('View', 'ThreadsPerPage')].id, | |
115 | first_post.id) |
|
116 | first_post.id) | |
116 |
|
117 | |||
117 | def test_reflinks(self): |
|
118 | def test_reflinks(self): | |
118 | """ |
|
119 | """ | |
119 | Tests that reflinks are parsed within post and connecting replies |
|
120 | Tests that reflinks are parsed within post and connecting replies | |
120 | to the replied posts. |
|
121 | to the replied posts. | |
121 |
|
122 | |||
122 | Local reflink example: [post]123[/post] |
|
123 | Local reflink example: [post]123[/post] | |
123 | Global reflink example: [post]key_type::key::123[/post] |
|
124 | Global reflink example: [post]key_type::key::123[/post] | |
124 | """ |
|
125 | """ | |
125 |
|
126 | |||
126 | key = KeyPair.objects.generate_key(primary=True) |
|
127 | key = KeyPair.objects.generate_key(primary=True) | |
127 |
|
128 | |||
128 | tag = Tag.objects.create(name='test_tag') |
|
129 | tag = Tag.objects.create(name='test_tag') | |
129 |
|
130 | |||
130 | post = Post.objects.create_post(title='', text='', tags=[tag]) |
|
131 | post = Post.objects.create_post(title='', text='', tags=[tag]) | |
131 | post_local_reflink = Post.objects.create_post(title='', |
|
132 | post_local_reflink = Post.objects.create_post(title='', | |
132 | text='[post]%d[/post]' % post.id, thread=post.get_thread()) |
|
133 | text='[post]%d[/post]' % post.id, thread=post.get_thread()) | |
133 |
|
134 | |||
134 | self.assertTrue(post_local_reflink in post.referenced_posts.all(), |
|
135 | self.assertTrue(post_local_reflink in post.referenced_posts.all(), | |
135 | 'Local reflink not connecting posts.') |
|
136 | 'Local reflink not connecting posts.') | |
136 |
|
137 | |||
137 |
|
138 | |||
138 | def test_thread_replies(self): |
|
139 | def test_thread_replies(self): | |
139 | """ |
|
140 | """ | |
140 | Tests that the replies can be queried from a thread in all possible |
|
141 | Tests that the replies can be queried from a thread in all possible | |
141 | ways. |
|
142 | ways. | |
142 | """ |
|
143 | """ | |
143 |
|
144 | |||
144 | tag = Tag.objects.create(name='test_tag') |
|
145 | tag = Tag.objects.create(name='test_tag') | |
145 | opening_post = Post.objects.create_post(title='title', text='text', |
|
146 | opening_post = Post.objects.create_post(title='title', text='text', | |
146 | tags=[tag]) |
|
147 | tags=[tag]) | |
147 | thread = opening_post.get_thread() |
|
148 | thread = opening_post.get_thread() | |
148 |
|
149 | |||
149 | Post.objects.create_post(title='title', text='text', thread=thread) |
|
150 | Post.objects.create_post(title='title', text='text', thread=thread) | |
150 | Post.objects.create_post(title='title', text='text', thread=thread) |
|
151 | Post.objects.create_post(title='title', text='text', thread=thread) | |
151 |
|
152 | |||
152 | replies = thread.get_replies() |
|
153 | replies = thread.get_replies() | |
153 | self.assertTrue(len(replies) > 0, 'No replies found for thread.') |
|
154 | self.assertTrue(len(replies) > 0, 'No replies found for thread.') | |
154 |
|
155 | |||
155 | replies = thread.get_replies(view_fields_only=True) |
|
156 | replies = thread.get_replies(view_fields_only=True) | |
156 | self.assertTrue(len(replies) > 0, |
|
157 | self.assertTrue(len(replies) > 0, | |
157 | 'No replies found for thread with view fields only.') |
|
158 | 'No replies found for thread with view fields only.') | |
158 |
|
159 | |||
159 | def test_bumplimit(self): |
|
160 | def test_bumplimit(self): | |
160 | """ |
|
161 | """ | |
161 | Tests that the thread bumpable status is changed and post uids and |
|
162 | Tests that the thread bumpable status is changed and post uids and | |
162 | last update times are updated across all post threads. |
|
163 | last update times are updated across all post threads. | |
163 | """ |
|
164 | """ | |
164 |
|
165 | |||
165 | op1 = Post.objects.create_post(title='title', text='text') |
|
166 | op1 = Post.objects.create_post(title='title', text='text') | |
166 | op2 = Post.objects.create_post(title='title', text='text') |
|
167 | op2 = Post.objects.create_post(title='title', text='text') | |
167 |
|
168 | |||
168 | thread1 = op1.get_thread() |
|
169 | thread1 = op1.get_thread() | |
169 | thread1.max_posts = 5 |
|
170 | thread1.max_posts = 5 | |
170 | thread1.save() |
|
171 | thread1.save() | |
171 |
|
172 | |||
172 | uid_1 = op1.uid |
|
173 | uid_1 = op1.uid | |
173 | uid_2 = op2.uid |
|
174 | uid_2 = op2.uid | |
174 |
|
175 | |||
175 | # Create multi reply |
|
176 | # Create multi reply | |
176 | Post.objects.create_post( |
|
177 | Post.objects.create_post( | |
177 | title='title', text='text', thread=thread1, |
|
178 | title='title', text='text', thread=thread1, | |
178 | opening_posts=[op1, op2]) |
|
179 | opening_posts=[op1, op2]) | |
179 | thread_update_time_2 = op2.get_thread().last_edit_time |
|
180 | thread_update_time_2 = op2.get_thread().last_edit_time | |
180 | for i in range(6): |
|
181 | for i in range(6): | |
181 | Post.objects.create_post(title='title', text='text', |
|
182 | Post.objects.create_post(title='title', text='text', | |
182 | thread=thread1) |
|
183 | thread=thread1) | |
183 |
|
184 | |||
184 | self.assertFalse(op1.get_thread().can_bump(), |
|
185 | self.assertFalse(op1.get_thread().can_bump(), | |
185 | 'Thread is bumpable when it should not be.') |
|
186 | 'Thread is bumpable when it should not be.') | |
186 | self.assertTrue(op2.get_thread().can_bump(), |
|
187 | self.assertTrue(op2.get_thread().can_bump(), | |
187 | 'Thread is not bumpable when it should be.') |
|
188 | 'Thread is not bumpable when it should be.') | |
188 | self.assertNotEqual( |
|
189 | self.assertNotEqual( | |
189 | uid_1, Post.objects.get(id=op1.id).uid, |
|
190 | uid_1, Post.objects.get(id=op1.id).uid, | |
190 | 'UID of the first OP should be changed but it is not.') |
|
191 | 'UID of the first OP should be changed but it is not.') | |
191 | self.assertEqual( |
|
192 | self.assertEqual( | |
192 | uid_2, Post.objects.get(id=op2.id).uid, |
|
193 | uid_2, Post.objects.get(id=op2.id).uid, | |
193 | 'UID of the first OP should not be changed but it is.') |
|
194 | 'UID of the first OP should not be changed but it is.') | |
194 |
|
195 | |||
195 | self.assertNotEqual( |
|
196 | self.assertNotEqual( | |
196 | thread_update_time_2, |
|
197 | thread_update_time_2, | |
197 | Thread.objects.get(id=op2.get_thread().id).last_edit_time, |
|
198 | Thread.objects.get(id=op2.get_thread().id).last_edit_time, | |
198 | 'Thread last update time should change when the other thread ' |
|
199 | 'Thread last update time should change when the other thread ' | |
199 | 'changes status.') |
|
200 | 'changes status.') |
@@ -1,48 +1,46 | |||||
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 = { |
|
11 | EXCLUDED_VIEWS = { | |
12 | 'banned', |
|
12 | 'banned', | |
13 | 'get_thread_diff', |
|
13 | 'get_thread_diff', | |
14 | 'api_sync_pull', |
|
14 | 'api_sync_pull', | |
15 | } |
|
15 | } | |
16 |
|
16 | |||
17 |
|
17 | |||
18 | class ViewTest(TestCase): |
|
18 | class ViewTest(TestCase): | |
19 |
|
19 | |||
20 | def test_all_views(self): |
|
20 | def test_all_views(self): | |
21 | """ |
|
21 | """ | |
22 | Try opening all views defined in ulrs.py that don't need additional |
|
22 | Try opening all views defined in ulrs.py that don't need additional | |
23 | parameters |
|
23 | parameters | |
24 | """ |
|
24 | """ | |
25 |
|
25 | |||
26 | client = Client() |
|
26 | client = Client() | |
27 | for url in urls.urlpatterns: |
|
27 | for url in urls.urlpatterns: | |
28 | try: |
|
28 | try: | |
29 | view_name = url.name |
|
29 | view_name = url.name | |
30 | if view_name in EXCLUDED_VIEWS: |
|
30 | if view_name in EXCLUDED_VIEWS: | |
31 | logger.debug('View {} is excluded.'.format(view_name)) |
|
31 | logger.debug('View {} is excluded.'.format(view_name)) | |
32 | continue |
|
32 | continue | |
33 |
|
33 | |||
34 | logger.debug('Testing view %s' % view_name) |
|
34 | logger.debug('Testing view %s' % view_name) | |
35 |
|
35 | |||
36 | try: |
|
36 | try: | |
37 | response = client.get(reverse(view_name)) |
|
37 | response = client.get(reverse(view_name)) | |
38 |
|
38 | |||
39 | self.assertEqual(HTTP_CODE_OK, response.status_code, |
|
39 | self.assertEqual(HTTP_CODE_OK, response.status_code, | |
40 | 'View not opened: {}'.format(view_name)) |
|
40 | 'View not opened: {}'.format(view_name)) | |
41 | except NoReverseMatch: |
|
41 | except NoReverseMatch: | |
42 | # This view just needs additional arguments |
|
42 | # This view just needs additional arguments | |
43 | pass |
|
43 | pass | |
44 | except Exception as e: |
|
|||
45 | self.fail('Got exception %s at %s view' % (e, view_name)) |
|
|||
46 | except AttributeError: |
|
44 | except AttributeError: | |
47 | # This is normal, some views do not have names |
|
45 | # This is normal, some views do not have names | |
48 | pass |
|
46 | pass |
@@ -1,92 +1,96 | |||||
1 | from django.conf.urls import patterns, url |
|
1 | from django.conf.urls import patterns, url | |
2 | from django.views.i18n import javascript_catalog |
|
2 | #from django.views.i18n import javascript_catalog | |
3 |
|
3 | |||
4 | from boards import views |
|
4 | from boards import views | |
5 | from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed |
|
5 | from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed | |
6 | from boards.views import api, tag_threads, all_threads, \ |
|
6 | from boards.views import api, tag_threads, all_threads, \ | |
7 | settings, all_tags, feed |
|
7 | settings, all_tags, feed | |
8 | from boards.views.authors import AuthorsView |
|
8 | from boards.views.authors import AuthorsView | |
9 | from boards.views.notifications import NotificationView |
|
9 | from boards.views.notifications import NotificationView | |
10 | from boards.views.search import BoardSearchView |
|
10 | from boards.views.search import BoardSearchView | |
11 | from boards.views.static import StaticPageView |
|
11 | from boards.views.static import StaticPageView | |
12 | from boards.views.preview import PostPreviewView |
|
12 | from boards.views.preview import PostPreviewView | |
13 | from boards.views.sync import get_post_sync_data, response_get, response_pull |
|
13 | from boards.views.sync import get_post_sync_data, response_get, response_pull | |
14 | from boards.views.random import RandomImageView |
|
14 | from boards.views.random import RandomImageView | |
|
15 | from boards.views.tag_gallery import TagGalleryView | |||
|
16 | from boards.views.translation import cached_javascript_catalog | |||
15 |
|
17 | |||
16 |
|
18 | |||
17 | js_info_dict = { |
|
19 | js_info_dict = { | |
18 | 'packages': ('boards',), |
|
20 | 'packages': ('boards',), | |
19 | } |
|
21 | } | |
20 |
|
22 | |||
21 | urlpatterns = patterns('', |
|
23 | urlpatterns = patterns('', | |
22 | # /boards/ |
|
24 | # /boards/ | |
23 | url(r'^$', all_threads.AllThreadsView.as_view(), name='index'), |
|
25 | url(r'^$', all_threads.AllThreadsView.as_view(), name='index'), | |
24 |
|
26 | |||
25 | # /boards/tag/tag_name/ |
|
27 | # /boards/tag/tag_name/ | |
26 | url(r'^tag/(?P<tag_name>\w+)/$', tag_threads.TagView.as_view(), |
|
28 | url(r'^tag/(?P<tag_name>\w+)/$', tag_threads.TagView.as_view(), | |
27 | name='tag'), |
|
29 | name='tag'), | |
28 |
|
30 | |||
29 | # /boards/thread/ |
|
31 | # /boards/thread/ | |
30 | url(r'^thread/(?P<post_id>\d+)/$', views.thread.NormalThreadView.as_view(), |
|
32 | url(r'^thread/(?P<post_id>\d+)/$', views.thread.NormalThreadView.as_view(), | |
31 | name='thread'), |
|
33 | name='thread'), | |
32 | url(r'^thread/(?P<post_id>\d+)/mode/gallery/$', views.thread.GalleryThreadView.as_view(), |
|
34 | url(r'^thread/(?P<post_id>\d+)/mode/gallery/$', views.thread.GalleryThreadView.as_view(), | |
33 | name='thread_gallery'), |
|
35 | name='thread_gallery'), | |
34 | url(r'^thread/(?P<post_id>\d+)/mode/tree/$', views.thread.TreeThreadView.as_view(), |
|
36 | url(r'^thread/(?P<post_id>\d+)/mode/tree/$', views.thread.TreeThreadView.as_view(), | |
35 | name='thread_tree'), |
|
37 | name='thread_tree'), | |
36 | # /feed/ |
|
38 | # /feed/ | |
37 | url(r'^feed/$', views.feed.FeedView.as_view(), name='feed'), |
|
39 | url(r'^feed/$', views.feed.FeedView.as_view(), name='feed'), | |
38 |
|
40 | |||
39 | url(r'^settings/$', settings.SettingsView.as_view(), name='settings'), |
|
41 | url(r'^settings/$', settings.SettingsView.as_view(), name='settings'), | |
40 | url(r'^tags/(?P<query>\w+)?/?$', all_tags.AllTagsView.as_view(), name='tags'), |
|
42 | url(r'^tags/(?P<query>\w+)?/?$', all_tags.AllTagsView.as_view(), name='tags'), | |
41 | url(r'^authors/$', AuthorsView.as_view(), name='authors'), |
|
43 | url(r'^authors/$', AuthorsView.as_view(), name='authors'), | |
42 |
|
44 | |||
43 | url(r'^banned/$', views.banned.BannedView.as_view(), name='banned'), |
|
45 | url(r'^banned/$', views.banned.BannedView.as_view(), name='banned'), | |
44 | url(r'^staticpage/(?P<name>\w+)/$', StaticPageView.as_view(), |
|
46 | url(r'^staticpage/(?P<name>\w+)/$', StaticPageView.as_view(), | |
45 | name='staticpage'), |
|
47 | name='staticpage'), | |
46 |
|
48 | |||
47 | url(r'^random/$', RandomImageView.as_view(), name='random'), |
|
49 | url(r'^random/$', RandomImageView.as_view(), name='random'), | |
|
50 | url(r'^tag/(?P<tag_name>\w+)/gallery/$', TagGalleryView.as_view(), name='tag_gallery'), | |||
48 |
|
51 | |||
49 | # RSS feeds |
|
52 | # RSS feeds | |
50 | url(r'^rss/$', AllThreadsFeed()), |
|
53 | url(r'^rss/$', AllThreadsFeed()), | |
51 | url(r'^page/(?P<page>\d+)/rss/$', AllThreadsFeed()), |
|
54 | url(r'^page/(?P<page>\d+)/rss/$', AllThreadsFeed()), | |
52 | url(r'^tag/(?P<tag_name>\w+)/rss/$', TagThreadsFeed()), |
|
55 | url(r'^tag/(?P<tag_name>\w+)/rss/$', TagThreadsFeed()), | |
53 | url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/rss/$', TagThreadsFeed()), |
|
56 | url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/rss/$', TagThreadsFeed()), | |
54 | url(r'^thread/(?P<post_id>\d+)/rss/$', ThreadPostsFeed()), |
|
57 | url(r'^thread/(?P<post_id>\d+)/rss/$', ThreadPostsFeed()), | |
55 |
|
58 | |||
56 | # i18n |
|
59 | # i18n | |
57 | url(r'^jsi18n/$', javascript_catalog, js_info_dict, |
|
60 | url(r'^jsi18n/$', cached_javascript_catalog, js_info_dict, | |
58 | name='js_info_dict'), |
|
61 | name='js_info_dict'), | |
59 |
|
62 | |||
60 | # API |
|
63 | # API | |
61 | url(r'^api/post/(?P<post_id>\d+)/$', api.get_post, name="get_post"), |
|
64 | url(r'^api/post/(?P<post_id>\d+)/$', api.get_post, name="get_post"), | |
62 | url(r'^api/diff_thread/$', api.api_get_threaddiff, name="get_thread_diff"), |
|
65 | url(r'^api/diff_thread/$', api.api_get_threaddiff, name="get_thread_diff"), | |
63 | url(r'^api/threads/(?P<count>\w+)/$', api.api_get_threads, |
|
66 | url(r'^api/threads/(?P<count>\w+)/$', api.api_get_threads, | |
64 | name='get_threads'), |
|
67 | name='get_threads'), | |
65 | url(r'^api/tags/$', api.api_get_tags, name='get_tags'), |
|
68 | url(r'^api/tags/$', api.api_get_tags, name='get_tags'), | |
66 | url(r'^api/thread/(?P<opening_post_id>\w+)/$', api.api_get_thread_posts, |
|
69 | url(r'^api/thread/(?P<opening_post_id>\w+)/$', api.api_get_thread_posts, | |
67 | name='get_thread'), |
|
70 | name='get_thread'), | |
68 | url(r'^api/add_post/(?P<opening_post_id>\w+)/$', api.api_add_post, |
|
71 | url(r'^api/add_post/(?P<opening_post_id>\w+)/$', api.api_add_post, | |
69 | name='add_post'), |
|
72 | name='add_post'), | |
70 | url(r'^api/notifications/(?P<username>\w+)/$', api.api_get_notifications, |
|
73 | url(r'^api/notifications/(?P<username>\w+)/$', api.api_get_notifications, | |
71 | name='api_notifications'), |
|
74 | name='api_notifications'), | |
72 | url(r'^api/preview/$', api.api_get_preview, name='preview'), |
|
75 | url(r'^api/preview/$', api.api_get_preview, name='preview'), | |
73 | url(r'^api/new_posts/$', api.api_get_new_posts, name='new_posts'), |
|
76 | url(r'^api/new_posts/$', api.api_get_new_posts, name='new_posts'), | |
74 |
|
77 | |||
75 | # Sync protocol API |
|
78 | # Sync protocol API | |
76 | url(r'^api/sync/pull/$', response_pull, name='api_sync_pull'), |
|
79 | url(r'^api/sync/pull/$', response_pull, name='api_sync_pull'), | |
77 | url(r'^api/sync/get/$', response_get, name='api_sync_pull'), |
|
80 | url(r'^api/sync/get/$', response_get, name='api_sync_pull'), | |
78 | # TODO 'get' request |
|
81 | # TODO 'get' request | |
79 |
|
82 | |||
80 | # Search |
|
83 | # Search | |
81 | url(r'^search/$', BoardSearchView.as_view(), name='search'), |
|
84 | url(r'^search/$', BoardSearchView.as_view(), name='search'), | |
82 |
|
85 | |||
83 | # Notifications |
|
86 | # Notifications | |
84 | url(r'^notifications/(?P<username>\w+)$', NotificationView.as_view(), name='notifications'), |
|
87 | url(r'^notifications/(?P<username>\w+)/$', NotificationView.as_view(), name='notifications'), | |
|
88 | url(r'^notifications/$', NotificationView.as_view(), name='notifications'), | |||
85 |
|
89 | |||
86 | # Post preview |
|
90 | # Post preview | |
87 | url(r'^preview/$', PostPreviewView.as_view(), name='preview'), |
|
91 | url(r'^preview/$', PostPreviewView.as_view(), name='preview'), | |
88 |
|
92 | |||
89 | url(r'^post_xml/(?P<post_id>\d+)$', get_post_sync_data, |
|
93 | url(r'^post_xml/(?P<post_id>\d+)$', get_post_sync_data, | |
90 | name='post_sync_data'), |
|
94 | name='post_sync_data'), | |
91 |
|
95 | |||
92 | ) |
|
96 | ) |
@@ -1,148 +1,144 | |||||
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 |
|
4 | import hashlib | |
5 | from random import random |
|
5 | from random import random | |
6 | import time |
|
6 | import time | |
7 | import hmac |
|
7 | import hmac | |
8 |
|
8 | |||
9 | from django.core.cache import cache |
|
9 | from django.core.cache import cache | |
10 | from django.db.models import Model |
|
10 | from django.db.models import Model | |
11 | from django import forms |
|
11 | from django import forms | |
|
12 | from django.template.defaultfilters import filesizeformat | |||
12 | from django.utils import timezone |
|
13 | from django.utils import timezone | |
13 | from django.utils.translation import ugettext_lazy as _ |
|
14 | from django.utils.translation import ugettext_lazy as _ | |
14 | import magic |
|
15 | import magic | |
15 | from portage import os |
|
16 | from portage import os | |
16 |
|
17 | |||
17 | import boards |
|
18 | import boards | |
18 | from boards.settings import get_bool |
|
19 | from boards.settings import get_bool | |
19 | from neboard import settings |
|
20 | from neboard import settings | |
20 |
|
21 | |||
21 | CACHE_KEY_DELIMITER = '_' |
|
22 | CACHE_KEY_DELIMITER = '_' | |
22 | PERMISSION_MODERATE = 'moderation' |
|
|||
23 |
|
23 | |||
24 | HTTP_FORWARDED = 'HTTP_X_FORWARDED_FOR' |
|
24 | HTTP_FORWARDED = 'HTTP_X_FORWARDED_FOR' | |
25 | META_REMOTE_ADDR = 'REMOTE_ADDR' |
|
25 | META_REMOTE_ADDR = 'REMOTE_ADDR' | |
26 |
|
26 | |||
27 | SETTING_MESSAGES = 'Messages' |
|
27 | SETTING_MESSAGES = 'Messages' | |
28 | SETTING_ANON_MODE = 'AnonymousMode' |
|
28 | SETTING_ANON_MODE = 'AnonymousMode' | |
29 |
|
29 | |||
30 | ANON_IP = '127.0.0.1' |
|
30 | ANON_IP = '127.0.0.1' | |
31 |
|
31 | |||
32 | UPLOAD_DIRS ={ |
|
32 | UPLOAD_DIRS ={ | |
33 | 'PostImage': 'images/', |
|
33 | 'PostImage': 'images/', | |
34 | 'Attachment': 'files/', |
|
34 | 'Attachment': 'files/', | |
35 | } |
|
35 | } | |
36 | FILE_EXTENSION_DELIMITER = '.' |
|
36 | FILE_EXTENSION_DELIMITER = '.' | |
37 |
|
37 | |||
38 |
|
38 | |||
39 | def is_anonymous_mode(): |
|
39 | def is_anonymous_mode(): | |
40 | return get_bool(SETTING_MESSAGES, SETTING_ANON_MODE) |
|
40 | return get_bool(SETTING_MESSAGES, SETTING_ANON_MODE) | |
41 |
|
41 | |||
42 |
|
42 | |||
43 | def get_client_ip(request): |
|
43 | def get_client_ip(request): | |
44 | if is_anonymous_mode(): |
|
44 | if is_anonymous_mode(): | |
45 | ip = ANON_IP |
|
45 | ip = ANON_IP | |
46 | else: |
|
46 | else: | |
47 | x_forwarded_for = request.META.get(HTTP_FORWARDED) |
|
47 | x_forwarded_for = request.META.get(HTTP_FORWARDED) | |
48 | if x_forwarded_for: |
|
48 | if x_forwarded_for: | |
49 | ip = x_forwarded_for.split(',')[-1].strip() |
|
49 | ip = x_forwarded_for.split(',')[-1].strip() | |
50 | else: |
|
50 | else: | |
51 | ip = request.META.get(META_REMOTE_ADDR) |
|
51 | ip = request.META.get(META_REMOTE_ADDR) | |
52 | return ip |
|
52 | return ip | |
53 |
|
53 | |||
54 |
|
54 | |||
55 | # TODO The output format is not epoch because it includes microseconds |
|
55 | # TODO The output format is not epoch because it includes microseconds | |
56 | def datetime_to_epoch(datetime): |
|
56 | def datetime_to_epoch(datetime): | |
57 | return int(time.mktime(timezone.localtime( |
|
57 | return int(time.mktime(timezone.localtime( | |
58 | datetime,timezone.get_current_timezone()).timetuple()) |
|
58 | datetime,timezone.get_current_timezone()).timetuple()) | |
59 | * 1000000 + datetime.microsecond) |
|
59 | * 1000000 + datetime.microsecond) | |
60 |
|
60 | |||
61 |
|
61 | |||
62 | def get_websocket_token(user_id='', timestamp=''): |
|
62 | def get_websocket_token(user_id='', timestamp=''): | |
63 | """ |
|
63 | """ | |
64 | Create token to validate information provided by new connection. |
|
64 | Create token to validate information provided by new connection. | |
65 | """ |
|
65 | """ | |
66 |
|
66 | |||
67 | sign = hmac.new(settings.CENTRIFUGE_PROJECT_SECRET.encode()) |
|
67 | sign = hmac.new(settings.CENTRIFUGE_PROJECT_SECRET.encode()) | |
68 | sign.update(settings.CENTRIFUGE_PROJECT_ID.encode()) |
|
68 | sign.update(settings.CENTRIFUGE_PROJECT_ID.encode()) | |
69 | sign.update(user_id.encode()) |
|
69 | sign.update(user_id.encode()) | |
70 | sign.update(timestamp.encode()) |
|
70 | sign.update(timestamp.encode()) | |
71 | token = sign.hexdigest() |
|
71 | token = sign.hexdigest() | |
72 |
|
72 | |||
73 | return token |
|
73 | return token | |
74 |
|
74 | |||
75 |
|
75 | |||
|
76 | # TODO Test this carefully | |||
76 | def cached_result(key_method=None): |
|
77 | def cached_result(key_method=None): | |
77 | """ |
|
78 | """ | |
78 | Caches method result in the Django's cache system, persisted by object name, |
|
79 | Caches method result in the Django's cache system, persisted by object name, | |
79 |
object name |
|
80 | object name, model id if object is a Django model, args and kwargs if any. | |
80 | """ |
|
81 | """ | |
81 | def _cached_result(function): |
|
82 | def _cached_result(function): | |
82 | def inner_func(obj, *args, **kwargs): |
|
83 | def inner_func(obj, *args, **kwargs): | |
83 | # TODO Include method arguments to the cache key |
|
|||
84 | cache_key_params = [obj.__class__.__name__, function.__name__] |
|
84 | cache_key_params = [obj.__class__.__name__, function.__name__] | |
|
85 | ||||
|
86 | cache_key_params += args | |||
|
87 | for key, value in kwargs: | |||
|
88 | cache_key_params.append(key + ':' + value) | |||
|
89 | ||||
85 | if isinstance(obj, Model): |
|
90 | if isinstance(obj, Model): | |
86 | cache_key_params.append(str(obj.id)) |
|
91 | cache_key_params.append(str(obj.id)) | |
87 |
|
92 | |||
88 | if key_method is not None: |
|
93 | if key_method is not None: | |
89 | cache_key_params += [str(arg) for arg in key_method(obj)] |
|
94 | cache_key_params += [str(arg) for arg in key_method(obj)] | |
90 |
|
95 | |||
91 | cache_key = CACHE_KEY_DELIMITER.join(cache_key_params) |
|
96 | cache_key = CACHE_KEY_DELIMITER.join(cache_key_params) | |
92 |
|
97 | |||
93 | persisted_result = cache.get(cache_key) |
|
98 | persisted_result = cache.get(cache_key) | |
94 | if persisted_result is not None: |
|
99 | if persisted_result is not None: | |
95 | result = persisted_result |
|
100 | result = persisted_result | |
96 | else: |
|
101 | else: | |
97 | result = function(obj, *args, **kwargs) |
|
102 | result = function(obj, *args, **kwargs) | |
98 | cache.set(cache_key, result) |
|
103 | cache.set(cache_key, result) | |
99 |
|
104 | |||
100 | return result |
|
105 | return result | |
101 |
|
106 | |||
102 | return inner_func |
|
107 | return inner_func | |
103 | return _cached_result |
|
108 | return _cached_result | |
104 |
|
109 | |||
105 |
|
110 | |||
106 | def is_moderator(request): |
|
|||
107 | try: |
|
|||
108 | moderate = request.user.has_perm(PERMISSION_MODERATE) |
|
|||
109 | except AttributeError: |
|
|||
110 | moderate = False |
|
|||
111 |
|
||||
112 | return moderate |
|
|||
113 |
|
||||
114 |
|
||||
115 | def get_file_hash(file) -> str: |
|
111 | def get_file_hash(file) -> str: | |
116 | md5 = hashlib.md5() |
|
112 | md5 = hashlib.md5() | |
117 | for chunk in file.chunks(): |
|
113 | for chunk in file.chunks(): | |
118 | md5.update(chunk) |
|
114 | md5.update(chunk) | |
119 | return md5.hexdigest() |
|
115 | return md5.hexdigest() | |
120 |
|
116 | |||
121 |
|
117 | |||
122 | def validate_file_size(size: int): |
|
118 | def validate_file_size(size: int): | |
123 | max_size = boards.settings.get_int('Forms', 'MaxFileSize') |
|
119 | max_size = boards.settings.get_int('Forms', 'MaxFileSize') | |
124 | if size > max_size: |
|
120 | if size > max_size: | |
125 | raise forms.ValidationError( |
|
121 | raise forms.ValidationError( | |
126 |
_('File must be less than %s b |
|
122 | _('File must be less than %s but is %s.') | |
127 | % str(max_size)) |
|
123 | % (filesizeformat(max_size), filesizeformat(size))) | |
128 |
|
124 | |||
129 |
|
125 | |||
130 | def get_extension(filename): |
|
126 | def get_extension(filename): | |
131 | return filename.split(FILE_EXTENSION_DELIMITER)[-1:][0] |
|
127 | return filename.split(FILE_EXTENSION_DELIMITER)[-1:][0] | |
132 |
|
128 | |||
133 |
|
129 | |||
134 | def get_upload_filename(model_instance, old_filename): |
|
130 | def get_upload_filename(model_instance, old_filename): | |
135 | # TODO Use something other than random number in file name |
|
131 | # TODO Use something other than random number in file name | |
136 | extension = get_extension(old_filename) |
|
132 | extension = get_extension(old_filename) | |
137 | new_name = '{}{}.{}'.format( |
|
133 | new_name = '{}{}.{}'.format( | |
138 | str(int(time.mktime(time.gmtime()))), |
|
134 | str(int(time.mktime(time.gmtime()))), | |
139 | str(int(random() * 1000)), |
|
135 | str(int(random() * 1000)), | |
140 | extension) |
|
136 | extension) | |
141 |
|
137 | |||
142 | directory = UPLOAD_DIRS[type(model_instance).__name__] |
|
138 | directory = UPLOAD_DIRS[type(model_instance).__name__] | |
143 |
|
139 | |||
144 | return os.path.join(directory, new_name) |
|
140 | return os.path.join(directory, new_name) | |
145 |
|
141 | |||
146 |
|
142 | |||
147 | def get_file_mimetype(file) -> str: |
|
143 | def get_file_mimetype(file) -> str: | |
148 | return magic.from_buffer(file.chunks().__next__(), mime=True).decode() |
|
144 | return magic.from_buffer(file.chunks().__next__(), mime=True).decode() |
@@ -1,157 +1,163 | |||||
|
1 | from dbus.decorators import method | |||
1 | from django.core.urlresolvers import reverse |
|
2 | from django.core.urlresolvers import reverse | |
2 | from django.core.files import File |
|
3 | from django.core.files import File | |
3 | from django.core.files.temp import NamedTemporaryFile |
|
4 | from django.core.files.temp import NamedTemporaryFile | |
4 | from django.core.paginator import EmptyPage |
|
5 | from django.core.paginator import EmptyPage | |
5 | from django.db import transaction |
|
6 | from django.db import transaction | |
6 | from django.http import Http404 |
|
7 | from django.http import Http404 | |
7 | from django.shortcuts import render, redirect |
|
8 | from django.shortcuts import render, redirect | |
8 | import requests |
|
9 | import requests | |
|
10 | from django.utils.decorators import method_decorator | |||
|
11 | from django.views.decorators.csrf import csrf_protect | |||
9 |
|
12 | |||
10 | from boards import utils, settings |
|
13 | from boards import utils, settings | |
11 | from boards.abstracts.paginator import get_paginator |
|
14 | from boards.abstracts.paginator import get_paginator | |
12 | from boards.abstracts.settingsmanager import get_settings_manager |
|
15 | from boards.abstracts.settingsmanager import get_settings_manager | |
13 | from boards.forms import ThreadForm, PlainErrorList |
|
16 | from boards.forms import ThreadForm, PlainErrorList | |
14 | from boards.models import Post, Thread, Ban, Tag, PostImage, Banner |
|
17 | from boards.models import Post, Thread, Ban, Tag, PostImage, Banner | |
15 | from boards.views.banned import BannedView |
|
18 | from boards.views.banned import BannedView | |
16 | from boards.views.base import BaseBoardView, CONTEXT_FORM |
|
19 | from boards.views.base import BaseBoardView, CONTEXT_FORM | |
17 | from boards.views.posting_mixin import PostMixin |
|
20 | from boards.views.posting_mixin import PostMixin | |
18 |
|
21 | from boards.views.mixins import FileUploadMixin, PaginatedMixin | ||
19 |
|
22 | |||
20 | FORM_TAGS = 'tags' |
|
23 | FORM_TAGS = 'tags' | |
21 | FORM_TEXT = 'text' |
|
24 | FORM_TEXT = 'text' | |
22 | FORM_TITLE = 'title' |
|
25 | FORM_TITLE = 'title' | |
23 | FORM_IMAGE = 'image' |
|
26 | FORM_IMAGE = 'image' | |
24 | FORM_THREADS = 'threads' |
|
27 | FORM_THREADS = 'threads' | |
25 |
|
28 | |||
26 | TAG_DELIMITER = ' ' |
|
29 | TAG_DELIMITER = ' ' | |
27 |
|
30 | |||
28 | PARAMETER_CURRENT_PAGE = 'current_page' |
|
31 | PARAMETER_CURRENT_PAGE = 'current_page' | |
29 | PARAMETER_PAGINATOR = 'paginator' |
|
32 | PARAMETER_PAGINATOR = 'paginator' | |
30 | PARAMETER_THREADS = 'threads' |
|
33 | PARAMETER_THREADS = 'threads' | |
31 | PARAMETER_BANNERS = 'banners' |
|
34 | PARAMETER_BANNERS = 'banners' | |
32 | PARAMETER_ADDITIONAL = 'additional_params' |
|
35 | PARAMETER_ADDITIONAL = 'additional_params' | |
33 |
|
36 | PARAMETER_MAX_FILE_SIZE = 'max_file_size' | ||
34 |
PARAMETER_ |
|
37 | PARAMETER_RSS_URL = 'rss_url' | |
35 | PARAMETER_NEXT_LINK = 'next_page_link' |
|
|||
36 |
|
38 | |||
37 | TEMPLATE = 'boards/all_threads.html' |
|
39 | TEMPLATE = 'boards/all_threads.html' | |
38 | DEFAULT_PAGE = 1 |
|
40 | DEFAULT_PAGE = 1 | |
39 |
|
41 | |||
40 |
|
42 | |||
41 | class AllThreadsView(PostMixin, BaseBoardView): |
|
43 | class AllThreadsView(PostMixin, FileUploadMixin, BaseBoardView, PaginatedMixin): | |
42 |
|
44 | |||
43 | def __init__(self): |
|
45 | def __init__(self): | |
44 | self.settings_manager = None |
|
46 | self.settings_manager = None | |
45 | super(AllThreadsView, self).__init__() |
|
47 | super(AllThreadsView, self).__init__() | |
46 |
|
48 | |||
|
49 | @method_decorator(csrf_protect) | |||
47 | def get(self, request, form: ThreadForm=None): |
|
50 | def get(self, request, form: ThreadForm=None): | |
48 | page = request.GET.get('page', DEFAULT_PAGE) |
|
51 | page = request.GET.get('page', DEFAULT_PAGE) | |
49 |
|
52 | |||
50 | params = self.get_context_data(request=request) |
|
53 | params = self.get_context_data(request=request) | |
51 |
|
54 | |||
52 | if not form: |
|
55 | if not form: | |
53 | form = ThreadForm(error_class=PlainErrorList) |
|
56 | form = ThreadForm(error_class=PlainErrorList) | |
54 |
|
57 | |||
55 | self.settings_manager = get_settings_manager(request) |
|
58 | self.settings_manager = get_settings_manager(request) | |
56 |
|
59 | |||
57 | threads = self.get_threads() |
|
60 | threads = self.get_threads() | |
58 |
|
61 | |||
59 | order = request.GET.get('order', 'bump') |
|
62 | order = request.GET.get('order', 'bump') | |
60 | if order == 'bump': |
|
63 | if order == 'bump': | |
61 | threads = threads.order_by('-bump_time') |
|
64 | threads = threads.order_by('-bump_time') | |
62 | else: |
|
65 | else: | |
63 | threads = threads.filter(multi_replies__opening=True).order_by('-multi_replies__pub_time') |
|
66 | threads = threads.filter(multi_replies__opening=True).order_by('-multi_replies__pub_time') | |
64 |
|
67 | |||
65 | paginator = get_paginator(threads, |
|
68 | paginator = get_paginator(threads, | |
66 | settings.get_int('View', 'ThreadsPerPage')) |
|
69 | settings.get_int('View', 'ThreadsPerPage')) | |
67 | paginator.current_page = int(page) |
|
70 | paginator.current_page = int(page) | |
68 |
|
71 | |||
69 | try: |
|
72 | try: | |
70 | threads = paginator.page(page).object_list |
|
73 | threads = paginator.page(page).object_list | |
71 | except EmptyPage: |
|
74 | except EmptyPage: | |
72 | raise Http404() |
|
75 | raise Http404() | |
73 |
|
76 | |||
74 | params[PARAMETER_THREADS] = threads |
|
77 | params[PARAMETER_THREADS] = threads | |
75 | params[CONTEXT_FORM] = form |
|
78 | params[CONTEXT_FORM] = form | |
76 | params[PARAMETER_BANNERS] = Banner.objects.order_by('-id').all() |
|
79 | params[PARAMETER_BANNERS] = Banner.objects.order_by('-id').all() | |
|
80 | params[PARAMETER_MAX_FILE_SIZE] = self.get_max_upload_size() | |||
|
81 | params[PARAMETER_RSS_URL] = self.get_rss_url() | |||
77 |
|
82 | |||
78 | paginator.set_url(self.get_reverse_url(), request.GET.dict()) |
|
83 | paginator.set_url(self.get_reverse_url(), request.GET.dict()) | |
79 | self.get_page_context(paginator, params, page) |
|
84 | self.get_page_context(paginator, params, page) | |
80 |
|
85 | |||
81 | return render(request, TEMPLATE, params) |
|
86 | return render(request, TEMPLATE, params) | |
82 |
|
87 | |||
|
88 | @method_decorator(csrf_protect) | |||
83 | def post(self, request): |
|
89 | def post(self, request): | |
84 | form = ThreadForm(request.POST, request.FILES, |
|
90 | form = ThreadForm(request.POST, request.FILES, | |
85 | error_class=PlainErrorList) |
|
91 | error_class=PlainErrorList) | |
86 | form.session = request.session |
|
92 | form.session = request.session | |
87 |
|
93 | |||
88 | if form.is_valid(): |
|
94 | if form.is_valid(): | |
89 | return self.create_thread(request, form) |
|
95 | return self.create_thread(request, form) | |
90 | if form.need_to_ban: |
|
96 | if form.need_to_ban: | |
91 | # Ban user because he is suspected to be a bot |
|
97 | # Ban user because he is suspected to be a bot | |
92 | self._ban_current_user(request) |
|
98 | self._ban_current_user(request) | |
93 |
|
99 | |||
94 | return self.get(request, form) |
|
100 | return self.get(request, form) | |
95 |
|
101 | |||
96 | def get_page_context(self, paginator, params, page): |
|
102 | def get_page_context(self, paginator, params, page): | |
97 | """ |
|
103 | """ | |
98 | Get pagination context variables |
|
104 | Get pagination context variables | |
99 | """ |
|
105 | """ | |
100 |
|
106 | |||
101 | params[PARAMETER_PAGINATOR] = paginator |
|
107 | params[PARAMETER_PAGINATOR] = paginator | |
102 | current_page = paginator.page(int(page)) |
|
108 | current_page = paginator.page(int(page)) | |
103 | params[PARAMETER_CURRENT_PAGE] = current_page |
|
109 | params[PARAMETER_CURRENT_PAGE] = current_page | |
104 | if current_page.has_previous(): |
|
110 | self.set_page_urls(paginator, params) | |
105 | params[PARAMETER_PREV_LINK] = paginator.get_page_url( |
|
|||
106 | current_page.previous_page_number()) |
|
|||
107 | if current_page.has_next(): |
|
|||
108 | params[PARAMETER_NEXT_LINK] = paginator.get_page_url( |
|
|||
109 | current_page.next_page_number()) |
|
|||
110 |
|
111 | |||
111 | def get_reverse_url(self): |
|
112 | def get_reverse_url(self): | |
112 | return reverse('index') |
|
113 | return reverse('index') | |
113 |
|
114 | |||
114 | @transaction.atomic |
|
115 | @transaction.atomic | |
115 | def create_thread(self, request, form: ThreadForm, html_response=True): |
|
116 | def create_thread(self, request, form: ThreadForm, html_response=True): | |
116 | """ |
|
117 | """ | |
117 | Creates a new thread with an opening post. |
|
118 | Creates a new thread with an opening post. | |
118 | """ |
|
119 | """ | |
119 |
|
120 | |||
120 | ip = utils.get_client_ip(request) |
|
121 | ip = utils.get_client_ip(request) | |
121 | is_banned = Ban.objects.filter(ip=ip).exists() |
|
122 | is_banned = Ban.objects.filter(ip=ip).exists() | |
122 |
|
123 | |||
123 | if is_banned: |
|
124 | if is_banned: | |
124 | if html_response: |
|
125 | if html_response: | |
125 | return redirect(BannedView().as_view()) |
|
126 | return redirect(BannedView().as_view()) | |
126 | else: |
|
127 | else: | |
127 | return |
|
128 | return | |
128 |
|
129 | |||
129 | data = form.cleaned_data |
|
130 | data = form.cleaned_data | |
130 |
|
131 | |||
131 | title = form.get_title() |
|
132 | title = form.get_title() | |
132 | text = data[FORM_TEXT] |
|
133 | text = data[FORM_TEXT] | |
133 | file = form.get_file() |
|
134 | file = form.get_file() | |
134 | threads = data[FORM_THREADS] |
|
135 | threads = data[FORM_THREADS] | |
135 |
|
136 | |||
136 | text = self._remove_invalid_links(text) |
|
137 | text = self._remove_invalid_links(text) | |
137 |
|
138 | |||
138 | tags = data[FORM_TAGS] |
|
139 | tags = data[FORM_TAGS] | |
|
140 | monochrome = form.is_monochrome() | |||
139 |
|
141 | |||
140 | post = Post.objects.create_post(title=title, text=text, file=file, |
|
142 | post = Post.objects.create_post(title=title, text=text, file=file, | |
141 | ip=ip, tags=tags, opening_posts=threads, |
|
143 | ip=ip, tags=tags, opening_posts=threads, | |
142 |
tripcode=form.get_tripcode() |
|
144 | tripcode=form.get_tripcode(), | |
|
145 | monochrome=monochrome) | |||
143 |
|
146 | |||
144 | # This is required to update the threads to which posts we have replied |
|
147 | # This is required to update the threads to which posts we have replied | |
145 | # when creating this one |
|
148 | # when creating this one | |
146 | post.notify_clients() |
|
149 | post.notify_clients() | |
147 |
|
150 | |||
148 | if html_response: |
|
151 | if html_response: | |
149 | return redirect(post.get_absolute_url()) |
|
152 | return redirect(post.get_absolute_url()) | |
150 |
|
153 | |||
151 | def get_threads(self): |
|
154 | def get_threads(self): | |
152 | """ |
|
155 | """ | |
153 | Gets list of threads that will be shown on a page. |
|
156 | Gets list of threads that will be shown on a page. | |
154 | """ |
|
157 | """ | |
155 |
|
158 | |||
156 | return Thread.objects\ |
|
159 | return Thread.objects\ | |
157 | .exclude(tags__in=self.settings_manager.get_hidden_tags()) |
|
160 | .exclude(tags__in=self.settings_manager.get_hidden_tags()) | |
|
161 | ||||
|
162 | def get_rss_url(self): | |||
|
163 | return self.get_reverse_url() + 'rss/' |
@@ -1,296 +1,295 | |||||
1 | from collections import OrderedDict |
|
|||
2 |
|
|
1 | import json | |
3 | import logging |
|
2 | import logging | |
4 |
|
3 | |||
5 | import xml.etree.ElementTree as ET |
|
4 | from django.core import serializers | |
6 |
|
||||
7 | from django.db import transaction |
|
5 | from django.db import transaction | |
8 | from django.db.models import Count |
|
|||
9 | from django.http import HttpResponse |
|
6 | from django.http import HttpResponse | |
10 | from django.shortcuts import get_object_or_404 |
|
7 | from django.shortcuts import get_object_or_404 | |
11 | from django.core import serializers |
|
8 | from django.views.decorators.csrf import csrf_protect | |
12 | from boards.abstracts.settingsmanager import get_settings_manager,\ |
|
|||
13 | FAV_THREAD_NO_UPDATES |
|
|||
14 |
|
9 | |||
|
10 | from boards.abstracts.settingsmanager import get_settings_manager | |||
15 | from boards.forms import PostForm, PlainErrorList |
|
11 | from boards.forms import PostForm, PlainErrorList | |
16 | from boards.models import Post, Thread, Tag, GlobalId |
|
12 | from boards.mdx_neboard import Parser | |
17 |
from boards.models |
|
13 | from boards.models import Post, Thread, Tag | |
|
14 | from boards.models.thread import STATUS_ARCHIVE | |||
|
15 | from boards.models.user import Notification | |||
18 | from boards.utils import datetime_to_epoch |
|
16 | from boards.utils import datetime_to_epoch | |
19 | from boards.views.thread import ThreadView |
|
17 | from boards.views.thread import ThreadView | |
20 | from boards.models.user import Notification |
|
|||
21 | from boards.mdx_neboard import Parser |
|
|||
22 |
|
||||
23 |
|
18 | |||
24 | __author__ = 'neko259' |
|
19 | __author__ = 'neko259' | |
25 |
|
20 | |||
26 | PARAMETER_TRUNCATED = 'truncated' |
|
21 | PARAMETER_TRUNCATED = 'truncated' | |
27 | PARAMETER_TAG = 'tag' |
|
22 | PARAMETER_TAG = 'tag' | |
28 | PARAMETER_OFFSET = 'offset' |
|
23 | PARAMETER_OFFSET = 'offset' | |
29 | PARAMETER_DIFF_TYPE = 'type' |
|
24 | PARAMETER_DIFF_TYPE = 'type' | |
30 | PARAMETER_POST = 'post' |
|
25 | PARAMETER_POST = 'post' | |
31 | PARAMETER_UPDATED = 'updated' |
|
26 | PARAMETER_UPDATED = 'updated' | |
32 | PARAMETER_LAST_UPDATE = 'last_update' |
|
27 | PARAMETER_LAST_UPDATE = 'last_update' | |
33 | PARAMETER_THREAD = 'thread' |
|
28 | PARAMETER_THREAD = 'thread' | |
34 | PARAMETER_UIDS = 'uids' |
|
29 | PARAMETER_UIDS = 'uids' | |
35 |
|
30 | |||
36 | DIFF_TYPE_HTML = 'html' |
|
31 | DIFF_TYPE_HTML = 'html' | |
37 | DIFF_TYPE_JSON = 'json' |
|
32 | DIFF_TYPE_JSON = 'json' | |
38 |
|
33 | |||
39 | STATUS_OK = 'ok' |
|
34 | STATUS_OK = 'ok' | |
40 | STATUS_ERROR = 'error' |
|
35 | STATUS_ERROR = 'error' | |
41 |
|
36 | |||
42 | logger = logging.getLogger(__name__) |
|
37 | logger = logging.getLogger(__name__) | |
43 |
|
38 | |||
44 |
|
39 | |||
45 | @transaction.atomic |
|
40 | @transaction.atomic | |
46 | def api_get_threaddiff(request): |
|
41 | def api_get_threaddiff(request): | |
47 | """ |
|
42 | """ | |
48 | Gets posts that were changed or added since time |
|
43 | Gets posts that were changed or added since time | |
49 | """ |
|
44 | """ | |
50 |
|
45 | |||
51 | thread_id = request.POST.get(PARAMETER_THREAD) |
|
46 | thread_id = request.POST.get(PARAMETER_THREAD) | |
52 |
uids_str = request.POST.get(PARAMETER_UIDS) |
|
47 | uids_str = request.POST.get(PARAMETER_UIDS) | |
53 | uids = uids_str.split(' ') |
|
48 | ||
|
49 | if not thread_id or not uids_str: | |||
|
50 | return HttpResponse(content='Invalid request.') | |||
|
51 | ||||
|
52 | uids = uids_str.strip().split(' ') | |||
54 |
|
53 | |||
55 | opening_post = get_object_or_404(Post, id=thread_id) |
|
54 | opening_post = get_object_or_404(Post, id=thread_id) | |
56 | thread = opening_post.get_thread() |
|
55 | thread = opening_post.get_thread() | |
57 |
|
56 | |||
58 | json_data = { |
|
57 | json_data = { | |
59 | PARAMETER_UPDATED: [], |
|
58 | PARAMETER_UPDATED: [], | |
60 | PARAMETER_LAST_UPDATE: None, # TODO Maybe this can be removed already? |
|
59 | PARAMETER_LAST_UPDATE: None, # TODO Maybe this can be removed already? | |
61 | } |
|
60 | } | |
62 | posts = Post.objects.filter(threads__in=[thread]).exclude(uid__in=uids) |
|
61 | posts = Post.objects.filter(threads__in=[thread]).exclude(uid__in=uids) | |
63 |
|
62 | |||
64 | diff_type = request.GET.get(PARAMETER_DIFF_TYPE, DIFF_TYPE_HTML) |
|
63 | diff_type = request.GET.get(PARAMETER_DIFF_TYPE, DIFF_TYPE_HTML) | |
65 |
|
64 | |||
66 | for post in posts: |
|
65 | for post in posts: | |
67 | json_data[PARAMETER_UPDATED].append(post.get_post_data( |
|
66 | json_data[PARAMETER_UPDATED].append(post.get_post_data( | |
68 | format_type=diff_type, request=request)) |
|
67 | format_type=diff_type, request=request)) | |
69 | json_data[PARAMETER_LAST_UPDATE] = str(thread.last_edit_time) |
|
68 | json_data[PARAMETER_LAST_UPDATE] = str(thread.last_edit_time) | |
70 |
|
69 | |||
71 | # If the tag is favorite, update the counter |
|
70 | # If the tag is favorite, update the counter | |
72 | settings_manager = get_settings_manager(request) |
|
71 | settings_manager = get_settings_manager(request) | |
73 | favorite = settings_manager.thread_is_fav(opening_post) |
|
72 | favorite = settings_manager.thread_is_fav(opening_post) | |
74 | if favorite: |
|
73 | if favorite: | |
75 | settings_manager.add_or_read_fav_thread(opening_post) |
|
74 | settings_manager.add_or_read_fav_thread(opening_post) | |
76 |
|
75 | |||
77 | return HttpResponse(content=json.dumps(json_data)) |
|
76 | return HttpResponse(content=json.dumps(json_data)) | |
78 |
|
77 | |||
79 |
|
78 | |||
|
79 | @csrf_protect | |||
80 | def api_add_post(request, opening_post_id): |
|
80 | def api_add_post(request, opening_post_id): | |
81 | """ |
|
81 | """ | |
82 | Adds a post and return the JSON response for it |
|
82 | Adds a post and return the JSON response for it | |
83 | """ |
|
83 | """ | |
84 |
|
84 | |||
85 | opening_post = get_object_or_404(Post, id=opening_post_id) |
|
85 | opening_post = get_object_or_404(Post, id=opening_post_id) | |
86 |
|
86 | |||
87 | logger.info('Adding post via api...') |
|
87 | logger.info('Adding post via api...') | |
88 |
|
88 | |||
89 | status = STATUS_OK |
|
89 | status = STATUS_OK | |
90 | errors = [] |
|
90 | errors = [] | |
91 |
|
91 | |||
92 | if request.method == 'POST': |
|
92 | if request.method == 'POST': | |
93 | form = PostForm(request.POST, request.FILES, error_class=PlainErrorList) |
|
93 | form = PostForm(request.POST, request.FILES, error_class=PlainErrorList) | |
94 | form.session = request.session |
|
94 | form.session = request.session | |
95 |
|
95 | |||
96 | if form.need_to_ban: |
|
96 | if form.need_to_ban: | |
97 | # Ban user because he is suspected to be a bot |
|
97 | # Ban user because he is suspected to be a bot | |
98 | # _ban_current_user(request) |
|
98 | # _ban_current_user(request) | |
99 | status = STATUS_ERROR |
|
99 | status = STATUS_ERROR | |
100 | if form.is_valid(): |
|
100 | if form.is_valid(): | |
101 | post = ThreadView().new_post(request, form, opening_post, |
|
101 | post = ThreadView().new_post(request, form, opening_post, | |
102 | html_response=False) |
|
102 | html_response=False) | |
103 | if not post: |
|
103 | if not post: | |
104 | status = STATUS_ERROR |
|
104 | status = STATUS_ERROR | |
105 | else: |
|
105 | else: | |
106 | logger.info('Added post #%d via api.' % post.id) |
|
106 | logger.info('Added post #%d via api.' % post.id) | |
107 | else: |
|
107 | else: | |
108 | status = STATUS_ERROR |
|
108 | status = STATUS_ERROR | |
109 | errors = form.as_json_errors() |
|
109 | errors = form.as_json_errors() | |
110 |
|
110 | |||
111 | response = { |
|
111 | response = { | |
112 | 'status': status, |
|
112 | 'status': status, | |
113 | 'errors': errors, |
|
113 | 'errors': errors, | |
114 | } |
|
114 | } | |
115 |
|
115 | |||
116 | return HttpResponse(content=json.dumps(response)) |
|
116 | return HttpResponse(content=json.dumps(response)) | |
117 |
|
117 | |||
118 |
|
118 | |||
119 | def get_post(request, post_id): |
|
119 | def get_post(request, post_id): | |
120 | """ |
|
120 | """ | |
121 | Gets the html of a post. Used for popups. Post can be truncated if used |
|
121 | Gets the html of a post. Used for popups. Post can be truncated if used | |
122 | in threads list with 'truncated' get parameter. |
|
122 | in threads list with 'truncated' get parameter. | |
123 | """ |
|
123 | """ | |
124 |
|
124 | |||
125 | post = get_object_or_404(Post, id=post_id) |
|
125 | post = get_object_or_404(Post, id=post_id) | |
126 | truncated = PARAMETER_TRUNCATED in request.GET |
|
126 | truncated = PARAMETER_TRUNCATED in request.GET | |
127 |
|
127 | |||
128 | return HttpResponse(content=post.get_view(truncated=truncated)) |
|
128 | return HttpResponse(content=post.get_view(truncated=truncated, need_op_data=True)) | |
129 |
|
129 | |||
130 |
|
130 | |||
131 | def api_get_threads(request, count): |
|
131 | def api_get_threads(request, count): | |
132 | """ |
|
132 | """ | |
133 | Gets the JSON thread opening posts list. |
|
133 | Gets the JSON thread opening posts list. | |
134 | Parameters that can be used for filtering: |
|
134 | Parameters that can be used for filtering: | |
135 | tag, offset (from which thread to get results) |
|
135 | tag, offset (from which thread to get results) | |
136 | """ |
|
136 | """ | |
137 |
|
137 | |||
138 | if PARAMETER_TAG in request.GET: |
|
138 | if PARAMETER_TAG in request.GET: | |
139 | tag_name = request.GET[PARAMETER_TAG] |
|
139 | tag_name = request.GET[PARAMETER_TAG] | |
140 | if tag_name is not None: |
|
140 | if tag_name is not None: | |
141 | tag = get_object_or_404(Tag, name=tag_name) |
|
141 | tag = get_object_or_404(Tag, name=tag_name) | |
142 |
threads = tag.get_threads(). |
|
142 | threads = tag.get_threads().exclude(status=STATUS_ARCHIVE) | |
143 | else: |
|
143 | else: | |
144 |
threads = Thread.objects. |
|
144 | threads = Thread.objects.exclude(status=STATUS_ARCHIVE) | |
145 |
|
145 | |||
146 | if PARAMETER_OFFSET in request.GET: |
|
146 | if PARAMETER_OFFSET in request.GET: | |
147 | offset = request.GET[PARAMETER_OFFSET] |
|
147 | offset = request.GET[PARAMETER_OFFSET] | |
148 | offset = int(offset) if offset is not None else 0 |
|
148 | offset = int(offset) if offset is not None else 0 | |
149 | else: |
|
149 | else: | |
150 | offset = 0 |
|
150 | offset = 0 | |
151 |
|
151 | |||
152 | threads = threads.order_by('-bump_time') |
|
152 | threads = threads.order_by('-bump_time') | |
153 | threads = threads[offset:offset + int(count)] |
|
153 | threads = threads[offset:offset + int(count)] | |
154 |
|
154 | |||
155 | opening_posts = [] |
|
155 | opening_posts = [] | |
156 | for thread in threads: |
|
156 | for thread in threads: | |
157 | opening_post = thread.get_opening_post() |
|
157 | opening_post = thread.get_opening_post() | |
158 |
|
158 | |||
159 | # TODO Add tags, replies and images count |
|
159 | # TODO Add tags, replies and images count | |
160 | post_data = opening_post.get_post_data(include_last_update=True) |
|
160 | post_data = opening_post.get_post_data(include_last_update=True) | |
161 |
post_data[' |
|
161 | post_data['status'] = thread.get_status() | |
162 | post_data['archived'] = thread.archived |
|
|||
163 |
|
162 | |||
164 | opening_posts.append(post_data) |
|
163 | opening_posts.append(post_data) | |
165 |
|
164 | |||
166 | return HttpResponse(content=json.dumps(opening_posts)) |
|
165 | return HttpResponse(content=json.dumps(opening_posts)) | |
167 |
|
166 | |||
168 |
|
167 | |||
169 | # TODO Test this |
|
168 | # TODO Test this | |
170 | def api_get_tags(request): |
|
169 | def api_get_tags(request): | |
171 | """ |
|
170 | """ | |
172 | Gets all tags or user tags. |
|
171 | Gets all tags or user tags. | |
173 | """ |
|
172 | """ | |
174 |
|
173 | |||
175 | # TODO Get favorite tags for the given user ID |
|
174 | # TODO Get favorite tags for the given user ID | |
176 |
|
175 | |||
177 | tags = Tag.objects.get_not_empty_tags() |
|
176 | tags = Tag.objects.get_not_empty_tags() | |
178 |
|
177 | |||
179 | term = request.GET.get('term') |
|
178 | term = request.GET.get('term') | |
180 | if term is not None: |
|
179 | if term is not None: | |
181 | tags = tags.filter(name__contains=term) |
|
180 | tags = tags.filter(name__contains=term) | |
182 |
|
181 | |||
183 | tag_names = [tag.name for tag in tags] |
|
182 | tag_names = [tag.name for tag in tags] | |
184 |
|
183 | |||
185 | return HttpResponse(content=json.dumps(tag_names)) |
|
184 | return HttpResponse(content=json.dumps(tag_names)) | |
186 |
|
185 | |||
187 |
|
186 | |||
188 | # TODO The result can be cached by the thread last update time |
|
187 | # TODO The result can be cached by the thread last update time | |
189 | # TODO Test this |
|
188 | # TODO Test this | |
190 | def api_get_thread_posts(request, opening_post_id): |
|
189 | def api_get_thread_posts(request, opening_post_id): | |
191 | """ |
|
190 | """ | |
192 | Gets the JSON array of thread posts |
|
191 | Gets the JSON array of thread posts | |
193 | """ |
|
192 | """ | |
194 |
|
193 | |||
195 | opening_post = get_object_or_404(Post, id=opening_post_id) |
|
194 | opening_post = get_object_or_404(Post, id=opening_post_id) | |
196 | thread = opening_post.get_thread() |
|
195 | thread = opening_post.get_thread() | |
197 | posts = thread.get_replies() |
|
196 | posts = thread.get_replies() | |
198 |
|
197 | |||
199 | json_data = { |
|
198 | json_data = { | |
200 | 'posts': [], |
|
199 | 'posts': [], | |
201 | 'last_update': None, |
|
200 | 'last_update': None, | |
202 | } |
|
201 | } | |
203 | json_post_list = [] |
|
202 | json_post_list = [] | |
204 |
|
203 | |||
205 | for post in posts: |
|
204 | for post in posts: | |
206 | json_post_list.append(post.get_post_data()) |
|
205 | json_post_list.append(post.get_post_data()) | |
207 | json_data['last_update'] = datetime_to_epoch(thread.last_edit_time) |
|
206 | json_data['last_update'] = datetime_to_epoch(thread.last_edit_time) | |
208 | json_data['posts'] = json_post_list |
|
207 | json_data['posts'] = json_post_list | |
209 |
|
208 | |||
210 | return HttpResponse(content=json.dumps(json_data)) |
|
209 | return HttpResponse(content=json.dumps(json_data)) | |
211 |
|
210 | |||
212 |
|
211 | |||
213 | def api_get_notifications(request, username): |
|
212 | def api_get_notifications(request, username): | |
214 | last_notification_id_str = request.GET.get('last', None) |
|
213 | last_notification_id_str = request.GET.get('last', None) | |
215 | last_id = int(last_notification_id_str) if last_notification_id_str is not None else None |
|
214 | last_id = int(last_notification_id_str) if last_notification_id_str is not None else None | |
216 |
|
215 | |||
217 | posts = Notification.objects.get_notification_posts(username=username, |
|
216 | posts = Notification.objects.get_notification_posts(usernames=username, | |
218 | last=last_id) |
|
217 | last=last_id) | |
219 |
|
218 | |||
220 | json_post_list = [] |
|
219 | json_post_list = [] | |
221 | for post in posts: |
|
220 | for post in posts: | |
222 | json_post_list.append(post.get_post_data()) |
|
221 | json_post_list.append(post.get_post_data()) | |
223 | return HttpResponse(content=json.dumps(json_post_list)) |
|
222 | return HttpResponse(content=json.dumps(json_post_list)) | |
224 |
|
223 | |||
225 |
|
224 | |||
226 | def api_get_post(request, post_id): |
|
225 | def api_get_post(request, post_id): | |
227 | """ |
|
226 | """ | |
228 | Gets the JSON of a post. This can be |
|
227 | Gets the JSON of a post. This can be | |
229 | used as and API for external clients. |
|
228 | used as and API for external clients. | |
230 | """ |
|
229 | """ | |
231 |
|
230 | |||
232 | post = get_object_or_404(Post, id=post_id) |
|
231 | post = get_object_or_404(Post, id=post_id) | |
233 |
|
232 | |||
234 | json = serializers.serialize("json", [post], fields=( |
|
233 | json = serializers.serialize("json", [post], fields=( | |
235 | "pub_time", "_text_rendered", "title", "text", "image", |
|
234 | "pub_time", "_text_rendered", "title", "text", "image", | |
236 | "image_width", "image_height", "replies", "tags" |
|
235 | "image_width", "image_height", "replies", "tags" | |
237 | )) |
|
236 | )) | |
238 |
|
237 | |||
239 | return HttpResponse(content=json) |
|
238 | return HttpResponse(content=json) | |
240 |
|
239 | |||
241 |
|
240 | |||
242 | def api_get_preview(request): |
|
241 | def api_get_preview(request): | |
243 | raw_text = request.POST['raw_text'] |
|
242 | raw_text = request.POST['raw_text'] | |
244 |
|
243 | |||
245 | parser = Parser() |
|
244 | parser = Parser() | |
246 | return HttpResponse(content=parser.parse(parser.preparse(raw_text))) |
|
245 | return HttpResponse(content=parser.parse(parser.preparse(raw_text))) | |
247 |
|
246 | |||
248 |
|
247 | |||
249 | def api_get_new_posts(request): |
|
248 | def api_get_new_posts(request): | |
250 | """ |
|
249 | """ | |
251 | Gets favorite threads and unread posts count. |
|
250 | Gets favorite threads and unread posts count. | |
252 | """ |
|
251 | """ | |
253 | posts = list() |
|
252 | posts = list() | |
254 |
|
253 | |||
255 | include_posts = 'include_posts' in request.GET |
|
254 | include_posts = 'include_posts' in request.GET | |
256 |
|
255 | |||
257 | settings_manager = get_settings_manager(request) |
|
256 | settings_manager = get_settings_manager(request) | |
258 | fav_threads = settings_manager.get_fav_threads() |
|
257 | fav_threads = settings_manager.get_fav_threads() | |
259 | fav_thread_ops = Post.objects.filter(id__in=fav_threads.keys())\ |
|
258 | fav_thread_ops = Post.objects.filter(id__in=fav_threads.keys())\ | |
260 | .order_by('-pub_time').prefetch_related('thread') |
|
259 | .order_by('-pub_time').prefetch_related('thread') | |
261 |
|
260 | |||
262 | ops = [{'op': op, 'last_id': fav_threads[str(op.id)]} for op in fav_thread_ops] |
|
261 | ops = [{'op': op, 'last_id': fav_threads[str(op.id)]} for op in fav_thread_ops] | |
263 | if include_posts: |
|
262 | if include_posts: | |
264 | new_post_threads = Thread.objects.get_new_posts(ops) |
|
263 | new_post_threads = Thread.objects.get_new_posts(ops) | |
265 | if new_post_threads: |
|
264 | if new_post_threads: | |
266 | thread_ids = {thread.id: thread for thread in new_post_threads} |
|
265 | thread_ids = {thread.id: thread for thread in new_post_threads} | |
267 | else: |
|
266 | else: | |
268 | thread_ids = dict() |
|
267 | thread_ids = dict() | |
269 |
|
268 | |||
270 | for op in fav_thread_ops: |
|
269 | for op in fav_thread_ops: | |
271 | fav_thread_dict = dict() |
|
270 | fav_thread_dict = dict() | |
272 |
|
271 | |||
273 | op_thread = op.get_thread() |
|
272 | op_thread = op.get_thread() | |
274 | if op_thread.id in thread_ids: |
|
273 | if op_thread.id in thread_ids: | |
275 | thread = thread_ids[op_thread.id] |
|
274 | thread = thread_ids[op_thread.id] | |
276 | new_post_count = thread.new_post_count |
|
275 | new_post_count = thread.new_post_count | |
277 | fav_thread_dict['newest_post_link'] = thread.get_replies()\ |
|
276 | fav_thread_dict['newest_post_link'] = thread.get_replies()\ | |
278 | .filter(id__gt=fav_threads[str(op.id)])\ |
|
277 | .filter(id__gt=fav_threads[str(op.id)])\ | |
279 | .first().get_absolute_url(thread=thread) |
|
278 | .first().get_absolute_url(thread=thread) | |
280 | else: |
|
279 | else: | |
281 | new_post_count = 0 |
|
280 | new_post_count = 0 | |
282 | fav_thread_dict['new_post_count'] = new_post_count |
|
281 | fav_thread_dict['new_post_count'] = new_post_count | |
283 |
|
282 | |||
284 | fav_thread_dict['id'] = op.id |
|
283 | fav_thread_dict['id'] = op.id | |
285 |
|
284 | |||
286 | fav_thread_dict['post_url'] = op.get_link_view() |
|
285 | fav_thread_dict['post_url'] = op.get_link_view() | |
287 | fav_thread_dict['title'] = op.title |
|
286 | fav_thread_dict['title'] = op.title | |
288 |
|
287 | |||
289 | posts.append(fav_thread_dict) |
|
288 | posts.append(fav_thread_dict) | |
290 | else: |
|
289 | else: | |
291 | fav_thread_dict = dict() |
|
290 | fav_thread_dict = dict() | |
292 | fav_thread_dict['new_post_count'] = \ |
|
291 | fav_thread_dict['new_post_count'] = \ | |
293 | Thread.objects.get_new_post_count(ops) |
|
292 | Thread.objects.get_new_post_count(ops) | |
294 | posts.append(fav_thread_dict) |
|
293 | posts.append(fav_thread_dict) | |
295 |
|
294 | |||
296 | return HttpResponse(content=json.dumps(posts)) |
|
295 | return HttpResponse(content=json.dumps(posts)) |
@@ -1,13 +1,34 | |||||
|
1 | import os | |||
|
2 | ||||
1 | from django.shortcuts import render |
|
3 | from django.shortcuts import render | |
2 |
|
4 | |||
|
5 | import neboard | |||
3 | from boards.authors import authors |
|
6 | from boards.authors import authors | |
|
7 | from boards.utils import cached_result | |||
4 | from boards.views.base import BaseBoardView |
|
8 | from boards.views.base import BaseBoardView | |
|
9 | from boards.models import Post | |||
|
10 | ||||
|
11 | ||||
|
12 | PARAM_AUTHORS = 'authors' | |||
|
13 | PARAM_MEDIA_SIZE = 'media_size' | |||
|
14 | PARAM_POST_COUNT = 'post_count' | |||
5 |
|
15 | |||
6 |
|
16 | |||
7 | class AuthorsView(BaseBoardView): |
|
17 | class AuthorsView(BaseBoardView): | |
8 |
|
18 | |||
9 | def get(self, request): |
|
19 | def get(self, request): | |
10 | params = dict() |
|
20 | params = dict() | |
11 |
params[ |
|
21 | params[PARAM_AUTHORS] = authors | |
|
22 | params[PARAM_MEDIA_SIZE] = self._get_directory_size(neboard.settings.MEDIA_ROOT) | |||
|
23 | params[PARAM_POST_COUNT] = Post.objects.count() | |||
12 |
|
24 | |||
13 | return render(request, 'boards/authors.html', params) |
|
25 | return render(request, 'boards/authors.html', params) | |
|
26 | ||||
|
27 | @cached_result() | |||
|
28 | def _get_directory_size(self, directory): | |||
|
29 | total_size = 0 | |||
|
30 | for dirpath, dirnames, filenames in os.walk(directory): | |||
|
31 | for f in filenames: | |||
|
32 | fp = os.path.join(dirpath, f) | |||
|
33 | total_size += os.path.getsize(fp) | |||
|
34 | return total_size |
@@ -1,26 +1,40 | |||||
|
1 | import boards | |||
|
2 | ||||
|
3 | ||||
1 | PARAM_NEXT = 'next' |
|
4 | PARAM_NEXT = 'next' | |
2 | PARAMETER_METHOD = 'method' |
|
5 | PARAMETER_METHOD = 'method' | |
3 |
|
6 | |||
4 |
|
7 | |||
5 | class DispatcherMixin: |
|
8 | class DispatcherMixin: | |
6 | """ |
|
9 | """ | |
7 | This class contains a dispather method that can run a method specified by |
|
10 | This class contains a dispather method that can run a method specified by | |
8 | 'method' request parameter. |
|
11 | 'method' request parameter. | |
9 | """ |
|
12 | """ | |
10 |
|
13 | |||
11 | def __init__(self): |
|
14 | def __init__(self): | |
12 | self.user = None |
|
15 | self.user = None | |
13 |
|
16 | |||
14 | def dispatch_method(self, *args, **kwargs): |
|
17 | def dispatch_method(self, *args, **kwargs): | |
15 | request = args[0] |
|
18 | request = args[0] | |
16 |
|
19 | |||
17 | self.user = request.user |
|
20 | self.user = request.user | |
18 |
|
21 | |||
19 | method_name = None |
|
22 | method_name = None | |
20 | if PARAMETER_METHOD in request.GET: |
|
23 | if PARAMETER_METHOD in request.GET: | |
21 | method_name = request.GET[PARAMETER_METHOD] |
|
24 | method_name = request.GET[PARAMETER_METHOD] | |
22 | elif PARAMETER_METHOD in request.POST: |
|
25 | elif PARAMETER_METHOD in request.POST: | |
23 | method_name = request.POST[PARAMETER_METHOD] |
|
26 | method_name = request.POST[PARAMETER_METHOD] | |
24 |
|
27 | |||
25 | if method_name: |
|
28 | if method_name: | |
26 | return getattr(self, method_name)(*args, **kwargs) |
|
29 | return getattr(self, method_name)(*args, **kwargs) | |
|
30 | ||||
|
31 | ||||
|
32 | class FileUploadMixin: | |||
|
33 | def get_max_upload_size(self): | |||
|
34 | return boards.settings.get_int('Forms', 'MaxFileSize') | |||
|
35 | ||||
|
36 | ||||
|
37 | class PaginatedMixin: | |||
|
38 | def set_page_urls(self, paginator, params): | |||
|
39 | params['prev_page_link'] = paginator.get_prev_page_url() | |||
|
40 | params['next_page_link'] = paginator.get_next_page_url() |
@@ -1,46 +1,49 | |||||
1 | from django.shortcuts import render |
|
1 | from django.shortcuts import render | |
2 |
|
2 | |||
3 | from boards.abstracts.paginator import get_paginator |
|
3 | from boards.abstracts.paginator import get_paginator | |
4 | from boards.abstracts.settingsmanager import get_settings_manager, \ |
|
4 | from boards.abstracts.settingsmanager import get_settings_manager, \ | |
5 | SETTING_USERNAME, SETTING_LAST_NOTIFICATION_ID |
|
5 | SETTING_USERNAME, SETTING_LAST_NOTIFICATION_ID | |
6 | from boards.models.user import Notification |
|
6 | from boards.models.user import Notification | |
7 | from boards.views.base import BaseBoardView |
|
7 | from boards.views.base import BaseBoardView | |
8 |
|
8 | |||
9 | DEFAULT_PAGE = '1' |
|
9 | DEFAULT_PAGE = '1' | |
10 |
|
10 | |||
11 | TEMPLATE = 'boards/notifications.html' |
|
11 | TEMPLATE = 'boards/notifications.html' | |
12 | PARAM_PAGE = 'page' |
|
12 | PARAM_PAGE = 'page' | |
13 | PARAM_USERNAME = 'notification_username' |
|
13 | PARAM_USERNAMES = 'notification_usernames' | |
14 | REQUEST_PAGE = 'page' |
|
14 | REQUEST_PAGE = 'page' | |
15 | RESULTS_PER_PAGE = 10 |
|
15 | RESULTS_PER_PAGE = 10 | |
16 |
|
16 | |||
17 |
|
17 | |||
18 | class NotificationView(BaseBoardView): |
|
18 | class NotificationView(BaseBoardView): | |
19 |
|
19 | |||
20 | def get(self, request, username): |
|
20 | def get(self, request, username=None): | |
21 | params = self.get_context_data() |
|
21 | params = self.get_context_data() | |
22 |
|
22 | |||
23 | settings_manager = get_settings_manager(request) |
|
23 | settings_manager = get_settings_manager(request) | |
24 |
|
24 | |||
25 | # If we open our notifications, reset the "new" count |
|
25 | # If we open our notifications, reset the "new" count | |
26 | my_username = settings_manager.get_setting(SETTING_USERNAME) |
|
26 | if username is None: | |
27 |
|
27 | notification_usernames = settings_manager.get_notification_usernames() | ||
28 | notification_username = username.lower() |
|
28 | else: | |
|
29 | notification_usernames = [username] | |||
29 |
|
30 | |||
30 | posts = Notification.objects.get_notification_posts( |
|
31 | posts = Notification.objects.get_notification_posts( | |
31 | username=notification_username) |
|
32 | usernames=notification_usernames) | |
32 | if notification_username == my_username: |
|
33 | ||
|
34 | if username is None: | |||
33 | last = posts.first() |
|
35 | last = posts.first() | |
34 | if last is not None: |
|
36 | if last is not None: | |
35 | last_id = last.id |
|
37 | last_id = last.id | |
36 | settings_manager.set_setting(SETTING_LAST_NOTIFICATION_ID, |
|
38 | settings_manager.set_setting(SETTING_LAST_NOTIFICATION_ID, | |
37 | last_id) |
|
39 | last_id) | |
38 |
|
40 | |||
|
41 | ||||
39 | paginator = get_paginator(posts, RESULTS_PER_PAGE) |
|
42 | paginator = get_paginator(posts, RESULTS_PER_PAGE) | |
40 |
|
43 | |||
41 | page = int(request.GET.get(REQUEST_PAGE, DEFAULT_PAGE)) |
|
44 | page = int(request.GET.get(REQUEST_PAGE, DEFAULT_PAGE)) | |
42 |
|
45 | |||
43 | params[PARAM_PAGE] = paginator.page(page) |
|
46 | params[PARAM_PAGE] = paginator.page(page) | |
44 | params[PARAM_USERNAME] = notification_username |
|
47 | params[PARAM_USERNAMES] = notification_usernames | |
45 |
|
48 | |||
46 | return render(request, TEMPLATE, params) |
|
49 | return render(request, TEMPLATE, params) |
@@ -1,30 +1,33 | |||||
1 | from boards.views.thread import ThreadView |
|
1 | from boards.views.thread import ThreadView | |
|
2 | from boards.views.mixins import FileUploadMixin | |||
2 |
|
3 | |||
3 | TEMPLATE_NORMAL = 'boards/thread_normal.html' |
|
4 | TEMPLATE_NORMAL = 'boards/thread_normal.html' | |
4 |
|
5 | |||
5 | CONTEXT_BUMPLIMIT_PRG = 'bumplimit_progress' |
|
6 | CONTEXT_BUMPLIMIT_PRG = 'bumplimit_progress' | |
6 | CONTEXT_POSTS_LEFT = 'posts_left' |
|
7 | CONTEXT_POSTS_LEFT = 'posts_left' | |
7 | CONTEXT_BUMPABLE = 'bumpable' |
|
8 | CONTEXT_BUMPABLE = 'bumpable' | |
|
9 | PARAM_MAX_FILE_SIZE = 'max_file_size' | |||
8 |
|
10 | |||
9 |
|
11 | |||
10 | class NormalThreadView(ThreadView): |
|
12 | class NormalThreadView(ThreadView, FileUploadMixin): | |
11 |
|
13 | |||
12 | def get_template(self): |
|
14 | def get_template(self): | |
13 | return TEMPLATE_NORMAL |
|
15 | return TEMPLATE_NORMAL | |
14 |
|
16 | |||
15 | def get_mode(self): |
|
17 | def get_mode(self): | |
16 | return 'normal' |
|
18 | return 'normal' | |
17 |
|
19 | |||
18 | def get_data(self, thread): |
|
20 | def get_data(self, thread): | |
19 | params = dict() |
|
21 | params = dict() | |
20 |
|
22 | |||
21 | bumpable = thread.can_bump() |
|
23 | bumpable = thread.can_bump() | |
22 | params[CONTEXT_BUMPABLE] = bumpable |
|
24 | params[CONTEXT_BUMPABLE] = bumpable | |
23 | max_posts = thread.max_posts |
|
25 | max_posts = thread.max_posts | |
24 | if bumpable and thread.has_post_limit(): |
|
26 | if bumpable and thread.has_post_limit(): | |
25 | left_posts = max_posts - thread.get_reply_count() |
|
27 | left_posts = max_posts - thread.get_reply_count() | |
26 | params[CONTEXT_POSTS_LEFT] = left_posts |
|
28 | params[CONTEXT_POSTS_LEFT] = left_posts | |
27 | params[CONTEXT_BUMPLIMIT_PRG] = str( |
|
29 | params[CONTEXT_BUMPLIMIT_PRG] = str( | |
28 | float(left_posts) / max_posts * 100) |
|
30 | float(left_posts) / max_posts * 100) | |
|
31 | params[PARAM_MAX_FILE_SIZE] = self.get_max_upload_size() | |||
29 |
|
32 | |||
30 | return params |
|
33 | return params |
@@ -1,173 +1,175 | |||||
1 | from django.contrib.auth.decorators import permission_required |
|
1 | from django.contrib.auth.decorators import permission_required | |
2 |
|
2 | |||
3 | from django.core.exceptions import ObjectDoesNotExist |
|
3 | from django.core.exceptions import ObjectDoesNotExist | |
|
4 | from django.core.urlresolvers import reverse | |||
4 | from django.http import Http404 |
|
5 | from django.http import Http404 | |
5 | from django.shortcuts import get_object_or_404, render, redirect |
|
6 | from django.shortcuts import get_object_or_404, render, redirect | |
|
7 | from django.template.context_processors import csrf | |||
|
8 | from django.utils.decorators import method_decorator | |||
|
9 | from django.views.decorators.csrf import csrf_protect | |||
6 | from django.views.generic.edit import FormMixin |
|
10 | from django.views.generic.edit import FormMixin | |
7 | from django.utils import timezone |
|
11 | from django.utils import timezone | |
8 | from django.utils.dateformat import format |
|
12 | from django.utils.dateformat import format | |
9 |
|
13 | |||
10 | from boards import utils, settings |
|
14 | from boards import utils, settings | |
11 | from boards.abstracts.settingsmanager import get_settings_manager |
|
15 | from boards.abstracts.settingsmanager import get_settings_manager | |
12 | from boards.forms import PostForm, PlainErrorList |
|
16 | from boards.forms import PostForm, PlainErrorList | |
13 | from boards.models import Post |
|
17 | from boards.models import Post | |
14 | from boards.views.base import BaseBoardView, CONTEXT_FORM |
|
18 | from boards.views.base import BaseBoardView, CONTEXT_FORM | |
15 | from boards.views.mixins import DispatcherMixin, PARAMETER_METHOD |
|
19 | from boards.views.mixins import DispatcherMixin, PARAMETER_METHOD | |
16 | from boards.views.posting_mixin import PostMixin |
|
20 | from boards.views.posting_mixin import PostMixin | |
17 | import neboard |
|
21 | import neboard | |
18 |
|
22 | |||
19 | REQ_POST_ID = 'post_id' |
|
23 | REQ_POST_ID = 'post_id' | |
20 |
|
24 | |||
21 | CONTEXT_LASTUPDATE = "last_update" |
|
25 | CONTEXT_LASTUPDATE = "last_update" | |
22 | CONTEXT_THREAD = 'thread' |
|
26 | CONTEXT_THREAD = 'thread' | |
23 | CONTEXT_WS_TOKEN = 'ws_token' |
|
27 | CONTEXT_WS_TOKEN = 'ws_token' | |
24 | CONTEXT_WS_PROJECT = 'ws_project' |
|
28 | CONTEXT_WS_PROJECT = 'ws_project' | |
25 | CONTEXT_WS_HOST = 'ws_host' |
|
29 | CONTEXT_WS_HOST = 'ws_host' | |
26 | CONTEXT_WS_PORT = 'ws_port' |
|
30 | CONTEXT_WS_PORT = 'ws_port' | |
27 | CONTEXT_WS_TIME = 'ws_token_time' |
|
31 | CONTEXT_WS_TIME = 'ws_token_time' | |
28 | CONTEXT_MODE = 'mode' |
|
32 | CONTEXT_MODE = 'mode' | |
29 | CONTEXT_OP = 'opening_post' |
|
33 | CONTEXT_OP = 'opening_post' | |
30 | CONTEXT_FAVORITE = 'is_favorite' |
|
34 | CONTEXT_FAVORITE = 'is_favorite' | |
|
35 | CONTEXT_RSS_URL = 'rss_url' | |||
31 |
|
36 | |||
32 | FORM_TITLE = 'title' |
|
37 | FORM_TITLE = 'title' | |
33 | FORM_TEXT = 'text' |
|
38 | FORM_TEXT = 'text' | |
34 | FORM_IMAGE = 'image' |
|
39 | FORM_IMAGE = 'image' | |
35 | FORM_THREADS = 'threads' |
|
40 | FORM_THREADS = 'threads' | |
36 |
|
41 | |||
37 |
|
42 | |||
38 | class ThreadView(BaseBoardView, PostMixin, FormMixin, DispatcherMixin): |
|
43 | class ThreadView(BaseBoardView, PostMixin, FormMixin, DispatcherMixin): | |
39 |
|
44 | |||
|
45 | @method_decorator(csrf_protect) | |||
40 | def get(self, request, post_id, form: PostForm=None): |
|
46 | def get(self, request, post_id, form: PostForm=None): | |
41 | try: |
|
47 | try: | |
42 | opening_post = Post.objects.get(id=post_id) |
|
48 | opening_post = Post.objects.get(id=post_id) | |
43 | except ObjectDoesNotExist: |
|
49 | except ObjectDoesNotExist: | |
44 | raise Http404 |
|
50 | raise Http404 | |
45 |
|
51 | |||
46 | # If the tag is favorite, update the counter |
|
52 | # If the tag is favorite, update the counter | |
47 | settings_manager = get_settings_manager(request) |
|
53 | settings_manager = get_settings_manager(request) | |
48 | favorite = settings_manager.thread_is_fav(opening_post) |
|
54 | favorite = settings_manager.thread_is_fav(opening_post) | |
49 | if favorite: |
|
55 | if favorite: | |
50 | settings_manager.add_or_read_fav_thread(opening_post) |
|
56 | settings_manager.add_or_read_fav_thread(opening_post) | |
51 |
|
57 | |||
52 | # If this is not OP, don't show it as it is |
|
58 | # If this is not OP, don't show it as it is | |
53 | if not opening_post.is_opening(): |
|
59 | if not opening_post.is_opening(): | |
54 | return redirect(opening_post.get_thread().get_opening_post() |
|
60 | return redirect(opening_post.get_thread().get_opening_post() | |
55 | .get_absolute_url()) |
|
61 | .get_absolute_url()) | |
56 |
|
62 | |||
57 | if not form: |
|
63 | if not form: | |
58 | form = PostForm(error_class=PlainErrorList) |
|
64 | form = PostForm(error_class=PlainErrorList) | |
59 |
|
65 | |||
60 | thread_to_show = opening_post.get_thread() |
|
66 | thread_to_show = opening_post.get_thread() | |
61 |
|
67 | |||
62 | params = dict() |
|
68 | params = dict() | |
63 |
|
69 | |||
64 | params[CONTEXT_FORM] = form |
|
70 | params[CONTEXT_FORM] = form | |
65 | params[CONTEXT_LASTUPDATE] = str(thread_to_show.last_edit_time) |
|
71 | params[CONTEXT_LASTUPDATE] = str(thread_to_show.last_edit_time) | |
66 | params[CONTEXT_THREAD] = thread_to_show |
|
72 | params[CONTEXT_THREAD] = thread_to_show | |
67 | params[CONTEXT_MODE] = self.get_mode() |
|
73 | params[CONTEXT_MODE] = self.get_mode() | |
68 | params[CONTEXT_OP] = opening_post |
|
74 | params[CONTEXT_OP] = opening_post | |
69 | params[CONTEXT_FAVORITE] = favorite |
|
75 | params[CONTEXT_FAVORITE] = favorite | |
|
76 | params[CONTEXT_RSS_URL] = self.get_rss_url(post_id) | |||
70 |
|
77 | |||
71 | if settings.get_bool('External', 'WebsocketsEnabled'): |
|
78 | if settings.get_bool('External', 'WebsocketsEnabled'): | |
72 | token_time = format(timezone.now(), u'U') |
|
79 | token_time = format(timezone.now(), u'U') | |
73 |
|
80 | |||
74 | params[CONTEXT_WS_TIME] = token_time |
|
81 | params[CONTEXT_WS_TIME] = token_time | |
75 | params[CONTEXT_WS_TOKEN] = utils.get_websocket_token( |
|
82 | params[CONTEXT_WS_TOKEN] = utils.get_websocket_token( | |
76 | timestamp=token_time) |
|
83 | timestamp=token_time) | |
77 | params[CONTEXT_WS_PROJECT] = neboard.settings.CENTRIFUGE_PROJECT_ID |
|
84 | params[CONTEXT_WS_PROJECT] = neboard.settings.CENTRIFUGE_PROJECT_ID | |
78 | params[CONTEXT_WS_HOST] = request.get_host().split(':')[0] |
|
85 | params[CONTEXT_WS_HOST] = request.get_host().split(':')[0] | |
79 | params[CONTEXT_WS_PORT] = neboard.settings.CENTRIFUGE_PORT |
|
86 | params[CONTEXT_WS_PORT] = neboard.settings.CENTRIFUGE_PORT | |
80 |
|
87 | |||
81 | params.update(self.get_data(thread_to_show)) |
|
88 | params.update(self.get_data(thread_to_show)) | |
82 |
|
89 | |||
83 | return render(request, self.get_template(), params) |
|
90 | return render(request, self.get_template(), params) | |
84 |
|
91 | |||
|
92 | @method_decorator(csrf_protect) | |||
85 | def post(self, request, post_id): |
|
93 | def post(self, request, post_id): | |
86 | opening_post = get_object_or_404(Post, id=post_id) |
|
94 | opening_post = get_object_or_404(Post, id=post_id) | |
87 |
|
95 | |||
88 | # If this is not OP, don't show it as it is |
|
96 | # If this is not OP, don't show it as it is | |
89 | if not opening_post.is_opening(): |
|
97 | if not opening_post.is_opening(): | |
90 | raise Http404 |
|
98 | raise Http404 | |
91 |
|
99 | |||
92 | if PARAMETER_METHOD in request.POST: |
|
100 | if PARAMETER_METHOD in request.POST: | |
93 | self.dispatch_method(request, opening_post) |
|
101 | self.dispatch_method(request, opening_post) | |
94 |
|
102 | |||
95 | return redirect('thread', post_id) # FIXME Different for different modes |
|
103 | return redirect('thread', post_id) # FIXME Different for different modes | |
96 |
|
104 | |||
97 | if not opening_post.get_thread().archived: |
|
105 | if not opening_post.get_thread().is_archived(): | |
98 | form = PostForm(request.POST, request.FILES, |
|
106 | form = PostForm(request.POST, request.FILES, | |
99 | error_class=PlainErrorList) |
|
107 | error_class=PlainErrorList) | |
100 | form.session = request.session |
|
108 | form.session = request.session | |
101 |
|
109 | |||
102 | if form.is_valid(): |
|
110 | if form.is_valid(): | |
103 | return self.new_post(request, form, opening_post) |
|
111 | return self.new_post(request, form, opening_post) | |
104 | if form.need_to_ban: |
|
112 | if form.need_to_ban: | |
105 | # Ban user because he is suspected to be a bot |
|
113 | # Ban user because he is suspected to be a bot | |
106 | self._ban_current_user(request) |
|
114 | self._ban_current_user(request) | |
107 |
|
115 | |||
108 | return self.get(request, post_id, form) |
|
116 | return self.get(request, post_id, form) | |
109 |
|
117 | |||
110 | def new_post(self, request, form: PostForm, opening_post: Post=None, |
|
118 | def new_post(self, request, form: PostForm, opening_post: Post=None, | |
111 | html_response=True): |
|
119 | html_response=True): | |
112 | """ |
|
120 | """ | |
113 | Adds a new post (in thread or as a reply). |
|
121 | Adds a new post (in thread or as a reply). | |
114 | """ |
|
122 | """ | |
115 |
|
123 | |||
116 | ip = utils.get_client_ip(request) |
|
124 | ip = utils.get_client_ip(request) | |
117 |
|
125 | |||
118 | data = form.cleaned_data |
|
126 | data = form.cleaned_data | |
119 |
|
127 | |||
120 | title = form.get_title() |
|
128 | title = form.get_title() | |
121 | text = data[FORM_TEXT] |
|
129 | text = data[FORM_TEXT] | |
122 | file = form.get_file() |
|
130 | file = form.get_file() | |
123 | threads = data[FORM_THREADS] |
|
131 | threads = data[FORM_THREADS] | |
124 |
|
132 | |||
125 | text = self._remove_invalid_links(text) |
|
133 | text = self._remove_invalid_links(text) | |
126 |
|
134 | |||
127 | post_thread = opening_post.get_thread() |
|
135 | post_thread = opening_post.get_thread() | |
128 |
|
136 | |||
129 | post = Post.objects.create_post(title=title, text=text, file=file, |
|
137 | post = Post.objects.create_post(title=title, text=text, file=file, | |
130 | thread=post_thread, ip=ip, |
|
138 | thread=post_thread, ip=ip, | |
131 | opening_posts=threads, |
|
139 | opening_posts=threads, | |
132 | tripcode=form.get_tripcode()) |
|
140 | tripcode=form.get_tripcode()) | |
133 | post.notify_clients() |
|
141 | post.notify_clients() | |
134 |
|
142 | |||
135 | if html_response: |
|
143 | if html_response: | |
136 | if opening_post: |
|
144 | if opening_post: | |
137 | return redirect(post.get_absolute_url()) |
|
145 | return redirect(post.get_absolute_url()) | |
138 | else: |
|
146 | else: | |
139 | return post |
|
147 | return post | |
140 |
|
148 | |||
141 | def get_data(self, thread) -> dict: |
|
149 | def get_data(self, thread) -> dict: | |
142 | """ |
|
150 | """ | |
143 | Returns context params for the view. |
|
151 | Returns context params for the view. | |
144 | """ |
|
152 | """ | |
145 |
|
153 | |||
146 | return dict() |
|
154 | return dict() | |
147 |
|
155 | |||
148 | def get_template(self) -> str: |
|
156 | def get_template(self) -> str: | |
149 | """ |
|
157 | """ | |
150 | Gets template to show the thread mode on. |
|
158 | Gets template to show the thread mode on. | |
151 | """ |
|
159 | """ | |
152 |
|
160 | |||
153 | pass |
|
161 | pass | |
154 |
|
162 | |||
155 | def get_mode(self) -> str: |
|
163 | def get_mode(self) -> str: | |
156 | pass |
|
164 | pass | |
157 |
|
165 | |||
158 | def subscribe(self, request, opening_post): |
|
166 | def subscribe(self, request, opening_post): | |
159 | settings_manager = get_settings_manager(request) |
|
167 | settings_manager = get_settings_manager(request) | |
160 | settings_manager.add_or_read_fav_thread(opening_post) |
|
168 | settings_manager.add_or_read_fav_thread(opening_post) | |
161 |
|
169 | |||
162 | def unsubscribe(self, request, opening_post): |
|
170 | def unsubscribe(self, request, opening_post): | |
163 | settings_manager = get_settings_manager(request) |
|
171 | settings_manager = get_settings_manager(request) | |
164 | settings_manager.del_fav_thread(opening_post) |
|
172 | settings_manager.del_fav_thread(opening_post) | |
165 |
|
173 | |||
166 | @permission_required('boards.post_hide_unhide') |
|
174 | def get_rss_url(self, opening_id): | |
167 | def toggle_hide_post(self, request, opening_post): |
|
175 | return reverse('thread', kwargs={'post_id': opening_id}) + 'rss/' | |
168 | post_id = request.GET.get(REQ_POST_ID) |
|
|||
169 |
|
||||
170 | if post_id: |
|
|||
171 | post = get_object_or_404(Post, id=post_id) |
|
|||
172 | post.set_hidden(not post.is_hidden()) |
|
|||
173 | post.save(update_fields=['hidden']) |
|
@@ -1,240 +1,241 | |||||
1 | # Django settings for neboard project. |
|
1 | # Django settings for neboard project. | |
2 | import os |
|
2 | import os | |
3 |
|
3 | |||
4 | DEBUG = True |
|
4 | DEBUG = True | |
5 | TEMPLATE_DEBUG = DEBUG |
|
5 | TEMPLATE_DEBUG = DEBUG | |
6 |
|
6 | |||
7 | ADMINS = ( |
|
7 | ADMINS = ( | |
8 | # ('Your Name', 'your_email@example.com'), |
|
8 | # ('Your Name', 'your_email@example.com'), | |
9 | ('admin', 'admin@example.com') |
|
9 | ('admin', 'admin@example.com') | |
10 | ) |
|
10 | ) | |
11 |
|
11 | |||
12 | MANAGERS = ADMINS |
|
12 | MANAGERS = ADMINS | |
13 |
|
13 | |||
14 | DATABASES = { |
|
14 | DATABASES = { | |
15 | 'default': { |
|
15 | 'default': { | |
16 | 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. |
|
16 | 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. | |
17 | 'NAME': 'database.db', # Or path to database file if using sqlite3. |
|
17 | 'NAME': 'database.db', # Or path to database file if using sqlite3. | |
18 | 'USER': '', # Not used with sqlite3. |
|
18 | 'USER': '', # Not used with sqlite3. | |
19 | 'PASSWORD': '', # Not used with sqlite3. |
|
19 | 'PASSWORD': '', # Not used with sqlite3. | |
20 | 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. |
|
20 | 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. | |
21 | 'PORT': '', # Set to empty string for default. Not used with sqlite3. |
|
21 | 'PORT': '', # Set to empty string for default. Not used with sqlite3. | |
22 | 'CONN_MAX_AGE': None, |
|
22 | 'CONN_MAX_AGE': None, | |
23 | } |
|
23 | } | |
24 | } |
|
24 | } | |
25 |
|
25 | |||
26 | # Local time zone for this installation. Choices can be found here: |
|
26 | # Local time zone for this installation. Choices can be found here: | |
27 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name |
|
27 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name | |
28 | # although not all choices may be available on all operating systems. |
|
28 | # although not all choices may be available on all operating systems. | |
29 | # In a Windows environment this must be set to your system time zone. |
|
29 | # In a Windows environment this must be set to your system time zone. | |
30 | TIME_ZONE = 'Europe/Kiev' |
|
30 | TIME_ZONE = 'Europe/Kiev' | |
31 |
|
31 | |||
32 | # Language code for this installation. All choices can be found here: |
|
32 | # Language code for this installation. All choices can be found here: | |
33 | # http://www.i18nguy.com/unicode/language-identifiers.html |
|
33 | # http://www.i18nguy.com/unicode/language-identifiers.html | |
34 | LANGUAGE_CODE = 'en' |
|
34 | LANGUAGE_CODE = 'en' | |
35 |
|
35 | |||
36 | SITE_ID = 1 |
|
36 | SITE_ID = 1 | |
37 |
|
37 | |||
38 | # If you set this to False, Django will make some optimizations so as not |
|
38 | # If you set this to False, Django will make some optimizations so as not | |
39 | # to load the internationalization machinery. |
|
39 | # to load the internationalization machinery. | |
40 | USE_I18N = True |
|
40 | USE_I18N = True | |
41 |
|
41 | |||
42 | # If you set this to False, Django will not format dates, numbers and |
|
42 | # If you set this to False, Django will not format dates, numbers and | |
43 | # calendars according to the current locale. |
|
43 | # calendars according to the current locale. | |
44 | USE_L10N = True |
|
44 | USE_L10N = True | |
45 |
|
45 | |||
46 | # If you set this to False, Django will not use timezone-aware datetimes. |
|
46 | # If you set this to False, Django will not use timezone-aware datetimes. | |
47 | USE_TZ = True |
|
47 | USE_TZ = True | |
48 |
|
48 | |||
49 | USE_ETAGS = True |
|
49 | USE_ETAGS = True | |
50 |
|
50 | |||
51 | # Absolute filesystem path to the directory that will hold user-uploaded files. |
|
51 | # Absolute filesystem path to the directory that will hold user-uploaded files. | |
52 | # Example: "/home/media/media.lawrence.com/media/" |
|
52 | # Example: "/home/media/media.lawrence.com/media/" | |
53 | MEDIA_ROOT = './media/' |
|
53 | MEDIA_ROOT = './media/' | |
54 |
|
54 | |||
55 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a |
|
55 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a | |
56 | # trailing slash. |
|
56 | # trailing slash. | |
57 | # Examples: "http://media.lawrence.com/media/", "http://example.com/media/" |
|
57 | # Examples: "http://media.lawrence.com/media/", "http://example.com/media/" | |
58 | MEDIA_URL = '/media/' |
|
58 | MEDIA_URL = '/media/' | |
59 |
|
59 | |||
60 | # Absolute path to the directory static files should be collected to. |
|
60 | # Absolute path to the directory static files should be collected to. | |
61 | # Don't put anything in this directory yourself; store your static files |
|
61 | # Don't put anything in this directory yourself; store your static files | |
62 | # in apps' "static/" subdirectories and in STATICFILES_DIRS. |
|
62 | # in apps' "static/" subdirectories and in STATICFILES_DIRS. | |
63 | # Example: "/home/media/media.lawrence.com/static/" |
|
63 | # Example: "/home/media/media.lawrence.com/static/" | |
64 | STATIC_ROOT = '' |
|
64 | STATIC_ROOT = '' | |
65 |
|
65 | |||
66 | # URL prefix for static files. |
|
66 | # URL prefix for static files. | |
67 | # Example: "http://media.lawrence.com/static/" |
|
67 | # Example: "http://media.lawrence.com/static/" | |
68 | STATIC_URL = '/static/' |
|
68 | STATIC_URL = '/static/' | |
69 |
|
69 | |||
70 | # Additional locations of static files |
|
70 | # Additional locations of static files | |
71 | # It is really a hack, put real paths, not related |
|
71 | # It is really a hack, put real paths, not related | |
72 | STATICFILES_DIRS = ( |
|
72 | STATICFILES_DIRS = ( | |
73 | os.path.dirname(__file__) + '/boards/static', |
|
73 | os.path.dirname(__file__) + '/boards/static', | |
74 |
|
74 | |||
75 | # '/d/work/python/django/neboard/neboard/boards/static', |
|
75 | # '/d/work/python/django/neboard/neboard/boards/static', | |
76 | # Put strings here, like "/home/html/static" or "C:/www/django/static". |
|
76 | # Put strings here, like "/home/html/static" or "C:/www/django/static". | |
77 | # Always use forward slashes, even on Windows. |
|
77 | # Always use forward slashes, even on Windows. | |
78 | # Don't forget to use absolute paths, not relative paths. |
|
78 | # Don't forget to use absolute paths, not relative paths. | |
79 | ) |
|
79 | ) | |
80 |
|
80 | |||
81 | # List of finder classes that know how to find static files in |
|
81 | # List of finder classes that know how to find static files in | |
82 | # various locations. |
|
82 | # various locations. | |
83 | STATICFILES_FINDERS = ( |
|
83 | STATICFILES_FINDERS = ( | |
84 | 'django.contrib.staticfiles.finders.FileSystemFinder', |
|
84 | 'django.contrib.staticfiles.finders.FileSystemFinder', | |
85 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', |
|
85 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', | |
86 | ) |
|
86 | ) | |
87 |
|
87 | |||
88 | if DEBUG: |
|
88 | if DEBUG: | |
89 | STATICFILES_STORAGE = \ |
|
89 | STATICFILES_STORAGE = \ | |
90 | 'django.contrib.staticfiles.storage.StaticFilesStorage' |
|
90 | 'django.contrib.staticfiles.storage.StaticFilesStorage' | |
91 | else: |
|
91 | else: | |
92 | STATICFILES_STORAGE = \ |
|
92 | STATICFILES_STORAGE = \ | |
93 | 'django.contrib.staticfiles.storage.CachedStaticFilesStorage' |
|
93 | 'django.contrib.staticfiles.storage.CachedStaticFilesStorage' | |
94 |
|
94 | |||
95 | # Make this unique, and don't share it with anybody. |
|
95 | # Make this unique, and don't share it with anybody. | |
96 | SECRET_KEY = '@1rc$o(7=tt#kd+4s$u6wchm**z^)4x90)7f6z(i&55@o11*8o' |
|
96 | SECRET_KEY = '@1rc$o(7=tt#kd+4s$u6wchm**z^)4x90)7f6z(i&55@o11*8o' | |
97 |
|
97 | |||
98 | # List of callables that know how to import templates from various sources. |
|
98 | TEMPLATES = [{ | |
99 | TEMPLATE_LOADERS = ( |
|
99 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', | |
100 | 'django.template.loaders.filesystem.Loader', |
|
100 | 'DIRS': ['templates'], | |
101 | 'django.template.loaders.app_directories.Loader', |
|
101 | 'OPTIONS': { | |
102 | ) |
|
102 | 'loaders': [ | |
|
103 | ('django.template.loaders.cached.Loader', [ | |||
|
104 | 'django.template.loaders.filesystem.Loader', | |||
|
105 | 'django.template.loaders.app_directories.Loader', | |||
|
106 | ]), | |||
|
107 | ], | |||
|
108 | 'context_processors': [ | |||
|
109 | 'django.template.context_processors.csrf', | |||
|
110 | 'django.core.context_processors.media', | |||
|
111 | 'django.core.context_processors.static', | |||
|
112 | 'django.core.context_processors.request', | |||
|
113 | 'django.contrib.auth.context_processors.auth', | |||
|
114 | 'boards.context_processors.user_and_ui_processor', | |||
|
115 | ], | |||
|
116 | }, | |||
|
117 | }] | |||
103 |
|
118 | |||
104 | TEMPLATE_CONTEXT_PROCESSORS = ( |
|
|||
105 | 'django.core.context_processors.media', |
|
|||
106 | 'django.core.context_processors.static', |
|
|||
107 | 'django.core.context_processors.request', |
|
|||
108 | 'django.contrib.auth.context_processors.auth', |
|
|||
109 | 'boards.context_processors.user_and_ui_processor', |
|
|||
110 | ) |
|
|||
111 |
|
119 | |||
112 | MIDDLEWARE_CLASSES = [ |
|
120 | MIDDLEWARE_CLASSES = [ | |
113 | 'django.middleware.http.ConditionalGetMiddleware', |
|
121 | 'django.middleware.http.ConditionalGetMiddleware', | |
114 | 'django.contrib.sessions.middleware.SessionMiddleware', |
|
122 | 'django.contrib.sessions.middleware.SessionMiddleware', | |
115 | 'django.middleware.locale.LocaleMiddleware', |
|
123 | 'django.middleware.locale.LocaleMiddleware', | |
116 | 'django.middleware.common.CommonMiddleware', |
|
124 | 'django.middleware.common.CommonMiddleware', | |
117 | 'django.contrib.auth.middleware.AuthenticationMiddleware', |
|
125 | 'django.contrib.auth.middleware.AuthenticationMiddleware', | |
118 | 'django.contrib.messages.middleware.MessageMiddleware', |
|
126 | 'django.contrib.messages.middleware.MessageMiddleware', | |
119 | 'boards.middlewares.BanMiddleware', |
|
127 | 'boards.middlewares.BanMiddleware', | |
120 | 'boards.middlewares.TimezoneMiddleware', |
|
128 | 'boards.middlewares.TimezoneMiddleware', | |
121 | ] |
|
129 | ] | |
122 |
|
130 | |||
123 | ROOT_URLCONF = 'neboard.urls' |
|
131 | ROOT_URLCONF = 'neboard.urls' | |
124 |
|
132 | |||
125 | # Python dotted path to the WSGI application used by Django's runserver. |
|
133 | # Python dotted path to the WSGI application used by Django's runserver. | |
126 | WSGI_APPLICATION = 'neboard.wsgi.application' |
|
134 | WSGI_APPLICATION = 'neboard.wsgi.application' | |
127 |
|
135 | |||
128 | TEMPLATE_DIRS = ( |
|
|||
129 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". |
|
|||
130 | # Always use forward slashes, even on Windows. |
|
|||
131 | # Don't forget to use absolute paths, not relative paths. |
|
|||
132 | 'templates', |
|
|||
133 | ) |
|
|||
134 |
|
||||
135 | INSTALLED_APPS = ( |
|
136 | INSTALLED_APPS = ( | |
136 | 'django.contrib.auth', |
|
137 | 'django.contrib.auth', | |
137 | 'django.contrib.contenttypes', |
|
138 | 'django.contrib.contenttypes', | |
138 | 'django.contrib.sessions', |
|
139 | 'django.contrib.sessions', | |
139 | # 'django.contrib.sites', |
|
140 | # 'django.contrib.sites', | |
140 | 'django.contrib.messages', |
|
141 | 'django.contrib.messages', | |
141 | 'django.contrib.staticfiles', |
|
142 | 'django.contrib.staticfiles', | |
142 | # Uncomment the next line to enable the admin: |
|
143 | # Uncomment the next line to enable the admin: | |
143 | 'django.contrib.admin', |
|
144 | 'django.contrib.admin', | |
144 | # Uncomment the next line to enable admin documentation: |
|
145 | # Uncomment the next line to enable admin documentation: | |
145 | # 'django.contrib.admindocs', |
|
146 | # 'django.contrib.admindocs', | |
146 | #'django.contrib.humanize', |
|
147 | #'django.contrib.humanize', | |
147 |
|
148 | |||
148 | 'debug_toolbar', |
|
149 | 'debug_toolbar', | |
149 |
|
150 | |||
150 | # Search |
|
151 | # Search | |
151 | 'haystack', |
|
152 | 'haystack', | |
152 |
|
153 | |||
153 | 'boards', |
|
154 | 'boards', | |
154 | ) |
|
155 | ) | |
155 |
|
156 | |||
156 | # A sample logging configuration. The only tangible logging |
|
157 | # A sample logging configuration. The only tangible logging | |
157 | # performed by this configuration is to send an email to |
|
158 | # performed by this configuration is to send an email to | |
158 | # the site admins on every HTTP 500 error when DEBUG=False. |
|
159 | # the site admins on every HTTP 500 error when DEBUG=False. | |
159 | # See http://docs.djangoproject.com/en/dev/topics/logging for |
|
160 | # See http://docs.djangoproject.com/en/dev/topics/logging for | |
160 | # more details on how to customize your logging configuration. |
|
161 | # more details on how to customize your logging configuration. | |
161 | LOGGING = { |
|
162 | LOGGING = { | |
162 | 'version': 1, |
|
163 | 'version': 1, | |
163 | 'disable_existing_loggers': False, |
|
164 | 'disable_existing_loggers': False, | |
164 | 'formatters': { |
|
165 | 'formatters': { | |
165 | 'verbose': { |
|
166 | 'verbose': { | |
166 | 'format': '%(levelname)s %(asctime)s %(name)s %(process)d %(thread)d %(message)s' |
|
167 | 'format': '%(levelname)s %(asctime)s %(name)s %(process)d %(thread)d %(message)s' | |
167 | }, |
|
168 | }, | |
168 | 'simple': { |
|
169 | 'simple': { | |
169 | 'format': '%(levelname)s %(asctime)s [%(name)s] %(message)s' |
|
170 | 'format': '%(levelname)s %(asctime)s [%(name)s] %(message)s' | |
170 | }, |
|
171 | }, | |
171 | }, |
|
172 | }, | |
172 | 'filters': { |
|
173 | 'filters': { | |
173 | 'require_debug_false': { |
|
174 | 'require_debug_false': { | |
174 | '()': 'django.utils.log.RequireDebugFalse' |
|
175 | '()': 'django.utils.log.RequireDebugFalse' | |
175 | } |
|
176 | } | |
176 | }, |
|
177 | }, | |
177 | 'handlers': { |
|
178 | 'handlers': { | |
178 | 'console': { |
|
179 | 'console': { | |
179 | 'level': 'DEBUG', |
|
180 | 'level': 'DEBUG', | |
180 | 'class': 'logging.StreamHandler', |
|
181 | 'class': 'logging.StreamHandler', | |
181 | 'formatter': 'simple' |
|
182 | 'formatter': 'simple' | |
182 | }, |
|
183 | }, | |
183 | }, |
|
184 | }, | |
184 | 'loggers': { |
|
185 | 'loggers': { | |
185 | 'boards': { |
|
186 | 'boards': { | |
186 | 'handlers': ['console'], |
|
187 | 'handlers': ['console'], | |
187 | 'level': 'DEBUG', |
|
188 | 'level': 'DEBUG', | |
188 | } |
|
189 | } | |
189 | }, |
|
190 | }, | |
190 | } |
|
191 | } | |
191 |
|
192 | |||
192 | HAYSTACK_CONNECTIONS = { |
|
193 | HAYSTACK_CONNECTIONS = { | |
193 | 'default': { |
|
194 | 'default': { | |
194 | 'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine', |
|
195 | 'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine', | |
195 | 'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'), |
|
196 | 'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'), | |
196 | }, |
|
197 | }, | |
197 | } |
|
198 | } | |
198 |
|
199 | |||
199 | THEMES = [ |
|
200 | THEMES = [ | |
200 | ('md', 'Mystic Dark'), |
|
201 | ('md', 'Mystic Dark'), | |
201 | ('md_centered', 'Mystic Dark (centered)'), |
|
202 | ('md_centered', 'Mystic Dark (centered)'), | |
202 | ('sw', 'Snow White'), |
|
203 | ('sw', 'Snow White'), | |
203 | ('pg', 'Photon Gray'), |
|
204 | ('pg', 'Photon Gray'), | |
204 | ] |
|
205 | ] | |
205 |
|
206 | |||
206 | IMAGE_VIEWERS = [ |
|
207 | IMAGE_VIEWERS = [ | |
207 | ('simple', 'Simple'), |
|
208 | ('simple', 'Simple'), | |
208 | ('popup', 'Popup'), |
|
209 | ('popup', 'Popup'), | |
209 | ] |
|
210 | ] | |
210 |
|
211 | |||
211 | ALLOWED_HOSTS = ['*'] |
|
212 | ALLOWED_HOSTS = ['*'] | |
212 |
|
213 | |||
213 | POSTING_DELAY = 20 # seconds |
|
214 | POSTING_DELAY = 20 # seconds | |
214 |
|
215 | |||
215 | # Websocket settins |
|
216 | # Websocket settins | |
216 | CENTRIFUGE_HOST = 'localhost' |
|
217 | CENTRIFUGE_HOST = 'localhost' | |
217 | CENTRIFUGE_PORT = '9090' |
|
218 | CENTRIFUGE_PORT = '9090' | |
218 |
|
219 | |||
219 | CENTRIFUGE_ADDRESS = 'http://{}:{}'.format(CENTRIFUGE_HOST, CENTRIFUGE_PORT) |
|
220 | CENTRIFUGE_ADDRESS = 'http://{}:{}'.format(CENTRIFUGE_HOST, CENTRIFUGE_PORT) | |
220 | CENTRIFUGE_PROJECT_ID = '<project id here>' |
|
221 | CENTRIFUGE_PROJECT_ID = '<project id here>' | |
221 | CENTRIFUGE_PROJECT_SECRET = '<project secret here>' |
|
222 | CENTRIFUGE_PROJECT_SECRET = '<project secret here>' | |
222 | CENTRIFUGE_TIMEOUT = 5 |
|
223 | CENTRIFUGE_TIMEOUT = 5 | |
223 |
|
224 | |||
224 | # Debug middlewares |
|
225 | # Debug middlewares | |
225 | MIDDLEWARE_CLASSES += [ |
|
226 | MIDDLEWARE_CLASSES += [ | |
226 | 'debug_toolbar.middleware.DebugToolbarMiddleware', |
|
227 | 'debug_toolbar.middleware.DebugToolbarMiddleware', | |
227 | ] |
|
228 | ] | |
228 |
|
229 | |||
229 | def custom_show_toolbar(request): |
|
230 | def custom_show_toolbar(request): | |
230 | return request.user.has_perm('admin.debug') |
|
231 | return request.user.has_perm('admin.debug') | |
231 |
|
232 | |||
232 | DEBUG_TOOLBAR_CONFIG = { |
|
233 | DEBUG_TOOLBAR_CONFIG = { | |
233 | 'ENABLE_STACKTRACES': True, |
|
234 | 'ENABLE_STACKTRACES': True, | |
234 | 'SHOW_TOOLBAR_CALLBACK': 'neboard.settings.custom_show_toolbar', |
|
235 | 'SHOW_TOOLBAR_CALLBACK': 'neboard.settings.custom_show_toolbar', | |
235 | } |
|
236 | } | |
236 |
|
237 | |||
237 | # FIXME Uncommenting this fails somehow. Need to investigate this |
|
238 | # FIXME Uncommenting this fails somehow. Need to investigate this | |
238 | #DEBUG_TOOLBAR_PANELS += ( |
|
239 | #DEBUG_TOOLBAR_PANELS += ( | |
239 | # 'debug_toolbar.panels.profiling.ProfilingDebugPanel', |
|
240 | # 'debug_toolbar.panels.profiling.ProfilingDebugPanel', | |
240 | #) |
|
241 | #) |
General Comments 0
You need to be logged in to leave comments.
Login now