Show More
@@ -0,0 +1,22 b'' | |||||
|
1 | VERSION = '2.5.1 Yasako' | |||
|
2 | SITE_NAME = 'Neboard' | |||
|
3 | ||||
|
4 | CACHE_TIMEOUT = 600 # Timeout for caching, if cache is used | |||
|
5 | LOGIN_TIMEOUT = 3600 # Timeout between login tries | |||
|
6 | MAX_TEXT_LENGTH = 30000 # Max post length in characters | |||
|
7 | MAX_IMAGE_SIZE = 8 * 1024 * 1024 # Max image size | |||
|
8 | ||||
|
9 | # Thread bumplimit | |||
|
10 | MAX_POSTS_PER_THREAD = 10 | |||
|
11 | # Old posts will be archived or deleted if this value is reached | |||
|
12 | MAX_THREAD_COUNT = 5 | |||
|
13 | THREADS_PER_PAGE = 3 | |||
|
14 | DEFAULT_THEME = 'md' | |||
|
15 | LAST_REPLIES_COUNT = 3 | |||
|
16 | ||||
|
17 | # Enable archiving threads instead of deletion when the thread limit is reached | |||
|
18 | ARCHIVE_THREADS = True | |||
|
19 | # Limit posting speed | |||
|
20 | LIMIT_POSTING_SPEED = False | |||
|
21 | # Thread update | |||
|
22 | WEBSOCKETS_ENABLED = True |
@@ -0,0 +1,18 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | from __future__ import unicode_literals | |||
|
3 | ||||
|
4 | from django.db import models, migrations | |||
|
5 | ||||
|
6 | ||||
|
7 | class Migration(migrations.Migration): | |||
|
8 | ||||
|
9 | dependencies = [ | |||
|
10 | ('boards', '0004_tag_required'), | |||
|
11 | ] | |||
|
12 | ||||
|
13 | operations = [ | |||
|
14 | migrations.RemoveField( | |||
|
15 | model_name='thread', | |||
|
16 | name='replies', | |||
|
17 | ), | |||
|
18 | ] |
@@ -0,0 +1,19 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | from __future__ import unicode_literals | |||
|
3 | ||||
|
4 | from django.db import models, migrations | |||
|
5 | ||||
|
6 | ||||
|
7 | class Migration(migrations.Migration): | |||
|
8 | ||||
|
9 | dependencies = [ | |||
|
10 | ('boards', '0005_remove_thread_replies'), | |||
|
11 | ] | |||
|
12 | ||||
|
13 | operations = [ | |||
|
14 | migrations.AlterField( | |||
|
15 | model_name='thread', | |||
|
16 | name='bump_time', | |||
|
17 | field=models.DateTimeField(db_index=True), | |||
|
18 | ), | |||
|
19 | ] |
@@ -0,0 +1,30 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | from __future__ import unicode_literals | |||
|
3 | ||||
|
4 | from django.db import models, migrations | |||
|
5 | ||||
|
6 | ||||
|
7 | class Migration(migrations.Migration): | |||
|
8 | ||||
|
9 | def thread_to_threads(apps, schema_editor): | |||
|
10 | Post = apps.get_model('boards', 'Post') | |||
|
11 | for post in Post.objects.all(): | |||
|
12 | post.threads.add(post.thread_new) | |||
|
13 | ||||
|
14 | dependencies = [ | |||
|
15 | ('boards', '0006_auto_20150201_2130'), | |||
|
16 | ] | |||
|
17 | ||||
|
18 | operations = [ | |||
|
19 | migrations.AddField( | |||
|
20 | model_name='post', | |||
|
21 | name='threads', | |||
|
22 | field=models.ManyToManyField(to='boards.Thread'), | |||
|
23 | preserve_default=True, | |||
|
24 | ), | |||
|
25 | migrations.RunPython(thread_to_threads), | |||
|
26 | migrations.RemoveField( | |||
|
27 | model_name='post', | |||
|
28 | name='thread_new', | |||
|
29 | ), | |||
|
30 | ] |
@@ -0,0 +1,19 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | from __future__ import unicode_literals | |||
|
3 | ||||
|
4 | from django.db import models, migrations | |||
|
5 | ||||
|
6 | ||||
|
7 | class Migration(migrations.Migration): | |||
|
8 | ||||
|
9 | dependencies = [ | |||
|
10 | ('boards', '0007_auto_20150205_1247'), | |||
|
11 | ] | |||
|
12 | ||||
|
13 | operations = [ | |||
|
14 | migrations.AlterField( | |||
|
15 | model_name='post', | |||
|
16 | name='threads', | |||
|
17 | field=models.ManyToManyField(to='boards.Thread', db_index=True), | |||
|
18 | ), | |||
|
19 | ] |
@@ -0,0 +1,28 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | from __future__ import unicode_literals | |||
|
3 | ||||
|
4 | from django.db import models, migrations | |||
|
5 | ||||
|
6 | ||||
|
7 | class Migration(migrations.Migration): | |||
|
8 | ||||
|
9 | def first_thread_to_thread(apps, schema_editor): | |||
|
10 | Post = apps.get_model('boards', 'Post') | |||
|
11 | for post in Post.objects.all(): | |||
|
12 | post.thread = post.threads.first() | |||
|
13 | post.save(update_fields=['thread']) | |||
|
14 | ||||
|
15 | ||||
|
16 | dependencies = [ | |||
|
17 | ('boards', '0008_auto_20150205_1304'), | |||
|
18 | ] | |||
|
19 | ||||
|
20 | operations = [ | |||
|
21 | migrations.AddField( | |||
|
22 | model_name='post', | |||
|
23 | name='thread', | |||
|
24 | field=models.ForeignKey(related_name='pt+', to='boards.Thread', default=None, null=True), | |||
|
25 | preserve_default=False, | |||
|
26 | ), | |||
|
27 | migrations.RunPython(first_thread_to_thread), | |||
|
28 | ] |
@@ -0,0 +1,44 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | from __future__ import unicode_literals | |||
|
3 | ||||
|
4 | from django.db import models, migrations | |||
|
5 | ||||
|
6 | ||||
|
7 | class Migration(migrations.Migration): | |||
|
8 | ||||
|
9 | def clean_duplicate_tags(apps, schema_editor): | |||
|
10 | Tag = apps.get_model('boards', 'Tag') | |||
|
11 | for tag in Tag.objects.all(): | |||
|
12 | tags_with_same_name = Tag.objects.filter(name=tag.name).all() | |||
|
13 | if len(tags_with_same_name) > 1: | |||
|
14 | for tag_duplicate in tags_with_same_name[1:]: | |||
|
15 | threads = tag_duplicate.thread_set.all() | |||
|
16 | for thread in threads: | |||
|
17 | thread.tags.add(tag) | |||
|
18 | tag_duplicate.delete() | |||
|
19 | ||||
|
20 | dependencies = [ | |||
|
21 | ('boards', '0009_post_thread'), | |||
|
22 | ] | |||
|
23 | ||||
|
24 | operations = [ | |||
|
25 | migrations.AlterField( | |||
|
26 | model_name='post', | |||
|
27 | name='thread', | |||
|
28 | field=models.ForeignKey(to='boards.Thread', related_name='pt+'), | |||
|
29 | preserve_default=True, | |||
|
30 | ), | |||
|
31 | migrations.RunPython(clean_duplicate_tags), | |||
|
32 | migrations.AlterField( | |||
|
33 | model_name='tag', | |||
|
34 | name='name', | |||
|
35 | field=models.CharField(db_index=True, unique=True, max_length=100), | |||
|
36 | preserve_default=True, | |||
|
37 | ), | |||
|
38 | migrations.AlterField( | |||
|
39 | model_name='tag', | |||
|
40 | name='required', | |||
|
41 | field=models.BooleanField(db_index=True, default=False), | |||
|
42 | preserve_default=True, | |||
|
43 | ), | |||
|
44 | ] |
@@ -0,0 +1,25 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | from __future__ import unicode_literals | |||
|
3 | ||||
|
4 | from django.db import models, migrations | |||
|
5 | ||||
|
6 | ||||
|
7 | class Migration(migrations.Migration): | |||
|
8 | ||||
|
9 | dependencies = [ | |||
|
10 | ('boards', '0010_auto_20150208_1451'), | |||
|
11 | ] | |||
|
12 | ||||
|
13 | operations = [ | |||
|
14 | migrations.CreateModel( | |||
|
15 | name='Notification', | |||
|
16 | fields=[ | |||
|
17 | ('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)), | |||
|
18 | ('name', models.TextField()), | |||
|
19 | ('post', models.ForeignKey(to='boards.Post')), | |||
|
20 | ], | |||
|
21 | options={ | |||
|
22 | }, | |||
|
23 | bases=(models.Model,), | |||
|
24 | ), | |||
|
25 | ] |
@@ -0,0 +1,63 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | from __future__ import unicode_literals | |||
|
3 | ||||
|
4 | from django.db import models, migrations | |||
|
5 | ||||
|
6 | ||||
|
7 | class Migration(migrations.Migration): | |||
|
8 | ||||
|
9 | dependencies = [ | |||
|
10 | ('boards', '0011_notification'), | |||
|
11 | ] | |||
|
12 | ||||
|
13 | operations = [ | |||
|
14 | migrations.CreateModel( | |||
|
15 | name='GlobalId', | |||
|
16 | fields=[ | |||
|
17 | ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')), | |||
|
18 | ('key', models.TextField()), | |||
|
19 | ('key_type', models.TextField()), | |||
|
20 | ('local_id', models.IntegerField()), | |||
|
21 | ], | |||
|
22 | options={ | |||
|
23 | }, | |||
|
24 | bases=(models.Model,), | |||
|
25 | ), | |||
|
26 | migrations.CreateModel( | |||
|
27 | name='KeyPair', | |||
|
28 | fields=[ | |||
|
29 | ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')), | |||
|
30 | ('public_key', models.TextField()), | |||
|
31 | ('private_key', models.TextField()), | |||
|
32 | ('key_type', models.TextField()), | |||
|
33 | ('primary', models.BooleanField(default=False)), | |||
|
34 | ], | |||
|
35 | options={ | |||
|
36 | }, | |||
|
37 | bases=(models.Model,), | |||
|
38 | ), | |||
|
39 | migrations.CreateModel( | |||
|
40 | name='Signature', | |||
|
41 | fields=[ | |||
|
42 | ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')), | |||
|
43 | ('key_type', models.TextField()), | |||
|
44 | ('key', models.TextField()), | |||
|
45 | ('signature', models.TextField()), | |||
|
46 | ], | |||
|
47 | options={ | |||
|
48 | }, | |||
|
49 | bases=(models.Model,), | |||
|
50 | ), | |||
|
51 | migrations.AddField( | |||
|
52 | model_name='post', | |||
|
53 | name='global_id', | |||
|
54 | field=models.OneToOneField(to='boards.GlobalId', null=True, blank=True), | |||
|
55 | preserve_default=True, | |||
|
56 | ), | |||
|
57 | migrations.AddField( | |||
|
58 | model_name='post', | |||
|
59 | name='signature', | |||
|
60 | field=models.ManyToManyField(null=True, blank=True, to='boards.Signature'), | |||
|
61 | preserve_default=True, | |||
|
62 | ), | |||
|
63 | ] |
@@ -0,0 +1,44 b'' | |||||
|
1 | from django.shortcuts import render | |||
|
2 | from boards.abstracts.paginator import get_paginator | |||
|
3 | from boards.abstracts.settingsmanager import get_settings_manager, \ | |||
|
4 | SETTING_USERNAME, SETTING_LAST_NOTIFICATION_ID | |||
|
5 | from boards.models import Post | |||
|
6 | from boards.models.user import Notification | |||
|
7 | from boards.views.base import BaseBoardView | |||
|
8 | ||||
|
9 | TEMPLATE = 'boards/notifications.html' | |||
|
10 | PARAM_PAGE = 'page' | |||
|
11 | PARAM_USERNAME = 'notification_username' | |||
|
12 | REQUEST_PAGE = 'page' | |||
|
13 | RESULTS_PER_PAGE = 10 | |||
|
14 | ||||
|
15 | ||||
|
16 | class NotificationView(BaseBoardView): | |||
|
17 | ||||
|
18 | def get(self, request, username): | |||
|
19 | params = self.get_context_data() | |||
|
20 | ||||
|
21 | settings_manager = get_settings_manager(request) | |||
|
22 | ||||
|
23 | # If we open our notifications, reset the "new" count | |||
|
24 | my_username = settings_manager.get_setting(SETTING_USERNAME) | |||
|
25 | ||||
|
26 | notification_username = username.lower() | |||
|
27 | ||||
|
28 | posts = Notification.objects.get_notification_posts( | |||
|
29 | username=notification_username) | |||
|
30 | if notification_username == my_username: | |||
|
31 | last = posts.first() | |||
|
32 | if last is not None: | |||
|
33 | last_id = last.id | |||
|
34 | settings_manager.set_setting(SETTING_LAST_NOTIFICATION_ID, | |||
|
35 | last_id) | |||
|
36 | ||||
|
37 | paginator = get_paginator(posts, RESULTS_PER_PAGE) | |||
|
38 | ||||
|
39 | page = int(request.GET.get(REQUEST_PAGE, '1')) | |||
|
40 | ||||
|
41 | params[PARAM_PAGE] = paginator.page(page) | |||
|
42 | params[PARAM_USERNAME] = notification_username | |||
|
43 | ||||
|
44 | return render(request, TEMPLATE, params) |
@@ -0,0 +1,3 b'' | |||||
|
1 | from boards.views.thread.thread import ThreadView | |||
|
2 | from boards.views.thread.normal import NormalThreadView | |||
|
3 | from boards.views.thread.gallery import GalleryThreadView |
@@ -0,0 +1,19 b'' | |||||
|
1 | from boards.views.thread import ThreadView | |||
|
2 | ||||
|
3 | TEMPLATE_GALLERY = 'boards/thread_gallery.html' | |||
|
4 | ||||
|
5 | CONTEXT_POSTS = 'posts' | |||
|
6 | ||||
|
7 | ||||
|
8 | class GalleryThreadView(ThreadView): | |||
|
9 | ||||
|
10 | def get_template(self): | |||
|
11 | return TEMPLATE_GALLERY | |||
|
12 | ||||
|
13 | def get_data(self, thread): | |||
|
14 | params = dict() | |||
|
15 | ||||
|
16 | params[CONTEXT_POSTS] = thread.get_replies_with_images( | |||
|
17 | view_fields_only=True) | |||
|
18 | ||||
|
19 | return params |
@@ -0,0 +1,38 b'' | |||||
|
1 | from boards import settings | |||
|
2 | from boards.views.thread import ThreadView | |||
|
3 | ||||
|
4 | TEMPLATE_NORMAL = 'boards/thread.html' | |||
|
5 | ||||
|
6 | CONTEXT_OP = 'opening_post' | |||
|
7 | CONTEXT_BUMPLIMIT_PRG = 'bumplimit_progress' | |||
|
8 | CONTEXT_POSTS_LEFT = 'posts_left' | |||
|
9 | CONTEXT_BUMPABLE = 'bumpable' | |||
|
10 | ||||
|
11 | FORM_TITLE = 'title' | |||
|
12 | FORM_TEXT = 'text' | |||
|
13 | FORM_IMAGE = 'image' | |||
|
14 | ||||
|
15 | MODE_GALLERY = 'gallery' | |||
|
16 | MODE_NORMAL = 'normal' | |||
|
17 | ||||
|
18 | ||||
|
19 | class NormalThreadView(ThreadView): | |||
|
20 | ||||
|
21 | def get_template(self): | |||
|
22 | return TEMPLATE_NORMAL | |||
|
23 | ||||
|
24 | def get_data(self, thread): | |||
|
25 | params = dict() | |||
|
26 | ||||
|
27 | bumpable = thread.can_bump() | |||
|
28 | params[CONTEXT_BUMPABLE] = bumpable | |||
|
29 | if bumpable: | |||
|
30 | left_posts = settings.MAX_POSTS_PER_THREAD \ | |||
|
31 | - thread.get_reply_count() | |||
|
32 | params[CONTEXT_POSTS_LEFT] = left_posts | |||
|
33 | params[CONTEXT_BUMPLIMIT_PRG] = str( | |||
|
34 | float(left_posts) / settings.MAX_POSTS_PER_THREAD * 100) | |||
|
35 | ||||
|
36 | params[CONTEXT_OP] = thread.get_opening_post() | |||
|
37 | ||||
|
38 | return params |
@@ -0,0 +1,133 b'' | |||||
|
1 | from django.core.urlresolvers import reverse | |||
|
2 | from django.core.exceptions import ObjectDoesNotExist | |||
|
3 | from django.db import transaction | |||
|
4 | from django.http import Http404 | |||
|
5 | from django.shortcuts import get_object_or_404, render, redirect | |||
|
6 | from django.views.generic.edit import FormMixin | |||
|
7 | ||||
|
8 | from boards import utils, settings | |||
|
9 | from boards.forms import PostForm, PlainErrorList | |||
|
10 | from boards.models import Post, Ban | |||
|
11 | from boards.views.banned import BannedView | |||
|
12 | from boards.views.base import BaseBoardView, CONTEXT_FORM | |||
|
13 | from boards.views.posting_mixin import PostMixin | |||
|
14 | import neboard | |||
|
15 | ||||
|
16 | TEMPLATE_GALLERY = 'boards/thread_gallery.html' | |||
|
17 | TEMPLATE_NORMAL = 'boards/thread.html' | |||
|
18 | ||||
|
19 | CONTEXT_POSTS = 'posts' | |||
|
20 | CONTEXT_OP = 'opening_post' | |||
|
21 | CONTEXT_BUMPLIMIT_PRG = 'bumplimit_progress' | |||
|
22 | CONTEXT_POSTS_LEFT = 'posts_left' | |||
|
23 | CONTEXT_LASTUPDATE = "last_update" | |||
|
24 | CONTEXT_MAX_REPLIES = 'max_replies' | |||
|
25 | CONTEXT_THREAD = 'thread' | |||
|
26 | CONTEXT_BUMPABLE = 'bumpable' | |||
|
27 | CONTEXT_WS_TOKEN = 'ws_token' | |||
|
28 | CONTEXT_WS_PROJECT = 'ws_project' | |||
|
29 | CONTEXT_WS_HOST = 'ws_host' | |||
|
30 | CONTEXT_WS_PORT = 'ws_port' | |||
|
31 | ||||
|
32 | FORM_TITLE = 'title' | |||
|
33 | FORM_TEXT = 'text' | |||
|
34 | FORM_IMAGE = 'image' | |||
|
35 | ||||
|
36 | ||||
|
37 | class ThreadView(BaseBoardView, PostMixin, FormMixin): | |||
|
38 | ||||
|
39 | def get(self, request, post_id, form: PostForm=None): | |||
|
40 | try: | |||
|
41 | opening_post = Post.objects.get(id=post_id) | |||
|
42 | except ObjectDoesNotExist: | |||
|
43 | raise Http404 | |||
|
44 | ||||
|
45 | # If this is not OP, don't show it as it is | |||
|
46 | if not opening_post.is_opening(): | |||
|
47 | return redirect(opening_post.get_thread().get_opening_post().get_url()) | |||
|
48 | ||||
|
49 | if not form: | |||
|
50 | form = PostForm(error_class=PlainErrorList) | |||
|
51 | ||||
|
52 | thread_to_show = opening_post.get_thread() | |||
|
53 | ||||
|
54 | params = dict() | |||
|
55 | ||||
|
56 | params[CONTEXT_FORM] = form | |||
|
57 | params[CONTEXT_LASTUPDATE] = str(utils.datetime_to_epoch( | |||
|
58 | thread_to_show.last_edit_time)) | |||
|
59 | params[CONTEXT_THREAD] = thread_to_show | |||
|
60 | params[CONTEXT_MAX_REPLIES] = settings.MAX_POSTS_PER_THREAD | |||
|
61 | ||||
|
62 | if settings.WEBSOCKETS_ENABLED: | |||
|
63 | params[CONTEXT_WS_TOKEN] = utils.get_websocket_token( | |||
|
64 | timestamp=params[CONTEXT_LASTUPDATE]) | |||
|
65 | params[CONTEXT_WS_PROJECT] = neboard.settings.CENTRIFUGE_PROJECT_ID | |||
|
66 | params[CONTEXT_WS_HOST] = request.get_host().split(':')[0] | |||
|
67 | params[CONTEXT_WS_PORT] = neboard.settings.CENTRIFUGE_PORT | |||
|
68 | ||||
|
69 | params.update(self.get_data(thread_to_show)) | |||
|
70 | ||||
|
71 | return render(request, self.get_template(), params) | |||
|
72 | ||||
|
73 | def post(self, request, post_id): | |||
|
74 | opening_post = get_object_or_404(Post, id=post_id) | |||
|
75 | ||||
|
76 | # If this is not OP, don't show it as it is | |||
|
77 | if not opening_post.is_opening(): | |||
|
78 | raise Http404 | |||
|
79 | ||||
|
80 | if not opening_post.get_thread().archived: | |||
|
81 | form = PostForm(request.POST, request.FILES, | |||
|
82 | error_class=PlainErrorList) | |||
|
83 | form.session = request.session | |||
|
84 | ||||
|
85 | if form.is_valid(): | |||
|
86 | return self.new_post(request, form, opening_post) | |||
|
87 | if form.need_to_ban: | |||
|
88 | # Ban user because he is suspected to be a bot | |||
|
89 | self._ban_current_user(request) | |||
|
90 | ||||
|
91 | return self.get(request, post_id, form) | |||
|
92 | ||||
|
93 | def new_post(self, request, form: PostForm, opening_post: Post=None, | |||
|
94 | html_response=True): | |||
|
95 | """ | |||
|
96 | Adds a new post (in thread or as a reply). | |||
|
97 | """ | |||
|
98 | ||||
|
99 | ip = utils.get_client_ip(request) | |||
|
100 | ||||
|
101 | data = form.cleaned_data | |||
|
102 | ||||
|
103 | title = data[FORM_TITLE] | |||
|
104 | text = data[FORM_TEXT] | |||
|
105 | image = form.get_image() | |||
|
106 | ||||
|
107 | text = self._remove_invalid_links(text) | |||
|
108 | ||||
|
109 | post_thread = opening_post.get_thread() | |||
|
110 | ||||
|
111 | post = Post.objects.create_post(title=title, text=text, image=image, | |||
|
112 | thread=post_thread, ip=ip) | |||
|
113 | post.send_to_websocket(request) | |||
|
114 | ||||
|
115 | if html_response: | |||
|
116 | if opening_post: | |||
|
117 | return redirect(post.get_url()) | |||
|
118 | else: | |||
|
119 | return post | |||
|
120 | ||||
|
121 | def get_data(self, thread): | |||
|
122 | """ | |||
|
123 | Returns context params for the view. | |||
|
124 | """ | |||
|
125 | ||||
|
126 | pass | |||
|
127 | ||||
|
128 | def get_template(self): | |||
|
129 | """ | |||
|
130 | Gets template to show the thread mode on. | |||
|
131 | """ | |||
|
132 | ||||
|
133 | pass |
@@ -20,3 +20,8 b' 4d998aba79e4abf0a2e78e93baaa2c2800b1c49c' | |||||
20 | 07fdef4ac33a859250d03f17c594089792bca615 2.2.1 |
|
20 | 07fdef4ac33a859250d03f17c594089792bca615 2.2.1 | |
21 | bcc74d45f060ecd3ff06ff448165aea0d026cb3e 2.2.2 |
|
21 | bcc74d45f060ecd3ff06ff448165aea0d026cb3e 2.2.2 | |
22 | b0e629ff24eb47a449ecfb455dc6cc600d18c9e2 2.2.3 |
|
22 | b0e629ff24eb47a449ecfb455dc6cc600d18c9e2 2.2.3 | |
|
23 | 1b52ba60f17fd7c90912c14d9d17e880b7952d01 2.2.4 | |||
|
24 | 957e2fec91468f739b0fc2b9936d564505048c68 2.3.0 | |||
|
25 | bb91141c6ea5c822ccbe2d46c3c48bdab683b77d 2.4.0 | |||
|
26 | 97eb184637e5691b288eaf6b03e8971f3364c239 2.5.0 | |||
|
27 | 119fafc5381b933bf30d97be0b278349f6135075 2.5.1 |
@@ -12,19 +12,12 b" SETTING_THEME = 'theme'" | |||||
12 | SETTING_FAVORITE_TAGS = 'favorite_tags' |
|
12 | SETTING_FAVORITE_TAGS = 'favorite_tags' | |
13 | SETTING_HIDDEN_TAGS = 'hidden_tags' |
|
13 | SETTING_HIDDEN_TAGS = 'hidden_tags' | |
14 | SETTING_PERMISSIONS = 'permissions' |
|
14 | SETTING_PERMISSIONS = 'permissions' | |
|
15 | SETTING_USERNAME = 'username' | |||
|
16 | SETTING_LAST_NOTIFICATION_ID = 'last_notification' | |||
15 |
|
17 | |||
16 | DEFAULT_THEME = 'md' |
|
18 | DEFAULT_THEME = 'md' | |
17 |
|
19 | |||
18 |
|
20 | |||
19 | def get_settings_manager(request): |
|
|||
20 | """ |
|
|||
21 | Get settings manager based on the request object. Currently only |
|
|||
22 | session-based manager is supported. In the future, cookie-based or |
|
|||
23 | database-based managers could be implemented. |
|
|||
24 | """ |
|
|||
25 | return SessionSettingsManager(request.session) |
|
|||
26 |
|
||||
27 |
|
||||
28 | class SettingsManager: |
|
21 | class SettingsManager: | |
29 | """ |
|
22 | """ | |
30 | Base settings manager class. get_setting and set_setting methods should |
|
23 | Base settings manager class. get_setting and set_setting methods should | |
@@ -77,9 +70,7 b' class SettingsManager:' | |||||
77 | tag_names = self.get_setting(SETTING_FAVORITE_TAGS) |
|
70 | tag_names = self.get_setting(SETTING_FAVORITE_TAGS) | |
78 | tags = [] |
|
71 | tags = [] | |
79 | if tag_names: |
|
72 | if tag_names: | |
80 |
|
|
73 | tags = Tag.objects.filter(name__in=tag_names) | |
81 | tag = get_object_or_404(Tag, name=tag_name) |
|
|||
82 | tags.append(tag) |
|
|||
83 | return tags |
|
74 | return tags | |
84 |
|
75 | |||
85 | def add_fav_tag(self, tag): |
|
76 | def add_fav_tag(self, tag): | |
@@ -145,3 +136,11 b' class SessionSettingsManager(SettingsMan' | |||||
145 | def set_setting(self, setting, value): |
|
136 | def set_setting(self, setting, value): | |
146 | self.session[setting] = value |
|
137 | self.session[setting] = value | |
147 |
|
138 | |||
|
139 | ||||
|
140 | def get_settings_manager(request) -> SettingsManager: | |||
|
141 | """ | |||
|
142 | Get settings manager based on the request object. Currently only | |||
|
143 | session-based manager is supported. In the future, cookie-based or | |||
|
144 | database-based managers could be implemented. | |||
|
145 | """ | |||
|
146 | return SessionSettingsManager(request.session) |
@@ -1,15 +1,27 b'' | |||||
1 | from django.contrib import admin |
|
1 | from django.contrib import admin | |
2 | from boards.models import Post, Tag, Ban, Thread, KeyPair |
|
2 | from boards.models import Post, Tag, Ban, Thread, KeyPair | |
|
3 | from django.utils.translation import ugettext_lazy as _ | |||
3 |
|
4 | |||
4 |
|
5 | |||
5 | @admin.register(Post) |
|
6 | @admin.register(Post) | |
6 | class PostAdmin(admin.ModelAdmin): |
|
7 | class PostAdmin(admin.ModelAdmin): | |
7 |
|
8 | |||
8 | list_display = ('id', 'title', 'text') |
|
9 | list_display = ('id', 'title', 'text') | |
9 |
list_filter = ('pub_time', |
|
10 | list_filter = ('pub_time',) | |
10 | search_fields = ('id', 'title', 'text') |
|
11 | search_fields = ('id', 'title', 'text') | |
11 | exclude = ('referenced_posts', 'refmap') |
|
12 | exclude = ('referenced_posts', 'refmap') | |
12 |
readonly_fields = ('poster_ip', 'thread |
|
13 | readonly_fields = ('poster_ip', 'threads', 'thread', 'images') | |
|
14 | ||||
|
15 | def ban_poster(self, request, queryset): | |||
|
16 | bans = 0 | |||
|
17 | for post in queryset: | |||
|
18 | poster_ip = post.poster_ip | |||
|
19 | ban, created = Ban.objects.get_or_create(ip=poster_ip) | |||
|
20 | if created: | |||
|
21 | bans += 1 | |||
|
22 | self.message_user(request, _('{} posters were banned').format(bans)) | |||
|
23 | ||||
|
24 | actions = ['ban_poster'] | |||
13 |
|
25 | |||
14 |
|
26 | |||
15 | @admin.register(Tag) |
|
27 | @admin.register(Tag) |
@@ -1,4 +1,6 b'' | |||||
1 | from boards.abstracts.settingsmanager import get_settings_manager |
|
1 | from boards.abstracts.settingsmanager import get_settings_manager, \ | |
|
2 | SETTING_USERNAME, SETTING_LAST_NOTIFICATION_ID | |||
|
3 | from boards.models.user import Notification | |||
2 |
|
4 | |||
3 | __author__ = 'neko259' |
|
5 | __author__ = 'neko259' | |
4 |
|
6 | |||
@@ -13,10 +15,34 b" CONTEXT_THEME = 'theme'" | |||||
13 | CONTEXT_PPD = 'posts_per_day' |
|
15 | CONTEXT_PPD = 'posts_per_day' | |
14 | CONTEXT_TAGS = 'tags' |
|
16 | CONTEXT_TAGS = 'tags' | |
15 | CONTEXT_USER = 'user' |
|
17 | CONTEXT_USER = 'user' | |
|
18 | CONTEXT_NEW_NOTIFICATIONS_COUNT = 'new_notifications_count' | |||
|
19 | CONTEXT_USERNAME = 'username' | |||
16 |
|
20 | |||
17 | PERMISSION_MODERATE = 'moderation' |
|
21 | PERMISSION_MODERATE = 'moderation' | |
18 |
|
22 | |||
19 |
|
23 | |||
|
24 | def get_notifications(context, request): | |||
|
25 | settings_manager = get_settings_manager(request) | |||
|
26 | username = settings_manager.get_setting(SETTING_USERNAME) | |||
|
27 | new_notifications_count = 0 | |||
|
28 | if username is not None and len(username) > 0: | |||
|
29 | last_notification_id = settings_manager.get_setting( | |||
|
30 | SETTING_LAST_NOTIFICATION_ID) | |||
|
31 | ||||
|
32 | new_notifications_count = Notification.objects.get_notification_posts( | |||
|
33 | username=username, last=last_notification_id).count() | |||
|
34 | context[CONTEXT_NEW_NOTIFICATIONS_COUNT] = new_notifications_count | |||
|
35 | context[CONTEXT_USERNAME] = username | |||
|
36 | ||||
|
37 | ||||
|
38 | def get_moderator_permissions(context, request): | |||
|
39 | try: | |||
|
40 | moderate = request.user.has_perm(PERMISSION_MODERATE) | |||
|
41 | except AttributeError: | |||
|
42 | moderate = False | |||
|
43 | context[CONTEXT_MODERATOR] = moderate | |||
|
44 | ||||
|
45 | ||||
20 | def user_and_ui_processor(request): |
|
46 | def user_and_ui_processor(request): | |
21 | context = dict() |
|
47 | context = dict() | |
22 |
|
48 | |||
@@ -29,13 +55,11 b' def user_and_ui_processor(request):' | |||||
29 | context[CONTEXT_THEME_CSS] = 'css/' + theme + '/base_page.css' |
|
55 | context[CONTEXT_THEME_CSS] = 'css/' + theme + '/base_page.css' | |
30 |
|
56 | |||
31 | # This shows the moderator panel |
|
57 | # This shows the moderator panel | |
32 | try: |
|
58 | get_moderator_permissions(context, request) | |
33 | moderate = request.user.has_perm(PERMISSION_MODERATE) |
|
|||
34 | except AttributeError: |
|
|||
35 | moderate = False |
|
|||
36 | context[CONTEXT_MODERATOR] = moderate |
|
|||
37 |
|
59 | |||
38 | context[CONTEXT_VERSION] = settings.VERSION |
|
60 | context[CONTEXT_VERSION] = settings.VERSION | |
39 | context[CONTEXT_SITE_NAME] = settings.SITE_NAME |
|
61 | context[CONTEXT_SITE_NAME] = settings.SITE_NAME | |
40 |
|
62 | |||
|
63 | get_notifications(context, request) | |||
|
64 | ||||
41 | return context |
|
65 | return context |
@@ -1,29 +1,38 b'' | |||||
1 | import re |
|
1 | import re | |
2 | import time |
|
2 | import time | |
3 | import hashlib |
|
|||
4 |
|
3 | |||
5 | from django import forms |
|
4 | from django import forms | |
|
5 | from django.core.files.uploadedfile import SimpleUploadedFile | |||
6 | from django.forms.util import ErrorList |
|
6 | from django.forms.util import ErrorList | |
7 | from django.utils.translation import ugettext_lazy as _ |
|
7 | from django.utils.translation import ugettext_lazy as _ | |
|
8 | import requests | |||
8 |
|
9 | |||
9 | from boards.mdx_neboard import formatters |
|
10 | from boards.mdx_neboard import formatters | |
10 | from boards.models.post import TITLE_MAX_LENGTH |
|
11 | from boards.models.post import TITLE_MAX_LENGTH | |
11 |
from boards.models import |
|
12 | from boards.models import Tag | |
12 | from neboard import settings |
|
13 | from neboard import settings | |
13 | from boards import utils |
|
|||
14 | import boards.settings as board_settings |
|
14 | import boards.settings as board_settings | |
15 |
|
15 | |||
|
16 | ||||
|
17 | CONTENT_TYPE_IMAGE = ( | |||
|
18 | 'image/jpeg', | |||
|
19 | 'image/png', | |||
|
20 | 'image/gif', | |||
|
21 | 'image/bmp', | |||
|
22 | ) | |||
|
23 | ||||
|
24 | REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE) | |||
|
25 | ||||
16 | VETERAN_POSTING_DELAY = 5 |
|
26 | VETERAN_POSTING_DELAY = 5 | |
17 |
|
27 | |||
18 | ATTRIBUTE_PLACEHOLDER = 'placeholder' |
|
28 | ATTRIBUTE_PLACEHOLDER = 'placeholder' | |
|
29 | ATTRIBUTE_ROWS = 'rows' | |||
19 |
|
30 | |||
20 | LAST_POST_TIME = 'last_post_time' |
|
31 | LAST_POST_TIME = 'last_post_time' | |
21 | LAST_LOGIN_TIME = 'last_login_time' |
|
32 | LAST_LOGIN_TIME = 'last_login_time' | |
22 |
TEXT_PLACEHOLDER = _(' |
|
33 | TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.') | |
23 | TAGS_PLACEHOLDER = _('tag1 several_words_tag') |
|
34 | TAGS_PLACEHOLDER = _('tag1 several_words_tag') | |
24 |
|
35 | |||
25 | ERROR_IMAGE_DUPLICATE = _('Such image was already posted') |
|
|||
26 |
|
||||
27 | LABEL_TITLE = _('Title') |
|
36 | LABEL_TITLE = _('Title') | |
28 | LABEL_TEXT = _('Text') |
|
37 | LABEL_TEXT = _('Text') | |
29 | LABEL_TAG = _('Tag') |
|
38 | LABEL_TAG = _('Tag') | |
@@ -31,10 +40,19 b" LABEL_SEARCH = _('Search')" | |||||
31 |
|
40 | |||
32 | TAG_MAX_LENGTH = 20 |
|
41 | TAG_MAX_LENGTH = 20 | |
33 |
|
42 | |||
34 | REGEX_TAG = r'^[\w\d]+$' |
|
43 | IMAGE_DOWNLOAD_CHUNK_BYTES = 100000 | |
|
44 | ||||
|
45 | HTTP_RESULT_OK = 200 | |||
|
46 | ||||
|
47 | TEXTAREA_ROWS = 4 | |||
35 |
|
48 | |||
36 |
|
49 | |||
37 | class FormatPanel(forms.Textarea): |
|
50 | class FormatPanel(forms.Textarea): | |
|
51 | """ | |||
|
52 | Panel for text formatting. Consists of buttons to add different tags to the | |||
|
53 | form text area. | |||
|
54 | """ | |||
|
55 | ||||
38 | def render(self, name, value, attrs=None): |
|
56 | def render(self, name, value, attrs=None): | |
39 | output = '<div id="mark-panel">' |
|
57 | output = '<div id="mark-panel">' | |
40 | for formatter in formatters: |
|
58 | for formatter in formatters: | |
@@ -59,6 +77,9 b' class PlainErrorList(ErrorList):' | |||||
59 |
|
77 | |||
60 |
|
78 | |||
61 | class NeboardForm(forms.Form): |
|
79 | class NeboardForm(forms.Form): | |
|
80 | """ | |||
|
81 | Form with neboard-specific formatting. | |||
|
82 | """ | |||
62 |
|
83 | |||
63 | def as_div(self): |
|
84 | def as_div(self): | |
64 | """ |
|
85 | """ | |
@@ -102,11 +123,18 b' class PostForm(NeboardForm):' | |||||
102 | title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False, |
|
123 | title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False, | |
103 | label=LABEL_TITLE) |
|
124 | label=LABEL_TITLE) | |
104 | text = forms.CharField( |
|
125 | text = forms.CharField( | |
105 |
widget=FormatPanel(attrs={ |
|
126 | widget=FormatPanel(attrs={ | |
|
127 | ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER, | |||
|
128 | ATTRIBUTE_ROWS: TEXTAREA_ROWS, | |||
|
129 | }), | |||
106 | required=False, label=LABEL_TEXT) |
|
130 | required=False, label=LABEL_TEXT) | |
107 | image = forms.ImageField(required=False, label=_('Image'), |
|
131 | image = forms.ImageField(required=False, label=_('Image'), | |
108 | widget=forms.ClearableFileInput( |
|
132 | widget=forms.ClearableFileInput( | |
109 | attrs={'accept': 'image/*'})) |
|
133 | attrs={'accept': 'image/*'})) | |
|
134 | image_url = forms.CharField(required=False, label=_('Image URL'), | |||
|
135 | widget=forms.TextInput( | |||
|
136 | attrs={ATTRIBUTE_PLACEHOLDER: | |||
|
137 | 'http://example.com/image.png'})) | |||
110 |
|
138 | |||
111 | # This field is for spam prevention only |
|
139 | # This field is for spam prevention only | |
112 | email = forms.CharField(max_length=100, required=False, label=_('e-mail'), |
|
140 | email = forms.CharField(max_length=100, required=False, label=_('e-mail'), | |
@@ -137,18 +165,22 b' class PostForm(NeboardForm):' | |||||
137 |
|
165 | |||
138 | def clean_image(self): |
|
166 | def clean_image(self): | |
139 | image = self.cleaned_data['image'] |
|
167 | image = self.cleaned_data['image'] | |
140 | if image: |
|
168 | ||
141 | if image.size > board_settings.MAX_IMAGE_SIZE: |
|
169 | self._validate_image(image) | |
142 | raise forms.ValidationError( |
|
170 | ||
143 | _('Image must be less than %s bytes') |
|
171 | return image | |
144 | % str(board_settings.MAX_IMAGE_SIZE)) |
|
172 | ||
|
173 | def clean_image_url(self): | |||
|
174 | url = self.cleaned_data['image_url'] | |||
145 |
|
175 | |||
146 | md5 = hashlib.md5() |
|
176 | image = None | |
147 | for chunk in image.chunks(): |
|
177 | if url: | |
148 | md5.update(chunk) |
|
178 | image = self._get_image_from_url(url) | |
149 | image_hash = md5.hexdigest() |
|
179 | ||
150 | if PostImage.objects.filter(hash=image_hash).exists(): |
|
180 | if not image: | |
151 |
raise forms.ValidationError( |
|
181 | raise forms.ValidationError(_('Invalid URL')) | |
|
182 | ||||
|
183 | self._validate_image(image) | |||
152 |
|
184 | |||
153 | return image |
|
185 | return image | |
154 |
|
186 | |||
@@ -170,14 +202,29 b' class PostForm(NeboardForm):' | |||||
170 |
|
202 | |||
171 | return cleaned_data |
|
203 | return cleaned_data | |
172 |
|
204 | |||
|
205 | def get_image(self): | |||
|
206 | """ | |||
|
207 | Gets image from file or URL. | |||
|
208 | """ | |||
|
209 | ||||
|
210 | image = self.cleaned_data['image'] | |||
|
211 | return image if image else self.cleaned_data['image_url'] | |||
|
212 | ||||
173 | def _clean_text_image(self): |
|
213 | def _clean_text_image(self): | |
174 | text = self.cleaned_data.get('text') |
|
214 | text = self.cleaned_data.get('text') | |
175 |
image = self. |
|
215 | image = self.get_image() | |
176 |
|
216 | |||
177 | if (not text) and (not image): |
|
217 | if (not text) and (not image): | |
178 | error_message = _('Either text or image must be entered.') |
|
218 | error_message = _('Either text or image must be entered.') | |
179 | self._errors['text'] = self.error_class([error_message]) |
|
219 | self._errors['text'] = self.error_class([error_message]) | |
180 |
|
220 | |||
|
221 | def _validate_image(self, image): | |||
|
222 | if image: | |||
|
223 | if image.size > board_settings.MAX_IMAGE_SIZE: | |||
|
224 | raise forms.ValidationError( | |||
|
225 | _('Image must be less than %s bytes') | |||
|
226 | % str(board_settings.MAX_IMAGE_SIZE)) | |||
|
227 | ||||
181 | def _validate_posting_speed(self): |
|
228 | def _validate_posting_speed(self): | |
182 | can_post = True |
|
229 | can_post = True | |
183 |
|
230 | |||
@@ -200,11 +247,56 b' class PostForm(NeboardForm):' | |||||
200 | if can_post: |
|
247 | if can_post: | |
201 | self.session[LAST_POST_TIME] = time.time() |
|
248 | self.session[LAST_POST_TIME] = time.time() | |
202 |
|
249 | |||
|
250 | def _get_image_from_url(self, url: str) -> SimpleUploadedFile: | |||
|
251 | """ | |||
|
252 | Gets an image file from URL. | |||
|
253 | """ | |||
|
254 | ||||
|
255 | img_temp = None | |||
|
256 | ||||
|
257 | try: | |||
|
258 | # Verify content headers | |||
|
259 | response_head = requests.head(url, verify=False) | |||
|
260 | content_type = response_head.headers['content-type'].split(';')[0] | |||
|
261 | if content_type in CONTENT_TYPE_IMAGE: | |||
|
262 | length_header = response_head.headers.get('content-length') | |||
|
263 | if length_header: | |||
|
264 | length = int(length_header) | |||
|
265 | if length > board_settings.MAX_IMAGE_SIZE: | |||
|
266 | raise forms.ValidationError( | |||
|
267 | _('Image must be less than %s bytes') | |||
|
268 | % str(board_settings.MAX_IMAGE_SIZE)) | |||
|
269 | ||||
|
270 | # Get the actual content into memory | |||
|
271 | response = requests.get(url, verify=False, stream=True) | |||
|
272 | ||||
|
273 | # Download image, stop if the size exceeds limit | |||
|
274 | size = 0 | |||
|
275 | content = b'' | |||
|
276 | for chunk in response.iter_content(IMAGE_DOWNLOAD_CHUNK_BYTES): | |||
|
277 | size += len(chunk) | |||
|
278 | if size > board_settings.MAX_IMAGE_SIZE: | |||
|
279 | # TODO Dedup this code into a method | |||
|
280 | raise forms.ValidationError( | |||
|
281 | _('Image must be less than %s bytes') | |||
|
282 | % str(board_settings.MAX_IMAGE_SIZE)) | |||
|
283 | content += chunk | |||
|
284 | ||||
|
285 | if response.status_code == HTTP_RESULT_OK and content: | |||
|
286 | # Set a dummy file name that will be replaced | |||
|
287 | # anyway, just keep the valid extension | |||
|
288 | filename = 'image.' + content_type.split('/')[1] | |||
|
289 | img_temp = SimpleUploadedFile(filename, content, | |||
|
290 | content_type) | |||
|
291 | except Exception: | |||
|
292 | # Just return no image | |||
|
293 | pass | |||
|
294 | ||||
|
295 | return img_temp | |||
|
296 | ||||
203 |
|
297 | |||
204 | class ThreadForm(PostForm): |
|
298 | class ThreadForm(PostForm): | |
205 |
|
299 | |||
206 | regex_tags = re.compile(r'^[\w\s\d]+$', re.UNICODE) |
|
|||
207 |
|
||||
208 | tags = forms.CharField( |
|
300 | tags = forms.CharField( | |
209 | widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}), |
|
301 | widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}), | |
210 | max_length=100, label=_('Tags'), required=True) |
|
302 | max_length=100, label=_('Tags'), required=True) | |
@@ -212,17 +304,17 b' class ThreadForm(PostForm):' | |||||
212 | def clean_tags(self): |
|
304 | def clean_tags(self): | |
213 | tags = self.cleaned_data['tags'].strip() |
|
305 | tags = self.cleaned_data['tags'].strip() | |
214 |
|
306 | |||
215 |
if not tags or not |
|
307 | if not tags or not REGEX_TAGS.match(tags): | |
216 | raise forms.ValidationError( |
|
308 | raise forms.ValidationError( | |
217 | _('Inappropriate characters in tags.')) |
|
309 | _('Inappropriate characters in tags.')) | |
218 |
|
310 | |||
219 | tag_models = [] |
|
|||
220 | required_tag_exists = False |
|
311 | required_tag_exists = False | |
221 | for tag in tags.split(): |
|
312 | for tag in tags.split(): | |
222 | tag_model = Tag.objects.filter(name=tag.strip().lower(), |
|
313 | tag_model = Tag.objects.filter(name=tag.strip().lower(), | |
223 | required=True) |
|
314 | required=True) | |
224 | if tag_model.exists(): |
|
315 | if tag_model.exists(): | |
225 | required_tag_exists = True |
|
316 | required_tag_exists = True | |
|
317 | break | |||
226 |
|
318 | |||
227 | if not required_tag_exists: |
|
319 | if not required_tag_exists: | |
228 | raise forms.ValidationError(_('Need at least 1 required tag.')) |
|
320 | raise forms.ValidationError(_('Need at least 1 required tag.')) | |
@@ -239,67 +331,16 b' class SettingsForm(NeboardForm):' | |||||
239 |
|
331 | |||
240 | theme = forms.ChoiceField(choices=settings.THEMES, |
|
332 | theme = forms.ChoiceField(choices=settings.THEMES, | |
241 | label=_('Theme')) |
|
333 | label=_('Theme')) | |
242 |
|
334 | username = forms.CharField(label=_('User name'), required=False) | ||
243 |
|
||||
244 | class AddTagForm(NeboardForm): |
|
|||
245 |
|
335 | |||
246 | tag = forms.CharField(max_length=TAG_MAX_LENGTH, label=LABEL_TAG) |
|
336 | def clean_username(self): | |
247 | method = forms.CharField(widget=forms.HiddenInput(), initial='add_tag') |
|
337 | username = self.cleaned_data['username'] | |
248 |
|
||||
249 | def clean_tag(self): |
|
|||
250 | tag = self.cleaned_data['tag'] |
|
|||
251 |
|
338 | |||
252 | regex_tag = re.compile(REGEX_TAG, re.UNICODE) |
|
339 | if username and not REGEX_TAGS.match(username): | |
253 | if not regex_tag.match(tag): |
|
340 | raise forms.ValidationError(_('Inappropriate characters.')) | |
254 | raise forms.ValidationError(_('Inappropriate characters in tags.')) |
|
|||
255 |
|
341 | |||
256 |
return |
|
342 | return username | |
257 |
|
||||
258 | def clean(self): |
|
|||
259 | cleaned_data = super(AddTagForm, self).clean() |
|
|||
260 |
|
||||
261 | return cleaned_data |
|
|||
262 |
|
343 | |||
263 |
|
344 | |||
264 | class SearchForm(NeboardForm): |
|
345 | class SearchForm(NeboardForm): | |
265 | query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False) |
|
346 | query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False) | |
266 |
|
||||
267 |
|
||||
268 | class LoginForm(NeboardForm): |
|
|||
269 |
|
||||
270 | password = forms.CharField() |
|
|||
271 |
|
||||
272 | session = None |
|
|||
273 |
|
||||
274 | def clean_password(self): |
|
|||
275 | password = self.cleaned_data['password'] |
|
|||
276 | if board_settings.MASTER_PASSWORD != password: |
|
|||
277 | raise forms.ValidationError(_('Invalid master password')) |
|
|||
278 |
|
||||
279 | return password |
|
|||
280 |
|
||||
281 | def _validate_login_speed(self): |
|
|||
282 | can_post = True |
|
|||
283 |
|
||||
284 | if LAST_LOGIN_TIME in self.session: |
|
|||
285 | now = time.time() |
|
|||
286 | last_login_time = self.session[LAST_LOGIN_TIME] |
|
|||
287 |
|
||||
288 | current_delay = int(now - last_login_time) |
|
|||
289 |
|
||||
290 | if current_delay < board_settings.LOGIN_TIMEOUT: |
|
|||
291 | error_message = _('Wait %s minutes after last login') % str( |
|
|||
292 | (board_settings.LOGIN_TIMEOUT - current_delay) / 60) |
|
|||
293 | self._errors['password'] = self.error_class([error_message]) |
|
|||
294 |
|
||||
295 | can_post = False |
|
|||
296 |
|
||||
297 | if can_post: |
|
|||
298 | self.session[LAST_LOGIN_TIME] = time.time() |
|
|||
299 |
|
||||
300 | def clean(self): |
|
|||
301 | self._validate_login_speed() |
|
|||
302 |
|
||||
303 | cleaned_data = super(LoginForm, self).clean() |
|
|||
304 |
|
||||
305 | return cleaned_data |
|
1 | NO CONTENT: modified file, binary diff hidden |
|
NO CONTENT: modified file, binary diff hidden |
@@ -7,7 +7,7 b' msgid ""' | |||||
7 | msgstr "" |
|
7 | msgstr "" | |
8 | "Project-Id-Version: PACKAGE VERSION\n" |
|
8 | "Project-Id-Version: PACKAGE VERSION\n" | |
9 | "Report-Msgid-Bugs-To: \n" |
|
9 | "Report-Msgid-Bugs-To: \n" | |
10 |
"POT-Creation-Date: 2015-0 |
|
10 | "POT-Creation-Date: 2015-03-03 23:49+0200\n" | |
11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" |
|
11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" | |
12 | "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" |
|
12 | "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" | |
13 | "Language-Team: LANGUAGE <LL@li.org>\n" |
|
13 | "Language-Team: LANGUAGE <LL@li.org>\n" | |
@@ -18,6 +18,10 b' msgstr ""' | |||||
18 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" |
|
18 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" | |
19 | "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" |
|
19 | "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" | |
20 |
|
20 | |||
|
21 | #: admin.py:22 | |||
|
22 | msgid "{} posters were banned" | |||
|
23 | msgstr "" | |||
|
24 | ||||
21 | #: authors.py:9 |
|
25 | #: authors.py:9 | |
22 | msgid "author" |
|
26 | msgid "author" | |
23 | msgstr "автор" |
|
27 | msgstr "автор" | |
@@ -34,92 +38,95 b' msgstr "\xd1\x80\xd0\xb0\xd0\xb7\xd1\x80\xd0\xb0\xd0\xb1\xd0\xbe\xd1\x82\xd1\x87\xd0\xb8\xd0\xba javascript"' | |||||
34 | msgid "designer" |
|
38 | msgid "designer" | |
35 | msgstr "дизайнер" |
|
39 | msgstr "дизайнер" | |
36 |
|
40 | |||
37 |
#: forms.py: |
|
41 | #: forms.py:33 | |
38 | msgid "Type message here. Use formatting panel for more advanced usage." |
|
42 | msgid "Type message here. Use formatting panel for more advanced usage." | |
39 | msgstr "" |
|
43 | msgstr "" | |
40 | "Вводите сообщение сюда. Используйте панель для более сложного форматирования." |
|
44 | "Вводите сообщение сюда. Используйте панель для более сложного форматирования." | |
41 |
|
45 | |||
42 |
#: forms.py: |
|
46 | #: forms.py:34 | |
43 | msgid "tag1 several_words_tag" |
|
47 | msgid "tag1 several_words_tag" | |
44 | msgstr "метка1 метка_из_нескольких_слов" |
|
48 | msgstr "метка1 метка_из_нескольких_слов" | |
45 |
|
49 | |||
46 |
#: forms.py: |
|
50 | #: forms.py:36 | |
47 | msgid "Such image was already posted" |
|
|||
48 | msgstr "Такое изображение уже было загружено" |
|
|||
49 |
|
||||
50 | #: forms.py:27 |
|
|||
51 | msgid "Title" |
|
51 | msgid "Title" | |
52 | msgstr "Заголовок" |
|
52 | msgstr "Заголовок" | |
53 |
|
53 | |||
54 |
#: forms.py: |
|
54 | #: forms.py:37 | |
55 | msgid "Text" |
|
55 | msgid "Text" | |
56 | msgstr "Текст" |
|
56 | msgstr "Текст" | |
57 |
|
57 | |||
58 |
#: forms.py: |
|
58 | #: forms.py:38 | |
59 | msgid "Tag" |
|
59 | msgid "Tag" | |
60 | msgstr "Метка" |
|
60 | msgstr "Метка" | |
61 |
|
61 | |||
62 |
#: forms.py:3 |
|
62 | #: forms.py:39 templates/boards/base.html:38 templates/search/search.html:9 | |
63 | #: templates/search/search.html.py:13 |
|
63 | #: templates/search/search.html.py:13 | |
64 | msgid "Search" |
|
64 | msgid "Search" | |
65 | msgstr "Поиск" |
|
65 | msgstr "Поиск" | |
66 |
|
66 | |||
67 |
#: forms.py:1 |
|
67 | #: forms.py:131 | |
68 | msgid "Image" |
|
68 | msgid "Image" | |
69 | msgstr "Изображение" |
|
69 | msgstr "Изображение" | |
70 |
|
70 | |||
71 |
#: forms.py:1 |
|
71 | #: forms.py:134 | |
|
72 | msgid "Image URL" | |||
|
73 | msgstr "URL изображения" | |||
|
74 | ||||
|
75 | #: forms.py:140 | |||
72 | msgid "e-mail" |
|
76 | msgid "e-mail" | |
73 | msgstr "" |
|
77 | msgstr "" | |
74 |
|
78 | |||
75 |
#: forms.py:1 |
|
79 | #: forms.py:151 | |
76 | #, python-format |
|
80 | #, python-format | |
77 | msgid "Title must have less than %s characters" |
|
81 | msgid "Title must have less than %s characters" | |
78 | msgstr "Заголовок должен иметь меньше %s символов" |
|
82 | msgstr "Заголовок должен иметь меньше %s символов" | |
79 |
|
83 | |||
80 |
#: forms.py:1 |
|
84 | #: forms.py:160 | |
81 | #, python-format |
|
85 | #, python-format | |
82 | msgid "Text must have less than %s characters" |
|
86 | msgid "Text must have less than %s characters" | |
83 | msgstr "Текст должен быть короче %s символов" |
|
87 | msgstr "Текст должен быть короче %s символов" | |
84 |
|
88 | |||
85 |
#: forms.py:1 |
|
89 | #: forms.py:181 | |
|
90 | msgid "Invalid URL" | |||
|
91 | msgstr "Неверный URL" | |||
|
92 | ||||
|
93 | #: forms.py:218 | |||
|
94 | msgid "Either text or image must be entered." | |||
|
95 | msgstr "Текст или картинка должны быть введены." | |||
|
96 | ||||
|
97 | #: forms.py:225 forms.py:267 forms.py:281 | |||
86 | #, python-format |
|
98 | #, python-format | |
87 | msgid "Image must be less than %s bytes" |
|
99 | msgid "Image must be less than %s bytes" | |
88 | msgstr "Изображение должно быть менее %s байт" |
|
100 | msgstr "Изображение должно быть менее %s байт" | |
89 |
|
101 | |||
90 |
#: forms.py:1 |
|
102 | #: forms.py:241 | |
91 | msgid "Either text or image must be entered." |
|
|||
92 | msgstr "Текст или картинка должны быть введены." |
|
|||
93 |
|
||||
94 | #: forms.py:194 |
|
|||
95 | #, python-format |
|
103 | #, python-format | |
96 | msgid "Wait %s seconds after last posting" |
|
104 | msgid "Wait %s seconds after last posting" | |
97 | msgstr "Подождите %s секунд после последнего постинга" |
|
105 | msgstr "Подождите %s секунд после последнего постинга" | |
98 |
|
106 | |||
99 |
#: forms.py: |
|
107 | #: forms.py:302 templates/boards/rss/post.html:10 templates/boards/tags.html:7 | |
100 | msgid "Tags" |
|
108 | msgid "Tags" | |
101 | msgstr "Метки" |
|
109 | msgstr "Метки" | |
102 |
|
110 | |||
103 | #: forms.py:217 forms.py:254 |
|
111 | #: forms.py:309 | |
104 | msgid "Inappropriate characters in tags." |
|
112 | msgid "Inappropriate characters in tags." | |
105 | msgstr "Недопустимые символы в метках." |
|
113 | msgstr "Недопустимые символы в метках." | |
106 |
|
114 | |||
107 |
#: forms.py: |
|
115 | #: forms.py:320 | |
108 | msgid "Need at least 1 required tag." |
|
116 | msgid "Need at least 1 required tag." | |
109 | msgstr "Нужна хотя бы 1 обязательная метка." |
|
117 | msgstr "Нужна хотя бы 1 обязательная метка." | |
110 |
|
118 | |||
111 |
#: forms.py: |
|
119 | #: forms.py:333 | |
112 | msgid "Theme" |
|
120 | msgid "Theme" | |
113 | msgstr "Тема" |
|
121 | msgstr "Тема" | |
114 |
|
122 | |||
115 |
#: forms.py: |
|
123 | #: forms.py:334 | |
116 | msgid "Invalid master password" |
|
124 | msgid "User name" | |
117 | msgstr "Неверный мастер-пароль" |
|
125 | msgstr "Имя пользователя" | |
118 |
|
126 | |||
119 |
#: forms.py: |
|
127 | #: forms.py:340 | |
120 | #, python-format |
|
128 | msgid "Inappropriate characters." | |
121 | msgid "Wait %s minutes after last login" |
|
129 | msgstr "Недопустимые символы." | |
122 | msgstr "Подождите %s минут после последнего входа" |
|
|||
123 |
|
130 | |||
124 | #: templates/boards/404.html:6 |
|
131 | #: templates/boards/404.html:6 | |
125 | msgid "Not found" |
|
132 | msgid "Not found" | |
@@ -157,20 +164,28 b' msgstr "\xd0\x92\xd1\x81\xd0\xb5 \xd1\x82\xd0\xb5\xd0\xbc\xd1\x8b"' | |||||
157 | msgid "Tag management" |
|
164 | msgid "Tag management" | |
158 | msgstr "Управление метками" |
|
165 | msgstr "Управление метками" | |
159 |
|
166 | |||
160 |
#: templates/boards/base.html: |
|
167 | #: templates/boards/base.html:41 templates/boards/notifications.html:7 | |
|
168 | msgid "Notifications" | |||
|
169 | msgstr "Уведомления" | |||
|
170 | ||||
|
171 | #: templates/boards/base.html:41 | |||
|
172 | msgid "notifications" | |||
|
173 | msgstr "уведомлений" | |||
|
174 | ||||
|
175 | #: templates/boards/base.html:44 templates/boards/settings.html:7 | |||
161 | msgid "Settings" |
|
176 | msgid "Settings" | |
162 | msgstr "Настройки" |
|
177 | msgstr "Настройки" | |
163 |
|
178 | |||
164 |
#: templates/boards/base.html:5 |
|
179 | #: templates/boards/base.html:57 | |
165 | msgid "Admin" |
|
180 | msgid "Admin" | |
166 | msgstr "" |
|
181 | msgstr "Администрирование" | |
167 |
|
182 | |||
168 |
#: templates/boards/base.html:5 |
|
183 | #: templates/boards/base.html:59 | |
169 | #, python-format |
|
184 | #, python-format | |
170 | msgid "Speed: %(ppd)s posts per day" |
|
185 | msgid "Speed: %(ppd)s posts per day" | |
171 | msgstr "Скорость: %(ppd)s сообщений в день" |
|
186 | msgstr "Скорость: %(ppd)s сообщений в день" | |
172 |
|
187 | |||
173 |
#: templates/boards/base.html: |
|
188 | #: templates/boards/base.html:61 | |
174 | msgid "Up" |
|
189 | msgid "Up" | |
175 | msgstr "Вверх" |
|
190 | msgstr "Вверх" | |
176 |
|
191 | |||
@@ -182,96 +197,95 b' msgstr "\xd0\x92\xd1\x85\xd0\xbe\xd0\xb4"' | |||||
182 | msgid "Insert your user id above" |
|
197 | msgid "Insert your user id above" | |
183 | msgstr "Вставьте свой ID пользователя выше" |
|
198 | msgstr "Вставьте свой ID пользователя выше" | |
184 |
|
199 | |||
|
200 | #: templates/boards/notifications.html:16 | |||
|
201 | #: templates/boards/posting_general.html:79 templates/search/search.html:22 | |||
|
202 | msgid "Previous page" | |||
|
203 | msgstr "Предыдущая страница" | |||
|
204 | ||||
|
205 | #: templates/boards/notifications.html:26 | |||
|
206 | #: templates/boards/posting_general.html:117 templates/search/search.html:33 | |||
|
207 | msgid "Next page" | |||
|
208 | msgstr "Следующая страница" | |||
|
209 | ||||
185 | #: templates/boards/post.html:19 templates/boards/staticpages/help.html:17 |
|
210 | #: templates/boards/post.html:19 templates/boards/staticpages/help.html:17 | |
186 | msgid "Quote" |
|
211 | msgid "Quote" | |
187 | msgstr "Цитата" |
|
212 | msgstr "Цитата" | |
188 |
|
213 | |||
189 |
#: templates/boards/post.html: |
|
214 | #: templates/boards/post.html:35 | |
190 | msgid "Open" |
|
215 | msgid "Open" | |
191 | msgstr "Открыть" |
|
216 | msgstr "Открыть" | |
192 |
|
217 | |||
193 |
#: templates/boards/post.html: |
|
218 | #: templates/boards/post.html:37 | |
194 | msgid "Reply" |
|
219 | msgid "Reply" | |
195 | msgstr "Ответ" |
|
220 | msgstr "Ответ" | |
196 |
|
221 | |||
197 |
#: templates/boards/post.html:3 |
|
222 | #: templates/boards/post.html:43 | |
198 | msgid "Edit" |
|
223 | msgid "Edit" | |
199 | msgstr "Изменить" |
|
224 | msgstr "Изменить" | |
200 |
|
225 | |||
201 |
#: templates/boards/post.html: |
|
226 | #: templates/boards/post.html:45 | |
202 | msgid "Edit thread" |
|
227 | msgid "Edit thread" | |
203 | msgstr "Изменить тему" |
|
228 | msgstr "Изменить тему" | |
204 |
|
229 | |||
205 |
#: templates/boards/post.html:7 |
|
230 | #: templates/boards/post.html:75 | |
206 | msgid "Replies" |
|
231 | msgid "Replies" | |
207 | msgstr "Ответы" |
|
232 | msgstr "Ответы" | |
208 |
|
233 | |||
209 |
#: templates/boards/post.html: |
|
234 | #: templates/boards/post.html:86 templates/boards/thread.html:86 | |
210 | #: templates/boards/thread_gallery.html:59 |
|
235 | #: templates/boards/thread_gallery.html:59 | |
211 | msgid "messages" |
|
236 | msgid "messages" | |
212 | msgstr "сообщений" |
|
237 | msgstr "сообщений" | |
213 |
|
238 | |||
214 |
#: templates/boards/post.html:8 |
|
239 | #: templates/boards/post.html:87 templates/boards/thread.html:87 | |
215 | #: templates/boards/thread_gallery.html:60 |
|
240 | #: templates/boards/thread_gallery.html:60 | |
216 | msgid "images" |
|
241 | msgid "images" | |
217 | msgstr "изображений" |
|
242 | msgstr "изображений" | |
218 |
|
243 | |||
219 |
#: templates/boards/post |
|
244 | #: templates/boards/posting_general.html:52 | |
220 | msgid "Tags:" |
|
|||
221 | msgstr "Метки:" |
|
|||
222 |
|
||||
223 | #: templates/boards/post_admin.html:30 |
|
|||
224 | msgid "Add tag" |
|
|||
225 | msgstr "Добавить метку" |
|
|||
226 |
|
||||
227 | #: templates/boards/posting_general.html:56 |
|
|||
228 | msgid "Show tag" |
|
245 | msgid "Show tag" | |
229 | msgstr "Показывать метку" |
|
246 | msgstr "Показывать метку" | |
230 |
|
247 | |||
231 |
#: templates/boards/posting_general.html:6 |
|
248 | #: templates/boards/posting_general.html:56 | |
232 | msgid "Hide tag" |
|
249 | msgid "Hide tag" | |
233 | msgstr "Скрывать метку" |
|
250 | msgstr "Скрывать метку" | |
234 |
|
251 | |||
235 |
#: templates/boards/posting_general.html:6 |
|
252 | #: templates/boards/posting_general.html:63 | |
236 | msgid "Edit tag" |
|
253 | msgid "Edit tag" | |
237 | msgstr "Изменить метку" |
|
254 | msgstr "Изменить метку" | |
238 |
|
255 | |||
239 |
#: templates/boards/posting_general.html: |
|
256 | #: templates/boards/posting_general.html:66 | |
240 | msgid "Previous page" |
|
257 | #, python-format | |
241 | msgstr "Предыдущая страница" |
|
258 | msgid "This tag has %(thread_count)s threads and %(post_count)s posts." | |
|
259 | msgstr "С этой меткой есть %(thread_count)s тем и %(post_count)s сообщений." | |||
242 |
|
260 | |||
243 |
#: templates/boards/posting_general.html:9 |
|
261 | #: templates/boards/posting_general.html:93 | |
244 | #, python-format |
|
262 | #, python-format | |
245 | msgid "Skipped %(count)s replies. Open thread to see all replies." |
|
263 | msgid "Skipped %(count)s replies. Open thread to see all replies." | |
246 | msgstr "Пропущено %(count)s ответов. Откройте тред, чтобы увидеть все ответы." |
|
264 | msgstr "Пропущено %(count)s ответов. Откройте тред, чтобы увидеть все ответы." | |
247 |
|
265 | |||
248 |
#: templates/boards/posting_general.html:12 |
|
266 | #: templates/boards/posting_general.html:122 | |
249 | msgid "Next page" |
|
|||
250 | msgstr "Следующая страница" |
|
|||
251 |
|
||||
252 | #: templates/boards/posting_general.html:129 |
|
|||
253 | msgid "No threads exist. Create the first one!" |
|
267 | msgid "No threads exist. Create the first one!" | |
254 | msgstr "Нет тем. Создайте первую!" |
|
268 | msgstr "Нет тем. Создайте первую!" | |
255 |
|
269 | |||
256 |
#: templates/boards/posting_general.html:1 |
|
270 | #: templates/boards/posting_general.html:128 | |
257 | msgid "Create new thread" |
|
271 | msgid "Create new thread" | |
258 | msgstr "Создать новую тему" |
|
272 | msgstr "Создать новую тему" | |
259 |
|
273 | |||
260 |
#: templates/boards/posting_general.html:1 |
|
274 | #: templates/boards/posting_general.html:133 templates/boards/preview.html:16 | |
261 |
#: templates/boards/thread.html:5 |
|
275 | #: templates/boards/thread.html:53 | |
262 | msgid "Post" |
|
276 | msgid "Post" | |
263 | msgstr "Отправить" |
|
277 | msgstr "Отправить" | |
264 |
|
278 | |||
265 |
#: templates/boards/posting_general.html:1 |
|
279 | #: templates/boards/posting_general.html:139 | |
266 | msgid "Tags must be delimited by spaces. Text or image is required." |
|
280 | msgid "Tags must be delimited by spaces. Text or image is required." | |
267 | msgstr "" |
|
281 | msgstr "" | |
268 | "Метки должны быть разделены пробелами. Текст или изображение обязательны." |
|
282 | "Метки должны быть разделены пробелами. Текст или изображение обязательны." | |
269 |
|
283 | |||
270 |
#: templates/boards/posting_general.html:14 |
|
284 | #: templates/boards/posting_general.html:142 templates/boards/thread.html:59 | |
271 | msgid "Text syntax" |
|
285 | msgid "Text syntax" | |
272 | msgstr "Синтаксис текста" |
|
286 | msgstr "Синтаксис текста" | |
273 |
|
287 | |||
274 |
#: templates/boards/posting_general.html:1 |
|
288 | #: templates/boards/posting_general.html:154 | |
275 | msgid "Pages:" |
|
289 | msgid "Pages:" | |
276 | msgstr "Страницы: " |
|
290 | msgstr "Страницы: " | |
277 |
|
291 | |||
@@ -291,11 +305,11 b' msgstr "\xd0\x92\xd1\x8b \xd0\xbc\xd0\xbe\xd0\xb4\xd0\xb5\xd1\x80\xd0\xb0\xd1\x82\xd0\xbe\xd1\x80."' | |||||
291 | msgid "Hidden tags:" |
|
305 | msgid "Hidden tags:" | |
292 | msgstr "Скрытые метки:" |
|
306 | msgstr "Скрытые метки:" | |
293 |
|
307 | |||
294 |
#: templates/boards/settings.html:2 |
|
308 | #: templates/boards/settings.html:27 | |
295 | msgid "No hidden tags." |
|
309 | msgid "No hidden tags." | |
296 | msgstr "Нет скрытых меток." |
|
310 | msgstr "Нет скрытых меток." | |
297 |
|
311 | |||
298 |
#: templates/boards/settings.html:3 |
|
312 | #: templates/boards/settings.html:36 | |
299 | msgid "Save" |
|
313 | msgid "Save" | |
300 | msgstr "Сохранить" |
|
314 | msgstr "Сохранить" | |
301 |
|
315 | |||
@@ -360,11 +374,10 b' msgstr "\xd1\x81\xd0\xbe\xd0\xbe\xd0\xb1\xd1\x89\xd0\xb5\xd0\xbd\xd0\xb8\xd0\xb9 \xd0\xb4\xd0\xbe \xd0\xb1\xd0\xb0\xd0\xbc\xd0\xbf\xd0\xbb\xd0\xb8\xd0\xbc\xd0\xb8\xd1\x82\xd0\xb0"' | |||||
360 | msgid "Reply to thread" |
|
374 | msgid "Reply to thread" | |
361 | msgstr "Ответить в тему" |
|
375 | msgstr "Ответить в тему" | |
362 |
|
376 | |||
363 |
#: templates/boards/thread.html:5 |
|
377 | #: templates/boards/thread.html:85 | |
364 |
msgid " |
|
378 | msgid "Update" | |
365 | msgstr "Переключить режим" |
|
379 | msgstr "Обновить" | |
366 |
|
380 | |||
367 |
#: templates/boards/thread.html: |
|
381 | #: templates/boards/thread.html:88 templates/boards/thread_gallery.html:61 | |
368 | msgid "Last update: " |
|
382 | msgid "Last update: " | |
369 | msgstr "Последнее обновление: " |
|
383 | msgstr "Последнее обновление: " | |
370 |
|
@@ -4,7 +4,7 b'' | |||||
4 | from django.core.management import BaseCommand |
|
4 | from django.core.management import BaseCommand | |
5 | from django.db import transaction |
|
5 | from django.db import transaction | |
6 |
|
6 | |||
7 | from boards.models import KeyPair |
|
7 | from boards.models import KeyPair, Post | |
8 |
|
8 | |||
9 |
|
9 | |||
10 | class Command(BaseCommand): |
|
10 | class Command(BaseCommand): | |
@@ -12,6 +12,11 b' class Command(BaseCommand):' | |||||
12 |
|
12 | |||
13 | @transaction.atomic |
|
13 | @transaction.atomic | |
14 | def handle(self, *args, **options): |
|
14 | def handle(self, *args, **options): | |
|
15 | first_key = not KeyPair.objects.has_primary() | |||
15 | key = KeyPair.objects.generate_key( |
|
16 | key = KeyPair.objects.generate_key( | |
16 | primary=not KeyPair.objects.has_primary()) |
|
17 | primary=first_key) | |
17 | print(key) No newline at end of file |
|
18 | print(key) | |
|
19 | ||||
|
20 | if first_key: | |||
|
21 | for post in Post.objects.filter(global_id=None): | |||
|
22 | post.set_global_id() No newline at end of file |
@@ -2,6 +2,7 b'' | |||||
2 |
|
2 | |||
3 | import re |
|
3 | import re | |
4 | import bbcode |
|
4 | import bbcode | |
|
5 | from django.core.urlresolvers import reverse | |||
5 |
|
6 | |||
6 | import boards |
|
7 | import boards | |
7 |
|
8 | |||
@@ -153,7 +154,6 b' def render_quote(tag_name, value, option' | |||||
153 | if 'source' in options: |
|
154 | if 'source' in options: | |
154 | source = options['source'] |
|
155 | source = options['source'] | |
155 |
|
156 | |||
156 | result = '' |
|
|||
157 | if source: |
|
157 | if source: | |
158 | result = '<div class="multiquote"><div class="quote-header">%s</div><div class="quote-text">%s</div></div>' % (source, value) |
|
158 | result = '<div class="multiquote"><div class="quote-header">%s</div><div class="quote-text">%s</div></div>' % (source, value) | |
159 | else: |
|
159 | else: | |
@@ -162,6 +162,13 b' def render_quote(tag_name, value, option' | |||||
162 | return result |
|
162 | return result | |
163 |
|
163 | |||
164 |
|
164 | |||
|
165 | def render_notification(tag_name, value, options, parent, content): | |||
|
166 | username = value.lower() | |||
|
167 | ||||
|
168 | return '<a href="{}" class="user-cast">@{}</a>'.format( | |||
|
169 | reverse('notifications', kwargs={'username': username}), username) | |||
|
170 | ||||
|
171 | ||||
165 | def preparse_text(text): |
|
172 | def preparse_text(text): | |
166 | """ |
|
173 | """ | |
167 | Performs manual parsing before the bbcode parser is used. |
|
174 | Performs manual parsing before the bbcode parser is used. | |
@@ -176,11 +183,11 b' def bbcode_extended(markup):' | |||||
176 | parser = bbcode.Parser(newline='<div class="br"></div>') |
|
183 | parser = bbcode.Parser(newline='<div class="br"></div>') | |
177 | parser.add_formatter('post', render_reflink, strip=True) |
|
184 | parser.add_formatter('post', render_reflink, strip=True) | |
178 | parser.add_formatter('quote', render_quote, strip=True) |
|
185 | parser.add_formatter('quote', render_quote, strip=True) | |
|
186 | parser.add_formatter('user', render_notification, strip=True) | |||
179 | parser.add_simple_formatter('comment', |
|
187 | parser.add_simple_formatter('comment', | |
180 | '<span class="comment">//%(value)s</span>') |
|
188 | '<span class="comment">//%(value)s</span>') | |
181 | parser.add_simple_formatter('spoiler', |
|
189 | parser.add_simple_formatter('spoiler', | |
182 | '<span class="spoiler">%(value)s</span>') |
|
190 | '<span class="spoiler">%(value)s</span>') | |
183 | # TODO Use <s> here |
|
|||
184 | parser.add_simple_formatter('s', |
|
191 | parser.add_simple_formatter('s', | |
185 | '<span class="strikethrough">%(value)s</span>') |
|
192 | '<span class="strikethrough">%(value)s</span>') | |
186 | # TODO Why not use built-in tag? |
|
193 | # TODO Why not use built-in tag? |
@@ -56,10 +56,7 b' class PostImage(models.Model, Viewable):' | |||||
56 | """ |
|
56 | """ | |
57 |
|
57 | |||
58 | if not self.pk and self.image: |
|
58 | if not self.pk and self.image: | |
59 | md5 = hashlib.md5() |
|
59 | self.hash = PostImage.get_hash(self.image) | |
60 | for chunk in self.image.chunks(): |
|
|||
61 | md5.update(chunk) |
|
|||
62 | self.hash = md5.hexdigest() |
|
|||
63 | super(PostImage, self).save(*args, **kwargs) |
|
60 | super(PostImage, self).save(*args, **kwargs) | |
64 |
|
61 | |||
65 | def __str__(self): |
|
62 | def __str__(self): | |
@@ -81,3 +78,13 b' class PostImage(models.Model, Viewable):' | |||||
81 | self.image.url_200x150, |
|
78 | self.image.url_200x150, | |
82 | str(self.hash), str(self.pre_width), |
|
79 | str(self.hash), str(self.pre_width), | |
83 | str(self.pre_height), str(self.width), str(self.height)) |
|
80 | str(self.pre_height), str(self.width), str(self.height)) | |
|
81 | ||||
|
82 | @staticmethod | |||
|
83 | def get_hash(image): | |||
|
84 | """ | |||
|
85 | Gets hash of an image. | |||
|
86 | """ | |||
|
87 | md5 = hashlib.md5() | |||
|
88 | for chunk in image.chunks(): | |||
|
89 | md5.update(chunk) | |||
|
90 | return md5.hexdigest() |
@@ -3,23 +3,24 b' from datetime import time as dtime' | |||||
3 | import logging |
|
3 | import logging | |
4 | import re |
|
4 | import re | |
5 | import xml.etree.ElementTree as et |
|
5 | import xml.etree.ElementTree as et | |
|
6 | from urllib.parse import unquote | |||
6 |
|
7 | |||
7 | from adjacent import Client |
|
8 | from adjacent import Client | |
8 | from django.core.cache import cache |
|
|||
9 | from django.core.urlresolvers import reverse |
|
9 | from django.core.urlresolvers import reverse | |
10 | from django.db import models, transaction |
|
10 | from django.db import models, transaction | |
11 | from django.db.models import TextField |
|
11 | from django.db.models import TextField | |
12 | from django.template.loader import render_to_string |
|
12 | from django.template.loader import render_to_string | |
13 | from django.utils import timezone |
|
13 | from django.utils import timezone | |
14 |
|
14 | |||
15 |
from boards.models import |
|
15 | from boards.models import KeyPair, GlobalId, Signature | |
16 | from boards import settings |
|
16 | from boards import settings, utils | |
17 | from boards.mdx_neboard import bbcode_extended |
|
17 | from boards.mdx_neboard import bbcode_extended | |
18 | from boards.models import PostImage |
|
18 | from boards.models import PostImage | |
19 | from boards.models.base import Viewable |
|
19 | from boards.models.base import Viewable | |
20 | from boards.models.thread import Thread |
|
20 | from boards.utils import datetime_to_epoch, cached_result | |
21 |
from boards import |
|
21 | from boards.models.user import Notification | |
22 | from boards.utils import datetime_to_epoch |
|
22 | import boards.models.thread | |
|
23 | ||||
23 |
|
24 | |||
24 | ENCODING_UNICODE = 'unicode' |
|
25 | ENCODING_UNICODE = 'unicode' | |
25 |
|
26 | |||
@@ -30,9 +31,6 b' WS_CHANNEL_THREAD = "thread:"' | |||||
30 |
|
31 | |||
31 | APP_LABEL_BOARDS = 'boards' |
|
32 | APP_LABEL_BOARDS = 'boards' | |
32 |
|
33 | |||
33 | CACHE_KEY_PPD = 'ppd' |
|
|||
34 | CACHE_KEY_POST_URL = 'post_url' |
|
|||
35 |
|
||||
36 | POSTS_PER_DAY_RANGE = 7 |
|
34 | POSTS_PER_DAY_RANGE = 7 | |
37 |
|
35 | |||
38 | BAN_REASON_AUTO = 'Auto' |
|
36 | BAN_REASON_AUTO = 'Auto' | |
@@ -49,6 +47,8 b" UNKNOWN_UA = ''" | |||||
49 |
|
47 | |||
50 | REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]') |
|
48 | REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]') | |
51 | REGEX_GLOBAL_REPLY = re.compile(r'\[post\](\w+)::([^:]+)::(\d+)\[/post\]') |
|
49 | REGEX_GLOBAL_REPLY = re.compile(r'\[post\](\w+)::([^:]+)::(\d+)\[/post\]') | |
|
50 | REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?') | |||
|
51 | REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]') | |||
52 |
|
52 | |||
53 | TAG_MODEL = 'model' |
|
53 | TAG_MODEL = 'model' | |
54 | TAG_REQUEST = 'request' |
|
54 | TAG_REQUEST = 'request' | |
@@ -111,8 +111,8 b' class PostManager(models.Manager):' | |||||
111 |
|
111 | |||
112 | posting_time = timezone.now() |
|
112 | posting_time = timezone.now() | |
113 | if not thread: |
|
113 | if not thread: | |
114 |
thread = Thread.objects.create( |
|
114 | thread = boards.models.thread.Thread.objects.create( | |
115 |
|
|
115 | bump_time=posting_time, last_edit_time=posting_time) | |
116 | new_thread = True |
|
116 | new_thread = True | |
117 | else: |
|
117 | else: | |
118 | new_thread = False |
|
118 | new_thread = False | |
@@ -122,11 +122,12 b' class PostManager(models.Manager):' | |||||
122 | post = self.create(title=title, |
|
122 | post = self.create(title=title, | |
123 | text=pre_text, |
|
123 | text=pre_text, | |
124 | pub_time=posting_time, |
|
124 | pub_time=posting_time, | |
125 | thread_new=thread, |
|
|||
126 | poster_ip=ip, |
|
125 | poster_ip=ip, | |
|
126 | thread=thread, | |||
127 | poster_user_agent=UNKNOWN_UA, # TODO Get UA at |
|
127 | poster_user_agent=UNKNOWN_UA, # TODO Get UA at | |
128 | # last! |
|
128 | # last! | |
129 | last_edit_time=posting_time) |
|
129 | last_edit_time=posting_time) | |
|
130 | post.threads.add(thread) | |||
130 |
|
131 | |||
131 | post.set_global_id() |
|
132 | post.set_global_id() | |
132 |
|
133 | |||
@@ -136,22 +137,29 b' class PostManager(models.Manager):' | |||||
136 | post, post.poster_ip)) |
|
137 | post, post.poster_ip)) | |
137 |
|
138 | |||
138 | if image: |
|
139 | if image: | |
139 | post_image = PostImage.objects.create(image=image) |
|
140 | # Try to find existing image. If it exists, assign it to the post | |
|
141 | # instead of createing the new one | |||
|
142 | image_hash = PostImage.get_hash(image) | |||
|
143 | existing = PostImage.objects.filter(hash=image_hash) | |||
|
144 | if len(existing) > 0: | |||
|
145 | post_image = existing[0] | |||
|
146 | else: | |||
|
147 | post_image = PostImage.objects.create(image=image) | |||
|
148 | logger.info('Created new image #{} for post #{}'.format( | |||
|
149 | post_image.id, post.id)) | |||
140 | post.images.add(post_image) |
|
150 | post.images.add(post_image) | |
141 | logger.info('Created image #{} for post #{}'.format( |
|
|||
142 | post_image.id, post.id)) |
|
|||
143 |
|
151 | |||
144 | thread.replies.add(post) |
|
|||
145 | list(map(thread.add_tag, tags)) |
|
152 | list(map(thread.add_tag, tags)) | |
146 |
|
153 | |||
147 | if new_thread: |
|
154 | if new_thread: | |
148 | Thread.objects.process_oldest_threads() |
|
155 | boards.models.thread.Thread.objects.process_oldest_threads() | |
149 | else: |
|
156 | else: | |
150 | thread.bump() |
|
157 | thread.bump() | |
151 | thread.last_edit_time = posting_time |
|
158 | thread.last_edit_time = posting_time | |
152 | thread.save() |
|
159 | thread.save() | |
153 |
|
160 | |||
154 |
|
|
161 | post.connect_replies() | |
|
162 | post.connect_notifications() | |||
155 |
|
163 | |||
156 | return post |
|
164 | return post | |
157 |
|
165 | |||
@@ -164,25 +172,7 b' class PostManager(models.Manager):' | |||||
164 | for post in posts: |
|
172 | for post in posts: | |
165 | post.delete() |
|
173 | post.delete() | |
166 |
|
174 | |||
167 | # TODO This can be moved into a post |
|
175 | @cached_result | |
168 | def connect_replies(self, post): |
|
|||
169 | """ |
|
|||
170 | Connects replies to a post to show them as a reflink map |
|
|||
171 | """ |
|
|||
172 |
|
||||
173 | for reply_number in post.get_replied_ids(): |
|
|||
174 | ref_post = self.filter(id=reply_number) |
|
|||
175 | if ref_post.count() > 0: |
|
|||
176 | referenced_post = ref_post[0] |
|
|||
177 | referenced_post.referenced_posts.add(post) |
|
|||
178 | referenced_post.last_edit_time = post.pub_time |
|
|||
179 | referenced_post.build_refmap() |
|
|||
180 | referenced_post.save(update_fields=['refmap', 'last_edit_time']) |
|
|||
181 |
|
||||
182 | referenced_thread = referenced_post.get_thread() |
|
|||
183 | referenced_thread.last_edit_time = post.pub_time |
|
|||
184 | referenced_thread.save(update_fields=['last_edit_time']) |
|
|||
185 |
|
||||
186 | def get_posts_per_day(self): |
|
176 | def get_posts_per_day(self): | |
187 | """ |
|
177 | """ | |
188 | Gets average count of posts per day for the last 7 days |
|
178 | Gets average count of posts per day for the last 7 days | |
@@ -191,11 +181,6 b' class PostManager(models.Manager):' | |||||
191 | day_end = date.today() |
|
181 | day_end = date.today() | |
192 | day_start = day_end - timedelta(POSTS_PER_DAY_RANGE) |
|
182 | day_start = day_end - timedelta(POSTS_PER_DAY_RANGE) | |
193 |
|
183 | |||
194 | cache_key = CACHE_KEY_PPD + str(day_end) |
|
|||
195 | ppd = cache.get(cache_key) |
|
|||
196 | if ppd: |
|
|||
197 | return ppd |
|
|||
198 |
|
||||
199 | day_time_start = timezone.make_aware(datetime.combine( |
|
184 | day_time_start = timezone.make_aware(datetime.combine( | |
200 | day_start, dtime()), timezone.get_current_timezone()) |
|
185 | day_start, dtime()), timezone.get_current_timezone()) | |
201 | day_time_end = timezone.make_aware(datetime.combine( |
|
186 | day_time_end = timezone.make_aware(datetime.combine( | |
@@ -207,7 +192,6 b' class PostManager(models.Manager):' | |||||
207 |
|
192 | |||
208 | ppd = posts_per_period / POSTS_PER_DAY_RANGE |
|
193 | ppd = posts_per_period / POSTS_PER_DAY_RANGE | |
209 |
|
194 | |||
210 | cache.set(cache_key, ppd) |
|
|||
211 | return ppd |
|
195 | return ppd | |
212 |
|
196 | |||
213 | # TODO Make a separate sync facade? |
|
197 | # TODO Make a separate sync facade? | |
@@ -295,7 +279,8 b' class PostManager(models.Manager):' | |||||
295 | # TODO Throw an exception? |
|
279 | # TODO Throw an exception? | |
296 | pass |
|
280 | pass | |
297 |
|
281 | |||
298 | def _preparse_text(self, text): |
|
282 | # TODO Make a separate parser module and move preparser there | |
|
283 | def _preparse_text(self, text: str) -> str: | |||
299 | """ |
|
284 | """ | |
300 | Preparses text to change patterns like '>>' to a proper bbcode |
|
285 | Preparses text to change patterns like '>>' to a proper bbcode | |
301 | tags. |
|
286 | tags. | |
@@ -304,6 +289,9 b' class PostManager(models.Manager):' | |||||
304 | for key, value in PREPARSE_PATTERNS.items(): |
|
289 | for key, value in PREPARSE_PATTERNS.items(): | |
305 | text = re.sub(key, value, text, flags=re.MULTILINE) |
|
290 | text = re.sub(key, value, text, flags=re.MULTILINE) | |
306 |
|
291 | |||
|
292 | for link in REGEX_URL.findall(text): | |||
|
293 | text = text.replace(link, unquote(link)) | |||
|
294 | ||||
307 | return text |
|
295 | return text | |
308 |
|
296 | |||
309 |
|
297 | |||
@@ -327,19 +315,15 b' class Post(models.Model, Viewable):' | |||||
327 | poster_ip = models.GenericIPAddressField() |
|
315 | poster_ip = models.GenericIPAddressField() | |
328 | poster_user_agent = models.TextField() |
|
316 | poster_user_agent = models.TextField() | |
329 |
|
317 | |||
330 | thread_new = models.ForeignKey('Thread', null=True, default=None, |
|
|||
331 | db_index=True) |
|
|||
332 | last_edit_time = models.DateTimeField() |
|
318 | last_edit_time = models.DateTimeField() | |
333 |
|
319 | |||
334 | # Replies to the post |
|
|||
335 | referenced_posts = models.ManyToManyField('Post', symmetrical=False, |
|
320 | referenced_posts = models.ManyToManyField('Post', symmetrical=False, | |
336 | null=True, |
|
321 | null=True, | |
337 | blank=True, related_name='rfp+', |
|
322 | blank=True, related_name='rfp+', | |
338 | db_index=True) |
|
323 | db_index=True) | |
339 |
|
||||
340 | # Replies map. This is built from the referenced posts list to speed up |
|
|||
341 | # page loading (no need to get all the referenced posts from the database). |
|
|||
342 | refmap = models.TextField(null=True, blank=True) |
|
324 | refmap = models.TextField(null=True, blank=True) | |
|
325 | threads = models.ManyToManyField('Thread', db_index=True) | |||
|
326 | thread = models.ForeignKey('Thread', db_index=True, related_name='pt+') | |||
343 |
|
327 | |||
344 | # Global ID with author key. If the message was downloaded from another |
|
328 | # Global ID with author key. If the message was downloaded from another | |
345 | # server, this indicates the server. |
|
329 | # server, this indicates the server. | |
@@ -367,26 +351,16 b' class Post(models.Model, Viewable):' | |||||
367 | Builds a replies map string from replies list. This is a cache to stop |
|
351 | Builds a replies map string from replies list. This is a cache to stop | |
368 | the server from recalculating the map on every post show. |
|
352 | the server from recalculating the map on every post show. | |
369 | """ |
|
353 | """ | |
370 | map_string = '' |
|
354 | post_urls = ['<a href="{}">>>{}</a>'.format( | |
|
355 | refpost.get_url(), refpost.id) for refpost in self.referenced_posts.all()] | |||
371 |
|
356 | |||
372 | first = True |
|
357 | self.refmap = ', '.join(post_urls) | |
373 | for refpost in self.referenced_posts.all(): |
|
|||
374 | if not first: |
|
|||
375 | map_string += ', ' |
|
|||
376 | map_string += '<a href="%s">>>%s</a>' % (refpost.get_url(), |
|
|||
377 | refpost.id) |
|
|||
378 | first = False |
|
|||
379 |
|
||||
380 | self.refmap = map_string |
|
|||
381 |
|
358 | |||
382 | def get_sorted_referenced_posts(self): |
|
359 | def get_sorted_referenced_posts(self): | |
383 | return self.refmap |
|
360 | return self.refmap | |
384 |
|
361 | |||
385 | def is_referenced(self) -> bool: |
|
362 | def is_referenced(self) -> bool: | |
386 | if not self.refmap: |
|
363 | return self.refmap and len(self.refmap) > 0 | |
387 | return False |
|
|||
388 | else: |
|
|||
389 | return len(self.refmap) > 0 |
|
|||
390 |
|
364 | |||
391 | def is_opening(self) -> bool: |
|
365 | def is_opening(self) -> bool: | |
392 | """ |
|
366 | """ | |
@@ -407,39 +381,36 b' class Post(models.Model, Viewable):' | |||||
407 | thread.last_edit_time = edit_time |
|
381 | thread.last_edit_time = edit_time | |
408 | thread.save(update_fields=['last_edit_time']) |
|
382 | thread.save(update_fields=['last_edit_time']) | |
409 |
|
383 | |||
410 | def get_url(self, thread=None): |
|
384 | @cached_result | |
|
385 | def get_url(self): | |||
411 | """ |
|
386 | """ | |
412 | Gets full url to the post. |
|
387 | Gets full url to the post. | |
413 | """ |
|
388 | """ | |
414 |
|
389 | |||
415 | cache_key = CACHE_KEY_POST_URL + str(self.id) |
|
390 | thread = self.get_thread() | |
416 | link = cache.get(cache_key) |
|
|||
417 |
|
391 | |||
418 | if not link: |
|
392 | opening_id = thread.get_opening_post_id() | |
419 | if not thread: |
|
|||
420 | thread = self.get_thread() |
|
|||
421 |
|
393 | |||
422 | opening_id = thread.get_opening_post_id() |
|
394 | if self.id != opening_id: | |
423 |
|
395 | link = reverse('thread', kwargs={ | ||
424 | if self.id != opening_id: |
|
396 | 'post_id': opening_id}) + '#' + str(self.id) | |
425 | link = reverse('thread', kwargs={ |
|
397 | else: | |
426 | 'post_id': opening_id}) + '#' + str(self.id) |
|
398 | link = reverse('thread', kwargs={'post_id': self.id}) | |
427 | else: |
|
|||
428 | link = reverse('thread', kwargs={'post_id': self.id}) |
|
|||
429 |
|
||||
430 | cache.set(cache_key, link) |
|
|||
431 |
|
399 | |||
432 | return link |
|
400 | return link | |
433 |
|
401 | |||
434 |
def get_thread(self) |
|
402 | def get_thread(self): | |
|
403 | return self.thread | |||
|
404 | ||||
|
405 | def get_threads(self): | |||
435 | """ |
|
406 | """ | |
436 | Gets post's thread. |
|
407 | Gets post's thread. | |
437 | """ |
|
408 | """ | |
438 |
|
409 | |||
439 |
return self.thread |
|
410 | return self.threads | |
440 |
|
411 | |||
441 | def get_referenced_posts(self): |
|
412 | def get_referenced_posts(self): | |
442 |
return self.referenced_posts.only('id', 'thread |
|
413 | return self.referenced_posts.only('id', 'threads') | |
443 |
|
414 | |||
444 | def get_view(self, moderator=False, need_open_link=False, |
|
415 | def get_view(self, moderator=False, need_open_link=False, | |
445 | truncated=False, *args, **kwargs): |
|
416 | truncated=False, *args, **kwargs): | |
@@ -449,8 +420,8 b' class Post(models.Model, Viewable):' | |||||
449 | are same for every post and don't need to be computed over and over. |
|
420 | are same for every post and don't need to be computed over and over. | |
450 | """ |
|
421 | """ | |
451 |
|
422 | |||
|
423 | thread = self.get_thread() | |||
452 | is_opening = kwargs.get(PARAMETER_IS_OPENING, self.is_opening()) |
|
424 | is_opening = kwargs.get(PARAMETER_IS_OPENING, self.is_opening()) | |
453 | thread = kwargs.get(PARAMETER_THREAD, self.get_thread()) |
|
|||
454 | can_bump = kwargs.get(PARAMETER_BUMPABLE, thread.can_bump()) |
|
425 | can_bump = kwargs.get(PARAMETER_BUMPABLE, thread.can_bump()) | |
455 |
|
426 | |||
456 | if is_opening: |
|
427 | if is_opening: | |
@@ -477,23 +448,24 b' class Post(models.Model, Viewable):' | |||||
477 |
|
448 | |||
478 | def delete(self, using=None): |
|
449 | def delete(self, using=None): | |
479 | """ |
|
450 | """ | |
480 |
Deletes all post images and the post itself. |
|
451 | Deletes all post images and the post itself. | |
481 | thread with all posts is deleted. |
|
|||
482 | """ |
|
452 | """ | |
483 |
|
453 | |||
484 |
self.images.all() |
|
454 | for image in self.images.all(): | |
|
455 | image_refs_count = Post.objects.filter(images__in=[image]).count() | |||
|
456 | if image_refs_count == 1: | |||
|
457 | image.delete() | |||
|
458 | ||||
485 | self.signature.all().delete() |
|
459 | self.signature.all().delete() | |
486 | if self.global_id: |
|
460 | if self.global_id: | |
487 | self.global_id.delete() |
|
461 | self.global_id.delete() | |
488 |
|
462 | |||
489 | if self.is_opening(): |
|
463 | thread = self.get_thread() | |
490 | self.get_thread().delete() |
|
464 | thread.last_edit_time = timezone.now() | |
491 |
|
|
465 | thread.save() | |
492 | thread = self.get_thread() |
|
|||
493 | thread.last_edit_time = timezone.now() |
|
|||
494 | thread.save() |
|
|||
495 |
|
466 | |||
496 | super(Post, self).delete(using) |
|
467 | super(Post, self).delete(using) | |
|
468 | ||||
497 | logging.getLogger('boards.post.delete').info( |
|
469 | logging.getLogger('boards.post.delete').info( | |
498 | 'Deleted post {}'.format(self)) |
|
470 | 'Deleted post {}'.format(self)) | |
499 |
|
471 | |||
@@ -573,7 +545,7 b' class Post(models.Model, Viewable):' | |||||
573 | post_json['image_preview'] = post_image.image.url_200x150 |
|
545 | post_json['image_preview'] = post_image.image.url_200x150 | |
574 | if include_last_update: |
|
546 | if include_last_update: | |
575 | post_json['bump_time'] = datetime_to_epoch( |
|
547 | post_json['bump_time'] = datetime_to_epoch( | |
576 |
self.thread |
|
548 | self.get_thread().bump_time) | |
577 | return post_json |
|
549 | return post_json | |
578 |
|
550 | |||
579 | def send_to_websocket(self, request, recursive=True): |
|
551 | def send_to_websocket(self, request, recursive=True): | |
@@ -606,7 +578,7 b' class Post(models.Model, Viewable):' | |||||
606 |
|
578 | |||
607 | # If post is in this thread, its thread was already notified. |
|
579 | # If post is in this thread, its thread was already notified. | |
608 | # Otherwise, notify its thread separately. |
|
580 | # Otherwise, notify its thread separately. | |
609 |
if ref_post.thread |
|
581 | if ref_post.get_thread().id != thread_id: | |
610 | ref_post.send_to_websocket(request, recursive=False) |
|
582 | ref_post.send_to_websocket(request, recursive=False) | |
611 |
|
583 | |||
612 | def save(self, force_insert=False, force_update=False, using=None, |
|
584 | def save(self, force_insert=False, force_update=False, using=None, | |
@@ -620,3 +592,40 b' class Post(models.Model, Viewable):' | |||||
620 |
|
592 | |||
621 | def get_raw_text(self) -> str: |
|
593 | def get_raw_text(self) -> str: | |
622 | return self.text |
|
594 | return self.text | |
|
595 | ||||
|
596 | def get_absolute_id(self) -> str: | |||
|
597 | """ | |||
|
598 | If the post has many threads, shows its main thread OP id in the post | |||
|
599 | ID. | |||
|
600 | """ | |||
|
601 | ||||
|
602 | if self.get_threads().count() > 1: | |||
|
603 | return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id) | |||
|
604 | else: | |||
|
605 | return str(self.id) | |||
|
606 | ||||
|
607 | def connect_notifications(self): | |||
|
608 | for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()): | |||
|
609 | user_name = reply_number.group(1).lower() | |||
|
610 | Notification.objects.get_or_create(name=user_name, post=self) | |||
|
611 | ||||
|
612 | def connect_replies(self): | |||
|
613 | """ | |||
|
614 | Connects replies to a post to show them as a reflink map | |||
|
615 | """ | |||
|
616 | ||||
|
617 | for post_id in self.get_replied_ids(): | |||
|
618 | ref_post = Post.objects.filter(id=post_id) | |||
|
619 | if ref_post.count() > 0: | |||
|
620 | referenced_post = ref_post[0] | |||
|
621 | referenced_post.referenced_posts.add(self) | |||
|
622 | referenced_post.last_edit_time = self.pub_time | |||
|
623 | referenced_post.build_refmap() | |||
|
624 | referenced_post.save(update_fields=['refmap', 'last_edit_time']) | |||
|
625 | ||||
|
626 | referenced_threads = referenced_post.get_threads().all() | |||
|
627 | for thread in referenced_threads: | |||
|
628 | thread.last_edit_time = self.pub_time | |||
|
629 | thread.save(update_fields=['last_edit_time']) | |||
|
630 | ||||
|
631 | self.threads.add(thread) |
@@ -4,6 +4,7 b' from django.db.models import Count' | |||||
4 | from django.core.urlresolvers import reverse |
|
4 | from django.core.urlresolvers import reverse | |
5 |
|
5 | |||
6 | from boards.models.base import Viewable |
|
6 | from boards.models.base import Viewable | |
|
7 | from boards.utils import cached_result | |||
7 |
|
8 | |||
8 |
|
9 | |||
9 | __author__ = 'neko259' |
|
10 | __author__ = 'neko259' | |
@@ -33,8 +34,8 b' class Tag(models.Model, Viewable):' | |||||
33 | app_label = 'boards' |
|
34 | app_label = 'boards' | |
34 | ordering = ('name',) |
|
35 | ordering = ('name',) | |
35 |
|
36 | |||
36 | name = models.CharField(max_length=100, db_index=True) |
|
37 | name = models.CharField(max_length=100, db_index=True, unique=True) | |
37 | required = models.BooleanField(default=False) |
|
38 | required = models.BooleanField(default=False, db_index=True) | |
38 |
|
39 | |||
39 | def __str__(self): |
|
40 | def __str__(self): | |
40 | return self.name |
|
41 | return self.name | |
@@ -69,3 +70,7 b' class Tag(models.Model, Viewable):' | |||||
69 | return render_to_string('boards/tag.html', { |
|
70 | return render_to_string('boards/tag.html', { | |
70 | 'tag': self, |
|
71 | 'tag': self, | |
71 | }) |
|
72 | }) | |
|
73 | ||||
|
74 | @cached_result | |||
|
75 | def get_post_count(self): | |||
|
76 | return self.get_threads().aggregate(num_posts=Count('post'))['num_posts'] |
@@ -1,9 +1,13 b'' | |||||
1 | import logging |
|
1 | import logging | |
|
2 | ||||
2 | from django.db.models import Count, Sum |
|
3 | from django.db.models import Count, Sum | |
3 | from django.utils import timezone |
|
4 | from django.utils import timezone | |
4 | from django.core.cache import cache |
|
|||
5 | from django.db import models |
|
5 | from django.db import models | |
|
6 | ||||
6 | from boards import settings |
|
7 | from boards import settings | |
|
8 | from boards.utils import cached_result | |||
|
9 | from boards.models.post import Post | |||
|
10 | ||||
7 |
|
11 | |||
8 | __author__ = 'neko259' |
|
12 | __author__ = 'neko259' | |
9 |
|
13 | |||
@@ -11,9 +15,6 b' from boards import settings' | |||||
11 | logger = logging.getLogger(__name__) |
|
15 | logger = logging.getLogger(__name__) | |
12 |
|
16 | |||
13 |
|
17 | |||
14 | CACHE_KEY_OPENING_POST = 'opening_post_id' |
|
|||
15 |
|
||||
16 |
|
||||
17 | class ThreadManager(models.Manager): |
|
18 | class ThreadManager(models.Manager): | |
18 | def process_oldest_threads(self): |
|
19 | def process_oldest_threads(self): | |
19 | """ |
|
20 | """ | |
@@ -50,10 +51,8 b' class Thread(models.Model):' | |||||
50 | app_label = 'boards' |
|
51 | app_label = 'boards' | |
51 |
|
52 | |||
52 | tags = models.ManyToManyField('Tag') |
|
53 | tags = models.ManyToManyField('Tag') | |
53 | bump_time = models.DateTimeField() |
|
54 | bump_time = models.DateTimeField(db_index=True) | |
54 | last_edit_time = models.DateTimeField() |
|
55 | last_edit_time = models.DateTimeField() | |
55 | replies = models.ManyToManyField('Post', symmetrical=False, null=True, |
|
|||
56 | blank=True, related_name='tre+') |
|
|||
57 | archived = models.BooleanField(default=False) |
|
56 | archived = models.BooleanField(default=False) | |
58 | bumpable = models.BooleanField(default=True) |
|
57 | bumpable = models.BooleanField(default=True) | |
59 |
|
58 | |||
@@ -78,10 +77,10 b' class Thread(models.Model):' | |||||
78 | logger.info('Bumped thread %d' % self.id) |
|
77 | logger.info('Bumped thread %d' % self.id) | |
79 |
|
78 | |||
80 | def get_reply_count(self): |
|
79 | def get_reply_count(self): | |
81 | return self.replies.count() |
|
80 | return self.get_replies().count() | |
82 |
|
81 | |||
83 | def get_images_count(self): |
|
82 | def get_images_count(self): | |
84 | return self.replies.annotate(images_count=Count( |
|
83 | return self.get_replies().annotate(images_count=Count( | |
85 | 'images')).aggregate(Sum('images_count'))['images_count__sum'] |
|
84 | 'images')).aggregate(Sum('images_count'))['images_count__sum'] | |
86 |
|
85 | |||
87 | def can_bump(self): |
|
86 | def can_bump(self): | |
@@ -121,12 +120,17 b' class Thread(models.Model):' | |||||
121 | Gets sorted thread posts |
|
120 | Gets sorted thread posts | |
122 | """ |
|
121 | """ | |
123 |
|
122 | |||
124 | query = self.replies.order_by('pub_time').prefetch_related('images') |
|
123 | query = Post.objects.filter(threads__in=[self]) | |
|
124 | query = query.order_by('pub_time').prefetch_related('images', 'thread', 'threads') | |||
125 | if view_fields_only: |
|
125 | if view_fields_only: | |
126 | query = query.defer('poster_user_agent') |
|
126 | query = query.defer('poster_user_agent') | |
127 | return query.all() |
|
127 | return query.all() | |
128 |
|
128 | |||
129 | def get_replies_with_images(self, view_fields_only=False): |
|
129 | def get_replies_with_images(self, view_fields_only=False): | |
|
130 | """ | |||
|
131 | Gets replies that have at least one image attached | |||
|
132 | """ | |||
|
133 | ||||
130 | return self.get_replies(view_fields_only).annotate(images_count=Count( |
|
134 | return self.get_replies(view_fields_only).annotate(images_count=Count( | |
131 | 'images')).filter(images_count__gt=0) |
|
135 | 'images')).filter(images_count__gt=0) | |
132 |
|
136 | |||
@@ -142,25 +146,20 b' class Thread(models.Model):' | |||||
142 | Gets the first post of the thread |
|
146 | Gets the first post of the thread | |
143 | """ |
|
147 | """ | |
144 |
|
148 | |||
145 | query = self.replies.order_by('pub_time') |
|
149 | query = self.get_replies().order_by('pub_time') | |
146 | if only_id: |
|
150 | if only_id: | |
147 | query = query.only('id') |
|
151 | query = query.only('id') | |
148 | opening_post = query.first() |
|
152 | opening_post = query.first() | |
149 |
|
153 | |||
150 | return opening_post |
|
154 | return opening_post | |
151 |
|
155 | |||
|
156 | @cached_result | |||
152 | def get_opening_post_id(self): |
|
157 | def get_opening_post_id(self): | |
153 | """ |
|
158 | """ | |
154 | Gets ID of the first thread post. |
|
159 | Gets ID of the first thread post. | |
155 | """ |
|
160 | """ | |
156 |
|
161 | |||
157 | cache_key = CACHE_KEY_OPENING_POST + str(self.id) |
|
162 | return self.get_opening_post(only_id=True).id | |
158 | opening_post_id = cache.get(cache_key) |
|
|||
159 | if not opening_post_id: |
|
|||
160 | opening_post_id = self.get_opening_post(only_id=True).id |
|
|||
161 | cache.set(cache_key, opening_post_id) |
|
|||
162 |
|
||||
163 | return opening_post_id |
|
|||
164 |
|
163 | |||
165 | def __unicode__(self): |
|
164 | def __unicode__(self): | |
166 | return str(self.id) |
|
165 | return str(self.id) | |
@@ -173,10 +172,14 b' class Thread(models.Model):' | |||||
173 | return self.get_opening_post().pub_time |
|
172 | return self.get_opening_post().pub_time | |
174 |
|
173 | |||
175 | def delete(self, using=None): |
|
174 | def delete(self, using=None): | |
176 | if self.replies.exists(): |
|
175 | """ | |
177 | self.replies.all().delete() |
|
176 | Deletes thread with all replies. | |
|
177 | """ | |||
|
178 | ||||
|
179 | for reply in self.get_replies().all(): | |||
|
180 | reply.delete() | |||
178 |
|
181 | |||
179 | super(Thread, self).delete(using) |
|
182 | super(Thread, self).delete(using) | |
180 |
|
183 | |||
181 | def __str__(self): |
|
184 | def __str__(self): | |
182 | return 'T#{}/{}'.format(self.id, self.get_opening_post_id()) No newline at end of file |
|
185 | return 'T#{}/{}'.format(self.id, self.get_opening_post_id()) |
@@ -1,5 +1,7 b'' | |||||
1 | from django.db import models |
|
1 | from django.db import models | |
2 |
|
2 | |||
|
3 | import boards.models.post | |||
|
4 | ||||
3 | __author__ = 'neko259' |
|
5 | __author__ = 'neko259' | |
4 |
|
6 | |||
5 | BAN_REASON_AUTO = 'Auto' |
|
7 | BAN_REASON_AUTO = 'Auto' | |
@@ -18,3 +20,27 b' class Ban(models.Model):' | |||||
18 |
|
20 | |||
19 | def __str__(self): |
|
21 | def __str__(self): | |
20 | return self.ip |
|
22 | return self.ip | |
|
23 | ||||
|
24 | ||||
|
25 | class NotificationManager(models.Manager): | |||
|
26 | def get_notification_posts(self, username: str, last: int = None): | |||
|
27 | i_username = username.lower() | |||
|
28 | ||||
|
29 | posts = boards.models.post.Post.objects.filter(notification__name=i_username) | |||
|
30 | if last is not None: | |||
|
31 | posts = posts.filter(id__gt=last) | |||
|
32 | posts = posts.order_by('-id') | |||
|
33 | ||||
|
34 | return posts | |||
|
35 | ||||
|
36 | ||||
|
37 | class Notification(models.Model): | |||
|
38 | ||||
|
39 | class Meta: | |||
|
40 | app_label = 'boards' | |||
|
41 | ||||
|
42 | objects = NotificationManager() | |||
|
43 | ||||
|
44 | post = models.ForeignKey('Post') | |||
|
45 | name = models.TextField() | |||
|
46 |
@@ -1,22 +1,3 b'' | |||||
1 | VERSION = '2.2.3 Miyu' |
|
1 | from boards.default_settings import * | |
2 | SITE_NAME = 'Neboard' |
|
|||
3 |
|
||||
4 | CACHE_TIMEOUT = 600 # Timeout for caching, if cache is used |
|
|||
5 | LOGIN_TIMEOUT = 3600 # Timeout between login tries |
|
|||
6 | MAX_TEXT_LENGTH = 30000 # Max post length in characters |
|
|||
7 | MAX_IMAGE_SIZE = 8 * 1024 * 1024 # Max image size |
|
|||
8 |
|
2 | |||
9 | # Thread bumplimit |
|
3 | # Site-specific settings go here No newline at end of file | |
10 | MAX_POSTS_PER_THREAD = 10 |
|
|||
11 | # Old posts will be archived or deleted if this value is reached |
|
|||
12 | MAX_THREAD_COUNT = 5 |
|
|||
13 | THREADS_PER_PAGE = 3 |
|
|||
14 | DEFAULT_THEME = 'md' |
|
|||
15 | LAST_REPLIES_COUNT = 3 |
|
|||
16 |
|
||||
17 | # Enable archiving threads instead of deletion when the thread limit is reached |
|
|||
18 | ARCHIVE_THREADS = True |
|
|||
19 | # Limit posting speed |
|
|||
20 | LIMIT_POSTING_SPEED = False |
|
|||
21 | # Thread update |
|
|||
22 | WEBSOCKETS_ENABLED = True |
|
@@ -3,7 +3,7 b'' | |||||
3 | font-weight: inherit; |
|
3 | font-weight: inherit; | |
4 | } |
|
4 | } | |
5 |
|
5 | |||
6 | b { |
|
6 | b, strong { | |
7 | font-weight: bold; |
|
7 | font-weight: bold; | |
8 | } |
|
8 | } | |
9 |
|
9 | |||
@@ -344,6 +344,11 b' li {' | |||||
344 | color: #e99d41; |
|
344 | color: #e99d41; | |
345 | float: right; |
|
345 | float: right; | |
346 | font-weight: bold; |
|
346 | font-weight: bold; | |
|
347 | opacity: 0.4; | |||
|
348 | } | |||
|
349 | ||||
|
350 | .moderator_info:hover { | |||
|
351 | opacity: 1; | |||
347 | } |
|
352 | } | |
348 |
|
353 | |||
349 | .refmap { |
|
354 | .refmap { | |
@@ -444,7 +449,6 b' pre {' | |||||
444 |
|
449 | |||
445 | .tag_item { |
|
450 | .tag_item { | |
446 | display: inline-block; |
|
451 | display: inline-block; | |
447 | border: 1px dashed #666; |
|
|||
448 | margin: 0.2ex; |
|
452 | margin: 0.2ex; | |
449 | padding: 0.1ex; |
|
453 | padding: 0.1ex; | |
450 | } |
|
454 | } | |
@@ -495,3 +499,19 b' ul {' | |||||
495 | .hljs, .hljs-subst, .hljs-tag .hljs-title, .lisp .hljs-title, .clojure .hljs-built_in, .nginx .hljs-title { |
|
499 | .hljs, .hljs-subst, .hljs-tag .hljs-title, .lisp .hljs-title, .clojure .hljs-built_in, .nginx .hljs-title { | |
496 | color: #fff; |
|
500 | color: #fff; | |
497 | } |
|
501 | } | |
|
502 | ||||
|
503 | #up { | |||
|
504 | position: fixed; | |||
|
505 | bottom: 5px; | |||
|
506 | right: 5px; | |||
|
507 | border: 1px solid #777; | |||
|
508 | background: #000; | |||
|
509 | padding: 4px; | |||
|
510 | } | |||
|
511 | ||||
|
512 | .user-cast { | |||
|
513 | border: solid #ffffff 1px; | |||
|
514 | padding: .2ex; | |||
|
515 | background: #152154; | |||
|
516 | color: #fff; | |||
|
517 | } No newline at end of file |
@@ -369,3 +369,20 b' li {' | |||||
369 | #id_q { |
|
369 | #id_q { | |
370 | margin-left: 1ex; |
|
370 | margin-left: 1ex; | |
371 | } |
|
371 | } | |
|
372 | ||||
|
373 | .br { | |||
|
374 | margin-top: 0.5em; | |||
|
375 | margin-bottom: 0.5em; | |||
|
376 | } | |||
|
377 | ||||
|
378 | .message, .refmap { | |||
|
379 | margin-top: .5em; | |||
|
380 | } | |||
|
381 | ||||
|
382 | .user-cast { | |||
|
383 | padding: 0.2em .5ex; | |||
|
384 | background: #008; | |||
|
385 | color: #FFF; | |||
|
386 | display: inline-block; | |||
|
387 | text-decoration: none; | |||
|
388 | } No newline at end of file |
@@ -1,5 +1,3 b'' | |||||
1 | var isCompact = false; |
|
|||
2 |
|
||||
3 |
|
|
1 | $('input[name=image]').wrap($('<div class="file_wrap"></div>')); | |
4 |
|
2 | |||
5 | $('body').on('change', 'input[name=image]', function(event) { |
|
3 | $('body').on('change', 'input[name=image]', function(event) { | |
@@ -21,21 +19,9 b' var isCompact = false;' | |||||
21 | } |
|
19 | } | |
22 | }); |
|
20 | }); | |
23 |
|
21 | |||
24 |
var f |
|
22 | var form = $('#form'); | |
25 |
|
23 | $('textarea').keypress(function(event) { | ||
26 | function swapForm() { |
|
24 | if (event.which == 13 && event.ctrlKey) { | |
27 | if (isCompact) { |
|
25 | form.submit(); | |
28 | // TODO Use IDs (change the django form code) instead of absolute numbers |
|
|||
29 | fullForm.find('textarea').appendTo(fullForm.find('.form-row')[4].children[0]); |
|
|||
30 | fullForm.find('.file_wrap').appendTo(fullForm.find('.form-row')[7].children[0]); |
|
|||
31 | fullForm.find('.form-row').show(); |
|
|||
32 |
|
||||
33 | scrollToBottom(); |
|
|||
34 | } else { |
|
|||
35 | fullForm.find('textarea').appendTo($('.compact-form-text')); |
|
|||
36 | fullForm.find('.file_wrap').insertBefore($('.compact-form-text')); |
|
|||
37 | fullForm.find('.form-row').hide(); |
|
|||
38 | fullForm.find('input[type=text]').val(''); |
|
|||
39 | } |
|
26 | } | |
40 | isCompact = !isCompact; |
|
27 | }); No newline at end of file | |
41 | } |
|
@@ -23,6 +23,8 b'' | |||||
23 | for the JavaScript code in this page. |
|
23 | for the JavaScript code in this page. | |
24 | */ |
|
24 | */ | |
25 |
|
25 | |||
|
26 | var $html = $("html, body"); | |||
|
27 | ||||
26 | function moveCaretToEnd(el) { |
|
28 | function moveCaretToEnd(el) { | |
27 | if (typeof el.selectionStart == "number") { |
|
29 | if (typeof el.selectionStart == "number") { | |
28 | el.selectionStart = el.selectionEnd = el.value.length; |
|
30 | el.selectionStart = el.selectionEnd = el.value.length; | |
@@ -48,14 +50,9 b' function addQuickReply(postId) {' | |||||
48 | $(textAreaId).focus(); |
|
50 | $(textAreaId).focus(); | |
49 | moveCaretToEnd(textarea); |
|
51 | moveCaretToEnd(textarea); | |
50 |
|
52 | |||
51 |
$ |
|
53 | $html.animate({ scrollTop: $(textAreaId).offset().top }, "slow"); | |
52 | } |
|
54 | } | |
53 |
|
55 | |||
54 | function scrollToBottom() { |
|
56 | function scrollToBottom() { | |
55 | var $target = $('html,body'); |
|
57 | $html.animate({scrollTop: $html.height()}, "fast"); | |
56 | $target.animate({scrollTop: $target.height()}, "fast"); |
|
58 | } No newline at end of file | |
57 | } |
|
|||
58 |
|
||||
59 | $(document).ready(function() { |
|
|||
60 | swapForm(); |
|
|||
61 | }) |
|
@@ -25,7 +25,6 b'' | |||||
25 |
|
25 | |||
26 | var wsUser = ''; |
|
26 | var wsUser = ''; | |
27 |
|
27 | |||
28 | var loading = false; |
|
|||
29 | var unreadPosts = 0; |
|
28 | var unreadPosts = 0; | |
30 | var documentOriginalTitle = ''; |
|
29 | var documentOriginalTitle = ''; | |
31 |
|
30 | |||
@@ -67,7 +66,7 b' function connectWebsocket() {' | |||||
67 |
|
66 | |||
68 | // For the case we closed the browser and missed some updates |
|
67 | // For the case we closed the browser and missed some updates | |
69 | getThreadDiff(); |
|
68 | getThreadDiff(); | |
70 |
$('#autoupdate'). |
|
69 | $('#autoupdate').hide(); | |
71 | }); |
|
70 | }); | |
72 |
|
71 | |||
73 | centrifuge.connect(); |
|
72 | centrifuge.connect(); | |
@@ -94,8 +93,6 b' function getThreadDiff() {' | |||||
94 | var post = $(postText); |
|
93 | var post = $(postText); | |
95 |
|
94 | |||
96 | updatePost(post) |
|
95 | updatePost(post) | |
97 |
|
||||
98 | lastPost = post; |
|
|||
99 | } |
|
96 | } | |
100 |
|
97 | |||
101 | var updatedPosts = data.updated; |
|
98 | var updatedPosts = data.updated; | |
@@ -297,8 +294,7 b' function showAsErrors(form, text) {' | |||||
297 | form.children('.form-errors').remove(); |
|
294 | form.children('.form-errors').remove(); | |
298 |
|
295 | |||
299 | if (text.length > 0) { |
|
296 | if (text.length > 0) { | |
300 | var errorList = $('<div class="form-errors">' + text |
|
297 | var errorList = $('<div class="form-errors">' + text + '<div>'); | |
301 | + '<div>'); |
|
|||
302 | errorList.appendTo(form); |
|
298 | errorList.appendTo(form); | |
303 | } |
|
299 | } | |
304 | } |
|
300 | } | |
@@ -331,4 +327,6 b' function processNewPost(post) {' | |||||
331 |
|
327 | |||
332 | resetForm(form); |
|
328 | resetForm(form); | |
333 | } |
|
329 | } | |
|
330 | ||||
|
331 | $('#autoupdate').click(getThreadDiff); | |||
334 | }); |
|
332 | }); |
@@ -28,14 +28,19 b'' | |||||
28 |
|
28 | |||
29 | <div class="navigation_panel header"> |
|
29 | <div class="navigation_panel header"> | |
30 | <a class="link" href="{% url 'index' %}">{% trans "All threads" %}</a> |
|
30 | <a class="link" href="{% url 'index' %}">{% trans "All threads" %}</a> | |
31 | {% for tag in tags %} |
|
31 | {% autoescape off %} | |
32 | {% autoescape off %} |
|
32 | {% for tag in tags %} | |
33 |
{{ tag.get_view }} |
|
33 | {{ tag.get_view }}, | |
34 |
{% end |
|
34 | {% endfor %} | |
35 |
{% end |
|
35 | {% endautoescape %} | |
36 | <a href="{% url 'tags' %}" title="{% trans 'Tag management' %}" |
|
36 | <a href="{% url 'tags' %}" title="{% trans 'Tag management' %}" | |
37 | >[...]</a>, |
|
37 | >[...]</a>, | |
38 |
|
|
38 | <a href="{% url 'search' %}" title="{% trans 'Search' %}">[S]</a>. | |
|
39 | ||||
|
40 | {% if username %} | |||
|
41 | <a href="{% url 'notifications' username %}" title="{% trans 'Notifications' %}"><b>{{ new_notifications_count }}</b> {% trans 'notifications' %}</a>. | |||
|
42 | {% endif %} | |||
|
43 | ||||
39 | <a class="link" href="{% url 'settings' %}">{% trans 'Settings' %}</a> |
|
44 | <a class="link" href="{% url 'settings' %}">{% trans 'Settings' %}</a> | |
40 | </div> |
|
45 | </div> | |
41 |
|
46 | |||
@@ -53,7 +58,7 b'' | |||||
53 | {% with ppd=posts_per_day|floatformat:2 %} |
|
58 | {% with ppd=posts_per_day|floatformat:2 %} | |
54 | {% blocktrans %}Speed: {{ ppd }} posts per day{% endblocktrans %} |
|
59 | {% blocktrans %}Speed: {{ ppd }} posts per day{% endblocktrans %} | |
55 | {% endwith %} |
|
60 | {% endwith %} | |
56 | <a class="link" href="#top">{% trans 'Up' %}</a> |
|
61 | <a class="link" href="#top" id="up">{% trans 'Up' %}</a> | |
57 | </div> |
|
62 | </div> | |
58 |
|
63 | |||
59 | </body> |
|
64 | </body> |
@@ -3,35 +3,27 b'' | |||||
3 | {% load board %} |
|
3 | {% load board %} | |
4 | {% load i18n %} |
|
4 | {% load i18n %} | |
5 |
|
5 | |||
|
6 | {% block head %} | |||
|
7 | <title>{{ site_name }} - {% trans 'Notifications' %} - {{ notification_username }}</title> | |||
|
8 | {% endblock %} | |||
|
9 | ||||
6 | {% block content %} |
|
10 | {% block content %} | |
7 | <div class="post-form-w"> |
|
11 | <div class="tag_info"><a href="{% url 'notifications' notification_username %}" class="user-cast">@{{ notification_username }}</a></div> | |
8 | <div class="post-form"> |
|
|||
9 | <h3>{% trans 'Search' %}</h3> |
|
|||
10 | <form method="get" action=""> |
|
|||
11 | {{ form.as_div }} |
|
|||
12 | <div class="form-submit"> |
|
|||
13 | <input type="submit" value="{% trans 'Search' %}"> |
|
|||
14 | </div> |
|
|||
15 | </form> |
|
|||
16 | </div> |
|
|||
17 | </div> |
|
|||
18 |
|
12 | |||
19 | {% if page %} |
|
13 | {% if page %} | |
20 | {% if page.has_previous %} |
|
14 | {% if page.has_previous %} | |
21 | <div class="page_link"> |
|
15 | <div class="page_link"> | |
22 |
<a href="? |
|
16 | <a href="?page={{ page.previous_page_number }}">{% trans "Previous page" %}</a> | |
23 | </a> |
|
|||
24 | </div> |
|
17 | </div> | |
25 | {% endif %} |
|
18 | {% endif %} | |
26 |
|
19 | |||
27 |
{% for |
|
20 | {% for post in page.object_list %} | |
28 | {{ result.object.get_search_view }} |
|
21 | {% post_view post %} | |
29 | {% endfor %} |
|
22 | {% endfor %} | |
30 |
|
23 | |||
31 | {% if page.has_next %} |
|
24 | {% if page.has_next %} | |
32 | <div class="page_link"> |
|
25 | <div class="page_link"> | |
33 |
<a href="? |
|
26 | <a href="?page={{ page.next_page_number }}">{% trans "Next page" %}</a> | |
34 | </a> |
|
|||
35 | </div> |
|
27 | </div> | |
36 | {% endif %} |
|
28 | {% endif %} | |
37 | {% endif %} |
|
29 | {% endif %} |
@@ -13,12 +13,12 b'' | |||||
13 | {% endif %} |
|
13 | {% endif %} | |
14 |
|
14 | |||
15 | <div class="post-info"> |
|
15 | <div class="post-info"> | |
16 |
<a class="post_id" href="{ |
|
16 | <a class="post_id" href="{{ post.get_url }}" | |
17 | {% if not truncated and not thread.archived %} |
|
17 | {% if not truncated and not thread.archived %} | |
18 | onclick="javascript:addQuickReply('{{ post.id }}'); return false;" |
|
18 | onclick="javascript:addQuickReply('{{ post.id }}'); return false;" | |
19 | title="{% trans 'Quote' %}" {% endif %}>({{ post.id }})</a> |
|
19 | title="{% trans 'Quote' %}" {% endif %}>({{ post.get_absolute_id }})</a> | |
20 | <span class="title">{{ post.title }}</span> |
|
20 | <span class="title">{{ post.title }}</span> | |
21 | <span class="pub_time">{{ post.pub_time }}</span> |
|
21 | <span class="pub_time">{{ post.pub_time|date:'r' }}</span> | |
22 | {% comment %} |
|
22 | {% comment %} | |
23 | Thread death time needs to be shown only if the thread is alredy archived |
|
23 | Thread death time needs to be shown only if the thread is alredy archived | |
24 | and this is an opening post (thread death time) or a post for popup |
|
24 | and this is an opening post (thread death time) or a post for popup | |
@@ -68,7 +68,7 b'' | |||||
68 | <div class="message"> |
|
68 | <div class="message"> | |
69 | {% autoescape off %} |
|
69 | {% autoescape off %} | |
70 | {% if truncated %} |
|
70 | {% if truncated %} | |
71 | {{ post.get_text|truncatewords_html:50 }} |
|
71 | {{ post.get_text|truncatewords_html:50|truncate_lines:3 }} | |
72 | {% else %} |
|
72 | {% else %} | |
73 | {{ post.get_text }} |
|
73 | {{ post.get_text }} | |
74 | {% endif %} |
|
74 | {% endif %} | |
@@ -91,11 +91,11 b'' | |||||
91 | {{ thread.get_images_count }} {% trans 'images' %}. |
|
91 | {{ thread.get_images_count }} {% trans 'images' %}. | |
92 | {% endif %} |
|
92 | {% endif %} | |
93 | <span class="tags"> |
|
93 | <span class="tags"> | |
94 | {% for tag in thread.get_tags %} |
|
94 | {% autoescape off %} | |
95 |
{% |
|
95 | {% for tag in thread.get_tags %} | |
96 | {{ tag.get_view }}{% if not forloop.last %},{% endif %} |
|
96 | {{ tag.get_view }}{% if not forloop.last %},{% endif %} | |
97 |
{% end |
|
97 | {% endfor %} | |
98 |
{% end |
|
98 | {% endautoescape %} | |
99 | </span> |
|
99 | </span> | |
100 | </div> |
|
100 | </div> | |
101 | {% endif %} |
|
101 | {% endif %} |
@@ -16,8 +16,6 b'' | |||||
16 | <link rel="prev" href=" |
|
16 | <link rel="prev" href=" | |
17 | {% if tag %} |
|
17 | {% if tag %} | |
18 | {% url "tag" tag_name=tag.name page=current_page.previous_page_number %} |
|
18 | {% url "tag" tag_name=tag.name page=current_page.previous_page_number %} | |
19 | {% elif archived %} |
|
|||
20 | {% url "archive" page=current_page.previous_page_number %} |
|
|||
21 | {% else %} |
|
19 | {% else %} | |
22 | {% url "index" page=current_page.previous_page_number %} |
|
20 | {% url "index" page=current_page.previous_page_number %} | |
23 | {% endif %} |
|
21 | {% endif %} | |
@@ -27,8 +25,6 b'' | |||||
27 | <link rel="next" href=" |
|
25 | <link rel="next" href=" | |
28 | {% if tag %} |
|
26 | {% if tag %} | |
29 | {% url "tag" tag_name=tag.name page=current_page.next_page_number %} |
|
27 | {% url "tag" tag_name=tag.name page=current_page.next_page_number %} | |
30 | {% elif archived %} |
|
|||
31 | {% url "archive" page=current_page.next_page_number %} |
|
|||
32 | {% else %} |
|
28 | {% else %} | |
33 | {% url "index" page=current_page.next_page_number %} |
|
29 | {% url "index" page=current_page.next_page_number %} | |
34 | {% endif %} |
|
30 | {% endif %} | |
@@ -44,14 +40,14 b'' | |||||
44 | {% if tag %} |
|
40 | {% if tag %} | |
45 | <div class="tag_info"> |
|
41 | <div class="tag_info"> | |
46 | <h2> |
|
42 | <h2> | |
47 |
{% if |
|
43 | {% if is_favorite %} | |
48 | <a href="{% url 'tag' tag.name %}?method=unsubscribe&next={{ request.path }}" |
|
44 | <a href="{% url 'tag' tag.name %}?method=unsubscribe&next={{ request.path }}" | |
49 | class="fav" rel="nofollow">★</a> |
|
45 | class="fav" rel="nofollow">★</a> | |
50 | {% else %} |
|
46 | {% else %} | |
51 | <a href="{% url 'tag' tag.name %}?method=subscribe&next={{ request.path }}" |
|
47 | <a href="{% url 'tag' tag.name %}?method=subscribe&next={{ request.path }}" | |
52 | class="not_fav" rel="nofollow">★</a> |
|
48 | class="not_fav" rel="nofollow">★</a> | |
53 | {% endif %} |
|
49 | {% endif %} | |
54 |
{% if |
|
50 | {% if is_hidden %} | |
55 | <a href="{% url 'tag' tag.name %}?method=unhide&next={{ request.path }}" |
|
51 | <a href="{% url 'tag' tag.name %}?method=unhide&next={{ request.path }}" | |
56 | title="{% trans 'Show tag' %}" |
|
52 | title="{% trans 'Show tag' %}" | |
57 | class="fav" rel="nofollow">H</a> |
|
53 | class="fav" rel="nofollow">H</a> | |
@@ -64,9 +60,10 b'' | |||||
64 | {{ tag.get_view }} |
|
60 | {{ tag.get_view }} | |
65 | {% endautoescape %} |
|
61 | {% endautoescape %} | |
66 | {% if moderator %} |
|
62 | {% if moderator %} | |
67 |
|
|
63 | <span class="moderator_info">[<a href="{% url 'admin:boards_tag_change' tag.id %}">{% trans 'Edit tag' %}</a>]</span> | |
68 | {% endif %} |
|
64 | {% endif %} | |
69 | </h2> |
|
65 | </h2> | |
|
66 | <p>{% blocktrans with thread_count=tag.get_thread_count post_count=tag.get_post_count %}This tag has {{ thread_count }} threads and {{ post_count }} posts.{% endblocktrans %}</p> | |||
70 | </div> |
|
67 | </div> | |
71 | {% endif %} |
|
68 | {% endif %} | |
72 |
|
69 | |||
@@ -76,8 +73,6 b'' | |||||
76 | <a href=" |
|
73 | <a href=" | |
77 | {% if tag %} |
|
74 | {% if tag %} | |
78 | {% url "tag" tag_name=tag.name page=current_page.previous_page_number %} |
|
75 | {% url "tag" tag_name=tag.name page=current_page.previous_page_number %} | |
79 | {% elif archived %} |
|
|||
80 | {% url "archive" page=current_page.previous_page_number %} |
|
|||
81 | {% else %} |
|
76 | {% else %} | |
82 | {% url "index" page=current_page.previous_page_number %} |
|
77 | {% url "index" page=current_page.previous_page_number %} | |
83 | {% endif %} |
|
78 | {% endif %} | |
@@ -88,8 +83,7 b'' | |||||
88 | {% for thread in threads %} |
|
83 | {% for thread in threads %} | |
89 | {% cache 600 thread_short thread.id thread.last_edit_time moderator LANGUAGE_CODE %} |
|
84 | {% cache 600 thread_short thread.id thread.last_edit_time moderator LANGUAGE_CODE %} | |
90 | <div class="thread"> |
|
85 | <div class="thread"> | |
91 | {% with can_bump=thread.can_bump %} |
|
86 | {% post_view thread.get_opening_post moderator is_opening=True thread=thread truncated=True need_open_link=True %} | |
92 | {% post_view thread.get_opening_post moderator is_opening=True thread=thread can_bump=can_bump truncated=True need_open_link=True %} |
|
|||
93 | {% if not thread.archived %} |
|
87 | {% if not thread.archived %} | |
94 | {% with last_replies=thread.get_last_replies %} |
|
88 | {% with last_replies=thread.get_last_replies %} | |
95 | {% if last_replies %} |
|
89 | {% if last_replies %} | |
@@ -102,13 +96,12 b'' | |||||
102 | {% endif %} |
|
96 | {% endif %} | |
103 | <div class="last-replies"> |
|
97 | <div class="last-replies"> | |
104 | {% for post in last_replies %} |
|
98 | {% for post in last_replies %} | |
105 |
{% post_view post |
|
99 | {% post_view post is_opening=False moderator=moderator truncated=True %} | |
106 | {% endfor %} |
|
100 | {% endfor %} | |
107 | </div> |
|
101 | </div> | |
108 | {% endif %} |
|
102 | {% endif %} | |
109 | {% endwith %} |
|
103 | {% endwith %} | |
110 | {% endif %} |
|
104 | {% endif %} | |
111 | {% endwith %} |
|
|||
112 | </div> |
|
105 | </div> | |
113 | {% endcache %} |
|
106 | {% endcache %} | |
114 | {% endfor %} |
|
107 | {% endfor %} | |
@@ -118,8 +111,6 b'' | |||||
118 | <a href=" |
|
111 | <a href=" | |
119 | {% if tag %} |
|
112 | {% if tag %} | |
120 | {% url "tag" tag_name=tag.name page=current_page.next_page_number %} |
|
113 | {% url "tag" tag_name=tag.name page=current_page.next_page_number %} | |
121 | {% elif archived %} |
|
|||
122 | {% url "archive" page=current_page.next_page_number %} |
|
|||
123 | {% else %} |
|
114 | {% else %} | |
124 | {% url "index" page=current_page.next_page_number %} |
|
115 | {% url "index" page=current_page.next_page_number %} | |
125 | {% endif %} |
|
116 | {% endif %} | |
@@ -136,11 +127,12 b'' | |||||
136 | <div class="post-form"> |
|
127 | <div class="post-form"> | |
137 | <div class="form-title">{% trans "Create new thread" %}</div> |
|
128 | <div class="form-title">{% trans "Create new thread" %}</div> | |
138 | <div class="swappable-form-full"> |
|
129 | <div class="swappable-form-full"> | |
139 | <form enctype="multipart/form-data" method="post">{% csrf_token %} |
|
130 | <form enctype="multipart/form-data" method="post"id="form">{% csrf_token %} | |
140 | {{ form.as_div }} |
|
131 | {{ form.as_div }} | |
141 | <div class="form-submit"> |
|
132 | <div class="form-submit"> | |
142 | <input type="submit" value="{% trans "Post" %}"/> |
|
133 | <input type="submit" value="{% trans "Post" %}"/> | |
143 | </div> |
|
134 | </div> | |
|
135 | (ctrl-enter) | |||
144 | </form> |
|
136 | </form> | |
145 | </div> |
|
137 | </div> | |
146 | <div> |
|
138 | <div> | |
@@ -163,8 +155,6 b'' | |||||
163 | <a href=" |
|
155 | <a href=" | |
164 | {% if tag %} |
|
156 | {% if tag %} | |
165 | {% url "tag" tag_name=tag.name page=paginator.page_range|first %} |
|
157 | {% url "tag" tag_name=tag.name page=paginator.page_range|first %} | |
166 | {% elif archived %} |
|
|||
167 | {% url "archive" page=paginator.page_range|first %} |
|
|||
168 | {% else %} |
|
158 | {% else %} | |
169 | {% url "index" page=paginator.page_range|first %} |
|
159 | {% url "index" page=paginator.page_range|first %} | |
170 | {% endif %} |
|
160 | {% endif %} | |
@@ -178,8 +168,6 b'' | |||||
178 | href=" |
|
168 | href=" | |
179 | {% if tag %} |
|
169 | {% if tag %} | |
180 | {% url "tag" tag_name=tag.name page=page %} |
|
170 | {% url "tag" tag_name=tag.name page=page %} | |
181 | {% elif archived %} |
|
|||
182 | {% url "archive" page=page %} |
|
|||
183 | {% else %} |
|
171 | {% else %} | |
184 | {% url "index" page=page %} |
|
172 | {% url "index" page=page %} | |
185 | {% endif %} |
|
173 | {% endif %} | |
@@ -190,8 +178,6 b'' | |||||
190 | <a href=" |
|
178 | <a href=" | |
191 | {% if tag %} |
|
179 | {% if tag %} | |
192 | {% url "tag" tag_name=tag.name page=paginator.page_range|last %} |
|
180 | {% url "tag" tag_name=tag.name page=paginator.page_range|last %} | |
193 | {% elif archived %} |
|
|||
194 | {% url "archive" page=paginator.page_range|last %} |
|
|||
195 | {% else %} |
|
181 | {% else %} | |
196 | {% url "index" page=paginator.page_range|last %} |
|
182 | {% url "index" page=paginator.page_range|last %} | |
197 | {% endif %} |
|
183 | {% endif %} |
@@ -18,8 +18,9 b'' | |||||
18 | {% if hidden_tags %} |
|
18 | {% if hidden_tags %} | |
19 | <p>{% trans 'Hidden tags:' %} |
|
19 | <p>{% trans 'Hidden tags:' %} | |
20 | {% for tag in hidden_tags %} |
|
20 | {% for tag in hidden_tags %} | |
21 | <a class="tag" href="{% url 'tag' tag.name %}"> |
|
21 | {% autoescape off %} | |
22 | #{{ tag.name }}</a>{% if not forloop.last %},{% endif %} |
|
22 | {{ tag.get_view }} | |
|
23 | {% endautoescape %} | |||
23 | {% endfor %} |
|
24 | {% endfor %} | |
24 | </p> |
|
25 | </p> | |
25 | {% else %} |
|
26 | {% else %} |
@@ -17,7 +17,7 b'' | |||||
17 |
|
17 | |||
18 | <div class="image-mode-tab"> |
|
18 | <div class="image-mode-tab"> | |
19 | <a class="current_mode" href="{% url 'thread' opening_post.id %}">{% trans 'Normal mode' %}</a>, |
|
19 | <a class="current_mode" href="{% url 'thread' opening_post.id %}">{% trans 'Normal mode' %}</a>, | |
20 |
<a href="{% url 'thread_ |
|
20 | <a href="{% url 'thread_gallery' opening_post.id %}">{% trans 'Gallery mode' %}</a> | |
21 | </div> |
|
21 | </div> | |
22 |
|
22 | |||
23 | {% if bumpable %} |
|
23 | {% if bumpable %} | |
@@ -34,30 +34,27 b'' | |||||
34 | {% with can_bump=thread.can_bump %} |
|
34 | {% with can_bump=thread.can_bump %} | |
35 | {% for post in thread.get_replies %} |
|
35 | {% for post in thread.get_replies %} | |
36 | {% with is_opening=forloop.first %} |
|
36 | {% with is_opening=forloop.first %} | |
37 |
{% post_view post moderator=moderator is_opening=is_opening |
|
37 | {% post_view post moderator=moderator is_opening=is_opening bumpable=can_bump opening_post_id=opening_post.id %} | |
38 | {% endwith %} |
|
38 | {% endwith %} | |
39 | {% endfor %} |
|
39 | {% endfor %} | |
40 | {% endwith %} |
|
40 | {% endwith %} | |
41 | </div> |
|
41 | </div> | |
42 |
|
42 | |||
43 | {% if not thread.archived %} |
|
43 | {% if not thread.archived %} | |
44 |
<div class="post-form-w" |
|
44 | <div class="post-form-w"> | |
45 | <script src="{% static 'js/panel.js' %}"></script> |
|
45 | <script src="{% static 'js/panel.js' %}"></script> | |
46 | <div class="form-title">{% trans "Reply to thread" %} #{{ opening_post.id }}</div> |
|
46 | <div class="form-title">{% trans "Reply to thread" %} #{{ opening_post.id }}</div> | |
47 | <div class="post-form" id="compact-form"> |
|
47 | <div class="post-form" id="compact-form"> | |
48 | <div class="swappable-form-full"> |
|
48 | <div class="swappable-form-full"> | |
49 | <form enctype="multipart/form-data" method="post" |
|
49 | <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %} | |
50 | >{% csrf_token %} |
|
|||
51 | <div class="compact-form-text"></div> |
|
50 | <div class="compact-form-text"></div> | |
52 | {{ form.as_div }} |
|
51 | {{ form.as_div }} | |
53 | <div class="form-submit"> |
|
52 | <div class="form-submit"> | |
54 | <input type="submit" value="{% trans "Post" %}"/> |
|
53 | <input type="submit" value="{% trans "Post" %}"/> | |
55 | </div> |
|
54 | </div> | |
|
55 | (ctrl-enter) | |||
56 | </form> |
|
56 | </form> | |
57 | </div> |
|
57 | </div> | |
58 | <a onclick="swapForm(); return false;" href="#"> |
|
|||
59 | {% trans 'Switch mode' %} |
|
|||
60 | </a> |
|
|||
61 | <div><a href="{% url "staticpage" name="help" %}"> |
|
58 | <div><a href="{% url "staticpage" name="help" %}"> | |
62 | {% trans 'Text syntax' %}</a></div> |
|
59 | {% trans 'Text syntax' %}</a></div> | |
63 | </div> |
|
60 | </div> | |
@@ -85,10 +82,10 b'' | |||||
85 | data-ws-host="{{ ws_host }}" |
|
82 | data-ws-host="{{ ws_host }}" | |
86 | data-ws-port="{{ ws_port }}"> |
|
83 | data-ws-port="{{ ws_port }}"> | |
87 | {% cache 600 thread_meta thread.last_edit_time moderator LANGUAGE_CODE %} |
|
84 | {% cache 600 thread_meta thread.last_edit_time moderator LANGUAGE_CODE %} | |
88 |
< |
|
85 | <button id="autoupdate">{% trans 'Update' %}</button> | |
89 | <span id="reply-count">{{ thread.get_reply_count }}</span>/{{ max_replies }} {% trans 'messages' %}, |
|
86 | <span id="reply-count">{{ thread.get_reply_count }}</span>/{{ max_replies }} {% trans 'messages' %}, | |
90 | <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}. |
|
87 | <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}. | |
91 | {% trans 'Last update: ' %}<span id="last-update">{{ thread.last_edit_time }}</span> |
|
88 | {% trans 'Last update: ' %}<span id="last-update">{{ thread.last_edit_time|date:'r' }}</span> | |
92 | [<a href="rss/">RSS</a>] |
|
89 | [<a href="rss/">RSS</a>] | |
93 | {% endcache %} |
|
90 | {% endcache %} | |
94 | </span> |
|
91 | </span> |
@@ -17,7 +17,7 b'' | |||||
17 | {% cache 600 thread_gallery_view thread.id thread.last_edit_time LANGUAGE_CODE request.get_host %} |
|
17 | {% cache 600 thread_gallery_view thread.id thread.last_edit_time LANGUAGE_CODE request.get_host %} | |
18 | <div class="image-mode-tab"> |
|
18 | <div class="image-mode-tab"> | |
19 | <a href="{% url 'thread' thread.get_opening_post.id %}">{% trans 'Normal mode' %}</a>, |
|
19 | <a href="{% url 'thread' thread.get_opening_post.id %}">{% trans 'Normal mode' %}</a>, | |
20 |
<a class="current_mode" href="{% url 'thread_ |
|
20 | <a class="current_mode" href="{% url 'thread_gallery' thread.get_opening_post.id %}">{% trans 'Gallery mode' %}</a> | |
21 | </div> |
|
21 | </div> | |
22 |
|
22 | |||
23 | <div id="posts-table"> |
|
23 | <div id="posts-table"> | |
@@ -54,11 +54,11 b'' | |||||
54 | {% get_current_language as LANGUAGE_CODE %} |
|
54 | {% get_current_language as LANGUAGE_CODE %} | |
55 |
|
55 | |||
56 | <span class="metapanel" data-last-update="{{ last_update }}"> |
|
56 | <span class="metapanel" data-last-update="{{ last_update }}"> | |
57 | {% cache 600 thread_meta thread.last_edit_time moderator LANGUAGE_CODE %} |
|
57 | {% cache 600 thread_gallery_meta thread.last_edit_time moderator LANGUAGE_CODE %} | |
58 | <span id="reply-count">{{ thread.get_reply_count }}</span>/{{ max_replies }} |
|
58 | <span id="reply-count">{{ thread.get_reply_count }}</span>/{{ max_replies }} | |
59 | {% trans 'messages' %}, |
|
59 | {% trans 'messages' %}, | |
60 | <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}. |
|
60 | <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}. | |
61 | {% trans 'Last update: ' %}{{ thread.last_edit_time }} |
|
61 | {% trans 'Last update: ' %}{{ thread.last_edit_time|date:'r' }} | |
62 | [<a href="rss/">RSS</a>] |
|
62 | [<a href="rss/">RSS</a>] | |
63 | {% endcache %} |
|
63 | {% endcache %} | |
64 | </span> |
|
64 | </span> |
@@ -1,6 +1,12 b'' | |||||
|
1 | import re | |||
1 | from django.shortcuts import get_object_or_404 |
|
2 | from django.shortcuts import get_object_or_404 | |
2 | from django import template |
|
3 | from django import template | |
3 |
|
4 | |||
|
5 | ELLIPSIZER = '...' | |||
|
6 | ||||
|
7 | REGEX_LINES = re.compile(r'(<div class="br"></div>)', re.U | re.S) | |||
|
8 | REGEX_TAG = re.compile(r'<(/)?([^ ]+?)(?:(\s*/)| .*?)?>', re.S) | |||
|
9 | ||||
4 |
|
10 | |||
5 | register = template.Library() |
|
11 | register = template.Library() | |
6 |
|
12 | |||
@@ -25,18 +31,6 b' def post_url(*args, **kwargs):' | |||||
25 | return post.get_url() |
|
31 | return post.get_url() | |
26 |
|
32 | |||
27 |
|
33 | |||
28 | @register.simple_tag(name='post_object_url') |
|
|||
29 | def post_object_url(*args, **kwargs): |
|
|||
30 | post = args[0] |
|
|||
31 |
|
||||
32 | if 'thread' in kwargs: |
|
|||
33 | post_thread = kwargs['thread'] |
|
|||
34 | else: |
|
|||
35 | post_thread = None |
|
|||
36 |
|
||||
37 | return post.get_url(thread=post_thread) |
|
|||
38 |
|
||||
39 |
|
||||
40 | @register.simple_tag(name='image_actions') |
|
34 | @register.simple_tag(name='image_actions') | |
41 | def image_actions(*args, **kwargs): |
|
35 | def image_actions(*args, **kwargs): | |
42 | image_link = args[0] |
|
36 | image_link = args[0] | |
@@ -65,10 +59,7 b' def post_view(post, moderator=False, nee' | |||||
65 | else: |
|
59 | else: | |
66 | is_opening = post.is_opening() |
|
60 | is_opening = post.is_opening() | |
67 |
|
61 | |||
68 | if 'thread' in kwargs: |
|
62 | thread = post.get_thread() | |
69 | thread = kwargs['thread'] |
|
|||
70 | else: |
|
|||
71 | thread = post.get_thread() |
|
|||
72 |
|
63 | |||
73 | if 'can_bump' in kwargs: |
|
64 | if 'can_bump' in kwargs: | |
74 | can_bump = kwargs['can_bump'] |
|
65 | can_bump = kwargs['can_bump'] | |
@@ -87,3 +78,68 b' def post_view(post, moderator=False, nee' | |||||
87 | 'truncated': truncated, |
|
78 | 'truncated': truncated, | |
88 | 'opening_post_id': opening_post_id, |
|
79 | 'opening_post_id': opening_post_id, | |
89 | } |
|
80 | } | |
|
81 | ||||
|
82 | ||||
|
83 | @register.filter(is_safe=True) | |||
|
84 | def truncate_lines(text, length): | |||
|
85 | if length <= 0: | |||
|
86 | return '' | |||
|
87 | ||||
|
88 | html4_singlets = ( | |||
|
89 | 'br', 'col', 'link', 'base', 'img', | |||
|
90 | 'param', 'area', 'hr', 'input' | |||
|
91 | ) | |||
|
92 | ||||
|
93 | # Count non-HTML chars/words and keep note of open tags | |||
|
94 | pos = 0 | |||
|
95 | end_text_pos = 0 | |||
|
96 | current_len = 0 | |||
|
97 | open_tags = [] | |||
|
98 | ||||
|
99 | while current_len <= length: | |||
|
100 | m = REGEX_LINES.search(text, pos) | |||
|
101 | if not m: | |||
|
102 | # Checked through whole string | |||
|
103 | break | |||
|
104 | pos = m.end(0) | |||
|
105 | if m.group(1): | |||
|
106 | # It's an actual non-HTML word or char | |||
|
107 | current_len += 1 | |||
|
108 | if current_len == length: | |||
|
109 | end_text_pos = m.start(0) | |||
|
110 | continue | |||
|
111 | # Check for tag | |||
|
112 | tag = REGEX_TAG.match(m.group(0)) | |||
|
113 | if not tag or current_len >= length: | |||
|
114 | # Don't worry about non tags or tags after our truncate point | |||
|
115 | continue | |||
|
116 | closing_tag, tagname, self_closing = tag.groups() | |||
|
117 | # Element names are always case-insensitive | |||
|
118 | tagname = tagname.lower() | |||
|
119 | if self_closing or tagname in html4_singlets: | |||
|
120 | pass | |||
|
121 | elif closing_tag: | |||
|
122 | # Check for match in open tags list | |||
|
123 | try: | |||
|
124 | i = open_tags.index(tagname) | |||
|
125 | except ValueError: | |||
|
126 | pass | |||
|
127 | else: | |||
|
128 | # SGML: An end tag closes, back to the matching start tag, | |||
|
129 | # all unclosed intervening start tags with omitted end tags | |||
|
130 | open_tags = open_tags[i + 1:] | |||
|
131 | else: | |||
|
132 | # Add it to the start of the open tags list | |||
|
133 | open_tags.insert(0, tagname) | |||
|
134 | ||||
|
135 | if current_len <= length: | |||
|
136 | return text | |||
|
137 | out = text[:end_text_pos] | |||
|
138 | ||||
|
139 | if not out.endswith(ELLIPSIZER): | |||
|
140 | out += ELLIPSIZER | |||
|
141 | # Close any tags still open | |||
|
142 | for tag in open_tags: | |||
|
143 | out += '</%s>' % tag | |||
|
144 | # Return string | |||
|
145 | return out |
@@ -1,11 +1,11 b'' | |||||
1 | import simplejson |
|
1 | import simplejson | |
2 |
|
2 | |||
3 | from django.test import TestCase |
|
3 | from django.test import TestCase | |
|
4 | from boards.views import api | |||
4 |
|
5 | |||
5 | from boards.models import Tag, Post |
|
6 | from boards.models import Tag, Post | |
6 | from boards.tests.mocks import MockRequest |
|
7 | from boards.tests.mocks import MockRequest | |
7 | from boards.utils import datetime_to_epoch |
|
8 | from boards.utils import datetime_to_epoch | |
8 | from boards.views.api import api_get_threaddiff |
|
|||
9 |
|
9 | |||
10 |
|
10 | |||
11 | class ApiTest(TestCase): |
|
11 | class ApiTest(TestCase): | |
@@ -17,9 +17,8 b' class ApiTest(TestCase):' | |||||
17 | last_edit_time = datetime_to_epoch(opening_post.last_edit_time) |
|
17 | last_edit_time = datetime_to_epoch(opening_post.last_edit_time) | |
18 |
|
18 | |||
19 | # Check the exact timestamp post was added |
|
19 | # Check the exact timestamp post was added | |
20 |
empty_response = api_get_threaddiff( |
|
20 | empty_response = api.api_get_threaddiff( | |
21 | str(opening_post.thread_new.id), |
|
21 | MockRequest(), str(opening_post.get_thread().id), str(last_edit_time)) | |
22 | str(last_edit_time)) |
|
|||
23 | diff = simplejson.loads(empty_response.content) |
|
22 | diff = simplejson.loads(empty_response.content) | |
24 | self.assertEqual(0, len(diff['added']), |
|
23 | self.assertEqual(0, len(diff['added']), | |
25 | 'There must be no added posts in the diff.') |
|
24 | 'There must be no added posts in the diff.') | |
@@ -28,23 +27,43 b' class ApiTest(TestCase):' | |||||
28 |
|
27 | |||
29 | reply = Post.objects.create_post(title='', |
|
28 | reply = Post.objects.create_post(title='', | |
30 | text='[post]%d[/post]\ntext' % opening_post.id, |
|
29 | text='[post]%d[/post]\ntext' % opening_post.id, | |
31 |
thread=opening_post.thread |
|
30 | thread=opening_post.get_thread()) | |
32 |
|
31 | |||
33 | # Check the timestamp before post was added |
|
32 | # Check the timestamp before post was added | |
34 |
response = api_get_threaddiff( |
|
33 | response = api.api_get_threaddiff( | |
35 | str(opening_post.thread_new.id), |
|
34 | MockRequest(), str(opening_post.get_thread().id), str(last_edit_time)) | |
36 | str(last_edit_time)) |
|
|||
37 | diff = simplejson.loads(response.content) |
|
35 | diff = simplejson.loads(response.content) | |
38 | self.assertEqual(1, len(diff['added']), |
|
36 | self.assertEqual(1, len(diff['added']), | |
39 | 'There must be 1 added posts in the diff.') |
|
37 | 'There must be 1 added posts in the diff.') | |
40 | self.assertEqual(1, len(diff['updated']), |
|
38 | self.assertEqual(1, len(diff['updated']), | |
41 | 'There must be 1 updated posts in the diff.') |
|
39 | 'There must be 1 updated posts in the diff.') | |
42 |
|
40 | |||
43 | empty_response = api_get_threaddiff(MockRequest(), |
|
41 | empty_response = api.api_get_threaddiff(MockRequest(), | |
44 |
str(opening_post.thread |
|
42 | str(opening_post.get_thread().id), | |
45 | str(datetime_to_epoch(reply.last_edit_time))) |
|
43 | str(datetime_to_epoch(reply.last_edit_time))) | |
46 | diff = simplejson.loads(empty_response.content) |
|
44 | diff = simplejson.loads(empty_response.content) | |
47 | self.assertEqual(0, len(diff['added']), |
|
45 | self.assertEqual(0, len(diff['added']), | |
48 | 'There must be no added posts in the diff.') |
|
46 | 'There must be no added posts in the diff.') | |
49 | self.assertEqual(0, len(diff['updated']), |
|
47 | self.assertEqual(0, len(diff['updated']), | |
50 | 'There must be no updated posts in the diff.') No newline at end of file |
|
48 | 'There must be no updated posts in the diff.') | |
|
49 | ||||
|
50 | def test_get_threads(self): | |||
|
51 | # Create 10 threads | |||
|
52 | tag = Tag.objects.create(name='test_tag') | |||
|
53 | for i in range(5): | |||
|
54 | Post.objects.create_post(title='title', text='text', tags=[tag]) | |||
|
55 | ||||
|
56 | # Get all threads | |||
|
57 | response = api.api_get_threads(MockRequest(), 5) | |||
|
58 | diff = simplejson.loads(response.content) | |||
|
59 | self.assertEqual(5, len(diff), 'Invalid thread list response.') | |||
|
60 | ||||
|
61 | # Get less threads then exist | |||
|
62 | response = api.api_get_threads(MockRequest(), 3) | |||
|
63 | diff = simplejson.loads(response.content) | |||
|
64 | self.assertEqual(3, len(diff), 'Invalid thread list response.') | |||
|
65 | ||||
|
66 | # Get more threads then exist | |||
|
67 | response = api.api_get_threads(MockRequest(), 10) | |||
|
68 | diff = simplejson.loads(response.content) | |||
|
69 | self.assertEqual(5, len(diff), 'Invalid thread list response.') |
@@ -25,3 +25,10 b' class ParserTest(TestCase):' | |||||
25 | self.assertEqual('[post]12[/post]\nText', |
|
25 | self.assertEqual('[post]12[/post]\nText', | |
26 | preparsed_text, 'Reflink not preparsed.') |
|
26 | preparsed_text, 'Reflink not preparsed.') | |
27 |
|
27 | |||
|
28 | def preparse_user(self): | |||
|
29 | raw_text = '@user\nuser@example.com\n@user\nuser @user' | |||
|
30 | preparsed_text = Post.objects._preparse_text(raw_text) | |||
|
31 | ||||
|
32 | self.assertEqual('[user]user[/user]\nuser@example.com\n[user]user[/user]\nuser [user]user[/user]', | |||
|
33 | preparsed_text, 'User link not preparsed.') | |||
|
34 |
@@ -7,7 +7,7 b' from boards.models import Tag, Post, Thr' | |||||
7 | class PostTests(TestCase): |
|
7 | class PostTests(TestCase): | |
8 |
|
8 | |||
9 | def _create_post(self): |
|
9 | def _create_post(self): | |
10 | tag = Tag.objects.create(name='test_tag') |
|
10 | tag, created = Tag.objects.get_or_create(name='test_tag') | |
11 | return Post.objects.create_post(title='title', text='text', |
|
11 | return Post.objects.create_post(title='title', text='text', | |
12 | tags=[tag]) |
|
12 | tags=[tag]) | |
13 |
|
13 | |||
@@ -37,7 +37,7 b' class PostTests(TestCase):' | |||||
37 | thread = opening_post.get_thread() |
|
37 | thread = opening_post.get_thread() | |
38 | reply = Post.objects.create_post("", "", thread=thread) |
|
38 | reply = Post.objects.create_post("", "", thread=thread) | |
39 |
|
39 | |||
40 |
|
|
40 | thread.delete() | |
41 |
|
41 | |||
42 | self.assertFalse(Post.objects.filter(id=reply.id).exists(), |
|
42 | self.assertFalse(Post.objects.filter(id=reply.id).exists(), | |
43 | 'Reply was not deleted with the thread.') |
|
43 | 'Reply was not deleted with the thread.') | |
@@ -76,7 +76,7 b' class PostTests(TestCase):' | |||||
76 |
|
76 | |||
77 | thread = opening_post.get_thread() |
|
77 | thread = opening_post.get_thread() | |
78 |
|
78 | |||
79 | self.assertEqual(3, thread.replies.count()) |
|
79 | self.assertEqual(3, thread.get_replies().count()) | |
80 |
|
80 | |||
81 | def test_create_post_with_tag(self): |
|
81 | def test_create_post_with_tag(self): | |
82 | """Test adding tag to post""" |
|
82 | """Test adding tag to post""" |
@@ -6,6 +6,7 b' from boards.views import api, tag_thread' | |||||
6 | settings, all_tags |
|
6 | settings, all_tags | |
7 | from boards.views.authors import AuthorsView |
|
7 | from boards.views.authors import AuthorsView | |
8 | from boards.views.ban import BanUserView |
|
8 | from boards.views.ban import BanUserView | |
|
9 | from boards.views.notifications import NotificationView | |||
9 | from boards.views.search import BoardSearchView |
|
10 | from boards.views.search import BoardSearchView | |
10 | from boards.views.static import StaticPageView |
|
11 | from boards.views.static import StaticPageView | |
11 | from boards.views.preview import PostPreviewView |
|
12 | from boards.views.preview import PostPreviewView | |
@@ -30,10 +31,10 b" urlpatterns = patterns(''," | |||||
30 | tag_threads.TagView.as_view(), name='tag'), |
|
31 | tag_threads.TagView.as_view(), name='tag'), | |
31 |
|
32 | |||
32 | # /boards/thread/ |
|
33 | # /boards/thread/ | |
33 | url(r'^thread/(?P<post_id>\w+)/$', views.thread.ThreadView.as_view(), |
|
34 | url(r'^thread/(?P<post_id>\w+)/$', views.thread.normal.NormalThreadView.as_view(), | |
34 | name='thread'), |
|
35 | name='thread'), | |
35 |
url(r'^thread/(?P<post_id>\w+)/mode/ |
|
36 | url(r'^thread/(?P<post_id>\w+)/mode/gallery/$', views.thread.gallery.GalleryThreadView.as_view(), | |
36 |
|
|
37 | name='thread_gallery'), | |
37 |
|
38 | |||
38 | url(r'^settings/$', settings.SettingsView.as_view(), name='settings'), |
|
39 | url(r'^settings/$', settings.SettingsView.as_view(), name='settings'), | |
39 | url(r'^tags/$', all_tags.AllTagsView.as_view(), name='tags'), |
|
40 | url(r'^tags/$', all_tags.AllTagsView.as_view(), name='tags'), | |
@@ -66,10 +67,15 b" urlpatterns = patterns(''," | |||||
66 | name='get_thread'), |
|
67 | name='get_thread'), | |
67 | url(r'^api/add_post/(?P<opening_post_id>\w+)/$', api.api_add_post, |
|
68 | url(r'^api/add_post/(?P<opening_post_id>\w+)/$', api.api_add_post, | |
68 | name='add_post'), |
|
69 | name='add_post'), | |
|
70 | url(r'^api/notifications/(?P<username>\w+)/$', api.api_get_notifications, | |||
|
71 | name='api_notifications'), | |||
69 |
|
72 | |||
70 | # Search |
|
73 | # Search | |
71 | url(r'^search/$', BoardSearchView.as_view(), name='search'), |
|
74 | url(r'^search/$', BoardSearchView.as_view(), name='search'), | |
72 |
|
75 | |||
|
76 | # Notifications | |||
|
77 | url(r'^notifications/(?P<username>\w+)$', NotificationView.as_view(), name='notifications'), | |||
|
78 | ||||
73 | # Post preview |
|
79 | # Post preview | |
74 | url(r'^preview/$', PostPreviewView.as_view(), name='preview'), |
|
80 | url(r'^preview/$', PostPreviewView.as_view(), name='preview'), | |
75 |
|
81 |
@@ -3,6 +3,8 b' This module contains helper functions an' | |||||
3 | """ |
|
3 | """ | |
4 | import time |
|
4 | import time | |
5 | import hmac |
|
5 | import hmac | |
|
6 | from django.core.cache import cache | |||
|
7 | from django.db.models import Model | |||
6 |
|
8 | |||
7 | from django.utils import timezone |
|
9 | from django.utils import timezone | |
8 |
|
10 | |||
@@ -23,6 +25,7 b' def get_client_ip(request):' | |||||
23 | return ip |
|
25 | return ip | |
24 |
|
26 | |||
25 |
|
27 | |||
|
28 | # TODO The output format is not epoch because it includes microseconds | |||
26 | def datetime_to_epoch(datetime): |
|
29 | def datetime_to_epoch(datetime): | |
27 | return int(time.mktime(timezone.localtime( |
|
30 | return int(time.mktime(timezone.localtime( | |
28 | datetime,timezone.get_current_timezone()).timetuple()) |
|
31 | datetime,timezone.get_current_timezone()).timetuple()) | |
@@ -40,4 +43,27 b" def get_websocket_token(user_id='', time" | |||||
40 | sign.update(timestamp.encode()) |
|
43 | sign.update(timestamp.encode()) | |
41 | token = sign.hexdigest() |
|
44 | token = sign.hexdigest() | |
42 |
|
45 | |||
43 | return token No newline at end of file |
|
46 | return token | |
|
47 | ||||
|
48 | ||||
|
49 | def cached_result(function): | |||
|
50 | """ | |||
|
51 | Caches method result in the Django's cache system, persisted by object name, | |||
|
52 | object name and model id if object is a Django model. | |||
|
53 | """ | |||
|
54 | def inner_func(obj, *args, **kwargs): | |||
|
55 | # TODO Include method arguments to the cache key | |||
|
56 | cache_key = obj.__class__.__name__ + '_' + function.__name__ | |||
|
57 | if isinstance(obj, Model): | |||
|
58 | cache_key += '_' + str(obj.id) | |||
|
59 | ||||
|
60 | persisted_result = cache.get(cache_key) | |||
|
61 | if persisted_result: | |||
|
62 | result = persisted_result | |||
|
63 | else: | |||
|
64 | result = function(obj, *args, **kwargs) | |||
|
65 | cache.set(cache_key, result) | |||
|
66 | ||||
|
67 | return result | |||
|
68 | ||||
|
69 | return inner_func |
@@ -1,11 +1,16 b'' | |||||
|
1 | from django.core.files import File | |||
|
2 | from django.core.files.temp import NamedTemporaryFile | |||
|
3 | from django.core.paginator import EmptyPage | |||
1 | from django.db import transaction |
|
4 | from django.db import transaction | |
|
5 | from django.http import Http404 | |||
2 | from django.shortcuts import render, redirect |
|
6 | from django.shortcuts import render, redirect | |
|
7 | import requests | |||
3 |
|
8 | |||
4 | from boards import utils, settings |
|
9 | from boards import utils, settings | |
5 | from boards.abstracts.paginator import get_paginator |
|
10 | from boards.abstracts.paginator import get_paginator | |
6 | from boards.abstracts.settingsmanager import get_settings_manager |
|
11 | from boards.abstracts.settingsmanager import get_settings_manager | |
7 | from boards.forms import ThreadForm, PlainErrorList |
|
12 | from boards.forms import ThreadForm, PlainErrorList | |
8 | from boards.models import Post, Thread, Ban, Tag |
|
13 | from boards.models import Post, Thread, Ban, Tag, PostImage | |
9 | from boards.views.banned import BannedView |
|
14 | from boards.views.banned import BannedView | |
10 | from boards.views.base import BaseBoardView, CONTEXT_FORM |
|
15 | from boards.views.base import BaseBoardView, CONTEXT_FORM | |
11 | from boards.views.posting_mixin import PostMixin |
|
16 | from boards.views.posting_mixin import PostMixin | |
@@ -32,7 +37,7 b' class AllThreadsView(PostMixin, BaseBoar' | |||||
32 | self.settings_manager = None |
|
37 | self.settings_manager = None | |
33 | super(AllThreadsView, self).__init__() |
|
38 | super(AllThreadsView, self).__init__() | |
34 |
|
39 | |||
35 | def get(self, request, page=DEFAULT_PAGE, form=None): |
|
40 | def get(self, request, page=DEFAULT_PAGE, form: ThreadForm=None): | |
36 | params = self.get_context_data(request=request) |
|
41 | params = self.get_context_data(request=request) | |
37 |
|
42 | |||
38 | if not form: |
|
43 | if not form: | |
@@ -43,7 +48,10 b' class AllThreadsView(PostMixin, BaseBoar' | |||||
43 | settings.THREADS_PER_PAGE) |
|
48 | settings.THREADS_PER_PAGE) | |
44 | paginator.current_page = int(page) |
|
49 | paginator.current_page = int(page) | |
45 |
|
50 | |||
46 | threads = paginator.page(page).object_list |
|
51 | try: | |
|
52 | threads = paginator.page(page).object_list | |||
|
53 | except EmptyPage: | |||
|
54 | raise Http404() | |||
47 |
|
55 | |||
48 | params[PARAMETER_THREADS] = threads |
|
56 | params[PARAMETER_THREADS] = threads | |
49 | params[CONTEXT_FORM] = form |
|
57 | params[CONTEXT_FORM] = form | |
@@ -92,7 +100,7 b' class AllThreadsView(PostMixin, BaseBoar' | |||||
92 | return tags |
|
100 | return tags | |
93 |
|
101 | |||
94 | @transaction.atomic |
|
102 | @transaction.atomic | |
95 | def create_thread(self, request, form, html_response=True): |
|
103 | def create_thread(self, request, form: ThreadForm, html_response=True): | |
96 | """ |
|
104 | """ | |
97 | Creates a new thread with an opening post. |
|
105 | Creates a new thread with an opening post. | |
98 | """ |
|
106 | """ | |
@@ -110,7 +118,7 b' class AllThreadsView(PostMixin, BaseBoar' | |||||
110 |
|
118 | |||
111 | title = data[FORM_TITLE] |
|
119 | title = data[FORM_TITLE] | |
112 | text = data[FORM_TEXT] |
|
120 | text = data[FORM_TEXT] | |
113 |
image = |
|
121 | image = form.get_image() | |
114 |
|
122 | |||
115 | text = self._remove_invalid_links(text) |
|
123 | text = self._remove_invalid_links(text) | |
116 |
|
124 | |||
@@ -133,5 +141,5 b' class AllThreadsView(PostMixin, BaseBoar' | |||||
133 | Gets list of threads that will be shown on a page. |
|
141 | Gets list of threads that will be shown on a page. | |
134 | """ |
|
142 | """ | |
135 |
|
143 | |||
136 |
return Thread.objects. |
|
144 | return Thread.objects.order_by('-bump_time')\ | |
137 | .exclude(tags__in=self.settings_manager.get_hidden_tags()) |
|
145 | .exclude(tags__in=self.settings_manager.get_hidden_tags()) |
@@ -12,6 +12,7 b' from boards.forms import PostForm, Plain' | |||||
12 | from boards.models import Post, Thread, Tag |
|
12 | from boards.models import Post, Thread, Tag | |
13 | from boards.utils import datetime_to_epoch |
|
13 | from boards.utils import datetime_to_epoch | |
14 | from boards.views.thread import ThreadView |
|
14 | from boards.views.thread import ThreadView | |
|
15 | from boards.models.user import Notification | |||
15 |
|
16 | |||
16 | __author__ = 'neko259' |
|
17 | __author__ = 'neko259' | |
17 |
|
18 | |||
@@ -48,10 +49,10 b' def api_get_threaddiff(request, thread_i' | |||||
48 | 'updated': [], |
|
49 | 'updated': [], | |
49 | 'last_update': None, |
|
50 | 'last_update': None, | |
50 | } |
|
51 | } | |
51 |
added_posts = Post.objects.filter(thread_n |
|
52 | added_posts = Post.objects.filter(threads__in=[thread], | |
52 | pub_time__gt=filter_time) \ |
|
53 | pub_time__gt=filter_time) \ | |
53 | .order_by('pub_time') |
|
54 | .order_by('pub_time') | |
54 |
updated_posts = Post.objects.filter(thread_n |
|
55 | updated_posts = Post.objects.filter(threads__in=[thread], | |
55 | pub_time__lte=filter_time, |
|
56 | pub_time__lte=filter_time, | |
56 | last_edit_time__gt=filter_time) |
|
57 | last_edit_time__gt=filter_time) | |
57 |
|
58 | |||
@@ -122,7 +123,6 b' def get_post(request, post_id):' | |||||
122 | return render(request, 'boards/api_post.html', context_instance=context) |
|
123 | return render(request, 'boards/api_post.html', context_instance=context) | |
123 |
|
124 | |||
124 |
|
125 | |||
125 | # TODO Test this |
|
|||
126 | def api_get_threads(request, count): |
|
126 | def api_get_threads(request, count): | |
127 | """ |
|
127 | """ | |
128 | Gets the JSON thread opening posts list. |
|
128 | Gets the JSON thread opening posts list. | |
@@ -152,8 +152,11 b' def api_get_threads(request, count):' | |||||
152 | opening_post = thread.get_opening_post() |
|
152 | opening_post = thread.get_opening_post() | |
153 |
|
153 | |||
154 | # TODO Add tags, replies and images count |
|
154 | # TODO Add tags, replies and images count | |
155 |
|
|
155 | post_data = get_post_data(opening_post.id, include_last_update=True) | |
156 | include_last_update=True)) |
|
156 | post_data['bumpable'] = thread.can_bump() | |
|
157 | post_data['archived'] = thread.archived | |||
|
158 | ||||
|
159 | opening_posts.append(post_data) | |||
157 |
|
160 | |||
158 | return HttpResponse(content=json.dumps(opening_posts)) |
|
161 | return HttpResponse(content=json.dumps(opening_posts)) | |
159 |
|
162 | |||
@@ -199,6 +202,20 b' def api_get_thread_posts(request, openin' | |||||
199 | return HttpResponse(content=json.dumps(json_data)) |
|
202 | return HttpResponse(content=json.dumps(json_data)) | |
200 |
|
203 | |||
201 |
|
204 | |||
|
205 | def api_get_notifications(request, username): | |||
|
206 | last_notification_id_str = request.GET.get('last', None) | |||
|
207 | last_id = int(last_notification_id_str) if last_notification_id_str is not None else None | |||
|
208 | ||||
|
209 | posts = Notification.objects.get_notification_posts(username=username, | |||
|
210 | last=last_id) | |||
|
211 | ||||
|
212 | json_post_list = [] | |||
|
213 | for post in posts: | |||
|
214 | json_post_list.append(get_post_data(post.id)) | |||
|
215 | return HttpResponse(content=json.dumps(json_post_list)) | |||
|
216 | ||||
|
217 | ||||
|
218 | ||||
202 | def api_get_post(request, post_id): |
|
219 | def api_get_post(request, post_id): | |
203 | """ |
|
220 | """ | |
204 | Gets the JSON of a post. This can be |
|
221 | Gets the JSON of a post. This can be |
@@ -1,5 +1,4 b'' | |||||
1 | from django.db import transaction |
|
1 | from django.db import transaction | |
2 | from django.template import RequestContext |
|
|||
3 | from django.views.generic import View |
|
2 | from django.views.generic import View | |
4 |
|
3 | |||
5 | from boards import utils |
|
4 | from boards import utils |
@@ -1,31 +1,37 b'' | |||||
1 | from django.db import transaction |
|
1 | from django.db import transaction | |
2 | from django.shortcuts import render, redirect |
|
2 | from django.shortcuts import render, redirect | |
3 |
|
3 | |||
4 | from boards.abstracts.settingsmanager import get_settings_manager |
|
4 | from boards.abstracts.settingsmanager import get_settings_manager, \ | |
|
5 | SETTING_USERNAME, SETTING_LAST_NOTIFICATION_ID | |||
5 | from boards.views.base import BaseBoardView, CONTEXT_FORM |
|
6 | from boards.views.base import BaseBoardView, CONTEXT_FORM | |
6 | from boards.forms import SettingsForm, PlainErrorList |
|
7 | from boards.forms import SettingsForm, PlainErrorList | |
7 |
|
8 | |||
8 | FORM_THEME = 'theme' |
|
9 | FORM_THEME = 'theme' | |
|
10 | FORM_USERNAME = 'username' | |||
9 |
|
11 | |||
10 | CONTEXT_HIDDEN_TAGS = 'hidden_tags' |
|
12 | CONTEXT_HIDDEN_TAGS = 'hidden_tags' | |
11 |
|
13 | |||
|
14 | TEMPLATE = 'boards/settings.html' | |||
|
15 | ||||
12 |
|
16 | |||
13 | class SettingsView(BaseBoardView): |
|
17 | class SettingsView(BaseBoardView): | |
14 |
|
18 | |||
15 | def get(self, request): |
|
19 | def get(self, request): | |
16 |
params = |
|
20 | params = dict() | |
17 | settings_manager = get_settings_manager(request) |
|
21 | settings_manager = get_settings_manager(request) | |
18 |
|
22 | |||
19 | selected_theme = settings_manager.get_theme() |
|
23 | selected_theme = settings_manager.get_theme() | |
20 |
|
24 | |||
21 |
form = SettingsForm( |
|
25 | form = SettingsForm( | |
22 | error_class=PlainErrorList) |
|
26 | initial={ | |
|
27 | FORM_THEME: selected_theme, | |||
|
28 | FORM_USERNAME: settings_manager.get_setting(SETTING_USERNAME)}, | |||
|
29 | error_class=PlainErrorList) | |||
23 |
|
30 | |||
24 | params[CONTEXT_FORM] = form |
|
31 | params[CONTEXT_FORM] = form | |
25 | params[CONTEXT_HIDDEN_TAGS] = settings_manager.get_hidden_tags() |
|
32 | params[CONTEXT_HIDDEN_TAGS] = settings_manager.get_hidden_tags() | |
26 |
|
33 | |||
27 | # TODO Use dict here |
|
34 | return render(request, TEMPLATE, params) | |
28 | return render(request, 'boards/settings.html', params) |
|
|||
29 |
|
35 | |||
30 | def post(self, request): |
|
36 | def post(self, request): | |
31 | settings_manager = get_settings_manager(request) |
|
37 | settings_manager = get_settings_manager(request) | |
@@ -35,7 +41,19 b' class SettingsView(BaseBoardView):' | |||||
35 |
|
41 | |||
36 | if form.is_valid(): |
|
42 | if form.is_valid(): | |
37 | selected_theme = form.cleaned_data[FORM_THEME] |
|
43 | selected_theme = form.cleaned_data[FORM_THEME] | |
|
44 | username = form.cleaned_data[FORM_USERNAME].lower() | |||
38 |
|
45 | |||
39 | settings_manager.set_theme(selected_theme) |
|
46 | settings_manager.set_theme(selected_theme) | |
40 |
|
47 | |||
41 | return redirect('settings') |
|
48 | settings_manager.set_setting(SETTING_USERNAME, username) | |
|
49 | settings_manager.set_setting(SETTING_LAST_NOTIFICATION_ID, None) | |||
|
50 | ||||
|
51 | return redirect('settings') | |||
|
52 | else: | |||
|
53 | params = dict() | |||
|
54 | ||||
|
55 | params[CONTEXT_FORM] = form | |||
|
56 | params[CONTEXT_HIDDEN_TAGS] = settings_manager.get_hidden_tags() | |||
|
57 | ||||
|
58 | return render(request, TEMPLATE, params) | |||
|
59 |
@@ -1,14 +1,16 b'' | |||||
1 | from django.shortcuts import get_object_or_404 |
|
1 | from django.shortcuts import get_object_or_404 | |
2 |
|
2 | |||
3 | from boards.abstracts.settingsmanager import get_settings_manager |
|
3 | from boards.abstracts.settingsmanager import get_settings_manager, \ | |
|
4 | SETTING_FAVORITE_TAGS, SETTING_HIDDEN_TAGS | |||
4 | from boards.models import Tag, Thread |
|
5 | from boards.models import Tag, Thread | |
5 | from boards.views.all_threads import AllThreadsView, DEFAULT_PAGE |
|
6 | from boards.views.all_threads import AllThreadsView, DEFAULT_PAGE | |
6 | from boards.views.mixins import DispatcherMixin, RedirectNextMixin |
|
7 | from boards.views.mixins import DispatcherMixin, RedirectNextMixin | |
7 | from boards.forms import ThreadForm, PlainErrorList |
|
8 | from boards.forms import ThreadForm, PlainErrorList | |
8 |
|
9 | |||
9 | PARAM_HIDDEN_TAGS = 'hidden_tags' |
|
10 | PARAM_HIDDEN_TAGS = 'hidden_tags' | |
10 | PARAM_FAV_TAGS = 'fav_tags' |
|
|||
11 | PARAM_TAG = 'tag' |
|
11 | PARAM_TAG = 'tag' | |
|
12 | PARAM_IS_FAVORITE = 'is_favorite' | |||
|
13 | PARAM_IS_HIDDEN = 'is_hidden' | |||
12 |
|
14 | |||
13 | __author__ = 'neko259' |
|
15 | __author__ = 'neko259' | |
14 |
|
16 | |||
@@ -30,8 +32,11 b' class TagView(AllThreadsView, Dispatcher' | |||||
30 | tag = get_object_or_404(Tag, name=self.tag_name) |
|
32 | tag = get_object_or_404(Tag, name=self.tag_name) | |
31 | params[PARAM_TAG] = tag |
|
33 | params[PARAM_TAG] = tag | |
32 |
|
34 | |||
33 | params[PARAM_FAV_TAGS] = settings_manager.get_fav_tags() |
|
35 | fav_tag_names = settings_manager.get_setting(SETTING_FAVORITE_TAGS) | |
34 | params[PARAM_HIDDEN_TAGS] = settings_manager.get_hidden_tags() |
|
36 | hidden_tag_names = settings_manager.get_setting(SETTING_HIDDEN_TAGS) | |
|
37 | ||||
|
38 | params[PARAM_IS_FAVORITE] = fav_tag_names is not None and tag.name in fav_tag_names | |||
|
39 | params[PARAM_IS_HIDDEN] = hidden_tag_names is not None and tag.name in hidden_tag_names | |||
35 |
|
40 | |||
36 | return params |
|
41 | return params | |
37 |
|
42 |
@@ -41,3 +41,37 b' images to a post' | |||||
41 | * Changed markdown to bbcode |
|
41 | * Changed markdown to bbcode | |
42 | * Removed linked tags |
|
42 | * Removed linked tags | |
43 | * [ADMIN] Added title to the post logs to make them more informative |
|
43 | * [ADMIN] Added title to the post logs to make them more informative | |
|
44 | ||||
|
45 | # 2.2 | |||
|
46 | * Support websockets for thread update | |||
|
47 | * Using div as line separator | |||
|
48 | * CSS and JS compressor | |||
|
49 | ||||
|
50 | # 2.2.1 | |||
|
51 | * Changed logs style | |||
|
52 | * Text preparsing. Support markdown-style text that parses into bbcode | |||
|
53 | * "bumpable" field for threads. If the thread became non-bumpable, it will | |||
|
54 | remain in this state even if the bumplimit changes or posts will be deleted from | |||
|
55 | it | |||
|
56 | ||||
|
57 | # 2.2.4 | |||
|
58 | * Default settings. There is a global neboard default settings file, but user | |||
|
59 | can override settings in the old settings.py | |||
|
60 | * Required tags. Some tags can be marked as required and you can't create thread | |||
|
61 | without at least one of them, while you can add any tags you want to it | |||
|
62 | * [ADMIN] Cosmetic changes in the admin site. Adding and removing tags is much | |||
|
63 | more simple now | |||
|
64 | * Don't save tag's threads as a separate table, use aggregation instead | |||
|
65 | ||||
|
66 | # 2.3.0 Neiro | |||
|
67 | * Image deduplication | |||
|
68 | ||||
|
69 | # 2.4.0 Korra | |||
|
70 | * Downloading images by URL | |||
|
71 | * [CODE] Cached properties | |||
|
72 | ||||
|
73 | # 2.5.0 Yasako | |||
|
74 | * User notifications | |||
|
75 | * Posting to many threads by one post | |||
|
76 | * Tag details | |||
|
77 | * Removed compact form |
@@ -45,6 +45,15 b' format. 2 formats are available: ``html`' | |||||
45 | * ``updated``: list of updated posts |
|
45 | * ``updated``: list of updated posts | |
46 | * ``last_update``: last update timestamp |
|
46 | * ``last_update``: last update timestamp | |
47 |
|
47 | |||
|
48 | ## Notifications ## | |||
|
49 | ||||
|
50 | /api/notifications/<username>/[?last=<id>] | |||
|
51 | ||||
|
52 | Get user notifications for user starting from the post ID. | |||
|
53 | ||||
|
54 | * ``username``: name of the notified user | |||
|
55 | * ``id``: ID of a last notification post | |||
|
56 | ||||
48 | ## General info ## |
|
57 | ## General info ## | |
49 |
|
58 | |||
50 | In case of incorrect request you can get http error 404. |
|
59 | In case of incorrect request you can get http error 404. |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
General Comments 0
You need to be logged in to leave comments.
Login now