##// END OF EJS Templates
Added favorite thread popup
neko259 -
r1340:668e9798 default
parent child Browse files
Show More
@@ -1,88 +1,68
1 from boards.abstracts.settingsmanager import get_settings_manager, \
1 from boards.abstracts.settingsmanager import get_settings_manager, \
2 SETTING_USERNAME, SETTING_LAST_NOTIFICATION_ID, SETTING_IMAGE_VIEWER
2 SETTING_USERNAME, SETTING_LAST_NOTIFICATION_ID, SETTING_IMAGE_VIEWER
3 from boards.models.user import Notification
3 from boards.models.user import Notification
4
4
5 __author__ = 'neko259'
5 __author__ = 'neko259'
6
6
7 from boards import settings, utils
7 from boards import settings, utils
8 from boards.models import Post, Tag
8 from boards.models import Post, Tag
9
9
10 CONTEXT_SITE_NAME = 'site_name'
10 CONTEXT_SITE_NAME = 'site_name'
11 CONTEXT_VERSION = 'version'
11 CONTEXT_VERSION = 'version'
12 CONTEXT_MODERATOR = 'moderator'
12 CONTEXT_MODERATOR = 'moderator'
13 CONTEXT_THEME_CSS = 'theme_css'
13 CONTEXT_THEME_CSS = 'theme_css'
14 CONTEXT_THEME = 'theme'
14 CONTEXT_THEME = 'theme'
15 CONTEXT_PPD = 'posts_per_day'
15 CONTEXT_PPD = 'posts_per_day'
16 CONTEXT_TAGS = 'tags'
16 CONTEXT_TAGS = 'tags'
17 CONTEXT_USER = 'user'
17 CONTEXT_USER = 'user'
18 CONTEXT_NEW_NOTIFICATIONS_COUNT = 'new_notifications_count'
18 CONTEXT_NEW_NOTIFICATIONS_COUNT = 'new_notifications_count'
19 CONTEXT_USERNAME = 'username'
19 CONTEXT_USERNAME = 'username'
20 CONTEXT_TAGS_STR = 'tags_str'
20 CONTEXT_TAGS_STR = 'tags_str'
21 CONTEXT_IMAGE_VIEWER = 'image_viewer'
21 CONTEXT_IMAGE_VIEWER = 'image_viewer'
22 CONTEXT_FAV_THREADS = 'fav_threads'
22 CONTEXT_HAS_FAV_THREADS = 'has_fav_threads'
23
23
24
24
25 def get_notifications(context, request):
25 def get_notifications(context, request):
26 settings_manager = get_settings_manager(request)
26 settings_manager = get_settings_manager(request)
27 username = settings_manager.get_setting(SETTING_USERNAME)
27 username = settings_manager.get_setting(SETTING_USERNAME)
28 new_notifications_count = 0
28 new_notifications_count = 0
29 if username is not None and len(username) > 0:
29 if username is not None and len(username) > 0:
30 last_notification_id = settings_manager.get_setting(
30 last_notification_id = settings_manager.get_setting(
31 SETTING_LAST_NOTIFICATION_ID)
31 SETTING_LAST_NOTIFICATION_ID)
32
32
33 new_notifications_count = Notification.objects.get_notification_posts(
33 new_notifications_count = Notification.objects.get_notification_posts(
34 username=username, last=last_notification_id).count()
34 username=username, last=last_notification_id).count()
35 context[CONTEXT_NEW_NOTIFICATIONS_COUNT] = new_notifications_count
35 context[CONTEXT_NEW_NOTIFICATIONS_COUNT] = new_notifications_count
36 context[CONTEXT_USERNAME] = username
36 context[CONTEXT_USERNAME] = username
37
37
38
38
39 def user_and_ui_processor(request):
39 def user_and_ui_processor(request):
40 context = dict()
40 context = dict()
41
41
42 context[CONTEXT_PPD] = float(Post.objects.get_posts_per_day())
42 context[CONTEXT_PPD] = float(Post.objects.get_posts_per_day())
43
43
44 settings_manager = get_settings_manager(request)
44 settings_manager = get_settings_manager(request)
45 fav_tags = settings_manager.get_fav_tags()
45 fav_tags = settings_manager.get_fav_tags()
46 context[CONTEXT_TAGS] = fav_tags
46 context[CONTEXT_TAGS] = fav_tags
47
47
48 _get_fav_threads(context, settings_manager)
49
50 context[CONTEXT_TAGS_STR] = Tag.objects.get_tag_url_list(fav_tags)
48 context[CONTEXT_TAGS_STR] = Tag.objects.get_tag_url_list(fav_tags)
51 theme = settings_manager.get_theme()
49 theme = settings_manager.get_theme()
52 context[CONTEXT_THEME] = theme
50 context[CONTEXT_THEME] = theme
53 context[CONTEXT_THEME_CSS] = 'css/' + theme + '/base_page.css'
51 context[CONTEXT_THEME_CSS] = 'css/' + theme + '/base_page.css'
54
52
55 # This shows the moderator panel
53 # This shows the moderator panel
56 context[CONTEXT_MODERATOR] = utils.is_moderator(request)
54 context[CONTEXT_MODERATOR] = utils.is_moderator(request)
57
55
58 context[CONTEXT_VERSION] = settings.get('Version', 'Version')
56 context[CONTEXT_VERSION] = settings.get('Version', 'Version')
59 context[CONTEXT_SITE_NAME] = settings.get('Version', 'SiteName')
57 context[CONTEXT_SITE_NAME] = settings.get('Version', 'SiteName')
60
58
61 context[CONTEXT_IMAGE_VIEWER] = settings_manager.get_setting(
59 context[CONTEXT_IMAGE_VIEWER] = settings_manager.get_setting(
62 SETTING_IMAGE_VIEWER,
60 SETTING_IMAGE_VIEWER,
63 default=settings.get('View', 'DefaultImageViewer'))
61 default=settings.get('View', 'DefaultImageViewer'))
64
62
63 context[CONTEXT_HAS_FAV_THREADS] =\
64 len(settings_manager.get_fav_threads()) > 0
65
65 get_notifications(context, request)
66 get_notifications(context, request)
66
67
67 return context
68 return context
68
69
70 def _get_fav_threads(context, settings_manager):
71 fav_threads_setting = settings_manager.get_fav_threads()
72 if fav_threads_setting:
73 fav_threads = Post.objects.filter(
74 id__in=fav_threads_setting.keys()).only('url', 'id', 'thread')\
75 .select_related('thread')
76
77 context_thread_list = []
78 for post in fav_threads:
79 new_replies = post.get_thread().get_replies_newer(fav_threads_setting[str(post.id)])
80
81 element = dict()
82 element['post'] = post
83 element['count'] = new_replies.count()
84 if element['count'] > 0:
85 element['new_post'] = new_replies.first().get_absolute_url()
86 context_thread_list.append(element)
87 context[CONTEXT_FAV_THREADS] = context_thread_list
88
1 NO CONTENT: modified file, binary diff hidden
NO CONTENT: modified file, binary diff hidden
@@ -1,480 +1,486
1 # SOME DESCRIPTIVE TITLE.
1 # SOME DESCRIPTIVE TITLE.
2 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
2 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3 # This file is distributed under the same license as the PACKAGE package.
3 # This file is distributed under the same license as the PACKAGE package.
4 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
4 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
5 #
5 #
6 msgid ""
6 msgid ""
7 msgstr ""
7 msgstr ""
8 "Project-Id-Version: PACKAGE VERSION\n"
8 "Project-Id-Version: PACKAGE VERSION\n"
9 "Report-Msgid-Bugs-To: \n"
9 "Report-Msgid-Bugs-To: \n"
10 "POT-Creation-Date: 2015-09-12 12:48+0300\n"
10 "POT-Creation-Date: 2015-09-12 12:48+0300\n"
11 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
11 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
12 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
12 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
13 "Language-Team: LANGUAGE <LL@li.org>\n"
13 "Language-Team: LANGUAGE <LL@li.org>\n"
14 "Language: ru\n"
14 "Language: ru\n"
15 "MIME-Version: 1.0\n"
15 "MIME-Version: 1.0\n"
16 "Content-Type: text/plain; charset=UTF-8\n"
16 "Content-Type: text/plain; charset=UTF-8\n"
17 "Content-Transfer-Encoding: 8bit\n"
17 "Content-Transfer-Encoding: 8bit\n"
18 "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
18 "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
19 "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
19 "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
20
20
21 #: admin.py:22
21 #: admin.py:22
22 msgid "{} posters were banned"
22 msgid "{} posters were banned"
23 msgstr ""
23 msgstr ""
24
24
25 #: authors.py:9
25 #: authors.py:9
26 msgid "author"
26 msgid "author"
27 msgstr "Π°Π²Ρ‚ΠΎΡ€"
27 msgstr "Π°Π²Ρ‚ΠΎΡ€"
28
28
29 #: authors.py:10
29 #: authors.py:10
30 msgid "developer"
30 msgid "developer"
31 msgstr "Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ"
31 msgstr "Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ"
32
32
33 #: authors.py:11
33 #: authors.py:11
34 msgid "javascript developer"
34 msgid "javascript developer"
35 msgstr "Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ javascript"
35 msgstr "Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ javascript"
36
36
37 #: authors.py:12
37 #: authors.py:12
38 msgid "designer"
38 msgid "designer"
39 msgstr "Π΄ΠΈΠ·Π°ΠΉΠ½Π΅Ρ€"
39 msgstr "Π΄ΠΈΠ·Π°ΠΉΠ½Π΅Ρ€"
40
40
41 #: forms.py:30
41 #: forms.py:30
42 msgid "Type message here. Use formatting panel for more advanced usage."
42 msgid "Type message here. Use formatting panel for more advanced usage."
43 msgstr ""
43 msgstr ""
44 "Π’Π²ΠΎΠ΄ΠΈΡ‚Π΅ сообщСниС сюда. Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉΡ‚Π΅ панСль для Π±ΠΎΠ»Π΅Π΅ слоТного форматирования."
44 "Π’Π²ΠΎΠ΄ΠΈΡ‚Π΅ сообщСниС сюда. Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉΡ‚Π΅ панСль для Π±ΠΎΠ»Π΅Π΅ слоТного форматирования."
45
45
46 #: forms.py:31
46 #: forms.py:31
47 msgid "music images i_dont_like_tags"
47 msgid "music images i_dont_like_tags"
48 msgstr "ΠΌΡƒΠ·Ρ‹ΠΊΠ° ΠΊΠ°Ρ€Ρ‚ΠΈΠ½ΠΊΠΈ Ρ‚Π΅Π³ΠΈ_Π½Π΅_Π½ΡƒΠΆΠ½Ρ‹"
48 msgstr "ΠΌΡƒΠ·Ρ‹ΠΊΠ° ΠΊΠ°Ρ€Ρ‚ΠΈΠ½ΠΊΠΈ Ρ‚Π΅Π³ΠΈ_Π½Π΅_Π½ΡƒΠΆΠ½Ρ‹"
49
49
50 #: forms.py:33
50 #: forms.py:33
51 msgid "Title"
51 msgid "Title"
52 msgstr "Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ"
52 msgstr "Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ"
53
53
54 #: forms.py:34
54 #: forms.py:34
55 msgid "Text"
55 msgid "Text"
56 msgstr "ВСкст"
56 msgstr "ВСкст"
57
57
58 #: forms.py:35
58 #: forms.py:35
59 msgid "Tag"
59 msgid "Tag"
60 msgstr "ΠœΠ΅Ρ‚ΠΊΠ°"
60 msgstr "ΠœΠ΅Ρ‚ΠΊΠ°"
61
61
62 #: forms.py:36 templates/boards/base.html:40 templates/search/search.html:7
62 #: forms.py:36 templates/boards/base.html:40 templates/search/search.html:7
63 msgid "Search"
63 msgid "Search"
64 msgstr "Поиск"
64 msgstr "Поиск"
65
65
66 #: forms.py:139
66 #: forms.py:139
67 msgid "File"
67 msgid "File"
68 msgstr "Π€Π°ΠΉΠ»"
68 msgstr "Π€Π°ΠΉΠ»"
69
69
70 #: forms.py:142
70 #: forms.py:142
71 msgid "File URL"
71 msgid "File URL"
72 msgstr "URL Ρ„Π°ΠΉΠ»Π°"
72 msgstr "URL Ρ„Π°ΠΉΠ»Π°"
73
73
74 #: forms.py:148
74 #: forms.py:148
75 msgid "e-mail"
75 msgid "e-mail"
76 msgstr ""
76 msgstr ""
77
77
78 #: forms.py:151
78 #: forms.py:151
79 msgid "Additional threads"
79 msgid "Additional threads"
80 msgstr "Π”ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Π΅ Ρ‚Π΅ΠΌΡ‹"
80 msgstr "Π”ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Π΅ Ρ‚Π΅ΠΌΡ‹"
81
81
82 #: forms.py:162
82 #: forms.py:162
83 #, python-format
83 #, python-format
84 msgid "Title must have less than %s characters"
84 msgid "Title must have less than %s characters"
85 msgstr "Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ Π΄ΠΎΠ»ΠΆΠ΅Π½ ΠΈΠΌΠ΅Ρ‚ΡŒ мСньшС %s символов"
85 msgstr "Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ Π΄ΠΎΠ»ΠΆΠ΅Π½ ΠΈΠΌΠ΅Ρ‚ΡŒ мСньшС %s символов"
86
86
87 #: forms.py:172
87 #: forms.py:172
88 #, python-format
88 #, python-format
89 msgid "Text must have less than %s characters"
89 msgid "Text must have less than %s characters"
90 msgstr "ВСкст Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±Ρ‹Ρ‚ΡŒ ΠΊΠΎΡ€ΠΎΡ‡Π΅ %s символов"
90 msgstr "ВСкст Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±Ρ‹Ρ‚ΡŒ ΠΊΠΎΡ€ΠΎΡ‡Π΅ %s символов"
91
91
92 #: forms.py:192
92 #: forms.py:192
93 msgid "Invalid URL"
93 msgid "Invalid URL"
94 msgstr "НСвСрный URL"
94 msgstr "НСвСрный URL"
95
95
96 #: forms.py:213
96 #: forms.py:213
97 msgid "Invalid additional thread list"
97 msgid "Invalid additional thread list"
98 msgstr "НСвСрный список Π΄ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Ρ… Ρ‚Π΅ΠΌ"
98 msgstr "НСвСрный список Π΄ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Ρ… Ρ‚Π΅ΠΌ"
99
99
100 #: forms.py:258
100 #: forms.py:258
101 msgid "Either text or file must be entered."
101 msgid "Either text or file must be entered."
102 msgstr "ВСкст ΠΈΠ»ΠΈ Ρ„Π°ΠΉΠ» Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ Π²Π²Π΅Π΄Π΅Π½Ρ‹."
102 msgstr "ВСкст ΠΈΠ»ΠΈ Ρ„Π°ΠΉΠ» Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ Π²Π²Π΅Π΄Π΅Π½Ρ‹."
103
103
104 #: forms.py:317 templates/boards/all_threads.html:148
104 #: forms.py:317 templates/boards/all_threads.html:148
105 #: templates/boards/rss/post.html:10 templates/boards/tags.html:6
105 #: templates/boards/rss/post.html:10 templates/boards/tags.html:6
106 msgid "Tags"
106 msgid "Tags"
107 msgstr "ΠœΠ΅Ρ‚ΠΊΠΈ"
107 msgstr "ΠœΠ΅Ρ‚ΠΊΠΈ"
108
108
109 #: forms.py:324
109 #: forms.py:324
110 msgid "Inappropriate characters in tags."
110 msgid "Inappropriate characters in tags."
111 msgstr "НСдопустимыС символы Π² ΠΌΠ΅Ρ‚ΠΊΠ°Ρ…."
111 msgstr "НСдопустимыС символы Π² ΠΌΠ΅Ρ‚ΠΊΠ°Ρ…."
112
112
113 #: forms.py:338
113 #: forms.py:338
114 msgid "Need at least one section."
114 msgid "Need at least one section."
115 msgstr "НуТСн хотя Π±Ρ‹ ΠΎΠ΄ΠΈΠ½ Ρ€Π°Π·Π΄Π΅Π»."
115 msgstr "НуТСн хотя Π±Ρ‹ ΠΎΠ΄ΠΈΠ½ Ρ€Π°Π·Π΄Π΅Π»."
116
116
117 #: forms.py:350
117 #: forms.py:350
118 msgid "Theme"
118 msgid "Theme"
119 msgstr "Π’Π΅ΠΌΠ°"
119 msgstr "Π’Π΅ΠΌΠ°"
120
120
121 #: forms.py:351
121 #: forms.py:351
122 msgid "Image view mode"
122 msgid "Image view mode"
123 msgstr "Π Π΅ΠΆΠΈΠΌ просмотра ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ"
123 msgstr "Π Π΅ΠΆΠΈΠΌ просмотра ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ"
124
124
125 #: forms.py:352
125 #: forms.py:352
126 msgid "User name"
126 msgid "User name"
127 msgstr "Имя ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ"
127 msgstr "Имя ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ"
128
128
129 #: forms.py:353
129 #: forms.py:353
130 msgid "Time zone"
130 msgid "Time zone"
131 msgstr "Часовой пояс"
131 msgstr "Часовой пояс"
132
132
133 #: forms.py:359
133 #: forms.py:359
134 msgid "Inappropriate characters."
134 msgid "Inappropriate characters."
135 msgstr "НСдопустимыС символы."
135 msgstr "НСдопустимыС символы."
136
136
137 #: templates/boards/404.html:6
137 #: templates/boards/404.html:6
138 msgid "Not found"
138 msgid "Not found"
139 msgstr "НС найдСно"
139 msgstr "НС найдСно"
140
140
141 #: templates/boards/404.html:12
141 #: templates/boards/404.html:12
142 msgid "This page does not exist"
142 msgid "This page does not exist"
143 msgstr "Π­Ρ‚ΠΎΠΉ страницы Π½Π΅ сущСствуСт"
143 msgstr "Π­Ρ‚ΠΎΠΉ страницы Π½Π΅ сущСствуСт"
144
144
145 #: templates/boards/all_threads.html:35
145 #: templates/boards/all_threads.html:35
146 msgid "Related message"
146 msgid "Related message"
147 msgstr "БвязанноС сообщСниС"
147 msgstr "БвязанноС сообщСниС"
148
148
149 #: templates/boards/all_threads.html:69
149 #: templates/boards/all_threads.html:69
150 msgid "Edit tag"
150 msgid "Edit tag"
151 msgstr "Π˜Π·ΠΌΠ΅Π½ΠΈΡ‚ΡŒ ΠΌΠ΅Ρ‚ΠΊΡƒ"
151 msgstr "Π˜Π·ΠΌΠ΅Π½ΠΈΡ‚ΡŒ ΠΌΠ΅Ρ‚ΠΊΡƒ"
152
152
153 #: templates/boards/all_threads.html:75
153 #: templates/boards/all_threads.html:75
154 #, python-format
154 #, python-format
155 msgid ""
155 msgid ""
156 "This tag has %(thread_count)s threads (%(active_thread_count)s active) and "
156 "This tag has %(thread_count)s threads (%(active_thread_count)s active) and "
157 "%(post_count)s posts."
157 "%(post_count)s posts."
158 msgstr ""
158 msgstr ""
159 "Π‘ этой ΠΌΠ΅Ρ‚ΠΊΠΎΠΉ Π΅ΡΡ‚ΡŒ %(thread_count)s Ρ‚Π΅ΠΌ (%(active_thread_count)s Π°ΠΊΡ‚ΠΈΠ²Π½Ρ‹Ρ…) ΠΈ "
159 "Π‘ этой ΠΌΠ΅Ρ‚ΠΊΠΎΠΉ Π΅ΡΡ‚ΡŒ %(thread_count)s Ρ‚Π΅ΠΌ (%(active_thread_count)s Π°ΠΊΡ‚ΠΈΠ²Π½Ρ‹Ρ…) ΠΈ "
160 "%(post_count)s сообщСний."
160 "%(post_count)s сообщСний."
161
161
162 #: templates/boards/all_threads.html:77
162 #: templates/boards/all_threads.html:77
163 msgid "Related tags:"
163 msgid "Related tags:"
164 msgstr "ΠŸΠΎΡ…ΠΎΠΆΠΈΠ΅ ΠΌΠ΅Ρ‚ΠΊΠΈ:"
164 msgstr "ΠŸΠΎΡ…ΠΎΠΆΠΈΠ΅ ΠΌΠ΅Ρ‚ΠΊΠΈ:"
165
165
166 #: templates/boards/all_threads.html:90 templates/boards/feed.html:30
166 #: templates/boards/all_threads.html:90 templates/boards/feed.html:30
167 #: templates/boards/notifications.html:17 templates/search/search.html:26
167 #: templates/boards/notifications.html:17 templates/search/search.html:26
168 msgid "Previous page"
168 msgid "Previous page"
169 msgstr "ΠŸΡ€Π΅Π΄Ρ‹Π΄ΡƒΡ‰Π°Ρ страница"
169 msgstr "ΠŸΡ€Π΅Π΄Ρ‹Π΄ΡƒΡ‰Π°Ρ страница"
170
170
171 #: templates/boards/all_threads.html:104
171 #: templates/boards/all_threads.html:104
172 #, python-format
172 #, python-format
173 #| msgid "Skipped %(count)s replies. Open thread to see all replies."
173 #| msgid "Skipped %(count)s replies. Open thread to see all replies."
174 msgid "Skipped %(count)s reply. Open thread to see all replies."
174 msgid "Skipped %(count)s reply. Open thread to see all replies."
175 msgid_plural "Skipped %(count)s replies. Open thread to see all replies."
175 msgid_plural "Skipped %(count)s replies. Open thread to see all replies."
176 msgstr[0] ""
176 msgstr[0] ""
177 "ΠŸΡ€ΠΎΠΏΡƒΡ‰Π΅Π½ %(count)s ΠΎΡ‚Π²Π΅Ρ‚. ΠžΡ‚ΠΊΡ€ΠΎΠΉΡ‚Π΅ Ρ‚Ρ€Π΅Π΄, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΡƒΠ²ΠΈΠ΄Π΅Ρ‚ΡŒ всС ΠΎΡ‚Π²Π΅Ρ‚Ρ‹."
177 "ΠŸΡ€ΠΎΠΏΡƒΡ‰Π΅Π½ %(count)s ΠΎΡ‚Π²Π΅Ρ‚. ΠžΡ‚ΠΊΡ€ΠΎΠΉΡ‚Π΅ Ρ‚Ρ€Π΅Π΄, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΡƒΠ²ΠΈΠ΄Π΅Ρ‚ΡŒ всС ΠΎΡ‚Π²Π΅Ρ‚Ρ‹."
178 msgstr[1] ""
178 msgstr[1] ""
179 "ΠŸΡ€ΠΎΠΏΡƒΡ‰Π΅Π½ΠΎ %(count)s ΠΎΡ‚Π²Π΅Ρ‚Π°. ΠžΡ‚ΠΊΡ€ΠΎΠΉΡ‚Π΅ Ρ‚Ρ€Π΅Π΄, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΡƒΠ²ΠΈΠ΄Π΅Ρ‚ΡŒ всС ΠΎΡ‚Π²Π΅Ρ‚Ρ‹."
179 "ΠŸΡ€ΠΎΠΏΡƒΡ‰Π΅Π½ΠΎ %(count)s ΠΎΡ‚Π²Π΅Ρ‚Π°. ΠžΡ‚ΠΊΡ€ΠΎΠΉΡ‚Π΅ Ρ‚Ρ€Π΅Π΄, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΡƒΠ²ΠΈΠ΄Π΅Ρ‚ΡŒ всС ΠΎΡ‚Π²Π΅Ρ‚Ρ‹."
180 msgstr[2] ""
180 msgstr[2] ""
181 "ΠŸΡ€ΠΎΠΏΡƒΡ‰Π΅Π½ΠΎ %(count)s ΠΎΡ‚Π²Π΅Ρ‚ΠΎΠ². ΠžΡ‚ΠΊΡ€ΠΎΠΉΡ‚Π΅ Ρ‚Ρ€Π΅Π΄, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΡƒΠ²ΠΈΠ΄Π΅Ρ‚ΡŒ всС ΠΎΡ‚Π²Π΅Ρ‚Ρ‹."
181 "ΠŸΡ€ΠΎΠΏΡƒΡ‰Π΅Π½ΠΎ %(count)s ΠΎΡ‚Π²Π΅Ρ‚ΠΎΠ². ΠžΡ‚ΠΊΡ€ΠΎΠΉΡ‚Π΅ Ρ‚Ρ€Π΅Π΄, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΡƒΠ²ΠΈΠ΄Π΅Ρ‚ΡŒ всС ΠΎΡ‚Π²Π΅Ρ‚Ρ‹."
182
182
183 #: templates/boards/all_threads.html:122 templates/boards/feed.html:40
183 #: templates/boards/all_threads.html:122 templates/boards/feed.html:40
184 #: templates/boards/notifications.html:27 templates/search/search.html:37
184 #: templates/boards/notifications.html:27 templates/search/search.html:37
185 msgid "Next page"
185 msgid "Next page"
186 msgstr "Π‘Π»Π΅Π΄ΡƒΡŽΡ‰Π°Ρ страница"
186 msgstr "Π‘Π»Π΅Π΄ΡƒΡŽΡ‰Π°Ρ страница"
187
187
188 #: templates/boards/all_threads.html:127
188 #: templates/boards/all_threads.html:127
189 msgid "No threads exist. Create the first one!"
189 msgid "No threads exist. Create the first one!"
190 msgstr "НСт Ρ‚Π΅ΠΌ. Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ ΠΏΠ΅Ρ€Π²ΡƒΡŽ!"
190 msgstr "НСт Ρ‚Π΅ΠΌ. Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ ΠΏΠ΅Ρ€Π²ΡƒΡŽ!"
191
191
192 #: templates/boards/all_threads.html:133
192 #: templates/boards/all_threads.html:133
193 msgid "Create new thread"
193 msgid "Create new thread"
194 msgstr "Π‘ΠΎΠ·Π΄Π°Ρ‚ΡŒ Π½ΠΎΠ²ΡƒΡŽ Ρ‚Π΅ΠΌΡƒ"
194 msgstr "Π‘ΠΎΠ·Π΄Π°Ρ‚ΡŒ Π½ΠΎΠ²ΡƒΡŽ Ρ‚Π΅ΠΌΡƒ"
195
195
196 #: templates/boards/all_threads.html:138 templates/boards/preview.html:16
196 #: templates/boards/all_threads.html:138 templates/boards/preview.html:16
197 #: templates/boards/thread_normal.html:51
197 #: templates/boards/thread_normal.html:51
198 msgid "Post"
198 msgid "Post"
199 msgstr "ΠžΡ‚ΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ"
199 msgstr "ΠžΡ‚ΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ"
200
200
201 #: templates/boards/all_threads.html:139 templates/boards/preview.html:6
201 #: templates/boards/all_threads.html:139 templates/boards/preview.html:6
202 #: templates/boards/staticpages/help.html:21
202 #: templates/boards/staticpages/help.html:21
203 #: templates/boards/thread_normal.html:52
203 #: templates/boards/thread_normal.html:52
204 msgid "Preview"
204 msgid "Preview"
205 msgstr "ΠŸΡ€Π΅Π΄ΠΏΡ€ΠΎΡΠΌΠΎΡ‚Ρ€"
205 msgstr "ΠŸΡ€Π΅Π΄ΠΏΡ€ΠΎΡΠΌΠΎΡ‚Ρ€"
206
206
207 #: templates/boards/all_threads.html:144
207 #: templates/boards/all_threads.html:144
208 msgid "Tags must be delimited by spaces. Text or image is required."
208 msgid "Tags must be delimited by spaces. Text or image is required."
209 msgstr ""
209 msgstr ""
210 "ΠœΠ΅Ρ‚ΠΊΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ Ρ€Π°Π·Π΄Π΅Π»Π΅Π½Ρ‹ ΠΏΡ€ΠΎΠ±Π΅Π»Π°ΠΌΠΈ. ВСкст ΠΈΠ»ΠΈ ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹."
210 "ΠœΠ΅Ρ‚ΠΊΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ Ρ€Π°Π·Π΄Π΅Π»Π΅Π½Ρ‹ ΠΏΡ€ΠΎΠ±Π΅Π»Π°ΠΌΠΈ. ВСкст ΠΈΠ»ΠΈ ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹."
211
211
212 #: templates/boards/all_threads.html:147 templates/boards/thread_normal.html:58
212 #: templates/boards/all_threads.html:147 templates/boards/thread_normal.html:58
213 msgid "Text syntax"
213 msgid "Text syntax"
214 msgstr "Бинтаксис тСкста"
214 msgstr "Бинтаксис тСкста"
215
215
216 #: templates/boards/all_threads.html:161 templates/boards/feed.html:53
216 #: templates/boards/all_threads.html:161 templates/boards/feed.html:53
217 msgid "Pages:"
217 msgid "Pages:"
218 msgstr "Π‘Ρ‚Ρ€Π°Π½ΠΈΡ†Ρ‹: "
218 msgstr "Π‘Ρ‚Ρ€Π°Π½ΠΈΡ†Ρ‹: "
219
219
220 #: templates/boards/authors.html:6 templates/boards/authors.html.py:12
220 #: templates/boards/authors.html:6 templates/boards/authors.html.py:12
221 msgid "Authors"
221 msgid "Authors"
222 msgstr "Авторы"
222 msgstr "Авторы"
223
223
224 #: templates/boards/authors.html:26
224 #: templates/boards/authors.html:26
225 msgid "Distributed under the"
225 msgid "Distributed under the"
226 msgstr "РаспространяСтся ΠΏΠΎΠ΄"
226 msgstr "РаспространяСтся ΠΏΠΎΠ΄"
227
227
228 #: templates/boards/authors.html:28
228 #: templates/boards/authors.html:28
229 msgid "license"
229 msgid "license"
230 msgstr "Π»ΠΈΡ†Π΅Π½Π·ΠΈΠ΅ΠΉ"
230 msgstr "Π»ΠΈΡ†Π΅Π½Π·ΠΈΠ΅ΠΉ"
231
231
232 #: templates/boards/authors.html:30
232 #: templates/boards/authors.html:30
233 msgid "Repository"
233 msgid "Repository"
234 msgstr "Π Π΅ΠΏΠΎΠ·ΠΈΡ‚ΠΎΡ€ΠΈΠΉ"
234 msgstr "Π Π΅ΠΏΠΎΠ·ΠΈΡ‚ΠΎΡ€ΠΈΠΉ"
235
235
236 #: templates/boards/base.html:14 templates/boards/base.html.py:41
236 #: templates/boards/base.html:14 templates/boards/base.html.py:41
237 msgid "Feed"
237 msgid "Feed"
238 msgstr "Π›Π΅Π½Ρ‚Π°"
238 msgstr "Π›Π΅Π½Ρ‚Π°"
239
239
240 #: templates/boards/base.html:31
240 #: templates/boards/base.html:31
241 msgid "All threads"
241 msgid "All threads"
242 msgstr "ВсС Ρ‚Π΅ΠΌΡ‹"
242 msgstr "ВсС Ρ‚Π΅ΠΌΡ‹"
243
243
244 #: templates/boards/base.html:37
244 #: templates/boards/base.html:37
245 msgid "Add tags"
245 msgid "Add tags"
246 msgstr "Π”ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ ΠΌΠ΅Ρ‚ΠΊΠΈ"
246 msgstr "Π”ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ ΠΌΠ΅Ρ‚ΠΊΠΈ"
247
247
248 #: templates/boards/base.html:39
248 #: templates/boards/base.html:39
249 msgid "Tag management"
249 msgid "Tag management"
250 msgstr "Π£ΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠ΅ ΠΌΠ΅Ρ‚ΠΊΠ°ΠΌΠΈ"
250 msgstr "Π£ΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠ΅ ΠΌΠ΅Ρ‚ΠΊΠ°ΠΌΠΈ"
251
251
252 #: templates/boards/base.html:39
252 #: templates/boards/base.html:39
253 msgid "tags"
253 msgid "tags"
254 msgstr "ΠΌΠ΅Ρ‚ΠΊΠΈ"
254 msgstr "ΠΌΠ΅Ρ‚ΠΊΠΈ"
255
255
256 #: templates/boards/base.html:40
256 #: templates/boards/base.html:40
257 msgid "search"
257 msgid "search"
258 msgstr "поиск"
258 msgstr "поиск"
259
259
260 #: templates/boards/base.html:41 templates/boards/feed.html:11
260 #: templates/boards/base.html:41 templates/boards/feed.html:11
261 msgid "feed"
261 msgid "feed"
262 msgstr "Π»Π΅Π½Ρ‚Π°"
262 msgstr "Π»Π΅Π½Ρ‚Π°"
263
263
264 #: templates/boards/base.html:42 templates/boards/random.html:6
264 #: templates/boards/base.html:42 templates/boards/random.html:6
265 msgid "Random images"
265 msgid "Random images"
266 msgstr "Π‘Π»ΡƒΡ‡Π°ΠΉΠ½Ρ‹Π΅ изобраТСния"
266 msgstr "Π‘Π»ΡƒΡ‡Π°ΠΉΠ½Ρ‹Π΅ изобраТСния"
267
267
268 #: templates/boards/base.html:42
268 #: templates/boards/base.html:42
269 msgid "random"
269 msgid "random"
270 msgstr "случайныС"
270 msgstr "случайныС"
271
271
272 #: templates/boards/base.html:45 templates/boards/base.html.py:46
272 #: templates/boards/base.html:45 templates/boards/base.html.py:46
273 #: templates/boards/notifications.html:8
273 #: templates/boards/notifications.html:8
274 msgid "Notifications"
274 msgid "Notifications"
275 msgstr "УвСдомлСния"
275 msgstr "УвСдомлСния"
276
276
277 #: templates/boards/base.html:53 templates/boards/settings.html:8
277 #: templates/boards/base.html:53 templates/boards/settings.html:8
278 msgid "Settings"
278 msgid "Settings"
279 msgstr "Настройки"
279 msgstr "Настройки"
280
280
281 #: templates/boards/base.html:79
281 #: templates/boards/base.html:79
282 msgid "Admin"
282 msgid "Admin"
283 msgstr "АдминистрированиС"
283 msgstr "АдминистрированиС"
284
284
285 #: templates/boards/base.html:81
285 #: templates/boards/base.html:81
286 #, python-format
286 #, python-format
287 msgid "Speed: %(ppd)s posts per day"
287 msgid "Speed: %(ppd)s posts per day"
288 msgstr "Π‘ΠΊΠΎΡ€ΠΎΡΡ‚ΡŒ: %(ppd)s сообщСний Π² дСнь"
288 msgstr "Π‘ΠΊΠΎΡ€ΠΎΡΡ‚ΡŒ: %(ppd)s сообщСний Π² дСнь"
289
289
290 #: templates/boards/base.html:83
290 #: templates/boards/base.html:83
291 msgid "Up"
291 msgid "Up"
292 msgstr "Π’Π²Π΅Ρ€Ρ…"
292 msgstr "Π’Π²Π΅Ρ€Ρ…"
293
293
294 #: templates/boards/feed.html:45
294 #: templates/boards/feed.html:45
295 msgid "No posts exist. Create the first one!"
295 msgid "No posts exist. Create the first one!"
296 msgstr "НСт сообщСний. Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ ΠΏΠ΅Ρ€Π²ΠΎΠ΅!"
296 msgstr "НСт сообщСний. Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ ΠΏΠ΅Ρ€Π²ΠΎΠ΅!"
297
297
298 #: templates/boards/post.html:32
298 #: templates/boards/post.html:32
299 msgid "Open"
299 msgid "Open"
300 msgstr "ΠžΡ‚ΠΊΡ€Ρ‹Ρ‚ΡŒ"
300 msgstr "ΠžΡ‚ΠΊΡ€Ρ‹Ρ‚ΡŒ"
301
301
302 #: templates/boards/post.html:34 templates/boards/post.html.py:45
302 #: templates/boards/post.html:34 templates/boards/post.html.py:45
303 msgid "Reply"
303 msgid "Reply"
304 msgstr "ΠžΡ‚Π²Π΅Ρ‚ΠΈΡ‚ΡŒ"
304 msgstr "ΠžΡ‚Π²Π΅Ρ‚ΠΈΡ‚ΡŒ"
305
305
306 #: templates/boards/post.html:40
306 #: templates/boards/post.html:40
307 msgid " in "
307 msgid " in "
308 msgstr " Π² "
308 msgstr " Π² "
309
309
310 #: templates/boards/post.html:50
310 #: templates/boards/post.html:50
311 msgid "Edit"
311 msgid "Edit"
312 msgstr "Π˜Π·ΠΌΠ΅Π½ΠΈΡ‚ΡŒ"
312 msgstr "Π˜Π·ΠΌΠ΅Π½ΠΈΡ‚ΡŒ"
313
313
314 #: templates/boards/post.html:52
314 #: templates/boards/post.html:52
315 msgid "Edit thread"
315 msgid "Edit thread"
316 msgstr "Π˜Π·ΠΌΠ΅Π½ΠΈΡ‚ΡŒ Ρ‚Π΅ΠΌΡƒ"
316 msgstr "Π˜Π·ΠΌΠ΅Π½ΠΈΡ‚ΡŒ Ρ‚Π΅ΠΌΡƒ"
317
317
318 #: templates/boards/post.html:94
318 #: templates/boards/post.html:94
319 msgid "Replies"
319 msgid "Replies"
320 msgstr "ΠžΡ‚Π²Π΅Ρ‚Ρ‹"
320 msgstr "ΠžΡ‚Π²Π΅Ρ‚Ρ‹"
321
321
322 #: templates/boards/post.html:105
322 #: templates/boards/post.html:105
323 #, python-format
323 #, python-format
324 msgid "%(count)s message"
324 msgid "%(count)s message"
325 msgid_plural "%(count)s messages"
325 msgid_plural "%(count)s messages"
326 msgstr[0] "%(count)s сообщСниС"
326 msgstr[0] "%(count)s сообщСниС"
327 msgstr[1] "%(count)s сообщСния"
327 msgstr[1] "%(count)s сообщСния"
328 msgstr[2] "%(count)s сообщСний"
328 msgstr[2] "%(count)s сообщСний"
329
329
330 #, python-format
330 #, python-format
331 msgid "Please wait %(delay)d second before sending message"
331 msgid "Please wait %(delay)d second before sending message"
332 msgid_plural "Please wait %(delay)d seconds before sending message"
332 msgid_plural "Please wait %(delay)d seconds before sending message"
333 msgstr[0] "ΠŸΠΎΠΆΠ°Π»ΡƒΠΉΡΡ‚Π° ΠΏΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡ‚Π΅ %(delay)d сСкунду ΠΏΠ΅Ρ€Π΅Π΄ ΠΎΡ‚ΠΏΡ€Π°Π²ΠΊΠΎΠΉ сообщСния"
333 msgstr[0] "ΠŸΠΎΠΆΠ°Π»ΡƒΠΉΡΡ‚Π° ΠΏΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡ‚Π΅ %(delay)d сСкунду ΠΏΠ΅Ρ€Π΅Π΄ ΠΎΡ‚ΠΏΡ€Π°Π²ΠΊΠΎΠΉ сообщСния"
334 msgstr[1] "ΠŸΠΎΠΆΠ°Π»ΡƒΠΉΡΡ‚Π° ΠΏΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡ‚Π΅ %(delay)d сСкунды ΠΏΠ΅Ρ€Π΅Π΄ ΠΎΡ‚ΠΏΡ€Π°Π²ΠΊΠΎΠΉ сообщСния"
334 msgstr[1] "ΠŸΠΎΠΆΠ°Π»ΡƒΠΉΡΡ‚Π° ΠΏΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡ‚Π΅ %(delay)d сСкунды ΠΏΠ΅Ρ€Π΅Π΄ ΠΎΡ‚ΠΏΡ€Π°Π²ΠΊΠΎΠΉ сообщСния"
335 msgstr[2] "ΠŸΠΎΠΆΠ°Π»ΡƒΠΉΡΡ‚Π° ΠΏΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡ‚Π΅ %(delay)d сСкунд ΠΏΠ΅Ρ€Π΅Π΄ ΠΎΡ‚ΠΏΡ€Π°Π²ΠΊΠΎΠΉ сообщСния"
335 msgstr[2] "ΠŸΠΎΠΆΠ°Π»ΡƒΠΉΡΡ‚Π° ΠΏΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡ‚Π΅ %(delay)d сСкунд ΠΏΠ΅Ρ€Π΅Π΄ ΠΎΡ‚ΠΏΡ€Π°Π²ΠΊΠΎΠΉ сообщСния"
336
336
337 #: templates/boards/post.html:106
337 #: templates/boards/post.html:106
338 #, python-format
338 #, python-format
339 msgid "%(count)s image"
339 msgid "%(count)s image"
340 msgid_plural "%(count)s images"
340 msgid_plural "%(count)s images"
341 msgstr[0] "%(count)s ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅"
341 msgstr[0] "%(count)s ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅"
342 msgstr[1] "%(count)s изобраТСния"
342 msgstr[1] "%(count)s изобраТСния"
343 msgstr[2] "%(count)s ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ"
343 msgstr[2] "%(count)s ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ"
344
344
345 #: templates/boards/rss/post.html:5
345 #: templates/boards/rss/post.html:5
346 msgid "Post image"
346 msgid "Post image"
347 msgstr "Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ сообщСния"
347 msgstr "Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ сообщСния"
348
348
349 #: templates/boards/settings.html:15
349 #: templates/boards/settings.html:15
350 msgid "You are moderator."
350 msgid "You are moderator."
351 msgstr "Π’Ρ‹ ΠΌΠΎΠ΄Π΅Ρ€Π°Ρ‚ΠΎΡ€."
351 msgstr "Π’Ρ‹ ΠΌΠΎΠ΄Π΅Ρ€Π°Ρ‚ΠΎΡ€."
352
352
353 #: templates/boards/settings.html:19
353 #: templates/boards/settings.html:19
354 msgid "Hidden tags:"
354 msgid "Hidden tags:"
355 msgstr "Π‘ΠΊΡ€Ρ‹Ρ‚Ρ‹Π΅ ΠΌΠ΅Ρ‚ΠΊΠΈ:"
355 msgstr "Π‘ΠΊΡ€Ρ‹Ρ‚Ρ‹Π΅ ΠΌΠ΅Ρ‚ΠΊΠΈ:"
356
356
357 #: templates/boards/settings.html:25
357 #: templates/boards/settings.html:25
358 msgid "No hidden tags."
358 msgid "No hidden tags."
359 msgstr "НСт скрытых ΠΌΠ΅Ρ‚ΠΎΠΊ."
359 msgstr "НСт скрытых ΠΌΠ΅Ρ‚ΠΎΠΊ."
360
360
361 #: templates/boards/settings.html:34
361 #: templates/boards/settings.html:34
362 msgid "Save"
362 msgid "Save"
363 msgstr "Π‘ΠΎΡ…Ρ€Π°Π½ΠΈΡ‚ΡŒ"
363 msgstr "Π‘ΠΎΡ…Ρ€Π°Π½ΠΈΡ‚ΡŒ"
364
364
365 #: templates/boards/staticpages/banned.html:6
365 #: templates/boards/staticpages/banned.html:6
366 msgid "Banned"
366 msgid "Banned"
367 msgstr "Π—Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Π½"
367 msgstr "Π—Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Π½"
368
368
369 #: templates/boards/staticpages/banned.html:11
369 #: templates/boards/staticpages/banned.html:11
370 msgid "Your IP address has been banned. Contact the administrator"
370 msgid "Your IP address has been banned. Contact the administrator"
371 msgstr "Π’Π°Ρˆ IP адрСс Π±Ρ‹Π» Π·Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Π½. Π‘Π²ΡΠΆΠΈΡ‚Π΅ΡΡŒ с администратором"
371 msgstr "Π’Π°Ρˆ IP адрСс Π±Ρ‹Π» Π·Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Π½. Π‘Π²ΡΠΆΠΈΡ‚Π΅ΡΡŒ с администратором"
372
372
373 #: templates/boards/staticpages/help.html:6
373 #: templates/boards/staticpages/help.html:6
374 #: templates/boards/staticpages/help.html:10
374 #: templates/boards/staticpages/help.html:10
375 msgid "Syntax"
375 msgid "Syntax"
376 msgstr "Бинтаксис"
376 msgstr "Бинтаксис"
377
377
378 #: templates/boards/staticpages/help.html:11
378 #: templates/boards/staticpages/help.html:11
379 msgid "Italic text"
379 msgid "Italic text"
380 msgstr "ΠšΡƒΡ€ΡΠΈΠ²Π½Ρ‹ΠΉ тСкст"
380 msgstr "ΠšΡƒΡ€ΡΠΈΠ²Π½Ρ‹ΠΉ тСкст"
381
381
382 #: templates/boards/staticpages/help.html:12
382 #: templates/boards/staticpages/help.html:12
383 msgid "Bold text"
383 msgid "Bold text"
384 msgstr "ΠŸΠΎΠ»ΡƒΠΆΠΈΡ€Π½Ρ‹ΠΉ тСкст"
384 msgstr "ΠŸΠΎΠ»ΡƒΠΆΠΈΡ€Π½Ρ‹ΠΉ тСкст"
385
385
386 #: templates/boards/staticpages/help.html:13
386 #: templates/boards/staticpages/help.html:13
387 msgid "Spoiler"
387 msgid "Spoiler"
388 msgstr "Π‘ΠΏΠΎΠΉΠ»Π΅Ρ€"
388 msgstr "Π‘ΠΏΠΎΠΉΠ»Π΅Ρ€"
389
389
390 #: templates/boards/staticpages/help.html:14
390 #: templates/boards/staticpages/help.html:14
391 msgid "Link to a post"
391 msgid "Link to a post"
392 msgstr "Бсылка Π½Π° сообщСниС"
392 msgstr "Бсылка Π½Π° сообщСниС"
393
393
394 #: templates/boards/staticpages/help.html:15
394 #: templates/boards/staticpages/help.html:15
395 msgid "Strikethrough text"
395 msgid "Strikethrough text"
396 msgstr "Π—Π°Ρ‡Π΅Ρ€ΠΊΠ½ΡƒΡ‚Ρ‹ΠΉ тСкст"
396 msgstr "Π—Π°Ρ‡Π΅Ρ€ΠΊΠ½ΡƒΡ‚Ρ‹ΠΉ тСкст"
397
397
398 #: templates/boards/staticpages/help.html:16
398 #: templates/boards/staticpages/help.html:16
399 msgid "Comment"
399 msgid "Comment"
400 msgstr "ΠšΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΉ"
400 msgstr "ΠšΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΉ"
401
401
402 #: templates/boards/staticpages/help.html:17
402 #: templates/boards/staticpages/help.html:17
403 #: templates/boards/staticpages/help.html:18
403 #: templates/boards/staticpages/help.html:18
404 msgid "Quote"
404 msgid "Quote"
405 msgstr "Π¦ΠΈΡ‚Π°Ρ‚Π°"
405 msgstr "Π¦ΠΈΡ‚Π°Ρ‚Π°"
406
406
407 #: templates/boards/staticpages/help.html:21
407 #: templates/boards/staticpages/help.html:21
408 msgid "You can try pasting the text and previewing the result here:"
408 msgid "You can try pasting the text and previewing the result here:"
409 msgstr "Π’Ρ‹ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ ΠΏΠΎΠΏΡ€ΠΎΠ±ΠΎΠ²Π°Ρ‚ΡŒ Π²ΡΡ‚Π°Π²ΠΈΡ‚ΡŒ тСкст ΠΈ ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΈΡ‚ΡŒ Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚ здСсь:"
409 msgstr "Π’Ρ‹ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ ΠΏΠΎΠΏΡ€ΠΎΠ±ΠΎΠ²Π°Ρ‚ΡŒ Π²ΡΡ‚Π°Π²ΠΈΡ‚ΡŒ тСкст ΠΈ ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΈΡ‚ΡŒ Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚ здСсь:"
410
410
411 #: templates/boards/tags.html:17
411 #: templates/boards/tags.html:17
412 msgid "Sections:"
412 msgid "Sections:"
413 msgstr "Π Π°Π·Π΄Π΅Π»Ρ‹:"
413 msgstr "Π Π°Π·Π΄Π΅Π»Ρ‹:"
414
414
415 #: templates/boards/tags.html:30
415 #: templates/boards/tags.html:30
416 msgid "Other tags:"
416 msgid "Other tags:"
417 msgstr "Π”Ρ€ΡƒΠ³ΠΈΠ΅ ΠΌΠ΅Ρ‚ΠΊΠΈ:"
417 msgstr "Π”Ρ€ΡƒΠ³ΠΈΠ΅ ΠΌΠ΅Ρ‚ΠΊΠΈ:"
418
418
419 #: templates/boards/tags.html:43
419 #: templates/boards/tags.html:43
420 msgid "All tags..."
420 msgid "All tags..."
421 msgstr "ВсС ΠΌΠ΅Ρ‚ΠΊΠΈ..."
421 msgstr "ВсС ΠΌΠ΅Ρ‚ΠΊΠΈ..."
422
422
423 #: templates/boards/thread.html:15
423 #: templates/boards/thread.html:15
424 msgid "Normal"
424 msgid "Normal"
425 msgstr "ΠΠΎΡ€ΠΌΠ°Π»ΡŒΠ½Ρ‹ΠΉ"
425 msgstr "ΠΠΎΡ€ΠΌΠ°Π»ΡŒΠ½Ρ‹ΠΉ"
426
426
427 #: templates/boards/thread.html:16
427 #: templates/boards/thread.html:16
428 msgid "Gallery"
428 msgid "Gallery"
429 msgstr "ГалСрСя"
429 msgstr "ГалСрСя"
430
430
431 #: templates/boards/thread.html:17
431 #: templates/boards/thread.html:17
432 msgid "Tree"
432 msgid "Tree"
433 msgstr "Π”Π΅Ρ€Π΅Π²ΠΎ"
433 msgstr "Π”Π΅Ρ€Π΅Π²ΠΎ"
434
434
435 #: templates/boards/thread.html:36
435 #: templates/boards/thread.html:36
436 msgid "message"
436 msgid "message"
437 msgid_plural "messages"
437 msgid_plural "messages"
438 msgstr[0] "сообщСниС"
438 msgstr[0] "сообщСниС"
439 msgstr[1] "сообщСния"
439 msgstr[1] "сообщСния"
440 msgstr[2] "сообщСний"
440 msgstr[2] "сообщСний"
441
441
442 #: templates/boards/thread.html:39
442 #: templates/boards/thread.html:39
443 msgid "image"
443 msgid "image"
444 msgid_plural "images"
444 msgid_plural "images"
445 msgstr[0] "ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅"
445 msgstr[0] "ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅"
446 msgstr[1] "изобраТСния"
446 msgstr[1] "изобраТСния"
447 msgstr[2] "ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ"
447 msgstr[2] "ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ"
448
448
449 #: templates/boards/thread.html:41
449 #: templates/boards/thread.html:41
450 msgid "Last update: "
450 msgid "Last update: "
451 msgstr "ПослСднСС обновлСниС: "
451 msgstr "ПослСднСС обновлСниС: "
452
452
453 #: templates/boards/thread_gallery.html:36
453 #: templates/boards/thread_gallery.html:36
454 msgid "No images."
454 msgid "No images."
455 msgstr "НСт ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ."
455 msgstr "НСт ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ."
456
456
457 #: templates/boards/thread_normal.html:30
457 #: templates/boards/thread_normal.html:30
458 msgid "posts to bumplimit"
458 msgid "posts to bumplimit"
459 msgstr "сообщСний Π΄ΠΎ Π±Π°ΠΌΠΏΠ»ΠΈΠΌΠΈΡ‚Π°"
459 msgstr "сообщСний Π΄ΠΎ Π±Π°ΠΌΠΏΠ»ΠΈΠΌΠΈΡ‚Π°"
460
460
461 #: templates/boards/thread_normal.html:44
461 #: templates/boards/thread_normal.html:44
462 msgid "Reply to thread"
462 msgid "Reply to thread"
463 msgstr "ΠžΡ‚Π²Π΅Ρ‚ΠΈΡ‚ΡŒ Π² Ρ‚Π΅ΠΌΡƒ"
463 msgstr "ΠžΡ‚Π²Π΅Ρ‚ΠΈΡ‚ΡŒ Π² Ρ‚Π΅ΠΌΡƒ"
464
464
465 #: templates/boards/thread_normal.html:44
465 #: templates/boards/thread_normal.html:44
466 msgid "to message "
466 msgid "to message "
467 msgstr "Π½Π° сообщСниС"
467 msgstr "Π½Π° сообщСниС"
468
468
469 #: templates/boards/thread_normal.html:59
469 #: templates/boards/thread_normal.html:59
470 msgid "Close form"
470 msgid "Close form"
471 msgstr "Π—Π°ΠΊΡ€Ρ‹Ρ‚ΡŒ Ρ„ΠΎΡ€ΠΌΡƒ"
471 msgstr "Π—Π°ΠΊΡ€Ρ‹Ρ‚ΡŒ Ρ„ΠΎΡ€ΠΌΡƒ"
472
472
473 #: templates/search/search.html:17
473 #: templates/search/search.html:17
474 msgid "Ok"
474 msgid "Ok"
475 msgstr "Ок"
475 msgstr "Ок"
476
476
477 #: utils.py:102
477 #: utils.py:102
478 #, python-format
478 #, python-format
479 msgid "File must be less than %s bytes"
479 msgid "File must be less than %s bytes"
480 msgstr "Π€Π°ΠΉΠ» Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±Ρ‹Ρ‚ΡŒ ΠΌΠ΅Π½Π΅Π΅ %s Π±Π°ΠΉΡ‚"
480 msgstr "Π€Π°ΠΉΠ» Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±Ρ‹Ρ‚ΡŒ ΠΌΠ΅Π½Π΅Π΅ %s Π±Π°ΠΉΡ‚"
481
482 msgid "favorites"
483 msgstr "ΠΈΠ·Π±Ρ€Π°Π½Π½ΠΎΠ΅"
484
485 msgid "Loading..."
486 msgstr "Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ°..." No newline at end of file
@@ -1,136 +1,145
1 .ui-button {
1 .ui-button {
2 display: none;
2 display: none;
3 }
3 }
4
4
5 .ui-dialog-content {
5 .ui-dialog-content {
6 padding: 0;
6 padding: 0;
7 min-height: 0;
7 min-height: 0;
8 }
8 }
9
9
10 .mark_btn {
10 .mark_btn {
11 cursor: pointer;
11 cursor: pointer;
12 }
12 }
13
13
14 .img-full {
14 .img-full {
15 position: fixed;
15 position: fixed;
16 background-color: #CCC;
16 background-color: #CCC;
17 border: 1px solid #000;
17 border: 1px solid #000;
18 cursor: pointer;
18 cursor: pointer;
19 }
19 }
20
20
21 .strikethrough {
21 .strikethrough {
22 text-decoration: line-through;
22 text-decoration: line-through;
23 }
23 }
24
24
25 .post_preview {
25 .post_preview {
26 z-index: 300;
26 z-index: 300;
27 position:absolute;
27 position:absolute;
28 }
28 }
29
29
30 .gallery_image {
30 .gallery_image {
31 display: inline-block;
31 display: inline-block;
32 }
32 }
33
33
34 @media print {
34 @media print {
35 .post-form-w {
35 .post-form-w {
36 display: none;
36 display: none;
37 }
37 }
38 }
38 }
39
39
40 input[name="image"] {
40 input[name="image"] {
41 display: block;
41 display: block;
42 width: 100px;
42 width: 100px;
43 height: 100px;
43 height: 100px;
44 cursor: pointer;
44 cursor: pointer;
45 position: absolute;
45 position: absolute;
46 opacity: 0;
46 opacity: 0;
47 z-index: 1;
47 z-index: 1;
48 }
48 }
49
49
50 .file_wrap {
50 .file_wrap {
51 width: 100px;
51 width: 100px;
52 height: 100px;
52 height: 100px;
53 border: solid 1px white;
53 border: solid 1px white;
54 display: inline-block;
54 display: inline-block;
55 }
55 }
56
56
57 form > .file_wrap {
57 form > .file_wrap {
58 float: left;
58 float: left;
59 }
59 }
60
60
61 .file-thumb {
61 .file-thumb {
62 width: 100px;
62 width: 100px;
63 height: 100px;
63 height: 100px;
64 background-size: cover;
64 background-size: cover;
65 background-position: center;
65 background-position: center;
66 }
66 }
67
67
68 .compact-form-text {
68 .compact-form-text {
69 margin-left:110px;
69 margin-left:110px;
70 }
70 }
71
71
72 textarea, input {
72 textarea, input {
73 -moz-box-sizing: border-box;
73 -moz-box-sizing: border-box;
74 -webkit-box-sizing: border-box;
74 -webkit-box-sizing: border-box;
75 box-sizing: border-box;
75 box-sizing: border-box;
76 }
76 }
77
77
78 .compact-form-text > textarea {
78 .compact-form-text > textarea {
79 height: 100px;
79 height: 100px;
80 width: 100%;
80 width: 100%;
81 }
81 }
82
82
83 .post-button-form {
83 .post-button-form {
84 display: inline;
84 display: inline;
85 }
85 }
86
86
87 .post-button-form > button, #autoupdate {
87 .post-button-form > button, #autoupdate {
88 border: none;
88 border: none;
89 margin: inherit;
89 margin: inherit;
90 padding: inherit;
90 padding: inherit;
91 background: none;
91 background: none;
92 font-size: inherit;
92 font-size: inherit;
93 }
93 }
94
94
95 #form-close-button {
95 #form-close-button {
96 display: none;
96 display: none;
97 }
97 }
98
98
99 .post-image-full {
99 .post-image-full {
100 width: 100%;
100 width: 100%;
101 height: auto;
101 height: auto;
102 }
102 }
103
103
104 #preview-text {
104 #preview-text {
105 display: none;
105 display: none;
106 }
106 }
107
107
108 .random-images-table {
108 .random-images-table {
109 text-align: center;
109 text-align: center;
110 width: 100%;
110 width: 100%;
111 }
111 }
112
112
113 .random-images-table > div {
113 .random-images-table > div {
114 margin-left: auto;
114 margin-left: auto;
115 margin-right: auto;
115 margin-right: auto;
116 }
116 }
117
117
118 .tag-image, .tag-text-data {
118 .tag-image, .tag-text-data {
119 display: inline-block;
119 display: inline-block;
120 }
120 }
121
121
122 .tag-text-data > h2 {
122 .tag-text-data > h2 {
123 margin: 0;
123 margin: 0;
124 }
124 }
125
125
126 .tag-image {
126 .tag-image {
127 margin-right: 5px;
127 margin-right: 5px;
128 }
128 }
129
129
130 .reply-to-message {
130 .reply-to-message {
131 display: none;
131 display: none;
132 }
132 }
133
133
134 .tripcode {
134 .tripcode {
135 padding: 2px;
135 padding: 2px;
136 }
137
138 #fav-panel {
139 display: none;
140 margin: 1ex;
141 }
142
143 #new-fav-post-count {
144 display: none;
136 } No newline at end of file
145 }
@@ -1,565 +1,569
1 * {
1 * {
2 text-decoration: none;
2 text-decoration: none;
3 font-weight: inherit;
3 font-weight: inherit;
4 }
4 }
5
5
6 b, strong {
6 b, strong {
7 font-weight: bold;
7 font-weight: bold;
8 }
8 }
9
9
10 html {
10 html {
11 background: #555;
11 background: #555;
12 color: #ffffff;
12 color: #ffffff;
13 }
13 }
14
14
15 body {
15 body {
16 margin: 0;
16 margin: 0;
17 }
17 }
18
18
19 #admin_panel {
19 #admin_panel {
20 background: #FF0000;
20 background: #FF0000;
21 color: #00FF00
21 color: #00FF00
22 }
22 }
23
23
24 .input_field_error {
24 .input_field_error {
25 color: #FF0000;
25 color: #FF0000;
26 }
26 }
27
27
28 .title {
28 .title {
29 font-weight: bold;
29 font-weight: bold;
30 color: #ffcc00;
30 color: #ffcc00;
31 }
31 }
32
32
33 .link, a {
33 .link, a {
34 color: #afdcec;
34 color: #afdcec;
35 }
35 }
36
36
37 .block {
37 .block {
38 display: inline-block;
38 display: inline-block;
39 vertical-align: top;
39 vertical-align: top;
40 }
40 }
41
41
42 .tag {
42 .tag {
43 color: #FFD37D;
43 color: #FFD37D;
44 }
44 }
45
45
46 .post_id {
46 .post_id {
47 color: #fff380;
47 color: #fff380;
48 }
48 }
49
49
50 .post, .dead_post, .archive_post, #posts-table {
50 .post, .dead_post, .archive_post, #posts-table {
51 background: #333;
51 background: #333;
52 padding: 10px;
52 padding: 10px;
53 clear: left;
53 clear: left;
54 word-wrap: break-word;
54 word-wrap: break-word;
55 border-top: 1px solid #777;
55 border-top: 1px solid #777;
56 border-bottom: 1px solid #777;
56 border-bottom: 1px solid #777;
57 }
57 }
58
58
59 .post + .post {
59 .post + .post {
60 border-top: none;
60 border-top: none;
61 }
61 }
62
62
63 .dead_post + .dead_post {
63 .dead_post + .dead_post {
64 border-top: none;
64 border-top: none;
65 }
65 }
66
66
67 .archive_post + .archive_post {
67 .archive_post + .archive_post {
68 border-top: none;
68 border-top: none;
69 }
69 }
70
70
71 .metadata {
71 .metadata {
72 padding-top: 5px;
72 padding-top: 5px;
73 margin-top: 10px;
73 margin-top: 10px;
74 border-top: solid 1px #666;
74 border-top: solid 1px #666;
75 color: #ddd;
75 color: #ddd;
76 }
76 }
77
77
78 .navigation_panel, .tag_info {
78 .navigation_panel, .tag_info {
79 background: #222;
79 background: #222;
80 margin-bottom: 5px;
80 margin-bottom: 5px;
81 margin-top: 5px;
81 margin-top: 5px;
82 padding: 10px;
82 padding: 10px;
83 border-bottom: solid 1px #888;
83 border-bottom: solid 1px #888;
84 border-top: solid 1px #888;
84 border-top: solid 1px #888;
85 color: #eee;
85 color: #eee;
86 }
86 }
87
87
88 .navigation_panel .link:first-child {
88 .navigation_panel .link:first-child {
89 border-right: 1px solid #fff;
89 border-right: 1px solid #fff;
90 font-weight: bold;
90 font-weight: bold;
91 margin-right: 1ex;
91 margin-right: 1ex;
92 padding-right: 1ex;
92 padding-right: 1ex;
93 }
93 }
94
94
95 .navigation_panel .right-link {
95 .navigation_panel .right-link {
96 border-left: 1px solid #fff;
96 border-left: 1px solid #fff;
97 border-right: none;
97 border-right: none;
98 float: right;
98 float: right;
99 margin-left: 1ex;
99 margin-left: 1ex;
100 margin-right: 0;
100 margin-right: 0;
101 padding-left: 1ex;
101 padding-left: 1ex;
102 padding-right: 0;
102 padding-right: 0;
103 }
103 }
104
104
105 .navigation_panel .link {
105 .navigation_panel .link {
106 font-weight: bold;
106 font-weight: bold;
107 }
107 }
108
108
109 .navigation_panel::after, .post::after {
109 .navigation_panel::after, .post::after {
110 clear: both;
110 clear: both;
111 content: ".";
111 content: ".";
112 display: block;
112 display: block;
113 height: 0;
113 height: 0;
114 line-height: 0;
114 line-height: 0;
115 visibility: hidden;
115 visibility: hidden;
116 }
116 }
117
117
118 .tag_info {
118 .tag_info {
119 text-align: center;
119 text-align: center;
120 }
120 }
121
121
122 .tag_info > .tag-text-data {
122 .tag_info > .tag-text-data {
123 text-align: left;
123 text-align: left;
124 }
124 }
125
125
126 .header {
126 .header {
127 border-bottom: solid 2px #ccc;
127 border-bottom: solid 2px #ccc;
128 margin-bottom: 5px;
128 margin-bottom: 5px;
129 border-top: none;
129 border-top: none;
130 margin-top: 0;
130 margin-top: 0;
131 }
131 }
132
132
133 .footer {
133 .footer {
134 border-top: solid 2px #ccc;
134 border-top: solid 2px #ccc;
135 margin-top: 5px;
135 margin-top: 5px;
136 border-bottom: none;
136 border-bottom: none;
137 margin-bottom: 0;
137 margin-bottom: 0;
138 }
138 }
139
139
140 p, .br {
140 p, .br {
141 margin-top: .5em;
141 margin-top: .5em;
142 margin-bottom: .5em;
142 margin-bottom: .5em;
143 }
143 }
144
144
145 .post-form-w {
145 .post-form-w {
146 background: #333344;
146 background: #333344;
147 border-top: solid 1px #888;
147 border-top: solid 1px #888;
148 border-bottom: solid 1px #888;
148 border-bottom: solid 1px #888;
149 color: #fff;
149 color: #fff;
150 padding: 10px;
150 padding: 10px;
151 margin-bottom: 5px;
151 margin-bottom: 5px;
152 margin-top: 5px;
152 margin-top: 5px;
153 }
153 }
154
154
155 .form-row {
155 .form-row {
156 width: 100%;
156 width: 100%;
157 display: table-row;
157 display: table-row;
158 }
158 }
159
159
160 .form-label {
160 .form-label {
161 padding: .25em 1ex .25em 0;
161 padding: .25em 1ex .25em 0;
162 vertical-align: top;
162 vertical-align: top;
163 display: table-cell;
163 display: table-cell;
164 }
164 }
165
165
166 .form-input {
166 .form-input {
167 padding: .25em 0;
167 padding: .25em 0;
168 width: 100%;
168 width: 100%;
169 display: table-cell;
169 display: table-cell;
170 }
170 }
171
171
172 .form-errors {
172 .form-errors {
173 font-weight: bolder;
173 font-weight: bolder;
174 vertical-align: middle;
174 vertical-align: middle;
175 display: table-cell;
175 display: table-cell;
176 }
176 }
177
177
178 .post-form input:not([name="image"]):not([type="checkbox"]):not([type="submit"]), .post-form textarea, .post-form select {
178 .post-form input:not([name="image"]):not([type="checkbox"]):not([type="submit"]), .post-form textarea, .post-form select {
179 background: #333;
179 background: #333;
180 color: #fff;
180 color: #fff;
181 border: solid 1px;
181 border: solid 1px;
182 padding: 0;
182 padding: 0;
183 font: medium sans-serif;
183 font: medium sans-serif;
184 width: 100%;
184 width: 100%;
185 }
185 }
186
186
187 .post-form textarea {
187 .post-form textarea {
188 resize: vertical;
188 resize: vertical;
189 }
189 }
190
190
191 .form-submit {
191 .form-submit {
192 display: table;
192 display: table;
193 margin-bottom: 1ex;
193 margin-bottom: 1ex;
194 margin-top: 1ex;
194 margin-top: 1ex;
195 }
195 }
196
196
197 .form-title {
197 .form-title {
198 font-weight: bold;
198 font-weight: bold;
199 font-size: 2ex;
199 font-size: 2ex;
200 margin-bottom: 0.5ex;
200 margin-bottom: 0.5ex;
201 }
201 }
202
202
203 input[type="submit"], button {
203 input[type="submit"], button {
204 background: #222;
204 background: #222;
205 border: solid 2px #fff;
205 border: solid 2px #fff;
206 color: #fff;
206 color: #fff;
207 padding: 0.5ex;
207 padding: 0.5ex;
208 margin-right: 0.5ex;
208 margin-right: 0.5ex;
209 }
209 }
210
210
211 input[type="submit"]:hover {
211 input[type="submit"]:hover {
212 background: #060;
212 background: #060;
213 }
213 }
214
214
215 .form-submit > button:hover {
215 .form-submit > button:hover {
216 background: #006;
216 background: #006;
217 }
217 }
218
218
219 blockquote {
219 blockquote {
220 border-left: solid 2px;
220 border-left: solid 2px;
221 padding-left: 5px;
221 padding-left: 5px;
222 color: #B1FB17;
222 color: #B1FB17;
223 margin: 0;
223 margin: 0;
224 }
224 }
225
225
226 .post > .image {
226 .post > .image {
227 float: left;
227 float: left;
228 margin: 0 1ex .5ex 0;
228 margin: 0 1ex .5ex 0;
229 min-width: 1px;
229 min-width: 1px;
230 text-align: center;
230 text-align: center;
231 display: table-row;
231 display: table-row;
232 }
232 }
233
233
234 .post > .metadata {
234 .post > .metadata {
235 clear: left;
235 clear: left;
236 }
236 }
237
237
238 .get {
238 .get {
239 font-weight: bold;
239 font-weight: bold;
240 color: #d55;
240 color: #d55;
241 }
241 }
242
242
243 * {
243 * {
244 text-decoration: none;
244 text-decoration: none;
245 }
245 }
246
246
247 .dead_post > .post-info {
247 .dead_post > .post-info {
248 font-style: italic;
248 font-style: italic;
249 }
249 }
250
250
251 .archive_post > .post-info {
251 .archive_post > .post-info {
252 text-decoration: line-through;
252 text-decoration: line-through;
253 }
253 }
254
254
255 .mark_btn {
255 .mark_btn {
256 border: 1px solid;
256 border: 1px solid;
257 padding: 2px 2ex;
257 padding: 2px 2ex;
258 display: inline-block;
258 display: inline-block;
259 margin: 0 5px 4px 0;
259 margin: 0 5px 4px 0;
260 }
260 }
261
261
262 .mark_btn:hover {
262 .mark_btn:hover {
263 background: #555;
263 background: #555;
264 }
264 }
265
265
266 .quote {
266 .quote {
267 color: #92cf38;
267 color: #92cf38;
268 font-style: italic;
268 font-style: italic;
269 }
269 }
270
270
271 .multiquote {
271 .multiquote {
272 padding: 3px;
272 padding: 3px;
273 display: inline-block;
273 display: inline-block;
274 background: #222;
274 background: #222;
275 border-style: solid;
275 border-style: solid;
276 border-width: 1px 1px 1px 4px;
276 border-width: 1px 1px 1px 4px;
277 font-size: 0.9em;
277 font-size: 0.9em;
278 }
278 }
279
279
280 .spoiler {
280 .spoiler {
281 background: black;
281 background: black;
282 color: black;
282 color: black;
283 padding: 0 1ex 0 1ex;
283 padding: 0 1ex 0 1ex;
284 }
284 }
285
285
286 .spoiler:hover {
286 .spoiler:hover {
287 color: #ddd;
287 color: #ddd;
288 }
288 }
289
289
290 .comment {
290 .comment {
291 color: #eb2;
291 color: #eb2;
292 }
292 }
293
293
294 a:hover {
294 a:hover {
295 text-decoration: underline;
295 text-decoration: underline;
296 }
296 }
297
297
298 .last-replies {
298 .last-replies {
299 margin-left: 3ex;
299 margin-left: 3ex;
300 margin-right: 3ex;
300 margin-right: 3ex;
301 border-left: solid 1px #777;
301 border-left: solid 1px #777;
302 border-right: solid 1px #777;
302 border-right: solid 1px #777;
303 }
303 }
304
304
305 .last-replies > .post:first-child {
305 .last-replies > .post:first-child {
306 border-top: none;
306 border-top: none;
307 }
307 }
308
308
309 .thread {
309 .thread {
310 margin-bottom: 3ex;
310 margin-bottom: 3ex;
311 margin-top: 1ex;
311 margin-top: 1ex;
312 }
312 }
313
313
314 .post:target {
314 .post:target {
315 border: solid 2px white;
315 border: solid 2px white;
316 }
316 }
317
317
318 pre{
318 pre{
319 white-space:pre-wrap
319 white-space:pre-wrap
320 }
320 }
321
321
322 li {
322 li {
323 list-style-position: inside;
323 list-style-position: inside;
324 }
324 }
325
325
326 .fancybox-skin {
326 .fancybox-skin {
327 position: relative;
327 position: relative;
328 background-color: #fff;
328 background-color: #fff;
329 color: #ddd;
329 color: #ddd;
330 text-shadow: none;
330 text-shadow: none;
331 }
331 }
332
332
333 .fancybox-image {
333 .fancybox-image {
334 border: 1px solid black;
334 border: 1px solid black;
335 }
335 }
336
336
337 .image-mode-tab {
337 .image-mode-tab {
338 background: #444;
338 background: #444;
339 color: #eee;
339 color: #eee;
340 margin-top: 5px;
340 margin-top: 5px;
341 padding: 5px;
341 padding: 5px;
342 border-top: 1px solid #888;
342 border-top: 1px solid #888;
343 border-bottom: 1px solid #888;
343 border-bottom: 1px solid #888;
344 }
344 }
345
345
346 .image-mode-tab > label {
346 .image-mode-tab > label {
347 margin: 0 1ex;
347 margin: 0 1ex;
348 }
348 }
349
349
350 .image-mode-tab > label > input {
350 .image-mode-tab > label > input {
351 margin-right: .5ex;
351 margin-right: .5ex;
352 }
352 }
353
353
354 #posts-table {
354 #posts-table {
355 margin-top: 5px;
355 margin-top: 5px;
356 margin-bottom: 5px;
356 margin-bottom: 5px;
357 }
357 }
358
358
359 .tag_info > h2 {
359 .tag_info > h2 {
360 margin: 0;
360 margin: 0;
361 }
361 }
362
362
363 .post-info {
363 .post-info {
364 color: #ddd;
364 color: #ddd;
365 margin-bottom: 1ex;
365 margin-bottom: 1ex;
366 }
366 }
367
367
368 .moderator_info {
368 .moderator_info {
369 color: #e99d41;
369 color: #e99d41;
370 opacity: 0.4;
370 opacity: 0.4;
371 }
371 }
372
372
373 .moderator_info:hover {
373 .moderator_info:hover {
374 opacity: 1;
374 opacity: 1;
375 }
375 }
376
376
377 .refmap {
377 .refmap {
378 font-size: 0.9em;
378 font-size: 0.9em;
379 color: #ccc;
379 color: #ccc;
380 margin-top: 1em;
380 margin-top: 1em;
381 }
381 }
382
382
383 .fav {
383 .fav {
384 color: yellow;
384 color: yellow;
385 }
385 }
386
386
387 .not_fav {
387 .not_fav {
388 color: #ccc;
388 color: #ccc;
389 }
389 }
390
390
391 .role {
391 .role {
392 text-decoration: underline;
392 text-decoration: underline;
393 }
393 }
394
394
395 .form-email {
395 .form-email {
396 display: none;
396 display: none;
397 }
397 }
398
398
399 .bar-value {
399 .bar-value {
400 background: rgba(50, 55, 164, 0.45);
400 background: rgba(50, 55, 164, 0.45);
401 font-size: 0.9em;
401 font-size: 0.9em;
402 height: 1.5em;
402 height: 1.5em;
403 }
403 }
404
404
405 .bar-bg {
405 .bar-bg {
406 position: relative;
406 position: relative;
407 border-top: solid 1px #888;
407 border-top: solid 1px #888;
408 border-bottom: solid 1px #888;
408 border-bottom: solid 1px #888;
409 margin-top: 5px;
409 margin-top: 5px;
410 overflow: hidden;
410 overflow: hidden;
411 }
411 }
412
412
413 .bar-text {
413 .bar-text {
414 padding: 2px;
414 padding: 2px;
415 position: absolute;
415 position: absolute;
416 left: 0;
416 left: 0;
417 top: 0;
417 top: 0;
418 }
418 }
419
419
420 .page_link {
420 .page_link {
421 background: #444;
421 background: #444;
422 border-top: solid 1px #888;
422 border-top: solid 1px #888;
423 border-bottom: solid 1px #888;
423 border-bottom: solid 1px #888;
424 padding: 5px;
424 padding: 5px;
425 color: #eee;
425 color: #eee;
426 font-size: 2ex;
426 font-size: 2ex;
427 margin-top: .5ex;
427 margin-top: .5ex;
428 margin-bottom: .5ex;
428 margin-bottom: .5ex;
429 }
429 }
430
430
431 .skipped_replies {
431 .skipped_replies {
432 padding: 5px;
432 padding: 5px;
433 margin-left: 3ex;
433 margin-left: 3ex;
434 margin-right: 3ex;
434 margin-right: 3ex;
435 border-left: solid 1px #888;
435 border-left: solid 1px #888;
436 border-right: solid 1px #888;
436 border-right: solid 1px #888;
437 border-bottom: solid 1px #888;
437 border-bottom: solid 1px #888;
438 background: #000;
438 background: #000;
439 }
439 }
440
440
441 .current_page {
441 .current_page {
442 padding: 2px;
442 padding: 2px;
443 background-color: #afdcec;
443 background-color: #afdcec;
444 color: #000;
444 color: #000;
445 }
445 }
446
446
447 .current_mode {
447 .current_mode {
448 font-weight: bold;
448 font-weight: bold;
449 }
449 }
450
450
451 .gallery_image {
451 .gallery_image {
452 border: solid 1px;
452 border: solid 1px;
453 margin: 0.5ex;
453 margin: 0.5ex;
454 text-align: center;
454 text-align: center;
455 padding: 1ex;
455 padding: 1ex;
456 }
456 }
457
457
458 code {
458 code {
459 border: dashed 1px #ccc;
459 border: dashed 1px #ccc;
460 background: #111;
460 background: #111;
461 padding: 2px;
461 padding: 2px;
462 font-size: 1.2em;
462 font-size: 1.2em;
463 display: inline-block;
463 display: inline-block;
464 }
464 }
465
465
466 pre {
466 pre {
467 overflow: auto;
467 overflow: auto;
468 }
468 }
469
469
470 .img-full {
470 .img-full {
471 background: #222;
471 background: #222;
472 border: solid 1px white;
472 border: solid 1px white;
473 }
473 }
474
474
475 .tag_item {
475 .tag_item {
476 display: inline-block;
476 display: inline-block;
477 }
477 }
478
478
479 #id_models li {
479 #id_models li {
480 list-style: none;
480 list-style: none;
481 }
481 }
482
482
483 #id_q {
483 #id_q {
484 margin-left: 1ex;
484 margin-left: 1ex;
485 }
485 }
486
486
487 ul {
487 ul {
488 padding-left: 0px;
488 padding-left: 0px;
489 }
489 }
490
490
491 .quote-header {
491 .quote-header {
492 border-bottom: 2px solid #ddd;
492 border-bottom: 2px solid #ddd;
493 margin-bottom: 1ex;
493 margin-bottom: 1ex;
494 padding-bottom: .5ex;
494 padding-bottom: .5ex;
495 color: #ddd;
495 color: #ddd;
496 font-size: 1.2em;
496 font-size: 1.2em;
497 }
497 }
498
498
499 /* Reflink preview */
499 /* Reflink preview */
500 .post_preview {
500 .post_preview {
501 border-left: 1px solid #777;
501 border-left: 1px solid #777;
502 border-right: 1px solid #777;
502 border-right: 1px solid #777;
503 max-width: 600px;
503 max-width: 600px;
504 }
504 }
505
505
506 /* Code highlighter */
506 /* Code highlighter */
507 .hljs {
507 .hljs {
508 color: #fff;
508 color: #fff;
509 background: #000;
509 background: #000;
510 display: inline-block;
510 display: inline-block;
511 }
511 }
512
512
513 .hljs, .hljs-subst, .hljs-tag .hljs-title, .lisp .hljs-title, .clojure .hljs-built_in, .nginx .hljs-title {
513 .hljs, .hljs-subst, .hljs-tag .hljs-title, .lisp .hljs-title, .clojure .hljs-built_in, .nginx .hljs-title {
514 color: #fff;
514 color: #fff;
515 }
515 }
516
516
517 #up {
517 #up {
518 position: fixed;
518 position: fixed;
519 bottom: 5px;
519 bottom: 5px;
520 right: 5px;
520 right: 5px;
521 border: 1px solid #777;
521 border: 1px solid #777;
522 background: #000;
522 background: #000;
523 padding: 4px;
523 padding: 4px;
524 opacity: 0.3;
524 opacity: 0.3;
525 }
525 }
526
526
527 #up:hover {
527 #up:hover {
528 opacity: 1;
528 opacity: 1;
529 }
529 }
530
530
531 .user-cast {
531 .user-cast {
532 border: solid #ffffff 1px;
532 border: solid #ffffff 1px;
533 padding: .2ex;
533 padding: .2ex;
534 background: #152154;
534 background: #152154;
535 color: #fff;
535 color: #fff;
536 }
536 }
537
537
538 .highlight {
538 .highlight {
539 background: #222;
539 background: #222;
540 }
540 }
541
541
542 .post-button-form > button:hover {
542 .post-button-form > button:hover {
543 text-decoration: underline;
543 text-decoration: underline;
544 }
544 }
545
545
546 .tree_reply > .post {
546 .tree_reply > .post {
547 margin-top: 1ex;
547 margin-top: 1ex;
548 border-left: solid 1px #777;
548 border-left: solid 1px #777;
549 padding-right: 0;
549 padding-right: 0;
550 }
550 }
551
551
552 #preview-text {
552 #preview-text {
553 border: solid 1px white;
553 border: solid 1px white;
554 margin: 1ex 0 1ex 0;
554 margin: 1ex 0 1ex 0;
555 padding: 1ex;
555 padding: 1ex;
556 }
556 }
557
557
558 .image-metadata {
558 .image-metadata {
559 font-style: italic;
559 font-style: italic;
560 font-size: 0.9em;
560 font-size: 0.9em;
561 }
561 }
562
562
563 .tripcode {
563 .tripcode {
564 color: white;
564 color: white;
565 }
565 }
566
567 #fav-panel {
568 border: 1px solid white;
569 }
@@ -1,56 +1,126
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 var FAV_POST_UPDATE_PERIOD = 10000;
27
26 /**
28 /**
27 * An email is a hidden file to prevent spam bots from posting. It has to be
29 * An email is a hidden file to prevent spam bots from posting. It has to be
28 * hidden.
30 * hidden.
29 */
31 */
30 function hideEmailFromForm() {
32 function hideEmailFromForm() {
31 $('.form-email').parent().parent().hide();
33 $('.form-email').parent().parent().hide();
32 }
34 }
33
35
34 /**
36 /**
35 * Highlight code blocks with code highlighter
37 * Highlight code blocks with code highlighter
36 */
38 */
37 function highlightCode(node) {
39 function highlightCode(node) {
38 node.find('pre code').each(function(i, e) {
40 node.find('pre code').each(function(i, e) {
39 hljs.highlightBlock(e);
41 hljs.highlightBlock(e);
40 });
42 });
41 }
43 }
42
44
45 function updateFavPosts() {
46 var includePostBody = $('#fav-panel').is(":visible");
47 var url = '/api/new_posts/';
48 if (includePostBody) {
49 url += '?include_posts'
50 }
51 $.getJSON(url,
52 function(data) {
53 var allNewPostCount = 0;
54
55 if (includePostBody) {
56 var favoriteThreadPanel = $('#fav-panel');
57 favoriteThreadPanel.empty();
58 }
59
60 $.each(data, function (_, dict) {
61 var newPostCount = dict.new_post_count;
62 allNewPostCount += newPostCount;
63
64 if (includePostBody) {
65 var post = $(dict.post);
66
67 var id = post.find('.post_id');
68 var title = post.find('.title');
69
70 var favThreadNode = $('<div class="post"></div>');
71 favThreadNode.append(id);
72 favThreadNode.append(' ');
73 favThreadNode.append(title);
74
75 if (newPostCount > 0) {
76 favThreadNode.append(' (<a href="' + dict.newest_post_link + '">+' + newPostCount + "</a>)");
77 }
78
79 favoriteThreadPanel.append(favThreadNode);
80 }
81 });
82
83 var newPostCountNode = $('#new-fav-post-count');
84 if (allNewPostCount > 0) {
85 newPostCountNode.text('(+' + allNewPostCount + ')');
86 newPostCountNode.show();
87 } else {
88 newPostCountNode.hide();
89 }
90 }
91 );
92 }
93
94 function initFavPanel() {
95 updateFavPosts();
96 setInterval(updateFavPosts, FAV_POST_UPDATE_PERIOD);
97 $('#fav-panel-btn').click(function() {
98 $('#fav-panel').toggle();
99 updateFavPosts();
100
101 return false;
102 });
103
104 $(document).on('keyup.removepic', function(e) {
105 if(e.which === 27) {
106 $('#fav-panel').hide();
107 }
108 });
109 }
110
43 $( document ).ready(function() {
111 $( document ).ready(function() {
44 hideEmailFromForm();
112 hideEmailFromForm();
45
113
46 $("a[href='#top']").click(function() {
114 $("a[href='#top']").click(function() {
47 $("html, body").animate({ scrollTop: 0 }, "slow");
115 $("html, body").animate({ scrollTop: 0 }, "slow");
48 return false;
116 return false;
49 });
117 });
50
118
51 addImgPreview();
119 addImgPreview();
52
120
53 addRefLinkPreview();
121 addRefLinkPreview();
54
122
55 highlightCode($(document));
123 highlightCode($(document));
124
125 initFavPanel();
56 });
126 });
@@ -1,457 +1,457
1 /*
1 /*
2 @licstart The following is the entire license notice for the
2 @licstart The following is the entire license notice for the
3 JavaScript code in this page.
3 JavaScript code in this page.
4
4
5
5
6 Copyright (C) 2013-2014 neko259
6 Copyright (C) 2013-2014 neko259
7
7
8 The JavaScript code in this page is free software: you can
8 The JavaScript code in this page is free software: you can
9 redistribute it and/or modify it under the terms of the GNU
9 redistribute it and/or modify it under the terms of the GNU
10 General Public License (GNU GPL) as published by the Free Software
10 General Public License (GNU GPL) as published by the Free Software
11 Foundation, either version 3 of the License, or (at your option)
11 Foundation, either version 3 of the License, or (at your option)
12 any later version. The code is distributed WITHOUT ANY WARRANTY;
12 any later version. The code is distributed WITHOUT ANY WARRANTY;
13 without even the implied warranty of MERCHANTABILITY or FITNESS
13 without even the implied warranty of MERCHANTABILITY or FITNESS
14 FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
14 FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
15
15
16 As additional permission under GNU GPL version 3 section 7, you
16 As additional permission under GNU GPL version 3 section 7, you
17 may distribute non-source (e.g., minimized or compacted) forms of
17 may distribute non-source (e.g., minimized or compacted) forms of
18 that code without the copy of the GNU GPL normally required by
18 that code without the copy of the GNU GPL normally required by
19 section 4, provided you include this license notice and a URL
19 section 4, provided you include this license notice and a URL
20 through which recipients can access the Corresponding Source.
20 through which recipients can access the Corresponding Source.
21
21
22 @licend The above is the entire license notice
22 @licend The above is the entire license notice
23 for the JavaScript code in this page.
23 for the JavaScript code in this page.
24 */
24 */
25
25
26 var CLASS_POST = '.post'
26 var CLASS_POST = '.post'
27
27
28 var POST_ADDED = 0;
28 var POST_ADDED = 0;
29 var POST_UPDATED = 1;
29 var POST_UPDATED = 1;
30
30
31 var JS_AUTOUPDATE_PERIOD = 20000;
31 var JS_AUTOUPDATE_PERIOD = 20000;
32
32
33 var ALLOWED_FOR_PARTIAL_UPDATE = [
33 var ALLOWED_FOR_PARTIAL_UPDATE = [
34 'refmap',
34 'refmap',
35 'post-info'
35 'post-info'
36 ];
36 ];
37
37
38 var ATTR_CLASS = 'class';
38 var ATTR_CLASS = 'class';
39 var ATTR_UID = 'data-uid';
39 var ATTR_UID = 'data-uid';
40
40
41 var wsUser = '';
41 var wsUser = '';
42
42
43 var unreadPosts = 0;
43 var unreadPosts = 0;
44 var documentOriginalTitle = '';
44 var documentOriginalTitle = '';
45
45
46 // Thread ID does not change, can be stored one time
46 // Thread ID does not change, can be stored one time
47 var threadId = $('div.thread').children(CLASS_POST).first().attr('id');
47 var threadId = $('div.thread').children(CLASS_POST).first().attr('id');
48
48
49 /**
49 /**
50 * Connect to websocket server and subscribe to thread updates. On any update we
50 * Connect to websocket server and subscribe to thread updates. On any update we
51 * request a thread diff.
51 * request a thread diff.
52 *
52 *
53 * @returns {boolean} true if connected, false otherwise
53 * @returns {boolean} true if connected, false otherwise
54 */
54 */
55 function connectWebsocket() {
55 function connectWebsocket() {
56 var metapanel = $('.metapanel')[0];
56 var metapanel = $('.metapanel')[0];
57
57
58 var wsHost = metapanel.getAttribute('data-ws-host');
58 var wsHost = metapanel.getAttribute('data-ws-host');
59 var wsPort = metapanel.getAttribute('data-ws-port');
59 var wsPort = metapanel.getAttribute('data-ws-port');
60
60
61 if (wsHost.length > 0 && wsPort.length > 0) {
61 if (wsHost.length > 0 && wsPort.length > 0) {
62 var centrifuge = new Centrifuge({
62 var centrifuge = new Centrifuge({
63 "url": 'ws://' + wsHost + ':' + wsPort + "/connection/websocket",
63 "url": 'ws://' + wsHost + ':' + wsPort + "/connection/websocket",
64 "project": metapanel.getAttribute('data-ws-project'),
64 "project": metapanel.getAttribute('data-ws-project'),
65 "user": wsUser,
65 "user": wsUser,
66 "timestamp": metapanel.getAttribute('data-ws-token-time'),
66 "timestamp": metapanel.getAttribute('data-ws-token-time'),
67 "token": metapanel.getAttribute('data-ws-token'),
67 "token": metapanel.getAttribute('data-ws-token'),
68 "debug": false
68 "debug": false
69 });
69 });
70
70
71 centrifuge.on('error', function(error_message) {
71 centrifuge.on('error', function(error_message) {
72 console.log("Error connecting to websocket server.");
72 console.log("Error connecting to websocket server.");
73 console.log(error_message);
73 console.log(error_message);
74 console.log("Using javascript update instead.");
74 console.log("Using javascript update instead.");
75
75
76 // If websockets don't work, enable JS update instead
76 // If websockets don't work, enable JS update instead
77 enableJsUpdate()
77 enableJsUpdate()
78 });
78 });
79
79
80 centrifuge.on('connect', function() {
80 centrifuge.on('connect', function() {
81 var channelName = 'thread:' + threadId;
81 var channelName = 'thread:' + threadId;
82 centrifuge.subscribe(channelName, function(message) {
82 centrifuge.subscribe(channelName, function(message) {
83 getThreadDiff();
83 getThreadDiff();
84 });
84 });
85
85
86 // For the case we closed the browser and missed some updates
86 // For the case we closed the browser and missed some updates
87 getThreadDiff();
87 getThreadDiff();
88 $('#autoupdate').hide();
88 $('#autoupdate').hide();
89 });
89 });
90
90
91 centrifuge.connect();
91 centrifuge.connect();
92
92
93 return true;
93 return true;
94 } else {
94 } else {
95 return false;
95 return false;
96 }
96 }
97 }
97 }
98
98
99 /**
99 /**
100 * Get diff of the posts from the current thread timestamp.
100 * Get diff of the posts from the current thread timestamp.
101 * This is required if the browser was closed and some post updates were
101 * This is required if the browser was closed and some post updates were
102 * missed.
102 * missed.
103 */
103 */
104 function getThreadDiff() {
104 function getThreadDiff() {
105 var all_posts = $('.post');
105 var all_posts = $('.post');
106
106
107 var uids = '';
107 var uids = '';
108 var posts = all_posts;
108 var posts = all_posts;
109 for (var i = 0; i < posts.length; i++) {
109 for (var i = 0; i < posts.length; i++) {
110 uids += posts[i].getAttribute('data-uid') + ' ';
110 uids += posts[i].getAttribute('data-uid') + ' ';
111 }
111 }
112
112
113 var data = {
113 var data = {
114 uids: uids,
114 uids: uids,
115 thread: threadId
115 thread: threadId
116 }
116 };
117
117
118 var diffUrl = '/api/diff_thread/';
118 var diffUrl = '/api/diff_thread/';
119
119
120 $.post(diffUrl,
120 $.post(diffUrl,
121 data,
121 data,
122 function(data) {
122 function(data) {
123 var updatedPosts = data.updated;
123 var updatedPosts = data.updated;
124 var addedPostCount = 0;
124 var addedPostCount = 0;
125
125
126 for (var i = 0; i < updatedPosts.length; i++) {
126 for (var i = 0; i < updatedPosts.length; i++) {
127 var postText = updatedPosts[i];
127 var postText = updatedPosts[i];
128 var post = $(postText);
128 var post = $(postText);
129
129
130 if (updatePost(post) == POST_ADDED) {
130 if (updatePost(post) == POST_ADDED) {
131 addedPostCount++;
131 addedPostCount++;
132 }
132 }
133 }
133 }
134
134
135 var hasMetaUpdates = updatedPosts.length > 0;
135 var hasMetaUpdates = updatedPosts.length > 0;
136 if (hasMetaUpdates) {
136 if (hasMetaUpdates) {
137 updateMetadataPanel();
137 updateMetadataPanel();
138 }
138 }
139
139
140 if (addedPostCount > 0) {
140 if (addedPostCount > 0) {
141 updateBumplimitProgress(addedPostCount);
141 updateBumplimitProgress(addedPostCount);
142 }
142 }
143
143
144 if (updatedPosts.length > 0) {
144 if (updatedPosts.length > 0) {
145 showNewPostsTitle(addedPostCount);
145 showNewPostsTitle(addedPostCount);
146 }
146 }
147
147
148 // TODO Process removed posts if any
148 // TODO Process removed posts if any
149 $('.metapanel').attr('data-last-update', data.last_update);
149 $('.metapanel').attr('data-last-update', data.last_update);
150 },
150 },
151 'json'
151 'json'
152 )
152 )
153 }
153 }
154
154
155 /**
155 /**
156 * Add or update the post on html page.
156 * Add or update the post on html page.
157 */
157 */
158 function updatePost(postHtml) {
158 function updatePost(postHtml) {
159 // This needs to be set on start because the page is scrolled after posts
159 // This needs to be set on start because the page is scrolled after posts
160 // are added or updated
160 // are added or updated
161 var bottom = isPageBottom();
161 var bottom = isPageBottom();
162
162
163 var post = $(postHtml);
163 var post = $(postHtml);
164
164
165 var threadBlock = $('div.thread');
165 var threadBlock = $('div.thread');
166
166
167 var postId = post.attr('id');
167 var postId = post.attr('id');
168
168
169 // If the post already exists, replace it. Otherwise add as a new one.
169 // If the post already exists, replace it. Otherwise add as a new one.
170 var existingPosts = threadBlock.children('.post[id=' + postId + ']');
170 var existingPosts = threadBlock.children('.post[id=' + postId + ']');
171
171
172 var type;
172 var type;
173
173
174 if (existingPosts.size() > 0) {
174 if (existingPosts.size() > 0) {
175 replacePartial(existingPosts.first(), post, false);
175 replacePartial(existingPosts.first(), post, false);
176 post = existingPosts.first();
176 post = existingPosts.first();
177
177
178 type = POST_UPDATED;
178 type = POST_UPDATED;
179 } else {
179 } else {
180 post.appendTo(threadBlock);
180 post.appendTo(threadBlock);
181
181
182 if (bottom) {
182 if (bottom) {
183 scrollToBottom();
183 scrollToBottom();
184 }
184 }
185
185
186 type = POST_ADDED;
186 type = POST_ADDED;
187 }
187 }
188
188
189 processNewPost(post);
189 processNewPost(post);
190
190
191 return type;
191 return type;
192 }
192 }
193
193
194 /**
194 /**
195 * Initiate a blinking animation on a node to show it was updated.
195 * Initiate a blinking animation on a node to show it was updated.
196 */
196 */
197 function blink(node) {
197 function blink(node) {
198 var blinkCount = 2;
198 var blinkCount = 2;
199
199
200 var nodeToAnimate = node;
200 var nodeToAnimate = node;
201 for (var i = 0; i < blinkCount; i++) {
201 for (var i = 0; i < blinkCount; i++) {
202 nodeToAnimate = nodeToAnimate.fadeTo('fast', 0.5).fadeTo('fast', 1.0);
202 nodeToAnimate = nodeToAnimate.fadeTo('fast', 0.5).fadeTo('fast', 1.0);
203 }
203 }
204 }
204 }
205
205
206 function isPageBottom() {
206 function isPageBottom() {
207 var scroll = $(window).scrollTop() / ($(document).height()
207 var scroll = $(window).scrollTop() / ($(document).height()
208 - $(window).height());
208 - $(window).height());
209
209
210 return scroll == 1
210 return scroll == 1
211 }
211 }
212
212
213 function enableJsUpdate() {
213 function enableJsUpdate() {
214 setInterval(getThreadDiff, JS_AUTOUPDATE_PERIOD);
214 setInterval(getThreadDiff, JS_AUTOUPDATE_PERIOD);
215 return true;
215 return true;
216 }
216 }
217
217
218 function initAutoupdate() {
218 function initAutoupdate() {
219 if (location.protocol === 'https:') {
219 if (location.protocol === 'https:') {
220 return enableJsUpdate();
220 return enableJsUpdate();
221 } else {
221 } else {
222 if (connectWebsocket()) {
222 if (connectWebsocket()) {
223 return true;
223 return true;
224 } else {
224 } else {
225 return enableJsUpdate();
225 return enableJsUpdate();
226 }
226 }
227 }
227 }
228 }
228 }
229
229
230 function getReplyCount() {
230 function getReplyCount() {
231 return $('.thread').children(CLASS_POST).length
231 return $('.thread').children(CLASS_POST).length
232 }
232 }
233
233
234 function getImageCount() {
234 function getImageCount() {
235 return $('.thread').find('img').length
235 return $('.thread').find('img').length
236 }
236 }
237
237
238 /**
238 /**
239 * Update post count, images count and last update time in the metadata
239 * Update post count, images count and last update time in the metadata
240 * panel.
240 * panel.
241 */
241 */
242 function updateMetadataPanel() {
242 function updateMetadataPanel() {
243 var replyCountField = $('#reply-count');
243 var replyCountField = $('#reply-count');
244 var imageCountField = $('#image-count');
244 var imageCountField = $('#image-count');
245
245
246 var replyCount = getReplyCount();
246 var replyCount = getReplyCount();
247 replyCountField.text(replyCount);
247 replyCountField.text(replyCount);
248 var imageCount = getImageCount();
248 var imageCount = getImageCount();
249 imageCountField.text(imageCount);
249 imageCountField.text(imageCount);
250
250
251 var lastUpdate = $('.post:last').children('.post-info').first()
251 var lastUpdate = $('.post:last').children('.post-info').first()
252 .children('.pub_time').first().html();
252 .children('.pub_time').first().html();
253 if (lastUpdate !== '') {
253 if (lastUpdate !== '') {
254 var lastUpdateField = $('#last-update');
254 var lastUpdateField = $('#last-update');
255 lastUpdateField.html(lastUpdate);
255 lastUpdateField.html(lastUpdate);
256 blink(lastUpdateField);
256 blink(lastUpdateField);
257 }
257 }
258
258
259 blink(replyCountField);
259 blink(replyCountField);
260 blink(imageCountField);
260 blink(imageCountField);
261
261
262 $('#message-count-text').text(ngettext('message', 'messages', replyCount));
262 $('#message-count-text').text(ngettext('message', 'messages', replyCount));
263 $('#image-count-text').text(ngettext('image', 'images', imageCount));
263 $('#image-count-text').text(ngettext('image', 'images', imageCount));
264 }
264 }
265
265
266 /**
266 /**
267 * Update bumplimit progress bar
267 * Update bumplimit progress bar
268 */
268 */
269 function updateBumplimitProgress(postDelta) {
269 function updateBumplimitProgress(postDelta) {
270 var progressBar = $('#bumplimit_progress');
270 var progressBar = $('#bumplimit_progress');
271 if (progressBar) {
271 if (progressBar) {
272 var postsToLimitElement = $('#left_to_limit');
272 var postsToLimitElement = $('#left_to_limit');
273
273
274 var oldPostsToLimit = parseInt(postsToLimitElement.text());
274 var oldPostsToLimit = parseInt(postsToLimitElement.text());
275 var postCount = getReplyCount();
275 var postCount = getReplyCount();
276 var bumplimit = postCount - postDelta + oldPostsToLimit;
276 var bumplimit = postCount - postDelta + oldPostsToLimit;
277
277
278 var newPostsToLimit = bumplimit - postCount;
278 var newPostsToLimit = bumplimit - postCount;
279 if (newPostsToLimit <= 0) {
279 if (newPostsToLimit <= 0) {
280 $('.bar-bg').remove();
280 $('.bar-bg').remove();
281 } else {
281 } else {
282 postsToLimitElement.text(newPostsToLimit);
282 postsToLimitElement.text(newPostsToLimit);
283 progressBar.width((100 - postCount / bumplimit * 100.0) + '%');
283 progressBar.width((100 - postCount / bumplimit * 100.0) + '%');
284 }
284 }
285 }
285 }
286 }
286 }
287
287
288 /**
288 /**
289 * Show 'new posts' text in the title if the document is not visible to a user
289 * Show 'new posts' text in the title if the document is not visible to a user
290 */
290 */
291 function showNewPostsTitle(newPostCount) {
291 function showNewPostsTitle(newPostCount) {
292 if (document.hidden) {
292 if (document.hidden) {
293 if (documentOriginalTitle === '') {
293 if (documentOriginalTitle === '') {
294 documentOriginalTitle = document.title;
294 documentOriginalTitle = document.title;
295 }
295 }
296 unreadPosts = unreadPosts + newPostCount;
296 unreadPosts = unreadPosts + newPostCount;
297
297
298 var newTitle = '* ';
298 var newTitle = '* ';
299 if (unreadPosts > 0) {
299 if (unreadPosts > 0) {
300 newTitle += '[' + unreadPosts + '] ';
300 newTitle += '[' + unreadPosts + '] ';
301 }
301 }
302 newTitle += documentOriginalTitle;
302 newTitle += documentOriginalTitle;
303
303
304 document.title = newTitle;
304 document.title = newTitle;
305
305
306 document.addEventListener('visibilitychange', function() {
306 document.addEventListener('visibilitychange', function() {
307 if (documentOriginalTitle !== '') {
307 if (documentOriginalTitle !== '') {
308 document.title = documentOriginalTitle;
308 document.title = documentOriginalTitle;
309 documentOriginalTitle = '';
309 documentOriginalTitle = '';
310 unreadPosts = 0;
310 unreadPosts = 0;
311 }
311 }
312
312
313 document.removeEventListener('visibilitychange', null);
313 document.removeEventListener('visibilitychange', null);
314 });
314 });
315 }
315 }
316 }
316 }
317
317
318 /**
318 /**
319 * Clear all entered values in the form fields
319 * Clear all entered values in the form fields
320 */
320 */
321 function resetForm(form) {
321 function resetForm(form) {
322 form.find('input:text, input:password, input:file, select, textarea').val('');
322 form.find('input:text, input:password, input:file, select, textarea').val('');
323 form.find('input:radio, input:checkbox')
323 form.find('input:radio, input:checkbox')
324 .removeAttr('checked').removeAttr('selected');
324 .removeAttr('checked').removeAttr('selected');
325 $('.file_wrap').find('.file-thumb').remove();
325 $('.file_wrap').find('.file-thumb').remove();
326 $('#preview-text').hide();
326 $('#preview-text').hide();
327 }
327 }
328
328
329 /**
329 /**
330 * When the form is posted, this method will be run as a callback
330 * When the form is posted, this method will be run as a callback
331 */
331 */
332 function updateOnPost(response, statusText, xhr, form) {
332 function updateOnPost(response, statusText, xhr, form) {
333 var json = $.parseJSON(response);
333 var json = $.parseJSON(response);
334 var status = json.status;
334 var status = json.status;
335
335
336 showAsErrors(form, '');
336 showAsErrors(form, '');
337
337
338 if (status === 'ok') {
338 if (status === 'ok') {
339 resetFormPosition();
339 resetFormPosition();
340 resetForm(form);
340 resetForm(form);
341 getThreadDiff();
341 getThreadDiff();
342 scrollToBottom();
342 scrollToBottom();
343 } else {
343 } else {
344 var errors = json.errors;
344 var errors = json.errors;
345 for (var i = 0; i < errors.length; i++) {
345 for (var i = 0; i < errors.length; i++) {
346 var fieldErrors = errors[i];
346 var fieldErrors = errors[i];
347
347
348 var error = fieldErrors.errors;
348 var error = fieldErrors.errors;
349
349
350 showAsErrors(form, error);
350 showAsErrors(form, error);
351 }
351 }
352 }
352 }
353 }
353 }
354
354
355 /**
355 /**
356 * Show text in the errors row of the form.
356 * Show text in the errors row of the form.
357 * @param form
357 * @param form
358 * @param text
358 * @param text
359 */
359 */
360 function showAsErrors(form, text) {
360 function showAsErrors(form, text) {
361 form.children('.form-errors').remove();
361 form.children('.form-errors').remove();
362
362
363 if (text.length > 0) {
363 if (text.length > 0) {
364 var errorList = $('<div class="form-errors">' + text + '<div>');
364 var errorList = $('<div class="form-errors">' + text + '<div>');
365 errorList.appendTo(form);
365 errorList.appendTo(form);
366 }
366 }
367 }
367 }
368
368
369 /**
369 /**
370 * Run js methods that are usually run on the document, on the new post
370 * Run js methods that are usually run on the document, on the new post
371 */
371 */
372 function processNewPost(post) {
372 function processNewPost(post) {
373 addRefLinkPreview(post[0]);
373 addRefLinkPreview(post[0]);
374 highlightCode(post);
374 highlightCode(post);
375 blink(post);
375 blink(post);
376 }
376 }
377
377
378 function replacePartial(oldNode, newNode, recursive) {
378 function replacePartial(oldNode, newNode, recursive) {
379 if (!equalNodes(oldNode, newNode)) {
379 if (!equalNodes(oldNode, newNode)) {
380 // Update parent node attributes
380 // Update parent node attributes
381 updateNodeAttr(oldNode, newNode, ATTR_CLASS);
381 updateNodeAttr(oldNode, newNode, ATTR_CLASS);
382 updateNodeAttr(oldNode, newNode, ATTR_UID);
382 updateNodeAttr(oldNode, newNode, ATTR_UID);
383
383
384 // Replace children
384 // Replace children
385 var children = oldNode.children();
385 var children = oldNode.children();
386 if (children.length == 0) {
386 if (children.length == 0) {
387 oldNode.replaceWith(newNode);
387 oldNode.replaceWith(newNode);
388 } else {
388 } else {
389 var newChildren = newNode.children();
389 var newChildren = newNode.children();
390 newChildren.each(function(i) {
390 newChildren.each(function(i) {
391 var newChild = newChildren.eq(i);
391 var newChild = newChildren.eq(i);
392 var newChildClass = newChild.attr(ATTR_CLASS);
392 var newChildClass = newChild.attr(ATTR_CLASS);
393
393
394 // Update only certain allowed blocks (e.g. not images)
394 // Update only certain allowed blocks (e.g. not images)
395 if (ALLOWED_FOR_PARTIAL_UPDATE.indexOf(newChildClass) > -1) {
395 if (ALLOWED_FOR_PARTIAL_UPDATE.indexOf(newChildClass) > -1) {
396 var oldChild = oldNode.children('.' + newChildClass);
396 var oldChild = oldNode.children('.' + newChildClass);
397
397
398 if (oldChild.length == 0) {
398 if (oldChild.length == 0) {
399 oldNode.append(newChild);
399 oldNode.append(newChild);
400 } else {
400 } else {
401 if (!equalNodes(oldChild, newChild)) {
401 if (!equalNodes(oldChild, newChild)) {
402 if (recursive) {
402 if (recursive) {
403 replacePartial(oldChild, newChild, false);
403 replacePartial(oldChild, newChild, false);
404 } else {
404 } else {
405 oldChild.replaceWith(newChild);
405 oldChild.replaceWith(newChild);
406 }
406 }
407 }
407 }
408 }
408 }
409 }
409 }
410 });
410 });
411 }
411 }
412 }
412 }
413 }
413 }
414
414
415 /**
415 /**
416 * Compare nodes by content
416 * Compare nodes by content
417 */
417 */
418 function equalNodes(node1, node2) {
418 function equalNodes(node1, node2) {
419 return node1[0].outerHTML == node2[0].outerHTML;
419 return node1[0].outerHTML == node2[0].outerHTML;
420 }
420 }
421
421
422 /**
422 /**
423 * Update attribute of a node if it has changed
423 * Update attribute of a node if it has changed
424 */
424 */
425 function updateNodeAttr(oldNode, newNode, attrName) {
425 function updateNodeAttr(oldNode, newNode, attrName) {
426 var oldAttr = oldNode.attr(attrName);
426 var oldAttr = oldNode.attr(attrName);
427 var newAttr = newNode.attr(attrName);
427 var newAttr = newNode.attr(attrName);
428 if (oldAttr != newAttr) {
428 if (oldAttr != newAttr) {
429 oldNode.attr(attrName, newAttr);
429 oldNode.attr(attrName, newAttr);
430 }
430 }
431 }
431 }
432
432
433 $(document).ready(function(){
433 $(document).ready(function(){
434 if (initAutoupdate()) {
434 if (initAutoupdate()) {
435 // Post form data over AJAX
435 // Post form data over AJAX
436 var threadId = $('div.thread').children('.post').first().attr('id');
436 var threadId = $('div.thread').children('.post').first().attr('id');
437
437
438 var form = $('#form');
438 var form = $('#form');
439
439
440 if (form.length > 0) {
440 if (form.length > 0) {
441 var options = {
441 var options = {
442 beforeSubmit: function(arr, $form, options) {
442 beforeSubmit: function(arr, $form, options) {
443 showAsErrors($('#form'), gettext('Sending message...'));
443 showAsErrors($('#form'), gettext('Sending message...'));
444 },
444 },
445 success: updateOnPost,
445 success: updateOnPost,
446 error: function() {
446 error: function() {
447 showAsErrors($('#form'), gettext('Server error!'));
447 showAsErrors($('#form'), gettext('Server error!'));
448 },
448 },
449 url: '/api/add_post/' + threadId + '/'
449 url: '/api/add_post/' + threadId + '/'
450 };
450 };
451
451
452 form.ajaxForm(options);
452 form.ajaxForm(options);
453
453
454 resetForm(form);
454 resetForm(form);
455 }
455 }
456 }
456 }
457 });
457 });
@@ -1,86 +1,79
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 'css/3party/jquery-ui.min.css' %}" media="all"/>
11 <link rel="stylesheet" type="text/css" href="{% static 'css/3party/jquery-ui.min.css' %}" media="all"/>
12 <link rel="stylesheet" type="text/css" href="{% static theme_css %}" media="all"/>
12 <link rel="stylesheet" type="text/css" href="{% static theme_css %}" media="all"/>
13
13
14 <link rel="alternate" type="application/rss+xml" href="rss/" title="{% trans 'Feed' %}"/>
14 <link rel="alternate" type="application/rss+xml" href="rss/" title="{% trans 'Feed' %}"/>
15
15
16 <link rel="icon" type="image/png"
16 <link rel="icon" type="image/png"
17 href="{% static 'favicon.png' %}">
17 href="{% static 'favicon.png' %}">
18
18
19 <meta name="viewport" content="width=device-width, initial-scale=1"/>
19 <meta name="viewport" content="width=device-width, initial-scale=1"/>
20 <meta charset="utf-8"/>
20 <meta charset="utf-8"/>
21
21
22 {% block head %}{% endblock %}
22 {% block head %}{% endblock %}
23 </head>
23 </head>
24 <body data-image-viewer="{{ image_viewer }}">
24 <body data-image-viewer="{{ image_viewer }}">
25 <script src="{% static 'js/jquery-2.0.1.min.js' %}"></script>
25 <script src="{% static 'js/jquery-2.0.1.min.js' %}"></script>
26 <script src="{% static 'js/3party/jquery-ui.min.js' %}"></script>
26 <script src="{% static 'js/3party/jquery-ui.min.js' %}"></script>
27 <script src="{% static 'js/jquery.mousewheel.js' %}"></script>
27 <script src="{% static 'js/jquery.mousewheel.js' %}"></script>
28 <script src="{% url 'js_info_dict' %}"></script>
28 <script src="{% url 'js_info_dict' %}"></script>
29
29
30 <div class="navigation_panel header">
30 <div class="navigation_panel header">
31 <a class="link" href="{% url 'index' %}">{% trans "All threads" %}</a>
31 <a class="link" href="{% url 'index' %}">{% trans "All threads" %}</a>
32 {% if tags_str %}
32 {% if tags_str %}
33 {% autoescape off %}
33 {% autoescape off %}
34 {{ tags_str }},
34 {{ tags_str }},
35 {% endautoescape %}
35 {% endautoescape %}
36 {% else %}
36 {% else %}
37 {% trans 'Add tags' %} β†’
37 {% trans 'Add tags' %} β†’
38 {% endif %}
38 {% endif %}
39 <a href="{% url 'tags' 'required'%}" title="{% trans 'Tag management' %}">{% trans "tags" %}</a>,
39 <a href="{% url 'tags' 'required'%}" title="{% trans 'Tag management' %}">{% trans "tags" %}</a>,
40 <a href="{% url 'search' %}" title="{% trans 'Search' %}">{% trans 'search' %}</a>,
40 <a href="{% url 'search' %}" title="{% trans 'Search' %}">{% trans 'search' %}</a>,
41 <a href="{% url 'feed' %}" title="{% trans 'Feed' %}">{% trans 'feed' %}</a>,
41 <a href="{% url 'feed' %}" title="{% trans 'Feed' %}">{% trans 'feed' %}</a>,
42 <a href="{% url 'random' %}" title="{% trans 'Random images' %}">{% trans 'random' %}</a>
42 <a href="{% url 'random' %}" title="{% trans 'Random images' %}">{% trans 'random' %}</a>{% if has_fav_threads %},
43
44 <a href="#" id="fav-panel-btn">{% trans 'favorites' %} <span id="new-fav-post-count"></span></a>
45 {% endif %}
43
46
44 {% if username %}
47 {% if username %}
45 <a class="right-link link" href="{% url 'notifications' username %}" title="{% trans 'Notifications' %}">
48 <a class="right-link link" href="{% url 'notifications' username %}" title="{% trans 'Notifications' %}">
46 {% trans 'Notifications' %}
49 {% trans 'Notifications' %}
47 {% ifnotequal new_notifications_count 0 %}
50 {% ifnotequal new_notifications_count 0 %}
48 (<b>{{ new_notifications_count }}</b>)
51 (<b>{{ new_notifications_count }}</b>)
49 {% endifnotequal %}
52 {% endifnotequal %}
50 </a>
53 </a>
51 {% endif %}
54 {% endif %}
52
55
53 <a class="right-link link" href="{% url 'settings' %}">{% trans 'Settings' %}</a>
56 <a class="right-link link" href="{% url 'settings' %}">{% trans 'Settings' %}</a>
54 </div>
57 </div>
55
58
56 {% if fav_threads %}
59 <div id="fav-panel"><div class="post">{% trans "Loading..." %}</div></div>
57 <div class="image-mode-tab">
58 β˜…
59 {% for thread in fav_threads %}
60 {% comment %}
61 If there are new posts in the thread, show their count.
62 {% endcomment %}
63 {{ thread.post.get_link_view|safe }}{% if thread.count %} (<a href="{{ thread.new_post }}">+{{ thread.count }}</a>){% endif %}{% if not forloop.last %}, {% endif %}
64 {% endfor %}
65 </div>
66 {% endif %}
67
60
68 {% block content %}{% endblock %}
61 {% block content %}{% endblock %}
69
62
70 <script src="{% static 'js/3party/highlight.min.js' %}"></script>
63 <script src="{% static 'js/3party/highlight.min.js' %}"></script>
71 <script src="{% static 'js/popup.js' %}"></script>
64 <script src="{% static 'js/popup.js' %}"></script>
72 <script src="{% static 'js/image.js' %}"></script>
65 <script src="{% static 'js/image.js' %}"></script>
73 <script src="{% static 'js/refpopup.js' %}"></script>
66 <script src="{% static 'js/refpopup.js' %}"></script>
74 <script src="{% static 'js/main.js' %}"></script>
67 <script src="{% static 'js/main.js' %}"></script>
75
68
76 <div class="navigation_panel footer">
69 <div class="navigation_panel footer">
77 {% block metapanel %}{% endblock %}
70 {% block metapanel %}{% endblock %}
78 [<a href="{% url 'admin:index' %}">{% trans 'Admin' %}</a>]
71 [<a href="{% url 'admin:index' %}">{% trans 'Admin' %}</a>]
79 {% with ppd=posts_per_day|floatformat:2 %}
72 {% with ppd=posts_per_day|floatformat:2 %}
80 {% blocktrans %}Speed: {{ ppd }} posts per day{% endblocktrans %}
73 {% blocktrans %}Speed: {{ ppd }} posts per day{% endblocktrans %}
81 {% endwith %}
74 {% endwith %}
82 <a class="link" href="#top" id="up">{% trans 'Up' %}</a>
75 <a class="link" href="#top" id="up">{% trans 'Up' %}</a>
83 </div>
76 </div>
84
77
85 </body>
78 </body>
86 </html>
79 </html>
@@ -1,82 +1,83
1 from django.conf.urls import patterns, url
1 from django.conf.urls import patterns, url
2 from django.views.i18n import javascript_catalog
2 from django.views.i18n import javascript_catalog
3
3
4 from boards import views
4 from boards import views
5 from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed
5 from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed
6 from boards.views import api, tag_threads, all_threads, \
6 from boards.views import api, tag_threads, all_threads, \
7 settings, all_tags, feed
7 settings, all_tags, feed
8 from boards.views.authors import AuthorsView
8 from boards.views.authors import AuthorsView
9 from boards.views.notifications import NotificationView
9 from boards.views.notifications import NotificationView
10 from boards.views.search import BoardSearchView
10 from boards.views.search import BoardSearchView
11 from boards.views.static import StaticPageView
11 from boards.views.static import StaticPageView
12 from boards.views.preview import PostPreviewView
12 from boards.views.preview import PostPreviewView
13 from boards.views.random import RandomImageView
13 from boards.views.random import RandomImageView
14
14
15
15
16 js_info_dict = {
16 js_info_dict = {
17 'packages': ('boards',),
17 'packages': ('boards',),
18 }
18 }
19
19
20 urlpatterns = patterns('',
20 urlpatterns = patterns('',
21 # /boards/
21 # /boards/
22 url(r'^$', all_threads.AllThreadsView.as_view(), name='index'),
22 url(r'^$', all_threads.AllThreadsView.as_view(), name='index'),
23
23
24 # /boards/tag/tag_name/
24 # /boards/tag/tag_name/
25 url(r'^tag/(?P<tag_name>\w+)/$', tag_threads.TagView.as_view(),
25 url(r'^tag/(?P<tag_name>\w+)/$', tag_threads.TagView.as_view(),
26 name='tag'),
26 name='tag'),
27
27
28 # /boards/thread/
28 # /boards/thread/
29 url(r'^thread/(?P<post_id>\d+)/$', views.thread.NormalThreadView.as_view(),
29 url(r'^thread/(?P<post_id>\d+)/$', views.thread.NormalThreadView.as_view(),
30 name='thread'),
30 name='thread'),
31 url(r'^thread/(?P<post_id>\d+)/mode/gallery/$', views.thread.GalleryThreadView.as_view(),
31 url(r'^thread/(?P<post_id>\d+)/mode/gallery/$', views.thread.GalleryThreadView.as_view(),
32 name='thread_gallery'),
32 name='thread_gallery'),
33 url(r'^thread/(?P<post_id>\d+)/mode/tree/$', views.thread.TreeThreadView.as_view(),
33 url(r'^thread/(?P<post_id>\d+)/mode/tree/$', views.thread.TreeThreadView.as_view(),
34 name='thread_tree'),
34 name='thread_tree'),
35 # /feed/
35 # /feed/
36 url(r'^feed/$', views.feed.FeedView.as_view(), name='feed'),
36 url(r'^feed/$', views.feed.FeedView.as_view(), name='feed'),
37
37
38 url(r'^settings/$', settings.SettingsView.as_view(), name='settings'),
38 url(r'^settings/$', settings.SettingsView.as_view(), name='settings'),
39 url(r'^tags/(?P<query>\w+)?/?$', all_tags.AllTagsView.as_view(), name='tags'),
39 url(r'^tags/(?P<query>\w+)?/?$', all_tags.AllTagsView.as_view(), name='tags'),
40 url(r'^authors/$', AuthorsView.as_view(), name='authors'),
40 url(r'^authors/$', AuthorsView.as_view(), name='authors'),
41
41
42 url(r'^banned/$', views.banned.BannedView.as_view(), name='banned'),
42 url(r'^banned/$', views.banned.BannedView.as_view(), name='banned'),
43 url(r'^staticpage/(?P<name>\w+)/$', StaticPageView.as_view(),
43 url(r'^staticpage/(?P<name>\w+)/$', StaticPageView.as_view(),
44 name='staticpage'),
44 name='staticpage'),
45
45
46 url(r'^random/$', RandomImageView.as_view(), name='random'),
46 url(r'^random/$', RandomImageView.as_view(), name='random'),
47
47
48 # RSS feeds
48 # RSS feeds
49 url(r'^rss/$', AllThreadsFeed()),
49 url(r'^rss/$', AllThreadsFeed()),
50 url(r'^page/(?P<page>\d+)/rss/$', AllThreadsFeed()),
50 url(r'^page/(?P<page>\d+)/rss/$', AllThreadsFeed()),
51 url(r'^tag/(?P<tag_name>\w+)/rss/$', TagThreadsFeed()),
51 url(r'^tag/(?P<tag_name>\w+)/rss/$', TagThreadsFeed()),
52 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/rss/$', TagThreadsFeed()),
52 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/rss/$', TagThreadsFeed()),
53 url(r'^thread/(?P<post_id>\d+)/rss/$', ThreadPostsFeed()),
53 url(r'^thread/(?P<post_id>\d+)/rss/$', ThreadPostsFeed()),
54
54
55 # i18n
55 # i18n
56 url(r'^jsi18n/$', javascript_catalog, js_info_dict,
56 url(r'^jsi18n/$', javascript_catalog, js_info_dict,
57 name='js_info_dict'),
57 name='js_info_dict'),
58
58
59 # API
59 # API
60 url(r'^api/post/(?P<post_id>\d+)/$', api.get_post, name="get_post"),
60 url(r'^api/post/(?P<post_id>\d+)/$', api.get_post, name="get_post"),
61 url(r'^api/diff_thread/$', api.api_get_threaddiff, name="get_thread_diff"),
61 url(r'^api/diff_thread/$', api.api_get_threaddiff, name="get_thread_diff"),
62 url(r'^api/threads/(?P<count>\w+)/$', api.api_get_threads,
62 url(r'^api/threads/(?P<count>\w+)/$', api.api_get_threads,
63 name='get_threads'),
63 name='get_threads'),
64 url(r'^api/tags/$', api.api_get_tags, name='get_tags'),
64 url(r'^api/tags/$', api.api_get_tags, name='get_tags'),
65 url(r'^api/thread/(?P<opening_post_id>\w+)/$', api.api_get_thread_posts,
65 url(r'^api/thread/(?P<opening_post_id>\w+)/$', api.api_get_thread_posts,
66 name='get_thread'),
66 name='get_thread'),
67 url(r'^api/add_post/(?P<opening_post_id>\w+)/$', api.api_add_post,
67 url(r'^api/add_post/(?P<opening_post_id>\w+)/$', api.api_add_post,
68 name='add_post'),
68 name='add_post'),
69 url(r'^api/notifications/(?P<username>\w+)/$', api.api_get_notifications,
69 url(r'^api/notifications/(?P<username>\w+)/$', api.api_get_notifications,
70 name='api_notifications'),
70 name='api_notifications'),
71 url(r'^api/preview/$', api.api_get_preview, name='preview'),
71 url(r'^api/preview/$', api.api_get_preview, name='preview'),
72 url(r'^api/new_posts/$', api.api_get_new_posts, name='new_posts'),
72
73
73 # Search
74 # Search
74 url(r'^search/$', BoardSearchView.as_view(), name='search'),
75 url(r'^search/$', BoardSearchView.as_view(), name='search'),
75
76
76 # Notifications
77 # Notifications
77 url(r'^notifications/(?P<username>\w+)$', NotificationView.as_view(), name='notifications'),
78 url(r'^notifications/(?P<username>\w+)$', NotificationView.as_view(), name='notifications'),
78
79
79 # Post preview
80 # Post preview
80 url(r'^preview/$', PostPreviewView.as_view(), name='preview')
81 url(r'^preview/$', PostPreviewView.as_view(), name='preview')
81
82
82 )
83 )
@@ -1,248 +1,274
1 from collections import OrderedDict
1 import json
2 import json
2 import logging
3 import logging
3
4
4 from django.db import transaction
5 from django.db import transaction
5 from django.http import HttpResponse
6 from django.http import HttpResponse
6 from django.shortcuts import get_object_or_404
7 from django.shortcuts import get_object_or_404
7 from django.core import serializers
8 from django.core import serializers
8 from boards.abstracts.settingsmanager import get_settings_manager
9 from boards.abstracts.settingsmanager import get_settings_manager
9
10
10 from boards.forms import PostForm, PlainErrorList
11 from boards.forms import PostForm, PlainErrorList
11 from boards.models import Post, Thread, Tag
12 from boards.models import Post, Thread, Tag
12 from boards.utils import datetime_to_epoch
13 from boards.utils import datetime_to_epoch
13 from boards.views.thread import ThreadView
14 from boards.views.thread import ThreadView
14 from boards.models.user import Notification
15 from boards.models.user import Notification
15 from boards.mdx_neboard import Parser
16 from boards.mdx_neboard import Parser
16
17
17
18
18 __author__ = 'neko259'
19 __author__ = 'neko259'
19
20
20 PARAMETER_TRUNCATED = 'truncated'
21 PARAMETER_TRUNCATED = 'truncated'
21 PARAMETER_TAG = 'tag'
22 PARAMETER_TAG = 'tag'
22 PARAMETER_OFFSET = 'offset'
23 PARAMETER_OFFSET = 'offset'
23 PARAMETER_DIFF_TYPE = 'type'
24 PARAMETER_DIFF_TYPE = 'type'
24 PARAMETER_POST = 'post'
25 PARAMETER_POST = 'post'
25 PARAMETER_UPDATED = 'updated'
26 PARAMETER_UPDATED = 'updated'
26 PARAMETER_LAST_UPDATE = 'last_update'
27 PARAMETER_LAST_UPDATE = 'last_update'
27 PARAMETER_THREAD = 'thread'
28 PARAMETER_THREAD = 'thread'
28 PARAMETER_UIDS = 'uids'
29 PARAMETER_UIDS = 'uids'
29
30
30 DIFF_TYPE_HTML = 'html'
31 DIFF_TYPE_HTML = 'html'
31 DIFF_TYPE_JSON = 'json'
32 DIFF_TYPE_JSON = 'json'
32
33
33 STATUS_OK = 'ok'
34 STATUS_OK = 'ok'
34 STATUS_ERROR = 'error'
35 STATUS_ERROR = 'error'
35
36
36 logger = logging.getLogger(__name__)
37 logger = logging.getLogger(__name__)
37
38
38
39
39 @transaction.atomic
40 @transaction.atomic
40 def api_get_threaddiff(request):
41 def api_get_threaddiff(request):
41 """
42 """
42 Gets posts that were changed or added since time
43 Gets posts that were changed or added since time
43 """
44 """
44
45
45 thread_id = request.POST.get(PARAMETER_THREAD)
46 thread_id = request.POST.get(PARAMETER_THREAD)
46 uids_str = request.POST.get(PARAMETER_UIDS).strip()
47 uids_str = request.POST.get(PARAMETER_UIDS).strip()
47 uids = uids_str.split(' ')
48 uids = uids_str.split(' ')
48
49
49 opening_post = get_object_or_404(Post, id=thread_id)
50 opening_post = get_object_or_404(Post, id=thread_id)
50 thread = opening_post.get_thread()
51 thread = opening_post.get_thread()
51
52
52 json_data = {
53 json_data = {
53 PARAMETER_UPDATED: [],
54 PARAMETER_UPDATED: [],
54 PARAMETER_LAST_UPDATE: None, # TODO Maybe this can be removed already?
55 PARAMETER_LAST_UPDATE: None, # TODO Maybe this can be removed already?
55 }
56 }
56 posts = Post.objects.filter(threads__in=[thread]).exclude(uid__in=uids)
57 posts = Post.objects.filter(threads__in=[thread]).exclude(uid__in=uids)
57
58
58 diff_type = request.GET.get(PARAMETER_DIFF_TYPE, DIFF_TYPE_HTML)
59 diff_type = request.GET.get(PARAMETER_DIFF_TYPE, DIFF_TYPE_HTML)
59
60
60 for post in posts:
61 for post in posts:
61 json_data[PARAMETER_UPDATED].append(get_post_data(post.id, diff_type,
62 json_data[PARAMETER_UPDATED].append(post.get_post_data(
62 request))
63 format_type=diff_type, request=request))
63 json_data[PARAMETER_LAST_UPDATE] = str(thread.last_edit_time)
64 json_data[PARAMETER_LAST_UPDATE] = str(thread.last_edit_time)
64
65
65 # If the tag is favorite, update the counter
66 # If the tag is favorite, update the counter
66 settings_manager = get_settings_manager(request)
67 settings_manager = get_settings_manager(request)
67 favorite = settings_manager.thread_is_fav(opening_post)
68 favorite = settings_manager.thread_is_fav(opening_post)
68 if favorite:
69 if favorite:
69 settings_manager.add_or_read_fav_thread(opening_post)
70 settings_manager.add_or_read_fav_thread(opening_post)
70
71
71 return HttpResponse(content=json.dumps(json_data))
72 return HttpResponse(content=json.dumps(json_data))
72
73
73
74
74 def api_add_post(request, opening_post_id):
75 def api_add_post(request, opening_post_id):
75 """
76 """
76 Adds a post and return the JSON response for it
77 Adds a post and return the JSON response for it
77 """
78 """
78
79
79 opening_post = get_object_or_404(Post, id=opening_post_id)
80 opening_post = get_object_or_404(Post, id=opening_post_id)
80
81
81 logger.info('Adding post via api...')
82 logger.info('Adding post via api...')
82
83
83 status = STATUS_OK
84 status = STATUS_OK
84 errors = []
85 errors = []
85
86
86 if request.method == 'POST':
87 if request.method == 'POST':
87 form = PostForm(request.POST, request.FILES, error_class=PlainErrorList)
88 form = PostForm(request.POST, request.FILES, error_class=PlainErrorList)
88 form.session = request.session
89 form.session = request.session
89
90
90 if form.need_to_ban:
91 if form.need_to_ban:
91 # Ban user because he is suspected to be a bot
92 # Ban user because he is suspected to be a bot
92 # _ban_current_user(request)
93 # _ban_current_user(request)
93 status = STATUS_ERROR
94 status = STATUS_ERROR
94 if form.is_valid():
95 if form.is_valid():
95 post = ThreadView().new_post(request, form, opening_post,
96 post = ThreadView().new_post(request, form, opening_post,
96 html_response=False)
97 html_response=False)
97 if not post:
98 if not post:
98 status = STATUS_ERROR
99 status = STATUS_ERROR
99 else:
100 else:
100 logger.info('Added post #%d via api.' % post.id)
101 logger.info('Added post #%d via api.' % post.id)
101 else:
102 else:
102 status = STATUS_ERROR
103 status = STATUS_ERROR
103 errors = form.as_json_errors()
104 errors = form.as_json_errors()
104
105
105 response = {
106 response = {
106 'status': status,
107 'status': status,
107 'errors': errors,
108 'errors': errors,
108 }
109 }
109
110
110 return HttpResponse(content=json.dumps(response))
111 return HttpResponse(content=json.dumps(response))
111
112
112
113
113 def get_post(request, post_id):
114 def get_post(request, post_id):
114 """
115 """
115 Gets the html of a post. Used for popups. Post can be truncated if used
116 Gets the html of a post. Used for popups. Post can be truncated if used
116 in threads list with 'truncated' get parameter.
117 in threads list with 'truncated' get parameter.
117 """
118 """
118
119
119 post = get_object_or_404(Post, id=post_id)
120 post = get_object_or_404(Post, id=post_id)
120 truncated = PARAMETER_TRUNCATED in request.GET
121 truncated = PARAMETER_TRUNCATED in request.GET
121
122
122 return HttpResponse(content=post.get_view(truncated=truncated))
123 return HttpResponse(content=post.get_view(truncated=truncated))
123
124
124
125
125 def api_get_threads(request, count):
126 def api_get_threads(request, count):
126 """
127 """
127 Gets the JSON thread opening posts list.
128 Gets the JSON thread opening posts list.
128 Parameters that can be used for filtering:
129 Parameters that can be used for filtering:
129 tag, offset (from which thread to get results)
130 tag, offset (from which thread to get results)
130 """
131 """
131
132
132 if PARAMETER_TAG in request.GET:
133 if PARAMETER_TAG in request.GET:
133 tag_name = request.GET[PARAMETER_TAG]
134 tag_name = request.GET[PARAMETER_TAG]
134 if tag_name is not None:
135 if tag_name is not None:
135 tag = get_object_or_404(Tag, name=tag_name)
136 tag = get_object_or_404(Tag, name=tag_name)
136 threads = tag.get_threads().filter(archived=False)
137 threads = tag.get_threads().filter(archived=False)
137 else:
138 else:
138 threads = Thread.objects.filter(archived=False)
139 threads = Thread.objects.filter(archived=False)
139
140
140 if PARAMETER_OFFSET in request.GET:
141 if PARAMETER_OFFSET in request.GET:
141 offset = request.GET[PARAMETER_OFFSET]
142 offset = request.GET[PARAMETER_OFFSET]
142 offset = int(offset) if offset is not None else 0
143 offset = int(offset) if offset is not None else 0
143 else:
144 else:
144 offset = 0
145 offset = 0
145
146
146 threads = threads.order_by('-bump_time')
147 threads = threads.order_by('-bump_time')
147 threads = threads[offset:offset + int(count)]
148 threads = threads[offset:offset + int(count)]
148
149
149 opening_posts = []
150 opening_posts = []
150 for thread in threads:
151 for thread in threads:
151 opening_post = thread.get_opening_post()
152 opening_post = thread.get_opening_post()
152
153
153 # TODO Add tags, replies and images count
154 # TODO Add tags, replies and images count
154 post_data = get_post_data(opening_post.id, include_last_update=True)
155 post_data = opening_post.get_post_data(include_last_update=True)
155 post_data['bumpable'] = thread.can_bump()
156 post_data['bumpable'] = thread.can_bump()
156 post_data['archived'] = thread.archived
157 post_data['archived'] = thread.archived
157
158
158 opening_posts.append(post_data)
159 opening_posts.append(post_data)
159
160
160 return HttpResponse(content=json.dumps(opening_posts))
161 return HttpResponse(content=json.dumps(opening_posts))
161
162
162
163
163 # TODO Test this
164 # TODO Test this
164 def api_get_tags(request):
165 def api_get_tags(request):
165 """
166 """
166 Gets all tags or user tags.
167 Gets all tags or user tags.
167 """
168 """
168
169
169 # TODO Get favorite tags for the given user ID
170 # TODO Get favorite tags for the given user ID
170
171
171 tags = Tag.objects.get_not_empty_tags()
172 tags = Tag.objects.get_not_empty_tags()
172
173
173 term = request.GET.get('term')
174 term = request.GET.get('term')
174 if term is not None:
175 if term is not None:
175 tags = tags.filter(name__contains=term)
176 tags = tags.filter(name__contains=term)
176
177
177 tag_names = [tag.name for tag in tags]
178 tag_names = [tag.name for tag in tags]
178
179
179 return HttpResponse(content=json.dumps(tag_names))
180 return HttpResponse(content=json.dumps(tag_names))
180
181
181
182
182 # TODO The result can be cached by the thread last update time
183 # TODO The result can be cached by the thread last update time
183 # TODO Test this
184 # TODO Test this
184 def api_get_thread_posts(request, opening_post_id):
185 def api_get_thread_posts(request, opening_post_id):
185 """
186 """
186 Gets the JSON array of thread posts
187 Gets the JSON array of thread posts
187 """
188 """
188
189
189 opening_post = get_object_or_404(Post, id=opening_post_id)
190 opening_post = get_object_or_404(Post, id=opening_post_id)
190 thread = opening_post.get_thread()
191 thread = opening_post.get_thread()
191 posts = thread.get_replies()
192 posts = thread.get_replies()
192
193
193 json_data = {
194 json_data = {
194 'posts': [],
195 'posts': [],
195 'last_update': None,
196 'last_update': None,
196 }
197 }
197 json_post_list = []
198 json_post_list = []
198
199
199 for post in posts:
200 for post in posts:
200 json_post_list.append(get_post_data(post.id))
201 json_post_list.append(post.get_post_data())
201 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
202 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
202 json_data['posts'] = json_post_list
203 json_data['posts'] = json_post_list
203
204
204 return HttpResponse(content=json.dumps(json_data))
205 return HttpResponse(content=json.dumps(json_data))
205
206
206
207
207 def api_get_notifications(request, username):
208 def api_get_notifications(request, username):
208 last_notification_id_str = request.GET.get('last', None)
209 last_notification_id_str = request.GET.get('last', None)
209 last_id = int(last_notification_id_str) if last_notification_id_str is not None else None
210 last_id = int(last_notification_id_str) if last_notification_id_str is not None else None
210
211
211 posts = Notification.objects.get_notification_posts(username=username,
212 posts = Notification.objects.get_notification_posts(username=username,
212 last=last_id)
213 last=last_id)
213
214
214 json_post_list = []
215 json_post_list = []
215 for post in posts:
216 for post in posts:
216 json_post_list.append(get_post_data(post.id))
217 json_post_list.append(post.get_post_data())
217 return HttpResponse(content=json.dumps(json_post_list))
218 return HttpResponse(content=json.dumps(json_post_list))
218
219
219
220
220 def api_get_post(request, post_id):
221 def api_get_post(request, post_id):
221 """
222 """
222 Gets the JSON of a post. This can be
223 Gets the JSON of a post. This can be
223 used as and API for external clients.
224 used as and API for external clients.
224 """
225 """
225
226
226 post = get_object_or_404(Post, id=post_id)
227 post = get_object_or_404(Post, id=post_id)
227
228
228 json = serializers.serialize("json", [post], fields=(
229 json = serializers.serialize("json", [post], fields=(
229 "pub_time", "_text_rendered", "title", "text", "image",
230 "pub_time", "_text_rendered", "title", "text", "image",
230 "image_width", "image_height", "replies", "tags"
231 "image_width", "image_height", "replies", "tags"
231 ))
232 ))
232
233
233 return HttpResponse(content=json)
234 return HttpResponse(content=json)
234
235
235
236
236 # TODO Remove this method and use post method directly
237 def get_post_data(post_id, format_type=DIFF_TYPE_JSON, request=None,
238 include_last_update=False):
239 post = get_object_or_404(Post, id=post_id)
240 return post.get_post_data(format_type=format_type, request=request,
241 include_last_update=include_last_update)
242
243
244 def api_get_preview(request):
237 def api_get_preview(request):
245 raw_text = request.POST['raw_text']
238 raw_text = request.POST['raw_text']
246
239
247 parser = Parser()
240 parser = Parser()
248 return HttpResponse(content=parser.parse(parser.preparse(raw_text)))
241 return HttpResponse(content=parser.parse(parser.preparse(raw_text)))
242
243
244 def api_get_new_posts(request):
245 """
246 Gets favorite threads and unread posts count.
247 """
248 posts = list()
249
250 include_posts = 'include_posts' in request.GET
251
252 settings_manager = get_settings_manager(request)
253 fav_threads = settings_manager.get_fav_threads()
254 fav_thread_ops = Post.objects.filter(id__in=fav_threads.keys())\
255 .order_by('-pub_time').prefetch_related('thread')
256
257 for op in fav_thread_ops:
258 last_read_post = fav_threads[str(op.id)]
259 new_posts = op.get_thread().get_replies_newer(last_read_post)
260 new_post_count = new_posts.count()
261 fav_thread_dict = dict()
262 fav_thread_dict['id'] = op.id
263 fav_thread_dict['new_post_count'] = new_post_count
264
265 if include_posts:
266 fav_thread_dict['post'] = op.get_post_data(
267 format_type=DIFF_TYPE_HTML)
268 if new_post_count > 0:
269 fav_thread_dict['newest_post_link'] = new_posts.first()\
270 .get_absolute_url()
271
272 posts.append(fav_thread_dict)
273
274 return HttpResponse(content=json.dumps(posts))
General Comments 0
You need to be logged in to leave comments. Login now