##// 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)
@@ -33,3 +33,4 b' 836d8bb9fcd930b952b9a02029442c71c2441983'
33 33 dfb6c481b1a2c33705de9a9b5304bc924c46b202 2.8.1
34 34 4a5bec08ccfb47a27f9e98698f12dd5b7246623b 2.8.2
35 35 604935b98f5b5e4a5e903594f048046e1fbb3519 2.8.3
36 c48ffdc671566069ed0f33644da1229277f3cd18 2.9.0
@@ -1,6 +1,7 b''
1 from django.shortcuts import get_object_or_404
2 1 from boards.models import Tag
3 2
3 MAX_TRIPCODE_COLLISIONS = 50
4
4 5 __author__ = 'neko259'
5 6
6 7 SESSION_SETTING = 'setting'
@@ -15,6 +16,7 b" 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
@@ -130,6 +132,7 b' class SessionSettingsManager(SettingsMan'
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):
@@ -1,5 +1,5 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]
@@ -9,7 +9,7 b' CacheTimeout = 600'
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]
@@ -1,7 +1,8 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
@@ -14,18 +15,11 b' from boards.models.post import TITLE_MAX'
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
@@ -47,7 +41,7 b" ERROR_SPEED = _('Please wait %s seconds "
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
@@ -137,17 +131,20 b' class NeboardForm(forms.Form):'
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'}))
@@ -181,27 +178,27 b' class PostForm(NeboardForm):'
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']
@@ -230,27 +227,40 b' class PostForm(NeboardForm):'
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):
@@ -284,16 +294,16 b' class PostForm(NeboardForm):'
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
@@ -302,30 +312,29 b' class PostForm(NeboardForm):'
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
@@ -356,8 +365,7 b' class ThreadForm(PostForm):'
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
1 NO CONTENT: modified file, binary diff hidden
@@ -7,7 +7,7 b' 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"
@@ -38,80 +38,84 b' msgstr "\xd1\x80\xd0\xb0\xd0\xb7\xd1\x80\xd0\xb0\xd0\xb1\xd0\xbe\xd1\x82\xd1\x87\xd0\xb8\xd0\xba javascript"'
38 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 "Метки"
@@ -121,26 +125,27 b' 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
@@ -156,54 +161,67 b' msgstr "\xd0\xad\xd1\x82\xd0\xbe\xd0\xb9 \xd1\x81\xd1\x82\xd1\x80\xd0\xb0\xd0\xbd\xd0\xb8\xd1\x86\xd1\x8b \xd0\xbd\xd0\xb5 \xd1\x81\xd1\x83\xd1\x89\xd0\xb5\xd1\x81\xd1\x82\xd0\xb2\xd1\x83\xd0\xb5\xd1\x82"'
156 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
@@ -251,25 +269,33 b' msgstr "\xd0\xbf\xd0\xbe\xd0\xb8\xd1\x81\xd0\xba"'
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
@@ -277,58 +303,62 b' msgstr "\xd0\x92\xd0\xb2\xd0\xb5\xd1\x80\xd1\x85"'
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 "Сохранить"
@@ -375,34 +405,35 b' msgstr "\xd0\x9a\xd0\xbe\xd0\xbc\xd0\xbc\xd0\xb5\xd0\xbd\xd1\x82\xd0\xb0\xd1\x80\xd0\xb8\xd0\xb9"'
375 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
@@ -418,14 +449,14 b' msgstr "\xd1\x81\xd0\xbe\xd0\xbe\xd0\xb1\xd1\x89\xd0\xb5\xd0\xbd\xd0\xb8\xd0\xb9 \xd0\xb4\xd0\xbe \xd0\xb1\xd0\xb0\xd0\xbc\xd0\xbf\xd0\xbb\xd0\xb8\xd0\xbc\xd0\xb8\xd1\x82\xd0\xb0"'
418 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 "Ок"
@@ -2,6 +2,8 b' 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
@@ -11,7 +13,7 b' from neboard.settings import MEDIA_ROOT'
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):
@@ -21,10 +23,24 b' class Command(BaseCommand):'
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))
@@ -130,7 +130,7 b' def render_reflink(tag_name, value, opti'
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
@@ -3,6 +3,7 b''
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
@@ -2,8 +2,12 b' 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'
@@ -20,7 +24,7 b" CSS_CLASS_THUMB = 'thumb'"
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]
@@ -29,14 +33,11 b' class PostImageManager(models.Manager):'
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):
@@ -51,15 +52,13 b' class PostImage(models.Model, Viewable):'
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)
@@ -81,13 +80,15 b' class PostImage(models.Model, Viewable):'
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"' \
@@ -98,8 +99,16 b' class PostImage(models.Model, Viewable):'
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,3 +1,5 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
@@ -6,15 +8,14 b' from django.core.exceptions import Objec'
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
@@ -61,8 +62,6 b' POST_VIEW_PARAMS = ('
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."""
@@ -79,7 +78,9 b' class Post(models.Model, Viewable):'
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
@@ -101,6 +102,8 b' class Post(models.Model, Viewable):'
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
@@ -126,7 +129,7 b' class Post(models.Model, Viewable):'
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)
@@ -209,10 +212,15 b' class Post(models.Model, Viewable):'
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
@@ -399,3 +407,19 b' class Post(models.Model, Viewable):'
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
@@ -5,7 +5,7 b' from django.db import models, transactio'
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'
@@ -14,11 +14,19 b' import boards.models'
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 """
@@ -50,49 +58,33 b' class PostManager(models.Manager):'
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
@@ -126,3 +118,23 b' class PostManager(models.Manager):'
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()
@@ -5,9 +5,12 b' from django.core.urlresolvers import rev'
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):
@@ -17,7 +20,7 b' class TagManager(models.Manager):'
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:
@@ -42,6 +45,7 b' class Tag(models.Model, Viewable):'
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
@@ -53,14 +57,20 b' class Tag(models.Model, Viewable):'
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
@@ -80,3 +90,20 b' class Tag(models.Model, Viewable):'
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])
@@ -65,7 +65,7 b' class Thread(models.Model):'
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)
@@ -225,3 +225,6 b' class Thread(models.Model):'
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)
@@ -98,8 +98,39 b' textarea, input {'
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
@@ -167,7 +167,7 b' p, .br {'
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;
@@ -192,17 +192,22 b' p, .br {'
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;
@@ -231,12 +236,12 b' blockquote {'
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 {
@@ -354,8 +359,6 b' li {'
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
@@ -437,7 +440,6 b' li {'
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 }
@@ -528,7 +530,7 b' ul {'
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 {
@@ -536,10 +538,9 b' ul {'
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 {
@@ -548,8 +549,11 b' ul {'
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 }
@@ -376,3 +376,8 b' input[type="submit"]:hover {'
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
@@ -407,3 +407,12 b' li {'
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 }
@@ -42,12 +42,24 b' SimpleImageViewer.prototype.view = funct'
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 }
@@ -24,6 +24,9 b''
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) {
@@ -46,6 +49,7 b' function resetFormPosition() {'
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) {
@@ -54,6 +58,8 b' function showFormAfter(blockToInsertAfte'
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) {
@@ -30,6 +30,14 b' 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;
@@ -165,7 +173,8 b' function updatePost(postHtml) {'
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 {
@@ -362,6 +371,64 b' function processNewPost(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
@@ -386,6 +453,4 b' function processNewPost(post) {'
386 453 resetForm(form);
387 454 }
388 455 }
389
390 $('#autoupdate').click(getThreadDiff);
391 456 });
@@ -38,6 +38,17 b''
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 %}
@@ -53,14 +64,23 b''
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
@@ -116,13 +136,13 b''
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>
@@ -38,7 +38,8 b''
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' %}">
@@ -61,7 +61,7 b''
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 %}
@@ -5,9 +5,16 b''
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
@@ -29,7 +36,7 b''
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 %}
@@ -43,7 +50,7 b''
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 %}
@@ -55,10 +62,13 b''
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 %}
@@ -72,6 +82,7 b''
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">
@@ -81,13 +92,10 b''
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 %}
@@ -98,9 +106,7 b''
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 %}
@@ -9,7 +9,6 b''
9 9 {% endblock %}
10 10
11 11 {% block content %}
12
13 12 <div class="post">
14 13 <p>
15 14 {% if moderator %}
@@ -19,9 +18,7 b''
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 %}
@@ -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>
@@ -8,20 +8,39 b''
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
@@ -31,9 +31,6 b''
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>
@@ -28,7 +28,7 b''
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 %}
@@ -36,10 +36,10 b''
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>
@@ -55,7 +55,3 b''
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 %}
@@ -11,6 +11,7 b' from boards.views.search import BoardSea'
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 = {
@@ -43,6 +44,8 b" urlpatterns = patterns('',"
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()),
@@ -1,6 +1,7 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
@@ -81,4 +82,11 b' def is_moderator(request):'
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()
@@ -4,6 +4,7 b' from boards.views.base import BaseBoardV'
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
@@ -13,10 +14,10 b' class AllTagsView(BaseBoardView):'
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)
@@ -139,9 +139,9 b' class AllThreadsView(PostMixin, BaseBoar'
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)
@@ -150,8 +150,9 b' class AllThreadsView(PostMixin, BaseBoar'
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
@@ -1,23 +1,18 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'
@@ -30,25 +25,34 b' 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 """
@@ -59,8 +63,14 b' class FeedView(PostMixin, BaseBoardView)'
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(
@@ -9,7 +9,6 b' from boards.views.base import BaseBoardV'
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'
@@ -3,7 +3,7 b' from django.core.urlresolvers import rev'
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
@@ -12,6 +12,9 b" 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
@@ -47,6 +50,9 b' class TagView(AllThreadsView, Dispatcher'
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):
@@ -84,7 +90,7 b' class TagView(AllThreadsView, Dispatcher'
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)
@@ -1,3 +1,4 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
@@ -100,18 +101,19 b' class ThreadView(BaseBoardView, PostMixi'
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:
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