Show More
@@ -1,27 +1,28 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 |
@@ -1,65 +1,68 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 |
|
2 | SETTING_USERNAME, SETTING_LAST_NOTIFICATION_ID | |
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 |
|
7 | from boards import settings | |
8 | from boards.models import Post |
|
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 |
|
21 | |||
21 | PERMISSION_MODERATE = 'moderation' |
|
22 | PERMISSION_MODERATE = 'moderation' | |
22 |
|
23 | |||
23 |
|
24 | |||
24 | def get_notifications(context, request): |
|
25 | def get_notifications(context, request): | |
25 | settings_manager = get_settings_manager(request) |
|
26 | settings_manager = get_settings_manager(request) | |
26 | username = settings_manager.get_setting(SETTING_USERNAME) |
|
27 | username = settings_manager.get_setting(SETTING_USERNAME) | |
27 | new_notifications_count = 0 |
|
28 | new_notifications_count = 0 | |
28 | if username is not None and len(username) > 0: |
|
29 | if username is not None and len(username) > 0: | |
29 | last_notification_id = settings_manager.get_setting( |
|
30 | last_notification_id = settings_manager.get_setting( | |
30 | SETTING_LAST_NOTIFICATION_ID) |
|
31 | SETTING_LAST_NOTIFICATION_ID) | |
31 |
|
32 | |||
32 | new_notifications_count = Notification.objects.get_notification_posts( |
|
33 | new_notifications_count = Notification.objects.get_notification_posts( | |
33 | username=username, last=last_notification_id).count() |
|
34 | username=username, last=last_notification_id).count() | |
34 | context[CONTEXT_NEW_NOTIFICATIONS_COUNT] = new_notifications_count |
|
35 | context[CONTEXT_NEW_NOTIFICATIONS_COUNT] = new_notifications_count | |
35 | context[CONTEXT_USERNAME] = username |
|
36 | context[CONTEXT_USERNAME] = username | |
36 |
|
37 | |||
37 |
|
38 | |||
38 | def get_moderator_permissions(context, request): |
|
39 | def get_moderator_permissions(context, request): | |
39 | try: |
|
40 | try: | |
40 | moderate = request.user.has_perm(PERMISSION_MODERATE) |
|
41 | moderate = request.user.has_perm(PERMISSION_MODERATE) | |
41 | except AttributeError: |
|
42 | except AttributeError: | |
42 | moderate = False |
|
43 | moderate = False | |
43 | context[CONTEXT_MODERATOR] = moderate |
|
44 | context[CONTEXT_MODERATOR] = moderate | |
44 |
|
45 | |||
45 |
|
46 | |||
46 | def user_and_ui_processor(request): |
|
47 | def user_and_ui_processor(request): | |
47 | context = dict() |
|
48 | context = dict() | |
48 |
|
49 | |||
49 | context[CONTEXT_PPD] = float(Post.objects.get_posts_per_day()) |
|
50 | context[CONTEXT_PPD] = float(Post.objects.get_posts_per_day()) | |
50 |
|
51 | |||
51 | settings_manager = get_settings_manager(request) |
|
52 | settings_manager = get_settings_manager(request) | |
52 |
|
|
53 | fav_tags = settings_manager.get_fav_tags() | |
|
54 | context[CONTEXT_TAGS] = fav_tags | |||
|
55 | context[CONTEXT_TAGS_STR] = Tag.objects.get_tag_url_list(fav_tags) | |||
53 | theme = settings_manager.get_theme() |
|
56 | theme = settings_manager.get_theme() | |
54 | context[CONTEXT_THEME] = theme |
|
57 | context[CONTEXT_THEME] = theme | |
55 | context[CONTEXT_THEME_CSS] = 'css/' + theme + '/base_page.css' |
|
58 | context[CONTEXT_THEME_CSS] = 'css/' + theme + '/base_page.css' | |
56 |
|
59 | |||
57 | # This shows the moderator panel |
|
60 | # This shows the moderator panel | |
58 | get_moderator_permissions(context, request) |
|
61 | get_moderator_permissions(context, request) | |
59 |
|
62 | |||
60 | context[CONTEXT_VERSION] = settings.VERSION |
|
63 | context[CONTEXT_VERSION] = settings.VERSION | |
61 | context[CONTEXT_SITE_NAME] = settings.SITE_NAME |
|
64 | context[CONTEXT_SITE_NAME] = settings.SITE_NAME | |
62 |
|
65 | |||
63 | get_notifications(context, request) |
|
66 | get_notifications(context, request) | |
64 |
|
67 | |||
65 | return context |
|
68 | return context |
@@ -1,22 +1,22 b'' | |||||
1 |
VERSION = '2.5. |
|
1 | VERSION = '2.5.2 Yasako' | |
2 | SITE_NAME = 'Neboard' |
|
2 | SITE_NAME = 'Neboard' | |
3 |
|
3 | |||
4 | CACHE_TIMEOUT = 600 # Timeout for caching, if cache is used |
|
4 | CACHE_TIMEOUT = 600 # Timeout for caching, if cache is used | |
5 | LOGIN_TIMEOUT = 3600 # Timeout between login tries |
|
5 | LOGIN_TIMEOUT = 3600 # Timeout between login tries | |
6 | MAX_TEXT_LENGTH = 30000 # Max post length in characters |
|
6 | MAX_TEXT_LENGTH = 30000 # Max post length in characters | |
7 | MAX_IMAGE_SIZE = 8 * 1024 * 1024 # Max image size |
|
7 | MAX_IMAGE_SIZE = 8 * 1024 * 1024 # Max image size | |
8 |
|
8 | |||
9 | # Thread bumplimit |
|
9 | # Thread bumplimit | |
10 | MAX_POSTS_PER_THREAD = 10 |
|
10 | MAX_POSTS_PER_THREAD = 10 | |
11 | # Old posts will be archived or deleted if this value is reached |
|
11 | # Old posts will be archived or deleted if this value is reached | |
12 | MAX_THREAD_COUNT = 5 |
|
12 | MAX_THREAD_COUNT = 5 | |
13 | THREADS_PER_PAGE = 3 |
|
13 | THREADS_PER_PAGE = 3 | |
14 | DEFAULT_THEME = 'md' |
|
14 | DEFAULT_THEME = 'md' | |
15 | LAST_REPLIES_COUNT = 3 |
|
15 | LAST_REPLIES_COUNT = 3 | |
16 |
|
16 | |||
17 | # Enable archiving threads instead of deletion when the thread limit is reached |
|
17 | # Enable archiving threads instead of deletion when the thread limit is reached | |
18 | ARCHIVE_THREADS = True |
|
18 | ARCHIVE_THREADS = True | |
19 | # Limit posting speed |
|
19 | # Limit posting speed | |
20 | LIMIT_POSTING_SPEED = False |
|
20 | LIMIT_POSTING_SPEED = False | |
21 | # Thread update |
|
21 | # Thread update | |
22 | WEBSOCKETS_ENABLED = True |
|
22 | WEBSOCKETS_ENABLED = True |
@@ -1,346 +1,338 b'' | |||||
1 | import re |
|
1 | import re | |
2 | import time |
|
2 | import time | |
3 |
|
3 | |||
4 | from django import forms |
|
4 | from django import forms | |
5 | from django.core.files.uploadedfile import SimpleUploadedFile |
|
5 | from django.core.files.uploadedfile import SimpleUploadedFile | |
6 | from django.forms.util import ErrorList |
|
6 | from django.forms.util import ErrorList | |
7 | from django.utils.translation import ugettext_lazy as _ |
|
7 | from django.utils.translation import ugettext_lazy as _ | |
8 | import requests |
|
8 | import requests | |
9 |
|
9 | |||
10 | from boards.mdx_neboard import formatters |
|
10 | from boards.mdx_neboard import formatters | |
11 | from boards.models.post import TITLE_MAX_LENGTH |
|
11 | from boards.models.post import TITLE_MAX_LENGTH | |
12 | from boards.models import Tag |
|
12 | from boards.models import Tag | |
13 | from neboard import settings |
|
13 | from neboard import settings | |
14 | import boards.settings as board_settings |
|
14 | import boards.settings as board_settings | |
15 |
|
15 | |||
16 |
|
16 | |||
17 | CONTENT_TYPE_IMAGE = ( |
|
17 | CONTENT_TYPE_IMAGE = ( | |
18 | 'image/jpeg', |
|
18 | 'image/jpeg', | |
19 | 'image/png', |
|
19 | 'image/png', | |
20 | 'image/gif', |
|
20 | 'image/gif', | |
21 | 'image/bmp', |
|
21 | 'image/bmp', | |
22 | ) |
|
22 | ) | |
23 |
|
23 | |||
24 | REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE) |
|
24 | REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE) | |
25 |
|
25 | |||
26 | VETERAN_POSTING_DELAY = 5 |
|
26 | VETERAN_POSTING_DELAY = 5 | |
27 |
|
27 | |||
28 | ATTRIBUTE_PLACEHOLDER = 'placeholder' |
|
28 | ATTRIBUTE_PLACEHOLDER = 'placeholder' | |
29 | ATTRIBUTE_ROWS = 'rows' |
|
29 | ATTRIBUTE_ROWS = 'rows' | |
30 |
|
30 | |||
31 | LAST_POST_TIME = 'last_post_time' |
|
31 | LAST_POST_TIME = 'last_post_time' | |
32 | LAST_LOGIN_TIME = 'last_login_time' |
|
32 | LAST_LOGIN_TIME = 'last_login_time' | |
33 | TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.') |
|
33 | TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.') | |
34 | TAGS_PLACEHOLDER = _('tag1 several_words_tag') |
|
34 | TAGS_PLACEHOLDER = _('tag1 several_words_tag') | |
35 |
|
35 | |||
36 | LABEL_TITLE = _('Title') |
|
36 | LABEL_TITLE = _('Title') | |
37 | LABEL_TEXT = _('Text') |
|
37 | LABEL_TEXT = _('Text') | |
38 | LABEL_TAG = _('Tag') |
|
38 | LABEL_TAG = _('Tag') | |
39 | LABEL_SEARCH = _('Search') |
|
39 | LABEL_SEARCH = _('Search') | |
40 |
|
40 | |||
41 | TAG_MAX_LENGTH = 20 |
|
41 | TAG_MAX_LENGTH = 20 | |
42 |
|
42 | |||
43 | IMAGE_DOWNLOAD_CHUNK_BYTES = 100000 |
|
43 | IMAGE_DOWNLOAD_CHUNK_BYTES = 100000 | |
44 |
|
44 | |||
45 | HTTP_RESULT_OK = 200 |
|
45 | HTTP_RESULT_OK = 200 | |
46 |
|
46 | |||
47 | TEXTAREA_ROWS = 4 |
|
47 | TEXTAREA_ROWS = 4 | |
48 |
|
48 | |||
49 |
|
49 | |||
50 | class FormatPanel(forms.Textarea): |
|
50 | class FormatPanel(forms.Textarea): | |
51 | """ |
|
51 | """ | |
52 | Panel for text formatting. Consists of buttons to add different tags to the |
|
52 | Panel for text formatting. Consists of buttons to add different tags to the | |
53 | form text area. |
|
53 | form text area. | |
54 | """ |
|
54 | """ | |
55 |
|
55 | |||
56 | def render(self, name, value, attrs=None): |
|
56 | def render(self, name, value, attrs=None): | |
57 | output = '<div id="mark-panel">' |
|
57 | output = '<div id="mark-panel">' | |
58 | for formatter in formatters: |
|
58 | for formatter in formatters: | |
59 | output += '<span class="mark_btn"' + \ |
|
59 | output += '<span class="mark_btn"' + \ | |
60 | ' onClick="addMarkToMsg(\'' + formatter.format_left + \ |
|
60 | ' onClick="addMarkToMsg(\'' + formatter.format_left + \ | |
61 | '\', \'' + formatter.format_right + '\')">' + \ |
|
61 | '\', \'' + formatter.format_right + '\')">' + \ | |
62 | formatter.preview_left + formatter.name + \ |
|
62 | formatter.preview_left + formatter.name + \ | |
63 | formatter.preview_right + '</span>' |
|
63 | formatter.preview_right + '</span>' | |
64 |
|
64 | |||
65 | output += '</div>' |
|
65 | output += '</div>' | |
66 | output += super(FormatPanel, self).render(name, value, attrs=None) |
|
66 | output += super(FormatPanel, self).render(name, value, attrs=None) | |
67 |
|
67 | |||
68 | return output |
|
68 | return output | |
69 |
|
69 | |||
70 |
|
70 | |||
71 | class PlainErrorList(ErrorList): |
|
71 | class PlainErrorList(ErrorList): | |
72 | def __unicode__(self): |
|
72 | def __unicode__(self): | |
73 | return self.as_text() |
|
73 | return self.as_text() | |
74 |
|
74 | |||
75 | def as_text(self): |
|
75 | def as_text(self): | |
76 | return ''.join(['(!) %s ' % e for e in self]) |
|
76 | return ''.join(['(!) %s ' % e for e in self]) | |
77 |
|
77 | |||
78 |
|
78 | |||
79 | class NeboardForm(forms.Form): |
|
79 | class NeboardForm(forms.Form): | |
80 | """ |
|
80 | """ | |
81 | Form with neboard-specific formatting. |
|
81 | Form with neboard-specific formatting. | |
82 | """ |
|
82 | """ | |
83 |
|
83 | |||
84 | def as_div(self): |
|
84 | def as_div(self): | |
85 | """ |
|
85 | """ | |
86 | Returns this form rendered as HTML <as_div>s. |
|
86 | Returns this form rendered as HTML <as_div>s. | |
87 | """ |
|
87 | """ | |
88 |
|
88 | |||
89 | return self._html_output( |
|
89 | return self._html_output( | |
90 | # TODO Do not show hidden rows in the list here |
|
90 | # TODO Do not show hidden rows in the list here | |
91 | normal_row='<div class="form-row"><div class="form-label">' |
|
91 | normal_row='<div class="form-row"><div class="form-label">' | |
92 | '%(label)s' |
|
92 | '%(label)s' | |
93 | '</div></div>' |
|
93 | '</div></div>' | |
94 | '<div class="form-row"><div class="form-input">' |
|
94 | '<div class="form-row"><div class="form-input">' | |
95 | '%(field)s' |
|
95 | '%(field)s' | |
96 | '</div></div>' |
|
96 | '</div></div>' | |
97 | '<div class="form-row">' |
|
97 | '<div class="form-row">' | |
98 | '%(help_text)s' |
|
98 | '%(help_text)s' | |
99 | '</div>', |
|
99 | '</div>', | |
100 | error_row='<div class="form-row">' |
|
100 | error_row='<div class="form-row">' | |
101 | '<div class="form-label"></div>' |
|
101 | '<div class="form-label"></div>' | |
102 | '<div class="form-errors">%s</div>' |
|
102 | '<div class="form-errors">%s</div>' | |
103 | '</div>', |
|
103 | '</div>', | |
104 | row_ender='</div>', |
|
104 | row_ender='</div>', | |
105 | help_text_html='%s', |
|
105 | help_text_html='%s', | |
106 | errors_on_separate_row=True) |
|
106 | errors_on_separate_row=True) | |
107 |
|
107 | |||
108 | def as_json_errors(self): |
|
108 | def as_json_errors(self): | |
109 | errors = [] |
|
109 | errors = [] | |
110 |
|
110 | |||
111 | for name, field in list(self.fields.items()): |
|
111 | for name, field in list(self.fields.items()): | |
112 | if self[name].errors: |
|
112 | if self[name].errors: | |
113 | errors.append({ |
|
113 | errors.append({ | |
114 | 'field': name, |
|
114 | 'field': name, | |
115 | 'errors': self[name].errors.as_text(), |
|
115 | 'errors': self[name].errors.as_text(), | |
116 | }) |
|
116 | }) | |
117 |
|
117 | |||
118 | return errors |
|
118 | return errors | |
119 |
|
119 | |||
120 |
|
120 | |||
121 | class PostForm(NeboardForm): |
|
121 | class PostForm(NeboardForm): | |
122 |
|
122 | |||
123 | title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False, |
|
123 | title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False, | |
124 | label=LABEL_TITLE) |
|
124 | label=LABEL_TITLE) | |
125 | text = forms.CharField( |
|
125 | text = forms.CharField( | |
126 | widget=FormatPanel(attrs={ |
|
126 | widget=FormatPanel(attrs={ | |
127 | ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER, |
|
127 | ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER, | |
128 | ATTRIBUTE_ROWS: TEXTAREA_ROWS, |
|
128 | ATTRIBUTE_ROWS: TEXTAREA_ROWS, | |
129 | }), |
|
129 | }), | |
130 | required=False, label=LABEL_TEXT) |
|
130 | required=False, label=LABEL_TEXT) | |
131 | image = forms.ImageField(required=False, label=_('Image'), |
|
131 | image = forms.ImageField(required=False, label=_('Image'), | |
132 | widget=forms.ClearableFileInput( |
|
132 | widget=forms.ClearableFileInput( | |
133 | attrs={'accept': 'image/*'})) |
|
133 | attrs={'accept': 'image/*'})) | |
134 | image_url = forms.CharField(required=False, label=_('Image URL'), |
|
134 | image_url = forms.CharField(required=False, label=_('Image URL'), | |
135 | widget=forms.TextInput( |
|
135 | widget=forms.TextInput( | |
136 | attrs={ATTRIBUTE_PLACEHOLDER: |
|
136 | attrs={ATTRIBUTE_PLACEHOLDER: | |
137 | 'http://example.com/image.png'})) |
|
137 | 'http://example.com/image.png'})) | |
138 |
|
138 | |||
139 | # This field is for spam prevention only |
|
139 | # This field is for spam prevention only | |
140 | email = forms.CharField(max_length=100, required=False, label=_('e-mail'), |
|
140 | email = forms.CharField(max_length=100, required=False, label=_('e-mail'), | |
141 | widget=forms.TextInput(attrs={ |
|
141 | widget=forms.TextInput(attrs={ | |
142 | 'class': 'form-email'})) |
|
142 | 'class': 'form-email'})) | |
143 |
|
143 | |||
144 | session = None |
|
144 | session = None | |
145 | need_to_ban = False |
|
145 | need_to_ban = False | |
146 |
|
146 | |||
147 | def clean_title(self): |
|
147 | def clean_title(self): | |
148 | title = self.cleaned_data['title'] |
|
148 | title = self.cleaned_data['title'] | |
149 | if title: |
|
149 | if title: | |
150 | if len(title) > TITLE_MAX_LENGTH: |
|
150 | if len(title) > TITLE_MAX_LENGTH: | |
151 | raise forms.ValidationError(_('Title must have less than %s ' |
|
151 | raise forms.ValidationError(_('Title must have less than %s ' | |
152 | 'characters') % |
|
152 | 'characters') % | |
153 | str(TITLE_MAX_LENGTH)) |
|
153 | str(TITLE_MAX_LENGTH)) | |
154 | return title |
|
154 | return title | |
155 |
|
155 | |||
156 | def clean_text(self): |
|
156 | def clean_text(self): | |
157 | text = self.cleaned_data['text'].strip() |
|
157 | text = self.cleaned_data['text'].strip() | |
158 | if text: |
|
158 | if text: | |
159 | if len(text) > board_settings.MAX_TEXT_LENGTH: |
|
159 | if len(text) > board_settings.MAX_TEXT_LENGTH: | |
160 | raise forms.ValidationError(_('Text must have less than %s ' |
|
160 | raise forms.ValidationError(_('Text must have less than %s ' | |
161 | 'characters') % |
|
161 | 'characters') % | |
162 | str(board_settings |
|
162 | str(board_settings | |
163 | .MAX_TEXT_LENGTH)) |
|
163 | .MAX_TEXT_LENGTH)) | |
164 | return text |
|
164 | return text | |
165 |
|
165 | |||
166 | def clean_image(self): |
|
166 | def clean_image(self): | |
167 | image = self.cleaned_data['image'] |
|
167 | image = self.cleaned_data['image'] | |
168 |
|
168 | |||
169 | self._validate_image(image) |
|
169 | if image: | |
|
170 | self.validate_image_size(image.size) | |||
170 |
|
171 | |||
171 | return image |
|
172 | return image | |
172 |
|
173 | |||
173 | def clean_image_url(self): |
|
174 | def clean_image_url(self): | |
174 | url = self.cleaned_data['image_url'] |
|
175 | url = self.cleaned_data['image_url'] | |
175 |
|
176 | |||
176 | image = None |
|
177 | image = None | |
177 | if url: |
|
178 | if url: | |
178 | image = self._get_image_from_url(url) |
|
179 | image = self._get_image_from_url(url) | |
179 |
|
180 | |||
180 | if not image: |
|
181 | if not image: | |
181 | raise forms.ValidationError(_('Invalid URL')) |
|
182 | raise forms.ValidationError(_('Invalid URL')) | |
182 |
|
183 | else: | ||
183 |
self. |
|
184 | self.validate_image_size(image.size) | |
184 |
|
185 | |||
185 | return image |
|
186 | return image | |
186 |
|
187 | |||
187 | def clean(self): |
|
188 | def clean(self): | |
188 | cleaned_data = super(PostForm, self).clean() |
|
189 | cleaned_data = super(PostForm, self).clean() | |
189 |
|
190 | |||
190 | if not self.session: |
|
191 | if not self.session: | |
191 | raise forms.ValidationError('Humans have sessions') |
|
192 | raise forms.ValidationError('Humans have sessions') | |
192 |
|
193 | |||
193 | if cleaned_data['email']: |
|
194 | if cleaned_data['email']: | |
194 | self.need_to_ban = True |
|
195 | self.need_to_ban = True | |
195 | raise forms.ValidationError('A human cannot enter a hidden field') |
|
196 | raise forms.ValidationError('A human cannot enter a hidden field') | |
196 |
|
197 | |||
197 | if not self.errors: |
|
198 | if not self.errors: | |
198 | self._clean_text_image() |
|
199 | self._clean_text_image() | |
199 |
|
200 | |||
200 | if not self.errors and self.session: |
|
201 | if not self.errors and self.session: | |
201 | self._validate_posting_speed() |
|
202 | self._validate_posting_speed() | |
202 |
|
203 | |||
203 | return cleaned_data |
|
204 | return cleaned_data | |
204 |
|
205 | |||
205 | def get_image(self): |
|
206 | def get_image(self): | |
206 | """ |
|
207 | """ | |
207 | Gets image from file or URL. |
|
208 | Gets image from file or URL. | |
208 | """ |
|
209 | """ | |
209 |
|
210 | |||
210 | image = self.cleaned_data['image'] |
|
211 | image = self.cleaned_data['image'] | |
211 | return image if image else self.cleaned_data['image_url'] |
|
212 | return image if image else self.cleaned_data['image_url'] | |
212 |
|
213 | |||
213 | def _clean_text_image(self): |
|
214 | def _clean_text_image(self): | |
214 | text = self.cleaned_data.get('text') |
|
215 | text = self.cleaned_data.get('text') | |
215 | image = self.get_image() |
|
216 | image = self.get_image() | |
216 |
|
217 | |||
217 | if (not text) and (not image): |
|
218 | if (not text) and (not image): | |
218 | error_message = _('Either text or image must be entered.') |
|
219 | error_message = _('Either text or image must be entered.') | |
219 | self._errors['text'] = self.error_class([error_message]) |
|
220 | self._errors['text'] = self.error_class([error_message]) | |
220 |
|
221 | |||
221 | def _validate_image(self, image): |
|
|||
222 | if image: |
|
|||
223 | if image.size > board_settings.MAX_IMAGE_SIZE: |
|
|||
224 | raise forms.ValidationError( |
|
|||
225 | _('Image must be less than %s bytes') |
|
|||
226 | % str(board_settings.MAX_IMAGE_SIZE)) |
|
|||
227 |
|
||||
228 | def _validate_posting_speed(self): |
|
222 | def _validate_posting_speed(self): | |
229 | can_post = True |
|
223 | can_post = True | |
230 |
|
224 | |||
231 | posting_delay = settings.POSTING_DELAY |
|
225 | posting_delay = settings.POSTING_DELAY | |
232 |
|
226 | |||
233 | if board_settings.LIMIT_POSTING_SPEED and LAST_POST_TIME in \ |
|
227 | if board_settings.LIMIT_POSTING_SPEED and LAST_POST_TIME in \ | |
234 | self.session: |
|
228 | self.session: | |
235 | now = time.time() |
|
229 | now = time.time() | |
236 | last_post_time = self.session[LAST_POST_TIME] |
|
230 | last_post_time = self.session[LAST_POST_TIME] | |
237 |
|
231 | |||
238 | current_delay = int(now - last_post_time) |
|
232 | current_delay = int(now - last_post_time) | |
239 |
|
233 | |||
240 | if current_delay < posting_delay: |
|
234 | if current_delay < posting_delay: | |
241 | error_message = _('Wait %s seconds after last posting') % str( |
|
235 | error_message = _('Wait %s seconds after last posting') % str( | |
242 | posting_delay - current_delay) |
|
236 | posting_delay - current_delay) | |
243 | self._errors['text'] = self.error_class([error_message]) |
|
237 | self._errors['text'] = self.error_class([error_message]) | |
244 |
|
238 | |||
245 | can_post = False |
|
239 | can_post = False | |
246 |
|
240 | |||
247 | if can_post: |
|
241 | if can_post: | |
248 | self.session[LAST_POST_TIME] = time.time() |
|
242 | self.session[LAST_POST_TIME] = time.time() | |
249 |
|
243 | |||
|
244 | def validate_image_size(self, size: int): | |||
|
245 | if size > board_settings.MAX_IMAGE_SIZE: | |||
|
246 | raise forms.ValidationError( | |||
|
247 | _('Image must be less than %s bytes') | |||
|
248 | % str(board_settings.MAX_IMAGE_SIZE)) | |||
|
249 | ||||
250 | def _get_image_from_url(self, url: str) -> SimpleUploadedFile: |
|
250 | def _get_image_from_url(self, url: str) -> SimpleUploadedFile: | |
251 | """ |
|
251 | """ | |
252 | Gets an image file from URL. |
|
252 | Gets an image file from URL. | |
253 | """ |
|
253 | """ | |
254 |
|
254 | |||
255 | img_temp = None |
|
255 | img_temp = None | |
256 |
|
256 | |||
257 | try: |
|
257 | try: | |
258 | # Verify content headers |
|
258 | # Verify content headers | |
259 | response_head = requests.head(url, verify=False) |
|
259 | response_head = requests.head(url, verify=False) | |
260 | content_type = response_head.headers['content-type'].split(';')[0] |
|
260 | content_type = response_head.headers['content-type'].split(';')[0] | |
261 | if content_type in CONTENT_TYPE_IMAGE: |
|
261 | if content_type in CONTENT_TYPE_IMAGE: | |
262 | length_header = response_head.headers.get('content-length') |
|
262 | length_header = response_head.headers.get('content-length') | |
263 | if length_header: |
|
263 | if length_header: | |
264 | length = int(length_header) |
|
264 | length = int(length_header) | |
265 | if length > board_settings.MAX_IMAGE_SIZE: |
|
265 | self.validate_image_size(length) | |
266 | raise forms.ValidationError( |
|
|||
267 | _('Image must be less than %s bytes') |
|
|||
268 | % str(board_settings.MAX_IMAGE_SIZE)) |
|
|||
269 |
|
||||
270 | # Get the actual content into memory |
|
266 | # Get the actual content into memory | |
271 | response = requests.get(url, verify=False, stream=True) |
|
267 | response = requests.get(url, verify=False, stream=True) | |
272 |
|
268 | |||
273 | # Download image, stop if the size exceeds limit |
|
269 | # Download image, stop if the size exceeds limit | |
274 | size = 0 |
|
270 | size = 0 | |
275 | content = b'' |
|
271 | content = b'' | |
276 | for chunk in response.iter_content(IMAGE_DOWNLOAD_CHUNK_BYTES): |
|
272 | for chunk in response.iter_content(IMAGE_DOWNLOAD_CHUNK_BYTES): | |
277 | size += len(chunk) |
|
273 | size += len(chunk) | |
278 | if size > board_settings.MAX_IMAGE_SIZE: |
|
274 | self.validate_image_size(size) | |
279 | # TODO Dedup this code into a method |
|
|||
280 | raise forms.ValidationError( |
|
|||
281 | _('Image must be less than %s bytes') |
|
|||
282 | % str(board_settings.MAX_IMAGE_SIZE)) |
|
|||
283 | content += chunk |
|
275 | content += chunk | |
284 |
|
276 | |||
285 | if response.status_code == HTTP_RESULT_OK and content: |
|
277 | if response.status_code == HTTP_RESULT_OK and content: | |
286 | # Set a dummy file name that will be replaced |
|
278 | # Set a dummy file name that will be replaced | |
287 | # anyway, just keep the valid extension |
|
279 | # anyway, just keep the valid extension | |
288 | filename = 'image.' + content_type.split('/')[1] |
|
280 | filename = 'image.' + content_type.split('/')[1] | |
289 | img_temp = SimpleUploadedFile(filename, content, |
|
281 | img_temp = SimpleUploadedFile(filename, content, | |
290 | content_type) |
|
282 | content_type) | |
291 | except Exception: |
|
283 | except Exception: | |
292 | # Just return no image |
|
284 | # Just return no image | |
293 | pass |
|
285 | pass | |
294 |
|
286 | |||
295 | return img_temp |
|
287 | return img_temp | |
296 |
|
288 | |||
297 |
|
289 | |||
298 | class ThreadForm(PostForm): |
|
290 | class ThreadForm(PostForm): | |
299 |
|
291 | |||
300 | tags = forms.CharField( |
|
292 | tags = forms.CharField( | |
301 | widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}), |
|
293 | widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}), | |
302 | max_length=100, label=_('Tags'), required=True) |
|
294 | max_length=100, label=_('Tags'), required=True) | |
303 |
|
295 | |||
304 | def clean_tags(self): |
|
296 | def clean_tags(self): | |
305 | tags = self.cleaned_data['tags'].strip() |
|
297 | tags = self.cleaned_data['tags'].strip() | |
306 |
|
298 | |||
307 | if not tags or not REGEX_TAGS.match(tags): |
|
299 | if not tags or not REGEX_TAGS.match(tags): | |
308 | raise forms.ValidationError( |
|
300 | raise forms.ValidationError( | |
309 | _('Inappropriate characters in tags.')) |
|
301 | _('Inappropriate characters in tags.')) | |
310 |
|
302 | |||
311 | required_tag_exists = False |
|
303 | required_tag_exists = False | |
312 | for tag in tags.split(): |
|
304 | for tag in tags.split(): | |
313 | tag_model = Tag.objects.filter(name=tag.strip().lower(), |
|
305 | tag_model = Tag.objects.filter(name=tag.strip().lower(), | |
314 | required=True) |
|
306 | required=True) | |
315 | if tag_model.exists(): |
|
307 | if tag_model.exists(): | |
316 | required_tag_exists = True |
|
308 | required_tag_exists = True | |
317 | break |
|
309 | break | |
318 |
|
310 | |||
319 | if not required_tag_exists: |
|
311 | if not required_tag_exists: | |
320 | raise forms.ValidationError(_('Need at least 1 required tag.')) |
|
312 | raise forms.ValidationError(_('Need at least 1 required tag.')) | |
321 |
|
313 | |||
322 | return tags |
|
314 | return tags | |
323 |
|
315 | |||
324 | def clean(self): |
|
316 | def clean(self): | |
325 | cleaned_data = super(ThreadForm, self).clean() |
|
317 | cleaned_data = super(ThreadForm, self).clean() | |
326 |
|
318 | |||
327 | return cleaned_data |
|
319 | return cleaned_data | |
328 |
|
320 | |||
329 |
|
321 | |||
330 | class SettingsForm(NeboardForm): |
|
322 | class SettingsForm(NeboardForm): | |
331 |
|
323 | |||
332 | theme = forms.ChoiceField(choices=settings.THEMES, |
|
324 | theme = forms.ChoiceField(choices=settings.THEMES, | |
333 | label=_('Theme')) |
|
325 | label=_('Theme')) | |
334 | username = forms.CharField(label=_('User name'), required=False) |
|
326 | username = forms.CharField(label=_('User name'), required=False) | |
335 |
|
327 | |||
336 | def clean_username(self): |
|
328 | def clean_username(self): | |
337 | username = self.cleaned_data['username'] |
|
329 | username = self.cleaned_data['username'] | |
338 |
|
330 | |||
339 | if username and not REGEX_TAGS.match(username): |
|
331 | if username and not REGEX_TAGS.match(username): | |
340 | raise forms.ValidationError(_('Inappropriate characters.')) |
|
332 | raise forms.ValidationError(_('Inappropriate characters.')) | |
341 |
|
333 | |||
342 | return username |
|
334 | return username | |
343 |
|
335 | |||
344 |
|
336 | |||
345 | class SearchForm(NeboardForm): |
|
337 | class SearchForm(NeboardForm): | |
346 | query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False) |
|
338 | query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False) |
@@ -1,90 +1,105 b'' | |||||
1 | import hashlib |
|
1 | import hashlib | |
2 | import os |
|
2 | import os | |
3 | from random import random |
|
3 | from random import random | |
4 | import time |
|
4 | import time | |
5 | from django.db import models |
|
5 | from django.db import models | |
6 | from boards import thumbs |
|
6 | from boards import thumbs | |
7 | from boards.models.base import Viewable |
|
7 | from boards.models.base import Viewable | |
8 |
|
8 | |||
9 | __author__ = 'neko259' |
|
9 | __author__ = 'neko259' | |
10 |
|
10 | |||
11 |
|
11 | |||
12 | IMAGE_THUMB_SIZE = (200, 150) |
|
12 | IMAGE_THUMB_SIZE = (200, 150) | |
13 | IMAGES_DIRECTORY = 'images/' |
|
13 | IMAGES_DIRECTORY = 'images/' | |
14 | FILE_EXTENSION_DELIMITER = '.' |
|
14 | FILE_EXTENSION_DELIMITER = '.' | |
15 | HASH_LENGTH = 36 |
|
15 | HASH_LENGTH = 36 | |
16 |
|
16 | |||
17 | CSS_CLASS_IMAGE = 'image' |
|
17 | CSS_CLASS_IMAGE = 'image' | |
18 | CSS_CLASS_THUMB = 'thumb' |
|
18 | CSS_CLASS_THUMB = 'thumb' | |
19 |
|
19 | |||
20 |
|
20 | |||
|
21 | class PostImageManager(models.Manager): | |||
|
22 | def create_with_hash(self, image): | |||
|
23 | image_hash = self.get_hash(image) | |||
|
24 | existing = self.filter(hash=image_hash) | |||
|
25 | if len(existing) > 0: | |||
|
26 | post_image = existing[0] | |||
|
27 | else: | |||
|
28 | post_image = PostImage.objects.create(image=image) | |||
|
29 | ||||
|
30 | return post_image | |||
|
31 | ||||
|
32 | def get_hash(self, image): | |||
|
33 | """ | |||
|
34 | Gets hash of an image. | |||
|
35 | """ | |||
|
36 | md5 = hashlib.md5() | |||
|
37 | for chunk in image.chunks(): | |||
|
38 | md5.update(chunk) | |||
|
39 | return md5.hexdigest() | |||
|
40 | ||||
|
41 | ||||
21 | class PostImage(models.Model, Viewable): |
|
42 | class PostImage(models.Model, Viewable): | |
|
43 | objects = PostImageManager() | |||
|
44 | ||||
22 | class Meta: |
|
45 | class Meta: | |
23 | app_label = 'boards' |
|
46 | app_label = 'boards' | |
24 | ordering = ('id',) |
|
47 | ordering = ('id',) | |
25 |
|
48 | |||
26 | def _update_image_filename(self, filename): |
|
49 | def _update_image_filename(self, filename): | |
27 | """ |
|
50 | """ | |
28 | Gets unique image filename |
|
51 | Gets unique image filename | |
29 | """ |
|
52 | """ | |
30 |
|
53 | |||
31 | path = IMAGES_DIRECTORY |
|
54 | path = IMAGES_DIRECTORY | |
32 | new_name = str(int(time.mktime(time.gmtime()))) |
|
55 | ||
33 | new_name += str(int(random() * 1000)) |
|
56 | # TODO Use something other than random number in file name | |
34 | new_name += FILE_EXTENSION_DELIMITER |
|
57 | new_name = '{}{}.{}'.format( | |
35 | new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0] |
|
58 | str(int(time.mktime(time.gmtime()))), | |
|
59 | str(int(random() * 1000)), | |||
|
60 | filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]) | |||
36 |
|
61 | |||
37 | return os.path.join(path, new_name) |
|
62 | return os.path.join(path, new_name) | |
38 |
|
63 | |||
39 | width = models.IntegerField(default=0) |
|
64 | width = models.IntegerField(default=0) | |
40 | height = models.IntegerField(default=0) |
|
65 | height = models.IntegerField(default=0) | |
41 |
|
66 | |||
42 | pre_width = models.IntegerField(default=0) |
|
67 | pre_width = models.IntegerField(default=0) | |
43 | pre_height = models.IntegerField(default=0) |
|
68 | pre_height = models.IntegerField(default=0) | |
44 |
|
69 | |||
45 | image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename, |
|
70 | image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename, | |
46 | blank=True, sizes=(IMAGE_THUMB_SIZE,), |
|
71 | blank=True, sizes=(IMAGE_THUMB_SIZE,), | |
47 | width_field='width', |
|
72 | width_field='width', | |
48 | height_field='height', |
|
73 | height_field='height', | |
49 | preview_width_field='pre_width', |
|
74 | preview_width_field='pre_width', | |
50 | preview_height_field='pre_height') |
|
75 | preview_height_field='pre_height') | |
51 | hash = models.CharField(max_length=HASH_LENGTH) |
|
76 | hash = models.CharField(max_length=HASH_LENGTH) | |
52 |
|
77 | |||
53 | def save(self, *args, **kwargs): |
|
78 | def save(self, *args, **kwargs): | |
54 | """ |
|
79 | """ | |
55 | Saves the model and computes the image hash for deduplication purposes. |
|
80 | Saves the model and computes the image hash for deduplication purposes. | |
56 | """ |
|
81 | """ | |
57 |
|
82 | |||
58 | if not self.pk and self.image: |
|
83 | if not self.pk and self.image: | |
59 | self.hash = PostImage.get_hash(self.image) |
|
84 | self.hash = PostImage.objects.get_hash(self.image) | |
60 | super(PostImage, self).save(*args, **kwargs) |
|
85 | super(PostImage, self).save(*args, **kwargs) | |
61 |
|
86 | |||
62 | def __str__(self): |
|
87 | def __str__(self): | |
63 | return self.image.url |
|
88 | return self.image.url | |
64 |
|
89 | |||
65 | def get_view(self): |
|
90 | def get_view(self): | |
66 | return '<div class="{}">' \ |
|
91 | return '<div class="{}">' \ | |
67 | '<a class="{}" href="{}">' \ |
|
92 | '<a class="{}" href="{}">' \ | |
68 | '<img' \ |
|
93 | '<img' \ | |
69 | ' src="{}"' \ |
|
94 | ' src="{}"' \ | |
70 | ' alt="{}"' \ |
|
95 | ' alt="{}"' \ | |
71 | ' width="{}"' \ |
|
96 | ' width="{}"' \ | |
72 | ' height="{}"' \ |
|
97 | ' height="{}"' \ | |
73 | ' data-width="{}"' \ |
|
98 | ' data-width="{}"' \ | |
74 | ' data-height="{}" />' \ |
|
99 | ' data-height="{}" />' \ | |
75 | '</a>' \ |
|
100 | '</a>' \ | |
76 | '</div>'\ |
|
101 | '</div>'\ | |
77 | .format(CSS_CLASS_IMAGE, CSS_CLASS_THUMB, self.image.url, |
|
102 | .format(CSS_CLASS_IMAGE, CSS_CLASS_THUMB, self.image.url, | |
78 | self.image.url_200x150, |
|
103 | self.image.url_200x150, | |
79 | str(self.hash), str(self.pre_width), |
|
104 | str(self.hash), str(self.pre_width), | |
80 | str(self.pre_height), str(self.width), str(self.height)) |
|
105 | str(self.pre_height), str(self.width), str(self.height)) | |
81 |
|
||||
82 | @staticmethod |
|
|||
83 | def get_hash(image): |
|
|||
84 | """ |
|
|||
85 | Gets hash of an image. |
|
|||
86 | """ |
|
|||
87 | md5 = hashlib.md5() |
|
|||
88 | for chunk in image.chunks(): |
|
|||
89 | md5.update(chunk) |
|
|||
90 | return md5.hexdigest() |
|
@@ -1,631 +1,621 b'' | |||||
1 | from datetime import datetime, timedelta, date |
|
1 | from datetime import datetime, timedelta, date | |
2 | from datetime import time as dtime |
|
2 | from datetime import time as dtime | |
3 | import logging |
|
3 | import logging | |
4 | import re |
|
4 | import re | |
5 | import xml.etree.ElementTree as et |
|
5 | import xml.etree.ElementTree as et | |
6 | from urllib.parse import unquote |
|
6 | from urllib.parse import unquote | |
7 |
|
7 | |||
8 | from adjacent import Client |
|
8 | from adjacent import Client | |
9 | from django.core.urlresolvers import reverse |
|
9 | from django.core.urlresolvers import reverse | |
10 | from django.db import models, transaction |
|
10 | from django.db import models, transaction | |
11 | from django.db.models import TextField |
|
11 | from django.db.models import TextField | |
12 | from django.template.loader import render_to_string |
|
12 | from django.template.loader import render_to_string | |
13 | from django.utils import timezone |
|
13 | from django.utils import timezone | |
14 |
|
14 | |||
15 | from boards.models import KeyPair, GlobalId, Signature |
|
15 | from boards.models import KeyPair, GlobalId, Signature | |
16 | from boards import settings, utils |
|
16 | from boards import settings, utils | |
17 | from boards.mdx_neboard import bbcode_extended |
|
17 | from boards.mdx_neboard import bbcode_extended | |
18 | from boards.models import PostImage |
|
18 | from boards.models import PostImage | |
19 | from boards.models.base import Viewable |
|
19 | from boards.models.base import Viewable | |
20 | from boards.utils import datetime_to_epoch, cached_result |
|
20 | from boards.utils import datetime_to_epoch, cached_result | |
21 | from boards.models.user import Notification |
|
21 | from boards.models.user import Notification | |
22 | import boards.models.thread |
|
22 | import boards.models.thread | |
23 |
|
23 | |||
24 |
|
24 | |||
25 | ENCODING_UNICODE = 'unicode' |
|
25 | ENCODING_UNICODE = 'unicode' | |
26 |
|
26 | |||
27 | WS_NOTIFICATION_TYPE_NEW_POST = 'new_post' |
|
27 | WS_NOTIFICATION_TYPE_NEW_POST = 'new_post' | |
28 | WS_NOTIFICATION_TYPE = 'notification_type' |
|
28 | WS_NOTIFICATION_TYPE = 'notification_type' | |
29 |
|
29 | |||
30 | WS_CHANNEL_THREAD = "thread:" |
|
30 | WS_CHANNEL_THREAD = "thread:" | |
31 |
|
31 | |||
32 | APP_LABEL_BOARDS = 'boards' |
|
32 | APP_LABEL_BOARDS = 'boards' | |
33 |
|
33 | |||
34 | POSTS_PER_DAY_RANGE = 7 |
|
34 | POSTS_PER_DAY_RANGE = 7 | |
35 |
|
35 | |||
36 | BAN_REASON_AUTO = 'Auto' |
|
36 | BAN_REASON_AUTO = 'Auto' | |
37 |
|
37 | |||
38 | IMAGE_THUMB_SIZE = (200, 150) |
|
38 | IMAGE_THUMB_SIZE = (200, 150) | |
39 |
|
39 | |||
40 | TITLE_MAX_LENGTH = 200 |
|
40 | TITLE_MAX_LENGTH = 200 | |
41 |
|
41 | |||
42 | # TODO This should be removed |
|
42 | # TODO This should be removed | |
43 | NO_IP = '0.0.0.0' |
|
43 | NO_IP = '0.0.0.0' | |
44 |
|
44 | |||
45 | # TODO Real user agent should be saved instead of this |
|
45 | # TODO Real user agent should be saved instead of this | |
46 | UNKNOWN_UA = '' |
|
46 | UNKNOWN_UA = '' | |
47 |
|
47 | |||
48 | REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]') |
|
48 | REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]') | |
49 | REGEX_GLOBAL_REPLY = re.compile(r'\[post\](\w+)::([^:]+)::(\d+)\[/post\]') |
|
49 | REGEX_GLOBAL_REPLY = re.compile(r'\[post\](\w+)::([^:]+)::(\d+)\[/post\]') | |
50 | REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?') |
|
50 | REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?') | |
51 | REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]') |
|
51 | REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]') | |
52 |
|
52 | |||
53 | TAG_MODEL = 'model' |
|
53 | TAG_MODEL = 'model' | |
54 | TAG_REQUEST = 'request' |
|
54 | TAG_REQUEST = 'request' | |
55 | TAG_RESPONSE = 'response' |
|
55 | TAG_RESPONSE = 'response' | |
56 | TAG_ID = 'id' |
|
56 | TAG_ID = 'id' | |
57 | TAG_STATUS = 'status' |
|
57 | TAG_STATUS = 'status' | |
58 | TAG_MODELS = 'models' |
|
58 | TAG_MODELS = 'models' | |
59 | TAG_TITLE = 'title' |
|
59 | TAG_TITLE = 'title' | |
60 | TAG_TEXT = 'text' |
|
60 | TAG_TEXT = 'text' | |
61 | TAG_THREAD = 'thread' |
|
61 | TAG_THREAD = 'thread' | |
62 | TAG_PUB_TIME = 'pub-time' |
|
62 | TAG_PUB_TIME = 'pub-time' | |
63 | TAG_SIGNATURES = 'signatures' |
|
63 | TAG_SIGNATURES = 'signatures' | |
64 | TAG_SIGNATURE = 'signature' |
|
64 | TAG_SIGNATURE = 'signature' | |
65 | TAG_CONTENT = 'content' |
|
65 | TAG_CONTENT = 'content' | |
66 | TAG_ATTACHMENTS = 'attachments' |
|
66 | TAG_ATTACHMENTS = 'attachments' | |
67 | TAG_ATTACHMENT = 'attachment' |
|
67 | TAG_ATTACHMENT = 'attachment' | |
68 |
|
68 | |||
69 | TYPE_GET = 'get' |
|
69 | TYPE_GET = 'get' | |
70 |
|
70 | |||
71 | ATTR_VERSION = 'version' |
|
71 | ATTR_VERSION = 'version' | |
72 | ATTR_TYPE = 'type' |
|
72 | ATTR_TYPE = 'type' | |
73 | ATTR_NAME = 'name' |
|
73 | ATTR_NAME = 'name' | |
74 | ATTR_VALUE = 'value' |
|
74 | ATTR_VALUE = 'value' | |
75 | ATTR_MIMETYPE = 'mimetype' |
|
75 | ATTR_MIMETYPE = 'mimetype' | |
76 |
|
76 | |||
77 | STATUS_SUCCESS = 'success' |
|
77 | STATUS_SUCCESS = 'success' | |
78 |
|
78 | |||
79 | PARAMETER_TRUNCATED = 'truncated' |
|
79 | PARAMETER_TRUNCATED = 'truncated' | |
80 | PARAMETER_TAG = 'tag' |
|
80 | PARAMETER_TAG = 'tag' | |
81 | PARAMETER_OFFSET = 'offset' |
|
81 | PARAMETER_OFFSET = 'offset' | |
82 | PARAMETER_DIFF_TYPE = 'type' |
|
82 | PARAMETER_DIFF_TYPE = 'type' | |
83 | PARAMETER_BUMPABLE = 'bumpable' |
|
83 | PARAMETER_BUMPABLE = 'bumpable' | |
84 | PARAMETER_THREAD = 'thread' |
|
84 | PARAMETER_THREAD = 'thread' | |
85 | PARAMETER_IS_OPENING = 'is_opening' |
|
85 | PARAMETER_IS_OPENING = 'is_opening' | |
86 | PARAMETER_MODERATOR = 'moderator' |
|
86 | PARAMETER_MODERATOR = 'moderator' | |
87 | PARAMETER_POST = 'post' |
|
87 | PARAMETER_POST = 'post' | |
88 | PARAMETER_OP_ID = 'opening_post_id' |
|
88 | PARAMETER_OP_ID = 'opening_post_id' | |
89 | PARAMETER_NEED_OPEN_LINK = 'need_open_link' |
|
89 | PARAMETER_NEED_OPEN_LINK = 'need_open_link' | |
90 |
|
90 | |||
91 | DIFF_TYPE_HTML = 'html' |
|
91 | DIFF_TYPE_HTML = 'html' | |
92 | DIFF_TYPE_JSON = 'json' |
|
92 | DIFF_TYPE_JSON = 'json' | |
93 |
|
93 | |||
94 | PREPARSE_PATTERNS = { |
|
94 | PREPARSE_PATTERNS = { | |
95 | r'>>(\d+)': r'[post]\1[/post]', # Reflink ">>123" |
|
95 | r'>>(\d+)': r'[post]\1[/post]', # Reflink ">>123" | |
96 | r'^>([^>].+)': r'[quote]\1[/quote]', # Quote ">text" |
|
96 | r'^>([^>].+)': r'[quote]\1[/quote]', # Quote ">text" | |
97 | r'^//(.+)': r'[comment]\1[/comment]', # Comment "//text" |
|
97 | r'^//(.+)': r'[comment]\1[/comment]', # Comment "//text" | |
98 | } |
|
98 | } | |
99 |
|
99 | |||
100 |
|
100 | |||
101 | class PostManager(models.Manager): |
|
101 | class PostManager(models.Manager): | |
102 | @transaction.atomic |
|
102 | @transaction.atomic | |
103 | def create_post(self, title: str, text: str, image=None, thread=None, |
|
103 | def create_post(self, title: str, text: str, image=None, thread=None, | |
104 | ip=NO_IP, tags: list=None): |
|
104 | ip=NO_IP, tags: list=None): | |
105 | """ |
|
105 | """ | |
106 | Creates new post |
|
106 | Creates new post | |
107 | """ |
|
107 | """ | |
108 |
|
108 | |||
109 | if not tags: |
|
109 | if not tags: | |
110 | tags = [] |
|
110 | tags = [] | |
111 |
|
111 | |||
112 | posting_time = timezone.now() |
|
112 | posting_time = timezone.now() | |
113 | if not thread: |
|
113 | if not thread: | |
114 | thread = boards.models.thread.Thread.objects.create( |
|
114 | thread = boards.models.thread.Thread.objects.create( | |
115 | bump_time=posting_time, last_edit_time=posting_time) |
|
115 | bump_time=posting_time, last_edit_time=posting_time) | |
116 | new_thread = True |
|
116 | new_thread = True | |
117 | else: |
|
117 | else: | |
118 | new_thread = False |
|
118 | new_thread = False | |
119 |
|
119 | |||
120 | pre_text = self._preparse_text(text) |
|
120 | pre_text = self._preparse_text(text) | |
121 |
|
121 | |||
122 | post = self.create(title=title, |
|
122 | post = self.create(title=title, | |
123 | text=pre_text, |
|
123 | text=pre_text, | |
124 | pub_time=posting_time, |
|
124 | pub_time=posting_time, | |
125 | poster_ip=ip, |
|
125 | poster_ip=ip, | |
126 | thread=thread, |
|
126 | thread=thread, | |
127 | poster_user_agent=UNKNOWN_UA, # TODO Get UA at |
|
127 | poster_user_agent=UNKNOWN_UA, # TODO Get UA at | |
128 | # last! |
|
128 | # last! | |
129 | last_edit_time=posting_time) |
|
129 | last_edit_time=posting_time) | |
130 | post.threads.add(thread) |
|
130 | post.threads.add(thread) | |
131 |
|
131 | |||
132 | post.set_global_id() |
|
132 | post.set_global_id() | |
133 |
|
133 | |||
134 | logger = logging.getLogger('boards.post.create') |
|
134 | logger = logging.getLogger('boards.post.create') | |
135 |
|
135 | |||
136 | logger.info('Created post {} by {}'.format( |
|
136 | logger.info('Created post {} by {}'.format( | |
137 | post, post.poster_ip)) |
|
137 | post, post.poster_ip)) | |
138 |
|
138 | |||
139 | if image: |
|
139 | if image: | |
140 | # Try to find existing image. If it exists, assign it to the post |
|
140 | post.images.add(PostImage.objects.create_with_hash(image)) | |
141 | # instead of createing the new one |
|
|||
142 | image_hash = PostImage.get_hash(image) |
|
|||
143 | existing = PostImage.objects.filter(hash=image_hash) |
|
|||
144 | if len(existing) > 0: |
|
|||
145 | post_image = existing[0] |
|
|||
146 | else: |
|
|||
147 | post_image = PostImage.objects.create(image=image) |
|
|||
148 | logger.info('Created new image #{} for post #{}'.format( |
|
|||
149 | post_image.id, post.id)) |
|
|||
150 | post.images.add(post_image) |
|
|||
151 |
|
141 | |||
152 | list(map(thread.add_tag, tags)) |
|
142 | list(map(thread.add_tag, tags)) | |
153 |
|
143 | |||
154 | if new_thread: |
|
144 | if new_thread: | |
155 | boards.models.thread.Thread.objects.process_oldest_threads() |
|
145 | boards.models.thread.Thread.objects.process_oldest_threads() | |
156 | else: |
|
146 | else: | |
|
147 | thread.last_edit_time = posting_time | |||
157 | thread.bump() |
|
148 | thread.bump() | |
158 | thread.last_edit_time = posting_time |
|
|||
159 | thread.save() |
|
149 | thread.save() | |
160 |
|
150 | |||
161 | post.connect_replies() |
|
151 | post.connect_replies() | |
162 | post.connect_notifications() |
|
152 | post.connect_notifications() | |
163 |
|
153 | |||
164 | return post |
|
154 | return post | |
165 |
|
155 | |||
166 | def delete_posts_by_ip(self, ip): |
|
156 | def delete_posts_by_ip(self, ip): | |
167 | """ |
|
157 | """ | |
168 | Deletes all posts of the author with same IP |
|
158 | Deletes all posts of the author with same IP | |
169 | """ |
|
159 | """ | |
170 |
|
160 | |||
171 | posts = self.filter(poster_ip=ip) |
|
161 | posts = self.filter(poster_ip=ip) | |
172 | for post in posts: |
|
162 | for post in posts: | |
173 | post.delete() |
|
163 | post.delete() | |
174 |
|
164 | |||
175 | @cached_result |
|
165 | @cached_result | |
176 | def get_posts_per_day(self): |
|
166 | def get_posts_per_day(self): | |
177 | """ |
|
167 | """ | |
178 | Gets average count of posts per day for the last 7 days |
|
168 | Gets average count of posts per day for the last 7 days | |
179 | """ |
|
169 | """ | |
180 |
|
170 | |||
181 | day_end = date.today() |
|
171 | day_end = date.today() | |
182 | day_start = day_end - timedelta(POSTS_PER_DAY_RANGE) |
|
172 | day_start = day_end - timedelta(POSTS_PER_DAY_RANGE) | |
183 |
|
173 | |||
184 | day_time_start = timezone.make_aware(datetime.combine( |
|
174 | day_time_start = timezone.make_aware(datetime.combine( | |
185 | day_start, dtime()), timezone.get_current_timezone()) |
|
175 | day_start, dtime()), timezone.get_current_timezone()) | |
186 | day_time_end = timezone.make_aware(datetime.combine( |
|
176 | day_time_end = timezone.make_aware(datetime.combine( | |
187 | day_end, dtime()), timezone.get_current_timezone()) |
|
177 | day_end, dtime()), timezone.get_current_timezone()) | |
188 |
|
178 | |||
189 | posts_per_period = float(self.filter( |
|
179 | posts_per_period = float(self.filter( | |
190 | pub_time__lte=day_time_end, |
|
180 | pub_time__lte=day_time_end, | |
191 | pub_time__gte=day_time_start).count()) |
|
181 | pub_time__gte=day_time_start).count()) | |
192 |
|
182 | |||
193 | ppd = posts_per_period / POSTS_PER_DAY_RANGE |
|
183 | ppd = posts_per_period / POSTS_PER_DAY_RANGE | |
194 |
|
184 | |||
195 | return ppd |
|
185 | return ppd | |
196 |
|
186 | |||
197 | # TODO Make a separate sync facade? |
|
187 | # TODO Make a separate sync facade? | |
198 | def generate_response_get(self, model_list: list): |
|
188 | def generate_response_get(self, model_list: list): | |
199 | response = et.Element(TAG_RESPONSE) |
|
189 | response = et.Element(TAG_RESPONSE) | |
200 |
|
190 | |||
201 | status = et.SubElement(response, TAG_STATUS) |
|
191 | status = et.SubElement(response, TAG_STATUS) | |
202 | status.text = STATUS_SUCCESS |
|
192 | status.text = STATUS_SUCCESS | |
203 |
|
193 | |||
204 | models = et.SubElement(response, TAG_MODELS) |
|
194 | models = et.SubElement(response, TAG_MODELS) | |
205 |
|
195 | |||
206 | for post in model_list: |
|
196 | for post in model_list: | |
207 | model = et.SubElement(models, TAG_MODEL) |
|
197 | model = et.SubElement(models, TAG_MODEL) | |
208 | model.set(ATTR_NAME, 'post') |
|
198 | model.set(ATTR_NAME, 'post') | |
209 |
|
199 | |||
210 | content_tag = et.SubElement(model, TAG_CONTENT) |
|
200 | content_tag = et.SubElement(model, TAG_CONTENT) | |
211 |
|
201 | |||
212 | tag_id = et.SubElement(content_tag, TAG_ID) |
|
202 | tag_id = et.SubElement(content_tag, TAG_ID) | |
213 | post.global_id.to_xml_element(tag_id) |
|
203 | post.global_id.to_xml_element(tag_id) | |
214 |
|
204 | |||
215 | title = et.SubElement(content_tag, TAG_TITLE) |
|
205 | title = et.SubElement(content_tag, TAG_TITLE) | |
216 | title.text = post.title |
|
206 | title.text = post.title | |
217 |
|
207 | |||
218 | text = et.SubElement(content_tag, TAG_TEXT) |
|
208 | text = et.SubElement(content_tag, TAG_TEXT) | |
219 | # TODO Replace local links by global ones in the text |
|
209 | # TODO Replace local links by global ones in the text | |
220 | text.text = post.get_raw_text() |
|
210 | text.text = post.get_raw_text() | |
221 |
|
211 | |||
222 | if not post.is_opening(): |
|
212 | if not post.is_opening(): | |
223 | thread = et.SubElement(content_tag, TAG_THREAD) |
|
213 | thread = et.SubElement(content_tag, TAG_THREAD) | |
224 | thread_id = et.SubElement(thread, TAG_ID) |
|
214 | thread_id = et.SubElement(thread, TAG_ID) | |
225 | post.get_thread().get_opening_post().global_id.to_xml_element(thread_id) |
|
215 | post.get_thread().get_opening_post().global_id.to_xml_element(thread_id) | |
226 | else: |
|
216 | else: | |
227 | # TODO Output tags here |
|
217 | # TODO Output tags here | |
228 | pass |
|
218 | pass | |
229 |
|
219 | |||
230 | pub_time = et.SubElement(content_tag, TAG_PUB_TIME) |
|
220 | pub_time = et.SubElement(content_tag, TAG_PUB_TIME) | |
231 | pub_time.text = str(post.get_pub_time_epoch()) |
|
221 | pub_time.text = str(post.get_pub_time_epoch()) | |
232 |
|
222 | |||
233 | signatures_tag = et.SubElement(model, TAG_SIGNATURES) |
|
223 | signatures_tag = et.SubElement(model, TAG_SIGNATURES) | |
234 | post_signatures = post.signature.all() |
|
224 | post_signatures = post.signature.all() | |
235 | if post_signatures: |
|
225 | if post_signatures: | |
236 | signatures = post.signatures |
|
226 | signatures = post.signatures | |
237 | else: |
|
227 | else: | |
238 | # TODO Maybe the signature can be computed only once after |
|
228 | # TODO Maybe the signature can be computed only once after | |
239 | # the post is added? Need to add some on_save signal queue |
|
229 | # the post is added? Need to add some on_save signal queue | |
240 | # and add this there. |
|
230 | # and add this there. | |
241 | key = KeyPair.objects.get(public_key=post.global_id.key) |
|
231 | key = KeyPair.objects.get(public_key=post.global_id.key) | |
242 | signatures = [Signature( |
|
232 | signatures = [Signature( | |
243 | key_type=key.key_type, |
|
233 | key_type=key.key_type, | |
244 | key=key.public_key, |
|
234 | key=key.public_key, | |
245 | signature=key.sign(et.tostring(model, ENCODING_UNICODE)), |
|
235 | signature=key.sign(et.tostring(model, ENCODING_UNICODE)), | |
246 | )] |
|
236 | )] | |
247 | for signature in signatures: |
|
237 | for signature in signatures: | |
248 | signature_tag = et.SubElement(signatures_tag, TAG_SIGNATURE) |
|
238 | signature_tag = et.SubElement(signatures_tag, TAG_SIGNATURE) | |
249 | signature_tag.set(ATTR_TYPE, signature.key_type) |
|
239 | signature_tag.set(ATTR_TYPE, signature.key_type) | |
250 | signature_tag.set(ATTR_VALUE, signature.signature) |
|
240 | signature_tag.set(ATTR_VALUE, signature.signature) | |
251 |
|
241 | |||
252 | return et.tostring(response, ENCODING_UNICODE) |
|
242 | return et.tostring(response, ENCODING_UNICODE) | |
253 |
|
243 | |||
254 | def parse_response_get(self, response_xml): |
|
244 | def parse_response_get(self, response_xml): | |
255 | tag_root = et.fromstring(response_xml) |
|
245 | tag_root = et.fromstring(response_xml) | |
256 | tag_status = tag_root[0] |
|
246 | tag_status = tag_root[0] | |
257 | if 'success' == tag_status.text: |
|
247 | if 'success' == tag_status.text: | |
258 | tag_models = tag_root[1] |
|
248 | tag_models = tag_root[1] | |
259 | for tag_model in tag_models: |
|
249 | for tag_model in tag_models: | |
260 | tag_content = tag_model[0] |
|
250 | tag_content = tag_model[0] | |
261 | tag_id = tag_content[1] |
|
251 | tag_id = tag_content[1] | |
262 | try: |
|
252 | try: | |
263 | GlobalId.from_xml_element(tag_id, existing=True) |
|
253 | GlobalId.from_xml_element(tag_id, existing=True) | |
264 | # If this post already exists, just continue |
|
254 | # If this post already exists, just continue | |
265 | # TODO Compare post content and update the post if necessary |
|
255 | # TODO Compare post content and update the post if necessary | |
266 | pass |
|
256 | pass | |
267 | except GlobalId.DoesNotExist: |
|
257 | except GlobalId.DoesNotExist: | |
268 | global_id = GlobalId.from_xml_element(tag_id) |
|
258 | global_id = GlobalId.from_xml_element(tag_id) | |
269 |
|
259 | |||
270 | title = tag_content.find(TAG_TITLE).text |
|
260 | title = tag_content.find(TAG_TITLE).text | |
271 | text = tag_content.find(TAG_TEXT).text |
|
261 | text = tag_content.find(TAG_TEXT).text | |
272 | # TODO Check that the replied posts are already present |
|
262 | # TODO Check that the replied posts are already present | |
273 | # before adding new ones |
|
263 | # before adding new ones | |
274 |
|
264 | |||
275 | # TODO Pub time, thread, tags |
|
265 | # TODO Pub time, thread, tags | |
276 |
|
266 | |||
277 | post = Post.objects.create(title=title, text=text) |
|
267 | post = Post.objects.create(title=title, text=text) | |
278 | else: |
|
268 | else: | |
279 | # TODO Throw an exception? |
|
269 | # TODO Throw an exception? | |
280 | pass |
|
270 | pass | |
281 |
|
271 | |||
282 | # TODO Make a separate parser module and move preparser there |
|
272 | # TODO Make a separate parser module and move preparser there | |
283 | def _preparse_text(self, text: str) -> str: |
|
273 | def _preparse_text(self, text: str) -> str: | |
284 | """ |
|
274 | """ | |
285 | Preparses text to change patterns like '>>' to a proper bbcode |
|
275 | Preparses text to change patterns like '>>' to a proper bbcode | |
286 | tags. |
|
276 | tags. | |
287 | """ |
|
277 | """ | |
288 |
|
278 | |||
289 | for key, value in PREPARSE_PATTERNS.items(): |
|
279 | for key, value in PREPARSE_PATTERNS.items(): | |
290 | text = re.sub(key, value, text, flags=re.MULTILINE) |
|
280 | text = re.sub(key, value, text, flags=re.MULTILINE) | |
291 |
|
281 | |||
292 | for link in REGEX_URL.findall(text): |
|
282 | for link in REGEX_URL.findall(text): | |
293 | text = text.replace(link, unquote(link)) |
|
283 | text = text.replace(link, unquote(link)) | |
294 |
|
284 | |||
295 | return text |
|
285 | return text | |
296 |
|
286 | |||
297 |
|
287 | |||
298 | class Post(models.Model, Viewable): |
|
288 | class Post(models.Model, Viewable): | |
299 | """A post is a message.""" |
|
289 | """A post is a message.""" | |
300 |
|
290 | |||
301 | objects = PostManager() |
|
291 | objects = PostManager() | |
302 |
|
292 | |||
303 | class Meta: |
|
293 | class Meta: | |
304 | app_label = APP_LABEL_BOARDS |
|
294 | app_label = APP_LABEL_BOARDS | |
305 | ordering = ('id',) |
|
295 | ordering = ('id',) | |
306 |
|
296 | |||
307 | title = models.CharField(max_length=TITLE_MAX_LENGTH) |
|
297 | title = models.CharField(max_length=TITLE_MAX_LENGTH) | |
308 | pub_time = models.DateTimeField() |
|
298 | pub_time = models.DateTimeField() | |
309 | text = TextField(blank=True, null=True) |
|
299 | text = TextField(blank=True, null=True) | |
310 | _text_rendered = TextField(blank=True, null=True, editable=False) |
|
300 | _text_rendered = TextField(blank=True, null=True, editable=False) | |
311 |
|
301 | |||
312 | images = models.ManyToManyField(PostImage, null=True, blank=True, |
|
302 | images = models.ManyToManyField(PostImage, null=True, blank=True, | |
313 | related_name='ip+', db_index=True) |
|
303 | related_name='ip+', db_index=True) | |
314 |
|
304 | |||
315 | poster_ip = models.GenericIPAddressField() |
|
305 | poster_ip = models.GenericIPAddressField() | |
316 | poster_user_agent = models.TextField() |
|
306 | poster_user_agent = models.TextField() | |
317 |
|
307 | |||
318 | last_edit_time = models.DateTimeField() |
|
308 | last_edit_time = models.DateTimeField() | |
319 |
|
309 | |||
320 | referenced_posts = models.ManyToManyField('Post', symmetrical=False, |
|
310 | referenced_posts = models.ManyToManyField('Post', symmetrical=False, | |
321 | null=True, |
|
311 | null=True, | |
322 | blank=True, related_name='rfp+', |
|
312 | blank=True, related_name='rfp+', | |
323 | db_index=True) |
|
313 | db_index=True) | |
324 | refmap = models.TextField(null=True, blank=True) |
|
314 | refmap = models.TextField(null=True, blank=True) | |
325 | threads = models.ManyToManyField('Thread', db_index=True) |
|
315 | threads = models.ManyToManyField('Thread', db_index=True) | |
326 | thread = models.ForeignKey('Thread', db_index=True, related_name='pt+') |
|
316 | thread = models.ForeignKey('Thread', db_index=True, related_name='pt+') | |
327 |
|
317 | |||
328 | # Global ID with author key. If the message was downloaded from another |
|
318 | # Global ID with author key. If the message was downloaded from another | |
329 | # server, this indicates the server. |
|
319 | # server, this indicates the server. | |
330 | global_id = models.OneToOneField('GlobalId', null=True, blank=True) |
|
320 | global_id = models.OneToOneField('GlobalId', null=True, blank=True) | |
331 |
|
321 | |||
332 | # One post can be signed by many nodes that give their trust to it |
|
322 | # One post can be signed by many nodes that give their trust to it | |
333 | signature = models.ManyToManyField('Signature', null=True, blank=True) |
|
323 | signature = models.ManyToManyField('Signature', null=True, blank=True) | |
334 |
|
324 | |||
335 | def __str__(self): |
|
325 | def __str__(self): | |
336 | return 'P#{}/{}'.format(self.id, self.title) |
|
326 | return 'P#{}/{}'.format(self.id, self.title) | |
337 |
|
327 | |||
338 | def get_title(self) -> str: |
|
328 | def get_title(self) -> str: | |
339 | """ |
|
329 | """ | |
340 | Gets original post title or part of its text. |
|
330 | Gets original post title or part of its text. | |
341 | """ |
|
331 | """ | |
342 |
|
332 | |||
343 | title = self.title |
|
333 | title = self.title | |
344 | if not title: |
|
334 | if not title: | |
345 | title = self.get_text() |
|
335 | title = self.get_text() | |
346 |
|
336 | |||
347 | return title |
|
337 | return title | |
348 |
|
338 | |||
349 | def build_refmap(self) -> None: |
|
339 | def build_refmap(self) -> None: | |
350 | """ |
|
340 | """ | |
351 | Builds a replies map string from replies list. This is a cache to stop |
|
341 | Builds a replies map string from replies list. This is a cache to stop | |
352 | the server from recalculating the map on every post show. |
|
342 | the server from recalculating the map on every post show. | |
353 | """ |
|
343 | """ | |
354 | post_urls = ['<a href="{}">>>{}</a>'.format( |
|
344 | post_urls = ['<a href="{}">>>{}</a>'.format( | |
355 | refpost.get_url(), refpost.id) for refpost in self.referenced_posts.all()] |
|
345 | refpost.get_url(), refpost.id) for refpost in self.referenced_posts.all()] | |
356 |
|
346 | |||
357 | self.refmap = ', '.join(post_urls) |
|
347 | self.refmap = ', '.join(post_urls) | |
358 |
|
348 | |||
359 | def get_sorted_referenced_posts(self): |
|
349 | def get_sorted_referenced_posts(self): | |
360 | return self.refmap |
|
350 | return self.refmap | |
361 |
|
351 | |||
362 | def is_referenced(self) -> bool: |
|
352 | def is_referenced(self) -> bool: | |
363 | return self.refmap and len(self.refmap) > 0 |
|
353 | return self.refmap and len(self.refmap) > 0 | |
364 |
|
354 | |||
365 | def is_opening(self) -> bool: |
|
355 | def is_opening(self) -> bool: | |
366 | """ |
|
356 | """ | |
367 | Checks if this is an opening post or just a reply. |
|
357 | Checks if this is an opening post or just a reply. | |
368 | """ |
|
358 | """ | |
369 |
|
359 | |||
370 | return self.get_thread().get_opening_post_id() == self.id |
|
360 | return self.get_thread().get_opening_post_id() == self.id | |
371 |
|
361 | |||
372 | @transaction.atomic |
|
362 | @transaction.atomic | |
373 | def add_tag(self, tag): |
|
363 | def add_tag(self, tag): | |
374 | edit_time = timezone.now() |
|
364 | edit_time = timezone.now() | |
375 |
|
365 | |||
376 | thread = self.get_thread() |
|
366 | thread = self.get_thread() | |
377 | thread.add_tag(tag) |
|
367 | thread.add_tag(tag) | |
378 | self.last_edit_time = edit_time |
|
368 | self.last_edit_time = edit_time | |
379 | self.save(update_fields=['last_edit_time']) |
|
369 | self.save(update_fields=['last_edit_time']) | |
380 |
|
370 | |||
381 | thread.last_edit_time = edit_time |
|
371 | thread.last_edit_time = edit_time | |
382 | thread.save(update_fields=['last_edit_time']) |
|
372 | thread.save(update_fields=['last_edit_time']) | |
383 |
|
373 | |||
384 | @cached_result |
|
374 | @cached_result | |
385 | def get_url(self): |
|
375 | def get_url(self): | |
386 | """ |
|
376 | """ | |
387 | Gets full url to the post. |
|
377 | Gets full url to the post. | |
388 | """ |
|
378 | """ | |
389 |
|
379 | |||
390 | thread = self.get_thread() |
|
380 | thread = self.get_thread() | |
391 |
|
381 | |||
392 | opening_id = thread.get_opening_post_id() |
|
382 | opening_id = thread.get_opening_post_id() | |
393 |
|
383 | |||
394 | if self.id != opening_id: |
|
384 | if self.id != opening_id: | |
395 | link = reverse('thread', kwargs={ |
|
385 | link = reverse('thread', kwargs={ | |
396 | 'post_id': opening_id}) + '#' + str(self.id) |
|
386 | 'post_id': opening_id}) + '#' + str(self.id) | |
397 | else: |
|
387 | else: | |
398 | link = reverse('thread', kwargs={'post_id': self.id}) |
|
388 | link = reverse('thread', kwargs={'post_id': self.id}) | |
399 |
|
389 | |||
400 | return link |
|
390 | return link | |
401 |
|
391 | |||
402 | def get_thread(self): |
|
392 | def get_thread(self): | |
403 | return self.thread |
|
393 | return self.thread | |
404 |
|
394 | |||
405 | def get_threads(self): |
|
395 | def get_threads(self): | |
406 | """ |
|
396 | """ | |
407 | Gets post's thread. |
|
397 | Gets post's thread. | |
408 | """ |
|
398 | """ | |
409 |
|
399 | |||
410 | return self.threads |
|
400 | return self.threads | |
411 |
|
401 | |||
412 | def get_referenced_posts(self): |
|
402 | def get_referenced_posts(self): | |
413 | return self.referenced_posts.only('id', 'threads') |
|
403 | return self.referenced_posts.only('id', 'threads') | |
414 |
|
404 | |||
415 | def get_view(self, moderator=False, need_open_link=False, |
|
405 | def get_view(self, moderator=False, need_open_link=False, | |
416 | truncated=False, *args, **kwargs): |
|
406 | truncated=False, *args, **kwargs): | |
417 | """ |
|
407 | """ | |
418 | Renders post's HTML view. Some of the post params can be passed over |
|
408 | Renders post's HTML view. Some of the post params can be passed over | |
419 | kwargs for the means of caching (if we view the thread, some params |
|
409 | kwargs for the means of caching (if we view the thread, some params | |
420 | are same for every post and don't need to be computed over and over. |
|
410 | are same for every post and don't need to be computed over and over. | |
421 | """ |
|
411 | """ | |
422 |
|
412 | |||
423 | thread = self.get_thread() |
|
413 | thread = self.get_thread() | |
424 | is_opening = kwargs.get(PARAMETER_IS_OPENING, self.is_opening()) |
|
414 | is_opening = kwargs.get(PARAMETER_IS_OPENING, self.is_opening()) | |
425 | can_bump = kwargs.get(PARAMETER_BUMPABLE, thread.can_bump()) |
|
415 | can_bump = kwargs.get(PARAMETER_BUMPABLE, thread.can_bump()) | |
426 |
|
416 | |||
427 | if is_opening: |
|
417 | if is_opening: | |
428 | opening_post_id = self.id |
|
418 | opening_post_id = self.id | |
429 | else: |
|
419 | else: | |
430 | opening_post_id = thread.get_opening_post_id() |
|
420 | opening_post_id = thread.get_opening_post_id() | |
431 |
|
421 | |||
432 | return render_to_string('boards/post.html', { |
|
422 | return render_to_string('boards/post.html', { | |
433 | PARAMETER_POST: self, |
|
423 | PARAMETER_POST: self, | |
434 | PARAMETER_MODERATOR: moderator, |
|
424 | PARAMETER_MODERATOR: moderator, | |
435 | PARAMETER_IS_OPENING: is_opening, |
|
425 | PARAMETER_IS_OPENING: is_opening, | |
436 | PARAMETER_THREAD: thread, |
|
426 | PARAMETER_THREAD: thread, | |
437 | PARAMETER_BUMPABLE: can_bump, |
|
427 | PARAMETER_BUMPABLE: can_bump, | |
438 | PARAMETER_NEED_OPEN_LINK: need_open_link, |
|
428 | PARAMETER_NEED_OPEN_LINK: need_open_link, | |
439 | PARAMETER_TRUNCATED: truncated, |
|
429 | PARAMETER_TRUNCATED: truncated, | |
440 | PARAMETER_OP_ID: opening_post_id, |
|
430 | PARAMETER_OP_ID: opening_post_id, | |
441 | }) |
|
431 | }) | |
442 |
|
432 | |||
443 | def get_search_view(self, *args, **kwargs): |
|
433 | def get_search_view(self, *args, **kwargs): | |
444 | return self.get_view(args, kwargs) |
|
434 | return self.get_view(args, kwargs) | |
445 |
|
435 | |||
446 | def get_first_image(self) -> PostImage: |
|
436 | def get_first_image(self) -> PostImage: | |
447 | return self.images.earliest('id') |
|
437 | return self.images.earliest('id') | |
448 |
|
438 | |||
449 | def delete(self, using=None): |
|
439 | def delete(self, using=None): | |
450 | """ |
|
440 | """ | |
451 | Deletes all post images and the post itself. |
|
441 | Deletes all post images and the post itself. | |
452 | """ |
|
442 | """ | |
453 |
|
443 | |||
454 | for image in self.images.all(): |
|
444 | for image in self.images.all(): | |
455 | image_refs_count = Post.objects.filter(images__in=[image]).count() |
|
445 | image_refs_count = Post.objects.filter(images__in=[image]).count() | |
456 | if image_refs_count == 1: |
|
446 | if image_refs_count == 1: | |
457 | image.delete() |
|
447 | image.delete() | |
458 |
|
448 | |||
459 | self.signature.all().delete() |
|
449 | self.signature.all().delete() | |
460 | if self.global_id: |
|
450 | if self.global_id: | |
461 | self.global_id.delete() |
|
451 | self.global_id.delete() | |
462 |
|
452 | |||
463 | thread = self.get_thread() |
|
453 | thread = self.get_thread() | |
464 | thread.last_edit_time = timezone.now() |
|
454 | thread.last_edit_time = timezone.now() | |
465 | thread.save() |
|
455 | thread.save() | |
466 |
|
456 | |||
467 | super(Post, self).delete(using) |
|
457 | super(Post, self).delete(using) | |
468 |
|
458 | |||
469 | logging.getLogger('boards.post.delete').info( |
|
459 | logging.getLogger('boards.post.delete').info( | |
470 | 'Deleted post {}'.format(self)) |
|
460 | 'Deleted post {}'.format(self)) | |
471 |
|
461 | |||
472 | def set_global_id(self, key_pair=None): |
|
462 | def set_global_id(self, key_pair=None): | |
473 | """ |
|
463 | """ | |
474 | Sets global id based on the given key pair. If no key pair is given, |
|
464 | Sets global id based on the given key pair. If no key pair is given, | |
475 | default one is used. |
|
465 | default one is used. | |
476 | """ |
|
466 | """ | |
477 |
|
467 | |||
478 | if key_pair: |
|
468 | if key_pair: | |
479 | key = key_pair |
|
469 | key = key_pair | |
480 | else: |
|
470 | else: | |
481 | try: |
|
471 | try: | |
482 | key = KeyPair.objects.get(primary=True) |
|
472 | key = KeyPair.objects.get(primary=True) | |
483 | except KeyPair.DoesNotExist: |
|
473 | except KeyPair.DoesNotExist: | |
484 | # Do not update the global id because there is no key defined |
|
474 | # Do not update the global id because there is no key defined | |
485 | return |
|
475 | return | |
486 | global_id = GlobalId(key_type=key.key_type, |
|
476 | global_id = GlobalId(key_type=key.key_type, | |
487 | key=key.public_key, |
|
477 | key=key.public_key, | |
488 | local_id = self.id) |
|
478 | local_id = self.id) | |
489 | global_id.save() |
|
479 | global_id.save() | |
490 |
|
480 | |||
491 | self.global_id = global_id |
|
481 | self.global_id = global_id | |
492 |
|
482 | |||
493 | self.save(update_fields=['global_id']) |
|
483 | self.save(update_fields=['global_id']) | |
494 |
|
484 | |||
495 | def get_pub_time_epoch(self): |
|
485 | def get_pub_time_epoch(self): | |
496 | return utils.datetime_to_epoch(self.pub_time) |
|
486 | return utils.datetime_to_epoch(self.pub_time) | |
497 |
|
487 | |||
498 | def get_replied_ids(self): |
|
488 | def get_replied_ids(self): | |
499 | """ |
|
489 | """ | |
500 | Gets ID list of the posts that this post replies. |
|
490 | Gets ID list of the posts that this post replies. | |
501 | """ |
|
491 | """ | |
502 |
|
492 | |||
503 | raw_text = self.get_raw_text() |
|
493 | raw_text = self.get_raw_text() | |
504 |
|
494 | |||
505 | local_replied = REGEX_REPLY.findall(raw_text) |
|
495 | local_replied = REGEX_REPLY.findall(raw_text) | |
506 | global_replied = [] |
|
496 | global_replied = [] | |
507 | for match in REGEX_GLOBAL_REPLY.findall(raw_text): |
|
497 | for match in REGEX_GLOBAL_REPLY.findall(raw_text): | |
508 | key_type = match[0] |
|
498 | key_type = match[0] | |
509 | key = match[1] |
|
499 | key = match[1] | |
510 | local_id = match[2] |
|
500 | local_id = match[2] | |
511 |
|
501 | |||
512 | try: |
|
502 | try: | |
513 | global_id = GlobalId.objects.get(key_type=key_type, |
|
503 | global_id = GlobalId.objects.get(key_type=key_type, | |
514 | key=key, local_id=local_id) |
|
504 | key=key, local_id=local_id) | |
515 | for post in Post.objects.filter(global_id=global_id).only('id'): |
|
505 | for post in Post.objects.filter(global_id=global_id).only('id'): | |
516 | global_replied.append(post.id) |
|
506 | global_replied.append(post.id) | |
517 | except GlobalId.DoesNotExist: |
|
507 | except GlobalId.DoesNotExist: | |
518 | pass |
|
508 | pass | |
519 | return local_replied + global_replied |
|
509 | return local_replied + global_replied | |
520 |
|
510 | |||
521 |
|
511 | |||
522 | def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None, |
|
512 | def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None, | |
523 | include_last_update=False): |
|
513 | include_last_update=False): | |
524 | """ |
|
514 | """ | |
525 | Gets post HTML or JSON data that can be rendered on a page or used by |
|
515 | Gets post HTML or JSON data that can be rendered on a page or used by | |
526 | API. |
|
516 | API. | |
527 | """ |
|
517 | """ | |
528 |
|
518 | |||
529 | if format_type == DIFF_TYPE_HTML: |
|
519 | if format_type == DIFF_TYPE_HTML: | |
530 | params = dict() |
|
520 | params = dict() | |
531 | params['post'] = self |
|
521 | params['post'] = self | |
532 | if PARAMETER_TRUNCATED in request.GET: |
|
522 | if PARAMETER_TRUNCATED in request.GET: | |
533 | params[PARAMETER_TRUNCATED] = True |
|
523 | params[PARAMETER_TRUNCATED] = True | |
534 |
|
524 | |||
535 | return render_to_string('boards/api_post.html', params) |
|
525 | return render_to_string('boards/api_post.html', params) | |
536 | elif format_type == DIFF_TYPE_JSON: |
|
526 | elif format_type == DIFF_TYPE_JSON: | |
537 | post_json = { |
|
527 | post_json = { | |
538 | 'id': self.id, |
|
528 | 'id': self.id, | |
539 | 'title': self.title, |
|
529 | 'title': self.title, | |
540 | 'text': self._text_rendered, |
|
530 | 'text': self._text_rendered, | |
541 | } |
|
531 | } | |
542 | if self.images.exists(): |
|
532 | if self.images.exists(): | |
543 | post_image = self.get_first_image() |
|
533 | post_image = self.get_first_image() | |
544 | post_json['image'] = post_image.image.url |
|
534 | post_json['image'] = post_image.image.url | |
545 | post_json['image_preview'] = post_image.image.url_200x150 |
|
535 | post_json['image_preview'] = post_image.image.url_200x150 | |
546 | if include_last_update: |
|
536 | if include_last_update: | |
547 | post_json['bump_time'] = datetime_to_epoch( |
|
537 | post_json['bump_time'] = datetime_to_epoch( | |
548 | self.get_thread().bump_time) |
|
538 | self.get_thread().bump_time) | |
549 | return post_json |
|
539 | return post_json | |
550 |
|
540 | |||
551 | def send_to_websocket(self, request, recursive=True): |
|
541 | def send_to_websocket(self, request, recursive=True): | |
552 | """ |
|
542 | """ | |
553 | Sends post HTML data to the thread web socket. |
|
543 | Sends post HTML data to the thread web socket. | |
554 | """ |
|
544 | """ | |
555 |
|
545 | |||
556 | if not settings.WEBSOCKETS_ENABLED: |
|
546 | if not settings.WEBSOCKETS_ENABLED: | |
557 | return |
|
547 | return | |
558 |
|
548 | |||
559 | client = Client() |
|
549 | client = Client() | |
560 |
|
550 | |||
561 | thread = self.get_thread() |
|
551 | thread = self.get_thread() | |
562 | thread_id = thread.id |
|
552 | thread_id = thread.id | |
563 | channel_name = WS_CHANNEL_THREAD + str(thread.get_opening_post_id()) |
|
553 | channel_name = WS_CHANNEL_THREAD + str(thread.get_opening_post_id()) | |
564 | client.publish(channel_name, { |
|
554 | client.publish(channel_name, { | |
565 | WS_NOTIFICATION_TYPE: WS_NOTIFICATION_TYPE_NEW_POST, |
|
555 | WS_NOTIFICATION_TYPE: WS_NOTIFICATION_TYPE_NEW_POST, | |
566 | }) |
|
556 | }) | |
567 | client.send() |
|
557 | client.send() | |
568 |
|
558 | |||
569 | logger = logging.getLogger('boards.post.websocket') |
|
559 | logger = logging.getLogger('boards.post.websocket') | |
570 |
|
560 | |||
571 | logger.info('Sent notification from post #{} to channel {}'.format( |
|
561 | logger.info('Sent notification from post #{} to channel {}'.format( | |
572 | self.id, channel_name)) |
|
562 | self.id, channel_name)) | |
573 |
|
563 | |||
574 | if recursive: |
|
564 | if recursive: | |
575 | for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()): |
|
565 | for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()): | |
576 | post_id = reply_number.group(1) |
|
566 | post_id = reply_number.group(1) | |
577 | ref_post = Post.objects.filter(id=post_id)[0] |
|
567 | ref_post = Post.objects.filter(id=post_id)[0] | |
578 |
|
568 | |||
579 | # If post is in this thread, its thread was already notified. |
|
569 | # If post is in this thread, its thread was already notified. | |
580 | # Otherwise, notify its thread separately. |
|
570 | # Otherwise, notify its thread separately. | |
581 | if ref_post.get_thread().id != thread_id: |
|
571 | if ref_post.get_thread().id != thread_id: | |
582 | ref_post.send_to_websocket(request, recursive=False) |
|
572 | ref_post.send_to_websocket(request, recursive=False) | |
583 |
|
573 | |||
584 | def save(self, force_insert=False, force_update=False, using=None, |
|
574 | def save(self, force_insert=False, force_update=False, using=None, | |
585 | update_fields=None): |
|
575 | update_fields=None): | |
586 | self._text_rendered = bbcode_extended(self.get_raw_text()) |
|
576 | self._text_rendered = bbcode_extended(self.get_raw_text()) | |
587 |
|
577 | |||
588 | super().save(force_insert, force_update, using, update_fields) |
|
578 | super().save(force_insert, force_update, using, update_fields) | |
589 |
|
579 | |||
590 | def get_text(self) -> str: |
|
580 | def get_text(self) -> str: | |
591 | return self._text_rendered |
|
581 | return self._text_rendered | |
592 |
|
582 | |||
593 | def get_raw_text(self) -> str: |
|
583 | def get_raw_text(self) -> str: | |
594 | return self.text |
|
584 | return self.text | |
595 |
|
585 | |||
596 | def get_absolute_id(self) -> str: |
|
586 | def get_absolute_id(self) -> str: | |
597 | """ |
|
587 | """ | |
598 | If the post has many threads, shows its main thread OP id in the post |
|
588 | If the post has many threads, shows its main thread OP id in the post | |
599 | ID. |
|
589 | ID. | |
600 | """ |
|
590 | """ | |
601 |
|
591 | |||
602 | if self.get_threads().count() > 1: |
|
592 | if self.get_threads().count() > 1: | |
603 | return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id) |
|
593 | return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id) | |
604 | else: |
|
594 | else: | |
605 | return str(self.id) |
|
595 | return str(self.id) | |
606 |
|
596 | |||
607 | def connect_notifications(self): |
|
597 | def connect_notifications(self): | |
608 | for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()): |
|
598 | for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()): | |
609 | user_name = reply_number.group(1).lower() |
|
599 | user_name = reply_number.group(1).lower() | |
610 | Notification.objects.get_or_create(name=user_name, post=self) |
|
600 | Notification.objects.get_or_create(name=user_name, post=self) | |
611 |
|
601 | |||
612 | def connect_replies(self): |
|
602 | def connect_replies(self): | |
613 | """ |
|
603 | """ | |
614 | Connects replies to a post to show them as a reflink map |
|
604 | Connects replies to a post to show them as a reflink map | |
615 | """ |
|
605 | """ | |
616 |
|
606 | |||
617 | for post_id in self.get_replied_ids(): |
|
607 | for post_id in self.get_replied_ids(): | |
618 | ref_post = Post.objects.filter(id=post_id) |
|
608 | ref_post = Post.objects.filter(id=post_id) | |
619 | if ref_post.count() > 0: |
|
609 | if ref_post.count() > 0: | |
620 | referenced_post = ref_post[0] |
|
610 | referenced_post = ref_post[0] | |
621 | referenced_post.referenced_posts.add(self) |
|
611 | referenced_post.referenced_posts.add(self) | |
622 | referenced_post.last_edit_time = self.pub_time |
|
612 | referenced_post.last_edit_time = self.pub_time | |
623 | referenced_post.build_refmap() |
|
613 | referenced_post.build_refmap() | |
624 | referenced_post.save(update_fields=['refmap', 'last_edit_time']) |
|
614 | referenced_post.save(update_fields=['refmap', 'last_edit_time']) | |
625 |
|
615 | |||
626 | referenced_threads = referenced_post.get_threads().all() |
|
616 | referenced_threads = referenced_post.get_threads().all() | |
627 | for thread in referenced_threads: |
|
617 | for thread in referenced_threads: | |
628 | thread.last_edit_time = self.pub_time |
|
618 | thread.last_edit_time = self.pub_time | |
629 | thread.save(update_fields=['last_edit_time']) |
|
619 | thread.save(update_fields=['last_edit_time']) | |
630 |
|
620 | |||
631 | self.threads.add(thread) |
|
621 | self.threads.add(thread) |
@@ -1,76 +1,83 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.filter(thread__archived=False)\ |
|
20 | return self.filter(thread__archived=False)\ | |
21 | .annotate(num_threads=Count('thread')).filter(num_threads__gt=0)\ |
|
21 | .annotate(num_threads=Count('thread')).filter(num_threads__gt=0)\ | |
22 | .order_by('-required', 'name') |
|
22 | .order_by('-required', 'name') | |
23 |
|
23 | |||
|
24 | def get_tag_url_list(self, tags: list) -> str: | |||
|
25 | """ | |||
|
26 | Gets a comma-separated list of tag links. | |||
|
27 | """ | |||
|
28 | ||||
|
29 | return ', '.join([tag.get_view() for tag in tags]) | |||
|
30 | ||||
24 |
|
31 | |||
25 | class Tag(models.Model, Viewable): |
|
32 | class Tag(models.Model, Viewable): | |
26 | """ |
|
33 | """ | |
27 | A tag is a text node assigned to the thread. The tag serves as a board |
|
34 | A tag is a text node assigned to the thread. The tag serves as a board | |
28 | section. There can be multiple tags for each thread |
|
35 | section. There can be multiple tags for each thread | |
29 | """ |
|
36 | """ | |
30 |
|
37 | |||
31 | objects = TagManager() |
|
38 | objects = TagManager() | |
32 |
|
39 | |||
33 | class Meta: |
|
40 | class Meta: | |
34 | app_label = 'boards' |
|
41 | app_label = 'boards' | |
35 | ordering = ('name',) |
|
42 | ordering = ('name',) | |
36 |
|
43 | |||
37 | name = models.CharField(max_length=100, db_index=True, unique=True) |
|
44 | name = models.CharField(max_length=100, db_index=True, unique=True) | |
38 | required = models.BooleanField(default=False, db_index=True) |
|
45 | required = models.BooleanField(default=False, db_index=True) | |
39 |
|
46 | |||
40 | def __str__(self): |
|
47 | def __str__(self): | |
41 | return self.name |
|
48 | return self.name | |
42 |
|
49 | |||
43 | def is_empty(self) -> bool: |
|
50 | def is_empty(self) -> bool: | |
44 | """ |
|
51 | """ | |
45 | Checks if the tag has some threads. |
|
52 | Checks if the tag has some threads. | |
46 | """ |
|
53 | """ | |
47 |
|
54 | |||
48 | return self.get_thread_count() == 0 |
|
55 | return self.get_thread_count() == 0 | |
49 |
|
56 | |||
50 | def get_thread_count(self) -> int: |
|
57 | def get_thread_count(self) -> int: | |
51 | return self.get_threads().count() |
|
58 | return self.get_threads().count() | |
52 |
|
59 | |||
53 | def get_url(self): |
|
60 | def get_url(self): | |
54 | return reverse('tag', kwargs={'tag_name': self.name}) |
|
61 | return reverse('tag', kwargs={'tag_name': self.name}) | |
55 |
|
62 | |||
56 | def get_threads(self): |
|
63 | def get_threads(self): | |
57 | return self.thread_set.order_by('-bump_time') |
|
64 | return self.thread_set.order_by('-bump_time') | |
58 |
|
65 | |||
59 | def is_required(self): |
|
66 | def is_required(self): | |
60 | return self.required |
|
67 | return self.required | |
61 |
|
68 | |||
62 | def get_view(self): |
|
69 | def get_view(self): | |
63 | link = '<a class="tag" href="{}">{}</a>'.format( |
|
70 | link = '<a class="tag" href="{}">{}</a>'.format( | |
64 | self.get_url(), self.name) |
|
71 | self.get_url(), self.name) | |
65 | if self.is_required(): |
|
72 | if self.is_required(): | |
66 | link = '<b>{}</b>'.format(link) |
|
73 | link = '<b>{}</b>'.format(link) | |
67 | return link |
|
74 | return link | |
68 |
|
75 | |||
69 | def get_search_view(self, *args, **kwargs): |
|
76 | def get_search_view(self, *args, **kwargs): | |
70 | return render_to_string('boards/tag.html', { |
|
77 | return render_to_string('boards/tag.html', { | |
71 | 'tag': self, |
|
78 | 'tag': self, | |
72 | }) |
|
79 | }) | |
73 |
|
80 | |||
74 | @cached_result |
|
81 | @cached_result | |
75 | def get_post_count(self): |
|
82 | def get_post_count(self): | |
76 | return self.get_threads().aggregate(num_posts=Count('post'))['num_posts'] |
|
83 | return self.get_threads().aggregate(num_posts=Count('post'))['num_posts'] |
@@ -1,185 +1,191 b'' | |||||
1 | import logging |
|
1 | import logging | |
2 |
|
2 | |||
3 | from django.db.models import Count, Sum |
|
3 | from django.db.models import Count, Sum | |
4 | from django.utils import timezone |
|
4 | from django.utils import timezone | |
5 | from django.db import models |
|
5 | from django.db import models | |
6 |
|
6 | |||
7 | from boards import settings |
|
7 | from boards import settings | |
|
8 | import boards | |||
8 | from boards.utils import cached_result |
|
9 | from boards.utils import cached_result | |
9 | from boards.models.post import Post |
|
10 | from boards.models.post import Post | |
10 |
|
11 | |||
11 |
|
12 | |||
12 | __author__ = 'neko259' |
|
13 | __author__ = 'neko259' | |
13 |
|
14 | |||
14 |
|
15 | |||
15 | logger = logging.getLogger(__name__) |
|
16 | logger = logging.getLogger(__name__) | |
16 |
|
17 | |||
17 |
|
18 | |||
18 | class ThreadManager(models.Manager): |
|
19 | class ThreadManager(models.Manager): | |
19 | def process_oldest_threads(self): |
|
20 | def process_oldest_threads(self): | |
20 | """ |
|
21 | """ | |
21 | Preserves maximum thread count. If there are too many threads, |
|
22 | Preserves maximum thread count. If there are too many threads, | |
22 | archive or delete the old ones. |
|
23 | archive or delete the old ones. | |
23 | """ |
|
24 | """ | |
24 |
|
25 | |||
25 | threads = Thread.objects.filter(archived=False).order_by('-bump_time') |
|
26 | threads = Thread.objects.filter(archived=False).order_by('-bump_time') | |
26 | thread_count = threads.count() |
|
27 | thread_count = threads.count() | |
27 |
|
28 | |||
28 | if thread_count > settings.MAX_THREAD_COUNT: |
|
29 | if thread_count > settings.MAX_THREAD_COUNT: | |
29 | num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT |
|
30 | num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT | |
30 | old_threads = threads[thread_count - num_threads_to_delete:] |
|
31 | old_threads = threads[thread_count - num_threads_to_delete:] | |
31 |
|
32 | |||
32 | for thread in old_threads: |
|
33 | for thread in old_threads: | |
33 | if settings.ARCHIVE_THREADS: |
|
34 | if settings.ARCHIVE_THREADS: | |
34 | self._archive_thread(thread) |
|
35 | self._archive_thread(thread) | |
35 | else: |
|
36 | else: | |
36 | thread.delete() |
|
37 | thread.delete() | |
37 |
|
38 | |||
38 | logger.info('Processed %d old threads' % num_threads_to_delete) |
|
39 | logger.info('Processed %d old threads' % num_threads_to_delete) | |
39 |
|
40 | |||
40 | def _archive_thread(self, thread): |
|
41 | def _archive_thread(self, thread): | |
41 | thread.archived = True |
|
42 | thread.archived = True | |
42 | thread.bumpable = False |
|
43 | thread.bumpable = False | |
43 | thread.last_edit_time = timezone.now() |
|
44 | thread.last_edit_time = timezone.now() | |
|
45 | thread.update_posts_time() | |||
44 | thread.save(update_fields=['archived', 'last_edit_time', 'bumpable']) |
|
46 | thread.save(update_fields=['archived', 'last_edit_time', 'bumpable']) | |
45 |
|
47 | |||
46 |
|
48 | |||
47 | class Thread(models.Model): |
|
49 | class Thread(models.Model): | |
48 | objects = ThreadManager() |
|
50 | objects = ThreadManager() | |
49 |
|
51 | |||
50 | class Meta: |
|
52 | class Meta: | |
51 | app_label = 'boards' |
|
53 | app_label = 'boards' | |
52 |
|
54 | |||
53 | tags = models.ManyToManyField('Tag') |
|
55 | tags = models.ManyToManyField('Tag') | |
54 | bump_time = models.DateTimeField(db_index=True) |
|
56 | bump_time = models.DateTimeField(db_index=True) | |
55 | last_edit_time = models.DateTimeField() |
|
57 | last_edit_time = models.DateTimeField() | |
56 | archived = models.BooleanField(default=False) |
|
58 | archived = models.BooleanField(default=False) | |
57 | bumpable = models.BooleanField(default=True) |
|
59 | bumpable = models.BooleanField(default=True) | |
58 |
|
60 | |||
59 | def get_tags(self): |
|
61 | def get_tags(self): | |
60 | """ |
|
62 | """ | |
61 | Gets a sorted tag list. |
|
63 | Gets a sorted tag list. | |
62 | """ |
|
64 | """ | |
63 |
|
65 | |||
64 | return self.tags.order_by('name') |
|
66 | return self.tags.order_by('name') | |
65 |
|
67 | |||
66 | def bump(self): |
|
68 | def bump(self): | |
67 | """ |
|
69 | """ | |
68 | Bumps (moves to up) thread if possible. |
|
70 | Bumps (moves to up) thread if possible. | |
69 | """ |
|
71 | """ | |
70 |
|
72 | |||
71 | if self.can_bump(): |
|
73 | if self.can_bump(): | |
72 |
self.bump_time = |
|
74 | self.bump_time = self.last_edit_time | |
73 |
|
75 | |||
74 | if self.get_reply_count() >= settings.MAX_POSTS_PER_THREAD: |
|
76 | if self.get_reply_count() >= settings.MAX_POSTS_PER_THREAD: | |
75 | self.bumpable = False |
|
77 | self.bumpable = False | |
|
78 | self.update_posts_time() | |||
76 |
|
79 | |||
77 | logger.info('Bumped thread %d' % self.id) |
|
80 | logger.info('Bumped thread %d' % self.id) | |
78 |
|
81 | |||
79 | def get_reply_count(self): |
|
82 | def get_reply_count(self): | |
80 | return self.get_replies().count() |
|
83 | return self.get_replies().count() | |
81 |
|
84 | |||
82 | def get_images_count(self): |
|
85 | def get_images_count(self): | |
83 | return self.get_replies().annotate(images_count=Count( |
|
86 | return self.get_replies().annotate(images_count=Count( | |
84 | 'images')).aggregate(Sum('images_count'))['images_count__sum'] |
|
87 | 'images')).aggregate(Sum('images_count'))['images_count__sum'] | |
85 |
|
88 | |||
86 | def can_bump(self): |
|
89 | def can_bump(self): | |
87 | """ |
|
90 | """ | |
88 | Checks if the thread can be bumped by replying to it. |
|
91 | Checks if the thread can be bumped by replying to it. | |
89 | """ |
|
92 | """ | |
90 |
|
93 | |||
91 | return self.bumpable |
|
94 | return self.bumpable and not self.archived | |
92 |
|
95 | |||
93 | def get_last_replies(self): |
|
96 | def get_last_replies(self): | |
94 | """ |
|
97 | """ | |
95 | Gets several last replies, not including opening post |
|
98 | Gets several last replies, not including opening post | |
96 | """ |
|
99 | """ | |
97 |
|
100 | |||
98 | if settings.LAST_REPLIES_COUNT > 0: |
|
101 | if settings.LAST_REPLIES_COUNT > 0: | |
99 | reply_count = self.get_reply_count() |
|
102 | reply_count = self.get_reply_count() | |
100 |
|
103 | |||
101 | if reply_count > 0: |
|
104 | if reply_count > 0: | |
102 | reply_count_to_show = min(settings.LAST_REPLIES_COUNT, |
|
105 | reply_count_to_show = min(settings.LAST_REPLIES_COUNT, | |
103 | reply_count - 1) |
|
106 | reply_count - 1) | |
104 | replies = self.get_replies() |
|
107 | replies = self.get_replies() | |
105 | last_replies = replies[reply_count - reply_count_to_show:] |
|
108 | last_replies = replies[reply_count - reply_count_to_show:] | |
106 |
|
109 | |||
107 | return last_replies |
|
110 | return last_replies | |
108 |
|
111 | |||
109 | def get_skipped_replies_count(self): |
|
112 | def get_skipped_replies_count(self): | |
110 | """ |
|
113 | """ | |
111 | Gets number of posts between opening post and last replies. |
|
114 | Gets number of posts between opening post and last replies. | |
112 | """ |
|
115 | """ | |
113 | reply_count = self.get_reply_count() |
|
116 | reply_count = self.get_reply_count() | |
114 | last_replies_count = min(settings.LAST_REPLIES_COUNT, |
|
117 | last_replies_count = min(settings.LAST_REPLIES_COUNT, | |
115 | reply_count - 1) |
|
118 | reply_count - 1) | |
116 | return reply_count - last_replies_count - 1 |
|
119 | return reply_count - last_replies_count - 1 | |
117 |
|
120 | |||
118 | def get_replies(self, view_fields_only=False): |
|
121 | def get_replies(self, view_fields_only=False): | |
119 | """ |
|
122 | """ | |
120 | Gets sorted thread posts |
|
123 | Gets sorted thread posts | |
121 | """ |
|
124 | """ | |
122 |
|
125 | |||
123 | query = Post.objects.filter(threads__in=[self]) |
|
126 | query = Post.objects.filter(threads__in=[self]) | |
124 | query = query.order_by('pub_time').prefetch_related('images', 'thread', 'threads') |
|
127 | query = query.order_by('pub_time').prefetch_related('images', 'thread', 'threads') | |
125 | if view_fields_only: |
|
128 | if view_fields_only: | |
126 | query = query.defer('poster_user_agent') |
|
129 | query = query.defer('poster_user_agent') | |
127 | return query.all() |
|
130 | return query.all() | |
128 |
|
131 | |||
129 | def get_replies_with_images(self, view_fields_only=False): |
|
132 | def get_replies_with_images(self, view_fields_only=False): | |
130 | """ |
|
133 | """ | |
131 | Gets replies that have at least one image attached |
|
134 | Gets replies that have at least one image attached | |
132 | """ |
|
135 | """ | |
133 |
|
136 | |||
134 | return self.get_replies(view_fields_only).annotate(images_count=Count( |
|
137 | return self.get_replies(view_fields_only).annotate(images_count=Count( | |
135 | 'images')).filter(images_count__gt=0) |
|
138 | 'images')).filter(images_count__gt=0) | |
136 |
|
139 | |||
137 | def add_tag(self, tag): |
|
140 | def add_tag(self, tag): | |
138 | """ |
|
141 | """ | |
139 | Connects thread to a tag and tag to a thread |
|
142 | Connects thread to a tag and tag to a thread | |
140 | """ |
|
143 | """ | |
141 |
|
144 | |||
142 | self.tags.add(tag) |
|
145 | self.tags.add(tag) | |
143 |
|
146 | |||
144 | def get_opening_post(self, only_id=False): |
|
147 | def get_opening_post(self, only_id=False): | |
145 | """ |
|
148 | """ | |
146 | Gets the first post of the thread |
|
149 | Gets the first post of the thread | |
147 | """ |
|
150 | """ | |
148 |
|
151 | |||
149 | query = self.get_replies().order_by('pub_time') |
|
152 | query = self.get_replies().order_by('pub_time') | |
150 | if only_id: |
|
153 | if only_id: | |
151 | query = query.only('id') |
|
154 | query = query.only('id') | |
152 | opening_post = query.first() |
|
155 | opening_post = query.first() | |
153 |
|
156 | |||
154 | return opening_post |
|
157 | return opening_post | |
155 |
|
158 | |||
156 | @cached_result |
|
159 | @cached_result | |
157 | def get_opening_post_id(self): |
|
160 | def get_opening_post_id(self): | |
158 | """ |
|
161 | """ | |
159 | Gets ID of the first thread post. |
|
162 | Gets ID of the first thread post. | |
160 | """ |
|
163 | """ | |
161 |
|
164 | |||
162 | return self.get_opening_post(only_id=True).id |
|
165 | return self.get_opening_post(only_id=True).id | |
163 |
|
166 | |||
164 | def __unicode__(self): |
|
|||
165 | return str(self.id) |
|
|||
166 |
|
||||
167 | def get_pub_time(self): |
|
167 | def get_pub_time(self): | |
168 | """ |
|
168 | """ | |
169 | Gets opening post's pub time because thread does not have its own one. |
|
169 | Gets opening post's pub time because thread does not have its own one. | |
170 | """ |
|
170 | """ | |
171 |
|
171 | |||
172 | return self.get_opening_post().pub_time |
|
172 | return self.get_opening_post().pub_time | |
173 |
|
173 | |||
174 | def delete(self, using=None): |
|
174 | def delete(self, using=None): | |
175 | """ |
|
175 | """ | |
176 | Deletes thread with all replies. |
|
176 | Deletes thread with all replies. | |
177 | """ |
|
177 | """ | |
178 |
|
178 | |||
179 | for reply in self.get_replies().all(): |
|
179 | for reply in self.get_replies().all(): | |
180 | reply.delete() |
|
180 | reply.delete() | |
181 |
|
181 | |||
182 | super(Thread, self).delete(using) |
|
182 | super(Thread, self).delete(using) | |
183 |
|
183 | |||
184 | def __str__(self): |
|
184 | def __str__(self): | |
185 | return 'T#{}/{}'.format(self.id, self.get_opening_post_id()) |
|
185 | return 'T#{}/{}'.format(self.id, self.get_opening_post_id()) | |
|
186 | ||||
|
187 | def get_tag_url_list(self): | |||
|
188 | return boards.models.Tag.objects.get_tag_url_list(self.get_tags()) | |||
|
189 | ||||
|
190 | def update_posts_time(self): | |||
|
191 | self.post_set.update(last_edit_time=self.last_edit_time) |
@@ -1,93 +1,99 b'' | |||||
1 | /* |
|
1 | /* | |
2 | @licstart The following is the entire license notice for the |
|
2 | @licstart The following is the entire license notice for the | |
3 | JavaScript code in this page. |
|
3 | JavaScript code in this page. | |
4 |
|
4 | |||
5 |
|
5 | |||
6 | Copyright (C) 2013 neko259 |
|
6 | Copyright (C) 2013 neko259 | |
7 |
|
7 | |||
8 | The JavaScript code in this page is free software: you can |
|
8 | The JavaScript code in this page is free software: you can | |
9 | redistribute it and/or modify it under the terms of the GNU |
|
9 | redistribute it and/or modify it under the terms of the GNU | |
10 | General Public License (GNU GPL) as published by the Free Software |
|
10 | General Public License (GNU GPL) as published by the Free Software | |
11 | Foundation, either version 3 of the License, or (at your option) |
|
11 | Foundation, either version 3 of the License, or (at your option) | |
12 | any later version. The code is distributed WITHOUT ANY WARRANTY; |
|
12 | any later version. The code is distributed WITHOUT ANY WARRANTY; | |
13 | without even the implied warranty of MERCHANTABILITY or FITNESS |
|
13 | without even the implied warranty of MERCHANTABILITY or FITNESS | |
14 | FOR A PARTICULAR PURPOSE. See the GNU GPL for more details. |
|
14 | FOR A PARTICULAR PURPOSE. See the GNU GPL for more details. | |
15 |
|
15 | |||
16 | As additional permission under GNU GPL version 3 section 7, you |
|
16 | As additional permission under GNU GPL version 3 section 7, you | |
17 | may distribute non-source (e.g., minimized or compacted) forms of |
|
17 | may distribute non-source (e.g., minimized or compacted) forms of | |
18 | that code without the copy of the GNU GPL normally required by |
|
18 | that code without the copy of the GNU GPL normally required by | |
19 | section 4, provided you include this license notice and a URL |
|
19 | section 4, provided you include this license notice and a URL | |
20 | through which recipients can access the Corresponding Source. |
|
20 | through which recipients can access the Corresponding Source. | |
21 |
|
21 | |||
22 | @licend The above is the entire license notice |
|
22 | @licend The above is the entire license notice | |
23 | for the JavaScript code in this page. |
|
23 | for the JavaScript code in this page. | |
24 | */ |
|
24 | */ | |
25 |
|
25 | |||
|
26 | if (window.Intl) { | |||
26 | var LOCALE = window.navigator.language; |
|
27 | var LOCALE = window.navigator.language; | |
27 | var FORMATTER = new Intl.DateTimeFormat( |
|
28 | var FORMATTER = new Intl.DateTimeFormat( | |
28 | LOCALE, |
|
29 | LOCALE, | |
29 | { |
|
30 | { | |
30 | weekday: 'short', year: 'numeric', month: 'short', day: 'numeric', |
|
31 | weekday: 'short', year: 'numeric', month: 'short', day: 'numeric', | |
31 | hour: 'numeric', minute: '2-digit', second: '2-digit' |
|
32 | hour: 'numeric', minute: '2-digit', second: '2-digit' | |
32 | } |
|
33 | } | |
33 | ); |
|
34 | ); | |
|
35 | } | |||
34 |
|
36 | |||
35 | /** |
|
37 | /** | |
36 | * An email is a hidden file to prevent spam bots from posting. It has to be |
|
38 | * An email is a hidden file to prevent spam bots from posting. It has to be | |
37 | * hidden. |
|
39 | * hidden. | |
38 | */ |
|
40 | */ | |
39 | function hideEmailFromForm() { |
|
41 | function hideEmailFromForm() { | |
40 | $('.form-email').parent().parent().hide(); |
|
42 | $('.form-email').parent().parent().hide(); | |
41 | } |
|
43 | } | |
42 |
|
44 | |||
43 | /** |
|
45 | /** | |
44 | * Highlight code blocks with code highlighter |
|
46 | * Highlight code blocks with code highlighter | |
45 | */ |
|
47 | */ | |
46 | function highlightCode(node) { |
|
48 | function highlightCode(node) { | |
47 | node.find('pre code').each(function(i, e) { |
|
49 | node.find('pre code').each(function(i, e) { | |
48 | hljs.highlightBlock(e); |
|
50 | hljs.highlightBlock(e); | |
49 | }); |
|
51 | }); | |
50 | } |
|
52 | } | |
51 |
|
53 | |||
52 | /** |
|
54 | /** | |
53 | * Translate timestamps to local ones for all <time> tags inside node. |
|
55 | * Translate timestamps to local ones for all <time> tags inside node. | |
54 | */ |
|
56 | */ | |
55 | function translate_time(node) { |
|
57 | function translate_time(node) { | |
|
58 | if (window.Intl === null) { | |||
|
59 | return; | |||
|
60 | } | |||
|
61 | ||||
56 | var elements; |
|
62 | var elements; | |
57 |
|
63 | |||
58 | if (node === null) { |
|
64 | if (node === null) { | |
59 | elements = $('time'); |
|
65 | elements = $('time'); | |
60 | } else { |
|
66 | } else { | |
61 | elements = node.find('time'); |
|
67 | elements = node.find('time'); | |
62 | } |
|
68 | } | |
63 |
|
69 | |||
64 | if (!elements.length) { |
|
70 | if (!elements.length) { | |
65 | return; |
|
71 | return; | |
66 | } |
|
72 | } | |
67 |
|
73 | |||
68 | elements.each(function() { |
|
74 | elements.each(function() { | |
69 | var element = $(this); |
|
75 | var element = $(this); | |
70 | var dateAttr = element.attr('datetime'); |
|
76 | var dateAttr = element.attr('datetime'); | |
71 | if (dateAttr) { |
|
77 | if (dateAttr) { | |
72 | var date = new Date(dateAttr); |
|
78 | var date = new Date(dateAttr); | |
73 | element.text(FORMATTER.format(date)); |
|
79 | element.text(FORMATTER.format(date)); | |
74 | } |
|
80 | } | |
75 | }); |
|
81 | }); | |
76 | } |
|
82 | } | |
77 |
|
83 | |||
78 | $( document ).ready(function() { |
|
84 | $( document ).ready(function() { | |
79 | hideEmailFromForm(); |
|
85 | hideEmailFromForm(); | |
80 |
|
86 | |||
81 | $("a[href='#top']").click(function() { |
|
87 | $("a[href='#top']").click(function() { | |
82 | $("html, body").animate({ scrollTop: 0 }, "slow"); |
|
88 | $("html, body").animate({ scrollTop: 0 }, "slow"); | |
83 | return false; |
|
89 | return false; | |
84 | }); |
|
90 | }); | |
85 |
|
91 | |||
86 | addImgPreview(); |
|
92 | addImgPreview(); | |
87 |
|
93 | |||
88 | addRefLinkPreview(); |
|
94 | addRefLinkPreview(); | |
89 |
|
95 | |||
90 | highlightCode($(document)); |
|
96 | highlightCode($(document)); | |
91 |
|
97 | |||
92 | translate_time(null); |
|
98 | translate_time(null); | |
93 | }); |
|
99 | }); |
@@ -1,334 +1,335 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 wsUser = ''; |
|
26 | var wsUser = ''; | |
27 |
|
27 | |||
28 | var unreadPosts = 0; |
|
28 | var unreadPosts = 0; | |
29 | var documentOriginalTitle = ''; |
|
29 | var documentOriginalTitle = ''; | |
30 |
|
30 | |||
31 | // Thread ID does not change, can be stored one time |
|
31 | // Thread ID does not change, can be stored one time | |
32 | var threadId = $('div.thread').children('.post').first().attr('id'); |
|
32 | var threadId = $('div.thread').children('.post').first().attr('id'); | |
33 |
|
33 | |||
34 | /** |
|
34 | /** | |
35 | * Connect to websocket server and subscribe to thread updates. On any update we |
|
35 | * Connect to websocket server and subscribe to thread updates. On any update we | |
36 | * request a thread diff. |
|
36 | * request a thread diff. | |
37 | * |
|
37 | * | |
38 | * @returns {boolean} true if connected, false otherwise |
|
38 | * @returns {boolean} true if connected, false otherwise | |
39 | */ |
|
39 | */ | |
40 | function connectWebsocket() { |
|
40 | function connectWebsocket() { | |
41 | var metapanel = $('.metapanel')[0]; |
|
41 | var metapanel = $('.metapanel')[0]; | |
42 |
|
42 | |||
43 | var wsHost = metapanel.getAttribute('data-ws-host'); |
|
43 | var wsHost = metapanel.getAttribute('data-ws-host'); | |
44 | var wsPort = metapanel.getAttribute('data-ws-port'); |
|
44 | var wsPort = metapanel.getAttribute('data-ws-port'); | |
45 |
|
45 | |||
46 | if (wsHost.length > 0 && wsPort.length > 0) |
|
46 | if (wsHost.length > 0 && wsPort.length > 0) | |
47 | var centrifuge = new Centrifuge({ |
|
47 | var centrifuge = new Centrifuge({ | |
48 | "url": 'ws://' + wsHost + ':' + wsPort + "/connection/websocket", |
|
48 | "url": 'ws://' + wsHost + ':' + wsPort + "/connection/websocket", | |
49 | "project": metapanel.getAttribute('data-ws-project'), |
|
49 | "project": metapanel.getAttribute('data-ws-project'), | |
50 | "user": wsUser, |
|
50 | "user": wsUser, | |
51 | "timestamp": metapanel.getAttribute('data-last-update'), |
|
51 | "timestamp": metapanel.getAttribute('data-last-update'), | |
52 | "token": metapanel.getAttribute('data-ws-token'), |
|
52 | "token": metapanel.getAttribute('data-ws-token'), | |
53 | "debug": false |
|
53 | "debug": false | |
54 | }); |
|
54 | }); | |
55 |
|
55 | |||
56 | centrifuge.on('error', function(error_message) { |
|
56 | centrifuge.on('error', function(error_message) { | |
57 | console.log("Error connecting to websocket server."); |
|
57 | console.log("Error connecting to websocket server."); | |
58 | return false; |
|
58 | return false; | |
59 | }); |
|
59 | }); | |
60 |
|
60 | |||
61 | centrifuge.on('connect', function() { |
|
61 | centrifuge.on('connect', function() { | |
62 | var channelName = 'thread:' + threadId; |
|
62 | var channelName = 'thread:' + threadId; | |
63 | centrifuge.subscribe(channelName, function(message) { |
|
63 | centrifuge.subscribe(channelName, function(message) { | |
64 | getThreadDiff(); |
|
64 | getThreadDiff(); | |
65 | }); |
|
65 | }); | |
66 |
|
66 | |||
67 | // For the case we closed the browser and missed some updates |
|
67 | // For the case we closed the browser and missed some updates | |
68 | getThreadDiff(); |
|
68 | getThreadDiff(); | |
69 | $('#autoupdate').hide(); |
|
69 | $('#autoupdate').hide(); | |
70 | }); |
|
70 | }); | |
71 |
|
71 | |||
72 | centrifuge.connect(); |
|
72 | centrifuge.connect(); | |
73 |
|
73 | |||
74 | return true; |
|
74 | return true; | |
75 | } |
|
75 | } | |
76 |
|
76 | |||
77 | /** |
|
77 | /** | |
78 | * Get diff of the posts from the current thread timestamp. |
|
78 | * Get diff of the posts from the current thread timestamp. | |
79 | * This is required if the browser was closed and some post updates were |
|
79 | * This is required if the browser was closed and some post updates were | |
80 | * missed. |
|
80 | * missed. | |
81 | */ |
|
81 | */ | |
82 | function getThreadDiff() { |
|
82 | function getThreadDiff() { | |
83 | var lastUpdateTime = $('.metapanel').attr('data-last-update'); |
|
83 | var lastUpdateTime = $('.metapanel').attr('data-last-update'); | |
84 |
|
84 | |||
85 | var diffUrl = '/api/diff_thread/' + threadId + '/' + lastUpdateTime + '/'; |
|
85 | var diffUrl = '/api/diff_thread/' + threadId + '/' + lastUpdateTime + '/'; | |
86 |
|
86 | |||
87 | $.getJSON(diffUrl) |
|
87 | $.getJSON(diffUrl) | |
88 | .success(function(data) { |
|
88 | .success(function(data) { | |
89 | var addedPosts = data.added; |
|
89 | var addedPosts = data.added; | |
90 |
|
90 | |||
91 | for (var i = 0; i < addedPosts.length; i++) { |
|
91 | for (var i = 0; i < addedPosts.length; i++) { | |
92 | var postText = addedPosts[i]; |
|
92 | var postText = addedPosts[i]; | |
93 | var post = $(postText); |
|
93 | var post = $(postText); | |
94 |
|
94 | |||
95 | updatePost(post) |
|
95 | updatePost(post) | |
96 | } |
|
96 | } | |
97 |
|
97 | |||
98 | var updatedPosts = data.updated; |
|
98 | var updatedPosts = data.updated; | |
99 |
|
99 | |||
100 | for (var i = 0; i < updatedPosts.length; i++) { |
|
100 | for (var i = 0; i < updatedPosts.length; i++) { | |
101 | var postText = updatedPosts[i]; |
|
101 | var postText = updatedPosts[i]; | |
102 | var post = $(postText); |
|
102 | var post = $(postText); | |
103 |
|
103 | |||
104 | updatePost(post) |
|
104 | updatePost(post) | |
105 | } |
|
105 | } | |
106 |
|
106 | |||
107 | // TODO Process removed posts if any |
|
107 | // TODO Process removed posts if any | |
108 | $('.metapanel').attr('data-last-update', data.last_update); |
|
108 | $('.metapanel').attr('data-last-update', data.last_update); | |
109 | }) |
|
109 | }) | |
110 | } |
|
110 | } | |
111 |
|
111 | |||
112 | /** |
|
112 | /** | |
113 | * Add or update the post on html page. |
|
113 | * Add or update the post on html page. | |
114 | */ |
|
114 | */ | |
115 | function updatePost(postHtml) { |
|
115 | function updatePost(postHtml) { | |
116 | // This needs to be set on start because the page is scrolled after posts |
|
116 | // This needs to be set on start because the page is scrolled after posts | |
117 | // are added or updated |
|
117 | // are added or updated | |
118 | var bottom = isPageBottom(); |
|
118 | var bottom = isPageBottom(); | |
119 |
|
119 | |||
120 | var post = $(postHtml); |
|
120 | var post = $(postHtml); | |
121 |
|
121 | |||
122 | var threadBlock = $('div.thread'); |
|
122 | var threadBlock = $('div.thread'); | |
123 |
|
123 | |||
124 | var lastUpdate = ''; |
|
124 | var lastUpdate = ''; | |
125 |
|
125 | |||
126 | var postId = post.attr('id'); |
|
126 | var postId = post.attr('id'); | |
127 |
|
127 | |||
128 | // If the post already exists, replace it. Otherwise add as a new one. |
|
128 | // If the post already exists, replace it. Otherwise add as a new one. | |
129 | var existingPosts = threadBlock.children('.post[id=' + postId + ']'); |
|
129 | var existingPosts = threadBlock.children('.post[id=' + postId + ']'); | |
130 |
|
130 | |||
131 | if (existingPosts.size() > 0) { |
|
131 | if (existingPosts.size() > 0) { | |
132 | existingPosts.replaceWith(post); |
|
132 | existingPosts.replaceWith(post); | |
133 | } else { |
|
133 | } else { | |
134 | var threadPosts = threadBlock.children('.post'); |
|
134 | var threadPosts = threadBlock.children('.post'); | |
135 | var lastPost = threadPosts.last(); |
|
135 | var lastPost = threadPosts.last(); | |
136 |
|
136 | |||
137 | post.appendTo(lastPost.parent()); |
|
137 | post.appendTo(lastPost.parent()); | |
138 |
|
138 | |||
139 | updateBumplimitProgress(1); |
|
139 | updateBumplimitProgress(1); | |
140 | showNewPostsTitle(1); |
|
140 | showNewPostsTitle(1); | |
141 |
|
141 | |||
142 | lastUpdate = post.children('.post-info').first() |
|
142 | lastUpdate = post.children('.post-info').first() | |
143 | .children('.pub_time').first().html(); |
|
143 | .children('.pub_time').first().html(); | |
144 |
|
144 | |||
145 | if (bottom) { |
|
145 | if (bottom) { | |
146 | scrollToBottom(); |
|
146 | scrollToBottom(); | |
147 | } |
|
147 | } | |
148 | } |
|
148 | } | |
149 |
|
149 | |||
150 | processNewPost(post); |
|
150 | processNewPost(post); | |
151 | updateMetadataPanel(lastUpdate) |
|
151 | updateMetadataPanel(lastUpdate) | |
152 | } |
|
152 | } | |
153 |
|
153 | |||
154 | /** |
|
154 | /** | |
155 | * Initiate a blinking animation on a node to show it was updated. |
|
155 | * Initiate a blinking animation on a node to show it was updated. | |
156 | */ |
|
156 | */ | |
157 | function blink(node) { |
|
157 | function blink(node) { | |
158 | var blinkCount = 2; |
|
158 | var blinkCount = 2; | |
159 |
|
159 | |||
160 | var nodeToAnimate = node; |
|
160 | var nodeToAnimate = node; | |
161 | for (var i = 0; i < blinkCount; i++) { |
|
161 | for (var i = 0; i < blinkCount; i++) { | |
162 | nodeToAnimate = nodeToAnimate.fadeTo('fast', 0.5).fadeTo('fast', 1.0); |
|
162 | nodeToAnimate = nodeToAnimate.fadeTo('fast', 0.5).fadeTo('fast', 1.0); | |
163 | } |
|
163 | } | |
164 | } |
|
164 | } | |
165 |
|
165 | |||
166 | function isPageBottom() { |
|
166 | function isPageBottom() { | |
167 | var scroll = $(window).scrollTop() / ($(document).height() |
|
167 | var scroll = $(window).scrollTop() / ($(document).height() | |
168 | - $(window).height()); |
|
168 | - $(window).height()); | |
169 |
|
169 | |||
170 | return scroll == 1 |
|
170 | return scroll == 1 | |
171 | } |
|
171 | } | |
172 |
|
172 | |||
173 | function initAutoupdate() { |
|
173 | function initAutoupdate() { | |
174 | return connectWebsocket(); |
|
174 | return connectWebsocket(); | |
175 | } |
|
175 | } | |
176 |
|
176 | |||
177 | function getReplyCount() { |
|
177 | function getReplyCount() { | |
178 | return $('.thread').children('.post').length |
|
178 | return $('.thread').children('.post').length | |
179 | } |
|
179 | } | |
180 |
|
180 | |||
181 | function getImageCount() { |
|
181 | function getImageCount() { | |
182 | return $('.thread').find('img').length |
|
182 | return $('.thread').find('img').length | |
183 | } |
|
183 | } | |
184 |
|
184 | |||
185 | /** |
|
185 | /** | |
186 | * Update post count, images count and last update time in the metadata |
|
186 | * Update post count, images count and last update time in the metadata | |
187 | * panel. |
|
187 | * panel. | |
188 | */ |
|
188 | */ | |
189 | function updateMetadataPanel(lastUpdate) { |
|
189 | function updateMetadataPanel(lastUpdate) { | |
190 | var replyCountField = $('#reply-count'); |
|
190 | var replyCountField = $('#reply-count'); | |
191 | var imageCountField = $('#image-count'); |
|
191 | var imageCountField = $('#image-count'); | |
192 |
|
192 | |||
193 | replyCountField.text(getReplyCount()); |
|
193 | replyCountField.text(getReplyCount()); | |
194 | imageCountField.text(getImageCount()); |
|
194 | imageCountField.text(getImageCount()); | |
195 |
|
195 | |||
196 | if (lastUpdate !== '') { |
|
196 | if (lastUpdate !== '') { | |
197 | var lastUpdateField = $('#last-update'); |
|
197 | var lastUpdateField = $('#last-update'); | |
198 | lastUpdateField.html(lastUpdate); |
|
198 | lastUpdateField.html(lastUpdate); | |
199 | translate_time(lastUpdateField); |
|
199 | translate_time(lastUpdateField); | |
200 | blink(lastUpdateField); |
|
200 | blink(lastUpdateField); | |
201 | } |
|
201 | } | |
202 |
|
202 | |||
203 | blink(replyCountField); |
|
203 | blink(replyCountField); | |
204 | blink(imageCountField); |
|
204 | blink(imageCountField); | |
205 | } |
|
205 | } | |
206 |
|
206 | |||
207 | /** |
|
207 | /** | |
208 | * Update bumplimit progress bar |
|
208 | * Update bumplimit progress bar | |
209 | */ |
|
209 | */ | |
210 | function updateBumplimitProgress(postDelta) { |
|
210 | function updateBumplimitProgress(postDelta) { | |
211 | var progressBar = $('#bumplimit_progress'); |
|
211 | var progressBar = $('#bumplimit_progress'); | |
212 | if (progressBar) { |
|
212 | if (progressBar) { | |
213 | var postsToLimitElement = $('#left_to_limit'); |
|
213 | var postsToLimitElement = $('#left_to_limit'); | |
214 |
|
214 | |||
215 | var oldPostsToLimit = parseInt(postsToLimitElement.text()); |
|
215 | var oldPostsToLimit = parseInt(postsToLimitElement.text()); | |
216 | var postCount = getReplyCount(); |
|
216 | var postCount = getReplyCount(); | |
217 | var bumplimit = postCount - postDelta + oldPostsToLimit; |
|
217 | var bumplimit = postCount - postDelta + oldPostsToLimit; | |
218 |
|
218 | |||
219 | var newPostsToLimit = bumplimit - postCount; |
|
219 | var newPostsToLimit = bumplimit - postCount; | |
220 | if (newPostsToLimit <= 0) { |
|
220 | if (newPostsToLimit <= 0) { | |
221 | $('.bar-bg').remove(); |
|
221 | $('.bar-bg').remove(); | |
222 | $('.thread').children('.post').addClass('dead_post'); |
|
|||
223 | } else { |
|
222 | } else { | |
224 | postsToLimitElement.text(newPostsToLimit); |
|
223 | postsToLimitElement.text(newPostsToLimit); | |
225 | progressBar.width((100 - postCount / bumplimit * 100.0) + '%'); |
|
224 | progressBar.width((100 - postCount / bumplimit * 100.0) + '%'); | |
226 | } |
|
225 | } | |
227 | } |
|
226 | } | |
228 | } |
|
227 | } | |
229 |
|
228 | |||
230 | /** |
|
229 | /** | |
231 | * Show 'new posts' text in the title if the document is not visible to a user |
|
230 | * Show 'new posts' text in the title if the document is not visible to a user | |
232 | */ |
|
231 | */ | |
233 | function showNewPostsTitle(newPostCount) { |
|
232 | function showNewPostsTitle(newPostCount) { | |
234 | if (document.hidden) { |
|
233 | if (document.hidden) { | |
235 | if (documentOriginalTitle === '') { |
|
234 | if (documentOriginalTitle === '') { | |
236 | documentOriginalTitle = document.title; |
|
235 | documentOriginalTitle = document.title; | |
237 | } |
|
236 | } | |
238 | unreadPosts = unreadPosts + newPostCount; |
|
237 | unreadPosts = unreadPosts + newPostCount; | |
239 | document.title = '[' + unreadPosts + '] ' + documentOriginalTitle; |
|
238 | document.title = '[' + unreadPosts + '] ' + documentOriginalTitle; | |
240 |
|
239 | |||
241 | document.addEventListener('visibilitychange', function() { |
|
240 | document.addEventListener('visibilitychange', function() { | |
242 | if (documentOriginalTitle !== '') { |
|
241 | if (documentOriginalTitle !== '') { | |
243 | document.title = documentOriginalTitle; |
|
242 | document.title = documentOriginalTitle; | |
244 | documentOriginalTitle = ''; |
|
243 | documentOriginalTitle = ''; | |
245 | unreadPosts = 0; |
|
244 | unreadPosts = 0; | |
246 | } |
|
245 | } | |
247 |
|
246 | |||
248 | document.removeEventListener('visibilitychange', null); |
|
247 | document.removeEventListener('visibilitychange', null); | |
249 | }); |
|
248 | }); | |
250 | } |
|
249 | } | |
251 | } |
|
250 | } | |
252 |
|
251 | |||
253 | /** |
|
252 | /** | |
254 | * Clear all entered values in the form fields |
|
253 | * Clear all entered values in the form fields | |
255 | */ |
|
254 | */ | |
256 | function resetForm(form) { |
|
255 | function resetForm(form) { | |
257 | form.find('input:text, input:password, input:file, select, textarea').val(''); |
|
256 | form.find('input:text, input:password, input:file, select, textarea').val(''); | |
258 | form.find('input:radio, input:checkbox') |
|
257 | form.find('input:radio, input:checkbox') | |
259 | .removeAttr('checked').removeAttr('selected'); |
|
258 | .removeAttr('checked').removeAttr('selected'); | |
260 | $('.file_wrap').find('.file-thumb').remove(); |
|
259 | $('.file_wrap').find('.file-thumb').remove(); | |
261 | } |
|
260 | } | |
262 |
|
261 | |||
263 | /** |
|
262 | /** | |
264 | * When the form is posted, this method will be run as a callback |
|
263 | * When the form is posted, this method will be run as a callback | |
265 | */ |
|
264 | */ | |
266 | function updateOnPost(response, statusText, xhr, form) { |
|
265 | function updateOnPost(response, statusText, xhr, form) { | |
267 | var json = $.parseJSON(response); |
|
266 | var json = $.parseJSON(response); | |
268 | var status = json.status; |
|
267 | var status = json.status; | |
269 |
|
268 | |||
270 | showAsErrors(form, ''); |
|
269 | showAsErrors(form, ''); | |
271 |
|
270 | |||
272 | if (status === 'ok') { |
|
271 | if (status === 'ok') { | |
273 | resetForm(form); |
|
272 | resetForm(form); | |
274 | getThreadDiff(); |
|
273 | getThreadDiff(); | |
275 | } else { |
|
274 | } else { | |
276 | var errors = json.errors; |
|
275 | var errors = json.errors; | |
277 | for (var i = 0; i < errors.length; i++) { |
|
276 | for (var i = 0; i < errors.length; i++) { | |
278 | var fieldErrors = errors[i]; |
|
277 | var fieldErrors = errors[i]; | |
279 |
|
278 | |||
280 | var error = fieldErrors.errors; |
|
279 | var error = fieldErrors.errors; | |
281 |
|
280 | |||
282 | showAsErrors(form, error); |
|
281 | showAsErrors(form, error); | |
283 | } |
|
282 | } | |
284 | } |
|
283 | } | |
285 |
|
284 | |||
286 | scrollToBottom(); |
|
285 | scrollToBottom(); | |
287 | } |
|
286 | } | |
288 |
|
287 | |||
289 | /** |
|
288 | /** | |
290 | * Show text in the errors row of the form. |
|
289 | * Show text in the errors row of the form. | |
291 | * @param form |
|
290 | * @param form | |
292 | * @param text |
|
291 | * @param text | |
293 | */ |
|
292 | */ | |
294 | function showAsErrors(form, text) { |
|
293 | function showAsErrors(form, text) { | |
295 | form.children('.form-errors').remove(); |
|
294 | form.children('.form-errors').remove(); | |
296 |
|
295 | |||
297 | if (text.length > 0) { |
|
296 | if (text.length > 0) { | |
298 | var errorList = $('<div class="form-errors">' + text + '<div>'); |
|
297 | var errorList = $('<div class="form-errors">' + text + '<div>'); | |
299 | errorList.appendTo(form); |
|
298 | errorList.appendTo(form); | |
300 | } |
|
299 | } | |
301 | } |
|
300 | } | |
302 |
|
301 | |||
303 | /** |
|
302 | /** | |
304 | * Run js methods that are usually run on the document, on the new post |
|
303 | * Run js methods that are usually run on the document, on the new post | |
305 | */ |
|
304 | */ | |
306 | function processNewPost(post) { |
|
305 | function processNewPost(post) { | |
307 | addRefLinkPreview(post[0]); |
|
306 | addRefLinkPreview(post[0]); | |
308 | highlightCode(post); |
|
307 | highlightCode(post); | |
309 | translate_time(post); |
|
308 | translate_time(post); | |
310 | blink(post); |
|
309 | blink(post); | |
311 | } |
|
310 | } | |
312 |
|
311 | |||
313 | $(document).ready(function(){ |
|
312 | $(document).ready(function(){ | |
314 | if (initAutoupdate()) { |
|
313 | if (initAutoupdate()) { | |
315 | // Post form data over AJAX |
|
314 | // Post form data over AJAX | |
316 | var threadId = $('div.thread').children('.post').first().attr('id'); |
|
315 | var threadId = $('div.thread').children('.post').first().attr('id'); | |
317 |
|
316 | |||
318 | var form = $('#form'); |
|
317 | var form = $('#form'); | |
319 |
|
318 | |||
|
319 | if (form.length > 0) { | |||
320 | var options = { |
|
320 | var options = { | |
321 | beforeSubmit: function(arr, $form, options) { |
|
321 | beforeSubmit: function(arr, $form, options) { | |
322 | showAsErrors($('form'), gettext('Sending message...')); |
|
322 | showAsErrors($('form'), gettext('Sending message...')); | |
323 | }, |
|
323 | }, | |
324 | success: updateOnPost, |
|
324 | success: updateOnPost, | |
325 | url: '/api/add_post/' + threadId + '/' |
|
325 | url: '/api/add_post/' + threadId + '/' | |
326 | }; |
|
326 | }; | |
327 |
|
327 | |||
328 | form.ajaxForm(options); |
|
328 | form.ajaxForm(options); | |
329 |
|
329 | |||
330 | resetForm(form); |
|
330 | resetForm(form); | |
331 | } |
|
331 | } | |
|
332 | } | |||
332 |
|
333 | |||
333 | $('#autoupdate').click(getThreadDiff); |
|
334 | $('#autoupdate').click(getThreadDiff); | |
334 | }); |
|
335 | }); |
@@ -1,65 +1,63 b'' | |||||
1 | {% load staticfiles %} |
|
1 | {% load staticfiles %} | |
2 | {% load i18n %} |
|
2 | {% load i18n %} | |
3 | {% load l10n %} |
|
3 | {% load l10n %} | |
4 | {% load static from staticfiles %} |
|
4 | {% load static from staticfiles %} | |
5 |
|
5 | |||
6 | <!DOCTYPE html> |
|
6 | <!DOCTYPE html> | |
7 | <html> |
|
7 | <html> | |
8 | <head> |
|
8 | <head> | |
9 | <link rel="stylesheet" type="text/css" href="{% static 'css/base.css' %}" media="all"/> |
|
9 | <link rel="stylesheet" type="text/css" href="{% static 'css/base.css' %}" media="all"/> | |
10 | <link rel="stylesheet" type="text/css" href="{% static 'css/3party/highlight.css' %}" media="all"/> |
|
10 | <link rel="stylesheet" type="text/css" href="{% static 'css/3party/highlight.css' %}" media="all"/> | |
11 | <link rel="stylesheet" type="text/css" href="{% static theme_css %}" media="all"/> |
|
11 | <link rel="stylesheet" type="text/css" href="{% static theme_css %}" media="all"/> | |
12 |
|
12 | |||
13 | <link rel="alternate" type="application/rss+xml" href="rss/" title="{% trans 'Feed' %}"/> |
|
13 | <link rel="alternate" type="application/rss+xml" href="rss/" title="{% trans 'Feed' %}"/> | |
14 |
|
14 | |||
15 | <link rel="icon" type="image/png" |
|
15 | <link rel="icon" type="image/png" | |
16 | href="{% static 'favicon.png' %}"> |
|
16 | href="{% static 'favicon.png' %}"> | |
17 |
|
17 | |||
18 | <meta name="viewport" content="width=device-width, initial-scale=1"/> |
|
18 | <meta name="viewport" content="width=device-width, initial-scale=1"/> | |
19 | <meta charset="utf-8"/> |
|
19 | <meta charset="utf-8"/> | |
20 |
|
20 | |||
21 | {% block head %}{% endblock %} |
|
21 | {% block head %}{% endblock %} | |
22 | </head> |
|
22 | </head> | |
23 | <body> |
|
23 | <body> | |
24 | <script src="{% static 'js/jquery-2.0.1.min.js' %}"></script> |
|
24 | <script src="{% static 'js/jquery-2.0.1.min.js' %}"></script> | |
25 | <script src="{% static 'js/jquery-ui-1.10.3.custom.min.js' %}"></script> |
|
25 | <script src="{% static 'js/jquery-ui-1.10.3.custom.min.js' %}"></script> | |
26 | <script src="{% static 'js/jquery.mousewheel.js' %}"></script> |
|
26 | <script src="{% static 'js/jquery.mousewheel.js' %}"></script> | |
27 | <script src="{% url 'js_info_dict' %}"></script> |
|
27 | <script src="{% url 'js_info_dict' %}"></script> | |
28 |
|
28 | |||
29 | <div class="navigation_panel header"> |
|
29 | <div class="navigation_panel header"> | |
30 | <a class="link" href="{% url 'index' %}">{% trans "All threads" %}</a> |
|
30 | <a class="link" href="{% url 'index' %}">{% trans "All threads" %}</a> | |
31 | {% autoescape off %} |
|
31 | {% autoescape off %} | |
32 |
{ |
|
32 | {{ tags_str }}, | |
33 | {{ tag.get_view }}, |
|
|||
34 | {% endfor %} |
|
|||
35 | {% endautoescape %} |
|
33 | {% endautoescape %} | |
36 | <a href="{% url 'tags' %}" title="{% trans 'Tag management' %}" |
|
34 | <a href="{% url 'tags' %}" title="{% trans 'Tag management' %}" | |
37 | >[...]</a>, |
|
35 | >[...]</a>, | |
38 | <a href="{% url 'search' %}" title="{% trans 'Search' %}">[S]</a>. |
|
36 | <a href="{% url 'search' %}" title="{% trans 'Search' %}">[S]</a>. | |
39 |
|
37 | |||
40 | {% if username %} |
|
38 | {% if username %} | |
41 | <a href="{% url 'notifications' username %}" title="{% trans 'Notifications' %}"><b>{{ new_notifications_count }}</b> {% trans 'notifications' %}</a>. |
|
39 | <a href="{% url 'notifications' username %}" title="{% trans 'Notifications' %}"><b>{{ new_notifications_count }}</b> {% trans 'notifications' %}</a>. | |
42 | {% endif %} |
|
40 | {% endif %} | |
43 |
|
41 | |||
44 | <a class="link" href="{% url 'settings' %}">{% trans 'Settings' %}</a> |
|
42 | <a class="link" href="{% url 'settings' %}">{% trans 'Settings' %}</a> | |
45 | </div> |
|
43 | </div> | |
46 |
|
44 | |||
47 | {% block content %}{% endblock %} |
|
45 | {% block content %}{% endblock %} | |
48 |
|
46 | |||
49 | <script src="{% static 'js/3party/highlight.min.js' %}"></script> |
|
47 | <script src="{% static 'js/3party/highlight.min.js' %}"></script> | |
50 | <script src="{% static 'js/popup.js' %}"></script> |
|
48 | <script src="{% static 'js/popup.js' %}"></script> | |
51 | <script src="{% static 'js/image.js' %}"></script> |
|
49 | <script src="{% static 'js/image.js' %}"></script> | |
52 | <script src="{% static 'js/refpopup.js' %}"></script> |
|
50 | <script src="{% static 'js/refpopup.js' %}"></script> | |
53 | <script src="{% static 'js/main.js' %}"></script> |
|
51 | <script src="{% static 'js/main.js' %}"></script> | |
54 |
|
52 | |||
55 | <div class="navigation_panel footer"> |
|
53 | <div class="navigation_panel footer"> | |
56 | {% block metapanel %}{% endblock %} |
|
54 | {% block metapanel %}{% endblock %} | |
57 | [<a href="{% url 'admin:index' %}">{% trans 'Admin' %}</a>] |
|
55 | [<a href="{% url 'admin:index' %}">{% trans 'Admin' %}</a>] | |
58 | {% with ppd=posts_per_day|floatformat:2 %} |
|
56 | {% with ppd=posts_per_day|floatformat:2 %} | |
59 | {% blocktrans %}Speed: {{ ppd }} posts per day{% endblocktrans %} |
|
57 | {% blocktrans %}Speed: {{ ppd }} posts per day{% endblocktrans %} | |
60 | {% endwith %} |
|
58 | {% endwith %} | |
61 | <a class="link" href="#top" id="up">{% trans 'Up' %}</a> |
|
59 | <a class="link" href="#top" id="up">{% trans 'Up' %}</a> | |
62 | </div> |
|
60 | </div> | |
63 |
|
61 | |||
64 | </body> |
|
62 | </body> | |
65 | </html> |
|
63 | </html> |
@@ -1,102 +1,100 b'' | |||||
1 | {% load i18n %} |
|
1 | {% load i18n %} | |
2 | {% load board %} |
|
2 | {% load board %} | |
3 | {% load cache %} |
|
3 | {% load cache %} | |
4 |
|
4 | |||
5 | {% get_current_language as LANGUAGE_CODE %} |
|
5 | {% get_current_language as LANGUAGE_CODE %} | |
6 |
|
6 | |||
7 | {% if thread.archived %} |
|
7 | {% if thread.archived %} | |
8 | <div class="post archive_post" id="{{ post.id }}"> |
|
8 | <div class="post archive_post" id="{{ post.id }}"> | |
9 | {% elif bumpable %} |
|
9 | {% elif bumpable %} | |
10 | <div class="post" id="{{ post.id }}"> |
|
10 | <div class="post" id="{{ post.id }}"> | |
11 | {% else %} |
|
11 | {% else %} | |
12 | <div class="post dead_post" id="{{ post.id }}"> |
|
12 | <div class="post dead_post" id="{{ post.id }}"> | |
13 | {% endif %} |
|
13 | {% endif %} | |
14 |
|
14 | |||
15 | <div class="post-info"> |
|
15 | <div class="post-info"> | |
16 | <a class="post_id" href="{{ post.get_url }}" |
|
16 | <a class="post_id" href="{{ post.get_url }}" | |
17 | {% if not truncated and not thread.archived %} |
|
17 | {% if not truncated and not thread.archived %} | |
18 | onclick="javascript:addQuickReply('{{ post.id }}'); return false;" |
|
18 | onclick="javascript:addQuickReply('{{ post.id }}'); return false;" | |
19 | title="{% trans 'Quote' %}" {% endif %}>({{ post.get_absolute_id }})</a> |
|
19 | title="{% trans 'Quote' %}" {% endif %}>({{ post.get_absolute_id }})</a> | |
20 | <span class="title">{{ post.title }}</span> |
|
20 | <span class="title">{{ post.title }}</span> | |
21 | <span class="pub_time"><time datetime="{{ post.pub_time|date:'c' }}">{{ post.pub_time|date:'r' }}</time></span> |
|
21 | <span class="pub_time"><time datetime="{{ post.pub_time|date:'c' }}">{{ post.pub_time|date:'r' }}</time></span> | |
22 | {% comment %} |
|
22 | {% comment %} | |
23 | Thread death time needs to be shown only if the thread is alredy archived |
|
23 | Thread death time needs to be shown only if the thread is alredy archived | |
24 | and this is an opening post (thread death time) or a post for popup |
|
24 | and this is an opening post (thread death time) or a post for popup | |
25 | (we don't see OP here so we show the death time in the post itself). |
|
25 | (we don't see OP here so we show the death time in the post itself). | |
26 | {% endcomment %} |
|
26 | {% endcomment %} | |
27 | {% if thread.archived %} |
|
27 | {% if thread.archived %} | |
28 | {% if is_opening %} |
|
28 | {% if is_opening %} | |
29 | β {{ thread.bump_time }} |
|
29 | β <time datetime="{{ thread.bump_time|date:'c' }}">{{ thread.bump_time|date:'r' }}</time> | |
30 | {% endif %} |
|
30 | {% endif %} | |
31 | {% endif %} |
|
31 | {% endif %} | |
32 | {% if is_opening and need_open_link %} |
|
32 | {% if is_opening and need_open_link %} | |
33 | {% if thread.archived %} |
|
33 | {% if thread.archived %} | |
34 | [<a class="link" href="{% url 'thread' post.id %}">{% trans "Open" %}</a>] |
|
34 | [<a class="link" href="{% url 'thread' post.id %}">{% trans "Open" %}</a>] | |
35 | {% else %} |
|
35 | {% else %} | |
36 | [<a class="link" href="{% url 'thread' post.id %}#form">{% trans "Reply" %}</a>] |
|
36 | [<a class="link" href="{% url 'thread' post.id %}#form">{% trans "Reply" %}</a>] | |
37 | {% endif %} |
|
37 | {% endif %} | |
38 | {% endif %} |
|
38 | {% endif %} | |
39 |
|
39 | |||
40 | {% if post.global_id %} |
|
40 | {% if post.global_id %} | |
41 | <a class="global-id" href=" |
|
41 | <a class="global-id" href=" | |
42 | {% url 'post_sync_data' post.id %}"> [RAW] </a> |
|
42 | {% url 'post_sync_data' post.id %}"> [RAW] </a> | |
43 | {% endif %} |
|
43 | {% endif %} | |
44 |
|
44 | |||
45 | {% if moderator %} |
|
45 | {% if moderator %} | |
46 | <span class="moderator_info"> |
|
46 | <span class="moderator_info"> | |
47 | [<a href="{% url 'admin:boards_post_change' post.id %}">{% trans 'Edit' %}</a>] |
|
47 | [<a href="{% url 'admin:boards_post_change' post.id %}">{% trans 'Edit' %}</a>] | |
48 | {% if is_opening %} |
|
48 | {% if is_opening %} | |
49 | [<a href="{% url 'admin:boards_thread_change' thread.id %}">{% trans 'Edit thread' %}</a>] |
|
49 | [<a href="{% url 'admin:boards_thread_change' thread.id %}">{% trans 'Edit thread' %}</a>] | |
50 | {% endif %} |
|
50 | {% endif %} | |
51 | </span> |
|
51 | </span> | |
52 | {% endif %} |
|
52 | {% endif %} | |
53 | </div> |
|
53 | </div> | |
54 | {% comment %} |
|
54 | {% comment %} | |
55 | Post images. Currently only 1 image can be posted and shown, but post model |
|
55 | Post images. Currently only 1 image can be posted and shown, but post model | |
56 | supports multiple. |
|
56 | supports multiple. | |
57 | {% endcomment %} |
|
57 | {% endcomment %} | |
58 | {% if post.images.exists %} |
|
58 | {% if post.images.exists %} | |
59 | {% with post.images.all.0 as image %} |
|
59 | {% with post.images.all.0 as image %} | |
60 | {% autoescape off %} |
|
60 | {% autoescape off %} | |
61 | {{ image.get_view }} |
|
61 | {{ image.get_view }} | |
62 | {% endautoescape %} |
|
62 | {% endautoescape %} | |
63 | {% endwith %} |
|
63 | {% endwith %} | |
64 | {% endif %} |
|
64 | {% endif %} | |
65 | {% comment %} |
|
65 | {% comment %} | |
66 | Post message (text) |
|
66 | Post message (text) | |
67 | {% endcomment %} |
|
67 | {% endcomment %} | |
68 | <div class="message"> |
|
68 | <div class="message"> | |
69 | {% autoescape off %} |
|
69 | {% autoescape off %} | |
70 | {% if truncated %} |
|
70 | {% if truncated %} | |
71 | {{ post.get_text|truncatewords_html:50 }} |
|
71 | {{ post.get_text|truncatewords_html:50 }} | |
72 | {% else %} |
|
72 | {% else %} | |
73 | {{ post.get_text }} |
|
73 | {{ post.get_text }} | |
74 | {% endif %} |
|
74 | {% endif %} | |
75 | {% endautoescape %} |
|
75 | {% endautoescape %} | |
76 | {% if post.is_referenced %} |
|
76 | {% if post.is_referenced %} | |
77 | <div class="refmap"> |
|
77 | <div class="refmap"> | |
78 | {% autoescape off %} |
|
78 | {% autoescape off %} | |
79 | {% trans "Replies" %}: {{ post.refmap }} |
|
79 | {% trans "Replies" %}: {{ post.refmap }} | |
80 | {% endautoescape %} |
|
80 | {% endautoescape %} | |
81 | </div> |
|
81 | </div> | |
82 | {% endif %} |
|
82 | {% endif %} | |
83 | </div> |
|
83 | </div> | |
84 | {% comment %} |
|
84 | {% comment %} | |
85 | Thread metadata: counters, tags etc |
|
85 | Thread metadata: counters, tags etc | |
86 | {% endcomment %} |
|
86 | {% endcomment %} | |
87 | {% if is_opening %} |
|
87 | {% if is_opening %} | |
88 | <div class="metadata"> |
|
88 | <div class="metadata"> | |
89 | {% if is_opening and need_open_link %} |
|
89 | {% if is_opening and need_open_link %} | |
90 | {{ thread.get_reply_count }} {% trans 'messages' %}, |
|
90 | {{ thread.get_reply_count }} {% trans 'messages' %}, | |
91 | {{ thread.get_images_count }} {% trans 'images' %}. |
|
91 | {{ thread.get_images_count }} {% trans 'images' %}. | |
92 | {% endif %} |
|
92 | {% endif %} | |
93 | <span class="tags"> |
|
93 | <span class="tags"> | |
94 | {% autoescape off %} |
|
94 | {% autoescape off %} | |
95 |
{ |
|
95 | {{ thread.get_tag_url_list }} | |
96 | {{ tag.get_view }}{% if not forloop.last %},{% endif %} |
|
|||
97 | {% endfor %} |
|
|||
98 | {% endautoescape %} |
|
96 | {% endautoescape %} | |
99 | </span> |
|
97 | </span> | |
100 | </div> |
|
98 | </div> | |
101 | {% endif %} |
|
99 | {% endif %} | |
102 | </div> |
|
100 | </div> |
@@ -1,188 +1,190 b'' | |||||
1 | {% extends "boards/base.html" %} |
|
1 | {% extends "boards/base.html" %} | |
2 |
|
2 | |||
3 | {% load i18n %} |
|
3 | {% load i18n %} | |
4 | {% load cache %} |
|
4 | {% load cache %} | |
5 | {% load board %} |
|
5 | {% load board %} | |
6 | {% load static %} |
|
6 | {% load static %} | |
7 |
|
7 | |||
8 | {% block head %} |
|
8 | {% block head %} | |
9 | {% if tag %} |
|
9 | {% if tag %} | |
10 | <title>{{ tag.name }} - {{ site_name }}</title> |
|
10 | <title>{{ tag.name }} - {{ site_name }}</title> | |
11 | {% else %} |
|
11 | {% else %} | |
12 | <title>{{ site_name }}</title> |
|
12 | <title>{{ site_name }}</title> | |
13 | {% endif %} |
|
13 | {% endif %} | |
14 |
|
14 | |||
15 | {% if current_page.has_previous %} |
|
15 | {% if current_page.has_previous %} | |
16 | <link rel="prev" href=" |
|
16 | <link rel="prev" href=" | |
17 | {% if tag %} |
|
17 | {% if tag %} | |
18 | {% url "tag" tag_name=tag.name page=current_page.previous_page_number %} |
|
18 | {% url "tag" tag_name=tag.name page=current_page.previous_page_number %} | |
19 | {% else %} |
|
19 | {% else %} | |
20 | {% url "index" page=current_page.previous_page_number %} |
|
20 | {% url "index" page=current_page.previous_page_number %} | |
21 | {% endif %} |
|
21 | {% endif %} | |
22 | " /> |
|
22 | " /> | |
23 | {% endif %} |
|
23 | {% endif %} | |
24 | {% if current_page.has_next %} |
|
24 | {% if current_page.has_next %} | |
25 | <link rel="next" href=" |
|
25 | <link rel="next" href=" | |
26 | {% if tag %} |
|
26 | {% if tag %} | |
27 | {% url "tag" tag_name=tag.name page=current_page.next_page_number %} |
|
27 | {% url "tag" tag_name=tag.name page=current_page.next_page_number %} | |
28 | {% else %} |
|
28 | {% else %} | |
29 | {% url "index" page=current_page.next_page_number %} |
|
29 | {% url "index" page=current_page.next_page_number %} | |
30 | {% endif %} |
|
30 | {% endif %} | |
31 | " /> |
|
31 | " /> | |
32 | {% endif %} |
|
32 | {% endif %} | |
33 |
|
33 | |||
34 | {% endblock %} |
|
34 | {% endblock %} | |
35 |
|
35 | |||
36 | {% block content %} |
|
36 | {% block content %} | |
37 |
|
37 | |||
38 | {% get_current_language as LANGUAGE_CODE %} |
|
38 | {% get_current_language as LANGUAGE_CODE %} | |
39 |
|
39 | |||
40 | {% if tag %} |
|
40 | {% if tag %} | |
41 | <div class="tag_info"> |
|
41 | <div class="tag_info"> | |
42 | <h2> |
|
42 | <h2> | |
43 | {% if is_favorite %} |
|
43 | {% if is_favorite %} | |
44 | <a href="{% url 'tag' tag.name %}?method=unsubscribe&next={{ request.path }}" |
|
44 | <a href="{% url 'tag' tag.name %}?method=unsubscribe&next={{ request.path }}" | |
45 | class="fav" rel="nofollow">β </a> |
|
45 | class="fav" rel="nofollow">β </a> | |
46 | {% else %} |
|
46 | {% else %} | |
47 | <a href="{% url 'tag' tag.name %}?method=subscribe&next={{ request.path }}" |
|
47 | <a href="{% url 'tag' tag.name %}?method=subscribe&next={{ request.path }}" | |
48 | class="not_fav" rel="nofollow">β </a> |
|
48 | class="not_fav" rel="nofollow">β </a> | |
49 | {% endif %} |
|
49 | {% endif %} | |
50 | {% if is_hidden %} |
|
50 | {% if is_hidden %} | |
51 | <a href="{% url 'tag' tag.name %}?method=unhide&next={{ request.path }}" |
|
51 | <a href="{% url 'tag' tag.name %}?method=unhide&next={{ request.path }}" | |
52 | title="{% trans 'Show tag' %}" |
|
52 | title="{% trans 'Show tag' %}" | |
53 | class="fav" rel="nofollow">H</a> |
|
53 | class="fav" rel="nofollow">H</a> | |
54 | {% else %} |
|
54 | {% else %} | |
55 | <a href="{% url 'tag' tag.name %}?method=hide&next={{ request.path }}" |
|
55 | <a href="{% url 'tag' tag.name %}?method=hide&next={{ request.path }}" | |
56 | title="{% trans 'Hide tag' %}" |
|
56 | title="{% trans 'Hide tag' %}" | |
57 | class="not_fav" rel="nofollow">H</a> |
|
57 | class="not_fav" rel="nofollow">H</a> | |
58 | {% endif %} |
|
58 | {% endif %} | |
59 | {% autoescape off %} |
|
59 | {% autoescape off %} | |
60 | {{ tag.get_view }} |
|
60 | {{ tag.get_view }} | |
61 | {% endautoescape %} |
|
61 | {% endautoescape %} | |
62 | {% if moderator %} |
|
62 | {% if moderator %} | |
63 | <span class="moderator_info">[<a href="{% url 'admin:boards_tag_change' tag.id %}">{% trans 'Edit tag' %}</a>]</span> |
|
63 | <span class="moderator_info">[<a href="{% url 'admin:boards_tag_change' tag.id %}">{% trans 'Edit tag' %}</a>]</span> | |
64 | {% endif %} |
|
64 | {% endif %} | |
65 | </h2> |
|
65 | </h2> | |
66 | <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> |
|
66 | <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> | |
67 | </div> |
|
67 | </div> | |
68 | {% endif %} |
|
68 | {% endif %} | |
69 |
|
69 | |||
70 | {% if threads %} |
|
70 | {% if threads %} | |
71 | {% if current_page.has_previous %} |
|
71 | {% if current_page.has_previous %} | |
72 | <div class="page_link"> |
|
72 | <div class="page_link"> | |
73 | <a href=" |
|
73 | <a href=" | |
74 | {% if tag %} |
|
74 | {% if tag %} | |
75 | {% url "tag" tag_name=tag.name page=current_page.previous_page_number %} |
|
75 | {% url "tag" tag_name=tag.name page=current_page.previous_page_number %} | |
76 | {% else %} |
|
76 | {% else %} | |
77 | {% url "index" page=current_page.previous_page_number %} |
|
77 | {% url "index" page=current_page.previous_page_number %} | |
78 | {% endif %} |
|
78 | {% endif %} | |
79 | ">{% trans "Previous page" %}</a> |
|
79 | ">{% trans "Previous page" %}</a> | |
80 | </div> |
|
80 | </div> | |
81 | {% endif %} |
|
81 | {% endif %} | |
82 |
|
82 | |||
83 | {% for thread in threads %} |
|
83 | {% for thread in threads %} | |
84 | {% cache 600 thread_short thread.id thread.last_edit_time moderator LANGUAGE_CODE %} |
|
84 | {% cache 600 thread_short thread.id thread.last_edit_time moderator LANGUAGE_CODE %} | |
85 | <div class="thread"> |
|
85 | <div class="thread"> | |
86 | {% post_view thread.get_opening_post moderator is_opening=True thread=thread truncated=True need_open_link=True %} |
|
86 | {% post_view thread.get_opening_post moderator is_opening=True thread=thread truncated=True need_open_link=True %} | |
87 | {% if not thread.archived %} |
|
87 | {% if not thread.archived %} | |
88 | {% with last_replies=thread.get_last_replies %} |
|
88 | {% with last_replies=thread.get_last_replies %} | |
89 | {% if last_replies %} |
|
89 | {% if last_replies %} | |
90 |
{% |
|
90 | {% with skipped_replies_count=thread.get_skipped_replies_count %} | |
|
91 | {% if skipped_replies_count %} | |||
91 | <div class="skipped_replies"> |
|
92 | <div class="skipped_replies"> | |
92 |
<a href="{% url 'thread' thread.get_opening_post |
|
93 | <a href="{% url 'thread' thread.get_opening_post_id %}"> | |
93 |
{% blocktrans with count= |
|
94 | {% blocktrans with count=skipped_replies_count %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %} | |
94 | </a> |
|
95 | </a> | |
95 | </div> |
|
96 | </div> | |
96 | {% endif %} |
|
97 | {% endif %} | |
|
98 | {% endwith %} | |||
97 | <div class="last-replies"> |
|
99 | <div class="last-replies"> | |
98 | {% for post in last_replies %} |
|
100 | {% for post in last_replies %} | |
99 | {% post_view post is_opening=False moderator=moderator truncated=True %} |
|
101 | {% post_view post is_opening=False moderator=moderator truncated=True %} | |
100 | {% endfor %} |
|
102 | {% endfor %} | |
101 | </div> |
|
103 | </div> | |
102 | {% endif %} |
|
104 | {% endif %} | |
103 | {% endwith %} |
|
105 | {% endwith %} | |
104 | {% endif %} |
|
106 | {% endif %} | |
105 | </div> |
|
107 | </div> | |
106 | {% endcache %} |
|
108 | {% endcache %} | |
107 | {% endfor %} |
|
109 | {% endfor %} | |
108 |
|
110 | |||
109 | {% if current_page.has_next %} |
|
111 | {% if current_page.has_next %} | |
110 | <div class="page_link"> |
|
112 | <div class="page_link"> | |
111 | <a href=" |
|
113 | <a href=" | |
112 | {% if tag %} |
|
114 | {% if tag %} | |
113 | {% url "tag" tag_name=tag.name page=current_page.next_page_number %} |
|
115 | {% url "tag" tag_name=tag.name page=current_page.next_page_number %} | |
114 | {% else %} |
|
116 | {% else %} | |
115 | {% url "index" page=current_page.next_page_number %} |
|
117 | {% url "index" page=current_page.next_page_number %} | |
116 | {% endif %} |
|
118 | {% endif %} | |
117 | ">{% trans "Next page" %}</a> |
|
119 | ">{% trans "Next page" %}</a> | |
118 | </div> |
|
120 | </div> | |
119 | {% endif %} |
|
121 | {% endif %} | |
120 | {% else %} |
|
122 | {% else %} | |
121 | <div class="post"> |
|
123 | <div class="post"> | |
122 | {% trans 'No threads exist. Create the first one!' %}</div> |
|
124 | {% trans 'No threads exist. Create the first one!' %}</div> | |
123 | {% endif %} |
|
125 | {% endif %} | |
124 |
|
126 | |||
125 | <div class="post-form-w"> |
|
127 | <div class="post-form-w"> | |
126 | <script src="{% static 'js/panel.js' %}"></script> |
|
128 | <script src="{% static 'js/panel.js' %}"></script> | |
127 | <div class="post-form"> |
|
129 | <div class="post-form"> | |
128 | <div class="form-title">{% trans "Create new thread" %}</div> |
|
130 | <div class="form-title">{% trans "Create new thread" %}</div> | |
129 | <div class="swappable-form-full"> |
|
131 | <div class="swappable-form-full"> | |
130 | <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %} |
|
132 | <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %} | |
131 | {{ form.as_div }} |
|
133 | {{ form.as_div }} | |
132 | <div class="form-submit"> |
|
134 | <div class="form-submit"> | |
133 | <input type="submit" value="{% trans "Post" %}"/> |
|
135 | <input type="submit" value="{% trans "Post" %}"/> | |
134 | </div> |
|
136 | </div> | |
135 | (ctrl-enter) |
|
137 | (ctrl-enter) | |
136 | </form> |
|
138 | </form> | |
137 | </div> |
|
139 | </div> | |
138 | <div> |
|
140 | <div> | |
139 | {% trans 'Tags must be delimited by spaces. Text or image is required.' %} |
|
141 | {% trans 'Tags must be delimited by spaces. Text or image is required.' %} | |
140 | </div> |
|
142 | </div> | |
141 | <div><a href="{% url "staticpage" name="help" %}"> |
|
143 | <div><a href="{% url "staticpage" name="help" %}"> | |
142 | {% trans 'Text syntax' %}</a></div> |
|
144 | {% trans 'Text syntax' %}</a></div> | |
143 | </div> |
|
145 | </div> | |
144 | </div> |
|
146 | </div> | |
145 |
|
147 | |||
146 | <script src="{% static 'js/form.js' %}"></script> |
|
148 | <script src="{% static 'js/form.js' %}"></script> | |
147 |
|
149 | |||
148 | {% endblock %} |
|
150 | {% endblock %} | |
149 |
|
151 | |||
150 | {% block metapanel %} |
|
152 | {% block metapanel %} | |
151 |
|
153 | |||
152 | <span class="metapanel"> |
|
154 | <span class="metapanel"> | |
153 | <b><a href="{% url "authors" %}">{{ site_name }}</a> {{ version }}</b> |
|
155 | <b><a href="{% url "authors" %}">{{ site_name }}</a> {{ version }}</b> | |
154 | {% trans "Pages:" %} |
|
156 | {% trans "Pages:" %} | |
155 | <a href=" |
|
157 | <a href=" | |
156 | {% if tag %} |
|
158 | {% if tag %} | |
157 | {% url "tag" tag_name=tag.name page=paginator.page_range|first %} |
|
159 | {% url "tag" tag_name=tag.name page=paginator.page_range|first %} | |
158 | {% else %} |
|
160 | {% else %} | |
159 | {% url "index" page=paginator.page_range|first %} |
|
161 | {% url "index" page=paginator.page_range|first %} | |
160 | {% endif %} |
|
162 | {% endif %} | |
161 | "><<</a> |
|
163 | "><<</a> | |
162 | [ |
|
164 | [ | |
163 | {% for page in paginator.center_range %} |
|
165 | {% for page in paginator.center_range %} | |
164 | <a |
|
166 | <a | |
165 | {% ifequal page current_page.number %} |
|
167 | {% ifequal page current_page.number %} | |
166 | class="current_page" |
|
168 | class="current_page" | |
167 | {% endifequal %} |
|
169 | {% endifequal %} | |
168 | href=" |
|
170 | href=" | |
169 | {% if tag %} |
|
171 | {% if tag %} | |
170 | {% url "tag" tag_name=tag.name page=page %} |
|
172 | {% url "tag" tag_name=tag.name page=page %} | |
171 | {% else %} |
|
173 | {% else %} | |
172 | {% url "index" page=page %} |
|
174 | {% url "index" page=page %} | |
173 | {% endif %} |
|
175 | {% endif %} | |
174 | ">{{ page }}</a> |
|
176 | ">{{ page }}</a> | |
175 | {% if not forloop.last %},{% endif %} |
|
177 | {% if not forloop.last %},{% endif %} | |
176 | {% endfor %} |
|
178 | {% endfor %} | |
177 | ] |
|
179 | ] | |
178 | <a href=" |
|
180 | <a href=" | |
179 | {% if tag %} |
|
181 | {% if tag %} | |
180 | {% url "tag" tag_name=tag.name page=paginator.page_range|last %} |
|
182 | {% url "tag" tag_name=tag.name page=paginator.page_range|last %} | |
181 | {% else %} |
|
183 | {% else %} | |
182 | {% url "index" page=paginator.page_range|last %} |
|
184 | {% url "index" page=paginator.page_range|last %} | |
183 | {% endif %} |
|
185 | {% endif %} | |
184 | ">>></a> |
|
186 | ">>></a> | |
185 | [<a href="rss/">RSS</a>] |
|
187 | [<a href="rss/">RSS</a>] | |
186 | </span> |
|
188 | </span> | |
187 |
|
189 | |||
188 | {% endblock %} |
|
190 | {% endblock %} |
@@ -1,93 +1,93 b'' | |||||
1 | {% extends "boards/base.html" %} |
|
1 | {% extends "boards/base.html" %} | |
2 |
|
2 | |||
3 | {% load i18n %} |
|
3 | {% load i18n %} | |
4 | {% load cache %} |
|
4 | {% load cache %} | |
5 | {% load static from staticfiles %} |
|
5 | {% load static from staticfiles %} | |
6 | {% load board %} |
|
6 | {% load board %} | |
7 |
|
7 | |||
8 | {% block head %} |
|
8 | {% block head %} | |
9 | <title>{{ opening_post.get_title|striptags|truncatewords:10 }} |
|
9 | <title>{{ opening_post.get_title|striptags|truncatewords:10 }} | |
10 | - {{ site_name }}</title> |
|
10 | - {{ site_name }}</title> | |
11 | {% endblock %} |
|
11 | {% endblock %} | |
12 |
|
12 | |||
13 | {% block content %} |
|
13 | {% block content %} | |
14 | {% get_current_language as LANGUAGE_CODE %} |
|
14 | {% get_current_language as LANGUAGE_CODE %} | |
15 |
|
15 | |||
16 | {% cache 600 thread_view thread.id thread.last_edit_time moderator LANGUAGE_CODE %} |
|
16 | {% cache 600 thread_view thread.id thread.last_edit_time moderator LANGUAGE_CODE %} | |
17 |
|
17 | |||
18 | <div class="image-mode-tab"> |
|
18 | <div class="image-mode-tab"> | |
19 | <a class="current_mode" href="{% url 'thread' opening_post.id %}">{% trans 'Normal mode' %}</a>, |
|
19 | <a class="current_mode" href="{% url 'thread' opening_post.id %}">{% trans 'Normal mode' %}</a>, | |
20 | <a href="{% url 'thread_gallery' opening_post.id %}">{% trans 'Gallery mode' %}</a> |
|
20 | <a href="{% url 'thread_gallery' opening_post.id %}">{% trans 'Gallery mode' %}</a> | |
21 | </div> |
|
21 | </div> | |
22 |
|
22 | |||
23 | {% if bumpable %} |
|
23 | {% if bumpable %} | |
24 | <div class="bar-bg"> |
|
24 | <div class="bar-bg"> | |
25 | <div class="bar-value" style="width:{{ bumplimit_progress }}%" id="bumplimit_progress"> |
|
25 | <div class="bar-value" style="width:{{ bumplimit_progress }}%" id="bumplimit_progress"> | |
26 | </div> |
|
26 | </div> | |
27 | <div class="bar-text"> |
|
27 | <div class="bar-text"> | |
28 | <span id="left_to_limit">{{ posts_left }}</span> {% trans 'posts to bumplimit' %} |
|
28 | <span id="left_to_limit">{{ posts_left }}</span> {% trans 'posts to bumplimit' %} | |
29 | </div> |
|
29 | </div> | |
30 | </div> |
|
30 | </div> | |
31 | {% endif %} |
|
31 | {% endif %} | |
32 |
|
32 | |||
33 | <div class="thread"> |
|
33 | <div class="thread"> | |
34 | {% with can_bump=thread.can_bump %} |
|
34 | {% with can_bump=thread.can_bump %} | |
35 | {% for post in thread.get_replies %} |
|
35 | {% for post in thread.get_replies %} | |
36 | {% with is_opening=forloop.first %} |
|
36 | {% with is_opening=forloop.first %} | |
37 | {% post_view post moderator=moderator is_opening=is_opening bumpable=can_bump opening_post_id=opening_post.id %} |
|
37 | {% post_view post moderator=moderator is_opening=is_opening bumpable=can_bump opening_post_id=opening_post.id %} | |
38 | {% endwith %} |
|
38 | {% endwith %} | |
39 | {% endfor %} |
|
39 | {% endfor %} | |
40 | {% endwith %} |
|
40 | {% endwith %} | |
41 | </div> |
|
41 | </div> | |
42 |
|
42 | |||
43 | {% if not thread.archived %} |
|
43 | {% if not thread.archived %} | |
44 | <div class="post-form-w"> |
|
44 | <div class="post-form-w"> | |
45 | <script src="{% static 'js/panel.js' %}"></script> |
|
45 | <script src="{% static 'js/panel.js' %}"></script> | |
46 | <div class="form-title">{% trans "Reply to thread" %} #{{ opening_post.id }}</div> |
|
46 | <div class="form-title">{% trans "Reply to thread" %} #{{ opening_post.id }}</div> | |
47 | <div class="post-form" id="compact-form"> |
|
47 | <div class="post-form" id="compact-form"> | |
48 | <div class="swappable-form-full"> |
|
48 | <div class="swappable-form-full"> | |
49 | <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %} |
|
49 | <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %} | |
50 | <div class="compact-form-text"></div> |
|
50 | <div class="compact-form-text"></div> | |
51 | {{ form.as_div }} |
|
51 | {{ form.as_div }} | |
52 | <div class="form-submit"> |
|
52 | <div class="form-submit"> | |
53 | <input type="submit" value="{% trans "Post" %}"/> |
|
53 | <input type="submit" value="{% trans "Post" %}"/> | |
54 | </div> |
|
54 | </div> | |
55 | (ctrl-enter) |
|
55 | (ctrl-enter) | |
56 | </form> |
|
56 | </form> | |
57 | </div> |
|
57 | </div> | |
58 | <div><a href="{% url "staticpage" name="help" %}"> |
|
58 | <div><a href="{% url "staticpage" name="help" %}"> | |
59 | {% trans 'Text syntax' %}</a></div> |
|
59 | {% trans 'Text syntax' %}</a></div> | |
60 | </div> |
|
60 | </div> | |
61 | </div> |
|
61 | </div> | |
62 |
|
62 | |||
63 | <script src="{% static 'js/jquery.form.min.js' %}"></script> |
|
63 | <script src="{% static 'js/jquery.form.min.js' %}"></script> | |
64 | <script src="{% static 'js/thread_update.js' %}"></script> |
|
|||
65 | <script src="{% static 'js/3party/centrifuge.js' %}"></script> |
|
|||
66 | {% endif %} |
|
64 | {% endif %} | |
67 |
|
65 | |||
68 | <script src="{% static 'js/form.js' %}"></script> |
|
66 | <script src="{% static 'js/form.js' %}"></script> | |
69 | <script src="{% static 'js/thread.js' %}"></script> |
|
67 | <script src="{% static 'js/thread.js' %}"></script> | |
|
68 | <script src="{% static 'js/thread_update.js' %}"></script> | |||
|
69 | <script src="{% static 'js/3party/centrifuge.js' %}"></script> | |||
70 |
|
70 | |||
71 | {% endcache %} |
|
71 | {% endcache %} | |
72 | {% endblock %} |
|
72 | {% endblock %} | |
73 |
|
73 | |||
74 | {% block metapanel %} |
|
74 | {% block metapanel %} | |
75 |
|
75 | |||
76 | {% get_current_language as LANGUAGE_CODE %} |
|
76 | {% get_current_language as LANGUAGE_CODE %} | |
77 |
|
77 | |||
78 | <span class="metapanel" |
|
78 | <span class="metapanel" | |
79 | data-last-update="{{ last_update }}" |
|
79 | data-last-update="{{ last_update }}" | |
80 | data-ws-token="{{ ws_token }}" |
|
80 | data-ws-token="{{ ws_token }}" | |
81 | data-ws-project="{{ ws_project }}" |
|
81 | data-ws-project="{{ ws_project }}" | |
82 | data-ws-host="{{ ws_host }}" |
|
82 | data-ws-host="{{ ws_host }}" | |
83 | data-ws-port="{{ ws_port }}"> |
|
83 | data-ws-port="{{ ws_port }}"> | |
84 | {% cache 600 thread_meta thread.last_edit_time moderator LANGUAGE_CODE %} |
|
84 | {% cache 600 thread_meta thread.last_edit_time moderator LANGUAGE_CODE %} | |
85 | <button id="autoupdate">{% trans 'Update' %}</button> |
|
85 | <button id="autoupdate">{% trans 'Update' %}</button> | |
86 | <span id="reply-count">{{ thread.get_reply_count }}</span>/{{ max_replies }} {% trans 'messages' %}, |
|
86 | <span id="reply-count">{{ thread.get_reply_count }}</span>/{{ max_replies }} {% trans 'messages' %}, | |
87 | <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}. |
|
87 | <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}. | |
88 | {% trans 'Last update: ' %}<span id="last-update"><time datetime="{{ thread.last_edit_time|date:'c' }}">{{ thread.last_edit_time|date:'r' }}</time></span> |
|
88 | {% trans 'Last update: ' %}<span id="last-update"><time datetime="{{ thread.last_edit_time|date:'c' }}">{{ thread.last_edit_time|date:'r' }}</time></span> | |
89 | [<a href="rss/">RSS</a>] |
|
89 | [<a href="rss/">RSS</a>] | |
90 | {% endcache %} |
|
90 | {% endcache %} | |
91 | </span> |
|
91 | </span> | |
92 |
|
92 | |||
93 | {% endblock %} |
|
93 | {% endblock %} |
@@ -1,145 +1,143 b'' | |||||
1 | import re |
|
1 | import re | |
2 | from django.shortcuts import get_object_or_404 |
|
2 | from django.shortcuts import get_object_or_404 | |
3 | from django import template |
|
3 | from django import template | |
4 |
|
4 | |||
5 | ELLIPSIZER = '...' |
|
5 | ELLIPSIZER = '...' | |
6 |
|
6 | |||
7 | REGEX_LINES = re.compile(r'(<div class="br"></div>)', re.U | re.S) |
|
7 | REGEX_LINES = re.compile(r'(<div class="br"></div>)', re.U | re.S) | |
8 | REGEX_TAG = re.compile(r'<(/)?([^ ]+?)(?:(\s*/)| .*?)?>', re.S) |
|
8 | REGEX_TAG = re.compile(r'<(/)?([^ ]+?)(?:(\s*/)| .*?)?>', re.S) | |
9 |
|
9 | |||
|
10 | IMG_ACTION_URL = '[<a href="{}">{}</a>]' | |||
|
11 | ||||
10 |
|
12 | |||
11 | register = template.Library() |
|
13 | register = template.Library() | |
12 |
|
14 | |||
13 | actions = [ |
|
15 | actions = [ | |
14 | { |
|
16 | { | |
15 | 'name': 'google', |
|
17 | 'name': 'google', | |
16 | 'link': 'http://google.com/searchbyimage?image_url=%s', |
|
18 | 'link': 'http://google.com/searchbyimage?image_url=%s', | |
17 | }, |
|
19 | }, | |
18 | { |
|
20 | { | |
19 | 'name': 'iqdb', |
|
21 | 'name': 'iqdb', | |
20 | 'link': 'http://iqdb.org/?url=%s', |
|
22 | 'link': 'http://iqdb.org/?url=%s', | |
21 | }, |
|
23 | }, | |
22 | ] |
|
24 | ] | |
23 |
|
25 | |||
24 |
|
26 | |||
25 | @register.simple_tag(name='post_url') |
|
27 | @register.simple_tag(name='post_url') | |
26 | def post_url(*args, **kwargs): |
|
28 | def post_url(*args, **kwargs): | |
27 | post_id = args[0] |
|
29 | post_id = args[0] | |
28 |
|
30 | |||
29 | post = get_object_or_404('Post', id=post_id) |
|
31 | post = get_object_or_404('Post', id=post_id) | |
30 |
|
32 | |||
31 | return post.get_url() |
|
33 | return post.get_url() | |
32 |
|
34 | |||
33 |
|
35 | |||
34 | @register.simple_tag(name='image_actions') |
|
36 | @register.simple_tag(name='image_actions') | |
35 | def image_actions(*args, **kwargs): |
|
37 | def image_actions(*args, **kwargs): | |
36 | image_link = args[0] |
|
38 | image_link = args[0] | |
37 | if len(args) > 1: |
|
39 | if len(args) > 1: | |
38 | image_link = 'http://' + args[1] + image_link # TODO https? |
|
40 | image_link = 'http://' + args[1] + image_link # TODO https? | |
39 |
|
41 | |||
40 | result = '' |
|
42 | return ', '.join([IMG_ACTION_URL.format( | |
41 |
|
43 | action['link'] % image_link, action['name'])for action in actions]) | ||
42 | for action in actions: |
|
|||
43 | result += '[<a href="' + action['link'] % image_link + '">' + \ |
|
|||
44 | action['name'] + '</a>]' |
|
|||
45 |
|
||||
46 | return result |
|
|||
47 |
|
44 | |||
48 |
|
45 | |||
49 | # TODO Use get_view of a post instead of this |
|
46 | # TODO Use get_view of a post instead of this | |
50 | @register.inclusion_tag('boards/post.html', name='post_view') |
|
47 | @register.inclusion_tag('boards/post.html', name='post_view') | |
51 | def post_view(post, moderator=False, need_open_link=False, truncated=False, |
|
48 | def post_view(post, moderator=False, need_open_link=False, truncated=False, | |
52 | **kwargs): |
|
49 | **kwargs): | |
53 | """ |
|
50 | """ | |
54 | Get post |
|
51 | Get post | |
55 | """ |
|
52 | """ | |
56 |
|
53 | |||
57 | if 'is_opening' in kwargs: |
|
54 | if 'is_opening' in kwargs: | |
58 | is_opening = kwargs['is_opening'] |
|
55 | is_opening = kwargs['is_opening'] | |
59 | else: |
|
56 | else: | |
60 | is_opening = post.is_opening() |
|
57 | is_opening = post.is_opening() | |
61 |
|
58 | |||
62 | thread = post.get_thread() |
|
59 | thread = post.get_thread() | |
63 |
|
60 | |||
64 | if 'can_bump' in kwargs: |
|
61 | if 'can_bump' in kwargs: | |
65 | can_bump = kwargs['can_bump'] |
|
62 | can_bump = kwargs['can_bump'] | |
66 | else: |
|
63 | else: | |
67 | can_bump = thread.can_bump() |
|
64 | can_bump = thread.can_bump() | |
68 |
|
65 | |||
69 | opening_post_id = thread.get_opening_post_id() |
|
66 | opening_post_id = thread.get_opening_post_id() | |
70 |
|
67 | |||
71 | return { |
|
68 | return { | |
72 | 'post': post, |
|
69 | 'post': post, | |
73 | 'moderator': moderator, |
|
70 | 'moderator': moderator, | |
74 | 'is_opening': is_opening, |
|
71 | 'is_opening': is_opening, | |
75 | 'thread': thread, |
|
72 | 'thread': thread, | |
76 | 'bumpable': can_bump, |
|
73 | 'bumpable': can_bump, | |
77 | 'need_open_link': need_open_link, |
|
74 | 'need_open_link': need_open_link, | |
78 | 'truncated': truncated, |
|
75 | 'truncated': truncated, | |
79 | 'opening_post_id': opening_post_id, |
|
76 | 'opening_post_id': opening_post_id, | |
80 | } |
|
77 | } | |
81 |
|
78 | |||
82 |
|
79 | |||
|
80 | # TODO Fix or remove this method | |||
83 | @register.filter(is_safe=True) |
|
81 | @register.filter(is_safe=True) | |
84 | def truncate_lines(text, length): |
|
82 | def truncate_lines(text, length): | |
85 | if length <= 0: |
|
83 | if length <= 0: | |
86 | return '' |
|
84 | return '' | |
87 |
|
85 | |||
88 | html4_singlets = ( |
|
86 | html4_singlets = ( | |
89 | 'br', 'col', 'link', 'base', 'img', |
|
87 | 'br', 'col', 'link', 'base', 'img', | |
90 | 'param', 'area', 'hr', 'input' |
|
88 | 'param', 'area', 'hr', 'input' | |
91 | ) |
|
89 | ) | |
92 |
|
90 | |||
93 | # Count non-HTML chars/words and keep note of open tags |
|
91 | # Count non-HTML chars/words and keep note of open tags | |
94 | pos = 0 |
|
92 | pos = 0 | |
95 | end_text_pos = 0 |
|
93 | end_text_pos = 0 | |
96 | current_len = 0 |
|
94 | current_len = 0 | |
97 | open_tags = [] |
|
95 | open_tags = [] | |
98 |
|
96 | |||
99 | while current_len <= length: |
|
97 | while current_len <= length: | |
100 | m = REGEX_LINES.search(text, pos) |
|
98 | m = REGEX_LINES.search(text, pos) | |
101 | if not m: |
|
99 | if not m: | |
102 | # Checked through whole string |
|
100 | # Checked through whole string | |
103 | break |
|
101 | break | |
104 | pos = m.end(0) |
|
102 | pos = m.end(0) | |
105 | if m.group(1): |
|
103 | if m.group(1): | |
106 | # It's an actual non-HTML word or char |
|
104 | # It's an actual non-HTML word or char | |
107 | current_len += 1 |
|
105 | current_len += 1 | |
108 | if current_len == length: |
|
106 | if current_len == length: | |
109 | end_text_pos = m.start(0) |
|
107 | end_text_pos = m.start(0) | |
110 | continue |
|
108 | continue | |
111 | # Check for tag |
|
109 | # Check for tag | |
112 | tag = REGEX_TAG.match(m.group(0)) |
|
110 | tag = REGEX_TAG.match(m.group(0)) | |
113 | if not tag or current_len >= length: |
|
111 | if not tag or current_len >= length: | |
114 | # Don't worry about non tags or tags after our truncate point |
|
112 | # Don't worry about non tags or tags after our truncate point | |
115 | continue |
|
113 | continue | |
116 | closing_tag, tagname, self_closing = tag.groups() |
|
114 | closing_tag, tagname, self_closing = tag.groups() | |
117 | # Element names are always case-insensitive |
|
115 | # Element names are always case-insensitive | |
118 | tagname = tagname.lower() |
|
116 | tagname = tagname.lower() | |
119 | if self_closing or tagname in html4_singlets: |
|
117 | if self_closing or tagname in html4_singlets: | |
120 | pass |
|
118 | pass | |
121 | elif closing_tag: |
|
119 | elif closing_tag: | |
122 | # Check for match in open tags list |
|
120 | # Check for match in open tags list | |
123 | try: |
|
121 | try: | |
124 | i = open_tags.index(tagname) |
|
122 | i = open_tags.index(tagname) | |
125 | except ValueError: |
|
123 | except ValueError: | |
126 | pass |
|
124 | pass | |
127 | else: |
|
125 | else: | |
128 | # SGML: An end tag closes, back to the matching start tag, |
|
126 | # SGML: An end tag closes, back to the matching start tag, | |
129 | # all unclosed intervening start tags with omitted end tags |
|
127 | # all unclosed intervening start tags with omitted end tags | |
130 | open_tags = open_tags[i + 1:] |
|
128 | open_tags = open_tags[i + 1:] | |
131 | else: |
|
129 | else: | |
132 | # Add it to the start of the open tags list |
|
130 | # Add it to the start of the open tags list | |
133 | open_tags.insert(0, tagname) |
|
131 | open_tags.insert(0, tagname) | |
134 |
|
132 | |||
135 | if current_len <= length: |
|
133 | if current_len <= length: | |
136 | return text |
|
134 | return text | |
137 | out = text[:end_text_pos] |
|
135 | out = text[:end_text_pos] | |
138 |
|
136 | |||
139 | if not out.endswith(ELLIPSIZER): |
|
137 | if not out.endswith(ELLIPSIZER): | |
140 | out += ELLIPSIZER |
|
138 | out += ELLIPSIZER | |
141 | # Close any tags still open |
|
139 | # Close any tags still open | |
142 | for tag in open_tags: |
|
140 | for tag in open_tags: | |
143 | out += '</%s>' % tag |
|
141 | out += '</%s>' % tag | |
144 | # Return string |
|
142 | # Return string | |
145 | return out |
|
143 | return out |
General Comments 0
You need to be logged in to leave comments.
Login now