Show More
@@ -0,0 +1,26 b'' | |||||
|
1 | class Tripcode: | |||
|
2 | def __init__(self, code_str): | |||
|
3 | self.tripcode = code_str | |||
|
4 | ||||
|
5 | def get_color(self): | |||
|
6 | return self.tripcode[:6] | |||
|
7 | ||||
|
8 | def get_background(self): | |||
|
9 | code = self.get_color() | |||
|
10 | result = '' | |||
|
11 | ||||
|
12 | for i in range(0, len(code), 2): | |||
|
13 | p = code[i:i+2] | |||
|
14 | background = hex(255 - int(p, 16))[2:] | |||
|
15 | if len(background) < 2: | |||
|
16 | background = '0' + background | |||
|
17 | result += background | |||
|
18 | ||||
|
19 | return result | |||
|
20 | ||||
|
21 | def get_short_text(self): | |||
|
22 | return self.tripcode[:8] | |||
|
23 | ||||
|
24 | def get_full_text(self): | |||
|
25 | return self.tripcode | |||
|
26 |
@@ -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', '0019_auto_20150519_1323'), | |||
|
11 | ] | |||
|
12 | ||||
|
13 | operations = [ | |||
|
14 | migrations.AlterField( | |||
|
15 | model_name='post', | |||
|
16 | name='images', | |||
|
17 | field=models.ManyToManyField(to='boards.PostImage', blank=True, null=True, db_index=True, related_name='post_images'), | |||
|
18 | ), | |||
|
19 | ] |
@@ -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', '0020_auto_20150731_1738'), | |||
|
11 | ] | |||
|
12 | ||||
|
13 | operations = [ | |||
|
14 | migrations.AddField( | |||
|
15 | model_name='tag', | |||
|
16 | name='description', | |||
|
17 | field=models.TextField(blank=True), | |||
|
18 | ), | |||
|
19 | ] |
@@ -0,0 +1,19 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | from __future__ import unicode_literals | |||
|
3 | ||||
|
4 | from django.db import models, migrations | |||
|
5 | ||||
|
6 | ||||
|
7 | class Migration(migrations.Migration): | |||
|
8 | ||||
|
9 | dependencies = [ | |||
|
10 | ('boards', '0021_tag_description'), | |||
|
11 | ] | |||
|
12 | ||||
|
13 | operations = [ | |||
|
14 | migrations.AlterField( | |||
|
15 | model_name='thread', | |||
|
16 | name='tags', | |||
|
17 | field=models.ManyToManyField(related_name='thread_tags', to='boards.Tag'), | |||
|
18 | ), | |||
|
19 | ] |
@@ -0,0 +1,29 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | from __future__ import unicode_literals | |||
|
3 | ||||
|
4 | from django.db import models, migrations | |||
|
5 | import boards.models.attachment | |||
|
6 | ||||
|
7 | ||||
|
8 | class Migration(migrations.Migration): | |||
|
9 | ||||
|
10 | dependencies = [ | |||
|
11 | ('boards', '0022_auto_20150812_1819'), | |||
|
12 | ] | |||
|
13 | ||||
|
14 | operations = [ | |||
|
15 | migrations.CreateModel( | |||
|
16 | name='Attachment', | |||
|
17 | fields=[ | |||
|
18 | ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)), | |||
|
19 | ('file', models.FileField(upload_to=boards.models.attachment.Attachment._update_filename)), | |||
|
20 | ('mimetype', models.CharField(max_length=50)), | |||
|
21 | ('hash', models.CharField(max_length=36)), | |||
|
22 | ], | |||
|
23 | ), | |||
|
24 | migrations.AddField( | |||
|
25 | model_name='post', | |||
|
26 | name='attachments', | |||
|
27 | field=models.ManyToManyField(blank=True, null=True, related_name='attachment_posts', to='boards.Attachment'), | |||
|
28 | ), | |||
|
29 | ] |
@@ -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', '0023_auto_20150818_1026'), | |||
|
11 | ] | |||
|
12 | ||||
|
13 | operations = [ | |||
|
14 | migrations.AddField( | |||
|
15 | model_name='post', | |||
|
16 | name='tripcode', | |||
|
17 | field=models.CharField(max_length=50, null=True), | |||
|
18 | ), | |||
|
19 | ] |
@@ -0,0 +1,21 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | from __future__ import unicode_literals | |||
|
3 | ||||
|
4 | from django.db import migrations | |||
|
5 | ||||
|
6 | ||||
|
7 | class Migration(migrations.Migration): | |||
|
8 | ||||
|
9 | def refuild_refmap(apps, schema_editor): | |||
|
10 | Post = apps.get_model('boards', 'Post') | |||
|
11 | for post in Post.objects.all(): | |||
|
12 | post.build_refmap() | |||
|
13 | post.save(update_fields=['refmap']) | |||
|
14 | ||||
|
15 | dependencies = [ | |||
|
16 | ('boards', '0024_post_tripcode'), | |||
|
17 | ] | |||
|
18 | ||||
|
19 | operations = [ | |||
|
20 | migrations.RunPython(refuild_refmap), | |||
|
21 | ] |
@@ -0,0 +1,48 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', '0025_auto_20150825_2049'), | |||
|
11 | ] | |||
|
12 | ||||
|
13 | operations = [ | |||
|
14 | migrations.CreateModel( | |||
|
15 | name='GlobalId', | |||
|
16 | fields=[ | |||
|
17 | ('id', models.AutoField(serialize=False, verbose_name='ID', primary_key=True, auto_created=True)), | |||
|
18 | ('key', models.TextField()), | |||
|
19 | ('key_type', models.TextField()), | |||
|
20 | ('local_id', models.IntegerField()), | |||
|
21 | ], | |||
|
22 | ), | |||
|
23 | migrations.CreateModel( | |||
|
24 | name='KeyPair', | |||
|
25 | fields=[ | |||
|
26 | ('id', models.AutoField(serialize=False, verbose_name='ID', primary_key=True, auto_created=True)), | |||
|
27 | ('public_key', models.TextField()), | |||
|
28 | ('private_key', models.TextField()), | |||
|
29 | ('key_type', models.TextField()), | |||
|
30 | ('primary', models.BooleanField(default=False)), | |||
|
31 | ], | |||
|
32 | ), | |||
|
33 | migrations.CreateModel( | |||
|
34 | name='Signature', | |||
|
35 | fields=[ | |||
|
36 | ('id', models.AutoField(serialize=False, verbose_name='ID', primary_key=True, auto_created=True)), | |||
|
37 | ('key_type', models.TextField()), | |||
|
38 | ('key', models.TextField()), | |||
|
39 | ('signature', models.TextField()), | |||
|
40 | ('global_id', models.ForeignKey(to='boards.GlobalId')), | |||
|
41 | ], | |||
|
42 | ), | |||
|
43 | migrations.AddField( | |||
|
44 | model_name='post', | |||
|
45 | name='global_id', | |||
|
46 | field=models.OneToOneField(to='boards.GlobalId', null=True, blank=True), | |||
|
47 | ), | |||
|
48 | ] |
@@ -0,0 +1,60 b'' | |||||
|
1 | import os | |||
|
2 | import time | |||
|
3 | from random import random | |||
|
4 | ||||
|
5 | from django.db import models | |||
|
6 | ||||
|
7 | from boards import utils | |||
|
8 | from boards.models.attachment.viewers import get_viewers, AbstractViewer | |||
|
9 | ||||
|
10 | FILES_DIRECTORY = 'files/' | |||
|
11 | FILE_EXTENSION_DELIMITER = '.' | |||
|
12 | ||||
|
13 | ||||
|
14 | class AttachmentManager(models.Manager): | |||
|
15 | def create_with_hash(self, file): | |||
|
16 | file_hash = utils.get_file_hash(file) | |||
|
17 | existing = self.filter(hash=file_hash) | |||
|
18 | if len(existing) > 0: | |||
|
19 | attachment = existing[0] | |||
|
20 | else: | |||
|
21 | file_type = file.name.split(FILE_EXTENSION_DELIMITER)[-1].lower() | |||
|
22 | attachment = Attachment.objects.create( | |||
|
23 | file=file, mimetype=file_type, hash=file_hash) | |||
|
24 | ||||
|
25 | return attachment | |||
|
26 | ||||
|
27 | ||||
|
28 | class Attachment(models.Model): | |||
|
29 | objects = AttachmentManager() | |||
|
30 | ||||
|
31 | # TODO Dedup the method | |||
|
32 | def _update_filename(self, filename): | |||
|
33 | """ | |||
|
34 | Gets unique filename | |||
|
35 | """ | |||
|
36 | ||||
|
37 | # TODO Use something other than random number in file name | |||
|
38 | new_name = '{}{}.{}'.format( | |||
|
39 | str(int(time.mktime(time.gmtime()))), | |||
|
40 | str(int(random() * 1000)), | |||
|
41 | filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]) | |||
|
42 | ||||
|
43 | return os.path.join(FILES_DIRECTORY, new_name) | |||
|
44 | ||||
|
45 | file = models.FileField(upload_to=_update_filename) | |||
|
46 | mimetype = models.CharField(max_length=50) | |||
|
47 | hash = models.CharField(max_length=36) | |||
|
48 | ||||
|
49 | def get_view(self): | |||
|
50 | file_viewer = None | |||
|
51 | for viewer in get_viewers(): | |||
|
52 | if viewer.supports(self.mimetype): | |||
|
53 | file_viewer = viewer | |||
|
54 | break | |||
|
55 | if file_viewer is None: | |||
|
56 | file_viewer = AbstractViewer | |||
|
57 | ||||
|
58 | return file_viewer(self.file, self.mimetype).get_view() | |||
|
59 | ||||
|
60 |
@@ -0,0 +1,70 b'' | |||||
|
1 | from django.template.defaultfilters import filesizeformat | |||
|
2 | from django.contrib.staticfiles.templatetags.staticfiles import static | |||
|
3 | ||||
|
4 | FILE_STUB_IMAGE = 'images/file.png' | |||
|
5 | ||||
|
6 | FILE_TYPES_VIDEO = ( | |||
|
7 | 'webm', | |||
|
8 | 'mp4', | |||
|
9 | ) | |||
|
10 | FILE_TYPE_SVG = 'svg' | |||
|
11 | FILE_TYPES_AUDIO = ( | |||
|
12 | 'ogg', | |||
|
13 | 'mp3', | |||
|
14 | ) | |||
|
15 | ||||
|
16 | ||||
|
17 | def get_viewers(): | |||
|
18 | return AbstractViewer.__subclasses__() | |||
|
19 | ||||
|
20 | ||||
|
21 | class AbstractViewer: | |||
|
22 | def __init__(self, file, file_type): | |||
|
23 | self.file = file | |||
|
24 | self.file_type = file_type | |||
|
25 | ||||
|
26 | @staticmethod | |||
|
27 | def supports(file_type): | |||
|
28 | return True | |||
|
29 | ||||
|
30 | def get_view(self): | |||
|
31 | return '<div class="image">'\ | |||
|
32 | '{}'\ | |||
|
33 | '<div class="image-metadata"><a href="{}" download >{}, {}</a></div>'\ | |||
|
34 | '</div>'.format(self.get_format_view(), self.file.url, | |||
|
35 | self.file_type, filesizeformat(self.file.size)) | |||
|
36 | ||||
|
37 | def get_format_view(self): | |||
|
38 | return '<a href="{}">'\ | |||
|
39 | '<img src="{}" width="200" height="150"/>'\ | |||
|
40 | '</a>'.format(self.file.url, static(FILE_STUB_IMAGE)) | |||
|
41 | ||||
|
42 | ||||
|
43 | class VideoViewer(AbstractViewer): | |||
|
44 | @staticmethod | |||
|
45 | def supports(file_type): | |||
|
46 | return file_type in FILE_TYPES_VIDEO | |||
|
47 | ||||
|
48 | def get_format_view(self): | |||
|
49 | return '<video width="200" height="150" controls src="{}"></video>'\ | |||
|
50 | .format(self.file.url) | |||
|
51 | ||||
|
52 | ||||
|
53 | class AudioViewer(AbstractViewer): | |||
|
54 | @staticmethod | |||
|
55 | def supports(file_type): | |||
|
56 | return file_type in FILE_TYPES_AUDIO | |||
|
57 | ||||
|
58 | def get_format_view(self): | |||
|
59 | return '<audio controls src="{}"></audio>'.format(self.file.url) | |||
|
60 | ||||
|
61 | ||||
|
62 | class SvgViewer(AbstractViewer): | |||
|
63 | @staticmethod | |||
|
64 | def supports(file_type): | |||
|
65 | return file_type == FILE_TYPE_SVG | |||
|
66 | ||||
|
67 | def get_format_view(self): | |||
|
68 | return '<a class="thumb" href="{}">'\ | |||
|
69 | '<img class="post-image-preview" width="200" height="150" src="{}" />'\ | |||
|
70 | '</a>'.format(self.file.url, self.file.url) |
1 | NO CONTENT: new file 100644, binary diff hidden |
|
NO CONTENT: new file 100644, binary diff hidden |
@@ -0,0 +1,37 b'' | |||||
|
1 | {% extends "boards/base.html" %} | |||
|
2 | ||||
|
3 | {% load i18n %} | |||
|
4 | ||||
|
5 | {% block head %} | |||
|
6 | <title>{% trans 'Random images' %} - {{ site_name }}</title> | |||
|
7 | {% endblock %} | |||
|
8 | ||||
|
9 | {% block content %} | |||
|
10 | ||||
|
11 | {% if images %} | |||
|
12 | <div class="random-images-table"> | |||
|
13 | <div> | |||
|
14 | {% for image in images %} | |||
|
15 | <div class="gallery_image"> | |||
|
16 | {% autoescape off %} | |||
|
17 | {{ image.get_view }} | |||
|
18 | {% endautoescape %} | |||
|
19 | {% with image.get_random_associated_post as post %} | |||
|
20 | {{ post.get_link_view|safe }} | |||
|
21 | <div> | |||
|
22 | {% for tag in post.get_thread.get_required_tags.all %} | |||
|
23 | {{ tag.get_view|safe }}{% if not forloop.last %},{% endif %} | |||
|
24 | {% endfor %} | |||
|
25 | </div> | |||
|
26 | {% endwith %} | |||
|
27 | </div> | |||
|
28 | {% if forloop.counter|divisibleby:"3" %} | |||
|
29 | </div> | |||
|
30 | <div> | |||
|
31 | {% endif %} | |||
|
32 | {% endfor %} | |||
|
33 | </div> | |||
|
34 | </div> | |||
|
35 | {% endif %} | |||
|
36 | ||||
|
37 | {% endblock %} |
@@ -0,0 +1,22 b'' | |||||
|
1 | from django.shortcuts import render | |||
|
2 | from django.views.generic import View | |||
|
3 | ||||
|
4 | from boards.models import PostImage | |||
|
5 | ||||
|
6 | __author__ = 'neko259' | |||
|
7 | ||||
|
8 | TEMPLATE = 'boards/random.html' | |||
|
9 | ||||
|
10 | CONTEXT_IMAGES = 'images' | |||
|
11 | ||||
|
12 | RANDOM_POST_COUNT = 9 | |||
|
13 | ||||
|
14 | ||||
|
15 | class RandomImageView(View): | |||
|
16 | def get(self, request): | |||
|
17 | params = dict() | |||
|
18 | ||||
|
19 | params[CONTEXT_IMAGES] = PostImage.objects.get_random_images( | |||
|
20 | RANDOM_POST_COUNT) | |||
|
21 | ||||
|
22 | return render(request, TEMPLATE, params) |
@@ -33,3 +33,4 b' 836d8bb9fcd930b952b9a02029442c71c2441983' | |||||
33 | dfb6c481b1a2c33705de9a9b5304bc924c46b202 2.8.1 |
|
33 | dfb6c481b1a2c33705de9a9b5304bc924c46b202 2.8.1 | |
34 | 4a5bec08ccfb47a27f9e98698f12dd5b7246623b 2.8.2 |
|
34 | 4a5bec08ccfb47a27f9e98698f12dd5b7246623b 2.8.2 | |
35 | 604935b98f5b5e4a5e903594f048046e1fbb3519 2.8.3 |
|
35 | 604935b98f5b5e4a5e903594f048046e1fbb3519 2.8.3 | |
|
36 | c48ffdc671566069ed0f33644da1229277f3cd18 2.9.0 |
@@ -1,6 +1,7 b'' | |||||
1 | from django.shortcuts import get_object_or_404 |
|
|||
2 |
|
|
1 | from boards.models import Tag | |
3 |
|
2 | |||
|
3 | MAX_TRIPCODE_COLLISIONS = 50 | |||
|
4 | ||||
4 | __author__ = 'neko259' |
|
5 | __author__ = 'neko259' | |
5 |
|
6 | |||
6 | SESSION_SETTING = 'setting' |
|
7 | SESSION_SETTING = 'setting' | |
@@ -15,6 +16,7 b" SETTING_PERMISSIONS = 'permissions'" | |||||
15 | SETTING_USERNAME = 'username' |
|
16 | SETTING_USERNAME = 'username' | |
16 | SETTING_LAST_NOTIFICATION_ID = 'last_notification' |
|
17 | SETTING_LAST_NOTIFICATION_ID = 'last_notification' | |
17 | SETTING_IMAGE_VIEWER = 'image_viewer' |
|
18 | SETTING_IMAGE_VIEWER = 'image_viewer' | |
|
19 | SETTING_TRIPCODE = 'tripcode' | |||
18 |
|
20 | |||
19 | DEFAULT_THEME = 'md' |
|
21 | DEFAULT_THEME = 'md' | |
20 |
|
22 | |||
@@ -130,6 +132,7 b' class SessionSettingsManager(SettingsMan' | |||||
130 | if setting in self.session: |
|
132 | if setting in self.session: | |
131 | return self.session[setting] |
|
133 | return self.session[setting] | |
132 | else: |
|
134 | else: | |
|
135 | self.set_setting(setting, default) | |||
133 | return default |
|
136 | return default | |
134 |
|
137 | |||
135 | def set_setting(self, setting, value): |
|
138 | def set_setting(self, setting, value): |
@@ -1,5 +1,5 b'' | |||||
1 | [Version] |
|
1 | [Version] | |
2 |
Version = 2. |
|
2 | Version = 2.9.0 Claire | |
3 | SiteName = Neboard DEV |
|
3 | SiteName = Neboard DEV | |
4 |
|
4 | |||
5 | [Cache] |
|
5 | [Cache] | |
@@ -9,7 +9,7 b' CacheTimeout = 600' | |||||
9 | [Forms] |
|
9 | [Forms] | |
10 | # Max post length in characters |
|
10 | # Max post length in characters | |
11 | MaxTextLength = 30000 |
|
11 | MaxTextLength = 30000 | |
12 |
Max |
|
12 | MaxFileSize = 8000000 | |
13 | LimitPostingSpeed = false |
|
13 | LimitPostingSpeed = false | |
14 |
|
14 | |||
15 | [Messages] |
|
15 | [Messages] |
@@ -1,7 +1,8 b'' | |||||
|
1 | import hashlib | |||
1 | import re |
|
2 | import re | |
2 | import time |
|
3 | import time | |
|
4 | ||||
3 | import pytz |
|
5 | import pytz | |
4 |
|
||||
5 | from django import forms |
|
6 | from django import forms | |
6 | from django.core.files.uploadedfile import SimpleUploadedFile |
|
7 | from django.core.files.uploadedfile import SimpleUploadedFile | |
7 | from django.core.exceptions import ObjectDoesNotExist |
|
8 | from django.core.exceptions import ObjectDoesNotExist | |
@@ -14,18 +15,11 b' from boards.models.post import TITLE_MAX' | |||||
14 | from boards.models import Tag, Post |
|
15 | from boards.models import Tag, Post | |
15 | from neboard import settings |
|
16 | from neboard import settings | |
16 | import boards.settings as board_settings |
|
17 | import boards.settings as board_settings | |
|
18 | import neboard | |||
17 |
|
19 | |||
18 | HEADER_CONTENT_LENGTH = 'content-length' |
|
20 | HEADER_CONTENT_LENGTH = 'content-length' | |
19 | HEADER_CONTENT_TYPE = 'content-type' |
|
21 | HEADER_CONTENT_TYPE = 'content-type' | |
20 |
|
22 | |||
21 | CONTENT_TYPE_IMAGE = ( |
|
|||
22 | 'image/jpeg', |
|
|||
23 | 'image/jpg', |
|
|||
24 | 'image/png', |
|
|||
25 | 'image/gif', |
|
|||
26 | 'image/bmp', |
|
|||
27 | ) |
|
|||
28 |
|
||||
29 | REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE) |
|
23 | REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE) | |
30 |
|
24 | |||
31 | VETERAN_POSTING_DELAY = 5 |
|
25 | VETERAN_POSTING_DELAY = 5 | |
@@ -47,7 +41,7 b" ERROR_SPEED = _('Please wait %s seconds " | |||||
47 |
|
41 | |||
48 | TAG_MAX_LENGTH = 20 |
|
42 | TAG_MAX_LENGTH = 20 | |
49 |
|
43 | |||
50 |
|
|
44 | FILE_DOWNLOAD_CHUNK_BYTES = 100000 | |
51 |
|
45 | |||
52 | HTTP_RESULT_OK = 200 |
|
46 | HTTP_RESULT_OK = 200 | |
53 |
|
47 | |||
@@ -137,17 +131,20 b' class NeboardForm(forms.Form):' | |||||
137 | class PostForm(NeboardForm): |
|
131 | class PostForm(NeboardForm): | |
138 |
|
132 | |||
139 | title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False, |
|
133 | title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False, | |
140 |
label=LABEL_TITLE |
|
134 | label=LABEL_TITLE, | |
|
135 | widget=forms.TextInput( | |||
|
136 | attrs={ATTRIBUTE_PLACEHOLDER: | |||
|
137 | 'test#tripcode'})) | |||
141 | text = forms.CharField( |
|
138 | text = forms.CharField( | |
142 | widget=FormatPanel(attrs={ |
|
139 | widget=FormatPanel(attrs={ | |
143 | ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER, |
|
140 | ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER, | |
144 | ATTRIBUTE_ROWS: TEXTAREA_ROWS, |
|
141 | ATTRIBUTE_ROWS: TEXTAREA_ROWS, | |
145 | }), |
|
142 | }), | |
146 | required=False, label=LABEL_TEXT) |
|
143 | required=False, label=LABEL_TEXT) | |
147 |
|
|
144 | file = forms.FileField(required=False, label=_('File'), | |
148 | widget=forms.ClearableFileInput( |
|
145 | widget=forms.ClearableFileInput( | |
149 |
attrs={'accept': ' |
|
146 | attrs={'accept': 'file/*'})) | |
150 |
|
|
147 | file_url = forms.CharField(required=False, label=_('File URL'), | |
151 | widget=forms.TextInput( |
|
148 | widget=forms.TextInput( | |
152 | attrs={ATTRIBUTE_PLACEHOLDER: |
|
149 | attrs={ATTRIBUTE_PLACEHOLDER: | |
153 | 'http://example.com/image.png'})) |
|
150 | 'http://example.com/image.png'})) | |
@@ -181,27 +178,27 b' class PostForm(NeboardForm):' | |||||
181 | 'characters') % str(max_length)) |
|
178 | 'characters') % str(max_length)) | |
182 | return text |
|
179 | return text | |
183 |
|
180 | |||
184 |
def clean_ |
|
181 | def clean_file(self): | |
185 |
|
|
182 | file = self.cleaned_data['file'] | |
186 |
|
183 | |||
187 |
if |
|
184 | if file: | |
188 |
self.validate_ |
|
185 | self.validate_file_size(file.size) | |
189 |
|
186 | |||
190 |
return |
|
187 | return file | |
191 |
|
188 | |||
192 |
def clean_ |
|
189 | def clean_file_url(self): | |
193 |
url = self.cleaned_data[' |
|
190 | url = self.cleaned_data['file_url'] | |
194 |
|
191 | |||
195 |
|
|
192 | file = None | |
196 | if url: |
|
193 | if url: | |
197 |
|
|
194 | file = self._get_file_from_url(url) | |
198 |
|
195 | |||
199 |
if not |
|
196 | if not file: | |
200 | raise forms.ValidationError(_('Invalid URL')) |
|
197 | raise forms.ValidationError(_('Invalid URL')) | |
201 | else: |
|
198 | else: | |
202 |
self.validate_ |
|
199 | self.validate_file_size(file.size) | |
203 |
|
200 | |||
204 |
return |
|
201 | return file | |
205 |
|
202 | |||
206 | def clean_threads(self): |
|
203 | def clean_threads(self): | |
207 | threads_str = self.cleaned_data['threads'] |
|
204 | threads_str = self.cleaned_data['threads'] | |
@@ -230,27 +227,40 b' class PostForm(NeboardForm):' | |||||
230 | raise forms.ValidationError('A human cannot enter a hidden field') |
|
227 | raise forms.ValidationError('A human cannot enter a hidden field') | |
231 |
|
228 | |||
232 | if not self.errors: |
|
229 | if not self.errors: | |
233 |
self._clean_text_ |
|
230 | self._clean_text_file() | |
234 |
|
231 | |||
235 | if not self.errors and self.session: |
|
232 | if not self.errors and self.session: | |
236 | self._validate_posting_speed() |
|
233 | self._validate_posting_speed() | |
237 |
|
234 | |||
238 | return cleaned_data |
|
235 | return cleaned_data | |
239 |
|
236 | |||
240 |
def get_ |
|
237 | def get_file(self): | |
241 | """ |
|
238 | """ | |
242 |
Gets |
|
239 | Gets file from form or URL. | |
243 | """ |
|
240 | """ | |
244 |
|
241 | |||
245 |
|
|
242 | file = self.cleaned_data['file'] | |
246 |
return |
|
243 | return file or self.cleaned_data['file_url'] | |
|
244 | ||||
|
245 | def get_tripcode(self): | |||
|
246 | title = self.cleaned_data['title'] | |||
|
247 | if title is not None and '#' in title: | |||
|
248 | code = title.split('#', maxsplit=1)[1] + neboard.settings.SECRET_KEY | |||
|
249 | return hashlib.md5(code.encode()).hexdigest() | |||
247 |
|
250 | |||
248 |
def |
|
251 | def get_title(self): | |
|
252 | title = self.cleaned_data['title'] | |||
|
253 | if title is not None and '#' in title: | |||
|
254 | return title.split('#', maxsplit=1)[0] | |||
|
255 | else: | |||
|
256 | return title | |||
|
257 | ||||
|
258 | def _clean_text_file(self): | |||
249 | text = self.cleaned_data.get('text') |
|
259 | text = self.cleaned_data.get('text') | |
250 |
|
|
260 | file = self.get_file() | |
251 |
|
261 | |||
252 |
if (not text) and (not |
|
262 | if (not text) and (not file): | |
253 |
error_message = _('Either text or |
|
263 | error_message = _('Either text or file must be entered.') | |
254 | self._errors['text'] = self.error_class([error_message]) |
|
264 | self._errors['text'] = self.error_class([error_message]) | |
255 |
|
265 | |||
256 | def _validate_posting_speed(self): |
|
266 | def _validate_posting_speed(self): | |
@@ -284,16 +294,16 b' class PostForm(NeboardForm):' | |||||
284 | if can_post: |
|
294 | if can_post: | |
285 | self.session[LAST_POST_TIME] = now |
|
295 | self.session[LAST_POST_TIME] = now | |
286 |
|
296 | |||
287 |
def validate_ |
|
297 | def validate_file_size(self, size: int): | |
288 |
max_size = board_settings.get_int('Forms', 'Max |
|
298 | max_size = board_settings.get_int('Forms', 'MaxFileSize') | |
289 | if size > max_size: |
|
299 | if size > max_size: | |
290 | raise forms.ValidationError( |
|
300 | raise forms.ValidationError( | |
291 |
_(' |
|
301 | _('File must be less than %s bytes') | |
292 | % str(max_size)) |
|
302 | % str(max_size)) | |
293 |
|
303 | |||
294 |
def _get_ |
|
304 | def _get_file_from_url(self, url: str) -> SimpleUploadedFile: | |
295 | """ |
|
305 | """ | |
296 |
Gets an |
|
306 | Gets an file file from URL. | |
297 | """ |
|
307 | """ | |
298 |
|
308 | |||
299 | img_temp = None |
|
309 | img_temp = None | |
@@ -302,30 +312,29 b' class PostForm(NeboardForm):' | |||||
302 | # Verify content headers |
|
312 | # Verify content headers | |
303 | response_head = requests.head(url, verify=False) |
|
313 | response_head = requests.head(url, verify=False) | |
304 | content_type = response_head.headers[HEADER_CONTENT_TYPE].split(';')[0] |
|
314 | content_type = response_head.headers[HEADER_CONTENT_TYPE].split(';')[0] | |
305 | if content_type in CONTENT_TYPE_IMAGE: |
|
315 | length_header = response_head.headers.get(HEADER_CONTENT_LENGTH) | |
306 | length_header = response_head.headers.get(HEADER_CONTENT_LENGTH) |
|
316 | if length_header: | |
307 |
|
|
317 | length = int(length_header) | |
308 | length = int(length_header) |
|
318 | self.validate_file_size(length) | |
309 | self.validate_image_size(length) |
|
319 | # Get the actual content into memory | |
310 | # Get the actual content into memory |
|
320 | response = requests.get(url, verify=False, stream=True) | |
311 | response = requests.get(url, verify=False, stream=True) |
|
|||
312 |
|
321 | |||
313 |
|
|
322 | # Download file, stop if the size exceeds limit | |
314 |
|
|
323 | size = 0 | |
315 |
|
|
324 | content = b'' | |
316 |
|
|
325 | for chunk in response.iter_content(FILE_DOWNLOAD_CHUNK_BYTES): | |
317 |
|
|
326 | size += len(chunk) | |
318 |
|
|
327 | self.validate_file_size(size) | |
319 |
|
|
328 | content += chunk | |
320 |
|
329 | |||
321 |
|
|
330 | if response.status_code == HTTP_RESULT_OK and content: | |
322 |
|
|
331 | # Set a dummy file name that will be replaced | |
323 |
|
|
332 | # anyway, just keep the valid extension | |
324 |
|
|
333 | filename = 'file.' + content_type.split('/')[1] | |
325 |
|
|
334 | img_temp = SimpleUploadedFile(filename, content, | |
326 |
|
|
335 | content_type) | |
327 | except Exception: |
|
336 | except Exception as e: | |
328 |
# Just return no |
|
337 | # Just return no file | |
329 | pass |
|
338 | pass | |
330 |
|
339 | |||
331 | return img_temp |
|
340 | return img_temp | |
@@ -356,8 +365,7 b' class ThreadForm(PostForm):' | |||||
356 | if not required_tag_exists: |
|
365 | if not required_tag_exists: | |
357 | all_tags = Tag.objects.filter(required=True) |
|
366 | all_tags = Tag.objects.filter(required=True) | |
358 | raise forms.ValidationError( |
|
367 | raise forms.ValidationError( | |
359 |
_('Need at least one |
|
368 | _('Need at least one section.')) | |
360 | + ', '.join([tag.name for tag in all_tags])) |
|
|||
361 |
|
369 | |||
362 | return tags |
|
370 | return tags | |
363 |
|
371 |
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-08-22 15:07+0300\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" | |
@@ -38,80 +38,84 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"' | |||||
38 | msgid "designer" |
|
38 | msgid "designer" | |
39 | msgstr "дизайнер" |
|
39 | msgstr "дизайнер" | |
40 |
|
40 | |||
41 |
#: forms.py:3 |
|
41 | #: forms.py:31 | |
42 | msgid "Type message here. Use formatting panel for more advanced usage." |
|
42 | msgid "Type message here. Use formatting panel for more advanced usage." | |
43 | msgstr "" |
|
43 | msgstr "" | |
44 | "Вводите сообщение сюда. Используйте панель для более сложного форматирования." |
|
44 | "Вводите сообщение сюда. Используйте панель для более сложного форматирования." | |
45 |
|
45 | |||
46 |
#: forms.py:3 |
|
46 | #: forms.py:32 | |
47 | msgid "music images i_dont_like_tags" |
|
47 | msgid "music images i_dont_like_tags" | |
48 | msgstr "музыка картинки теги_не_нужны" |
|
48 | msgstr "музыка картинки теги_не_нужны" | |
49 |
|
49 | |||
50 |
#: forms.py:3 |
|
50 | #: forms.py:34 | |
51 | msgid "Title" |
|
51 | msgid "Title" | |
52 | msgstr "Заголовок" |
|
52 | msgstr "Заголовок" | |
53 |
|
53 | |||
54 |
#: forms.py:3 |
|
54 | #: forms.py:35 | |
55 | msgid "Text" |
|
55 | msgid "Text" | |
56 | msgstr "Текст" |
|
56 | msgstr "Текст" | |
57 |
|
57 | |||
58 |
#: forms.py: |
|
58 | #: forms.py:36 | |
59 | msgid "Tag" |
|
59 | msgid "Tag" | |
60 | msgstr "Метка" |
|
60 | msgstr "Метка" | |
61 |
|
61 | |||
62 |
#: forms.py: |
|
62 | #: forms.py:37 templates/boards/base.html:40 templates/search/search.html:7 | |
63 | msgid "Search" |
|
63 | msgid "Search" | |
64 | msgstr "Поиск" |
|
64 | msgstr "Поиск" | |
65 |
|
65 | |||
66 |
#: forms.py: |
|
66 | #: forms.py:39 | |
67 | #, python-format |
|
67 | #, python-format | |
68 | msgid "Please wait %s seconds before sending message" |
|
68 | msgid "Please wait %s seconds before sending message" | |
69 | msgstr "Пожалуйста подождите %s секунд перед отправкой сообщения" |
|
69 | msgstr "Пожалуйста подождите %s секунд перед отправкой сообщения" | |
70 |
|
70 | |||
71 |
#: forms.py:14 |
|
71 | #: forms.py:140 | |
72 |
msgid " |
|
72 | msgid "File" | |
73 |
msgstr " |
|
73 | msgstr "Файл" | |
74 |
|
74 | |||
75 |
#: forms.py:14 |
|
75 | #: forms.py:143 | |
76 |
msgid " |
|
76 | msgid "File URL" | |
77 |
msgstr "URL |
|
77 | msgstr "URL файла" | |
78 |
|
78 | |||
79 |
#: forms.py:1 |
|
79 | #: forms.py:149 | |
80 | msgid "e-mail" |
|
80 | msgid "e-mail" | |
81 | msgstr "" |
|
81 | msgstr "" | |
82 |
|
82 | |||
83 |
#: forms.py:15 |
|
83 | #: forms.py:152 | |
84 | msgid "Additional threads" |
|
84 | msgid "Additional threads" | |
85 | msgstr "Дополнительные темы" |
|
85 | msgstr "Дополнительные темы" | |
86 |
|
86 | |||
87 |
#: forms.py:1 |
|
87 | #: forms.py:155 | |
|
88 | msgid "Tripcode" | |||
|
89 | msgstr "Трипкод" | |||
|
90 | ||||
|
91 | #: forms.py:164 | |||
88 | #, python-format |
|
92 | #, python-format | |
89 | msgid "Title must have less than %s characters" |
|
93 | msgid "Title must have less than %s characters" | |
90 | msgstr "Заголовок должен иметь меньше %s символов" |
|
94 | msgstr "Заголовок должен иметь меньше %s символов" | |
91 |
|
95 | |||
92 |
#: forms.py:17 |
|
96 | #: forms.py:174 | |
93 | #, python-format |
|
97 | #, python-format | |
94 | msgid "Text must have less than %s characters" |
|
98 | msgid "Text must have less than %s characters" | |
95 | msgstr "Текст должен быть короче %s символов" |
|
99 | msgstr "Текст должен быть короче %s символов" | |
96 |
|
100 | |||
97 |
#: forms.py:19 |
|
101 | #: forms.py:194 | |
98 | msgid "Invalid URL" |
|
102 | msgid "Invalid URL" | |
99 | msgstr "Неверный URL" |
|
103 | msgstr "Неверный URL" | |
100 |
|
104 | |||
101 |
#: forms.py:21 |
|
105 | #: forms.py:215 | |
102 | msgid "Invalid additional thread list" |
|
106 | msgid "Invalid additional thread list" | |
103 | msgstr "Неверный список дополнительных тем" |
|
107 | msgstr "Неверный список дополнительных тем" | |
104 |
|
108 | |||
105 |
#: forms.py:25 |
|
109 | #: forms.py:251 | |
106 |
msgid "Either text or |
|
110 | msgid "Either text or file must be entered." | |
107 |
msgstr "Текст или |
|
111 | msgstr "Текст или файл должны быть введены." | |
108 |
|
112 | |||
109 |
#: forms.py:28 |
|
113 | #: forms.py:289 | |
110 | #, python-format |
|
114 | #, python-format | |
111 |
msgid " |
|
115 | msgid "File must be less than %s bytes" | |
112 |
msgstr " |
|
116 | msgstr "Файл должен быть менее %s байт" | |
113 |
|
117 | |||
114 |
#: forms.py:335 templates/boards/all_threads.html:1 |
|
118 | #: forms.py:335 templates/boards/all_threads.html:154 | |
115 | #: templates/boards/rss/post.html:10 templates/boards/tags.html:6 |
|
119 | #: templates/boards/rss/post.html:10 templates/boards/tags.html:6 | |
116 | msgid "Tags" |
|
120 | msgid "Tags" | |
117 | msgstr "Метки" |
|
121 | msgstr "Метки" | |
@@ -121,26 +125,27 b' msgid "Inappropriate characters in tags.' | |||||
121 | msgstr "Недопустимые символы в метках." |
|
125 | msgstr "Недопустимые символы в метках." | |
122 |
|
126 | |||
123 | #: forms.py:356 |
|
127 | #: forms.py:356 | |
124 |
msgid "Need at least one |
|
128 | msgid "Need at least one section." | |
125 |
msgstr "Нужн |
|
129 | msgstr "Нужен хотя бы один раздел." | |
126 |
|
130 | |||
127 |
#: forms.py:36 |
|
131 | #: forms.py:368 | |
128 | msgid "Theme" |
|
132 | msgid "Theme" | |
129 | msgstr "Тема" |
|
133 | msgstr "Тема" | |
130 |
|
134 | |||
131 |
#: forms.py:3 |
|
135 | #: forms.py:369 | |
|
136 | #| msgid "Image view mode" | |||
132 | msgid "Image view mode" |
|
137 | msgid "Image view mode" | |
133 | msgstr "Режим просмотра изображений" |
|
138 | msgstr "Режим просмотра изображений" | |
134 |
|
139 | |||
135 |
#: forms.py:37 |
|
140 | #: forms.py:370 | |
136 | msgid "User name" |
|
141 | msgid "User name" | |
137 | msgstr "Имя пользователя" |
|
142 | msgstr "Имя пользователя" | |
138 |
|
143 | |||
139 |
#: forms.py:37 |
|
144 | #: forms.py:371 | |
140 | msgid "Time zone" |
|
145 | msgid "Time zone" | |
141 | msgstr "Часовой пояс" |
|
146 | msgstr "Часовой пояс" | |
142 |
|
147 | |||
143 |
#: forms.py:37 |
|
148 | #: forms.py:377 | |
144 | msgid "Inappropriate characters." |
|
149 | msgid "Inappropriate characters." | |
145 | msgstr "Недопустимые символы." |
|
150 | msgstr "Недопустимые символы." | |
146 |
|
151 | |||
@@ -156,54 +161,67 b' msgstr "\xd0\xad\xd1\x82\xd0\xbe\xd0\xb9 \xd1\x81\xd1\x82\xd1\x80\xd0\xb0\xd0\xbd\xd0\xb8\xd1\x86\xd1\x8b \xd0\xbd\xd0\xb5 \xd1\x81\xd1\x83\xd1\x89\xd0\xb5\xd1\x81\xd1\x82\xd0\xb2\xd1\x83\xd0\xb5\xd1\x82"' | |||||
156 | msgid "Related message" |
|
161 | msgid "Related message" | |
157 | msgstr "Связанное сообщение" |
|
162 | msgstr "Связанное сообщение" | |
158 |
|
163 | |||
159 |
#: templates/boards/all_threads.html: |
|
164 | #: templates/boards/all_threads.html:71 | |
160 | msgid "Edit tag" |
|
165 | msgid "Edit tag" | |
161 | msgstr "Изменить метку" |
|
166 | msgstr "Изменить метку" | |
162 |
|
167 | |||
163 |
#: templates/boards/all_threads.html: |
|
168 | #: templates/boards/all_threads.html:79 | |
164 | #, python-format |
|
169 | #, python-format | |
165 | msgid "This tag has %(thread_count)s threads and %(post_count)s posts." |
|
170 | msgid "" | |
166 | msgstr "С этой меткой есть %(thread_count)s тем и %(post_count)s сообщений." |
|
171 | "This tag has %(thread_count)s threads (%(active_thread_count)s active) and " | |
|
172 | "%(post_count)s posts." | |||
|
173 | msgstr "" | |||
|
174 | "С этой меткой есть %(thread_count)s тем (%(active_thread_count)s активных) и " | |||
|
175 | "%(post_count)s сообщений." | |||
167 |
|
176 | |||
168 |
#: templates/boards/all_threads.html: |
|
177 | #: templates/boards/all_threads.html:81 | |
|
178 | msgid "Related tags:" | |||
|
179 | msgstr "Похожие метки:" | |||
|
180 | ||||
|
181 | #: templates/boards/all_threads.html:96 templates/boards/feed.html:30 | |||
169 | #: templates/boards/notifications.html:17 templates/search/search.html:26 |
|
182 | #: templates/boards/notifications.html:17 templates/search/search.html:26 | |
170 | msgid "Previous page" |
|
183 | msgid "Previous page" | |
171 | msgstr "Предыдущая страница" |
|
184 | msgstr "Предыдущая страница" | |
172 |
|
185 | |||
173 |
#: templates/boards/all_threads.html: |
|
186 | #: templates/boards/all_threads.html:110 | |
174 | #, python-format |
|
187 | #, python-format | |
175 | msgid "Skipped %(count)s replies. Open thread to see all replies." |
|
188 | msgid "Skipped %(count)s replies. Open thread to see all replies." | |
176 | msgstr "Пропущено %(count)s ответов. Откройте тред, чтобы увидеть все ответы." |
|
189 | msgstr "Пропущено %(count)s ответов. Откройте тред, чтобы увидеть все ответы." | |
177 |
|
190 | |||
178 |
#: templates/boards/all_threads.html:1 |
|
191 | #: templates/boards/all_threads.html:128 templates/boards/feed.html:40 | |
179 | #: templates/boards/notifications.html:27 templates/search/search.html:37 |
|
192 | #: templates/boards/notifications.html:27 templates/search/search.html:37 | |
180 | msgid "Next page" |
|
193 | msgid "Next page" | |
181 | msgstr "Следующая страница" |
|
194 | msgstr "Следующая страница" | |
182 |
|
195 | |||
183 |
#: templates/boards/all_threads.html:1 |
|
196 | #: templates/boards/all_threads.html:133 | |
184 | msgid "No threads exist. Create the first one!" |
|
197 | msgid "No threads exist. Create the first one!" | |
185 | msgstr "Нет тем. Создайте первую!" |
|
198 | msgstr "Нет тем. Создайте первую!" | |
186 |
|
199 | |||
187 |
#: templates/boards/all_threads.html:1 |
|
200 | #: templates/boards/all_threads.html:139 | |
188 | msgid "Create new thread" |
|
201 | msgid "Create new thread" | |
189 | msgstr "Создать новую тему" |
|
202 | msgstr "Создать новую тему" | |
190 |
|
203 | |||
191 |
#: templates/boards/all_threads.html:1 |
|
204 | #: templates/boards/all_threads.html:144 templates/boards/preview.html:16 | |
192 | #: templates/boards/thread_normal.html:38 |
|
205 | #: templates/boards/thread_normal.html:38 | |
193 | msgid "Post" |
|
206 | msgid "Post" | |
194 | msgstr "Отправить" |
|
207 | msgstr "Отправить" | |
195 |
|
208 | |||
196 |
#: templates/boards/all_threads.html:1 |
|
209 | #: templates/boards/all_threads.html:149 | |
197 | msgid "Tags must be delimited by spaces. Text or image is required." |
|
210 | msgid "Tags must be delimited by spaces. Text or image is required." | |
198 | msgstr "" |
|
211 | msgstr "" | |
199 | "Метки должны быть разделены пробелами. Текст или изображение обязательны." |
|
212 | "Метки должны быть разделены пробелами. Текст или изображение обязательны." | |
200 |
|
213 | |||
201 |
#: templates/boards/all_threads.html:1 |
|
214 | #: templates/boards/all_threads.html:151 templates/boards/preview.html:6 | |
202 |
#: templates/boards/th |
|
215 | #: templates/boards/staticpages/help.html:21 | |
|
216 | #: templates/boards/thread_normal.html:42 | |||
|
217 | msgid "Preview" | |||
|
218 | msgstr "Предпросмотр" | |||
|
219 | ||||
|
220 | #: templates/boards/all_threads.html:153 templates/boards/thread_normal.html:45 | |||
203 | msgid "Text syntax" |
|
221 | msgid "Text syntax" | |
204 | msgstr "Синтаксис текста" |
|
222 | msgstr "Синтаксис текста" | |
205 |
|
223 | |||
206 |
#: templates/boards/all_threads.html:1 |
|
224 | #: templates/boards/all_threads.html:167 templates/boards/feed.html:53 | |
207 | msgid "Pages:" |
|
225 | msgid "Pages:" | |
208 | msgstr "Страницы: " |
|
226 | msgstr "Страницы: " | |
209 |
|
227 | |||
@@ -251,25 +269,33 b' msgstr "\xd0\xbf\xd0\xbe\xd0\xb8\xd1\x81\xd0\xba"' | |||||
251 | msgid "feed" |
|
269 | msgid "feed" | |
252 | msgstr "лента" |
|
270 | msgstr "лента" | |
253 |
|
271 | |||
254 |
#: templates/boards/base.html:4 |
|
272 | #: templates/boards/base.html:42 templates/boards/random.html:6 | |
|
273 | msgid "Random images" | |||
|
274 | msgstr "Случайные изображения" | |||
|
275 | ||||
|
276 | #: templates/boards/base.html:42 | |||
|
277 | msgid "random" | |||
|
278 | msgstr "случайные" | |||
|
279 | ||||
|
280 | #: templates/boards/base.html:45 templates/boards/base.html.py:46 | |||
255 | #: templates/boards/notifications.html:8 |
|
281 | #: templates/boards/notifications.html:8 | |
256 | msgid "Notifications" |
|
282 | msgid "Notifications" | |
257 | msgstr "Уведомления" |
|
283 | msgstr "Уведомления" | |
258 |
|
284 | |||
259 |
#: templates/boards/base.html:5 |
|
285 | #: templates/boards/base.html:53 templates/boards/settings.html:8 | |
260 | msgid "Settings" |
|
286 | msgid "Settings" | |
261 | msgstr "Настройки" |
|
287 | msgstr "Настройки" | |
262 |
|
288 | |||
263 |
#: templates/boards/base.html:6 |
|
289 | #: templates/boards/base.html:66 | |
264 | msgid "Admin" |
|
290 | msgid "Admin" | |
265 | msgstr "Администрирование" |
|
291 | msgstr "Администрирование" | |
266 |
|
292 | |||
267 |
#: templates/boards/base.html:6 |
|
293 | #: templates/boards/base.html:68 | |
268 | #, python-format |
|
294 | #, python-format | |
269 | msgid "Speed: %(ppd)s posts per day" |
|
295 | msgid "Speed: %(ppd)s posts per day" | |
270 | msgstr "Скорость: %(ppd)s сообщений в день" |
|
296 | msgstr "Скорость: %(ppd)s сообщений в день" | |
271 |
|
297 | |||
272 |
#: templates/boards/base.html: |
|
298 | #: templates/boards/base.html:70 | |
273 | msgid "Up" |
|
299 | msgid "Up" | |
274 | msgstr "Вверх" |
|
300 | msgstr "Вверх" | |
275 |
|
301 | |||
@@ -277,58 +303,62 b' msgstr "\xd0\x92\xd0\xb2\xd0\xb5\xd1\x80\xd1\x85"' | |||||
277 | msgid "No posts exist. Create the first one!" |
|
303 | msgid "No posts exist. Create the first one!" | |
278 | msgstr "Нет сообщений. Создайте первое!" |
|
304 | msgstr "Нет сообщений. Создайте первое!" | |
279 |
|
305 | |||
280 |
#: templates/boards/post.html: |
|
306 | #: templates/boards/post.html:30 | |
281 | msgid "Open" |
|
307 | msgid "Open" | |
282 | msgstr "Открыть" |
|
308 | msgstr "Открыть" | |
283 |
|
309 | |||
284 |
#: templates/boards/post.html:2 |
|
310 | #: templates/boards/post.html:32 templates/boards/post.html.py:43 | |
285 | msgid "Reply" |
|
311 | msgid "Reply" | |
286 | msgstr "Ответить" |
|
312 | msgstr "Ответить" | |
287 |
|
313 | |||
288 |
#: templates/boards/post.html:3 |
|
314 | #: templates/boards/post.html:38 | |
289 | msgid " in " |
|
315 | msgid " in " | |
290 | msgstr " в " |
|
316 | msgstr " в " | |
291 |
|
317 | |||
292 |
#: templates/boards/post.html:4 |
|
318 | #: templates/boards/post.html:48 | |
293 | msgid "Edit" |
|
319 | msgid "Edit" | |
294 | msgstr "Изменить" |
|
320 | msgstr "Изменить" | |
295 |
|
321 | |||
296 |
#: templates/boards/post.html: |
|
322 | #: templates/boards/post.html:50 | |
297 | msgid "Edit thread" |
|
323 | msgid "Edit thread" | |
298 | msgstr "Изменить тему" |
|
324 | msgstr "Изменить тему" | |
299 |
|
325 | |||
300 |
#: templates/boards/post.html: |
|
326 | #: templates/boards/post.html:97 | |
301 | msgid "Replies" |
|
327 | msgid "Replies" | |
302 | msgstr "Ответы" |
|
328 | msgstr "Ответы" | |
303 |
|
329 | |||
304 |
#: templates/boards/post.html:9 |
|
330 | #: templates/boards/post.html:109 templates/boards/thread.html:34 | |
305 | msgid "messages" |
|
331 | msgid "messages" | |
306 | msgstr "сообщений" |
|
332 | msgstr "сообщений" | |
307 |
|
333 | |||
308 |
#: templates/boards/post.html: |
|
334 | #: templates/boards/post.html:110 templates/boards/thread.html:35 | |
309 | msgid "images" |
|
335 | msgid "images" | |
310 | msgstr "изображений" |
|
336 | msgstr "изображений" | |
311 |
|
337 | |||
312 | #: templates/boards/preview.html:6 templates/boards/staticpages/help.html:20 |
|
|||
313 | msgid "Preview" |
|
|||
314 | msgstr "Предпросмотр" |
|
|||
315 |
|
||||
316 | #: templates/boards/rss/post.html:5 |
|
338 | #: templates/boards/rss/post.html:5 | |
317 | msgid "Post image" |
|
339 | msgid "Post image" | |
318 | msgstr "Изображение сообщения" |
|
340 | msgstr "Изображение сообщения" | |
319 |
|
341 | |||
320 |
#: templates/boards/settings.html:1 |
|
342 | #: templates/boards/settings.html:15 | |
321 | msgid "You are moderator." |
|
343 | msgid "You are moderator." | |
322 | msgstr "Вы модератор." |
|
344 | msgstr "Вы модератор." | |
323 |
|
345 | |||
324 |
#: templates/boards/settings.html: |
|
346 | #: templates/boards/settings.html:19 | |
325 | msgid "Hidden tags:" |
|
347 | msgid "Hidden tags:" | |
326 | msgstr "Скрытые метки:" |
|
348 | msgstr "Скрытые метки:" | |
327 |
|
349 | |||
328 |
#: templates/boards/settings.html:2 |
|
350 | #: templates/boards/settings.html:27 | |
329 | msgid "No hidden tags." |
|
351 | msgid "No hidden tags." | |
330 | msgstr "Нет скрытых меток." |
|
352 | msgstr "Нет скрытых меток." | |
331 |
|
353 | |||
|
354 | #: templates/boards/settings.html:29 | |||
|
355 | msgid "Tripcode:" | |||
|
356 | msgstr "Трипкод:" | |||
|
357 | ||||
|
358 | #: templates/boards/settings.html:29 | |||
|
359 | msgid "reset" | |||
|
360 | msgstr "сбросить" | |||
|
361 | ||||
332 | #: templates/boards/settings.html:37 |
|
362 | #: templates/boards/settings.html:37 | |
333 | msgid "Save" |
|
363 | msgid "Save" | |
334 | msgstr "Сохранить" |
|
364 | msgstr "Сохранить" | |
@@ -375,34 +405,35 b' msgstr "\xd0\x9a\xd0\xbe\xd0\xbc\xd0\xbc\xd0\xb5\xd0\xbd\xd1\x82\xd0\xb0\xd1\x80\xd0\xb8\xd0\xb9"' | |||||
375 | msgid "Quote" |
|
405 | msgid "Quote" | |
376 | msgstr "Цитата" |
|
406 | msgstr "Цитата" | |
377 |
|
407 | |||
378 |
#: templates/boards/staticpages/help.html:2 |
|
408 | #: templates/boards/staticpages/help.html:21 | |
379 | msgid "You can try pasting the text and previewing the result here:" |
|
409 | msgid "You can try pasting the text and previewing the result here:" | |
380 | msgstr "Вы можете попробовать вставить текст и проверить результат здесь:" |
|
410 | msgstr "Вы можете попробовать вставить текст и проверить результат здесь:" | |
381 |
|
411 | |||
382 |
#: templates/boards/tags.html: |
|
412 | #: templates/boards/tags.html:17 | |
383 | msgid "No tags found." |
|
413 | msgid "Sections:" | |
384 |
msgstr " |
|
414 | msgstr "Разделы:" | |
385 |
|
415 | |||
386 |
#: templates/boards/tags.html: |
|
416 | #: templates/boards/tags.html:30 | |
387 |
msgid " |
|
417 | msgid "Other tags:" | |
388 |
msgstr " |
|
418 | msgstr "Другие метки:" | |
|
419 | ||||
|
420 | #: templates/boards/tags.html:43 | |||
|
421 | msgid "All tags..." | |||
|
422 | msgstr "Все метки..." | |||
389 |
|
423 | |||
390 | #: templates/boards/thread.html:15 |
|
424 | #: templates/boards/thread.html:15 | |
391 | #| msgid "Normal mode" |
|
|||
392 | msgid "Normal" |
|
425 | msgid "Normal" | |
393 | msgstr "Нормальный" |
|
426 | msgstr "Нормальный" | |
394 |
|
427 | |||
395 | #: templates/boards/thread.html:16 |
|
428 | #: templates/boards/thread.html:16 | |
396 | #| msgid "Gallery mode" |
|
|||
397 | msgid "Gallery" |
|
429 | msgid "Gallery" | |
398 | msgstr "Галерея" |
|
430 | msgstr "Галерея" | |
399 |
|
431 | |||
400 | #: templates/boards/thread.html:17 |
|
432 | #: templates/boards/thread.html:17 | |
401 | #| msgid "Tree mode" |
|
|||
402 | msgid "Tree" |
|
433 | msgid "Tree" | |
403 | msgstr "Дерево" |
|
434 | msgstr "Дерево" | |
404 |
|
435 | |||
405 |
#: templates/boards/thread.html:3 |
|
436 | #: templates/boards/thread.html:36 | |
406 | msgid "Last update: " |
|
437 | msgid "Last update: " | |
407 | msgstr "Последнее обновление: " |
|
438 | msgstr "Последнее обновление: " | |
408 |
|
439 | |||
@@ -418,14 +449,14 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"' | |||||
418 | msgid "Reply to thread" |
|
449 | msgid "Reply to thread" | |
419 | msgstr "Ответить в тему" |
|
450 | msgstr "Ответить в тему" | |
420 |
|
451 | |||
421 |
#: templates/boards/thread_normal.html: |
|
452 | #: templates/boards/thread_normal.html:31 | |
|
453 | msgid "to message " | |||
|
454 | msgstr "на сообщение" | |||
|
455 | ||||
|
456 | #: templates/boards/thread_normal.html:46 | |||
422 | msgid "Close form" |
|
457 | msgid "Close form" | |
423 | msgstr "Закрыть форму" |
|
458 | msgstr "Закрыть форму" | |
424 |
|
459 | |||
425 | #: templates/boards/thread_normal.html:58 |
|
|||
426 | msgid "Update" |
|
|||
427 | msgstr "Обновить" |
|
|||
428 |
|
||||
429 | #: templates/search/search.html:17 |
|
460 | #: templates/search/search.html:17 | |
430 | msgid "Ok" |
|
461 | msgid "Ok" | |
431 | msgstr "Ок" |
|
462 | msgstr "Ок" |
@@ -2,6 +2,8 b' import os' | |||||
2 |
|
2 | |||
3 | from django.core.management import BaseCommand |
|
3 | from django.core.management import BaseCommand | |
4 | from django.db import transaction |
|
4 | from django.db import transaction | |
|
5 | from boards.models import Attachment | |||
|
6 | from boards.models.attachment import FILES_DIRECTORY | |||
5 |
|
7 | |||
6 | from boards.models.image import IMAGES_DIRECTORY, PostImage, IMAGE_THUMB_SIZE |
|
8 | from boards.models.image import IMAGES_DIRECTORY, PostImage, IMAGE_THUMB_SIZE | |
7 | from neboard.settings import MEDIA_ROOT |
|
9 | from neboard.settings import MEDIA_ROOT | |
@@ -11,7 +13,7 b' from neboard.settings import MEDIA_ROOT' | |||||
11 |
|
13 | |||
12 |
|
14 | |||
13 | class Command(BaseCommand): |
|
15 | class Command(BaseCommand): | |
14 |
help = 'Remove |
|
16 | help = 'Remove files whose models were deleted' | |
15 |
|
17 | |||
16 | @transaction.atomic |
|
18 | @transaction.atomic | |
17 | def handle(self, *args, **options): |
|
19 | def handle(self, *args, **options): | |
@@ -21,10 +23,24 b' class Command(BaseCommand):' | |||||
21 | model_files = os.listdir(MEDIA_ROOT + IMAGES_DIRECTORY) |
|
23 | model_files = os.listdir(MEDIA_ROOT + IMAGES_DIRECTORY) | |
22 | for file in model_files: |
|
24 | for file in model_files: | |
23 | image_name = file if thumb_prefix not in file else file.replace(thumb_prefix, '') |
|
25 | image_name = file if thumb_prefix not in file else file.replace(thumb_prefix, '') | |
24 |
found = PostImage.objects.filter( |
|
26 | found = PostImage.objects.filter( | |
|
27 | image=IMAGES_DIRECTORY + image_name).exists() | |||
25 |
|
28 | |||
26 | if not found: |
|
29 | if not found: | |
27 | print('Missing {}'.format(image_name)) |
|
30 | print('Missing {}'.format(image_name)) | |
28 | os.remove(MEDIA_ROOT + IMAGES_DIRECTORY + file) |
|
31 | os.remove(MEDIA_ROOT + IMAGES_DIRECTORY + file) | |
29 | count += 1 |
|
32 | count += 1 | |
30 | print('Deleted {} image files.'.format(count)) No newline at end of file |
|
33 | print('Deleted {} image files.'.format(count)) | |
|
34 | ||||
|
35 | count = 0 | |||
|
36 | model_files = os.listdir(MEDIA_ROOT + FILES_DIRECTORY) | |||
|
37 | for file in model_files: | |||
|
38 | found = Attachment.objects.filter(file=FILES_DIRECTORY + file)\ | |||
|
39 | .exists() | |||
|
40 | ||||
|
41 | if not found: | |||
|
42 | print('Missing {}'.format(file)) | |||
|
43 | os.remove(MEDIA_ROOT + FILES_DIRECTORY + file) | |||
|
44 | count += 1 | |||
|
45 | ||||
|
46 | print('Deleted {} attachment files.'.format(count)) |
@@ -130,7 +130,7 b' def render_reflink(tag_name, value, opti' | |||||
130 | try: |
|
130 | try: | |
131 | post = boards.models.Post.objects.get(id=post_id) |
|
131 | post = boards.models.Post.objects.get(id=post_id) | |
132 |
|
132 | |||
133 | result = '<a href="%s">>>%s</a>' % (post.get_absolute_url(), post_id) |
|
133 | result = post.get_link_view() | |
134 | except ObjectDoesNotExist: |
|
134 | except ObjectDoesNotExist: | |
135 | pass |
|
135 | pass | |
136 |
|
136 |
@@ -3,6 +3,7 b'' | |||||
3 | from boards.models.signature import GlobalId, Signature |
|
3 | from boards.models.signature import GlobalId, Signature | |
4 | from boards.models.sync_key import KeyPair |
|
4 | from boards.models.sync_key import KeyPair | |
5 | from boards.models.image import PostImage |
|
5 | from boards.models.image import PostImage | |
|
6 | from boards.models.attachment import Attachment | |||
6 | from boards.models.thread import Thread |
|
7 | from boards.models.thread import Thread | |
7 | from boards.models.post import Post |
|
8 | from boards.models.post import Post | |
8 | from boards.models.tag import Tag |
|
9 | from boards.models.tag import Tag |
@@ -2,8 +2,12 b' import hashlib' | |||||
2 | import os |
|
2 | import os | |
3 | from random import random |
|
3 | from random import random | |
4 | import time |
|
4 | import time | |
|
5 | ||||
5 | from django.db import models |
|
6 | from django.db import models | |
6 | from boards import thumbs |
|
7 | from django.template.defaultfilters import filesizeformat | |
|
8 | ||||
|
9 | from boards import thumbs, utils | |||
|
10 | import boards | |||
7 | from boards.models.base import Viewable |
|
11 | from boards.models.base import Viewable | |
8 |
|
12 | |||
9 | __author__ = 'neko259' |
|
13 | __author__ = 'neko259' | |
@@ -20,7 +24,7 b" CSS_CLASS_THUMB = 'thumb'" | |||||
20 |
|
24 | |||
21 | class PostImageManager(models.Manager): |
|
25 | class PostImageManager(models.Manager): | |
22 | def create_with_hash(self, image): |
|
26 | def create_with_hash(self, image): | |
23 |
image_hash = s |
|
27 | image_hash = utils.get_file_hash(image) | |
24 | existing = self.filter(hash=image_hash) |
|
28 | existing = self.filter(hash=image_hash) | |
25 | if len(existing) > 0: |
|
29 | if len(existing) > 0: | |
26 | post_image = existing[0] |
|
30 | post_image = existing[0] | |
@@ -29,14 +33,11 b' class PostImageManager(models.Manager):' | |||||
29 |
|
33 | |||
30 | return post_image |
|
34 | return post_image | |
31 |
|
35 | |||
32 | def get_hash(self, image): |
|
36 | def get_random_images(self, count, include_archived=False, tags=None): | |
33 | """ |
|
37 | images = self.filter(post_images__thread__archived=include_archived) | |
34 | Gets hash of an image. |
|
38 | if tags is not None: | |
35 | """ |
|
39 | images = images.filter(post_images__threads__tags__in=tags) | |
36 | md5 = hashlib.md5() |
|
40 | return images.order_by('?')[:count] | |
37 | for chunk in image.chunks(): |
|
|||
38 | md5.update(chunk) |
|
|||
39 | return md5.hexdigest() |
|
|||
40 |
|
41 | |||
41 |
|
42 | |||
42 | class PostImage(models.Model, Viewable): |
|
43 | class PostImage(models.Model, Viewable): | |
@@ -51,15 +52,13 b' class PostImage(models.Model, Viewable):' | |||||
51 | Gets unique image filename |
|
52 | Gets unique image filename | |
52 | """ |
|
53 | """ | |
53 |
|
54 | |||
54 | path = IMAGES_DIRECTORY |
|
|||
55 |
|
||||
56 | # TODO Use something other than random number in file name |
|
55 | # TODO Use something other than random number in file name | |
57 | new_name = '{}{}.{}'.format( |
|
56 | new_name = '{}{}.{}'.format( | |
58 | str(int(time.mktime(time.gmtime()))), |
|
57 | str(int(time.mktime(time.gmtime()))), | |
59 | str(int(random() * 1000)), |
|
58 | str(int(random() * 1000)), | |
60 | filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]) |
|
59 | filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]) | |
61 |
|
60 | |||
62 |
return os.path.join( |
|
61 | return os.path.join(IMAGES_DIRECTORY, new_name) | |
63 |
|
62 | |||
64 | width = models.IntegerField(default=0) |
|
63 | width = models.IntegerField(default=0) | |
65 | height = models.IntegerField(default=0) |
|
64 | height = models.IntegerField(default=0) | |
@@ -81,13 +80,15 b' class PostImage(models.Model, Viewable):' | |||||
81 | """ |
|
80 | """ | |
82 |
|
81 | |||
83 | if not self.pk and self.image: |
|
82 | if not self.pk and self.image: | |
84 |
self.hash = |
|
83 | self.hash = utils.get_file_hash(self.image) | |
85 | super(PostImage, self).save(*args, **kwargs) |
|
84 | super(PostImage, self).save(*args, **kwargs) | |
86 |
|
85 | |||
87 | def __str__(self): |
|
86 | def __str__(self): | |
88 | return self.image.url |
|
87 | return self.image.url | |
89 |
|
88 | |||
90 | def get_view(self): |
|
89 | def get_view(self): | |
|
90 | metadata = '{}, {}'.format(self.image.name.split('.')[-1], | |||
|
91 | filesizeformat(self.image.size)) | |||
91 | return '<div class="{}">' \ |
|
92 | return '<div class="{}">' \ | |
92 | '<a class="{}" href="{full}">' \ |
|
93 | '<a class="{}" href="{full}">' \ | |
93 | '<img class="post-image-preview"' \ |
|
94 | '<img class="post-image-preview"' \ | |
@@ -98,8 +99,16 b' class PostImage(models.Model, Viewable):' | |||||
98 | ' data-width="{}"' \ |
|
99 | ' data-width="{}"' \ | |
99 | ' data-height="{}" />' \ |
|
100 | ' data-height="{}" />' \ | |
100 | '</a>' \ |
|
101 | '</a>' \ | |
|
102 | '<div class="image-metadata">'\ | |||
|
103 | '<a href="{full}" download>{image_meta}</a>'\ | |||
|
104 | '</div>' \ | |||
101 | '</div>'\ |
|
105 | '</div>'\ | |
102 | .format(CSS_CLASS_IMAGE, CSS_CLASS_THUMB, |
|
106 | .format(CSS_CLASS_IMAGE, CSS_CLASS_THUMB, | |
103 | self.image.url_200x150, |
|
107 | self.image.url_200x150, | |
104 | str(self.hash), str(self.pre_width), |
|
108 | str(self.hash), str(self.pre_width), | |
105 |
str(self.pre_height), str(self.width), str(self.height), |
|
109 | str(self.pre_height), str(self.width), str(self.height), | |
|
110 | full=self.image.url, image_meta=metadata) | |||
|
111 | ||||
|
112 | def get_random_associated_post(self): | |||
|
113 | posts = boards.models.Post.objects.filter(images__in=[self]) | |||
|
114 | return posts.order_by('?').first() |
@@ -1,3 +1,5 b'' | |||||
|
1 | from datetime import datetime, timedelta, date | |||
|
2 | from datetime import time as dtime | |||
1 | import logging |
|
3 | import logging | |
2 | import re |
|
4 | import re | |
3 | import uuid |
|
5 | import uuid | |
@@ -6,15 +8,14 b' from django.core.exceptions import Objec' | |||||
6 | from django.core.urlresolvers import reverse |
|
8 | from django.core.urlresolvers import reverse | |
7 | from django.db import models |
|
9 | from django.db import models | |
8 | from django.db.models import TextField, QuerySet |
|
10 | from django.db.models import TextField, QuerySet | |
9 |
|
||||
10 | from django.template.loader import render_to_string |
|
11 | from django.template.loader import render_to_string | |
11 |
|
||||
12 | from django.utils import timezone |
|
12 | from django.utils import timezone | |
13 |
|
13 | |||
|
14 | from boards.abstracts.tripcode import Tripcode | |||
14 | from boards.mdx_neboard import Parser |
|
15 | from boards.mdx_neboard import Parser | |
15 | from boards.models import KeyPair, GlobalId |
|
16 | from boards.models import KeyPair, GlobalId | |
16 | from boards import settings |
|
17 | from boards import settings | |
17 | from boards.models import PostImage |
|
18 | from boards.models import PostImage, Attachment | |
18 | from boards.models.base import Viewable |
|
19 | from boards.models.base import Viewable | |
19 | from boards.models.post.export import get_exporter, DIFF_TYPE_JSON |
|
20 | from boards.models.post.export import get_exporter, DIFF_TYPE_JSON | |
20 | from boards.models.post.manager import PostManager |
|
21 | from boards.models.post.manager import PostManager | |
@@ -61,8 +62,6 b' POST_VIEW_PARAMS = (' | |||||
61 | 'mode_tree', |
|
62 | 'mode_tree', | |
62 | ) |
|
63 | ) | |
63 |
|
64 | |||
64 | REFMAP_STR = '<a href="{}">>>{}</a>' |
|
|||
65 |
|
||||
66 |
|
65 | |||
67 | class Post(models.Model, Viewable): |
|
66 | class Post(models.Model, Viewable): | |
68 | """A post is a message.""" |
|
67 | """A post is a message.""" | |
@@ -79,7 +78,9 b' class Post(models.Model, Viewable):' | |||||
79 | _text_rendered = TextField(blank=True, null=True, editable=False) |
|
78 | _text_rendered = TextField(blank=True, null=True, editable=False) | |
80 |
|
79 | |||
81 | images = models.ManyToManyField(PostImage, null=True, blank=True, |
|
80 | images = models.ManyToManyField(PostImage, null=True, blank=True, | |
82 |
related_name=' |
|
81 | related_name='post_images', db_index=True) | |
|
82 | attachments = models.ManyToManyField(Attachment, null=True, blank=True, | |||
|
83 | related_name='attachment_posts') | |||
83 |
|
84 | |||
84 | poster_ip = models.GenericIPAddressField() |
|
85 | poster_ip = models.GenericIPAddressField() | |
85 |
|
86 | |||
@@ -101,6 +102,8 b' class Post(models.Model, Viewable):' | |||||
101 | # server, this indicates the server. |
|
102 | # server, this indicates the server. | |
102 | global_id = models.OneToOneField('GlobalId', null=True, blank=True) |
|
103 | global_id = models.OneToOneField('GlobalId', null=True, blank=True) | |
103 |
|
104 | |||
|
105 | tripcode = models.CharField(max_length=50, null=True) | |||
|
106 | ||||
104 | def __str__(self): |
|
107 | def __str__(self): | |
105 | return 'P#{}/{}'.format(self.id, self.title) |
|
108 | return 'P#{}/{}'.format(self.id, self.title) | |
106 |
|
109 | |||
@@ -126,7 +129,7 b' class Post(models.Model, Viewable):' | |||||
126 | the server from recalculating the map on every post show. |
|
129 | the server from recalculating the map on every post show. | |
127 | """ |
|
130 | """ | |
128 |
|
131 | |||
129 |
post_urls = [ |
|
132 | post_urls = [refpost.get_link_view() | |
130 | for refpost in self.referenced_posts.all()] |
|
133 | for refpost in self.referenced_posts.all()] | |
131 |
|
134 | |||
132 | self.refmap = ', '.join(post_urls) |
|
135 | self.refmap = ', '.join(post_urls) | |
@@ -209,10 +212,15 b' class Post(models.Model, Viewable):' | |||||
209 | """ |
|
212 | """ | |
210 |
|
213 | |||
211 | for image in self.images.all(): |
|
214 | for image in self.images.all(): | |
212 |
image_refs_count = |
|
215 | image_refs_count = image.post_images.count() | |
213 | if image_refs_count == 1: |
|
216 | if image_refs_count == 1: | |
214 | image.delete() |
|
217 | image.delete() | |
215 |
|
218 | |||
|
219 | for attachment in self.attachments.all(): | |||
|
220 | attachment_refs_count = attachment.attachment_posts.count() | |||
|
221 | if attachment_refs_count == 1: | |||
|
222 | attachment.delete() | |||
|
223 | ||||
216 | if self.global_id: |
|
224 | if self.global_id: | |
217 | self.global_id.delete() |
|
225 | self.global_id.delete() | |
218 |
|
226 | |||
@@ -399,3 +407,19 b' class Post(models.Model, Viewable):' | |||||
399 | thread.last_edit_time = self.last_edit_time |
|
407 | thread.last_edit_time = self.last_edit_time | |
400 | thread.save(update_fields=['last_edit_time', 'bumpable']) |
|
408 | thread.save(update_fields=['last_edit_time', 'bumpable']) | |
401 | self.threads.add(opening_post.get_thread()) |
|
409 | self.threads.add(opening_post.get_thread()) | |
|
410 | ||||
|
411 | def get_tripcode(self): | |||
|
412 | if self.tripcode: | |||
|
413 | return Tripcode(self.tripcode) | |||
|
414 | ||||
|
415 | def get_link_view(self): | |||
|
416 | """ | |||
|
417 | Gets view of a reflink to the post. | |||
|
418 | """ | |||
|
419 | ||||
|
420 | result = '<a href="{}">>>{}</a>'.format(self.get_absolute_url(), | |||
|
421 | self.id) | |||
|
422 | if self.is_opening(): | |||
|
423 | result = '<b>{}</b>'.format(result) | |||
|
424 | ||||
|
425 | return result |
@@ -5,7 +5,7 b' from django.db import models, transactio' | |||||
5 | from django.utils import timezone |
|
5 | from django.utils import timezone | |
6 | from boards import utils |
|
6 | from boards import utils | |
7 | from boards.mdx_neboard import Parser |
|
7 | from boards.mdx_neboard import Parser | |
8 | from boards.models import PostImage |
|
8 | from boards.models import PostImage, Attachment | |
9 | import boards.models |
|
9 | import boards.models | |
10 |
|
10 | |||
11 | __author__ = 'vurdalak' |
|
11 | __author__ = 'vurdalak' | |
@@ -14,11 +14,19 b' import boards.models' | |||||
14 | NO_IP = '0.0.0.0' |
|
14 | NO_IP = '0.0.0.0' | |
15 | POSTS_PER_DAY_RANGE = 7 |
|
15 | POSTS_PER_DAY_RANGE = 7 | |
16 |
|
16 | |||
|
17 | IMAGE_TYPES = ( | |||
|
18 | 'jpeg', | |||
|
19 | 'jpg', | |||
|
20 | 'png', | |||
|
21 | 'bmp', | |||
|
22 | 'gif', | |||
|
23 | ) | |||
|
24 | ||||
17 |
|
25 | |||
18 | class PostManager(models.Manager): |
|
26 | class PostManager(models.Manager): | |
19 | @transaction.atomic |
|
27 | @transaction.atomic | |
20 |
def create_post(self, title: str, text: str, |
|
28 | def create_post(self, title: str, text: str, file=None, thread=None, | |
21 | ip=NO_IP, tags: list=None, opening_posts: list=None): |
|
29 | ip=NO_IP, tags: list=None, opening_posts: list=None, tripcode=None): | |
22 | """ |
|
30 | """ | |
23 | Creates new post |
|
31 | Creates new post | |
24 | """ |
|
32 | """ | |
@@ -50,49 +58,33 b' class PostManager(models.Manager):' | |||||
50 | pub_time=posting_time, |
|
58 | pub_time=posting_time, | |
51 | poster_ip=ip, |
|
59 | poster_ip=ip, | |
52 | thread=thread, |
|
60 | thread=thread, | |
53 |
last_edit_time=posting_time |
|
61 | last_edit_time=posting_time, | |
|
62 | tripcode=tripcode) | |||
54 | post.threads.add(thread) |
|
63 | post.threads.add(thread) | |
55 |
|
64 | |||
56 | post.set_global_id() |
|
|||
57 |
|
||||
58 | logger = logging.getLogger('boards.post.create') |
|
65 | logger = logging.getLogger('boards.post.create') | |
59 |
|
66 | |||
60 | logger.info('Created post {} by {}'.format(post, post.poster_ip)) |
|
67 | logger.info('Created post {} by {}'.format(post, post.poster_ip)) | |
61 |
|
68 | |||
62 | if image: |
|
69 | # TODO Move this to other place | |
63 | post.images.add(PostImage.objects.create_with_hash(image)) |
|
70 | if file: | |
64 |
|
71 | file_type = file.name.split('.')[-1].lower() | ||
65 | if not new_thread: |
|
72 | if file_type in IMAGE_TYPES: | |
66 | thread.last_edit_time = posting_time |
|
73 | post.images.add(PostImage.objects.create_with_hash(file)) | |
67 |
|
|
74 | else: | |
68 | thread.save() |
|
75 | post.attachments.add(Attachment.objects.create_with_hash(file)) | |
69 |
|
76 | |||
70 | post.build_url() |
|
77 | post.build_url() | |
71 | post.connect_replies() |
|
78 | post.connect_replies() | |
72 | post.connect_threads(opening_posts) |
|
79 | post.connect_threads(opening_posts) | |
73 | post.connect_notifications() |
|
80 | post.connect_notifications() | |
74 |
|
81 | post.set_global_id() | ||
75 | return post |
|
|||
76 |
|
82 | |||
77 | @transaction.atomic |
|
83 | # Thread needs to be bumped only when the post is already created | |
78 | def import_post(self, title: str, text: str, pub_time: str, global_id, |
|
84 | if not new_thread: | |
79 | opening_post=None, tags=list()): |
|
85 | thread.last_edit_time = posting_time | |
80 | if opening_post is None: |
|
86 | thread.bump() | |
81 | thread = boards.models.thread.Thread.objects.create( |
|
87 | thread.save() | |
82 | bump_time=pub_time, last_edit_time=pub_time) |
|
|||
83 | list(map(thread.tags.add, tags)) |
|
|||
84 | else: |
|
|||
85 | thread = opening_post.get_thread() |
|
|||
86 |
|
||||
87 | post = self.create(title=title, text=text, |
|
|||
88 | pub_time=pub_time, |
|
|||
89 | poster_ip=NO_IP, |
|
|||
90 | last_edit_time=pub_time, |
|
|||
91 | thread_id=thread.id, global_id=global_id) |
|
|||
92 |
|
||||
93 | post.build_url() |
|
|||
94 | post.connect_replies() |
|
|||
95 | post.connect_notifications() |
|
|||
96 |
|
88 | |||
97 | return post |
|
89 | return post | |
98 |
|
90 | |||
@@ -126,3 +118,23 b' class PostManager(models.Manager):' | |||||
126 | ppd = posts_per_period / POSTS_PER_DAY_RANGE |
|
118 | ppd = posts_per_period / POSTS_PER_DAY_RANGE | |
127 |
|
119 | |||
128 | return ppd |
|
120 | return ppd | |
|
121 | ||||
|
122 | @transaction.atomic | |||
|
123 | def import_post(self, title: str, text: str, pub_time: str, global_id, | |||
|
124 | opening_post=None, tags=list()): | |||
|
125 | if opening_post is None: | |||
|
126 | thread = boards.models.thread.Thread.objects.create( | |||
|
127 | bump_time=pub_time, last_edit_time=pub_time) | |||
|
128 | list(map(thread.tags.add, tags)) | |||
|
129 | else: | |||
|
130 | thread = opening_post.get_thread() | |||
|
131 | ||||
|
132 | post = self.create(title=title, text=text, | |||
|
133 | pub_time=pub_time, | |||
|
134 | poster_ip=NO_IP, | |||
|
135 | last_edit_time=pub_time, | |||
|
136 | thread_id=thread.id, global_id=global_id) | |||
|
137 | ||||
|
138 | post.build_url() | |||
|
139 | post.connect_replies() | |||
|
140 | post.connect_notifications() |
@@ -5,9 +5,12 b' from django.core.urlresolvers import rev' | |||||
5 |
|
5 | |||
6 | from boards.models.base import Viewable |
|
6 | from boards.models.base import Viewable | |
7 | from boards.utils import cached_result |
|
7 | from boards.utils import cached_result | |
|
8 | import boards | |||
|
9 | ||||
|
10 | __author__ = 'neko259' | |||
8 |
|
11 | |||
9 |
|
12 | |||
10 | __author__ = 'neko259' |
|
13 | RELATED_TAGS_COUNT = 5 | |
11 |
|
14 | |||
12 |
|
15 | |||
13 | class TagManager(models.Manager): |
|
16 | class TagManager(models.Manager): | |
@@ -17,7 +20,7 b' class TagManager(models.Manager):' | |||||
17 | Gets tags that have non-archived threads. |
|
20 | Gets tags that have non-archived threads. | |
18 | """ |
|
21 | """ | |
19 |
|
22 | |||
20 | return self.annotate(num_threads=Count('thread')).filter(num_threads__gt=0)\ |
|
23 | return self.annotate(num_threads=Count('thread_tags')).filter(num_threads__gt=0)\ | |
21 | .order_by('-required', 'name') |
|
24 | .order_by('-required', 'name') | |
22 |
|
25 | |||
23 | def get_tag_url_list(self, tags: list) -> str: |
|
26 | def get_tag_url_list(self, tags: list) -> str: | |
@@ -42,6 +45,7 b' class Tag(models.Model, Viewable):' | |||||
42 |
|
45 | |||
43 | name = models.CharField(max_length=100, db_index=True, unique=True) |
|
46 | name = models.CharField(max_length=100, db_index=True, unique=True) | |
44 | required = models.BooleanField(default=False, db_index=True) |
|
47 | required = models.BooleanField(default=False, db_index=True) | |
|
48 | description = models.TextField(blank=True) | |||
45 |
|
49 | |||
46 | def __str__(self): |
|
50 | def __str__(self): | |
47 | return self.name |
|
51 | return self.name | |
@@ -53,14 +57,20 b' class Tag(models.Model, Viewable):' | |||||
53 |
|
57 | |||
54 | return self.get_thread_count() == 0 |
|
58 | return self.get_thread_count() == 0 | |
55 |
|
59 | |||
56 | def get_thread_count(self) -> int: |
|
60 | def get_thread_count(self, archived=None) -> int: | |
57 |
|
|
61 | threads = self.get_threads() | |
|
62 | if archived is not None: | |||
|
63 | threads = threads.filter(archived=archived) | |||
|
64 | return threads.count() | |||
|
65 | ||||
|
66 | def get_active_thread_count(self) -> int: | |||
|
67 | return self.get_thread_count(archived=False) | |||
58 |
|
68 | |||
59 | def get_absolute_url(self): |
|
69 | def get_absolute_url(self): | |
60 | return reverse('tag', kwargs={'tag_name': self.name}) |
|
70 | return reverse('tag', kwargs={'tag_name': self.name}) | |
61 |
|
71 | |||
62 | def get_threads(self): |
|
72 | def get_threads(self): | |
63 |
return self.thread_s |
|
73 | return self.thread_tags.order_by('-bump_time') | |
64 |
|
74 | |||
65 | def is_required(self): |
|
75 | def is_required(self): | |
66 | return self.required |
|
76 | return self.required | |
@@ -80,3 +90,20 b' class Tag(models.Model, Viewable):' | |||||
80 | @cached_result() |
|
90 | @cached_result() | |
81 | def get_post_count(self): |
|
91 | def get_post_count(self): | |
82 | return self.get_threads().aggregate(num_posts=Count('post'))['num_posts'] |
|
92 | return self.get_threads().aggregate(num_posts=Count('post'))['num_posts'] | |
|
93 | ||||
|
94 | def get_description(self): | |||
|
95 | return self.description | |||
|
96 | ||||
|
97 | def get_random_image_post(self, archived=False): | |||
|
98 | posts = boards.models.Post.objects.annotate(images_count=Count( | |||
|
99 | 'images')).filter(images_count__gt=0, threads__tags__in=[self]) | |||
|
100 | if archived is not None: | |||
|
101 | posts = posts.filter(thread__archived=archived) | |||
|
102 | return posts.order_by('?').first() | |||
|
103 | ||||
|
104 | def get_first_letter(self): | |||
|
105 | return self.name and self.name[0] or '' | |||
|
106 | ||||
|
107 | def get_related_tags(self): | |||
|
108 | return set(Tag.objects.filter(thread_tags__in=self.get_threads()).exclude( | |||
|
109 | id=self.id).order_by('?')[:RELATED_TAGS_COUNT]) |
@@ -65,7 +65,7 b' class Thread(models.Model):' | |||||
65 | class Meta: |
|
65 | class Meta: | |
66 | app_label = 'boards' |
|
66 | app_label = 'boards' | |
67 |
|
67 | |||
68 | tags = models.ManyToManyField('Tag') |
|
68 | tags = models.ManyToManyField('Tag', related_name='thread_tags') | |
69 | bump_time = models.DateTimeField(db_index=True) |
|
69 | bump_time = models.DateTimeField(db_index=True) | |
70 | last_edit_time = models.DateTimeField() |
|
70 | last_edit_time = models.DateTimeField() | |
71 | archived = models.BooleanField(default=False) |
|
71 | archived = models.BooleanField(default=False) | |
@@ -225,3 +225,6 b' class Thread(models.Model):' | |||||
225 |
|
225 | |||
226 | def get_absolute_url(self): |
|
226 | def get_absolute_url(self): | |
227 | return self.get_opening_post().get_absolute_url() |
|
227 | return self.get_opening_post().get_absolute_url() | |
|
228 | ||||
|
229 | def get_required_tags(self): | |||
|
230 | return self.get_tags().filter(required=True) |
@@ -98,8 +98,39 b' textarea, input {' | |||||
98 |
|
98 | |||
99 | .post-image-full { |
|
99 | .post-image-full { | |
100 | width: 100%; |
|
100 | width: 100%; | |
|
101 | height: auto; | |||
101 | } |
|
102 | } | |
102 |
|
103 | |||
103 | #preview-text { |
|
104 | #preview-text { | |
104 | display: none; |
|
105 | display: none; | |
105 | } |
|
106 | } | |
|
107 | ||||
|
108 | .random-images-table { | |||
|
109 | text-align: center; | |||
|
110 | width: 100%; | |||
|
111 | } | |||
|
112 | ||||
|
113 | .random-images-table > div { | |||
|
114 | margin-left: auto; | |||
|
115 | margin-right: auto; | |||
|
116 | } | |||
|
117 | ||||
|
118 | .tag-image, .tag-text-data { | |||
|
119 | display: inline-block; | |||
|
120 | } | |||
|
121 | ||||
|
122 | .tag-text-data > h2 { | |||
|
123 | margin: 0; | |||
|
124 | } | |||
|
125 | ||||
|
126 | .tag-image { | |||
|
127 | margin-right: 5px; | |||
|
128 | } | |||
|
129 | ||||
|
130 | .reply-to-message { | |||
|
131 | display: none; | |||
|
132 | } | |||
|
133 | ||||
|
134 | .tripcode { | |||
|
135 | padding: 2px; | |||
|
136 | } No newline at end of file |
@@ -167,7 +167,7 b' p, .br {' | |||||
167 | display: table-cell; |
|
167 | display: table-cell; | |
168 | } |
|
168 | } | |
169 |
|
169 | |||
170 | .post-form input:not([name="image"]), .post-form textarea, .post-form select { |
|
170 | .post-form input:not([name="image"]):not([type="checkbox"]):not([type="submit"]), .post-form textarea, .post-form select { | |
171 | background: #333; |
|
171 | background: #333; | |
172 | color: #fff; |
|
172 | color: #fff; | |
173 | border: solid 1px; |
|
173 | border: solid 1px; | |
@@ -192,17 +192,22 b' p, .br {' | |||||
192 | margin-bottom: 0.5ex; |
|
192 | margin-bottom: 0.5ex; | |
193 | } |
|
193 | } | |
194 |
|
194 | |||
195 |
|
|
195 | input[type="submit"], button { | |
196 | background: #222; |
|
196 | background: #222; | |
197 | border: solid 2px #fff; |
|
197 | border: solid 2px #fff; | |
198 | color: #fff; |
|
198 | color: #fff; | |
199 | padding: 0.5ex; |
|
199 | padding: 0.5ex; | |
|
200 | margin-right: 0.5ex; | |||
200 | } |
|
201 | } | |
201 |
|
202 | |||
202 | input[type="submit"]:hover { |
|
203 | input[type="submit"]:hover { | |
203 | background: #060; |
|
204 | background: #060; | |
204 | } |
|
205 | } | |
205 |
|
206 | |||
|
207 | .form-submit > button:hover { | |||
|
208 | background: #006; | |||
|
209 | } | |||
|
210 | ||||
206 | blockquote { |
|
211 | blockquote { | |
207 | border-left: solid 2px; |
|
212 | border-left: solid 2px; | |
208 | padding-left: 5px; |
|
213 | padding-left: 5px; | |
@@ -231,12 +236,12 b' blockquote {' | |||||
231 | text-decoration: none; |
|
236 | text-decoration: none; | |
232 | } |
|
237 | } | |
233 |
|
238 | |||
234 | .dead_post { |
|
239 | .dead_post > .post-info { | |
235 | border-left: solid 5px #982C2C; |
|
240 | font-style: italic; | |
236 | } |
|
241 | } | |
237 |
|
242 | |||
238 | .archive_post { |
|
243 | .archive_post > .post-info { | |
239 | border-left: solid 5px #B7B7B7; |
|
244 | text-decoration: line-through; | |
240 | } |
|
245 | } | |
241 |
|
246 | |||
242 | .mark_btn { |
|
247 | .mark_btn { | |
@@ -354,8 +359,6 b' li {' | |||||
354 |
|
359 | |||
355 | .moderator_info { |
|
360 | .moderator_info { | |
356 | color: #e99d41; |
|
361 | color: #e99d41; | |
357 | float: right; |
|
|||
358 | font-weight: bold; |
|
|||
359 | opacity: 0.4; |
|
362 | opacity: 0.4; | |
360 | } |
|
363 | } | |
361 |
|
364 | |||
@@ -437,7 +440,6 b' li {' | |||||
437 |
|
440 | |||
438 | .gallery_image { |
|
441 | .gallery_image { | |
439 | border: solid 1px; |
|
442 | border: solid 1px; | |
440 | padding: 0.5ex; |
|
|||
441 | margin: 0.5ex; |
|
443 | margin: 0.5ex; | |
442 | text-align: center; |
|
444 | text-align: center; | |
443 | } |
|
445 | } | |
@@ -528,7 +530,7 b' ul {' | |||||
528 | } |
|
530 | } | |
529 |
|
531 | |||
530 | .highlight { |
|
532 | .highlight { | |
531 |
background |
|
533 | background: #222; | |
532 | } |
|
534 | } | |
533 |
|
535 | |||
534 | .post-button-form > button:hover { |
|
536 | .post-button-form > button:hover { | |
@@ -536,10 +538,9 b' ul {' | |||||
536 | } |
|
538 | } | |
537 |
|
539 | |||
538 | .tree_reply > .post { |
|
540 | .tree_reply > .post { | |
539 | margin-left: 1ex; |
|
|||
540 | margin-top: 1ex; |
|
541 | margin-top: 1ex; | |
541 | border-left: solid 1px #777; |
|
542 | border-left: solid 1px #777; | |
542 | border-right: solid 1px #777; |
|
543 | padding-right: 0; | |
543 | } |
|
544 | } | |
544 |
|
545 | |||
545 | #preview-text { |
|
546 | #preview-text { | |
@@ -548,8 +549,11 b' ul {' | |||||
548 | padding: 1ex; |
|
549 | padding: 1ex; | |
549 | } |
|
550 | } | |
550 |
|
551 | |||
551 | button { |
|
552 | .image-metadata { | |
552 | border: 1px solid white; |
|
553 | font-style: italic; | |
553 | margin-bottom: .5ex; |
|
554 | font-size: 0.9em; | |
554 | margin-top: .5ex; |
|
|||
555 | } |
|
555 | } | |
|
556 | ||||
|
557 | .tripcode { | |||
|
558 | color: white; | |||
|
559 | } |
@@ -376,3 +376,8 b' input[type="submit"]:hover {' | |||||
376 | margin: 1ex 0 1ex 0; |
|
376 | margin: 1ex 0 1ex 0; | |
377 | padding: 1ex; |
|
377 | padding: 1ex; | |
378 | } |
|
378 | } | |
|
379 | ||||
|
380 | .image-metadata { | |||
|
381 | font-style: italic; | |||
|
382 | font-size: 0.9em; | |||
|
383 | } No newline at end of file |
@@ -407,3 +407,12 b' li {' | |||||
407 | margin: 1ex 0 1ex 0; |
|
407 | margin: 1ex 0 1ex 0; | |
408 | padding: 1ex; |
|
408 | padding: 1ex; | |
409 | } |
|
409 | } | |
|
410 | ||||
|
411 | .image-metadata { | |||
|
412 | font-style: italic; | |||
|
413 | font-size: 0.9em; | |||
|
414 | } | |||
|
415 | ||||
|
416 | audio { | |||
|
417 | margin-top: 1em; | |||
|
418 | } |
@@ -42,12 +42,24 b' SimpleImageViewer.prototype.view = funct' | |||||
42 |
|
42 | |||
43 | // When we first enlarge an image, a full image needs to be created |
|
43 | // When we first enlarge an image, a full image needs to be created | |
44 | if (images.length == 1) { |
|
44 | if (images.length == 1) { | |
|
45 | var thumb = images.first(); | |||
|
46 | ||||
|
47 | var width = thumb.attr('data-width'); | |||
|
48 | var height = thumb.attr('data-height'); | |||
|
49 | ||||
|
50 | if (width == null || height == null) { | |||
|
51 | width = '100%'; | |||
|
52 | height = '100%'; | |||
|
53 | } | |||
|
54 | ||||
45 | var parent = images.first().parent(); |
|
55 | var parent = images.first().parent(); | |
46 | var link = parent.attr('href'); |
|
56 | var link = parent.attr('href'); | |
47 |
|
57 | |||
48 | var fullImg = $('<img />') |
|
58 | var fullImg = $('<img />') | |
49 | .addClass(FULL_IMG_CLASS) |
|
59 | .addClass(FULL_IMG_CLASS) | |
50 |
.attr('src', link) |
|
60 | .attr('src', link) | |
|
61 | .attr('width', width) | |||
|
62 | .attr('height', height); | |||
51 |
|
63 | |||
52 | parent.append(fullImg); |
|
64 | parent.append(fullImg); | |
53 | } |
|
65 | } |
@@ -24,6 +24,9 b'' | |||||
24 | */ |
|
24 | */ | |
25 |
|
25 | |||
26 | var CLOSE_BUTTON = '#form-close-button'; |
|
26 | var CLOSE_BUTTON = '#form-close-button'; | |
|
27 | var REPLY_TO_MSG = '.reply-to-message'; | |||
|
28 | var REPLY_TO_MSG_ID = '#reply-to-message-id'; | |||
|
29 | ||||
27 | var $html = $("html, body"); |
|
30 | var $html = $("html, body"); | |
28 |
|
31 | |||
29 | function moveCaretToEnd(el) { |
|
32 | function moveCaretToEnd(el) { | |
@@ -46,6 +49,7 b' function resetFormPosition() {' | |||||
46 | form.insertAfter($('.thread')); |
|
49 | form.insertAfter($('.thread')); | |
47 |
|
50 | |||
48 | $(CLOSE_BUTTON).hide(); |
|
51 | $(CLOSE_BUTTON).hide(); | |
|
52 | $(REPLY_TO_MSG).hide(); | |||
49 | } |
|
53 | } | |
50 |
|
54 | |||
51 | function showFormAfter(blockToInsertAfter) { |
|
55 | function showFormAfter(blockToInsertAfter) { | |
@@ -54,6 +58,8 b' function showFormAfter(blockToInsertAfte' | |||||
54 |
|
58 | |||
55 | $(CLOSE_BUTTON).show(); |
|
59 | $(CLOSE_BUTTON).show(); | |
56 | form.show(); |
|
60 | form.show(); | |
|
61 | $(REPLY_TO_MSG_ID).text(blockToInsertAfter.attr('id')); | |||
|
62 | $(REPLY_TO_MSG).show(); | |||
57 | } |
|
63 | } | |
58 |
|
64 | |||
59 | function addQuickReply(postId) { |
|
65 | function addQuickReply(postId) { |
@@ -30,6 +30,14 b' var POST_UPDATED = 1;' | |||||
30 |
|
30 | |||
31 | var JS_AUTOUPDATE_PERIOD = 20000; |
|
31 | var JS_AUTOUPDATE_PERIOD = 20000; | |
32 |
|
32 | |||
|
33 | var ALLOWED_FOR_PARTIAL_UPDATE = [ | |||
|
34 | 'refmap', | |||
|
35 | 'post-info' | |||
|
36 | ]; | |||
|
37 | ||||
|
38 | var ATTR_CLASS = 'class'; | |||
|
39 | var ATTR_UID = 'data-uid'; | |||
|
40 | ||||
33 | var wsUser = ''; |
|
41 | var wsUser = ''; | |
34 |
|
42 | |||
35 | var unreadPosts = 0; |
|
43 | var unreadPosts = 0; | |
@@ -165,7 +173,8 b' function updatePost(postHtml) {' | |||||
165 | var type; |
|
173 | var type; | |
166 |
|
174 | |||
167 | if (existingPosts.size() > 0) { |
|
175 | if (existingPosts.size() > 0) { | |
168 |
existingPosts. |
|
176 | replacePartial(existingPosts.first(), post, false); | |
|
177 | post = existingPosts.first(); | |||
169 |
|
178 | |||
170 | type = POST_UPDATED; |
|
179 | type = POST_UPDATED; | |
171 | } else { |
|
180 | } else { | |
@@ -362,6 +371,64 b' function processNewPost(post) {' | |||||
362 | blink(post); |
|
371 | blink(post); | |
363 | } |
|
372 | } | |
364 |
|
373 | |||
|
374 | function replacePartial(oldNode, newNode, recursive) { | |||
|
375 | if (!equalNodes(oldNode, newNode)) { | |||
|
376 | // Update parent node attributes | |||
|
377 | updateNodeAttr(oldNode, newNode, ATTR_CLASS); | |||
|
378 | updateNodeAttr(oldNode, newNode, ATTR_UID); | |||
|
379 | ||||
|
380 | // Replace children | |||
|
381 | var children = oldNode.children(); | |||
|
382 | if (children.length == 0) { | |||
|
383 | console.log(oldContent); | |||
|
384 | console.log(newContent) | |||
|
385 | ||||
|
386 | oldNode.replaceWith(newNode); | |||
|
387 | } else { | |||
|
388 | var newChildren = newNode.children(); | |||
|
389 | newChildren.each(function(i) { | |||
|
390 | var newChild = newChildren.eq(i); | |||
|
391 | var newChildClass = newChild.attr(ATTR_CLASS); | |||
|
392 | ||||
|
393 | // Update only certain allowed blocks (e.g. not images) | |||
|
394 | if (ALLOWED_FOR_PARTIAL_UPDATE.indexOf(newChildClass) > -1) { | |||
|
395 | var oldChild = oldNode.children('.' + newChildClass); | |||
|
396 | ||||
|
397 | if (oldChild.length == 0) { | |||
|
398 | oldNode.append(newChild); | |||
|
399 | } else { | |||
|
400 | if (!equalNodes(oldChild, newChild)) { | |||
|
401 | if (recursive) { | |||
|
402 | replacePartial(oldChild, newChild, false); | |||
|
403 | } else { | |||
|
404 | oldChild.replaceWith(newChild); | |||
|
405 | } | |||
|
406 | } | |||
|
407 | } | |||
|
408 | } | |||
|
409 | }); | |||
|
410 | } | |||
|
411 | } | |||
|
412 | } | |||
|
413 | ||||
|
414 | /** | |||
|
415 | * Compare nodes by content | |||
|
416 | */ | |||
|
417 | function equalNodes(node1, node2) { | |||
|
418 | return node1[0].outerHTML == node2[0].outerHTML; | |||
|
419 | } | |||
|
420 | ||||
|
421 | /** | |||
|
422 | * Update attribute of a node if it has changed | |||
|
423 | */ | |||
|
424 | function updateNodeAttr(oldNode, newNode, attrName) { | |||
|
425 | var oldAttr = oldNode.attr(attrName); | |||
|
426 | var newAttr = newNode.attr(attrName); | |||
|
427 | if (oldAttr != newAttr) { | |||
|
428 | oldNode.attr(attrName, newAttr); | |||
|
429 | }; | |||
|
430 | } | |||
|
431 | ||||
365 | $(document).ready(function(){ |
|
432 | $(document).ready(function(){ | |
366 | if (initAutoupdate()) { |
|
433 | if (initAutoupdate()) { | |
367 | // Post form data over AJAX |
|
434 | // Post form data over AJAX | |
@@ -386,6 +453,4 b' function processNewPost(post) {' | |||||
386 | resetForm(form); |
|
453 | resetForm(form); | |
387 | } |
|
454 | } | |
388 | } |
|
455 | } | |
389 |
|
||||
390 | $('#autoupdate').click(getThreadDiff); |
|
|||
391 | }); |
|
456 | }); |
@@ -38,29 +38,49 b'' | |||||
38 |
|
38 | |||
39 | {% if tag %} |
|
39 | {% if tag %} | |
40 | <div class="tag_info"> |
|
40 | <div class="tag_info"> | |
41 | <h2> |
|
41 | {% if random_image_post %} | |
42 | <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form"> |
|
42 | <div class="tag-image"> | |
43 | {% if is_favorite %} |
|
43 | {% with image=random_image_post.images.first %} | |
44 | <button name="method" value="unsubscribe" class="fav">★</button> |
|
44 | <a href="{{ random_image_post.get_absolute_url }}"><img | |
45 | {% else %} |
|
45 | src="{{ image.image.url_200x150 }}" | |
46 | <button name="method" value="subscribe" class="not_fav">★</button> |
|
46 | width="{{ image.pre_width }}" | |
|
47 | height="{{ image.pre_height }}"/></a> | |||
|
48 | {% endwith %} | |||
|
49 | </div> | |||
|
50 | {% endif %} | |||
|
51 | <div class="tag-text-data"> | |||
|
52 | <h2> | |||
|
53 | <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form"> | |||
|
54 | {% if is_favorite %} | |||
|
55 | <button name="method" value="unsubscribe" class="fav">★</button> | |||
|
56 | {% else %} | |||
|
57 | <button name="method" value="subscribe" class="not_fav">★</button> | |||
|
58 | {% endif %} | |||
|
59 | </form> | |||
|
60 | <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form"> | |||
|
61 | {% if is_hidden %} | |||
|
62 | <button name="method" value="unhide" class="fav">H</button> | |||
|
63 | {% else %} | |||
|
64 | <button name="method" value="hide" class="not_fav">H</button> | |||
|
65 | {% endif %} | |||
|
66 | </form> | |||
|
67 | {{ tag.get_view|safe }} | |||
|
68 | {% if moderator %} | |||
|
69 | <span class="moderator_info">| <a href="{% url 'admin:boards_tag_change' tag.id %}">{% trans 'Edit tag' %}</a></span> | |||
47 | {% endif %} |
|
70 | {% endif %} | |
48 |
</ |
|
71 | </h2> | |
49 | <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form"> |
|
72 | {% if tag.get_description %} | |
50 | {% if is_hidden %} |
|
73 | <p>{{ tag.get_description|safe }}</p> | |
51 | <button name="method" value="unhide" class="fav">H</button> |
|
|||
52 | {% else %} |
|
|||
53 | <button name="method" value="hide" class="not_fav">H</button> |
|
|||
54 | {% endif %} |
|
|||
55 | </form> |
|
|||
56 | {% autoescape off %} |
|
|||
57 | {{ tag.get_view }} |
|
|||
58 | {% endautoescape %} |
|
|||
59 | {% if moderator %} |
|
|||
60 | <span class="moderator_info">[<a href="{% url 'admin:boards_tag_change' tag.id %}">{% trans 'Edit tag' %}</a>]</span> |
|
|||
61 | {% endif %} |
|
74 | {% endif %} | |
62 | </h2> |
|
75 | <p>{% blocktrans with active_thread_count=tag.get_active_thread_count thread_count=tag.get_thread_count post_count=tag.get_post_count %}This tag has {{ thread_count }} threads ({{ active_thread_count}} active) and {{ post_count }} posts.{% endblocktrans %}</p> | |
63 | <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> |
|
76 | {% if related_tags %} | |
|
77 | <p>{% trans 'Related tags:' %} | |||
|
78 | {% for rel_tag in related_tags %} | |||
|
79 | {{ rel_tag.get_view|safe }}{% if not forloop.last %}, {% else %}.{% endif %} | |||
|
80 | {% endfor %} | |||
|
81 | </p> | |||
|
82 | {% endif %} | |||
|
83 | </div> | |||
64 | </div> |
|
84 | </div> | |
65 | {% endif %} |
|
85 | {% endif %} | |
66 |
|
86 | |||
@@ -116,13 +136,13 b'' | |||||
116 | {{ form.as_div }} |
|
136 | {{ form.as_div }} | |
117 | <div class="form-submit"> |
|
137 | <div class="form-submit"> | |
118 | <input type="submit" value="{% trans "Post" %}"/> |
|
138 | <input type="submit" value="{% trans "Post" %}"/> | |
|
139 | <button id="preview-button" onclick="return false;">{% trans 'Preview' %}</button> | |||
119 | </div> |
|
140 | </div> | |
120 | </form> |
|
141 | </form> | |
121 | </div> |
|
142 | </div> | |
122 | <div> |
|
143 | <div> | |
123 | {% trans 'Tags must be delimited by spaces. Text or image is required.' %} |
|
144 | {% trans 'Tags must be delimited by spaces. Text or image is required.' %} | |
124 | </div> |
|
145 | </div> | |
125 | <div><button id="preview-button">{% trans 'Preview' %}</button></div> |
|
|||
126 | <div id="preview-text"></div> |
|
146 | <div id="preview-text"></div> | |
127 | <div><a href="{% url "staticpage" name="help" %}">{% trans 'Text syntax' %}</a></div> |
|
147 | <div><a href="{% url "staticpage" name="help" %}">{% trans 'Text syntax' %}</a></div> | |
128 | <div><a href="{% url "tags" "required" %}">{% trans 'Tags' %}</a></div> |
|
148 | <div><a href="{% url "tags" "required" %}">{% trans 'Tags' %}</a></div> |
@@ -37,8 +37,9 b'' | |||||
37 | {% trans 'Add tags' %} → |
|
37 | {% trans 'Add tags' %} → | |
38 | {% endif %} |
|
38 | {% endif %} | |
39 | <a href="{% url 'tags' 'required'%}" title="{% trans 'Tag management' %}">{% trans "tags" %}</a>, |
|
39 | <a href="{% url 'tags' 'required'%}" title="{% trans 'Tag management' %}">{% trans "tags" %}</a>, | |
40 |
|
|
40 | <a href="{% url 'search' %}" title="{% trans 'Search' %}">{% trans 'search' %}</a>, | |
41 | <a href="{% url 'feed' %}" title="{% trans 'Feed' %}">{% trans 'feed' %}</a> |
|
41 | <a href="{% url 'feed' %}" title="{% trans 'Feed' %}">{% trans 'feed' %}</a>, | |
|
42 | <a href="{% url 'random' %}" title="{% trans 'Random images' %}">{% trans 'random' %}</a> | |||
42 |
|
43 | |||
43 | {% if username %} |
|
44 | {% if username %} | |
44 | <a class="right-link link" href="{% url 'notifications' username %}" title="{% trans 'Notifications' %}"> |
|
45 | <a class="right-link link" href="{% url 'notifications' username %}" title="{% trans 'Notifications' %}"> |
@@ -61,7 +61,7 b'' | |||||
61 | {% ifequal page current_page.number %} |
|
61 | {% ifequal page current_page.number %} | |
62 | class="current_page" |
|
62 | class="current_page" | |
63 | {% endifequal %} |
|
63 | {% endifequal %} | |
64 | href="{% url "feed" %}?page={{ page }}">{{ page }}</a> |
|
64 | href="{% url "feed" %}?page={{ page }}{{ additional_attrs }}">{{ page }}</a> | |
65 | {% if not forloop.last %},{% endif %} |
|
65 | {% if not forloop.last %},{% endif %} | |
66 | {% endfor %} |
|
66 | {% endfor %} | |
67 | {% endwith %} |
|
67 | {% endwith %} |
@@ -5,9 +5,16 b'' | |||||
5 |
|
5 | |||
6 | <div class="{{ css_class }}" id="{{ post.id }}" data-uid="{{ post.uid }}"> |
|
6 | <div class="{{ css_class }}" id="{{ post.id }}" data-uid="{{ post.uid }}"> | |
7 | <div class="post-info"> |
|
7 | <div class="post-info"> | |
8 |
<a class="post_id" href="{{ post.get_absolute_url }}"> |
|
8 | <a class="post_id" href="{{ post.get_absolute_url }}">#{{ post.get_absolute_id }}</a> | |
9 | <span class="title">{{ post.title }}</span> |
|
9 | <span class="title">{{ post.title }}</span> | |
10 | <span class="pub_time"><time datetime="{{ post.pub_time|date:'c' }}">{{ post.pub_time }}</time></span> |
|
10 | <span class="pub_time"><time datetime="{{ post.pub_time|date:'c' }}">{{ post.pub_time }}</time></span> | |
|
11 | {% if post.tripcode %} | |||
|
12 | {% with tripcode=post.get_tripcode %} | |||
|
13 | <a href="{% url 'feed' %}?tripcode={{ tripcode.get_full_text }}" | |||
|
14 | class="tripcode" title="{{ tripcode.get_full_text }}" | |||
|
15 | style="border: solid 2px #{{ tripcode.get_color }}; border-left: solid 1ex #{{ tripcode.get_color }};">{{ tripcode.get_short_text }}</a> | |||
|
16 | {% endwith %} | |||
|
17 | {% endif %} | |||
11 | {% comment %} |
|
18 | {% comment %} | |
12 | Thread death time needs to be shown only if the thread is alredy archived |
|
19 | Thread death time needs to be shown only if the thread is alredy archived | |
13 | and this is an opening post (thread death time) or a post for popup |
|
20 | and this is an opening post (thread death time) or a post for popup | |
@@ -29,7 +36,7 b'' | |||||
29 | {% else %} |
|
36 | {% else %} | |
30 | {% if need_op_data %} |
|
37 | {% if need_op_data %} | |
31 | {% with thread.get_opening_post as op %} |
|
38 | {% with thread.get_opening_post as op %} | |
32 |
{% trans " in " %} |
|
39 | {% trans " in " %}{{ op.get_link_view|safe }} <span class="title">{{ op.get_title|striptags|truncatewords:5 }}</span> | |
33 | {% endwith %} |
|
40 | {% endwith %} | |
34 | {% endif %} |
|
41 | {% endif %} | |
35 | {% endif %} |
|
42 | {% endif %} | |
@@ -43,9 +50,9 b'' | |||||
43 |
|
50 | |||
44 | {% if moderator %} |
|
51 | {% if moderator %} | |
45 | <span class="moderator_info"> |
|
52 | <span class="moderator_info"> | |
46 | <a href="{% url 'admin:boards_post_change' post.id %}">{% trans 'Edit' %}</a> |
|
53 | | <a href="{% url 'admin:boards_post_change' post.id %}">{% trans 'Edit' %}</a> | |
47 | {% if is_opening %} |
|
54 | {% if is_opening %} | |
48 |
|
|
55 | | <a href="{% url 'admin:boards_thread_change' thread.id %}">{% trans 'Edit thread' %}</a> | |
49 | {% endif %} |
|
56 | {% endif %} | |
50 | </span> |
|
57 | </span> | |
51 | {% endif %} |
|
58 | {% endif %} | |
@@ -55,10 +62,13 b'' | |||||
55 | supports multiple. |
|
62 | supports multiple. | |
56 | {% endcomment %} |
|
63 | {% endcomment %} | |
57 | {% if post.images.exists %} |
|
64 | {% if post.images.exists %} | |
58 |
{% with post.images. |
|
65 | {% with post.images.first as image %} | |
59 |
{ |
|
66 | {{ image.get_view|safe }} | |
60 | {{ image.get_view }} |
|
67 | {% endwith %} | |
61 | {% endautoescape %} |
|
68 | {% endif %} | |
|
69 | {% if post.attachments.exists %} | |||
|
70 | {% with post.attachments.first as file %} | |||
|
71 | {{ file.get_view|safe }} | |||
62 | {% endwith %} |
|
72 | {% endwith %} | |
63 | {% endif %} |
|
73 | {% endif %} | |
64 | {% comment %} |
|
74 | {% comment %} | |
@@ -72,22 +82,20 b'' | |||||
72 | {{ post.get_text }} |
|
82 | {{ post.get_text }} | |
73 | {% endif %} |
|
83 | {% endif %} | |
74 | {% endautoescape %} |
|
84 | {% endautoescape %} | |
75 | {% if post.is_referenced %} |
|
85 | </div> | |
76 | {% if mode_tree %} |
|
86 | {% if post.is_referenced %} | |
77 | <div class="tree_reply"> |
|
87 | {% if mode_tree %} | |
78 | {% for refpost in post.get_referenced_posts %} |
|
88 | <div class="tree_reply"> | |
79 |
|
|
89 | {% for refpost in post.get_referenced_posts %} | |
80 |
{% |
|
90 | {% post_view refpost mode_tree=True %} | |
81 |
|
|
91 | {% endfor %} | |
82 |
|
|
92 | </div> | |
83 | <div class="refmap"> |
|
93 | {% else %} | |
84 | {% autoescape off %} |
|
94 | <div class="refmap"> | |
85 |
|
|
95 | {% trans "Replies" %}: {{ post.refmap|safe }} | |
86 | {% endautoescape %} |
|
96 | </div> | |
87 | </div> |
|
|||
88 | {% endif %} |
|
|||
89 | {% endif %} |
|
97 | {% endif %} | |
90 | </div> |
|
98 | {% endif %} | |
91 | {% comment %} |
|
99 | {% comment %} | |
92 | Thread metadata: counters, tags etc |
|
100 | Thread metadata: counters, tags etc | |
93 | {% endcomment %} |
|
101 | {% endcomment %} | |
@@ -98,9 +106,7 b'' | |||||
98 | {{ thread.get_images_count }} {% trans 'images' %}. |
|
106 | {{ thread.get_images_count }} {% trans 'images' %}. | |
99 | {% endif %} |
|
107 | {% endif %} | |
100 | <span class="tags"> |
|
108 | <span class="tags"> | |
101 |
{ |
|
109 | {{ thread.get_tag_url_list|safe }} | |
102 | {{ thread.get_tag_url_list }} |
|
|||
103 | {% endautoescape %} |
|
|||
104 | </span> |
|
110 | </span> | |
105 | </div> |
|
111 | </div> | |
106 | {% endif %} |
|
112 | {% endif %} |
@@ -9,7 +9,6 b'' | |||||
9 | {% endblock %} |
|
9 | {% endblock %} | |
10 |
|
10 | |||
11 | {% block content %} |
|
11 | {% block content %} | |
12 |
|
||||
13 | <div class="post"> |
|
12 | <div class="post"> | |
14 | <p> |
|
13 | <p> | |
15 | {% if moderator %} |
|
14 | {% if moderator %} | |
@@ -19,9 +18,7 b'' | |||||
19 | {% if hidden_tags %} |
|
18 | {% if hidden_tags %} | |
20 | <p>{% trans 'Hidden tags:' %} |
|
19 | <p>{% trans 'Hidden tags:' %} | |
21 | {% for tag in hidden_tags %} |
|
20 | {% for tag in hidden_tags %} | |
22 |
{ |
|
21 | {{ tag.get_view|safe }} | |
23 | {{ tag.get_view }} |
|
|||
24 | {% endautoescape %} |
|
|||
25 | {% endfor %} |
|
22 | {% endfor %} | |
26 | </p> |
|
23 | </p> | |
27 | {% else %} |
|
24 | {% else %} |
@@ -1,5 +1,3 b'' | |||||
1 | <div class="post"> |
|
1 | <div class="post"> | |
2 | {% autoescape off %} |
|
2 | {{ tag.get_view|safe }} | |
3 | {{ tag.get_view }} |
|
|||
4 | {% endautoescape %} |
|
|||
5 | </div> |
|
3 | </div> |
@@ -8,20 +8,39 b'' | |||||
8 |
|
8 | |||
9 | {% block content %} |
|
9 | {% block content %} | |
10 |
|
10 | |||
|
11 | {% regroup section_tags by get_first_letter as section_tag_list %} | |||
|
12 | {% regroup all_tags by get_first_letter as other_tag_list %} | |||
|
13 | ||||
11 | <div class="post"> |
|
14 | <div class="post"> | |
12 |
{% if |
|
15 | {% if section_tags %} | |
13 | {% for tag in all_tags %} |
|
16 | <div> | |
14 | <div class="tag_item"> |
|
17 | {% trans 'Sections:' %} | |
|
18 | {% for letter in section_tag_list %} | |||
|
19 | <br />({{ letter.grouper|upper }}) | |||
|
20 | {% for tag in letter.list %} | |||
15 | {% autoescape off %} |
|
21 | {% autoescape off %} | |
16 | {{ tag.get_view }} |
|
22 | {{ tag.get_view }}{% if not forloop.last %},{% endif %} | |
17 | {% endautoescape %} |
|
23 | {% endautoescape %} | |
18 |
|
|
24 | {% endfor %} | |
19 | {% endfor %} |
|
25 | {% endfor %} | |
20 | {% else %} |
|
26 | </div> | |
21 | {% trans 'No tags found.' %} |
|
|||
22 | {% endif %} |
|
27 | {% endif %} | |
|
28 | {% if all_tags %} | |||
|
29 | <div> | |||
|
30 | {% trans 'Other tags:' %} | |||
|
31 | {% for letter in other_tag_list %} | |||
|
32 | <br />({{ letter.grouper|upper }}) | |||
|
33 | {% for tag in letter.list %} | |||
|
34 | {% autoescape off %} | |||
|
35 | {{ tag.get_view }}{% if not forloop.last %},{% endif %} | |||
|
36 | {% endautoescape %} | |||
|
37 | {% endfor %} | |||
|
38 | {% endfor %} | |||
|
39 | </div> | |||
|
40 | {% endif %} | |||
|
41 | ||||
23 | {% if query %} |
|
42 | {% if query %} | |
24 | <div><a href="{% url 'tags' %}">{% trans 'All tags' %}</a></div> |
|
43 | <div><a href="{% url 'tags' %}">{% trans 'All tags...' %}</a></div> | |
25 | {% endif %} |
|
44 | {% endif %} | |
26 | </div> |
|
45 | </div> | |
27 |
|
46 |
@@ -31,9 +31,6 b'' | |||||
31 | data-ws-host="{{ ws_host }}" |
|
31 | data-ws-host="{{ ws_host }}" | |
32 | data-ws-port="{{ ws_port }}"> |
|
32 | data-ws-port="{{ ws_port }}"> | |
33 |
|
33 | |||
34 | {% block thread_meta_panel %} |
|
|||
35 | {% endblock %} |
|
|||
36 |
|
||||
37 | <span id="reply-count">{{ thread.get_reply_count }}</span>{% if thread.has_post_limit %}/{{ thread.max_posts }}{% endif %} {% trans 'messages' %}, |
|
34 | <span id="reply-count">{{ thread.get_reply_count }}</span>{% if thread.has_post_limit %}/{{ thread.max_posts }}{% endif %} {% trans 'messages' %}, | |
38 | <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}. |
|
35 | <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}. | |
39 | {% trans 'Last update: ' %}<span id="last-update"><time datetime="{{ thread.last_edit_time|date:'c' }}">{{ thread.last_edit_time }}</time></span> |
|
36 | {% trans 'Last update: ' %}<span id="last-update"><time datetime="{{ thread.last_edit_time|date:'c' }}">{{ thread.last_edit_time }}</time></span> |
@@ -28,7 +28,7 b'' | |||||
28 | {% if not thread.archived %} |
|
28 | {% if not thread.archived %} | |
29 | <div class="post-form-w"> |
|
29 | <div class="post-form-w"> | |
30 | <script src="{% static 'js/panel.js' %}"></script> |
|
30 | <script src="{% static 'js/panel.js' %}"></script> | |
31 | <div class="form-title">{% trans "Reply to thread" %} #{{ opening_post.id }}</div> |
|
31 | <div class="form-title">{% trans "Reply to thread" %} #{{ opening_post.id }}<span class="reply-to-message"> {% trans "to message " %} #<span id="reply-to-message-id"></span></span></div> | |
32 | <div class="post-form" id="compact-form"> |
|
32 | <div class="post-form" id="compact-form"> | |
33 | <div class="swappable-form-full"> |
|
33 | <div class="swappable-form-full"> | |
34 | <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %} |
|
34 | <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %} | |
@@ -36,10 +36,10 b'' | |||||
36 | {{ form.as_div }} |
|
36 | {{ form.as_div }} | |
37 | <div class="form-submit"> |
|
37 | <div class="form-submit"> | |
38 | <input type="submit" value="{% trans "Post" %}"/> |
|
38 | <input type="submit" value="{% trans "Post" %}"/> | |
|
39 | <button id="preview-button" onclick="return false;">{% trans 'Preview' %}</button> | |||
39 | </div> |
|
40 | </div> | |
40 | </form> |
|
41 | </form> | |
41 | </div> |
|
42 | </div> | |
42 | <div><button id="preview-button">{% trans 'Preview' %}</button></div> |
|
|||
43 | <div id="preview-text"></div> |
|
43 | <div id="preview-text"></div> | |
44 | <div><a href="{% url "staticpage" name="help" %}"> |
|
44 | <div><a href="{% url "staticpage" name="help" %}"> | |
45 | {% trans 'Text syntax' %}</a></div> |
|
45 | {% trans 'Text syntax' %}</a></div> | |
@@ -55,7 +55,3 b'' | |||||
55 | <script src="{% static 'js/thread_update.js' %}"></script> |
|
55 | <script src="{% static 'js/thread_update.js' %}"></script> | |
56 | <script src="{% static 'js/3party/centrifuge.js' %}"></script> |
|
56 | <script src="{% static 'js/3party/centrifuge.js' %}"></script> | |
57 | {% endblock %} |
|
57 | {% endblock %} | |
58 |
|
||||
59 | {% block thread_meta_panel %} |
|
|||
60 | <button id="autoupdate">{% trans 'Update' %}</button> |
|
|||
61 | {% endblock %} |
|
@@ -11,6 +11,7 b' from boards.views.search import BoardSea' | |||||
11 | from boards.views.static import StaticPageView |
|
11 | from boards.views.static import StaticPageView | |
12 | from boards.views.preview import PostPreviewView |
|
12 | from boards.views.preview import PostPreviewView | |
13 | from boards.views.sync import get_post_sync_data, response_get |
|
13 | from boards.views.sync import get_post_sync_data, response_get | |
|
14 | from boards.views.random import RandomImageView | |||
14 |
|
15 | |||
15 |
|
16 | |||
16 | js_info_dict = { |
|
17 | js_info_dict = { | |
@@ -43,6 +44,8 b" urlpatterns = patterns(''," | |||||
43 | url(r'^staticpage/(?P<name>\w+)/$', StaticPageView.as_view(), |
|
44 | url(r'^staticpage/(?P<name>\w+)/$', StaticPageView.as_view(), | |
44 | name='staticpage'), |
|
45 | name='staticpage'), | |
45 |
|
46 | |||
|
47 | url(r'^random/$', RandomImageView.as_view(), name='random'), | |||
|
48 | ||||
46 | # RSS feeds |
|
49 | # RSS feeds | |
47 | url(r'^rss/$', AllThreadsFeed()), |
|
50 | url(r'^rss/$', AllThreadsFeed()), | |
48 | url(r'^page/(?P<page>\d+)/rss/$', AllThreadsFeed()), |
|
51 | url(r'^page/(?P<page>\d+)/rss/$', AllThreadsFeed()), |
@@ -1,6 +1,7 b'' | |||||
1 | """ |
|
1 | """ | |
2 | This module contains helper functions and helper classes. |
|
2 | This module contains helper functions and helper classes. | |
3 | """ |
|
3 | """ | |
|
4 | import hashlib | |||
4 | import time |
|
5 | import time | |
5 | import hmac |
|
6 | import hmac | |
6 |
|
7 | |||
@@ -81,4 +82,11 b' def is_moderator(request):' | |||||
81 | except AttributeError: |
|
82 | except AttributeError: | |
82 | moderate = False |
|
83 | moderate = False | |
83 |
|
84 | |||
84 | return moderate No newline at end of file |
|
85 | return moderate | |
|
86 | ||||
|
87 | ||||
|
88 | def get_file_hash(file) -> str: | |||
|
89 | md5 = hashlib.md5() | |||
|
90 | for chunk in file.chunks(): | |||
|
91 | md5.update(chunk) | |||
|
92 | return md5.hexdigest() |
@@ -4,6 +4,7 b' from boards.views.base import BaseBoardV' | |||||
4 | from boards.models.tag import Tag |
|
4 | from boards.models.tag import Tag | |
5 |
|
5 | |||
6 |
|
6 | |||
|
7 | PARAM_SECTION_TAGS = 'section_tags' | |||
7 | PARAM_TAGS = 'all_tags' |
|
8 | PARAM_TAGS = 'all_tags' | |
8 | PARAM_QUERY = 'query' |
|
9 | PARAM_QUERY = 'query' | |
9 |
|
10 | |||
@@ -13,10 +14,10 b' class AllTagsView(BaseBoardView):' | |||||
13 | def get(self, request, query=None): |
|
14 | def get(self, request, query=None): | |
14 | params = dict() |
|
15 | params = dict() | |
15 |
|
16 | |||
16 | if query == 'required': |
|
17 | params[PARAM_SECTION_TAGS] = Tag.objects.filter(required=True) | |
17 | params[PARAM_TAGS] = Tag.objects.filter(required=True) |
|
18 | if query != 'required': | |
18 | else: |
|
19 | params[PARAM_TAGS] = Tag.objects.get_not_empty_tags().filter( | |
19 | params[PARAM_TAGS] = Tag.objects.get_not_empty_tags() |
|
20 | required=False) | |
20 | params[PARAM_QUERY] = query |
|
21 | params[PARAM_QUERY] = query | |
21 |
|
22 | |||
22 | return render(request, 'boards/tags.html', params) |
|
23 | return render(request, 'boards/tags.html', params) |
@@ -139,9 +139,9 b' class AllThreadsView(PostMixin, BaseBoar' | |||||
139 |
|
139 | |||
140 | data = form.cleaned_data |
|
140 | data = form.cleaned_data | |
141 |
|
141 | |||
142 | title = data[FORM_TITLE] |
|
142 | title = form.get_title() | |
143 | text = data[FORM_TEXT] |
|
143 | text = data[FORM_TEXT] | |
144 |
|
|
144 | file = form.get_file() | |
145 | threads = data[FORM_THREADS] |
|
145 | threads = data[FORM_THREADS] | |
146 |
|
146 | |||
147 | text = self._remove_invalid_links(text) |
|
147 | text = self._remove_invalid_links(text) | |
@@ -150,8 +150,9 b' class AllThreadsView(PostMixin, BaseBoar' | |||||
150 |
|
150 | |||
151 | tags = self.parse_tags_string(tag_strings) |
|
151 | tags = self.parse_tags_string(tag_strings) | |
152 |
|
152 | |||
153 |
post = Post.objects.create_post(title=title, text=text, |
|
153 | post = Post.objects.create_post(title=title, text=text, file=file, | |
154 |
ip=ip, tags=tags, opening_posts=threads |
|
154 | ip=ip, tags=tags, opening_posts=threads, | |
|
155 | tripcode=form.get_tripcode()) | |||
155 |
|
156 | |||
156 | # This is required to update the threads to which posts we have replied |
|
157 | # This is required to update the threads to which posts we have replied | |
157 | # when creating this one |
|
158 | # when creating this one |
@@ -1,23 +1,18 b'' | |||||
1 | from django.core.urlresolvers import reverse |
|
1 | from django.core.urlresolvers import reverse | |
2 |
from django. |
|
2 | from django.shortcuts import render | |
3 | from django.core.files.temp import NamedTemporaryFile |
|
|||
4 | from django.core.paginator import EmptyPage |
|
|||
5 | from django.db import transaction |
|
|||
6 | from django.http import Http404 |
|
|||
7 | from django.shortcuts import render, redirect |
|
|||
8 | import requests |
|
|||
9 |
|
3 | |||
10 | from boards import utils, settings |
|
|||
11 | from boards.abstracts.paginator import get_paginator |
|
4 | from boards.abstracts.paginator import get_paginator | |
12 | from boards.abstracts.settingsmanager import get_settings_manager |
|
5 | from boards.abstracts.settingsmanager import get_settings_manager | |
13 |
from boards.models import Post |
|
6 | from boards.models import Post | |
14 | from boards.views.base import BaseBoardView |
|
7 | from boards.views.base import BaseBoardView | |
15 | from boards.views.posting_mixin import PostMixin |
|
8 | from boards.views.posting_mixin import PostMixin | |
16 |
|
9 | |||
|
10 | POSTS_PER_PAGE = 10 | |||
17 |
|
11 | |||
18 | PARAMETER_CURRENT_PAGE = 'current_page' |
|
12 | PARAMETER_CURRENT_PAGE = 'current_page' | |
19 | PARAMETER_PAGINATOR = 'paginator' |
|
13 | PARAMETER_PAGINATOR = 'paginator' | |
20 | PARAMETER_POSTS = 'posts' |
|
14 | PARAMETER_POSTS = 'posts' | |
|
15 | PARAMETER_ADDITONAL_ATTRS = 'additional_attrs' | |||
21 |
|
16 | |||
22 | PARAMETER_PREV_LINK = 'prev_page_link' |
|
17 | PARAMETER_PREV_LINK = 'prev_page_link' | |
23 | PARAMETER_NEXT_LINK = 'next_page_link' |
|
18 | PARAMETER_NEXT_LINK = 'next_page_link' | |
@@ -30,25 +25,34 b' class FeedView(PostMixin, BaseBoardView)' | |||||
30 |
|
25 | |||
31 | def get(self, request): |
|
26 | def get(self, request): | |
32 | page = request.GET.get('page', DEFAULT_PAGE) |
|
27 | page = request.GET.get('page', DEFAULT_PAGE) | |
|
28 | tripcode = request.GET.get('tripcode', None) | |||
33 |
|
29 | |||
34 | params = self.get_context_data(request=request) |
|
30 | params = self.get_context_data(request=request) | |
35 |
|
31 | |||
36 | settings_manager = get_settings_manager(request) |
|
32 | settings_manager = get_settings_manager(request) | |
37 |
|
33 | |||
38 |
p |
|
34 | posts = Post.objects.exclude( | |
39 |
|
|
35 | threads__tags__in=settings_manager.get_hidden_tags()).order_by( | |
40 | .order_by('-pub_time') |
|
36 | '-pub_time').prefetch_related('images', 'thread', 'threads') | |
41 | .prefetch_related('images', 'thread', 'threads'), 10) |
|
37 | if tripcode: | |
|
38 | posts = posts.filter(tripcode=tripcode) | |||
|
39 | ||||
|
40 | paginator = get_paginator(posts, POSTS_PER_PAGE) | |||
42 | paginator.current_page = int(page) |
|
41 | paginator.current_page = int(page) | |
43 |
|
42 | |||
44 | params[PARAMETER_POSTS] = paginator.page(page).object_list |
|
43 | params[PARAMETER_POSTS] = paginator.page(page).object_list | |
45 |
|
44 | |||
46 | self.get_page_context(paginator, params, page) |
|
45 | additional_params = dict() | |
|
46 | if tripcode: | |||
|
47 | additional_params['tripcode'] = tripcode | |||
|
48 | params[PARAMETER_ADDITONAL_ATTRS] = '&tripcode=' + tripcode | |||
|
49 | ||||
|
50 | self.get_page_context(paginator, params, page, additional_params) | |||
47 |
|
51 | |||
48 | return render(request, TEMPLATE, params) |
|
52 | return render(request, TEMPLATE, params) | |
49 |
|
53 | |||
50 | # TODO Dedup this into PagedMixin |
|
54 | # TODO Dedup this into PagedMixin | |
51 | def get_page_context(self, paginator, params, page): |
|
55 | def get_page_context(self, paginator, params, page, additional_params): | |
52 | """ |
|
56 | """ | |
53 | Get pagination context variables |
|
57 | Get pagination context variables | |
54 | """ |
|
58 | """ | |
@@ -59,8 +63,14 b' class FeedView(PostMixin, BaseBoardView)' | |||||
59 | if current_page.has_previous(): |
|
63 | if current_page.has_previous(): | |
60 | params[PARAMETER_PREV_LINK] = self.get_previous_page_link( |
|
64 | params[PARAMETER_PREV_LINK] = self.get_previous_page_link( | |
61 | current_page) |
|
65 | current_page) | |
|
66 | for param in additional_params.keys(): | |||
|
67 | params[PARAMETER_PREV_LINK] += '&{}={}'.format( | |||
|
68 | param, additional_params[param]) | |||
62 | if current_page.has_next(): |
|
69 | if current_page.has_next(): | |
63 | params[PARAMETER_NEXT_LINK] = self.get_next_page_link(current_page) |
|
70 | params[PARAMETER_NEXT_LINK] = self.get_next_page_link(current_page) | |
|
71 | for param in additional_params.keys(): | |||
|
72 | params[PARAMETER_NEXT_LINK] += '&{}={}'.format( | |||
|
73 | param, additional_params[param]) | |||
64 |
|
74 | |||
65 | def get_previous_page_link(self, current_page): |
|
75 | def get_previous_page_link(self, current_page): | |
66 | return reverse('feed') + '?page={}'.format( |
|
76 | return reverse('feed') + '?page={}'.format( |
@@ -9,7 +9,6 b' from boards.views.base import BaseBoardV' | |||||
9 | from boards.forms import SettingsForm, PlainErrorList |
|
9 | from boards.forms import SettingsForm, PlainErrorList | |
10 | from boards import settings |
|
10 | from boards import settings | |
11 |
|
11 | |||
12 |
|
||||
13 | FORM_THEME = 'theme' |
|
12 | FORM_THEME = 'theme' | |
14 | FORM_USERNAME = 'username' |
|
13 | FORM_USERNAME = 'username' | |
15 | FORM_TIMEZONE = 'timezone' |
|
14 | FORM_TIMEZONE = 'timezone' |
@@ -3,7 +3,7 b' from django.core.urlresolvers import rev' | |||||
3 |
|
3 | |||
4 | from boards.abstracts.settingsmanager import get_settings_manager, \ |
|
4 | from boards.abstracts.settingsmanager import get_settings_manager, \ | |
5 | SETTING_FAVORITE_TAGS, SETTING_HIDDEN_TAGS |
|
5 | SETTING_FAVORITE_TAGS, SETTING_HIDDEN_TAGS | |
6 | from boards.models import Tag |
|
6 | from boards.models import Tag, PostImage | |
7 | from boards.views.all_threads import AllThreadsView, DEFAULT_PAGE |
|
7 | from boards.views.all_threads import AllThreadsView, DEFAULT_PAGE | |
8 | from boards.views.mixins import DispatcherMixin |
|
8 | from boards.views.mixins import DispatcherMixin | |
9 | from boards.forms import ThreadForm, PlainErrorList |
|
9 | from boards.forms import ThreadForm, PlainErrorList | |
@@ -12,6 +12,9 b" PARAM_HIDDEN_TAGS = 'hidden_tags'" | |||||
12 | PARAM_TAG = 'tag' |
|
12 | PARAM_TAG = 'tag' | |
13 | PARAM_IS_FAVORITE = 'is_favorite' |
|
13 | PARAM_IS_FAVORITE = 'is_favorite' | |
14 | PARAM_IS_HIDDEN = 'is_hidden' |
|
14 | PARAM_IS_HIDDEN = 'is_hidden' | |
|
15 | PARAM_RANDOM_IMAGE_POST = 'random_image_post' | |||
|
16 | PARAM_RELATED_TAGS = 'related_tags' | |||
|
17 | ||||
15 |
|
18 | |||
16 | __author__ = 'neko259' |
|
19 | __author__ = 'neko259' | |
17 |
|
20 | |||
@@ -47,6 +50,9 b' class TagView(AllThreadsView, Dispatcher' | |||||
47 | params[PARAM_IS_FAVORITE] = fav_tag_names is not None and tag.name in fav_tag_names |
|
50 | params[PARAM_IS_FAVORITE] = fav_tag_names is not None and tag.name in fav_tag_names | |
48 | params[PARAM_IS_HIDDEN] = hidden_tag_names is not None and tag.name in hidden_tag_names |
|
51 | params[PARAM_IS_HIDDEN] = hidden_tag_names is not None and tag.name in hidden_tag_names | |
49 |
|
52 | |||
|
53 | params[PARAM_RANDOM_IMAGE_POST] = tag.get_random_image_post() | |||
|
54 | params[PARAM_RELATED_TAGS] = tag.get_related_tags() | |||
|
55 | ||||
50 | return params |
|
56 | return params | |
51 |
|
57 | |||
52 | def get_previous_page_link(self, current_page): |
|
58 | def get_previous_page_link(self, current_page): | |
@@ -84,7 +90,7 b' class TagView(AllThreadsView, Dispatcher' | |||||
84 | # Ban user because he is suspected to be a bot |
|
90 | # Ban user because he is suspected to be a bot | |
85 | self._ban_current_user(request) |
|
91 | self._ban_current_user(request) | |
86 |
|
92 | |||
87 |
return self.get(request, tag_name, |
|
93 | return self.get(request, tag_name, form) | |
88 |
|
94 | |||
89 | def subscribe(self, request): |
|
95 | def subscribe(self, request): | |
90 | tag = get_object_or_404(Tag, name=self.tag_name) |
|
96 | tag = get_object_or_404(Tag, name=self.tag_name) |
@@ -1,3 +1,4 b'' | |||||
|
1 | import hashlib | |||
1 | from django.core.exceptions import ObjectDoesNotExist |
|
2 | from django.core.exceptions import ObjectDoesNotExist | |
2 | from django.http import Http404 |
|
3 | from django.http import Http404 | |
3 | from django.shortcuts import get_object_or_404, render, redirect |
|
4 | from django.shortcuts import get_object_or_404, render, redirect | |
@@ -100,18 +101,19 b' class ThreadView(BaseBoardView, PostMixi' | |||||
100 |
|
101 | |||
101 | data = form.cleaned_data |
|
102 | data = form.cleaned_data | |
102 |
|
103 | |||
103 | title = data[FORM_TITLE] |
|
104 | title = form.get_title() | |
104 | text = data[FORM_TEXT] |
|
105 | text = data[FORM_TEXT] | |
105 |
|
|
106 | file = form.get_file() | |
106 | threads = data[FORM_THREADS] |
|
107 | threads = data[FORM_THREADS] | |
107 |
|
108 | |||
108 | text = self._remove_invalid_links(text) |
|
109 | text = self._remove_invalid_links(text) | |
109 |
|
110 | |||
110 | post_thread = opening_post.get_thread() |
|
111 | post_thread = opening_post.get_thread() | |
111 |
|
112 | |||
112 |
post = Post.objects.create_post(title=title, text=text, |
|
113 | post = Post.objects.create_post(title=title, text=text, file=file, | |
113 | thread=post_thread, ip=ip, |
|
114 | thread=post_thread, ip=ip, | |
114 |
opening_posts=threads |
|
115 | opening_posts=threads, | |
|
116 | tripcode=form.get_tripcode()) | |||
115 | post.notify_clients() |
|
117 | post.notify_clients() | |
116 |
|
118 | |||
117 | if html_response: |
|
119 | if html_response: |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
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