##// END OF EJS Templates
Merged with default branch
neko259 -
r1441:f2404e3c merge decentral
parent child Browse files
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', 'uid')
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', 'archived', 'ip',
59 list_display = ('id', 'op', 'title', 'reply_count', 'status', 'ip',
59 'display_tags')
60 'display_tags')
60 list_filter = ('bump_time', 'archived', 'bumpable')
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.10.0 BT
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 = false
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 SETTING_USERNAME, SETTING_LAST_NOTIFICATION_ID, SETTING_IMAGE_VIEWER
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, utils
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_setting(SETTING_USERNAME)
27 usernames = settings_manager.get_notification_usernames()
28 new_notifications_count = 0
28 new_notifications_count = 0
29 if username is not None and len(username) > 0:
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=None)
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_TAGS.match(username):
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 bytes"
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">&gt;{}</span>'.format(
151 result = '<span class="quote">&gt;{}</span>'.format(
150 value.replace(LINE_BREAK_HTML,
152 value.replace(LINE_BREAK_HTML,
151 '{}&gt;'.format(LINE_BREAK_HTML)))
153 '{}&gt;'.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 = 100000
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 content += chunk
51 file.write(chunk)
46
52
47 if response.status_code == HTTP_RESULT_OK and content:
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, include_archived=False, tags=None):
32 def get_random_images(self, count, tags=None):
31 images = self.filter(post_images__thread__archived=include_archived)
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 self.id:
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', 'bumpable'])
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', 'bumpable'])
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="{}">&gt;&gt;{}</a>'.format(self.get_absolute_url(),
436 result = '<a href="{}">&gt;&gt;{}</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 post.get_text(),post.poster_ip))
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, archived=None, bumpable=None) -> int:
66 def get_thread_count(self, status=None) -> int:
65 threads = self.get_threads()
67 threads = self.get_threads()
66 if archived is not None:
68 if status is not None:
67 threads = threads.filter(archived=archived)
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(archived=False, bumpable=True)
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(archived=False, bumpable=False)
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(archived=True)
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, archived=False):
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 archived is not None:
112 if status is not None:
113 posts = posts.filter(thread__archived=archived)
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.filter(archived=False).order_by('-bump_time')
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=['archived', 'last_edit_time', 'bumpable'])
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: str, last: int = None):
25 def get_notification_posts(self, usernames: list, last: int = None):
26 i_username = username.lower()
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__ = 'neko259'
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.filter(archived=False).order_by('-id')
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.filter(archived=False).order_by('-id')
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.attr('data-width');
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.attr('data-width');
112 var full_img_w = getFullImageWidth(imgElement);
82 var full_img_h = imgElement.attr('data-height');
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, $form, options) {
428 beforeSubmit: function(arr, form, options) {
443 showAsErrors($('#form'), gettext('Sending message...'));
429 showAsErrors(form, gettext('Sending message...'));
444 },
430 },
445 success: updateOnPost,
431 success: updateOnPost,
446 error: function() {
432 error: function() {
447 showAsErrors($('#form'), gettext('Server error!'));
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 'Related message' %}: <a href="{{ banner.post.get_absolute_url }}">>>{{ banner.post.id }}</a></div>
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 }}"/></a>
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">H</button>
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=tag.get_archived_thread_count %}{{ count }} archived thread{% plural %}archived threads{% endblocktrans %},
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 }} &gt;
96 {{ parent.get_view|safe }} &gt;
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 moderator=moderator thread=thread truncated=True need_open_link=True %}
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 moderator=moderator truncated=True %}
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 {% for role in values.roles %}
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' username %}" title="{% trans '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">&gt;{% trans 'Quote' %}</span>[/quote]</p>
17 <p>[quote]<span class="quote">&gt;{% trans 'Quote' %}</span>[/quote]</p>
18 <p>[quote source=src]<div class="multiquote"><div class="quote-header">src</div><div class="quote-text">{% trans 'Quote' %}</div></div><br />[/quote]</p>
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 'Related message' %}: <a href="{{ banner.post.get_absolute_url }}">>>{{ banner.post.id }}</a></div>
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 }}"/></a>
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 {% if is_favorite %}
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=tag.get_archived_thread_count %}{{ count }} archived thread{% plural %}archived threads{% endblocktrans %},
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 }} &gt;
74 {{ parent.get_view|safe }} &gt;
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 threads %}
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.number %}
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 {% trans 'Last update: ' %}<span id="last-update"><time datetime="{{ thread.last_edit_time|date:'c' }}">{{ thread.last_edit_time }}</time></span>
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 moderator=moderator reply_link=True %}
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 moderator=moderator mode_tree=True %}
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.filter(archived=False)))
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.filter(archived=False)
108 all_threads = Thread.objects.exclude(status=STATUS_ARCHIVE)
108
109
109 paginator = Paginator(Thread.objects.filter(archived=False),
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 and model id if object is a Django model.
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 bytes')
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_PREV_LINK = 'prev_page_link'
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 import json
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.post.sync import SyncManager
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).strip()
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().filter(archived=False)
142 threads = tag.get_threads().exclude(status=STATUS_ARCHIVE)
143 else:
143 else:
144 threads = Thread.objects.filter(archived=False)
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['bumpable'] = thread.can_bump()
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['authors'] = authors
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&amp;55@o11*8o'
96 SECRET_KEY = '@1rc$o(7=tt#kd+4s$u6wchm**z^)4x90)7f6z(i&amp;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