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