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