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 | 35 | 604935b98f5b5e4a5e903594f048046e1fbb3519 2.8.3 |
|
36 | 36 | c48ffdc671566069ed0f33644da1229277f3cd18 2.9.0 |
|
37 | 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 | 12 | class DividedPaginator(Paginator): |
|
13 | 13 | |
|
14 | 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 | 24 | def _left_range(self): |
|
18 | 25 | return self.page_range[:self.lookaround_size] |
@@ -67,8 +74,18 b' class DividedPaginator(Paginator):' | |||
|
67 | 74 | def get_page_url(self, page): |
|
68 | 75 | self.params['page'] = page |
|
69 | 76 | url_params = '?' + '&'.join(['{}={}'.format(key, self.params[key]) |
|
70 | for key in self.params.keys()]) | |
|
77 | for key in self.params.keys()]) | |
|
71 | 78 | return self.link + url_params |
|
72 | 79 | |
|
73 | 80 | def supports_urls(self): |
|
74 | 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 | 143 | def thread_is_fav(self, opening_post): |
|
144 | 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 | 154 | class SessionSettingsManager(SettingsManager): |
|
147 | 155 | """ |
|
148 | 156 | Session-based settings manager. All settings are saved to the user's |
@@ -10,7 +10,8 b' class PostAdmin(admin.ModelAdmin):' | |||
|
10 | 10 | list_filter = ('pub_time',) |
|
11 | 11 | search_fields = ('id', 'title', 'text', 'poster_ip') |
|
12 | 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 | 16 | def ban_poster(self, request, queryset): |
|
16 | 17 | bans = 0 |
@@ -55,9 +56,9 b' class ThreadAdmin(admin.ModelAdmin):' | |||
|
55 | 56 | def op(self, obj: Thread): |
|
56 | 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 | 60 | 'display_tags') |
|
60 |
list_filter = ('bump_time', ' |
|
|
61 | list_filter = ('bump_time', 'status') | |
|
61 | 62 | search_fields = ('id', 'title') |
|
62 | 63 | filter_horizontal = ('tags',) |
|
63 | 64 |
@@ -1,5 +1,5 b'' | |||
|
1 | 1 | [Version] |
|
2 |
Version = 2.1 |
|
|
2 | Version = 2.11.0 Yuko | |
|
3 | 3 | SiteName = Neboard DEV |
|
4 | 4 | |
|
5 | 5 | [Cache] |
@@ -10,7 +10,8 b' CacheTimeout = 600' | |||
|
10 | 10 | # Max post length in characters |
|
11 | 11 | MaxTextLength = 30000 |
|
12 | 12 | MaxFileSize = 8000000 |
|
13 |
LimitPostingSpeed = |
|
|
13 | LimitPostingSpeed = true | |
|
14 | PowDifficulty = 20 | |
|
14 | 15 | |
|
15 | 16 | [Messages] |
|
16 | 17 | # Thread bumplimit |
@@ -24,6 +25,7 b' DefaultTheme = md' | |||
|
24 | 25 | DefaultImageViewer = simple |
|
25 | 26 | LastRepliesCount = 3 |
|
26 | 27 | ThreadsPerPage = 3 |
|
28 | ImagesPerPageGallery = 20 | |
|
27 | 29 | |
|
28 | 30 | [Storage] |
|
29 | 31 | # Enable archiving threads instead of deletion when the thread limit is reached |
@@ -32,3 +34,6 b' ArchiveThreads = true' | |||
|
32 | 34 | [External] |
|
33 | 35 | # Thread update |
|
34 | 36 | WebsocketsEnabled = false |
|
37 | ||
|
38 | [RSS] | |
|
39 | MaxItems = 20 |
@@ -1,39 +1,39 b'' | |||
|
1 | 1 | from boards.abstracts.settingsmanager import get_settings_manager, \ |
|
2 |
|
|
|
2 | SETTING_LAST_NOTIFICATION_ID, SETTING_IMAGE_VIEWER | |
|
3 | 3 | from boards.models.user import Notification |
|
4 | 4 | |
|
5 | 5 | __author__ = 'neko259' |
|
6 | 6 | |
|
7 |
from boards import settings |
|
|
7 | from boards import settings | |
|
8 | 8 | from boards.models import Post, Tag |
|
9 | 9 | |
|
10 | 10 | CONTEXT_SITE_NAME = 'site_name' |
|
11 | 11 | CONTEXT_VERSION = 'version' |
|
12 | CONTEXT_MODERATOR = 'moderator' | |
|
13 | 12 | CONTEXT_THEME_CSS = 'theme_css' |
|
14 | 13 | CONTEXT_THEME = 'theme' |
|
15 | 14 | CONTEXT_PPD = 'posts_per_day' |
|
16 | 15 | CONTEXT_TAGS = 'tags' |
|
17 | 16 | CONTEXT_USER = 'user' |
|
18 | 17 | CONTEXT_NEW_NOTIFICATIONS_COUNT = 'new_notifications_count' |
|
19 | CONTEXT_USERNAME = 'username' | |
|
18 | CONTEXT_USERNAMES = 'usernames' | |
|
20 | 19 | CONTEXT_TAGS_STR = 'tags_str' |
|
21 | 20 | CONTEXT_IMAGE_VIEWER = 'image_viewer' |
|
22 | 21 | CONTEXT_HAS_FAV_THREADS = 'has_fav_threads' |
|
22 | CONTEXT_POW_DIFFICULTY = 'pow_difficulty' | |
|
23 | 23 | |
|
24 | 24 | |
|
25 | 25 | def get_notifications(context, request): |
|
26 | 26 | settings_manager = get_settings_manager(request) |
|
27 |
username = settings_manager.get_ |
|
|
27 | usernames = settings_manager.get_notification_usernames() | |
|
28 | 28 | new_notifications_count = 0 |
|
29 |
if username is not None |
|
|
29 | if usernames is not None: | |
|
30 | 30 | last_notification_id = settings_manager.get_setting( |
|
31 | 31 | SETTING_LAST_NOTIFICATION_ID) |
|
32 | 32 | |
|
33 | 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 | 35 | context[CONTEXT_NEW_NOTIFICATIONS_COUNT] = new_notifications_count |
|
36 | context[CONTEXT_USERNAME] = username | |
|
36 | context[CONTEXT_USERNAMES] = usernames | |
|
37 | 37 | |
|
38 | 38 | |
|
39 | 39 | def user_and_ui_processor(request): |
@@ -50,12 +50,12 b' def user_and_ui_processor(request):' | |||
|
50 | 50 | context[CONTEXT_THEME] = theme |
|
51 | 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 | 53 | context[CONTEXT_VERSION] = settings.get('Version', 'Version') |
|
57 | 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 | 59 | context[CONTEXT_IMAGE_VIEWER] = settings_manager.get_setting( |
|
60 | 60 | SETTING_IMAGE_VIEWER, |
|
61 | 61 | default=settings.get('View', 'DefaultImageViewer')) |
@@ -2,6 +2,7 b' import hashlib' | |||
|
2 | 2 | import re |
|
3 | 3 | import time |
|
4 | 4 | import logging |
|
5 | ||
|
5 | 6 | import pytz |
|
6 | 7 | |
|
7 | 8 | from django import forms |
@@ -9,6 +10,7 b' from django.core.files.uploadedfile impo' | |||
|
9 | 10 | from django.core.exceptions import ObjectDoesNotExist |
|
10 | 11 | from django.forms.util import ErrorList |
|
11 | 12 | from django.utils.translation import ugettext_lazy as _, ungettext_lazy |
|
13 | from django.utils import timezone | |
|
12 | 14 | |
|
13 | 15 | from boards.mdx_neboard import formatters |
|
14 | 16 | from boards.models.attachment.downloaders import Downloader |
@@ -20,7 +22,11 b' from neboard import settings' | |||
|
20 | 22 | import boards.settings as board_settings |
|
21 | 23 | import neboard |
|
22 | 24 | |
|
25 | POW_HASH_LENGTH = 16 | |
|
26 | POW_LIFE_MINUTES = 1 | |
|
27 | ||
|
23 | 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 | 31 | VETERAN_POSTING_DELAY = 5 |
|
26 | 32 | |
@@ -82,7 +88,7 b' class FormatPanel(forms.Textarea):' | |||
|
82 | 88 | formatter.preview_right + '</span>' |
|
83 | 89 | |
|
84 | 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 | 93 | return output |
|
88 | 94 | |
@@ -168,6 +174,10 b' class PostForm(NeboardForm):' | |||
|
168 | 174 | widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: |
|
169 | 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 | 181 | session = None |
|
172 | 182 | need_to_ban = False |
|
173 | 183 | |
@@ -238,7 +248,7 b' class PostForm(NeboardForm):' | |||
|
238 | 248 | for thread_id in threads_id_list: |
|
239 | 249 | try: |
|
240 | 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 | 252 | raise ObjectDoesNotExist() |
|
243 | 253 | threads.append(thread) |
|
244 | 254 | except (ObjectDoesNotExist, ValueError): |
@@ -256,8 +266,13 b' class PostForm(NeboardForm):' | |||
|
256 | 266 | if not self.errors: |
|
257 | 267 | self._clean_text_file() |
|
258 | 268 | |
|
259 | if not self.errors and self.session: | |
|
260 | self._validate_posting_speed() | |
|
269 | limit_speed = board_settings.get_bool('Forms', 'LimitPostingSpeed') | |
|
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 | 277 | return cleaned_data |
|
263 | 278 | |
@@ -341,8 +356,26 b' class PostForm(NeboardForm):' | |||
|
341 | 356 | except forms.ValidationError as e: |
|
342 | 357 | raise e |
|
343 | 358 | except Exception as e: |
|
344 | # Just return no file | |
|
345 | pass | |
|
359 | raise forms.ValidationError(e) | |
|
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 | 381 | class ThreadForm(PostForm): |
@@ -350,6 +383,7 b' class ThreadForm(PostForm):' | |||
|
350 | 383 | tags = forms.CharField( |
|
351 | 384 | widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}), |
|
352 | 385 | max_length=100, label=_('Tags'), required=True) |
|
386 | monochrome = forms.BooleanField(label=_('Monochrome'), required=False) | |
|
353 | 387 | |
|
354 | 388 | def clean_tags(self): |
|
355 | 389 | tags = self.cleaned_data['tags'].strip() |
@@ -385,6 +419,9 b' class ThreadForm(PostForm):' | |||
|
385 | 419 | |
|
386 | 420 | return cleaned_data |
|
387 | 421 | |
|
422 | def is_monochrome(self): | |
|
423 | return self.cleaned_data['monochrome'] | |
|
424 | ||
|
388 | 425 | |
|
389 | 426 | class SettingsForm(NeboardForm): |
|
390 | 427 | |
@@ -396,7 +433,7 b' class SettingsForm(NeboardForm):' | |||
|
396 | 433 | def clean_username(self): |
|
397 | 434 | username = self.cleaned_data['username'] |
|
398 | 435 | |
|
399 |
if username and not REGEX_ |
|
|
436 | if username and not REGEX_USERNAMES.match(username): | |
|
400 | 437 | raise forms.ValidationError(_('Inappropriate characters.')) |
|
401 | 438 | |
|
402 | 439 | return username |
|
1 | NO CONTENT: modified file, binary diff hidden |
@@ -143,8 +143,8 b' msgid "This page does not exist"' | |||
|
143 | 143 | msgstr "Этой страницы не существует" |
|
144 | 144 | |
|
145 | 145 | #: templates/boards/all_threads.html:35 |
|
146 | msgid "Related message" | |
|
147 | msgstr "Связанное сообщение" | |
|
146 | msgid "Details" | |
|
147 | msgstr "Подробности" | |
|
148 | 148 | |
|
149 | 149 | #: templates/boards/all_threads.html:69 |
|
150 | 150 | msgid "Edit tag" |
@@ -488,8 +488,8 b' msgstr "\xd0\x9e\xd0\xba"' | |||
|
488 | 488 | |
|
489 | 489 | #: utils.py:120 |
|
490 | 490 | #, python-format |
|
491 |
msgid "File must be less than %s b |
|
|
492 |
msgstr "Файл должен быть менее %s |
|
|
491 | msgid "File must be less than %s but is %s." | |
|
492 | msgstr "Файл должен быть менее %s, но его размер %s." | |
|
493 | 493 | |
|
494 | 494 | msgid "Please wait %(delay)d second before sending message" |
|
495 | 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 | 500 | msgid "New threads" |
|
501 | 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 |
@@ -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 | 53 | msgid "Server error!" |
|
54 | 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 | 14 | @transaction.atomic |
|
15 | 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 | 17 | .filter(num_threads=0).order_by('-required', 'name') |
|
18 | 18 | print('Removing {} empty tags'.format(empty.count())) |
|
19 | 19 | empty.delete() |
@@ -141,6 +141,8 b' def render_quote(tag_name, value, option' | |||
|
141 | 141 | source = '' |
|
142 | 142 | if 'source' in options: |
|
143 | 143 | source = options['source'] |
|
144 | elif 'quote' in options: | |
|
145 | source = options['quote'] | |
|
144 | 146 | |
|
145 | 147 | if source: |
|
146 | 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 | 6 | from boards.models.signature import GlobalId, Signature |
|
4 | 7 | from boards.models.sync_key import KeyPair |
@@ -38,4 +38,5 b' class Attachment(models.Model):' | |||
|
38 | 38 | |
|
39 | 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 | 1 | import os |
|
2 | 2 | import re |
|
3 | 3 | |
|
4 | from django.core.files.uploadedfile import SimpleUploadedFile | |
|
4 | from django.core.files.uploadedfile import SimpleUploadedFile, \ | |
|
5 | TemporaryUploadedFile | |
|
5 | 6 | from pytube import YouTube |
|
6 | 7 | import requests |
|
7 | 8 | |
@@ -14,9 +15,9 b' HTTP_RESULT_OK = 200' | |||
|
14 | 15 | HEADER_CONTENT_LENGTH = 'content-length' |
|
15 | 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 | 23 | class Downloader: |
@@ -38,17 +39,19 b' class Downloader:' | |||
|
38 | 39 | |
|
39 | 40 | # Download file, stop if the size exceeds limit |
|
40 | 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 | 48 | for chunk in response.iter_content(FILE_DOWNLOAD_CHUNK_BYTES): |
|
43 | 49 | size += len(chunk) |
|
44 | 50 | validate_file_size(size) |
|
45 |
|
|
|
51 | file.write(chunk) | |
|
46 | 52 | |
|
47 |
if response.status_code == HTTP_RESULT_OK |
|
|
48 | # Set a dummy file name that will be replaced | |
|
49 | # anyway, just keep the valid extension | |
|
50 | filename = 'file.' + content_type.split('/')[1] | |
|
51 | return SimpleUploadedFile(filename, content, content_type) | |
|
53 | if response.status_code == HTTP_RESULT_OK: | |
|
54 | return file | |
|
52 | 55 | |
|
53 | 56 | |
|
54 | 57 | class YouTubeDownloader(Downloader): |
@@ -3,8 +3,11 b' from django.db import models' | |||
|
3 | 3 | |
|
4 | 4 | class Banner(models.Model): |
|
5 | 5 | title = models.TextField() |
|
6 | text = models.TextField() | |
|
6 | text = models.TextField(blank=True, null=True) | |
|
7 | 7 | post = models.ForeignKey('Post') |
|
8 | 8 | |
|
9 | 9 | def __str__(self): |
|
10 | 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 | 4 | from boards import thumbs, utils |
|
5 | 5 | import boards |
|
6 | 6 | from boards.models.base import Viewable |
|
7 | from boards.models import STATUS_ARCHIVE | |
|
7 | 8 | from boards.utils import get_upload_filename |
|
8 | 9 | |
|
10 | ||
|
9 | 11 | __author__ = 'neko259' |
|
10 | 12 | |
|
11 | 13 | |
@@ -27,8 +29,8 b' class PostImageManager(models.Manager):' | |||
|
27 | 29 | |
|
28 | 30 | return post_image |
|
29 | 31 | |
|
30 |
def get_random_images(self, count, |
|
|
31 |
images = self. |
|
|
32 | def get_random_images(self, count, tags=None): | |
|
33 | images = self.exclude(post_images__thread__status=STATUS_ARCHIVE) | |
|
32 | 34 | if tags is not None: |
|
33 | 35 | images = images.filter(post_images__threads__tags__in=tags) |
|
34 | 36 | return images.order_by('?')[:count] |
@@ -23,6 +23,7 b" CSS_CLS_HIDDEN_POST = 'hidden_post'" | |||
|
23 | 23 | CSS_CLS_DEAD_POST = 'dead_post' |
|
24 | 24 | CSS_CLS_ARCHIVE_POST = 'archive_post' |
|
25 | 25 | CSS_CLS_POST = 'post' |
|
26 | CSS_CLS_MONOCHROME = 'monochrome' | |
|
26 | 27 | |
|
27 | 28 | TITLE_MAX_WORDS = 10 |
|
28 | 29 | |
@@ -46,7 +47,6 b" PARAMETER_DIFF_TYPE = 'type'" | |||
|
46 | 47 | PARAMETER_CSS_CLASS = 'css_class' |
|
47 | 48 | PARAMETER_THREAD = 'thread' |
|
48 | 49 | PARAMETER_IS_OPENING = 'is_opening' |
|
49 | PARAMETER_MODERATOR = 'moderator' | |
|
50 | 50 | PARAMETER_POST = 'post' |
|
51 | 51 | PARAMETER_OP_ID = 'opening_post_id' |
|
52 | 52 | PARAMETER_NEED_OPEN_LINK = 'need_open_link' |
@@ -56,10 +56,10 b" PARAMETER_NEED_OP_DATA = 'need_op_data'" | |||
|
56 | 56 | POST_VIEW_PARAMS = ( |
|
57 | 57 | 'need_op_data', |
|
58 | 58 | 'reply_link', |
|
59 | 'moderator', | |
|
60 | 59 | 'need_open_link', |
|
61 | 60 | 'truncated', |
|
62 | 61 | 'mode_tree', |
|
62 | 'perms', | |
|
63 | 63 | ) |
|
64 | 64 | |
|
65 | 65 | |
@@ -185,12 +185,14 b' class Post(models.Model, Viewable):' | |||
|
185 | 185 | thread = self.get_thread() |
|
186 | 186 | |
|
187 | 187 | css_classes = [CSS_CLS_POST] |
|
188 | if thread.archived: | |
|
188 | if thread.is_archived(): | |
|
189 | 189 | css_classes.append(CSS_CLS_ARCHIVE_POST) |
|
190 | 190 | elif not thread.can_bump(): |
|
191 | 191 | css_classes.append(CSS_CLS_DEAD_POST) |
|
192 | 192 | if self.is_hidden(): |
|
193 | 193 | css_classes.append(CSS_CLS_HIDDEN_POST) |
|
194 | if thread.is_monochrome(): | |
|
195 | css_classes.append(CSS_CLS_MONOCHROME) | |
|
194 | 196 | |
|
195 | 197 | params = dict() |
|
196 | 198 | for param in POST_VIEW_PARAMS: |
@@ -332,20 +334,29 b' class Post(models.Model, Viewable):' | |||
|
332 | 334 | |
|
333 | 335 | def save(self, force_insert=False, force_update=False, using=None, |
|
334 | 336 | update_fields=None): |
|
337 | new_post = self.id is None | |
|
338 | ||
|
335 | 339 | self._text_rendered = Parser().parse(self.get_raw_text()) |
|
336 | 340 | |
|
337 | 341 | self.uid = str(uuid.uuid4()) |
|
338 | 342 | if update_fields is not None and 'uid' not in update_fields: |
|
339 | 343 | update_fields += ['uid'] |
|
340 | 344 | |
|
341 |
if |
|
|
345 | if not new_post: | |
|
342 | 346 | for thread in self.get_threads().all(): |
|
343 | 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 | 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 | 360 | def get_text(self) -> str: |
|
350 | 361 | return self._text_rendered |
|
351 | 362 | |
@@ -380,12 +391,12 b' class Post(models.Model, Viewable):' | |||
|
380 | 391 | else: |
|
381 | 392 | return str(self.id) |
|
382 | 393 | |
|
383 | def connect_notifications(self): | |
|
394 | def _connect_notifications(self): | |
|
384 | 395 | for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()): |
|
385 | 396 | user_name = reply_number.group(1).lower() |
|
386 | 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 | 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 | 422 | thread.update_bump_status() |
|
412 | 423 | |
|
413 | 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 | 426 | self.threads.add(opening_post.get_thread()) |
|
416 | 427 | |
|
417 | 428 | def get_tripcode(self): |
@@ -1,3 +1,5 b'' | |||
|
1 | from django.contrib.auth.context_processors import PermWrapper | |
|
2 | ||
|
1 | 3 | from boards import utils |
|
2 | 4 | |
|
3 | 5 | |
@@ -24,7 +26,7 b' class HtmlExporter(Exporter):' | |||
|
24 | 26 | reply_link = True |
|
25 | 27 | |
|
26 | 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 | 32 | class JsonExporter(Exporter): |
@@ -31,13 +31,15 b' class PostManager(models.Manager):' | |||
|
31 | 31 | @transaction.atomic |
|
32 | 32 | def create_post(self, title: str, text: str, file=None, thread=None, |
|
33 | 33 | ip=NO_IP, tags: list=None, opening_posts: list=None, |
|
34 | tripcode=''): | |
|
34 | tripcode='', monochrome=False): | |
|
35 | 35 | """ |
|
36 | 36 | Creates new post |
|
37 | 37 | """ |
|
38 | 38 | |
|
39 | 39 | if not utils.is_anonymous_mode(): |
|
40 | 40 | is_banned = Ban.objects.filter(ip=ip).exists() |
|
41 | else: | |
|
42 | is_banned = False | |
|
41 | 43 | |
|
42 | 44 | # TODO Raise specific exception and catch it in the views |
|
43 | 45 | if is_banned: |
@@ -52,7 +54,8 b' class PostManager(models.Manager):' | |||
|
52 | 54 | new_thread = False |
|
53 | 55 | if not thread: |
|
54 | 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 | 59 | list(map(thread.tags.add, tags)) |
|
57 | 60 | boards.models.thread.Thread.objects.process_oldest_threads() |
|
58 | 61 | new_thread = True |
@@ -72,7 +75,7 b' class PostManager(models.Manager):' | |||
|
72 | 75 | logger = logging.getLogger('boards.post.create') |
|
73 | 76 | |
|
74 | 77 | logger.info('Created post [{}] with text [{}] by {}'.format(post, |
|
75 |
|
|
|
78 | post.get_text(),post.poster_ip)) | |
|
76 | 79 | |
|
77 | 80 | # TODO Move this to other place |
|
78 | 81 | if file: |
@@ -82,10 +85,7 b' class PostManager(models.Manager):' | |||
|
82 | 85 | else: |
|
83 | 86 | post.attachments.add(Attachment.objects.create_with_hash(file)) |
|
84 | 87 | |
|
85 | post.build_url() | |
|
86 | post.connect_replies() | |
|
87 | 88 | post.connect_threads(opening_posts) |
|
88 | post.connect_notifications() | |
|
89 | 89 | post.set_global_id() |
|
90 | 90 | |
|
91 | 91 | # Thread needs to be bumped only when the post is already created |
@@ -147,6 +147,3 b' class PostManager(models.Manager):' | |||
|
147 | 147 | thread=thread) |
|
148 | 148 | |
|
149 | 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 | 4 | from django.db.models import Count |
|
5 | 5 | from django.core.urlresolvers import reverse |
|
6 | 6 | |
|
7 | from boards.models import PostImage | |
|
7 | 8 | from boards.models.base import Viewable |
|
9 | from boards.models.thread import STATUS_ACTIVE, STATUS_BUMPLIMIT, STATUS_ARCHIVE | |
|
8 | 10 | from boards.utils import cached_result |
|
9 | 11 | import boards |
|
10 | 12 | |
@@ -61,22 +63,20 b' class Tag(models.Model, Viewable):' | |||
|
61 | 63 | |
|
62 | 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 | 67 | threads = self.get_threads() |
|
66 |
if |
|
|
67 |
threads = threads.filter( |
|
|
68 | if bumpable is not None: | |
|
69 | threads = threads.filter(bumpable=bumpable) | |
|
68 | if status is not None: | |
|
69 | threads = threads.filter(status=status) | |
|
70 | 70 | return threads.count() |
|
71 | 71 | |
|
72 | 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 | 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 | 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 | 81 | def get_absolute_url(self): |
|
82 | 82 | return reverse('tag', kwargs={'tag_name': self.name}) |
@@ -106,11 +106,11 b' class Tag(models.Model, Viewable):' | |||
|
106 | 106 | def get_description(self): |
|
107 | 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 | 110 | posts = boards.models.Post.objects.annotate(images_count=Count( |
|
111 | 111 | 'images')).filter(images_count__gt=0, threads__tags__in=[self]) |
|
112 |
if |
|
|
113 |
posts = posts.filter(thread__ |
|
|
112 | if status is not None: | |
|
113 | posts = posts.filter(thread__status__in=status) | |
|
114 | 114 | return posts.order_by('?').first() |
|
115 | 115 | |
|
116 | 116 | def get_first_letter(self): |
@@ -141,3 +141,7 b' class Tag(models.Model, Viewable):' | |||
|
141 | 141 | |
|
142 | 142 | def get_children(self): |
|
143 | 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 | 5 | from django.utils import timezone |
|
6 | 6 | from django.db import models |
|
7 | 7 | |
|
8 | from boards.models import STATUS_BUMPLIMIT, STATUS_ACTIVE, STATUS_ARCHIVE | |
|
9 | ||
|
8 | 10 | from boards import settings |
|
9 | 11 | import boards |
|
10 | 12 | from boards.utils import cached_result, datetime_to_epoch |
@@ -25,6 +27,12 b" WS_NOTIFICATION_TYPE = 'notification_typ" | |||
|
25 | 27 | |
|
26 | 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 | 37 | class ThreadManager(models.Manager): |
|
30 | 38 | def process_oldest_threads(self): |
@@ -33,7 +41,7 b' class ThreadManager(models.Manager):' | |||
|
33 | 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 | 45 | thread_count = threads.count() |
|
38 | 46 | |
|
39 | 47 | max_thread_count = settings.get_int('Messages', 'MaxThreadCount') |
@@ -50,11 +58,10 b' class ThreadManager(models.Manager):' | |||
|
50 | 58 | logger.info('Processed %d old threads' % num_threads_to_delete) |
|
51 | 59 | |
|
52 | 60 | def _archive_thread(self, thread): |
|
53 | thread.archived = True | |
|
54 | thread.bumpable = False | |
|
61 | thread.status = STATUS_ARCHIVE | |
|
55 | 62 | thread.last_edit_time = timezone.now() |
|
56 | 63 | thread.update_posts_time() |
|
57 |
thread.save(update_fields=[' |
|
|
64 | thread.save(update_fields=['last_edit_time', 'status']) | |
|
58 | 65 | |
|
59 | 66 | def get_new_posts(self, datas): |
|
60 | 67 | query = None |
@@ -90,9 +97,10 b' class Thread(models.Model):' | |||
|
90 | 97 | tags = models.ManyToManyField('Tag', related_name='thread_tags') |
|
91 | 98 | bump_time = models.DateTimeField(db_index=True) |
|
92 | 99 | last_edit_time = models.DateTimeField() |
|
93 | archived = models.BooleanField(default=False) | |
|
94 | bumpable = models.BooleanField(default=True) | |
|
95 | 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 | 105 | def get_tags(self) -> QuerySet: |
|
98 | 106 | """ |
@@ -118,7 +126,7 b' class Thread(models.Model):' | |||
|
118 | 126 | |
|
119 | 127 | def update_bump_status(self, exclude_posts=None): |
|
120 | 128 | if self.has_post_limit() and self.get_reply_count() >= self.max_posts: |
|
121 | self.bumpable = False | |
|
129 | self.status = STATUS_BUMPLIMIT | |
|
122 | 130 | self.update_posts_time(exclude_posts=exclude_posts) |
|
123 | 131 | |
|
124 | 132 | def _get_cache_key(self): |
@@ -138,7 +146,7 b' class Thread(models.Model):' | |||
|
138 | 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 | 151 | def get_last_replies(self) -> QuerySet: |
|
144 | 152 | """ |
@@ -255,4 +263,10 b' class Thread(models.Model):' | |||
|
255 | 263 | return self.get_replies().filter(id__gt=post_id) |
|
256 | 264 | |
|
257 | 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 | 24 | class NotificationManager(models.Manager): |
|
25 |
def get_notification_posts(self, username: st |
|
|
26 |
|
|
|
27 | ||
|
28 | posts = boards.models.post.Post.objects.filter(notification__name=i_username) | |
|
25 | def get_notification_posts(self, usernames: list, last: int = None): | |
|
26 | lower_names = [username.lower() for username in usernames] | |
|
27 | posts = boards.models.post.Post.objects.filter( | |
|
28 | notification__name__in=lower_names).distinct() | |
|
29 | 29 | if last is not None: |
|
30 | 30 | posts = posts.filter(id__gt=last) |
|
31 | 31 | posts = posts.order_by('-id') |
@@ -3,8 +3,12 b' from django.core.urlresolvers import rev' | |||
|
3 | 3 | from django.shortcuts import get_object_or_404 |
|
4 | 4 | from boards.models import Post, Tag, Thread |
|
5 | 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 | 14 | # TODO Make tests for all of these |
@@ -15,7 +19,7 b' class AllThreadsFeed(Feed):' | |||
|
15 | 19 | description_template = 'boards/rss/post.html' |
|
16 | 20 | |
|
17 | 21 | def items(self): |
|
18 |
return Thread.objects. |
|
|
22 | return Thread.objects.exclude(status=STATUS_ARCHIVE).order_by('-id')[:MAX_ITEMS] | |
|
19 | 23 | |
|
20 | 24 | def item_title(self, item): |
|
21 | 25 | return item.get_opening_post().title |
@@ -33,7 +37,7 b' class TagThreadsFeed(Feed):' | |||
|
33 | 37 | description_template = 'boards/rss/post.html' |
|
34 | 38 | |
|
35 | 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 | 42 | def get_object(self, request, tag_name): |
|
39 | 43 | return get_object_or_404(Tag, name=tag_name) |
@@ -57,7 +61,7 b' class ThreadPostsFeed(Feed):' | |||
|
57 | 61 | description_template = 'boards/rss/post.html' |
|
58 | 62 | |
|
59 | 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 | 66 | def get_object(self, request, post_id): |
|
63 | 67 | return get_object_or_404(Post, id=post_id) |
@@ -90,6 +90,7 b' textarea, input {' | |||
|
90 | 90 | padding: inherit; |
|
91 | 91 | background: none; |
|
92 | 92 | font-size: inherit; |
|
93 | cursor: pointer; | |
|
93 | 94 | } |
|
94 | 95 | |
|
95 | 96 | #form-close-button { |
@@ -151,3 +152,8 b' textarea, input {' | |||
|
151 | 152 | .hidden_post:hover { |
|
152 | 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 | 388 | color: #ccc; |
|
389 | 389 | } |
|
390 | 390 | |
|
391 | .role { | |
|
392 | text-decoration: underline; | |
|
393 | } | |
|
394 | ||
|
395 | 391 | .form-email { |
|
396 | 392 | display: none; |
|
397 | 393 | } |
@@ -566,7 +562,6 b' ul {' | |||
|
566 | 562 | } |
|
567 | 563 | |
|
568 | 564 | .image-metadata { |
|
569 | font-style: italic; | |
|
570 | 565 | font-size: 0.9em; |
|
571 | 566 | } |
|
572 | 567 | |
@@ -577,3 +572,7 b' ul {' | |||
|
577 | 572 | #fav-panel { |
|
578 | 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 | 302 | color: #555; |
|
303 | 303 | } |
|
304 | 304 | |
|
305 | .role { | |
|
306 | text-decoration: underline; | |
|
307 | } | |
|
308 | ||
|
309 | 305 | .form-email { |
|
310 | 306 | display: none; |
|
311 | 307 | } |
@@ -380,4 +376,8 b' input[type="submit"]:hover {' | |||
|
380 | 376 | .image-metadata { |
|
381 | 377 | font-style: italic; |
|
382 | 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 | 279 | color: #ccc; |
|
280 | 280 | } |
|
281 | 281 | |
|
282 | .role { | |
|
283 | text-decoration: underline; | |
|
284 | } | |
|
285 | ||
|
286 | 282 | .form-email { |
|
287 | 283 | display: none; |
|
288 | 284 | } |
@@ -416,3 +412,7 b' li {' | |||
|
416 | 412 | audio { |
|
417 | 413 | margin-top: 1em; |
|
418 | 414 | } |
|
415 | ||
|
416 | .post-blink { | |
|
417 | background-color: #ccc; | |
|
418 | } |
@@ -22,7 +22,7 b'' | |||
|
22 | 22 | var form = $('#form'); |
|
23 | 23 | $('textarea').keypress(function(event) { |
|
24 | 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 | 40 | previewTextBlock.html(data); |
|
41 | 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 | 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 | 70 | function ImageViewer() {} |
|
40 | 71 | ImageViewer.prototype.view = function (post) {}; |
|
41 | 72 | |
@@ -48,8 +79,8 b' SimpleImageViewer.prototype.view = funct' | |||
|
48 | 79 | if (images.length == 1) { |
|
49 | 80 | var thumb = images.first(); |
|
50 | 81 | |
|
51 |
var width = thumb |
|
|
52 | var height = thumb.attr('data-height'); | |
|
82 | var width = getFullImageWidth(thumb); | |
|
83 | var height = getFullImageHeight(thumb); | |
|
53 | 84 | |
|
54 | 85 | if (width == null || height == null) { |
|
55 | 86 | width = '100%'; |
@@ -76,10 +107,10 b' PopupImageViewer.prototype.view = functi' | |||
|
76 | 107 | |
|
77 | 108 | var existingPopups = $('#' + thumb_id); |
|
78 | 109 | if (!existingPopups.length) { |
|
79 | var imgElement= el.find('img'); | |
|
110 | var imgElement = el.find('img'); | |
|
80 | 111 | |
|
81 |
var full_img_w = imgElement |
|
|
82 |
var full_img_h = imgElement |
|
|
112 | var full_img_w = getFullImageWidth(imgElement); | |
|
113 | var full_img_h = getFullImageHeight(imgElement); | |
|
83 | 114 | |
|
84 | 115 | var win = $(window); |
|
85 | 116 | |
@@ -156,16 +187,6 b' PopupImageViewer.prototype.view = functi' | |||
|
156 | 187 | }; |
|
157 | 188 | |
|
158 | 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 | 190 | //keybind |
|
170 | 191 | $(document).on('keyup.removepic', function(e) { |
|
171 | 192 | if(e.which === 27) { |
@@ -24,6 +24,7 b'' | |||
|
24 | 24 | */ |
|
25 | 25 | |
|
26 | 26 | var FAV_POST_UPDATE_PERIOD = 10000; |
|
27 | var ITEM_VOLUME_LEVEL = 'volumeLevel'; | |
|
27 | 28 | |
|
28 | 29 | /** |
|
29 | 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 | 142 | $( document ).ready(function() { |
|
112 | 143 | hideEmailFromForm(); |
|
113 | 144 | |
@@ -123,4 +154,7 b' function initFavPanel() {' | |||
|
123 | 154 | highlightCode($(document)); |
|
124 | 155 | |
|
125 | 156 | initFavPanel(); |
|
157 | ||
|
158 | var volumeUsers = $("video,audio"); | |
|
159 | processVolumeUser(volumeUsers); | |
|
126 | 160 | }); |
@@ -18,9 +18,8 b' function $each(list, fn) {' | |||
|
18 | 18 | function mkPreview(cln, html) { |
|
19 | 19 | cln.innerHTML = html; |
|
20 | 20 | |
|
21 | highlightCode($(cln)); | |
|
22 | addRefLinkPreview(cln); | |
|
23 | }; | |
|
21 | addScriptsToPost($(cln)); | |
|
22 | } | |
|
24 | 23 | |
|
25 | 24 | function isElementInViewport (el) { |
|
26 | 25 | //special bonus for those using jQuery |
@@ -28,7 +28,11 b" var CLASS_POST = '.post'" | |||
|
28 | 28 | var POST_ADDED = 0; |
|
29 | 29 | var POST_UPDATED = 1; |
|
30 | 30 | |
|
31 | // TODO These need to be syncronized with board settings. | |
|
31 | 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 | 37 | var ALLOWED_FOR_PARTIAL_UPDATE = [ |
|
34 | 38 | 'refmap', |
@@ -45,6 +49,7 b" var documentOriginalTitle = '';" | |||
|
45 | 49 | |
|
46 | 50 | // Thread ID does not change, can be stored one time |
|
47 | 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 | 55 | * Connect to websocket server and subscribe to thread updates. On any update we |
@@ -195,12 +200,7 b' function updatePost(postHtml) {' | |||
|
195 | 200 | * Initiate a blinking animation on a node to show it was updated. |
|
196 | 201 | */ |
|
197 | 202 | function blink(node) { |
|
198 | var blinkCount = 2; | |
|
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 | } | |
|
203 | node.effect('highlight', { color: blinkColor }, BLINK_SPEED); | |
|
204 | 204 | } |
|
205 | 205 | |
|
206 | 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 | 357 | * Run js methods that are usually run on the document, on the new post |
|
371 | 358 | */ |
|
372 | 359 | function processNewPost(post) { |
|
373 | addRefLinkPreview(post[0]); | |
|
374 | highlightCode(post); | |
|
360 | addScriptsToPost(post); | |
|
375 | 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 | 420 | if (initAutoupdate()) { |
|
435 | 421 | // Post form data over AJAX |
|
436 | 422 | var threadId = $('div.thread').children('.post').first().attr('id'); |
@@ -439,14 +425,15 b' function updateNodeAttr(oldNode, newNode' | |||
|
439 | 425 | |
|
440 | 426 | if (form.length > 0) { |
|
441 | 427 | var options = { |
|
442 |
beforeSubmit: function(arr, |
|
|
443 |
showAsErrors( |
|
|
428 | beforeSubmit: function(arr, form, options) { | |
|
429 | showAsErrors(form, gettext('Sending message...')); | |
|
444 | 430 | }, |
|
445 | 431 | success: updateOnPost, |
|
446 | 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 | 439 | form.ajaxForm(options); |
@@ -31,8 +31,8 b'' | |||
|
31 | 31 | {% for banner in banners %} |
|
32 | 32 | <div class="post"> |
|
33 | 33 | <div class="title">{{ banner.title }}</div> |
|
34 | <div>{{ banner.text }}</div> | |
|
35 |
<div>{% trans ' |
|
|
34 | <div>{{ banner.get_text|safe }}</div> | |
|
35 | <div>{% trans 'Details' %}: <a href="{{ banner.post.get_absolute_url }}">>>{{ banner.post.id }}</a></div> | |
|
36 | 36 | </div> |
|
37 | 37 | {% endfor %} |
|
38 | 38 | |
@@ -44,38 +44,50 b'' | |||
|
44 | 44 | <a href="{{ random_image_post.get_absolute_url }}"><img |
|
45 | 45 | src="{{ image.image.url_200x150 }}" |
|
46 | 46 | width="{{ image.pre_width }}" |
|
47 |
height="{{ image.pre_height }}" |
|
|
47 | height="{{ image.pre_height }}" | |
|
48 | alt="{{ random_image_post.id }}"/></a> | |
|
48 | 49 | {% endwith %} |
|
49 | 50 | </div> |
|
50 | 51 | {% endif %} |
|
51 | 52 | <div class="tag-text-data"> |
|
52 | 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 | 60 | <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form"> |
|
54 | 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 | 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 | 65 | {% endif %} |
|
59 | 66 | </form> |
|
60 | 67 | <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form"> |
|
61 | 68 | {% if is_hidden %} |
|
62 |
<button name="method" value="unhide" class="fav"> |
|
|
69 | <button name="method" value="unhide" class="fav">{% trans "Show" %}</button> | |
|
63 | 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 | 72 | {% endif %} |
|
66 | 73 | </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> | |
|
70 | {% endif %} | |
|
71 | </h2> | |
|
74 | <a href="{% url 'tag_gallery' tag.name %}">{% trans 'Gallery' %}</a> | |
|
75 | </p> | |
|
72 | 76 | {% if tag.get_description %} |
|
73 | 77 | <p>{{ tag.get_description|safe }}</p> |
|
74 | 78 | {% endif %} |
|
75 | 79 | <p> |
|
76 | {% blocktrans count count=tag.get_active_thread_count %}{{ count }} active thread{% plural %}active threads{% endblocktrans %}, | |
|
77 | {% blocktrans count count=tag.get_bumplimit_thread_count %}{{ count }} thread in bumplimit{% plural %} threads in bumplimit{% endblocktrans %}, | |
|
78 |
{% blocktrans count count= |
|
|
80 | {% with active_count=tag.get_active_thread_count bumplimit_count=tag.get_bumplimit_thread_count archived_count=tag.get_archived_thread_count %} | |
|
81 | {% if active_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 | 91 | {% blocktrans count count=tag.get_post_count %}{{ count }} message{% plural %}messages{% endblocktrans %}. |
|
80 | 92 | </p> |
|
81 | 93 | {% if tag.get_all_parents %} |
@@ -99,7 +111,7 b'' | |||
|
99 | 111 | |
|
100 | 112 | {% for thread in threads %} |
|
101 | 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 | 115 | {% if not thread.archived %} |
|
104 | 116 | {% with last_replies=thread.get_last_replies %} |
|
105 | 117 | {% if last_replies %} |
@@ -114,7 +126,7 b'' | |||
|
114 | 126 | {% endwith %} |
|
115 | 127 | <div class="last-replies"> |
|
116 | 128 | {% for post in last_replies %} |
|
117 |
{% post_view post |
|
|
129 | {% post_view post truncated=True %} | |
|
118 | 130 | {% endfor %} |
|
119 | 131 | </div> |
|
120 | 132 | {% endif %} |
@@ -148,6 +160,9 b'' | |||
|
148 | 160 | </div> |
|
149 | 161 | <div> |
|
150 | 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 | 166 | </div> |
|
152 | 167 | <div id="preview-text"></div> |
|
153 | 168 | <div><a href="{% url "staticpage" name="help" %}">{% trans 'Text syntax' %}</a></div> |
@@ -156,6 +171,8 b'' | |||
|
156 | 171 | </div> |
|
157 | 172 | |
|
158 | 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 | 176 | <script src="{% static 'js/thread_create.js' %}"></script> |
|
160 | 177 | |
|
161 | 178 | {% endblock %} |
@@ -180,7 +197,6 b'' | |||
|
180 | 197 | {% endfor %} |
|
181 | 198 | {% endwith %} |
|
182 | 199 | ] |
|
183 | [<a href="rss/">RSS</a>] | |
|
184 | 200 | </span> |
|
185 | 201 | |
|
186 | 202 | {% endblock %} |
@@ -9,6 +9,9 b'' | |||
|
9 | 9 | {% block content %} |
|
10 | 10 | <div class="post"> |
|
11 | 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 | 15 | <h2>{% trans 'Authors' %}</h2> |
|
13 | 16 | {% for nick, values in authors.items %} |
|
14 | 17 | <p> |
@@ -16,10 +19,7 b'' | |||
|
16 | 19 | {% for value in values.contacts %} |
|
17 | 20 | <a href="mailto:{{ value }}">{{ value }}</a> |
|
18 | 21 | {% endfor %} - |
|
19 |
{ |
|
|
20 | <span class="role">{% trans role %}</span> | |
|
21 | {% if not forloop.last %}, {% endif %} | |
|
22 | {% endfor %} | |
|
22 | {{ values.roles|join:', ' }} | |
|
23 | 23 | </p> |
|
24 | 24 | {% endfor %} |
|
25 | 25 | <br /> |
@@ -11,7 +11,9 b'' | |||
|
11 | 11 | <link rel="stylesheet" type="text/css" href="{% static 'css/3party/jquery-ui.min.css' %}" media="all"/> |
|
12 | 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 | 18 | <link rel="icon" type="image/png" |
|
17 | 19 | href="{% static 'favicon.png' %}"> |
@@ -21,11 +23,8 b'' | |||
|
21 | 23 | |
|
22 | 24 | {% block head %}{% endblock %} |
|
23 | 25 | </head> |
|
24 | <body data-image-viewer="{{ image_viewer }}"> | |
|
26 | <body data-image-viewer="{{ image_viewer }}" data-pow-difficulty="{{ pow_difficulty }}"> | |
|
25 | 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 | 29 | <div class="navigation_panel header"> |
|
31 | 30 | <a class="link" href="{% url 'index' %}">{% trans "All threads" %}</a> |
@@ -44,8 +43,8 b'' | |||
|
44 | 43 | <a href="#" id="fav-panel-btn">{% trans 'favorites' %} <span id="new-fav-post-count"></span></a> |
|
45 | 44 | {% endif %} |
|
46 | 45 | |
|
47 | {% if username %} | |
|
48 |
<a class="right-link link" href="{% url 'notifications' |
|
|
46 | {% if usernames %} | |
|
47 | <a class="right-link link" href="{% url 'notifications' %}" title="{% trans 'Notifications' %}"> | |
|
49 | 48 | {% trans 'Notifications' %} |
|
50 | 49 | {% ifnotequal new_notifications_count 0 %} |
|
51 | 50 | (<b>{{ new_notifications_count }}</b>) |
@@ -60,7 +59,12 b'' | |||
|
60 | 59 | |
|
61 | 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 | 64 | <script src="{% static 'js/3party/highlight.min.js' %}"></script> |
|
65 | ||
|
66 | <script src="{% url 'js_info_dict' %}"></script> | |
|
67 | ||
|
64 | 68 | <script src="{% static 'js/popup.js' %}"></script> |
|
65 | 69 | <script src="{% static 'js/image.js' %}"></script> |
|
66 | 70 | <script src="{% static 'js/refpopup.js' %}"></script> |
@@ -68,6 +72,9 b'' | |||
|
68 | 72 | |
|
69 | 73 | <div class="navigation_panel footer"> |
|
70 | 74 | {% block metapanel %}{% endblock %} |
|
75 | {% if rss_url %} | |
|
76 | [<a href="{{ rss_url }}">RSS</a>] | |
|
77 | {% endif %} | |
|
71 | 78 | [<a href="{% url 'admin:index' %}">{% trans 'Admin' %}</a>] |
|
72 | 79 | [<a href="{% url 'index' %}?order=pub">{% trans 'New threads' %}</a>] |
|
73 | 80 | {% with ppd=posts_per_day|floatformat:2 %} |
@@ -5,11 +5,15 b'' | |||
|
5 | 5 | |
|
6 | 6 | {% block head %} |
|
7 | 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 | 9 | {% endblock %} |
|
10 | 10 | |
|
11 | 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 | 18 | {% if page %} |
|
15 | 19 | {% if page.has_previous %} |
@@ -21,14 +21,14 b'' | |||
|
21 | 21 | and this is an opening post (thread death time) or a post for popup |
|
22 | 22 | (we don't see OP here so we show the death time in the post itself). |
|
23 | 23 | {% endcomment %} |
|
24 | {% if thread.archived %} | |
|
24 | {% if thread.is_archived %} | |
|
25 | 25 | {% if is_opening %} |
|
26 | 26 | — <time datetime="{{ thread.bump_time|date:'c' }}">{{ thread.bump_time }}</time> |
|
27 | 27 | {% endif %} |
|
28 | 28 | {% endif %} |
|
29 | 29 | {% if is_opening %} |
|
30 | 30 | {% if need_open_link %} |
|
31 | {% if thread.archived %} | |
|
31 | {% if thread.is_archived %} | |
|
32 | 32 | <a class="link" href="{% url 'thread' post.id %}">{% trans "Open" %}</a> |
|
33 | 33 | {% else %} |
|
34 | 34 | <a class="link" href="{% url 'thread' post.id %}#form">{% trans "Reply" %}</a> |
@@ -41,7 +41,7 b'' | |||
|
41 | 41 | {% endwith %} |
|
42 | 42 | {% endif %} |
|
43 | 43 | {% endif %} |
|
44 | {% if reply_link and not thread.archived %} | |
|
44 | {% if reply_link and not thread.is_archived %} | |
|
45 | 45 | <a href="#form" onclick="addQuickReply('{{ post.id }}'); return false;">{% trans 'Reply' %}</a> |
|
46 | 46 | {% endif %} |
|
47 | 47 | |
@@ -49,15 +49,16 b'' | |||
|
49 | 49 | <a class="global-id" href="{% url 'post_sync_data' post.id %}"> [RAW] </a> |
|
50 | 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 | 53 | <span class="moderator_info"> |
|
54 | | <a href="{% url 'admin:boards_post_change' post.id %}">{% trans 'Edit' %}</a> | |
|
55 | {% if is_opening %} | |
|
56 | | <a href="{% url 'admin:boards_thread_change' thread.id %}">{% trans 'Edit thread' %}</a> | |
|
54 | {% if perms.boards.change_post or perms.boards.delete_post %} | |
|
55 | | <a href="{% url 'admin:boards_post_change' post.id %}">{% trans 'Edit' %}</a> | |
|
57 | 56 | {% endif %} |
|
58 | <form action="{% url 'thread' thread.get_opening_post_id %}?post_id={{ post.id }}" method="post" class="post-button-form"> | |
|
59 | | <button name="method" value="toggle_hide_post">H</button> | |
|
60 | </form> | |
|
57 | {% if perms.boards.change_thread or perms_boards.delete_thread %} | |
|
58 | {% if is_opening %} | |
|
59 | | <a href="{% url 'admin:boards_thread_change' thread.id %}">{% trans 'Edit thread' %}</a> | |
|
60 | {% endif %} | |
|
61 | {% endif %} | |
|
61 | 62 | </form> |
|
62 | 63 | </span> |
|
63 | 64 | {% endif %} |
@@ -15,7 +15,7 b'' | |||
|
15 | 15 | <p>[s]<span class="strikethrough">{% trans 'Strikethrough text' %}</span>[/s]</p> |
|
16 | 16 | <p>[comment]<span class="comment">{% trans 'Comment' %}</span>[/comment]</p> |
|
17 | 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 | 19 | <p>[tag]<a class="tag">tag</a>[/tag]</p> |
|
20 | 20 | <br/> |
|
21 | 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 | 8 | {% block head %} |
|
9 | 9 | <meta name="robots" content="noindex"> |
|
10 | 10 | |
|
11 | {% if tag %} | |
|
12 | <title>{{ tag.name }} - {{ site_name }}</title> | |
|
13 | {% else %} | |
|
14 | <title>{{ site_name }}</title> | |
|
15 | {% endif %} | |
|
11 | <title>{{ tag.name }} - {% trans 'Gallery' %} - {{ site_name }}</title> | |
|
16 | 12 | |
|
17 | 13 | {% if prev_page_link %} |
|
18 | 14 | <link rel="prev" href="{{ prev_page_link }}" /> |
@@ -31,12 +27,11 b'' | |||
|
31 | 27 | {% for banner in banners %} |
|
32 | 28 | <div class="post"> |
|
33 | 29 | <div class="title">{{ banner.title }}</div> |
|
34 | <div>{{ banner.text }}</div> | |
|
35 |
<div>{% trans ' |
|
|
30 | <div>{{ banner.get_text }}</div> | |
|
31 | <div>{% trans 'Details' %}: <a href="{{ banner.post.get_absolute_url }}">>>{{ banner.post.id }}</a></div> | |
|
36 | 32 | </div> |
|
37 | 33 | {% endfor %} |
|
38 | 34 | |
|
39 | {% if tag %} | |
|
40 | 35 | <div class="tag_info" style="border-bottom: solid .5ex #{{ tag.get_color }}"> |
|
41 | 36 | {% if random_image_post %} |
|
42 | 37 | <div class="tag-image"> |
@@ -44,28 +39,15 b'' | |||
|
44 | 39 | <a href="{{ random_image_post.get_absolute_url }}"><img |
|
45 | 40 | src="{{ image.image.url_200x150 }}" |
|
46 | 41 | width="{{ image.pre_width }}" |
|
47 |
height="{{ image.pre_height }}" |
|
|
42 | height="{{ image.pre_height }}" | |
|
43 | alt="{{ random_image_post.id }}"/></a> | |
|
48 | 44 | {% endwith %} |
|
49 | 45 | </div> |
|
50 | 46 | {% endif %} |
|
51 | 47 | <div class="tag-text-data"> |
|
52 | 48 | <h2> |
|
53 | <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form"> | |
|
54 |
|
|
|
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 %} | |
|
49 | /{{ tag.get_view|safe }}/ | |
|
50 | {% if perms.change_tag %} | |
|
69 | 51 | <span class="moderator_info">| <a href="{% url 'admin:boards_tag_change' tag.id %}">{% trans 'Edit tag' %}</a></span> |
|
70 | 52 | {% endif %} |
|
71 | 53 | </h2> |
@@ -73,9 +55,17 b'' | |||
|
73 | 55 | <p>{{ tag.get_description|safe }}</p> |
|
74 | 56 | {% endif %} |
|
75 | 57 | <p> |
|
76 | {% blocktrans count count=tag.get_active_thread_count %}{{ count }} active thread{% plural %}active threads{% endblocktrans %}, | |
|
77 | {% blocktrans count count=tag.get_bumplimit_thread_count %}{{ count }} thread in bumplimit{% plural %} threads in bumplimit{% endblocktrans %}, | |
|
78 |
{% blocktrans count count= |
|
|
58 | {% with active_count=tag.get_active_thread_count bumplimit_count=tag.get_bumplimit_thread_count archived_count=tag.get_archived_thread_count %} | |
|
59 | {% if active_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 | 69 | {% blocktrans count count=tag.get_post_count %}{{ count }} message{% plural %}messages{% endblocktrans %}. |
|
80 | 70 | </p> |
|
81 | 71 | {% if tag.get_all_parents %} |
@@ -88,76 +78,29 b'' | |||
|
88 | 78 | {% endif %} |
|
89 | 79 | </div> |
|
90 | 80 | </div> |
|
91 | {% endif %} | |
|
92 | 81 | |
|
93 |
{% if |
|
|
94 | {% if prev_page_link %} | |
|
95 | <div class="page_link"> | |
|
96 | <a href="{{ prev_page_link }}">{% trans "Previous page" %}</a> | |
|
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> | |
|
82 | {% if prev_page_link %} | |
|
83 | <div class="page_link"> | |
|
84 | <a href="{{ prev_page_link }}">{% trans "Previous page" %}</a> | |
|
85 | </div> | |
|
134 | 86 | {% endif %} |
|
135 | 87 | |
|
136 | <div class="post-form-w"> | |
|
137 | <script src="{% static 'js/panel.js' %}"></script> | |
|
138 | <div class="post-form"> | |
|
139 | <div class="form-title">{% trans "Create new thread" %}</div> | |
|
140 | <div class="swappable-form-full"> | |
|
141 | <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %} | |
|
142 | {{ form.as_div }} | |
|
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> | |
|
88 | {% for image in images %} | |
|
89 | <div class="gallery_image"> | |
|
90 | {% autoescape off %} | |
|
91 | {{ image.get_view }} | |
|
92 | {% endautoescape %} | |
|
93 | <div class="gallery_image_metadata"> | |
|
94 | {{ image.width }}x{{ image.height }} | |
|
148 | 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 | 96 | </div> |
|
156 | </div> | |
|
97 | {% endfor %} | |
|
157 | 98 | |
|
158 | <script src="{% static 'js/form.js' %}"></script> | |
|
159 | <script src="{% static 'js/thread_create.js' %}"></script> | |
|
160 | ||
|
99 | {% if next_page_link %} | |
|
100 | <div class="page_link"> | |
|
101 | <a href="{{ next_page_link }}">{% trans "Next page" %}</a> | |
|
102 | </div> | |
|
103 | {% endif %} | |
|
161 | 104 | {% endblock %} |
|
162 | 105 | |
|
163 | 106 | {% block metapanel %} |
@@ -172,7 +115,7 b'' | |||
|
172 | 115 | …, |
|
173 | 116 | {% endif %} |
|
174 | 117 | <a |
|
175 |
{% ifequal page current_page |
|
|
118 | {% ifequal page paginator.current_page %} | |
|
176 | 119 | class="current_page" |
|
177 | 120 | {% endifequal %} |
|
178 | 121 | href="{% page_url paginator page %}">{{ page }}</a> |
@@ -180,7 +123,6 b'' | |||
|
180 | 123 | {% endfor %} |
|
181 | 124 | {% endwith %} |
|
182 | 125 | ] |
|
183 | [<a href="rss/">RSS</a>] | |
|
184 | 126 | </span> |
|
185 | 127 | |
|
186 | 128 | {% endblock %} |
@@ -37,8 +37,7 b'' | |||
|
37 | 37 | {% with images_count=thread.get_images_count%} |
|
38 | 38 | <span id="image-count">{{ images_count }}</span> <span id="image-count-text">{% blocktrans count count=images_count %}image{% plural %}images{% endblocktrans %}</span>. |
|
39 | 39 | {% endwith %} |
|
40 |
|
|
|
41 | [<a href="rss/">RSS</a>] | |
|
40 | {% trans 'Last update: ' %}<span id="last-update"><time datetime="{{ thread.last_edit_time|date:'c' }}">{{ thread.last_edit_time }}</time></span> | |
|
42 | 41 | </span> |
|
43 | 42 | |
|
44 | 43 | {% endblock %} |
@@ -12,6 +12,7 b'' | |||
|
12 | 12 | <div class="tag_info"> |
|
13 | 13 | <h2> |
|
14 | 14 | <form action="{% url 'thread' opening_post.id %}" method="post" class="post-button-form"> |
|
15 | {% csrf_token %} | |
|
15 | 16 | {% if is_favorite %} |
|
16 | 17 | <button name="method" value="unsubscribe" class="fav">★</button> |
|
17 | 18 | {% else %} |
@@ -34,11 +35,11 b'' | |||
|
34 | 35 | |
|
35 | 36 | <div class="thread"> |
|
36 | 37 | {% for post in thread.get_replies %} |
|
37 |
{% post_view post |
|
|
38 | {% post_view post reply_link=True %} | |
|
38 | 39 | {% endfor %} |
|
39 | 40 | </div> |
|
40 | 41 | |
|
41 | {% if not thread.archived %} | |
|
42 | {% if not thread.is_archived %} | |
|
42 | 43 | <div class="post-form-w"> |
|
43 | 44 | <script src="{% static 'js/panel.js' %}"></script> |
|
44 | 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 | 55 | </form> |
|
55 | 56 | </div> |
|
56 | 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 | 63 | <div><a href="{% url "staticpage" name="help" %}"> |
|
58 | 64 | {% trans 'Text syntax' %}</a></div> |
|
59 | 65 | <div><a id="form-close-button" href="#" onClick="resetFormPosition(); return false;">{% trans 'Close form' %}</a></div> |
|
60 | 66 | </div> |
|
61 | 67 | </div> |
|
62 | 68 | |
|
69 | <script src="{% static 'js/form.js' %}"></script> | |
|
63 | 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 | 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 | 77 | <script src="{% static 'js/3party/centrifuge.js' %}"></script> |
|
70 | 78 | {% endblock %} |
@@ -11,7 +11,7 b'' | |||
|
11 | 11 | |
|
12 | 12 | <div class="thread"> |
|
13 | 13 | {% for post in thread.get_top_level_replies %} |
|
14 |
{% post_view post mode |
|
|
14 | {% post_view post mode_tree=True %} | |
|
15 | 15 | {% endfor %} |
|
16 | 16 | </div> |
|
17 | 17 |
@@ -39,8 +39,9 b' def image_actions(*args, **kwargs):' | |||
|
39 | 39 | action['link'] % image_link, action['name']) for action in actions]) |
|
40 | 40 | |
|
41 | 41 | |
|
42 | @register.simple_tag(name='post_view') | |
|
43 | def post_view(post, *args, **kwargs): | |
|
42 | @register.simple_tag(name='post_view', takes_context=True) | |
|
43 | def post_view(context, post, *args, **kwargs): | |
|
44 | kwargs['perms'] = context['perms'] | |
|
44 | 45 | return post.get_view(*args, **kwargs) |
|
45 | 46 | |
|
46 | 47 | @register.simple_tag(name='page_url') |
@@ -31,6 +31,7 b' class ApiTest(TestCase):' | |||
|
31 | 31 | req = MockRequest() |
|
32 | 32 | req.POST['thread'] = opening_post.id |
|
33 | 33 | req.POST['uids'] = ' '.join(uids) |
|
34 | req.user = None | |
|
34 | 35 | # Check the timestamp before post was added |
|
35 | 36 | response = api.api_get_threaddiff(req) |
|
36 | 37 | diff = simplejson.loads(response.content) |
@@ -3,6 +3,7 b' from django.test import TestCase' | |||
|
3 | 3 | |
|
4 | 4 | from boards import settings |
|
5 | 5 | from boards.models import Tag, Post, Thread, KeyPair |
|
6 | from boards.models.thread import STATUS_ARCHIVE | |
|
6 | 7 | |
|
7 | 8 | |
|
8 | 9 | class PostTests(TestCase): |
@@ -96,7 +97,7 b' class PostTests(TestCase):' | |||
|
96 | 97 | self._create_post() |
|
97 | 98 | |
|
98 | 99 | self.assertEqual(settings.get_int('Messages', 'MaxThreadCount'), |
|
99 |
len(Thread.objects. |
|
|
100 | len(Thread.objects.exclude(status=STATUS_ARCHIVE))) | |
|
100 | 101 | |
|
101 | 102 | def test_pages(self): |
|
102 | 103 | """Test that the thread list is properly split into pages""" |
@@ -104,9 +105,9 b' class PostTests(TestCase):' | |||
|
104 | 105 | for i in range(settings.get_int('Messages', 'MaxThreadCount')): |
|
105 | 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 | 111 | settings.get_int('View', 'ThreadsPerPage')) |
|
111 | 112 | posts_in_second_page = paginator.page(2).object_list |
|
112 | 113 | first_post = posts_in_second_page[0] |
@@ -41,8 +41,6 b' class ViewTest(TestCase):' | |||
|
41 | 41 | except NoReverseMatch: |
|
42 | 42 | # This view just needs additional arguments |
|
43 | 43 | pass |
|
44 | except Exception as e: | |
|
45 | self.fail('Got exception %s at %s view' % (e, view_name)) | |
|
46 | 44 | except AttributeError: |
|
47 | 45 | # This is normal, some views do not have names |
|
48 | 46 | pass |
@@ -1,5 +1,5 b'' | |||
|
1 | 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 | 4 | from boards import views |
|
5 | 5 | from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed |
@@ -12,6 +12,8 b' from boards.views.static import StaticPa' | |||
|
12 | 12 | from boards.views.preview import PostPreviewView |
|
13 | 13 | from boards.views.sync import get_post_sync_data, response_get, response_pull |
|
14 | 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 | 19 | js_info_dict = { |
@@ -45,6 +47,7 b" urlpatterns = patterns(''," | |||
|
45 | 47 | name='staticpage'), |
|
46 | 48 | |
|
47 | 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 | 52 | # RSS feeds |
|
50 | 53 | url(r'^rss/$', AllThreadsFeed()), |
@@ -54,7 +57,7 b" urlpatterns = patterns(''," | |||
|
54 | 57 | url(r'^thread/(?P<post_id>\d+)/rss/$', ThreadPostsFeed()), |
|
55 | 58 | |
|
56 | 59 | # i18n |
|
57 | url(r'^jsi18n/$', javascript_catalog, js_info_dict, | |
|
60 | url(r'^jsi18n/$', cached_javascript_catalog, js_info_dict, | |
|
58 | 61 | name='js_info_dict'), |
|
59 | 62 | |
|
60 | 63 | # API |
@@ -81,7 +84,8 b" urlpatterns = patterns(''," | |||
|
81 | 84 | url(r'^search/$', BoardSearchView.as_view(), name='search'), |
|
82 | 85 | |
|
83 | 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 | 90 | # Post preview |
|
87 | 91 | url(r'^preview/$', PostPreviewView.as_view(), name='preview'), |
@@ -9,6 +9,7 b' import hmac' | |||
|
9 | 9 | from django.core.cache import cache |
|
10 | 10 | from django.db.models import Model |
|
11 | 11 | from django import forms |
|
12 | from django.template.defaultfilters import filesizeformat | |
|
12 | 13 | from django.utils import timezone |
|
13 | 14 | from django.utils.translation import ugettext_lazy as _ |
|
14 | 15 | import magic |
@@ -19,7 +20,6 b' from boards.settings import get_bool' | |||
|
19 | 20 | from neboard import settings |
|
20 | 21 | |
|
21 | 22 | CACHE_KEY_DELIMITER = '_' |
|
22 | PERMISSION_MODERATE = 'moderation' | |
|
23 | 23 | |
|
24 | 24 | HTTP_FORWARDED = 'HTTP_X_FORWARDED_FOR' |
|
25 | 25 | META_REMOTE_ADDR = 'REMOTE_ADDR' |
@@ -73,15 +73,20 b" def get_websocket_token(user_id='', time" | |||
|
73 | 73 | return token |
|
74 | 74 | |
|
75 | 75 | |
|
76 | # TODO Test this carefully | |
|
76 | 77 | def cached_result(key_method=None): |
|
77 | 78 | """ |
|
78 | 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 | 82 | def _cached_result(function): |
|
82 | 83 | def inner_func(obj, *args, **kwargs): |
|
83 | # TODO Include method arguments to the cache key | |
|
84 | 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 | 90 | if isinstance(obj, Model): |
|
86 | 91 | cache_key_params.append(str(obj.id)) |
|
87 | 92 | |
@@ -103,15 +108,6 b' def cached_result(key_method=None):' | |||
|
103 | 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 | 111 | def get_file_hash(file) -> str: |
|
116 | 112 | md5 = hashlib.md5() |
|
117 | 113 | for chunk in file.chunks(): |
@@ -123,8 +119,8 b' def validate_file_size(size: int):' | |||
|
123 | 119 | max_size = boards.settings.get_int('Forms', 'MaxFileSize') |
|
124 | 120 | if size > max_size: |
|
125 | 121 | raise forms.ValidationError( |
|
126 |
_('File must be less than %s b |
|
|
127 | % str(max_size)) | |
|
122 | _('File must be less than %s but is %s.') | |
|
123 | % (filesizeformat(max_size), filesizeformat(size))) | |
|
128 | 124 | |
|
129 | 125 | |
|
130 | 126 | def get_extension(filename): |
@@ -1,3 +1,4 b'' | |||
|
1 | from dbus.decorators import method | |
|
1 | 2 | from django.core.urlresolvers import reverse |
|
2 | 3 | from django.core.files import File |
|
3 | 4 | from django.core.files.temp import NamedTemporaryFile |
@@ -6,6 +7,8 b' from django.db import transaction' | |||
|
6 | 7 | from django.http import Http404 |
|
7 | 8 | from django.shortcuts import render, redirect |
|
8 | 9 | import requests |
|
10 | from django.utils.decorators import method_decorator | |
|
11 | from django.views.decorators.csrf import csrf_protect | |
|
9 | 12 | |
|
10 | 13 | from boards import utils, settings |
|
11 | 14 | from boards.abstracts.paginator import get_paginator |
@@ -15,7 +18,7 b' from boards.models import Post, Thread, ' | |||
|
15 | 18 | from boards.views.banned import BannedView |
|
16 | 19 | from boards.views.base import BaseBoardView, CONTEXT_FORM |
|
17 | 20 | from boards.views.posting_mixin import PostMixin |
|
18 | ||
|
21 | from boards.views.mixins import FileUploadMixin, PaginatedMixin | |
|
19 | 22 | |
|
20 | 23 | FORM_TAGS = 'tags' |
|
21 | 24 | FORM_TEXT = 'text' |
@@ -30,20 +33,20 b" PARAMETER_PAGINATOR = 'paginator'" | |||
|
30 | 33 | PARAMETER_THREADS = 'threads' |
|
31 | 34 | PARAMETER_BANNERS = 'banners' |
|
32 | 35 | PARAMETER_ADDITIONAL = 'additional_params' |
|
33 | ||
|
34 |
PARAMETER_ |
|
|
35 | PARAMETER_NEXT_LINK = 'next_page_link' | |
|
36 | PARAMETER_MAX_FILE_SIZE = 'max_file_size' | |
|
37 | PARAMETER_RSS_URL = 'rss_url' | |
|
36 | 38 | |
|
37 | 39 | TEMPLATE = 'boards/all_threads.html' |
|
38 | 40 | DEFAULT_PAGE = 1 |
|
39 | 41 | |
|
40 | 42 | |
|
41 | class AllThreadsView(PostMixin, BaseBoardView): | |
|
43 | class AllThreadsView(PostMixin, FileUploadMixin, BaseBoardView, PaginatedMixin): | |
|
42 | 44 | |
|
43 | 45 | def __init__(self): |
|
44 | 46 | self.settings_manager = None |
|
45 | 47 | super(AllThreadsView, self).__init__() |
|
46 | 48 | |
|
49 | @method_decorator(csrf_protect) | |
|
47 | 50 | def get(self, request, form: ThreadForm=None): |
|
48 | 51 | page = request.GET.get('page', DEFAULT_PAGE) |
|
49 | 52 | |
@@ -74,12 +77,15 b' class AllThreadsView(PostMixin, BaseBoar' | |||
|
74 | 77 | params[PARAMETER_THREADS] = threads |
|
75 | 78 | params[CONTEXT_FORM] = form |
|
76 | 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 | 83 | paginator.set_url(self.get_reverse_url(), request.GET.dict()) |
|
79 | 84 | self.get_page_context(paginator, params, page) |
|
80 | 85 | |
|
81 | 86 | return render(request, TEMPLATE, params) |
|
82 | 87 | |
|
88 | @method_decorator(csrf_protect) | |
|
83 | 89 | def post(self, request): |
|
84 | 90 | form = ThreadForm(request.POST, request.FILES, |
|
85 | 91 | error_class=PlainErrorList) |
@@ -101,12 +107,7 b' class AllThreadsView(PostMixin, BaseBoar' | |||
|
101 | 107 | params[PARAMETER_PAGINATOR] = paginator |
|
102 | 108 | current_page = paginator.page(int(page)) |
|
103 | 109 | params[PARAMETER_CURRENT_PAGE] = current_page |
|
104 | if current_page.has_previous(): | |
|
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 | self.set_page_urls(paginator, params) | |
|
110 | 111 | |
|
111 | 112 | def get_reverse_url(self): |
|
112 | 113 | return reverse('index') |
@@ -136,10 +137,12 b' class AllThreadsView(PostMixin, BaseBoar' | |||
|
136 | 137 | text = self._remove_invalid_links(text) |
|
137 | 138 | |
|
138 | 139 | tags = data[FORM_TAGS] |
|
140 | monochrome = form.is_monochrome() | |
|
139 | 141 | |
|
140 | 142 | post = Post.objects.create_post(title=title, text=text, file=file, |
|
141 | 143 | ip=ip, tags=tags, opening_posts=threads, |
|
142 |
tripcode=form.get_tripcode() |
|
|
144 | tripcode=form.get_tripcode(), | |
|
145 | monochrome=monochrome) | |
|
143 | 146 | |
|
144 | 147 | # This is required to update the threads to which posts we have replied |
|
145 | 148 | # when creating this one |
@@ -155,3 +158,6 b' class AllThreadsView(PostMixin, BaseBoar' | |||
|
155 | 158 | |
|
156 | 159 | return Thread.objects\ |
|
157 | 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 |
|
|
3 | 2 | import logging |
|
4 | 3 | |
|
5 | import xml.etree.ElementTree as ET | |
|
6 | ||
|
4 | from django.core import serializers | |
|
7 | 5 | from django.db import transaction |
|
8 | from django.db.models import Count | |
|
9 | 6 | from django.http import HttpResponse |
|
10 | 7 | from django.shortcuts import get_object_or_404 |
|
11 | from django.core import serializers | |
|
12 | from boards.abstracts.settingsmanager import get_settings_manager,\ | |
|
13 | FAV_THREAD_NO_UPDATES | |
|
8 | from django.views.decorators.csrf import csrf_protect | |
|
14 | 9 | |
|
10 | from boards.abstracts.settingsmanager import get_settings_manager | |
|
15 | 11 | from boards.forms import PostForm, PlainErrorList |
|
16 | from boards.models import Post, Thread, Tag, GlobalId | |
|
17 |
from boards.models |
|
|
12 | from boards.mdx_neboard import Parser | |
|
13 | from boards.models import Post, Thread, Tag | |
|
14 | from boards.models.thread import STATUS_ARCHIVE | |
|
15 | from boards.models.user import Notification | |
|
18 | 16 | from boards.utils import datetime_to_epoch |
|
19 | 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 | 19 | __author__ = 'neko259' |
|
25 | 20 | |
@@ -49,8 +44,12 b' def api_get_threaddiff(request):' | |||
|
49 | 44 | """ |
|
50 | 45 | |
|
51 | 46 | thread_id = request.POST.get(PARAMETER_THREAD) |
|
52 |
uids_str = request.POST.get(PARAMETER_UIDS) |
|
|
53 | uids = uids_str.split(' ') | |
|
47 | uids_str = request.POST.get(PARAMETER_UIDS) | |
|
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 | 54 | opening_post = get_object_or_404(Post, id=thread_id) |
|
56 | 55 | thread = opening_post.get_thread() |
@@ -77,6 +76,7 b' def api_get_threaddiff(request):' | |||
|
77 | 76 | return HttpResponse(content=json.dumps(json_data)) |
|
78 | 77 | |
|
79 | 78 | |
|
79 | @csrf_protect | |
|
80 | 80 | def api_add_post(request, opening_post_id): |
|
81 | 81 | """ |
|
82 | 82 | Adds a post and return the JSON response for it |
@@ -125,7 +125,7 b' def get_post(request, post_id):' | |||
|
125 | 125 | post = get_object_or_404(Post, id=post_id) |
|
126 | 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 | 131 | def api_get_threads(request, count): |
@@ -139,9 +139,9 b' def api_get_threads(request, count):' | |||
|
139 | 139 | tag_name = request.GET[PARAMETER_TAG] |
|
140 | 140 | if tag_name is not None: |
|
141 | 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 | 143 | else: |
|
144 |
threads = Thread.objects. |
|
|
144 | threads = Thread.objects.exclude(status=STATUS_ARCHIVE) | |
|
145 | 145 | |
|
146 | 146 | if PARAMETER_OFFSET in request.GET: |
|
147 | 147 | offset = request.GET[PARAMETER_OFFSET] |
@@ -158,8 +158,7 b' def api_get_threads(request, count):' | |||
|
158 | 158 | |
|
159 | 159 | # TODO Add tags, replies and images count |
|
160 | 160 | post_data = opening_post.get_post_data(include_last_update=True) |
|
161 |
post_data[' |
|
|
162 | post_data['archived'] = thread.archived | |
|
161 | post_data['status'] = thread.get_status() | |
|
163 | 162 | |
|
164 | 163 | opening_posts.append(post_data) |
|
165 | 164 | |
@@ -214,7 +213,7 b' def api_get_notifications(request, usern' | |||
|
214 | 213 | last_notification_id_str = request.GET.get('last', None) |
|
215 | 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 | 217 | last=last_id) |
|
219 | 218 | |
|
220 | 219 | json_post_list = [] |
@@ -1,13 +1,34 b'' | |||
|
1 | import os | |
|
2 | ||
|
1 | 3 | from django.shortcuts import render |
|
2 | 4 | |
|
5 | import neboard | |
|
3 | 6 | from boards.authors import authors |
|
7 | from boards.utils import cached_result | |
|
4 | 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 | 17 | class AuthorsView(BaseBoardView): |
|
8 | 18 | |
|
9 | 19 | def get(self, request): |
|
10 | 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 | 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 | 4 | PARAM_NEXT = 'next' |
|
2 | 5 | PARAMETER_METHOD = 'method' |
|
3 | 6 | |
@@ -24,3 +27,14 b' class DispatcherMixin:' | |||
|
24 | 27 | |
|
25 | 28 | if method_name: |
|
26 | 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 | 11 | TEMPLATE = 'boards/notifications.html' |
|
12 | 12 | PARAM_PAGE = 'page' |
|
13 | PARAM_USERNAME = 'notification_username' | |
|
13 | PARAM_USERNAMES = 'notification_usernames' | |
|
14 | 14 | REQUEST_PAGE = 'page' |
|
15 | 15 | RESULTS_PER_PAGE = 10 |
|
16 | 16 | |
|
17 | 17 | |
|
18 | 18 | class NotificationView(BaseBoardView): |
|
19 | 19 | |
|
20 | def get(self, request, username): | |
|
20 | def get(self, request, username=None): | |
|
21 | 21 | params = self.get_context_data() |
|
22 | 22 | |
|
23 | 23 | settings_manager = get_settings_manager(request) |
|
24 | 24 | |
|
25 | 25 | # If we open our notifications, reset the "new" count |
|
26 | my_username = settings_manager.get_setting(SETTING_USERNAME) | |
|
27 | ||
|
28 | notification_username = username.lower() | |
|
26 | if username is None: | |
|
27 | notification_usernames = settings_manager.get_notification_usernames() | |
|
28 | else: | |
|
29 | notification_usernames = [username] | |
|
29 | 30 | |
|
30 | 31 | posts = Notification.objects.get_notification_posts( |
|
31 | username=notification_username) | |
|
32 | if notification_username == my_username: | |
|
32 | usernames=notification_usernames) | |
|
33 | ||
|
34 | if username is None: | |
|
33 | 35 | last = posts.first() |
|
34 | 36 | if last is not None: |
|
35 | 37 | last_id = last.id |
|
36 | 38 | settings_manager.set_setting(SETTING_LAST_NOTIFICATION_ID, |
|
37 | 39 | last_id) |
|
38 | 40 | |
|
41 | ||
|
39 | 42 | paginator = get_paginator(posts, RESULTS_PER_PAGE) |
|
40 | 43 | |
|
41 | 44 | page = int(request.GET.get(REQUEST_PAGE, DEFAULT_PAGE)) |
|
42 | 45 | |
|
43 | 46 | params[PARAM_PAGE] = paginator.page(page) |
|
44 | params[PARAM_USERNAME] = notification_username | |
|
47 | params[PARAM_USERNAMES] = notification_usernames | |
|
45 | 48 | |
|
46 | 49 | return render(request, TEMPLATE, params) |
@@ -1,13 +1,15 b'' | |||
|
1 | 1 | from boards.views.thread import ThreadView |
|
2 | from boards.views.mixins import FileUploadMixin | |
|
2 | 3 | |
|
3 | 4 | TEMPLATE_NORMAL = 'boards/thread_normal.html' |
|
4 | 5 | |
|
5 | 6 | CONTEXT_BUMPLIMIT_PRG = 'bumplimit_progress' |
|
6 | 7 | CONTEXT_POSTS_LEFT = 'posts_left' |
|
7 | 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 | 14 | def get_template(self): |
|
13 | 15 | return TEMPLATE_NORMAL |
@@ -26,5 +28,6 b' class NormalThreadView(ThreadView):' | |||
|
26 | 28 | params[CONTEXT_POSTS_LEFT] = left_posts |
|
27 | 29 | params[CONTEXT_BUMPLIMIT_PRG] = str( |
|
28 | 30 | float(left_posts) / max_posts * 100) |
|
31 | params[PARAM_MAX_FILE_SIZE] = self.get_max_upload_size() | |
|
29 | 32 | |
|
30 | 33 | return params |
@@ -1,8 +1,12 b'' | |||
|
1 | 1 | from django.contrib.auth.decorators import permission_required |
|
2 | 2 | |
|
3 | 3 | from django.core.exceptions import ObjectDoesNotExist |
|
4 | from django.core.urlresolvers import reverse | |
|
4 | 5 | from django.http import Http404 |
|
5 | 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 | 10 | from django.views.generic.edit import FormMixin |
|
7 | 11 | from django.utils import timezone |
|
8 | 12 | from django.utils.dateformat import format |
@@ -28,6 +32,7 b" CONTEXT_WS_TIME = 'ws_token_time'" | |||
|
28 | 32 | CONTEXT_MODE = 'mode' |
|
29 | 33 | CONTEXT_OP = 'opening_post' |
|
30 | 34 | CONTEXT_FAVORITE = 'is_favorite' |
|
35 | CONTEXT_RSS_URL = 'rss_url' | |
|
31 | 36 | |
|
32 | 37 | FORM_TITLE = 'title' |
|
33 | 38 | FORM_TEXT = 'text' |
@@ -37,6 +42,7 b" FORM_THREADS = 'threads'" | |||
|
37 | 42 | |
|
38 | 43 | class ThreadView(BaseBoardView, PostMixin, FormMixin, DispatcherMixin): |
|
39 | 44 | |
|
45 | @method_decorator(csrf_protect) | |
|
40 | 46 | def get(self, request, post_id, form: PostForm=None): |
|
41 | 47 | try: |
|
42 | 48 | opening_post = Post.objects.get(id=post_id) |
@@ -67,6 +73,7 b' class ThreadView(BaseBoardView, PostMixi' | |||
|
67 | 73 | params[CONTEXT_MODE] = self.get_mode() |
|
68 | 74 | params[CONTEXT_OP] = opening_post |
|
69 | 75 | params[CONTEXT_FAVORITE] = favorite |
|
76 | params[CONTEXT_RSS_URL] = self.get_rss_url(post_id) | |
|
70 | 77 | |
|
71 | 78 | if settings.get_bool('External', 'WebsocketsEnabled'): |
|
72 | 79 | token_time = format(timezone.now(), u'U') |
@@ -82,6 +89,7 b' class ThreadView(BaseBoardView, PostMixi' | |||
|
82 | 89 | |
|
83 | 90 | return render(request, self.get_template(), params) |
|
84 | 91 | |
|
92 | @method_decorator(csrf_protect) | |
|
85 | 93 | def post(self, request, post_id): |
|
86 | 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 | 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 | 106 | form = PostForm(request.POST, request.FILES, |
|
99 | 107 | error_class=PlainErrorList) |
|
100 | 108 | form.session = request.session |
@@ -163,11 +171,5 b' class ThreadView(BaseBoardView, PostMixi' | |||
|
163 | 171 | settings_manager = get_settings_manager(request) |
|
164 | 172 | settings_manager.del_fav_thread(opening_post) |
|
165 | 173 | |
|
166 | @permission_required('boards.post_hide_unhide') | |
|
167 | def toggle_hide_post(self, request, opening_post): | |
|
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']) | |
|
174 | def get_rss_url(self, opening_id): | |
|
175 | return reverse('thread', kwargs={'post_id': opening_id}) + 'rss/' |
@@ -95,19 +95,27 b' else:' | |||
|
95 | 95 | # Make this unique, and don't share it with anybody. |
|
96 | 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. | |
|
99 | TEMPLATE_LOADERS = ( | |
|
100 | 'django.template.loaders.filesystem.Loader', | |
|
101 | 'django.template.loaders.app_directories.Loader', | |
|
102 | ) | |
|
98 | TEMPLATES = [{ | |
|
99 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', | |
|
100 | 'DIRS': ['templates'], | |
|
101 | 'OPTIONS': { | |
|
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 | 120 | MIDDLEWARE_CLASSES = [ |
|
113 | 121 | 'django.middleware.http.ConditionalGetMiddleware', |
@@ -125,13 +133,6 b" ROOT_URLCONF = 'neboard.urls'" | |||
|
125 | 133 | # Python dotted path to the WSGI application used by Django's runserver. |
|
126 | 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 | 136 | INSTALLED_APPS = ( |
|
136 | 137 | 'django.contrib.auth', |
|
137 | 138 | 'django.contrib.contenttypes', |
General Comments 0
You need to be logged in to leave comments.
Login now