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 |
@@ -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) |
@@ -1,35 +1,36 b'' | |||
|
1 | 1 | bc8fce57a613175450b8b6d933cdd85f22c04658 1.1 |
|
2 | 2 | 784258eb652c563c288ca7652c33f52cd4733d83 1.1-stable |
|
3 | 3 | 1b53a22467a8fccc798935d7a26efe78e4bc7b25 1.2-stable |
|
4 | 4 | 1713fb7543386089e364c39703b79e57d3d851f0 1.3 |
|
5 | 5 | 80f183ebbe132ea8433eacae9431360f31fe7083 1.4 |
|
6 | 6 | 4330ff5a2bf6c543d8aaae8a43de1dc062f3bd13 1.4.1 |
|
7 | 7 | 8531d7b001392289a6b761f38c73a257606552ad 1.5 |
|
8 | 8 | 78e843c8b04b5a81cee5aa24601e305fae75da24 1.5.1 |
|
9 | 9 | 4f92838730ed9aa1d17651bbcdca19a097fd0c37 1.6 |
|
10 | 10 | 4bac2f37ea463337ddd27f98e7985407a74de504 1.7 |
|
11 | 11 | 1c4febea92c6503ae557fba73b2768659ae90d24 1.7.1 |
|
12 | 12 | 56a4a4578fc454ee455e33dd74a2cc82234bcb59 1.7.2 |
|
13 | 13 | 34d6f3d5deb22be56b6c1512ec654bd7f6e03bcc 1.7.3 |
|
14 | 14 | f5cca33d29c673b67d43f310bebc4e3a21c6d04c 1.7.4 |
|
15 | 15 | 7f7c33ba6e3f3797ca866c5ed5d358a2393f1371 1.8 |
|
16 | 16 | a6b9dd9547bdc17b681502efcceb17aa5c09adf4 1.8.1 |
|
17 | 17 | 8318fa1615d1946e4519f5735ae880909521990d 2.0 |
|
18 | 18 | e23590ee7e2067a3f0fc3cbcfd66404b47127feb 2.1 |
|
19 | 19 | 4d998aba79e4abf0a2e78e93baaa2c2800b1c49c 2.2 |
|
20 | 20 | 07fdef4ac33a859250d03f17c594089792bca615 2.2.1 |
|
21 | 21 | bcc74d45f060ecd3ff06ff448165aea0d026cb3e 2.2.2 |
|
22 | 22 | b0e629ff24eb47a449ecfb455dc6cc600d18c9e2 2.2.3 |
|
23 | 23 | 1b52ba60f17fd7c90912c14d9d17e880b7952d01 2.2.4 |
|
24 | 24 | 957e2fec91468f739b0fc2b9936d564505048c68 2.3.0 |
|
25 | 25 | bb91141c6ea5c822ccbe2d46c3c48bdab683b77d 2.4.0 |
|
26 | 26 | 97eb184637e5691b288eaf6b03e8971f3364c239 2.5.0 |
|
27 | 27 | 119fafc5381b933bf30d97be0b278349f6135075 2.5.1 |
|
28 | 28 | d528d76d3242cced614fa11bb63f3d342e4e1d09 2.5.2 |
|
29 | 29 | 1b631781ced34fbdeec032e7674bc4e131724699 2.6.0 |
|
30 | 30 | 0f2ef17dc0de678ada279bf7eedf6c5585f1fd7a 2.6.1 |
|
31 | 31 | d53fc814a424d7fd90f23025c87b87baa164450e 2.7.0 |
|
32 | 32 | 836d8bb9fcd930b952b9a02029442c71c2441983 2.8.0 |
|
33 | 33 | dfb6c481b1a2c33705de9a9b5304bc924c46b202 2.8.1 |
|
34 | 34 | 4a5bec08ccfb47a27f9e98698f12dd5b7246623b 2.8.2 |
|
35 | 35 | 604935b98f5b5e4a5e903594f048046e1fbb3519 2.8.3 |
|
36 | c48ffdc671566069ed0f33644da1229277f3cd18 2.9.0 |
@@ -1,145 +1,148 b'' | |||
|
1 | from django.shortcuts import get_object_or_404 | |
|
2 | 1 |
|
|
3 | 2 | |
|
3 | MAX_TRIPCODE_COLLISIONS = 50 | |
|
4 | ||
|
4 | 5 | __author__ = 'neko259' |
|
5 | 6 | |
|
6 | 7 | SESSION_SETTING = 'setting' |
|
7 | 8 | |
|
8 | 9 | # Remove this, it is not used any more cause there is a user's permission |
|
9 | 10 | PERMISSION_MODERATE = 'moderator' |
|
10 | 11 | |
|
11 | 12 | SETTING_THEME = 'theme' |
|
12 | 13 | SETTING_FAVORITE_TAGS = 'favorite_tags' |
|
13 | 14 | SETTING_HIDDEN_TAGS = 'hidden_tags' |
|
14 | 15 | SETTING_PERMISSIONS = 'permissions' |
|
15 | 16 | SETTING_USERNAME = 'username' |
|
16 | 17 | SETTING_LAST_NOTIFICATION_ID = 'last_notification' |
|
17 | 18 | SETTING_IMAGE_VIEWER = 'image_viewer' |
|
19 | SETTING_TRIPCODE = 'tripcode' | |
|
18 | 20 | |
|
19 | 21 | DEFAULT_THEME = 'md' |
|
20 | 22 | |
|
21 | 23 | |
|
22 | 24 | class SettingsManager: |
|
23 | 25 | """ |
|
24 | 26 | Base settings manager class. get_setting and set_setting methods should |
|
25 | 27 | be overriden. |
|
26 | 28 | """ |
|
27 | 29 | def __init__(self): |
|
28 | 30 | pass |
|
29 | 31 | |
|
30 | 32 | def get_theme(self) -> str: |
|
31 | 33 | theme = self.get_setting(SETTING_THEME) |
|
32 | 34 | if not theme: |
|
33 | 35 | theme = DEFAULT_THEME |
|
34 | 36 | self.set_setting(SETTING_THEME, theme) |
|
35 | 37 | |
|
36 | 38 | return theme |
|
37 | 39 | |
|
38 | 40 | def set_theme(self, theme): |
|
39 | 41 | self.set_setting(SETTING_THEME, theme) |
|
40 | 42 | |
|
41 | 43 | def has_permission(self, permission): |
|
42 | 44 | permissions = self.get_setting(SETTING_PERMISSIONS) |
|
43 | 45 | if permissions: |
|
44 | 46 | return permission in permissions |
|
45 | 47 | else: |
|
46 | 48 | return False |
|
47 | 49 | |
|
48 | 50 | def get_setting(self, setting, default=None): |
|
49 | 51 | pass |
|
50 | 52 | |
|
51 | 53 | def set_setting(self, setting, value): |
|
52 | 54 | pass |
|
53 | 55 | |
|
54 | 56 | def add_permission(self, permission): |
|
55 | 57 | permissions = self.get_setting(SETTING_PERMISSIONS) |
|
56 | 58 | if not permissions: |
|
57 | 59 | permissions = [permission] |
|
58 | 60 | else: |
|
59 | 61 | permissions.append(permission) |
|
60 | 62 | self.set_setting(SETTING_PERMISSIONS, permissions) |
|
61 | 63 | |
|
62 | 64 | def del_permission(self, permission): |
|
63 | 65 | permissions = self.get_setting(SETTING_PERMISSIONS) |
|
64 | 66 | if not permissions: |
|
65 | 67 | permissions = [] |
|
66 | 68 | else: |
|
67 | 69 | permissions.remove(permission) |
|
68 | 70 | self.set_setting(SETTING_PERMISSIONS, permissions) |
|
69 | 71 | |
|
70 | 72 | def get_fav_tags(self) -> list: |
|
71 | 73 | tag_names = self.get_setting(SETTING_FAVORITE_TAGS) |
|
72 | 74 | tags = [] |
|
73 | 75 | if tag_names: |
|
74 | 76 | tags = list(Tag.objects.filter(name__in=tag_names)) |
|
75 | 77 | return tags |
|
76 | 78 | |
|
77 | 79 | def add_fav_tag(self, tag): |
|
78 | 80 | tags = self.get_setting(SETTING_FAVORITE_TAGS) |
|
79 | 81 | if not tags: |
|
80 | 82 | tags = [tag.name] |
|
81 | 83 | else: |
|
82 | 84 | if not tag.name in tags: |
|
83 | 85 | tags.append(tag.name) |
|
84 | 86 | |
|
85 | 87 | tags.sort() |
|
86 | 88 | self.set_setting(SETTING_FAVORITE_TAGS, tags) |
|
87 | 89 | |
|
88 | 90 | def del_fav_tag(self, tag): |
|
89 | 91 | tags = self.get_setting(SETTING_FAVORITE_TAGS) |
|
90 | 92 | if tag.name in tags: |
|
91 | 93 | tags.remove(tag.name) |
|
92 | 94 | self.set_setting(SETTING_FAVORITE_TAGS, tags) |
|
93 | 95 | |
|
94 | 96 | def get_hidden_tags(self) -> list: |
|
95 | 97 | tag_names = self.get_setting(SETTING_HIDDEN_TAGS) |
|
96 | 98 | tags = [] |
|
97 | 99 | if tag_names: |
|
98 | 100 | tags = list(Tag.objects.filter(name__in=tag_names)) |
|
99 | 101 | |
|
100 | 102 | return tags |
|
101 | 103 | |
|
102 | 104 | def add_hidden_tag(self, tag): |
|
103 | 105 | tags = self.get_setting(SETTING_HIDDEN_TAGS) |
|
104 | 106 | if not tags: |
|
105 | 107 | tags = [tag.name] |
|
106 | 108 | else: |
|
107 | 109 | if not tag.name in tags: |
|
108 | 110 | tags.append(tag.name) |
|
109 | 111 | |
|
110 | 112 | tags.sort() |
|
111 | 113 | self.set_setting(SETTING_HIDDEN_TAGS, tags) |
|
112 | 114 | |
|
113 | 115 | def del_hidden_tag(self, tag): |
|
114 | 116 | tags = self.get_setting(SETTING_HIDDEN_TAGS) |
|
115 | 117 | if tag.name in tags: |
|
116 | 118 | tags.remove(tag.name) |
|
117 | 119 | self.set_setting(SETTING_HIDDEN_TAGS, tags) |
|
118 | 120 | |
|
119 | 121 | |
|
120 | 122 | class SessionSettingsManager(SettingsManager): |
|
121 | 123 | """ |
|
122 | 124 | Session-based settings manager. All settings are saved to the user's |
|
123 | 125 | session. |
|
124 | 126 | """ |
|
125 | 127 | def __init__(self, session): |
|
126 | 128 | SettingsManager.__init__(self) |
|
127 | 129 | self.session = session |
|
128 | 130 | |
|
129 | 131 | def get_setting(self, setting, default=None): |
|
130 | 132 | if setting in self.session: |
|
131 | 133 | return self.session[setting] |
|
132 | 134 | else: |
|
135 | self.set_setting(setting, default) | |
|
133 | 136 | return default |
|
134 | 137 | |
|
135 | 138 | def set_setting(self, setting, value): |
|
136 | 139 | self.session[setting] = value |
|
137 | 140 | |
|
138 | 141 | |
|
139 | 142 | def get_settings_manager(request) -> SettingsManager: |
|
140 | 143 | """ |
|
141 | 144 | Get settings manager based on the request object. Currently only |
|
142 | 145 | session-based manager is supported. In the future, cookie-based or |
|
143 | 146 | database-based managers could be implemented. |
|
144 | 147 | """ |
|
145 | 148 | return SessionSettingsManager(request.session) |
@@ -1,33 +1,33 b'' | |||
|
1 | 1 | [Version] |
|
2 |
Version = 2. |
|
|
2 | Version = 2.9.0 Claire | |
|
3 | 3 | SiteName = Neboard DEV |
|
4 | 4 | |
|
5 | 5 | [Cache] |
|
6 | 6 | # Timeout for caching, if cache is used |
|
7 | 7 | CacheTimeout = 600 |
|
8 | 8 | |
|
9 | 9 | [Forms] |
|
10 | 10 | # Max post length in characters |
|
11 | 11 | MaxTextLength = 30000 |
|
12 |
Max |
|
|
12 | MaxFileSize = 8000000 | |
|
13 | 13 | LimitPostingSpeed = false |
|
14 | 14 | |
|
15 | 15 | [Messages] |
|
16 | 16 | # Thread bumplimit |
|
17 | 17 | MaxPostsPerThread = 10 |
|
18 | 18 | # Old posts will be archived or deleted if this value is reached |
|
19 | 19 | MaxThreadCount = 5 |
|
20 | 20 | |
|
21 | 21 | [View] |
|
22 | 22 | DefaultTheme = md |
|
23 | 23 | DefaultImageViewer = simple |
|
24 | 24 | LastRepliesCount = 3 |
|
25 | 25 | ThreadsPerPage = 3 |
|
26 | 26 | |
|
27 | 27 | [Storage] |
|
28 | 28 | # Enable archiving threads instead of deletion when the thread limit is reached |
|
29 | 29 | ArchiveThreads = true |
|
30 | 30 | |
|
31 | 31 | [External] |
|
32 | 32 | # Thread update |
|
33 | 33 | WebsocketsEnabled = false |
@@ -1,387 +1,395 b'' | |||
|
1 | import hashlib | |
|
1 | 2 | import re |
|
2 | 3 | import time |
|
4 | ||
|
3 | 5 | import pytz |
|
4 | ||
|
5 | 6 | from django import forms |
|
6 | 7 | from django.core.files.uploadedfile import SimpleUploadedFile |
|
7 | 8 | from django.core.exceptions import ObjectDoesNotExist |
|
8 | 9 | from django.forms.util import ErrorList |
|
9 | 10 | from django.utils.translation import ugettext_lazy as _ |
|
10 | 11 | import requests |
|
11 | 12 | |
|
12 | 13 | from boards.mdx_neboard import formatters |
|
13 | 14 | from boards.models.post import TITLE_MAX_LENGTH |
|
14 | 15 | from boards.models import Tag, Post |
|
15 | 16 | from neboard import settings |
|
16 | 17 | import boards.settings as board_settings |
|
18 | import neboard | |
|
17 | 19 | |
|
18 | 20 | HEADER_CONTENT_LENGTH = 'content-length' |
|
19 | 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 | 23 | REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE) |
|
30 | 24 | |
|
31 | 25 | VETERAN_POSTING_DELAY = 5 |
|
32 | 26 | |
|
33 | 27 | ATTRIBUTE_PLACEHOLDER = 'placeholder' |
|
34 | 28 | ATTRIBUTE_ROWS = 'rows' |
|
35 | 29 | |
|
36 | 30 | LAST_POST_TIME = 'last_post_time' |
|
37 | 31 | LAST_LOGIN_TIME = 'last_login_time' |
|
38 | 32 | TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.') |
|
39 | 33 | TAGS_PLACEHOLDER = _('music images i_dont_like_tags') |
|
40 | 34 | |
|
41 | 35 | LABEL_TITLE = _('Title') |
|
42 | 36 | LABEL_TEXT = _('Text') |
|
43 | 37 | LABEL_TAG = _('Tag') |
|
44 | 38 | LABEL_SEARCH = _('Search') |
|
45 | 39 | |
|
46 | 40 | ERROR_SPEED = _('Please wait %s seconds before sending message') |
|
47 | 41 | |
|
48 | 42 | TAG_MAX_LENGTH = 20 |
|
49 | 43 | |
|
50 |
|
|
|
44 | FILE_DOWNLOAD_CHUNK_BYTES = 100000 | |
|
51 | 45 | |
|
52 | 46 | HTTP_RESULT_OK = 200 |
|
53 | 47 | |
|
54 | 48 | TEXTAREA_ROWS = 4 |
|
55 | 49 | |
|
56 | 50 | |
|
57 | 51 | def get_timezones(): |
|
58 | 52 | timezones = [] |
|
59 | 53 | for tz in pytz.common_timezones: |
|
60 | 54 | timezones.append((tz, tz),) |
|
61 | 55 | return timezones |
|
62 | 56 | |
|
63 | 57 | |
|
64 | 58 | class FormatPanel(forms.Textarea): |
|
65 | 59 | """ |
|
66 | 60 | Panel for text formatting. Consists of buttons to add different tags to the |
|
67 | 61 | form text area. |
|
68 | 62 | """ |
|
69 | 63 | |
|
70 | 64 | def render(self, name, value, attrs=None): |
|
71 | 65 | output = '<div id="mark-panel">' |
|
72 | 66 | for formatter in formatters: |
|
73 | 67 | output += '<span class="mark_btn"' + \ |
|
74 | 68 | ' onClick="addMarkToMsg(\'' + formatter.format_left + \ |
|
75 | 69 | '\', \'' + formatter.format_right + '\')">' + \ |
|
76 | 70 | formatter.preview_left + formatter.name + \ |
|
77 | 71 | formatter.preview_right + '</span>' |
|
78 | 72 | |
|
79 | 73 | output += '</div>' |
|
80 | 74 | output += super(FormatPanel, self).render(name, value, attrs=None) |
|
81 | 75 | |
|
82 | 76 | return output |
|
83 | 77 | |
|
84 | 78 | |
|
85 | 79 | class PlainErrorList(ErrorList): |
|
86 | 80 | def __unicode__(self): |
|
87 | 81 | return self.as_text() |
|
88 | 82 | |
|
89 | 83 | def as_text(self): |
|
90 | 84 | return ''.join(['(!) %s ' % e for e in self]) |
|
91 | 85 | |
|
92 | 86 | |
|
93 | 87 | class NeboardForm(forms.Form): |
|
94 | 88 | """ |
|
95 | 89 | Form with neboard-specific formatting. |
|
96 | 90 | """ |
|
97 | 91 | |
|
98 | 92 | def as_div(self): |
|
99 | 93 | """ |
|
100 | 94 | Returns this form rendered as HTML <as_div>s. |
|
101 | 95 | """ |
|
102 | 96 | |
|
103 | 97 | return self._html_output( |
|
104 | 98 | # TODO Do not show hidden rows in the list here |
|
105 | 99 | normal_row='<div class="form-row">' |
|
106 | 100 | '<div class="form-label">' |
|
107 | 101 | '%(label)s' |
|
108 | 102 | '</div>' |
|
109 | 103 | '<div class="form-input">' |
|
110 | 104 | '%(field)s' |
|
111 | 105 | '</div>' |
|
112 | 106 | '</div>' |
|
113 | 107 | '<div class="form-row">' |
|
114 | 108 | '%(help_text)s' |
|
115 | 109 | '</div>', |
|
116 | 110 | error_row='<div class="form-row">' |
|
117 | 111 | '<div class="form-label"></div>' |
|
118 | 112 | '<div class="form-errors">%s</div>' |
|
119 | 113 | '</div>', |
|
120 | 114 | row_ender='</div>', |
|
121 | 115 | help_text_html='%s', |
|
122 | 116 | errors_on_separate_row=True) |
|
123 | 117 | |
|
124 | 118 | def as_json_errors(self): |
|
125 | 119 | errors = [] |
|
126 | 120 | |
|
127 | 121 | for name, field in list(self.fields.items()): |
|
128 | 122 | if self[name].errors: |
|
129 | 123 | errors.append({ |
|
130 | 124 | 'field': name, |
|
131 | 125 | 'errors': self[name].errors.as_text(), |
|
132 | 126 | }) |
|
133 | 127 | |
|
134 | 128 | return errors |
|
135 | 129 | |
|
136 | 130 | |
|
137 | 131 | class PostForm(NeboardForm): |
|
138 | 132 | |
|
139 | 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 | 138 | text = forms.CharField( |
|
142 | 139 | widget=FormatPanel(attrs={ |
|
143 | 140 | ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER, |
|
144 | 141 | ATTRIBUTE_ROWS: TEXTAREA_ROWS, |
|
145 | 142 | }), |
|
146 | 143 | required=False, label=LABEL_TEXT) |
|
147 |
|
|
|
144 | file = forms.FileField(required=False, label=_('File'), | |
|
148 | 145 | widget=forms.ClearableFileInput( |
|
149 |
attrs={'accept': ' |
|
|
150 |
|
|
|
146 | attrs={'accept': 'file/*'})) | |
|
147 | file_url = forms.CharField(required=False, label=_('File URL'), | |
|
151 | 148 | widget=forms.TextInput( |
|
152 | 149 | attrs={ATTRIBUTE_PLACEHOLDER: |
|
153 | 150 | 'http://example.com/image.png'})) |
|
154 | 151 | |
|
155 | 152 | # This field is for spam prevention only |
|
156 | 153 | email = forms.CharField(max_length=100, required=False, label=_('e-mail'), |
|
157 | 154 | widget=forms.TextInput(attrs={ |
|
158 | 155 | 'class': 'form-email'})) |
|
159 | 156 | threads = forms.CharField(required=False, label=_('Additional threads'), |
|
160 | 157 | widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: |
|
161 | 158 | '123 456 789'})) |
|
162 | 159 | |
|
163 | 160 | session = None |
|
164 | 161 | need_to_ban = False |
|
165 | 162 | |
|
166 | 163 | def clean_title(self): |
|
167 | 164 | title = self.cleaned_data['title'] |
|
168 | 165 | if title: |
|
169 | 166 | if len(title) > TITLE_MAX_LENGTH: |
|
170 | 167 | raise forms.ValidationError(_('Title must have less than %s ' |
|
171 | 168 | 'characters') % |
|
172 | 169 | str(TITLE_MAX_LENGTH)) |
|
173 | 170 | return title |
|
174 | 171 | |
|
175 | 172 | def clean_text(self): |
|
176 | 173 | text = self.cleaned_data['text'].strip() |
|
177 | 174 | if text: |
|
178 | 175 | max_length = board_settings.get_int('Forms', 'MaxTextLength') |
|
179 | 176 | if len(text) > max_length: |
|
180 | 177 | raise forms.ValidationError(_('Text must have less than %s ' |
|
181 | 178 | 'characters') % str(max_length)) |
|
182 | 179 | return text |
|
183 | 180 | |
|
184 |
def clean_ |
|
|
185 |
|
|
|
181 | def clean_file(self): | |
|
182 | file = self.cleaned_data['file'] | |
|
186 | 183 | |
|
187 |
if |
|
|
188 |
self.validate_ |
|
|
184 | if file: | |
|
185 | self.validate_file_size(file.size) | |
|
189 | 186 | |
|
190 |
return |
|
|
187 | return file | |
|
191 | 188 | |
|
192 |
def clean_ |
|
|
193 |
url = self.cleaned_data[' |
|
|
189 | def clean_file_url(self): | |
|
190 | url = self.cleaned_data['file_url'] | |
|
194 | 191 | |
|
195 |
|
|
|
192 | file = None | |
|
196 | 193 | if url: |
|
197 |
|
|
|
194 | file = self._get_file_from_url(url) | |
|
198 | 195 | |
|
199 |
if not |
|
|
196 | if not file: | |
|
200 | 197 | raise forms.ValidationError(_('Invalid URL')) |
|
201 | 198 | else: |
|
202 |
self.validate_ |
|
|
199 | self.validate_file_size(file.size) | |
|
203 | 200 | |
|
204 |
return |
|
|
201 | return file | |
|
205 | 202 | |
|
206 | 203 | def clean_threads(self): |
|
207 | 204 | threads_str = self.cleaned_data['threads'] |
|
208 | 205 | |
|
209 | 206 | if len(threads_str) > 0: |
|
210 | 207 | threads_id_list = threads_str.split(' ') |
|
211 | 208 | |
|
212 | 209 | threads = list() |
|
213 | 210 | |
|
214 | 211 | for thread_id in threads_id_list: |
|
215 | 212 | try: |
|
216 | 213 | thread = Post.objects.get(id=int(thread_id)) |
|
217 | 214 | if not thread.is_opening() or thread.get_thread().archived: |
|
218 | 215 | raise ObjectDoesNotExist() |
|
219 | 216 | threads.append(thread) |
|
220 | 217 | except (ObjectDoesNotExist, ValueError): |
|
221 | 218 | raise forms.ValidationError(_('Invalid additional thread list')) |
|
222 | 219 | |
|
223 | 220 | return threads |
|
224 | 221 | |
|
225 | 222 | def clean(self): |
|
226 | 223 | cleaned_data = super(PostForm, self).clean() |
|
227 | 224 | |
|
228 | 225 | if cleaned_data['email']: |
|
229 | 226 | self.need_to_ban = True |
|
230 | 227 | raise forms.ValidationError('A human cannot enter a hidden field') |
|
231 | 228 | |
|
232 | 229 | if not self.errors: |
|
233 |
self._clean_text_ |
|
|
230 | self._clean_text_file() | |
|
234 | 231 | |
|
235 | 232 | if not self.errors and self.session: |
|
236 | 233 | self._validate_posting_speed() |
|
237 | 234 | |
|
238 | 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 |
|
|
|
246 |
return |
|
|
242 | file = self.cleaned_data['file'] | |
|
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 | 259 | text = self.cleaned_data.get('text') |
|
250 |
|
|
|
260 | file = self.get_file() | |
|
251 | 261 | |
|
252 |
if (not text) and (not |
|
|
253 |
error_message = _('Either text or |
|
|
262 | if (not text) and (not file): | |
|
263 | error_message = _('Either text or file must be entered.') | |
|
254 | 264 | self._errors['text'] = self.error_class([error_message]) |
|
255 | 265 | |
|
256 | 266 | def _validate_posting_speed(self): |
|
257 | 267 | can_post = True |
|
258 | 268 | |
|
259 | 269 | posting_delay = settings.POSTING_DELAY |
|
260 | 270 | |
|
261 | 271 | if board_settings.get_bool('Forms', 'LimitPostingSpeed'): |
|
262 | 272 | now = time.time() |
|
263 | 273 | |
|
264 | 274 | current_delay = 0 |
|
265 | 275 | need_delay = False |
|
266 | 276 | |
|
267 | 277 | if not LAST_POST_TIME in self.session: |
|
268 | 278 | self.session[LAST_POST_TIME] = now |
|
269 | 279 | |
|
270 | 280 | need_delay = True |
|
271 | 281 | else: |
|
272 | 282 | last_post_time = self.session.get(LAST_POST_TIME) |
|
273 | 283 | current_delay = int(now - last_post_time) |
|
274 | 284 | |
|
275 | 285 | need_delay = current_delay < posting_delay |
|
276 | 286 | |
|
277 | 287 | if need_delay: |
|
278 | 288 | error_message = ERROR_SPEED % str(posting_delay |
|
279 | 289 | - current_delay) |
|
280 | 290 | self._errors['text'] = self.error_class([error_message]) |
|
281 | 291 | |
|
282 | 292 | can_post = False |
|
283 | 293 | |
|
284 | 294 | if can_post: |
|
285 | 295 | self.session[LAST_POST_TIME] = now |
|
286 | 296 | |
|
287 |
def validate_ |
|
|
288 |
max_size = board_settings.get_int('Forms', 'Max |
|
|
297 | def validate_file_size(self, size: int): | |
|
298 | max_size = board_settings.get_int('Forms', 'MaxFileSize') | |
|
289 | 299 | if size > max_size: |
|
290 | 300 | raise forms.ValidationError( |
|
291 |
_(' |
|
|
301 | _('File must be less than %s bytes') | |
|
292 | 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 | 309 | img_temp = None |
|
300 | 310 | |
|
301 | 311 | try: |
|
302 | 312 | # Verify content headers |
|
303 | 313 | response_head = requests.head(url, verify=False) |
|
304 | 314 | content_type = response_head.headers[HEADER_CONTENT_TYPE].split(';')[0] |
|
305 | if content_type in CONTENT_TYPE_IMAGE: | |
|
306 | length_header = response_head.headers.get(HEADER_CONTENT_LENGTH) | |
|
307 |
|
|
|
308 | length = int(length_header) | |
|
309 | self.validate_image_size(length) | |
|
310 | # Get the actual content into memory | |
|
311 | response = requests.get(url, verify=False, stream=True) | |
|
315 | length_header = response_head.headers.get(HEADER_CONTENT_LENGTH) | |
|
316 | if length_header: | |
|
317 | length = int(length_header) | |
|
318 | self.validate_file_size(length) | |
|
319 | # Get the actual content into memory | |
|
320 | response = requests.get(url, verify=False, stream=True) | |
|
312 | 321 | |
|
313 |
|
|
|
314 |
|
|
|
315 |
|
|
|
316 |
|
|
|
317 |
|
|
|
318 |
|
|
|
319 |
|
|
|
322 | # Download file, stop if the size exceeds limit | |
|
323 | size = 0 | |
|
324 | content = b'' | |
|
325 | for chunk in response.iter_content(FILE_DOWNLOAD_CHUNK_BYTES): | |
|
326 | size += len(chunk) | |
|
327 | self.validate_file_size(size) | |
|
328 | content += chunk | |
|
320 | 329 | |
|
321 |
|
|
|
322 |
|
|
|
323 |
|
|
|
324 |
|
|
|
325 |
|
|
|
326 |
|
|
|
327 | except Exception: | |
|
328 |
# Just return no |
|
|
330 | if response.status_code == HTTP_RESULT_OK and content: | |
|
331 | # Set a dummy file name that will be replaced | |
|
332 | # anyway, just keep the valid extension | |
|
333 | filename = 'file.' + content_type.split('/')[1] | |
|
334 | img_temp = SimpleUploadedFile(filename, content, | |
|
335 | content_type) | |
|
336 | except Exception as e: | |
|
337 | # Just return no file | |
|
329 | 338 | pass |
|
330 | 339 | |
|
331 | 340 | return img_temp |
|
332 | 341 | |
|
333 | 342 | |
|
334 | 343 | class ThreadForm(PostForm): |
|
335 | 344 | |
|
336 | 345 | tags = forms.CharField( |
|
337 | 346 | widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}), |
|
338 | 347 | max_length=100, label=_('Tags'), required=True) |
|
339 | 348 | |
|
340 | 349 | def clean_tags(self): |
|
341 | 350 | tags = self.cleaned_data['tags'].strip() |
|
342 | 351 | |
|
343 | 352 | if not tags or not REGEX_TAGS.match(tags): |
|
344 | 353 | raise forms.ValidationError( |
|
345 | 354 | _('Inappropriate characters in tags.')) |
|
346 | 355 | |
|
347 | 356 | required_tag_exists = False |
|
348 | 357 | for tag in tags.split(): |
|
349 | 358 | try: |
|
350 | 359 | Tag.objects.get(name=tag.strip().lower(), required=True) |
|
351 | 360 | required_tag_exists = True |
|
352 | 361 | break |
|
353 | 362 | except ObjectDoesNotExist: |
|
354 | 363 | pass |
|
355 | 364 | |
|
356 | 365 | if not required_tag_exists: |
|
357 | 366 | all_tags = Tag.objects.filter(required=True) |
|
358 | 367 | raise forms.ValidationError( |
|
359 |
_('Need at least one |
|
|
360 | + ', '.join([tag.name for tag in all_tags])) | |
|
368 | _('Need at least one section.')) | |
|
361 | 369 | |
|
362 | 370 | return tags |
|
363 | 371 | |
|
364 | 372 | def clean(self): |
|
365 | 373 | cleaned_data = super(ThreadForm, self).clean() |
|
366 | 374 | |
|
367 | 375 | return cleaned_data |
|
368 | 376 | |
|
369 | 377 | |
|
370 | 378 | class SettingsForm(NeboardForm): |
|
371 | 379 | |
|
372 | 380 | theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme')) |
|
373 | 381 | image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('Image view mode')) |
|
374 | 382 | username = forms.CharField(label=_('User name'), required=False) |
|
375 | 383 | timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone')) |
|
376 | 384 | |
|
377 | 385 | def clean_username(self): |
|
378 | 386 | username = self.cleaned_data['username'] |
|
379 | 387 | |
|
380 | 388 | if username and not REGEX_TAGS.match(username): |
|
381 | 389 | raise forms.ValidationError(_('Inappropriate characters.')) |
|
382 | 390 | |
|
383 | 391 | return username |
|
384 | 392 | |
|
385 | 393 | |
|
386 | 394 | class SearchForm(NeboardForm): |
|
387 | 395 | query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False) |
|
1 | NO CONTENT: modified file, binary diff hidden |
@@ -1,431 +1,462 b'' | |||
|
1 | 1 | # SOME DESCRIPTIVE TITLE. |
|
2 | 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER |
|
3 | 3 | # This file is distributed under the same license as the PACKAGE package. |
|
4 | 4 | # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. |
|
5 | 5 | # |
|
6 | 6 | msgid "" |
|
7 | 7 | msgstr "" |
|
8 | 8 | "Project-Id-Version: PACKAGE VERSION\n" |
|
9 | 9 | "Report-Msgid-Bugs-To: \n" |
|
10 |
"POT-Creation-Date: 2015-0 |
|
|
10 | "POT-Creation-Date: 2015-08-22 15:07+0300\n" | |
|
11 | 11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" |
|
12 | 12 | "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" |
|
13 | 13 | "Language-Team: LANGUAGE <LL@li.org>\n" |
|
14 | 14 | "Language: ru\n" |
|
15 | 15 | "MIME-Version: 1.0\n" |
|
16 | 16 | "Content-Type: text/plain; charset=UTF-8\n" |
|
17 | 17 | "Content-Transfer-Encoding: 8bit\n" |
|
18 | 18 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" |
|
19 | 19 | "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" |
|
20 | 20 | |
|
21 | 21 | #: admin.py:22 |
|
22 | 22 | msgid "{} posters were banned" |
|
23 | 23 | msgstr "" |
|
24 | 24 | |
|
25 | 25 | #: authors.py:9 |
|
26 | 26 | msgid "author" |
|
27 | 27 | msgstr "автор" |
|
28 | 28 | |
|
29 | 29 | #: authors.py:10 |
|
30 | 30 | msgid "developer" |
|
31 | 31 | msgstr "разработчик" |
|
32 | 32 | |
|
33 | 33 | #: authors.py:11 |
|
34 | 34 | msgid "javascript developer" |
|
35 | 35 | msgstr "разработчик javascript" |
|
36 | 36 | |
|
37 | 37 | #: authors.py:12 |
|
38 | 38 | msgid "designer" |
|
39 | 39 | msgstr "дизайнер" |
|
40 | 40 | |
|
41 |
#: forms.py:3 |
|
|
41 | #: forms.py:31 | |
|
42 | 42 | msgid "Type message here. Use formatting panel for more advanced usage." |
|
43 | 43 | msgstr "" |
|
44 | 44 | "Вводите сообщение сюда. Используйте панель для более сложного форматирования." |
|
45 | 45 | |
|
46 |
#: forms.py:3 |
|
|
46 | #: forms.py:32 | |
|
47 | 47 | msgid "music images i_dont_like_tags" |
|
48 | 48 | msgstr "музыка картинки теги_не_нужны" |
|
49 | 49 | |
|
50 |
#: forms.py:3 |
|
|
50 | #: forms.py:34 | |
|
51 | 51 | msgid "Title" |
|
52 | 52 | msgstr "Заголовок" |
|
53 | 53 | |
|
54 |
#: forms.py:3 |
|
|
54 | #: forms.py:35 | |
|
55 | 55 | msgid "Text" |
|
56 | 56 | msgstr "Текст" |
|
57 | 57 | |
|
58 |
#: forms.py: |
|
|
58 | #: forms.py:36 | |
|
59 | 59 | msgid "Tag" |
|
60 | 60 | msgstr "Метка" |
|
61 | 61 | |
|
62 |
#: forms.py: |
|
|
62 | #: forms.py:37 templates/boards/base.html:40 templates/search/search.html:7 | |
|
63 | 63 | msgid "Search" |
|
64 | 64 | msgstr "Поиск" |
|
65 | 65 | |
|
66 |
#: forms.py: |
|
|
66 | #: forms.py:39 | |
|
67 | 67 | #, python-format |
|
68 | 68 | msgid "Please wait %s seconds before sending message" |
|
69 | 69 | msgstr "Пожалуйста подождите %s секунд перед отправкой сообщения" |
|
70 | 70 | |
|
71 |
#: forms.py:14 |
|
|
72 |
msgid " |
|
|
73 |
msgstr " |
|
|
71 | #: forms.py:140 | |
|
72 | msgid "File" | |
|
73 | msgstr "Файл" | |
|
74 | 74 | |
|
75 |
#: forms.py:14 |
|
|
76 |
msgid " |
|
|
77 |
msgstr "URL |
|
|
75 | #: forms.py:143 | |
|
76 | msgid "File URL" | |
|
77 | msgstr "URL файла" | |
|
78 | 78 | |
|
79 |
#: forms.py:1 |
|
|
79 | #: forms.py:149 | |
|
80 | 80 | msgid "e-mail" |
|
81 | 81 | msgstr "" |
|
82 | 82 | |
|
83 |
#: forms.py:15 |
|
|
83 | #: forms.py:152 | |
|
84 | 84 | msgid "Additional threads" |
|
85 | 85 | msgstr "Дополнительные темы" |
|
86 | 86 | |
|
87 |
#: forms.py:1 |
|
|
87 | #: forms.py:155 | |
|
88 | msgid "Tripcode" | |
|
89 | msgstr "Трипкод" | |
|
90 | ||
|
91 | #: forms.py:164 | |
|
88 | 92 | #, python-format |
|
89 | 93 | msgid "Title must have less than %s characters" |
|
90 | 94 | msgstr "Заголовок должен иметь меньше %s символов" |
|
91 | 95 | |
|
92 |
#: forms.py:17 |
|
|
96 | #: forms.py:174 | |
|
93 | 97 | #, python-format |
|
94 | 98 | msgid "Text must have less than %s characters" |
|
95 | 99 | msgstr "Текст должен быть короче %s символов" |
|
96 | 100 | |
|
97 |
#: forms.py:19 |
|
|
101 | #: forms.py:194 | |
|
98 | 102 | msgid "Invalid URL" |
|
99 | 103 | msgstr "Неверный URL" |
|
100 | 104 | |
|
101 |
#: forms.py:21 |
|
|
105 | #: forms.py:215 | |
|
102 | 106 | msgid "Invalid additional thread list" |
|
103 | 107 | msgstr "Неверный список дополнительных тем" |
|
104 | 108 | |
|
105 |
#: forms.py:25 |
|
|
106 |
msgid "Either text or |
|
|
107 |
msgstr "Текст или |
|
|
109 | #: forms.py:251 | |
|
110 | msgid "Either text or file must be entered." | |
|
111 | msgstr "Текст или файл должны быть введены." | |
|
108 | 112 | |
|
109 |
#: forms.py:28 |
|
|
113 | #: forms.py:289 | |
|
110 | 114 | #, python-format |
|
111 |
msgid " |
|
|
112 |
msgstr " |
|
|
115 | msgid "File must be less than %s bytes" | |
|
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 | 119 | #: templates/boards/rss/post.html:10 templates/boards/tags.html:6 |
|
116 | 120 | msgid "Tags" |
|
117 | 121 | msgstr "Метки" |
|
118 | 122 | |
|
119 | 123 | #: forms.py:342 |
|
120 | 124 | msgid "Inappropriate characters in tags." |
|
121 | 125 | msgstr "Недопустимые символы в метках." |
|
122 | 126 | |
|
123 | 127 | #: forms.py:356 |
|
124 |
msgid "Need at least one |
|
|
125 |
msgstr "Нужн |
|
|
128 | msgid "Need at least one section." | |
|
129 | msgstr "Нужен хотя бы один раздел." | |
|
126 | 130 | |
|
127 |
#: forms.py:36 |
|
|
131 | #: forms.py:368 | |
|
128 | 132 | msgid "Theme" |
|
129 | 133 | msgstr "Тема" |
|
130 | 134 | |
|
131 |
#: forms.py:3 |
|
|
135 | #: forms.py:369 | |
|
136 | #| msgid "Image view mode" | |
|
132 | 137 | msgid "Image view mode" |
|
133 | 138 | msgstr "Режим просмотра изображений" |
|
134 | 139 | |
|
135 |
#: forms.py:37 |
|
|
140 | #: forms.py:370 | |
|
136 | 141 | msgid "User name" |
|
137 | 142 | msgstr "Имя пользователя" |
|
138 | 143 | |
|
139 |
#: forms.py:37 |
|
|
144 | #: forms.py:371 | |
|
140 | 145 | msgid "Time zone" |
|
141 | 146 | msgstr "Часовой пояс" |
|
142 | 147 | |
|
143 |
#: forms.py:37 |
|
|
148 | #: forms.py:377 | |
|
144 | 149 | msgid "Inappropriate characters." |
|
145 | 150 | msgstr "Недопустимые символы." |
|
146 | 151 | |
|
147 | 152 | #: templates/boards/404.html:6 |
|
148 | 153 | msgid "Not found" |
|
149 | 154 | msgstr "Не найдено" |
|
150 | 155 | |
|
151 | 156 | #: templates/boards/404.html:12 |
|
152 | 157 | msgid "This page does not exist" |
|
153 | 158 | msgstr "Этой страницы не существует" |
|
154 | 159 | |
|
155 | 160 | #: templates/boards/all_threads.html:35 |
|
156 | 161 | msgid "Related message" |
|
157 | 162 | msgstr "Связанное сообщение" |
|
158 | 163 | |
|
159 |
#: templates/boards/all_threads.html: |
|
|
164 | #: templates/boards/all_threads.html:71 | |
|
160 | 165 | msgid "Edit tag" |
|
161 | 166 | msgstr "Изменить метку" |
|
162 | 167 | |
|
163 |
#: templates/boards/all_threads.html: |
|
|
168 | #: templates/boards/all_threads.html:79 | |
|
164 | 169 | #, python-format |
|
165 | msgid "This tag has %(thread_count)s threads and %(post_count)s posts." | |
|
166 | msgstr "С этой меткой есть %(thread_count)s тем и %(post_count)s сообщений." | |
|
170 | msgid "" | |
|
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 | 182 | #: templates/boards/notifications.html:17 templates/search/search.html:26 |
|
170 | 183 | msgid "Previous page" |
|
171 | 184 | msgstr "Предыдущая страница" |
|
172 | 185 | |
|
173 |
#: templates/boards/all_threads.html: |
|
|
186 | #: templates/boards/all_threads.html:110 | |
|
174 | 187 | #, python-format |
|
175 | 188 | msgid "Skipped %(count)s replies. Open thread to see all replies." |
|
176 | 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 | 192 | #: templates/boards/notifications.html:27 templates/search/search.html:37 |
|
180 | 193 | msgid "Next page" |
|
181 | 194 | msgstr "Следующая страница" |
|
182 | 195 | |
|
183 |
#: templates/boards/all_threads.html:1 |
|
|
196 | #: templates/boards/all_threads.html:133 | |
|
184 | 197 | msgid "No threads exist. Create the first one!" |
|
185 | 198 | msgstr "Нет тем. Создайте первую!" |
|
186 | 199 | |
|
187 |
#: templates/boards/all_threads.html:1 |
|
|
200 | #: templates/boards/all_threads.html:139 | |
|
188 | 201 | msgid "Create new thread" |
|
189 | 202 | msgstr "Создать новую тему" |
|
190 | 203 | |
|
191 |
#: templates/boards/all_threads.html:1 |
|
|
204 | #: templates/boards/all_threads.html:144 templates/boards/preview.html:16 | |
|
192 | 205 | #: templates/boards/thread_normal.html:38 |
|
193 | 206 | msgid "Post" |
|
194 | 207 | msgstr "Отправить" |
|
195 | 208 | |
|
196 |
#: templates/boards/all_threads.html:1 |
|
|
209 | #: templates/boards/all_threads.html:149 | |
|
197 | 210 | msgid "Tags must be delimited by spaces. Text or image is required." |
|
198 | 211 | msgstr "" |
|
199 | 212 | "Метки должны быть разделены пробелами. Текст или изображение обязательны." |
|
200 | 213 | |
|
201 |
#: templates/boards/all_threads.html:1 |
|
|
202 |
#: templates/boards/th |
|
|
214 | #: templates/boards/all_threads.html:151 templates/boards/preview.html:6 | |
|
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 | 221 | msgid "Text syntax" |
|
204 | 222 | msgstr "Синтаксис текста" |
|
205 | 223 | |
|
206 |
#: templates/boards/all_threads.html:1 |
|
|
224 | #: templates/boards/all_threads.html:167 templates/boards/feed.html:53 | |
|
207 | 225 | msgid "Pages:" |
|
208 | 226 | msgstr "Страницы: " |
|
209 | 227 | |
|
210 | 228 | #: templates/boards/authors.html:6 templates/boards/authors.html.py:12 |
|
211 | 229 | msgid "Authors" |
|
212 | 230 | msgstr "Авторы" |
|
213 | 231 | |
|
214 | 232 | #: templates/boards/authors.html:26 |
|
215 | 233 | msgid "Distributed under the" |
|
216 | 234 | msgstr "Распространяется под" |
|
217 | 235 | |
|
218 | 236 | #: templates/boards/authors.html:28 |
|
219 | 237 | msgid "license" |
|
220 | 238 | msgstr "лицензией" |
|
221 | 239 | |
|
222 | 240 | #: templates/boards/authors.html:30 |
|
223 | 241 | msgid "Repository" |
|
224 | 242 | msgstr "Репозиторий" |
|
225 | 243 | |
|
226 | 244 | #: templates/boards/base.html:14 templates/boards/base.html.py:41 |
|
227 | 245 | msgid "Feed" |
|
228 | 246 | msgstr "Лента" |
|
229 | 247 | |
|
230 | 248 | #: templates/boards/base.html:31 |
|
231 | 249 | msgid "All threads" |
|
232 | 250 | msgstr "Все темы" |
|
233 | 251 | |
|
234 | 252 | #: templates/boards/base.html:37 |
|
235 | 253 | msgid "Add tags" |
|
236 | 254 | msgstr "Добавить метки" |
|
237 | 255 | |
|
238 | 256 | #: templates/boards/base.html:39 |
|
239 | 257 | msgid "Tag management" |
|
240 | 258 | msgstr "Управление метками" |
|
241 | 259 | |
|
242 | 260 | #: templates/boards/base.html:39 |
|
243 | 261 | msgid "tags" |
|
244 | 262 | msgstr "метки" |
|
245 | 263 | |
|
246 | 264 | #: templates/boards/base.html:40 |
|
247 | 265 | msgid "search" |
|
248 | 266 | msgstr "поиск" |
|
249 | 267 | |
|
250 | 268 | #: templates/boards/base.html:41 templates/boards/feed.html:11 |
|
251 | 269 | msgid "feed" |
|
252 | 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 | 281 | #: templates/boards/notifications.html:8 |
|
256 | 282 | msgid "Notifications" |
|
257 | 283 | msgstr "Уведомления" |
|
258 | 284 | |
|
259 |
#: templates/boards/base.html:5 |
|
|
285 | #: templates/boards/base.html:53 templates/boards/settings.html:8 | |
|
260 | 286 | msgid "Settings" |
|
261 | 287 | msgstr "Настройки" |
|
262 | 288 | |
|
263 |
#: templates/boards/base.html:6 |
|
|
289 | #: templates/boards/base.html:66 | |
|
264 | 290 | msgid "Admin" |
|
265 | 291 | msgstr "Администрирование" |
|
266 | 292 | |
|
267 |
#: templates/boards/base.html:6 |
|
|
293 | #: templates/boards/base.html:68 | |
|
268 | 294 | #, python-format |
|
269 | 295 | msgid "Speed: %(ppd)s posts per day" |
|
270 | 296 | msgstr "Скорость: %(ppd)s сообщений в день" |
|
271 | 297 | |
|
272 |
#: templates/boards/base.html: |
|
|
298 | #: templates/boards/base.html:70 | |
|
273 | 299 | msgid "Up" |
|
274 | 300 | msgstr "Вверх" |
|
275 | 301 | |
|
276 | 302 | #: templates/boards/feed.html:45 |
|
277 | 303 | msgid "No posts exist. Create the first one!" |
|
278 | 304 | msgstr "Нет сообщений. Создайте первое!" |
|
279 | 305 | |
|
280 |
#: templates/boards/post.html: |
|
|
306 | #: templates/boards/post.html:30 | |
|
281 | 307 | msgid "Open" |
|
282 | 308 | msgstr "Открыть" |
|
283 | 309 | |
|
284 |
#: templates/boards/post.html:2 |
|
|
310 | #: templates/boards/post.html:32 templates/boards/post.html.py:43 | |
|
285 | 311 | msgid "Reply" |
|
286 | 312 | msgstr "Ответить" |
|
287 | 313 | |
|
288 |
#: templates/boards/post.html:3 |
|
|
314 | #: templates/boards/post.html:38 | |
|
289 | 315 | msgid " in " |
|
290 | 316 | msgstr " в " |
|
291 | 317 | |
|
292 |
#: templates/boards/post.html:4 |
|
|
318 | #: templates/boards/post.html:48 | |
|
293 | 319 | msgid "Edit" |
|
294 | 320 | msgstr "Изменить" |
|
295 | 321 | |
|
296 |
#: templates/boards/post.html: |
|
|
322 | #: templates/boards/post.html:50 | |
|
297 | 323 | msgid "Edit thread" |
|
298 | 324 | msgstr "Изменить тему" |
|
299 | 325 | |
|
300 |
#: templates/boards/post.html: |
|
|
326 | #: templates/boards/post.html:97 | |
|
301 | 327 | msgid "Replies" |
|
302 | 328 | msgstr "Ответы" |
|
303 | 329 | |
|
304 |
#: templates/boards/post.html:9 |
|
|
330 | #: templates/boards/post.html:109 templates/boards/thread.html:34 | |
|
305 | 331 | msgid "messages" |
|
306 | 332 | msgstr "сообщений" |
|
307 | 333 | |
|
308 |
#: templates/boards/post.html: |
|
|
334 | #: templates/boards/post.html:110 templates/boards/thread.html:35 | |
|
309 | 335 | msgid "images" |
|
310 | 336 | msgstr "изображений" |
|
311 | 337 | |
|
312 | #: templates/boards/preview.html:6 templates/boards/staticpages/help.html:20 | |
|
313 | msgid "Preview" | |
|
314 | msgstr "Предпросмотр" | |
|
315 | ||
|
316 | 338 | #: templates/boards/rss/post.html:5 |
|
317 | 339 | msgid "Post image" |
|
318 | 340 | msgstr "Изображение сообщения" |
|
319 | 341 | |
|
320 |
#: templates/boards/settings.html:1 |
|
|
342 | #: templates/boards/settings.html:15 | |
|
321 | 343 | msgid "You are moderator." |
|
322 | 344 | msgstr "Вы модератор." |
|
323 | 345 | |
|
324 |
#: templates/boards/settings.html: |
|
|
346 | #: templates/boards/settings.html:19 | |
|
325 | 347 | msgid "Hidden tags:" |
|
326 | 348 | msgstr "Скрытые метки:" |
|
327 | 349 | |
|
328 |
#: templates/boards/settings.html:2 |
|
|
350 | #: templates/boards/settings.html:27 | |
|
329 | 351 | msgid "No hidden tags." |
|
330 | 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 | 362 | #: templates/boards/settings.html:37 |
|
333 | 363 | msgid "Save" |
|
334 | 364 | msgstr "Сохранить" |
|
335 | 365 | |
|
336 | 366 | #: templates/boards/staticpages/banned.html:6 |
|
337 | 367 | msgid "Banned" |
|
338 | 368 | msgstr "Заблокирован" |
|
339 | 369 | |
|
340 | 370 | #: templates/boards/staticpages/banned.html:11 |
|
341 | 371 | msgid "Your IP address has been banned. Contact the administrator" |
|
342 | 372 | msgstr "Ваш IP адрес был заблокирован. Свяжитесь с администратором" |
|
343 | 373 | |
|
344 | 374 | #: templates/boards/staticpages/help.html:6 |
|
345 | 375 | #: templates/boards/staticpages/help.html:10 |
|
346 | 376 | msgid "Syntax" |
|
347 | 377 | msgstr "Синтаксис" |
|
348 | 378 | |
|
349 | 379 | #: templates/boards/staticpages/help.html:11 |
|
350 | 380 | msgid "Italic text" |
|
351 | 381 | msgstr "Курсивный текст" |
|
352 | 382 | |
|
353 | 383 | #: templates/boards/staticpages/help.html:12 |
|
354 | 384 | msgid "Bold text" |
|
355 | 385 | msgstr "Полужирный текст" |
|
356 | 386 | |
|
357 | 387 | #: templates/boards/staticpages/help.html:13 |
|
358 | 388 | msgid "Spoiler" |
|
359 | 389 | msgstr "Спойлер" |
|
360 | 390 | |
|
361 | 391 | #: templates/boards/staticpages/help.html:14 |
|
362 | 392 | msgid "Link to a post" |
|
363 | 393 | msgstr "Ссылка на сообщение" |
|
364 | 394 | |
|
365 | 395 | #: templates/boards/staticpages/help.html:15 |
|
366 | 396 | msgid "Strikethrough text" |
|
367 | 397 | msgstr "Зачеркнутый текст" |
|
368 | 398 | |
|
369 | 399 | #: templates/boards/staticpages/help.html:16 |
|
370 | 400 | msgid "Comment" |
|
371 | 401 | msgstr "Комментарий" |
|
372 | 402 | |
|
373 | 403 | #: templates/boards/staticpages/help.html:17 |
|
374 | 404 | #: templates/boards/staticpages/help.html:18 |
|
375 | 405 | msgid "Quote" |
|
376 | 406 | msgstr "Цитата" |
|
377 | 407 | |
|
378 |
#: templates/boards/staticpages/help.html:2 |
|
|
408 | #: templates/boards/staticpages/help.html:21 | |
|
379 | 409 | msgid "You can try pasting the text and previewing the result here:" |
|
380 | 410 | msgstr "Вы можете попробовать вставить текст и проверить результат здесь:" |
|
381 | 411 | |
|
382 |
#: templates/boards/tags.html: |
|
|
383 | msgid "No tags found." | |
|
384 |
msgstr " |
|
|
412 | #: templates/boards/tags.html:17 | |
|
413 | msgid "Sections:" | |
|
414 | msgstr "Разделы:" | |
|
385 | 415 | |
|
386 |
#: templates/boards/tags.html: |
|
|
387 |
msgid " |
|
|
388 |
msgstr " |
|
|
416 | #: templates/boards/tags.html:30 | |
|
417 | msgid "Other tags:" | |
|
418 | msgstr "Другие метки:" | |
|
419 | ||
|
420 | #: templates/boards/tags.html:43 | |
|
421 | msgid "All tags..." | |
|
422 | msgstr "Все метки..." | |
|
389 | 423 | |
|
390 | 424 | #: templates/boards/thread.html:15 |
|
391 | #| msgid "Normal mode" | |
|
392 | 425 | msgid "Normal" |
|
393 | 426 | msgstr "Нормальный" |
|
394 | 427 | |
|
395 | 428 | #: templates/boards/thread.html:16 |
|
396 | #| msgid "Gallery mode" | |
|
397 | 429 | msgid "Gallery" |
|
398 | 430 | msgstr "Галерея" |
|
399 | 431 | |
|
400 | 432 | #: templates/boards/thread.html:17 |
|
401 | #| msgid "Tree mode" | |
|
402 | 433 | msgid "Tree" |
|
403 | 434 | msgstr "Дерево" |
|
404 | 435 | |
|
405 |
#: templates/boards/thread.html:3 |
|
|
436 | #: templates/boards/thread.html:36 | |
|
406 | 437 | msgid "Last update: " |
|
407 | 438 | msgstr "Последнее обновление: " |
|
408 | 439 | |
|
409 | 440 | #: templates/boards/thread_gallery.html:36 |
|
410 | 441 | msgid "No images." |
|
411 | 442 | msgstr "Нет изображений." |
|
412 | 443 | |
|
413 | 444 | #: templates/boards/thread_normal.html:17 |
|
414 | 445 | msgid "posts to bumplimit" |
|
415 | 446 | msgstr "сообщений до бамплимита" |
|
416 | 447 | |
|
417 | 448 | #: templates/boards/thread_normal.html:31 |
|
418 | 449 | msgid "Reply to thread" |
|
419 | 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 | 457 | msgid "Close form" |
|
423 | 458 | msgstr "Закрыть форму" |
|
424 | 459 | |
|
425 | #: templates/boards/thread_normal.html:58 | |
|
426 | msgid "Update" | |
|
427 | msgstr "Обновить" | |
|
428 | ||
|
429 | 460 | #: templates/search/search.html:17 |
|
430 | 461 | msgid "Ok" |
|
431 | 462 | msgstr "Ок" |
@@ -1,30 +1,46 b'' | |||
|
1 | 1 | import os |
|
2 | 2 | |
|
3 | 3 | from django.core.management import BaseCommand |
|
4 | 4 | from django.db import transaction |
|
5 | from boards.models import Attachment | |
|
6 | from boards.models.attachment import FILES_DIRECTORY | |
|
5 | 7 | |
|
6 | 8 | from boards.models.image import IMAGES_DIRECTORY, PostImage, IMAGE_THUMB_SIZE |
|
7 | 9 | from neboard.settings import MEDIA_ROOT |
|
8 | 10 | |
|
9 | 11 | |
|
10 | 12 | __author__ = 'neko259' |
|
11 | 13 | |
|
12 | 14 | |
|
13 | 15 | class Command(BaseCommand): |
|
14 |
help = 'Remove |
|
|
16 | help = 'Remove files whose models were deleted' | |
|
15 | 17 | |
|
16 | 18 | @transaction.atomic |
|
17 | 19 | def handle(self, *args, **options): |
|
18 | 20 | count = 0 |
|
19 | 21 | thumb_prefix = '.{}x{}'.format(*IMAGE_THUMB_SIZE) |
|
20 | 22 | |
|
21 | 23 | model_files = os.listdir(MEDIA_ROOT + IMAGES_DIRECTORY) |
|
22 | 24 | for file in model_files: |
|
23 | 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 | 29 | if not found: |
|
27 | 30 | print('Missing {}'.format(image_name)) |
|
28 | 31 | os.remove(MEDIA_ROOT + IMAGES_DIRECTORY + file) |
|
29 | 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)) |
@@ -1,230 +1,230 b'' | |||
|
1 | 1 | # coding=utf-8 |
|
2 | 2 | |
|
3 | 3 | import re |
|
4 | 4 | import bbcode |
|
5 | 5 | |
|
6 | 6 | from urllib.parse import unquote |
|
7 | 7 | |
|
8 | 8 | from django.core.exceptions import ObjectDoesNotExist |
|
9 | 9 | from django.core.urlresolvers import reverse |
|
10 | 10 | |
|
11 | 11 | import boards |
|
12 | 12 | |
|
13 | 13 | |
|
14 | 14 | __author__ = 'neko259' |
|
15 | 15 | |
|
16 | 16 | |
|
17 | 17 | REFLINK_PATTERN = re.compile(r'^\d+$') |
|
18 | 18 | MULTI_NEWLINES_PATTERN = re.compile(r'(\r?\n){2,}') |
|
19 | 19 | ONE_NEWLINE = '\n' |
|
20 | 20 | REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?') |
|
21 | 21 | LINE_BREAK_HTML = '<div class="br"></div>' |
|
22 | 22 | |
|
23 | 23 | |
|
24 | 24 | class TextFormatter(): |
|
25 | 25 | """ |
|
26 | 26 | An interface for formatter that can be used in the text format panel |
|
27 | 27 | """ |
|
28 | 28 | |
|
29 | 29 | def __init__(self): |
|
30 | 30 | pass |
|
31 | 31 | |
|
32 | 32 | name = '' |
|
33 | 33 | |
|
34 | 34 | # Left and right tags for the button preview |
|
35 | 35 | preview_left = '' |
|
36 | 36 | preview_right = '' |
|
37 | 37 | |
|
38 | 38 | # Left and right characters for the textarea input |
|
39 | 39 | format_left = '' |
|
40 | 40 | format_right = '' |
|
41 | 41 | |
|
42 | 42 | |
|
43 | 43 | class AutolinkPattern(): |
|
44 | 44 | def handleMatch(self, m): |
|
45 | 45 | link_element = etree.Element('a') |
|
46 | 46 | href = m.group(2) |
|
47 | 47 | link_element.set('href', href) |
|
48 | 48 | link_element.text = href |
|
49 | 49 | |
|
50 | 50 | return link_element |
|
51 | 51 | |
|
52 | 52 | |
|
53 | 53 | class QuotePattern(TextFormatter): |
|
54 | 54 | name = '>q' |
|
55 | 55 | preview_left = '<span class="quote">' |
|
56 | 56 | preview_right = '</span>' |
|
57 | 57 | |
|
58 | 58 | format_left = '[quote]' |
|
59 | 59 | format_right = '[/quote]' |
|
60 | 60 | |
|
61 | 61 | |
|
62 | 62 | class SpoilerPattern(TextFormatter): |
|
63 | 63 | name = 'spoiler' |
|
64 | 64 | preview_left = '<span class="spoiler">' |
|
65 | 65 | preview_right = '</span>' |
|
66 | 66 | |
|
67 | 67 | format_left = '[spoiler]' |
|
68 | 68 | format_right = '[/spoiler]' |
|
69 | 69 | |
|
70 | 70 | def handleMatch(self, m): |
|
71 | 71 | quote_element = etree.Element('span') |
|
72 | 72 | quote_element.set('class', 'spoiler') |
|
73 | 73 | quote_element.text = m.group(2) |
|
74 | 74 | |
|
75 | 75 | return quote_element |
|
76 | 76 | |
|
77 | 77 | |
|
78 | 78 | class CommentPattern(TextFormatter): |
|
79 | 79 | name = '' |
|
80 | 80 | preview_left = '<span class="comment">// ' |
|
81 | 81 | preview_right = '</span>' |
|
82 | 82 | |
|
83 | 83 | format_left = '[comment]' |
|
84 | 84 | format_right = '[/comment]' |
|
85 | 85 | |
|
86 | 86 | |
|
87 | 87 | # TODO Use <s> tag here |
|
88 | 88 | class StrikeThroughPattern(TextFormatter): |
|
89 | 89 | name = 's' |
|
90 | 90 | preview_left = '<span class="strikethrough">' |
|
91 | 91 | preview_right = '</span>' |
|
92 | 92 | |
|
93 | 93 | format_left = '[s]' |
|
94 | 94 | format_right = '[/s]' |
|
95 | 95 | |
|
96 | 96 | |
|
97 | 97 | class ItalicPattern(TextFormatter): |
|
98 | 98 | name = 'i' |
|
99 | 99 | preview_left = '<i>' |
|
100 | 100 | preview_right = '</i>' |
|
101 | 101 | |
|
102 | 102 | format_left = '[i]' |
|
103 | 103 | format_right = '[/i]' |
|
104 | 104 | |
|
105 | 105 | |
|
106 | 106 | class BoldPattern(TextFormatter): |
|
107 | 107 | name = 'b' |
|
108 | 108 | preview_left = '<b>' |
|
109 | 109 | preview_right = '</b>' |
|
110 | 110 | |
|
111 | 111 | format_left = '[b]' |
|
112 | 112 | format_right = '[/b]' |
|
113 | 113 | |
|
114 | 114 | |
|
115 | 115 | class CodePattern(TextFormatter): |
|
116 | 116 | name = 'code' |
|
117 | 117 | preview_left = '<code>' |
|
118 | 118 | preview_right = '</code>' |
|
119 | 119 | |
|
120 | 120 | format_left = '[code]' |
|
121 | 121 | format_right = '[/code]' |
|
122 | 122 | |
|
123 | 123 | |
|
124 | 124 | def render_reflink(tag_name, value, options, parent, context): |
|
125 | 125 | result = '>>%s' % value |
|
126 | 126 | |
|
127 | 127 | if REFLINK_PATTERN.match(value): |
|
128 | 128 | post_id = int(value) |
|
129 | 129 | |
|
130 | 130 | try: |
|
131 | 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 | 134 | except ObjectDoesNotExist: |
|
135 | 135 | pass |
|
136 | 136 | |
|
137 | 137 | return result |
|
138 | 138 | |
|
139 | 139 | |
|
140 | 140 | def render_quote(tag_name, value, options, parent, context): |
|
141 | 141 | source = '' |
|
142 | 142 | if 'source' in options: |
|
143 | 143 | source = options['source'] |
|
144 | 144 | |
|
145 | 145 | if source: |
|
146 | 146 | result = '<div class="multiquote"><div class="quote-header">%s</div><div class="quote-text">%s</div></div>' % (source, value) |
|
147 | 147 | else: |
|
148 | 148 | # Insert a ">" at the start of every line |
|
149 | 149 | result = '<span class="quote">>{}</span>'.format( |
|
150 | 150 | value.replace(LINE_BREAK_HTML, |
|
151 | 151 | '{}>'.format(LINE_BREAK_HTML))) |
|
152 | 152 | |
|
153 | 153 | return result |
|
154 | 154 | |
|
155 | 155 | |
|
156 | 156 | def render_notification(tag_name, value, options, parent, content): |
|
157 | 157 | username = value.lower() |
|
158 | 158 | |
|
159 | 159 | return '<a href="{}" class="user-cast">@{}</a>'.format( |
|
160 | 160 | reverse('notifications', kwargs={'username': username}), username) |
|
161 | 161 | |
|
162 | 162 | |
|
163 | 163 | def render_tag(tag_name, value, options, parent, context): |
|
164 | 164 | tag_name = value.lower() |
|
165 | 165 | |
|
166 | 166 | try: |
|
167 | 167 | url = boards.models.Tag.objects.get(name=tag_name).get_view() |
|
168 | 168 | except ObjectDoesNotExist: |
|
169 | 169 | url = tag_name |
|
170 | 170 | |
|
171 | 171 | return url |
|
172 | 172 | |
|
173 | 173 | |
|
174 | 174 | formatters = [ |
|
175 | 175 | QuotePattern, |
|
176 | 176 | SpoilerPattern, |
|
177 | 177 | ItalicPattern, |
|
178 | 178 | BoldPattern, |
|
179 | 179 | CommentPattern, |
|
180 | 180 | StrikeThroughPattern, |
|
181 | 181 | CodePattern, |
|
182 | 182 | ] |
|
183 | 183 | |
|
184 | 184 | |
|
185 | 185 | PREPARSE_PATTERNS = { |
|
186 | 186 | r'(?<!>)>>(\d+)': r'[post]\1[/post]', # Reflink ">>123" |
|
187 | 187 | r'^>([^>].+)': r'[quote]\1[/quote]', # Quote ">text" |
|
188 | 188 | r'^//(.+)': r'[comment]\1[/comment]', # Comment "//text" |
|
189 | 189 | r'\B@(\w+)': r'[user]\1[/user]', # User notification "@user" |
|
190 | 190 | } |
|
191 | 191 | |
|
192 | 192 | |
|
193 | 193 | class Parser: |
|
194 | 194 | def __init__(self): |
|
195 | 195 | # The newline hack is added because br's margin does not work in all |
|
196 | 196 | # browsers except firefox, when the div's does. |
|
197 | 197 | self.parser = bbcode.Parser(newline=LINE_BREAK_HTML) |
|
198 | 198 | |
|
199 | 199 | self.parser.add_formatter('post', render_reflink, strip=True) |
|
200 | 200 | self.parser.add_formatter('quote', render_quote, strip=True) |
|
201 | 201 | self.parser.add_formatter('user', render_notification, strip=True) |
|
202 | 202 | self.parser.add_formatter('tag', render_tag, strip=True) |
|
203 | 203 | self.parser.add_simple_formatter( |
|
204 | 204 | 'comment', '<span class="comment">//%(value)s</span>') |
|
205 | 205 | self.parser.add_simple_formatter( |
|
206 | 206 | 'spoiler', '<span class="spoiler">%(value)s</span>') |
|
207 | 207 | self.parser.add_simple_formatter( |
|
208 | 208 | 's', '<span class="strikethrough">%(value)s</span>') |
|
209 | 209 | # TODO Why not use built-in tag? |
|
210 | 210 | self.parser.add_simple_formatter('code', |
|
211 | 211 | '<pre><code>%(value)s</pre></code>', |
|
212 | 212 | render_embedded=False) |
|
213 | 213 | |
|
214 | 214 | def preparse(self, text): |
|
215 | 215 | """ |
|
216 | 216 | Performs manual parsing before the bbcode parser is used. |
|
217 | 217 | Preparsed text is saved as raw and the text before preparsing is lost. |
|
218 | 218 | """ |
|
219 | 219 | new_text = MULTI_NEWLINES_PATTERN.sub(ONE_NEWLINE, text) |
|
220 | 220 | |
|
221 | 221 | for key, value in PREPARSE_PATTERNS.items(): |
|
222 | 222 | new_text = re.sub(key, value, new_text, flags=re.MULTILINE) |
|
223 | 223 | |
|
224 | 224 | for link in REGEX_URL.findall(text): |
|
225 | 225 | new_text = new_text.replace(link, unquote(link)) |
|
226 | 226 | |
|
227 | 227 | return new_text |
|
228 | 228 | |
|
229 | 229 | def parse(self, text): |
|
230 | 230 | return self.parser.format(text) |
@@ -1,10 +1,11 b'' | |||
|
1 | 1 | __author__ = 'neko259' |
|
2 | 2 | |
|
3 | 3 | from boards.models.signature import GlobalId, Signature |
|
4 | 4 | from boards.models.sync_key import KeyPair |
|
5 | 5 | from boards.models.image import PostImage |
|
6 | from boards.models.attachment import Attachment | |
|
6 | 7 | from boards.models.thread import Thread |
|
7 | 8 | from boards.models.post import Post |
|
8 | 9 | from boards.models.tag import Tag |
|
9 | 10 | from boards.models.user import Ban |
|
10 | 11 | from boards.models.banner import Banner |
@@ -1,105 +1,114 b'' | |||
|
1 | 1 | import hashlib |
|
2 | 2 | import os |
|
3 | 3 | from random import random |
|
4 | 4 | import time |
|
5 | ||
|
5 | 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 | 11 | from boards.models.base import Viewable |
|
8 | 12 | |
|
9 | 13 | __author__ = 'neko259' |
|
10 | 14 | |
|
11 | 15 | |
|
12 | 16 | IMAGE_THUMB_SIZE = (200, 150) |
|
13 | 17 | IMAGES_DIRECTORY = 'images/' |
|
14 | 18 | FILE_EXTENSION_DELIMITER = '.' |
|
15 | 19 | HASH_LENGTH = 36 |
|
16 | 20 | |
|
17 | 21 | CSS_CLASS_IMAGE = 'image' |
|
18 | 22 | CSS_CLASS_THUMB = 'thumb' |
|
19 | 23 | |
|
20 | 24 | |
|
21 | 25 | class PostImageManager(models.Manager): |
|
22 | 26 | def create_with_hash(self, image): |
|
23 |
image_hash = s |
|
|
27 | image_hash = utils.get_file_hash(image) | |
|
24 | 28 | existing = self.filter(hash=image_hash) |
|
25 | 29 | if len(existing) > 0: |
|
26 | 30 | post_image = existing[0] |
|
27 | 31 | else: |
|
28 | 32 | post_image = PostImage.objects.create(image=image) |
|
29 | 33 | |
|
30 | 34 | return post_image |
|
31 | 35 | |
|
32 | def get_hash(self, image): | |
|
33 | """ | |
|
34 | Gets hash of an image. | |
|
35 | """ | |
|
36 | md5 = hashlib.md5() | |
|
37 | for chunk in image.chunks(): | |
|
38 | md5.update(chunk) | |
|
39 | return md5.hexdigest() | |
|
36 | def get_random_images(self, count, include_archived=False, tags=None): | |
|
37 | images = self.filter(post_images__thread__archived=include_archived) | |
|
38 | if tags is not None: | |
|
39 | images = images.filter(post_images__threads__tags__in=tags) | |
|
40 | return images.order_by('?')[:count] | |
|
40 | 41 | |
|
41 | 42 | |
|
42 | 43 | class PostImage(models.Model, Viewable): |
|
43 | 44 | objects = PostImageManager() |
|
44 | 45 | |
|
45 | 46 | class Meta: |
|
46 | 47 | app_label = 'boards' |
|
47 | 48 | ordering = ('id',) |
|
48 | 49 | |
|
49 | 50 | def _update_image_filename(self, filename): |
|
50 | 51 | """ |
|
51 | 52 | Gets unique image filename |
|
52 | 53 | """ |
|
53 | 54 | |
|
54 | path = IMAGES_DIRECTORY | |
|
55 | ||
|
56 | 55 | # TODO Use something other than random number in file name |
|
57 | 56 | new_name = '{}{}.{}'.format( |
|
58 | 57 | str(int(time.mktime(time.gmtime()))), |
|
59 | 58 | str(int(random() * 1000)), |
|
60 | 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 | 63 | width = models.IntegerField(default=0) |
|
65 | 64 | height = models.IntegerField(default=0) |
|
66 | 65 | |
|
67 | 66 | pre_width = models.IntegerField(default=0) |
|
68 | 67 | pre_height = models.IntegerField(default=0) |
|
69 | 68 | |
|
70 | 69 | image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename, |
|
71 | 70 | blank=True, sizes=(IMAGE_THUMB_SIZE,), |
|
72 | 71 | width_field='width', |
|
73 | 72 | height_field='height', |
|
74 | 73 | preview_width_field='pre_width', |
|
75 | 74 | preview_height_field='pre_height') |
|
76 | 75 | hash = models.CharField(max_length=HASH_LENGTH) |
|
77 | 76 | |
|
78 | 77 | def save(self, *args, **kwargs): |
|
79 | 78 | """ |
|
80 | 79 | Saves the model and computes the image hash for deduplication purposes. |
|
81 | 80 | """ |
|
82 | 81 | |
|
83 | 82 | if not self.pk and self.image: |
|
84 |
self.hash = |
|
|
83 | self.hash = utils.get_file_hash(self.image) | |
|
85 | 84 | super(PostImage, self).save(*args, **kwargs) |
|
86 | 85 | |
|
87 | 86 | def __str__(self): |
|
88 | 87 | return self.image.url |
|
89 | 88 | |
|
90 | 89 | def get_view(self): |
|
90 | metadata = '{}, {}'.format(self.image.name.split('.')[-1], | |
|
91 | filesizeformat(self.image.size)) | |
|
91 | 92 | return '<div class="{}">' \ |
|
92 | 93 | '<a class="{}" href="{full}">' \ |
|
93 | 94 | '<img class="post-image-preview"' \ |
|
94 | 95 | ' src="{}"' \ |
|
95 | 96 | ' alt="{}"' \ |
|
96 | 97 | ' width="{}"' \ |
|
97 | 98 | ' height="{}"' \ |
|
98 | 99 | ' data-width="{}"' \ |
|
99 | 100 | ' data-height="{}" />' \ |
|
100 | 101 | '</a>' \ |
|
102 | '<div class="image-metadata">'\ | |
|
103 | '<a href="{full}" download>{image_meta}</a>'\ | |
|
104 | '</div>' \ | |
|
101 | 105 | '</div>'\ |
|
102 | 106 | .format(CSS_CLASS_IMAGE, CSS_CLASS_THUMB, |
|
103 | 107 | self.image.url_200x150, |
|
104 | 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,401 +1,425 b'' | |||
|
1 | from datetime import datetime, timedelta, date | |
|
2 | from datetime import time as dtime | |
|
1 | 3 | import logging |
|
2 | 4 | import re |
|
3 | 5 | import uuid |
|
4 | 6 | |
|
5 | 7 | from django.core.exceptions import ObjectDoesNotExist |
|
6 | 8 | from django.core.urlresolvers import reverse |
|
7 | 9 | from django.db import models |
|
8 | 10 | from django.db.models import TextField, QuerySet |
|
9 | ||
|
10 | 11 | from django.template.loader import render_to_string |
|
11 | ||
|
12 | 12 | from django.utils import timezone |
|
13 | 13 | |
|
14 | from boards.abstracts.tripcode import Tripcode | |
|
14 | 15 | from boards.mdx_neboard import Parser |
|
15 | 16 | from boards.models import KeyPair, GlobalId |
|
16 | 17 | from boards import settings |
|
17 | from boards.models import PostImage | |
|
18 | from boards.models import PostImage, Attachment | |
|
18 | 19 | from boards.models.base import Viewable |
|
19 | 20 | from boards.models.post.export import get_exporter, DIFF_TYPE_JSON |
|
20 | 21 | from boards.models.post.manager import PostManager |
|
21 | 22 | from boards.models.user import Notification |
|
22 | 23 | |
|
23 | 24 | WS_NOTIFICATION_TYPE_NEW_POST = 'new_post' |
|
24 | 25 | WS_NOTIFICATION_TYPE = 'notification_type' |
|
25 | 26 | |
|
26 | 27 | WS_CHANNEL_THREAD = "thread:" |
|
27 | 28 | |
|
28 | 29 | APP_LABEL_BOARDS = 'boards' |
|
29 | 30 | |
|
30 | 31 | BAN_REASON_AUTO = 'Auto' |
|
31 | 32 | |
|
32 | 33 | IMAGE_THUMB_SIZE = (200, 150) |
|
33 | 34 | |
|
34 | 35 | TITLE_MAX_LENGTH = 200 |
|
35 | 36 | |
|
36 | 37 | REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]') |
|
37 | 38 | REGEX_GLOBAL_REPLY = re.compile(r'\[post\](\w+)::([^:]+)::(\d+)\[/post\]') |
|
38 | 39 | REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?') |
|
39 | 40 | REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]') |
|
40 | 41 | |
|
41 | 42 | PARAMETER_TRUNCATED = 'truncated' |
|
42 | 43 | PARAMETER_TAG = 'tag' |
|
43 | 44 | PARAMETER_OFFSET = 'offset' |
|
44 | 45 | PARAMETER_DIFF_TYPE = 'type' |
|
45 | 46 | PARAMETER_CSS_CLASS = 'css_class' |
|
46 | 47 | PARAMETER_THREAD = 'thread' |
|
47 | 48 | PARAMETER_IS_OPENING = 'is_opening' |
|
48 | 49 | PARAMETER_MODERATOR = 'moderator' |
|
49 | 50 | PARAMETER_POST = 'post' |
|
50 | 51 | PARAMETER_OP_ID = 'opening_post_id' |
|
51 | 52 | PARAMETER_NEED_OPEN_LINK = 'need_open_link' |
|
52 | 53 | PARAMETER_REPLY_LINK = 'reply_link' |
|
53 | 54 | PARAMETER_NEED_OP_DATA = 'need_op_data' |
|
54 | 55 | |
|
55 | 56 | POST_VIEW_PARAMS = ( |
|
56 | 57 | 'need_op_data', |
|
57 | 58 | 'reply_link', |
|
58 | 59 | 'moderator', |
|
59 | 60 | 'need_open_link', |
|
60 | 61 | 'truncated', |
|
61 | 62 | 'mode_tree', |
|
62 | 63 | ) |
|
63 | 64 | |
|
64 | REFMAP_STR = '<a href="{}">>>{}</a>' | |
|
65 | ||
|
66 | 65 | |
|
67 | 66 | class Post(models.Model, Viewable): |
|
68 | 67 | """A post is a message.""" |
|
69 | 68 | |
|
70 | 69 | objects = PostManager() |
|
71 | 70 | |
|
72 | 71 | class Meta: |
|
73 | 72 | app_label = APP_LABEL_BOARDS |
|
74 | 73 | ordering = ('id',) |
|
75 | 74 | |
|
76 | 75 | title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True) |
|
77 | 76 | pub_time = models.DateTimeField() |
|
78 | 77 | text = TextField(blank=True, null=True) |
|
79 | 78 | _text_rendered = TextField(blank=True, null=True, editable=False) |
|
80 | 79 | |
|
81 | 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 | 85 | poster_ip = models.GenericIPAddressField() |
|
85 | 86 | |
|
86 | 87 | # TODO This field can be removed cause UID is used for update now |
|
87 | 88 | last_edit_time = models.DateTimeField() |
|
88 | 89 | |
|
89 | 90 | referenced_posts = models.ManyToManyField('Post', symmetrical=False, |
|
90 | 91 | null=True, |
|
91 | 92 | blank=True, related_name='refposts', |
|
92 | 93 | db_index=True) |
|
93 | 94 | refmap = models.TextField(null=True, blank=True) |
|
94 | 95 | threads = models.ManyToManyField('Thread', db_index=True) |
|
95 | 96 | thread = models.ForeignKey('Thread', db_index=True, related_name='pt+') |
|
96 | 97 | |
|
97 | 98 | url = models.TextField() |
|
98 | 99 | uid = models.TextField(db_index=True) |
|
99 | 100 | |
|
100 | 101 | # Global ID with author key. If the message was downloaded from another |
|
101 | 102 | # server, this indicates the server. |
|
102 | 103 | global_id = models.OneToOneField('GlobalId', null=True, blank=True) |
|
103 | 104 | |
|
105 | tripcode = models.CharField(max_length=50, null=True) | |
|
106 | ||
|
104 | 107 | def __str__(self): |
|
105 | 108 | return 'P#{}/{}'.format(self.id, self.title) |
|
106 | 109 | |
|
107 | 110 | def get_referenced_posts(self): |
|
108 | 111 | threads = self.get_threads().all() |
|
109 | 112 | return self.referenced_posts.filter(threads__in=threads) \ |
|
110 | 113 | .order_by('pub_time').distinct().all() |
|
111 | 114 | |
|
112 | 115 | def get_title(self) -> str: |
|
113 | 116 | """ |
|
114 | 117 | Gets original post title or part of its text. |
|
115 | 118 | """ |
|
116 | 119 | |
|
117 | 120 | title = self.title |
|
118 | 121 | if not title: |
|
119 | 122 | title = self.get_text() |
|
120 | 123 | |
|
121 | 124 | return title |
|
122 | 125 | |
|
123 | 126 | def build_refmap(self) -> None: |
|
124 | 127 | """ |
|
125 | 128 | Builds a replies map string from replies list. This is a cache to stop |
|
126 | 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 | 133 | for refpost in self.referenced_posts.all()] |
|
131 | 134 | |
|
132 | 135 | self.refmap = ', '.join(post_urls) |
|
133 | 136 | |
|
134 | 137 | def is_referenced(self) -> bool: |
|
135 | 138 | return self.refmap and len(self.refmap) > 0 |
|
136 | 139 | |
|
137 | 140 | def is_opening(self) -> bool: |
|
138 | 141 | """ |
|
139 | 142 | Checks if this is an opening post or just a reply. |
|
140 | 143 | """ |
|
141 | 144 | |
|
142 | 145 | return self.get_thread().get_opening_post_id() == self.id |
|
143 | 146 | |
|
144 | 147 | def get_absolute_url(self): |
|
145 | 148 | if self.url: |
|
146 | 149 | return self.url |
|
147 | 150 | else: |
|
148 | 151 | opening_id = self.get_thread().get_opening_post_id() |
|
149 | 152 | post_url = reverse('thread', kwargs={'post_id': opening_id}) |
|
150 | 153 | if self.id != opening_id: |
|
151 | 154 | post_url += '#' + str(self.id) |
|
152 | 155 | return post_url |
|
153 | 156 | |
|
154 | 157 | def get_thread(self): |
|
155 | 158 | return self.thread |
|
156 | 159 | |
|
157 | 160 | def get_threads(self) -> QuerySet: |
|
158 | 161 | """ |
|
159 | 162 | Gets post's thread. |
|
160 | 163 | """ |
|
161 | 164 | |
|
162 | 165 | return self.threads |
|
163 | 166 | |
|
164 | 167 | def get_view(self, *args, **kwargs) -> str: |
|
165 | 168 | """ |
|
166 | 169 | Renders post's HTML view. Some of the post params can be passed over |
|
167 | 170 | kwargs for the means of caching (if we view the thread, some params |
|
168 | 171 | are same for every post and don't need to be computed over and over. |
|
169 | 172 | """ |
|
170 | 173 | |
|
171 | 174 | thread = self.get_thread() |
|
172 | 175 | is_opening = kwargs.get(PARAMETER_IS_OPENING, self.is_opening()) |
|
173 | 176 | |
|
174 | 177 | if is_opening: |
|
175 | 178 | opening_post_id = self.id |
|
176 | 179 | else: |
|
177 | 180 | opening_post_id = thread.get_opening_post_id() |
|
178 | 181 | |
|
179 | 182 | css_class = 'post' |
|
180 | 183 | if thread.archived: |
|
181 | 184 | css_class += ' archive_post' |
|
182 | 185 | elif not thread.can_bump(): |
|
183 | 186 | css_class += ' dead_post' |
|
184 | 187 | |
|
185 | 188 | params = dict() |
|
186 | 189 | for param in POST_VIEW_PARAMS: |
|
187 | 190 | if param in kwargs: |
|
188 | 191 | params[param] = kwargs[param] |
|
189 | 192 | |
|
190 | 193 | params.update({ |
|
191 | 194 | PARAMETER_POST: self, |
|
192 | 195 | PARAMETER_IS_OPENING: is_opening, |
|
193 | 196 | PARAMETER_THREAD: thread, |
|
194 | 197 | PARAMETER_CSS_CLASS: css_class, |
|
195 | 198 | PARAMETER_OP_ID: opening_post_id, |
|
196 | 199 | }) |
|
197 | 200 | |
|
198 | 201 | return render_to_string('boards/post.html', params) |
|
199 | 202 | |
|
200 | 203 | def get_search_view(self, *args, **kwargs): |
|
201 | 204 | return self.get_view(need_op_data=True, *args, **kwargs) |
|
202 | 205 | |
|
203 | 206 | def get_first_image(self) -> PostImage: |
|
204 | 207 | return self.images.earliest('id') |
|
205 | 208 | |
|
206 | 209 | def delete(self, using=None): |
|
207 | 210 | """ |
|
208 | 211 | Deletes all post images and the post itself. |
|
209 | 212 | """ |
|
210 | 213 | |
|
211 | 214 | for image in self.images.all(): |
|
212 |
image_refs_count = |
|
|
215 | image_refs_count = image.post_images.count() | |
|
213 | 216 | if image_refs_count == 1: |
|
214 | 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 | 224 | if self.global_id: |
|
217 | 225 | self.global_id.delete() |
|
218 | 226 | |
|
219 | 227 | thread = self.get_thread() |
|
220 | 228 | thread.last_edit_time = timezone.now() |
|
221 | 229 | thread.save() |
|
222 | 230 | |
|
223 | 231 | super(Post, self).delete(using) |
|
224 | 232 | |
|
225 | 233 | logging.getLogger('boards.post.delete').info( |
|
226 | 234 | 'Deleted post {}'.format(self)) |
|
227 | 235 | |
|
228 | 236 | def set_global_id(self, key_pair=None): |
|
229 | 237 | """ |
|
230 | 238 | Sets global id based on the given key pair. If no key pair is given, |
|
231 | 239 | default one is used. |
|
232 | 240 | """ |
|
233 | 241 | |
|
234 | 242 | if key_pair: |
|
235 | 243 | key = key_pair |
|
236 | 244 | else: |
|
237 | 245 | try: |
|
238 | 246 | key = KeyPair.objects.get(primary=True) |
|
239 | 247 | except KeyPair.DoesNotExist: |
|
240 | 248 | # Do not update the global id because there is no key defined |
|
241 | 249 | return |
|
242 | 250 | global_id = GlobalId(key_type=key.key_type, |
|
243 | 251 | key=key.public_key, |
|
244 | 252 | local_id=self.id) |
|
245 | 253 | global_id.save() |
|
246 | 254 | |
|
247 | 255 | self.global_id = global_id |
|
248 | 256 | |
|
249 | 257 | self.save(update_fields=['global_id']) |
|
250 | 258 | |
|
251 | 259 | def get_pub_time_str(self): |
|
252 | 260 | return str(self.pub_time) |
|
253 | 261 | |
|
254 | 262 | def get_replied_ids(self): |
|
255 | 263 | """ |
|
256 | 264 | Gets ID list of the posts that this post replies. |
|
257 | 265 | """ |
|
258 | 266 | |
|
259 | 267 | raw_text = self.get_raw_text() |
|
260 | 268 | |
|
261 | 269 | local_replied = REGEX_REPLY.findall(raw_text) |
|
262 | 270 | global_replied = [] |
|
263 | 271 | for match in REGEX_GLOBAL_REPLY.findall(raw_text): |
|
264 | 272 | key_type = match[0] |
|
265 | 273 | key = match[1] |
|
266 | 274 | local_id = match[2] |
|
267 | 275 | |
|
268 | 276 | try: |
|
269 | 277 | global_id = GlobalId.objects.get(key_type=key_type, |
|
270 | 278 | key=key, local_id=local_id) |
|
271 | 279 | for post in Post.objects.filter(global_id=global_id).only('id'): |
|
272 | 280 | global_replied.append(post.id) |
|
273 | 281 | except GlobalId.DoesNotExist: |
|
274 | 282 | pass |
|
275 | 283 | return local_replied + global_replied |
|
276 | 284 | |
|
277 | 285 | def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None, |
|
278 | 286 | include_last_update=False) -> str: |
|
279 | 287 | """ |
|
280 | 288 | Gets post HTML or JSON data that can be rendered on a page or used by |
|
281 | 289 | API. |
|
282 | 290 | """ |
|
283 | 291 | |
|
284 | 292 | return get_exporter(format_type).export(self, request, |
|
285 | 293 | include_last_update) |
|
286 | 294 | |
|
287 | 295 | def notify_clients(self, recursive=True): |
|
288 | 296 | """ |
|
289 | 297 | Sends post HTML data to the thread web socket. |
|
290 | 298 | """ |
|
291 | 299 | |
|
292 | 300 | if not settings.get_bool('External', 'WebsocketsEnabled'): |
|
293 | 301 | return |
|
294 | 302 | |
|
295 | 303 | thread_ids = list() |
|
296 | 304 | for thread in self.get_threads().all(): |
|
297 | 305 | thread_ids.append(thread.id) |
|
298 | 306 | |
|
299 | 307 | thread.notify_clients() |
|
300 | 308 | |
|
301 | 309 | if recursive: |
|
302 | 310 | for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()): |
|
303 | 311 | post_id = reply_number.group(1) |
|
304 | 312 | |
|
305 | 313 | try: |
|
306 | 314 | ref_post = Post.objects.get(id=post_id) |
|
307 | 315 | |
|
308 | 316 | if ref_post.get_threads().exclude(id__in=thread_ids).exists(): |
|
309 | 317 | # If post is in this thread, its thread was already notified. |
|
310 | 318 | # Otherwise, notify its thread separately. |
|
311 | 319 | ref_post.notify_clients(recursive=False) |
|
312 | 320 | except ObjectDoesNotExist: |
|
313 | 321 | pass |
|
314 | 322 | |
|
315 | 323 | def build_url(self): |
|
316 | 324 | self.url = self.get_absolute_url() |
|
317 | 325 | self.save(update_fields=['url']) |
|
318 | 326 | |
|
319 | 327 | def save(self, force_insert=False, force_update=False, using=None, |
|
320 | 328 | update_fields=None): |
|
321 | 329 | self._text_rendered = Parser().parse(self.get_raw_text()) |
|
322 | 330 | |
|
323 | 331 | self.uid = str(uuid.uuid4()) |
|
324 | 332 | if update_fields is not None and 'uid' not in update_fields: |
|
325 | 333 | update_fields += ['uid'] |
|
326 | 334 | |
|
327 | 335 | if self.id: |
|
328 | 336 | for thread in self.get_threads().all(): |
|
329 | 337 | thread.last_edit_time = self.last_edit_time |
|
330 | 338 | |
|
331 | 339 | thread.save(update_fields=['last_edit_time']) |
|
332 | 340 | |
|
333 | 341 | super().save(force_insert, force_update, using, update_fields) |
|
334 | 342 | |
|
335 | 343 | def get_text(self) -> str: |
|
336 | 344 | return self._text_rendered |
|
337 | 345 | |
|
338 | 346 | def get_raw_text(self) -> str: |
|
339 | 347 | return self.text |
|
340 | 348 | |
|
341 | 349 | def get_sync_text(self) -> str: |
|
342 | 350 | """ |
|
343 | 351 | Returns text applicable for sync. It has absolute post reflinks. |
|
344 | 352 | """ |
|
345 | 353 | |
|
346 | 354 | replacements = dict() |
|
347 | 355 | for post_id in REGEX_REPLY.findall(self.get_raw_text()): |
|
348 | 356 | absolute_post_id = str(Post.objects.get(id=post_id).global_id) |
|
349 | 357 | replacements[post_id] = absolute_post_id |
|
350 | 358 | |
|
351 | 359 | text = self.get_raw_text() |
|
352 | 360 | for key in replacements: |
|
353 | 361 | text = text.replace('[post]{}[/post]'.format(key), |
|
354 | 362 | '[post]{}[/post]'.format(replacements[key])) |
|
355 | 363 | |
|
356 | 364 | return text |
|
357 | 365 | |
|
358 | 366 | def get_absolute_id(self) -> str: |
|
359 | 367 | """ |
|
360 | 368 | If the post has many threads, shows its main thread OP id in the post |
|
361 | 369 | ID. |
|
362 | 370 | """ |
|
363 | 371 | |
|
364 | 372 | if self.get_threads().count() > 1: |
|
365 | 373 | return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id) |
|
366 | 374 | else: |
|
367 | 375 | return str(self.id) |
|
368 | 376 | |
|
369 | 377 | def connect_notifications(self): |
|
370 | 378 | for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()): |
|
371 | 379 | user_name = reply_number.group(1).lower() |
|
372 | 380 | Notification.objects.get_or_create(name=user_name, post=self) |
|
373 | 381 | |
|
374 | 382 | def connect_replies(self): |
|
375 | 383 | """ |
|
376 | 384 | Connects replies to a post to show them as a reflink map |
|
377 | 385 | """ |
|
378 | 386 | |
|
379 | 387 | for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()): |
|
380 | 388 | post_id = reply_number.group(1) |
|
381 | 389 | |
|
382 | 390 | try: |
|
383 | 391 | referenced_post = Post.objects.get(id=post_id) |
|
384 | 392 | |
|
385 | 393 | referenced_post.referenced_posts.add(self) |
|
386 | 394 | referenced_post.last_edit_time = self.pub_time |
|
387 | 395 | referenced_post.build_refmap() |
|
388 | 396 | referenced_post.save(update_fields=['refmap', 'last_edit_time']) |
|
389 | 397 | except ObjectDoesNotExist: |
|
390 | 398 | pass |
|
391 | 399 | |
|
392 | 400 | def connect_threads(self, opening_posts): |
|
393 | 401 | for opening_post in opening_posts: |
|
394 | 402 | threads = opening_post.get_threads().all() |
|
395 | 403 | for thread in threads: |
|
396 | 404 | if thread.can_bump(): |
|
397 | 405 | thread.update_bump_status() |
|
398 | 406 | |
|
399 | 407 | thread.last_edit_time = self.last_edit_time |
|
400 | 408 | thread.save(update_fields=['last_edit_time', 'bumpable']) |
|
401 | 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 |
@@ -1,128 +1,140 b'' | |||
|
1 | 1 | from datetime import datetime, timedelta, date |
|
2 | 2 | from datetime import time as dtime |
|
3 | 3 | import logging |
|
4 | 4 | from django.db import models, transaction |
|
5 | 5 | from django.utils import timezone |
|
6 | 6 | from boards import utils |
|
7 | 7 | from boards.mdx_neboard import Parser |
|
8 | from boards.models import PostImage | |
|
8 | from boards.models import PostImage, Attachment | |
|
9 | 9 | import boards.models |
|
10 | 10 | |
|
11 | 11 | __author__ = 'vurdalak' |
|
12 | 12 | |
|
13 | 13 | |
|
14 | 14 | NO_IP = '0.0.0.0' |
|
15 | 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 | 26 | class PostManager(models.Manager): |
|
19 | 27 | @transaction.atomic |
|
20 |
def create_post(self, title: str, text: str, |
|
|
21 | ip=NO_IP, tags: list=None, opening_posts: list=None): | |
|
28 | def create_post(self, title: str, text: str, file=None, thread=None, | |
|
29 | ip=NO_IP, tags: list=None, opening_posts: list=None, tripcode=None): | |
|
22 | 30 | """ |
|
23 | 31 | Creates new post |
|
24 | 32 | """ |
|
25 | 33 | |
|
26 | 34 | is_banned = boards.models.Ban.objects.filter(ip=ip).exists() |
|
27 | 35 | |
|
28 | 36 | # TODO Raise specific exception and catch it in the views |
|
29 | 37 | if is_banned: |
|
30 | 38 | raise Exception("This user is banned") |
|
31 | 39 | |
|
32 | 40 | if not tags: |
|
33 | 41 | tags = [] |
|
34 | 42 | if not opening_posts: |
|
35 | 43 | opening_posts = [] |
|
36 | 44 | |
|
37 | 45 | posting_time = timezone.now() |
|
38 | 46 | new_thread = False |
|
39 | 47 | if not thread: |
|
40 | 48 | thread = boards.models.thread.Thread.objects.create( |
|
41 | 49 | bump_time=posting_time, last_edit_time=posting_time) |
|
42 | 50 | list(map(thread.tags.add, tags)) |
|
43 | 51 | boards.models.thread.Thread.objects.process_oldest_threads() |
|
44 | 52 | new_thread = True |
|
45 | 53 | |
|
46 | 54 | pre_text = Parser().preparse(text) |
|
47 | 55 | |
|
48 | 56 | post = self.create(title=title, |
|
49 | 57 | text=pre_text, |
|
50 | 58 | pub_time=posting_time, |
|
51 | 59 | poster_ip=ip, |
|
52 | 60 | thread=thread, |
|
53 |
last_edit_time=posting_time |
|
|
61 | last_edit_time=posting_time, | |
|
62 | tripcode=tripcode) | |
|
54 | 63 | post.threads.add(thread) |
|
55 | 64 | |
|
56 | post.set_global_id() | |
|
57 | ||
|
58 | 65 | logger = logging.getLogger('boards.post.create') |
|
59 | 66 | |
|
60 | 67 | logger.info('Created post {} by {}'.format(post, post.poster_ip)) |
|
61 | 68 | |
|
62 | if image: | |
|
63 | post.images.add(PostImage.objects.create_with_hash(image)) | |
|
64 | ||
|
65 | if not new_thread: | |
|
66 | thread.last_edit_time = posting_time | |
|
67 |
|
|
|
68 | thread.save() | |
|
69 | # TODO Move this to other place | |
|
70 | if file: | |
|
71 | file_type = file.name.split('.')[-1].lower() | |
|
72 | if file_type in IMAGE_TYPES: | |
|
73 | post.images.add(PostImage.objects.create_with_hash(file)) | |
|
74 | else: | |
|
75 | post.attachments.add(Attachment.objects.create_with_hash(file)) | |
|
69 | 76 | |
|
70 | 77 | post.build_url() |
|
71 | 78 | post.connect_replies() |
|
72 | 79 | post.connect_threads(opening_posts) |
|
73 | 80 | post.connect_notifications() |
|
74 | ||
|
75 | return post | |
|
81 | post.set_global_id() | |
|
76 | 82 | |
|
77 | @transaction.atomic | |
|
78 | def import_post(self, title: str, text: str, pub_time: str, global_id, | |
|
79 | opening_post=None, tags=list()): | |
|
80 | if opening_post is None: | |
|
81 | thread = boards.models.thread.Thread.objects.create( | |
|
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() | |
|
83 | # Thread needs to be bumped only when the post is already created | |
|
84 | if not new_thread: | |
|
85 | thread.last_edit_time = posting_time | |
|
86 | thread.bump() | |
|
87 | thread.save() | |
|
96 | 88 | |
|
97 | 89 | return post |
|
98 | 90 | |
|
99 | 91 | def delete_posts_by_ip(self, ip): |
|
100 | 92 | """ |
|
101 | 93 | Deletes all posts of the author with same IP |
|
102 | 94 | """ |
|
103 | 95 | |
|
104 | 96 | posts = self.filter(poster_ip=ip) |
|
105 | 97 | for post in posts: |
|
106 | 98 | post.delete() |
|
107 | 99 | |
|
108 | 100 | @utils.cached_result() |
|
109 | 101 | def get_posts_per_day(self) -> float: |
|
110 | 102 | """ |
|
111 | 103 | Gets average count of posts per day for the last 7 days |
|
112 | 104 | """ |
|
113 | 105 | |
|
114 | 106 | day_end = date.today() |
|
115 | 107 | day_start = day_end - timedelta(POSTS_PER_DAY_RANGE) |
|
116 | 108 | |
|
117 | 109 | day_time_start = timezone.make_aware(datetime.combine( |
|
118 | 110 | day_start, dtime()), timezone.get_current_timezone()) |
|
119 | 111 | day_time_end = timezone.make_aware(datetime.combine( |
|
120 | 112 | day_end, dtime()), timezone.get_current_timezone()) |
|
121 | 113 | |
|
122 | 114 | posts_per_period = float(self.filter( |
|
123 | 115 | pub_time__lte=day_time_end, |
|
124 | 116 | pub_time__gte=day_time_start).count()) |
|
125 | 117 | |
|
126 | 118 | ppd = posts_per_period / POSTS_PER_DAY_RANGE |
|
127 | 119 | |
|
128 | 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() |
@@ -1,82 +1,109 b'' | |||
|
1 | 1 | from django.template.loader import render_to_string |
|
2 | 2 | from django.db import models |
|
3 | 3 | from django.db.models import Count |
|
4 | 4 | from django.core.urlresolvers import reverse |
|
5 | 5 | |
|
6 | 6 | from boards.models.base import Viewable |
|
7 | 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 | 16 | class TagManager(models.Manager): |
|
14 | 17 | |
|
15 | 18 | def get_not_empty_tags(self): |
|
16 | 19 | """ |
|
17 | 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 | 24 | .order_by('-required', 'name') |
|
22 | 25 | |
|
23 | 26 | def get_tag_url_list(self, tags: list) -> str: |
|
24 | 27 | """ |
|
25 | 28 | Gets a comma-separated list of tag links. |
|
26 | 29 | """ |
|
27 | 30 | |
|
28 | 31 | return ', '.join([tag.get_view() for tag in tags]) |
|
29 | 32 | |
|
30 | 33 | |
|
31 | 34 | class Tag(models.Model, Viewable): |
|
32 | 35 | """ |
|
33 | 36 | A tag is a text node assigned to the thread. The tag serves as a board |
|
34 | 37 | section. There can be multiple tags for each thread |
|
35 | 38 | """ |
|
36 | 39 | |
|
37 | 40 | objects = TagManager() |
|
38 | 41 | |
|
39 | 42 | class Meta: |
|
40 | 43 | app_label = 'boards' |
|
41 | 44 | ordering = ('name',) |
|
42 | 45 | |
|
43 | 46 | name = models.CharField(max_length=100, db_index=True, unique=True) |
|
44 | 47 | required = models.BooleanField(default=False, db_index=True) |
|
48 | description = models.TextField(blank=True) | |
|
45 | 49 | |
|
46 | 50 | def __str__(self): |
|
47 | 51 | return self.name |
|
48 | 52 | |
|
49 | 53 | def is_empty(self) -> bool: |
|
50 | 54 | """ |
|
51 | 55 | Checks if the tag has some threads. |
|
52 | 56 | """ |
|
53 | 57 | |
|
54 | 58 | return self.get_thread_count() == 0 |
|
55 | 59 | |
|
56 | def get_thread_count(self) -> int: | |
|
57 |
|
|
|
60 | def get_thread_count(self, archived=None) -> int: | |
|
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 | 69 | def get_absolute_url(self): |
|
60 | 70 | return reverse('tag', kwargs={'tag_name': self.name}) |
|
61 | 71 | |
|
62 | 72 | def get_threads(self): |
|
63 |
return self.thread_s |
|
|
73 | return self.thread_tags.order_by('-bump_time') | |
|
64 | 74 | |
|
65 | 75 | def is_required(self): |
|
66 | 76 | return self.required |
|
67 | 77 | |
|
68 | 78 | def get_view(self): |
|
69 | 79 | link = '<a class="tag" href="{}">{}</a>'.format( |
|
70 | 80 | self.get_absolute_url(), self.name) |
|
71 | 81 | if self.is_required(): |
|
72 | 82 | link = '<b>{}</b>'.format(link) |
|
73 | 83 | return link |
|
74 | 84 | |
|
75 | 85 | def get_search_view(self, *args, **kwargs): |
|
76 | 86 | return render_to_string('boards/tag.html', { |
|
77 | 87 | 'tag': self, |
|
78 | 88 | }) |
|
79 | 89 | |
|
80 | 90 | @cached_result() |
|
81 | 91 | def get_post_count(self): |
|
82 | 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]) |
@@ -1,227 +1,230 b'' | |||
|
1 | 1 | import logging |
|
2 | 2 | from adjacent import Client |
|
3 | 3 | |
|
4 | 4 | from django.db.models import Count, Sum, QuerySet |
|
5 | 5 | from django.utils import timezone |
|
6 | 6 | from django.db import models |
|
7 | 7 | |
|
8 | 8 | from boards import settings |
|
9 | 9 | import boards |
|
10 | 10 | from boards.utils import cached_result, datetime_to_epoch |
|
11 | 11 | from boards.models.post import Post |
|
12 | 12 | from boards.models.tag import Tag |
|
13 | 13 | |
|
14 | 14 | |
|
15 | 15 | __author__ = 'neko259' |
|
16 | 16 | |
|
17 | 17 | |
|
18 | 18 | logger = logging.getLogger(__name__) |
|
19 | 19 | |
|
20 | 20 | |
|
21 | 21 | WS_NOTIFICATION_TYPE_NEW_POST = 'new_post' |
|
22 | 22 | WS_NOTIFICATION_TYPE = 'notification_type' |
|
23 | 23 | |
|
24 | 24 | WS_CHANNEL_THREAD = "thread:" |
|
25 | 25 | |
|
26 | 26 | |
|
27 | 27 | class ThreadManager(models.Manager): |
|
28 | 28 | def process_oldest_threads(self): |
|
29 | 29 | """ |
|
30 | 30 | Preserves maximum thread count. If there are too many threads, |
|
31 | 31 | archive or delete the old ones. |
|
32 | 32 | """ |
|
33 | 33 | |
|
34 | 34 | threads = Thread.objects.filter(archived=False).order_by('-bump_time') |
|
35 | 35 | thread_count = threads.count() |
|
36 | 36 | |
|
37 | 37 | max_thread_count = settings.get_int('Messages', 'MaxThreadCount') |
|
38 | 38 | if thread_count > max_thread_count: |
|
39 | 39 | num_threads_to_delete = thread_count - max_thread_count |
|
40 | 40 | old_threads = threads[thread_count - num_threads_to_delete:] |
|
41 | 41 | |
|
42 | 42 | for thread in old_threads: |
|
43 | 43 | if settings.get_bool('Storage', 'ArchiveThreads'): |
|
44 | 44 | self._archive_thread(thread) |
|
45 | 45 | else: |
|
46 | 46 | thread.delete() |
|
47 | 47 | |
|
48 | 48 | logger.info('Processed %d old threads' % num_threads_to_delete) |
|
49 | 49 | |
|
50 | 50 | def _archive_thread(self, thread): |
|
51 | 51 | thread.archived = True |
|
52 | 52 | thread.bumpable = False |
|
53 | 53 | thread.last_edit_time = timezone.now() |
|
54 | 54 | thread.update_posts_time() |
|
55 | 55 | thread.save(update_fields=['archived', 'last_edit_time', 'bumpable']) |
|
56 | 56 | |
|
57 | 57 | |
|
58 | 58 | def get_thread_max_posts(): |
|
59 | 59 | return settings.get_int('Messages', 'MaxPostsPerThread') |
|
60 | 60 | |
|
61 | 61 | |
|
62 | 62 | class Thread(models.Model): |
|
63 | 63 | objects = ThreadManager() |
|
64 | 64 | |
|
65 | 65 | class Meta: |
|
66 | 66 | app_label = 'boards' |
|
67 | 67 | |
|
68 | tags = models.ManyToManyField('Tag') | |
|
68 | tags = models.ManyToManyField('Tag', related_name='thread_tags') | |
|
69 | 69 | bump_time = models.DateTimeField(db_index=True) |
|
70 | 70 | last_edit_time = models.DateTimeField() |
|
71 | 71 | archived = models.BooleanField(default=False) |
|
72 | 72 | bumpable = models.BooleanField(default=True) |
|
73 | 73 | max_posts = models.IntegerField(default=get_thread_max_posts) |
|
74 | 74 | |
|
75 | 75 | def get_tags(self) -> QuerySet: |
|
76 | 76 | """ |
|
77 | 77 | Gets a sorted tag list. |
|
78 | 78 | """ |
|
79 | 79 | |
|
80 | 80 | return self.tags.order_by('name') |
|
81 | 81 | |
|
82 | 82 | def bump(self): |
|
83 | 83 | """ |
|
84 | 84 | Bumps (moves to up) thread if possible. |
|
85 | 85 | """ |
|
86 | 86 | |
|
87 | 87 | if self.can_bump(): |
|
88 | 88 | self.bump_time = self.last_edit_time |
|
89 | 89 | |
|
90 | 90 | self.update_bump_status() |
|
91 | 91 | |
|
92 | 92 | logger.info('Bumped thread %d' % self.id) |
|
93 | 93 | |
|
94 | 94 | def has_post_limit(self) -> bool: |
|
95 | 95 | return self.max_posts > 0 |
|
96 | 96 | |
|
97 | 97 | def update_bump_status(self, exclude_posts=None): |
|
98 | 98 | if self.has_post_limit() and self.get_reply_count() >= self.max_posts: |
|
99 | 99 | self.bumpable = False |
|
100 | 100 | self.update_posts_time(exclude_posts=exclude_posts) |
|
101 | 101 | |
|
102 | 102 | def _get_cache_key(self): |
|
103 | 103 | return [datetime_to_epoch(self.last_edit_time)] |
|
104 | 104 | |
|
105 | 105 | @cached_result(key_method=_get_cache_key) |
|
106 | 106 | def get_reply_count(self) -> int: |
|
107 | 107 | return self.get_replies().count() |
|
108 | 108 | |
|
109 | 109 | @cached_result(key_method=_get_cache_key) |
|
110 | 110 | def get_images_count(self) -> int: |
|
111 | 111 | return self.get_replies().annotate(images_count=Count( |
|
112 | 112 | 'images')).aggregate(Sum('images_count'))['images_count__sum'] |
|
113 | 113 | |
|
114 | 114 | def can_bump(self) -> bool: |
|
115 | 115 | """ |
|
116 | 116 | Checks if the thread can be bumped by replying to it. |
|
117 | 117 | """ |
|
118 | 118 | |
|
119 | 119 | return self.bumpable and not self.archived |
|
120 | 120 | |
|
121 | 121 | def get_last_replies(self) -> QuerySet: |
|
122 | 122 | """ |
|
123 | 123 | Gets several last replies, not including opening post |
|
124 | 124 | """ |
|
125 | 125 | |
|
126 | 126 | last_replies_count = settings.get_int('View', 'LastRepliesCount') |
|
127 | 127 | |
|
128 | 128 | if last_replies_count > 0: |
|
129 | 129 | reply_count = self.get_reply_count() |
|
130 | 130 | |
|
131 | 131 | if reply_count > 0: |
|
132 | 132 | reply_count_to_show = min(last_replies_count, |
|
133 | 133 | reply_count - 1) |
|
134 | 134 | replies = self.get_replies() |
|
135 | 135 | last_replies = replies[reply_count - reply_count_to_show:] |
|
136 | 136 | |
|
137 | 137 | return last_replies |
|
138 | 138 | |
|
139 | 139 | def get_skipped_replies_count(self) -> int: |
|
140 | 140 | """ |
|
141 | 141 | Gets number of posts between opening post and last replies. |
|
142 | 142 | """ |
|
143 | 143 | reply_count = self.get_reply_count() |
|
144 | 144 | last_replies_count = min(settings.get_int('View', 'LastRepliesCount'), |
|
145 | 145 | reply_count - 1) |
|
146 | 146 | return reply_count - last_replies_count - 1 |
|
147 | 147 | |
|
148 | 148 | def get_replies(self, view_fields_only=False) -> QuerySet: |
|
149 | 149 | """ |
|
150 | 150 | Gets sorted thread posts |
|
151 | 151 | """ |
|
152 | 152 | |
|
153 | 153 | query = Post.objects.filter(threads__in=[self]) |
|
154 | 154 | query = query.order_by('pub_time').prefetch_related('images', 'thread', 'threads') |
|
155 | 155 | if view_fields_only: |
|
156 | 156 | query = query.defer('poster_ip') |
|
157 | 157 | return query.all() |
|
158 | 158 | |
|
159 | 159 | def get_top_level_replies(self) -> QuerySet: |
|
160 | 160 | return self.get_replies().exclude(refposts__threads__in=[self]) |
|
161 | 161 | |
|
162 | 162 | def get_replies_with_images(self, view_fields_only=False) -> QuerySet: |
|
163 | 163 | """ |
|
164 | 164 | Gets replies that have at least one image attached |
|
165 | 165 | """ |
|
166 | 166 | |
|
167 | 167 | return self.get_replies(view_fields_only).annotate(images_count=Count( |
|
168 | 168 | 'images')).filter(images_count__gt=0) |
|
169 | 169 | |
|
170 | 170 | def get_opening_post(self, only_id=False) -> Post: |
|
171 | 171 | """ |
|
172 | 172 | Gets the first post of the thread |
|
173 | 173 | """ |
|
174 | 174 | |
|
175 | 175 | query = self.get_replies().order_by('pub_time') |
|
176 | 176 | if only_id: |
|
177 | 177 | query = query.only('id') |
|
178 | 178 | opening_post = query.first() |
|
179 | 179 | |
|
180 | 180 | return opening_post |
|
181 | 181 | |
|
182 | 182 | @cached_result() |
|
183 | 183 | def get_opening_post_id(self) -> int: |
|
184 | 184 | """ |
|
185 | 185 | Gets ID of the first thread post. |
|
186 | 186 | """ |
|
187 | 187 | |
|
188 | 188 | return self.get_opening_post(only_id=True).id |
|
189 | 189 | |
|
190 | 190 | def get_pub_time(self): |
|
191 | 191 | """ |
|
192 | 192 | Gets opening post's pub time because thread does not have its own one. |
|
193 | 193 | """ |
|
194 | 194 | |
|
195 | 195 | return self.get_opening_post().pub_time |
|
196 | 196 | |
|
197 | 197 | def __str__(self): |
|
198 | 198 | return 'T#{}/{}'.format(self.id, self.get_opening_post_id()) |
|
199 | 199 | |
|
200 | 200 | def get_tag_url_list(self) -> list: |
|
201 | 201 | return boards.models.Tag.objects.get_tag_url_list(self.get_tags()) |
|
202 | 202 | |
|
203 | 203 | def update_posts_time(self, exclude_posts=None): |
|
204 | 204 | last_edit_time = self.last_edit_time |
|
205 | 205 | |
|
206 | 206 | for post in self.post_set.all(): |
|
207 | 207 | if exclude_posts is None or post not in exclude_posts: |
|
208 | 208 | # Manual update is required because uids are generated on save |
|
209 | 209 | post.last_edit_time = last_edit_time |
|
210 | 210 | post.save(update_fields=['last_edit_time']) |
|
211 | 211 | |
|
212 | 212 | post.get_threads().update(last_edit_time=last_edit_time) |
|
213 | 213 | |
|
214 | 214 | def notify_clients(self): |
|
215 | 215 | if not settings.get_bool('External', 'WebsocketsEnabled'): |
|
216 | 216 | return |
|
217 | 217 | |
|
218 | 218 | client = Client() |
|
219 | 219 | |
|
220 | 220 | channel_name = WS_CHANNEL_THREAD + str(self.get_opening_post_id()) |
|
221 | 221 | client.publish(channel_name, { |
|
222 | 222 | WS_NOTIFICATION_TYPE: WS_NOTIFICATION_TYPE_NEW_POST, |
|
223 | 223 | }) |
|
224 | 224 | client.send() |
|
225 | 225 | |
|
226 | 226 | def get_absolute_url(self): |
|
227 | 227 | return self.get_opening_post().get_absolute_url() |
|
228 | ||
|
229 | def get_required_tags(self): | |
|
230 | return self.get_tags().filter(required=True) |
@@ -1,105 +1,136 b'' | |||
|
1 | 1 | .ui-button { |
|
2 | 2 | display: none; |
|
3 | 3 | } |
|
4 | 4 | |
|
5 | 5 | .ui-dialog-content { |
|
6 | 6 | padding: 0; |
|
7 | 7 | min-height: 0; |
|
8 | 8 | } |
|
9 | 9 | |
|
10 | 10 | .mark_btn { |
|
11 | 11 | cursor: pointer; |
|
12 | 12 | } |
|
13 | 13 | |
|
14 | 14 | .img-full { |
|
15 | 15 | position: fixed; |
|
16 | 16 | background-color: #CCC; |
|
17 | 17 | border: 1px solid #000; |
|
18 | 18 | cursor: pointer; |
|
19 | 19 | } |
|
20 | 20 | |
|
21 | 21 | .strikethrough { |
|
22 | 22 | text-decoration: line-through; |
|
23 | 23 | } |
|
24 | 24 | |
|
25 | 25 | .post_preview { |
|
26 | 26 | z-index: 300; |
|
27 | 27 | position:absolute; |
|
28 | 28 | } |
|
29 | 29 | |
|
30 | 30 | .gallery_image { |
|
31 | 31 | display: inline-block; |
|
32 | 32 | } |
|
33 | 33 | |
|
34 | 34 | @media print { |
|
35 | 35 | .post-form-w { |
|
36 | 36 | display: none; |
|
37 | 37 | } |
|
38 | 38 | } |
|
39 | 39 | |
|
40 | 40 | input[name="image"] { |
|
41 | 41 | display: block; |
|
42 | 42 | width: 100px; |
|
43 | 43 | height: 100px; |
|
44 | 44 | cursor: pointer; |
|
45 | 45 | position: absolute; |
|
46 | 46 | opacity: 0; |
|
47 | 47 | z-index: 1; |
|
48 | 48 | } |
|
49 | 49 | |
|
50 | 50 | .file_wrap { |
|
51 | 51 | width: 100px; |
|
52 | 52 | height: 100px; |
|
53 | 53 | border: solid 1px white; |
|
54 | 54 | display: inline-block; |
|
55 | 55 | } |
|
56 | 56 | |
|
57 | 57 | form > .file_wrap { |
|
58 | 58 | float: left; |
|
59 | 59 | } |
|
60 | 60 | |
|
61 | 61 | .file-thumb { |
|
62 | 62 | width: 100px; |
|
63 | 63 | height: 100px; |
|
64 | 64 | background-size: cover; |
|
65 | 65 | background-position: center; |
|
66 | 66 | } |
|
67 | 67 | |
|
68 | 68 | .compact-form-text { |
|
69 | 69 | margin-left:110px; |
|
70 | 70 | } |
|
71 | 71 | |
|
72 | 72 | textarea, input { |
|
73 | 73 | -moz-box-sizing: border-box; |
|
74 | 74 | -webkit-box-sizing: border-box; |
|
75 | 75 | box-sizing: border-box; |
|
76 | 76 | } |
|
77 | 77 | |
|
78 | 78 | .compact-form-text > textarea { |
|
79 | 79 | height: 100px; |
|
80 | 80 | width: 100%; |
|
81 | 81 | } |
|
82 | 82 | |
|
83 | 83 | .post-button-form { |
|
84 | 84 | display: inline; |
|
85 | 85 | } |
|
86 | 86 | |
|
87 | 87 | .post-button-form > button, #autoupdate { |
|
88 | 88 | border: none; |
|
89 | 89 | margin: inherit; |
|
90 | 90 | padding: inherit; |
|
91 | 91 | background: none; |
|
92 | 92 | font-size: inherit; |
|
93 | 93 | } |
|
94 | 94 | |
|
95 | 95 | #form-close-button { |
|
96 | 96 | display: none; |
|
97 | 97 | } |
|
98 | 98 | |
|
99 | 99 | .post-image-full { |
|
100 | 100 | width: 100%; |
|
101 | height: auto; | |
|
101 | 102 | } |
|
102 | 103 | |
|
103 | 104 | #preview-text { |
|
104 | 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 |
@@ -1,555 +1,559 b'' | |||
|
1 | 1 | * { |
|
2 | 2 | text-decoration: none; |
|
3 | 3 | font-weight: inherit; |
|
4 | 4 | } |
|
5 | 5 | |
|
6 | 6 | b, strong { |
|
7 | 7 | font-weight: bold; |
|
8 | 8 | } |
|
9 | 9 | |
|
10 | 10 | html { |
|
11 | 11 | background: #555; |
|
12 | 12 | color: #ffffff; |
|
13 | 13 | } |
|
14 | 14 | |
|
15 | 15 | body { |
|
16 | 16 | margin: 0; |
|
17 | 17 | } |
|
18 | 18 | |
|
19 | 19 | #admin_panel { |
|
20 | 20 | background: #FF0000; |
|
21 | 21 | color: #00FF00 |
|
22 | 22 | } |
|
23 | 23 | |
|
24 | 24 | .input_field_error { |
|
25 | 25 | color: #FF0000; |
|
26 | 26 | } |
|
27 | 27 | |
|
28 | 28 | .title { |
|
29 | 29 | font-weight: bold; |
|
30 | 30 | color: #ffcc00; |
|
31 | 31 | } |
|
32 | 32 | |
|
33 | 33 | .link, a { |
|
34 | 34 | color: #afdcec; |
|
35 | 35 | } |
|
36 | 36 | |
|
37 | 37 | .block { |
|
38 | 38 | display: inline-block; |
|
39 | 39 | vertical-align: top; |
|
40 | 40 | } |
|
41 | 41 | |
|
42 | 42 | .tag { |
|
43 | 43 | color: #FFD37D; |
|
44 | 44 | } |
|
45 | 45 | |
|
46 | 46 | .post_id { |
|
47 | 47 | color: #fff380; |
|
48 | 48 | } |
|
49 | 49 | |
|
50 | 50 | .post, .dead_post, .archive_post, #posts-table { |
|
51 | 51 | background: #333; |
|
52 | 52 | padding: 10px; |
|
53 | 53 | clear: left; |
|
54 | 54 | word-wrap: break-word; |
|
55 | 55 | border-top: 1px solid #777; |
|
56 | 56 | border-bottom: 1px solid #777; |
|
57 | 57 | } |
|
58 | 58 | |
|
59 | 59 | .post + .post { |
|
60 | 60 | border-top: none; |
|
61 | 61 | } |
|
62 | 62 | |
|
63 | 63 | .dead_post + .dead_post { |
|
64 | 64 | border-top: none; |
|
65 | 65 | } |
|
66 | 66 | |
|
67 | 67 | .archive_post + .archive_post { |
|
68 | 68 | border-top: none; |
|
69 | 69 | } |
|
70 | 70 | |
|
71 | 71 | .metadata { |
|
72 | 72 | padding-top: 5px; |
|
73 | 73 | margin-top: 10px; |
|
74 | 74 | border-top: solid 1px #666; |
|
75 | 75 | color: #ddd; |
|
76 | 76 | } |
|
77 | 77 | |
|
78 | 78 | .navigation_panel, .tag_info { |
|
79 | 79 | background: #222; |
|
80 | 80 | margin-bottom: 5px; |
|
81 | 81 | margin-top: 5px; |
|
82 | 82 | padding: 10px; |
|
83 | 83 | border-bottom: solid 1px #888; |
|
84 | 84 | border-top: solid 1px #888; |
|
85 | 85 | color: #eee; |
|
86 | 86 | } |
|
87 | 87 | |
|
88 | 88 | .navigation_panel .link:first-child { |
|
89 | 89 | border-right: 1px solid #fff; |
|
90 | 90 | font-weight: bold; |
|
91 | 91 | margin-right: 1ex; |
|
92 | 92 | padding-right: 1ex; |
|
93 | 93 | } |
|
94 | 94 | |
|
95 | 95 | .navigation_panel .right-link { |
|
96 | 96 | border-left: 1px solid #fff; |
|
97 | 97 | border-right: none; |
|
98 | 98 | float: right; |
|
99 | 99 | margin-left: 1ex; |
|
100 | 100 | margin-right: 0; |
|
101 | 101 | padding-left: 1ex; |
|
102 | 102 | padding-right: 0; |
|
103 | 103 | } |
|
104 | 104 | |
|
105 | 105 | .navigation_panel .link { |
|
106 | 106 | font-weight: bold; |
|
107 | 107 | } |
|
108 | 108 | |
|
109 | 109 | .navigation_panel::after, .post::after { |
|
110 | 110 | clear: both; |
|
111 | 111 | content: "."; |
|
112 | 112 | display: block; |
|
113 | 113 | height: 0; |
|
114 | 114 | line-height: 0; |
|
115 | 115 | visibility: hidden; |
|
116 | 116 | } |
|
117 | 117 | |
|
118 | 118 | .header { |
|
119 | 119 | border-bottom: solid 2px #ccc; |
|
120 | 120 | margin-bottom: 5px; |
|
121 | 121 | border-top: none; |
|
122 | 122 | margin-top: 0; |
|
123 | 123 | } |
|
124 | 124 | |
|
125 | 125 | .footer { |
|
126 | 126 | border-top: solid 2px #ccc; |
|
127 | 127 | margin-top: 5px; |
|
128 | 128 | border-bottom: none; |
|
129 | 129 | margin-bottom: 0; |
|
130 | 130 | } |
|
131 | 131 | |
|
132 | 132 | p, .br { |
|
133 | 133 | margin-top: .5em; |
|
134 | 134 | margin-bottom: .5em; |
|
135 | 135 | } |
|
136 | 136 | |
|
137 | 137 | .post-form-w { |
|
138 | 138 | background: #333344; |
|
139 | 139 | border-top: solid 1px #888; |
|
140 | 140 | border-bottom: solid 1px #888; |
|
141 | 141 | color: #fff; |
|
142 | 142 | padding: 10px; |
|
143 | 143 | margin-bottom: 5px; |
|
144 | 144 | margin-top: 5px; |
|
145 | 145 | } |
|
146 | 146 | |
|
147 | 147 | .form-row { |
|
148 | 148 | width: 100%; |
|
149 | 149 | display: table-row; |
|
150 | 150 | } |
|
151 | 151 | |
|
152 | 152 | .form-label { |
|
153 | 153 | padding: .25em 1ex .25em 0; |
|
154 | 154 | vertical-align: top; |
|
155 | 155 | display: table-cell; |
|
156 | 156 | } |
|
157 | 157 | |
|
158 | 158 | .form-input { |
|
159 | 159 | padding: .25em 0; |
|
160 | 160 | width: 100%; |
|
161 | 161 | display: table-cell; |
|
162 | 162 | } |
|
163 | 163 | |
|
164 | 164 | .form-errors { |
|
165 | 165 | font-weight: bolder; |
|
166 | 166 | vertical-align: middle; |
|
167 | 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 | 171 | background: #333; |
|
172 | 172 | color: #fff; |
|
173 | 173 | border: solid 1px; |
|
174 | 174 | padding: 0; |
|
175 | 175 | font: medium sans-serif; |
|
176 | 176 | width: 100%; |
|
177 | 177 | } |
|
178 | 178 | |
|
179 | 179 | .post-form textarea { |
|
180 | 180 | resize: vertical; |
|
181 | 181 | } |
|
182 | 182 | |
|
183 | 183 | .form-submit { |
|
184 | 184 | display: table; |
|
185 | 185 | margin-bottom: 1ex; |
|
186 | 186 | margin-top: 1ex; |
|
187 | 187 | } |
|
188 | 188 | |
|
189 | 189 | .form-title { |
|
190 | 190 | font-weight: bold; |
|
191 | 191 | font-size: 2ex; |
|
192 | 192 | margin-bottom: 0.5ex; |
|
193 | 193 | } |
|
194 | 194 | |
|
195 |
|
|
|
195 | input[type="submit"], button { | |
|
196 | 196 | background: #222; |
|
197 | 197 | border: solid 2px #fff; |
|
198 | 198 | color: #fff; |
|
199 | 199 | padding: 0.5ex; |
|
200 | margin-right: 0.5ex; | |
|
200 | 201 | } |
|
201 | 202 | |
|
202 | 203 | input[type="submit"]:hover { |
|
203 | 204 | background: #060; |
|
204 | 205 | } |
|
205 | 206 | |
|
207 | .form-submit > button:hover { | |
|
208 | background: #006; | |
|
209 | } | |
|
210 | ||
|
206 | 211 | blockquote { |
|
207 | 212 | border-left: solid 2px; |
|
208 | 213 | padding-left: 5px; |
|
209 | 214 | color: #B1FB17; |
|
210 | 215 | margin: 0; |
|
211 | 216 | } |
|
212 | 217 | |
|
213 | 218 | .post > .image { |
|
214 | 219 | float: left; |
|
215 | 220 | margin: 0 1ex .5ex 0; |
|
216 | 221 | min-width: 1px; |
|
217 | 222 | text-align: center; |
|
218 | 223 | display: table-row; |
|
219 | 224 | } |
|
220 | 225 | |
|
221 | 226 | .post > .metadata { |
|
222 | 227 | clear: left; |
|
223 | 228 | } |
|
224 | 229 | |
|
225 | 230 | .get { |
|
226 | 231 | font-weight: bold; |
|
227 | 232 | color: #d55; |
|
228 | 233 | } |
|
229 | 234 | |
|
230 | 235 | * { |
|
231 | 236 | text-decoration: none; |
|
232 | 237 | } |
|
233 | 238 | |
|
234 | .dead_post { | |
|
235 | border-left: solid 5px #982C2C; | |
|
239 | .dead_post > .post-info { | |
|
240 | font-style: italic; | |
|
236 | 241 | } |
|
237 | 242 | |
|
238 | .archive_post { | |
|
239 | border-left: solid 5px #B7B7B7; | |
|
243 | .archive_post > .post-info { | |
|
244 | text-decoration: line-through; | |
|
240 | 245 | } |
|
241 | 246 | |
|
242 | 247 | .mark_btn { |
|
243 | 248 | border: 1px solid; |
|
244 | 249 | padding: 2px 2ex; |
|
245 | 250 | display: inline-block; |
|
246 | 251 | margin: 0 5px 4px 0; |
|
247 | 252 | } |
|
248 | 253 | |
|
249 | 254 | .mark_btn:hover { |
|
250 | 255 | background: #555; |
|
251 | 256 | } |
|
252 | 257 | |
|
253 | 258 | .quote { |
|
254 | 259 | color: #92cf38; |
|
255 | 260 | font-style: italic; |
|
256 | 261 | } |
|
257 | 262 | |
|
258 | 263 | .multiquote { |
|
259 | 264 | padding: 3px; |
|
260 | 265 | display: inline-block; |
|
261 | 266 | background: #222; |
|
262 | 267 | border-style: solid; |
|
263 | 268 | border-width: 1px 1px 1px 4px; |
|
264 | 269 | font-size: 0.9em; |
|
265 | 270 | } |
|
266 | 271 | |
|
267 | 272 | .spoiler { |
|
268 | 273 | background: black; |
|
269 | 274 | color: black; |
|
270 | 275 | padding: 0 1ex 0 1ex; |
|
271 | 276 | } |
|
272 | 277 | |
|
273 | 278 | .spoiler:hover { |
|
274 | 279 | color: #ddd; |
|
275 | 280 | } |
|
276 | 281 | |
|
277 | 282 | .comment { |
|
278 | 283 | color: #eb2; |
|
279 | 284 | } |
|
280 | 285 | |
|
281 | 286 | a:hover { |
|
282 | 287 | text-decoration: underline; |
|
283 | 288 | } |
|
284 | 289 | |
|
285 | 290 | .last-replies { |
|
286 | 291 | margin-left: 3ex; |
|
287 | 292 | margin-right: 3ex; |
|
288 | 293 | border-left: solid 1px #777; |
|
289 | 294 | border-right: solid 1px #777; |
|
290 | 295 | } |
|
291 | 296 | |
|
292 | 297 | .last-replies > .post:first-child { |
|
293 | 298 | border-top: none; |
|
294 | 299 | } |
|
295 | 300 | |
|
296 | 301 | .thread { |
|
297 | 302 | margin-bottom: 3ex; |
|
298 | 303 | margin-top: 1ex; |
|
299 | 304 | } |
|
300 | 305 | |
|
301 | 306 | .post:target { |
|
302 | 307 | border: solid 2px white; |
|
303 | 308 | } |
|
304 | 309 | |
|
305 | 310 | pre{ |
|
306 | 311 | white-space:pre-wrap |
|
307 | 312 | } |
|
308 | 313 | |
|
309 | 314 | li { |
|
310 | 315 | list-style-position: inside; |
|
311 | 316 | } |
|
312 | 317 | |
|
313 | 318 | .fancybox-skin { |
|
314 | 319 | position: relative; |
|
315 | 320 | background-color: #fff; |
|
316 | 321 | color: #ddd; |
|
317 | 322 | text-shadow: none; |
|
318 | 323 | } |
|
319 | 324 | |
|
320 | 325 | .fancybox-image { |
|
321 | 326 | border: 1px solid black; |
|
322 | 327 | } |
|
323 | 328 | |
|
324 | 329 | .image-mode-tab { |
|
325 | 330 | background: #444; |
|
326 | 331 | color: #eee; |
|
327 | 332 | margin-top: 5px; |
|
328 | 333 | padding: 5px; |
|
329 | 334 | border-top: 1px solid #888; |
|
330 | 335 | border-bottom: 1px solid #888; |
|
331 | 336 | } |
|
332 | 337 | |
|
333 | 338 | .image-mode-tab > label { |
|
334 | 339 | margin: 0 1ex; |
|
335 | 340 | } |
|
336 | 341 | |
|
337 | 342 | .image-mode-tab > label > input { |
|
338 | 343 | margin-right: .5ex; |
|
339 | 344 | } |
|
340 | 345 | |
|
341 | 346 | #posts-table { |
|
342 | 347 | margin-top: 5px; |
|
343 | 348 | margin-bottom: 5px; |
|
344 | 349 | } |
|
345 | 350 | |
|
346 | 351 | .tag_info > h2 { |
|
347 | 352 | margin: 0; |
|
348 | 353 | } |
|
349 | 354 | |
|
350 | 355 | .post-info { |
|
351 | 356 | color: #ddd; |
|
352 | 357 | margin-bottom: 1ex; |
|
353 | 358 | } |
|
354 | 359 | |
|
355 | 360 | .moderator_info { |
|
356 | 361 | color: #e99d41; |
|
357 | float: right; | |
|
358 | font-weight: bold; | |
|
359 | 362 | opacity: 0.4; |
|
360 | 363 | } |
|
361 | 364 | |
|
362 | 365 | .moderator_info:hover { |
|
363 | 366 | opacity: 1; |
|
364 | 367 | } |
|
365 | 368 | |
|
366 | 369 | .refmap { |
|
367 | 370 | font-size: 0.9em; |
|
368 | 371 | color: #ccc; |
|
369 | 372 | margin-top: 1em; |
|
370 | 373 | } |
|
371 | 374 | |
|
372 | 375 | .fav { |
|
373 | 376 | color: yellow; |
|
374 | 377 | } |
|
375 | 378 | |
|
376 | 379 | .not_fav { |
|
377 | 380 | color: #ccc; |
|
378 | 381 | } |
|
379 | 382 | |
|
380 | 383 | .role { |
|
381 | 384 | text-decoration: underline; |
|
382 | 385 | } |
|
383 | 386 | |
|
384 | 387 | .form-email { |
|
385 | 388 | display: none; |
|
386 | 389 | } |
|
387 | 390 | |
|
388 | 391 | .bar-value { |
|
389 | 392 | background: rgba(50, 55, 164, 0.45); |
|
390 | 393 | font-size: 0.9em; |
|
391 | 394 | height: 1.5em; |
|
392 | 395 | } |
|
393 | 396 | |
|
394 | 397 | .bar-bg { |
|
395 | 398 | position: relative; |
|
396 | 399 | border-top: solid 1px #888; |
|
397 | 400 | border-bottom: solid 1px #888; |
|
398 | 401 | margin-top: 5px; |
|
399 | 402 | overflow: hidden; |
|
400 | 403 | } |
|
401 | 404 | |
|
402 | 405 | .bar-text { |
|
403 | 406 | padding: 2px; |
|
404 | 407 | position: absolute; |
|
405 | 408 | left: 0; |
|
406 | 409 | top: 0; |
|
407 | 410 | } |
|
408 | 411 | |
|
409 | 412 | .page_link { |
|
410 | 413 | background: #444; |
|
411 | 414 | border-top: solid 1px #888; |
|
412 | 415 | border-bottom: solid 1px #888; |
|
413 | 416 | padding: 5px; |
|
414 | 417 | color: #eee; |
|
415 | 418 | font-size: 2ex; |
|
416 | 419 | } |
|
417 | 420 | |
|
418 | 421 | .skipped_replies { |
|
419 | 422 | padding: 5px; |
|
420 | 423 | margin-left: 3ex; |
|
421 | 424 | margin-right: 3ex; |
|
422 | 425 | border-left: solid 1px #888; |
|
423 | 426 | border-right: solid 1px #888; |
|
424 | 427 | border-bottom: solid 1px #888; |
|
425 | 428 | background: #000; |
|
426 | 429 | } |
|
427 | 430 | |
|
428 | 431 | .current_page { |
|
429 | 432 | padding: 2px; |
|
430 | 433 | background-color: #afdcec; |
|
431 | 434 | color: #000; |
|
432 | 435 | } |
|
433 | 436 | |
|
434 | 437 | .current_mode { |
|
435 | 438 | font-weight: bold; |
|
436 | 439 | } |
|
437 | 440 | |
|
438 | 441 | .gallery_image { |
|
439 | 442 | border: solid 1px; |
|
440 | padding: 0.5ex; | |
|
441 | 443 | margin: 0.5ex; |
|
442 | 444 | text-align: center; |
|
443 | 445 | } |
|
444 | 446 | |
|
445 | 447 | code { |
|
446 | 448 | border: dashed 1px #ccc; |
|
447 | 449 | background: #111; |
|
448 | 450 | padding: 2px; |
|
449 | 451 | font-size: 1.2em; |
|
450 | 452 | display: inline-block; |
|
451 | 453 | } |
|
452 | 454 | |
|
453 | 455 | pre { |
|
454 | 456 | overflow: auto; |
|
455 | 457 | } |
|
456 | 458 | |
|
457 | 459 | .img-full { |
|
458 | 460 | background: #222; |
|
459 | 461 | border: solid 1px white; |
|
460 | 462 | } |
|
461 | 463 | |
|
462 | 464 | .tag_item { |
|
463 | 465 | display: inline-block; |
|
464 | 466 | } |
|
465 | 467 | |
|
466 | 468 | #id_models li { |
|
467 | 469 | list-style: none; |
|
468 | 470 | } |
|
469 | 471 | |
|
470 | 472 | #id_q { |
|
471 | 473 | margin-left: 1ex; |
|
472 | 474 | } |
|
473 | 475 | |
|
474 | 476 | ul { |
|
475 | 477 | padding-left: 0px; |
|
476 | 478 | } |
|
477 | 479 | |
|
478 | 480 | .quote-header { |
|
479 | 481 | border-bottom: 2px solid #ddd; |
|
480 | 482 | margin-bottom: 1ex; |
|
481 | 483 | padding-bottom: .5ex; |
|
482 | 484 | color: #ddd; |
|
483 | 485 | font-size: 1.2em; |
|
484 | 486 | } |
|
485 | 487 | |
|
486 | 488 | .global-id { |
|
487 | 489 | font-weight: bolder; |
|
488 | 490 | opacity: .5; |
|
489 | 491 | } |
|
490 | 492 | |
|
491 | 493 | /* Post */ |
|
492 | 494 | .post > .message, .post > .image { |
|
493 | 495 | padding-left: 1em; |
|
494 | 496 | } |
|
495 | 497 | |
|
496 | 498 | /* Reflink preview */ |
|
497 | 499 | .post_preview { |
|
498 | 500 | border-left: 1px solid #777; |
|
499 | 501 | border-right: 1px solid #777; |
|
500 | 502 | max-width: 600px; |
|
501 | 503 | } |
|
502 | 504 | |
|
503 | 505 | /* Code highlighter */ |
|
504 | 506 | .hljs { |
|
505 | 507 | color: #fff; |
|
506 | 508 | background: #000; |
|
507 | 509 | display: inline-block; |
|
508 | 510 | } |
|
509 | 511 | |
|
510 | 512 | .hljs, .hljs-subst, .hljs-tag .hljs-title, .lisp .hljs-title, .clojure .hljs-built_in, .nginx .hljs-title { |
|
511 | 513 | color: #fff; |
|
512 | 514 | } |
|
513 | 515 | |
|
514 | 516 | #up { |
|
515 | 517 | position: fixed; |
|
516 | 518 | bottom: 5px; |
|
517 | 519 | right: 5px; |
|
518 | 520 | border: 1px solid #777; |
|
519 | 521 | background: #000; |
|
520 | 522 | padding: 4px; |
|
521 | 523 | } |
|
522 | 524 | |
|
523 | 525 | .user-cast { |
|
524 | 526 | border: solid #ffffff 1px; |
|
525 | 527 | padding: .2ex; |
|
526 | 528 | background: #152154; |
|
527 | 529 | color: #fff; |
|
528 | 530 | } |
|
529 | 531 | |
|
530 | 532 | .highlight { |
|
531 |
background |
|
|
533 | background: #222; | |
|
532 | 534 | } |
|
533 | 535 | |
|
534 | 536 | .post-button-form > button:hover { |
|
535 | 537 | text-decoration: underline; |
|
536 | 538 | } |
|
537 | 539 | |
|
538 | 540 | .tree_reply > .post { |
|
539 | margin-left: 1ex; | |
|
540 | 541 | margin-top: 1ex; |
|
541 | 542 | border-left: solid 1px #777; |
|
542 | border-right: solid 1px #777; | |
|
543 | padding-right: 0; | |
|
543 | 544 | } |
|
544 | 545 | |
|
545 | 546 | #preview-text { |
|
546 | 547 | border: solid 1px white; |
|
547 | 548 | margin: 1ex 0 1ex 0; |
|
548 | 549 | padding: 1ex; |
|
549 | 550 | } |
|
550 | 551 | |
|
551 | button { | |
|
552 | border: 1px solid white; | |
|
553 | margin-bottom: .5ex; | |
|
554 | margin-top: .5ex; | |
|
552 | .image-metadata { | |
|
553 | font-style: italic; | |
|
554 | font-size: 0.9em; | |
|
555 | 555 | } |
|
556 | ||
|
557 | .tripcode { | |
|
558 | color: white; | |
|
559 | } |
@@ -1,378 +1,383 b'' | |||
|
1 | 1 | html { |
|
2 | 2 | background: rgb(238, 238, 238); |
|
3 | 3 | color: rgb(51, 51, 51); |
|
4 | 4 | } |
|
5 | 5 | |
|
6 | 6 | #admin_panel { |
|
7 | 7 | background: #FF0000; |
|
8 | 8 | color: #00FF00 |
|
9 | 9 | } |
|
10 | 10 | |
|
11 | 11 | .input_field { |
|
12 | 12 | |
|
13 | 13 | } |
|
14 | 14 | |
|
15 | 15 | .input_field_name { |
|
16 | 16 | |
|
17 | 17 | } |
|
18 | 18 | |
|
19 | 19 | .input_field_error { |
|
20 | 20 | color: #FF0000; |
|
21 | 21 | } |
|
22 | 22 | |
|
23 | 23 | |
|
24 | 24 | .title { |
|
25 | 25 | font-weight: bold; |
|
26 | 26 | color: #333; |
|
27 | 27 | font-size: 2ex; |
|
28 | 28 | } |
|
29 | 29 | |
|
30 | 30 | .link, a { |
|
31 | 31 | color: #ff7000; |
|
32 | 32 | } |
|
33 | 33 | |
|
34 | 34 | .block { |
|
35 | 35 | display: inline-block; |
|
36 | 36 | vertical-align: top; |
|
37 | 37 | } |
|
38 | 38 | |
|
39 | 39 | .tag { |
|
40 | 40 | color: #222; |
|
41 | 41 | } |
|
42 | 42 | |
|
43 | 43 | .post_id:hover { |
|
44 | 44 | color: #11f; |
|
45 | 45 | } |
|
46 | 46 | |
|
47 | 47 | .post_id { |
|
48 | 48 | color: #444; |
|
49 | 49 | } |
|
50 | 50 | |
|
51 | 51 | .post, .dead_post, #posts-table { |
|
52 | 52 | margin: 5px; |
|
53 | 53 | padding: 10px; |
|
54 | 54 | background: rgb(221, 221, 221); |
|
55 | 55 | border: 1px solid rgb(204, 204, 204); |
|
56 | 56 | border-radius: 5px 5px 5px 5px; |
|
57 | 57 | clear: left; |
|
58 | 58 | word-wrap: break-word; |
|
59 | 59 | display: table; |
|
60 | 60 | } |
|
61 | 61 | |
|
62 | 62 | .metadata { |
|
63 | 63 | padding: 5px; |
|
64 | 64 | margin-top: 10px; |
|
65 | 65 | border: solid 1px #666; |
|
66 | 66 | font-size: 0.9em; |
|
67 | 67 | display: table; |
|
68 | 68 | } |
|
69 | 69 | |
|
70 | 70 | .navigation_panel, .tag_info, .page_link { |
|
71 | 71 | margin: 5px; |
|
72 | 72 | padding: 10px; |
|
73 | 73 | border: 1px solid rgb(204, 204, 204); |
|
74 | 74 | border-radius: 5px 5px 5px 5px; |
|
75 | 75 | } |
|
76 | 76 | |
|
77 | 77 | .navigation_panel .link { |
|
78 | 78 | border-right: 1px solid #000; |
|
79 | 79 | font-weight: bold; |
|
80 | 80 | margin-right: 1ex; |
|
81 | 81 | padding-right: 1ex; |
|
82 | 82 | } |
|
83 | 83 | .navigation_panel .link:last-child { |
|
84 | 84 | border-left: 1px solid #000; |
|
85 | 85 | border-right: none; |
|
86 | 86 | float: right; |
|
87 | 87 | margin-left: 1ex; |
|
88 | 88 | margin-right: 0; |
|
89 | 89 | padding-left: 1ex; |
|
90 | 90 | padding-right: 0; |
|
91 | 91 | } |
|
92 | 92 | |
|
93 | 93 | .navigation_panel::after, .post::after { |
|
94 | 94 | clear: both; |
|
95 | 95 | content: "."; |
|
96 | 96 | display: block; |
|
97 | 97 | height: 0; |
|
98 | 98 | line-height: 0; |
|
99 | 99 | visibility: hidden; |
|
100 | 100 | } |
|
101 | 101 | |
|
102 | 102 | p { |
|
103 | 103 | margin-top: .5em; |
|
104 | 104 | margin-bottom: .5em; |
|
105 | 105 | } |
|
106 | 106 | |
|
107 | 107 | .post-form-w { |
|
108 | 108 | display: table; |
|
109 | 109 | padding: 10px; |
|
110 | 110 | margin: 5px |
|
111 | 111 | } |
|
112 | 112 | |
|
113 | 113 | .form-row { |
|
114 | 114 | display: table-row; |
|
115 | 115 | } |
|
116 | 116 | |
|
117 | 117 | .form-label, .form-input, .form-errors { |
|
118 | 118 | display: table-cell; |
|
119 | 119 | } |
|
120 | 120 | |
|
121 | 121 | .form-label { |
|
122 | 122 | padding: .25em 1ex .25em 0; |
|
123 | 123 | vertical-align: top; |
|
124 | 124 | } |
|
125 | 125 | |
|
126 | 126 | .form-input { |
|
127 | 127 | padding: .25em 0; |
|
128 | 128 | } |
|
129 | 129 | |
|
130 | 130 | .form-errors { |
|
131 | 131 | padding-left: 1ex; |
|
132 | 132 | font-weight: bold; |
|
133 | 133 | vertical-align: middle; |
|
134 | 134 | } |
|
135 | 135 | |
|
136 | 136 | .post-form input:not([name="image"]), .post-form textarea { |
|
137 | 137 | background: #fff; |
|
138 | 138 | color: #000; |
|
139 | 139 | border: solid 1px; |
|
140 | 140 | padding: 0; |
|
141 | 141 | width: 100%; |
|
142 | 142 | font: medium sans; |
|
143 | 143 | } |
|
144 | 144 | |
|
145 | 145 | .form-submit { |
|
146 | 146 | border-bottom: 2px solid #ddd; |
|
147 | 147 | margin-bottom: .5em; |
|
148 | 148 | padding-bottom: .5em; |
|
149 | 149 | } |
|
150 | 150 | |
|
151 | 151 | .form-title { |
|
152 | 152 | font-weight: bold; |
|
153 | 153 | } |
|
154 | 154 | |
|
155 | 155 | input[type="submit"] { |
|
156 | 156 | background: #fff; |
|
157 | 157 | border: solid 1px #000; |
|
158 | 158 | color: #000; |
|
159 | 159 | } |
|
160 | 160 | |
|
161 | 161 | blockquote { |
|
162 | 162 | border-left: solid 2px; |
|
163 | 163 | padding-left: 5px; |
|
164 | 164 | color: #B1FB17; |
|
165 | 165 | margin: 0; |
|
166 | 166 | } |
|
167 | 167 | |
|
168 | 168 | .post > .image { |
|
169 | 169 | float: left; |
|
170 | 170 | margin: 0 1ex .5ex 0; |
|
171 | 171 | min-width: 1px; |
|
172 | 172 | text-align: center; |
|
173 | 173 | display: table-row; |
|
174 | 174 | } |
|
175 | 175 | |
|
176 | 176 | .post > .metadata { |
|
177 | 177 | clear: left; |
|
178 | 178 | } |
|
179 | 179 | |
|
180 | 180 | .get { |
|
181 | 181 | font-weight: bold; |
|
182 | 182 | color: #d55; |
|
183 | 183 | } |
|
184 | 184 | |
|
185 | 185 | * { |
|
186 | 186 | text-decoration: none; |
|
187 | 187 | } |
|
188 | 188 | |
|
189 | 189 | .dead_post { |
|
190 | 190 | border-top: solid #d5494f; |
|
191 | 191 | } |
|
192 | 192 | |
|
193 | 193 | .archive_post { |
|
194 | 194 | border-top: solid #575e9f; |
|
195 | 195 | } |
|
196 | 196 | |
|
197 | 197 | .quote { |
|
198 | 198 | color: #080; |
|
199 | 199 | font-style: italic; |
|
200 | 200 | } |
|
201 | 201 | |
|
202 | 202 | .spoiler { |
|
203 | 203 | background: white; |
|
204 | 204 | color: white; |
|
205 | 205 | } |
|
206 | 206 | |
|
207 | 207 | .spoiler:hover { |
|
208 | 208 | color: black; |
|
209 | 209 | } |
|
210 | 210 | |
|
211 | 211 | .comment { |
|
212 | 212 | color: #8B6914; |
|
213 | 213 | font-style: italic; |
|
214 | 214 | } |
|
215 | 215 | |
|
216 | 216 | a:hover { |
|
217 | 217 | text-decoration: underline; |
|
218 | 218 | } |
|
219 | 219 | |
|
220 | 220 | .last-replies { |
|
221 | 221 | margin-left: 3ex; |
|
222 | 222 | } |
|
223 | 223 | |
|
224 | 224 | .thread { |
|
225 | 225 | margin-bottom: 3ex; |
|
226 | 226 | } |
|
227 | 227 | |
|
228 | 228 | .post:target { |
|
229 | 229 | border: solid 2px black; |
|
230 | 230 | } |
|
231 | 231 | |
|
232 | 232 | pre{ |
|
233 | 233 | white-space:pre-wrap |
|
234 | 234 | } |
|
235 | 235 | |
|
236 | 236 | li { |
|
237 | 237 | list-style-position: inside; |
|
238 | 238 | } |
|
239 | 239 | |
|
240 | 240 | .fancybox-skin { |
|
241 | 241 | position: relative; |
|
242 | 242 | background-color: #fff; |
|
243 | 243 | color: #ddd; |
|
244 | 244 | text-shadow: none; |
|
245 | 245 | } |
|
246 | 246 | |
|
247 | 247 | .fancybox-image { |
|
248 | 248 | border: 1px solid black; |
|
249 | 249 | } |
|
250 | 250 | |
|
251 | 251 | .image-mode-tab { |
|
252 | 252 | display: table; |
|
253 | 253 | margin: 5px; |
|
254 | 254 | padding: 5px; |
|
255 | 255 | background: rgb(221, 221, 221); |
|
256 | 256 | border: 1px solid rgb(204, 204, 204); |
|
257 | 257 | border-radius: 5px 5px 5px 5px; |
|
258 | 258 | } |
|
259 | 259 | |
|
260 | 260 | .image-mode-tab > label { |
|
261 | 261 | margin: 0 1ex; |
|
262 | 262 | } |
|
263 | 263 | |
|
264 | 264 | .image-mode-tab > label > input { |
|
265 | 265 | margin-right: .5ex; |
|
266 | 266 | } |
|
267 | 267 | |
|
268 | 268 | #posts-table { |
|
269 | 269 | margin: 5px; |
|
270 | 270 | } |
|
271 | 271 | |
|
272 | 272 | .tag_info, .page_link { |
|
273 | 273 | display: table; |
|
274 | 274 | } |
|
275 | 275 | |
|
276 | 276 | .tag_info > h2 { |
|
277 | 277 | margin: 0; |
|
278 | 278 | } |
|
279 | 279 | |
|
280 | 280 | .moderator_info { |
|
281 | 281 | color: #e99d41; |
|
282 | 282 | border: dashed 1px; |
|
283 | 283 | padding: 3px; |
|
284 | 284 | } |
|
285 | 285 | |
|
286 | 286 | .refmap { |
|
287 | 287 | font-size: 0.9em; |
|
288 | 288 | color: #444; |
|
289 | 289 | margin-top: 1em; |
|
290 | 290 | } |
|
291 | 291 | |
|
292 | 292 | input[type="submit"]:hover { |
|
293 | 293 | background: #ccc; |
|
294 | 294 | } |
|
295 | 295 | |
|
296 | 296 | |
|
297 | 297 | .fav { |
|
298 | 298 | color: rgb(255, 102, 0); |
|
299 | 299 | } |
|
300 | 300 | |
|
301 | 301 | .not_fav { |
|
302 | 302 | color: #555; |
|
303 | 303 | } |
|
304 | 304 | |
|
305 | 305 | .role { |
|
306 | 306 | text-decoration: underline; |
|
307 | 307 | } |
|
308 | 308 | |
|
309 | 309 | .form-email { |
|
310 | 310 | display: none; |
|
311 | 311 | } |
|
312 | 312 | |
|
313 | 313 | .mark_btn { |
|
314 | 314 | padding: 2px 2ex; |
|
315 | 315 | border: 1px solid; |
|
316 | 316 | } |
|
317 | 317 | |
|
318 | 318 | .mark_btn:hover { |
|
319 | 319 | background: #ccc; |
|
320 | 320 | } |
|
321 | 321 | |
|
322 | 322 | .bar-value { |
|
323 | 323 | background: rgba(251, 199, 16, 0.61); |
|
324 | 324 | padding: 2px; |
|
325 | 325 | font-size: 0.9em; |
|
326 | 326 | height: 1.5em; |
|
327 | 327 | } |
|
328 | 328 | |
|
329 | 329 | .bar-bg { |
|
330 | 330 | position: relative; |
|
331 | 331 | border: 1px solid rgb(204, 204, 204); |
|
332 | 332 | border-radius: 5px 5px 5px 5px; |
|
333 | 333 | margin: 5px; |
|
334 | 334 | overflow: hidden; |
|
335 | 335 | } |
|
336 | 336 | |
|
337 | 337 | .bar-text { |
|
338 | 338 | padding: 2px; |
|
339 | 339 | position: absolute; |
|
340 | 340 | left: 0; |
|
341 | 341 | top: 0; |
|
342 | 342 | } |
|
343 | 343 | |
|
344 | 344 | .skipped_replies { |
|
345 | 345 | margin: 5px; |
|
346 | 346 | } |
|
347 | 347 | |
|
348 | 348 | .current_page, .current_mode { |
|
349 | 349 | border: solid 1px #000; |
|
350 | 350 | padding: 2px; |
|
351 | 351 | } |
|
352 | 352 | |
|
353 | 353 | .tag_item { |
|
354 | 354 | display: inline-block; |
|
355 | 355 | border: 1px solid #ccc; |
|
356 | 356 | margin: 0.3ex; |
|
357 | 357 | padding: 0.2ex; |
|
358 | 358 | } |
|
359 | 359 | |
|
360 | 360 | .multiquote { |
|
361 | 361 | padding: 3px; |
|
362 | 362 | display: inline-block; |
|
363 | 363 | background: #ddd; |
|
364 | 364 | border-style: solid; |
|
365 | 365 | border-width: 1px 1px 1px 4px; |
|
366 | 366 | border-color: #222; |
|
367 | 367 | font-size: 0.9em; |
|
368 | 368 | } |
|
369 | 369 | |
|
370 | 370 | .highlight { |
|
371 | 371 | background-color: #F9E8A5; |
|
372 | 372 | } |
|
373 | 373 | |
|
374 | 374 | #preview-text { |
|
375 | 375 | border: solid 1px black; |
|
376 | 376 | margin: 1ex 0 1ex 0; |
|
377 | 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 |
@@ -1,409 +1,418 b'' | |||
|
1 | 1 | * { |
|
2 | 2 | font-size: inherit; |
|
3 | 3 | margin: 0; |
|
4 | 4 | padding: 0; |
|
5 | 5 | } |
|
6 | 6 | html { |
|
7 | 7 | background: #fff; |
|
8 | 8 | color: #000; |
|
9 | 9 | font: medium sans-serif; |
|
10 | 10 | } |
|
11 | 11 | a { |
|
12 | 12 | color: inherit; |
|
13 | 13 | text-decoration: underline; |
|
14 | 14 | } |
|
15 | 15 | li { |
|
16 | 16 | list-style-position: inside; |
|
17 | 17 | } |
|
18 | 18 | |
|
19 | 19 | #admin_panel { |
|
20 | 20 | background: #182F6F; |
|
21 | 21 | color: #fff; |
|
22 | 22 | padding: .5ex 1ex .5ex 1ex; |
|
23 | 23 | } |
|
24 | 24 | |
|
25 | 25 | .navigation_panel { |
|
26 | 26 | background: #182F6F; |
|
27 | 27 | color: #B4CFEC; |
|
28 | 28 | margin-bottom: 1em; |
|
29 | 29 | padding: .5ex 1ex 1ex 1ex; |
|
30 | 30 | } |
|
31 | 31 | .navigation_panel::after { |
|
32 | 32 | clear: both; |
|
33 | 33 | content: "."; |
|
34 | 34 | display: block; |
|
35 | 35 | height: 0; |
|
36 | 36 | line-height: 0; |
|
37 | 37 | visibility: hidden; |
|
38 | 38 | } |
|
39 | 39 | |
|
40 | 40 | .navigation_panel a:link, .navigation_panel a:visited, .navigation_panel a:hover { |
|
41 | 41 | text-decoration: none; |
|
42 | 42 | } |
|
43 | 43 | |
|
44 | 44 | .navigation_panel .link { |
|
45 | 45 | border-right: 1px solid #fff; |
|
46 | 46 | color: #fff; |
|
47 | 47 | font-weight: bold; |
|
48 | 48 | margin-right: 1ex; |
|
49 | 49 | padding-right: 1ex; |
|
50 | 50 | } |
|
51 | 51 | .navigation_panel .right-link { |
|
52 | 52 | border-left: 1px solid #fff; |
|
53 | 53 | border-right: none; |
|
54 | 54 | float: right; |
|
55 | 55 | margin-left: 1ex; |
|
56 | 56 | margin-right: 0; |
|
57 | 57 | padding-left: 1ex; |
|
58 | 58 | padding-right: 0; |
|
59 | 59 | } |
|
60 | 60 | |
|
61 | 61 | .navigation_panel .tag { |
|
62 | 62 | color: #fff; |
|
63 | 63 | } |
|
64 | 64 | |
|
65 | 65 | .input_field { |
|
66 | 66 | |
|
67 | 67 | } |
|
68 | 68 | |
|
69 | 69 | .input_field_name { |
|
70 | 70 | |
|
71 | 71 | } |
|
72 | 72 | |
|
73 | 73 | .input_field_error { |
|
74 | 74 | color: #FF0000; |
|
75 | 75 | } |
|
76 | 76 | |
|
77 | 77 | |
|
78 | 78 | .title { |
|
79 | 79 | color: #182F6F; |
|
80 | 80 | font-weight: bold; |
|
81 | 81 | } |
|
82 | 82 | |
|
83 | 83 | .post-form-w { |
|
84 | 84 | background: #182F6F; |
|
85 | 85 | border-radius: 1ex; |
|
86 | 86 | color: #fff; |
|
87 | 87 | margin: 1em 1ex; |
|
88 | 88 | padding: 1ex; |
|
89 | 89 | } |
|
90 | 90 | |
|
91 | 91 | .form-row { |
|
92 | 92 | display: table; |
|
93 | 93 | width: 100%; |
|
94 | 94 | } |
|
95 | 95 | .form-label, .form-input { |
|
96 | 96 | display: table-cell; |
|
97 | 97 | vertical-align: top; |
|
98 | 98 | } |
|
99 | 99 | .form-label { |
|
100 | 100 | padding: .25em 1ex .25em 0; |
|
101 | 101 | width: 14ex; |
|
102 | 102 | } |
|
103 | 103 | .form-input { |
|
104 | 104 | padding: .25em 0; |
|
105 | 105 | } |
|
106 | 106 | .form-input > * { |
|
107 | 107 | background: #fff; |
|
108 | 108 | color: #000; |
|
109 | 109 | border: none; |
|
110 | 110 | padding: 0; |
|
111 | 111 | resize: vertical; |
|
112 | 112 | } |
|
113 | 113 | |
|
114 | 114 | .form-input > :not(.file_wrap) { |
|
115 | 115 | width: 100%; |
|
116 | 116 | } |
|
117 | 117 | |
|
118 | 118 | .form-submit { |
|
119 | 119 | border-bottom: 1px solid #666; |
|
120 | 120 | margin-bottom: .5em; |
|
121 | 121 | padding-bottom: .5em; |
|
122 | 122 | } |
|
123 | 123 | .form-title { |
|
124 | 124 | font-weight: bold; |
|
125 | 125 | margin-bottom: .5em; |
|
126 | 126 | } |
|
127 | 127 | .post-form .settings_item { |
|
128 | 128 | margin: .5em 0; |
|
129 | 129 | } |
|
130 | 130 | .form-submit input { |
|
131 | 131 | margin-top: .5em; |
|
132 | 132 | padding: .2em 1ex; |
|
133 | 133 | } |
|
134 | 134 | .form-label { |
|
135 | 135 | text-align: left; |
|
136 | 136 | } |
|
137 | 137 | |
|
138 | 138 | .block { |
|
139 | 139 | display: inline-block; |
|
140 | 140 | vertical-align: top; |
|
141 | 141 | } |
|
142 | 142 | |
|
143 | 143 | .post_id { |
|
144 | 144 | color: #a00; |
|
145 | 145 | } |
|
146 | 146 | |
|
147 | 147 | .post { |
|
148 | 148 | clear: left; |
|
149 | 149 | margin: 0 1ex 1em 1ex; |
|
150 | 150 | overflow-x: auto; |
|
151 | 151 | word-wrap: break-word; |
|
152 | 152 | background: #FFF; |
|
153 | 153 | padding: 1ex; |
|
154 | 154 | border: 1px solid #666; |
|
155 | 155 | box-shadow: 1px 1px 2px 1px #666; |
|
156 | 156 | } |
|
157 | 157 | |
|
158 | 158 | #posts > .post:last-child { |
|
159 | 159 | border-bottom: none; |
|
160 | 160 | padding-bottom: 0; |
|
161 | 161 | } |
|
162 | 162 | |
|
163 | 163 | .metadata { |
|
164 | 164 | background: #C0E4E8; |
|
165 | 165 | border: 1px solid #7F9699; |
|
166 | 166 | border-radius: .4ex; |
|
167 | 167 | display: table; |
|
168 | 168 | margin-top: .5em; |
|
169 | 169 | padding: .4em; |
|
170 | 170 | } |
|
171 | 171 | |
|
172 | 172 | .post ul, .post ol { |
|
173 | 173 | margin: .5em 0 .5em 3ex; |
|
174 | 174 | } |
|
175 | 175 | .post li { |
|
176 | 176 | margin: .2em 0; |
|
177 | 177 | } |
|
178 | 178 | .post p { |
|
179 | 179 | margin: .5em 0; |
|
180 | 180 | } |
|
181 | 181 | .post blockquote { |
|
182 | 182 | border-left: 3px solid #182F6F; |
|
183 | 183 | margin: .5em 0 .5em 3ex; |
|
184 | 184 | padding-left: 1ex; |
|
185 | 185 | } |
|
186 | 186 | .post blockquote > blockquote { |
|
187 | 187 | padding-top: .1em; |
|
188 | 188 | } |
|
189 | 189 | |
|
190 | 190 | .post > .image { |
|
191 | 191 | float: left; |
|
192 | 192 | margin-right: 1ex; |
|
193 | 193 | } |
|
194 | 194 | .post > .metadata { |
|
195 | 195 | clear: left; |
|
196 | 196 | } |
|
197 | 197 | |
|
198 | 198 | .post > .message .get { |
|
199 | 199 | color: #182F6F; font-weight: bold; |
|
200 | 200 | } |
|
201 | 201 | |
|
202 | 202 | .dead_post > .metadata { |
|
203 | 203 | background: #eee; |
|
204 | 204 | } |
|
205 | 205 | |
|
206 | 206 | .quote, .multiquote { |
|
207 | 207 | color: #182F6F; |
|
208 | 208 | } |
|
209 | 209 | |
|
210 | 210 | .spoiler { |
|
211 | 211 | background: black; |
|
212 | 212 | color: black; |
|
213 | 213 | } |
|
214 | 214 | |
|
215 | 215 | .spoiler:hover { |
|
216 | 216 | background: #ffffff; |
|
217 | 217 | } |
|
218 | 218 | |
|
219 | 219 | .comment { |
|
220 | 220 | color: #557055; |
|
221 | 221 | } |
|
222 | 222 | |
|
223 | 223 | .last-replies { |
|
224 | 224 | margin-left: 6ex; |
|
225 | 225 | } |
|
226 | 226 | |
|
227 | 227 | .thread > .post > .message > .post-info { |
|
228 | 228 | border-bottom: 1px solid #ccc; |
|
229 | 229 | padding-bottom: .5em; |
|
230 | 230 | } |
|
231 | 231 | |
|
232 | 232 | :target .post_id { |
|
233 | 233 | background: #182F6F; |
|
234 | 234 | color: #FFF; |
|
235 | 235 | text-decoration: none; |
|
236 | 236 | } |
|
237 | 237 | |
|
238 | 238 | .image-mode-tab { |
|
239 | 239 | background: #182F6F; |
|
240 | 240 | color: #FFF; |
|
241 | 241 | display: table; |
|
242 | 242 | margin: 1em auto 1em 0; |
|
243 | 243 | padding: .2em .5ex; |
|
244 | 244 | } |
|
245 | 245 | |
|
246 | 246 | .image-mode-tab > label { |
|
247 | 247 | margin: 0 1ex; |
|
248 | 248 | } |
|
249 | 249 | |
|
250 | 250 | .image-mode-tab > label > input { |
|
251 | 251 | margin-right: .5ex; |
|
252 | 252 | } |
|
253 | 253 | |
|
254 | 254 | .tag_info, .page_link { |
|
255 | 255 | margin: 1em 0; |
|
256 | 256 | text-align: center; |
|
257 | 257 | } |
|
258 | 258 | |
|
259 | 259 | .form-errors { |
|
260 | 260 | margin-left: 1ex; |
|
261 | 261 | } |
|
262 | 262 | |
|
263 | 263 | .moderator_info { |
|
264 | 264 | font-weight: bold; |
|
265 | 265 | float: right; |
|
266 | 266 | } |
|
267 | 267 | |
|
268 | 268 | .refmap { |
|
269 | 269 | border: 1px dashed #aaa; |
|
270 | 270 | padding: 0.5em; |
|
271 | 271 | display: table; |
|
272 | 272 | } |
|
273 | 273 | |
|
274 | 274 | .fav { |
|
275 | 275 | color: blue; |
|
276 | 276 | } |
|
277 | 277 | |
|
278 | 278 | .not_fav { |
|
279 | 279 | color: #ccc; |
|
280 | 280 | } |
|
281 | 281 | |
|
282 | 282 | .role { |
|
283 | 283 | text-decoration: underline; |
|
284 | 284 | } |
|
285 | 285 | |
|
286 | 286 | .form-email { |
|
287 | 287 | display: none; |
|
288 | 288 | } |
|
289 | 289 | |
|
290 | 290 | .bar-value { |
|
291 | 291 | background: #E3E7F2; |
|
292 | 292 | padding: .1em 1ex; |
|
293 | 293 | moz-box-sizing: border-box; |
|
294 | 294 | box-sizing: border-box; |
|
295 | 295 | height: 1.5em; |
|
296 | 296 | } |
|
297 | 297 | |
|
298 | 298 | .bar-bg { |
|
299 | 299 | background: #EA4649; |
|
300 | 300 | border: 1px solid #666; |
|
301 | 301 | margin: 0 1ex 1em 1ex; |
|
302 | 302 | position: relative; |
|
303 | 303 | overflow: hidden; |
|
304 | 304 | } |
|
305 | 305 | |
|
306 | 306 | .bar-text { |
|
307 | 307 | padding: 2px; |
|
308 | 308 | position: absolute; |
|
309 | 309 | left: 0; |
|
310 | 310 | top: 0; |
|
311 | 311 | } |
|
312 | 312 | |
|
313 | 313 | .skipped_replies { |
|
314 | 314 | margin: 1ex; |
|
315 | 315 | } |
|
316 | 316 | |
|
317 | 317 | #mark-panel { |
|
318 | 318 | background: #eee; |
|
319 | 319 | border-bottom: 1px solid #182F6F; |
|
320 | 320 | } |
|
321 | 321 | |
|
322 | 322 | .mark_btn { |
|
323 | 323 | display: inline-block; |
|
324 | 324 | padding: .2em 1ex; |
|
325 | 325 | border-left: 1px solid #182F6F; |
|
326 | 326 | } |
|
327 | 327 | |
|
328 | 328 | .mark_btn:first-child { |
|
329 | 329 | border-left: none; |
|
330 | 330 | } |
|
331 | 331 | |
|
332 | 332 | .mark_btn:last-child { |
|
333 | 333 | border-right: 1px solid #182F6F; |
|
334 | 334 | } |
|
335 | 335 | |
|
336 | 336 | .current_page { |
|
337 | 337 | border-bottom: 1px solid #FFF; |
|
338 | 338 | padding: 0px 0.5ex; |
|
339 | 339 | } |
|
340 | 340 | |
|
341 | 341 | .image-mode-tab a { |
|
342 | 342 | text-decoration: none; |
|
343 | 343 | } |
|
344 | 344 | .image-mode-tab .current_mode::before { |
|
345 | 345 | content: "✓ "; |
|
346 | 346 | padding: 0 0 0 .5ex; |
|
347 | 347 | color: #182F6F; |
|
348 | 348 | background: #FFF; |
|
349 | 349 | } |
|
350 | 350 | .image-mode-tab .current_mode { |
|
351 | 351 | padding: 0 .5ex 0 0; |
|
352 | 352 | color: #182F6F; |
|
353 | 353 | background: #FFF; |
|
354 | 354 | } |
|
355 | 355 | |
|
356 | 356 | .gallery_image_metadata { |
|
357 | 357 | margin-bottom: 1em; |
|
358 | 358 | } |
|
359 | 359 | |
|
360 | 360 | .gallery_image { |
|
361 | 361 | padding: 4px; |
|
362 | 362 | margin: .5em 0 0 1ex; |
|
363 | 363 | text-align: center; |
|
364 | 364 | vertical-align: top; |
|
365 | 365 | } |
|
366 | 366 | |
|
367 | 367 | .swappable-form-full > form { |
|
368 | 368 | display: table; |
|
369 | 369 | width: 100%; |
|
370 | 370 | } |
|
371 | 371 | |
|
372 | 372 | #id_models li { |
|
373 | 373 | list-style: none; |
|
374 | 374 | } |
|
375 | 375 | |
|
376 | 376 | #id_q { |
|
377 | 377 | margin-left: 1ex; |
|
378 | 378 | } |
|
379 | 379 | |
|
380 | 380 | .br { |
|
381 | 381 | margin-top: 0.5em; |
|
382 | 382 | margin-bottom: 0.5em; |
|
383 | 383 | } |
|
384 | 384 | |
|
385 | 385 | .message, .refmap { |
|
386 | 386 | margin-top: .5em; |
|
387 | 387 | } |
|
388 | 388 | |
|
389 | 389 | .user-cast { |
|
390 | 390 | padding: 0.2em .5ex; |
|
391 | 391 | background: #008; |
|
392 | 392 | color: #FFF; |
|
393 | 393 | display: inline-block; |
|
394 | 394 | text-decoration: none; |
|
395 | 395 | } |
|
396 | 396 | |
|
397 | 397 | .highlight { |
|
398 | 398 | background-color: #D4F0F9; |
|
399 | 399 | } |
|
400 | 400 | |
|
401 | 401 | .dead_post { |
|
402 | 402 | border-right: 1ex solid #666; |
|
403 | 403 | } |
|
404 | 404 | |
|
405 | 405 | #preview-text { |
|
406 | 406 | border: solid 1px white; |
|
407 | 407 | margin: 1ex 0 1ex 0; |
|
408 | 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 | } |
@@ -1,148 +1,160 b'' | |||
|
1 | 1 | /* |
|
2 | 2 | @licstart The following is the entire license notice for the |
|
3 | 3 | JavaScript code in this page. |
|
4 | 4 | |
|
5 | 5 | |
|
6 | 6 | Copyright (C) 2013 neko259 |
|
7 | 7 | |
|
8 | 8 | The JavaScript code in this page is free software: you can |
|
9 | 9 | redistribute it and/or modify it under the terms of the GNU |
|
10 | 10 | General Public License (GNU GPL) as published by the Free Software |
|
11 | 11 | Foundation, either version 3 of the License, or (at your option) |
|
12 | 12 | any later version. The code is distributed WITHOUT ANY WARRANTY; |
|
13 | 13 | without even the implied warranty of MERCHANTABILITY or FITNESS |
|
14 | 14 | FOR A PARTICULAR PURPOSE. See the GNU GPL for more details. |
|
15 | 15 | |
|
16 | 16 | As additional permission under GNU GPL version 3 section 7, you |
|
17 | 17 | may distribute non-source (e.g., minimized or compacted) forms of |
|
18 | 18 | that code without the copy of the GNU GPL normally required by |
|
19 | 19 | section 4, provided you include this license notice and a URL |
|
20 | 20 | through which recipients can access the Corresponding Source. |
|
21 | 21 | |
|
22 | 22 | @licend The above is the entire license notice |
|
23 | 23 | for the JavaScript code in this page. |
|
24 | 24 | */ |
|
25 | 25 | |
|
26 | 26 | |
|
27 | 27 | var IMAGE_VIEWERS = [ |
|
28 | 28 | ['simple', new SimpleImageViewer()], |
|
29 | 29 | ['popup', new PopupImageViewer()] |
|
30 | 30 | ]; |
|
31 | 31 | |
|
32 | 32 | var FULL_IMG_CLASS = 'post-image-full'; |
|
33 | 33 | |
|
34 | 34 | |
|
35 | 35 | function ImageViewer() {} |
|
36 | 36 | ImageViewer.prototype.view = function (post) {}; |
|
37 | 37 | |
|
38 | 38 | function SimpleImageViewer() {} |
|
39 | 39 | SimpleImageViewer.prototype.view = function (post) { |
|
40 | 40 | var images = post.find('img'); |
|
41 | 41 | images.toggle(); |
|
42 | 42 | |
|
43 | 43 | // When we first enlarge an image, a full image needs to be created |
|
44 | 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 | 55 | var parent = images.first().parent(); |
|
46 | 56 | var link = parent.attr('href'); |
|
47 | 57 | |
|
48 | 58 | var fullImg = $('<img />') |
|
49 | 59 | .addClass(FULL_IMG_CLASS) |
|
50 |
.attr('src', link) |
|
|
60 | .attr('src', link) | |
|
61 | .attr('width', width) | |
|
62 | .attr('height', height); | |
|
51 | 63 | |
|
52 | 64 | parent.append(fullImg); |
|
53 | 65 | } |
|
54 | 66 | }; |
|
55 | 67 | |
|
56 | 68 | function PopupImageViewer() {} |
|
57 | 69 | PopupImageViewer.prototype.view = function (post) { |
|
58 | 70 | var margin = 20; //..change |
|
59 | 71 | |
|
60 | 72 | var el = post; |
|
61 | 73 | var thumb_id = 'full' + el.find('img').attr('alt'); |
|
62 | 74 | |
|
63 | 75 | var existingPopups = $('#' + thumb_id); |
|
64 | 76 | if(!existingPopups.length) { |
|
65 | 77 | var imgElement= el.find('img'); |
|
66 | 78 | |
|
67 | 79 | var img_w = imgElement.attr('data-width'); |
|
68 | 80 | var img_h = imgElement.attr('data-height'); |
|
69 | 81 | |
|
70 | 82 | var win = $(window); |
|
71 | 83 | |
|
72 | 84 | var win_w = win.width(); |
|
73 | 85 | var win_h = win.height(); |
|
74 | 86 | //new image size |
|
75 | 87 | if (img_w > win_w) { |
|
76 | 88 | img_h = img_h * (win_w/img_w) - margin; |
|
77 | 89 | img_w = win_w - margin; |
|
78 | 90 | } |
|
79 | 91 | if (img_h > win_h) { |
|
80 | 92 | img_w = img_w * (win_h/img_h) - margin; |
|
81 | 93 | img_h = win_h - margin; |
|
82 | 94 | } |
|
83 | 95 | |
|
84 | 96 | var img_pv = new Image(); |
|
85 | 97 | var newImage = $(img_pv); |
|
86 | 98 | newImage.addClass('img-full') |
|
87 | 99 | .attr('id', thumb_id) |
|
88 | 100 | .attr('src', $(el).attr('href')) |
|
89 | 101 | .appendTo($(el)) |
|
90 | 102 | .css({ |
|
91 | 103 | 'width': img_w, |
|
92 | 104 | 'height': img_h, |
|
93 | 105 | 'left': (win_w - img_w) / 2, |
|
94 | 106 | 'top': ((win_h - img_h) / 2) |
|
95 | 107 | }) |
|
96 | 108 | //scaling preview |
|
97 | 109 | .mousewheel(function(event, delta) { |
|
98 | 110 | var cx = event.originalEvent.clientX, |
|
99 | 111 | cy = event.originalEvent.clientY, |
|
100 | 112 | i_w = parseFloat(newImage.width()), |
|
101 | 113 | i_h = parseFloat(newImage.height()), |
|
102 | 114 | newIW = i_w * (delta > 0 ? 1.25 : 0.8), |
|
103 | 115 | newIH = i_h * (delta > 0 ? 1.25 : 0.8); |
|
104 | 116 | |
|
105 | 117 | newImage.width(newIW); |
|
106 | 118 | newImage.height(newIH); |
|
107 | 119 | //set position |
|
108 | 120 | newImage.css({ |
|
109 | 121 | left: parseInt(cx - (newIW/i_w) * (cx - parseInt($(img_pv).position().left, 10)), 10), |
|
110 | 122 | top: parseInt(cy - (newIH/i_h) * (cy - parseInt($(img_pv).position().top, 10)), 10) |
|
111 | 123 | }); |
|
112 | 124 | |
|
113 | 125 | return false; |
|
114 | 126 | } |
|
115 | 127 | ) |
|
116 | 128 | .draggable({ |
|
117 | 129 | addClasses: false, |
|
118 | 130 | stack: '.img-full' |
|
119 | 131 | }); |
|
120 | 132 | } else { |
|
121 | 133 | existingPopups.remove(); |
|
122 | 134 | } |
|
123 | 135 | }; |
|
124 | 136 | |
|
125 | 137 | function addImgPreview() { |
|
126 | 138 | var viewerName = $('body').attr('data-image-viewer'); |
|
127 | 139 | var viewer = ImageViewer(); |
|
128 | 140 | for (var i = 0; i < IMAGE_VIEWERS.length; i++) { |
|
129 | 141 | var item = IMAGE_VIEWERS[i]; |
|
130 | 142 | if (item[0] === viewerName) { |
|
131 | 143 | viewer = item[1]; |
|
132 | 144 | break; |
|
133 | 145 | } |
|
134 | 146 | } |
|
135 | 147 | |
|
136 | 148 | //keybind |
|
137 | 149 | $(document).on('keyup.removepic', function(e) { |
|
138 | 150 | if(e.which === 27) { |
|
139 | 151 | $('.img-full').remove(); |
|
140 | 152 | } |
|
141 | 153 | }); |
|
142 | 154 | |
|
143 | 155 | $('body').on('click', '.thumb', function() { |
|
144 | 156 | viewer.view($(this)); |
|
145 | 157 | |
|
146 | 158 | return false; |
|
147 | 159 | }); |
|
148 | 160 | } |
@@ -1,103 +1,109 b'' | |||
|
1 | 1 | /* |
|
2 | 2 | @licstart The following is the entire license notice for the |
|
3 | 3 | JavaScript code in this page. |
|
4 | 4 | |
|
5 | 5 | |
|
6 | 6 | Copyright (C) 2013 neko259 |
|
7 | 7 | |
|
8 | 8 | The JavaScript code in this page is free software: you can |
|
9 | 9 | redistribute it and/or modify it under the terms of the GNU |
|
10 | 10 | General Public License (GNU GPL) as published by the Free Software |
|
11 | 11 | Foundation, either version 3 of the License, or (at your option) |
|
12 | 12 | any later version. The code is distributed WITHOUT ANY WARRANTY; |
|
13 | 13 | without even the implied warranty of MERCHANTABILITY or FITNESS |
|
14 | 14 | FOR A PARTICULAR PURPOSE. See the GNU GPL for more details. |
|
15 | 15 | |
|
16 | 16 | As additional permission under GNU GPL version 3 section 7, you |
|
17 | 17 | may distribute non-source (e.g., minimized or compacted) forms of |
|
18 | 18 | that code without the copy of the GNU GPL normally required by |
|
19 | 19 | section 4, provided you include this license notice and a URL |
|
20 | 20 | through which recipients can access the Corresponding Source. |
|
21 | 21 | |
|
22 | 22 | @licend The above is the entire license notice |
|
23 | 23 | for the JavaScript code in this page. |
|
24 | 24 | */ |
|
25 | 25 | |
|
26 | 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 | 30 | var $html = $("html, body"); |
|
28 | 31 | |
|
29 | 32 | function moveCaretToEnd(el) { |
|
30 | 33 | if (typeof el.selectionStart == "number") { |
|
31 | 34 | el.selectionStart = el.selectionEnd = el.value.length; |
|
32 | 35 | } else if (typeof el.createTextRange != "undefined") { |
|
33 | 36 | el.focus(); |
|
34 | 37 | var range = el.createTextRange(); |
|
35 | 38 | range.collapse(false); |
|
36 | 39 | range.select(); |
|
37 | 40 | } |
|
38 | 41 | } |
|
39 | 42 | |
|
40 | 43 | function getForm() { |
|
41 | 44 | return $('.post-form-w'); |
|
42 | 45 | } |
|
43 | 46 | |
|
44 | 47 | function resetFormPosition() { |
|
45 | 48 | var form = getForm(); |
|
46 | 49 | form.insertAfter($('.thread')); |
|
47 | 50 | |
|
48 | 51 | $(CLOSE_BUTTON).hide(); |
|
52 | $(REPLY_TO_MSG).hide(); | |
|
49 | 53 | } |
|
50 | 54 | |
|
51 | 55 | function showFormAfter(blockToInsertAfter) { |
|
52 | 56 | var form = getForm(); |
|
53 | 57 | form.insertAfter(blockToInsertAfter); |
|
54 | 58 | |
|
55 | 59 | $(CLOSE_BUTTON).show(); |
|
56 | 60 | form.show(); |
|
61 | $(REPLY_TO_MSG_ID).text(blockToInsertAfter.attr('id')); | |
|
62 | $(REPLY_TO_MSG).show(); | |
|
57 | 63 | } |
|
58 | 64 | |
|
59 | 65 | function addQuickReply(postId) { |
|
60 | 66 | // If we click "reply" on the same post, it means "cancel" |
|
61 | 67 | if (getForm().prev().attr('id') == postId) { |
|
62 | 68 | resetFormPosition(); |
|
63 | 69 | } else { |
|
64 | 70 | var postLinkRaw = '[post]' + postId + '[/post]' |
|
65 | 71 | |
|
66 | 72 | var textToAdd = ''; |
|
67 | 73 | var blockToInsert = null; |
|
68 | 74 | |
|
69 | 75 | var textAreaJq = $('textarea'); |
|
70 | 76 | |
|
71 | 77 | if (postId != null) { |
|
72 | 78 | var post = $('#' + postId); |
|
73 | 79 | |
|
74 | 80 | // If this is not OP, add reflink to the post. If there already is |
|
75 | 81 | // the same reflink, don't add it again. |
|
76 | 82 | if (!post.is(':first-child') && textAreaJq.val().indexOf(postLinkRaw) < 0) { |
|
77 | 83 | textToAdd += postLinkRaw + '\n'; |
|
78 | 84 | } |
|
79 | 85 | |
|
80 | 86 | blockToInsert = post; |
|
81 | 87 | } else { |
|
82 | 88 | blockToInsert = $('.thread'); |
|
83 | 89 | } |
|
84 | 90 | |
|
85 | 91 | var selection = window.getSelection().toString(); |
|
86 | 92 | if (selection.length > 0) { |
|
87 | 93 | textToAdd += '[quote]' + selection + '[/quote]\n'; |
|
88 | 94 | } |
|
89 | 95 | |
|
90 | 96 | textAreaJq.val(textAreaJq.val()+ textToAdd); |
|
91 | 97 | |
|
92 | 98 | showFormAfter(blockToInsert); |
|
93 | 99 | |
|
94 | 100 | textAreaJq.focus(); |
|
95 | 101 | var textarea = document.getElementsByTagName('textarea')[0]; |
|
96 | 102 | moveCaretToEnd(textarea); |
|
97 | 103 | } |
|
98 | 104 | } |
|
99 | 105 | |
|
100 | 106 | function scrollToBottom() { |
|
101 | 107 | $html.animate({scrollTop: $html.height()}, "fast"); |
|
102 | 108 | } |
|
103 | 109 |
@@ -1,391 +1,456 b'' | |||
|
1 | 1 | /* |
|
2 | 2 | @licstart The following is the entire license notice for the |
|
3 | 3 | JavaScript code in this page. |
|
4 | 4 | |
|
5 | 5 | |
|
6 | 6 | Copyright (C) 2013-2014 neko259 |
|
7 | 7 | |
|
8 | 8 | The JavaScript code in this page is free software: you can |
|
9 | 9 | redistribute it and/or modify it under the terms of the GNU |
|
10 | 10 | General Public License (GNU GPL) as published by the Free Software |
|
11 | 11 | Foundation, either version 3 of the License, or (at your option) |
|
12 | 12 | any later version. The code is distributed WITHOUT ANY WARRANTY; |
|
13 | 13 | without even the implied warranty of MERCHANTABILITY or FITNESS |
|
14 | 14 | FOR A PARTICULAR PURPOSE. See the GNU GPL for more details. |
|
15 | 15 | |
|
16 | 16 | As additional permission under GNU GPL version 3 section 7, you |
|
17 | 17 | may distribute non-source (e.g., minimized or compacted) forms of |
|
18 | 18 | that code without the copy of the GNU GPL normally required by |
|
19 | 19 | section 4, provided you include this license notice and a URL |
|
20 | 20 | through which recipients can access the Corresponding Source. |
|
21 | 21 | |
|
22 | 22 | @licend The above is the entire license notice |
|
23 | 23 | for the JavaScript code in this page. |
|
24 | 24 | */ |
|
25 | 25 | |
|
26 | 26 | var CLASS_POST = '.post' |
|
27 | 27 | |
|
28 | 28 | var POST_ADDED = 0; |
|
29 | 29 | var POST_UPDATED = 1; |
|
30 | 30 | |
|
31 | 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 | 41 | var wsUser = ''; |
|
34 | 42 | |
|
35 | 43 | var unreadPosts = 0; |
|
36 | 44 | var documentOriginalTitle = ''; |
|
37 | 45 | |
|
38 | 46 | // Thread ID does not change, can be stored one time |
|
39 | 47 | var threadId = $('div.thread').children(CLASS_POST).first().attr('id'); |
|
40 | 48 | |
|
41 | 49 | /** |
|
42 | 50 | * Connect to websocket server and subscribe to thread updates. On any update we |
|
43 | 51 | * request a thread diff. |
|
44 | 52 | * |
|
45 | 53 | * @returns {boolean} true if connected, false otherwise |
|
46 | 54 | */ |
|
47 | 55 | function connectWebsocket() { |
|
48 | 56 | var metapanel = $('.metapanel')[0]; |
|
49 | 57 | |
|
50 | 58 | var wsHost = metapanel.getAttribute('data-ws-host'); |
|
51 | 59 | var wsPort = metapanel.getAttribute('data-ws-port'); |
|
52 | 60 | |
|
53 | 61 | if (wsHost.length > 0 && wsPort.length > 0) { |
|
54 | 62 | var centrifuge = new Centrifuge({ |
|
55 | 63 | "url": 'ws://' + wsHost + ':' + wsPort + "/connection/websocket", |
|
56 | 64 | "project": metapanel.getAttribute('data-ws-project'), |
|
57 | 65 | "user": wsUser, |
|
58 | 66 | "timestamp": metapanel.getAttribute('data-ws-token-time'), |
|
59 | 67 | "token": metapanel.getAttribute('data-ws-token'), |
|
60 | 68 | "debug": false |
|
61 | 69 | }); |
|
62 | 70 | |
|
63 | 71 | centrifuge.on('error', function(error_message) { |
|
64 | 72 | console.log("Error connecting to websocket server."); |
|
65 | 73 | console.log(error_message); |
|
66 | 74 | console.log("Using javascript update instead."); |
|
67 | 75 | |
|
68 | 76 | // If websockets don't work, enable JS update instead |
|
69 | 77 | enableJsUpdate() |
|
70 | 78 | }); |
|
71 | 79 | |
|
72 | 80 | centrifuge.on('connect', function() { |
|
73 | 81 | var channelName = 'thread:' + threadId; |
|
74 | 82 | centrifuge.subscribe(channelName, function(message) { |
|
75 | 83 | getThreadDiff(); |
|
76 | 84 | }); |
|
77 | 85 | |
|
78 | 86 | // For the case we closed the browser and missed some updates |
|
79 | 87 | getThreadDiff(); |
|
80 | 88 | $('#autoupdate').hide(); |
|
81 | 89 | }); |
|
82 | 90 | |
|
83 | 91 | centrifuge.connect(); |
|
84 | 92 | |
|
85 | 93 | return true; |
|
86 | 94 | } else { |
|
87 | 95 | return false; |
|
88 | 96 | } |
|
89 | 97 | } |
|
90 | 98 | |
|
91 | 99 | /** |
|
92 | 100 | * Get diff of the posts from the current thread timestamp. |
|
93 | 101 | * This is required if the browser was closed and some post updates were |
|
94 | 102 | * missed. |
|
95 | 103 | */ |
|
96 | 104 | function getThreadDiff() { |
|
97 | 105 | var lastUpdateTime = $('.metapanel').attr('data-last-update'); |
|
98 | 106 | var lastPostId = $('.post').last().attr('id'); |
|
99 | 107 | |
|
100 | 108 | var uids = ''; |
|
101 | 109 | var posts = $('.post'); |
|
102 | 110 | for (var i = 0; i < posts.length; i++) { |
|
103 | 111 | uids += posts[i].getAttribute('data-uid') + ' '; |
|
104 | 112 | } |
|
105 | 113 | |
|
106 | 114 | var data = { |
|
107 | 115 | uids: uids, |
|
108 | 116 | thread: threadId |
|
109 | 117 | } |
|
110 | 118 | |
|
111 | 119 | var diffUrl = '/api/diff_thread/'; |
|
112 | 120 | |
|
113 | 121 | $.post(diffUrl, |
|
114 | 122 | data, |
|
115 | 123 | function(data) { |
|
116 | 124 | var updatedPosts = data.updated; |
|
117 | 125 | var addedPostCount = 0; |
|
118 | 126 | |
|
119 | 127 | for (var i = 0; i < updatedPosts.length; i++) { |
|
120 | 128 | var postText = updatedPosts[i]; |
|
121 | 129 | var post = $(postText); |
|
122 | 130 | |
|
123 | 131 | if (updatePost(post) == POST_ADDED) { |
|
124 | 132 | addedPostCount++; |
|
125 | 133 | } |
|
126 | 134 | } |
|
127 | 135 | |
|
128 | 136 | var hasMetaUpdates = updatedPosts.length > 0; |
|
129 | 137 | if (hasMetaUpdates) { |
|
130 | 138 | updateMetadataPanel(); |
|
131 | 139 | } |
|
132 | 140 | |
|
133 | 141 | if (addedPostCount > 0) { |
|
134 | 142 | updateBumplimitProgress(addedPostCount); |
|
135 | 143 | } |
|
136 | 144 | |
|
137 | 145 | if (updatedPosts.length > 0) { |
|
138 | 146 | showNewPostsTitle(addedPostCount); |
|
139 | 147 | } |
|
140 | 148 | |
|
141 | 149 | // TODO Process removed posts if any |
|
142 | 150 | $('.metapanel').attr('data-last-update', data.last_update); |
|
143 | 151 | }, |
|
144 | 152 | 'json' |
|
145 | 153 | ) |
|
146 | 154 | } |
|
147 | 155 | |
|
148 | 156 | /** |
|
149 | 157 | * Add or update the post on html page. |
|
150 | 158 | */ |
|
151 | 159 | function updatePost(postHtml) { |
|
152 | 160 | // This needs to be set on start because the page is scrolled after posts |
|
153 | 161 | // are added or updated |
|
154 | 162 | var bottom = isPageBottom(); |
|
155 | 163 | |
|
156 | 164 | var post = $(postHtml); |
|
157 | 165 | |
|
158 | 166 | var threadBlock = $('div.thread'); |
|
159 | 167 | |
|
160 | 168 | var postId = post.attr('id'); |
|
161 | 169 | |
|
162 | 170 | // If the post already exists, replace it. Otherwise add as a new one. |
|
163 | 171 | var existingPosts = threadBlock.children('.post[id=' + postId + ']'); |
|
164 | 172 | |
|
165 | 173 | var type; |
|
166 | 174 | |
|
167 | 175 | if (existingPosts.size() > 0) { |
|
168 |
existingPosts. |
|
|
176 | replacePartial(existingPosts.first(), post, false); | |
|
177 | post = existingPosts.first(); | |
|
169 | 178 | |
|
170 | 179 | type = POST_UPDATED; |
|
171 | 180 | } else { |
|
172 | 181 | post.appendTo(threadBlock); |
|
173 | 182 | |
|
174 | 183 | if (bottom) { |
|
175 | 184 | scrollToBottom(); |
|
176 | 185 | } |
|
177 | 186 | |
|
178 | 187 | type = POST_ADDED; |
|
179 | 188 | } |
|
180 | 189 | |
|
181 | 190 | processNewPost(post); |
|
182 | 191 | |
|
183 | 192 | return type; |
|
184 | 193 | } |
|
185 | 194 | |
|
186 | 195 | /** |
|
187 | 196 | * Initiate a blinking animation on a node to show it was updated. |
|
188 | 197 | */ |
|
189 | 198 | function blink(node) { |
|
190 | 199 | var blinkCount = 2; |
|
191 | 200 | |
|
192 | 201 | var nodeToAnimate = node; |
|
193 | 202 | for (var i = 0; i < blinkCount; i++) { |
|
194 | 203 | nodeToAnimate = nodeToAnimate.fadeTo('fast', 0.5).fadeTo('fast', 1.0); |
|
195 | 204 | } |
|
196 | 205 | } |
|
197 | 206 | |
|
198 | 207 | function isPageBottom() { |
|
199 | 208 | var scroll = $(window).scrollTop() / ($(document).height() |
|
200 | 209 | - $(window).height()); |
|
201 | 210 | |
|
202 | 211 | return scroll == 1 |
|
203 | 212 | } |
|
204 | 213 | |
|
205 | 214 | function enableJsUpdate() { |
|
206 | 215 | setInterval(getThreadDiff, JS_AUTOUPDATE_PERIOD); |
|
207 | 216 | return true; |
|
208 | 217 | } |
|
209 | 218 | |
|
210 | 219 | function initAutoupdate() { |
|
211 | 220 | if (location.protocol === 'https:') { |
|
212 | 221 | return enableJsUpdate(); |
|
213 | 222 | } else { |
|
214 | 223 | if (connectWebsocket()) { |
|
215 | 224 | return true; |
|
216 | 225 | } else { |
|
217 | 226 | return enableJsUpdate(); |
|
218 | 227 | } |
|
219 | 228 | } |
|
220 | 229 | } |
|
221 | 230 | |
|
222 | 231 | function getReplyCount() { |
|
223 | 232 | return $('.thread').children(CLASS_POST).length |
|
224 | 233 | } |
|
225 | 234 | |
|
226 | 235 | function getImageCount() { |
|
227 | 236 | return $('.thread').find('img').length |
|
228 | 237 | } |
|
229 | 238 | |
|
230 | 239 | /** |
|
231 | 240 | * Update post count, images count and last update time in the metadata |
|
232 | 241 | * panel. |
|
233 | 242 | */ |
|
234 | 243 | function updateMetadataPanel() { |
|
235 | 244 | var replyCountField = $('#reply-count'); |
|
236 | 245 | var imageCountField = $('#image-count'); |
|
237 | 246 | |
|
238 | 247 | replyCountField.text(getReplyCount()); |
|
239 | 248 | imageCountField.text(getImageCount()); |
|
240 | 249 | |
|
241 | 250 | var lastUpdate = $('.post:last').children('.post-info').first() |
|
242 | 251 | .children('.pub_time').first().html(); |
|
243 | 252 | if (lastUpdate !== '') { |
|
244 | 253 | var lastUpdateField = $('#last-update'); |
|
245 | 254 | lastUpdateField.html(lastUpdate); |
|
246 | 255 | blink(lastUpdateField); |
|
247 | 256 | } |
|
248 | 257 | |
|
249 | 258 | blink(replyCountField); |
|
250 | 259 | blink(imageCountField); |
|
251 | 260 | } |
|
252 | 261 | |
|
253 | 262 | /** |
|
254 | 263 | * Update bumplimit progress bar |
|
255 | 264 | */ |
|
256 | 265 | function updateBumplimitProgress(postDelta) { |
|
257 | 266 | var progressBar = $('#bumplimit_progress'); |
|
258 | 267 | if (progressBar) { |
|
259 | 268 | var postsToLimitElement = $('#left_to_limit'); |
|
260 | 269 | |
|
261 | 270 | var oldPostsToLimit = parseInt(postsToLimitElement.text()); |
|
262 | 271 | var postCount = getReplyCount(); |
|
263 | 272 | var bumplimit = postCount - postDelta + oldPostsToLimit; |
|
264 | 273 | |
|
265 | 274 | var newPostsToLimit = bumplimit - postCount; |
|
266 | 275 | if (newPostsToLimit <= 0) { |
|
267 | 276 | $('.bar-bg').remove(); |
|
268 | 277 | } else { |
|
269 | 278 | postsToLimitElement.text(newPostsToLimit); |
|
270 | 279 | progressBar.width((100 - postCount / bumplimit * 100.0) + '%'); |
|
271 | 280 | } |
|
272 | 281 | } |
|
273 | 282 | } |
|
274 | 283 | |
|
275 | 284 | /** |
|
276 | 285 | * Show 'new posts' text in the title if the document is not visible to a user |
|
277 | 286 | */ |
|
278 | 287 | function showNewPostsTitle(newPostCount) { |
|
279 | 288 | if (document.hidden) { |
|
280 | 289 | if (documentOriginalTitle === '') { |
|
281 | 290 | documentOriginalTitle = document.title; |
|
282 | 291 | } |
|
283 | 292 | unreadPosts = unreadPosts + newPostCount; |
|
284 | 293 | |
|
285 | 294 | var newTitle = '* '; |
|
286 | 295 | if (unreadPosts > 0) { |
|
287 | 296 | newTitle += '[' + unreadPosts + '] '; |
|
288 | 297 | } |
|
289 | 298 | newTitle += documentOriginalTitle; |
|
290 | 299 | |
|
291 | 300 | document.title = newTitle; |
|
292 | 301 | |
|
293 | 302 | document.addEventListener('visibilitychange', function() { |
|
294 | 303 | if (documentOriginalTitle !== '') { |
|
295 | 304 | document.title = documentOriginalTitle; |
|
296 | 305 | documentOriginalTitle = ''; |
|
297 | 306 | unreadPosts = 0; |
|
298 | 307 | } |
|
299 | 308 | |
|
300 | 309 | document.removeEventListener('visibilitychange', null); |
|
301 | 310 | }); |
|
302 | 311 | } |
|
303 | 312 | } |
|
304 | 313 | |
|
305 | 314 | /** |
|
306 | 315 | * Clear all entered values in the form fields |
|
307 | 316 | */ |
|
308 | 317 | function resetForm(form) { |
|
309 | 318 | form.find('input:text, input:password, input:file, select, textarea').val(''); |
|
310 | 319 | form.find('input:radio, input:checkbox') |
|
311 | 320 | .removeAttr('checked').removeAttr('selected'); |
|
312 | 321 | $('.file_wrap').find('.file-thumb').remove(); |
|
313 | 322 | $('#preview-text').hide(); |
|
314 | 323 | } |
|
315 | 324 | |
|
316 | 325 | /** |
|
317 | 326 | * When the form is posted, this method will be run as a callback |
|
318 | 327 | */ |
|
319 | 328 | function updateOnPost(response, statusText, xhr, form) { |
|
320 | 329 | var json = $.parseJSON(response); |
|
321 | 330 | var status = json.status; |
|
322 | 331 | |
|
323 | 332 | showAsErrors(form, ''); |
|
324 | 333 | |
|
325 | 334 | if (status === 'ok') { |
|
326 | 335 | resetFormPosition(); |
|
327 | 336 | resetForm(form); |
|
328 | 337 | getThreadDiff(); |
|
329 | 338 | scrollToBottom(); |
|
330 | 339 | } else { |
|
331 | 340 | var errors = json.errors; |
|
332 | 341 | for (var i = 0; i < errors.length; i++) { |
|
333 | 342 | var fieldErrors = errors[i]; |
|
334 | 343 | |
|
335 | 344 | var error = fieldErrors.errors; |
|
336 | 345 | |
|
337 | 346 | showAsErrors(form, error); |
|
338 | 347 | } |
|
339 | 348 | } |
|
340 | 349 | } |
|
341 | 350 | |
|
342 | 351 | /** |
|
343 | 352 | * Show text in the errors row of the form. |
|
344 | 353 | * @param form |
|
345 | 354 | * @param text |
|
346 | 355 | */ |
|
347 | 356 | function showAsErrors(form, text) { |
|
348 | 357 | form.children('.form-errors').remove(); |
|
349 | 358 | |
|
350 | 359 | if (text.length > 0) { |
|
351 | 360 | var errorList = $('<div class="form-errors">' + text + '<div>'); |
|
352 | 361 | errorList.appendTo(form); |
|
353 | 362 | } |
|
354 | 363 | } |
|
355 | 364 | |
|
356 | 365 | /** |
|
357 | 366 | * Run js methods that are usually run on the document, on the new post |
|
358 | 367 | */ |
|
359 | 368 | function processNewPost(post) { |
|
360 | 369 | addRefLinkPreview(post[0]); |
|
361 | 370 | highlightCode(post); |
|
362 | 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 | 432 | $(document).ready(function(){ |
|
366 | 433 | if (initAutoupdate()) { |
|
367 | 434 | // Post form data over AJAX |
|
368 | 435 | var threadId = $('div.thread').children('.post').first().attr('id'); |
|
369 | 436 | |
|
370 | 437 | var form = $('#form'); |
|
371 | 438 | |
|
372 | 439 | if (form.length > 0) { |
|
373 | 440 | var options = { |
|
374 | 441 | beforeSubmit: function(arr, $form, options) { |
|
375 | 442 | showAsErrors($('form'), gettext('Sending message...')); |
|
376 | 443 | }, |
|
377 | 444 | success: updateOnPost, |
|
378 | 445 | error: function() { |
|
379 | 446 | showAsErrors($('form'), gettext('Server error!')); |
|
380 | 447 | }, |
|
381 | 448 | url: '/api/add_post/' + threadId + '/' |
|
382 | 449 | }; |
|
383 | 450 | |
|
384 | 451 | form.ajaxForm(options); |
|
385 | 452 | |
|
386 | 453 | resetForm(form); |
|
387 | 454 | } |
|
388 | 455 | } |
|
389 | ||
|
390 | $('#autoupdate').click(getThreadDiff); | |
|
391 | 456 | }); |
@@ -1,166 +1,186 b'' | |||
|
1 | 1 | {% extends "boards/base.html" %} |
|
2 | 2 | |
|
3 | 3 | {% load i18n %} |
|
4 | 4 | {% load board %} |
|
5 | 5 | {% load static %} |
|
6 | 6 | {% load tz %} |
|
7 | 7 | |
|
8 | 8 | {% block head %} |
|
9 | 9 | <meta name="robots" content="noindex"> |
|
10 | 10 | |
|
11 | 11 | {% if tag %} |
|
12 | 12 | <title>{{ tag.name }} - {{ site_name }}</title> |
|
13 | 13 | {% else %} |
|
14 | 14 | <title>{{ site_name }}</title> |
|
15 | 15 | {% endif %} |
|
16 | 16 | |
|
17 | 17 | {% if prev_page_link %} |
|
18 | 18 | <link rel="prev" href="{{ prev_page_link }}" /> |
|
19 | 19 | {% endif %} |
|
20 | 20 | {% if next_page_link %} |
|
21 | 21 | <link rel="next" href="{{ next_page_link }}" /> |
|
22 | 22 | {% endif %} |
|
23 | 23 | |
|
24 | 24 | {% endblock %} |
|
25 | 25 | |
|
26 | 26 | {% block content %} |
|
27 | 27 | |
|
28 | 28 | {% get_current_language as LANGUAGE_CODE %} |
|
29 | 29 | {% get_current_timezone as TIME_ZONE %} |
|
30 | 30 | |
|
31 | 31 | {% for banner in banners %} |
|
32 | 32 | <div class="post"> |
|
33 | 33 | <div class="title">{{ banner.title }}</div> |
|
34 | 34 | <div>{{ banner.text }}</div> |
|
35 | 35 | <div>{% trans 'Related message' %}: <a href="{{ banner.post.get_absolute_url }}">>>{{ banner.post.id }}</a></div> |
|
36 | 36 | </div> |
|
37 | 37 | {% endfor %} |
|
38 | 38 | |
|
39 | 39 | {% if tag %} |
|
40 | 40 | <div class="tag_info"> |
|
41 | <h2> | |
|
42 | <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form"> | |
|
43 | {% if is_favorite %} | |
|
44 | <button name="method" value="unsubscribe" class="fav">★</button> | |
|
45 | {% else %} | |
|
46 | <button name="method" value="subscribe" class="not_fav">★</button> | |
|
41 | {% if random_image_post %} | |
|
42 | <div class="tag-image"> | |
|
43 | {% with image=random_image_post.images.first %} | |
|
44 | <a href="{{ random_image_post.get_absolute_url }}"><img | |
|
45 | src="{{ image.image.url_200x150 }}" | |
|
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 | 70 | {% endif %} |
|
48 |
</ |
|
|
49 | <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form"> | |
|
50 | {% if is_hidden %} | |
|
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> | |
|
71 | </h2> | |
|
72 | {% if tag.get_description %} | |
|
73 | <p>{{ tag.get_description|safe }}</p> | |
|
61 | 74 | {% endif %} |
|
62 | </h2> | |
|
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> | |
|
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> | |
|
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 | 84 | </div> |
|
65 | 85 | {% endif %} |
|
66 | 86 | |
|
67 | 87 | {% if threads %} |
|
68 | 88 | {% if prev_page_link %} |
|
69 | 89 | <div class="page_link"> |
|
70 | 90 | <a href="{{ prev_page_link }}">{% trans "Previous page" %}</a> |
|
71 | 91 | </div> |
|
72 | 92 | {% endif %} |
|
73 | 93 | |
|
74 | 94 | {% for thread in threads %} |
|
75 | 95 | <div class="thread"> |
|
76 | 96 | {% post_view thread.get_opening_post moderator=moderator is_opening=True thread=thread truncated=True need_open_link=True %} |
|
77 | 97 | {% if not thread.archived %} |
|
78 | 98 | {% with last_replies=thread.get_last_replies %} |
|
79 | 99 | {% if last_replies %} |
|
80 | 100 | {% with skipped_replies_count=thread.get_skipped_replies_count %} |
|
81 | 101 | {% if skipped_replies_count %} |
|
82 | 102 | <div class="skipped_replies"> |
|
83 | 103 | <a href="{% url 'thread' thread.get_opening_post_id %}"> |
|
84 | 104 | {% blocktrans with count=skipped_replies_count %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %} |
|
85 | 105 | </a> |
|
86 | 106 | </div> |
|
87 | 107 | {% endif %} |
|
88 | 108 | {% endwith %} |
|
89 | 109 | <div class="last-replies"> |
|
90 | 110 | {% for post in last_replies %} |
|
91 | 111 | {% post_view post is_opening=False moderator=moderator truncated=True %} |
|
92 | 112 | {% endfor %} |
|
93 | 113 | </div> |
|
94 | 114 | {% endif %} |
|
95 | 115 | {% endwith %} |
|
96 | 116 | {% endif %} |
|
97 | 117 | </div> |
|
98 | 118 | {% endfor %} |
|
99 | 119 | |
|
100 | 120 | {% if next_page_link %} |
|
101 | 121 | <div class="page_link"> |
|
102 | 122 | <a href="{{ next_page_link }}">{% trans "Next page" %}</a> |
|
103 | 123 | </div> |
|
104 | 124 | {% endif %} |
|
105 | 125 | {% else %} |
|
106 | 126 | <div class="post"> |
|
107 | 127 | {% trans 'No threads exist. Create the first one!' %}</div> |
|
108 | 128 | {% endif %} |
|
109 | 129 | |
|
110 | 130 | <div class="post-form-w"> |
|
111 | 131 | <script src="{% static 'js/panel.js' %}"></script> |
|
112 | 132 | <div class="post-form"> |
|
113 | 133 | <div class="form-title">{% trans "Create new thread" %}</div> |
|
114 | 134 | <div class="swappable-form-full"> |
|
115 | 135 | <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %} |
|
116 | 136 | {{ form.as_div }} |
|
117 | 137 | <div class="form-submit"> |
|
118 | 138 | <input type="submit" value="{% trans "Post" %}"/> |
|
139 | <button id="preview-button" onclick="return false;">{% trans 'Preview' %}</button> | |
|
119 | 140 | </div> |
|
120 | 141 | </form> |
|
121 | 142 | </div> |
|
122 | 143 | <div> |
|
123 | 144 | {% trans 'Tags must be delimited by spaces. Text or image is required.' %} |
|
124 | 145 | </div> |
|
125 | <div><button id="preview-button">{% trans 'Preview' %}</button></div> | |
|
126 | 146 | <div id="preview-text"></div> |
|
127 | 147 | <div><a href="{% url "staticpage" name="help" %}">{% trans 'Text syntax' %}</a></div> |
|
128 | 148 | <div><a href="{% url "tags" "required" %}">{% trans 'Tags' %}</a></div> |
|
129 | 149 | </div> |
|
130 | 150 | </div> |
|
131 | 151 | |
|
132 | 152 | <script src="{% static 'js/form.js' %}"></script> |
|
133 | 153 | <script src="{% static 'js/thread_create.js' %}"></script> |
|
134 | 154 | |
|
135 | 155 | {% endblock %} |
|
136 | 156 | |
|
137 | 157 | {% block metapanel %} |
|
138 | 158 | |
|
139 | 159 | <span class="metapanel"> |
|
140 | 160 | <b><a href="{% url "authors" %}">{{ site_name }}</a> {{ version }}</b> |
|
141 | 161 | {% trans "Pages:" %} |
|
142 | 162 | [ |
|
143 | 163 | {% with dividers=paginator.get_dividers %} |
|
144 | 164 | {% for page in paginator.get_divided_range %} |
|
145 | 165 | {% if page in dividers %} |
|
146 | 166 | …, |
|
147 | 167 | {% endif %} |
|
148 | 168 | <a |
|
149 | 169 | {% ifequal page current_page.number %} |
|
150 | 170 | class="current_page" |
|
151 | 171 | {% endifequal %} |
|
152 | 172 | href=" |
|
153 | 173 | {% if tag %} |
|
154 | 174 | {% url "tag" tag_name=tag.name %}?page={{ page }} |
|
155 | 175 | {% else %} |
|
156 | 176 | {% url "index" %}?page={{ page }} |
|
157 | 177 | {% endif %} |
|
158 | 178 | ">{{ page }}</a> |
|
159 | 179 | {% if not forloop.last %},{% endif %} |
|
160 | 180 | {% endfor %} |
|
161 | 181 | {% endwith %} |
|
162 | 182 | ] |
|
163 | 183 | [<a href="rss/">RSS</a>] |
|
164 | 184 | </span> |
|
165 | 185 | |
|
166 | 186 | {% endblock %} |
@@ -1,73 +1,74 b'' | |||
|
1 | 1 | {% load staticfiles %} |
|
2 | 2 | {% load i18n %} |
|
3 | 3 | {% load l10n %} |
|
4 | 4 | {% load static from staticfiles %} |
|
5 | 5 | |
|
6 | 6 | <!DOCTYPE html> |
|
7 | 7 | <html> |
|
8 | 8 | <head> |
|
9 | 9 | <link rel="stylesheet" type="text/css" href="{% static 'css/base.css' %}" media="all"/> |
|
10 | 10 | <link rel="stylesheet" type="text/css" href="{% static 'css/3party/highlight.css' %}" media="all"/> |
|
11 | 11 | <link rel="stylesheet" type="text/css" href="{% static 'css/3party/jquery-ui.min.css' %}" media="all"/> |
|
12 | 12 | <link rel="stylesheet" type="text/css" href="{% static theme_css %}" media="all"/> |
|
13 | 13 | |
|
14 | 14 | <link rel="alternate" type="application/rss+xml" href="rss/" title="{% trans 'Feed' %}"/> |
|
15 | 15 | |
|
16 | 16 | <link rel="icon" type="image/png" |
|
17 | 17 | href="{% static 'favicon.png' %}"> |
|
18 | 18 | |
|
19 | 19 | <meta name="viewport" content="width=device-width, initial-scale=1"/> |
|
20 | 20 | <meta charset="utf-8"/> |
|
21 | 21 | |
|
22 | 22 | {% block head %}{% endblock %} |
|
23 | 23 | </head> |
|
24 | 24 | <body data-image-viewer="{{ image_viewer }}"> |
|
25 | 25 | <script src="{% static 'js/jquery-2.0.1.min.js' %}"></script> |
|
26 | 26 | <script src="{% static 'js/3party/jquery-ui.min.js' %}"></script> |
|
27 | 27 | <script src="{% static 'js/jquery.mousewheel.js' %}"></script> |
|
28 | 28 | <script src="{% url 'js_info_dict' %}"></script> |
|
29 | 29 | |
|
30 | 30 | <div class="navigation_panel header"> |
|
31 | 31 | <a class="link" href="{% url 'index' %}">{% trans "All threads" %}</a> |
|
32 | 32 | {% if tags_str %} |
|
33 | 33 | {% autoescape off %} |
|
34 | 34 | {{ tags_str }}, |
|
35 | 35 | {% endautoescape %} |
|
36 | 36 | {% else %} |
|
37 | 37 | {% trans 'Add tags' %} → |
|
38 | 38 | {% endif %} |
|
39 | 39 | <a href="{% url 'tags' 'required'%}" title="{% trans 'Tag management' %}">{% trans "tags" %}</a>, |
|
40 |
|
|
|
41 | <a href="{% url 'feed' %}" title="{% trans 'Feed' %}">{% trans 'feed' %}</a> | |
|
40 | <a href="{% url 'search' %}" title="{% trans 'Search' %}">{% trans 'search' %}</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 | 44 | {% if username %} |
|
44 | 45 | <a class="right-link link" href="{% url 'notifications' username %}" title="{% trans 'Notifications' %}"> |
|
45 | 46 | {% trans 'Notifications' %} |
|
46 | 47 | {% ifnotequal new_notifications_count 0 %} |
|
47 | 48 | (<b>{{ new_notifications_count }}</b>) |
|
48 | 49 | {% endifnotequal %} |
|
49 | 50 | </a> |
|
50 | 51 | {% endif %} |
|
51 | 52 | |
|
52 | 53 | <a class="right-link link" href="{% url 'settings' %}">{% trans 'Settings' %}</a> |
|
53 | 54 | </div> |
|
54 | 55 | |
|
55 | 56 | {% block content %}{% endblock %} |
|
56 | 57 | |
|
57 | 58 | <script src="{% static 'js/3party/highlight.min.js' %}"></script> |
|
58 | 59 | <script src="{% static 'js/popup.js' %}"></script> |
|
59 | 60 | <script src="{% static 'js/image.js' %}"></script> |
|
60 | 61 | <script src="{% static 'js/refpopup.js' %}"></script> |
|
61 | 62 | <script src="{% static 'js/main.js' %}"></script> |
|
62 | 63 | |
|
63 | 64 | <div class="navigation_panel footer"> |
|
64 | 65 | {% block metapanel %}{% endblock %} |
|
65 | 66 | [<a href="{% url 'admin:index' %}">{% trans 'Admin' %}</a>] |
|
66 | 67 | {% with ppd=posts_per_day|floatformat:2 %} |
|
67 | 68 | {% blocktrans %}Speed: {{ ppd }} posts per day{% endblocktrans %} |
|
68 | 69 | {% endwith %} |
|
69 | 70 | <a class="link" href="#top" id="up">{% trans 'Up' %}</a> |
|
70 | 71 | </div> |
|
71 | 72 | |
|
72 | 73 | </body> |
|
73 | 74 | </html> |
@@ -1,71 +1,71 b'' | |||
|
1 | 1 | {% extends "boards/base.html" %} |
|
2 | 2 | |
|
3 | 3 | {% load i18n %} |
|
4 | 4 | {% load board %} |
|
5 | 5 | {% load static %} |
|
6 | 6 | {% load tz %} |
|
7 | 7 | |
|
8 | 8 | {% block head %} |
|
9 | 9 | <meta name="robots" content="noindex"> |
|
10 | 10 | |
|
11 | 11 | <title>{{ site_name }} - {% trans "feed" %}</title> |
|
12 | 12 | |
|
13 | 13 | {% if prev_page_link %} |
|
14 | 14 | <link rel="prev" href="{{ prev_page_link }}" /> |
|
15 | 15 | {% endif %} |
|
16 | 16 | {% if next_page_link %} |
|
17 | 17 | <link rel="next" href="{{ next_page_link }}" /> |
|
18 | 18 | {% endif %} |
|
19 | 19 | |
|
20 | 20 | {% endblock %} |
|
21 | 21 | |
|
22 | 22 | {% block content %} |
|
23 | 23 | |
|
24 | 24 | {% get_current_language as LANGUAGE_CODE %} |
|
25 | 25 | {% get_current_timezone as TIME_ZONE %} |
|
26 | 26 | |
|
27 | 27 | {% if posts %} |
|
28 | 28 | {% if prev_page_link %} |
|
29 | 29 | <div class="page_link"> |
|
30 | 30 | <a href="{{ prev_page_link }}">{% trans "Previous page" %}</a> |
|
31 | 31 | </div> |
|
32 | 32 | {% endif %} |
|
33 | 33 | |
|
34 | 34 | {% for post in posts %} |
|
35 | 35 | {% post_view post moderator=moderator truncated=True need_op_data=True %} |
|
36 | 36 | {% endfor %} |
|
37 | 37 | |
|
38 | 38 | {% if next_page_link %} |
|
39 | 39 | <div class="page_link"> |
|
40 | 40 | <a href="{{ next_page_link }}">{% trans "Next page" %}</a> |
|
41 | 41 | </div> |
|
42 | 42 | {% endif %} |
|
43 | 43 | {% else %} |
|
44 | 44 | <div class="post"> |
|
45 | 45 | {% trans 'No posts exist. Create the first one!' %}</div> |
|
46 | 46 | {% endif %} |
|
47 | 47 | {% endblock %} |
|
48 | 48 | |
|
49 | 49 | {% block metapanel %} |
|
50 | 50 | |
|
51 | 51 | <span class="metapanel"> |
|
52 | 52 | <b><a href="{% url "authors" %}">{{ site_name }}</a> {{ version }}</b> |
|
53 | 53 | {% trans "Pages:" %} |
|
54 | 54 | [ |
|
55 | 55 | {% with dividers=paginator.get_dividers %} |
|
56 | 56 | {% for page in paginator.get_divided_range %} |
|
57 | 57 | {% if page in dividers %} |
|
58 | 58 | …, |
|
59 | 59 | {% endif %} |
|
60 | 60 | <a |
|
61 | 61 | {% ifequal page current_page.number %} |
|
62 | 62 | class="current_page" |
|
63 | 63 | {% endifequal %} |
|
64 | href="{% url "feed" %}?page={{ page }}">{{ page }}</a> | |
|
64 | href="{% url "feed" %}?page={{ page }}{{ additional_attrs }}">{{ page }}</a> | |
|
65 | 65 | {% if not forloop.last %},{% endif %} |
|
66 | 66 | {% endfor %} |
|
67 | 67 | {% endwith %} |
|
68 | 68 | ] |
|
69 | 69 | </span> |
|
70 | 70 | |
|
71 | 71 | {% endblock %} |
@@ -1,107 +1,113 b'' | |||
|
1 | 1 | {% load i18n %} |
|
2 | 2 | {% load board %} |
|
3 | 3 | |
|
4 | 4 | {% get_current_language as LANGUAGE_CODE %} |
|
5 | 5 | |
|
6 | 6 | <div class="{{ css_class }}" id="{{ post.id }}" data-uid="{{ post.uid }}"> |
|
7 | 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 | 9 | <span class="title">{{ post.title }}</span> |
|
10 | 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 | 18 | {% comment %} |
|
12 | 19 | Thread death time needs to be shown only if the thread is alredy archived |
|
13 | 20 | and this is an opening post (thread death time) or a post for popup |
|
14 | 21 | (we don't see OP here so we show the death time in the post itself). |
|
15 | 22 | {% endcomment %} |
|
16 | 23 | {% if thread.archived %} |
|
17 | 24 | {% if is_opening %} |
|
18 | 25 | — <time datetime="{{ thread.bump_time|date:'c' }}">{{ thread.bump_time }}</time> |
|
19 | 26 | {% endif %} |
|
20 | 27 | {% endif %} |
|
21 | 28 | {% if is_opening %} |
|
22 | 29 | {% if need_open_link %} |
|
23 | 30 | {% if thread.archived %} |
|
24 | 31 | <a class="link" href="{% url 'thread' post.id %}">{% trans "Open" %}</a> |
|
25 | 32 | {% else %} |
|
26 | 33 | <a class="link" href="{% url 'thread' post.id %}#form">{% trans "Reply" %}</a> |
|
27 | 34 | {% endif %} |
|
28 | 35 | {% endif %} |
|
29 | 36 | {% else %} |
|
30 | 37 | {% if need_op_data %} |
|
31 | 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 | 40 | {% endwith %} |
|
34 | 41 | {% endif %} |
|
35 | 42 | {% endif %} |
|
36 | 43 | {% if reply_link and not thread.archived %} |
|
37 | 44 | <a href="#form" onclick="addQuickReply('{{ post.id }}'); return false;">{% trans 'Reply' %}</a> |
|
38 | 45 | {% endif %} |
|
39 | 46 | |
|
40 | 47 | {% if post.global_id %} |
|
41 | 48 | <a class="global-id" href="{% url 'post_sync_data' post.id %}"> [RAW] </a> |
|
42 | 49 | {% endif %} |
|
43 | 50 | |
|
44 | 51 | {% if moderator %} |
|
45 | 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 | 54 | {% if is_opening %} |
|
48 |
|
|
|
55 | | <a href="{% url 'admin:boards_thread_change' thread.id %}">{% trans 'Edit thread' %}</a> | |
|
49 | 56 | {% endif %} |
|
50 | 57 | </span> |
|
51 | 58 | {% endif %} |
|
52 | 59 | </div> |
|
53 | 60 | {% comment %} |
|
54 | 61 | Post images. Currently only 1 image can be posted and shown, but post model |
|
55 | 62 | supports multiple. |
|
56 | 63 | {% endcomment %} |
|
57 | 64 | {% if post.images.exists %} |
|
58 |
{% with post.images. |
|
|
59 |
{ |
|
|
60 | {{ image.get_view }} | |
|
61 | {% endautoescape %} | |
|
65 | {% with post.images.first as image %} | |
|
66 | {{ image.get_view|safe }} | |
|
67 | {% endwith %} | |
|
68 | {% endif %} | |
|
69 | {% if post.attachments.exists %} | |
|
70 | {% with post.attachments.first as file %} | |
|
71 | {{ file.get_view|safe }} | |
|
62 | 72 | {% endwith %} |
|
63 | 73 | {% endif %} |
|
64 | 74 | {% comment %} |
|
65 | 75 | Post message (text) |
|
66 | 76 | {% endcomment %} |
|
67 | 77 | <div class="message"> |
|
68 | 78 | {% autoescape off %} |
|
69 | 79 | {% if truncated %} |
|
70 | 80 | {{ post.get_text|truncatewords_html:50 }} |
|
71 | 81 | {% else %} |
|
72 | 82 | {{ post.get_text }} |
|
73 | 83 | {% endif %} |
|
74 | 84 | {% endautoescape %} |
|
75 | {% if post.is_referenced %} | |
|
76 | {% if mode_tree %} | |
|
77 | <div class="tree_reply"> | |
|
78 | {% for refpost in post.get_referenced_posts %} | |
|
79 |
|
|
|
80 |
{% |
|
|
81 |
|
|
|
82 |
|
|
|
83 | <div class="refmap"> | |
|
84 | {% autoescape off %} | |
|
85 |
|
|
|
86 | {% endautoescape %} | |
|
87 | </div> | |
|
88 | {% endif %} | |
|
85 | </div> | |
|
86 | {% if post.is_referenced %} | |
|
87 | {% if mode_tree %} | |
|
88 | <div class="tree_reply"> | |
|
89 | {% for refpost in post.get_referenced_posts %} | |
|
90 | {% post_view refpost mode_tree=True %} | |
|
91 | {% endfor %} | |
|
92 | </div> | |
|
93 | {% else %} | |
|
94 | <div class="refmap"> | |
|
95 | {% trans "Replies" %}: {{ post.refmap|safe }} | |
|
96 | </div> | |
|
89 | 97 | {% endif %} |
|
90 | </div> | |
|
98 | {% endif %} | |
|
91 | 99 | {% comment %} |
|
92 | 100 | Thread metadata: counters, tags etc |
|
93 | 101 | {% endcomment %} |
|
94 | 102 | {% if is_opening %} |
|
95 | 103 | <div class="metadata"> |
|
96 | 104 | {% if is_opening and need_open_link %} |
|
97 | 105 | {{ thread.get_reply_count }} {% trans 'messages' %}, |
|
98 | 106 | {{ thread.get_images_count }} {% trans 'images' %}. |
|
99 | 107 | {% endif %} |
|
100 | 108 | <span class="tags"> |
|
101 |
{ |
|
|
102 | {{ thread.get_tag_url_list }} | |
|
103 | {% endautoescape %} | |
|
109 | {{ thread.get_tag_url_list|safe }} | |
|
104 | 110 | </span> |
|
105 | 111 | </div> |
|
106 | 112 | {% endif %} |
|
107 | 113 | </div> |
@@ -1,43 +1,40 b'' | |||
|
1 | 1 | {% extends "boards/base.html" %} |
|
2 | 2 | |
|
3 | 3 | {% load i18n %} |
|
4 | 4 | {% load tz %} |
|
5 | 5 | |
|
6 | 6 | {% block head %} |
|
7 | 7 | <meta name="robots" content="noindex"> |
|
8 | 8 | <title>{% trans 'Settings' %} - {{ site_name }}</title> |
|
9 | 9 | {% endblock %} |
|
10 | 10 | |
|
11 | 11 | {% block content %} |
|
12 | ||
|
13 | 12 | <div class="post"> |
|
14 | 13 | <p> |
|
15 | 14 | {% if moderator %} |
|
16 | 15 | {% trans 'You are moderator.' %} |
|
17 | 16 | {% endif %} |
|
18 | 17 | </p> |
|
19 | 18 | {% if hidden_tags %} |
|
20 | 19 | <p>{% trans 'Hidden tags:' %} |
|
21 | 20 | {% for tag in hidden_tags %} |
|
22 |
{ |
|
|
23 | {{ tag.get_view }} | |
|
24 | {% endautoescape %} | |
|
21 | {{ tag.get_view|safe }} | |
|
25 | 22 | {% endfor %} |
|
26 | 23 | </p> |
|
27 | 24 | {% else %} |
|
28 | 25 | <p>{% trans 'No hidden tags.' %}</p> |
|
29 | 26 | {% endif %} |
|
30 | 27 | </div> |
|
31 | 28 | |
|
32 | 29 | <div class="post-form-w"> |
|
33 | 30 | <div class="post-form"> |
|
34 | 31 | <form method="post">{% csrf_token %} |
|
35 | 32 | {{ form.as_div }} |
|
36 | 33 | <div class="form-submit"> |
|
37 | 34 | <input type="submit" value="{% trans "Save" %}" /> |
|
38 | 35 | </div> |
|
39 | 36 | </form> |
|
40 | 37 | </div> |
|
41 | 38 | </div> |
|
42 | 39 | |
|
43 | 40 | {% endblock %} |
@@ -1,5 +1,3 b'' | |||
|
1 | 1 | <div class="post"> |
|
2 | {% autoescape off %} | |
|
3 | {{ tag.get_view }} | |
|
4 | {% endautoescape %} | |
|
2 | {{ tag.get_view|safe }} | |
|
5 | 3 | </div> |
@@ -1,28 +1,47 b'' | |||
|
1 | 1 | {% extends "boards/base.html" %} |
|
2 | 2 | |
|
3 | 3 | {% load i18n %} |
|
4 | 4 | |
|
5 | 5 | {% block head %} |
|
6 | 6 | <title>Neboard - {% trans "Tags" %}</title> |
|
7 | 7 | {% endblock %} |
|
8 | 8 | |
|
9 | 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 | 14 | <div class="post"> |
|
12 |
{% if |
|
|
13 | {% for tag in all_tags %} | |
|
14 | <div class="tag_item"> | |
|
15 | {% if section_tags %} | |
|
16 | <div> | |
|
17 | {% trans 'Sections:' %} | |
|
18 | {% for letter in section_tag_list %} | |
|
19 | <br />({{ letter.grouper|upper }}) | |
|
20 | {% for tag in letter.list %} | |
|
15 | 21 | {% autoescape off %} |
|
16 | {{ tag.get_view }} | |
|
22 | {{ tag.get_view }}{% if not forloop.last %},{% endif %} | |
|
17 | 23 | {% endautoescape %} |
|
18 |
|
|
|
24 | {% endfor %} | |
|
19 | 25 | {% endfor %} |
|
20 | {% else %} | |
|
21 | {% trans 'No tags found.' %} | |
|
26 | </div> | |
|
22 | 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 | 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 | 44 | {% endif %} |
|
26 | 45 | </div> |
|
27 | 46 | |
|
28 | 47 | {% endblock %} |
@@ -1,43 +1,40 b'' | |||
|
1 | 1 | {% extends "boards/base.html" %} |
|
2 | 2 | |
|
3 | 3 | {% load i18n %} |
|
4 | 4 | {% load static from staticfiles %} |
|
5 | 5 | {% load board %} |
|
6 | 6 | {% load tz %} |
|
7 | 7 | |
|
8 | 8 | {% block head %} |
|
9 | 9 | <title>{{ opening_post.get_title|striptags|truncatewords:10 }} |
|
10 | 10 | - {{ site_name }}</title> |
|
11 | 11 | {% endblock %} |
|
12 | 12 | |
|
13 | 13 | {% block content %} |
|
14 | 14 | <div class="image-mode-tab"> |
|
15 | 15 | <a {% ifequal mode 'normal' %}class="current_mode"{% endifequal %} href="{% url 'thread' opening_post.id %}">{% trans 'Normal' %}</a>, |
|
16 | 16 | <a {% ifequal mode 'gallery' %}class="current_mode"{% endifequal %} href="{% url 'thread_gallery' opening_post.id %}">{% trans 'Gallery' %}</a>, |
|
17 | 17 | <a {% ifequal mode 'tree' %}class="current_mode"{% endifequal %} href="{% url 'thread_tree' opening_post.id %}">{% trans 'Tree' %}</a> |
|
18 | 18 | </div> |
|
19 | 19 | |
|
20 | 20 | {% block thread_content %} |
|
21 | 21 | {% endblock %} |
|
22 | 22 | {% endblock %} |
|
23 | 23 | |
|
24 | 24 | {% block metapanel %} |
|
25 | 25 | |
|
26 | 26 | <span class="metapanel" |
|
27 | 27 | data-last-update="{{ last_update }}" |
|
28 | 28 | data-ws-token-time="{{ ws_token_time }}" |
|
29 | 29 | data-ws-token="{{ ws_token }}" |
|
30 | 30 | data-ws-project="{{ ws_project }}" |
|
31 | 31 | data-ws-host="{{ ws_host }}" |
|
32 | 32 | data-ws-port="{{ ws_port }}"> |
|
33 | 33 | |
|
34 | {% block thread_meta_panel %} | |
|
35 | {% endblock %} | |
|
36 | ||
|
37 | 34 | <span id="reply-count">{{ thread.get_reply_count }}</span>{% if thread.has_post_limit %}/{{ thread.max_posts }}{% endif %} {% trans 'messages' %}, |
|
38 | 35 | <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}. |
|
39 | 36 | {% trans 'Last update: ' %}<span id="last-update"><time datetime="{{ thread.last_edit_time|date:'c' }}">{{ thread.last_edit_time }}</time></span> |
|
40 | 37 | [<a href="rss/">RSS</a>] |
|
41 | 38 | </span> |
|
42 | 39 | |
|
43 | 40 | {% endblock %} |
@@ -1,61 +1,57 b'' | |||
|
1 | 1 | {% extends "boards/thread.html" %} |
|
2 | 2 | |
|
3 | 3 | {% load i18n %} |
|
4 | 4 | {% load static from staticfiles %} |
|
5 | 5 | {% load board %} |
|
6 | 6 | {% load tz %} |
|
7 | 7 | |
|
8 | 8 | {% block thread_content %} |
|
9 | 9 | {% get_current_language as LANGUAGE_CODE %} |
|
10 | 10 | {% get_current_timezone as TIME_ZONE %} |
|
11 | 11 | |
|
12 | 12 | {% if bumpable and thread.has_post_limit %} |
|
13 | 13 | <div class="bar-bg"> |
|
14 | 14 | <div class="bar-value" style="width:{{ bumplimit_progress }}%" id="bumplimit_progress"> |
|
15 | 15 | </div> |
|
16 | 16 | <div class="bar-text"> |
|
17 | 17 | <span id="left_to_limit">{{ posts_left }}</span> {% trans 'posts to bumplimit' %} |
|
18 | 18 | </div> |
|
19 | 19 | </div> |
|
20 | 20 | {% endif %} |
|
21 | 21 | |
|
22 | 22 | <div class="thread"> |
|
23 | 23 | {% for post in thread.get_replies %} |
|
24 | 24 | {% post_view post moderator=moderator reply_link=True %} |
|
25 | 25 | {% endfor %} |
|
26 | 26 | </div> |
|
27 | 27 | |
|
28 | 28 | {% if not thread.archived %} |
|
29 | 29 | <div class="post-form-w"> |
|
30 | 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 | 32 | <div class="post-form" id="compact-form"> |
|
33 | 33 | <div class="swappable-form-full"> |
|
34 | 34 | <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %} |
|
35 | 35 | <div class="compact-form-text"></div> |
|
36 | 36 | {{ form.as_div }} |
|
37 | 37 | <div class="form-submit"> |
|
38 | 38 | <input type="submit" value="{% trans "Post" %}"/> |
|
39 | <button id="preview-button" onclick="return false;">{% trans 'Preview' %}</button> | |
|
39 | 40 | </div> |
|
40 | 41 | </form> |
|
41 | 42 | </div> |
|
42 | <div><button id="preview-button">{% trans 'Preview' %}</button></div> | |
|
43 | 43 | <div id="preview-text"></div> |
|
44 | 44 | <div><a href="{% url "staticpage" name="help" %}"> |
|
45 | 45 | {% trans 'Text syntax' %}</a></div> |
|
46 | 46 | <div><a id="form-close-button" href="#" onClick="resetFormPosition(); return false;">{% trans 'Close form' %}</a></div> |
|
47 | 47 | </div> |
|
48 | 48 | </div> |
|
49 | 49 | |
|
50 | 50 | <script src="{% static 'js/jquery.form.min.js' %}"></script> |
|
51 | 51 | {% endif %} |
|
52 | 52 | |
|
53 | 53 | <script src="{% static 'js/form.js' %}"></script> |
|
54 | 54 | <script src="{% static 'js/thread.js' %}"></script> |
|
55 | 55 | <script src="{% static 'js/thread_update.js' %}"></script> |
|
56 | 56 | <script src="{% static 'js/3party/centrifuge.js' %}"></script> |
|
57 | 57 | {% endblock %} |
|
58 | ||
|
59 | {% block thread_meta_panel %} | |
|
60 | <button id="autoupdate">{% trans 'Update' %}</button> | |
|
61 | {% endblock %} |
@@ -1,88 +1,91 b'' | |||
|
1 | 1 | from django.conf.urls import patterns, url |
|
2 | 2 | from django.views.i18n import javascript_catalog |
|
3 | 3 | |
|
4 | 4 | from boards import views |
|
5 | 5 | from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed |
|
6 | 6 | from boards.views import api, tag_threads, all_threads, \ |
|
7 | 7 | settings, all_tags, feed |
|
8 | 8 | from boards.views.authors import AuthorsView |
|
9 | 9 | from boards.views.notifications import NotificationView |
|
10 | 10 | from boards.views.search import BoardSearchView |
|
11 | 11 | from boards.views.static import StaticPageView |
|
12 | 12 | from boards.views.preview import PostPreviewView |
|
13 | 13 | from boards.views.sync import get_post_sync_data, response_get |
|
14 | from boards.views.random import RandomImageView | |
|
14 | 15 | |
|
15 | 16 | |
|
16 | 17 | js_info_dict = { |
|
17 | 18 | 'packages': ('boards',), |
|
18 | 19 | } |
|
19 | 20 | |
|
20 | 21 | urlpatterns = patterns('', |
|
21 | 22 | # /boards/ |
|
22 | 23 | url(r'^$', all_threads.AllThreadsView.as_view(), name='index'), |
|
23 | 24 | |
|
24 | 25 | # /boards/tag/tag_name/ |
|
25 | 26 | url(r'^tag/(?P<tag_name>\w+)/$', tag_threads.TagView.as_view(), |
|
26 | 27 | name='tag'), |
|
27 | 28 | |
|
28 | 29 | # /boards/thread/ |
|
29 | 30 | url(r'^thread/(?P<post_id>\d+)/$', views.thread.NormalThreadView.as_view(), |
|
30 | 31 | name='thread'), |
|
31 | 32 | url(r'^thread/(?P<post_id>\d+)/mode/gallery/$', views.thread.GalleryThreadView.as_view(), |
|
32 | 33 | name='thread_gallery'), |
|
33 | 34 | url(r'^thread/(?P<post_id>\d+)/mode/tree/$', views.thread.TreeThreadView.as_view(), |
|
34 | 35 | name='thread_tree'), |
|
35 | 36 | # /feed/ |
|
36 | 37 | url(r'^feed/$', views.feed.FeedView.as_view(), name='feed'), |
|
37 | 38 | |
|
38 | 39 | url(r'^settings/$', settings.SettingsView.as_view(), name='settings'), |
|
39 | 40 | url(r'^tags/(?P<query>\w+)?/?$', all_tags.AllTagsView.as_view(), name='tags'), |
|
40 | 41 | url(r'^authors/$', AuthorsView.as_view(), name='authors'), |
|
41 | 42 | |
|
42 | 43 | url(r'^banned/$', views.banned.BannedView.as_view(), name='banned'), |
|
43 | 44 | url(r'^staticpage/(?P<name>\w+)/$', StaticPageView.as_view(), |
|
44 | 45 | name='staticpage'), |
|
45 | 46 | |
|
47 | url(r'^random/$', RandomImageView.as_view(), name='random'), | |
|
48 | ||
|
46 | 49 | # RSS feeds |
|
47 | 50 | url(r'^rss/$', AllThreadsFeed()), |
|
48 | 51 | url(r'^page/(?P<page>\d+)/rss/$', AllThreadsFeed()), |
|
49 | 52 | url(r'^tag/(?P<tag_name>\w+)/rss/$', TagThreadsFeed()), |
|
50 | 53 | url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/rss/$', TagThreadsFeed()), |
|
51 | 54 | url(r'^thread/(?P<post_id>\d+)/rss/$', ThreadPostsFeed()), |
|
52 | 55 | |
|
53 | 56 | # i18n |
|
54 | 57 | url(r'^jsi18n/$', javascript_catalog, js_info_dict, |
|
55 | 58 | name='js_info_dict'), |
|
56 | 59 | |
|
57 | 60 | # API |
|
58 | 61 | url(r'^api/post/(?P<post_id>\d+)/$', api.get_post, name="get_post"), |
|
59 | 62 | url(r'^api/diff_thread/$', api.api_get_threaddiff, name="get_thread_diff"), |
|
60 | 63 | url(r'^api/threads/(?P<count>\w+)/$', api.api_get_threads, |
|
61 | 64 | name='get_threads'), |
|
62 | 65 | url(r'^api/tags/$', api.api_get_tags, name='get_tags'), |
|
63 | 66 | url(r'^api/thread/(?P<opening_post_id>\w+)/$', api.api_get_thread_posts, |
|
64 | 67 | name='get_thread'), |
|
65 | 68 | url(r'^api/add_post/(?P<opening_post_id>\w+)/$', api.api_add_post, |
|
66 | 69 | name='add_post'), |
|
67 | 70 | url(r'^api/notifications/(?P<username>\w+)/$', api.api_get_notifications, |
|
68 | 71 | name='api_notifications'), |
|
69 | 72 | url(r'^api/preview/$', api.api_get_preview, name='preview'), |
|
70 | 73 | |
|
71 | 74 | # Sync protocol API |
|
72 | 75 | url(r'^api/sync/pull/$', api.sync_pull, name='api_sync_pull'), |
|
73 | 76 | url(r'^api/sync/get/$', response_get, name='api_sync_pull'), |
|
74 | 77 | # TODO 'get' request |
|
75 | 78 | |
|
76 | 79 | # Search |
|
77 | 80 | url(r'^search/$', BoardSearchView.as_view(), name='search'), |
|
78 | 81 | |
|
79 | 82 | # Notifications |
|
80 | 83 | url(r'^notifications/(?P<username>\w+)$', NotificationView.as_view(), name='notifications'), |
|
81 | 84 | |
|
82 | 85 | # Post preview |
|
83 | 86 | url(r'^preview/$', PostPreviewView.as_view(), name='preview'), |
|
84 | 87 | |
|
85 | 88 | url(r'^post_xml/(?P<post_id>\d+)$', get_post_sync_data, |
|
86 | 89 | name='post_sync_data'), |
|
87 | 90 | |
|
88 | 91 | ) |
@@ -1,84 +1,92 b'' | |||
|
1 | 1 | """ |
|
2 | 2 | This module contains helper functions and helper classes. |
|
3 | 3 | """ |
|
4 | import hashlib | |
|
4 | 5 | import time |
|
5 | 6 | import hmac |
|
6 | 7 | |
|
7 | 8 | from django.core.cache import cache |
|
8 | 9 | from django.db.models import Model |
|
9 | 10 | |
|
10 | 11 | from django.utils import timezone |
|
11 | 12 | |
|
12 | 13 | from neboard import settings |
|
13 | 14 | |
|
14 | 15 | |
|
15 | 16 | CACHE_KEY_DELIMITER = '_' |
|
16 | 17 | PERMISSION_MODERATE = 'moderation' |
|
17 | 18 | |
|
18 | 19 | def get_client_ip(request): |
|
19 | 20 | x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') |
|
20 | 21 | if x_forwarded_for: |
|
21 | 22 | ip = x_forwarded_for.split(',')[-1].strip() |
|
22 | 23 | else: |
|
23 | 24 | ip = request.META.get('REMOTE_ADDR') |
|
24 | 25 | return ip |
|
25 | 26 | |
|
26 | 27 | |
|
27 | 28 | # TODO The output format is not epoch because it includes microseconds |
|
28 | 29 | def datetime_to_epoch(datetime): |
|
29 | 30 | return int(time.mktime(timezone.localtime( |
|
30 | 31 | datetime,timezone.get_current_timezone()).timetuple()) |
|
31 | 32 | * 1000000 + datetime.microsecond) |
|
32 | 33 | |
|
33 | 34 | |
|
34 | 35 | def get_websocket_token(user_id='', timestamp=''): |
|
35 | 36 | """ |
|
36 | 37 | Create token to validate information provided by new connection. |
|
37 | 38 | """ |
|
38 | 39 | |
|
39 | 40 | sign = hmac.new(settings.CENTRIFUGE_PROJECT_SECRET.encode()) |
|
40 | 41 | sign.update(settings.CENTRIFUGE_PROJECT_ID.encode()) |
|
41 | 42 | sign.update(user_id.encode()) |
|
42 | 43 | sign.update(timestamp.encode()) |
|
43 | 44 | token = sign.hexdigest() |
|
44 | 45 | |
|
45 | 46 | return token |
|
46 | 47 | |
|
47 | 48 | |
|
48 | 49 | def cached_result(key_method=None): |
|
49 | 50 | """ |
|
50 | 51 | Caches method result in the Django's cache system, persisted by object name, |
|
51 | 52 | object name and model id if object is a Django model. |
|
52 | 53 | """ |
|
53 | 54 | def _cached_result(function): |
|
54 | 55 | def inner_func(obj, *args, **kwargs): |
|
55 | 56 | # TODO Include method arguments to the cache key |
|
56 | 57 | cache_key_params = [obj.__class__.__name__, function.__name__] |
|
57 | 58 | if isinstance(obj, Model): |
|
58 | 59 | cache_key_params.append(str(obj.id)) |
|
59 | 60 | |
|
60 | 61 | if key_method is not None: |
|
61 | 62 | cache_key_params += [str(arg) for arg in key_method(obj)] |
|
62 | 63 | |
|
63 | 64 | cache_key = CACHE_KEY_DELIMITER.join(cache_key_params) |
|
64 | 65 | |
|
65 | 66 | persisted_result = cache.get(cache_key) |
|
66 | 67 | if persisted_result is not None: |
|
67 | 68 | result = persisted_result |
|
68 | 69 | else: |
|
69 | 70 | result = function(obj, *args, **kwargs) |
|
70 | 71 | cache.set(cache_key, result) |
|
71 | 72 | |
|
72 | 73 | return result |
|
73 | 74 | |
|
74 | 75 | return inner_func |
|
75 | 76 | return _cached_result |
|
76 | 77 | |
|
77 | 78 | |
|
78 | 79 | def is_moderator(request): |
|
79 | 80 | try: |
|
80 | 81 | moderate = request.user.has_perm(PERMISSION_MODERATE) |
|
81 | 82 | except AttributeError: |
|
82 | 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() |
@@ -1,22 +1,23 b'' | |||
|
1 | 1 | from django.shortcuts import render |
|
2 | 2 | |
|
3 | 3 | from boards.views.base import BaseBoardView |
|
4 | 4 | from boards.models.tag import Tag |
|
5 | 5 | |
|
6 | 6 | |
|
7 | PARAM_SECTION_TAGS = 'section_tags' | |
|
7 | 8 | PARAM_TAGS = 'all_tags' |
|
8 | 9 | PARAM_QUERY = 'query' |
|
9 | 10 | |
|
10 | 11 | |
|
11 | 12 | class AllTagsView(BaseBoardView): |
|
12 | 13 | |
|
13 | 14 | def get(self, request, query=None): |
|
14 | 15 | params = dict() |
|
15 | 16 | |
|
16 | if query == 'required': | |
|
17 | params[PARAM_TAGS] = Tag.objects.filter(required=True) | |
|
18 | else: | |
|
19 | params[PARAM_TAGS] = Tag.objects.get_not_empty_tags() | |
|
17 | params[PARAM_SECTION_TAGS] = Tag.objects.filter(required=True) | |
|
18 | if query != 'required': | |
|
19 | params[PARAM_TAGS] = Tag.objects.get_not_empty_tags().filter( | |
|
20 | required=False) | |
|
20 | 21 | params[PARAM_QUERY] = query |
|
21 | 22 | |
|
22 | 23 | return render(request, 'boards/tags.html', params) |
@@ -1,169 +1,170 b'' | |||
|
1 | 1 | from django.core.urlresolvers import reverse |
|
2 | 2 | from django.core.files import File |
|
3 | 3 | from django.core.files.temp import NamedTemporaryFile |
|
4 | 4 | from django.core.paginator import EmptyPage |
|
5 | 5 | from django.db import transaction |
|
6 | 6 | from django.http import Http404 |
|
7 | 7 | from django.shortcuts import render, redirect |
|
8 | 8 | import requests |
|
9 | 9 | |
|
10 | 10 | from boards import utils, settings |
|
11 | 11 | from boards.abstracts.paginator import get_paginator |
|
12 | 12 | from boards.abstracts.settingsmanager import get_settings_manager |
|
13 | 13 | from boards.forms import ThreadForm, PlainErrorList |
|
14 | 14 | from boards.models import Post, Thread, Ban, Tag, PostImage, Banner |
|
15 | 15 | from boards.views.banned import BannedView |
|
16 | 16 | from boards.views.base import BaseBoardView, CONTEXT_FORM |
|
17 | 17 | from boards.views.posting_mixin import PostMixin |
|
18 | 18 | |
|
19 | 19 | |
|
20 | 20 | FORM_TAGS = 'tags' |
|
21 | 21 | FORM_TEXT = 'text' |
|
22 | 22 | FORM_TITLE = 'title' |
|
23 | 23 | FORM_IMAGE = 'image' |
|
24 | 24 | FORM_THREADS = 'threads' |
|
25 | 25 | |
|
26 | 26 | TAG_DELIMITER = ' ' |
|
27 | 27 | |
|
28 | 28 | PARAMETER_CURRENT_PAGE = 'current_page' |
|
29 | 29 | PARAMETER_PAGINATOR = 'paginator' |
|
30 | 30 | PARAMETER_THREADS = 'threads' |
|
31 | 31 | PARAMETER_BANNERS = 'banners' |
|
32 | 32 | |
|
33 | 33 | PARAMETER_PREV_LINK = 'prev_page_link' |
|
34 | 34 | PARAMETER_NEXT_LINK = 'next_page_link' |
|
35 | 35 | |
|
36 | 36 | TEMPLATE = 'boards/all_threads.html' |
|
37 | 37 | DEFAULT_PAGE = 1 |
|
38 | 38 | |
|
39 | 39 | |
|
40 | 40 | class AllThreadsView(PostMixin, BaseBoardView): |
|
41 | 41 | |
|
42 | 42 | def __init__(self): |
|
43 | 43 | self.settings_manager = None |
|
44 | 44 | super(AllThreadsView, self).__init__() |
|
45 | 45 | |
|
46 | 46 | def get(self, request, form: ThreadForm=None): |
|
47 | 47 | page = request.GET.get('page', DEFAULT_PAGE) |
|
48 | 48 | |
|
49 | 49 | params = self.get_context_data(request=request) |
|
50 | 50 | |
|
51 | 51 | if not form: |
|
52 | 52 | form = ThreadForm(error_class=PlainErrorList) |
|
53 | 53 | |
|
54 | 54 | self.settings_manager = get_settings_manager(request) |
|
55 | 55 | paginator = get_paginator(self.get_threads(), |
|
56 | 56 | settings.get_int('View', 'ThreadsPerPage')) |
|
57 | 57 | paginator.current_page = int(page) |
|
58 | 58 | |
|
59 | 59 | try: |
|
60 | 60 | threads = paginator.page(page).object_list |
|
61 | 61 | except EmptyPage: |
|
62 | 62 | raise Http404() |
|
63 | 63 | |
|
64 | 64 | params[PARAMETER_THREADS] = threads |
|
65 | 65 | params[CONTEXT_FORM] = form |
|
66 | 66 | params[PARAMETER_BANNERS] = Banner.objects.order_by('-id').all() |
|
67 | 67 | |
|
68 | 68 | self.get_page_context(paginator, params, page) |
|
69 | 69 | |
|
70 | 70 | return render(request, TEMPLATE, params) |
|
71 | 71 | |
|
72 | 72 | def post(self, request): |
|
73 | 73 | form = ThreadForm(request.POST, request.FILES, |
|
74 | 74 | error_class=PlainErrorList) |
|
75 | 75 | form.session = request.session |
|
76 | 76 | |
|
77 | 77 | if form.is_valid(): |
|
78 | 78 | return self.create_thread(request, form) |
|
79 | 79 | if form.need_to_ban: |
|
80 | 80 | # Ban user because he is suspected to be a bot |
|
81 | 81 | self._ban_current_user(request) |
|
82 | 82 | |
|
83 | 83 | return self.get(request, form) |
|
84 | 84 | |
|
85 | 85 | def get_page_context(self, paginator, params, page): |
|
86 | 86 | """ |
|
87 | 87 | Get pagination context variables |
|
88 | 88 | """ |
|
89 | 89 | |
|
90 | 90 | params[PARAMETER_PAGINATOR] = paginator |
|
91 | 91 | current_page = paginator.page(int(page)) |
|
92 | 92 | params[PARAMETER_CURRENT_PAGE] = current_page |
|
93 | 93 | if current_page.has_previous(): |
|
94 | 94 | params[PARAMETER_PREV_LINK] = self.get_previous_page_link( |
|
95 | 95 | current_page) |
|
96 | 96 | if current_page.has_next(): |
|
97 | 97 | params[PARAMETER_NEXT_LINK] = self.get_next_page_link(current_page) |
|
98 | 98 | |
|
99 | 99 | def get_previous_page_link(self, current_page): |
|
100 | 100 | return reverse('index') + '?page=' \ |
|
101 | 101 | + str(current_page.previous_page_number()) |
|
102 | 102 | |
|
103 | 103 | def get_next_page_link(self, current_page): |
|
104 | 104 | return reverse('index') + '?page=' \ |
|
105 | 105 | + str(current_page.next_page_number()) |
|
106 | 106 | |
|
107 | 107 | @staticmethod |
|
108 | 108 | def parse_tags_string(tag_strings): |
|
109 | 109 | """ |
|
110 | 110 | Parses tag list string and returns tag object list. |
|
111 | 111 | """ |
|
112 | 112 | |
|
113 | 113 | tags = [] |
|
114 | 114 | |
|
115 | 115 | if tag_strings: |
|
116 | 116 | tag_strings = tag_strings.split(TAG_DELIMITER) |
|
117 | 117 | for tag_name in tag_strings: |
|
118 | 118 | tag_name = tag_name.strip().lower() |
|
119 | 119 | if len(tag_name) > 0: |
|
120 | 120 | tag, created = Tag.objects.get_or_create(name=tag_name) |
|
121 | 121 | tags.append(tag) |
|
122 | 122 | |
|
123 | 123 | return tags |
|
124 | 124 | |
|
125 | 125 | @transaction.atomic |
|
126 | 126 | def create_thread(self, request, form: ThreadForm, html_response=True): |
|
127 | 127 | """ |
|
128 | 128 | Creates a new thread with an opening post. |
|
129 | 129 | """ |
|
130 | 130 | |
|
131 | 131 | ip = utils.get_client_ip(request) |
|
132 | 132 | is_banned = Ban.objects.filter(ip=ip).exists() |
|
133 | 133 | |
|
134 | 134 | if is_banned: |
|
135 | 135 | if html_response: |
|
136 | 136 | return redirect(BannedView().as_view()) |
|
137 | 137 | else: |
|
138 | 138 | return |
|
139 | 139 | |
|
140 | 140 | data = form.cleaned_data |
|
141 | 141 | |
|
142 | title = data[FORM_TITLE] | |
|
142 | title = form.get_title() | |
|
143 | 143 | text = data[FORM_TEXT] |
|
144 |
|
|
|
144 | file = form.get_file() | |
|
145 | 145 | threads = data[FORM_THREADS] |
|
146 | 146 | |
|
147 | 147 | text = self._remove_invalid_links(text) |
|
148 | 148 | |
|
149 | 149 | tag_strings = data[FORM_TAGS] |
|
150 | 150 | |
|
151 | 151 | tags = self.parse_tags_string(tag_strings) |
|
152 | 152 | |
|
153 |
post = Post.objects.create_post(title=title, text=text, |
|
|
154 |
ip=ip, tags=tags, opening_posts=threads |
|
|
153 | post = Post.objects.create_post(title=title, text=text, file=file, | |
|
154 | ip=ip, tags=tags, opening_posts=threads, | |
|
155 | tripcode=form.get_tripcode()) | |
|
155 | 156 | |
|
156 | 157 | # This is required to update the threads to which posts we have replied |
|
157 | 158 | # when creating this one |
|
158 | 159 | post.notify_clients() |
|
159 | 160 | |
|
160 | 161 | if html_response: |
|
161 | 162 | return redirect(post.get_absolute_url()) |
|
162 | 163 | |
|
163 | 164 | def get_threads(self): |
|
164 | 165 | """ |
|
165 | 166 | Gets list of threads that will be shown on a page. |
|
166 | 167 | """ |
|
167 | 168 | |
|
168 | 169 | return Thread.objects.order_by('-bump_time')\ |
|
169 | 170 | .exclude(tags__in=self.settings_manager.get_hidden_tags()) |
@@ -1,71 +1,81 b'' | |||
|
1 | 1 | from django.core.urlresolvers import reverse |
|
2 |
from django. |
|
|
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 | |
|
2 | from django.shortcuts import render | |
|
9 | 3 | |
|
10 | from boards import utils, settings | |
|
11 | 4 | from boards.abstracts.paginator import get_paginator |
|
12 | 5 | from boards.abstracts.settingsmanager import get_settings_manager |
|
13 |
from boards.models import Post |
|
|
6 | from boards.models import Post | |
|
14 | 7 | from boards.views.base import BaseBoardView |
|
15 | 8 | from boards.views.posting_mixin import PostMixin |
|
16 | 9 | |
|
10 | POSTS_PER_PAGE = 10 | |
|
17 | 11 | |
|
18 | 12 | PARAMETER_CURRENT_PAGE = 'current_page' |
|
19 | 13 | PARAMETER_PAGINATOR = 'paginator' |
|
20 | 14 | PARAMETER_POSTS = 'posts' |
|
15 | PARAMETER_ADDITONAL_ATTRS = 'additional_attrs' | |
|
21 | 16 | |
|
22 | 17 | PARAMETER_PREV_LINK = 'prev_page_link' |
|
23 | 18 | PARAMETER_NEXT_LINK = 'next_page_link' |
|
24 | 19 | |
|
25 | 20 | TEMPLATE = 'boards/feed.html' |
|
26 | 21 | DEFAULT_PAGE = 1 |
|
27 | 22 | |
|
28 | 23 | |
|
29 | 24 | class FeedView(PostMixin, BaseBoardView): |
|
30 | 25 | |
|
31 | 26 | def get(self, request): |
|
32 | 27 | page = request.GET.get('page', DEFAULT_PAGE) |
|
28 | tripcode = request.GET.get('tripcode', None) | |
|
33 | 29 | |
|
34 | 30 | params = self.get_context_data(request=request) |
|
35 | 31 | |
|
36 | 32 | settings_manager = get_settings_manager(request) |
|
37 | 33 | |
|
38 |
p |
|
|
39 |
|
|
|
40 | .order_by('-pub_time') | |
|
41 | .prefetch_related('images', 'thread', 'threads'), 10) | |
|
34 | posts = Post.objects.exclude( | |
|
35 | threads__tags__in=settings_manager.get_hidden_tags()).order_by( | |
|
36 | '-pub_time').prefetch_related('images', 'thread', 'threads') | |
|
37 | if tripcode: | |
|
38 | posts = posts.filter(tripcode=tripcode) | |
|
39 | ||
|
40 | paginator = get_paginator(posts, POSTS_PER_PAGE) | |
|
42 | 41 | paginator.current_page = int(page) |
|
43 | 42 | |
|
44 | 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 | 52 | return render(request, TEMPLATE, params) |
|
49 | 53 | |
|
50 | 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 | 57 | Get pagination context variables |
|
54 | 58 | """ |
|
55 | 59 | |
|
56 | 60 | params[PARAMETER_PAGINATOR] = paginator |
|
57 | 61 | current_page = paginator.page(int(page)) |
|
58 | 62 | params[PARAMETER_CURRENT_PAGE] = current_page |
|
59 | 63 | if current_page.has_previous(): |
|
60 | 64 | params[PARAMETER_PREV_LINK] = self.get_previous_page_link( |
|
61 | 65 | current_page) |
|
66 | for param in additional_params.keys(): | |
|
67 | params[PARAMETER_PREV_LINK] += '&{}={}'.format( | |
|
68 | param, additional_params[param]) | |
|
62 | 69 | if current_page.has_next(): |
|
63 | 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 | 75 | def get_previous_page_link(self, current_page): |
|
66 | 76 | return reverse('feed') + '?page={}'.format( |
|
67 | 77 | current_page.previous_page_number()) |
|
68 | 78 | |
|
69 | 79 | def get_next_page_link(self, current_page): |
|
70 | 80 | return reverse('feed') + '?page={}'.format( |
|
71 | 81 | current_page.next_page_number()) |
@@ -1,77 +1,76 b'' | |||
|
1 | 1 | from django.db import transaction |
|
2 | 2 | from django.shortcuts import render, redirect |
|
3 | 3 | from django.utils import timezone |
|
4 | 4 | |
|
5 | 5 | from boards.abstracts.settingsmanager import get_settings_manager, \ |
|
6 | 6 | SETTING_USERNAME, SETTING_LAST_NOTIFICATION_ID, SETTING_IMAGE_VIEWER |
|
7 | 7 | from boards.middlewares import SESSION_TIMEZONE |
|
8 | 8 | from boards.views.base import BaseBoardView, CONTEXT_FORM |
|
9 | 9 | from boards.forms import SettingsForm, PlainErrorList |
|
10 | 10 | from boards import settings |
|
11 | 11 | |
|
12 | ||
|
13 | 12 | FORM_THEME = 'theme' |
|
14 | 13 | FORM_USERNAME = 'username' |
|
15 | 14 | FORM_TIMEZONE = 'timezone' |
|
16 | 15 | FORM_IMAGE_VIEWER = 'image_viewer' |
|
17 | 16 | |
|
18 | 17 | CONTEXT_HIDDEN_TAGS = 'hidden_tags' |
|
19 | 18 | |
|
20 | 19 | TEMPLATE = 'boards/settings.html' |
|
21 | 20 | |
|
22 | 21 | |
|
23 | 22 | class SettingsView(BaseBoardView): |
|
24 | 23 | |
|
25 | 24 | def get(self, request): |
|
26 | 25 | params = dict() |
|
27 | 26 | settings_manager = get_settings_manager(request) |
|
28 | 27 | |
|
29 | 28 | selected_theme = settings_manager.get_theme() |
|
30 | 29 | |
|
31 | 30 | form = SettingsForm( |
|
32 | 31 | initial={ |
|
33 | 32 | FORM_THEME: selected_theme, |
|
34 | 33 | FORM_IMAGE_VIEWER: settings_manager.get_setting( |
|
35 | 34 | SETTING_IMAGE_VIEWER, |
|
36 | 35 | default=settings.get('View', 'DefaultImageViewer')), |
|
37 | 36 | FORM_USERNAME: settings_manager.get_setting(SETTING_USERNAME), |
|
38 | 37 | FORM_TIMEZONE: request.session.get( |
|
39 | 38 | SESSION_TIMEZONE, timezone.get_current_timezone()), |
|
40 | 39 | }, |
|
41 | 40 | error_class=PlainErrorList) |
|
42 | 41 | |
|
43 | 42 | params[CONTEXT_FORM] = form |
|
44 | 43 | params[CONTEXT_HIDDEN_TAGS] = settings_manager.get_hidden_tags() |
|
45 | 44 | |
|
46 | 45 | return render(request, TEMPLATE, params) |
|
47 | 46 | |
|
48 | 47 | def post(self, request): |
|
49 | 48 | settings_manager = get_settings_manager(request) |
|
50 | 49 | |
|
51 | 50 | with transaction.atomic(): |
|
52 | 51 | form = SettingsForm(request.POST, error_class=PlainErrorList) |
|
53 | 52 | |
|
54 | 53 | if form.is_valid(): |
|
55 | 54 | selected_theme = form.cleaned_data[FORM_THEME] |
|
56 | 55 | username = form.cleaned_data[FORM_USERNAME].lower() |
|
57 | 56 | |
|
58 | 57 | settings_manager.set_theme(selected_theme) |
|
59 | 58 | settings_manager.set_setting(SETTING_IMAGE_VIEWER, |
|
60 | 59 | form.cleaned_data[FORM_IMAGE_VIEWER]) |
|
61 | 60 | |
|
62 | 61 | old_username = settings_manager.get_setting(SETTING_USERNAME) |
|
63 | 62 | if username != old_username: |
|
64 | 63 | settings_manager.set_setting(SETTING_USERNAME, username) |
|
65 | 64 | settings_manager.set_setting(SETTING_LAST_NOTIFICATION_ID, None) |
|
66 | 65 | |
|
67 | 66 | request.session[SESSION_TIMEZONE] = form.cleaned_data[FORM_TIMEZONE] |
|
68 | 67 | |
|
69 | 68 | return redirect('settings') |
|
70 | 69 | else: |
|
71 | 70 | params = dict() |
|
72 | 71 | |
|
73 | 72 | params[CONTEXT_FORM] = form |
|
74 | 73 | params[CONTEXT_HIDDEN_TAGS] = settings_manager.get_hidden_tags() |
|
75 | 74 | |
|
76 | 75 | return render(request, TEMPLATE, params) |
|
77 | 76 |
@@ -1,120 +1,126 b'' | |||
|
1 | 1 | from django.shortcuts import get_object_or_404, redirect |
|
2 | 2 | from django.core.urlresolvers import reverse |
|
3 | 3 | |
|
4 | 4 | from boards.abstracts.settingsmanager import get_settings_manager, \ |
|
5 | 5 | SETTING_FAVORITE_TAGS, SETTING_HIDDEN_TAGS |
|
6 | from boards.models import Tag | |
|
6 | from boards.models import Tag, PostImage | |
|
7 | 7 | from boards.views.all_threads import AllThreadsView, DEFAULT_PAGE |
|
8 | 8 | from boards.views.mixins import DispatcherMixin |
|
9 | 9 | from boards.forms import ThreadForm, PlainErrorList |
|
10 | 10 | |
|
11 | 11 | PARAM_HIDDEN_TAGS = 'hidden_tags' |
|
12 | 12 | PARAM_TAG = 'tag' |
|
13 | 13 | PARAM_IS_FAVORITE = 'is_favorite' |
|
14 | 14 | PARAM_IS_HIDDEN = 'is_hidden' |
|
15 | PARAM_RANDOM_IMAGE_POST = 'random_image_post' | |
|
16 | PARAM_RELATED_TAGS = 'related_tags' | |
|
17 | ||
|
15 | 18 | |
|
16 | 19 | __author__ = 'neko259' |
|
17 | 20 | |
|
18 | 21 | |
|
19 | 22 | class TagView(AllThreadsView, DispatcherMixin): |
|
20 | 23 | |
|
21 | 24 | tag_name = None |
|
22 | 25 | |
|
23 | 26 | def get_threads(self): |
|
24 | 27 | tag = get_object_or_404(Tag, name=self.tag_name) |
|
25 | 28 | |
|
26 | 29 | hidden_tags = self.settings_manager.get_hidden_tags() |
|
27 | 30 | |
|
28 | 31 | try: |
|
29 | 32 | hidden_tags.remove(tag) |
|
30 | 33 | except ValueError: |
|
31 | 34 | pass |
|
32 | 35 | |
|
33 | 36 | return tag.get_threads().exclude( |
|
34 | 37 | tags__in=hidden_tags) |
|
35 | 38 | |
|
36 | 39 | def get_context_data(self, **kwargs): |
|
37 | 40 | params = super(TagView, self).get_context_data(**kwargs) |
|
38 | 41 | |
|
39 | 42 | settings_manager = get_settings_manager(kwargs['request']) |
|
40 | 43 | |
|
41 | 44 | tag = get_object_or_404(Tag, name=self.tag_name) |
|
42 | 45 | params[PARAM_TAG] = tag |
|
43 | 46 | |
|
44 | 47 | fav_tag_names = settings_manager.get_setting(SETTING_FAVORITE_TAGS) |
|
45 | 48 | hidden_tag_names = settings_manager.get_setting(SETTING_HIDDEN_TAGS) |
|
46 | 49 | |
|
47 | 50 | params[PARAM_IS_FAVORITE] = fav_tag_names is not None and tag.name in fav_tag_names |
|
48 | 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 | 56 | return params |
|
51 | 57 | |
|
52 | 58 | def get_previous_page_link(self, current_page): |
|
53 | 59 | return reverse('tag', kwargs={ |
|
54 | 60 | 'tag_name': self.tag_name, |
|
55 | 61 | }) + '?page=' + str(current_page.previous_page_number()) |
|
56 | 62 | |
|
57 | 63 | def get_next_page_link(self, current_page): |
|
58 | 64 | return reverse('tag', kwargs={ |
|
59 | 65 | 'tag_name': self.tag_name, |
|
60 | 66 | }) + '?page=' + str(current_page.next_page_number()) |
|
61 | 67 | |
|
62 | 68 | def get(self, request, tag_name, form=None): |
|
63 | 69 | self.tag_name = tag_name |
|
64 | 70 | |
|
65 | 71 | return super(TagView, self).get(request, form) |
|
66 | 72 | |
|
67 | 73 | |
|
68 | 74 | def post(self, request, tag_name): |
|
69 | 75 | self.tag_name = tag_name |
|
70 | 76 | |
|
71 | 77 | if 'method' in request.POST: |
|
72 | 78 | self.dispatch_method(request) |
|
73 | 79 | form = None |
|
74 | 80 | |
|
75 | 81 | return redirect('tag', tag_name) |
|
76 | 82 | else: |
|
77 | 83 | form = ThreadForm(request.POST, request.FILES, |
|
78 | 84 | error_class=PlainErrorList) |
|
79 | 85 | form.session = request.session |
|
80 | 86 | |
|
81 | 87 | if form.is_valid(): |
|
82 | 88 | return self.create_thread(request, form) |
|
83 | 89 | if form.need_to_ban: |
|
84 | 90 | # Ban user because he is suspected to be a bot |
|
85 | 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 | 95 | def subscribe(self, request): |
|
90 | 96 | tag = get_object_or_404(Tag, name=self.tag_name) |
|
91 | 97 | |
|
92 | 98 | settings_manager = get_settings_manager(request) |
|
93 | 99 | settings_manager.add_fav_tag(tag) |
|
94 | 100 | |
|
95 | 101 | def unsubscribe(self, request): |
|
96 | 102 | tag = get_object_or_404(Tag, name=self.tag_name) |
|
97 | 103 | |
|
98 | 104 | settings_manager = get_settings_manager(request) |
|
99 | 105 | settings_manager.del_fav_tag(tag) |
|
100 | 106 | |
|
101 | 107 | def hide(self, request): |
|
102 | 108 | """ |
|
103 | 109 | Adds tag to user's hidden tags. Threads with this tag will not be |
|
104 | 110 | shown. |
|
105 | 111 | """ |
|
106 | 112 | |
|
107 | 113 | tag = get_object_or_404(Tag, name=self.tag_name) |
|
108 | 114 | |
|
109 | 115 | settings_manager = get_settings_manager(request) |
|
110 | 116 | settings_manager.add_hidden_tag(tag) |
|
111 | 117 | |
|
112 | 118 | def unhide(self, request): |
|
113 | 119 | """ |
|
114 | 120 | Removed tag from user's hidden tags. |
|
115 | 121 | """ |
|
116 | 122 | |
|
117 | 123 | tag = get_object_or_404(Tag, name=self.tag_name) |
|
118 | 124 | |
|
119 | 125 | settings_manager = get_settings_manager(request) |
|
120 | 126 | settings_manager.del_hidden_tag(tag) |
@@ -1,138 +1,140 b'' | |||
|
1 | import hashlib | |
|
1 | 2 | from django.core.exceptions import ObjectDoesNotExist |
|
2 | 3 | from django.http import Http404 |
|
3 | 4 | from django.shortcuts import get_object_or_404, render, redirect |
|
4 | 5 | from django.views.generic.edit import FormMixin |
|
5 | 6 | from django.utils import timezone |
|
6 | 7 | from django.utils.dateformat import format |
|
7 | 8 | |
|
8 | 9 | from boards import utils, settings |
|
9 | 10 | from boards.forms import PostForm, PlainErrorList |
|
10 | 11 | from boards.models import Post |
|
11 | 12 | from boards.views.base import BaseBoardView, CONTEXT_FORM |
|
12 | 13 | from boards.views.posting_mixin import PostMixin |
|
13 | 14 | |
|
14 | 15 | import neboard |
|
15 | 16 | |
|
16 | 17 | |
|
17 | 18 | CONTEXT_LASTUPDATE = "last_update" |
|
18 | 19 | CONTEXT_THREAD = 'thread' |
|
19 | 20 | CONTEXT_WS_TOKEN = 'ws_token' |
|
20 | 21 | CONTEXT_WS_PROJECT = 'ws_project' |
|
21 | 22 | CONTEXT_WS_HOST = 'ws_host' |
|
22 | 23 | CONTEXT_WS_PORT = 'ws_port' |
|
23 | 24 | CONTEXT_WS_TIME = 'ws_token_time' |
|
24 | 25 | CONTEXT_MODE = 'mode' |
|
25 | 26 | CONTEXT_OP = 'opening_post' |
|
26 | 27 | |
|
27 | 28 | FORM_TITLE = 'title' |
|
28 | 29 | FORM_TEXT = 'text' |
|
29 | 30 | FORM_IMAGE = 'image' |
|
30 | 31 | FORM_THREADS = 'threads' |
|
31 | 32 | |
|
32 | 33 | |
|
33 | 34 | class ThreadView(BaseBoardView, PostMixin, FormMixin): |
|
34 | 35 | |
|
35 | 36 | def get(self, request, post_id, form: PostForm=None): |
|
36 | 37 | try: |
|
37 | 38 | opening_post = Post.objects.get(id=post_id) |
|
38 | 39 | except ObjectDoesNotExist: |
|
39 | 40 | raise Http404 |
|
40 | 41 | |
|
41 | 42 | # If this is not OP, don't show it as it is |
|
42 | 43 | if not opening_post.is_opening(): |
|
43 | 44 | return redirect(opening_post.get_thread().get_opening_post() |
|
44 | 45 | .get_absolute_url()) |
|
45 | 46 | |
|
46 | 47 | if not form: |
|
47 | 48 | form = PostForm(error_class=PlainErrorList) |
|
48 | 49 | |
|
49 | 50 | thread_to_show = opening_post.get_thread() |
|
50 | 51 | |
|
51 | 52 | params = dict() |
|
52 | 53 | |
|
53 | 54 | params[CONTEXT_FORM] = form |
|
54 | 55 | params[CONTEXT_LASTUPDATE] = str(thread_to_show.last_edit_time) |
|
55 | 56 | params[CONTEXT_THREAD] = thread_to_show |
|
56 | 57 | params[CONTEXT_MODE] = self.get_mode() |
|
57 | 58 | params[CONTEXT_OP] = opening_post |
|
58 | 59 | |
|
59 | 60 | if settings.get_bool('External', 'WebsocketsEnabled'): |
|
60 | 61 | token_time = format(timezone.now(), u'U') |
|
61 | 62 | |
|
62 | 63 | params[CONTEXT_WS_TIME] = token_time |
|
63 | 64 | params[CONTEXT_WS_TOKEN] = utils.get_websocket_token( |
|
64 | 65 | timestamp=token_time) |
|
65 | 66 | params[CONTEXT_WS_PROJECT] = neboard.settings.CENTRIFUGE_PROJECT_ID |
|
66 | 67 | params[CONTEXT_WS_HOST] = request.get_host().split(':')[0] |
|
67 | 68 | params[CONTEXT_WS_PORT] = neboard.settings.CENTRIFUGE_PORT |
|
68 | 69 | |
|
69 | 70 | params.update(self.get_data(thread_to_show)) |
|
70 | 71 | |
|
71 | 72 | return render(request, self.get_template(), params) |
|
72 | 73 | |
|
73 | 74 | def post(self, request, post_id): |
|
74 | 75 | opening_post = get_object_or_404(Post, id=post_id) |
|
75 | 76 | |
|
76 | 77 | # If this is not OP, don't show it as it is |
|
77 | 78 | if not opening_post.is_opening(): |
|
78 | 79 | raise Http404 |
|
79 | 80 | |
|
80 | 81 | if not opening_post.get_thread().archived: |
|
81 | 82 | form = PostForm(request.POST, request.FILES, |
|
82 | 83 | error_class=PlainErrorList) |
|
83 | 84 | form.session = request.session |
|
84 | 85 | |
|
85 | 86 | if form.is_valid(): |
|
86 | 87 | return self.new_post(request, form, opening_post) |
|
87 | 88 | if form.need_to_ban: |
|
88 | 89 | # Ban user because he is suspected to be a bot |
|
89 | 90 | self._ban_current_user(request) |
|
90 | 91 | |
|
91 | 92 | return self.get(request, post_id, form) |
|
92 | 93 | |
|
93 | 94 | def new_post(self, request, form: PostForm, opening_post: Post=None, |
|
94 | 95 | html_response=True): |
|
95 | 96 | """ |
|
96 | 97 | Adds a new post (in thread or as a reply). |
|
97 | 98 | """ |
|
98 | 99 | |
|
99 | 100 | ip = utils.get_client_ip(request) |
|
100 | 101 | |
|
101 | 102 | data = form.cleaned_data |
|
102 | 103 | |
|
103 | title = data[FORM_TITLE] | |
|
104 | title = form.get_title() | |
|
104 | 105 | text = data[FORM_TEXT] |
|
105 |
|
|
|
106 | file = form.get_file() | |
|
106 | 107 | threads = data[FORM_THREADS] |
|
107 | 108 | |
|
108 | 109 | text = self._remove_invalid_links(text) |
|
109 | 110 | |
|
110 | 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 | 114 | thread=post_thread, ip=ip, |
|
114 |
opening_posts=threads |
|
|
115 | opening_posts=threads, | |
|
116 | tripcode=form.get_tripcode()) | |
|
115 | 117 | post.notify_clients() |
|
116 | 118 | |
|
117 | 119 | if html_response: |
|
118 | 120 | if opening_post: |
|
119 | 121 | return redirect(post.get_absolute_url()) |
|
120 | 122 | else: |
|
121 | 123 | return post |
|
122 | 124 | |
|
123 | 125 | def get_data(self, thread) -> dict: |
|
124 | 126 | """ |
|
125 | 127 | Returns context params for the view. |
|
126 | 128 | """ |
|
127 | 129 | |
|
128 | 130 | return dict() |
|
129 | 131 | |
|
130 | 132 | def get_template(self) -> str: |
|
131 | 133 | """ |
|
132 | 134 | Gets template to show the thread mode on. |
|
133 | 135 | """ |
|
134 | 136 | |
|
135 | 137 | pass |
|
136 | 138 | |
|
137 | 139 | def get_mode(self) -> str: |
|
138 | 140 | pass |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
General Comments 0
You need to be logged in to leave comments.
Login now