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