##// END OF EJS Templates
Partly merged with default branch
neko259 -
r1157:d41f2b1c merge decentral
parent child Browse files
Show More
@@ -0,0 +1,33 b''
1 [Version]
2 Version = 2.7.0 Chani
3 SiteName = Neboard
4
5 [Cache]
6 # Timeout for caching, if cache is used
7 CacheTimeout = 600
8
9 [Forms]
10 # Max post length in characters
11 MaxTextLength = 30000
12 MaxImageSize = 8000000
13 LimitPostingSpeed = false
14
15 [Messages]
16 # Thread bumplimit
17 MaxPostsPerThread = 10
18 # Old posts will be archived or deleted if this value is reached
19 MaxThreadCount = 5
20
21 [View]
22 DefaultTheme = md
23 DefaultImageViewer = simple
24 LastRepliesCount = 3
25 ThreadsPerPage = 3
26
27 [Storage]
28 # Enable archiving threads instead of deletion when the thread limit is reached
29 ArchiveThreads = true
30
31 [External]
32 # Thread update
33 WebsocketsEnabled = false
@@ -0,0 +1,23 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', '0017_auto_20150503_1847'),
11 ]
12
13 operations = [
14 migrations.CreateModel(
15 name='Banner',
16 fields=[
17 ('id', models.AutoField(serialize=False, primary_key=True, verbose_name='ID', auto_created=True)),
18 ('title', models.TextField()),
19 ('text', models.TextField()),
20 ('post', models.ForeignKey(to='boards.Post')),
21 ],
22 ),
23 ]
@@ -0,0 +1,10 b''
1 from django.db import models
2
3
4 class Banner(models.Model):
5 title = models.TextField()
6 text = models.TextField()
7 post = models.ForeignKey('Post')
8
9 def __str__(self):
10 return self.title
@@ -0,0 +1,55 b''
1 from boards import utils
2
3
4 PARAMETER_TRUNCATED = 'truncated'
5
6 DIFF_TYPE_HTML = 'html'
7 DIFF_TYPE_JSON = 'json'
8
9
10 class Exporter():
11 @staticmethod
12 def export(post, request, include_last_update) -> str:
13 pass
14
15
16 class HtmlExporter(Exporter):
17 @staticmethod
18 def export(post, request, include_last_update):
19 if request is not None and PARAMETER_TRUNCATED in request.GET:
20 truncated = True
21 reply_link = False
22 else:
23 truncated = False
24 reply_link = True
25
26 return post.get_view(truncated=truncated, reply_link=reply_link,
27 moderator=utils.is_moderator(request))
28
29
30 class JsonExporter(Exporter):
31 @staticmethod
32 def export(post, request, include_last_update):
33 post_json = {
34 'id': post.id,
35 'title': post.title,
36 'text': post.get_raw_text(),
37 }
38 if post.images.exists():
39 post_image = post.get_first_image()
40 post_json['image'] = post_image.image.url
41 post_json['image_preview'] = post_image.image.url_200x150
42 if include_last_update:
43 post_json['bump_time'] = utils.datetime_to_epoch(
44 post.get_thread().bump_time)
45 return post_json
46
47
48 EXPORTERS = {
49 DIFF_TYPE_HTML: HtmlExporter,
50 DIFF_TYPE_JSON: JsonExporter,
51 }
52
53
54 def get_exporter(export_type: str) -> Exporter:
55 return EXPORTERS[export_type]()
@@ -1,30 +1,31 b''
1 bc8fce57a613175450b8b6d933cdd85f22c04658 1.1
1 bc8fce57a613175450b8b6d933cdd85f22c04658 1.1
2 784258eb652c563c288ca7652c33f52cd4733d83 1.1-stable
2 784258eb652c563c288ca7652c33f52cd4733d83 1.1-stable
3 1b53a22467a8fccc798935d7a26efe78e4bc7b25 1.2-stable
3 1b53a22467a8fccc798935d7a26efe78e4bc7b25 1.2-stable
4 1713fb7543386089e364c39703b79e57d3d851f0 1.3
4 1713fb7543386089e364c39703b79e57d3d851f0 1.3
5 80f183ebbe132ea8433eacae9431360f31fe7083 1.4
5 80f183ebbe132ea8433eacae9431360f31fe7083 1.4
6 4330ff5a2bf6c543d8aaae8a43de1dc062f3bd13 1.4.1
6 4330ff5a2bf6c543d8aaae8a43de1dc062f3bd13 1.4.1
7 8531d7b001392289a6b761f38c73a257606552ad 1.5
7 8531d7b001392289a6b761f38c73a257606552ad 1.5
8 78e843c8b04b5a81cee5aa24601e305fae75da24 1.5.1
8 78e843c8b04b5a81cee5aa24601e305fae75da24 1.5.1
9 4f92838730ed9aa1d17651bbcdca19a097fd0c37 1.6
9 4f92838730ed9aa1d17651bbcdca19a097fd0c37 1.6
10 4bac2f37ea463337ddd27f98e7985407a74de504 1.7
10 4bac2f37ea463337ddd27f98e7985407a74de504 1.7
11 1c4febea92c6503ae557fba73b2768659ae90d24 1.7.1
11 1c4febea92c6503ae557fba73b2768659ae90d24 1.7.1
12 56a4a4578fc454ee455e33dd74a2cc82234bcb59 1.7.2
12 56a4a4578fc454ee455e33dd74a2cc82234bcb59 1.7.2
13 34d6f3d5deb22be56b6c1512ec654bd7f6e03bcc 1.7.3
13 34d6f3d5deb22be56b6c1512ec654bd7f6e03bcc 1.7.3
14 f5cca33d29c673b67d43f310bebc4e3a21c6d04c 1.7.4
14 f5cca33d29c673b67d43f310bebc4e3a21c6d04c 1.7.4
15 7f7c33ba6e3f3797ca866c5ed5d358a2393f1371 1.8
15 7f7c33ba6e3f3797ca866c5ed5d358a2393f1371 1.8
16 a6b9dd9547bdc17b681502efcceb17aa5c09adf4 1.8.1
16 a6b9dd9547bdc17b681502efcceb17aa5c09adf4 1.8.1
17 8318fa1615d1946e4519f5735ae880909521990d 2.0
17 8318fa1615d1946e4519f5735ae880909521990d 2.0
18 e23590ee7e2067a3f0fc3cbcfd66404b47127feb 2.1
18 e23590ee7e2067a3f0fc3cbcfd66404b47127feb 2.1
19 4d998aba79e4abf0a2e78e93baaa2c2800b1c49c 2.2
19 4d998aba79e4abf0a2e78e93baaa2c2800b1c49c 2.2
20 07fdef4ac33a859250d03f17c594089792bca615 2.2.1
20 07fdef4ac33a859250d03f17c594089792bca615 2.2.1
21 bcc74d45f060ecd3ff06ff448165aea0d026cb3e 2.2.2
21 bcc74d45f060ecd3ff06ff448165aea0d026cb3e 2.2.2
22 b0e629ff24eb47a449ecfb455dc6cc600d18c9e2 2.2.3
22 b0e629ff24eb47a449ecfb455dc6cc600d18c9e2 2.2.3
23 1b52ba60f17fd7c90912c14d9d17e880b7952d01 2.2.4
23 1b52ba60f17fd7c90912c14d9d17e880b7952d01 2.2.4
24 957e2fec91468f739b0fc2b9936d564505048c68 2.3.0
24 957e2fec91468f739b0fc2b9936d564505048c68 2.3.0
25 bb91141c6ea5c822ccbe2d46c3c48bdab683b77d 2.4.0
25 bb91141c6ea5c822ccbe2d46c3c48bdab683b77d 2.4.0
26 97eb184637e5691b288eaf6b03e8971f3364c239 2.5.0
26 97eb184637e5691b288eaf6b03e8971f3364c239 2.5.0
27 119fafc5381b933bf30d97be0b278349f6135075 2.5.1
27 119fafc5381b933bf30d97be0b278349f6135075 2.5.1
28 d528d76d3242cced614fa11bb63f3d342e4e1d09 2.5.2
28 d528d76d3242cced614fa11bb63f3d342e4e1d09 2.5.2
29 1b631781ced34fbdeec032e7674bc4e131724699 2.6.0
29 1b631781ced34fbdeec032e7674bc4e131724699 2.6.0
30 0f2ef17dc0de678ada279bf7eedf6c5585f1fd7a 2.6.1
30 0f2ef17dc0de678ada279bf7eedf6c5585f1fd7a 2.6.1
31 d53fc814a424d7fd90f23025c87b87baa164450e 2.7.0
@@ -1,66 +1,71 b''
1 from django.contrib import admin
1 from django.contrib import admin
2 from boards.models import Post, Tag, Ban, Thread, KeyPair
2 from boards.models import Post, Tag, Ban, Thread, KeyPair, Banner
3 from django.utils.translation import ugettext_lazy as _
3 from django.utils.translation import ugettext_lazy as _
4
4
5
5
6 @admin.register(Post)
6 @admin.register(Post)
7 class PostAdmin(admin.ModelAdmin):
7 class PostAdmin(admin.ModelAdmin):
8
8
9 list_display = ('id', 'title', 'text', 'poster_ip')
9 list_display = ('id', 'title', 'text', 'poster_ip')
10 list_filter = ('pub_time',)
10 list_filter = ('pub_time',)
11 search_fields = ('id', 'title', 'text', 'poster_ip')
11 search_fields = ('id', 'title', 'text', 'poster_ip')
12 exclude = ('referenced_posts', 'refmap')
12 exclude = ('referenced_posts', 'refmap')
13 readonly_fields = ('poster_ip', 'threads', 'thread', 'images', 'uid')
13 readonly_fields = ('poster_ip', 'threads', 'thread', 'images', 'uid')
14
14
15 def ban_poster(self, request, queryset):
15 def ban_poster(self, request, queryset):
16 bans = 0
16 bans = 0
17 for post in queryset:
17 for post in queryset:
18 poster_ip = post.poster_ip
18 poster_ip = post.poster_ip
19 ban, created = Ban.objects.get_or_create(ip=poster_ip)
19 ban, created = Ban.objects.get_or_create(ip=poster_ip)
20 if created:
20 if created:
21 bans += 1
21 bans += 1
22 self.message_user(request, _('{} posters were banned').format(bans))
22 self.message_user(request, _('{} posters were banned').format(bans))
23
23
24 actions = ['ban_poster']
24 actions = ['ban_poster']
25
25
26
26
27 @admin.register(Tag)
27 @admin.register(Tag)
28 class TagAdmin(admin.ModelAdmin):
28 class TagAdmin(admin.ModelAdmin):
29
29
30 def thread_count(self, obj: Tag) -> int:
30 def thread_count(self, obj: Tag) -> int:
31 return obj.get_thread_count()
31 return obj.get_thread_count()
32
32
33 list_display = ('name', 'thread_count')
33 list_display = ('name', 'thread_count')
34 search_fields = ('name',)
34 search_fields = ('name',)
35
35
36
36
37 @admin.register(Thread)
37 @admin.register(Thread)
38 class ThreadAdmin(admin.ModelAdmin):
38 class ThreadAdmin(admin.ModelAdmin):
39
39
40 def title(self, obj: Thread) -> str:
40 def title(self, obj: Thread) -> str:
41 return obj.get_opening_post().get_title()
41 return obj.get_opening_post().get_title()
42
42
43 def reply_count(self, obj: Thread) -> int:
43 def reply_count(self, obj: Thread) -> int:
44 return obj.get_reply_count()
44 return obj.get_reply_count()
45
45
46 def ip(self, obj: Thread):
46 def ip(self, obj: Thread):
47 return obj.get_opening_post().poster_ip
47 return obj.get_opening_post().poster_ip
48
48
49 list_display = ('id', 'title', 'reply_count', 'archived', 'ip')
49 list_display = ('id', 'title', 'reply_count', 'archived', 'ip')
50 list_filter = ('bump_time', 'archived', 'bumpable')
50 list_filter = ('bump_time', 'archived', 'bumpable')
51 search_fields = ('id', 'title')
51 search_fields = ('id', 'title')
52 filter_horizontal = ('tags',)
52 filter_horizontal = ('tags',)
53
53
54
54
55 @admin.register(KeyPair)
55 @admin.register(KeyPair)
56 class KeyPairAdmin(admin.ModelAdmin):
56 class KeyPairAdmin(admin.ModelAdmin):
57 list_display = ('public_key', 'primary')
57 list_display = ('public_key', 'primary')
58 list_filter = ('primary',)
58 list_filter = ('primary',)
59 search_fields = ('public_key',)
59 search_fields = ('public_key',)
60
60
61
61
62 @admin.register(Ban)
62 @admin.register(Ban)
63 class BanAdmin(admin.ModelAdmin):
63 class BanAdmin(admin.ModelAdmin):
64 list_display = ('ip', 'can_read')
64 list_display = ('ip', 'can_read')
65 list_filter = ('can_read',)
65 list_filter = ('can_read',)
66 search_fields = ('ip',)
66 search_fields = ('ip',)
67
68
69 @admin.register(Banner)
70 class BannerAdmin(admin.ModelAdmin):
71 list_display = ('title', 'text')
@@ -1,62 +1,63 b''
1 from boards.abstracts.settingsmanager import get_settings_manager, \
1 from boards.abstracts.settingsmanager import get_settings_manager, \
2 SETTING_USERNAME, SETTING_LAST_NOTIFICATION_ID, SETTING_IMAGE_VIEWER
2 SETTING_USERNAME, SETTING_LAST_NOTIFICATION_ID, SETTING_IMAGE_VIEWER
3 from boards.models.user import Notification
3 from boards.models.user import Notification
4
4
5 __author__ = 'neko259'
5 __author__ = 'neko259'
6
6
7 from boards import settings, utils
7 from boards import settings, utils
8 from boards.models import Post, Tag
8 from boards.models import Post, Tag
9
9
10 CONTEXT_SITE_NAME = 'site_name'
10 CONTEXT_SITE_NAME = 'site_name'
11 CONTEXT_VERSION = 'version'
11 CONTEXT_VERSION = 'version'
12 CONTEXT_MODERATOR = 'moderator'
12 CONTEXT_MODERATOR = 'moderator'
13 CONTEXT_THEME_CSS = 'theme_css'
13 CONTEXT_THEME_CSS = 'theme_css'
14 CONTEXT_THEME = 'theme'
14 CONTEXT_THEME = 'theme'
15 CONTEXT_PPD = 'posts_per_day'
15 CONTEXT_PPD = 'posts_per_day'
16 CONTEXT_TAGS = 'tags'
16 CONTEXT_TAGS = 'tags'
17 CONTEXT_USER = 'user'
17 CONTEXT_USER = 'user'
18 CONTEXT_NEW_NOTIFICATIONS_COUNT = 'new_notifications_count'
18 CONTEXT_NEW_NOTIFICATIONS_COUNT = 'new_notifications_count'
19 CONTEXT_USERNAME = 'username'
19 CONTEXT_USERNAME = 'username'
20 CONTEXT_TAGS_STR = 'tags_str'
20 CONTEXT_TAGS_STR = 'tags_str'
21 CONTEXT_IMAGE_VIEWER = 'image_viewer'
21 CONTEXT_IMAGE_VIEWER = 'image_viewer'
22
22
23
23
24 def get_notifications(context, request):
24 def get_notifications(context, request):
25 settings_manager = get_settings_manager(request)
25 settings_manager = get_settings_manager(request)
26 username = settings_manager.get_setting(SETTING_USERNAME)
26 username = settings_manager.get_setting(SETTING_USERNAME)
27 new_notifications_count = 0
27 new_notifications_count = 0
28 if username is not None and len(username) > 0:
28 if username is not None and len(username) > 0:
29 last_notification_id = settings_manager.get_setting(
29 last_notification_id = settings_manager.get_setting(
30 SETTING_LAST_NOTIFICATION_ID)
30 SETTING_LAST_NOTIFICATION_ID)
31
31
32 new_notifications_count = Notification.objects.get_notification_posts(
32 new_notifications_count = Notification.objects.get_notification_posts(
33 username=username, last=last_notification_id).count()
33 username=username, last=last_notification_id).count()
34 context[CONTEXT_NEW_NOTIFICATIONS_COUNT] = new_notifications_count
34 context[CONTEXT_NEW_NOTIFICATIONS_COUNT] = new_notifications_count
35 context[CONTEXT_USERNAME] = username
35 context[CONTEXT_USERNAME] = username
36
36
37
37
38 def user_and_ui_processor(request):
38 def user_and_ui_processor(request):
39 context = dict()
39 context = dict()
40
40
41 context[CONTEXT_PPD] = float(Post.objects.get_posts_per_day())
41 context[CONTEXT_PPD] = float(Post.objects.get_posts_per_day())
42
42
43 settings_manager = get_settings_manager(request)
43 settings_manager = get_settings_manager(request)
44 fav_tags = settings_manager.get_fav_tags()
44 fav_tags = settings_manager.get_fav_tags()
45 context[CONTEXT_TAGS] = fav_tags
45 context[CONTEXT_TAGS] = fav_tags
46 context[CONTEXT_TAGS_STR] = Tag.objects.get_tag_url_list(fav_tags)
46 context[CONTEXT_TAGS_STR] = Tag.objects.get_tag_url_list(fav_tags)
47 theme = settings_manager.get_theme()
47 theme = settings_manager.get_theme()
48 context[CONTEXT_THEME] = theme
48 context[CONTEXT_THEME] = theme
49 context[CONTEXT_THEME_CSS] = 'css/' + theme + '/base_page.css'
49 context[CONTEXT_THEME_CSS] = 'css/' + theme + '/base_page.css'
50
50
51 # This shows the moderator panel
51 # This shows the moderator panel
52 context[CONTEXT_MODERATOR] = utils.is_moderator(request)
52 context[CONTEXT_MODERATOR] = utils.is_moderator(request)
53
53
54 context[CONTEXT_VERSION] = settings.VERSION
54 context[CONTEXT_VERSION] = settings.get('Version', 'Version')
55 context[CONTEXT_SITE_NAME] = settings.SITE_NAME
55 context[CONTEXT_SITE_NAME] = settings.get('Version', 'SiteName')
56
56
57 context[CONTEXT_IMAGE_VIEWER] = settings_manager.get_setting(
57 context[CONTEXT_IMAGE_VIEWER] = settings_manager.get_setting(
58 SETTING_IMAGE_VIEWER, default=settings.DEFAULT_IMAGE_VIEWER)
58 SETTING_IMAGE_VIEWER,
59 default=settings.get('View', 'DefaultImageViewer'))
59
60
60 get_notifications(context, request)
61 get_notifications(context, request)
61
62
62 return context
63 return context
@@ -1,384 +1,384 b''
1 import re
1 import re
2 import time
2 import time
3 import pytz
3 import pytz
4
4
5 from django import forms
5 from django import forms
6 from django.core.files.uploadedfile import SimpleUploadedFile
6 from django.core.files.uploadedfile import SimpleUploadedFile
7 from django.core.exceptions import ObjectDoesNotExist
7 from django.core.exceptions import ObjectDoesNotExist
8 from django.forms.util import ErrorList
8 from django.forms.util import ErrorList
9 from django.utils.translation import ugettext_lazy as _
9 from django.utils.translation import ugettext_lazy as _
10 import requests
10 import requests
11
11
12 from boards.mdx_neboard import formatters
12 from boards.mdx_neboard import formatters
13 from boards.models.post import TITLE_MAX_LENGTH
13 from boards.models.post import TITLE_MAX_LENGTH
14 from boards.models import Tag, Post
14 from boards.models import Tag, Post
15 from neboard import settings
15 from neboard import settings
16 import boards.settings as board_settings
16 import boards.settings as board_settings
17
17
18
18
19 CONTENT_TYPE_IMAGE = (
19 CONTENT_TYPE_IMAGE = (
20 'image/jpeg',
20 'image/jpeg',
21 'image/png',
21 'image/png',
22 'image/gif',
22 'image/gif',
23 'image/bmp',
23 'image/bmp',
24 )
24 )
25
25
26 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
26 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
27
27
28 VETERAN_POSTING_DELAY = 5
28 VETERAN_POSTING_DELAY = 5
29
29
30 ATTRIBUTE_PLACEHOLDER = 'placeholder'
30 ATTRIBUTE_PLACEHOLDER = 'placeholder'
31 ATTRIBUTE_ROWS = 'rows'
31 ATTRIBUTE_ROWS = 'rows'
32
32
33 LAST_POST_TIME = 'last_post_time'
33 LAST_POST_TIME = 'last_post_time'
34 LAST_LOGIN_TIME = 'last_login_time'
34 LAST_LOGIN_TIME = 'last_login_time'
35 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
35 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
36 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
36 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
37
37
38 LABEL_TITLE = _('Title')
38 LABEL_TITLE = _('Title')
39 LABEL_TEXT = _('Text')
39 LABEL_TEXT = _('Text')
40 LABEL_TAG = _('Tag')
40 LABEL_TAG = _('Tag')
41 LABEL_SEARCH = _('Search')
41 LABEL_SEARCH = _('Search')
42
42
43 ERROR_SPEED = _('Please wait %s seconds before sending message')
43 ERROR_SPEED = _('Please wait %s seconds before sending message')
44
44
45 TAG_MAX_LENGTH = 20
45 TAG_MAX_LENGTH = 20
46
46
47 IMAGE_DOWNLOAD_CHUNK_BYTES = 100000
47 IMAGE_DOWNLOAD_CHUNK_BYTES = 100000
48
48
49 HTTP_RESULT_OK = 200
49 HTTP_RESULT_OK = 200
50
50
51 TEXTAREA_ROWS = 4
51 TEXTAREA_ROWS = 4
52
52
53
53
54 def get_timezones():
54 def get_timezones():
55 timezones = []
55 timezones = []
56 for tz in pytz.common_timezones:
56 for tz in pytz.common_timezones:
57 timezones.append((tz, tz),)
57 timezones.append((tz, tz),)
58 return timezones
58 return timezones
59
59
60
60
61 class FormatPanel(forms.Textarea):
61 class FormatPanel(forms.Textarea):
62 """
62 """
63 Panel for text formatting. Consists of buttons to add different tags to the
63 Panel for text formatting. Consists of buttons to add different tags to the
64 form text area.
64 form text area.
65 """
65 """
66
66
67 def render(self, name, value, attrs=None):
67 def render(self, name, value, attrs=None):
68 output = '<div id="mark-panel">'
68 output = '<div id="mark-panel">'
69 for formatter in formatters:
69 for formatter in formatters:
70 output += '<span class="mark_btn"' + \
70 output += '<span class="mark_btn"' + \
71 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
71 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
72 '\', \'' + formatter.format_right + '\')">' + \
72 '\', \'' + formatter.format_right + '\')">' + \
73 formatter.preview_left + formatter.name + \
73 formatter.preview_left + formatter.name + \
74 formatter.preview_right + '</span>'
74 formatter.preview_right + '</span>'
75
75
76 output += '</div>'
76 output += '</div>'
77 output += super(FormatPanel, self).render(name, value, attrs=None)
77 output += super(FormatPanel, self).render(name, value, attrs=None)
78
78
79 return output
79 return output
80
80
81
81
82 class PlainErrorList(ErrorList):
82 class PlainErrorList(ErrorList):
83 def __unicode__(self):
83 def __unicode__(self):
84 return self.as_text()
84 return self.as_text()
85
85
86 def as_text(self):
86 def as_text(self):
87 return ''.join(['(!) %s ' % e for e in self])
87 return ''.join(['(!) %s ' % e for e in self])
88
88
89
89
90 class NeboardForm(forms.Form):
90 class NeboardForm(forms.Form):
91 """
91 """
92 Form with neboard-specific formatting.
92 Form with neboard-specific formatting.
93 """
93 """
94
94
95 def as_div(self):
95 def as_div(self):
96 """
96 """
97 Returns this form rendered as HTML <as_div>s.
97 Returns this form rendered as HTML <as_div>s.
98 """
98 """
99
99
100 return self._html_output(
100 return self._html_output(
101 # TODO Do not show hidden rows in the list here
101 # TODO Do not show hidden rows in the list here
102 normal_row='<div class="form-row">'
102 normal_row='<div class="form-row">'
103 '<div class="form-label">'
103 '<div class="form-label">'
104 '%(label)s'
104 '%(label)s'
105 '</div>'
105 '</div>'
106 '<div class="form-input">'
106 '<div class="form-input">'
107 '%(field)s'
107 '%(field)s'
108 '</div>'
108 '</div>'
109 '</div>'
109 '</div>'
110 '<div class="form-row">'
110 '<div class="form-row">'
111 '%(help_text)s'
111 '%(help_text)s'
112 '</div>',
112 '</div>',
113 error_row='<div class="form-row">'
113 error_row='<div class="form-row">'
114 '<div class="form-label"></div>'
114 '<div class="form-label"></div>'
115 '<div class="form-errors">%s</div>'
115 '<div class="form-errors">%s</div>'
116 '</div>',
116 '</div>',
117 row_ender='</div>',
117 row_ender='</div>',
118 help_text_html='%s',
118 help_text_html='%s',
119 errors_on_separate_row=True)
119 errors_on_separate_row=True)
120
120
121 def as_json_errors(self):
121 def as_json_errors(self):
122 errors = []
122 errors = []
123
123
124 for name, field in list(self.fields.items()):
124 for name, field in list(self.fields.items()):
125 if self[name].errors:
125 if self[name].errors:
126 errors.append({
126 errors.append({
127 'field': name,
127 'field': name,
128 'errors': self[name].errors.as_text(),
128 'errors': self[name].errors.as_text(),
129 })
129 })
130
130
131 return errors
131 return errors
132
132
133
133
134 class PostForm(NeboardForm):
134 class PostForm(NeboardForm):
135
135
136 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
136 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
137 label=LABEL_TITLE)
137 label=LABEL_TITLE)
138 text = forms.CharField(
138 text = forms.CharField(
139 widget=FormatPanel(attrs={
139 widget=FormatPanel(attrs={
140 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
140 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
141 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
141 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
142 }),
142 }),
143 required=False, label=LABEL_TEXT)
143 required=False, label=LABEL_TEXT)
144 image = forms.ImageField(required=False, label=_('Image'),
144 image = forms.ImageField(required=False, label=_('Image'),
145 widget=forms.ClearableFileInput(
145 widget=forms.ClearableFileInput(
146 attrs={'accept': 'image/*'}))
146 attrs={'accept': 'image/*'}))
147 image_url = forms.CharField(required=False, label=_('Image URL'),
147 image_url = forms.CharField(required=False, label=_('Image URL'),
148 widget=forms.TextInput(
148 widget=forms.TextInput(
149 attrs={ATTRIBUTE_PLACEHOLDER:
149 attrs={ATTRIBUTE_PLACEHOLDER:
150 'http://example.com/image.png'}))
150 'http://example.com/image.png'}))
151
151
152 # This field is for spam prevention only
152 # This field is for spam prevention only
153 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
153 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
154 widget=forms.TextInput(attrs={
154 widget=forms.TextInput(attrs={
155 'class': 'form-email'}))
155 'class': 'form-email'}))
156 threads = forms.CharField(required=False, label=_('Additional threads'),
156 threads = forms.CharField(required=False, label=_('Additional threads'),
157 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
157 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
158 '123 456 789'}))
158 '123 456 789'}))
159
159
160 session = None
160 session = None
161 need_to_ban = False
161 need_to_ban = False
162
162
163 def clean_title(self):
163 def clean_title(self):
164 title = self.cleaned_data['title']
164 title = self.cleaned_data['title']
165 if title:
165 if title:
166 if len(title) > TITLE_MAX_LENGTH:
166 if len(title) > TITLE_MAX_LENGTH:
167 raise forms.ValidationError(_('Title must have less than %s '
167 raise forms.ValidationError(_('Title must have less than %s '
168 'characters') %
168 'characters') %
169 str(TITLE_MAX_LENGTH))
169 str(TITLE_MAX_LENGTH))
170 return title
170 return title
171
171
172 def clean_text(self):
172 def clean_text(self):
173 text = self.cleaned_data['text'].strip()
173 text = self.cleaned_data['text'].strip()
174 if text:
174 if text:
175 if len(text) > board_settings.MAX_TEXT_LENGTH:
175 max_length = board_settings.get_int('Forms', 'MaxTextLength')
176 if len(text) > max_length:
176 raise forms.ValidationError(_('Text must have less than %s '
177 raise forms.ValidationError(_('Text must have less than %s '
177 'characters') %
178 'characters') % str(max_length))
178 str(board_settings
179 .MAX_TEXT_LENGTH))
180 return text
179 return text
181
180
182 def clean_image(self):
181 def clean_image(self):
183 image = self.cleaned_data['image']
182 image = self.cleaned_data['image']
184
183
185 if image:
184 if image:
186 self.validate_image_size(image.size)
185 self.validate_image_size(image.size)
187
186
188 return image
187 return image
189
188
190 def clean_image_url(self):
189 def clean_image_url(self):
191 url = self.cleaned_data['image_url']
190 url = self.cleaned_data['image_url']
192
191
193 image = None
192 image = None
194 if url:
193 if url:
195 image = self._get_image_from_url(url)
194 image = self._get_image_from_url(url)
196
195
197 if not image:
196 if not image:
198 raise forms.ValidationError(_('Invalid URL'))
197 raise forms.ValidationError(_('Invalid URL'))
199 else:
198 else:
200 self.validate_image_size(image.size)
199 self.validate_image_size(image.size)
201
200
202 return image
201 return image
203
202
204 def clean_threads(self):
203 def clean_threads(self):
205 threads_str = self.cleaned_data['threads']
204 threads_str = self.cleaned_data['threads']
206
205
207 if len(threads_str) > 0:
206 if len(threads_str) > 0:
208 threads_id_list = threads_str.split(' ')
207 threads_id_list = threads_str.split(' ')
209
208
210 threads = list()
209 threads = list()
211
210
212 for thread_id in threads_id_list:
211 for thread_id in threads_id_list:
213 try:
212 try:
214 thread = Post.objects.get(id=int(thread_id))
213 thread = Post.objects.get(id=int(thread_id))
215 if not thread.is_opening() or thread.get_thread().archived:
214 if not thread.is_opening() or thread.get_thread().archived:
216 raise ObjectDoesNotExist()
215 raise ObjectDoesNotExist()
217 threads.append(thread)
216 threads.append(thread)
218 except (ObjectDoesNotExist, ValueError):
217 except (ObjectDoesNotExist, ValueError):
219 raise forms.ValidationError(_('Invalid additional thread list'))
218 raise forms.ValidationError(_('Invalid additional thread list'))
220
219
221 return threads
220 return threads
222
221
223 def clean(self):
222 def clean(self):
224 cleaned_data = super(PostForm, self).clean()
223 cleaned_data = super(PostForm, self).clean()
225
224
226 if cleaned_data['email']:
225 if cleaned_data['email']:
227 self.need_to_ban = True
226 self.need_to_ban = True
228 raise forms.ValidationError('A human cannot enter a hidden field')
227 raise forms.ValidationError('A human cannot enter a hidden field')
229
228
230 if not self.errors:
229 if not self.errors:
231 self._clean_text_image()
230 self._clean_text_image()
232
231
233 if not self.errors and self.session:
232 if not self.errors and self.session:
234 self._validate_posting_speed()
233 self._validate_posting_speed()
235
234
236 return cleaned_data
235 return cleaned_data
237
236
238 def get_image(self):
237 def get_image(self):
239 """
238 """
240 Gets image from file or URL.
239 Gets image from file or URL.
241 """
240 """
242
241
243 image = self.cleaned_data['image']
242 image = self.cleaned_data['image']
244 return image if image else self.cleaned_data['image_url']
243 return image if image else self.cleaned_data['image_url']
245
244
246 def _clean_text_image(self):
245 def _clean_text_image(self):
247 text = self.cleaned_data.get('text')
246 text = self.cleaned_data.get('text')
248 image = self.get_image()
247 image = self.get_image()
249
248
250 if (not text) and (not image):
249 if (not text) and (not image):
251 error_message = _('Either text or image must be entered.')
250 error_message = _('Either text or image must be entered.')
252 self._errors['text'] = self.error_class([error_message])
251 self._errors['text'] = self.error_class([error_message])
253
252
254 def _validate_posting_speed(self):
253 def _validate_posting_speed(self):
255 can_post = True
254 can_post = True
256
255
257 posting_delay = settings.POSTING_DELAY
256 posting_delay = settings.POSTING_DELAY
258
257
259 if board_settings.LIMIT_POSTING_SPEED:
258 if board_settings.get_bool('Forms', 'LimitPostingSpeed'):
260 now = time.time()
259 now = time.time()
261
260
262 current_delay = 0
261 current_delay = 0
263 need_delay = False
262 need_delay = False
264
263
265 if not LAST_POST_TIME in self.session:
264 if not LAST_POST_TIME in self.session:
266 self.session[LAST_POST_TIME] = now
265 self.session[LAST_POST_TIME] = now
267
266
268 need_delay = True
267 need_delay = True
269 else:
268 else:
270 last_post_time = self.session.get(LAST_POST_TIME)
269 last_post_time = self.session.get(LAST_POST_TIME)
271 current_delay = int(now - last_post_time)
270 current_delay = int(now - last_post_time)
272
271
273 need_delay = current_delay < posting_delay
272 need_delay = current_delay < posting_delay
274
273
275 if need_delay:
274 if need_delay:
276 error_message = ERROR_SPEED % str(posting_delay
275 error_message = ERROR_SPEED % str(posting_delay
277 - current_delay)
276 - current_delay)
278 self._errors['text'] = self.error_class([error_message])
277 self._errors['text'] = self.error_class([error_message])
279
278
280 can_post = False
279 can_post = False
281
280
282 if can_post:
281 if can_post:
283 self.session[LAST_POST_TIME] = now
282 self.session[LAST_POST_TIME] = now
284
283
285 def validate_image_size(self, size: int):
284 def validate_image_size(self, size: int):
286 if size > board_settings.MAX_IMAGE_SIZE:
285 max_size = board_settings.get_int('Forms', 'MaxImageSize')
286 if size > max_size:
287 raise forms.ValidationError(
287 raise forms.ValidationError(
288 _('Image must be less than %s bytes')
288 _('Image must be less than %s bytes')
289 % str(board_settings.MAX_IMAGE_SIZE))
289 % str(max_size))
290
290
291 def _get_image_from_url(self, url: str) -> SimpleUploadedFile:
291 def _get_image_from_url(self, url: str) -> SimpleUploadedFile:
292 """
292 """
293 Gets an image file from URL.
293 Gets an image file from URL.
294 """
294 """
295
295
296 img_temp = None
296 img_temp = None
297
297
298 try:
298 try:
299 # Verify content headers
299 # Verify content headers
300 response_head = requests.head(url, verify=False)
300 response_head = requests.head(url, verify=False)
301 content_type = response_head.headers['content-type'].split(';')[0]
301 content_type = response_head.headers['content-type'].split(';')[0]
302 if content_type in CONTENT_TYPE_IMAGE:
302 if content_type in CONTENT_TYPE_IMAGE:
303 length_header = response_head.headers.get('content-length')
303 length_header = response_head.headers.get('content-length')
304 if length_header:
304 if length_header:
305 length = int(length_header)
305 length = int(length_header)
306 self.validate_image_size(length)
306 self.validate_image_size(length)
307 # Get the actual content into memory
307 # Get the actual content into memory
308 response = requests.get(url, verify=False, stream=True)
308 response = requests.get(url, verify=False, stream=True)
309
309
310 # Download image, stop if the size exceeds limit
310 # Download image, stop if the size exceeds limit
311 size = 0
311 size = 0
312 content = b''
312 content = b''
313 for chunk in response.iter_content(IMAGE_DOWNLOAD_CHUNK_BYTES):
313 for chunk in response.iter_content(IMAGE_DOWNLOAD_CHUNK_BYTES):
314 size += len(chunk)
314 size += len(chunk)
315 self.validate_image_size(size)
315 self.validate_image_size(size)
316 content += chunk
316 content += chunk
317
317
318 if response.status_code == HTTP_RESULT_OK and content:
318 if response.status_code == HTTP_RESULT_OK and content:
319 # Set a dummy file name that will be replaced
319 # Set a dummy file name that will be replaced
320 # anyway, just keep the valid extension
320 # anyway, just keep the valid extension
321 filename = 'image.' + content_type.split('/')[1]
321 filename = 'image.' + content_type.split('/')[1]
322 img_temp = SimpleUploadedFile(filename, content,
322 img_temp = SimpleUploadedFile(filename, content,
323 content_type)
323 content_type)
324 except Exception:
324 except Exception:
325 # Just return no image
325 # Just return no image
326 pass
326 pass
327
327
328 return img_temp
328 return img_temp
329
329
330
330
331 class ThreadForm(PostForm):
331 class ThreadForm(PostForm):
332
332
333 tags = forms.CharField(
333 tags = forms.CharField(
334 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
334 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
335 max_length=100, label=_('Tags'), required=True)
335 max_length=100, label=_('Tags'), required=True)
336
336
337 def clean_tags(self):
337 def clean_tags(self):
338 tags = self.cleaned_data['tags'].strip()
338 tags = self.cleaned_data['tags'].strip()
339
339
340 if not tags or not REGEX_TAGS.match(tags):
340 if not tags or not REGEX_TAGS.match(tags):
341 raise forms.ValidationError(
341 raise forms.ValidationError(
342 _('Inappropriate characters in tags.'))
342 _('Inappropriate characters in tags.'))
343
343
344 required_tag_exists = False
344 required_tag_exists = False
345 for tag in tags.split():
345 for tag in tags.split():
346 try:
346 try:
347 Tag.objects.get(name=tag.strip().lower(), required=True)
347 Tag.objects.get(name=tag.strip().lower(), required=True)
348 required_tag_exists = True
348 required_tag_exists = True
349 break
349 break
350 except ObjectDoesNotExist:
350 except ObjectDoesNotExist:
351 pass
351 pass
352
352
353 if not required_tag_exists:
353 if not required_tag_exists:
354 all_tags = Tag.objects.filter(required=True)
354 all_tags = Tag.objects.filter(required=True)
355 raise forms.ValidationError(
355 raise forms.ValidationError(
356 _('Need at least one of the tags: ')
356 _('Need at least one of the tags: ')
357 + ', '.join([tag.name for tag in all_tags]))
357 + ', '.join([tag.name for tag in all_tags]))
358
358
359 return tags
359 return tags
360
360
361 def clean(self):
361 def clean(self):
362 cleaned_data = super(ThreadForm, self).clean()
362 cleaned_data = super(ThreadForm, self).clean()
363
363
364 return cleaned_data
364 return cleaned_data
365
365
366
366
367 class SettingsForm(NeboardForm):
367 class SettingsForm(NeboardForm):
368
368
369 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
369 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
370 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('Image view mode'))
370 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('Image view mode'))
371 username = forms.CharField(label=_('User name'), required=False)
371 username = forms.CharField(label=_('User name'), required=False)
372 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
372 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
373
373
374 def clean_username(self):
374 def clean_username(self):
375 username = self.cleaned_data['username']
375 username = self.cleaned_data['username']
376
376
377 if username and not REGEX_TAGS.match(username):
377 if username and not REGEX_TAGS.match(username):
378 raise forms.ValidationError(_('Inappropriate characters.'))
378 raise forms.ValidationError(_('Inappropriate characters.'))
379
379
380 return username
380 return username
381
381
382
382
383 class SearchForm(NeboardForm):
383 class SearchForm(NeboardForm):
384 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
384 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
1 NO CONTENT: modified file, binary diff hidden
NO CONTENT: modified file, binary diff hidden
@@ -1,408 +1,408 b''
1 # SOME DESCRIPTIVE TITLE.
1 # SOME DESCRIPTIVE TITLE.
2 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
2 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3 # This file is distributed under the same license as the PACKAGE package.
3 # This file is distributed under the same license as the PACKAGE package.
4 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
4 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
5 #
5 #
6 msgid ""
6 msgid ""
7 msgstr ""
7 msgstr ""
8 "Project-Id-Version: PACKAGE VERSION\n"
8 "Project-Id-Version: PACKAGE VERSION\n"
9 "Report-Msgid-Bugs-To: \n"
9 "Report-Msgid-Bugs-To: \n"
10 "POT-Creation-Date: 2015-04-21 21:10+0300\n"
10 "POT-Creation-Date: 2015-05-07 13:10+0300\n"
11 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
11 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
12 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
12 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
13 "Language-Team: LANGUAGE <LL@li.org>\n"
13 "Language-Team: LANGUAGE <LL@li.org>\n"
14 "Language: ru\n"
14 "Language: ru\n"
15 "MIME-Version: 1.0\n"
15 "MIME-Version: 1.0\n"
16 "Content-Type: text/plain; charset=UTF-8\n"
16 "Content-Type: text/plain; charset=UTF-8\n"
17 "Content-Transfer-Encoding: 8bit\n"
17 "Content-Transfer-Encoding: 8bit\n"
18 "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
18 "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
19 "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
19 "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
20
20
21 #: admin.py:22
21 #: admin.py:22
22 msgid "{} posters were banned"
22 msgid "{} posters were banned"
23 msgstr ""
23 msgstr ""
24
24
25 #: authors.py:9
25 #: authors.py:9
26 msgid "author"
26 msgid "author"
27 msgstr "автор"
27 msgstr "автор"
28
28
29 #: authors.py:10
29 #: authors.py:10
30 msgid "developer"
30 msgid "developer"
31 msgstr "разработчик"
31 msgstr "разработчик"
32
32
33 #: authors.py:11
33 #: authors.py:11
34 msgid "javascript developer"
34 msgid "javascript developer"
35 msgstr "разработчик javascript"
35 msgstr "разработчик javascript"
36
36
37 #: authors.py:12
37 #: authors.py:12
38 msgid "designer"
38 msgid "designer"
39 msgstr "дизайнер"
39 msgstr "дизайнер"
40
40
41 #: forms.py:35
41 #: forms.py:35
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:36
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:38
51 msgid "Title"
51 msgid "Title"
52 msgstr "Заголовок"
52 msgstr "Заголовок"
53
53
54 #: forms.py:39
54 #: forms.py:39
55 msgid "Text"
55 msgid "Text"
56 msgstr "Текст"
56 msgstr "Текст"
57
57
58 #: forms.py:40
58 #: forms.py:40
59 msgid "Tag"
59 msgid "Tag"
60 msgstr "Метка"
60 msgstr "Метка"
61
61
62 #: forms.py:41 templates/boards/base.html:41 templates/search/search.html:7
62 #: forms.py:41 templates/boards/base.html:41 templates/search/search.html:7
63 msgid "Search"
63 msgid "Search"
64 msgstr "Поиск"
64 msgstr "Поиск"
65
65
66 #: forms.py:43
66 #: forms.py:43
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:142
71 #: forms.py:144
72 msgid "Image"
72 msgid "Image"
73 msgstr "Изображение"
73 msgstr "Изображение"
74
74
75 #: forms.py:145
75 #: forms.py:147
76 msgid "Image URL"
76 msgid "Image URL"
77 msgstr "URL изображения"
77 msgstr "URL изображения"
78
78
79 #: forms.py:151
79 #: forms.py:153
80 msgid "e-mail"
80 msgid "e-mail"
81 msgstr ""
81 msgstr ""
82
82
83 #: forms.py:154
83 #: forms.py:156
84 msgid "Additional threads"
84 msgid "Additional threads"
85 msgstr "Дополнительные темы"
85 msgstr "Дополнительные темы"
86
86
87 #: forms.py:165
87 #: forms.py:167
88 #, python-format
88 #, python-format
89 msgid "Title must have less than %s characters"
89 msgid "Title must have less than %s characters"
90 msgstr "Заголовок должен иметь меньше %s символов"
90 msgstr "Заголовок должен иметь меньше %s символов"
91
91
92 #: forms.py:174
92 #: forms.py:176
93 #, python-format
93 #, python-format
94 msgid "Text must have less than %s characters"
94 msgid "Text must have less than %s characters"
95 msgstr "Текст должен быть короче %s символов"
95 msgstr "Текст должен быть короче %s символов"
96
96
97 #: forms.py:196
97 #: forms.py:198
98 msgid "Invalid URL"
98 msgid "Invalid URL"
99 msgstr "Неверный URL"
99 msgstr "Неверный URL"
100
100
101 #: forms.py:217
101 #: forms.py:219
102 msgid "Invalid additional thread list"
102 msgid "Invalid additional thread list"
103 msgstr "Неверный список дополнительных тем"
103 msgstr "Неверный список дополнительных тем"
104
104
105 #: forms.py:249
105 #: forms.py:251
106 msgid "Either text or image must be entered."
106 msgid "Either text or image must be entered."
107 msgstr "Текст или картинка должны быть введены."
107 msgstr "Текст или картинка должны быть введены."
108
108
109 #: forms.py:286
109 #: forms.py:288
110 #, python-format
110 #, python-format
111 msgid "Image must be less than %s bytes"
111 msgid "Image must be less than %s bytes"
112 msgstr "Изображение должно быть менее %s байт"
112 msgstr "Изображение должно быть менее %s байт"
113
113
114 #: forms.py:333 templates/boards/posting_general.html:145
114 #: forms.py:335 templates/boards/posting_general.html:129
115 #: templates/boards/rss/post.html:10 templates/boards/tags.html:7
115 #: templates/boards/rss/post.html:10 templates/boards/tags.html:7
116 msgid "Tags"
116 msgid "Tags"
117 msgstr "Метки"
117 msgstr "Метки"
118
118
119 #: forms.py:340
119 #: forms.py:342
120 msgid "Inappropriate characters in tags."
120 msgid "Inappropriate characters in tags."
121 msgstr "Недопустимые символы в метках."
121 msgstr "Недопустимые символы в метках."
122
122
123 #: forms.py:354
123 #: forms.py:356
124 msgid "Need at least one of the tags: "
124 msgid "Need at least one of the tags: "
125 msgstr "Нужна хотя бы одна из меток: "
125 msgstr "Нужна хотя бы одна из меток: "
126
126
127 #: forms.py:367
127 #: forms.py:369
128 msgid "Theme"
128 msgid "Theme"
129 msgstr "Тема"
129 msgstr "Тема"
130
130
131 #: forms.py:368
131 #: forms.py:370
132 msgid "Image view mode"
132 msgid "Image view mode"
133 msgstr "Режим просмотра изображений"
133 msgstr "Режим просмотра изображений"
134
134
135 #: forms.py:369
135 #: forms.py:371
136 msgid "User name"
136 msgid "User name"
137 msgstr "Имя пользователя"
137 msgstr "Имя пользователя"
138
138
139 #: forms.py:370
139 #: forms.py:372
140 msgid "Time zone"
140 msgid "Time zone"
141 msgstr "Часовой пояс"
141 msgstr "Часовой пояс"
142
142
143 #: forms.py:376
143 #: forms.py:378
144 msgid "Inappropriate characters."
144 msgid "Inappropriate characters."
145 msgstr "Недопустимые символы."
145 msgstr "Недопустимые символы."
146
146
147 #: templates/boards/404.html:6
147 #: templates/boards/404.html:6
148 msgid "Not found"
148 msgid "Not found"
149 msgstr "Не найдено"
149 msgstr "Не найдено"
150
150
151 #: templates/boards/404.html:12
151 #: templates/boards/404.html:12
152 msgid "This page does not exist"
152 msgid "This page does not exist"
153 msgstr "Этой страницы не существует"
153 msgstr "Этой страницы не существует"
154
154
155 #: templates/boards/authors.html:6 templates/boards/authors.html.py:12
155 #: templates/boards/authors.html:6 templates/boards/authors.html.py:12
156 msgid "Authors"
156 msgid "Authors"
157 msgstr "Авторы"
157 msgstr "Авторы"
158
158
159 #: templates/boards/authors.html:26
159 #: templates/boards/authors.html:26
160 msgid "Distributed under the"
160 msgid "Distributed under the"
161 msgstr "Распространяется под"
161 msgstr "Распространяется под"
162
162
163 #: templates/boards/authors.html:28
163 #: templates/boards/authors.html:28
164 msgid "license"
164 msgid "license"
165 msgstr "лицензией"
165 msgstr "лицензией"
166
166
167 #: templates/boards/authors.html:30
167 #: templates/boards/authors.html:30
168 msgid "Repository"
168 msgid "Repository"
169 msgstr "Репозиторий"
169 msgstr "Репозиторий"
170
170
171 #: templates/boards/base.html:14
171 #: templates/boards/base.html:14
172 msgid "Feed"
172 msgid "Feed"
173 msgstr "Лента"
173 msgstr "Лента"
174
174
175 #: templates/boards/base.html:31
175 #: templates/boards/base.html:31
176 msgid "All threads"
176 msgid "All threads"
177 msgstr "Все темы"
177 msgstr "Все темы"
178
178
179 #: templates/boards/base.html:37
179 #: templates/boards/base.html:37
180 msgid "Add tags"
180 msgid "Add tags"
181 msgstr "Добавить метки"
181 msgstr "Добавить метки"
182
182
183 #: templates/boards/base.html:39
183 #: templates/boards/base.html:39
184 msgid "Tag management"
184 msgid "Tag management"
185 msgstr "Управление метками"
185 msgstr "Управление метками"
186
186
187 #: templates/boards/base.html:44 templates/boards/base.html.py:45
187 #: templates/boards/base.html:44 templates/boards/base.html.py:45
188 #: templates/boards/notifications.html:8
188 #: templates/boards/notifications.html:8
189 msgid "Notifications"
189 msgid "Notifications"
190 msgstr "Уведомления"
190 msgstr "Уведомления"
191
191
192 #: templates/boards/base.html:52 templates/boards/settings.html:9
192 #: templates/boards/base.html:52 templates/boards/settings.html:8
193 msgid "Settings"
193 msgid "Settings"
194 msgstr "Настройки"
194 msgstr "Настройки"
195
195
196 #: templates/boards/base.html:65
196 #: templates/boards/base.html:65
197 msgid "Admin"
197 msgid "Admin"
198 msgstr "Администрирование"
198 msgstr "Администрирование"
199
199
200 #: templates/boards/base.html:67
200 #: templates/boards/base.html:67
201 #, python-format
201 #, python-format
202 msgid "Speed: %(ppd)s posts per day"
202 msgid "Speed: %(ppd)s posts per day"
203 msgstr "Скорость: %(ppd)s сообщений в день"
203 msgstr "Скорость: %(ppd)s сообщений в день"
204
204
205 #: templates/boards/base.html:69
205 #: templates/boards/base.html:69
206 msgid "Up"
206 msgid "Up"
207 msgstr "Вверх"
207 msgstr "Вверх"
208
208
209 #: templates/boards/notifications.html:17
209 #: templates/boards/notifications.html:17
210 #: templates/boards/posting_general.html:80 templates/search/search.html:26
210 #: templates/boards/posting_general.html:70 templates/search/search.html:26
211 msgid "Previous page"
211 msgid "Previous page"
212 msgstr "Предыдущая страница"
212 msgstr "Предыдущая страница"
213
213
214 #: templates/boards/notifications.html:27
214 #: templates/boards/notifications.html:27
215 #: templates/boards/posting_general.html:118 templates/search/search.html:37
215 #: templates/boards/posting_general.html:102 templates/search/search.html:37
216 msgid "Next page"
216 msgid "Next page"
217 msgstr "Следующая страница"
217 msgstr "Следующая страница"
218
218
219 #: templates/boards/post.html:24
219 #: templates/boards/post.html:24
220 msgid "Open"
220 msgid "Open"
221 msgstr "Открыть"
221 msgstr "Открыть"
222
222
223 #: templates/boards/post.html:26 templates/boards/post.html.py:30
223 #: templates/boards/post.html:26 templates/boards/post.html.py:30
224 msgid "Reply"
224 msgid "Reply"
225 msgstr "Ответить"
225 msgstr "Ответить"
226
226
227 #: templates/boards/post.html:35
227 #: templates/boards/post.html:35
228 msgid "Edit"
228 msgid "Edit"
229 msgstr "Изменить"
229 msgstr "Изменить"
230
230
231 #: templates/boards/post.html:37
231 #: templates/boards/post.html:37
232 msgid "Edit thread"
232 msgid "Edit thread"
233 msgstr "Изменить тему"
233 msgstr "Изменить тему"
234
234
235 #: templates/boards/post.html:69
235 #: templates/boards/post.html:69
236 msgid "Replies"
236 msgid "Replies"
237 msgstr "Ответы"
237 msgstr "Ответы"
238
238
239 #: templates/boards/post.html:81 templates/boards/thread.html:26
239 #: templates/boards/post.html:81 templates/boards/thread.html:26
240 msgid "messages"
240 msgid "messages"
241 msgstr "сообщений"
241 msgstr "сообщений"
242
242
243 #: templates/boards/post.html:82 templates/boards/thread.html:27
243 #: templates/boards/post.html:82 templates/boards/thread.html:27
244 msgid "images"
244 msgid "images"
245 msgstr "изображений"
245 msgstr "изображений"
246
246
247 #: templates/boards/posting_general.html:64
247 #: templates/boards/posting_general.html:35
248 #| msgid "messages"
249 msgid "Related message"
250 msgstr "Связанное сообщение"
251
252 #: templates/boards/posting_general.html:60
248 msgid "Edit tag"
253 msgid "Edit tag"
249 msgstr "Изменить метку"
254 msgstr "Изменить метку"
250
255
251 #: templates/boards/posting_general.html:67
256 #: templates/boards/posting_general.html:63
252 #, python-format
257 #, python-format
253 msgid "This tag has %(thread_count)s threads and %(post_count)s posts."
258 msgid "This tag has %(thread_count)s threads and %(post_count)s posts."
254 msgstr "С этой меткой есть %(thread_count)s тем и %(post_count)s сообщений."
259 msgstr "С этой меткой есть %(thread_count)s тем и %(post_count)s сообщений."
255
260
256 #: templates/boards/posting_general.html:94
261 #: templates/boards/posting_general.html:84
257 #, python-format
262 #, python-format
258 msgid "Skipped %(count)s replies. Open thread to see all replies."
263 msgid "Skipped %(count)s replies. Open thread to see all replies."
259 msgstr "Пропущено %(count)s ответов. Откройте тред, чтобы увидеть все ответы."
264 msgstr "Пропущено %(count)s ответов. Откройте тред, чтобы увидеть все ответы."
260
265
261 #: templates/boards/posting_general.html:123
266 #: templates/boards/posting_general.html:107
262 msgid "No threads exist. Create the first one!"
267 msgid "No threads exist. Create the first one!"
263 msgstr "Нет тем. Создайте первую!"
268 msgstr "Нет тем. Создайте первую!"
264
269
265 #: templates/boards/posting_general.html:129
270 #: templates/boards/posting_general.html:113
266 msgid "Create new thread"
271 msgid "Create new thread"
267 msgstr "Создать новую тему"
272 msgstr "Создать новую тему"
268
273
269 #: templates/boards/posting_general.html:134 templates/boards/preview.html:16
274 #: templates/boards/posting_general.html:118 templates/boards/preview.html:16
270 #: templates/boards/thread_normal.html:43
275 #: templates/boards/thread_normal.html:43
271 msgid "Post"
276 msgid "Post"
272 msgstr "Отправить"
277 msgstr "Отправить"
273
278
274 #: templates/boards/posting_general.html:139
279 #: templates/boards/posting_general.html:123
275 msgid "Tags must be delimited by spaces. Text or image is required."
280 msgid "Tags must be delimited by spaces. Text or image is required."
276 msgstr ""
281 msgstr ""
277 "Метки должны быть разделены пробелами. Текст или изображение обязательны."
282 "Метки должны быть разделены пробелами. Текст или изображение обязательны."
278
283
279 #: templates/boards/posting_general.html:142
284 #: templates/boards/posting_general.html:126
280 #: templates/boards/thread_normal.html:48
285 #: templates/boards/thread_normal.html:48
281 msgid "Text syntax"
286 msgid "Text syntax"
282 msgstr "Синтаксис текста"
287 msgstr "Синтаксис текста"
283
288
284 #: templates/boards/posting_general.html:159
289 #: templates/boards/posting_general.html:143
285 msgid "Pages:"
290 msgid "Pages:"
286 msgstr "Страницы: "
291 msgstr "Страницы: "
287
292
288 #: templates/boards/preview.html:6 templates/boards/staticpages/help.html:20
293 #: templates/boards/preview.html:6 templates/boards/staticpages/help.html:20
289 msgid "Preview"
294 msgid "Preview"
290 msgstr "Предпросмотр"
295 msgstr "Предпросмотр"
291
296
292 #: templates/boards/rss/post.html:5
297 #: templates/boards/rss/post.html:5
293 msgid "Post image"
298 msgid "Post image"
294 msgstr "Изображение сообщения"
299 msgstr "Изображение сообщения"
295
300
296 #: templates/boards/settings.html:17
301 #: templates/boards/settings.html:16
297 msgid "You are moderator."
302 msgid "You are moderator."
298 msgstr "Вы модератор."
303 msgstr "Вы модератор."
299
304
300 #: templates/boards/settings.html:21
305 #: templates/boards/settings.html:20
301 msgid "Hidden tags:"
306 msgid "Hidden tags:"
302 msgstr "Скрытые метки:"
307 msgstr "Скрытые метки:"
303
308
304 #: templates/boards/settings.html:29
309 #: templates/boards/settings.html:28
305 msgid "No hidden tags."
310 msgid "No hidden tags."
306 msgstr "Нет скрытых меток."
311 msgstr "Нет скрытых меток."
307
312
308 #: templates/boards/settings.html:38
313 #: templates/boards/settings.html:37
309 msgid "Save"
314 msgid "Save"
310 msgstr "Сохранить"
315 msgstr "Сохранить"
311
316
312 #: templates/boards/staticpages/banned.html:6
317 #: templates/boards/staticpages/banned.html:6
313 msgid "Banned"
318 msgid "Banned"
314 msgstr "Заблокирован"
319 msgstr "Заблокирован"
315
320
316 #: templates/boards/staticpages/banned.html:11
321 #: templates/boards/staticpages/banned.html:11
317 msgid "Your IP address has been banned. Contact the administrator"
322 msgid "Your IP address has been banned. Contact the administrator"
318 msgstr "Ваш IP адрес был заблокирован. Свяжитесь с администратором"
323 msgstr "Ваш IP адрес был заблокирован. Свяжитесь с администратором"
319
324
320 #: templates/boards/staticpages/help.html:6
325 #: templates/boards/staticpages/help.html:6
321 #: templates/boards/staticpages/help.html:10
326 #: templates/boards/staticpages/help.html:10
322 msgid "Syntax"
327 msgid "Syntax"
323 msgstr "Синтаксис"
328 msgstr "Синтаксис"
324
329
325 #: templates/boards/staticpages/help.html:11
330 #: templates/boards/staticpages/help.html:11
326 msgid "Italic text"
331 msgid "Italic text"
327 msgstr "Курсивный текст"
332 msgstr "Курсивный текст"
328
333
329 #: templates/boards/staticpages/help.html:12
334 #: templates/boards/staticpages/help.html:12
330 msgid "Bold text"
335 msgid "Bold text"
331 msgstr "Полужирный текст"
336 msgstr "Полужирный текст"
332
337
333 #: templates/boards/staticpages/help.html:13
338 #: templates/boards/staticpages/help.html:13
334 msgid "Spoiler"
339 msgid "Spoiler"
335 msgstr "Спойлер"
340 msgstr "Спойлер"
336
341
337 #: templates/boards/staticpages/help.html:14
342 #: templates/boards/staticpages/help.html:14
338 msgid "Link to a post"
343 msgid "Link to a post"
339 msgstr "Ссылка на сообщение"
344 msgstr "Ссылка на сообщение"
340
345
341 #: templates/boards/staticpages/help.html:15
346 #: templates/boards/staticpages/help.html:15
342 msgid "Strikethrough text"
347 msgid "Strikethrough text"
343 msgstr "Зачеркнутый текст"
348 msgstr "Зачеркнутый текст"
344
349
345 #: templates/boards/staticpages/help.html:16
350 #: templates/boards/staticpages/help.html:16
346 msgid "Comment"
351 msgid "Comment"
347 msgstr "Комментарий"
352 msgstr "Комментарий"
348
353
349 #: templates/boards/staticpages/help.html:17
354 #: templates/boards/staticpages/help.html:17
350 #: templates/boards/staticpages/help.html:18
355 #: templates/boards/staticpages/help.html:18
351 msgid "Quote"
356 msgid "Quote"
352 msgstr "Цитата"
357 msgstr "Цитата"
353
358
354 #: templates/boards/staticpages/help.html:20
359 #: templates/boards/staticpages/help.html:20
355 msgid "You can try pasting the text and previewing the result here:"
360 msgid "You can try pasting the text and previewing the result here:"
356 msgstr "Вы можете попробовать вставить текст и проверить результат здесь:"
361 msgstr "Вы можете попробовать вставить текст и проверить результат здесь:"
357
362
358 #: templates/boards/tags.html:23
363 #: templates/boards/tags.html:23
359 msgid "No tags found."
364 msgid "No tags found."
360 msgstr "Метки не найдены."
365 msgstr "Метки не найдены."
361
366
362 #: templates/boards/tags.html:26
367 #: templates/boards/tags.html:26
363 msgid "All tags"
368 msgid "All tags"
364 msgstr "Все метки"
369 msgstr "Все метки"
365
370
366 #: templates/boards/thread.html:28
371 #: templates/boards/thread.html:28
367 msgid "Last update: "
372 msgid "Last update: "
368 msgstr "Последнее обновление: "
373 msgstr "Последнее обновление: "
369
374
370 #: templates/boards/thread_gallery.html:19
375 #: templates/boards/thread_gallery.html:19
371 #: templates/boards/thread_normal.html:13
376 #: templates/boards/thread_normal.html:13
372 msgid "Normal mode"
377 msgid "Normal mode"
373 msgstr "Нормальный режим"
378 msgstr "Нормальный режим"
374
379
375 #: templates/boards/thread_gallery.html:20
380 #: templates/boards/thread_gallery.html:20
376 #: templates/boards/thread_normal.html:14
381 #: templates/boards/thread_normal.html:14
377 msgid "Gallery mode"
382 msgid "Gallery mode"
378 msgstr "Режим галереи"
383 msgstr "Режим галереи"
379
384
380 #: templates/boards/thread_gallery.html:50
385 #: templates/boards/thread_gallery.html:41
381 msgid "No images."
386 msgid "No images."
382 msgstr "Нет изображений."
387 msgstr "Нет изображений."
383
388
384 #: templates/boards/thread_normal.html:22
389 #: templates/boards/thread_normal.html:22
385 msgid "posts to bumplimit"
390 msgid "posts to bumplimit"
386 msgstr "сообщений до бамплимита"
391 msgstr "сообщений до бамплимита"
387
392
388 #: templates/boards/thread_normal.html:36
393 #: templates/boards/thread_normal.html:36
389 msgid "Reply to thread"
394 msgid "Reply to thread"
390 msgstr "Ответить в тему"
395 msgstr "Ответить в тему"
391
396
392 #: templates/boards/thread_normal.html:49
397 #: templates/boards/thread_normal.html:49
393 msgid "Close form"
398 msgid "Close form"
394 msgstr "Закрыть форму"
399 msgstr "Закрыть форму"
395
400
396 #: templates/boards/thread_normal.html:63
401 #: templates/boards/thread_normal.html:63
397 msgid "Update"
402 msgid "Update"
398 msgstr "Обновить"
403 msgstr "Обновить"
399
404
400 #: templates/search/search.html:17
405 #: templates/search/search.html:17
401 msgid "Ok"
406 msgid "Ok"
402 msgstr "Ок"
407 msgstr "Ок"
403
408
404 #~ msgid "Wait %s seconds after last posting"
405 #~ msgstr "Подождите %s секунд после последнего постинга"
406
407 #~ msgid "tag1 several_words_tag"
408 #~ msgstr "метка1 метка_из_нескольких_слов"
@@ -1,9 +1,10 b''
1 __author__ = 'neko259'
1 __author__ = 'neko259'
2
2
3 from boards.models.signature import GlobalId, Signature
3 from boards.models.signature import GlobalId, Signature
4 from boards.models.sync_key import KeyPair
4 from boards.models.sync_key import KeyPair
5 from boards.models.image import PostImage
5 from boards.models.image import PostImage
6 from boards.models.thread import Thread
6 from boards.models.thread import Thread
7 from boards.models.post import Post
7 from boards.models.post import Post
8 from boards.models.tag import Tag
8 from boards.models.tag import Tag
9 from boards.models.user import Ban
9 from boards.models.user import Ban
10 from boards.models.banner import Banner
1 NO CONTENT: file renamed from boards/models/post.py to boards/models/post/__init__.py
NO CONTENT: file renamed from boards/models/post.py to boards/models/post/__init__.py
@@ -1,82 +1,86 b''
1 from django.template.loader import render_to_string
1 from django.template.loader import render_to_string
2 from django.db import models
2 from django.db import models
3 from django.db.models import Count
3 from django.db.models import Count
4 from django.core.urlresolvers import reverse
4 from django.core.urlresolvers import reverse
5
5
6 from boards.models.base import Viewable
6 from boards.models.base import Viewable
7 from boards.utils import cached_result
7 from boards.utils import cached_result
8
8
9
9
10 __author__ = 'neko259'
10 __author__ = 'neko259'
11
11
12
12
13 class TagManager(models.Manager):
13 class TagManager(models.Manager):
14
14
15 def get_not_empty_tags(self):
15 def get_not_empty_tags(self):
16 """
16 """
17 Gets tags that have non-archived threads.
17 Gets tags that have non-archived threads.
18 """
18 """
19
19
20 return self.annotate(num_threads=Count('thread')).filter(num_threads__gt=0)\
20 return self.annotate(num_threads=Count('thread')).filter(num_threads__gt=0)\
21 .order_by('-required', 'name')
21 .order_by('-required', 'name')
22
22
23 def get_tag_url_list(self, tags: list) -> str:
23 def get_tag_url_list(self, tags: list) -> str:
24 """
24 """
25 Gets a comma-separated list of tag links.
25 Gets a comma-separated list of tag links.
26 """
26 """
27
27
28 return ', '.join([tag.get_view() for tag in tags])
28 return ', '.join([tag.get_view() for tag in tags])
29
29
30
30
31 class Tag(models.Model, Viewable):
31 class Tag(models.Model, Viewable):
32 """
32 """
33 A tag is a text node assigned to the thread. The tag serves as a board
33 A tag is a text node assigned to the thread. The tag serves as a board
34 section. There can be multiple tags for each thread
34 section. There can be multiple tags for each thread
35 """
35 """
36
36
37 objects = TagManager()
37 objects = TagManager()
38
38
39 class Meta:
39 class Meta:
40 app_label = 'boards'
40 app_label = 'boards'
41 ordering = ('name',)
41 ordering = ('name',)
42
42
43 name = models.CharField(max_length=100, db_index=True, unique=True)
43 name = models.CharField(max_length=100, db_index=True, unique=True)
44 required = models.BooleanField(default=False, db_index=True)
44 required = models.BooleanField(default=False, db_index=True)
45
45
46 def __str__(self):
46 def __str__(self):
47 return self.name
47 return self.name
48
48
49 def is_empty(self) -> bool:
49 def is_empty(self) -> bool:
50 """
50 """
51 Checks if the tag has some threads.
51 Checks if the tag has some threads.
52 """
52 """
53
53
54 return self.get_thread_count() == 0
54 return self.get_thread_count() == 0
55
55
56 def get_thread_count(self) -> int:
56 def get_thread_count(self) -> int:
57 return self.get_threads().count()
57 return self.get_threads().count()
58
58
59 # TODO Remove this and use get_absolute_url
59 def get_url(self):
60 def get_url(self):
60 return reverse('tag', kwargs={'tag_name': self.name})
61 return reverse('tag', kwargs={'tag_name': self.name})
61
62
63 def get_absolute_url(self):
64 return self.get_url()
65
62 def get_threads(self):
66 def get_threads(self):
63 return self.thread_set.order_by('-bump_time')
67 return self.thread_set.order_by('-bump_time')
64
68
65 def is_required(self):
69 def is_required(self):
66 return self.required
70 return self.required
67
71
68 def get_view(self):
72 def get_view(self):
69 link = '<a class="tag" href="{}">{}</a>'.format(
73 link = '<a class="tag" href="{}">{}</a>'.format(
70 self.get_url(), self.name)
74 self.get_absolute_url(), self.name)
71 if self.is_required():
75 if self.is_required():
72 link = '<b>{}</b>'.format(link)
76 link = '<b>{}</b>'.format(link)
73 return link
77 return link
74
78
75 def get_search_view(self, *args, **kwargs):
79 def get_search_view(self, *args, **kwargs):
76 return render_to_string('boards/tag.html', {
80 return render_to_string('boards/tag.html', {
77 'tag': self,
81 'tag': self,
78 })
82 })
79
83
80 @cached_result()
84 @cached_result()
81 def get_post_count(self):
85 def get_post_count(self):
82 return self.get_threads().aggregate(num_posts=Count('post'))['num_posts']
86 return self.get_threads().aggregate(num_posts=Count('post'))['num_posts']
@@ -1,234 +1,240 b''
1 import logging
1 import logging
2 from adjacent import Client
2 from adjacent import Client
3
3
4 from django.db.models import Count, Sum
4 from django.db.models import Count, Sum
5 from django.utils import timezone
5 from django.utils import timezone
6 from django.db import models
6 from django.db import models
7
7
8 from boards import settings
8 from boards import settings
9 import boards
9 import boards
10 from boards.utils import cached_result, datetime_to_epoch
10 from boards.utils import cached_result, datetime_to_epoch
11 from boards.models.post import Post
11 from boards.models.post import Post
12 from boards.models.tag import Tag
12 from boards.models.tag import Tag
13
13
14
14
15 __author__ = 'neko259'
15 __author__ = 'neko259'
16
16
17
17
18 logger = logging.getLogger(__name__)
18 logger = logging.getLogger(__name__)
19
19
20
20
21 WS_NOTIFICATION_TYPE_NEW_POST = 'new_post'
21 WS_NOTIFICATION_TYPE_NEW_POST = 'new_post'
22 WS_NOTIFICATION_TYPE = 'notification_type'
22 WS_NOTIFICATION_TYPE = 'notification_type'
23
23
24 WS_CHANNEL_THREAD = "thread:"
24 WS_CHANNEL_THREAD = "thread:"
25
25
26
26
27 class ThreadManager(models.Manager):
27 class ThreadManager(models.Manager):
28 def process_oldest_threads(self):
28 def process_oldest_threads(self):
29 """
29 """
30 Preserves maximum thread count. If there are too many threads,
30 Preserves maximum thread count. If there are too many threads,
31 archive or delete the old ones.
31 archive or delete the old ones.
32 """
32 """
33
33
34 threads = Thread.objects.filter(archived=False).order_by('-bump_time')
34 threads = Thread.objects.filter(archived=False).order_by('-bump_time')
35 thread_count = threads.count()
35 thread_count = threads.count()
36
36
37 if thread_count > settings.MAX_THREAD_COUNT:
37 max_thread_count = settings.get_int('Messages', 'MaxThreadCount')
38 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
38 if thread_count > max_thread_count:
39 num_threads_to_delete = thread_count - max_thread_count
39 old_threads = threads[thread_count - num_threads_to_delete:]
40 old_threads = threads[thread_count - num_threads_to_delete:]
40
41
41 for thread in old_threads:
42 for thread in old_threads:
42 if settings.ARCHIVE_THREADS:
43 if settings.get_bool('Storage', 'ArchiveThreads'):
43 self._archive_thread(thread)
44 self._archive_thread(thread)
44 else:
45 else:
45 thread.delete()
46 thread.delete()
46
47
47 logger.info('Processed %d old threads' % num_threads_to_delete)
48 logger.info('Processed %d old threads' % num_threads_to_delete)
48
49
49 def _archive_thread(self, thread):
50 def _archive_thread(self, thread):
50 thread.archived = True
51 thread.archived = True
51 thread.bumpable = False
52 thread.bumpable = False
52 thread.last_edit_time = timezone.now()
53 thread.last_edit_time = timezone.now()
53 thread.update_posts_time()
54 thread.update_posts_time()
54 thread.save(update_fields=['archived', 'last_edit_time', 'bumpable'])
55 thread.save(update_fields=['archived', 'last_edit_time', 'bumpable'])
55
56
56
57
57 def get_thread_max_posts():
58 def get_thread_max_posts():
58 return settings.MAX_POSTS_PER_THREAD
59 return settings.get_int('Messages', 'MaxPostsPerThread')
59
60
60
61
61 class Thread(models.Model):
62 class Thread(models.Model):
62 objects = ThreadManager()
63 objects = ThreadManager()
63
64
64 class Meta:
65 class Meta:
65 app_label = 'boards'
66 app_label = 'boards'
66
67
67 tags = models.ManyToManyField('Tag')
68 tags = models.ManyToManyField('Tag')
68 bump_time = models.DateTimeField(db_index=True)
69 bump_time = models.DateTimeField(db_index=True)
69 last_edit_time = models.DateTimeField()
70 last_edit_time = models.DateTimeField()
70 archived = models.BooleanField(default=False)
71 archived = models.BooleanField(default=False)
71 bumpable = models.BooleanField(default=True)
72 bumpable = models.BooleanField(default=True)
72 max_posts = models.IntegerField(default=get_thread_max_posts)
73 max_posts = models.IntegerField(default=get_thread_max_posts)
73
74
74 def get_tags(self) -> list:
75 def get_tags(self) -> list:
75 """
76 """
76 Gets a sorted tag list.
77 Gets a sorted tag list.
77 """
78 """
78
79
79 return self.tags.order_by('name')
80 return self.tags.order_by('name')
80
81
81 def bump(self):
82 def bump(self):
82 """
83 """
83 Bumps (moves to up) thread if possible.
84 Bumps (moves to up) thread if possible.
84 """
85 """
85
86
86 if self.can_bump():
87 if self.can_bump():
87 self.bump_time = self.last_edit_time
88 self.bump_time = self.last_edit_time
88
89
89 self.update_bump_status()
90 self.update_bump_status()
90
91
91 logger.info('Bumped thread %d' % self.id)
92 logger.info('Bumped thread %d' % self.id)
92
93
93 def has_post_limit(self) -> bool:
94 def has_post_limit(self) -> bool:
94 return self.max_posts > 0
95 return self.max_posts > 0
95
96
96 def update_bump_status(self, exclude_posts=None):
97 def update_bump_status(self, exclude_posts=None):
97 if self.has_post_limit() and self.get_reply_count() >= self.max_posts:
98 if self.has_post_limit() and self.get_reply_count() >= self.max_posts:
98 self.bumpable = False
99 self.bumpable = False
99 self.update_posts_time(exclude_posts=exclude_posts)
100 self.update_posts_time(exclude_posts=exclude_posts)
100
101
101 def _get_cache_key(self):
102 def _get_cache_key(self):
102 return [datetime_to_epoch(self.last_edit_time)]
103 return [datetime_to_epoch(self.last_edit_time)]
103
104
104 @cached_result(key_method=_get_cache_key)
105 @cached_result(key_method=_get_cache_key)
105 def get_reply_count(self) -> int:
106 def get_reply_count(self) -> int:
106 return self.get_replies().count()
107 return self.get_replies().count()
107
108
108 @cached_result(key_method=_get_cache_key)
109 @cached_result(key_method=_get_cache_key)
109 def get_images_count(self) -> int:
110 def get_images_count(self) -> int:
110 return self.get_replies().annotate(images_count=Count(
111 return self.get_replies().annotate(images_count=Count(
111 'images')).aggregate(Sum('images_count'))['images_count__sum']
112 'images')).aggregate(Sum('images_count'))['images_count__sum']
112
113
113 def can_bump(self) -> bool:
114 def can_bump(self) -> bool:
114 """
115 """
115 Checks if the thread can be bumped by replying to it.
116 Checks if the thread can be bumped by replying to it.
116 """
117 """
117
118
118 return self.bumpable and not self.archived
119 return self.bumpable and not self.archived
119
120
120 def get_last_replies(self) -> list:
121 def get_last_replies(self) -> list:
121 """
122 """
122 Gets several last replies, not including opening post
123 Gets several last replies, not including opening post
123 """
124 """
124
125
125 if settings.LAST_REPLIES_COUNT > 0:
126 last_replies_count = settings.get_int('View', 'LastRepliesCount')
127
128 if last_replies_count > 0:
126 reply_count = self.get_reply_count()
129 reply_count = self.get_reply_count()
127
130
128 if reply_count > 0:
131 if reply_count > 0:
129 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
132 reply_count_to_show = min(last_replies_count,
130 reply_count - 1)
133 reply_count - 1)
131 replies = self.get_replies()
134 replies = self.get_replies()
132 last_replies = replies[reply_count - reply_count_to_show:]
135 last_replies = replies[reply_count - reply_count_to_show:]
133
136
134 return last_replies
137 return last_replies
135
138
136 def get_skipped_replies_count(self) -> int:
139 def get_skipped_replies_count(self) -> int:
137 """
140 """
138 Gets number of posts between opening post and last replies.
141 Gets number of posts between opening post and last replies.
139 """
142 """
140 reply_count = self.get_reply_count()
143 reply_count = self.get_reply_count()
141 last_replies_count = min(settings.LAST_REPLIES_COUNT,
144 last_replies_count = min(settings.get_int('View', 'LastRepliesCount'),
142 reply_count - 1)
145 reply_count - 1)
143 return reply_count - last_replies_count - 1
146 return reply_count - last_replies_count - 1
144
147
145 def get_replies(self, view_fields_only=False) -> list:
148 def get_replies(self, view_fields_only=False) -> list:
146 """
149 """
147 Gets sorted thread posts
150 Gets sorted thread posts
148 """
151 """
149
152
150 query = Post.objects.filter(threads__in=[self])
153 query = Post.objects.filter(threads__in=[self])
151 query = query.order_by('pub_time').prefetch_related('images', 'thread', 'threads')
154 query = query.order_by('pub_time').prefetch_related('images', 'thread', 'threads')
152 if view_fields_only:
155 if view_fields_only:
153 query = query.defer('poster_ip')
156 query = query.defer('poster_ip')
154 return query.all()
157 return query.all()
155
158
156 def get_replies_with_images(self, view_fields_only=False) -> list:
159 def get_replies_with_images(self, view_fields_only=False) -> list:
157 """
160 """
158 Gets replies that have at least one image attached
161 Gets replies that have at least one image attached
159 """
162 """
160
163
161 return self.get_replies(view_fields_only).annotate(images_count=Count(
164 return self.get_replies(view_fields_only).annotate(images_count=Count(
162 'images')).filter(images_count__gt=0)
165 'images')).filter(images_count__gt=0)
163
166
164 # TODO Do we still need this?
167 # TODO Do we still need this?
165 def add_tag(self, tag: Tag):
168 def add_tag(self, tag: Tag):
166 """
169 """
167 Connects thread to a tag and tag to a thread
170 Connects thread to a tag and tag to a thread
168 """
171 """
169
172
170 self.tags.add(tag)
173 self.tags.add(tag)
171
174
172 def get_opening_post(self, only_id=False) -> Post:
175 def get_opening_post(self, only_id=False) -> Post:
173 """
176 """
174 Gets the first post of the thread
177 Gets the first post of the thread
175 """
178 """
176
179
177 query = self.get_replies().order_by('pub_time')
180 query = self.get_replies().order_by('pub_time')
178 if only_id:
181 if only_id:
179 query = query.only('id')
182 query = query.only('id')
180 opening_post = query.first()
183 opening_post = query.first()
181
184
182 return opening_post
185 return opening_post
183
186
184 @cached_result()
187 @cached_result()
185 def get_opening_post_id(self) -> int:
188 def get_opening_post_id(self) -> int:
186 """
189 """
187 Gets ID of the first thread post.
190 Gets ID of the first thread post.
188 """
191 """
189
192
190 return self.get_opening_post(only_id=True).id
193 return self.get_opening_post(only_id=True).id
191
194
192 def get_pub_time(self):
195 def get_pub_time(self):
193 """
196 """
194 Gets opening post's pub time because thread does not have its own one.
197 Gets opening post's pub time because thread does not have its own one.
195 """
198 """
196
199
197 return self.get_opening_post().pub_time
200 return self.get_opening_post().pub_time
198
201
199 def delete(self, using=None):
202 def delete(self, using=None):
200 """
203 """
201 Deletes thread with all replies.
204 Deletes thread with all replies.
202 """
205 """
203
206
204 for reply in self.get_replies().all():
207 for reply in self.get_replies().all():
205 reply.delete()
208 reply.delete()
206
209
207 super(Thread, self).delete(using)
210 super(Thread, self).delete(using)
208
211
209 def __str__(self):
212 def __str__(self):
210 return 'T#{}/{}'.format(self.id, self.get_opening_post_id())
213 return 'T#{}/{}'.format(self.id, self.get_opening_post_id())
211
214
212 def get_tag_url_list(self) -> list:
215 def get_tag_url_list(self) -> list:
213 return boards.models.Tag.objects.get_tag_url_list(self.get_tags())
216 return boards.models.Tag.objects.get_tag_url_list(self.get_tags())
214
217
215 def update_posts_time(self, exclude_posts=None):
218 def update_posts_time(self, exclude_posts=None):
216 for post in self.post_set.all():
219 for post in self.post_set.all():
217 if exclude_posts is not None and post not in exclude_posts:
220 if exclude_posts is not None and post not in exclude_posts:
218 # Manual update is required because uids are generated on save
221 # Manual update is required because uids are generated on save
219 post.last_edit_time = self.last_edit_time
222 post.last_edit_time = self.last_edit_time
220 post.save(update_fields=['last_edit_time'])
223 post.save(update_fields=['last_edit_time'])
221
224
222 post.threads.update(last_edit_time=self.last_edit_time)
225 post.threads.update(last_edit_time=self.last_edit_time)
223
226
224 def notify_clients(self):
227 def notify_clients(self):
225 if not settings.WEBSOCKETS_ENABLED:
228 if not settings.get_bool('External', 'WebsocketsEnabled'):
226 return
229 return
227
230
228 client = Client()
231 client = Client()
229
232
230 channel_name = WS_CHANNEL_THREAD + str(self.get_opening_post_id())
233 channel_name = WS_CHANNEL_THREAD + str(self.get_opening_post_id())
231 client.publish(channel_name, {
234 client.publish(channel_name, {
232 WS_NOTIFICATION_TYPE: WS_NOTIFICATION_TYPE_NEW_POST,
235 WS_NOTIFICATION_TYPE: WS_NOTIFICATION_TYPE_NEW_POST,
233 })
236 })
234 client.send()
237 client.send()
238
239 def get_absolute_url(self):
240 return self.get_opening_post().get_absolute_url()
@@ -1,80 +1,80 b''
1 from django.contrib.syndication.views import Feed
1 from django.contrib.syndication.views import Feed
2 from django.core.urlresolvers import reverse
2 from django.core.urlresolvers import reverse
3 from django.shortcuts import get_object_or_404
3 from django.shortcuts import get_object_or_404
4 from boards.models import Post, Tag, Thread
4 from boards.models import Post, Tag, Thread
5 from boards import settings
5 from boards import settings
6
6
7 __author__ = 'neko259'
7 __author__ = 'neko259'
8
8
9
9
10 # TODO Make tests for all of these
10 # TODO Make tests for all of these
11 class AllThreadsFeed(Feed):
11 class AllThreadsFeed(Feed):
12
12
13 title = settings.SITE_NAME + ' - All threads'
13 title = settings.get('Version', 'SiteName') + ' - All threads'
14 link = '/'
14 link = '/'
15 description_template = 'boards/rss/post.html'
15 description_template = 'boards/rss/post.html'
16
16
17 def items(self):
17 def items(self):
18 return Thread.objects.filter(archived=False).order_by('-id')
18 return Thread.objects.filter(archived=False).order_by('-id')
19
19
20 def item_title(self, item):
20 def item_title(self, item):
21 return item.get_opening_post().title
21 return item.get_opening_post().title
22
22
23 def item_link(self, item):
23 def item_link(self, item):
24 return reverse('thread', args={item.get_opening_post_id()})
24 return reverse('thread', args={item.get_opening_post_id()})
25
25
26 def item_pubdate(self, item):
26 def item_pubdate(self, item):
27 return item.get_pub_time()
27 return item.get_pub_time()
28
28
29
29
30 class TagThreadsFeed(Feed):
30 class TagThreadsFeed(Feed):
31
31
32 link = '/'
32 link = '/'
33 description_template = 'boards/rss/post.html'
33 description_template = 'boards/rss/post.html'
34
34
35 def items(self, obj):
35 def items(self, obj):
36 return obj.threads.filter(archived=False).order_by('-id')
36 return obj.threads.filter(archived=False).order_by('-id')
37
37
38 def get_object(self, request, tag_name):
38 def get_object(self, request, tag_name):
39 return get_object_or_404(Tag, name=tag_name)
39 return get_object_or_404(Tag, name=tag_name)
40
40
41 def item_title(self, item):
41 def item_title(self, item):
42 return item.get_opening_post().title
42 return item.get_opening_post().title
43
43
44 def item_link(self, item):
44 def item_link(self, item):
45 return reverse('thread', args={item.get_opening_post_id()})
45 return reverse('thread', args={item.get_opening_post_id()})
46
46
47 def item_pubdate(self, item):
47 def item_pubdate(self, item):
48 return item.get_pub_time()
48 return item.get_pub_time()
49
49
50 def title(self, obj):
50 def title(self, obj):
51 return obj.name
51 return obj.name
52
52
53
53
54 class ThreadPostsFeed(Feed):
54 class ThreadPostsFeed(Feed):
55
55
56 link = '/'
56 link = '/'
57 description_template = 'boards/rss/post.html'
57 description_template = 'boards/rss/post.html'
58
58
59 def items(self, obj):
59 def items(self, obj):
60 return obj.get_thread().get_replies()
60 return obj.get_thread().get_replies()
61
61
62 def get_object(self, request, post_id):
62 def get_object(self, request, post_id):
63 return get_object_or_404(Post, id=post_id)
63 return get_object_or_404(Post, id=post_id)
64
64
65 def item_title(self, item):
65 def item_title(self, item):
66 return item.title
66 return item.title
67
67
68 def item_link(self, item):
68 def item_link(self, item):
69 if not item.is_opening():
69 if not item.is_opening():
70 return reverse('thread', args={
70 return reverse('thread', args={
71 item.get_thread().get_opening_post_id()
71 item.get_thread().get_opening_post_id()
72 }) + "#" + str(item.id)
72 }) + "#" + str(item.id)
73 else:
73 else:
74 return reverse('thread', args={item.id})
74 return reverse('thread', args={item.id})
75
75
76 def item_pubdate(self, item):
76 def item_pubdate(self, item):
77 return item.pub_time
77 return item.pub_time
78
78
79 def title(self, obj):
79 def title(self, obj):
80 return obj.title
80 return obj.title
@@ -1,2 +1,18 b''
1 from boards.default_settings import *
1 import configparser
2
3
4 config = configparser.ConfigParser()
5 config.read('boards/config/default_settings.ini')
6 config.read('boards/config/settings.ini')
7
2
8
9 def get(section, name):
10 return config[section][name]
11
12
13 def get_int(section, name):
14 return int(get(section, name))
15
16
17 def get_bool(section, name):
18 return get(section, name) == 'true'
@@ -1,365 +1,383 b''
1 /*
1 /*
2 @licstart The following is the entire license notice for the
2 @licstart The following is the entire license notice for the
3 JavaScript code in this page.
3 JavaScript code in this page.
4
4
5
5
6 Copyright (C) 2013-2014 neko259
6 Copyright (C) 2013-2014 neko259
7
7
8 The JavaScript code in this page is free software: you can
8 The JavaScript code in this page is free software: you can
9 redistribute it and/or modify it under the terms of the GNU
9 redistribute it and/or modify it under the terms of the GNU
10 General Public License (GNU GPL) as published by the Free Software
10 General Public License (GNU GPL) as published by the Free Software
11 Foundation, either version 3 of the License, or (at your option)
11 Foundation, either version 3 of the License, or (at your option)
12 any later version. The code is distributed WITHOUT ANY WARRANTY;
12 any later version. The code is distributed WITHOUT ANY WARRANTY;
13 without even the implied warranty of MERCHANTABILITY or FITNESS
13 without even the implied warranty of MERCHANTABILITY or FITNESS
14 FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
14 FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
15
15
16 As additional permission under GNU GPL version 3 section 7, you
16 As additional permission under GNU GPL version 3 section 7, you
17 may distribute non-source (e.g., minimized or compacted) forms of
17 may distribute non-source (e.g., minimized or compacted) forms of
18 that code without the copy of the GNU GPL normally required by
18 that code without the copy of the GNU GPL normally required by
19 section 4, provided you include this license notice and a URL
19 section 4, provided you include this license notice and a URL
20 through which recipients can access the Corresponding Source.
20 through which recipients can access the Corresponding Source.
21
21
22 @licend The above is the entire license notice
22 @licend The above is the entire license notice
23 for the JavaScript code in this page.
23 for the JavaScript code in this page.
24 */
24 */
25
25
26 var CLASS_POST = '.post'
26 var CLASS_POST = '.post'
27
27
28 var POST_ADDED = 0;
28 var POST_ADDED = 0;
29 var POST_UPDATED = 1;
29 var POST_UPDATED = 1;
30
30
31 var JS_AUTOUPDATE_PERIOD = 20000;
32
31 var wsUser = '';
33 var wsUser = '';
32
34
33 var unreadPosts = 0;
35 var unreadPosts = 0;
34 var documentOriginalTitle = '';
36 var documentOriginalTitle = '';
35
37
36 // Thread ID does not change, can be stored one time
38 // Thread ID does not change, can be stored one time
37 var threadId = $('div.thread').children(CLASS_POST).first().attr('id');
39 var threadId = $('div.thread').children(CLASS_POST).first().attr('id');
38
40
39 /**
41 /**
40 * Connect to websocket server and subscribe to thread updates. On any update we
42 * Connect to websocket server and subscribe to thread updates. On any update we
41 * request a thread diff.
43 * request a thread diff.
42 *
44 *
43 * @returns {boolean} true if connected, false otherwise
45 * @returns {boolean} true if connected, false otherwise
44 */
46 */
45 function connectWebsocket() {
47 function connectWebsocket() {
46 var metapanel = $('.metapanel')[0];
48 var metapanel = $('.metapanel')[0];
47
49
48 var wsHost = metapanel.getAttribute('data-ws-host');
50 var wsHost = metapanel.getAttribute('data-ws-host');
49 var wsPort = metapanel.getAttribute('data-ws-port');
51 var wsPort = metapanel.getAttribute('data-ws-port');
50
52
51 if (wsHost.length > 0 && wsPort.length > 0)
53 if (wsHost.length > 0 && wsPort.length > 0) {
52 var centrifuge = new Centrifuge({
54 var centrifuge = new Centrifuge({
53 "url": 'ws://' + wsHost + ':' + wsPort + "/connection/websocket",
55 "url": 'ws://' + wsHost + ':' + wsPort + "/connection/websocket",
54 "project": metapanel.getAttribute('data-ws-project'),
56 "project": metapanel.getAttribute('data-ws-project'),
55 "user": wsUser,
57 "user": wsUser,
56 "timestamp": metapanel.getAttribute('data-ws-token-time'),
58 "timestamp": metapanel.getAttribute('data-ws-token-time'),
57 "token": metapanel.getAttribute('data-ws-token'),
59 "token": metapanel.getAttribute('data-ws-token'),
58 "debug": false
60 "debug": false
59 });
61 });
60
62
61 centrifuge.on('error', function(error_message) {
63 centrifuge.on('error', function(error_message) {
62 console.log("Error connecting to websocket server.");
64 console.log("Error connecting to websocket server.");
63 console.log(error_message);
65 console.log(error_message);
64 return false;
66 return false;
65 });
66
67 centrifuge.on('connect', function() {
68 var channelName = 'thread:' + threadId;
69 centrifuge.subscribe(channelName, function(message) {
70 getThreadDiff();
71 });
67 });
72
68
73 // For the case we closed the browser and missed some updates
69 centrifuge.on('connect', function() {
74 getThreadDiff();
70 var channelName = 'thread:' + threadId;
75 $('#autoupdate').hide();
71 centrifuge.subscribe(channelName, function(message) {
76 });
72 getThreadDiff();
73 });
77
74
78 centrifuge.connect();
75 // For the case we closed the browser and missed some updates
76 getThreadDiff();
77 $('#autoupdate').hide();
78 });
79
79
80 return true;
80 centrifuge.connect();
81
82 return true;
83 } else {
84 return false;
85 }
81 }
86 }
82
87
83 /**
88 /**
84 * Get diff of the posts from the current thread timestamp.
89 * Get diff of the posts from the current thread timestamp.
85 * This is required if the browser was closed and some post updates were
90 * This is required if the browser was closed and some post updates were
86 * missed.
91 * missed.
87 */
92 */
88 function getThreadDiff() {
93 function getThreadDiff() {
89 var lastUpdateTime = $('.metapanel').attr('data-last-update');
94 var lastUpdateTime = $('.metapanel').attr('data-last-update');
90 var lastPostId = $('.post').last().attr('id');
95 var lastPostId = $('.post').last().attr('id');
91
96
92 var uids = '';
97 var uids = '';
93 var posts = $('.post');
98 var posts = $('.post');
94 for (var i = 0; i < posts.length; i++) {
99 for (var i = 0; i < posts.length; i++) {
95 uids += posts[i].getAttribute('data-uid') + ' ';
100 uids += posts[i].getAttribute('data-uid') + ' ';
96 }
101 }
97
102
98 var data = {
103 var data = {
99 uids: uids
104 uids: uids
100 }
105 }
101
106
102 var diffUrl = '/api/diff_thread?thread=' + threadId;
107 var diffUrl = '/api/diff_thread?thread=' + threadId;
103
108
104 $.post(diffUrl,
109 $.post(diffUrl,
105 data,
110 data,
106 function(data) {
111 function(data) {
107 var updatedPosts = data.updated;
112 var updatedPosts = data.updated;
108 var addedPostCount = 0;
113 var addedPostCount = 0;
109
114
110 for (var i = 0; i < updatedPosts.length; i++) {
115 for (var i = 0; i < updatedPosts.length; i++) {
111 var postText = updatedPosts[i];
116 var postText = updatedPosts[i];
112 var post = $(postText);
117 var post = $(postText);
113
118
114 if (updatePost(post) == POST_ADDED) {
119 if (updatePost(post) == POST_ADDED) {
115 addedPostCount++;
120 addedPostCount++;
116 }
121 }
117 }
122 }
118
123
119 var hasMetaUpdates = updatedPosts.length > 0;
124 var hasMetaUpdates = updatedPosts.length > 0;
120 if (hasMetaUpdates) {
125 if (hasMetaUpdates) {
121 updateMetadataPanel();
126 updateMetadataPanel();
122 }
127 }
123
128
124 if (addedPostCount > 0) {
129 if (addedPostCount > 0) {
125 updateBumplimitProgress(addedPostCount);
130 updateBumplimitProgress(addedPostCount);
126 }
131 }
127
132
128 if (updatedPosts.length > 0) {
133 if (updatedPosts.length > 0) {
129 showNewPostsTitle(addedPostCount);
134 showNewPostsTitle(addedPostCount);
130 }
135 }
131
136
132 // TODO Process removed posts if any
137 // TODO Process removed posts if any
133 $('.metapanel').attr('data-last-update', data.last_update);
138 $('.metapanel').attr('data-last-update', data.last_update);
134 },
139 },
135 'json'
140 'json'
136 )
141 )
137 }
142 }
138
143
139 /**
144 /**
140 * Add or update the post on html page.
145 * Add or update the post on html page.
141 */
146 */
142 function updatePost(postHtml) {
147 function updatePost(postHtml) {
143 // This needs to be set on start because the page is scrolled after posts
148 // This needs to be set on start because the page is scrolled after posts
144 // are added or updated
149 // are added or updated
145 var bottom = isPageBottom();
150 var bottom = isPageBottom();
146
151
147 var post = $(postHtml);
152 var post = $(postHtml);
148
153
149 var threadBlock = $('div.thread');
154 var threadBlock = $('div.thread');
150
155
151 var postId = post.attr('id');
156 var postId = post.attr('id');
152
157
153 // If the post already exists, replace it. Otherwise add as a new one.
158 // If the post already exists, replace it. Otherwise add as a new one.
154 var existingPosts = threadBlock.children('.post[id=' + postId + ']');
159 var existingPosts = threadBlock.children('.post[id=' + postId + ']');
155
160
156 var type;
161 var type;
157
162
158 if (existingPosts.size() > 0) {
163 if (existingPosts.size() > 0) {
159 existingPosts.replaceWith(post);
164 existingPosts.replaceWith(post);
160
165
161 type = POST_UPDATED;
166 type = POST_UPDATED;
162 } else {
167 } else {
163 post.appendTo(threadBlock);
168 post.appendTo(threadBlock);
164
169
165 if (bottom) {
170 if (bottom) {
166 scrollToBottom();
171 scrollToBottom();
167 }
172 }
168
173
169 type = POST_ADDED;
174 type = POST_ADDED;
170 }
175 }
171
176
172 processNewPost(post);
177 processNewPost(post);
173
178
174 return type;
179 return type;
175 }
180 }
176
181
177 /**
182 /**
178 * Initiate a blinking animation on a node to show it was updated.
183 * Initiate a blinking animation on a node to show it was updated.
179 */
184 */
180 function blink(node) {
185 function blink(node) {
181 var blinkCount = 2;
186 var blinkCount = 2;
182
187
183 var nodeToAnimate = node;
188 var nodeToAnimate = node;
184 for (var i = 0; i < blinkCount; i++) {
189 for (var i = 0; i < blinkCount; i++) {
185 nodeToAnimate = nodeToAnimate.fadeTo('fast', 0.5).fadeTo('fast', 1.0);
190 nodeToAnimate = nodeToAnimate.fadeTo('fast', 0.5).fadeTo('fast', 1.0);
186 }
191 }
187 }
192 }
188
193
189 function isPageBottom() {
194 function isPageBottom() {
190 var scroll = $(window).scrollTop() / ($(document).height()
195 var scroll = $(window).scrollTop() / ($(document).height()
191 - $(window).height());
196 - $(window).height());
192
197
193 return scroll == 1
198 return scroll == 1
194 }
199 }
195
200
201 function enableJsUpdate() {
202 setInterval(getThreadDiff, JS_AUTOUPDATE_PERIOD);
203 return true;
204 }
205
196 function initAutoupdate() {
206 function initAutoupdate() {
197 return connectWebsocket();
207 if (location.protocol === 'https:') {
208 return enableJsUpdate();
209 } else {
210 if (connectWebsocket()) {
211 return true;
212 } else {
213 return enableJsUpdate();
214 }
215 }
198 }
216 }
199
217
200 function getReplyCount() {
218 function getReplyCount() {
201 return $('.thread').children(CLASS_POST).length
219 return $('.thread').children(CLASS_POST).length
202 }
220 }
203
221
204 function getImageCount() {
222 function getImageCount() {
205 return $('.thread').find('img').length
223 return $('.thread').find('img').length
206 }
224 }
207
225
208 /**
226 /**
209 * Update post count, images count and last update time in the metadata
227 * Update post count, images count and last update time in the metadata
210 * panel.
228 * panel.
211 */
229 */
212 function updateMetadataPanel() {
230 function updateMetadataPanel() {
213 var replyCountField = $('#reply-count');
231 var replyCountField = $('#reply-count');
214 var imageCountField = $('#image-count');
232 var imageCountField = $('#image-count');
215
233
216 replyCountField.text(getReplyCount());
234 replyCountField.text(getReplyCount());
217 imageCountField.text(getImageCount());
235 imageCountField.text(getImageCount());
218
236
219 var lastUpdate = $('.post:last').children('.post-info').first()
237 var lastUpdate = $('.post:last').children('.post-info').first()
220 .children('.pub_time').first().html();
238 .children('.pub_time').first().html();
221 if (lastUpdate !== '') {
239 if (lastUpdate !== '') {
222 var lastUpdateField = $('#last-update');
240 var lastUpdateField = $('#last-update');
223 lastUpdateField.html(lastUpdate);
241 lastUpdateField.html(lastUpdate);
224 blink(lastUpdateField);
242 blink(lastUpdateField);
225 }
243 }
226
244
227 blink(replyCountField);
245 blink(replyCountField);
228 blink(imageCountField);
246 blink(imageCountField);
229 }
247 }
230
248
231 /**
249 /**
232 * Update bumplimit progress bar
250 * Update bumplimit progress bar
233 */
251 */
234 function updateBumplimitProgress(postDelta) {
252 function updateBumplimitProgress(postDelta) {
235 var progressBar = $('#bumplimit_progress');
253 var progressBar = $('#bumplimit_progress');
236 if (progressBar) {
254 if (progressBar) {
237 var postsToLimitElement = $('#left_to_limit');
255 var postsToLimitElement = $('#left_to_limit');
238
256
239 var oldPostsToLimit = parseInt(postsToLimitElement.text());
257 var oldPostsToLimit = parseInt(postsToLimitElement.text());
240 var postCount = getReplyCount();
258 var postCount = getReplyCount();
241 var bumplimit = postCount - postDelta + oldPostsToLimit;
259 var bumplimit = postCount - postDelta + oldPostsToLimit;
242
260
243 var newPostsToLimit = bumplimit - postCount;
261 var newPostsToLimit = bumplimit - postCount;
244 if (newPostsToLimit <= 0) {
262 if (newPostsToLimit <= 0) {
245 $('.bar-bg').remove();
263 $('.bar-bg').remove();
246 } else {
264 } else {
247 postsToLimitElement.text(newPostsToLimit);
265 postsToLimitElement.text(newPostsToLimit);
248 progressBar.width((100 - postCount / bumplimit * 100.0) + '%');
266 progressBar.width((100 - postCount / bumplimit * 100.0) + '%');
249 }
267 }
250 }
268 }
251 }
269 }
252
270
253 /**
271 /**
254 * Show 'new posts' text in the title if the document is not visible to a user
272 * Show 'new posts' text in the title if the document is not visible to a user
255 */
273 */
256 function showNewPostsTitle(newPostCount) {
274 function showNewPostsTitle(newPostCount) {
257 if (document.hidden) {
275 if (document.hidden) {
258 if (documentOriginalTitle === '') {
276 if (documentOriginalTitle === '') {
259 documentOriginalTitle = document.title;
277 documentOriginalTitle = document.title;
260 }
278 }
261 unreadPosts = unreadPosts + newPostCount;
279 unreadPosts = unreadPosts + newPostCount;
262
280
263 var newTitle = '* ';
281 var newTitle = '* ';
264 if (unreadPosts > 0) {
282 if (unreadPosts > 0) {
265 newTitle += '[' + unreadPosts + '] ';
283 newTitle += '[' + unreadPosts + '] ';
266 }
284 }
267 newTitle += documentOriginalTitle;
285 newTitle += documentOriginalTitle;
268
286
269 document.title = newTitle;
287 document.title = newTitle;
270
288
271 document.addEventListener('visibilitychange', function() {
289 document.addEventListener('visibilitychange', function() {
272 if (documentOriginalTitle !== '') {
290 if (documentOriginalTitle !== '') {
273 document.title = documentOriginalTitle;
291 document.title = documentOriginalTitle;
274 documentOriginalTitle = '';
292 documentOriginalTitle = '';
275 unreadPosts = 0;
293 unreadPosts = 0;
276 }
294 }
277
295
278 document.removeEventListener('visibilitychange', null);
296 document.removeEventListener('visibilitychange', null);
279 });
297 });
280 }
298 }
281 }
299 }
282
300
283 /**
301 /**
284 * Clear all entered values in the form fields
302 * Clear all entered values in the form fields
285 */
303 */
286 function resetForm(form) {
304 function resetForm(form) {
287 form.find('input:text, input:password, input:file, select, textarea').val('');
305 form.find('input:text, input:password, input:file, select, textarea').val('');
288 form.find('input:radio, input:checkbox')
306 form.find('input:radio, input:checkbox')
289 .removeAttr('checked').removeAttr('selected');
307 .removeAttr('checked').removeAttr('selected');
290 $('.file_wrap').find('.file-thumb').remove();
308 $('.file_wrap').find('.file-thumb').remove();
291 }
309 }
292
310
293 /**
311 /**
294 * When the form is posted, this method will be run as a callback
312 * When the form is posted, this method will be run as a callback
295 */
313 */
296 function updateOnPost(response, statusText, xhr, form) {
314 function updateOnPost(response, statusText, xhr, form) {
297 var json = $.parseJSON(response);
315 var json = $.parseJSON(response);
298 var status = json.status;
316 var status = json.status;
299
317
300 showAsErrors(form, '');
318 showAsErrors(form, '');
301
319
302 if (status === 'ok') {
320 if (status === 'ok') {
303 resetFormPosition();
321 resetFormPosition();
304 resetForm(form);
322 resetForm(form);
305 getThreadDiff();
323 getThreadDiff();
306 scrollToBottom();
324 scrollToBottom();
307 } else {
325 } else {
308 var errors = json.errors;
326 var errors = json.errors;
309 for (var i = 0; i < errors.length; i++) {
327 for (var i = 0; i < errors.length; i++) {
310 var fieldErrors = errors[i];
328 var fieldErrors = errors[i];
311
329
312 var error = fieldErrors.errors;
330 var error = fieldErrors.errors;
313
331
314 showAsErrors(form, error);
332 showAsErrors(form, error);
315 }
333 }
316 }
334 }
317 }
335 }
318
336
319 /**
337 /**
320 * Show text in the errors row of the form.
338 * Show text in the errors row of the form.
321 * @param form
339 * @param form
322 * @param text
340 * @param text
323 */
341 */
324 function showAsErrors(form, text) {
342 function showAsErrors(form, text) {
325 form.children('.form-errors').remove();
343 form.children('.form-errors').remove();
326
344
327 if (text.length > 0) {
345 if (text.length > 0) {
328 var errorList = $('<div class="form-errors">' + text + '<div>');
346 var errorList = $('<div class="form-errors">' + text + '<div>');
329 errorList.appendTo(form);
347 errorList.appendTo(form);
330 }
348 }
331 }
349 }
332
350
333 /**
351 /**
334 * Run js methods that are usually run on the document, on the new post
352 * Run js methods that are usually run on the document, on the new post
335 */
353 */
336 function processNewPost(post) {
354 function processNewPost(post) {
337 addRefLinkPreview(post[0]);
355 addRefLinkPreview(post[0]);
338 highlightCode(post);
356 highlightCode(post);
339 blink(post);
357 blink(post);
340 }
358 }
341
359
342 $(document).ready(function(){
360 $(document).ready(function(){
343 if (initAutoupdate()) {
361 if (initAutoupdate()) {
344 // Post form data over AJAX
362 // Post form data over AJAX
345 var threadId = $('div.thread').children('.post').first().attr('id');
363 var threadId = $('div.thread').children('.post').first().attr('id');
346
364
347 var form = $('#form');
365 var form = $('#form');
348
366
349 if (form.length > 0) {
367 if (form.length > 0) {
350 var options = {
368 var options = {
351 beforeSubmit: function(arr, $form, options) {
369 beforeSubmit: function(arr, $form, options) {
352 showAsErrors($('form'), gettext('Sending message...'));
370 showAsErrors($('form'), gettext('Sending message...'));
353 },
371 },
354 success: updateOnPost,
372 success: updateOnPost,
355 url: '/api/add_post/' + threadId + '/'
373 url: '/api/add_post/' + threadId + '/'
356 };
374 };
357
375
358 form.ajaxForm(options);
376 form.ajaxForm(options);
359
377
360 resetForm(form);
378 resetForm(form);
361 }
379 }
362 }
380 }
363
381
364 $('#autoupdate').click(getThreadDiff);
382 $('#autoupdate').click(getThreadDiff);
365 });
383 });
@@ -1,169 +1,177 b''
1 {% extends "boards/base.html" %}
1 {% extends "boards/base.html" %}
2
2
3 {% load i18n %}
3 {% load i18n %}
4 {% load board %}
4 {% load board %}
5 {% load static %}
5 {% load static %}
6 {% load tz %}
6 {% load tz %}
7
7
8 {% block head %}
8 {% block head %}
9 <meta name="robots" content="noindex">
9 <meta name="robots" content="noindex">
10
10
11 {% if tag %}
11 {% if tag %}
12 <title>{{ tag.name }} - {{ site_name }}</title>
12 <title>{{ tag.name }} - {{ site_name }}</title>
13 {% else %}
13 {% else %}
14 <title>{{ site_name }}</title>
14 <title>{{ site_name }}</title>
15 {% endif %}
15 {% endif %}
16
16
17 {% if prev_page_link %}
17 {% if prev_page_link %}
18 <link rel="prev" href="{{ prev_page_link }}" />
18 <link rel="prev" href="{{ prev_page_link }}" />
19 {% endif %}
19 {% endif %}
20 {% if next_page_link %}
20 {% if next_page_link %}
21 <link rel="next" href="{{ next_page_link }}" />
21 <link rel="next" href="{{ next_page_link }}" />
22 {% endif %}
22 {% endif %}
23
23
24 {% endblock %}
24 {% endblock %}
25
25
26 {% block content %}
26 {% block content %}
27
27
28 {% get_current_language as LANGUAGE_CODE %}
28 {% get_current_language as LANGUAGE_CODE %}
29 {% get_current_timezone as TIME_ZONE %}
29 {% get_current_timezone as TIME_ZONE %}
30
30
31 {% for banner in banners %}
32 <div class="post">
33 <div class="title">{{ banner.title }}</div>
34 <div>{{ banner.text }}</div>
35 <div>{% trans 'Related message' %}: <a href="{{ banner.post.get_url }}">>>{{ banner.post.id }}</a></div>
36 </div>
37 {% endfor %}
38
31 {% if tag %}
39 {% if tag %}
32 <div class="tag_info">
40 <div class="tag_info">
33 <h2>
41 <h2>
34 <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form">
42 <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form">
35 {% if is_favorite %}
43 {% if is_favorite %}
36 <button name="method" value="unsubscribe" class="fav"></button>
44 <button name="method" value="unsubscribe" class="fav"></button>
37 {% else %}
45 {% else %}
38 <button name="method" value="subscribe" class="not_fav"></button>
46 <button name="method" value="subscribe" class="not_fav"></button>
39 {% endif %}
47 {% endif %}
40 </form>
48 </form>
41 <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form">
49 <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form">
42 {% if is_hidden %}
50 {% if is_hidden %}
43 <button name="method" value="unhide" class="fav">H</button>
51 <button name="method" value="unhide" class="fav">H</button>
44 {% else %}
52 {% else %}
45 <button name="method" value="hide" class="not_fav">H</button>
53 <button name="method" value="hide" class="not_fav">H</button>
46 {% endif %}
54 {% endif %}
47 </form>
55 </form>
48 {% autoescape off %}
56 {% autoescape off %}
49 {{ tag.get_view }}
57 {{ tag.get_view }}
50 {% endautoescape %}
58 {% endautoescape %}
51 {% if moderator %}
59 {% if moderator %}
52 <span class="moderator_info">[<a href="{% url 'admin:boards_tag_change' tag.id %}">{% trans 'Edit tag' %}</a>]</span>
60 <span class="moderator_info">[<a href="{% url 'admin:boards_tag_change' tag.id %}">{% trans 'Edit tag' %}</a>]</span>
53 {% endif %}
61 {% endif %}
54 </h2>
62 </h2>
55 <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>
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>
56 </div>
64 </div>
57 {% endif %}
65 {% endif %}
58
66
59 {% if threads %}
67 {% if threads %}
60 {% if prev_page_link %}
68 {% if prev_page_link %}
61 <div class="page_link">
69 <div class="page_link">
62 <a href="{{ prev_page_link }}">{% trans "Previous page" %}</a>
70 <a href="{{ prev_page_link }}">{% trans "Previous page" %}</a>
63 </div>
71 </div>
64 {% endif %}
72 {% endif %}
65
73
66 {% for thread in threads %}
74 {% for thread in threads %}
67 <div class="thread">
75 <div class="thread">
68 {% post_view thread.get_opening_post moderator=moderator is_opening=True thread=thread truncated=True need_open_link=True %}
76 {% post_view thread.get_opening_post moderator=moderator is_opening=True thread=thread truncated=True need_open_link=True %}
69 {% if not thread.archived %}
77 {% if not thread.archived %}
70 {% with last_replies=thread.get_last_replies %}
78 {% with last_replies=thread.get_last_replies %}
71 {% if last_replies %}
79 {% if last_replies %}
72 {% with skipped_replies_count=thread.get_skipped_replies_count %}
80 {% with skipped_replies_count=thread.get_skipped_replies_count %}
73 {% if skipped_replies_count %}
81 {% if skipped_replies_count %}
74 <div class="skipped_replies">
82 <div class="skipped_replies">
75 <a href="{% url 'thread' thread.get_opening_post_id %}">
83 <a href="{% url 'thread' thread.get_opening_post_id %}">
76 {% blocktrans with count=skipped_replies_count %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %}
84 {% blocktrans with count=skipped_replies_count %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %}
77 </a>
85 </a>
78 </div>
86 </div>
79 {% endif %}
87 {% endif %}
80 {% endwith %}
88 {% endwith %}
81 <div class="last-replies">
89 <div class="last-replies">
82 {% for post in last_replies %}
90 {% for post in last_replies %}
83 {% post_view post is_opening=False moderator=moderator truncated=True %}
91 {% post_view post is_opening=False moderator=moderator truncated=True %}
84 {% endfor %}
92 {% endfor %}
85 </div>
93 </div>
86 {% endif %}
94 {% endif %}
87 {% endwith %}
95 {% endwith %}
88 {% endif %}
96 {% endif %}
89 </div>
97 </div>
90 {% endfor %}
98 {% endfor %}
91
99
92 {% if next_page_link %}
100 {% if next_page_link %}
93 <div class="page_link">
101 <div class="page_link">
94 <a href="{{ next_page_link }}">{% trans "Next page" %}</a>
102 <a href="{{ next_page_link }}">{% trans "Next page" %}</a>
95 </div>
103 </div>
96 {% endif %}
104 {% endif %}
97 {% else %}
105 {% else %}
98 <div class="post">
106 <div class="post">
99 {% trans 'No threads exist. Create the first one!' %}</div>
107 {% trans 'No threads exist. Create the first one!' %}</div>
100 {% endif %}
108 {% endif %}
101
109
102 <div class="post-form-w">
110 <div class="post-form-w">
103 <script src="{% static 'js/panel.js' %}"></script>
111 <script src="{% static 'js/panel.js' %}"></script>
104 <div class="post-form">
112 <div class="post-form">
105 <div class="form-title">{% trans "Create new thread" %}</div>
113 <div class="form-title">{% trans "Create new thread" %}</div>
106 <div class="swappable-form-full">
114 <div class="swappable-form-full">
107 <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %}
115 <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %}
108 {{ form.as_div }}
116 {{ form.as_div }}
109 <div class="form-submit">
117 <div class="form-submit">
110 <input type="submit" value="{% trans "Post" %}"/>
118 <input type="submit" value="{% trans "Post" %}"/>
111 </div>
119 </div>
112 </form>
120 </form>
113 </div>
121 </div>
114 <div>
122 <div>
115 {% trans 'Tags must be delimited by spaces. Text or image is required.' %}
123 {% trans 'Tags must be delimited by spaces. Text or image is required.' %}
116 </div>
124 </div>
117 <div>
125 <div>
118 <a href="{% url "staticpage" name="help" %}">{% trans 'Text syntax' %}</a>
126 <a href="{% url "staticpage" name="help" %}">{% trans 'Text syntax' %}</a>
119 </div>
127 </div>
120 <div>
128 <div>
121 <a href="{% url "tags" "required" %}">{% trans 'Tags' %}</a>
129 <a href="{% url "tags" "required" %}">{% trans 'Tags' %}</a>
122 </div>
130 </div>
123 </div>
131 </div>
124 </div>
132 </div>
125
133
126 <script src="{% static 'js/form.js' %}"></script>
134 <script src="{% static 'js/form.js' %}"></script>
127 <script src="{% static 'js/thread_create.js' %}"></script>
135 <script src="{% static 'js/thread_create.js' %}"></script>
128
136
129 {% endblock %}
137 {% endblock %}
130
138
131 {% block metapanel %}
139 {% block metapanel %}
132
140
133 <span class="metapanel">
141 <span class="metapanel">
134 <b><a href="{% url "authors" %}">{{ site_name }}</a> {{ version }}</b>
142 <b><a href="{% url "authors" %}">{{ site_name }}</a> {{ version }}</b>
135 {% trans "Pages:" %}
143 {% trans "Pages:" %}
136 <a href="
144 <a href="
137 {% if tag %}
145 {% if tag %}
138 {% url "tag" tag_name=tag.name page=paginator.page_range|first %}
146 {% url "tag" tag_name=tag.name page=paginator.page_range|first %}
139 {% else %}
147 {% else %}
140 {% url "index" page=paginator.page_range|first %}
148 {% url "index" page=paginator.page_range|first %}
141 {% endif %}
149 {% endif %}
142 ">&lt;&lt;</a>
150 ">&lt;&lt;</a>
143 [
151 [
144 {% for page in paginator.center_range %}
152 {% for page in paginator.center_range %}
145 <a
153 <a
146 {% ifequal page current_page.number %}
154 {% ifequal page current_page.number %}
147 class="current_page"
155 class="current_page"
148 {% endifequal %}
156 {% endifequal %}
149 href="
157 href="
150 {% if tag %}
158 {% if tag %}
151 {% url "tag" tag_name=tag.name page=page %}
159 {% url "tag" tag_name=tag.name page=page %}
152 {% else %}
160 {% else %}
153 {% url "index" page=page %}
161 {% url "index" page=page %}
154 {% endif %}
162 {% endif %}
155 ">{{ page }}</a>
163 ">{{ page }}</a>
156 {% if not forloop.last %},{% endif %}
164 {% if not forloop.last %},{% endif %}
157 {% endfor %}
165 {% endfor %}
158 ]
166 ]
159 <a href="
167 <a href="
160 {% if tag %}
168 {% if tag %}
161 {% url "tag" tag_name=tag.name page=paginator.page_range|last %}
169 {% url "tag" tag_name=tag.name page=paginator.page_range|last %}
162 {% else %}
170 {% else %}
163 {% url "index" page=paginator.page_range|last %}
171 {% url "index" page=paginator.page_range|last %}
164 {% endif %}
172 {% endif %}
165 ">&gt;&gt;</a>
173 ">&gt;&gt;</a>
166 [<a href="rss/">RSS</a>]
174 [<a href="rss/">RSS</a>]
167 </span>
175 </span>
168
176
169 {% endblock %}
177 {% endblock %}
@@ -1,44 +1,43 b''
1 {% extends "boards/base.html" %}
1 {% extends "boards/base.html" %}
2
2
3 {% load i18n %}
3 {% load i18n %}
4 {% load humanize %}
5 {% load tz %}
4 {% load tz %}
6
5
7 {% block head %}
6 {% block head %}
8 <meta name="robots" content="noindex">
7 <meta name="robots" content="noindex">
9 <title>{% trans 'Settings' %} - {{ site_name }}</title>
8 <title>{% trans 'Settings' %} - {{ site_name }}</title>
10 {% endblock %}
9 {% endblock %}
11
10
12 {% block content %}
11 {% block content %}
13
12
14 <div class="post">
13 <div class="post">
15 <p>
14 <p>
16 {% if moderator %}
15 {% if moderator %}
17 {% trans 'You are moderator.' %}
16 {% trans 'You are moderator.' %}
18 {% endif %}
17 {% endif %}
19 </p>
18 </p>
20 {% if hidden_tags %}
19 {% if hidden_tags %}
21 <p>{% trans 'Hidden tags:' %}
20 <p>{% trans 'Hidden tags:' %}
22 {% for tag in hidden_tags %}
21 {% for tag in hidden_tags %}
23 {% autoescape off %}
22 {% autoescape off %}
24 {{ tag.get_view }}
23 {{ tag.get_view }}
25 {% endautoescape %}
24 {% endautoescape %}
26 {% endfor %}
25 {% endfor %}
27 </p>
26 </p>
28 {% else %}
27 {% else %}
29 <p>{% trans 'No hidden tags.' %}</p>
28 <p>{% trans 'No hidden tags.' %}</p>
30 {% endif %}
29 {% endif %}
31 </div>
30 </div>
32
31
33 <div class="post-form-w">
32 <div class="post-form-w">
34 <div class="post-form">
33 <div class="post-form">
35 <form method="post">{% csrf_token %}
34 <form method="post">{% csrf_token %}
36 {{ form.as_div }}
35 {{ form.as_div }}
37 <div class="form-submit">
36 <div class="form-submit">
38 <input type="submit" value="{% trans "Save" %}" />
37 <input type="submit" value="{% trans "Save" %}" />
39 </div>
38 </div>
40 </form>
39 </form>
41 </div>
40 </div>
42 </div>
41 </div>
43
42
44 {% endblock %}
43 {% endblock %}
@@ -1,163 +1,163 b''
1 from django.core.paginator import Paginator
1 from django.core.paginator import Paginator
2 from django.test import TestCase
2 from django.test import TestCase
3 from boards import settings
3 from boards import settings
4 from boards.models import Tag, Post, Thread, KeyPair
4 from boards.models import Tag, Post, Thread, KeyPair
5
5
6
6
7 class PostTests(TestCase):
7 class PostTests(TestCase):
8
8
9 def _create_post(self):
9 def _create_post(self):
10 tag, created = Tag.objects.get_or_create(name='test_tag')
10 tag, created = Tag.objects.get_or_create(name='test_tag')
11 return Post.objects.create_post(title='title', text='text',
11 return Post.objects.create_post(title='title', text='text',
12 tags=[tag])
12 tags=[tag])
13
13
14 def test_post_add(self):
14 def test_post_add(self):
15 """Test adding post"""
15 """Test adding post"""
16
16
17 post = self._create_post()
17 post = self._create_post()
18
18
19 self.assertIsNotNone(post, 'No post was created.')
19 self.assertIsNotNone(post, 'No post was created.')
20 self.assertEqual('test_tag', post.get_thread().tags.all()[0].name,
20 self.assertEqual('test_tag', post.get_thread().tags.all()[0].name,
21 'No tags were added to the post.')
21 'No tags were added to the post.')
22
22
23 def test_delete_post(self):
23 def test_delete_post(self):
24 """Test post deletion"""
24 """Test post deletion"""
25
25
26 post = self._create_post()
26 post = self._create_post()
27 post_id = post.id
27 post_id = post.id
28
28
29 post.delete()
29 post.delete()
30
30
31 self.assertFalse(Post.objects.filter(id=post_id).exists())
31 self.assertFalse(Post.objects.filter(id=post_id).exists())
32
32
33 def test_delete_thread(self):
33 def test_delete_thread(self):
34 """Test thread deletion"""
34 """Test thread deletion"""
35
35
36 opening_post = self._create_post()
36 opening_post = self._create_post()
37 thread = opening_post.get_thread()
37 thread = opening_post.get_thread()
38 reply = Post.objects.create_post("", "", thread=thread)
38 reply = Post.objects.create_post("", "", thread=thread)
39
39
40 thread.delete()
40 thread.delete()
41
41
42 self.assertFalse(Post.objects.filter(id=reply.id).exists(),
42 self.assertFalse(Post.objects.filter(id=reply.id).exists(),
43 'Reply was not deleted with the thread.')
43 'Reply was not deleted with the thread.')
44 self.assertFalse(Post.objects.filter(id=opening_post.id).exists(),
44 self.assertFalse(Post.objects.filter(id=opening_post.id).exists(),
45 'Opening post was not deleted with the thread.')
45 'Opening post was not deleted with the thread.')
46
46
47 def test_post_to_thread(self):
47 def test_post_to_thread(self):
48 """Test adding post to a thread"""
48 """Test adding post to a thread"""
49
49
50 op = self._create_post()
50 op = self._create_post()
51 post = Post.objects.create_post("", "", thread=op.get_thread())
51 post = Post.objects.create_post("", "", thread=op.get_thread())
52
52
53 self.assertIsNotNone(post, 'Reply to thread wasn\'t created')
53 self.assertIsNotNone(post, 'Reply to thread wasn\'t created')
54 self.assertEqual(op.get_thread().last_edit_time, post.pub_time,
54 self.assertEqual(op.get_thread().last_edit_time, post.pub_time,
55 'Post\'s create time doesn\'t match thread last edit'
55 'Post\'s create time doesn\'t match thread last edit'
56 ' time')
56 ' time')
57
57
58 def test_delete_posts_by_ip(self):
58 def test_delete_posts_by_ip(self):
59 """Test deleting posts with the given ip"""
59 """Test deleting posts with the given ip"""
60
60
61 post = self._create_post()
61 post = self._create_post()
62 post_id = post.id
62 post_id = post.id
63
63
64 Post.objects.delete_posts_by_ip('0.0.0.0')
64 Post.objects.delete_posts_by_ip('0.0.0.0')
65
65
66 self.assertFalse(Post.objects.filter(id=post_id).exists())
66 self.assertFalse(Post.objects.filter(id=post_id).exists())
67
67
68 def test_get_thread(self):
68 def test_get_thread(self):
69 """Test getting all posts of a thread"""
69 """Test getting all posts of a thread"""
70
70
71 opening_post = self._create_post()
71 opening_post = self._create_post()
72
72
73 for i in range(2):
73 for i in range(2):
74 Post.objects.create_post('title', 'text',
74 Post.objects.create_post('title', 'text',
75 thread=opening_post.get_thread())
75 thread=opening_post.get_thread())
76
76
77 thread = opening_post.get_thread()
77 thread = opening_post.get_thread()
78
78
79 self.assertEqual(3, thread.get_replies().count())
79 self.assertEqual(3, thread.get_replies().count())
80
80
81 def test_create_post_with_tag(self):
81 def test_create_post_with_tag(self):
82 """Test adding tag to post"""
82 """Test adding tag to post"""
83
83
84 tag = Tag.objects.create(name='test_tag')
84 tag = Tag.objects.create(name='test_tag')
85 post = Post.objects.create_post(title='title', text='text', tags=[tag])
85 post = Post.objects.create_post(title='title', text='text', tags=[tag])
86
86
87 thread = post.get_thread()
87 thread = post.get_thread()
88 self.assertIsNotNone(post, 'Post not created')
88 self.assertIsNotNone(post, 'Post not created')
89 self.assertTrue(tag in thread.tags.all(), 'Tag not added to thread')
89 self.assertTrue(tag in thread.tags.all(), 'Tag not added to thread')
90
90
91 def test_thread_max_count(self):
91 def test_thread_max_count(self):
92 """Test deletion of old posts when the max thread count is reached"""
92 """Test deletion of old posts when the max thread count is reached"""
93
93
94 for i in range(settings.MAX_THREAD_COUNT + 1):
94 for i in range(settings.get_int('Messages', 'MaxThreadCount') + 1):
95 self._create_post()
95 self._create_post()
96
96
97 self.assertEqual(settings.MAX_THREAD_COUNT,
97 self.assertEqual(settings.get_int('Messages', 'MaxThreadCount'),
98 len(Thread.objects.filter(archived=False)))
98 len(Thread.objects.filter(archived=False)))
99
99
100 def test_pages(self):
100 def test_pages(self):
101 """Test that the thread list is properly split into pages"""
101 """Test that the thread list is properly split into pages"""
102
102
103 for i in range(settings.MAX_THREAD_COUNT):
103 for i in range(settings.get_int('Messages', 'MaxThreadCount')):
104 self._create_post()
104 self._create_post()
105
105
106 all_threads = Thread.objects.filter(archived=False)
106 all_threads = Thread.objects.filter(archived=False)
107
107
108 paginator = Paginator(Thread.objects.filter(archived=False),
108 paginator = Paginator(Thread.objects.filter(archived=False),
109 settings.THREADS_PER_PAGE)
109 settings.get_int('View', 'ThreadsPerPage'))
110 posts_in_second_page = paginator.page(2).object_list
110 posts_in_second_page = paginator.page(2).object_list
111 first_post = posts_in_second_page[0]
111 first_post = posts_in_second_page[0]
112
112
113 self.assertEqual(all_threads[settings.THREADS_PER_PAGE].id,
113 self.assertEqual(all_threads[settings.get_int('View', 'ThreadsPerPage')].id,
114 first_post.id)
114 first_post.id)
115
115
116 def test_reflinks(self):
116 def test_reflinks(self):
117 """
117 """
118 Tests that reflinks are parsed within post and connecting replies
118 Tests that reflinks are parsed within post and connecting replies
119 to the replied posts.
119 to the replied posts.
120
120
121 Local reflink example: [post]123[/post]
121 Local reflink example: [post]123[/post]
122 Global reflink example: [post]key_type::key::123[/post]
122 Global reflink example: [post]key_type::key::123[/post]
123 """
123 """
124
124
125 key = KeyPair.objects.generate_key(primary=True)
125 key = KeyPair.objects.generate_key(primary=True)
126
126
127 tag = Tag.objects.create(name='test_tag')
127 tag = Tag.objects.create(name='test_tag')
128
128
129 post = Post.objects.create_post(title='', text='', tags=[tag])
129 post = Post.objects.create_post(title='', text='', tags=[tag])
130 post_local_reflink = Post.objects.create_post(title='',
130 post_local_reflink = Post.objects.create_post(title='',
131 text='[post]%d[/post]' % post.id, thread=post.get_thread())
131 text='[post]%d[/post]' % post.id, thread=post.get_thread())
132
132
133 self.assertTrue(post_local_reflink in post.referenced_posts.all(),
133 self.assertTrue(post_local_reflink in post.referenced_posts.all(),
134 'Local reflink not connecting posts.')
134 'Local reflink not connecting posts.')
135
135
136 post_global_reflink = Post.objects.create_post(title='',
136 post_global_reflink = Post.objects.create_post(title='',
137 text='[post]%s::%s::%d[/post]' % (
137 text='[post]%s::%s::%d[/post]' % (
138 post.global_id.key_type, post.global_id.key, post.id),
138 post.global_id.key_type, post.global_id.key, post.id),
139 thread=post.get_thread())
139 thread=post.get_thread())
140
140
141 self.assertTrue(post_global_reflink in post.referenced_posts.all(),
141 self.assertTrue(post_global_reflink in post.referenced_posts.all(),
142 'Global reflink not connecting posts.')
142 'Global reflink not connecting posts.')
143
143
144 def test_thread_replies(self):
144 def test_thread_replies(self):
145 """
145 """
146 Tests that the replies can be queried from a thread in all possible
146 Tests that the replies can be queried from a thread in all possible
147 ways.
147 ways.
148 """
148 """
149
149
150 tag = Tag.objects.create(name='test_tag')
150 tag = Tag.objects.create(name='test_tag')
151 opening_post = Post.objects.create_post(title='title', text='text',
151 opening_post = Post.objects.create_post(title='title', text='text',
152 tags=[tag])
152 tags=[tag])
153 thread = opening_post.get_thread()
153 thread = opening_post.get_thread()
154
154
155 reply1 = Post.objects.create_post(title='title', text='text', thread=thread)
155 reply1 = Post.objects.create_post(title='title', text='text', thread=thread)
156 reply2 = Post.objects.create_post(title='title', text='text', thread=thread)
156 reply2 = Post.objects.create_post(title='title', text='text', thread=thread)
157
157
158 replies = thread.get_replies()
158 replies = thread.get_replies()
159 self.assertTrue(len(replies) > 0, 'No replies found for thread.')
159 self.assertTrue(len(replies) > 0, 'No replies found for thread.')
160
160
161 replies = thread.get_replies(view_fields_only=True)
161 replies = thread.get_replies(view_fields_only=True)
162 self.assertTrue(len(replies) > 0,
162 self.assertTrue(len(replies) > 0,
163 'No replies found for thread with view fields only.')
163 'No replies found for thread with view fields only.')
@@ -1,167 +1,169 b''
1 from django.core.urlresolvers import reverse
1 from django.core.urlresolvers import reverse
2 from django.core.files import File
2 from django.core.files import File
3 from django.core.files.temp import NamedTemporaryFile
3 from django.core.files.temp import NamedTemporaryFile
4 from django.core.paginator import EmptyPage
4 from django.core.paginator import EmptyPage
5 from django.db import transaction
5 from django.db import transaction
6 from django.http import Http404
6 from django.http import Http404
7 from django.shortcuts import render, redirect
7 from django.shortcuts import render, redirect
8 import requests
8 import requests
9
9
10 from boards import utils, settings
10 from boards import utils, settings
11 from boards.abstracts.paginator import get_paginator
11 from boards.abstracts.paginator import get_paginator
12 from boards.abstracts.settingsmanager import get_settings_manager
12 from boards.abstracts.settingsmanager import get_settings_manager
13 from boards.forms import ThreadForm, PlainErrorList
13 from boards.forms import ThreadForm, PlainErrorList
14 from boards.models import Post, Thread, Ban, Tag, PostImage
14 from boards.models import Post, Thread, Ban, Tag, PostImage, Banner
15 from boards.views.banned import BannedView
15 from boards.views.banned import BannedView
16 from boards.views.base import BaseBoardView, CONTEXT_FORM
16 from boards.views.base import BaseBoardView, CONTEXT_FORM
17 from boards.views.posting_mixin import PostMixin
17 from boards.views.posting_mixin import PostMixin
18
18
19
19
20 FORM_TAGS = 'tags'
20 FORM_TAGS = 'tags'
21 FORM_TEXT = 'text'
21 FORM_TEXT = 'text'
22 FORM_TITLE = 'title'
22 FORM_TITLE = 'title'
23 FORM_IMAGE = 'image'
23 FORM_IMAGE = 'image'
24 FORM_THREADS = 'threads'
24 FORM_THREADS = 'threads'
25
25
26 TAG_DELIMITER = ' '
26 TAG_DELIMITER = ' '
27
27
28 PARAMETER_CURRENT_PAGE = 'current_page'
28 PARAMETER_CURRENT_PAGE = 'current_page'
29 PARAMETER_PAGINATOR = 'paginator'
29 PARAMETER_PAGINATOR = 'paginator'
30 PARAMETER_THREADS = 'threads'
30 PARAMETER_THREADS = 'threads'
31 PARAMETER_BANNERS = 'banners'
31
32
32 PARAMETER_PREV_LINK = 'prev_page_link'
33 PARAMETER_PREV_LINK = 'prev_page_link'
33 PARAMETER_NEXT_LINK = 'next_page_link'
34 PARAMETER_NEXT_LINK = 'next_page_link'
34
35
35 TEMPLATE = 'boards/posting_general.html'
36 TEMPLATE = 'boards/posting_general.html'
36 DEFAULT_PAGE = 1
37 DEFAULT_PAGE = 1
37
38
38
39
39 class AllThreadsView(PostMixin, BaseBoardView):
40 class AllThreadsView(PostMixin, BaseBoardView):
40
41
41 def __init__(self):
42 def __init__(self):
42 self.settings_manager = None
43 self.settings_manager = None
43 super(AllThreadsView, self).__init__()
44 super(AllThreadsView, self).__init__()
44
45
45 def get(self, request, page=DEFAULT_PAGE, form: ThreadForm=None):
46 def get(self, request, page=DEFAULT_PAGE, form: ThreadForm=None):
46 params = self.get_context_data(request=request)
47 params = self.get_context_data(request=request)
47
48
48 if not form:
49 if not form:
49 form = ThreadForm(error_class=PlainErrorList)
50 form = ThreadForm(error_class=PlainErrorList)
50
51
51 self.settings_manager = get_settings_manager(request)
52 self.settings_manager = get_settings_manager(request)
52 paginator = get_paginator(self.get_threads(),
53 paginator = get_paginator(self.get_threads(),
53 settings.THREADS_PER_PAGE)
54 settings.get_int('View', 'ThreadsPerPage'))
54 paginator.current_page = int(page)
55 paginator.current_page = int(page)
55
56
56 try:
57 try:
57 threads = paginator.page(page).object_list
58 threads = paginator.page(page).object_list
58 except EmptyPage:
59 except EmptyPage:
59 raise Http404()
60 raise Http404()
60
61
61 params[PARAMETER_THREADS] = threads
62 params[PARAMETER_THREADS] = threads
62 params[CONTEXT_FORM] = form
63 params[CONTEXT_FORM] = form
64 params[PARAMETER_BANNERS] = Banner.objects.order_by('-id').all()
63
65
64 self.get_page_context(paginator, params, page)
66 self.get_page_context(paginator, params, page)
65
67
66 return render(request, TEMPLATE, params)
68 return render(request, TEMPLATE, params)
67
69
68 def post(self, request, page=DEFAULT_PAGE):
70 def post(self, request, page=DEFAULT_PAGE):
69 form = ThreadForm(request.POST, request.FILES,
71 form = ThreadForm(request.POST, request.FILES,
70 error_class=PlainErrorList)
72 error_class=PlainErrorList)
71 form.session = request.session
73 form.session = request.session
72
74
73 if form.is_valid():
75 if form.is_valid():
74 return self.create_thread(request, form)
76 return self.create_thread(request, form)
75 if form.need_to_ban:
77 if form.need_to_ban:
76 # Ban user because he is suspected to be a bot
78 # Ban user because he is suspected to be a bot
77 self._ban_current_user(request)
79 self._ban_current_user(request)
78
80
79 return self.get(request, page, form)
81 return self.get(request, page, form)
80
82
81 def get_page_context(self, paginator, params, page):
83 def get_page_context(self, paginator, params, page):
82 """
84 """
83 Get pagination context variables
85 Get pagination context variables
84 """
86 """
85
87
86 params[PARAMETER_PAGINATOR] = paginator
88 params[PARAMETER_PAGINATOR] = paginator
87 current_page = paginator.page(int(page))
89 current_page = paginator.page(int(page))
88 params[PARAMETER_CURRENT_PAGE] = current_page
90 params[PARAMETER_CURRENT_PAGE] = current_page
89 if current_page.has_previous():
91 if current_page.has_previous():
90 params[PARAMETER_PREV_LINK] = self.get_previous_page_link(
92 params[PARAMETER_PREV_LINK] = self.get_previous_page_link(
91 current_page)
93 current_page)
92 if current_page.has_next():
94 if current_page.has_next():
93 params[PARAMETER_NEXT_LINK] = self.get_next_page_link(current_page)
95 params[PARAMETER_NEXT_LINK] = self.get_next_page_link(current_page)
94
96
95 def get_previous_page_link(self, current_page):
97 def get_previous_page_link(self, current_page):
96 return reverse('index', kwargs={
98 return reverse('index', kwargs={
97 'page': current_page.previous_page_number(),
99 'page': current_page.previous_page_number(),
98 })
100 })
99
101
100 def get_next_page_link(self, current_page):
102 def get_next_page_link(self, current_page):
101 return reverse('index', kwargs={
103 return reverse('index', kwargs={
102 'page': current_page.next_page_number(),
104 'page': current_page.next_page_number(),
103 })
105 })
104
106
105 @staticmethod
107 @staticmethod
106 def parse_tags_string(tag_strings):
108 def parse_tags_string(tag_strings):
107 """
109 """
108 Parses tag list string and returns tag object list.
110 Parses tag list string and returns tag object list.
109 """
111 """
110
112
111 tags = []
113 tags = []
112
114
113 if tag_strings:
115 if tag_strings:
114 tag_strings = tag_strings.split(TAG_DELIMITER)
116 tag_strings = tag_strings.split(TAG_DELIMITER)
115 for tag_name in tag_strings:
117 for tag_name in tag_strings:
116 tag_name = tag_name.strip().lower()
118 tag_name = tag_name.strip().lower()
117 if len(tag_name) > 0:
119 if len(tag_name) > 0:
118 tag, created = Tag.objects.get_or_create(name=tag_name)
120 tag, created = Tag.objects.get_or_create(name=tag_name)
119 tags.append(tag)
121 tags.append(tag)
120
122
121 return tags
123 return tags
122
124
123 @transaction.atomic
125 @transaction.atomic
124 def create_thread(self, request, form: ThreadForm, html_response=True):
126 def create_thread(self, request, form: ThreadForm, html_response=True):
125 """
127 """
126 Creates a new thread with an opening post.
128 Creates a new thread with an opening post.
127 """
129 """
128
130
129 ip = utils.get_client_ip(request)
131 ip = utils.get_client_ip(request)
130 is_banned = Ban.objects.filter(ip=ip).exists()
132 is_banned = Ban.objects.filter(ip=ip).exists()
131
133
132 if is_banned:
134 if is_banned:
133 if html_response:
135 if html_response:
134 return redirect(BannedView().as_view())
136 return redirect(BannedView().as_view())
135 else:
137 else:
136 return
138 return
137
139
138 data = form.cleaned_data
140 data = form.cleaned_data
139
141
140 title = data[FORM_TITLE]
142 title = data[FORM_TITLE]
141 text = data[FORM_TEXT]
143 text = data[FORM_TEXT]
142 image = form.get_image()
144 image = form.get_image()
143 threads = data[FORM_THREADS]
145 threads = data[FORM_THREADS]
144
146
145 text = self._remove_invalid_links(text)
147 text = self._remove_invalid_links(text)
146
148
147 tag_strings = data[FORM_TAGS]
149 tag_strings = data[FORM_TAGS]
148
150
149 tags = self.parse_tags_string(tag_strings)
151 tags = self.parse_tags_string(tag_strings)
150
152
151 post = Post.objects.create_post(title=title, text=text, image=image,
153 post = Post.objects.create_post(title=title, text=text, image=image,
152 ip=ip, tags=tags, threads=threads)
154 ip=ip, tags=tags, threads=threads)
153
155
154 # This is required to update the threads to which posts we have replied
156 # This is required to update the threads to which posts we have replied
155 # when creating this one
157 # when creating this one
156 post.notify_clients()
158 post.notify_clients()
157
159
158 if html_response:
160 if html_response:
159 return redirect(post.get_url())
161 return redirect(post.get_url())
160
162
161 def get_threads(self):
163 def get_threads(self):
162 """
164 """
163 Gets list of threads that will be shown on a page.
165 Gets list of threads that will be shown on a page.
164 """
166 """
165
167
166 return Thread.objects.order_by('-bump_time')\
168 return Thread.objects.order_by('-bump_time')\
167 .exclude(tags__in=self.settings_manager.get_hidden_tags())
169 .exclude(tags__in=self.settings_manager.get_hidden_tags())
@@ -1,76 +1,77 b''
1 from django.db import transaction
1 from django.db import transaction
2 from django.shortcuts import render, redirect
2 from django.shortcuts import render, redirect
3 from django.utils import timezone
3 from django.utils import timezone
4
4
5 from boards.abstracts.settingsmanager import get_settings_manager, \
5 from boards.abstracts.settingsmanager import get_settings_manager, \
6 SETTING_USERNAME, SETTING_LAST_NOTIFICATION_ID, SETTING_IMAGE_VIEWER
6 SETTING_USERNAME, SETTING_LAST_NOTIFICATION_ID, SETTING_IMAGE_VIEWER
7 from boards.middlewares import SESSION_TIMEZONE
7 from boards.middlewares import SESSION_TIMEZONE
8 from boards.views.base import BaseBoardView, CONTEXT_FORM
8 from boards.views.base import BaseBoardView, CONTEXT_FORM
9 from boards.forms import SettingsForm, PlainErrorList
9 from boards.forms import SettingsForm, PlainErrorList
10 from boards import settings
10 from boards import settings
11
11
12
12
13 FORM_THEME = 'theme'
13 FORM_THEME = 'theme'
14 FORM_USERNAME = 'username'
14 FORM_USERNAME = 'username'
15 FORM_TIMEZONE = 'timezone'
15 FORM_TIMEZONE = 'timezone'
16 FORM_IMAGE_VIEWER = 'image_viewer'
16 FORM_IMAGE_VIEWER = 'image_viewer'
17
17
18 CONTEXT_HIDDEN_TAGS = 'hidden_tags'
18 CONTEXT_HIDDEN_TAGS = 'hidden_tags'
19
19
20 TEMPLATE = 'boards/settings.html'
20 TEMPLATE = 'boards/settings.html'
21
21
22
22
23 class SettingsView(BaseBoardView):
23 class SettingsView(BaseBoardView):
24
24
25 def get(self, request):
25 def get(self, request):
26 params = dict()
26 params = dict()
27 settings_manager = get_settings_manager(request)
27 settings_manager = get_settings_manager(request)
28
28
29 selected_theme = settings_manager.get_theme()
29 selected_theme = settings_manager.get_theme()
30
30
31 form = SettingsForm(
31 form = SettingsForm(
32 initial={
32 initial={
33 FORM_THEME: selected_theme,
33 FORM_THEME: selected_theme,
34 FORM_IMAGE_VIEWER: settings_manager.get_setting(
34 FORM_IMAGE_VIEWER: settings_manager.get_setting(
35 SETTING_IMAGE_VIEWER, default=settings.DEFAULT_IMAGE_VIEWER),
35 SETTING_IMAGE_VIEWER,
36 default=settings.get('View', 'DefaultImageViewer')),
36 FORM_USERNAME: settings_manager.get_setting(SETTING_USERNAME),
37 FORM_USERNAME: settings_manager.get_setting(SETTING_USERNAME),
37 FORM_TIMEZONE: request.session.get(
38 FORM_TIMEZONE: request.session.get(
38 SESSION_TIMEZONE, timezone.get_current_timezone()),
39 SESSION_TIMEZONE, timezone.get_current_timezone()),
39 },
40 },
40 error_class=PlainErrorList)
41 error_class=PlainErrorList)
41
42
42 params[CONTEXT_FORM] = form
43 params[CONTEXT_FORM] = form
43 params[CONTEXT_HIDDEN_TAGS] = settings_manager.get_hidden_tags()
44 params[CONTEXT_HIDDEN_TAGS] = settings_manager.get_hidden_tags()
44
45
45 return render(request, TEMPLATE, params)
46 return render(request, TEMPLATE, params)
46
47
47 def post(self, request):
48 def post(self, request):
48 settings_manager = get_settings_manager(request)
49 settings_manager = get_settings_manager(request)
49
50
50 with transaction.atomic():
51 with transaction.atomic():
51 form = SettingsForm(request.POST, error_class=PlainErrorList)
52 form = SettingsForm(request.POST, error_class=PlainErrorList)
52
53
53 if form.is_valid():
54 if form.is_valid():
54 selected_theme = form.cleaned_data[FORM_THEME]
55 selected_theme = form.cleaned_data[FORM_THEME]
55 username = form.cleaned_data[FORM_USERNAME].lower()
56 username = form.cleaned_data[FORM_USERNAME].lower()
56
57
57 settings_manager.set_theme(selected_theme)
58 settings_manager.set_theme(selected_theme)
58 settings_manager.set_setting(SETTING_IMAGE_VIEWER,
59 settings_manager.set_setting(SETTING_IMAGE_VIEWER,
59 form.cleaned_data[FORM_IMAGE_VIEWER])
60 form.cleaned_data[FORM_IMAGE_VIEWER])
60
61
61 old_username = settings_manager.get_setting(SETTING_USERNAME)
62 old_username = settings_manager.get_setting(SETTING_USERNAME)
62 if username != old_username:
63 if username != old_username:
63 settings_manager.set_setting(SETTING_USERNAME, username)
64 settings_manager.set_setting(SETTING_USERNAME, username)
64 settings_manager.set_setting(SETTING_LAST_NOTIFICATION_ID, None)
65 settings_manager.set_setting(SETTING_LAST_NOTIFICATION_ID, None)
65
66
66 request.session[SESSION_TIMEZONE] = form.cleaned_data[FORM_TIMEZONE]
67 request.session[SESSION_TIMEZONE] = form.cleaned_data[FORM_TIMEZONE]
67
68
68 return redirect('settings')
69 return redirect('settings')
69 else:
70 else:
70 params = dict()
71 params = dict()
71
72
72 params[CONTEXT_FORM] = form
73 params[CONTEXT_FORM] = form
73 params[CONTEXT_HIDDEN_TAGS] = settings_manager.get_hidden_tags()
74 params[CONTEXT_HIDDEN_TAGS] = settings_manager.get_hidden_tags()
74
75
75 return render(request, TEMPLATE, params)
76 return render(request, TEMPLATE, params)
76
77
@@ -1,130 +1,130 b''
1 from django.core.exceptions import ObjectDoesNotExist
1 from django.core.exceptions import ObjectDoesNotExist
2 from django.http import Http404
2 from django.http import Http404
3 from django.shortcuts import get_object_or_404, render, redirect
3 from django.shortcuts import get_object_or_404, render, redirect
4 from django.views.generic.edit import FormMixin
4 from django.views.generic.edit import FormMixin
5 from django.utils import timezone
5 from django.utils import timezone
6 from django.utils.dateformat import format
6 from django.utils.dateformat import format
7
7
8 from boards import utils, settings
8 from boards import utils, settings
9 from boards.forms import PostForm, PlainErrorList
9 from boards.forms import PostForm, PlainErrorList
10 from boards.models import Post
10 from boards.models import Post
11 from boards.views.base import BaseBoardView, CONTEXT_FORM
11 from boards.views.base import BaseBoardView, CONTEXT_FORM
12 from boards.views.posting_mixin import PostMixin
12 from boards.views.posting_mixin import PostMixin
13
13
14 import neboard
14 import neboard
15
15
16
16
17 CONTEXT_LASTUPDATE = "last_update"
17 CONTEXT_LASTUPDATE = "last_update"
18 CONTEXT_THREAD = 'thread'
18 CONTEXT_THREAD = 'thread'
19 CONTEXT_WS_TOKEN = 'ws_token'
19 CONTEXT_WS_TOKEN = 'ws_token'
20 CONTEXT_WS_PROJECT = 'ws_project'
20 CONTEXT_WS_PROJECT = 'ws_project'
21 CONTEXT_WS_HOST = 'ws_host'
21 CONTEXT_WS_HOST = 'ws_host'
22 CONTEXT_WS_PORT = 'ws_port'
22 CONTEXT_WS_PORT = 'ws_port'
23 CONTEXT_WS_TIME = 'ws_token_time'
23 CONTEXT_WS_TIME = 'ws_token_time'
24
24
25 FORM_TITLE = 'title'
25 FORM_TITLE = 'title'
26 FORM_TEXT = 'text'
26 FORM_TEXT = 'text'
27 FORM_IMAGE = 'image'
27 FORM_IMAGE = 'image'
28 FORM_THREADS = 'threads'
28 FORM_THREADS = 'threads'
29
29
30
30
31 class ThreadView(BaseBoardView, PostMixin, FormMixin):
31 class ThreadView(BaseBoardView, PostMixin, FormMixin):
32
32
33 def get(self, request, post_id, form: PostForm=None):
33 def get(self, request, post_id, form: PostForm=None):
34 try:
34 try:
35 opening_post = Post.objects.get(id=post_id)
35 opening_post = Post.objects.get(id=post_id)
36 except ObjectDoesNotExist:
36 except ObjectDoesNotExist:
37 raise Http404
37 raise Http404
38
38
39 # If this is not OP, don't show it as it is
39 # If this is not OP, don't show it as it is
40 if not opening_post.is_opening():
40 if not opening_post.is_opening():
41 return redirect(opening_post.get_thread().get_opening_post().get_url())
41 return redirect(opening_post.get_thread().get_opening_post().get_url())
42
42
43 if not form:
43 if not form:
44 form = PostForm(error_class=PlainErrorList)
44 form = PostForm(error_class=PlainErrorList)
45
45
46 thread_to_show = opening_post.get_thread()
46 thread_to_show = opening_post.get_thread()
47
47
48 params = dict()
48 params = dict()
49
49
50 params[CONTEXT_FORM] = form
50 params[CONTEXT_FORM] = form
51 params[CONTEXT_LASTUPDATE] = str(thread_to_show.last_edit_time)
51 params[CONTEXT_LASTUPDATE] = str(thread_to_show.last_edit_time)
52 params[CONTEXT_THREAD] = thread_to_show
52 params[CONTEXT_THREAD] = thread_to_show
53
53
54 if settings.WEBSOCKETS_ENABLED:
54 if settings.get_bool('External', 'WebsocketsEnabled'):
55 token_time = format(timezone.now(), u'U')
55 token_time = format(timezone.now(), u'U')
56
56
57 params[CONTEXT_WS_TIME] = token_time
57 params[CONTEXT_WS_TIME] = token_time
58 params[CONTEXT_WS_TOKEN] = utils.get_websocket_token(
58 params[CONTEXT_WS_TOKEN] = utils.get_websocket_token(
59 timestamp=token_time)
59 timestamp=token_time)
60 params[CONTEXT_WS_PROJECT] = neboard.settings.CENTRIFUGE_PROJECT_ID
60 params[CONTEXT_WS_PROJECT] = neboard.settings.CENTRIFUGE_PROJECT_ID
61 params[CONTEXT_WS_HOST] = request.get_host().split(':')[0]
61 params[CONTEXT_WS_HOST] = request.get_host().split(':')[0]
62 params[CONTEXT_WS_PORT] = neboard.settings.CENTRIFUGE_PORT
62 params[CONTEXT_WS_PORT] = neboard.settings.CENTRIFUGE_PORT
63
63
64 params.update(self.get_data(thread_to_show))
64 params.update(self.get_data(thread_to_show))
65
65
66 return render(request, self.get_template(), params)
66 return render(request, self.get_template(), params)
67
67
68 def post(self, request, post_id):
68 def post(self, request, post_id):
69 opening_post = get_object_or_404(Post, id=post_id)
69 opening_post = get_object_or_404(Post, id=post_id)
70
70
71 # If this is not OP, don't show it as it is
71 # If this is not OP, don't show it as it is
72 if not opening_post.is_opening():
72 if not opening_post.is_opening():
73 raise Http404
73 raise Http404
74
74
75 if not opening_post.get_thread().archived:
75 if not opening_post.get_thread().archived:
76 form = PostForm(request.POST, request.FILES,
76 form = PostForm(request.POST, request.FILES,
77 error_class=PlainErrorList)
77 error_class=PlainErrorList)
78 form.session = request.session
78 form.session = request.session
79
79
80 if form.is_valid():
80 if form.is_valid():
81 return self.new_post(request, form, opening_post)
81 return self.new_post(request, form, opening_post)
82 if form.need_to_ban:
82 if form.need_to_ban:
83 # Ban user because he is suspected to be a bot
83 # Ban user because he is suspected to be a bot
84 self._ban_current_user(request)
84 self._ban_current_user(request)
85
85
86 return self.get(request, post_id, form)
86 return self.get(request, post_id, form)
87
87
88 def new_post(self, request, form: PostForm, opening_post: Post=None,
88 def new_post(self, request, form: PostForm, opening_post: Post=None,
89 html_response=True):
89 html_response=True):
90 """
90 """
91 Adds a new post (in thread or as a reply).
91 Adds a new post (in thread or as a reply).
92 """
92 """
93
93
94 ip = utils.get_client_ip(request)
94 ip = utils.get_client_ip(request)
95
95
96 data = form.cleaned_data
96 data = form.cleaned_data
97
97
98 title = data[FORM_TITLE]
98 title = data[FORM_TITLE]
99 text = data[FORM_TEXT]
99 text = data[FORM_TEXT]
100 image = form.get_image()
100 image = form.get_image()
101 threads = data[FORM_THREADS]
101 threads = data[FORM_THREADS]
102
102
103 text = self._remove_invalid_links(text)
103 text = self._remove_invalid_links(text)
104
104
105 post_thread = opening_post.get_thread()
105 post_thread = opening_post.get_thread()
106
106
107 post = Post.objects.create_post(title=title, text=text, image=image,
107 post = Post.objects.create_post(title=title, text=text, image=image,
108 thread=post_thread, ip=ip,
108 thread=post_thread, ip=ip,
109 threads=threads)
109 threads=threads)
110 post.notify_clients()
110 post.notify_clients()
111
111
112 if html_response:
112 if html_response:
113 if opening_post:
113 if opening_post:
114 return redirect(post.get_url())
114 return redirect(post.get_url())
115 else:
115 else:
116 return post
116 return post
117
117
118 def get_data(self, thread):
118 def get_data(self, thread):
119 """
119 """
120 Returns context params for the view.
120 Returns context params for the view.
121 """
121 """
122
122
123 pass
123 pass
124
124
125 def get_template(self):
125 def get_template(self):
126 """
126 """
127 Gets template to show the thread mode on.
127 Gets template to show the thread mode on.
128 """
128 """
129
129
130 pass
130 pass
@@ -1,240 +1,240 b''
1 # Django settings for neboard project.
1 # Django settings for neboard project.
2 import os
2 import os
3
3
4 DEBUG = True
4 DEBUG = True
5 TEMPLATE_DEBUG = DEBUG
5 TEMPLATE_DEBUG = DEBUG
6
6
7 ADMINS = (
7 ADMINS = (
8 # ('Your Name', 'your_email@example.com'),
8 # ('Your Name', 'your_email@example.com'),
9 ('admin', 'admin@example.com')
9 ('admin', 'admin@example.com')
10 )
10 )
11
11
12 MANAGERS = ADMINS
12 MANAGERS = ADMINS
13
13
14 DATABASES = {
14 DATABASES = {
15 'default': {
15 'default': {
16 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
16 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
17 'NAME': 'database.db', # Or path to database file if using sqlite3.
17 'NAME': 'database.db', # Or path to database file if using sqlite3.
18 'USER': '', # Not used with sqlite3.
18 'USER': '', # Not used with sqlite3.
19 'PASSWORD': '', # Not used with sqlite3.
19 'PASSWORD': '', # Not used with sqlite3.
20 'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
20 'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
21 'PORT': '', # Set to empty string for default. Not used with sqlite3.
21 'PORT': '', # Set to empty string for default. Not used with sqlite3.
22 'CONN_MAX_AGE': None,
22 'CONN_MAX_AGE': None,
23 }
23 }
24 }
24 }
25
25
26 # Local time zone for this installation. Choices can be found here:
26 # Local time zone for this installation. Choices can be found here:
27 # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
27 # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
28 # although not all choices may be available on all operating systems.
28 # although not all choices may be available on all operating systems.
29 # In a Windows environment this must be set to your system time zone.
29 # In a Windows environment this must be set to your system time zone.
30 TIME_ZONE = 'Europe/Kiev'
30 TIME_ZONE = 'Europe/Kiev'
31
31
32 # Language code for this installation. All choices can be found here:
32 # Language code for this installation. All choices can be found here:
33 # http://www.i18nguy.com/unicode/language-identifiers.html
33 # http://www.i18nguy.com/unicode/language-identifiers.html
34 LANGUAGE_CODE = 'en'
34 LANGUAGE_CODE = 'en'
35
35
36 SITE_ID = 1
36 SITE_ID = 1
37
37
38 # If you set this to False, Django will make some optimizations so as not
38 # If you set this to False, Django will make some optimizations so as not
39 # to load the internationalization machinery.
39 # to load the internationalization machinery.
40 USE_I18N = True
40 USE_I18N = True
41
41
42 # If you set this to False, Django will not format dates, numbers and
42 # If you set this to False, Django will not format dates, numbers and
43 # calendars according to the current locale.
43 # calendars according to the current locale.
44 USE_L10N = True
44 USE_L10N = True
45
45
46 # If you set this to False, Django will not use timezone-aware datetimes.
46 # If you set this to False, Django will not use timezone-aware datetimes.
47 USE_TZ = True
47 USE_TZ = True
48
48
49 USE_ETAGS = True
49 USE_ETAGS = True
50
50
51 # Absolute filesystem path to the directory that will hold user-uploaded files.
51 # Absolute filesystem path to the directory that will hold user-uploaded files.
52 # Example: "/home/media/media.lawrence.com/media/"
52 # Example: "/home/media/media.lawrence.com/media/"
53 MEDIA_ROOT = './media/'
53 MEDIA_ROOT = './media/'
54
54
55 # URL that handles the media served from MEDIA_ROOT. Make sure to use a
55 # URL that handles the media served from MEDIA_ROOT. Make sure to use a
56 # trailing slash.
56 # trailing slash.
57 # Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
57 # Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
58 MEDIA_URL = '/media/'
58 MEDIA_URL = '/media/'
59
59
60 # Absolute path to the directory static files should be collected to.
60 # Absolute path to the directory static files should be collected to.
61 # Don't put anything in this directory yourself; store your static files
61 # Don't put anything in this directory yourself; store your static files
62 # in apps' "static/" subdirectories and in STATICFILES_DIRS.
62 # in apps' "static/" subdirectories and in STATICFILES_DIRS.
63 # Example: "/home/media/media.lawrence.com/static/"
63 # Example: "/home/media/media.lawrence.com/static/"
64 STATIC_ROOT = ''
64 STATIC_ROOT = ''
65
65
66 # URL prefix for static files.
66 # URL prefix for static files.
67 # Example: "http://media.lawrence.com/static/"
67 # Example: "http://media.lawrence.com/static/"
68 STATIC_URL = '/static/'
68 STATIC_URL = '/static/'
69
69
70 # Additional locations of static files
70 # Additional locations of static files
71 # It is really a hack, put real paths, not related
71 # It is really a hack, put real paths, not related
72 STATICFILES_DIRS = (
72 STATICFILES_DIRS = (
73 os.path.dirname(__file__) + '/boards/static',
73 os.path.dirname(__file__) + '/boards/static',
74
74
75 # '/d/work/python/django/neboard/neboard/boards/static',
75 # '/d/work/python/django/neboard/neboard/boards/static',
76 # Put strings here, like "/home/html/static" or "C:/www/django/static".
76 # Put strings here, like "/home/html/static" or "C:/www/django/static".
77 # Always use forward slashes, even on Windows.
77 # Always use forward slashes, even on Windows.
78 # Don't forget to use absolute paths, not relative paths.
78 # Don't forget to use absolute paths, not relative paths.
79 )
79 )
80
80
81 # List of finder classes that know how to find static files in
81 # List of finder classes that know how to find static files in
82 # various locations.
82 # various locations.
83 STATICFILES_FINDERS = (
83 STATICFILES_FINDERS = (
84 'django.contrib.staticfiles.finders.FileSystemFinder',
84 'django.contrib.staticfiles.finders.FileSystemFinder',
85 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
85 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
86 'compressor.finders.CompressorFinder',
86 'compressor.finders.CompressorFinder',
87 )
87 )
88
88
89 if DEBUG:
89 if DEBUG:
90 STATICFILES_STORAGE = \
90 STATICFILES_STORAGE = \
91 'django.contrib.staticfiles.storage.StaticFilesStorage'
91 'django.contrib.staticfiles.storage.StaticFilesStorage'
92 else:
92 else:
93 STATICFILES_STORAGE = \
93 STATICFILES_STORAGE = \
94 'django.contrib.staticfiles.storage.CachedStaticFilesStorage'
94 'django.contrib.staticfiles.storage.CachedStaticFilesStorage'
95
95
96 # Make this unique, and don't share it with anybody.
96 # Make this unique, and don't share it with anybody.
97 SECRET_KEY = '@1rc$o(7=tt#kd+4s$u6wchm**z^)4x90)7f6z(i&amp;55@o11*8o'
97 SECRET_KEY = '@1rc$o(7=tt#kd+4s$u6wchm**z^)4x90)7f6z(i&amp;55@o11*8o'
98
98
99 # List of callables that know how to import templates from various sources.
99 # List of callables that know how to import templates from various sources.
100 TEMPLATE_LOADERS = (
100 TEMPLATE_LOADERS = (
101 'django.template.loaders.filesystem.Loader',
101 'django.template.loaders.filesystem.Loader',
102 'django.template.loaders.app_directories.Loader',
102 'django.template.loaders.app_directories.Loader',
103 )
103 )
104
104
105 TEMPLATE_CONTEXT_PROCESSORS = (
105 TEMPLATE_CONTEXT_PROCESSORS = (
106 'django.core.context_processors.media',
106 'django.core.context_processors.media',
107 'django.core.context_processors.static',
107 'django.core.context_processors.static',
108 'django.core.context_processors.request',
108 'django.core.context_processors.request',
109 'django.contrib.auth.context_processors.auth',
109 'django.contrib.auth.context_processors.auth',
110 'boards.context_processors.user_and_ui_processor',
110 'boards.context_processors.user_and_ui_processor',
111 )
111 )
112
112
113 MIDDLEWARE_CLASSES = (
113 MIDDLEWARE_CLASSES = (
114 'django.middleware.http.ConditionalGetMiddleware',
114 'django.middleware.http.ConditionalGetMiddleware',
115 'django.contrib.sessions.middleware.SessionMiddleware',
115 'django.contrib.sessions.middleware.SessionMiddleware',
116 'django.middleware.locale.LocaleMiddleware',
116 'django.middleware.locale.LocaleMiddleware',
117 'django.middleware.common.CommonMiddleware',
117 'django.middleware.common.CommonMiddleware',
118 'django.contrib.auth.middleware.AuthenticationMiddleware',
118 'django.contrib.auth.middleware.AuthenticationMiddleware',
119 'django.contrib.messages.middleware.MessageMiddleware',
119 'django.contrib.messages.middleware.MessageMiddleware',
120 'boards.middlewares.BanMiddleware',
120 'boards.middlewares.BanMiddleware',
121 'boards.middlewares.TimezoneMiddleware',
121 'boards.middlewares.TimezoneMiddleware',
122 )
122 )
123
123
124 ROOT_URLCONF = 'neboard.urls'
124 ROOT_URLCONF = 'neboard.urls'
125
125
126 # Python dotted path to the WSGI application used by Django's runserver.
126 # Python dotted path to the WSGI application used by Django's runserver.
127 WSGI_APPLICATION = 'neboard.wsgi.application'
127 WSGI_APPLICATION = 'neboard.wsgi.application'
128
128
129 TEMPLATE_DIRS = (
129 TEMPLATE_DIRS = (
130 # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
130 # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
131 # Always use forward slashes, even on Windows.
131 # Always use forward slashes, even on Windows.
132 # Don't forget to use absolute paths, not relative paths.
132 # Don't forget to use absolute paths, not relative paths.
133 'templates',
133 'templates',
134 )
134 )
135
135
136 INSTALLED_APPS = (
136 INSTALLED_APPS = (
137 'django.contrib.auth',
137 'django.contrib.auth',
138 'django.contrib.contenttypes',
138 'django.contrib.contenttypes',
139 'django.contrib.sessions',
139 'django.contrib.sessions',
140 # 'django.contrib.sites',
140 # 'django.contrib.sites',
141 'django.contrib.messages',
141 'django.contrib.messages',
142 'django.contrib.staticfiles',
142 'django.contrib.staticfiles',
143 # Uncomment the next line to enable the admin:
143 # Uncomment the next line to enable the admin:
144 'django.contrib.admin',
144 'django.contrib.admin',
145 # Uncomment the next line to enable admin documentation:
145 # Uncomment the next line to enable admin documentation:
146 # 'django.contrib.admindocs',
146 # 'django.contrib.admindocs',
147 'django.contrib.humanize',
147 #'django.contrib.humanize',
148
148
149 'debug_toolbar',
149 'debug_toolbar',
150
150
151 # Search
151 # Search
152 'haystack',
152 'haystack',
153
153
154 'boards',
154 'boards',
155 )
155 )
156
156
157 # A sample logging configuration. The only tangible logging
157 # A sample logging configuration. The only tangible logging
158 # performed by this configuration is to send an email to
158 # performed by this configuration is to send an email to
159 # the site admins on every HTTP 500 error when DEBUG=False.
159 # the site admins on every HTTP 500 error when DEBUG=False.
160 # See http://docs.djangoproject.com/en/dev/topics/logging for
160 # See http://docs.djangoproject.com/en/dev/topics/logging for
161 # more details on how to customize your logging configuration.
161 # more details on how to customize your logging configuration.
162 LOGGING = {
162 LOGGING = {
163 'version': 1,
163 'version': 1,
164 'disable_existing_loggers': False,
164 'disable_existing_loggers': False,
165 'formatters': {
165 'formatters': {
166 'verbose': {
166 'verbose': {
167 'format': '%(levelname)s %(asctime)s %(name)s %(process)d %(thread)d %(message)s'
167 'format': '%(levelname)s %(asctime)s %(name)s %(process)d %(thread)d %(message)s'
168 },
168 },
169 'simple': {
169 'simple': {
170 'format': '%(levelname)s %(asctime)s [%(name)s] %(message)s'
170 'format': '%(levelname)s %(asctime)s [%(name)s] %(message)s'
171 },
171 },
172 },
172 },
173 'filters': {
173 'filters': {
174 'require_debug_false': {
174 'require_debug_false': {
175 '()': 'django.utils.log.RequireDebugFalse'
175 '()': 'django.utils.log.RequireDebugFalse'
176 }
176 }
177 },
177 },
178 'handlers': {
178 'handlers': {
179 'console': {
179 'console': {
180 'level': 'DEBUG',
180 'level': 'DEBUG',
181 'class': 'logging.StreamHandler',
181 'class': 'logging.StreamHandler',
182 'formatter': 'simple'
182 'formatter': 'simple'
183 },
183 },
184 },
184 },
185 'loggers': {
185 'loggers': {
186 'boards': {
186 'boards': {
187 'handlers': ['console'],
187 'handlers': ['console'],
188 'level': 'DEBUG',
188 'level': 'DEBUG',
189 }
189 }
190 },
190 },
191 }
191 }
192
192
193 HAYSTACK_CONNECTIONS = {
193 HAYSTACK_CONNECTIONS = {
194 'default': {
194 'default': {
195 'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine',
195 'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine',
196 'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'),
196 'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'),
197 },
197 },
198 }
198 }
199
199
200 THEMES = [
200 THEMES = [
201 ('md', 'Mystic Dark'),
201 ('md', 'Mystic Dark'),
202 ('md_centered', 'Mystic Dark (centered)'),
202 ('md_centered', 'Mystic Dark (centered)'),
203 ('sw', 'Snow White'),
203 ('sw', 'Snow White'),
204 ('pg', 'Photon Gray'),
204 ('pg', 'Photon Gray'),
205 ]
205 ]
206
206
207 IMAGE_VIEWERS = [
207 IMAGE_VIEWERS = [
208 ('simple', 'Simple'),
208 ('simple', 'Simple'),
209 ('popup', 'Popup'),
209 ('popup', 'Popup'),
210 ]
210 ]
211
211
212 POSTING_DELAY = 20 # seconds
212 POSTING_DELAY = 20 # seconds
213
213
214 # Websocket settins
214 # Websocket settins
215 CENTRIFUGE_HOST = 'localhost'
215 CENTRIFUGE_HOST = 'localhost'
216 CENTRIFUGE_PORT = '9090'
216 CENTRIFUGE_PORT = '9090'
217
217
218 CENTRIFUGE_ADDRESS = 'http://{}:{}'.format(CENTRIFUGE_HOST, CENTRIFUGE_PORT)
218 CENTRIFUGE_ADDRESS = 'http://{}:{}'.format(CENTRIFUGE_HOST, CENTRIFUGE_PORT)
219 CENTRIFUGE_PROJECT_ID = '<project id here>'
219 CENTRIFUGE_PROJECT_ID = '<project id here>'
220 CENTRIFUGE_PROJECT_SECRET = '<project secret here>'
220 CENTRIFUGE_PROJECT_SECRET = '<project secret here>'
221 CENTRIFUGE_TIMEOUT = 5
221 CENTRIFUGE_TIMEOUT = 5
222
222
223 # Debug mode middlewares
223 # Debug mode middlewares
224 if DEBUG:
224 if DEBUG:
225 MIDDLEWARE_CLASSES += (
225 MIDDLEWARE_CLASSES += (
226 'debug_toolbar.middleware.DebugToolbarMiddleware',
226 'debug_toolbar.middleware.DebugToolbarMiddleware',
227 )
227 )
228
228
229 def custom_show_toolbar(request):
229 def custom_show_toolbar(request):
230 return True
230 return True
231
231
232 DEBUG_TOOLBAR_CONFIG = {
232 DEBUG_TOOLBAR_CONFIG = {
233 'ENABLE_STACKTRACES': True,
233 'ENABLE_STACKTRACES': True,
234 'SHOW_TOOLBAR_CALLBACK': 'neboard.settings.custom_show_toolbar',
234 'SHOW_TOOLBAR_CALLBACK': 'neboard.settings.custom_show_toolbar',
235 }
235 }
236
236
237 # FIXME Uncommenting this fails somehow. Need to investigate this
237 # FIXME Uncommenting this fails somehow. Need to investigate this
238 #DEBUG_TOOLBAR_PANELS += (
238 #DEBUG_TOOLBAR_PANELS += (
239 # 'debug_toolbar.panels.profiling.ProfilingDebugPanel',
239 # 'debug_toolbar.panels.profiling.ProfilingDebugPanel',
240 #)
240 #)
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