##// END OF EJS Templates
Merged with default branch
neko259 -
r1441:f2404e3c merge decentral
parent child Browse files
Show More
@@ -0,0 +1,32 b''
1 # -*- coding: utf-8 -*-
2 from __future__ import unicode_literals
3
4 from django.db import migrations, models
5
6
7 class Migration(migrations.Migration):
8
9 def bumpable_and_opening_to_status(apps, schema_editor):
10 Thread = apps.get_model('boards', 'Thread')
11 for thread in Thread.objects.all():
12 if thread.archived:
13 thread.status = 'archived'
14 elif not thread.bumpable:
15 thread.status = 'bumplimit'
16 else:
17 thread.status = 'active'
18 thread.save(update_fields=['status'])
19
20
21 dependencies = [
22 ('boards', '0035_auto_20151021_1346'),
23 ]
24
25 operations = [
26 migrations.AddField(
27 model_name='thread',
28 name='status',
29 field=models.CharField(default='active', max_length=50),
30 ),
31 migrations.RunPython(bumpable_and_opening_to_status),
32 ]
@@ -0,0 +1,22 b''
1 # -*- coding: utf-8 -*-
2 from __future__ import unicode_literals
3
4 from django.db import migrations, models
5
6
7 class Migration(migrations.Migration):
8
9 dependencies = [
10 ('boards', '0036_thread_status'),
11 ]
12
13 operations = [
14 migrations.RemoveField(
15 model_name='thread',
16 name='archived',
17 ),
18 migrations.RemoveField(
19 model_name='thread',
20 name='bumpable',
21 ),
22 ]
@@ -0,0 +1,19 b''
1 # -*- coding: utf-8 -*-
2 from __future__ import unicode_literals
3
4 from django.db import migrations, models
5
6
7 class Migration(migrations.Migration):
8
9 dependencies = [
10 ('boards', '0037_auto_20151122_2155'),
11 ]
12
13 operations = [
14 migrations.AlterField(
15 model_name='banner',
16 name='text',
17 field=models.TextField(null=True, blank=True),
18 ),
19 ]
@@ -0,0 +1,19 b''
1 # -*- coding: utf-8 -*-
2 from __future__ import unicode_literals
3
4 from django.db import migrations, models
5
6
7 class Migration(migrations.Migration):
8
9 dependencies = [
10 ('boards', '0038_auto_20151123_1203'),
11 ]
12
13 operations = [
14 migrations.AlterField(
15 model_name='thread',
16 name='status',
17 field=models.CharField(max_length=50, choices=[('active', 'active'), ('bumplimit', 'bumplimit'), ('archived', 'archived')], default='active'),
18 ),
19 ]
@@ -0,0 +1,19 b''
1 # -*- coding: utf-8 -*-
2 from __future__ import unicode_literals
3
4 from django.db import migrations, models
5
6
7 class Migration(migrations.Migration):
8
9 dependencies = [
10 ('boards', '0039_auto_20151203_1841'),
11 ]
12
13 operations = [
14 migrations.AddField(
15 model_name='thread',
16 name='monochrome',
17 field=models.BooleanField(default=False),
18 ),
19 ]
@@ -0,0 +1,16 b''
1 /*
2 CryptoJS v3.1.2
3 code.google.com/p/crypto-js
4 (c) 2009-2013 by Jeff Mott. All rights reserved.
5 code.google.com/p/crypto-js/wiki/License
6 */
7 var CryptoJS=CryptoJS||function(h,s){var f={},t=f.lib={},g=function(){},j=t.Base={extend:function(a){g.prototype=this;var c=new g;a&&c.mixIn(a);c.hasOwnProperty("init")||(c.init=function(){c.$super.init.apply(this,arguments)});c.init.prototype=c;c.$super=this;return c},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var c in a)a.hasOwnProperty(c)&&(this[c]=a[c]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}},
8 q=t.WordArray=j.extend({init:function(a,c){a=this.words=a||[];this.sigBytes=c!=s?c:4*a.length},toString:function(a){return(a||u).stringify(this)},concat:function(a){var c=this.words,d=a.words,b=this.sigBytes;a=a.sigBytes;this.clamp();if(b%4)for(var e=0;e<a;e++)c[b+e>>>2]|=(d[e>>>2]>>>24-8*(e%4)&255)<<24-8*((b+e)%4);else if(65535<d.length)for(e=0;e<a;e+=4)c[b+e>>>2]=d[e>>>2];else c.push.apply(c,d);this.sigBytes+=a;return this},clamp:function(){var a=this.words,c=this.sigBytes;a[c>>>2]&=4294967295<<
9 32-8*(c%4);a.length=h.ceil(c/4)},clone:function(){var a=j.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var c=[],d=0;d<a;d+=4)c.push(4294967296*h.random()|0);return new q.init(c,a)}}),v=f.enc={},u=v.Hex={stringify:function(a){var c=a.words;a=a.sigBytes;for(var d=[],b=0;b<a;b++){var e=c[b>>>2]>>>24-8*(b%4)&255;d.push((e>>>4).toString(16));d.push((e&15).toString(16))}return d.join("")},parse:function(a){for(var c=a.length,d=[],b=0;b<c;b+=2)d[b>>>3]|=parseInt(a.substr(b,
10 2),16)<<24-4*(b%8);return new q.init(d,c/2)}},k=v.Latin1={stringify:function(a){var c=a.words;a=a.sigBytes;for(var d=[],b=0;b<a;b++)d.push(String.fromCharCode(c[b>>>2]>>>24-8*(b%4)&255));return d.join("")},parse:function(a){for(var c=a.length,d=[],b=0;b<c;b++)d[b>>>2]|=(a.charCodeAt(b)&255)<<24-8*(b%4);return new q.init(d,c)}},l=v.Utf8={stringify:function(a){try{return decodeURIComponent(escape(k.stringify(a)))}catch(c){throw Error("Malformed UTF-8 data");}},parse:function(a){return k.parse(unescape(encodeURIComponent(a)))}},
11 x=t.BufferedBlockAlgorithm=j.extend({reset:function(){this._data=new q.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=l.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var c=this._data,d=c.words,b=c.sigBytes,e=this.blockSize,f=b/(4*e),f=a?h.ceil(f):h.max((f|0)-this._minBufferSize,0);a=f*e;b=h.min(4*a,b);if(a){for(var m=0;m<a;m+=e)this._doProcessBlock(d,m);m=d.splice(0,a);c.sigBytes-=b}return new q.init(m,b)},clone:function(){var a=j.clone.call(this);
12 a._data=this._data.clone();return a},_minBufferSize:0});t.Hasher=x.extend({cfg:j.extend(),init:function(a){this.cfg=this.cfg.extend(a);this.reset()},reset:function(){x.reset.call(this);this._doReset()},update:function(a){this._append(a);this._process();return this},finalize:function(a){a&&this._append(a);return this._doFinalize()},blockSize:16,_createHelper:function(a){return function(c,d){return(new a.init(d)).finalize(c)}},_createHmacHelper:function(a){return function(c,d){return(new w.HMAC.init(a,
13 d)).finalize(c)}}});var w=f.algo={};return f}(Math);
14 (function(h){for(var s=CryptoJS,f=s.lib,t=f.WordArray,g=f.Hasher,f=s.algo,j=[],q=[],v=function(a){return 4294967296*(a-(a|0))|0},u=2,k=0;64>k;){var l;a:{l=u;for(var x=h.sqrt(l),w=2;w<=x;w++)if(!(l%w)){l=!1;break a}l=!0}l&&(8>k&&(j[k]=v(h.pow(u,0.5))),q[k]=v(h.pow(u,1/3)),k++);u++}var a=[],f=f.SHA256=g.extend({_doReset:function(){this._hash=new t.init(j.slice(0))},_doProcessBlock:function(c,d){for(var b=this._hash.words,e=b[0],f=b[1],m=b[2],h=b[3],p=b[4],j=b[5],k=b[6],l=b[7],n=0;64>n;n++){if(16>n)a[n]=
15 c[d+n]|0;else{var r=a[n-15],g=a[n-2];a[n]=((r<<25|r>>>7)^(r<<14|r>>>18)^r>>>3)+a[n-7]+((g<<15|g>>>17)^(g<<13|g>>>19)^g>>>10)+a[n-16]}r=l+((p<<26|p>>>6)^(p<<21|p>>>11)^(p<<7|p>>>25))+(p&j^~p&k)+q[n]+a[n];g=((e<<30|e>>>2)^(e<<19|e>>>13)^(e<<10|e>>>22))+(e&f^e&m^f&m);l=k;k=j;j=p;p=h+r|0;h=m;m=f;f=e;e=r+g|0}b[0]=b[0]+e|0;b[1]=b[1]+f|0;b[2]=b[2]+m|0;b[3]=b[3]+h|0;b[4]=b[4]+p|0;b[5]=b[5]+j|0;b[6]=b[6]+k|0;b[7]=b[7]+l|0},_doFinalize:function(){var a=this._data,d=a.words,b=8*this._nDataBytes,e=8*a.sigBytes;
16 d[e>>>5]|=128<<24-e%32;d[(e+64>>>9<<4)+14]=h.floor(b/4294967296);d[(e+64>>>9<<4)+15]=b;a.sigBytes=4*d.length;this._process();return this._hash},clone:function(){var a=g.clone.call(this);a._hash=this._hash.clone();return a}});s.SHA256=g._createHelper(f);s.HmacSHA256=g._createHmacHelper(f)})(Math);
@@ -0,0 +1,54 b''
1 var POW_COMPUTING_TIMEOUT = 2;
2 var POW_HASH_LENGTH = 16;
3
4
5 function computeHash(iteration, guess, target, payload, timestamp, hasher) {
6 iteration += 1;
7 var hash = hasher(payload + iteration).toString();
8 guess = hash.substring(0, POW_HASH_LENGTH);
9
10 if (guess <= target) {
11 //console.log("Iteration: ", iteration);
12 //console.log("Guess: ", guess);
13 //console.log("Target: ", target);
14
15 var data = {
16 iteration: iteration,
17 timestamp: timestamp,
18 guess: guess
19 };
20 self.postMessage(data);
21 } else {
22 //console.log("Iteration: ", iteration);
23 //console.log("Guess: ", guess);
24 //console.log("Target: ", target);
25
26 setTimeout(function() {
27 computeHash(iteration, guess, target, payload, timestamp, hasher);
28 }, POW_COMPUTING_TIMEOUT);
29 }
30 }
31
32 function doWork(message, hasher, difficulty) {
33 var timestamp = Date.now();
34 var iteration = 0;
35 var payload = timestamp + message;
36
37 var target = parseInt(Math.pow(2, POW_HASH_LENGTH * 3) / difficulty).toString();
38 while (target.length < POW_HASH_LENGTH) {
39 target = '0' + target;
40 }
41
42 var guess = target + '0';
43
44 setTimeout(function() {
45 computeHash(iteration, guess, target, payload, timestamp, hasher);
46 }, POW_COMPUTING_TIMEOUT);
47 }
48
49 self.onmessage = function(e) {
50 var difficulty = e.data.difficulty;
51 importScripts(e.data.hasher);
52 var hasher = CryptoJS.SHA256;
53 self.doWork(e.data.msg, hasher, difficulty);
54 };
@@ -0,0 +1,31 b''
1 from django.core.urlresolvers import reverse
2 from django.shortcuts import get_object_or_404, render
3
4 from boards import settings
5 from boards.abstracts.paginator import get_paginator
6 from boards.models import Tag
7 from boards.views.base import BaseBoardView
8 from boards.views.mixins import PaginatedMixin
9
10 IMAGES_PER_PAGE = settings.get_int('View', 'ImagesPerPageGallery')
11
12 TEMPLATE = 'boards/tag_gallery.html'
13
14
15 class TagGalleryView(BaseBoardView, PaginatedMixin):
16
17 def get(self, request, tag_name):
18 page = int(request.GET.get('page', 1))
19
20 params = dict()
21 tag = get_object_or_404(Tag, name=tag_name)
22 params['tag'] = tag
23 paginator = get_paginator(tag.get_images(), IMAGES_PER_PAGE,
24 current_page=page)
25 params['paginator'] = paginator
26 params['images'] = paginator.page(page).object_list
27 paginator.set_url(reverse('tag_gallery', kwargs={'tag_name': tag_name}),
28 request.GET.dict())
29 self.set_page_urls(paginator, params)
30
31 return render(request, TEMPLATE, params) No newline at end of file
@@ -0,0 +1,7 b''
1 from django.views.decorators.cache import cache_page
2 from django.views.i18n import javascript_catalog
3
4 @cache_page(600)
5 def cached_javascript_catalog(request, domain='djangojs', packages=None):
6 return javascript_catalog(request, domain, packages)
7
@@ -35,3 +35,5 b' 4a5bec08ccfb47a27f9e98698f12dd5b7246623b'
35 604935b98f5b5e4a5e903594f048046e1fbb3519 2.8.3
35 604935b98f5b5e4a5e903594f048046e1fbb3519 2.8.3
36 c48ffdc671566069ed0f33644da1229277f3cd18 2.9.0
36 c48ffdc671566069ed0f33644da1229277f3cd18 2.9.0
37 d66dc192d4e089ba85325afeef5229b73cb0fde4 2.10.0
37 d66dc192d4e089ba85325afeef5229b73cb0fde4 2.10.0
38 1c22a38cca9ae3bee13d6f263792c0629d0061f6 2.10.1
39 3076e0d03339f3b41dcc71fb6af2b4169920846c 2.11.0
@@ -12,7 +12,14 b' def get_paginator(*args, **kwargs):'
12 class DividedPaginator(Paginator):
12 class DividedPaginator(Paginator):
13
13
14 lookaround_size = PAGINATOR_LOOKAROUND_SIZE
14 lookaround_size = PAGINATOR_LOOKAROUND_SIZE
15 current_page = 0
15
16 def __init__(self, object_list, per_page, orphans=0,
17 allow_empty_first_page=True, current_page=1):
18 super().__init__(object_list, per_page, orphans, allow_empty_first_page)
19
20 self.link = None
21 self.params = None
22 self.current_page = current_page
16
23
17 def _left_range(self):
24 def _left_range(self):
18 return self.page_range[:self.lookaround_size]
25 return self.page_range[:self.lookaround_size]
@@ -67,8 +74,18 b' class DividedPaginator(Paginator):'
67 def get_page_url(self, page):
74 def get_page_url(self, page):
68 self.params['page'] = page
75 self.params['page'] = page
69 url_params = '?' + '&'.join(['{}={}'.format(key, self.params[key])
76 url_params = '?' + '&'.join(['{}={}'.format(key, self.params[key])
70 for key in self.params.keys()])
77 for key in self.params.keys()])
71 return self.link + url_params
78 return self.link + url_params
72
79
73 def supports_urls(self):
80 def supports_urls(self):
74 return self.link is not None and self.params is not None
81 return self.link is not None and self.params is not None
82
83 def get_next_page_url(self):
84 current = self.page(self.current_page)
85 if current.has_next():
86 return self.get_page_url(current.next_page_number())
87
88 def get_prev_page_url(self):
89 current = self.page(self.current_page)
90 if current.has_previous():
91 return self.get_page_url(current.previous_page_number()) No newline at end of file
@@ -143,6 +143,14 b' class SettingsManager:'
143 def thread_is_fav(self, opening_post):
143 def thread_is_fav(self, opening_post):
144 return str(opening_post.id) in self.get_fav_threads()
144 return str(opening_post.id) in self.get_fav_threads()
145
145
146 def get_notification_usernames(self):
147 name_list = self.get_setting(SETTING_USERNAME)
148 if name_list is not None and len(name_list) > 0:
149 return name_list.lower().split(',')
150 else:
151 return list()
152
153
146 class SessionSettingsManager(SettingsManager):
154 class SessionSettingsManager(SettingsManager):
147 """
155 """
148 Session-based settings manager. All settings are saved to the user's
156 Session-based settings manager. All settings are saved to the user's
@@ -10,7 +10,8 b' class PostAdmin(admin.ModelAdmin):'
10 list_filter = ('pub_time',)
10 list_filter = ('pub_time',)
11 search_fields = ('id', 'title', 'text', 'poster_ip')
11 search_fields = ('id', 'title', 'text', 'poster_ip')
12 exclude = ('referenced_posts', 'refmap')
12 exclude = ('referenced_posts', 'refmap')
13 readonly_fields = ('poster_ip', 'threads', 'thread', 'images', 'uid')
13 readonly_fields = ('poster_ip', 'threads', 'thread', 'images',
14 'attachments', 'uid', 'url', 'pub_time', 'opening')
14
15
15 def ban_poster(self, request, queryset):
16 def ban_poster(self, request, queryset):
16 bans = 0
17 bans = 0
@@ -55,9 +56,9 b' class ThreadAdmin(admin.ModelAdmin):'
55 def op(self, obj: Thread):
56 def op(self, obj: Thread):
56 return obj.get_opening_post_id()
57 return obj.get_opening_post_id()
57
58
58 list_display = ('id', 'op', 'title', 'reply_count', 'archived', 'ip',
59 list_display = ('id', 'op', 'title', 'reply_count', 'status', 'ip',
59 'display_tags')
60 'display_tags')
60 list_filter = ('bump_time', 'archived', 'bumpable')
61 list_filter = ('bump_time', 'status')
61 search_fields = ('id', 'title')
62 search_fields = ('id', 'title')
62 filter_horizontal = ('tags',)
63 filter_horizontal = ('tags',)
63
64
@@ -1,5 +1,5 b''
1 [Version]
1 [Version]
2 Version = 2.10.0 BT
2 Version = 2.11.0 Yuko
3 SiteName = Neboard DEV
3 SiteName = Neboard DEV
4
4
5 [Cache]
5 [Cache]
@@ -10,7 +10,8 b' CacheTimeout = 600'
10 # Max post length in characters
10 # Max post length in characters
11 MaxTextLength = 30000
11 MaxTextLength = 30000
12 MaxFileSize = 8000000
12 MaxFileSize = 8000000
13 LimitPostingSpeed = false
13 LimitPostingSpeed = true
14 PowDifficulty = 20
14
15
15 [Messages]
16 [Messages]
16 # Thread bumplimit
17 # Thread bumplimit
@@ -24,6 +25,7 b' DefaultTheme = md'
24 DefaultImageViewer = simple
25 DefaultImageViewer = simple
25 LastRepliesCount = 3
26 LastRepliesCount = 3
26 ThreadsPerPage = 3
27 ThreadsPerPage = 3
28 ImagesPerPageGallery = 20
27
29
28 [Storage]
30 [Storage]
29 # Enable archiving threads instead of deletion when the thread limit is reached
31 # Enable archiving threads instead of deletion when the thread limit is reached
@@ -32,3 +34,6 b' ArchiveThreads = true'
32 [External]
34 [External]
33 # Thread update
35 # Thread update
34 WebsocketsEnabled = false
36 WebsocketsEnabled = false
37
38 [RSS]
39 MaxItems = 20
@@ -1,39 +1,39 b''
1 from boards.abstracts.settingsmanager import get_settings_manager, \
1 from boards.abstracts.settingsmanager import get_settings_manager, \
2 SETTING_USERNAME, SETTING_LAST_NOTIFICATION_ID, SETTING_IMAGE_VIEWER
2 SETTING_LAST_NOTIFICATION_ID, SETTING_IMAGE_VIEWER
3 from boards.models.user import Notification
3 from boards.models.user import Notification
4
4
5 __author__ = 'neko259'
5 __author__ = 'neko259'
6
6
7 from boards import settings, utils
7 from boards import settings
8 from boards.models import Post, Tag
8 from boards.models import Post, Tag
9
9
10 CONTEXT_SITE_NAME = 'site_name'
10 CONTEXT_SITE_NAME = 'site_name'
11 CONTEXT_VERSION = 'version'
11 CONTEXT_VERSION = 'version'
12 CONTEXT_MODERATOR = 'moderator'
13 CONTEXT_THEME_CSS = 'theme_css'
12 CONTEXT_THEME_CSS = 'theme_css'
14 CONTEXT_THEME = 'theme'
13 CONTEXT_THEME = 'theme'
15 CONTEXT_PPD = 'posts_per_day'
14 CONTEXT_PPD = 'posts_per_day'
16 CONTEXT_TAGS = 'tags'
15 CONTEXT_TAGS = 'tags'
17 CONTEXT_USER = 'user'
16 CONTEXT_USER = 'user'
18 CONTEXT_NEW_NOTIFICATIONS_COUNT = 'new_notifications_count'
17 CONTEXT_NEW_NOTIFICATIONS_COUNT = 'new_notifications_count'
19 CONTEXT_USERNAME = 'username'
18 CONTEXT_USERNAMES = 'usernames'
20 CONTEXT_TAGS_STR = 'tags_str'
19 CONTEXT_TAGS_STR = 'tags_str'
21 CONTEXT_IMAGE_VIEWER = 'image_viewer'
20 CONTEXT_IMAGE_VIEWER = 'image_viewer'
22 CONTEXT_HAS_FAV_THREADS = 'has_fav_threads'
21 CONTEXT_HAS_FAV_THREADS = 'has_fav_threads'
22 CONTEXT_POW_DIFFICULTY = 'pow_difficulty'
23
23
24
24
25 def get_notifications(context, request):
25 def get_notifications(context, request):
26 settings_manager = get_settings_manager(request)
26 settings_manager = get_settings_manager(request)
27 username = settings_manager.get_setting(SETTING_USERNAME)
27 usernames = settings_manager.get_notification_usernames()
28 new_notifications_count = 0
28 new_notifications_count = 0
29 if username is not None and len(username) > 0:
29 if usernames is not None:
30 last_notification_id = settings_manager.get_setting(
30 last_notification_id = settings_manager.get_setting(
31 SETTING_LAST_NOTIFICATION_ID)
31 SETTING_LAST_NOTIFICATION_ID)
32
32
33 new_notifications_count = Notification.objects.get_notification_posts(
33 new_notifications_count = Notification.objects.get_notification_posts(
34 username=username, last=last_notification_id).count()
34 usernames=usernames, last=last_notification_id).count()
35 context[CONTEXT_NEW_NOTIFICATIONS_COUNT] = new_notifications_count
35 context[CONTEXT_NEW_NOTIFICATIONS_COUNT] = new_notifications_count
36 context[CONTEXT_USERNAME] = username
36 context[CONTEXT_USERNAMES] = usernames
37
37
38
38
39 def user_and_ui_processor(request):
39 def user_and_ui_processor(request):
@@ -50,12 +50,12 b' def user_and_ui_processor(request):'
50 context[CONTEXT_THEME] = theme
50 context[CONTEXT_THEME] = theme
51 context[CONTEXT_THEME_CSS] = 'css/' + theme + '/base_page.css'
51 context[CONTEXT_THEME_CSS] = 'css/' + theme + '/base_page.css'
52
52
53 # This shows the moderator panel
54 context[CONTEXT_MODERATOR] = utils.is_moderator(request)
55
56 context[CONTEXT_VERSION] = settings.get('Version', 'Version')
53 context[CONTEXT_VERSION] = settings.get('Version', 'Version')
57 context[CONTEXT_SITE_NAME] = settings.get('Version', 'SiteName')
54 context[CONTEXT_SITE_NAME] = settings.get('Version', 'SiteName')
58
55
56 if settings.get_bool('Forms', 'LimitPostingSpeed'):
57 context[CONTEXT_POW_DIFFICULTY] = settings.get_int('Forms', 'PowDifficulty')
58
59 context[CONTEXT_IMAGE_VIEWER] = settings_manager.get_setting(
59 context[CONTEXT_IMAGE_VIEWER] = settings_manager.get_setting(
60 SETTING_IMAGE_VIEWER,
60 SETTING_IMAGE_VIEWER,
61 default=settings.get('View', 'DefaultImageViewer'))
61 default=settings.get('View', 'DefaultImageViewer'))
@@ -2,6 +2,7 b' import hashlib'
2 import re
2 import re
3 import time
3 import time
4 import logging
4 import logging
5
5 import pytz
6 import pytz
6
7
7 from django import forms
8 from django import forms
@@ -9,6 +10,7 b' from django.core.files.uploadedfile impo'
9 from django.core.exceptions import ObjectDoesNotExist
10 from django.core.exceptions import ObjectDoesNotExist
10 from django.forms.util import ErrorList
11 from django.forms.util import ErrorList
11 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
12 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
13 from django.utils import timezone
12
14
13 from boards.mdx_neboard import formatters
15 from boards.mdx_neboard import formatters
14 from boards.models.attachment.downloaders import Downloader
16 from boards.models.attachment.downloaders import Downloader
@@ -20,7 +22,11 b' from neboard import settings'
20 import boards.settings as board_settings
22 import boards.settings as board_settings
21 import neboard
23 import neboard
22
24
25 POW_HASH_LENGTH = 16
26 POW_LIFE_MINUTES = 1
27
23 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
28 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
29 REGEX_USERNAMES = re.compile(r'^[\w\s\d,]+$', re.UNICODE)
24
30
25 VETERAN_POSTING_DELAY = 5
31 VETERAN_POSTING_DELAY = 5
26
32
@@ -82,7 +88,7 b' class FormatPanel(forms.Textarea):'
82 formatter.preview_right + '</span>'
88 formatter.preview_right + '</span>'
83
89
84 output += '</div>'
90 output += '</div>'
85 output += super(FormatPanel, self).render(name, value, attrs=None)
91 output += super(FormatPanel, self).render(name, value, attrs=attrs)
86
92
87 return output
93 return output
88
94
@@ -168,6 +174,10 b' class PostForm(NeboardForm):'
168 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
174 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
169 '123 456 789'}))
175 '123 456 789'}))
170
176
177 guess = forms.CharField(widget=forms.HiddenInput(), required=False)
178 timestamp = forms.CharField(widget=forms.HiddenInput(), required=False)
179 iteration = forms.CharField(widget=forms.HiddenInput(), required=False)
180
171 session = None
181 session = None
172 need_to_ban = False
182 need_to_ban = False
173
183
@@ -238,7 +248,7 b' class PostForm(NeboardForm):'
238 for thread_id in threads_id_list:
248 for thread_id in threads_id_list:
239 try:
249 try:
240 thread = Post.objects.get(id=int(thread_id))
250 thread = Post.objects.get(id=int(thread_id))
241 if not thread.is_opening() or thread.get_thread().archived:
251 if not thread.is_opening() or thread.get_thread().is_archived():
242 raise ObjectDoesNotExist()
252 raise ObjectDoesNotExist()
243 threads.append(thread)
253 threads.append(thread)
244 except (ObjectDoesNotExist, ValueError):
254 except (ObjectDoesNotExist, ValueError):
@@ -256,8 +266,13 b' class PostForm(NeboardForm):'
256 if not self.errors:
266 if not self.errors:
257 self._clean_text_file()
267 self._clean_text_file()
258
268
259 if not self.errors and self.session:
269 limit_speed = board_settings.get_bool('Forms', 'LimitPostingSpeed')
260 self._validate_posting_speed()
270 if not self.errors and limit_speed:
271 pow_difficulty = board_settings.get_int('Forms', 'PowDifficulty')
272 if pow_difficulty > 0 and cleaned_data['timestamp'] and cleaned_data['iteration'] and cleaned_data['guess']:
273 self._validate_hash(cleaned_data['timestamp'], cleaned_data['iteration'], cleaned_data['guess'], cleaned_data['text'])
274 else:
275 self._validate_posting_speed()
261
276
262 return cleaned_data
277 return cleaned_data
263
278
@@ -341,8 +356,26 b' class PostForm(NeboardForm):'
341 except forms.ValidationError as e:
356 except forms.ValidationError as e:
342 raise e
357 raise e
343 except Exception as e:
358 except Exception as e:
344 # Just return no file
359 raise forms.ValidationError(e)
345 pass
360
361 def _validate_hash(self, timestamp: str, iteration: str, guess: str, message: str):
362 post_time = timezone.datetime.fromtimestamp(
363 int(timestamp[:-3]), tz=timezone.get_current_timezone())
364 timedelta = (timezone.now() - post_time).seconds / 60
365 if timedelta > POW_LIFE_MINUTES:
366 self._errors['text'] = self.error_class([_('Stale PoW.')])
367
368 payload = timestamp + message.replace('\r\n', '\n')
369 difficulty = board_settings.get_int('Forms', 'PowDifficulty')
370 target = str(int(2 ** (POW_HASH_LENGTH * 3) / difficulty))
371 if len(target) < POW_HASH_LENGTH:
372 target = '0' * (POW_HASH_LENGTH - len(target)) + target
373
374 computed_guess = hashlib.sha256((payload + iteration).encode())\
375 .hexdigest()[0:POW_HASH_LENGTH]
376 if guess != computed_guess or guess > target:
377 self._errors['text'] = self.error_class(
378 [_('Invalid PoW.')])
346
379
347
380
348 class ThreadForm(PostForm):
381 class ThreadForm(PostForm):
@@ -350,6 +383,7 b' class ThreadForm(PostForm):'
350 tags = forms.CharField(
383 tags = forms.CharField(
351 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
384 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
352 max_length=100, label=_('Tags'), required=True)
385 max_length=100, label=_('Tags'), required=True)
386 monochrome = forms.BooleanField(label=_('Monochrome'), required=False)
353
387
354 def clean_tags(self):
388 def clean_tags(self):
355 tags = self.cleaned_data['tags'].strip()
389 tags = self.cleaned_data['tags'].strip()
@@ -385,6 +419,9 b' class ThreadForm(PostForm):'
385
419
386 return cleaned_data
420 return cleaned_data
387
421
422 def is_monochrome(self):
423 return self.cleaned_data['monochrome']
424
388
425
389 class SettingsForm(NeboardForm):
426 class SettingsForm(NeboardForm):
390
427
@@ -396,7 +433,7 b' class SettingsForm(NeboardForm):'
396 def clean_username(self):
433 def clean_username(self):
397 username = self.cleaned_data['username']
434 username = self.cleaned_data['username']
398
435
399 if username and not REGEX_TAGS.match(username):
436 if username and not REGEX_USERNAMES.match(username):
400 raise forms.ValidationError(_('Inappropriate characters.'))
437 raise forms.ValidationError(_('Inappropriate characters.'))
401
438
402 return username
439 return username
1 NO CONTENT: modified file, binary diff hidden
NO CONTENT: modified file, binary diff hidden
@@ -143,8 +143,8 b' msgid "This page does not exist"'
143 msgstr "Этой страницы не существует"
143 msgstr "Этой страницы не существует"
144
144
145 #: templates/boards/all_threads.html:35
145 #: templates/boards/all_threads.html:35
146 msgid "Related message"
146 msgid "Details"
147 msgstr "Связанное сообщение"
147 msgstr "Подробности"
148
148
149 #: templates/boards/all_threads.html:69
149 #: templates/boards/all_threads.html:69
150 msgid "Edit tag"
150 msgid "Edit tag"
@@ -488,8 +488,8 b' msgstr "\xd0\x9e\xd0\xba"'
488
488
489 #: utils.py:120
489 #: utils.py:120
490 #, python-format
490 #, python-format
491 msgid "File must be less than %s bytes"
491 msgid "File must be less than %s but is %s."
492 msgstr "Файл должен быть менее %s байт"
492 msgstr "Файл должен быть менее %s, но его размер %s."
493
493
494 msgid "Please wait %(delay)d second before sending message"
494 msgid "Please wait %(delay)d second before sending message"
495 msgid_plural "Please wait %(delay)d seconds before sending message"
495 msgid_plural "Please wait %(delay)d seconds before sending message"
@@ -499,3 +499,34 b' msgstr[2] "\xd0\x9f\xd0\xbe\xd0\xb6\xd0\xb0\xd0\xbb\xd1\x83\xd0\xb9\xd1\x81\xd1\x82\xd0\xb0 \xd0\xbf\xd0\xbe\xd0\xb4\xd0\xbe\xd0\xb6\xd0\xb4\xd0\xb8\xd1\x82\xd0\xb5 %(delay)d \xd1\x81\xd0\xb5\xd0\xba\xd1\x83\xd0\xbd\xd0\xb4 \xd0\xbf\xd0\xb5\xd1\x80\xd0\xb5\xd0\xb4 \xd0\xbe\xd1\x82\xd0\xbf\xd1\x80\xd0\xb0\xd0\xb2\xd0\xba\xd0\xbe\xd0\xb9 \xd1\x81\xd0\xbe\xd0\xbe\xd0\xb1\xd1\x89\xd0\xb5\xd0\xbd\xd0\xb8\xd1\x8f"'
499
499
500 msgid "New threads"
500 msgid "New threads"
501 msgstr "Новые темы"
501 msgstr "Новые темы"
502
503 #, python-format
504 msgid "Max file size is %(size)s."
505 msgstr "Максимальный размер файла %(size)s."
506
507 msgid "Size of media:"
508 msgstr "Размер медиа:"
509
510 msgid "Statistics"
511 msgstr "Статистика"
512
513 msgid "Invalid PoW."
514 msgstr "Неверный PoW."
515
516 msgid "Stale PoW."
517 msgstr "PoW устарел."
518
519 msgid "Show"
520 msgstr "Показывать"
521
522 msgid "Hide"
523 msgstr "Скрывать"
524
525 msgid "Add to favorites"
526 msgstr "Добавить в избранное"
527
528 msgid "Remove from favorites"
529 msgstr "Убрать из избранного"
530
531 msgid "Monochrome"
532 msgstr "Монохромный" No newline at end of file
1 NO CONTENT: modified file, binary diff hidden
NO CONTENT: modified file, binary diff hidden
@@ -53,3 +53,5 b' msgstr "\xd0\x9e\xd1\x82\xd0\xbf\xd1\x80\xd0\xb0\xd0\xb2\xd0\xba\xd0\xb0 \xd1\x81\xd0\xbe\xd0\xbe\xd0\xb1\xd1\x89\xd0\xb5\xd0\xbd\xd0\xb8\xd1\x8f..."'
53 msgid "Server error!"
53 msgid "Server error!"
54 msgstr "Ошибка сервера!"
54 msgstr "Ошибка сервера!"
55
55
56 msgid "Computing PoW..."
57 msgstr "Расчёт PoW..." No newline at end of file
@@ -13,7 +13,7 b' class Command(BaseCommand):'
13
13
14 @transaction.atomic
14 @transaction.atomic
15 def handle(self, *args, **options):
15 def handle(self, *args, **options):
16 empty = Tag.objects.annotate(num_threads=Count('thread'))\
16 empty = Tag.objects.annotate(num_threads=Count('thread_tags'))\
17 .filter(num_threads=0).order_by('-required', 'name')
17 .filter(num_threads=0).order_by('-required', 'name')
18 print('Removing {} empty tags'.format(empty.count()))
18 print('Removing {} empty tags'.format(empty.count()))
19 empty.delete()
19 empty.delete()
@@ -141,6 +141,8 b' def render_quote(tag_name, value, option'
141 source = ''
141 source = ''
142 if 'source' in options:
142 if 'source' in options:
143 source = options['source']
143 source = options['source']
144 elif 'quote' in options:
145 source = options['quote']
144
146
145 if source:
147 if source:
146 result = '<div class="multiquote"><div class="quote-header">%s</div><div class="quote-text">%s</div></div>' % (source, value)
148 result = '<div class="multiquote"><div class="quote-header">%s</div><div class="quote-text">%s</div></div>' % (source, value)
@@ -1,4 +1,7 b''
1 __author__ = 'neko259'
1 STATUS_ACTIVE = 'active'
2 STATUS_BUMPLIMIT = 'bumplimit'
3 STATUS_ARCHIVE = 'archived'
4
2
5
3 from boards.models.signature import GlobalId, Signature
6 from boards.models.signature import GlobalId, Signature
4 from boards.models.sync_key import KeyPair
7 from boards.models.sync_key import KeyPair
@@ -38,4 +38,5 b' class Attachment(models.Model):'
38
38
39 return file_viewer(self.file, self.mimetype).get_view()
39 return file_viewer(self.file, self.mimetype).get_view()
40
40
41
41 def __str__(self):
42 return self.file.url
@@ -1,7 +1,8 b''
1 import os
1 import os
2 import re
2 import re
3
3
4 from django.core.files.uploadedfile import SimpleUploadedFile
4 from django.core.files.uploadedfile import SimpleUploadedFile, \
5 TemporaryUploadedFile
5 from pytube import YouTube
6 from pytube import YouTube
6 import requests
7 import requests
7
8
@@ -14,9 +15,9 b' HTTP_RESULT_OK = 200'
14 HEADER_CONTENT_LENGTH = 'content-length'
15 HEADER_CONTENT_LENGTH = 'content-length'
15 HEADER_CONTENT_TYPE = 'content-type'
16 HEADER_CONTENT_TYPE = 'content-type'
16
17
17 FILE_DOWNLOAD_CHUNK_BYTES = 100000
18 FILE_DOWNLOAD_CHUNK_BYTES = 200000
18
19
19 YOUTUBE_URL = re.compile(r'https?://www\.youtube\.com/watch\?v=\w+')
20 YOUTUBE_URL = re.compile(r'https?://(www\.youtube\.com/watch\?v=|youtu.be/)\w+')
20
21
21
22
22 class Downloader:
23 class Downloader:
@@ -38,17 +39,19 b' class Downloader:'
38
39
39 # Download file, stop if the size exceeds limit
40 # Download file, stop if the size exceeds limit
40 size = 0
41 size = 0
41 content = b''
42
43 # Set a dummy file name that will be replaced
44 # anyway, just keep the valid extension
45 filename = 'file.' + content_type.split('/')[1]
46
47 file = TemporaryUploadedFile(filename, content_type, 0, None, None)
42 for chunk in response.iter_content(FILE_DOWNLOAD_CHUNK_BYTES):
48 for chunk in response.iter_content(FILE_DOWNLOAD_CHUNK_BYTES):
43 size += len(chunk)
49 size += len(chunk)
44 validate_file_size(size)
50 validate_file_size(size)
45 content += chunk
51 file.write(chunk)
46
52
47 if response.status_code == HTTP_RESULT_OK and content:
53 if response.status_code == HTTP_RESULT_OK:
48 # Set a dummy file name that will be replaced
54 return file
49 # anyway, just keep the valid extension
50 filename = 'file.' + content_type.split('/')[1]
51 return SimpleUploadedFile(filename, content, content_type)
52
55
53
56
54 class YouTubeDownloader(Downloader):
57 class YouTubeDownloader(Downloader):
@@ -3,8 +3,11 b' from django.db import models'
3
3
4 class Banner(models.Model):
4 class Banner(models.Model):
5 title = models.TextField()
5 title = models.TextField()
6 text = models.TextField()
6 text = models.TextField(blank=True, null=True)
7 post = models.ForeignKey('Post')
7 post = models.ForeignKey('Post')
8
8
9 def __str__(self):
9 def __str__(self):
10 return self.title
10 return self.title
11
12 def get_text(self) -> str:
13 return self.text or self.post.get_text()
@@ -4,8 +4,10 b' from django.template.defaultfilters impo'
4 from boards import thumbs, utils
4 from boards import thumbs, utils
5 import boards
5 import boards
6 from boards.models.base import Viewable
6 from boards.models.base import Viewable
7 from boards.models import STATUS_ARCHIVE
7 from boards.utils import get_upload_filename
8 from boards.utils import get_upload_filename
8
9
10
9 __author__ = 'neko259'
11 __author__ = 'neko259'
10
12
11
13
@@ -27,8 +29,8 b' class PostImageManager(models.Manager):'
27
29
28 return post_image
30 return post_image
29
31
30 def get_random_images(self, count, include_archived=False, tags=None):
32 def get_random_images(self, count, tags=None):
31 images = self.filter(post_images__thread__archived=include_archived)
33 images = self.exclude(post_images__thread__status=STATUS_ARCHIVE)
32 if tags is not None:
34 if tags is not None:
33 images = images.filter(post_images__threads__tags__in=tags)
35 images = images.filter(post_images__threads__tags__in=tags)
34 return images.order_by('?')[:count]
36 return images.order_by('?')[:count]
@@ -23,6 +23,7 b" CSS_CLS_HIDDEN_POST = 'hidden_post'"
23 CSS_CLS_DEAD_POST = 'dead_post'
23 CSS_CLS_DEAD_POST = 'dead_post'
24 CSS_CLS_ARCHIVE_POST = 'archive_post'
24 CSS_CLS_ARCHIVE_POST = 'archive_post'
25 CSS_CLS_POST = 'post'
25 CSS_CLS_POST = 'post'
26 CSS_CLS_MONOCHROME = 'monochrome'
26
27
27 TITLE_MAX_WORDS = 10
28 TITLE_MAX_WORDS = 10
28
29
@@ -46,7 +47,6 b" PARAMETER_DIFF_TYPE = 'type'"
46 PARAMETER_CSS_CLASS = 'css_class'
47 PARAMETER_CSS_CLASS = 'css_class'
47 PARAMETER_THREAD = 'thread'
48 PARAMETER_THREAD = 'thread'
48 PARAMETER_IS_OPENING = 'is_opening'
49 PARAMETER_IS_OPENING = 'is_opening'
49 PARAMETER_MODERATOR = 'moderator'
50 PARAMETER_POST = 'post'
50 PARAMETER_POST = 'post'
51 PARAMETER_OP_ID = 'opening_post_id'
51 PARAMETER_OP_ID = 'opening_post_id'
52 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
52 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
@@ -56,10 +56,10 b" PARAMETER_NEED_OP_DATA = 'need_op_data'"
56 POST_VIEW_PARAMS = (
56 POST_VIEW_PARAMS = (
57 'need_op_data',
57 'need_op_data',
58 'reply_link',
58 'reply_link',
59 'moderator',
60 'need_open_link',
59 'need_open_link',
61 'truncated',
60 'truncated',
62 'mode_tree',
61 'mode_tree',
62 'perms',
63 )
63 )
64
64
65
65
@@ -185,12 +185,14 b' class Post(models.Model, Viewable):'
185 thread = self.get_thread()
185 thread = self.get_thread()
186
186
187 css_classes = [CSS_CLS_POST]
187 css_classes = [CSS_CLS_POST]
188 if thread.archived:
188 if thread.is_archived():
189 css_classes.append(CSS_CLS_ARCHIVE_POST)
189 css_classes.append(CSS_CLS_ARCHIVE_POST)
190 elif not thread.can_bump():
190 elif not thread.can_bump():
191 css_classes.append(CSS_CLS_DEAD_POST)
191 css_classes.append(CSS_CLS_DEAD_POST)
192 if self.is_hidden():
192 if self.is_hidden():
193 css_classes.append(CSS_CLS_HIDDEN_POST)
193 css_classes.append(CSS_CLS_HIDDEN_POST)
194 if thread.is_monochrome():
195 css_classes.append(CSS_CLS_MONOCHROME)
194
196
195 params = dict()
197 params = dict()
196 for param in POST_VIEW_PARAMS:
198 for param in POST_VIEW_PARAMS:
@@ -332,20 +334,29 b' class Post(models.Model, Viewable):'
332
334
333 def save(self, force_insert=False, force_update=False, using=None,
335 def save(self, force_insert=False, force_update=False, using=None,
334 update_fields=None):
336 update_fields=None):
337 new_post = self.id is None
338
335 self._text_rendered = Parser().parse(self.get_raw_text())
339 self._text_rendered = Parser().parse(self.get_raw_text())
336
340
337 self.uid = str(uuid.uuid4())
341 self.uid = str(uuid.uuid4())
338 if update_fields is not None and 'uid' not in update_fields:
342 if update_fields is not None and 'uid' not in update_fields:
339 update_fields += ['uid']
343 update_fields += ['uid']
340
344
341 if self.id:
345 if not new_post:
342 for thread in self.get_threads().all():
346 for thread in self.get_threads().all():
343 thread.last_edit_time = self.last_edit_time
347 thread.last_edit_time = self.last_edit_time
344
348
345 thread.save(update_fields=['last_edit_time', 'bumpable'])
349 thread.save(update_fields=['last_edit_time', 'status'])
346
350
347 super().save(force_insert, force_update, using, update_fields)
351 super().save(force_insert, force_update, using, update_fields)
348
352
353 # Post save triggers
354 if new_post:
355 self.build_url()
356
357 self._connect_replies()
358 self._connect_notifications()
359
349 def get_text(self) -> str:
360 def get_text(self) -> str:
350 return self._text_rendered
361 return self._text_rendered
351
362
@@ -380,12 +391,12 b' class Post(models.Model, Viewable):'
380 else:
391 else:
381 return str(self.id)
392 return str(self.id)
382
393
383 def connect_notifications(self):
394 def _connect_notifications(self):
384 for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()):
395 for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()):
385 user_name = reply_number.group(1).lower()
396 user_name = reply_number.group(1).lower()
386 Notification.objects.get_or_create(name=user_name, post=self)
397 Notification.objects.get_or_create(name=user_name, post=self)
387
398
388 def connect_replies(self):
399 def _connect_replies(self):
389 """
400 """
390 Connects replies to a post to show them as a reflink map
401 Connects replies to a post to show them as a reflink map
391 """
402 """
@@ -411,7 +422,7 b' class Post(models.Model, Viewable):'
411 thread.update_bump_status()
422 thread.update_bump_status()
412
423
413 thread.last_edit_time = self.last_edit_time
424 thread.last_edit_time = self.last_edit_time
414 thread.save(update_fields=['last_edit_time', 'bumpable'])
425 thread.save(update_fields=['last_edit_time', 'status'])
415 self.threads.add(opening_post.get_thread())
426 self.threads.add(opening_post.get_thread())
416
427
417 def get_tripcode(self):
428 def get_tripcode(self):
@@ -1,3 +1,5 b''
1 from django.contrib.auth.context_processors import PermWrapper
2
1 from boards import utils
3 from boards import utils
2
4
3
5
@@ -24,7 +26,7 b' class HtmlExporter(Exporter):'
24 reply_link = True
26 reply_link = True
25
27
26 return post.get_view(truncated=truncated, reply_link=reply_link,
28 return post.get_view(truncated=truncated, reply_link=reply_link,
27 moderator=utils.is_moderator(request))
29 perms=PermWrapper(request.user))
28
30
29
31
30 class JsonExporter(Exporter):
32 class JsonExporter(Exporter):
@@ -31,13 +31,15 b' class PostManager(models.Manager):'
31 @transaction.atomic
31 @transaction.atomic
32 def create_post(self, title: str, text: str, file=None, thread=None,
32 def create_post(self, title: str, text: str, file=None, thread=None,
33 ip=NO_IP, tags: list=None, opening_posts: list=None,
33 ip=NO_IP, tags: list=None, opening_posts: list=None,
34 tripcode=''):
34 tripcode='', monochrome=False):
35 """
35 """
36 Creates new post
36 Creates new post
37 """
37 """
38
38
39 if not utils.is_anonymous_mode():
39 if not utils.is_anonymous_mode():
40 is_banned = Ban.objects.filter(ip=ip).exists()
40 is_banned = Ban.objects.filter(ip=ip).exists()
41 else:
42 is_banned = False
41
43
42 # TODO Raise specific exception and catch it in the views
44 # TODO Raise specific exception and catch it in the views
43 if is_banned:
45 if is_banned:
@@ -52,7 +54,8 b' class PostManager(models.Manager):'
52 new_thread = False
54 new_thread = False
53 if not thread:
55 if not thread:
54 thread = boards.models.thread.Thread.objects.create(
56 thread = boards.models.thread.Thread.objects.create(
55 bump_time=posting_time, last_edit_time=posting_time)
57 bump_time=posting_time, last_edit_time=posting_time,
58 monochrome=monochrome)
56 list(map(thread.tags.add, tags))
59 list(map(thread.tags.add, tags))
57 boards.models.thread.Thread.objects.process_oldest_threads()
60 boards.models.thread.Thread.objects.process_oldest_threads()
58 new_thread = True
61 new_thread = True
@@ -72,7 +75,7 b' class PostManager(models.Manager):'
72 logger = logging.getLogger('boards.post.create')
75 logger = logging.getLogger('boards.post.create')
73
76
74 logger.info('Created post [{}] with text [{}] by {}'.format(post,
77 logger.info('Created post [{}] with text [{}] by {}'.format(post,
75 post.get_text(),post.poster_ip))
78 post.get_text(),post.poster_ip))
76
79
77 # TODO Move this to other place
80 # TODO Move this to other place
78 if file:
81 if file:
@@ -82,10 +85,7 b' class PostManager(models.Manager):'
82 else:
85 else:
83 post.attachments.add(Attachment.objects.create_with_hash(file))
86 post.attachments.add(Attachment.objects.create_with_hash(file))
84
87
85 post.build_url()
86 post.connect_replies()
87 post.connect_threads(opening_posts)
88 post.connect_threads(opening_posts)
88 post.connect_notifications()
89 post.set_global_id()
89 post.set_global_id()
90
90
91 # Thread needs to be bumped only when the post is already created
91 # Thread needs to be bumped only when the post is already created
@@ -147,6 +147,3 b' class PostManager(models.Manager):'
147 thread=thread)
147 thread=thread)
148
148
149 post.threads.add(thread)
149 post.threads.add(thread)
150 post.build_url()
151 post.connect_replies()
152 post.connect_notifications()
@@ -4,7 +4,9 b' from django.db import models'
4 from django.db.models import Count
4 from django.db.models import Count
5 from django.core.urlresolvers import reverse
5 from django.core.urlresolvers import reverse
6
6
7 from boards.models import PostImage
7 from boards.models.base import Viewable
8 from boards.models.base import Viewable
9 from boards.models.thread import STATUS_ACTIVE, STATUS_BUMPLIMIT, STATUS_ARCHIVE
8 from boards.utils import cached_result
10 from boards.utils import cached_result
9 import boards
11 import boards
10
12
@@ -61,22 +63,20 b' class Tag(models.Model, Viewable):'
61
63
62 return self.get_thread_count() == 0
64 return self.get_thread_count() == 0
63
65
64 def get_thread_count(self, archived=None, bumpable=None) -> int:
66 def get_thread_count(self, status=None) -> int:
65 threads = self.get_threads()
67 threads = self.get_threads()
66 if archived is not None:
68 if status is not None:
67 threads = threads.filter(archived=archived)
69 threads = threads.filter(status=status)
68 if bumpable is not None:
69 threads = threads.filter(bumpable=bumpable)
70 return threads.count()
70 return threads.count()
71
71
72 def get_active_thread_count(self) -> int:
72 def get_active_thread_count(self) -> int:
73 return self.get_thread_count(archived=False, bumpable=True)
73 return self.get_thread_count(status=STATUS_ACTIVE)
74
74
75 def get_bumplimit_thread_count(self) -> int:
75 def get_bumplimit_thread_count(self) -> int:
76 return self.get_thread_count(archived=False, bumpable=False)
76 return self.get_thread_count(status=STATUS_BUMPLIMIT)
77
77
78 def get_archived_thread_count(self) -> int:
78 def get_archived_thread_count(self) -> int:
79 return self.get_thread_count(archived=True)
79 return self.get_thread_count(status=STATUS_ARCHIVE)
80
80
81 def get_absolute_url(self):
81 def get_absolute_url(self):
82 return reverse('tag', kwargs={'tag_name': self.name})
82 return reverse('tag', kwargs={'tag_name': self.name})
@@ -106,11 +106,11 b' class Tag(models.Model, Viewable):'
106 def get_description(self):
106 def get_description(self):
107 return self.description
107 return self.description
108
108
109 def get_random_image_post(self, archived=False):
109 def get_random_image_post(self, status=[STATUS_ACTIVE, STATUS_BUMPLIMIT]):
110 posts = boards.models.Post.objects.annotate(images_count=Count(
110 posts = boards.models.Post.objects.annotate(images_count=Count(
111 'images')).filter(images_count__gt=0, threads__tags__in=[self])
111 'images')).filter(images_count__gt=0, threads__tags__in=[self])
112 if archived is not None:
112 if status is not None:
113 posts = posts.filter(thread__archived=archived)
113 posts = posts.filter(thread__status__in=status)
114 return posts.order_by('?').first()
114 return posts.order_by('?').first()
115
115
116 def get_first_letter(self):
116 def get_first_letter(self):
@@ -141,3 +141,7 b' class Tag(models.Model, Viewable):'
141
141
142 def get_children(self):
142 def get_children(self):
143 return self.children
143 return self.children
144
145 def get_images(self):
146 return PostImage.objects.filter(post_images__thread__tags__in=[self])\
147 .order_by('-post_images__pub_time') No newline at end of file
@@ -5,6 +5,8 b' from django.db.models import Count, Sum,'
5 from django.utils import timezone
5 from django.utils import timezone
6 from django.db import models
6 from django.db import models
7
7
8 from boards.models import STATUS_BUMPLIMIT, STATUS_ACTIVE, STATUS_ARCHIVE
9
8 from boards import settings
10 from boards import settings
9 import boards
11 import boards
10 from boards.utils import cached_result, datetime_to_epoch
12 from boards.utils import cached_result, datetime_to_epoch
@@ -25,6 +27,12 b" WS_NOTIFICATION_TYPE = 'notification_typ"
25
27
26 WS_CHANNEL_THREAD = "thread:"
28 WS_CHANNEL_THREAD = "thread:"
27
29
30 STATUS_CHOICES = (
31 (STATUS_ACTIVE, STATUS_ACTIVE),
32 (STATUS_BUMPLIMIT, STATUS_BUMPLIMIT),
33 (STATUS_ARCHIVE, STATUS_ARCHIVE),
34 )
35
28
36
29 class ThreadManager(models.Manager):
37 class ThreadManager(models.Manager):
30 def process_oldest_threads(self):
38 def process_oldest_threads(self):
@@ -33,7 +41,7 b' class ThreadManager(models.Manager):'
33 archive or delete the old ones.
41 archive or delete the old ones.
34 """
42 """
35
43
36 threads = Thread.objects.filter(archived=False).order_by('-bump_time')
44 threads = Thread.objects.exclude(status=STATUS_ARCHIVE).order_by('-bump_time')
37 thread_count = threads.count()
45 thread_count = threads.count()
38
46
39 max_thread_count = settings.get_int('Messages', 'MaxThreadCount')
47 max_thread_count = settings.get_int('Messages', 'MaxThreadCount')
@@ -50,11 +58,10 b' class ThreadManager(models.Manager):'
50 logger.info('Processed %d old threads' % num_threads_to_delete)
58 logger.info('Processed %d old threads' % num_threads_to_delete)
51
59
52 def _archive_thread(self, thread):
60 def _archive_thread(self, thread):
53 thread.archived = True
61 thread.status = STATUS_ARCHIVE
54 thread.bumpable = False
55 thread.last_edit_time = timezone.now()
62 thread.last_edit_time = timezone.now()
56 thread.update_posts_time()
63 thread.update_posts_time()
57 thread.save(update_fields=['archived', 'last_edit_time', 'bumpable'])
64 thread.save(update_fields=['last_edit_time', 'status'])
58
65
59 def get_new_posts(self, datas):
66 def get_new_posts(self, datas):
60 query = None
67 query = None
@@ -90,9 +97,10 b' class Thread(models.Model):'
90 tags = models.ManyToManyField('Tag', related_name='thread_tags')
97 tags = models.ManyToManyField('Tag', related_name='thread_tags')
91 bump_time = models.DateTimeField(db_index=True)
98 bump_time = models.DateTimeField(db_index=True)
92 last_edit_time = models.DateTimeField()
99 last_edit_time = models.DateTimeField()
93 archived = models.BooleanField(default=False)
94 bumpable = models.BooleanField(default=True)
95 max_posts = models.IntegerField(default=get_thread_max_posts)
100 max_posts = models.IntegerField(default=get_thread_max_posts)
101 status = models.CharField(max_length=50, default=STATUS_ACTIVE,
102 choices=STATUS_CHOICES)
103 monochrome = models.BooleanField(default=False)
96
104
97 def get_tags(self) -> QuerySet:
105 def get_tags(self) -> QuerySet:
98 """
106 """
@@ -118,7 +126,7 b' class Thread(models.Model):'
118
126
119 def update_bump_status(self, exclude_posts=None):
127 def update_bump_status(self, exclude_posts=None):
120 if self.has_post_limit() and self.get_reply_count() >= self.max_posts:
128 if self.has_post_limit() and self.get_reply_count() >= self.max_posts:
121 self.bumpable = False
129 self.status = STATUS_BUMPLIMIT
122 self.update_posts_time(exclude_posts=exclude_posts)
130 self.update_posts_time(exclude_posts=exclude_posts)
123
131
124 def _get_cache_key(self):
132 def _get_cache_key(self):
@@ -138,7 +146,7 b' class Thread(models.Model):'
138 Checks if the thread can be bumped by replying to it.
146 Checks if the thread can be bumped by replying to it.
139 """
147 """
140
148
141 return self.bumpable and not self.is_archived()
149 return self.get_status() == STATUS_ACTIVE
142
150
143 def get_last_replies(self) -> QuerySet:
151 def get_last_replies(self) -> QuerySet:
144 """
152 """
@@ -255,4 +263,10 b' class Thread(models.Model):'
255 return self.get_replies().filter(id__gt=post_id)
263 return self.get_replies().filter(id__gt=post_id)
256
264
257 def is_archived(self):
265 def is_archived(self):
258 return self.archived
266 return self.get_status() == STATUS_ARCHIVE
267
268 def get_status(self):
269 return self.status
270
271 def is_monochrome(self):
272 return self.monochrome
@@ -22,10 +22,10 b' class Ban(models.Model):'
22
22
23
23
24 class NotificationManager(models.Manager):
24 class NotificationManager(models.Manager):
25 def get_notification_posts(self, username: str, last: int = None):
25 def get_notification_posts(self, usernames: list, last: int = None):
26 i_username = username.lower()
26 lower_names = [username.lower() for username in usernames]
27
27 posts = boards.models.post.Post.objects.filter(
28 posts = boards.models.post.Post.objects.filter(notification__name=i_username)
28 notification__name__in=lower_names).distinct()
29 if last is not None:
29 if last is not None:
30 posts = posts.filter(id__gt=last)
30 posts = posts.filter(id__gt=last)
31 posts = posts.order_by('-id')
31 posts = posts.order_by('-id')
@@ -3,8 +3,12 b' from django.core.urlresolvers import rev'
3 from django.shortcuts import get_object_or_404
3 from django.shortcuts import get_object_or_404
4 from boards.models import Post, Tag, Thread
4 from boards.models import Post, Tag, Thread
5 from boards import settings
5 from boards import settings
6 from boards.models.thread import STATUS_ARCHIVE
6
7
7 __author__ = 'neko259'
8 __author__ = 'nekorin'
9
10
11 MAX_ITEMS = settings.get_int('RSS', 'MaxItems')
8
12
9
13
10 # TODO Make tests for all of these
14 # TODO Make tests for all of these
@@ -15,7 +19,7 b' class AllThreadsFeed(Feed):'
15 description_template = 'boards/rss/post.html'
19 description_template = 'boards/rss/post.html'
16
20
17 def items(self):
21 def items(self):
18 return Thread.objects.filter(archived=False).order_by('-id')
22 return Thread.objects.exclude(status=STATUS_ARCHIVE).order_by('-id')[:MAX_ITEMS]
19
23
20 def item_title(self, item):
24 def item_title(self, item):
21 return item.get_opening_post().title
25 return item.get_opening_post().title
@@ -33,7 +37,7 b' class TagThreadsFeed(Feed):'
33 description_template = 'boards/rss/post.html'
37 description_template = 'boards/rss/post.html'
34
38
35 def items(self, obj):
39 def items(self, obj):
36 return obj.threads.filter(archived=False).order_by('-id')
40 return obj.get_threads().exclude(status=STATUS_ARCHIVE).order_by('-id')[:MAX_ITEMS]
37
41
38 def get_object(self, request, tag_name):
42 def get_object(self, request, tag_name):
39 return get_object_or_404(Tag, name=tag_name)
43 return get_object_or_404(Tag, name=tag_name)
@@ -57,7 +61,7 b' class ThreadPostsFeed(Feed):'
57 description_template = 'boards/rss/post.html'
61 description_template = 'boards/rss/post.html'
58
62
59 def items(self, obj):
63 def items(self, obj):
60 return obj.get_thread().get_replies()
64 return obj.get_thread().get_replies().order_by('-pub_time')[:MAX_ITEMS]
61
65
62 def get_object(self, request, post_id):
66 def get_object(self, request, post_id):
63 return get_object_or_404(Post, id=post_id)
67 return get_object_or_404(Post, id=post_id)
@@ -90,6 +90,7 b' textarea, input {'
90 padding: inherit;
90 padding: inherit;
91 background: none;
91 background: none;
92 font-size: inherit;
92 font-size: inherit;
93 cursor: pointer;
93 }
94 }
94
95
95 #form-close-button {
96 #form-close-button {
@@ -151,3 +152,8 b' textarea, input {'
151 .hidden_post:hover {
152 .hidden_post:hover {
152 opacity: 1;
153 opacity: 1;
153 }
154 }
155
156 .monochrome > .image > .thumb > img {
157 filter: grayscale(100%);
158 -webkit-filter: grayscale(100%);
159 }
@@ -388,10 +388,6 b' li {'
388 color: #ccc;
388 color: #ccc;
389 }
389 }
390
390
391 .role {
392 text-decoration: underline;
393 }
394
395 .form-email {
391 .form-email {
396 display: none;
392 display: none;
397 }
393 }
@@ -566,7 +562,6 b' ul {'
566 }
562 }
567
563
568 .image-metadata {
564 .image-metadata {
569 font-style: italic;
570 font-size: 0.9em;
565 font-size: 0.9em;
571 }
566 }
572
567
@@ -577,3 +572,7 b' ul {'
577 #fav-panel {
572 #fav-panel {
578 border: 1px solid white;
573 border: 1px solid white;
579 }
574 }
575
576 .post-blink {
577 background-color: #000;
578 }
@@ -302,10 +302,6 b' input[type="submit"]:hover {'
302 color: #555;
302 color: #555;
303 }
303 }
304
304
305 .role {
306 text-decoration: underline;
307 }
308
309 .form-email {
305 .form-email {
310 display: none;
306 display: none;
311 }
307 }
@@ -380,4 +376,8 b' input[type="submit"]:hover {'
380 .image-metadata {
376 .image-metadata {
381 font-style: italic;
377 font-style: italic;
382 font-size: 0.9em;
378 font-size: 0.9em;
383 } No newline at end of file
379 }
380
381 .post-blink {
382 background-color: #333;
383 }
@@ -279,10 +279,6 b' li {'
279 color: #ccc;
279 color: #ccc;
280 }
280 }
281
281
282 .role {
283 text-decoration: underline;
284 }
285
286 .form-email {
282 .form-email {
287 display: none;
283 display: none;
288 }
284 }
@@ -416,3 +412,7 b' li {'
416 audio {
412 audio {
417 margin-top: 1em;
413 margin-top: 1em;
418 }
414 }
415
416 .post-blink {
417 background-color: #ccc;
418 }
@@ -22,7 +22,7 b''
22 var form = $('#form');
22 var form = $('#form');
23 $('textarea').keypress(function(event) {
23 $('textarea').keypress(function(event) {
24 if (event.which == 13 && event.ctrlKey) {
24 if (event.which == 13 && event.ctrlKey) {
25 form.submit();
25 form.find('input[type=submit]').click();
26 }
26 }
27 });
27 });
28
28
@@ -40,4 +40,56 b" var form = $('#form');"
40 previewTextBlock.html(data);
40 previewTextBlock.html(data);
41 previewTextBlock.show();
41 previewTextBlock.show();
42 })
42 })
43 })
43 });
44
45 /**
46 * Show text in the errors row of the form.
47 * @param form
48 * @param text
49 */
50 function showAsErrors(form, text) {
51 form.children('.form-errors').remove();
52
53 if (text.length > 0) {
54 var errorList = $('<div class="form-errors">' + text + '<div>');
55 errorList.appendTo(form);
56 }
57 }
58
59 function addHiddenInput(form, name, value) {
60 form.find('input[name=' + name + ']').val(value);
61 }
62
63 $(document).ready(function() {
64 var powDifficulty = parseInt($('body').attr('data-pow-difficulty'));
65 if (powDifficulty > 0) {
66 var worker = new Worker($('#powScript').attr('src'));
67 worker.onmessage = function(e) {
68 var form = $('#form');
69 addHiddenInput(form, 'timestamp', e.data.timestamp);
70 addHiddenInput(form, 'iteration', e.data.iteration);
71 addHiddenInput(form, 'guess', e.data.guess);
72
73 form.submit();
74 form.find('input[type=submit]').toggle();
75 };
76
77 var form = $('#form');
78 var submitButton = form.find('input[type=submit]');
79 submitButton.click(function() {
80 showAsErrors(form, gettext('Computing PoW...'));
81 submitButton.toggle();
82
83 var msg = $('textarea').val().trim();
84
85 var data = {
86 msg: msg,
87 difficulty: parseInt($('body').attr('data-pow-difficulty')),
88 hasher: $('#sha256Script').attr('src')
89 };
90 worker.postMessage(data);
91
92 return false;
93 });
94 }
95 });
@@ -36,6 +36,37 b" var FULL_IMG_CLASS = 'post-image-full';"
36 var ATTR_SCALE = 'scale';
36 var ATTR_SCALE = 'scale';
37
37
38
38
39 // Init image viewer
40 var viewerName = $('body').attr('data-image-viewer');
41 var viewer = ImageViewer();
42 for (var i = 0; i < IMAGE_VIEWERS.length; i++) {
43 var item = IMAGE_VIEWERS[i];
44 if (item[0] === viewerName) {
45 viewer = item[1];
46 break;
47 }
48 }
49
50
51 function getFullImageWidth(previewImage) {
52 var full_img_w = previewImage.attr('data-width');
53 if (full_img_w == null) {
54 full_img_w = previewImage[0].naturalWidth;
55 }
56
57 return full_img_w;
58 }
59
60 function getFullImageHeight(previewImage) {
61 var full_img_h = previewImage.attr('data-height');
62 if (full_img_h == null) {
63 full_img_h = previewImage[0].naturalHeight;
64 }
65
66 return full_img_h;
67 }
68
69
39 function ImageViewer() {}
70 function ImageViewer() {}
40 ImageViewer.prototype.view = function (post) {};
71 ImageViewer.prototype.view = function (post) {};
41
72
@@ -48,8 +79,8 b' SimpleImageViewer.prototype.view = funct'
48 if (images.length == 1) {
79 if (images.length == 1) {
49 var thumb = images.first();
80 var thumb = images.first();
50
81
51 var width = thumb.attr('data-width');
82 var width = getFullImageWidth(thumb);
52 var height = thumb.attr('data-height');
83 var height = getFullImageHeight(thumb);
53
84
54 if (width == null || height == null) {
85 if (width == null || height == null) {
55 width = '100%';
86 width = '100%';
@@ -76,10 +107,10 b' PopupImageViewer.prototype.view = functi'
76
107
77 var existingPopups = $('#' + thumb_id);
108 var existingPopups = $('#' + thumb_id);
78 if (!existingPopups.length) {
109 if (!existingPopups.length) {
79 var imgElement= el.find('img');
110 var imgElement = el.find('img');
80
111
81 var full_img_w = imgElement.attr('data-width');
112 var full_img_w = getFullImageWidth(imgElement);
82 var full_img_h = imgElement.attr('data-height');
113 var full_img_h = getFullImageHeight(imgElement);
83
114
84 var win = $(window);
115 var win = $(window);
85
116
@@ -156,16 +187,6 b' PopupImageViewer.prototype.view = functi'
156 };
187 };
157
188
158 function addImgPreview() {
189 function addImgPreview() {
159 var viewerName = $('body').attr('data-image-viewer');
160 var viewer = ImageViewer();
161 for (var i = 0; i < IMAGE_VIEWERS.length; i++) {
162 var item = IMAGE_VIEWERS[i];
163 if (item[0] === viewerName) {
164 viewer = item[1];
165 break;
166 }
167 }
168
169 //keybind
190 //keybind
170 $(document).on('keyup.removepic', function(e) {
191 $(document).on('keyup.removepic', function(e) {
171 if(e.which === 27) {
192 if(e.which === 27) {
@@ -24,6 +24,7 b''
24 */
24 */
25
25
26 var FAV_POST_UPDATE_PERIOD = 10000;
26 var FAV_POST_UPDATE_PERIOD = 10000;
27 var ITEM_VOLUME_LEVEL = 'volumeLevel';
27
28
28 /**
29 /**
29 * An email is a hidden file to prevent spam bots from posting. It has to be
30 * An email is a hidden file to prevent spam bots from posting. It has to be
@@ -108,6 +109,36 b' function initFavPanel() {'
108 }
109 }
109 }
110 }
110
111
112 function setVolumeLevel(level) {
113 localStorage.setItem(ITEM_VOLUME_LEVEL, level);
114 }
115
116 function getVolumeLevel() {
117 var level = localStorage.getItem(ITEM_VOLUME_LEVEL);
118 if (level == null) {
119 level = 1.0;
120 }
121 return level
122 }
123
124 function processVolumeUser(node) {
125 node.prop("volume", getVolumeLevel());
126 node.on('volumechange', function(event) {
127 setVolumeLevel(event.target.volume);
128 $("video,audio").prop("volume", getVolumeLevel());
129 });
130 }
131
132 /**
133 * Add all scripts than need to work on post, when the post is added to the
134 * document.
135 */
136 function addScriptsToPost(post) {
137 addRefLinkPreview(post[0]);
138 highlightCode(post);
139 processVolumeUser(post.find("video,audio"));
140 }
141
111 $( document ).ready(function() {
142 $( document ).ready(function() {
112 hideEmailFromForm();
143 hideEmailFromForm();
113
144
@@ -123,4 +154,7 b' function initFavPanel() {'
123 highlightCode($(document));
154 highlightCode($(document));
124
155
125 initFavPanel();
156 initFavPanel();
157
158 var volumeUsers = $("video,audio");
159 processVolumeUser(volumeUsers);
126 });
160 });
@@ -18,9 +18,8 b' function $each(list, fn) {'
18 function mkPreview(cln, html) {
18 function mkPreview(cln, html) {
19 cln.innerHTML = html;
19 cln.innerHTML = html;
20
20
21 highlightCode($(cln));
21 addScriptsToPost($(cln));
22 addRefLinkPreview(cln);
22 }
23 };
24
23
25 function isElementInViewport (el) {
24 function isElementInViewport (el) {
26 //special bonus for those using jQuery
25 //special bonus for those using jQuery
@@ -28,7 +28,11 b" var CLASS_POST = '.post'"
28 var POST_ADDED = 0;
28 var POST_ADDED = 0;
29 var POST_UPDATED = 1;
29 var POST_UPDATED = 1;
30
30
31 // TODO These need to be syncronized with board settings.
31 var JS_AUTOUPDATE_PERIOD = 20000;
32 var JS_AUTOUPDATE_PERIOD = 20000;
33 // TODO This needs to be the same for attachment download time limit.
34 var POST_AJAX_TIMEOUT = 30000;
35 var BLINK_SPEED = 500;
32
36
33 var ALLOWED_FOR_PARTIAL_UPDATE = [
37 var ALLOWED_FOR_PARTIAL_UPDATE = [
34 'refmap',
38 'refmap',
@@ -45,6 +49,7 b" var documentOriginalTitle = '';"
45
49
46 // Thread ID does not change, can be stored one time
50 // Thread ID does not change, can be stored one time
47 var threadId = $('div.thread').children(CLASS_POST).first().attr('id');
51 var threadId = $('div.thread').children(CLASS_POST).first().attr('id');
52 var blinkColor = $('<div class="post-blink"></div>').css('background-color');
48
53
49 /**
54 /**
50 * Connect to websocket server and subscribe to thread updates. On any update we
55 * Connect to websocket server and subscribe to thread updates. On any update we
@@ -195,12 +200,7 b' function updatePost(postHtml) {'
195 * Initiate a blinking animation on a node to show it was updated.
200 * Initiate a blinking animation on a node to show it was updated.
196 */
201 */
197 function blink(node) {
202 function blink(node) {
198 var blinkCount = 2;
203 node.effect('highlight', { color: blinkColor }, BLINK_SPEED);
199
200 var nodeToAnimate = node;
201 for (var i = 0; i < blinkCount; i++) {
202 nodeToAnimate = nodeToAnimate.fadeTo('fast', 0.5).fadeTo('fast', 1.0);
203 }
204 }
204 }
205
205
206 function isPageBottom() {
206 function isPageBottom() {
@@ -352,26 +352,12 b' function updateOnPost(response, statusTe'
352 }
352 }
353 }
353 }
354
354
355 /**
356 * Show text in the errors row of the form.
357 * @param form
358 * @param text
359 */
360 function showAsErrors(form, text) {
361 form.children('.form-errors').remove();
362
363 if (text.length > 0) {
364 var errorList = $('<div class="form-errors">' + text + '<div>');
365 errorList.appendTo(form);
366 }
367 }
368
355
369 /**
356 /**
370 * Run js methods that are usually run on the document, on the new post
357 * Run js methods that are usually run on the document, on the new post
371 */
358 */
372 function processNewPost(post) {
359 function processNewPost(post) {
373 addRefLinkPreview(post[0]);
360 addScriptsToPost(post);
374 highlightCode(post);
375 blink(post);
361 blink(post);
376 }
362 }
377
363
@@ -430,7 +416,7 b' function updateNodeAttr(oldNode, newNode'
430 }
416 }
431 }
417 }
432
418
433 $(document).ready(function(){
419 $(document).ready(function() {
434 if (initAutoupdate()) {
420 if (initAutoupdate()) {
435 // Post form data over AJAX
421 // Post form data over AJAX
436 var threadId = $('div.thread').children('.post').first().attr('id');
422 var threadId = $('div.thread').children('.post').first().attr('id');
@@ -439,14 +425,15 b' function updateNodeAttr(oldNode, newNode'
439
425
440 if (form.length > 0) {
426 if (form.length > 0) {
441 var options = {
427 var options = {
442 beforeSubmit: function(arr, $form, options) {
428 beforeSubmit: function(arr, form, options) {
443 showAsErrors($('#form'), gettext('Sending message...'));
429 showAsErrors(form, gettext('Sending message...'));
444 },
430 },
445 success: updateOnPost,
431 success: updateOnPost,
446 error: function() {
432 error: function() {
447 showAsErrors($('#form'), gettext('Server error!'));
433 showAsErrors(form, gettext('Server error!'));
448 },
434 },
449 url: '/api/add_post/' + threadId + '/'
435 url: '/api/add_post/' + threadId + '/',
436 timeout: POST_AJAX_TIMEOUT
450 };
437 };
451
438
452 form.ajaxForm(options);
439 form.ajaxForm(options);
@@ -31,8 +31,8 b''
31 {% for banner in banners %}
31 {% for banner in banners %}
32 <div class="post">
32 <div class="post">
33 <div class="title">{{ banner.title }}</div>
33 <div class="title">{{ banner.title }}</div>
34 <div>{{ banner.text }}</div>
34 <div>{{ banner.get_text|safe }}</div>
35 <div>{% trans 'Related message' %}: <a href="{{ banner.post.get_absolute_url }}">>>{{ banner.post.id }}</a></div>
35 <div>{% trans 'Details' %}: <a href="{{ banner.post.get_absolute_url }}">>>{{ banner.post.id }}</a></div>
36 </div>
36 </div>
37 {% endfor %}
37 {% endfor %}
38
38
@@ -44,38 +44,50 b''
44 <a href="{{ random_image_post.get_absolute_url }}"><img
44 <a href="{{ random_image_post.get_absolute_url }}"><img
45 src="{{ image.image.url_200x150 }}"
45 src="{{ image.image.url_200x150 }}"
46 width="{{ image.pre_width }}"
46 width="{{ image.pre_width }}"
47 height="{{ image.pre_height }}"/></a>
47 height="{{ image.pre_height }}"
48 alt="{{ random_image_post.id }}"/></a>
48 {% endwith %}
49 {% endwith %}
49 </div>
50 </div>
50 {% endif %}
51 {% endif %}
51 <div class="tag-text-data">
52 <div class="tag-text-data">
52 <h2>
53 <h2>
54 /{{ tag.get_view|safe }}/
55 {% if perms.change_tag %}
56 <span class="moderator_info">| <a href="{% url 'admin:boards_tag_change' tag.id %}">{% trans 'Edit tag' %}</a></span>
57 {% endif %}
58 </h2>
59 <p>
53 <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form">
60 <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form">
54 {% if is_favorite %}
61 {% if is_favorite %}
55 <button name="method" value="unsubscribe" class="fav"></button>
62 <button name="method" value="unsubscribe" class="fav"> {% trans "Remove from favorites" %}</button>
56 {% else %}
63 {% else %}
57 <button name="method" value="subscribe" class="not_fav"></button>
64 <button name="method" value="subscribe" class="not_fav"> {% trans "Add to favorites" %}</button>
58 {% endif %}
65 {% endif %}
59 </form>
66 </form>
60 <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form">
67 <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form">
61 {% if is_hidden %}
68 {% if is_hidden %}
62 <button name="method" value="unhide" class="fav">H</button>
69 <button name="method" value="unhide" class="fav">{% trans "Show" %}</button>
63 {% else %}
70 {% else %}
64 <button name="method" value="hide" class="not_fav">H</button>
71 <button name="method" value="hide" class="not_fav">{% trans "Hide" %}</button>
65 {% endif %}
72 {% endif %}
66 </form>
73 </form>
67 {{ tag.get_view|safe }}
74 <a href="{% url 'tag_gallery' tag.name %}">{% trans 'Gallery' %}</a>
68 {% if moderator %}
75 </p>
69 <span class="moderator_info">| <a href="{% url 'admin:boards_tag_change' tag.id %}">{% trans 'Edit tag' %}</a></span>
70 {% endif %}
71 </h2>
72 {% if tag.get_description %}
76 {% if tag.get_description %}
73 <p>{{ tag.get_description|safe }}</p>
77 <p>{{ tag.get_description|safe }}</p>
74 {% endif %}
78 {% endif %}
75 <p>
79 <p>
76 {% blocktrans count count=tag.get_active_thread_count %}{{ count }} active thread{% plural %}active threads{% endblocktrans %},
80 {% with active_count=tag.get_active_thread_count bumplimit_count=tag.get_bumplimit_thread_count archived_count=tag.get_archived_thread_count %}
77 {% blocktrans count count=tag.get_bumplimit_thread_count %}{{ count }} thread in bumplimit{% plural %} threads in bumplimit{% endblocktrans %},
81 {% if active_count %}
78 {% blocktrans count count=tag.get_archived_thread_count %}{{ count }} archived thread{% plural %}archived threads{% endblocktrans %},
82 {% blocktrans count count=active_count %}{{ count }} active thread{% plural %}active threads{% endblocktrans %},
83 {% endif %}
84 {% if bumplimit_count %}
85 {% blocktrans count count=bumplimit_count %}{{ count }} thread in bumplimit{% plural %} threads in bumplimit{% endblocktrans %},
86 {% endif %}
87 {% if archived_count %}
88 {% blocktrans count count=archived_count %}{{ count }} archived thread{% plural %}archived threads{% endblocktrans %},
89 {% endif %}
90 {% endwith %}
79 {% blocktrans count count=tag.get_post_count %}{{ count }} message{% plural %}messages{% endblocktrans %}.
91 {% blocktrans count count=tag.get_post_count %}{{ count }} message{% plural %}messages{% endblocktrans %}.
80 </p>
92 </p>
81 {% if tag.get_all_parents %}
93 {% if tag.get_all_parents %}
@@ -99,7 +111,7 b''
99
111
100 {% for thread in threads %}
112 {% for thread in threads %}
101 <div class="thread">
113 <div class="thread">
102 {% post_view thread.get_opening_post moderator=moderator thread=thread truncated=True need_open_link=True %}
114 {% post_view thread.get_opening_post thread=thread truncated=True need_open_link=True %}
103 {% if not thread.archived %}
115 {% if not thread.archived %}
104 {% with last_replies=thread.get_last_replies %}
116 {% with last_replies=thread.get_last_replies %}
105 {% if last_replies %}
117 {% if last_replies %}
@@ -114,7 +126,7 b''
114 {% endwith %}
126 {% endwith %}
115 <div class="last-replies">
127 <div class="last-replies">
116 {% for post in last_replies %}
128 {% for post in last_replies %}
117 {% post_view post moderator=moderator truncated=True %}
129 {% post_view post truncated=True %}
118 {% endfor %}
130 {% endfor %}
119 </div>
131 </div>
120 {% endif %}
132 {% endif %}
@@ -148,6 +160,9 b''
148 </div>
160 </div>
149 <div>
161 <div>
150 {% trans 'Tags must be delimited by spaces. Text or image is required.' %}
162 {% trans 'Tags must be delimited by spaces. Text or image is required.' %}
163 {% with size=max_file_size|filesizeformat %}
164 {% blocktrans %}Max file size is {{ size }}.{% endblocktrans %}
165 {% endwith %}
151 </div>
166 </div>
152 <div id="preview-text"></div>
167 <div id="preview-text"></div>
153 <div><a href="{% url "staticpage" name="help" %}">{% trans 'Text syntax' %}</a></div>
168 <div><a href="{% url "staticpage" name="help" %}">{% trans 'Text syntax' %}</a></div>
@@ -156,6 +171,8 b''
156 </div>
171 </div>
157
172
158 <script src="{% static 'js/form.js' %}"></script>
173 <script src="{% static 'js/form.js' %}"></script>
174 <script id="sha256Script" src="{% static 'js/3party/sha256.js' %}"></script>
175 <script id="powScript" src="{% static 'js/proof_of_work.js' %}"></script>
159 <script src="{% static 'js/thread_create.js' %}"></script>
176 <script src="{% static 'js/thread_create.js' %}"></script>
160
177
161 {% endblock %}
178 {% endblock %}
@@ -180,7 +197,6 b''
180 {% endfor %}
197 {% endfor %}
181 {% endwith %}
198 {% endwith %}
182 ]
199 ]
183 [<a href="rss/">RSS</a>]
184 </span>
200 </span>
185
201
186 {% endblock %}
202 {% endblock %}
@@ -9,6 +9,9 b''
9 {% block content %}
9 {% block content %}
10 <div class="post">
10 <div class="post">
11 <p><img src="{{ STATIC_URL }}favicon.png" width="200" /></p>
11 <p><img src="{{ STATIC_URL }}favicon.png" width="200" /></p>
12 <h2>{% trans 'Statistics' %}</h2>
13 <p>{% trans 'Size of media:' %} {{ media_size|filesizeformat }}.
14 <p>{% blocktrans count count=post_count %}{{ count }} message{% plural %}messages{% endblocktrans %}.</p>
12 <h2>{% trans 'Authors' %}</h2>
15 <h2>{% trans 'Authors' %}</h2>
13 {% for nick, values in authors.items %}
16 {% for nick, values in authors.items %}
14 <p>
17 <p>
@@ -16,10 +19,7 b''
16 {% for value in values.contacts %}
19 {% for value in values.contacts %}
17 <a href="mailto:{{ value }}">{{ value }}</a>
20 <a href="mailto:{{ value }}">{{ value }}</a>
18 {% endfor %} -
21 {% endfor %} -
19 {% for role in values.roles %}
22 {{ values.roles|join:', ' }}
20 <span class="role">{% trans role %}</span>
21 {% if not forloop.last %}, {% endif %}
22 {% endfor %}
23 </p>
23 </p>
24 {% endfor %}
24 {% endfor %}
25 <br />
25 <br />
@@ -11,7 +11,9 b''
11 <link rel="stylesheet" type="text/css" href="{% static 'css/3party/jquery-ui.min.css' %}" media="all"/>
11 <link rel="stylesheet" type="text/css" href="{% static 'css/3party/jquery-ui.min.css' %}" media="all"/>
12 <link rel="stylesheet" type="text/css" href="{% static theme_css %}" media="all"/>
12 <link rel="stylesheet" type="text/css" href="{% static theme_css %}" media="all"/>
13
13
14 <link rel="alternate" type="application/rss+xml" href="rss/" title="{% trans 'Feed' %}"/>
14 {% if rss_url %}
15 <link rel="alternate" type="application/rss+xml" href="{{ rss_url }}" title="{% trans 'Feed' %}"/>
16 {% endif %}
15
17
16 <link rel="icon" type="image/png"
18 <link rel="icon" type="image/png"
17 href="{% static 'favicon.png' %}">
19 href="{% static 'favicon.png' %}">
@@ -21,11 +23,8 b''
21
23
22 {% block head %}{% endblock %}
24 {% block head %}{% endblock %}
23 </head>
25 </head>
24 <body data-image-viewer="{{ image_viewer }}">
26 <body data-image-viewer="{{ image_viewer }}" data-pow-difficulty="{{ pow_difficulty }}">
25 <script src="{% static 'js/jquery-2.0.1.min.js' %}"></script>
27 <script src="{% static 'js/jquery-2.0.1.min.js' %}"></script>
26 <script src="{% static 'js/3party/jquery-ui.min.js' %}"></script>
27 <script src="{% static 'js/jquery.mousewheel.js' %}"></script>
28 <script src="{% url 'js_info_dict' %}"></script>
29
28
30 <div class="navigation_panel header">
29 <div class="navigation_panel header">
31 <a class="link" href="{% url 'index' %}">{% trans "All threads" %}</a>
30 <a class="link" href="{% url 'index' %}">{% trans "All threads" %}</a>
@@ -44,8 +43,8 b''
44 <a href="#" id="fav-panel-btn">{% trans 'favorites' %} <span id="new-fav-post-count"></span></a>
43 <a href="#" id="fav-panel-btn">{% trans 'favorites' %} <span id="new-fav-post-count"></span></a>
45 {% endif %}
44 {% endif %}
46
45
47 {% if username %}
46 {% if usernames %}
48 <a class="right-link link" href="{% url 'notifications' username %}" title="{% trans 'Notifications' %}">
47 <a class="right-link link" href="{% url 'notifications' %}" title="{% trans 'Notifications' %}">
49 {% trans 'Notifications' %}
48 {% trans 'Notifications' %}
50 {% ifnotequal new_notifications_count 0 %}
49 {% ifnotequal new_notifications_count 0 %}
51 (<b>{{ new_notifications_count }}</b>)
50 (<b>{{ new_notifications_count }}</b>)
@@ -60,7 +59,12 b''
60
59
61 {% block content %}{% endblock %}
60 {% block content %}{% endblock %}
62
61
62 <script src="{% static 'js/3party/jquery-ui.min.js' %}"></script>
63 <script src="{% static 'js/jquery.mousewheel.js' %}"></script>
63 <script src="{% static 'js/3party/highlight.min.js' %}"></script>
64 <script src="{% static 'js/3party/highlight.min.js' %}"></script>
65
66 <script src="{% url 'js_info_dict' %}"></script>
67
64 <script src="{% static 'js/popup.js' %}"></script>
68 <script src="{% static 'js/popup.js' %}"></script>
65 <script src="{% static 'js/image.js' %}"></script>
69 <script src="{% static 'js/image.js' %}"></script>
66 <script src="{% static 'js/refpopup.js' %}"></script>
70 <script src="{% static 'js/refpopup.js' %}"></script>
@@ -68,6 +72,9 b''
68
72
69 <div class="navigation_panel footer">
73 <div class="navigation_panel footer">
70 {% block metapanel %}{% endblock %}
74 {% block metapanel %}{% endblock %}
75 {% if rss_url %}
76 [<a href="{{ rss_url }}">RSS</a>]
77 {% endif %}
71 [<a href="{% url 'admin:index' %}">{% trans 'Admin' %}</a>]
78 [<a href="{% url 'admin:index' %}">{% trans 'Admin' %}</a>]
72 [<a href="{% url 'index' %}?order=pub">{% trans 'New threads' %}</a>]
79 [<a href="{% url 'index' %}?order=pub">{% trans 'New threads' %}</a>]
73 {% with ppd=posts_per_day|floatformat:2 %}
80 {% with ppd=posts_per_day|floatformat:2 %}
@@ -5,11 +5,15 b''
5
5
6 {% block head %}
6 {% block head %}
7 <meta name="robots" content="noindex">
7 <meta name="robots" content="noindex">
8 <title>{{ site_name }} - {% trans 'Notifications' %} - {{ notification_username }}</title>
8 <title>{{ site_name }} - {% trans 'Notifications' %} - {{ notification_usernames|join:', ' }}</title>
9 {% endblock %}
9 {% endblock %}
10
10
11 {% block content %}
11 {% block content %}
12 <div class="tag_info"><a href="{% url 'notifications' notification_username %}" class="user-cast">@{{ notification_username }}</a></div>
12 <div class="tag_info">
13 {% for username in notification_usernames %}
14 <a href="{% url 'notifications' username %}" class="user-cast">@{{ username }}</a>
15 {% endfor %}
16 </div>
13
17
14 {% if page %}
18 {% if page %}
15 {% if page.has_previous %}
19 {% if page.has_previous %}
@@ -21,14 +21,14 b''
21 and this is an opening post (thread death time) or a post for popup
21 and this is an opening post (thread death time) or a post for popup
22 (we don't see OP here so we show the death time in the post itself).
22 (we don't see OP here so we show the death time in the post itself).
23 {% endcomment %}
23 {% endcomment %}
24 {% if thread.archived %}
24 {% if thread.is_archived %}
25 {% if is_opening %}
25 {% if is_opening %}
26 <time datetime="{{ thread.bump_time|date:'c' }}">{{ thread.bump_time }}</time>
26 <time datetime="{{ thread.bump_time|date:'c' }}">{{ thread.bump_time }}</time>
27 {% endif %}
27 {% endif %}
28 {% endif %}
28 {% endif %}
29 {% if is_opening %}
29 {% if is_opening %}
30 {% if need_open_link %}
30 {% if need_open_link %}
31 {% if thread.archived %}
31 {% if thread.is_archived %}
32 <a class="link" href="{% url 'thread' post.id %}">{% trans "Open" %}</a>
32 <a class="link" href="{% url 'thread' post.id %}">{% trans "Open" %}</a>
33 {% else %}
33 {% else %}
34 <a class="link" href="{% url 'thread' post.id %}#form">{% trans "Reply" %}</a>
34 <a class="link" href="{% url 'thread' post.id %}#form">{% trans "Reply" %}</a>
@@ -41,7 +41,7 b''
41 {% endwith %}
41 {% endwith %}
42 {% endif %}
42 {% endif %}
43 {% endif %}
43 {% endif %}
44 {% if reply_link and not thread.archived %}
44 {% if reply_link and not thread.is_archived %}
45 <a href="#form" onclick="addQuickReply('{{ post.id }}'); return false;">{% trans 'Reply' %}</a>
45 <a href="#form" onclick="addQuickReply('{{ post.id }}'); return false;">{% trans 'Reply' %}</a>
46 {% endif %}
46 {% endif %}
47
47
@@ -49,15 +49,16 b''
49 <a class="global-id" href="{% url 'post_sync_data' post.id %}"> [RAW] </a>
49 <a class="global-id" href="{% url 'post_sync_data' post.id %}"> [RAW] </a>
50 {% endif %}
50 {% endif %}
51
51
52 {% if moderator %}
52 {% if perms.boards.change_post or perms.boards.delete_post or perms.boards.change_thread or perms_boards.delete_thread %}
53 <span class="moderator_info">
53 <span class="moderator_info">
54 | <a href="{% url 'admin:boards_post_change' post.id %}">{% trans 'Edit' %}</a>
54 {% if perms.boards.change_post or perms.boards.delete_post %}
55 {% if is_opening %}
55 | <a href="{% url 'admin:boards_post_change' post.id %}">{% trans 'Edit' %}</a>
56 | <a href="{% url 'admin:boards_thread_change' thread.id %}">{% trans 'Edit thread' %}</a>
57 {% endif %}
56 {% endif %}
58 <form action="{% url 'thread' thread.get_opening_post_id %}?post_id={{ post.id }}" method="post" class="post-button-form">
57 {% if perms.boards.change_thread or perms_boards.delete_thread %}
59 | <button name="method" value="toggle_hide_post">H</button>
58 {% if is_opening %}
60 </form>
59 | <a href="{% url 'admin:boards_thread_change' thread.id %}">{% trans 'Edit thread' %}</a>
60 {% endif %}
61 {% endif %}
61 </form>
62 </form>
62 </span>
63 </span>
63 {% endif %}
64 {% endif %}
@@ -15,7 +15,7 b''
15 <p>[s]<span class="strikethrough">{% trans 'Strikethrough text' %}</span>[/s]</p>
15 <p>[s]<span class="strikethrough">{% trans 'Strikethrough text' %}</span>[/s]</p>
16 <p>[comment]<span class="comment">{% trans 'Comment' %}</span>[/comment]</p>
16 <p>[comment]<span class="comment">{% trans 'Comment' %}</span>[/comment]</p>
17 <p>[quote]<span class="quote">&gt;{% trans 'Quote' %}</span>[/quote]</p>
17 <p>[quote]<span class="quote">&gt;{% trans 'Quote' %}</span>[/quote]</p>
18 <p>[quote source=src]<div class="multiquote"><div class="quote-header">src</div><div class="quote-text">{% trans 'Quote' %}</div></div><br />[/quote]</p>
18 <p>[quote=src]<div class="multiquote"><div class="quote-header">src</div><div class="quote-text">{% trans 'Quote' %}</div></div><br />[/quote]</p>
19 <p>[tag]<a class="tag">tag</a>[/tag]</p>
19 <p>[tag]<a class="tag">tag</a>[/tag]</p>
20 <br/>
20 <br/>
21 <p>{% trans 'You can try pasting the text and previewing the result here:' %} <a href="{% url 'preview' %}">{% trans 'Preview' %}</a></p>
21 <p>{% trans 'You can try pasting the text and previewing the result here:' %} <a href="{% url 'preview' %}">{% trans 'Preview' %}</a></p>
@@ -8,11 +8,7 b''
8 {% block head %}
8 {% block head %}
9 <meta name="robots" content="noindex">
9 <meta name="robots" content="noindex">
10
10
11 {% if tag %}
11 <title>{{ tag.name }} - {% trans 'Gallery' %} - {{ site_name }}</title>
12 <title>{{ tag.name }} - {{ site_name }}</title>
13 {% else %}
14 <title>{{ site_name }}</title>
15 {% endif %}
16
12
17 {% if prev_page_link %}
13 {% if prev_page_link %}
18 <link rel="prev" href="{{ prev_page_link }}" />
14 <link rel="prev" href="{{ prev_page_link }}" />
@@ -31,12 +27,11 b''
31 {% for banner in banners %}
27 {% for banner in banners %}
32 <div class="post">
28 <div class="post">
33 <div class="title">{{ banner.title }}</div>
29 <div class="title">{{ banner.title }}</div>
34 <div>{{ banner.text }}</div>
30 <div>{{ banner.get_text }}</div>
35 <div>{% trans 'Related message' %}: <a href="{{ banner.post.get_absolute_url }}">>>{{ banner.post.id }}</a></div>
31 <div>{% trans 'Details' %}: <a href="{{ banner.post.get_absolute_url }}">>>{{ banner.post.id }}</a></div>
36 </div>
32 </div>
37 {% endfor %}
33 {% endfor %}
38
34
39 {% if tag %}
40 <div class="tag_info" style="border-bottom: solid .5ex #{{ tag.get_color }}">
35 <div class="tag_info" style="border-bottom: solid .5ex #{{ tag.get_color }}">
41 {% if random_image_post %}
36 {% if random_image_post %}
42 <div class="tag-image">
37 <div class="tag-image">
@@ -44,28 +39,15 b''
44 <a href="{{ random_image_post.get_absolute_url }}"><img
39 <a href="{{ random_image_post.get_absolute_url }}"><img
45 src="{{ image.image.url_200x150 }}"
40 src="{{ image.image.url_200x150 }}"
46 width="{{ image.pre_width }}"
41 width="{{ image.pre_width }}"
47 height="{{ image.pre_height }}"/></a>
42 height="{{ image.pre_height }}"
43 alt="{{ random_image_post.id }}"/></a>
48 {% endwith %}
44 {% endwith %}
49 </div>
45 </div>
50 {% endif %}
46 {% endif %}
51 <div class="tag-text-data">
47 <div class="tag-text-data">
52 <h2>
48 <h2>
53 <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form">
49 /{{ tag.get_view|safe }}/
54 {% if is_favorite %}
50 {% if perms.change_tag %}
55 <button name="method" value="unsubscribe" class="fav"></button>
56 {% else %}
57 <button name="method" value="subscribe" class="not_fav"></button>
58 {% endif %}
59 </form>
60 <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form">
61 {% if is_hidden %}
62 <button name="method" value="unhide" class="fav">H</button>
63 {% else %}
64 <button name="method" value="hide" class="not_fav">H</button>
65 {% endif %}
66 </form>
67 {{ tag.get_view|safe }}
68 {% if moderator %}
69 <span class="moderator_info">| <a href="{% url 'admin:boards_tag_change' tag.id %}">{% trans 'Edit tag' %}</a></span>
51 <span class="moderator_info">| <a href="{% url 'admin:boards_tag_change' tag.id %}">{% trans 'Edit tag' %}</a></span>
70 {% endif %}
52 {% endif %}
71 </h2>
53 </h2>
@@ -73,9 +55,17 b''
73 <p>{{ tag.get_description|safe }}</p>
55 <p>{{ tag.get_description|safe }}</p>
74 {% endif %}
56 {% endif %}
75 <p>
57 <p>
76 {% blocktrans count count=tag.get_active_thread_count %}{{ count }} active thread{% plural %}active threads{% endblocktrans %},
58 {% with active_count=tag.get_active_thread_count bumplimit_count=tag.get_bumplimit_thread_count archived_count=tag.get_archived_thread_count %}
77 {% blocktrans count count=tag.get_bumplimit_thread_count %}{{ count }} thread in bumplimit{% plural %} threads in bumplimit{% endblocktrans %},
59 {% if active_count %}
78 {% blocktrans count count=tag.get_archived_thread_count %}{{ count }} archived thread{% plural %}archived threads{% endblocktrans %},
60 {% blocktrans count count=active_count %}{{ count }} active thread{% plural %}active threads{% endblocktrans %},
61 {% endif %}
62 {% if bumplimit_count %}
63 {% blocktrans count count=bumplimit_count %}{{ count }} thread in bumplimit{% plural %} threads in bumplimit{% endblocktrans %},
64 {% endif %}
65 {% if archived_count %}
66 {% blocktrans count count=archived_count %}{{ count }} archived thread{% plural %}archived threads{% endblocktrans %},
67 {% endif %}
68 {% endwith %}
79 {% blocktrans count count=tag.get_post_count %}{{ count }} message{% plural %}messages{% endblocktrans %}.
69 {% blocktrans count count=tag.get_post_count %}{{ count }} message{% plural %}messages{% endblocktrans %}.
80 </p>
70 </p>
81 {% if tag.get_all_parents %}
71 {% if tag.get_all_parents %}
@@ -88,76 +78,29 b''
88 {% endif %}
78 {% endif %}
89 </div>
79 </div>
90 </div>
80 </div>
91 {% endif %}
92
81
93 {% if threads %}
82 {% if prev_page_link %}
94 {% if prev_page_link %}
83 <div class="page_link">
95 <div class="page_link">
84 <a href="{{ prev_page_link }}">{% trans "Previous page" %}</a>
96 <a href="{{ prev_page_link }}">{% trans "Previous page" %}</a>
85 </div>
97 </div>
98 {% endif %}
99
100 {% for thread in threads %}
101 <div class="thread">
102 {% post_view thread.get_opening_post moderator=moderator thread=thread truncated=True need_open_link=True %}
103 {% if not thread.archived %}
104 {% with last_replies=thread.get_last_replies %}
105 {% if last_replies %}
106 {% with skipped_replies_count=thread.get_skipped_replies_count %}
107 {% if skipped_replies_count %}
108 <div class="skipped_replies">
109 <a href="{% url 'thread' thread.get_opening_post_id %}">
110 {% blocktrans count count=skipped_replies_count %}Skipped {{ count }} reply. Open thread to see all replies.{% plural %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %}
111 </a>
112 </div>
113 {% endif %}
114 {% endwith %}
115 <div class="last-replies">
116 {% for post in last_replies %}
117 {% post_view post moderator=moderator truncated=True %}
118 {% endfor %}
119 </div>
120 {% endif %}
121 {% endwith %}
122 {% endif %}
123 </div>
124 {% endfor %}
125
126 {% if next_page_link %}
127 <div class="page_link">
128 <a href="{{ next_page_link }}">{% trans "Next page" %}</a>
129 </div>
130 {% endif %}
131 {% else %}
132 <div class="post">
133 {% trans 'No threads exist. Create the first one!' %}</div>
134 {% endif %}
86 {% endif %}
135
87
136 <div class="post-form-w">
88 {% for image in images %}
137 <script src="{% static 'js/panel.js' %}"></script>
89 <div class="gallery_image">
138 <div class="post-form">
90 {% autoescape off %}
139 <div class="form-title">{% trans "Create new thread" %}</div>
91 {{ image.get_view }}
140 <div class="swappable-form-full">
92 {% endautoescape %}
141 <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %}
93 <div class="gallery_image_metadata">
142 {{ form.as_div }}
94 {{ image.width }}x{{ image.height }}
143 <div class="form-submit">
144 <input type="submit" value="{% trans "Post" %}"/>
145 <button id="preview-button" onclick="return false;">{% trans 'Preview' %}</button>
146 </div>
147 </form>
148 </div>
95 </div>
149 <div>
150 {% trans 'Tags must be delimited by spaces. Text or image is required.' %}
151 </div>
152 <div id="preview-text"></div>
153 <div><a href="{% url "staticpage" name="help" %}">{% trans 'Text syntax' %}</a></div>
154 <div><a href="{% url "tags" "required" %}">{% trans 'Tags' %}</a></div>
155 </div>
96 </div>
156 </div>
97 {% endfor %}
157
98
158 <script src="{% static 'js/form.js' %}"></script>
99 {% if next_page_link %}
159 <script src="{% static 'js/thread_create.js' %}"></script>
100 <div class="page_link">
160
101 <a href="{{ next_page_link }}">{% trans "Next page" %}</a>
102 </div>
103 {% endif %}
161 {% endblock %}
104 {% endblock %}
162
105
163 {% block metapanel %}
106 {% block metapanel %}
@@ -172,7 +115,7 b''
172 …,
115 …,
173 {% endif %}
116 {% endif %}
174 <a
117 <a
175 {% ifequal page current_page.number %}
118 {% ifequal page paginator.current_page %}
176 class="current_page"
119 class="current_page"
177 {% endifequal %}
120 {% endifequal %}
178 href="{% page_url paginator page %}">{{ page }}</a>
121 href="{% page_url paginator page %}">{{ page }}</a>
@@ -180,7 +123,6 b''
180 {% endfor %}
123 {% endfor %}
181 {% endwith %}
124 {% endwith %}
182 ]
125 ]
183 [<a href="rss/">RSS</a>]
184 </span>
126 </span>
185
127
186 {% endblock %}
128 {% endblock %}
@@ -37,8 +37,7 b''
37 {% with images_count=thread.get_images_count%}
37 {% with images_count=thread.get_images_count%}
38 <span id="image-count">{{ images_count }}</span> <span id="image-count-text">{% blocktrans count count=images_count %}image{% plural %}images{% endblocktrans %}</span>.
38 <span id="image-count">{{ images_count }}</span> <span id="image-count-text">{% blocktrans count count=images_count %}image{% plural %}images{% endblocktrans %}</span>.
39 {% endwith %}
39 {% endwith %}
40 {% trans 'Last update: ' %}<span id="last-update"><time datetime="{{ thread.last_edit_time|date:'c' }}">{{ thread.last_edit_time }}</time></span>
40 {% trans 'Last update: ' %}<span id="last-update"><time datetime="{{ thread.last_edit_time|date:'c' }}">{{ thread.last_edit_time }}</time></span>
41 [<a href="rss/">RSS</a>]
42 </span>
41 </span>
43
42
44 {% endblock %}
43 {% endblock %}
@@ -12,6 +12,7 b''
12 <div class="tag_info">
12 <div class="tag_info">
13 <h2>
13 <h2>
14 <form action="{% url 'thread' opening_post.id %}" method="post" class="post-button-form">
14 <form action="{% url 'thread' opening_post.id %}" method="post" class="post-button-form">
15 {% csrf_token %}
15 {% if is_favorite %}
16 {% if is_favorite %}
16 <button name="method" value="unsubscribe" class="fav"></button>
17 <button name="method" value="unsubscribe" class="fav"></button>
17 {% else %}
18 {% else %}
@@ -34,11 +35,11 b''
34
35
35 <div class="thread">
36 <div class="thread">
36 {% for post in thread.get_replies %}
37 {% for post in thread.get_replies %}
37 {% post_view post moderator=moderator reply_link=True %}
38 {% post_view post reply_link=True %}
38 {% endfor %}
39 {% endfor %}
39 </div>
40 </div>
40
41
41 {% if not thread.archived %}
42 {% if not thread.is_archived %}
42 <div class="post-form-w">
43 <div class="post-form-w">
43 <script src="{% static 'js/panel.js' %}"></script>
44 <script src="{% static 'js/panel.js' %}"></script>
44 <div class="form-title">{% trans "Reply to thread" %} #{{ opening_post.id }}<span class="reply-to-message"> {% trans "to message " %} #<span id="reply-to-message-id"></span></span></div>
45 <div class="form-title">{% trans "Reply to thread" %} #{{ opening_post.id }}<span class="reply-to-message"> {% trans "to message " %} #<span id="reply-to-message-id"></span></span></div>
@@ -54,17 +55,24 b''
54 </form>
55 </form>
55 </div>
56 </div>
56 <div id="preview-text"></div>
57 <div id="preview-text"></div>
58 <div>
59 {% with size=max_file_size|filesizeformat %}
60 {% blocktrans %}Max file size is {{ size }}.{% endblocktrans %}
61 {% endwith %}
62 </div>
57 <div><a href="{% url "staticpage" name="help" %}">
63 <div><a href="{% url "staticpage" name="help" %}">
58 {% trans 'Text syntax' %}</a></div>
64 {% trans 'Text syntax' %}</a></div>
59 <div><a id="form-close-button" href="#" onClick="resetFormPosition(); return false;">{% trans 'Close form' %}</a></div>
65 <div><a id="form-close-button" href="#" onClick="resetFormPosition(); return false;">{% trans 'Close form' %}</a></div>
60 </div>
66 </div>
61 </div>
67 </div>
62
68
69 <script src="{% static 'js/form.js' %}"></script>
63 <script src="{% static 'js/jquery.form.min.js' %}"></script>
70 <script src="{% static 'js/jquery.form.min.js' %}"></script>
71 <script id="sha256Script" src="{% static 'js/3party/sha256.js' %}"></script>
72 <script id="powScript" src="{% static 'js/proof_of_work.js' %}"></script>
73 <script src="{% static 'js/thread.js' %}"></script>
74 <script src="{% static 'js/thread_update.js' %}"></script>
64 {% endif %}
75 {% endif %}
65
76
66 <script src="{% static 'js/form.js' %}"></script>
67 <script src="{% static 'js/thread.js' %}"></script>
68 <script src="{% static 'js/thread_update.js' %}"></script>
69 <script src="{% static 'js/3party/centrifuge.js' %}"></script>
77 <script src="{% static 'js/3party/centrifuge.js' %}"></script>
70 {% endblock %}
78 {% endblock %}
@@ -11,7 +11,7 b''
11
11
12 <div class="thread">
12 <div class="thread">
13 {% for post in thread.get_top_level_replies %}
13 {% for post in thread.get_top_level_replies %}
14 {% post_view post moderator=moderator mode_tree=True %}
14 {% post_view post mode_tree=True %}
15 {% endfor %}
15 {% endfor %}
16 </div>
16 </div>
17
17
@@ -39,8 +39,9 b' def image_actions(*args, **kwargs):'
39 action['link'] % image_link, action['name']) for action in actions])
39 action['link'] % image_link, action['name']) for action in actions])
40
40
41
41
42 @register.simple_tag(name='post_view')
42 @register.simple_tag(name='post_view', takes_context=True)
43 def post_view(post, *args, **kwargs):
43 def post_view(context, post, *args, **kwargs):
44 kwargs['perms'] = context['perms']
44 return post.get_view(*args, **kwargs)
45 return post.get_view(*args, **kwargs)
45
46
46 @register.simple_tag(name='page_url')
47 @register.simple_tag(name='page_url')
@@ -31,6 +31,7 b' class ApiTest(TestCase):'
31 req = MockRequest()
31 req = MockRequest()
32 req.POST['thread'] = opening_post.id
32 req.POST['thread'] = opening_post.id
33 req.POST['uids'] = ' '.join(uids)
33 req.POST['uids'] = ' '.join(uids)
34 req.user = None
34 # Check the timestamp before post was added
35 # Check the timestamp before post was added
35 response = api.api_get_threaddiff(req)
36 response = api.api_get_threaddiff(req)
36 diff = simplejson.loads(response.content)
37 diff = simplejson.loads(response.content)
@@ -3,6 +3,7 b' from django.test import TestCase'
3
3
4 from boards import settings
4 from boards import settings
5 from boards.models import Tag, Post, Thread, KeyPair
5 from boards.models import Tag, Post, Thread, KeyPair
6 from boards.models.thread import STATUS_ARCHIVE
6
7
7
8
8 class PostTests(TestCase):
9 class PostTests(TestCase):
@@ -96,7 +97,7 b' class PostTests(TestCase):'
96 self._create_post()
97 self._create_post()
97
98
98 self.assertEqual(settings.get_int('Messages', 'MaxThreadCount'),
99 self.assertEqual(settings.get_int('Messages', 'MaxThreadCount'),
99 len(Thread.objects.filter(archived=False)))
100 len(Thread.objects.exclude(status=STATUS_ARCHIVE)))
100
101
101 def test_pages(self):
102 def test_pages(self):
102 """Test that the thread list is properly split into pages"""
103 """Test that the thread list is properly split into pages"""
@@ -104,9 +105,9 b' class PostTests(TestCase):'
104 for i in range(settings.get_int('Messages', 'MaxThreadCount')):
105 for i in range(settings.get_int('Messages', 'MaxThreadCount')):
105 self._create_post()
106 self._create_post()
106
107
107 all_threads = Thread.objects.filter(archived=False)
108 all_threads = Thread.objects.exclude(status=STATUS_ARCHIVE)
108
109
109 paginator = Paginator(Thread.objects.filter(archived=False),
110 paginator = Paginator(Thread.objects.exclude(status=STATUS_ARCHIVE),
110 settings.get_int('View', 'ThreadsPerPage'))
111 settings.get_int('View', 'ThreadsPerPage'))
111 posts_in_second_page = paginator.page(2).object_list
112 posts_in_second_page = paginator.page(2).object_list
112 first_post = posts_in_second_page[0]
113 first_post = posts_in_second_page[0]
@@ -41,8 +41,6 b' class ViewTest(TestCase):'
41 except NoReverseMatch:
41 except NoReverseMatch:
42 # This view just needs additional arguments
42 # This view just needs additional arguments
43 pass
43 pass
44 except Exception as e:
45 self.fail('Got exception %s at %s view' % (e, view_name))
46 except AttributeError:
44 except AttributeError:
47 # This is normal, some views do not have names
45 # This is normal, some views do not have names
48 pass
46 pass
@@ -1,5 +1,5 b''
1 from django.conf.urls import patterns, url
1 from django.conf.urls import patterns, url
2 from django.views.i18n import javascript_catalog
2 #from django.views.i18n import javascript_catalog
3
3
4 from boards import views
4 from boards import views
5 from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed
5 from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed
@@ -12,6 +12,8 b' from boards.views.static import StaticPa'
12 from boards.views.preview import PostPreviewView
12 from boards.views.preview import PostPreviewView
13 from boards.views.sync import get_post_sync_data, response_get, response_pull
13 from boards.views.sync import get_post_sync_data, response_get, response_pull
14 from boards.views.random import RandomImageView
14 from boards.views.random import RandomImageView
15 from boards.views.tag_gallery import TagGalleryView
16 from boards.views.translation import cached_javascript_catalog
15
17
16
18
17 js_info_dict = {
19 js_info_dict = {
@@ -45,6 +47,7 b" urlpatterns = patterns('',"
45 name='staticpage'),
47 name='staticpage'),
46
48
47 url(r'^random/$', RandomImageView.as_view(), name='random'),
49 url(r'^random/$', RandomImageView.as_view(), name='random'),
50 url(r'^tag/(?P<tag_name>\w+)/gallery/$', TagGalleryView.as_view(), name='tag_gallery'),
48
51
49 # RSS feeds
52 # RSS feeds
50 url(r'^rss/$', AllThreadsFeed()),
53 url(r'^rss/$', AllThreadsFeed()),
@@ -54,7 +57,7 b" urlpatterns = patterns('',"
54 url(r'^thread/(?P<post_id>\d+)/rss/$', ThreadPostsFeed()),
57 url(r'^thread/(?P<post_id>\d+)/rss/$', ThreadPostsFeed()),
55
58
56 # i18n
59 # i18n
57 url(r'^jsi18n/$', javascript_catalog, js_info_dict,
60 url(r'^jsi18n/$', cached_javascript_catalog, js_info_dict,
58 name='js_info_dict'),
61 name='js_info_dict'),
59
62
60 # API
63 # API
@@ -81,7 +84,8 b" urlpatterns = patterns('',"
81 url(r'^search/$', BoardSearchView.as_view(), name='search'),
84 url(r'^search/$', BoardSearchView.as_view(), name='search'),
82
85
83 # Notifications
86 # Notifications
84 url(r'^notifications/(?P<username>\w+)$', NotificationView.as_view(), name='notifications'),
87 url(r'^notifications/(?P<username>\w+)/$', NotificationView.as_view(), name='notifications'),
88 url(r'^notifications/$', NotificationView.as_view(), name='notifications'),
85
89
86 # Post preview
90 # Post preview
87 url(r'^preview/$', PostPreviewView.as_view(), name='preview'),
91 url(r'^preview/$', PostPreviewView.as_view(), name='preview'),
@@ -9,6 +9,7 b' import hmac'
9 from django.core.cache import cache
9 from django.core.cache import cache
10 from django.db.models import Model
10 from django.db.models import Model
11 from django import forms
11 from django import forms
12 from django.template.defaultfilters import filesizeformat
12 from django.utils import timezone
13 from django.utils import timezone
13 from django.utils.translation import ugettext_lazy as _
14 from django.utils.translation import ugettext_lazy as _
14 import magic
15 import magic
@@ -19,7 +20,6 b' from boards.settings import get_bool'
19 from neboard import settings
20 from neboard import settings
20
21
21 CACHE_KEY_DELIMITER = '_'
22 CACHE_KEY_DELIMITER = '_'
22 PERMISSION_MODERATE = 'moderation'
23
23
24 HTTP_FORWARDED = 'HTTP_X_FORWARDED_FOR'
24 HTTP_FORWARDED = 'HTTP_X_FORWARDED_FOR'
25 META_REMOTE_ADDR = 'REMOTE_ADDR'
25 META_REMOTE_ADDR = 'REMOTE_ADDR'
@@ -73,15 +73,20 b" def get_websocket_token(user_id='', time"
73 return token
73 return token
74
74
75
75
76 # TODO Test this carefully
76 def cached_result(key_method=None):
77 def cached_result(key_method=None):
77 """
78 """
78 Caches method result in the Django's cache system, persisted by object name,
79 Caches method result in the Django's cache system, persisted by object name,
79 object name and model id if object is a Django model.
80 object name, model id if object is a Django model, args and kwargs if any.
80 """
81 """
81 def _cached_result(function):
82 def _cached_result(function):
82 def inner_func(obj, *args, **kwargs):
83 def inner_func(obj, *args, **kwargs):
83 # TODO Include method arguments to the cache key
84 cache_key_params = [obj.__class__.__name__, function.__name__]
84 cache_key_params = [obj.__class__.__name__, function.__name__]
85
86 cache_key_params += args
87 for key, value in kwargs:
88 cache_key_params.append(key + ':' + value)
89
85 if isinstance(obj, Model):
90 if isinstance(obj, Model):
86 cache_key_params.append(str(obj.id))
91 cache_key_params.append(str(obj.id))
87
92
@@ -103,15 +108,6 b' def cached_result(key_method=None):'
103 return _cached_result
108 return _cached_result
104
109
105
110
106 def is_moderator(request):
107 try:
108 moderate = request.user.has_perm(PERMISSION_MODERATE)
109 except AttributeError:
110 moderate = False
111
112 return moderate
113
114
115 def get_file_hash(file) -> str:
111 def get_file_hash(file) -> str:
116 md5 = hashlib.md5()
112 md5 = hashlib.md5()
117 for chunk in file.chunks():
113 for chunk in file.chunks():
@@ -123,8 +119,8 b' def validate_file_size(size: int):'
123 max_size = boards.settings.get_int('Forms', 'MaxFileSize')
119 max_size = boards.settings.get_int('Forms', 'MaxFileSize')
124 if size > max_size:
120 if size > max_size:
125 raise forms.ValidationError(
121 raise forms.ValidationError(
126 _('File must be less than %s bytes')
122 _('File must be less than %s but is %s.')
127 % str(max_size))
123 % (filesizeformat(max_size), filesizeformat(size)))
128
124
129
125
130 def get_extension(filename):
126 def get_extension(filename):
@@ -1,3 +1,4 b''
1 from dbus.decorators import method
1 from django.core.urlresolvers import reverse
2 from django.core.urlresolvers import reverse
2 from django.core.files import File
3 from django.core.files import File
3 from django.core.files.temp import NamedTemporaryFile
4 from django.core.files.temp import NamedTemporaryFile
@@ -6,6 +7,8 b' from django.db import transaction'
6 from django.http import Http404
7 from django.http import Http404
7 from django.shortcuts import render, redirect
8 from django.shortcuts import render, redirect
8 import requests
9 import requests
10 from django.utils.decorators import method_decorator
11 from django.views.decorators.csrf import csrf_protect
9
12
10 from boards import utils, settings
13 from boards import utils, settings
11 from boards.abstracts.paginator import get_paginator
14 from boards.abstracts.paginator import get_paginator
@@ -15,7 +18,7 b' from boards.models import Post, Thread, '
15 from boards.views.banned import BannedView
18 from boards.views.banned import BannedView
16 from boards.views.base import BaseBoardView, CONTEXT_FORM
19 from boards.views.base import BaseBoardView, CONTEXT_FORM
17 from boards.views.posting_mixin import PostMixin
20 from boards.views.posting_mixin import PostMixin
18
21 from boards.views.mixins import FileUploadMixin, PaginatedMixin
19
22
20 FORM_TAGS = 'tags'
23 FORM_TAGS = 'tags'
21 FORM_TEXT = 'text'
24 FORM_TEXT = 'text'
@@ -30,20 +33,20 b" PARAMETER_PAGINATOR = 'paginator'"
30 PARAMETER_THREADS = 'threads'
33 PARAMETER_THREADS = 'threads'
31 PARAMETER_BANNERS = 'banners'
34 PARAMETER_BANNERS = 'banners'
32 PARAMETER_ADDITIONAL = 'additional_params'
35 PARAMETER_ADDITIONAL = 'additional_params'
33
36 PARAMETER_MAX_FILE_SIZE = 'max_file_size'
34 PARAMETER_PREV_LINK = 'prev_page_link'
37 PARAMETER_RSS_URL = 'rss_url'
35 PARAMETER_NEXT_LINK = 'next_page_link'
36
38
37 TEMPLATE = 'boards/all_threads.html'
39 TEMPLATE = 'boards/all_threads.html'
38 DEFAULT_PAGE = 1
40 DEFAULT_PAGE = 1
39
41
40
42
41 class AllThreadsView(PostMixin, BaseBoardView):
43 class AllThreadsView(PostMixin, FileUploadMixin, BaseBoardView, PaginatedMixin):
42
44
43 def __init__(self):
45 def __init__(self):
44 self.settings_manager = None
46 self.settings_manager = None
45 super(AllThreadsView, self).__init__()
47 super(AllThreadsView, self).__init__()
46
48
49 @method_decorator(csrf_protect)
47 def get(self, request, form: ThreadForm=None):
50 def get(self, request, form: ThreadForm=None):
48 page = request.GET.get('page', DEFAULT_PAGE)
51 page = request.GET.get('page', DEFAULT_PAGE)
49
52
@@ -74,12 +77,15 b' class AllThreadsView(PostMixin, BaseBoar'
74 params[PARAMETER_THREADS] = threads
77 params[PARAMETER_THREADS] = threads
75 params[CONTEXT_FORM] = form
78 params[CONTEXT_FORM] = form
76 params[PARAMETER_BANNERS] = Banner.objects.order_by('-id').all()
79 params[PARAMETER_BANNERS] = Banner.objects.order_by('-id').all()
80 params[PARAMETER_MAX_FILE_SIZE] = self.get_max_upload_size()
81 params[PARAMETER_RSS_URL] = self.get_rss_url()
77
82
78 paginator.set_url(self.get_reverse_url(), request.GET.dict())
83 paginator.set_url(self.get_reverse_url(), request.GET.dict())
79 self.get_page_context(paginator, params, page)
84 self.get_page_context(paginator, params, page)
80
85
81 return render(request, TEMPLATE, params)
86 return render(request, TEMPLATE, params)
82
87
88 @method_decorator(csrf_protect)
83 def post(self, request):
89 def post(self, request):
84 form = ThreadForm(request.POST, request.FILES,
90 form = ThreadForm(request.POST, request.FILES,
85 error_class=PlainErrorList)
91 error_class=PlainErrorList)
@@ -101,12 +107,7 b' class AllThreadsView(PostMixin, BaseBoar'
101 params[PARAMETER_PAGINATOR] = paginator
107 params[PARAMETER_PAGINATOR] = paginator
102 current_page = paginator.page(int(page))
108 current_page = paginator.page(int(page))
103 params[PARAMETER_CURRENT_PAGE] = current_page
109 params[PARAMETER_CURRENT_PAGE] = current_page
104 if current_page.has_previous():
110 self.set_page_urls(paginator, params)
105 params[PARAMETER_PREV_LINK] = paginator.get_page_url(
106 current_page.previous_page_number())
107 if current_page.has_next():
108 params[PARAMETER_NEXT_LINK] = paginator.get_page_url(
109 current_page.next_page_number())
110
111
111 def get_reverse_url(self):
112 def get_reverse_url(self):
112 return reverse('index')
113 return reverse('index')
@@ -136,10 +137,12 b' class AllThreadsView(PostMixin, BaseBoar'
136 text = self._remove_invalid_links(text)
137 text = self._remove_invalid_links(text)
137
138
138 tags = data[FORM_TAGS]
139 tags = data[FORM_TAGS]
140 monochrome = form.is_monochrome()
139
141
140 post = Post.objects.create_post(title=title, text=text, file=file,
142 post = Post.objects.create_post(title=title, text=text, file=file,
141 ip=ip, tags=tags, opening_posts=threads,
143 ip=ip, tags=tags, opening_posts=threads,
142 tripcode=form.get_tripcode())
144 tripcode=form.get_tripcode(),
145 monochrome=monochrome)
143
146
144 # This is required to update the threads to which posts we have replied
147 # This is required to update the threads to which posts we have replied
145 # when creating this one
148 # when creating this one
@@ -155,3 +158,6 b' class AllThreadsView(PostMixin, BaseBoar'
155
158
156 return Thread.objects\
159 return Thread.objects\
157 .exclude(tags__in=self.settings_manager.get_hidden_tags())
160 .exclude(tags__in=self.settings_manager.get_hidden_tags())
161
162 def get_rss_url(self):
163 return self.get_reverse_url() + 'rss/'
@@ -1,25 +1,20 b''
1 from collections import OrderedDict
2 import json
1 import json
3 import logging
2 import logging
4
3
5 import xml.etree.ElementTree as ET
4 from django.core import serializers
6
7 from django.db import transaction
5 from django.db import transaction
8 from django.db.models import Count
9 from django.http import HttpResponse
6 from django.http import HttpResponse
10 from django.shortcuts import get_object_or_404
7 from django.shortcuts import get_object_or_404
11 from django.core import serializers
8 from django.views.decorators.csrf import csrf_protect
12 from boards.abstracts.settingsmanager import get_settings_manager,\
13 FAV_THREAD_NO_UPDATES
14
9
10 from boards.abstracts.settingsmanager import get_settings_manager
15 from boards.forms import PostForm, PlainErrorList
11 from boards.forms import PostForm, PlainErrorList
16 from boards.models import Post, Thread, Tag, GlobalId
12 from boards.mdx_neboard import Parser
17 from boards.models.post.sync import SyncManager
13 from boards.models import Post, Thread, Tag
14 from boards.models.thread import STATUS_ARCHIVE
15 from boards.models.user import Notification
18 from boards.utils import datetime_to_epoch
16 from boards.utils import datetime_to_epoch
19 from boards.views.thread import ThreadView
17 from boards.views.thread import ThreadView
20 from boards.models.user import Notification
21 from boards.mdx_neboard import Parser
22
23
18
24 __author__ = 'neko259'
19 __author__ = 'neko259'
25
20
@@ -49,8 +44,12 b' def api_get_threaddiff(request):'
49 """
44 """
50
45
51 thread_id = request.POST.get(PARAMETER_THREAD)
46 thread_id = request.POST.get(PARAMETER_THREAD)
52 uids_str = request.POST.get(PARAMETER_UIDS).strip()
47 uids_str = request.POST.get(PARAMETER_UIDS)
53 uids = uids_str.split(' ')
48
49 if not thread_id or not uids_str:
50 return HttpResponse(content='Invalid request.')
51
52 uids = uids_str.strip().split(' ')
54
53
55 opening_post = get_object_or_404(Post, id=thread_id)
54 opening_post = get_object_or_404(Post, id=thread_id)
56 thread = opening_post.get_thread()
55 thread = opening_post.get_thread()
@@ -77,6 +76,7 b' def api_get_threaddiff(request):'
77 return HttpResponse(content=json.dumps(json_data))
76 return HttpResponse(content=json.dumps(json_data))
78
77
79
78
79 @csrf_protect
80 def api_add_post(request, opening_post_id):
80 def api_add_post(request, opening_post_id):
81 """
81 """
82 Adds a post and return the JSON response for it
82 Adds a post and return the JSON response for it
@@ -125,7 +125,7 b' def get_post(request, post_id):'
125 post = get_object_or_404(Post, id=post_id)
125 post = get_object_or_404(Post, id=post_id)
126 truncated = PARAMETER_TRUNCATED in request.GET
126 truncated = PARAMETER_TRUNCATED in request.GET
127
127
128 return HttpResponse(content=post.get_view(truncated=truncated))
128 return HttpResponse(content=post.get_view(truncated=truncated, need_op_data=True))
129
129
130
130
131 def api_get_threads(request, count):
131 def api_get_threads(request, count):
@@ -139,9 +139,9 b' def api_get_threads(request, count):'
139 tag_name = request.GET[PARAMETER_TAG]
139 tag_name = request.GET[PARAMETER_TAG]
140 if tag_name is not None:
140 if tag_name is not None:
141 tag = get_object_or_404(Tag, name=tag_name)
141 tag = get_object_or_404(Tag, name=tag_name)
142 threads = tag.get_threads().filter(archived=False)
142 threads = tag.get_threads().exclude(status=STATUS_ARCHIVE)
143 else:
143 else:
144 threads = Thread.objects.filter(archived=False)
144 threads = Thread.objects.exclude(status=STATUS_ARCHIVE)
145
145
146 if PARAMETER_OFFSET in request.GET:
146 if PARAMETER_OFFSET in request.GET:
147 offset = request.GET[PARAMETER_OFFSET]
147 offset = request.GET[PARAMETER_OFFSET]
@@ -158,8 +158,7 b' def api_get_threads(request, count):'
158
158
159 # TODO Add tags, replies and images count
159 # TODO Add tags, replies and images count
160 post_data = opening_post.get_post_data(include_last_update=True)
160 post_data = opening_post.get_post_data(include_last_update=True)
161 post_data['bumpable'] = thread.can_bump()
161 post_data['status'] = thread.get_status()
162 post_data['archived'] = thread.archived
163
162
164 opening_posts.append(post_data)
163 opening_posts.append(post_data)
165
164
@@ -214,7 +213,7 b' def api_get_notifications(request, usern'
214 last_notification_id_str = request.GET.get('last', None)
213 last_notification_id_str = request.GET.get('last', None)
215 last_id = int(last_notification_id_str) if last_notification_id_str is not None else None
214 last_id = int(last_notification_id_str) if last_notification_id_str is not None else None
216
215
217 posts = Notification.objects.get_notification_posts(username=username,
216 posts = Notification.objects.get_notification_posts(usernames=username,
218 last=last_id)
217 last=last_id)
219
218
220 json_post_list = []
219 json_post_list = []
@@ -1,13 +1,34 b''
1 import os
2
1 from django.shortcuts import render
3 from django.shortcuts import render
2
4
5 import neboard
3 from boards.authors import authors
6 from boards.authors import authors
7 from boards.utils import cached_result
4 from boards.views.base import BaseBoardView
8 from boards.views.base import BaseBoardView
9 from boards.models import Post
10
11
12 PARAM_AUTHORS = 'authors'
13 PARAM_MEDIA_SIZE = 'media_size'
14 PARAM_POST_COUNT = 'post_count'
5
15
6
16
7 class AuthorsView(BaseBoardView):
17 class AuthorsView(BaseBoardView):
8
18
9 def get(self, request):
19 def get(self, request):
10 params = dict()
20 params = dict()
11 params['authors'] = authors
21 params[PARAM_AUTHORS] = authors
22 params[PARAM_MEDIA_SIZE] = self._get_directory_size(neboard.settings.MEDIA_ROOT)
23 params[PARAM_POST_COUNT] = Post.objects.count()
12
24
13 return render(request, 'boards/authors.html', params)
25 return render(request, 'boards/authors.html', params)
26
27 @cached_result()
28 def _get_directory_size(self, directory):
29 total_size = 0
30 for dirpath, dirnames, filenames in os.walk(directory):
31 for f in filenames:
32 fp = os.path.join(dirpath, f)
33 total_size += os.path.getsize(fp)
34 return total_size
@@ -1,3 +1,6 b''
1 import boards
2
3
1 PARAM_NEXT = 'next'
4 PARAM_NEXT = 'next'
2 PARAMETER_METHOD = 'method'
5 PARAMETER_METHOD = 'method'
3
6
@@ -24,3 +27,14 b' class DispatcherMixin:'
24
27
25 if method_name:
28 if method_name:
26 return getattr(self, method_name)(*args, **kwargs)
29 return getattr(self, method_name)(*args, **kwargs)
30
31
32 class FileUploadMixin:
33 def get_max_upload_size(self):
34 return boards.settings.get_int('Forms', 'MaxFileSize')
35
36
37 class PaginatedMixin:
38 def set_page_urls(self, paginator, params):
39 params['prev_page_link'] = paginator.get_prev_page_url()
40 params['next_page_link'] = paginator.get_next_page_url()
@@ -10,37 +10,40 b" DEFAULT_PAGE = '1'"
10
10
11 TEMPLATE = 'boards/notifications.html'
11 TEMPLATE = 'boards/notifications.html'
12 PARAM_PAGE = 'page'
12 PARAM_PAGE = 'page'
13 PARAM_USERNAME = 'notification_username'
13 PARAM_USERNAMES = 'notification_usernames'
14 REQUEST_PAGE = 'page'
14 REQUEST_PAGE = 'page'
15 RESULTS_PER_PAGE = 10
15 RESULTS_PER_PAGE = 10
16
16
17
17
18 class NotificationView(BaseBoardView):
18 class NotificationView(BaseBoardView):
19
19
20 def get(self, request, username):
20 def get(self, request, username=None):
21 params = self.get_context_data()
21 params = self.get_context_data()
22
22
23 settings_manager = get_settings_manager(request)
23 settings_manager = get_settings_manager(request)
24
24
25 # If we open our notifications, reset the "new" count
25 # If we open our notifications, reset the "new" count
26 my_username = settings_manager.get_setting(SETTING_USERNAME)
26 if username is None:
27
27 notification_usernames = settings_manager.get_notification_usernames()
28 notification_username = username.lower()
28 else:
29 notification_usernames = [username]
29
30
30 posts = Notification.objects.get_notification_posts(
31 posts = Notification.objects.get_notification_posts(
31 username=notification_username)
32 usernames=notification_usernames)
32 if notification_username == my_username:
33
34 if username is None:
33 last = posts.first()
35 last = posts.first()
34 if last is not None:
36 if last is not None:
35 last_id = last.id
37 last_id = last.id
36 settings_manager.set_setting(SETTING_LAST_NOTIFICATION_ID,
38 settings_manager.set_setting(SETTING_LAST_NOTIFICATION_ID,
37 last_id)
39 last_id)
38
40
41
39 paginator = get_paginator(posts, RESULTS_PER_PAGE)
42 paginator = get_paginator(posts, RESULTS_PER_PAGE)
40
43
41 page = int(request.GET.get(REQUEST_PAGE, DEFAULT_PAGE))
44 page = int(request.GET.get(REQUEST_PAGE, DEFAULT_PAGE))
42
45
43 params[PARAM_PAGE] = paginator.page(page)
46 params[PARAM_PAGE] = paginator.page(page)
44 params[PARAM_USERNAME] = notification_username
47 params[PARAM_USERNAMES] = notification_usernames
45
48
46 return render(request, TEMPLATE, params)
49 return render(request, TEMPLATE, params)
@@ -1,13 +1,15 b''
1 from boards.views.thread import ThreadView
1 from boards.views.thread import ThreadView
2 from boards.views.mixins import FileUploadMixin
2
3
3 TEMPLATE_NORMAL = 'boards/thread_normal.html'
4 TEMPLATE_NORMAL = 'boards/thread_normal.html'
4
5
5 CONTEXT_BUMPLIMIT_PRG = 'bumplimit_progress'
6 CONTEXT_BUMPLIMIT_PRG = 'bumplimit_progress'
6 CONTEXT_POSTS_LEFT = 'posts_left'
7 CONTEXT_POSTS_LEFT = 'posts_left'
7 CONTEXT_BUMPABLE = 'bumpable'
8 CONTEXT_BUMPABLE = 'bumpable'
9 PARAM_MAX_FILE_SIZE = 'max_file_size'
8
10
9
11
10 class NormalThreadView(ThreadView):
12 class NormalThreadView(ThreadView, FileUploadMixin):
11
13
12 def get_template(self):
14 def get_template(self):
13 return TEMPLATE_NORMAL
15 return TEMPLATE_NORMAL
@@ -26,5 +28,6 b' class NormalThreadView(ThreadView):'
26 params[CONTEXT_POSTS_LEFT] = left_posts
28 params[CONTEXT_POSTS_LEFT] = left_posts
27 params[CONTEXT_BUMPLIMIT_PRG] = str(
29 params[CONTEXT_BUMPLIMIT_PRG] = str(
28 float(left_posts) / max_posts * 100)
30 float(left_posts) / max_posts * 100)
31 params[PARAM_MAX_FILE_SIZE] = self.get_max_upload_size()
29
32
30 return params
33 return params
@@ -1,8 +1,12 b''
1 from django.contrib.auth.decorators import permission_required
1 from django.contrib.auth.decorators import permission_required
2
2
3 from django.core.exceptions import ObjectDoesNotExist
3 from django.core.exceptions import ObjectDoesNotExist
4 from django.core.urlresolvers import reverse
4 from django.http import Http404
5 from django.http import Http404
5 from django.shortcuts import get_object_or_404, render, redirect
6 from django.shortcuts import get_object_or_404, render, redirect
7 from django.template.context_processors import csrf
8 from django.utils.decorators import method_decorator
9 from django.views.decorators.csrf import csrf_protect
6 from django.views.generic.edit import FormMixin
10 from django.views.generic.edit import FormMixin
7 from django.utils import timezone
11 from django.utils import timezone
8 from django.utils.dateformat import format
12 from django.utils.dateformat import format
@@ -28,6 +32,7 b" CONTEXT_WS_TIME = 'ws_token_time'"
28 CONTEXT_MODE = 'mode'
32 CONTEXT_MODE = 'mode'
29 CONTEXT_OP = 'opening_post'
33 CONTEXT_OP = 'opening_post'
30 CONTEXT_FAVORITE = 'is_favorite'
34 CONTEXT_FAVORITE = 'is_favorite'
35 CONTEXT_RSS_URL = 'rss_url'
31
36
32 FORM_TITLE = 'title'
37 FORM_TITLE = 'title'
33 FORM_TEXT = 'text'
38 FORM_TEXT = 'text'
@@ -37,6 +42,7 b" FORM_THREADS = 'threads'"
37
42
38 class ThreadView(BaseBoardView, PostMixin, FormMixin, DispatcherMixin):
43 class ThreadView(BaseBoardView, PostMixin, FormMixin, DispatcherMixin):
39
44
45 @method_decorator(csrf_protect)
40 def get(self, request, post_id, form: PostForm=None):
46 def get(self, request, post_id, form: PostForm=None):
41 try:
47 try:
42 opening_post = Post.objects.get(id=post_id)
48 opening_post = Post.objects.get(id=post_id)
@@ -67,6 +73,7 b' class ThreadView(BaseBoardView, PostMixi'
67 params[CONTEXT_MODE] = self.get_mode()
73 params[CONTEXT_MODE] = self.get_mode()
68 params[CONTEXT_OP] = opening_post
74 params[CONTEXT_OP] = opening_post
69 params[CONTEXT_FAVORITE] = favorite
75 params[CONTEXT_FAVORITE] = favorite
76 params[CONTEXT_RSS_URL] = self.get_rss_url(post_id)
70
77
71 if settings.get_bool('External', 'WebsocketsEnabled'):
78 if settings.get_bool('External', 'WebsocketsEnabled'):
72 token_time = format(timezone.now(), u'U')
79 token_time = format(timezone.now(), u'U')
@@ -82,6 +89,7 b' class ThreadView(BaseBoardView, PostMixi'
82
89
83 return render(request, self.get_template(), params)
90 return render(request, self.get_template(), params)
84
91
92 @method_decorator(csrf_protect)
85 def post(self, request, post_id):
93 def post(self, request, post_id):
86 opening_post = get_object_or_404(Post, id=post_id)
94 opening_post = get_object_or_404(Post, id=post_id)
87
95
@@ -94,7 +102,7 b' class ThreadView(BaseBoardView, PostMixi'
94
102
95 return redirect('thread', post_id) # FIXME Different for different modes
103 return redirect('thread', post_id) # FIXME Different for different modes
96
104
97 if not opening_post.get_thread().archived:
105 if not opening_post.get_thread().is_archived():
98 form = PostForm(request.POST, request.FILES,
106 form = PostForm(request.POST, request.FILES,
99 error_class=PlainErrorList)
107 error_class=PlainErrorList)
100 form.session = request.session
108 form.session = request.session
@@ -163,11 +171,5 b' class ThreadView(BaseBoardView, PostMixi'
163 settings_manager = get_settings_manager(request)
171 settings_manager = get_settings_manager(request)
164 settings_manager.del_fav_thread(opening_post)
172 settings_manager.del_fav_thread(opening_post)
165
173
166 @permission_required('boards.post_hide_unhide')
174 def get_rss_url(self, opening_id):
167 def toggle_hide_post(self, request, opening_post):
175 return reverse('thread', kwargs={'post_id': opening_id}) + 'rss/'
168 post_id = request.GET.get(REQ_POST_ID)
169
170 if post_id:
171 post = get_object_or_404(Post, id=post_id)
172 post.set_hidden(not post.is_hidden())
173 post.save(update_fields=['hidden'])
@@ -95,19 +95,27 b' else:'
95 # Make this unique, and don't share it with anybody.
95 # Make this unique, and don't share it with anybody.
96 SECRET_KEY = '@1rc$o(7=tt#kd+4s$u6wchm**z^)4x90)7f6z(i&amp;55@o11*8o'
96 SECRET_KEY = '@1rc$o(7=tt#kd+4s$u6wchm**z^)4x90)7f6z(i&amp;55@o11*8o'
97
97
98 # List of callables that know how to import templates from various sources.
98 TEMPLATES = [{
99 TEMPLATE_LOADERS = (
99 'BACKEND': 'django.template.backends.django.DjangoTemplates',
100 'django.template.loaders.filesystem.Loader',
100 'DIRS': ['templates'],
101 'django.template.loaders.app_directories.Loader',
101 'OPTIONS': {
102 )
102 'loaders': [
103 ('django.template.loaders.cached.Loader', [
104 'django.template.loaders.filesystem.Loader',
105 'django.template.loaders.app_directories.Loader',
106 ]),
107 ],
108 'context_processors': [
109 'django.template.context_processors.csrf',
110 'django.core.context_processors.media',
111 'django.core.context_processors.static',
112 'django.core.context_processors.request',
113 'django.contrib.auth.context_processors.auth',
114 'boards.context_processors.user_and_ui_processor',
115 ],
116 },
117 }]
103
118
104 TEMPLATE_CONTEXT_PROCESSORS = (
105 'django.core.context_processors.media',
106 'django.core.context_processors.static',
107 'django.core.context_processors.request',
108 'django.contrib.auth.context_processors.auth',
109 'boards.context_processors.user_and_ui_processor',
110 )
111
119
112 MIDDLEWARE_CLASSES = [
120 MIDDLEWARE_CLASSES = [
113 'django.middleware.http.ConditionalGetMiddleware',
121 'django.middleware.http.ConditionalGetMiddleware',
@@ -125,13 +133,6 b" ROOT_URLCONF = 'neboard.urls'"
125 # Python dotted path to the WSGI application used by Django's runserver.
133 # Python dotted path to the WSGI application used by Django's runserver.
126 WSGI_APPLICATION = 'neboard.wsgi.application'
134 WSGI_APPLICATION = 'neboard.wsgi.application'
127
135
128 TEMPLATE_DIRS = (
129 # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
130 # Always use forward slashes, even on Windows.
131 # Don't forget to use absolute paths, not relative paths.
132 'templates',
133 )
134
135 INSTALLED_APPS = (
136 INSTALLED_APPS = (
136 'django.contrib.auth',
137 'django.contrib.auth',
137 'django.contrib.contenttypes',
138 'django.contrib.contenttypes',
General Comments 0
You need to be logged in to leave comments. Login now