Show More
@@ -0,0 +1,32 b'' | |||||
|
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 b'' | |||||
|
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 b'' | |||||
|
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 b'' | |||||
|
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 b'' | |||||
|
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 b'' | |||||
|
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 b'' | |||||
|
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 b'' | |||||
|
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 b'' | |||||
|
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 |
@@ -35,3 +35,5 b' 4a5bec08ccfb47a27f9e98698f12dd5b7246623b' | |||||
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 |
@@ -12,7 +12,14 b' def get_paginator(*args, **kwargs):' | |||||
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] | |
@@ -67,8 +74,18 b' class DividedPaginator(Paginator):' | |||||
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 |
@@ -143,6 +143,14 b' class SettingsManager:' | |||||
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 |
@@ -10,7 +10,8 b' class PostAdmin(admin.ModelAdmin):' | |||||
10 | list_filter = ('pub_time',) |
|
10 | list_filter = ('pub_time',) | |
11 | search_fields = ('id', 'title', 'text', 'poster_ip') |
|
11 | search_fields = ('id', 'title', 'text', 'poster_ip') | |
12 | exclude = ('referenced_posts', 'refmap') |
|
12 | exclude = ('referenced_posts', 'refmap') | |
13 |
readonly_fields = ('poster_ip', 'threads', 'thread', 'images', |
|
13 | readonly_fields = ('poster_ip', 'threads', 'thread', 'images', | |
|
14 | 'attachments', 'uid', 'url', 'pub_time', 'opening') | |||
14 |
|
15 | |||
15 | def ban_poster(self, request, queryset): |
|
16 | def ban_poster(self, request, queryset): | |
16 | bans = 0 |
|
17 | bans = 0 | |
@@ -55,9 +56,9 b' class ThreadAdmin(admin.ModelAdmin):' | |||||
55 | def op(self, obj: Thread): |
|
56 | def op(self, obj: Thread): | |
56 | return obj.get_opening_post_id() |
|
57 | return obj.get_opening_post_id() | |
57 |
|
58 | |||
58 |
list_display = ('id', 'op', 'title', 'reply_count', ' |
|
59 | list_display = ('id', 'op', 'title', 'reply_count', 'status', 'ip', | |
59 | 'display_tags') |
|
60 | 'display_tags') | |
60 |
list_filter = ('bump_time', ' |
|
61 | list_filter = ('bump_time', 'status') | |
61 | search_fields = ('id', 'title') |
|
62 | search_fields = ('id', 'title') | |
62 | filter_horizontal = ('tags',) |
|
63 | filter_horizontal = ('tags',) | |
63 |
|
64 |
@@ -1,5 +1,5 b'' | |||||
1 | [Version] |
|
1 | [Version] | |
2 |
Version = 2.1 |
|
2 | Version = 2.11.0 Yuko | |
3 | SiteName = Neboard DEV |
|
3 | SiteName = Neboard DEV | |
4 |
|
4 | |||
5 | [Cache] |
|
5 | [Cache] | |
@@ -10,7 +10,8 b' CacheTimeout = 600' | |||||
10 | # Max post length in characters |
|
10 | # Max post length in characters | |
11 | MaxTextLength = 30000 |
|
11 | MaxTextLength = 30000 | |
12 | MaxFileSize = 8000000 |
|
12 | MaxFileSize = 8000000 | |
13 |
LimitPostingSpeed = |
|
13 | LimitPostingSpeed = true | |
|
14 | PowDifficulty = 20 | |||
14 |
|
15 | |||
15 | [Messages] |
|
16 | [Messages] | |
16 | # Thread bumplimit |
|
17 | # Thread bumplimit | |
@@ -24,6 +25,7 b' 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 | |
@@ -32,3 +34,6 b' ArchiveThreads = true' | |||||
32 | [External] |
|
34 | [External] | |
33 | # Thread update |
|
35 | # Thread update | |
34 | WebsocketsEnabled = false |
|
36 | WebsocketsEnabled = false | |
|
37 | ||||
|
38 | [RSS] | |||
|
39 | MaxItems = 20 |
@@ -1,39 +1,39 b'' | |||||
1 | from boards.abstracts.settingsmanager import get_settings_manager, \ |
|
1 | from boards.abstracts.settingsmanager import get_settings_manager, \ | |
2 |
|
|
2 | SETTING_LAST_NOTIFICATION_ID, SETTING_IMAGE_VIEWER | |
3 | from boards.models.user import Notification |
|
3 | from boards.models.user import Notification | |
4 |
|
4 | |||
5 | __author__ = 'neko259' |
|
5 | __author__ = 'neko259' | |
6 |
|
6 | |||
7 |
from boards import settings |
|
7 | from boards import settings | |
8 | from boards.models import Post, Tag |
|
8 | from boards.models import Post, Tag | |
9 |
|
9 | |||
10 | CONTEXT_SITE_NAME = 'site_name' |
|
10 | CONTEXT_SITE_NAME = 'site_name' | |
11 | CONTEXT_VERSION = 'version' |
|
11 | CONTEXT_VERSION = 'version' | |
12 | CONTEXT_MODERATOR = 'moderator' |
|
|||
13 | CONTEXT_THEME_CSS = 'theme_css' |
|
12 | CONTEXT_THEME_CSS = 'theme_css' | |
14 | CONTEXT_THEME = 'theme' |
|
13 | CONTEXT_THEME = 'theme' | |
15 | CONTEXT_PPD = 'posts_per_day' |
|
14 | CONTEXT_PPD = 'posts_per_day' | |
16 | CONTEXT_TAGS = 'tags' |
|
15 | CONTEXT_TAGS = 'tags' | |
17 | CONTEXT_USER = 'user' |
|
16 | CONTEXT_USER = 'user' | |
18 | CONTEXT_NEW_NOTIFICATIONS_COUNT = 'new_notifications_count' |
|
17 | CONTEXT_NEW_NOTIFICATIONS_COUNT = 'new_notifications_count' | |
19 | CONTEXT_USERNAME = 'username' |
|
18 | CONTEXT_USERNAMES = 'usernames' | |
20 | CONTEXT_TAGS_STR = 'tags_str' |
|
19 | CONTEXT_TAGS_STR = 'tags_str' | |
21 | CONTEXT_IMAGE_VIEWER = 'image_viewer' |
|
20 | CONTEXT_IMAGE_VIEWER = 'image_viewer' | |
22 | CONTEXT_HAS_FAV_THREADS = 'has_fav_threads' |
|
21 | CONTEXT_HAS_FAV_THREADS = 'has_fav_threads' | |
|
22 | CONTEXT_POW_DIFFICULTY = 'pow_difficulty' | |||
23 |
|
23 | |||
24 |
|
24 | |||
25 | def get_notifications(context, request): |
|
25 | def get_notifications(context, request): | |
26 | settings_manager = get_settings_manager(request) |
|
26 | settings_manager = get_settings_manager(request) | |
27 |
username = settings_manager.get_ |
|
27 | usernames = settings_manager.get_notification_usernames() | |
28 | new_notifications_count = 0 |
|
28 | new_notifications_count = 0 | |
29 |
if username is not None |
|
29 | if usernames is not None: | |
30 | last_notification_id = settings_manager.get_setting( |
|
30 | last_notification_id = settings_manager.get_setting( | |
31 | SETTING_LAST_NOTIFICATION_ID) |
|
31 | SETTING_LAST_NOTIFICATION_ID) | |
32 |
|
32 | |||
33 | new_notifications_count = Notification.objects.get_notification_posts( |
|
33 | new_notifications_count = Notification.objects.get_notification_posts( | |
34 | username=username, last=last_notification_id).count() |
|
34 | usernames=usernames, last=last_notification_id).count() | |
35 | context[CONTEXT_NEW_NOTIFICATIONS_COUNT] = new_notifications_count |
|
35 | context[CONTEXT_NEW_NOTIFICATIONS_COUNT] = new_notifications_count | |
36 | context[CONTEXT_USERNAME] = username |
|
36 | context[CONTEXT_USERNAMES] = usernames | |
37 |
|
37 | |||
38 |
|
38 | |||
39 | def user_and_ui_processor(request): |
|
39 | def user_and_ui_processor(request): | |
@@ -50,12 +50,12 b' def user_and_ui_processor(request):' | |||||
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')) |
@@ -2,6 +2,7 b' 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 | |
@@ -9,6 +10,7 b' from django.core.files.uploadedfile impo' | |||||
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 | |
@@ -20,7 +22,11 b' 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 | |||
@@ -82,7 +88,7 b' class FormatPanel(forms.Textarea):' | |||||
82 | formatter.preview_right + '</span>' |
|
88 | formatter.preview_right + '</span>' | |
83 |
|
89 | |||
84 | output += '</div>' |
|
90 | output += '</div>' | |
85 |
output += super(FormatPanel, self).render(name, value, attrs= |
|
91 | output += super(FormatPanel, self).render(name, value, attrs=attrs) | |
86 |
|
92 | |||
87 | return output |
|
93 | return output | |
88 |
|
94 | |||
@@ -168,6 +174,10 b' class PostForm(NeboardForm):' | |||||
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 | |||
@@ -238,7 +248,7 b' class PostForm(NeboardForm):' | |||||
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): | |
@@ -256,8 +266,13 b' class PostForm(NeboardForm):' | |||||
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 | |||
@@ -341,8 +356,26 b' class PostForm(NeboardForm):' | |||||
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): | |
@@ -350,6 +383,7 b' class ThreadForm(PostForm):' | |||||
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() | |
@@ -385,6 +419,9 b' class ThreadForm(PostForm):' | |||||
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 | |||
@@ -396,7 +433,7 b' class SettingsForm(NeboardForm):' | |||||
396 | def clean_username(self): |
|
433 | def clean_username(self): | |
397 | username = self.cleaned_data['username'] |
|
434 | username = self.cleaned_data['username'] | |
398 |
|
435 | |||
399 |
if username and not REGEX_ |
|
436 | if username and not REGEX_USERNAMES.match(username): | |
400 | raise forms.ValidationError(_('Inappropriate characters.')) |
|
437 | raise forms.ValidationError(_('Inappropriate characters.')) | |
401 |
|
438 | |||
402 | return username |
|
439 | return username |
1 | NO CONTENT: modified file, binary diff hidden |
|
NO CONTENT: modified file, binary diff hidden |
@@ -143,8 +143,8 b' 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" | |
@@ -488,8 +488,8 b' msgstr "\xd0\x9e\xd0\xba"' | |||||
488 |
|
488 | |||
489 | #: utils.py:120 |
|
489 | #: utils.py:120 | |
490 | #, python-format |
|
490 | #, python-format | |
491 |
msgid "File must be less than %s b |
|
491 | msgid "File must be less than %s but is %s." | |
492 |
msgstr "Файл должен быть менее %s |
|
492 | msgstr "Файл должен быть менее %s, но его размер %s." | |
493 |
|
493 | |||
494 | msgid "Please wait %(delay)d second before sending message" |
|
494 | msgid "Please wait %(delay)d second before sending message" | |
495 | msgid_plural "Please wait %(delay)d seconds before sending message" |
|
495 | msgid_plural "Please wait %(delay)d seconds before sending message" | |
@@ -499,3 +499,34 b' msgstr[2] "\xd0\x9f\xd0\xbe\xd0\xb6\xd0\xb0\xd0\xbb\xd1\x83\xd0\xb9\xd1\x81\xd1\x82\xd0\xb0 \xd0\xbf\xd0\xbe\xd0\xb4\xd0\xbe\xd0\xb6\xd0\xb4\xd0\xb8\xd1\x82\xd0\xb5 %(delay)d \xd1\x81\xd0\xb5\xd0\xba\xd1\x83\xd0\xbd\xd0\xb4 \xd0\xbf\xd0\xb5\xd1\x80\xd0\xb5\xd0\xb4 \xd0\xbe\xd1\x82\xd0\xbf\xd1\x80\xd0\xb0\xd0\xb2\xd0\xba\xd0\xbe\xd0\xb9 \xd1\x81\xd0\xbe\xd0\xbe\xd0\xb1\xd1\x89\xd0\xb5\xd0\xbd\xd0\xb8\xd1\x8f"' | |||||
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 |
@@ -53,3 +53,5 b' msgstr "\xd0\x9e\xd1\x82\xd0\xbf\xd1\x80\xd0\xb0\xd0\xb2\xd0\xba\xd0\xb0 \xd1\x81\xd0\xbe\xd0\xbe\xd0\xb1\xd1\x89\xd0\xb5\xd0\xbd\xd0\xb8\xd1\x8f..."' | |||||
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 |
@@ -13,7 +13,7 b' class Command(BaseCommand):' | |||||
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() |
@@ -141,6 +141,8 b' def render_quote(tag_name, value, option' | |||||
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) |
@@ -1,4 +1,7 b'' | |||||
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 |
@@ -38,4 +38,5 b' class Attachment(models.Model):' | |||||
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,7 +1,8 b'' | |||||
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 | |||
@@ -14,9 +15,9 b' HTTP_RESULT_OK = 200' | |||||
14 | HEADER_CONTENT_LENGTH = 'content-length' |
|
15 | HEADER_CONTENT_LENGTH = 'content-length' | |
15 | HEADER_CONTENT_TYPE = 'content-type' |
|
16 | HEADER_CONTENT_TYPE = 'content-type' | |
16 |
|
17 | |||
17 |
FILE_DOWNLOAD_CHUNK_BYTES = |
|
18 | FILE_DOWNLOAD_CHUNK_BYTES = 200000 | |
18 |
|
19 | |||
19 | YOUTUBE_URL = re.compile(r'https?://www\.youtube\.com/watch\?v=\w+') |
|
20 | YOUTUBE_URL = re.compile(r'https?://(www\.youtube\.com/watch\?v=|youtu.be/)\w+') | |
20 |
|
21 | |||
21 |
|
22 | |||
22 | class Downloader: |
|
23 | class Downloader: | |
@@ -38,17 +39,19 b' class Downloader:' | |||||
38 |
|
39 | |||
39 | # Download file, stop if the size exceeds limit |
|
40 | # Download file, stop if the size exceeds limit | |
40 | size = 0 |
|
41 | size = 0 | |
41 | content = b'' |
|
42 | ||
|
43 | # Set a dummy file name that will be replaced | |||
|
44 | # anyway, just keep the valid extension | |||
|
45 | filename = 'file.' + content_type.split('/')[1] | |||
|
46 | ||||
|
47 | file = TemporaryUploadedFile(filename, content_type, 0, None, None) | |||
42 | for chunk in response.iter_content(FILE_DOWNLOAD_CHUNK_BYTES): |
|
48 | for chunk in response.iter_content(FILE_DOWNLOAD_CHUNK_BYTES): | |
43 | size += len(chunk) |
|
49 | size += len(chunk) | |
44 | validate_file_size(size) |
|
50 | validate_file_size(size) | |
45 |
|
|
51 | file.write(chunk) | |
46 |
|
52 | |||
47 |
if response.status_code == HTTP_RESULT_OK |
|
53 | if response.status_code == HTTP_RESULT_OK: | |
48 | # Set a dummy file name that will be replaced |
|
54 | return file | |
49 | # anyway, just keep the valid extension |
|
|||
50 | filename = 'file.' + content_type.split('/')[1] |
|
|||
51 | return SimpleUploadedFile(filename, content, content_type) |
|
|||
52 |
|
55 | |||
53 |
|
56 | |||
54 | class YouTubeDownloader(Downloader): |
|
57 | class YouTubeDownloader(Downloader): |
@@ -3,8 +3,11 b' from django.db import models' | |||||
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() |
@@ -4,8 +4,10 b' from django.template.defaultfilters impo' | |||||
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 | |||
@@ -27,8 +29,8 b' class PostImageManager(models.Manager):' | |||||
27 |
|
29 | |||
28 | return post_image |
|
30 | return post_image | |
29 |
|
31 | |||
30 |
def get_random_images(self, count, |
|
32 | def get_random_images(self, count, tags=None): | |
31 |
images = self. |
|
33 | images = self.exclude(post_images__thread__status=STATUS_ARCHIVE) | |
32 | if tags is not None: |
|
34 | if tags is not None: | |
33 | images = images.filter(post_images__threads__tags__in=tags) |
|
35 | images = images.filter(post_images__threads__tags__in=tags) | |
34 | return images.order_by('?')[:count] |
|
36 | return images.order_by('?')[:count] |
@@ -23,6 +23,7 b" 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 | |||
@@ -46,7 +47,6 b" 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' | |
@@ -56,10 +56,10 b" PARAMETER_NEED_OP_DATA = 'need_op_data'" | |||||
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 | |||
@@ -185,12 +185,14 b' class Post(models.Model, Viewable):' | |||||
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: | |
@@ -332,20 +334,29 b' class Post(models.Model, Viewable):' | |||||
332 |
|
334 | |||
333 | def save(self, force_insert=False, force_update=False, using=None, |
|
335 | def save(self, force_insert=False, force_update=False, using=None, | |
334 | update_fields=None): |
|
336 | update_fields=None): | |
|
337 | new_post = self.id is None | |||
|
338 | ||||
335 | self._text_rendered = Parser().parse(self.get_raw_text()) |
|
339 | self._text_rendered = Parser().parse(self.get_raw_text()) | |
336 |
|
340 | |||
337 | self.uid = str(uuid.uuid4()) |
|
341 | self.uid = str(uuid.uuid4()) | |
338 | if update_fields is not None and 'uid' not in update_fields: |
|
342 | if update_fields is not None and 'uid' not in update_fields: | |
339 | update_fields += ['uid'] |
|
343 | update_fields += ['uid'] | |
340 |
|
344 | |||
341 |
if |
|
345 | if not new_post: | |
342 | for thread in self.get_threads().all(): |
|
346 | for thread in self.get_threads().all(): | |
343 | thread.last_edit_time = self.last_edit_time |
|
347 | thread.last_edit_time = self.last_edit_time | |
344 |
|
348 | |||
345 |
thread.save(update_fields=['last_edit_time', ' |
|
349 | thread.save(update_fields=['last_edit_time', 'status']) | |
346 |
|
350 | |||
347 | super().save(force_insert, force_update, using, update_fields) |
|
351 | super().save(force_insert, force_update, using, update_fields) | |
348 |
|
352 | |||
|
353 | # Post save triggers | |||
|
354 | if new_post: | |||
|
355 | self.build_url() | |||
|
356 | ||||
|
357 | self._connect_replies() | |||
|
358 | self._connect_notifications() | |||
|
359 | ||||
349 | def get_text(self) -> str: |
|
360 | def get_text(self) -> str: | |
350 | return self._text_rendered |
|
361 | return self._text_rendered | |
351 |
|
362 | |||
@@ -380,12 +391,12 b' class Post(models.Model, Viewable):' | |||||
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 | """ | |
@@ -411,7 +422,7 b' class Post(models.Model, Viewable):' | |||||
411 | thread.update_bump_status() |
|
422 | thread.update_bump_status() | |
412 |
|
423 | |||
413 | thread.last_edit_time = self.last_edit_time |
|
424 | thread.last_edit_time = self.last_edit_time | |
414 |
thread.save(update_fields=['last_edit_time', ' |
|
425 | thread.save(update_fields=['last_edit_time', 'status']) | |
415 | self.threads.add(opening_post.get_thread()) |
|
426 | self.threads.add(opening_post.get_thread()) | |
416 |
|
427 | |||
417 | def get_tripcode(self): |
|
428 | def get_tripcode(self): |
@@ -1,3 +1,5 b'' | |||||
|
1 | from django.contrib.auth.context_processors import PermWrapper | |||
|
2 | ||||
1 | from boards import utils |
|
3 | from boards import utils | |
2 |
|
4 | |||
3 |
|
5 | |||
@@ -24,7 +26,7 b' class HtmlExporter(Exporter):' | |||||
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,13 +31,15 b' 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: | |
@@ -52,7 +54,8 b' class PostManager(models.Manager):' | |||||
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 | |
@@ -72,7 +75,7 b' class PostManager(models.Manager):' | |||||
72 | logger = logging.getLogger('boards.post.create') |
|
75 | logger = logging.getLogger('boards.post.create') | |
73 |
|
76 | |||
74 | logger.info('Created post [{}] with text [{}] by {}'.format(post, |
|
77 | logger.info('Created post [{}] with text [{}] by {}'.format(post, | |
75 |
|
|
78 | post.get_text(),post.poster_ip)) | |
76 |
|
79 | |||
77 | # TODO Move this to other place |
|
80 | # TODO Move this to other place | |
78 | if file: |
|
81 | if file: | |
@@ -82,10 +85,7 b' class PostManager(models.Manager):' | |||||
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 | |
@@ -147,6 +147,3 b' class PostManager(models.Manager):' | |||||
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() |
|
@@ -4,7 +4,9 b' 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 | |||
@@ -61,22 +63,20 b' class Tag(models.Model, Viewable):' | |||||
61 |
|
63 | |||
62 | return self.get_thread_count() == 0 |
|
64 | return self.get_thread_count() == 0 | |
63 |
|
65 | |||
64 |
def get_thread_count(self, |
|
66 | def get_thread_count(self, status=None) -> int: | |
65 | threads = self.get_threads() |
|
67 | threads = self.get_threads() | |
66 |
if |
|
68 | if status is not None: | |
67 |
threads = threads.filter( |
|
69 | threads = threads.filter(status=status) | |
68 | if bumpable is not None: |
|
|||
69 | threads = threads.filter(bumpable=bumpable) |
|
|||
70 | return threads.count() |
|
70 | return threads.count() | |
71 |
|
71 | |||
72 | def get_active_thread_count(self) -> int: |
|
72 | def get_active_thread_count(self) -> int: | |
73 |
return self.get_thread_count( |
|
73 | return self.get_thread_count(status=STATUS_ACTIVE) | |
74 |
|
74 | |||
75 | def get_bumplimit_thread_count(self) -> int: |
|
75 | def get_bumplimit_thread_count(self) -> int: | |
76 |
return self.get_thread_count( |
|
76 | return self.get_thread_count(status=STATUS_BUMPLIMIT) | |
77 |
|
77 | |||
78 | def get_archived_thread_count(self) -> int: |
|
78 | def get_archived_thread_count(self) -> int: | |
79 |
return self.get_thread_count( |
|
79 | return self.get_thread_count(status=STATUS_ARCHIVE) | |
80 |
|
80 | |||
81 | def get_absolute_url(self): |
|
81 | def get_absolute_url(self): | |
82 | return reverse('tag', kwargs={'tag_name': self.name}) |
|
82 | return reverse('tag', kwargs={'tag_name': self.name}) | |
@@ -106,11 +106,11 b' class Tag(models.Model, Viewable):' | |||||
106 | def get_description(self): |
|
106 | def get_description(self): | |
107 | return self.description |
|
107 | return self.description | |
108 |
|
108 | |||
109 |
def get_random_image_post(self, |
|
109 | def get_random_image_post(self, status=[STATUS_ACTIVE, STATUS_BUMPLIMIT]): | |
110 | posts = boards.models.Post.objects.annotate(images_count=Count( |
|
110 | posts = boards.models.Post.objects.annotate(images_count=Count( | |
111 | 'images')).filter(images_count__gt=0, threads__tags__in=[self]) |
|
111 | 'images')).filter(images_count__gt=0, threads__tags__in=[self]) | |
112 |
if |
|
112 | if status is not None: | |
113 |
posts = posts.filter(thread__ |
|
113 | posts = posts.filter(thread__status__in=status) | |
114 | return posts.order_by('?').first() |
|
114 | return posts.order_by('?').first() | |
115 |
|
115 | |||
116 | def get_first_letter(self): |
|
116 | def get_first_letter(self): | |
@@ -141,3 +141,7 b' class Tag(models.Model, Viewable):' | |||||
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 |
@@ -5,6 +5,8 b' from django.db.models import Count, Sum,' | |||||
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 | |
@@ -25,6 +27,12 b" WS_NOTIFICATION_TYPE = 'notification_typ" | |||||
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): | |
@@ -33,7 +41,7 b' class ThreadManager(models.Manager):' | |||||
33 | archive or delete the old ones. |
|
41 | archive or delete the old ones. | |
34 | """ |
|
42 | """ | |
35 |
|
43 | |||
36 |
threads = Thread.objects. |
|
44 | threads = Thread.objects.exclude(status=STATUS_ARCHIVE).order_by('-bump_time') | |
37 | thread_count = threads.count() |
|
45 | thread_count = threads.count() | |
38 |
|
46 | |||
39 | max_thread_count = settings.get_int('Messages', 'MaxThreadCount') |
|
47 | max_thread_count = settings.get_int('Messages', 'MaxThreadCount') | |
@@ -50,11 +58,10 b' class ThreadManager(models.Manager):' | |||||
50 | logger.info('Processed %d old threads' % num_threads_to_delete) |
|
58 | logger.info('Processed %d old threads' % num_threads_to_delete) | |
51 |
|
59 | |||
52 | def _archive_thread(self, thread): |
|
60 | def _archive_thread(self, thread): | |
53 | thread.archived = True |
|
61 | thread.status = STATUS_ARCHIVE | |
54 | thread.bumpable = False |
|
|||
55 | thread.last_edit_time = timezone.now() |
|
62 | thread.last_edit_time = timezone.now() | |
56 | thread.update_posts_time() |
|
63 | thread.update_posts_time() | |
57 |
thread.save(update_fields=[' |
|
64 | thread.save(update_fields=['last_edit_time', 'status']) | |
58 |
|
65 | |||
59 | def get_new_posts(self, datas): |
|
66 | def get_new_posts(self, datas): | |
60 | query = None |
|
67 | query = None | |
@@ -90,9 +97,10 b' class Thread(models.Model):' | |||||
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 | """ | |
@@ -118,7 +126,7 b' class Thread(models.Model):' | |||||
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): | |
@@ -138,7 +146,7 b' class Thread(models.Model):' | |||||
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 | """ | |
@@ -255,4 +263,10 b' class Thread(models.Model):' | |||||
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 |
@@ -22,10 +22,10 b' class Ban(models.Model):' | |||||
22 |
|
22 | |||
23 |
|
23 | |||
24 | class NotificationManager(models.Manager): |
|
24 | class NotificationManager(models.Manager): | |
25 |
def get_notification_posts(self, username: st |
|
25 | def get_notification_posts(self, usernames: list, last: int = None): | |
26 |
|
|
26 | lower_names = [username.lower() for username in usernames] | |
27 |
|
27 | posts = boards.models.post.Post.objects.filter( | ||
28 | posts = boards.models.post.Post.objects.filter(notification__name=i_username) |
|
28 | notification__name__in=lower_names).distinct() | |
29 | if last is not None: |
|
29 | if last is not None: | |
30 | posts = posts.filter(id__gt=last) |
|
30 | posts = posts.filter(id__gt=last) | |
31 | posts = posts.order_by('-id') |
|
31 | posts = posts.order_by('-id') |
@@ -3,8 +3,12 b' from django.core.urlresolvers import rev' | |||||
3 | from django.shortcuts import get_object_or_404 |
|
3 | from django.shortcuts import get_object_or_404 | |
4 | from boards.models import Post, Tag, Thread |
|
4 | from boards.models import Post, Tag, Thread | |
5 | from boards import settings |
|
5 | from boards import settings | |
|
6 | from boards.models.thread import STATUS_ARCHIVE | |||
6 |
|
7 | |||
7 |
__author__ = 'neko |
|
8 | __author__ = 'nekorin' | |
|
9 | ||||
|
10 | ||||
|
11 | MAX_ITEMS = settings.get_int('RSS', 'MaxItems') | |||
8 |
|
12 | |||
9 |
|
13 | |||
10 | # TODO Make tests for all of these |
|
14 | # TODO Make tests for all of these | |
@@ -15,7 +19,7 b' class AllThreadsFeed(Feed):' | |||||
15 | description_template = 'boards/rss/post.html' |
|
19 | description_template = 'boards/rss/post.html' | |
16 |
|
20 | |||
17 | def items(self): |
|
21 | def items(self): | |
18 |
return Thread.objects. |
|
22 | return Thread.objects.exclude(status=STATUS_ARCHIVE).order_by('-id')[:MAX_ITEMS] | |
19 |
|
23 | |||
20 | def item_title(self, item): |
|
24 | def item_title(self, item): | |
21 | return item.get_opening_post().title |
|
25 | return item.get_opening_post().title | |
@@ -33,7 +37,7 b' class TagThreadsFeed(Feed):' | |||||
33 | description_template = 'boards/rss/post.html' |
|
37 | description_template = 'boards/rss/post.html' | |
34 |
|
38 | |||
35 | def items(self, obj): |
|
39 | def items(self, obj): | |
36 |
return obj.threads. |
|
40 | return obj.get_threads().exclude(status=STATUS_ARCHIVE).order_by('-id')[:MAX_ITEMS] | |
37 |
|
41 | |||
38 | def get_object(self, request, tag_name): |
|
42 | def get_object(self, request, tag_name): | |
39 | return get_object_or_404(Tag, name=tag_name) |
|
43 | return get_object_or_404(Tag, name=tag_name) | |
@@ -57,7 +61,7 b' class ThreadPostsFeed(Feed):' | |||||
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) |
@@ -90,6 +90,7 b' textarea, input {' | |||||
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 { | |
@@ -151,3 +152,8 b' textarea, input {' | |||||
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 | } |
@@ -388,10 +388,6 b' li {' | |||||
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 | } | |
@@ -566,7 +562,6 b' ul {' | |||||
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 | |||
@@ -577,3 +572,7 b' ul {' | |||||
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 | } |
@@ -302,10 +302,6 b' input[type="submit"]:hover {' | |||||
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 | } | |
@@ -380,4 +376,8 b' input[type="submit"]:hover {' | |||||
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 | } |
@@ -279,10 +279,6 b' li {' | |||||
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 | } | |
@@ -416,3 +412,7 b' li {' | |||||
416 | audio { |
|
412 | audio { | |
417 | margin-top: 1em; |
|
413 | margin-top: 1em; | |
418 | } |
|
414 | } | |
|
415 | ||||
|
416 | .post-blink { | |||
|
417 | background-color: #ccc; | |||
|
418 | } |
@@ -22,7 +22,7 b'' | |||||
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 | |||
@@ -40,4 +40,56 b" var form = $('#form');" | |||||
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 | }); |
@@ -36,6 +36,37 b" var FULL_IMG_CLASS = 'post-image-full';" | |||||
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 | |||
@@ -48,8 +79,8 b' SimpleImageViewer.prototype.view = funct' | |||||
48 | if (images.length == 1) { |
|
79 | if (images.length == 1) { | |
49 | var thumb = images.first(); |
|
80 | var thumb = images.first(); | |
50 |
|
81 | |||
51 |
var width = thumb |
|
82 | var width = getFullImageWidth(thumb); | |
52 | var height = thumb.attr('data-height'); |
|
83 | var height = getFullImageHeight(thumb); | |
53 |
|
84 | |||
54 | if (width == null || height == null) { |
|
85 | if (width == null || height == null) { | |
55 | width = '100%'; |
|
86 | width = '100%'; | |
@@ -76,10 +107,10 b' PopupImageViewer.prototype.view = functi' | |||||
76 |
|
107 | |||
77 | var existingPopups = $('#' + thumb_id); |
|
108 | var existingPopups = $('#' + thumb_id); | |
78 | if (!existingPopups.length) { |
|
109 | if (!existingPopups.length) { | |
79 | var imgElement= el.find('img'); |
|
110 | var imgElement = el.find('img'); | |
80 |
|
111 | |||
81 |
var full_img_w = imgElement |
|
112 | var full_img_w = getFullImageWidth(imgElement); | |
82 |
var full_img_h = imgElement |
|
113 | var full_img_h = getFullImageHeight(imgElement); | |
83 |
|
114 | |||
84 | var win = $(window); |
|
115 | var win = $(window); | |
85 |
|
116 | |||
@@ -156,16 +187,6 b' PopupImageViewer.prototype.view = functi' | |||||
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) { |
@@ -24,6 +24,7 b'' | |||||
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 | |
@@ -108,6 +109,36 b' function initFavPanel() {' | |||||
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 | |||
@@ -123,4 +154,7 b' function initFavPanel() {' | |||||
123 | highlightCode($(document)); |
|
154 | highlightCode($(document)); | |
124 |
|
155 | |||
125 | initFavPanel(); |
|
156 | initFavPanel(); | |
|
157 | ||||
|
158 | var volumeUsers = $("video,audio"); | |||
|
159 | processVolumeUser(volumeUsers); | |||
126 | }); |
|
160 | }); |
@@ -18,9 +18,8 b' function $each(list, fn) {' | |||||
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 |
@@ -28,7 +28,11 b" var CLASS_POST = '.post'" | |||||
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', | |
@@ -45,6 +49,7 b" 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 | |
@@ -195,12 +200,7 b' function updatePost(postHtml) {' | |||||
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() { | |
@@ -352,26 +352,12 b' function updateOnPost(response, statusTe' | |||||
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 | |||
@@ -430,7 +416,7 b' function updateNodeAttr(oldNode, newNode' | |||||
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'); | |
@@ -439,14 +425,15 b' function updateNodeAttr(oldNode, newNode' | |||||
439 |
|
425 | |||
440 | if (form.length > 0) { |
|
426 | if (form.length > 0) { | |
441 | var options = { |
|
427 | var options = { | |
442 |
beforeSubmit: function(arr, |
|
428 | beforeSubmit: function(arr, form, options) { | |
443 |
showAsErrors( |
|
429 | showAsErrors(form, gettext('Sending message...')); | |
444 | }, |
|
430 | }, | |
445 | success: updateOnPost, |
|
431 | success: updateOnPost, | |
446 | error: function() { |
|
432 | error: function() { | |
447 |
showAsErrors( |
|
433 | showAsErrors(form, gettext('Server error!')); | |
448 | }, |
|
434 | }, | |
449 | url: '/api/add_post/' + threadId + '/' |
|
435 | url: '/api/add_post/' + threadId + '/', | |
|
436 | timeout: POST_AJAX_TIMEOUT | |||
450 | }; |
|
437 | }; | |
451 |
|
438 | |||
452 | form.ajaxForm(options); |
|
439 | form.ajaxForm(options); |
@@ -31,8 +31,8 b'' | |||||
31 | {% for banner in banners %} |
|
31 | {% for banner in banners %} | |
32 | <div class="post"> |
|
32 | <div class="post"> | |
33 | <div class="title">{{ banner.title }}</div> |
|
33 | <div class="title">{{ banner.title }}</div> | |
34 | <div>{{ banner.text }}</div> |
|
34 | <div>{{ banner.get_text|safe }}</div> | |
35 |
<div>{% trans ' |
|
35 | <div>{% trans 'Details' %}: <a href="{{ banner.post.get_absolute_url }}">>>{{ banner.post.id }}</a></div> | |
36 | </div> |
|
36 | </div> | |
37 | {% endfor %} |
|
37 | {% endfor %} | |
38 |
|
38 | |||
@@ -44,38 +44,50 b'' | |||||
44 | <a href="{{ random_image_post.get_absolute_url }}"><img |
|
44 | <a href="{{ random_image_post.get_absolute_url }}"><img | |
45 | src="{{ image.image.url_200x150 }}" |
|
45 | src="{{ image.image.url_200x150 }}" | |
46 | width="{{ image.pre_width }}" |
|
46 | width="{{ image.pre_width }}" | |
47 |
height="{{ image.pre_height }}" |
|
47 | height="{{ image.pre_height }}" | |
|
48 | alt="{{ random_image_post.id }}"/></a> | |||
48 | {% endwith %} |
|
49 | {% endwith %} | |
49 | </div> |
|
50 | </div> | |
50 | {% endif %} |
|
51 | {% endif %} | |
51 | <div class="tag-text-data"> |
|
52 | <div class="tag-text-data"> | |
52 | <h2> |
|
53 | <h2> | |
|
54 | /{{ tag.get_view|safe }}/ | |||
|
55 | {% if perms.change_tag %} | |||
|
56 | <span class="moderator_info">| <a href="{% url 'admin:boards_tag_change' tag.id %}">{% trans 'Edit tag' %}</a></span> | |||
|
57 | {% endif %} | |||
|
58 | </h2> | |||
|
59 | <p> | |||
53 | <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form"> |
|
60 | <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form"> | |
54 | {% if is_favorite %} |
|
61 | {% if is_favorite %} | |
55 | <button name="method" value="unsubscribe" class="fav">★</button> |
|
62 | <button name="method" value="unsubscribe" class="fav">★ {% trans "Remove from favorites" %}</button> | |
56 | {% else %} |
|
63 | {% else %} | |
57 | <button name="method" value="subscribe" class="not_fav">★</button> |
|
64 | <button name="method" value="subscribe" class="not_fav">★ {% trans "Add to favorites" %}</button> | |
58 | {% endif %} |
|
65 | {% endif %} | |
59 | </form> |
|
66 | </form> | |
60 | <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form"> |
|
67 | <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form"> | |
61 | {% if is_hidden %} |
|
68 | {% if is_hidden %} | |
62 |
<button name="method" value="unhide" class="fav"> |
|
69 | <button name="method" value="unhide" class="fav">{% trans "Show" %}</button> | |
63 | {% else %} |
|
70 | {% else %} | |
64 | <button name="method" value="hide" class="not_fav">H</button> |
|
71 | <button name="method" value="hide" class="not_fav">{% trans "Hide" %}</button> | |
65 | {% endif %} |
|
72 | {% endif %} | |
66 | </form> |
|
73 | </form> | |
67 | {{ tag.get_view|safe }} |
|
74 | <a href="{% url 'tag_gallery' tag.name %}">{% trans 'Gallery' %}</a> | |
68 | {% if moderator %} |
|
75 | </p> | |
69 | <span class="moderator_info">| <a href="{% url 'admin:boards_tag_change' tag.id %}">{% trans 'Edit tag' %}</a></span> |
|
|||
70 | {% endif %} |
|
|||
71 | </h2> |
|
|||
72 | {% if tag.get_description %} |
|
76 | {% if tag.get_description %} | |
73 | <p>{{ tag.get_description|safe }}</p> |
|
77 | <p>{{ tag.get_description|safe }}</p> | |
74 | {% endif %} |
|
78 | {% endif %} | |
75 | <p> |
|
79 | <p> | |
76 | {% blocktrans count count=tag.get_active_thread_count %}{{ count }} active thread{% plural %}active threads{% endblocktrans %}, |
|
80 | {% with active_count=tag.get_active_thread_count bumplimit_count=tag.get_bumplimit_thread_count archived_count=tag.get_archived_thread_count %} | |
77 | {% blocktrans count count=tag.get_bumplimit_thread_count %}{{ count }} thread in bumplimit{% plural %} threads in bumplimit{% endblocktrans %}, |
|
81 | {% if active_count %} | |
78 |
{% blocktrans count count= |
|
82 | {% blocktrans count count=active_count %}{{ count }} active thread{% plural %}active threads{% endblocktrans %}, | |
|
83 | {% endif %} | |||
|
84 | {% if bumplimit_count %} | |||
|
85 | {% blocktrans count count=bumplimit_count %}{{ count }} thread in bumplimit{% plural %} threads in bumplimit{% endblocktrans %}, | |||
|
86 | {% endif %} | |||
|
87 | {% if archived_count %} | |||
|
88 | {% blocktrans count count=archived_count %}{{ count }} archived thread{% plural %}archived threads{% endblocktrans %}, | |||
|
89 | {% endif %} | |||
|
90 | {% endwith %} | |||
79 | {% blocktrans count count=tag.get_post_count %}{{ count }} message{% plural %}messages{% endblocktrans %}. |
|
91 | {% blocktrans count count=tag.get_post_count %}{{ count }} message{% plural %}messages{% endblocktrans %}. | |
80 | </p> |
|
92 | </p> | |
81 | {% if tag.get_all_parents %} |
|
93 | {% if tag.get_all_parents %} | |
@@ -99,7 +111,7 b'' | |||||
99 |
|
111 | |||
100 | {% for thread in threads %} |
|
112 | {% for thread in threads %} | |
101 | <div class="thread"> |
|
113 | <div class="thread"> | |
102 |
{% post_view thread.get_opening_post |
|
114 | {% post_view thread.get_opening_post thread=thread truncated=True need_open_link=True %} | |
103 | {% if not thread.archived %} |
|
115 | {% if not thread.archived %} | |
104 | {% with last_replies=thread.get_last_replies %} |
|
116 | {% with last_replies=thread.get_last_replies %} | |
105 | {% if last_replies %} |
|
117 | {% if last_replies %} | |
@@ -114,7 +126,7 b'' | |||||
114 | {% endwith %} |
|
126 | {% endwith %} | |
115 | <div class="last-replies"> |
|
127 | <div class="last-replies"> | |
116 | {% for post in last_replies %} |
|
128 | {% for post in last_replies %} | |
117 |
{% post_view post |
|
129 | {% post_view post truncated=True %} | |
118 | {% endfor %} |
|
130 | {% endfor %} | |
119 | </div> |
|
131 | </div> | |
120 | {% endif %} |
|
132 | {% endif %} | |
@@ -148,6 +160,9 b'' | |||||
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> | |
@@ -156,6 +171,8 b'' | |||||
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 %} | |
@@ -180,7 +197,6 b'' | |||||
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 %} |
@@ -9,6 +9,9 b'' | |||||
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> | |
@@ -16,10 +19,7 b'' | |||||
16 | {% for value in values.contacts %} |
|
19 | {% for value in values.contacts %} | |
17 | <a href="mailto:{{ value }}">{{ value }}</a> |
|
20 | <a href="mailto:{{ value }}">{{ value }}</a> | |
18 | {% endfor %} - |
|
21 | {% endfor %} - | |
19 |
{ |
|
22 | {{ values.roles|join:', ' }} | |
20 | <span class="role">{% trans role %}</span> |
|
|||
21 | {% if not forloop.last %}, {% endif %} |
|
|||
22 | {% endfor %} |
|
|||
23 | </p> |
|
23 | </p> | |
24 | {% endfor %} |
|
24 | {% endfor %} | |
25 | <br /> |
|
25 | <br /> |
@@ -11,7 +11,9 b'' | |||||
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' %}"> | |
@@ -21,11 +23,8 b'' | |||||
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> | |
@@ -44,8 +43,8 b'' | |||||
44 | <a href="#" id="fav-panel-btn">{% trans 'favorites' %} <span id="new-fav-post-count"></span></a> |
|
43 | <a href="#" id="fav-panel-btn">{% trans 'favorites' %} <span id="new-fav-post-count"></span></a> | |
45 | {% endif %} |
|
44 | {% endif %} | |
46 |
|
45 | |||
47 | {% if username %} |
|
46 | {% if usernames %} | |
48 |
<a class="right-link link" href="{% url 'notifications' |
|
47 | <a class="right-link link" href="{% url 'notifications' %}" title="{% trans 'Notifications' %}"> | |
49 | {% trans 'Notifications' %} |
|
48 | {% trans 'Notifications' %} | |
50 | {% ifnotequal new_notifications_count 0 %} |
|
49 | {% ifnotequal new_notifications_count 0 %} | |
51 | (<b>{{ new_notifications_count }}</b>) |
|
50 | (<b>{{ new_notifications_count }}</b>) | |
@@ -60,7 +59,12 b'' | |||||
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> | |
@@ -68,6 +72,9 b'' | |||||
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 %} |
@@ -5,11 +5,15 b'' | |||||
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 %} |
@@ -21,14 +21,14 b'' | |||||
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> | |
@@ -41,7 +41,7 b'' | |||||
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 | |||
@@ -49,15 +49,16 b'' | |||||
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 %} |
@@ -15,7 +15,7 b'' | |||||
15 | <p>[s]<span class="strikethrough">{% trans 'Strikethrough text' %}</span>[/s]</p> |
|
15 | <p>[s]<span class="strikethrough">{% trans 'Strikethrough text' %}</span>[/s]</p> | |
16 | <p>[comment]<span class="comment">{% trans 'Comment' %}</span>[/comment]</p> |
|
16 | <p>[comment]<span class="comment">{% trans 'Comment' %}</span>[/comment]</p> | |
17 | <p>[quote]<span class="quote">>{% trans 'Quote' %}</span>[/quote]</p> |
|
17 | <p>[quote]<span class="quote">>{% trans 'Quote' %}</span>[/quote]</p> | |
18 |
<p>[quote |
|
18 | <p>[quote=src]<div class="multiquote"><div class="quote-header">src</div><div class="quote-text">{% trans 'Quote' %}</div></div><br />[/quote]</p> | |
19 | <p>[tag]<a class="tag">tag</a>[/tag]</p> |
|
19 | <p>[tag]<a class="tag">tag</a>[/tag]</p> | |
20 | <br/> |
|
20 | <br/> | |
21 | <p>{% trans 'You can try pasting the text and previewing the result here:' %} <a href="{% url 'preview' %}">{% trans 'Preview' %}</a></p> |
|
21 | <p>{% trans 'You can try pasting the text and previewing the result here:' %} <a href="{% url 'preview' %}">{% trans 'Preview' %}</a></p> |
@@ -8,11 +8,7 b'' | |||||
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 }}" /> | |
@@ -31,12 +27,11 b'' | |||||
31 | {% for banner in banners %} |
|
27 | {% for banner in banners %} | |
32 | <div class="post"> |
|
28 | <div class="post"> | |
33 | <div class="title">{{ banner.title }}</div> |
|
29 | <div class="title">{{ banner.title }}</div> | |
34 | <div>{{ banner.text }}</div> |
|
30 | <div>{{ banner.get_text }}</div> | |
35 |
<div>{% trans ' |
|
31 | <div>{% trans 'Details' %}: <a href="{{ banner.post.get_absolute_url }}">>>{{ banner.post.id }}</a></div> | |
36 | </div> |
|
32 | </div> | |
37 | {% endfor %} |
|
33 | {% endfor %} | |
38 |
|
34 | |||
39 | {% if tag %} |
|
|||
40 | <div class="tag_info" style="border-bottom: solid .5ex #{{ tag.get_color }}"> |
|
35 | <div class="tag_info" style="border-bottom: solid .5ex #{{ tag.get_color }}"> | |
41 | {% if random_image_post %} |
|
36 | {% if random_image_post %} | |
42 | <div class="tag-image"> |
|
37 | <div class="tag-image"> | |
@@ -44,28 +39,15 b'' | |||||
44 | <a href="{{ random_image_post.get_absolute_url }}"><img |
|
39 | <a href="{{ random_image_post.get_absolute_url }}"><img | |
45 | src="{{ image.image.url_200x150 }}" |
|
40 | src="{{ image.image.url_200x150 }}" | |
46 | width="{{ image.pre_width }}" |
|
41 | width="{{ image.pre_width }}" | |
47 |
height="{{ image.pre_height }}" |
|
42 | height="{{ image.pre_height }}" | |
|
43 | alt="{{ random_image_post.id }}"/></a> | |||
48 | {% endwith %} |
|
44 | {% endwith %} | |
49 | </div> |
|
45 | </div> | |
50 | {% endif %} |
|
46 | {% endif %} | |
51 | <div class="tag-text-data"> |
|
47 | <div class="tag-text-data"> | |
52 | <h2> |
|
48 | <h2> | |
53 | <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form"> |
|
49 | /{{ tag.get_view|safe }}/ | |
54 |
|
|
50 | {% if perms.change_tag %} | |
55 | <button name="method" value="unsubscribe" class="fav">★</button> |
|
|||
56 | {% else %} |
|
|||
57 | <button name="method" value="subscribe" class="not_fav">★</button> |
|
|||
58 | {% endif %} |
|
|||
59 | </form> |
|
|||
60 | <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form"> |
|
|||
61 | {% if is_hidden %} |
|
|||
62 | <button name="method" value="unhide" class="fav">H</button> |
|
|||
63 | {% else %} |
|
|||
64 | <button name="method" value="hide" class="not_fav">H</button> |
|
|||
65 | {% endif %} |
|
|||
66 | </form> |
|
|||
67 | {{ tag.get_view|safe }} |
|
|||
68 | {% if moderator %} |
|
|||
69 | <span class="moderator_info">| <a href="{% url 'admin:boards_tag_change' tag.id %}">{% trans 'Edit tag' %}</a></span> |
|
51 | <span class="moderator_info">| <a href="{% url 'admin:boards_tag_change' tag.id %}">{% trans 'Edit tag' %}</a></span> | |
70 | {% endif %} |
|
52 | {% endif %} | |
71 | </h2> |
|
53 | </h2> | |
@@ -73,9 +55,17 b'' | |||||
73 | <p>{{ tag.get_description|safe }}</p> |
|
55 | <p>{{ tag.get_description|safe }}</p> | |
74 | {% endif %} |
|
56 | {% endif %} | |
75 | <p> |
|
57 | <p> | |
76 | {% blocktrans count count=tag.get_active_thread_count %}{{ count }} active thread{% plural %}active threads{% endblocktrans %}, |
|
58 | {% with active_count=tag.get_active_thread_count bumplimit_count=tag.get_bumplimit_thread_count archived_count=tag.get_archived_thread_count %} | |
77 | {% blocktrans count count=tag.get_bumplimit_thread_count %}{{ count }} thread in bumplimit{% plural %} threads in bumplimit{% endblocktrans %}, |
|
59 | {% if active_count %} | |
78 |
{% blocktrans count count= |
|
60 | {% blocktrans count count=active_count %}{{ count }} active thread{% plural %}active threads{% endblocktrans %}, | |
|
61 | {% endif %} | |||
|
62 | {% if bumplimit_count %} | |||
|
63 | {% blocktrans count count=bumplimit_count %}{{ count }} thread in bumplimit{% plural %} threads in bumplimit{% endblocktrans %}, | |||
|
64 | {% endif %} | |||
|
65 | {% if archived_count %} | |||
|
66 | {% blocktrans count count=archived_count %}{{ count }} archived thread{% plural %}archived threads{% endblocktrans %}, | |||
|
67 | {% endif %} | |||
|
68 | {% endwith %} | |||
79 | {% blocktrans count count=tag.get_post_count %}{{ count }} message{% plural %}messages{% endblocktrans %}. |
|
69 | {% blocktrans count count=tag.get_post_count %}{{ count }} message{% plural %}messages{% endblocktrans %}. | |
80 | </p> |
|
70 | </p> | |
81 | {% if tag.get_all_parents %} |
|
71 | {% if tag.get_all_parents %} | |
@@ -88,76 +78,29 b'' | |||||
88 | {% endif %} |
|
78 | {% endif %} | |
89 | </div> |
|
79 | </div> | |
90 | </div> |
|
80 | </div> | |
91 | {% endif %} |
|
|||
92 |
|
81 | |||
93 |
{% if |
|
82 | {% if prev_page_link %} | |
94 | {% if prev_page_link %} |
|
83 | <div class="page_link"> | |
95 | <div class="page_link"> |
|
84 | <a href="{{ prev_page_link }}">{% trans "Previous page" %}</a> | |
96 | <a href="{{ prev_page_link }}">{% trans "Previous page" %}</a> |
|
85 | </div> | |
97 | </div> |
|
|||
98 | {% endif %} |
|
|||
99 |
|
||||
100 | {% for thread in threads %} |
|
|||
101 | <div class="thread"> |
|
|||
102 | {% post_view thread.get_opening_post moderator=moderator thread=thread truncated=True need_open_link=True %} |
|
|||
103 | {% if not thread.archived %} |
|
|||
104 | {% with last_replies=thread.get_last_replies %} |
|
|||
105 | {% if last_replies %} |
|
|||
106 | {% with skipped_replies_count=thread.get_skipped_replies_count %} |
|
|||
107 | {% if skipped_replies_count %} |
|
|||
108 | <div class="skipped_replies"> |
|
|||
109 | <a href="{% url 'thread' thread.get_opening_post_id %}"> |
|
|||
110 | {% blocktrans count count=skipped_replies_count %}Skipped {{ count }} reply. Open thread to see all replies.{% plural %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %} |
|
|||
111 | </a> |
|
|||
112 | </div> |
|
|||
113 | {% endif %} |
|
|||
114 | {% endwith %} |
|
|||
115 | <div class="last-replies"> |
|
|||
116 | {% for post in last_replies %} |
|
|||
117 | {% post_view post moderator=moderator truncated=True %} |
|
|||
118 | {% endfor %} |
|
|||
119 | </div> |
|
|||
120 | {% endif %} |
|
|||
121 | {% endwith %} |
|
|||
122 | {% endif %} |
|
|||
123 | </div> |
|
|||
124 | {% endfor %} |
|
|||
125 |
|
||||
126 | {% if next_page_link %} |
|
|||
127 | <div class="page_link"> |
|
|||
128 | <a href="{{ next_page_link }}">{% trans "Next page" %}</a> |
|
|||
129 | </div> |
|
|||
130 | {% endif %} |
|
|||
131 | {% else %} |
|
|||
132 | <div class="post"> |
|
|||
133 | {% trans 'No threads exist. Create the first one!' %}</div> |
|
|||
134 | {% endif %} |
|
86 | {% endif %} | |
135 |
|
87 | |||
136 | <div class="post-form-w"> |
|
88 | {% for image in images %} | |
137 | <script src="{% static 'js/panel.js' %}"></script> |
|
89 | <div class="gallery_image"> | |
138 | <div class="post-form"> |
|
90 | {% autoescape off %} | |
139 | <div class="form-title">{% trans "Create new thread" %}</div> |
|
91 | {{ image.get_view }} | |
140 | <div class="swappable-form-full"> |
|
92 | {% endautoescape %} | |
141 | <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %} |
|
93 | <div class="gallery_image_metadata"> | |
142 | {{ form.as_div }} |
|
94 | {{ image.width }}x{{ image.height }} | |
143 | <div class="form-submit"> |
|
|||
144 | <input type="submit" value="{% trans "Post" %}"/> |
|
|||
145 | <button id="preview-button" onclick="return false;">{% trans 'Preview' %}</button> |
|
|||
146 | </div> |
|
|||
147 | </form> |
|
|||
148 | </div> |
|
95 | </div> | |
149 | <div> |
|
|||
150 | {% trans 'Tags must be delimited by spaces. Text or image is required.' %} |
|
|||
151 | </div> |
|
|||
152 | <div id="preview-text"></div> |
|
|||
153 | <div><a href="{% url "staticpage" name="help" %}">{% trans 'Text syntax' %}</a></div> |
|
|||
154 | <div><a href="{% url "tags" "required" %}">{% trans 'Tags' %}</a></div> |
|
|||
155 | </div> |
|
96 | </div> | |
156 | </div> |
|
97 | {% endfor %} | |
157 |
|
98 | |||
158 | <script src="{% static 'js/form.js' %}"></script> |
|
99 | {% if next_page_link %} | |
159 | <script src="{% static 'js/thread_create.js' %}"></script> |
|
100 | <div class="page_link"> | |
160 |
|
101 | <a href="{{ next_page_link }}">{% trans "Next page" %}</a> | ||
|
102 | </div> | |||
|
103 | {% endif %} | |||
161 | {% endblock %} |
|
104 | {% endblock %} | |
162 |
|
105 | |||
163 | {% block metapanel %} |
|
106 | {% block metapanel %} | |
@@ -172,7 +115,7 b'' | |||||
172 | …, |
|
115 | …, | |
173 | {% endif %} |
|
116 | {% endif %} | |
174 | <a |
|
117 | <a | |
175 |
{% ifequal page current_page |
|
118 | {% ifequal page paginator.current_page %} | |
176 | class="current_page" |
|
119 | class="current_page" | |
177 | {% endifequal %} |
|
120 | {% endifequal %} | |
178 | href="{% page_url paginator page %}">{{ page }}</a> |
|
121 | href="{% page_url paginator page %}">{{ page }}</a> | |
@@ -180,7 +123,6 b'' | |||||
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 %} |
@@ -37,8 +37,7 b'' | |||||
37 | {% with images_count=thread.get_images_count%} |
|
37 | {% with images_count=thread.get_images_count%} | |
38 | <span id="image-count">{{ images_count }}</span> <span id="image-count-text">{% blocktrans count count=images_count %}image{% plural %}images{% endblocktrans %}</span>. |
|
38 | <span id="image-count">{{ images_count }}</span> <span id="image-count-text">{% blocktrans count count=images_count %}image{% plural %}images{% endblocktrans %}</span>. | |
39 | {% endwith %} |
|
39 | {% endwith %} | |
40 |
|
|
40 | {% trans 'Last update: ' %}<span id="last-update"><time datetime="{{ thread.last_edit_time|date:'c' }}">{{ thread.last_edit_time }}</time></span> | |
41 | [<a href="rss/">RSS</a>] |
|
|||
42 | </span> |
|
41 | </span> | |
43 |
|
42 | |||
44 | {% endblock %} |
|
43 | {% endblock %} |
@@ -12,6 +12,7 b'' | |||||
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 %} | |
@@ -34,11 +35,11 b'' | |||||
34 |
|
35 | |||
35 | <div class="thread"> |
|
36 | <div class="thread"> | |
36 | {% for post in thread.get_replies %} |
|
37 | {% for post in thread.get_replies %} | |
37 |
{% post_view post |
|
38 | {% post_view post reply_link=True %} | |
38 | {% endfor %} |
|
39 | {% endfor %} | |
39 | </div> |
|
40 | </div> | |
40 |
|
41 | |||
41 | {% if not thread.archived %} |
|
42 | {% if not thread.is_archived %} | |
42 | <div class="post-form-w"> |
|
43 | <div class="post-form-w"> | |
43 | <script src="{% static 'js/panel.js' %}"></script> |
|
44 | <script src="{% static 'js/panel.js' %}"></script> | |
44 | <div class="form-title">{% trans "Reply to thread" %} #{{ opening_post.id }}<span class="reply-to-message"> {% trans "to message " %} #<span id="reply-to-message-id"></span></span></div> |
|
45 | <div class="form-title">{% trans "Reply to thread" %} #{{ opening_post.id }}<span class="reply-to-message"> {% trans "to message " %} #<span id="reply-to-message-id"></span></span></div> | |
@@ -54,17 +55,24 b'' | |||||
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 %} |
@@ -11,7 +11,7 b'' | |||||
11 |
|
11 | |||
12 | <div class="thread"> |
|
12 | <div class="thread"> | |
13 | {% for post in thread.get_top_level_replies %} |
|
13 | {% for post in thread.get_top_level_replies %} | |
14 |
{% post_view post mode |
|
14 | {% post_view post mode_tree=True %} | |
15 | {% endfor %} |
|
15 | {% endfor %} | |
16 | </div> |
|
16 | </div> | |
17 |
|
17 |
@@ -39,8 +39,9 b' def image_actions(*args, **kwargs):' | |||||
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') |
@@ -31,6 +31,7 b' class ApiTest(TestCase):' | |||||
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) |
@@ -3,6 +3,7 b' 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): | |
@@ -96,7 +97,7 b' class PostTests(TestCase):' | |||||
96 | self._create_post() |
|
97 | self._create_post() | |
97 |
|
98 | |||
98 | self.assertEqual(settings.get_int('Messages', 'MaxThreadCount'), |
|
99 | self.assertEqual(settings.get_int('Messages', 'MaxThreadCount'), | |
99 |
len(Thread.objects. |
|
100 | len(Thread.objects.exclude(status=STATUS_ARCHIVE))) | |
100 |
|
101 | |||
101 | def test_pages(self): |
|
102 | def test_pages(self): | |
102 | """Test that the thread list is properly split into pages""" |
|
103 | """Test that the thread list is properly split into pages""" | |
@@ -104,9 +105,9 b' class PostTests(TestCase):' | |||||
104 | for i in range(settings.get_int('Messages', 'MaxThreadCount')): |
|
105 | for i in range(settings.get_int('Messages', 'MaxThreadCount')): | |
105 | self._create_post() |
|
106 | self._create_post() | |
106 |
|
107 | |||
107 |
all_threads = Thread.objects. |
|
108 | all_threads = Thread.objects.exclude(status=STATUS_ARCHIVE) | |
108 |
|
109 | |||
109 |
paginator = Paginator(Thread.objects. |
|
110 | paginator = Paginator(Thread.objects.exclude(status=STATUS_ARCHIVE), | |
110 | settings.get_int('View', 'ThreadsPerPage')) |
|
111 | settings.get_int('View', 'ThreadsPerPage')) | |
111 | posts_in_second_page = paginator.page(2).object_list |
|
112 | posts_in_second_page = paginator.page(2).object_list | |
112 | first_post = posts_in_second_page[0] |
|
113 | first_post = posts_in_second_page[0] |
@@ -41,8 +41,6 b' class ViewTest(TestCase):' | |||||
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,5 +1,5 b'' | |||||
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 | |
@@ -12,6 +12,8 b' from boards.views.static import StaticPa' | |||||
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 = { | |
@@ -45,6 +47,7 b" urlpatterns = patterns(''," | |||||
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()), | |
@@ -54,7 +57,7 b" urlpatterns = patterns(''," | |||||
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 | |
@@ -81,7 +84,8 b" urlpatterns = patterns(''," | |||||
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'), |
@@ -9,6 +9,7 b' import hmac' | |||||
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 | |
@@ -19,7 +20,6 b' 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' | |
@@ -73,15 +73,20 b" def get_websocket_token(user_id='', time" | |||||
73 | return token |
|
73 | return token | |
74 |
|
74 | |||
75 |
|
75 | |||
|
76 | # TODO Test this carefully | |||
76 | def cached_result(key_method=None): |
|
77 | def cached_result(key_method=None): | |
77 | """ |
|
78 | """ | |
78 | Caches method result in the Django's cache system, persisted by object name, |
|
79 | Caches method result in the Django's cache system, persisted by object name, | |
79 |
object name |
|
80 | object name, model id if object is a Django model, args and kwargs if any. | |
80 | """ |
|
81 | """ | |
81 | def _cached_result(function): |
|
82 | def _cached_result(function): | |
82 | def inner_func(obj, *args, **kwargs): |
|
83 | def inner_func(obj, *args, **kwargs): | |
83 | # TODO Include method arguments to the cache key |
|
|||
84 | cache_key_params = [obj.__class__.__name__, function.__name__] |
|
84 | cache_key_params = [obj.__class__.__name__, function.__name__] | |
|
85 | ||||
|
86 | cache_key_params += args | |||
|
87 | for key, value in kwargs: | |||
|
88 | cache_key_params.append(key + ':' + value) | |||
|
89 | ||||
85 | if isinstance(obj, Model): |
|
90 | if isinstance(obj, Model): | |
86 | cache_key_params.append(str(obj.id)) |
|
91 | cache_key_params.append(str(obj.id)) | |
87 |
|
92 | |||
@@ -103,15 +108,6 b' def cached_result(key_method=None):' | |||||
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(): | |
@@ -123,8 +119,8 b' def validate_file_size(size: int):' | |||||
123 | max_size = boards.settings.get_int('Forms', 'MaxFileSize') |
|
119 | max_size = boards.settings.get_int('Forms', 'MaxFileSize') | |
124 | if size > max_size: |
|
120 | if size > max_size: | |
125 | raise forms.ValidationError( |
|
121 | raise forms.ValidationError( | |
126 |
_('File must be less than %s b |
|
122 | _('File must be less than %s but is %s.') | |
127 | % str(max_size)) |
|
123 | % (filesizeformat(max_size), filesizeformat(size))) | |
128 |
|
124 | |||
129 |
|
125 | |||
130 | def get_extension(filename): |
|
126 | def get_extension(filename): |
@@ -1,3 +1,4 b'' | |||||
|
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 | |
@@ -6,6 +7,8 b' 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 | |
@@ -15,7 +18,7 b' from boards.models import Post, Thread, ' | |||||
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' | |
@@ -30,20 +33,20 b" PARAMETER_PAGINATOR = 'paginator'" | |||||
30 | PARAMETER_THREADS = 'threads' |
|
33 | PARAMETER_THREADS = 'threads' | |
31 | PARAMETER_BANNERS = 'banners' |
|
34 | PARAMETER_BANNERS = 'banners' | |
32 | PARAMETER_ADDITIONAL = 'additional_params' |
|
35 | PARAMETER_ADDITIONAL = 'additional_params' | |
33 |
|
36 | PARAMETER_MAX_FILE_SIZE = 'max_file_size' | ||
34 |
PARAMETER_ |
|
37 | PARAMETER_RSS_URL = 'rss_url' | |
35 | PARAMETER_NEXT_LINK = 'next_page_link' |
|
|||
36 |
|
38 | |||
37 | TEMPLATE = 'boards/all_threads.html' |
|
39 | TEMPLATE = 'boards/all_threads.html' | |
38 | DEFAULT_PAGE = 1 |
|
40 | DEFAULT_PAGE = 1 | |
39 |
|
41 | |||
40 |
|
42 | |||
41 | class AllThreadsView(PostMixin, BaseBoardView): |
|
43 | class AllThreadsView(PostMixin, FileUploadMixin, BaseBoardView, PaginatedMixin): | |
42 |
|
44 | |||
43 | def __init__(self): |
|
45 | def __init__(self): | |
44 | self.settings_manager = None |
|
46 | self.settings_manager = None | |
45 | super(AllThreadsView, self).__init__() |
|
47 | super(AllThreadsView, self).__init__() | |
46 |
|
48 | |||
|
49 | @method_decorator(csrf_protect) | |||
47 | def get(self, request, form: ThreadForm=None): |
|
50 | def get(self, request, form: ThreadForm=None): | |
48 | page = request.GET.get('page', DEFAULT_PAGE) |
|
51 | page = request.GET.get('page', DEFAULT_PAGE) | |
49 |
|
52 | |||
@@ -74,12 +77,15 b' class AllThreadsView(PostMixin, BaseBoar' | |||||
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) | |
@@ -101,12 +107,7 b' class AllThreadsView(PostMixin, BaseBoar' | |||||
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') | |
@@ -136,10 +137,12 b' class AllThreadsView(PostMixin, BaseBoar' | |||||
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 | |
@@ -155,3 +158,6 b' class AllThreadsView(PostMixin, BaseBoar' | |||||
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,25 +1,20 b'' | |||||
1 | from collections import OrderedDict |
|
|||
2 |
|
|
1 | import json | |
3 | import logging |
|
2 | import logging | |
4 |
|
3 | |||
5 | import xml.etree.ElementTree as ET |
|
4 | from django.core import serializers | |
6 |
|
||||
7 | from django.db import transaction |
|
5 | from django.db import transaction | |
8 | from django.db.models import Count |
|
|||
9 | from django.http import HttpResponse |
|
6 | from django.http import HttpResponse | |
10 | from django.shortcuts import get_object_or_404 |
|
7 | from django.shortcuts import get_object_or_404 | |
11 | from django.core import serializers |
|
8 | from django.views.decorators.csrf import csrf_protect | |
12 | from boards.abstracts.settingsmanager import get_settings_manager,\ |
|
|||
13 | FAV_THREAD_NO_UPDATES |
|
|||
14 |
|
9 | |||
|
10 | from boards.abstracts.settingsmanager import get_settings_manager | |||
15 | from boards.forms import PostForm, PlainErrorList |
|
11 | from boards.forms import PostForm, PlainErrorList | |
16 | from boards.models import Post, Thread, Tag, GlobalId |
|
12 | from boards.mdx_neboard import Parser | |
17 |
from boards.models |
|
13 | from boards.models import Post, Thread, Tag | |
|
14 | from boards.models.thread import STATUS_ARCHIVE | |||
|
15 | from boards.models.user import Notification | |||
18 | from boards.utils import datetime_to_epoch |
|
16 | from boards.utils import datetime_to_epoch | |
19 | from boards.views.thread import ThreadView |
|
17 | from boards.views.thread import ThreadView | |
20 | from boards.models.user import Notification |
|
|||
21 | from boards.mdx_neboard import Parser |
|
|||
22 |
|
||||
23 |
|
18 | |||
24 | __author__ = 'neko259' |
|
19 | __author__ = 'neko259' | |
25 |
|
20 | |||
@@ -49,8 +44,12 b' def api_get_threaddiff(request):' | |||||
49 | """ |
|
44 | """ | |
50 |
|
45 | |||
51 | thread_id = request.POST.get(PARAMETER_THREAD) |
|
46 | thread_id = request.POST.get(PARAMETER_THREAD) | |
52 |
uids_str = request.POST.get(PARAMETER_UIDS) |
|
47 | uids_str = request.POST.get(PARAMETER_UIDS) | |
53 | uids = uids_str.split(' ') |
|
48 | ||
|
49 | if not thread_id or not uids_str: | |||
|
50 | return HttpResponse(content='Invalid request.') | |||
|
51 | ||||
|
52 | uids = uids_str.strip().split(' ') | |||
54 |
|
53 | |||
55 | opening_post = get_object_or_404(Post, id=thread_id) |
|
54 | opening_post = get_object_or_404(Post, id=thread_id) | |
56 | thread = opening_post.get_thread() |
|
55 | thread = opening_post.get_thread() | |
@@ -77,6 +76,7 b' def api_get_threaddiff(request):' | |||||
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 | |
@@ -125,7 +125,7 b' def get_post(request, post_id):' | |||||
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): | |
@@ -139,9 +139,9 b' def api_get_threads(request, count):' | |||||
139 | tag_name = request.GET[PARAMETER_TAG] |
|
139 | tag_name = request.GET[PARAMETER_TAG] | |
140 | if tag_name is not None: |
|
140 | if tag_name is not None: | |
141 | tag = get_object_or_404(Tag, name=tag_name) |
|
141 | tag = get_object_or_404(Tag, name=tag_name) | |
142 |
threads = tag.get_threads(). |
|
142 | threads = tag.get_threads().exclude(status=STATUS_ARCHIVE) | |
143 | else: |
|
143 | else: | |
144 |
threads = Thread.objects. |
|
144 | threads = Thread.objects.exclude(status=STATUS_ARCHIVE) | |
145 |
|
145 | |||
146 | if PARAMETER_OFFSET in request.GET: |
|
146 | if PARAMETER_OFFSET in request.GET: | |
147 | offset = request.GET[PARAMETER_OFFSET] |
|
147 | offset = request.GET[PARAMETER_OFFSET] | |
@@ -158,8 +158,7 b' def api_get_threads(request, count):' | |||||
158 |
|
158 | |||
159 | # TODO Add tags, replies and images count |
|
159 | # TODO Add tags, replies and images count | |
160 | post_data = opening_post.get_post_data(include_last_update=True) |
|
160 | post_data = opening_post.get_post_data(include_last_update=True) | |
161 |
post_data[' |
|
161 | post_data['status'] = thread.get_status() | |
162 | post_data['archived'] = thread.archived |
|
|||
163 |
|
162 | |||
164 | opening_posts.append(post_data) |
|
163 | opening_posts.append(post_data) | |
165 |
|
164 | |||
@@ -214,7 +213,7 b' def api_get_notifications(request, usern' | |||||
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 = [] |
@@ -1,13 +1,34 b'' | |||||
|
1 | import os | |||
|
2 | ||||
1 | from django.shortcuts import render |
|
3 | from django.shortcuts import render | |
2 |
|
4 | |||
|
5 | import neboard | |||
3 | from boards.authors import authors |
|
6 | from boards.authors import authors | |
|
7 | from boards.utils import cached_result | |||
4 | from boards.views.base import BaseBoardView |
|
8 | from boards.views.base import BaseBoardView | |
|
9 | from boards.models import Post | |||
|
10 | ||||
|
11 | ||||
|
12 | PARAM_AUTHORS = 'authors' | |||
|
13 | PARAM_MEDIA_SIZE = 'media_size' | |||
|
14 | PARAM_POST_COUNT = 'post_count' | |||
5 |
|
15 | |||
6 |
|
16 | |||
7 | class AuthorsView(BaseBoardView): |
|
17 | class AuthorsView(BaseBoardView): | |
8 |
|
18 | |||
9 | def get(self, request): |
|
19 | def get(self, request): | |
10 | params = dict() |
|
20 | params = dict() | |
11 |
params[ |
|
21 | params[PARAM_AUTHORS] = authors | |
|
22 | params[PARAM_MEDIA_SIZE] = self._get_directory_size(neboard.settings.MEDIA_ROOT) | |||
|
23 | params[PARAM_POST_COUNT] = Post.objects.count() | |||
12 |
|
24 | |||
13 | return render(request, 'boards/authors.html', params) |
|
25 | return render(request, 'boards/authors.html', params) | |
|
26 | ||||
|
27 | @cached_result() | |||
|
28 | def _get_directory_size(self, directory): | |||
|
29 | total_size = 0 | |||
|
30 | for dirpath, dirnames, filenames in os.walk(directory): | |||
|
31 | for f in filenames: | |||
|
32 | fp = os.path.join(dirpath, f) | |||
|
33 | total_size += os.path.getsize(fp) | |||
|
34 | return total_size |
@@ -1,3 +1,6 b'' | |||||
|
1 | import boards | |||
|
2 | ||||
|
3 | ||||
1 | PARAM_NEXT = 'next' |
|
4 | PARAM_NEXT = 'next' | |
2 | PARAMETER_METHOD = 'method' |
|
5 | PARAMETER_METHOD = 'method' | |
3 |
|
6 | |||
@@ -24,3 +27,14 b' class DispatcherMixin:' | |||||
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() |
@@ -10,37 +10,40 b" 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,13 +1,15 b'' | |||||
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 | |
@@ -26,5 +28,6 b' class NormalThreadView(ThreadView):' | |||||
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,8 +1,12 b'' | |||||
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 | |
@@ -28,6 +32,7 b" 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' | |
@@ -37,6 +42,7 b" FORM_THREADS = 'threads'" | |||||
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) | |
@@ -67,6 +73,7 b' class ThreadView(BaseBoardView, PostMixi' | |||||
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') | |
@@ -82,6 +89,7 b' class ThreadView(BaseBoardView, PostMixi' | |||||
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 | |||
@@ -94,7 +102,7 b' class ThreadView(BaseBoardView, PostMixi' | |||||
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 | |
@@ -163,11 +171,5 b' class ThreadView(BaseBoardView, PostMixi' | |||||
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']) |
|
@@ -95,19 +95,27 b' else:' | |||||
95 | # Make this unique, and don't share it with anybody. |
|
95 | # Make this unique, and don't share it with anybody. | |
96 | SECRET_KEY = '@1rc$o(7=tt#kd+4s$u6wchm**z^)4x90)7f6z(i&55@o11*8o' |
|
96 | SECRET_KEY = '@1rc$o(7=tt#kd+4s$u6wchm**z^)4x90)7f6z(i&55@o11*8o' | |
97 |
|
97 | |||
98 | # List of callables that know how to import templates from various sources. |
|
98 | TEMPLATES = [{ | |
99 | TEMPLATE_LOADERS = ( |
|
99 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', | |
100 | 'django.template.loaders.filesystem.Loader', |
|
100 | 'DIRS': ['templates'], | |
101 | 'django.template.loaders.app_directories.Loader', |
|
101 | 'OPTIONS': { | |
102 | ) |
|
102 | 'loaders': [ | |
|
103 | ('django.template.loaders.cached.Loader', [ | |||
|
104 | 'django.template.loaders.filesystem.Loader', | |||
|
105 | 'django.template.loaders.app_directories.Loader', | |||
|
106 | ]), | |||
|
107 | ], | |||
|
108 | 'context_processors': [ | |||
|
109 | 'django.template.context_processors.csrf', | |||
|
110 | 'django.core.context_processors.media', | |||
|
111 | 'django.core.context_processors.static', | |||
|
112 | 'django.core.context_processors.request', | |||
|
113 | 'django.contrib.auth.context_processors.auth', | |||
|
114 | 'boards.context_processors.user_and_ui_processor', | |||
|
115 | ], | |||
|
116 | }, | |||
|
117 | }] | |||
103 |
|
118 | |||
104 | TEMPLATE_CONTEXT_PROCESSORS = ( |
|
|||
105 | 'django.core.context_processors.media', |
|
|||
106 | 'django.core.context_processors.static', |
|
|||
107 | 'django.core.context_processors.request', |
|
|||
108 | 'django.contrib.auth.context_processors.auth', |
|
|||
109 | 'boards.context_processors.user_and_ui_processor', |
|
|||
110 | ) |
|
|||
111 |
|
119 | |||
112 | MIDDLEWARE_CLASSES = [ |
|
120 | MIDDLEWARE_CLASSES = [ | |
113 | 'django.middleware.http.ConditionalGetMiddleware', |
|
121 | 'django.middleware.http.ConditionalGetMiddleware', | |
@@ -125,13 +133,6 b" ROOT_URLCONF = 'neboard.urls'" | |||||
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', |
General Comments 0
You need to be logged in to leave comments.
Login now