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