##// END OF EJS Templates
Added local stickers feature
neko259 -
r1940:1f7b0788 default
parent child Browse files
Show More
@@ -1,29 +1,29 b''
1 1 from boards.abstracts.settingsmanager import SessionSettingsManager
2 2 from boards.models import Attachment
3 3
4 4
5 5 class AttachmentAlias:
6 6 def get_image(self, alias):
7 7 pass
8 8
9 9
10 10 class SessionAttachmentAlias(AttachmentAlias):
11 11 def __init__(self, session):
12 12 self.session = session
13 13
14 14 def get_image(self, alias):
15 15 settings_manager = SessionSettingsManager(self.session)
16 return settings_manager.get_image_by_alias(alias)
16 return settings_manager.get_attachment_by_alias(alias)
17 17
18 18
19 19 class ModelAttachmentAlias(AttachmentAlias):
20 20 def get_image(self, alias):
21 21 return Attachment.objects.get_by_alias(alias)
22 22
23 23
24 24 def get_image_by_alias(alias, session):
25 25 image = SessionAttachmentAlias(session).get_image(alias)\
26 26 or ModelAttachmentAlias().get_image(alias)
27 27
28 28 if image is not None:
29 29 return image
@@ -1,205 +1,219 b''
1 1 from boards import settings
2 from boards.models import Tag, TagAlias
2 from boards.models import Tag, TagAlias, Attachment
3 from boards.models.attachment import AttachmentSticker
3 4 from boards.models.thread import FAV_THREAD_NO_UPDATES
4 5 from boards.models.tag import DEFAULT_LOCALE
5 6
6 7 MAX_TRIPCODE_COLLISIONS = 50
7 8
8 9 __author__ = 'neko259'
9 10
10 11 SESSION_SETTING = 'setting'
11 12
12 13 # Remove this, it is not used any more cause there is a user's permission
13 14 PERMISSION_MODERATE = 'moderator'
14 15
15 16 SETTING_THEME = 'theme'
16 17 SETTING_FAVORITE_TAGS = 'favorite_tags'
17 18 SETTING_FAVORITE_THREADS = 'favorite_threads'
18 19 SETTING_HIDDEN_TAGS = 'hidden_tags'
19 20 SETTING_PERMISSIONS = 'permissions'
20 21 SETTING_USERNAME = 'username'
21 22 SETTING_LAST_NOTIFICATION_ID = 'last_notification'
22 23 SETTING_IMAGE_VIEWER = 'image_viewer'
23 24 SETTING_TRIPCODE = 'tripcode'
24 25 SETTING_IMAGES = 'images_aliases'
25 26 SETTING_ONLY_FAVORITES = 'only_favorites'
26 27
27 28 DEFAULT_THEME = 'md'
28 29
29 30
30 31 class SettingsManager:
31 32 """
32 33 Base settings manager class. get_setting and set_setting methods should
33 34 be overriden.
34 35 """
35 36 def __init__(self):
36 37 pass
37 38
38 39 def get_theme(self) -> str:
39 40 theme = self.get_setting(SETTING_THEME)
40 41 if not theme:
41 42 theme = DEFAULT_THEME
42 43 self.set_setting(SETTING_THEME, theme)
43 44
44 45 return theme
45 46
46 47 def set_theme(self, theme):
47 48 self.set_setting(SETTING_THEME, theme)
48 49
49 50 def has_permission(self, permission):
50 51 permissions = self.get_setting(SETTING_PERMISSIONS)
51 52 if permissions:
52 53 return permission in permissions
53 54 else:
54 55 return False
55 56
56 57 def get_setting(self, setting, default=None):
57 58 pass
58 59
59 60 def set_setting(self, setting, value):
60 61 pass
61 62
62 63 def add_permission(self, permission):
63 64 permissions = self.get_setting(SETTING_PERMISSIONS)
64 65 if not permissions:
65 66 permissions = [permission]
66 67 else:
67 68 permissions.append(permission)
68 69 self.set_setting(SETTING_PERMISSIONS, permissions)
69 70
70 71 def del_permission(self, permission):
71 72 permissions = self.get_setting(SETTING_PERMISSIONS)
72 73 if not permissions:
73 74 permissions = []
74 75 else:
75 76 permissions.remove(permission)
76 77 self.set_setting(SETTING_PERMISSIONS, permissions)
77 78
78 79 def get_fav_tags(self) -> list:
79 80 tag_names = self.get_setting(SETTING_FAVORITE_TAGS)
80 81 tags = []
81 82 if tag_names:
82 83 tags = list(Tag.objects.filter(aliases__in=TagAlias.objects
83 84 .filter_localized(parent__aliases__name__in=tag_names))
84 85 .order_by('aliases__name'))
85 86 return tags
86 87
87 88 def add_fav_tag(self, tag):
88 89 tags = self.get_setting(SETTING_FAVORITE_TAGS)
89 90 if not tags:
90 91 tags = [tag.get_name()]
91 92 else:
92 93 if not tag.get_name() in tags:
93 94 tags.append(tag.get_name())
94 95
95 96 tags.sort()
96 97 self.set_setting(SETTING_FAVORITE_TAGS, tags)
97 98
98 99 def del_fav_tag(self, tag):
99 100 tags = self.get_setting(SETTING_FAVORITE_TAGS)
100 101 if tag.get_name() in tags:
101 102 tags.remove(tag.get_name())
102 103 self.set_setting(SETTING_FAVORITE_TAGS, tags)
103 104
104 105 def get_hidden_tags(self) -> list:
105 106 tag_names = self.get_setting(SETTING_HIDDEN_TAGS)
106 107 tags = []
107 108 if tag_names:
108 109 tags = list(Tag.objects.filter(aliases__in=TagAlias.objects
109 110 .filter_localized(parent__aliases__name__in=tag_names))
110 111 .order_by('aliases__name'))
111 112
112 113 return tags
113 114
114 115 def add_hidden_tag(self, tag):
115 116 tags = self.get_setting(SETTING_HIDDEN_TAGS)
116 117 if not tags:
117 118 tags = [tag.get_name()]
118 119 else:
119 120 if not tag.get_name() in tags:
120 121 tags.append(tag.get_name())
121 122
122 123 tags.sort()
123 124 self.set_setting(SETTING_HIDDEN_TAGS, tags)
124 125
125 126 def del_hidden_tag(self, tag):
126 127 tags = self.get_setting(SETTING_HIDDEN_TAGS)
127 128 if tag.get_name() in tags:
128 129 tags.remove(tag.get_name())
129 130 self.set_setting(SETTING_HIDDEN_TAGS, tags)
130 131
131 132 def get_fav_threads(self) -> dict:
132 133 return self.get_setting(SETTING_FAVORITE_THREADS, default=dict())
133 134
134 135 def add_or_read_fav_thread(self, opening_post):
135 136 threads = self.get_fav_threads()
136 137
137 138 max_fav_threads = settings.get_int('View', 'MaxFavoriteThreads')
138 139 if (str(opening_post.id) in threads) or (len(threads) < max_fav_threads):
139 140 thread = opening_post.get_thread()
140 141 # Don't check for new posts if the thread is archived already
141 142 if thread.is_archived():
142 143 last_id = FAV_THREAD_NO_UPDATES
143 144 else:
144 145 last_id = thread.get_replies().last().id
145 146 threads[str(opening_post.id)] = last_id
146 147 self.set_setting(SETTING_FAVORITE_THREADS, threads)
147 148
148 149 def del_fav_thread(self, opening_post):
149 150 threads = self.get_fav_threads()
150 151 if self.thread_is_fav(opening_post):
151 152 del threads[str(opening_post.id)]
152 153 self.set_setting(SETTING_FAVORITE_THREADS, threads)
153 154
154 155 def thread_is_fav(self, opening_post):
155 156 return str(opening_post.id) in self.get_fav_threads()
156 157
157 158 def get_notification_usernames(self):
158 159 names = set()
159 160 name_list = self.get_setting(SETTING_USERNAME)
160 161 if name_list is not None:
161 162 name_list = name_list.strip()
162 163 if len(name_list) > 0:
163 164 names = name_list.lower().split(',')
164 165 names = set(name.strip() for name in names)
165 166 return names
166 167
167 def get_image_by_alias(self, alias):
168 def get_attachment_by_alias(self, alias):
168 169 images = self.get_setting(SETTING_IMAGES)
169 if images is not None and len(images) > 0:
170 return images.get(alias)
170 if images and alias in images:
171 return Attachment.objects.get(id=images.get(alias))
171 172
172 def add_image_alias(self, alias, image):
173 def add_attachment_alias(self, alias, attachment):
173 174 images = self.get_setting(SETTING_IMAGES)
174 175 if images is None:
175 176 images = dict()
176 images.put(alias, image)
177 images[alias] = attachment.id
178 self.set_setting(SETTING_IMAGES, images)
179
180 def remove_attachment_alias(self, alias):
181 images = self.get_setting(SETTING_IMAGES)
182 del images[alias]
183 self.set_setting(SETTING_IMAGES, images)
184
185 def get_stickers(self):
186 images = self.get_setting(SETTING_IMAGES)
187 if images:
188 return [AttachmentSticker(name=key,
189 attachment=Attachment.objects.get(id=value))
190 for key, value in images.items()]
177 191
178 192
179 193 class SessionSettingsManager(SettingsManager):
180 194 """
181 195 Session-based settings manager. All settings are saved to the user's
182 196 session.
183 197 """
184 198 def __init__(self, session):
185 199 SettingsManager.__init__(self)
186 200 self.session = session
187 201
188 202 def get_setting(self, setting, default=None):
189 203 if setting in self.session:
190 204 return self.session[setting]
191 205 else:
192 206 self.set_setting(setting, default)
193 207 return default
194 208
195 209 def set_setting(self, setting, value):
196 210 self.session[setting] = value
197 211
198 212
199 213 def get_settings_manager(request) -> SettingsManager:
200 214 """
201 215 Get settings manager based on the request object. Currently only
202 216 session-based manager is supported. In the future, cookie-based or
203 217 database-based managers could be implemented.
204 218 """
205 219 return SessionSettingsManager(request.session)
1 NO CONTENT: modified file, binary diff hidden
@@ -1,624 +1,633 b''
1 1 # SOME DESCRIPTIVE TITLE.
2 2 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3 3 # This file is distributed under the same license as the PACKAGE package.
4 4 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
5 5 #
6 6 msgid ""
7 7 msgstr ""
8 8 "Project-Id-Version: PACKAGE VERSION\n"
9 9 "Report-Msgid-Bugs-To: \n"
10 10 "POT-Creation-Date: 2015-10-09 23:21+0300\n"
11 11 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
12 12 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
13 13 "Language-Team: LANGUAGE <LL@li.org>\n"
14 14 "Language: ru\n"
15 15 "MIME-Version: 1.0\n"
16 16 "Content-Type: text/plain; charset=UTF-8\n"
17 17 "Content-Transfer-Encoding: 8bit\n"
18 18 "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
19 19 "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
20 20
21 21 #: admin.py:22
22 22 msgid "{} posters were banned"
23 23 msgstr ""
24 24
25 25 #: authors.py:9
26 26 msgid "author"
27 27 msgstr "Π°Π²Ρ‚ΠΎΡ€"
28 28
29 29 #: authors.py:10
30 30 msgid "developer"
31 31 msgstr "Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ"
32 32
33 33 #: authors.py:11
34 34 msgid "javascript developer"
35 35 msgstr "Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ javascript"
36 36
37 37 #: authors.py:12
38 38 msgid "designer"
39 39 msgstr "Π΄ΠΈΠ·Π°ΠΉΠ½Π΅Ρ€"
40 40
41 41 #: forms.py:30
42 42 msgid "Type message here. Use formatting panel for more advanced usage."
43 43 msgstr ""
44 44 "Π’Π²ΠΎΠ΄ΠΈΡ‚Π΅ сообщСниС сюда. Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉΡ‚Π΅ панСль для Π±ΠΎΠ»Π΅Π΅ слоТного форматирования."
45 45
46 46 #: forms.py:31
47 47 msgid "music images i_dont_like_tags"
48 48 msgstr "ΠΌΡƒΠ·Ρ‹ΠΊΠ° ΠΊΠ°Ρ€Ρ‚ΠΈΠ½ΠΊΠΈ Ρ‚Π΅Π³ΠΈ_Π½Π΅_Π½ΡƒΠΆΠ½Ρ‹"
49 49
50 50 #: forms.py:33
51 51 msgid "Title"
52 52 msgstr "Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ"
53 53
54 54 #: forms.py:34
55 55 msgid "Text"
56 56 msgstr "ВСкст"
57 57
58 58 #: forms.py:35
59 59 msgid "Tag"
60 60 msgstr "ΠœΠ΅Ρ‚ΠΊΠ°"
61 61
62 62 #: forms.py:36 templates/boards/base.html:40 templates/search/search.html:7
63 63 msgid "Search"
64 64 msgstr "Поиск"
65 65
66 66 #: forms.py:48
67 67 msgid "File 1"
68 68 msgstr "Π€Π°ΠΉΠ» 1"
69 69
70 70 #: forms.py:48
71 71 msgid "File 2"
72 72 msgstr "Π€Π°ΠΉΠ» 2"
73 73
74 74 #: forms.py:142
75 75 msgid "File URL"
76 76 msgstr "URL Ρ„Π°ΠΉΠ»Π°"
77 77
78 78 #: forms.py:148
79 79 msgid "e-mail"
80 80 msgstr ""
81 81
82 82 #: forms.py:151
83 83 msgid "Additional threads"
84 84 msgstr "Π”ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Π΅ Ρ‚Π΅ΠΌΡ‹"
85 85
86 86 #: forms.py:162
87 87 #, python-format
88 88 msgid "Title must have less than %s characters"
89 89 msgstr "Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ Π΄ΠΎΠ»ΠΆΠ΅Π½ ΠΈΠΌΠ΅Ρ‚ΡŒ мСньшС %s символов"
90 90
91 91 #: forms.py:172
92 92 #, python-format
93 93 msgid "Text must have less than %s characters"
94 94 msgstr "ВСкст Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±Ρ‹Ρ‚ΡŒ ΠΊΠΎΡ€ΠΎΡ‡Π΅ %s символов"
95 95
96 96 #: forms.py:192
97 97 msgid "Invalid URL"
98 98 msgstr "НСвСрный URL"
99 99
100 100 #: forms.py:213
101 101 msgid "Invalid additional thread list"
102 102 msgstr "НСвСрный список Π΄ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Ρ… Ρ‚Π΅ΠΌ"
103 103
104 104 #: forms.py:258
105 105 msgid "Either text or file must be entered."
106 106 msgstr "ВСкст ΠΈΠ»ΠΈ Ρ„Π°ΠΉΠ» Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ Π²Π²Π΅Π΄Π΅Π½Ρ‹."
107 107
108 108 #: forms.py:317 templates/boards/all_threads.html:153
109 109 #: templates/boards/rss/post.html:10 templates/boards/tags.html:6
110 110 msgid "Tags"
111 111 msgstr "ΠœΠ΅Ρ‚ΠΊΠΈ"
112 112
113 113 #: forms.py:324
114 114 msgid "Inappropriate characters in tags."
115 115 msgstr "НСдопустимыС символы Π² ΠΌΠ΅Ρ‚ΠΊΠ°Ρ…."
116 116
117 117 #: forms.py:344
118 118 msgid "Need at least one section."
119 119 msgstr "НуТСн хотя Π±Ρ‹ ΠΎΠ΄ΠΈΠ½ Ρ€Π°Π·Π΄Π΅Π»."
120 120
121 121 #: forms.py:356
122 122 msgid "Theme"
123 123 msgstr "Π’Π΅ΠΌΠ°"
124 124
125 125 #: forms.py:357
126 126 msgid "Image view mode"
127 127 msgstr "Π Π΅ΠΆΠΈΠΌ просмотра ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ"
128 128
129 129 #: forms.py:358
130 130 msgid "User name"
131 131 msgstr "Имя ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ"
132 132
133 133 #: forms.py:359
134 134 msgid "Time zone"
135 135 msgstr "Часовой пояс"
136 136
137 137 #: forms.py:365
138 138 msgid "Inappropriate characters."
139 139 msgstr "НСдопустимыС символы."
140 140
141 141 #: templates/boards/404.html:6
142 142 msgid "Not found"
143 143 msgstr "НС найдСно"
144 144
145 145 #: templates/boards/404.html:12
146 146 msgid "This page does not exist"
147 147 msgstr "Π­Ρ‚ΠΎΠΉ страницы Π½Π΅ сущСствуСт"
148 148
149 149 #: templates/boards/all_threads.html:35
150 150 msgid "Details"
151 151 msgstr "ΠŸΠΎΠ΄Ρ€ΠΎΠ±Π½ΠΎΡΡ‚ΠΈ"
152 152
153 153 #: templates/boards/all_threads.html:69
154 154 msgid "Edit tag"
155 155 msgstr "Π˜Π·ΠΌΠ΅Π½ΠΈΡ‚ΡŒ ΠΌΠ΅Ρ‚ΠΊΡƒ"
156 156
157 157 #: templates/boards/all_threads.html:76
158 158 #, python-format
159 159 msgid "%(count)s active thread"
160 160 msgid_plural "%(count)s active threads"
161 161 msgstr[0] "%(count)s активная Ρ‚Π΅ΠΌΠ°"
162 162 msgstr[1] "%(count)s Π°ΠΊΡ‚ΠΈΠ²Π½Ρ‹Π΅ Ρ‚Π΅ΠΌΡ‹"
163 163 msgstr[2] "%(count)s Π°ΠΊΡ‚ΠΈΠ²Π½Ρ‹Ρ… Ρ‚Π΅ΠΌ"
164 164
165 165 #: templates/boards/all_threads.html:76
166 166 #, python-format
167 167 msgid "%(count)s thread in bumplimit"
168 168 msgid_plural "%(count)s threads in bumplimit"
169 169 msgstr[0] "%(count)s Ρ‚Π΅ΠΌΠ° Π² Π±Π°ΠΌΠΏΠ»ΠΈΠΌΠΈΡ‚Π΅"
170 170 msgstr[1] "%(count)s Ρ‚Π΅ΠΌΡ‹ Π² Π±Π°ΠΌΠΏΠ»ΠΈΠΌΠΈΡ‚Π΅"
171 171 msgstr[2] "%(count)s Ρ‚Π΅ΠΌ Π² Π±Π°ΠΌΠΏΠ»ΠΈΠΌΠΈΡ‚Π΅"
172 172
173 173 #: templates/boards/all_threads.html:77
174 174 #, python-format
175 175 msgid "%(count)s archived thread"
176 176 msgid_plural "%(count)s archived thread"
177 177 msgstr[0] "%(count)s архивная Ρ‚Π΅ΠΌΠ°"
178 178 msgstr[1] "%(count)s Π°Ρ€Ρ…ΠΈΠ²Π½Ρ‹Π΅ Ρ‚Π΅ΠΌΡ‹"
179 179 msgstr[2] "%(count)s Π°Ρ€Ρ…ΠΈΠ²Π½Ρ‹Ρ… Ρ‚Π΅ΠΌ"
180 180
181 181 #: templates/boards/all_threads.html:78 templates/boards/post.html:102
182 182 #, python-format
183 183 #| msgid "%(count)s message"
184 184 #| msgid_plural "%(count)s messages"
185 185 msgid "%(count)s message"
186 186 msgid_plural "%(count)s messages"
187 187 msgstr[0] "%(count)s сообщСниС"
188 188 msgstr[1] "%(count)s сообщСния"
189 189 msgstr[2] "%(count)s сообщСний"
190 190
191 191 #: templates/boards/all_threads.html:95 templates/boards/feed.html:30
192 192 #: templates/boards/notifications.html:17 templates/search/search.html:26
193 193 msgid "Previous page"
194 194 msgstr "ΠŸΡ€Π΅Π΄Ρ‹Π΄ΡƒΡ‰Π°Ρ страница"
195 195
196 196 #: templates/boards/all_threads.html:109
197 197 #, python-format
198 198 msgid "Skipped %(count)s reply. Open thread to see all replies."
199 199 msgid_plural "Skipped %(count)s replies. Open thread to see all replies."
200 200 msgstr[0] "ΠŸΡ€ΠΎΠΏΡƒΡ‰Π΅Π½ %(count)s ΠΎΡ‚Π²Π΅Ρ‚. ΠžΡ‚ΠΊΡ€ΠΎΠΉΡ‚Π΅ Ρ‚Ρ€Π΅Π΄, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΡƒΠ²ΠΈΠ΄Π΅Ρ‚ΡŒ всС ΠΎΡ‚Π²Π΅Ρ‚Ρ‹."
201 201 msgstr[1] ""
202 202 "ΠŸΡ€ΠΎΠΏΡƒΡ‰Π΅Π½ΠΎ %(count)s ΠΎΡ‚Π²Π΅Ρ‚Π°. ΠžΡ‚ΠΊΡ€ΠΎΠΉΡ‚Π΅ Ρ‚Ρ€Π΅Π΄, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΡƒΠ²ΠΈΠ΄Π΅Ρ‚ΡŒ всС ΠΎΡ‚Π²Π΅Ρ‚Ρ‹."
203 203 msgstr[2] ""
204 204 "ΠŸΡ€ΠΎΠΏΡƒΡ‰Π΅Π½ΠΎ %(count)s ΠΎΡ‚Π²Π΅Ρ‚ΠΎΠ². ΠžΡ‚ΠΊΡ€ΠΎΠΉΡ‚Π΅ Ρ‚Ρ€Π΅Π΄, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΡƒΠ²ΠΈΠ΄Π΅Ρ‚ΡŒ всС ΠΎΡ‚Π²Π΅Ρ‚Ρ‹."
205 205
206 206 #: templates/boards/all_threads.html:127 templates/boards/feed.html:40
207 207 #: templates/boards/notifications.html:27 templates/search/search.html:37
208 208 msgid "Next page"
209 209 msgstr "Π‘Π»Π΅Π΄ΡƒΡŽΡ‰Π°Ρ страница"
210 210
211 211 #: templates/boards/all_threads.html:132
212 212 msgid "No threads exist. Create the first one!"
213 213 msgstr "НСт Ρ‚Π΅ΠΌ. Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ ΠΏΠ΅Ρ€Π²ΡƒΡŽ!"
214 214
215 215 #: templates/boards/all_threads.html:138
216 216 msgid "Create new thread"
217 217 msgstr "Π‘ΠΎΠ·Π΄Π°Ρ‚ΡŒ Π½ΠΎΠ²ΡƒΡŽ Ρ‚Π΅ΠΌΡƒ"
218 218
219 219 #: templates/boards/all_threads.html:143 templates/boards/preview.html:16
220 220 #: templates/boards/thread_normal.html:51
221 221 msgid "Post"
222 222 msgstr "ΠžΡ‚ΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ"
223 223
224 224 #: templates/boards/all_threads.html:144 templates/boards/preview.html:6
225 225 #: templates/boards/staticpages/help.html:21
226 226 #: templates/boards/thread_normal.html:52
227 227 msgid "Preview"
228 228 msgstr "ΠŸΡ€Π΅Π΄ΠΏΡ€ΠΎΡΠΌΠΎΡ‚Ρ€"
229 229
230 230 #: templates/boards/all_threads.html:149
231 231 msgid "Tags must be delimited by spaces. Text or image is required."
232 232 msgstr ""
233 233 "ΠœΠ΅Ρ‚ΠΊΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ Ρ€Π°Π·Π΄Π΅Π»Π΅Π½Ρ‹ ΠΏΡ€ΠΎΠ±Π΅Π»Π°ΠΌΠΈ. ВСкст ΠΈΠ»ΠΈ ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹."
234 234
235 235 #: templates/boards/all_threads.html:152 templates/boards/thread_normal.html:58
236 236 msgid "Text syntax"
237 237 msgstr "Бинтаксис тСкста"
238 238
239 239 #: templates/boards/all_threads.html:166 templates/boards/feed.html:53
240 240 msgid "Pages:"
241 241 msgstr "Π‘Ρ‚Ρ€Π°Π½ΠΈΡ†Ρ‹: "
242 242
243 243 #: templates/boards/authors.html:6 templates/boards/authors.html.py:12
244 244 msgid "Authors"
245 245 msgstr "Авторы"
246 246
247 247 #: templates/boards/authors.html:26
248 248 msgid "Distributed under the"
249 249 msgstr "РаспространяСтся ΠΏΠΎΠ΄"
250 250
251 251 #: templates/boards/authors.html:28
252 252 msgid "license"
253 253 msgstr "Π»ΠΈΡ†Π΅Π½Π·ΠΈΠ΅ΠΉ"
254 254
255 255 #: templates/boards/authors.html:30
256 256 msgid "Repository"
257 257 msgstr "Π Π΅ΠΏΠΎΠ·ΠΈΡ‚ΠΎΡ€ΠΈΠΉ"
258 258
259 259 #: templates/boards/base.html:14 templates/boards/base.html.py:41
260 260 msgid "Feed"
261 261 msgstr "Π›Π΅Π½Ρ‚Π°"
262 262
263 263 #: templates/boards/base.html:31
264 264 msgid "All threads"
265 265 msgstr "ВсС Ρ‚Π΅ΠΌΡ‹"
266 266
267 267 #: templates/boards/base.html:37
268 268 msgid "Add tags"
269 269 msgstr "Π”ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ ΠΌΠ΅Ρ‚ΠΊΠΈ"
270 270
271 271 #: templates/boards/base.html:39
272 272 msgid "Tag management"
273 273 msgstr "Π£ΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠ΅ ΠΌΠ΅Ρ‚ΠΊΠ°ΠΌΠΈ"
274 274
275 275 #: templates/boards/base.html:39
276 276 msgid "tags"
277 277 msgstr "ΠΌΠ΅Ρ‚ΠΊΠΈ"
278 278
279 279 #: templates/boards/base.html:40
280 280 msgid "search"
281 281 msgstr "поиск"
282 282
283 283 #: templates/boards/base.html:41 templates/boards/feed.html:11
284 284 msgid "feed"
285 285 msgstr "Π»Π΅Π½Ρ‚Π°"
286 286
287 287 #: templates/boards/base.html:42 templates/boards/random.html:6
288 288 msgid "Random images"
289 289 msgstr "Π‘Π»ΡƒΡ‡Π°ΠΉΠ½Ρ‹Π΅ изобраТСния"
290 290
291 291 #: templates/boards/base.html:42
292 292 msgid "random"
293 293 msgstr "случайныС"
294 294
295 295 #: templates/boards/base.html:44
296 296 msgid "favorites"
297 297 msgstr "ΠΈΠ·Π±Ρ€Π°Π½Π½ΠΎΠ΅"
298 298
299 299 #: templates/boards/base.html:48 templates/boards/base.html.py:49
300 300 #: templates/boards/notifications.html:8
301 301 msgid "Notifications"
302 302 msgstr "УвСдомлСния"
303 303
304 304 #: templates/boards/base.html:56 templates/boards/settings.html:8
305 305 msgid "Settings"
306 306 msgstr "Настройки"
307 307
308 308 #: templates/boards/base.html:59
309 309 msgid "Loading..."
310 310 msgstr "Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ°..."
311 311
312 312 #: templates/boards/base.html:71
313 313 msgid "Admin"
314 314 msgstr "АдминистрированиС"
315 315
316 316 #: templates/boards/base.html:73
317 317 #, python-format
318 318 msgid "Speed: %(ppd)s posts per day"
319 319 msgstr "Π‘ΠΊΠΎΡ€ΠΎΡΡ‚ΡŒ: %(ppd)s сообщСний Π² дСнь"
320 320
321 321 #: templates/boards/base.html:75
322 322 msgid "Up"
323 323 msgstr "Π’Π²Π΅Ρ€Ρ…"
324 324
325 325 #: templates/boards/feed.html:45
326 326 msgid "No posts exist. Create the first one!"
327 327 msgstr "НСт сообщСний. Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ ΠΏΠ΅Ρ€Π²ΠΎΠ΅!"
328 328
329 329 #: templates/boards/post.html:33
330 330 msgid "Open"
331 331 msgstr "ΠžΡ‚ΠΊΡ€Ρ‹Ρ‚ΡŒ"
332 332
333 333 #: templates/boards/post.html:35 templates/boards/post.html.py:46
334 334 msgid "Reply"
335 335 msgstr "ΠžΡ‚Π²Π΅Ρ‚ΠΈΡ‚ΡŒ"
336 336
337 337 #: templates/boards/post.html:41
338 338 msgid " in "
339 339 msgstr " Π² "
340 340
341 341 #: templates/boards/post.html:51
342 342 msgid "Edit"
343 343 msgstr "Π˜Π·ΠΌΠ΅Π½ΠΈΡ‚ΡŒ"
344 344
345 345 #: templates/boards/post.html:53
346 346 msgid "Edit thread"
347 347 msgstr "Π˜Π·ΠΌΠ΅Π½ΠΈΡ‚ΡŒ Ρ‚Π΅ΠΌΡƒ"
348 348
349 349 #: templates/boards/post.html:91
350 350 msgid "Replies"
351 351 msgstr "ΠžΡ‚Π²Π΅Ρ‚Ρ‹"
352 352
353 353 #: templates/boards/post.html:103
354 354 #, python-format
355 355 msgid "%(count)s image"
356 356 msgid_plural "%(count)s images"
357 357 msgstr[0] "%(count)s ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅"
358 358 msgstr[1] "%(count)s изобраТСния"
359 359 msgstr[2] "%(count)s ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ"
360 360
361 361 #: templates/boards/rss/post.html:5
362 362 msgid "Post image"
363 363 msgstr "Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ сообщСния"
364 364
365 365 #: templates/boards/settings.html:15
366 366 msgid "You are moderator."
367 367 msgstr "Π’Ρ‹ ΠΌΠΎΠ΄Π΅Ρ€Π°Ρ‚ΠΎΡ€."
368 368
369 369 #: templates/boards/settings.html:19
370 370 msgid "Hidden tags:"
371 371 msgstr "Π‘ΠΊΡ€Ρ‹Ρ‚Ρ‹Π΅ ΠΌΠ΅Ρ‚ΠΊΠΈ:"
372 372
373 373 #: templates/boards/settings.html:25
374 374 msgid "No hidden tags."
375 375 msgstr "НСт скрытых ΠΌΠ΅Ρ‚ΠΎΠΊ."
376 376
377 377 #: templates/boards/settings.html:34
378 378 msgid "Save"
379 379 msgstr "Π‘ΠΎΡ…Ρ€Π°Π½ΠΈΡ‚ΡŒ"
380 380
381 381 #: templates/boards/staticpages/banned.html:6
382 382 msgid "Banned"
383 383 msgstr "Π—Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Π½"
384 384
385 385 #: templates/boards/staticpages/banned.html:11
386 386 msgid "Your IP address has been banned. Contact the administrator"
387 387 msgstr "Π’Π°Ρˆ IP адрСс Π±Ρ‹Π» Π·Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Π½. Π‘Π²ΡΠΆΠΈΡ‚Π΅ΡΡŒ с администратором"
388 388
389 389 #: templates/boards/staticpages/help.html:6
390 390 #: templates/boards/staticpages/help.html:10
391 391 msgid "Syntax"
392 392 msgstr "Бинтаксис"
393 393
394 394 #: templates/boards/staticpages/help.html:11
395 395 msgid "Italic text"
396 396 msgstr "ΠšΡƒΡ€ΡΠΈΠ²Π½Ρ‹ΠΉ тСкст"
397 397
398 398 #: templates/boards/staticpages/help.html:12
399 399 msgid "Bold text"
400 400 msgstr "ΠŸΠΎΠ»ΡƒΠΆΠΈΡ€Π½Ρ‹ΠΉ тСкст"
401 401
402 402 #: templates/boards/staticpages/help.html:13
403 403 msgid "Spoiler"
404 404 msgstr "Π‘ΠΏΠΎΠΉΠ»Π΅Ρ€"
405 405
406 406 #: templates/boards/staticpages/help.html:14
407 407 msgid "Link to a post"
408 408 msgstr "Бсылка Π½Π° сообщСниС"
409 409
410 410 #: templates/boards/staticpages/help.html:15
411 411 msgid "Strikethrough text"
412 412 msgstr "Π—Π°Ρ‡Π΅Ρ€ΠΊΠ½ΡƒΡ‚Ρ‹ΠΉ тСкст"
413 413
414 414 #: templates/boards/staticpages/help.html:16
415 415 msgid "Comment"
416 416 msgstr "ΠšΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΉ"
417 417
418 418 #: templates/boards/staticpages/help.html:17
419 419 #: templates/boards/staticpages/help.html:18
420 420 msgid "Quote"
421 421 msgstr "Π¦ΠΈΡ‚Π°Ρ‚Π°"
422 422
423 423 #: templates/boards/staticpages/help.html:21
424 424 msgid "You can try pasting the text and previewing the result here:"
425 425 msgstr "Π’Ρ‹ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ ΠΏΠΎΠΏΡ€ΠΎΠ±ΠΎΠ²Π°Ρ‚ΡŒ Π²ΡΡ‚Π°Π²ΠΈΡ‚ΡŒ тСкст ΠΈ ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΈΡ‚ΡŒ Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚ здСсь:"
426 426
427 427 #: templates/boards/tags.html:17
428 428 msgid "Sections:"
429 429 msgstr "Π Π°Π·Π΄Π΅Π»Ρ‹:"
430 430
431 431 #: templates/boards/tags.html:30
432 432 msgid "Other tags:"
433 433 msgstr "Π”Ρ€ΡƒΠ³ΠΈΠ΅ ΠΌΠ΅Ρ‚ΠΊΠΈ:"
434 434
435 435 #: templates/boards/tags.html:43
436 436 msgid "All tags..."
437 437 msgstr "ВсС ΠΌΠ΅Ρ‚ΠΊΠΈ..."
438 438
439 439 #: templates/boards/thread.html:14
440 440 msgid "Normal"
441 441 msgstr "ΠΠΎΡ€ΠΌΠ°Π»ΡŒΠ½Ρ‹ΠΉ"
442 442
443 443 #: templates/boards/thread.html:15
444 444 msgid "Gallery"
445 445 msgstr "ГалСрСя"
446 446
447 447 #: templates/boards/thread.html:16
448 448 msgid "Tree"
449 449 msgstr "Π”Π΅Ρ€Π΅Π²ΠΎ"
450 450
451 451 #: templates/boards/thread.html:35
452 452 msgid "message"
453 453 msgid_plural "messages"
454 454 msgstr[0] "сообщСниС"
455 455 msgstr[1] "сообщСния"
456 456 msgstr[2] "сообщСний"
457 457
458 458 #: templates/boards/thread.html:38
459 459 msgid "image"
460 460 msgid_plural "images"
461 461 msgstr[0] "ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅"
462 462 msgstr[1] "изобраТСния"
463 463 msgstr[2] "ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ"
464 464
465 465 #: templates/boards/thread.html:40
466 466 msgid "Last update: "
467 467 msgstr "ПослСднСС обновлСниС: "
468 468
469 469 #: templates/boards/thread_gallery.html:36
470 470 msgid "No images."
471 471 msgstr "НСт ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ."
472 472
473 473 #: templates/boards/thread_normal.html:30
474 474 msgid "posts to bumplimit"
475 475 msgstr "сообщСний Π΄ΠΎ Π±Π°ΠΌΠΏΠ»ΠΈΠΌΠΈΡ‚Π°"
476 476
477 477 #: templates/boards/thread_normal.html:44
478 478 msgid "Reply to thread"
479 479 msgstr "ΠžΡ‚Π²Π΅Ρ‚ΠΈΡ‚ΡŒ Π² Ρ‚Π΅ΠΌΡƒ"
480 480
481 481 #: templates/boards/thread_normal.html:44
482 482 msgid "to message "
483 483 msgstr "Π½Π° сообщСниС"
484 484
485 485 #: templates/boards/thread_normal.html:59
486 486 msgid "Reset form"
487 487 msgstr "Π‘Π±Ρ€ΠΎΡΠΈΡ‚ΡŒ Ρ„ΠΎΡ€ΠΌΡƒ"
488 488
489 489 #: templates/search/search.html:17
490 490 msgid "Ok"
491 491 msgstr "Ок"
492 492
493 493 #: utils.py:120
494 494 #, python-format
495 495 msgid "File must be less than %s but is %s."
496 496 msgstr "Π€Π°ΠΉΠ» Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±Ρ‹Ρ‚ΡŒ ΠΌΠ΅Π½Π΅Π΅ %s, Π½ΠΎ Π΅Π³ΠΎ Ρ€Π°Π·ΠΌΠ΅Ρ€ %s."
497 497
498 498 msgid "Please wait %(delay)d second before sending message"
499 499 msgid_plural "Please wait %(delay)d seconds before sending message"
500 500 msgstr[0] "ΠŸΠΎΠΆΠ°Π»ΡƒΠΉΡΡ‚Π° ΠΏΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡ‚Π΅ %(delay)d сСкунду ΠΏΠ΅Ρ€Π΅Π΄ ΠΎΡ‚ΠΏΡ€Π°Π²ΠΊΠΎΠΉ сообщСния"
501 501 msgstr[1] "ΠŸΠΎΠΆΠ°Π»ΡƒΠΉΡΡ‚Π° ΠΏΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡ‚Π΅ %(delay)d сСкунды ΠΏΠ΅Ρ€Π΅Π΄ ΠΎΡ‚ΠΏΡ€Π°Π²ΠΊΠΎΠΉ сообщСния"
502 502 msgstr[2] "ΠŸΠΎΠΆΠ°Π»ΡƒΠΉΡΡ‚Π° ΠΏΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡ‚Π΅ %(delay)d сСкунд ΠΏΠ΅Ρ€Π΅Π΄ ΠΎΡ‚ΠΏΡ€Π°Π²ΠΊΠΎΠΉ сообщСния"
503 503
504 504 msgid "New threads"
505 505 msgstr "НовыС Ρ‚Π΅ΠΌΡ‹"
506 506
507 507 #, python-format
508 508 msgid "Max file size is %(size)s."
509 509 msgstr "ΠœΠ°ΠΊΡΠΈΠΌΠ°Π»ΡŒΠ½Ρ‹ΠΉ Ρ€Π°Π·ΠΌΠ΅Ρ€ Ρ„Π°ΠΉΠ»Π° %(size)s."
510 510
511 511 msgid "Size of media:"
512 512 msgstr "Π Π°Π·ΠΌΠ΅Ρ€ ΠΌΠ΅Π΄ΠΈΠ°:"
513 513
514 514 msgid "Statistics"
515 515 msgstr "Бтатистика"
516 516
517 517 msgid "Invalid PoW."
518 518 msgstr "НСвСрный PoW."
519 519
520 520 msgid "Stale PoW."
521 521 msgstr "PoW устарСл."
522 522
523 523 msgid "Show"
524 524 msgstr "ΠŸΠΎΠΊΠ°Π·Ρ‹Π²Π°Ρ‚ΡŒ"
525 525
526 526 msgid "Hide"
527 527 msgstr "Π‘ΠΊΡ€Ρ‹Π²Π°Ρ‚ΡŒ"
528 528
529 529 msgid "Add to favorites"
530 530 msgstr "Π”ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ Π² ΠΈΠ·Π±Ρ€Π°Π½Π½ΠΎΠ΅"
531 531
532 532 msgid "Remove from favorites"
533 533 msgstr "Π£Π±Ρ€Π°Ρ‚ΡŒ ΠΈΠ· ΠΈΠ·Π±Ρ€Π°Π½Π½ΠΎΠ³ΠΎ"
534 534
535 535 msgid "Monochrome"
536 536 msgstr "ΠœΠΎΠ½ΠΎΡ…Ρ€ΠΎΠΌΠ½Ρ‹ΠΉ"
537 537
538 538 msgid "Subsections: "
539 539 msgstr "ΠŸΠΎΠ΄Ρ€Π°Π·Π΄Π΅Π»Ρ‹: "
540 540
541 541 msgid "Change file source"
542 542 msgstr "Π˜Π·ΠΌΠ΅Π½ΠΈΡ‚ΡŒ источник Ρ„Π°ΠΉΠ»Π°"
543 543
544 544 msgid "interesting"
545 545 msgstr "интСрСсноС"
546 546
547 547 msgid "images"
548 548 msgstr "изобраТСния"
549 549
550 550 msgid "Delete post"
551 551 msgstr "Π£Π΄Π°Π»ΠΈΡ‚ΡŒ пост"
552 552
553 553 msgid "Delete thread"
554 554 msgstr "Π£Π΄Π°Π»ΠΈΡ‚ΡŒ Ρ‚Π΅ΠΌΡƒ"
555 555
556 556 msgid "Messages per day/week/month:"
557 557 msgstr "Π‘ΠΎΠΎΠ±Ρ‰Π΅Π½ΠΈΠΉ Π·Π° дСнь/нСдСлю/мСсяц:"
558 558
559 559 msgid "Subscribe to thread"
560 560 msgstr "ΠŸΠΎΠ΄ΠΏΠΈΡΠ°Ρ‚ΡŒΡΡ Π½Π° Ρ‚Π΅ΠΌΡƒ"
561 561
562 562 msgid "Active threads:"
563 563 msgstr "АктивныС Ρ‚Π΅ΠΌΡ‹:"
564 564
565 565 msgid "No active threads today."
566 566 msgstr "БСгодня Π½Π΅Ρ‚ Π°ΠΊΡ‚ΠΈΠ²Π½Ρ‹Ρ… Ρ‚Π΅ΠΌ."
567 567
568 568 msgid "Insert URLs on separate lines."
569 569 msgstr "ВставляйтС ссылки Π½Π° ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½Ρ‹Ρ… строках."
570 570
571 571 msgid "You can post no more than %(files)d file."
572 572 msgid_plural "You can post no more than %(files)d files."
573 573 msgstr[0] "Π’Ρ‹ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ ΠΎΡ‚ΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ Π½Π΅ Π±ΠΎΠ»Π΅Π΅ %(files)d Ρ„Π°ΠΉΠ»Π°."
574 574 msgstr[1] "Π’Ρ‹ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ ΠΎΡ‚ΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ Π½Π΅ Π±ΠΎΠ»Π΅Π΅ %(files)d Ρ„Π°ΠΉΠ»ΠΎΠ²."
575 575 msgstr[2] "Π’Ρ‹ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ ΠΎΡ‚ΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ Π½Π΅ Π±ΠΎΠ»Π΅Π΅ %(files)d Ρ„Π°ΠΉΠ»ΠΎΠ²."
576 576
577 577 #, python-format
578 578 msgid "Max file number is %(max_files)s."
579 579 msgstr "МаксимальноС количСство Ρ„Π°ΠΉΠ»ΠΎΠ² %(max_files)s."
580 580
581 581 msgid "Moderation"
582 582 msgstr "ΠœΠΎΠ΄Π΅Ρ€Π°Ρ†ΠΈΡ"
583 583
584 584 msgid "Check for duplicates"
585 585 msgstr "ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΡ‚ΡŒ Π½Π° Π΄ΡƒΠ±Π»ΠΈΠΊΠ°Ρ‚Ρ‹"
586 586
587 587 msgid "Some files are already present on the board."
588 588 msgstr "НСкоторыС Ρ„Π°ΠΉΠ»Ρ‹ ΡƒΠΆΠ΅ ΠΏΡ€ΠΈΡΡƒΡ‚ΡΡ‚Π²ΡƒΡŽΡ‚ Π½Π° Π±ΠΎΡ€Π΄Π΅."
589 589
590 590 msgid "Do not download URLs"
591 591 msgstr "НС Π·Π°Π³Ρ€ΡƒΠΆΠ°Ρ‚ΡŒ ссылки"
592 592
593 593 msgid "Ban and delete"
594 594 msgstr "Π—Π°Π±Π°Π½ΠΈΡ‚ΡŒ ΠΈ ΡƒΠ΄Π°Π»ΠΈΡ‚ΡŒ"
595 595
596 596 msgid "Are you sure?"
597 597 msgstr "Π’Ρ‹ ΡƒΠ²Π΅Ρ€Π΅Π½Ρ‹?"
598 598
599 599 msgid "Ban"
600 600 msgstr "Π—Π°Π±Π°Π½ΠΈΡ‚ΡŒ"
601 601
602 602 msgid "URL download mode"
603 603 msgstr "Π Π΅ΠΆΠΈΠΌ Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ ссылок"
604 604
605 605 msgid "Download or add URL"
606 606 msgstr "Π—Π°Π³Ρ€ΡƒΠ·ΠΈΡ‚ΡŒ ΠΈΠ»ΠΈ Π΄ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ ссылку"
607 607
608 608 msgid "Download or fail"
609 609 msgstr "Π—Π°Π³Ρ€ΡƒΠ·ΠΈΡ‚ΡŒ ΠΈΠ»ΠΈ ΠΎΡ‚ΠΊΠ°Π·Π°Ρ‚ΡŒ"
610 610
611 611 msgid "Insert as URLs"
612 612 msgstr "Π’ΡΡ‚Π°Π²Π»ΡΡ‚ΡŒ ΠΊΠ°ΠΊ ссылки"
613 613
614 614 msgid "Help"
615 615 msgstr "Π‘ΠΏΡ€Π°Π²ΠΊΠ°"
616 616
617 617 msgid "View available stickers:"
618 618 msgstr "ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ доступныС стикСры:"
619 619
620 620 msgid "Stickers"
621 621 msgstr "Π‘Ρ‚ΠΈΠΊΠ΅Ρ€Ρ‹"
622 622
623 623 msgid "Available by addresses:"
624 624 msgstr "Доступно ΠΏΠΎ адрСсам:"
625
626 msgid "Local stickers"
627 msgstr "Π›ΠΎΠΊΠ°Π»ΡŒΠ½Ρ‹Π΅ стикСры"
628
629 msgid "Global stickers"
630 msgstr "Π“Π»ΠΎΠ±Π°Π»ΡŒΠ½Ρ‹Π΅ стикСры"
631
632 msgid "Remove sticker"
633 msgstr "Π£Π΄Π°Π»ΠΈΡ‚ΡŒ стикСр"
1 NO CONTENT: modified file, binary diff hidden
@@ -1,61 +1,63 b''
1 1 # SOME DESCRIPTIVE TITLE.
2 2 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3 3 # This file is distributed under the same license as the PACKAGE package.
4 4 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
5 5 #
6 6 #, fuzzy
7 7 msgid ""
8 8 msgstr ""
9 9 "Project-Id-Version: PACKAGE VERSION\n"
10 10 "Report-Msgid-Bugs-To: \n"
11 11 "POT-Creation-Date: 2015-09-04 18:47+0300\n"
12 12 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
13 13 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
14 14 "Language-Team: LANGUAGE <LL@li.org>\n"
15 15 "Language: \n"
16 16 "MIME-Version: 1.0\n"
17 17 "Content-Type: text/plain; charset=UTF-8\n"
18 18 "Content-Transfer-Encoding: 8bit\n"
19 19 "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
20 20 "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
21 21
22 22 #: static/js/3party/jquery-ui.min.js:8
23 23 msgid "'"
24 24 msgstr ""
25 25
26 26 #: static/js/refpopup.js:72
27 27 msgid "Loading..."
28 28 msgstr "Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ°..."
29 29
30 30 #: static/js/refpopup.js:91
31 31 msgid "Post not found"
32 32 msgstr "Π‘ΠΎΠΎΠ±Ρ‰Π΅Π½ΠΈΠ΅ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½ΠΎ"
33 33
34 34 #: static/js/thread_update.js:261
35 35 msgid "message"
36 36 msgid_plural "messages"
37 37 msgstr[0] "сообщСниС"
38 38 msgstr[1] "сообщСния"
39 39 msgstr[2] "сообщСний"
40 40
41 41 #: static/js/thread_update.js:262
42 42 msgid "image"
43 43 msgid_plural "images"
44 44 msgstr[0] "ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅"
45 45 msgstr[1] "изобраТСния"
46 46 msgstr[2] "ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ"
47 47
48 48 #: static/js/thread_update.js:445
49 49 msgid "Sending message..."
50 50 msgstr "ΠžΡ‚ΠΏΡ€Π°Π²ΠΊΠ° сообщСния..."
51 51
52 52 #: static/js/thread_update.js:449
53 53 msgid "Server error!"
54 54 msgstr "Ошибка сСрвСра!"
55 55
56 56 msgid "Computing PoW..."
57 57 msgstr "Расчёт PoW..."
58 58
59 59 msgid "Duplicates search"
60 60 msgstr "Поиск Π΄ΡƒΠ±Π»ΠΈΠΊΠ°Ρ‚ΠΎΠ²"
61 61
62 msgid "Add local sticker"
63 msgstr "Π”ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ Π»ΠΎΠΊΠ°Π»ΡŒΠ½Ρ‹ΠΉ стикСр"
1 NO CONTENT: modified file, binary diff hidden
@@ -1,624 +1,633 b''
1 1 # SOME DESCRIPTIVE TITLE.
2 2 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3 3 # This file is distributed under the same license as the PACKAGE package.
4 4 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
5 5 #
6 6 msgid ""
7 7 msgstr ""
8 8 "Project-Id-Version: PACKAGE VERSION\n"
9 9 "Report-Msgid-Bugs-To: \n"
10 10 "POT-Creation-Date: 2015-10-09 23:21+0300\n"
11 11 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
12 12 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
13 13 "Language-Team: LANGUAGE <LL@li.org>\n"
14 14 "Language: ru\n"
15 15 "MIME-Version: 1.0\n"
16 16 "Content-Type: text/plain; charset=UTF-8\n"
17 17 "Content-Transfer-Encoding: 8bit\n"
18 18 "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
19 19 "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
20 20
21 21 #: admin.py:22
22 22 msgid "{} posters were banned"
23 23 msgstr "{} постСрів Π·Π°Π±Π»ΠΎΠΊΠΎΠ²Π°Π½ΠΎ"
24 24
25 25 #: authors.py:9
26 26 msgid "author"
27 27 msgstr "Π°Π²Ρ‚ΠΎΡ€"
28 28
29 29 #: authors.py:10
30 30 msgid "developer"
31 31 msgstr "Ρ€ΠΎΠ·Ρ€ΠΎΠ±Π½ΠΈΠΊ"
32 32
33 33 #: authors.py:11
34 34 msgid "javascript developer"
35 35 msgstr "javascript-Ρ€ΠΎΠ·Ρ€ΠΎΠ±Π½ΠΈΠΊ"
36 36
37 37 #: authors.py:12
38 38 msgid "designer"
39 39 msgstr "Π΄ΠΈΠ·Π°ΠΉΠ½Π΅Ρ€"
40 40
41 41 #: forms.py:30
42 42 msgid "Type message here. Use formatting panel for more advanced usage."
43 43 msgstr ""
44 44 "Π’Π²Π΅Π΄Ρ–Ρ‚ΡŒ сюди повідомлСння. ΠšΠΎΡ€ΠΈΡΡ‚Π°ΠΉΡ‚Π΅ панСль для ΡΠΊΠ»Π°Π΄Π½Ρ–ΡˆΠΎΠ³ΠΎ форматування."
45 45
46 46 #: forms.py:31
47 47 msgid "music images i_dont_like_tags"
48 48 msgstr "ΠΌΡƒΠ·ΠΈΠΊΠ° зобраТСння ΠΌΡ–Ρ‚ΠΊΠΈ_Π½Π΅_ΠΏΠΎΡ‚Ρ€Ρ–Π±Π½Ρ–"
49 49
50 50 #: forms.py:33
51 51 msgid "Title"
52 52 msgstr "Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ"
53 53
54 54 #: forms.py:34
55 55 msgid "Text"
56 56 msgstr "ВСкст"
57 57
58 58 #: forms.py:35
59 59 msgid "Tag"
60 60 msgstr "ΠœΡ–Ρ‚ΠΊΠ°"
61 61
62 62 #: forms.py:36 templates/boards/base.html:40 templates/search/search.html:7
63 63 msgid "Search"
64 64 msgstr "ΠŸΠΎΡˆΡƒΠΊ"
65 65
66 66 #: forms.py:48
67 67 msgid "File 1"
68 68 msgstr "Π€Π°ΠΉΠ» 1"
69 69
70 70 #: forms.py:48
71 71 msgid "File 2"
72 72 msgstr "Π€Π°ΠΉΠ» 2"
73 73
74 74 #: forms.py:142
75 75 msgid "File URL"
76 76 msgstr "URL Ρ„Π°ΠΉΠ»Ρƒ"
77 77
78 78 #: forms.py:148
79 79 msgid "e-mail"
80 80 msgstr ""
81 81
82 82 #: forms.py:151
83 83 msgid "Additional threads"
84 84 msgstr "Π”ΠΎΠ΄Π°Ρ‚ΠΊΠΎΠ²Ρ– Π½ΠΈΡ‚ΠΊΠΈ"
85 85
86 86 #: forms.py:162
87 87 #, python-format
88 88 msgid "Title must have less than %s characters"
89 89 msgstr "Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ ΠΌΠ°Ρ” містити мСншС %s символів"
90 90
91 91 #: forms.py:172
92 92 #, python-format
93 93 msgid "Text must have less than %s characters"
94 94 msgstr "ВСкст ΠΌΠ°Ρ” Π±ΡƒΡ‚ΠΈ ΠΊΠΎΡ€ΠΎΡ‚ΡˆΠ΅ %s символів"
95 95
96 96 #: forms.py:192
97 97 msgid "Invalid URL"
98 98 msgstr "Π₯ΠΈΠ±Π½ΠΈΠΉ URL"
99 99
100 100 #: forms.py:213
101 101 msgid "Invalid additional thread list"
102 102 msgstr "Π₯ΠΈΠ±Π½ΠΈΠΉ ΠΏΠ΅Ρ€Π΅Π»Ρ–ΠΊ Π΄ΠΎΠ΄Π°Ρ‚ΠΊΠΎΠ²ΠΈΡ… Π½ΠΈΡ‚ΠΎΠΊ"
103 103
104 104 #: forms.py:258
105 105 msgid "Either text or file must be entered."
106 106 msgstr "Π‘Π»Ρ–Π΄ Π΄ΠΎΠ΄Π°Ρ‚ΠΈ тСкст Π°Π±ΠΎ Ρ„Π°ΠΉΠ»."
107 107
108 108 #: forms.py:317 templates/boards/all_threads.html:153
109 109 #: templates/boards/rss/post.html:10 templates/boards/tags.html:6
110 110 msgid "Tags"
111 111 msgstr "ΠœΡ–Ρ‚ΠΊΠΈ"
112 112
113 113 #: forms.py:324
114 114 msgid "Inappropriate characters in tags."
115 115 msgstr "НСприйнятні символи Ρƒ ΠΌΡ–Ρ‚ΠΊΠ°Ρ…."
116 116
117 117 #: forms.py:344
118 118 msgid "Need at least one section."
119 119 msgstr "ΠœΡƒΡΠΈΡ‚ΡŒ Π±ΡƒΡ‚ΠΈ Ρ…ΠΎΡ‡Π° Π± ΠΎΠ΄ΠΈΠ½ Ρ€ΠΎΠ·Π΄Ρ–Π»."
120 120
121 121 #: forms.py:356
122 122 msgid "Theme"
123 123 msgstr "Π’Π΅ΠΌΠ°"
124 124
125 125 #: forms.py:357
126 126 msgid "Image view mode"
127 127 msgstr "Π Π΅ΠΆΠΈΠΌ пСрСгляду Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΡŒ"
128 128
129 129 #: forms.py:358
130 130 msgid "User name"
131 131 msgstr "Им'я користувача"
132 132
133 133 #: forms.py:359
134 134 msgid "Time zone"
135 135 msgstr "Часовий пояс"
136 136
137 137 #: forms.py:365
138 138 msgid "Inappropriate characters."
139 139 msgstr "НСприйнятні символи."
140 140
141 141 #: templates/boards/404.html:6
142 142 msgid "Not found"
143 143 msgstr "Загубилося"
144 144
145 145 #: templates/boards/404.html:12
146 146 msgid "This page does not exist"
147 147 msgstr "НСма ΠΏΡ€Π°Π²Π΄ΠΎΠ½ΡŒΠΊΠΈ Π½Π° світі, ΠΎΠΉ нСма…"
148 148
149 149 #: templates/boards/all_threads.html:35
150 150 msgid "Details"
151 151 msgstr "Π”Π΅Ρ‚Π°Π»Ρ–"
152 152
153 153 #: templates/boards/all_threads.html:69
154 154 msgid "Edit tag"
155 155 msgstr "Π—ΠΌΡ–Π½ΠΈΡ‚ΠΈ ΠΌΡ–Ρ‚ΠΊΡƒ"
156 156
157 157 #: templates/boards/all_threads.html:76
158 158 #, python-format
159 159 msgid "%(count)s active thread"
160 160 msgid_plural "%(count)s active threads"
161 161 msgstr[0] "%(count)s Π°ΠΊΡ‚ΠΈΠ²Π½Π° Π½ΠΈΡ‚ΠΊΠ°"
162 162 msgstr[1] "%(count)s Π°ΠΊΡ‚ΠΈΠ²Π½Ρ– Π½ΠΈΡ‚ΠΊΠΈ"
163 163 msgstr[2] "%(count)s Π°ΠΊΡ‚ΠΈΠ²Π½ΠΈΡ… Π½ΠΈΡ‚ΠΎΠΊ"
164 164
165 165 #: templates/boards/all_threads.html:76
166 166 #, python-format
167 167 msgid "%(count)s thread in bumplimit"
168 168 msgid_plural "%(count)s threads in bumplimit"
169 169 msgstr[0] "%(count)s Π½ΠΈΡ‚ΠΊΠ° Π² бампляматі"
170 170 msgstr[1] "%(count)s Π½ΠΈΡ‚ΠΊΠΈ Π² бампляматі"
171 171 msgstr[2] "%(count)s Π½ΠΈΡ‚ΠΎΠΊ Ρƒ бампляматі"
172 172
173 173 #: templates/boards/all_threads.html:77
174 174 #, python-format
175 175 msgid "%(count)s archived thread"
176 176 msgid_plural "%(count)s archived thread"
177 177 msgstr[0] "%(count)s Π°Ρ€Ρ…Ρ–Π²Π½Π° Π½ΠΈΡ‚ΠΊΠ°"
178 178 msgstr[1] "%(count)s Π°Ρ€Ρ…Ρ–Π²Π½Ρ– Π½ΠΈΡ‚ΠΊΠΈ"
179 179 msgstr[2] "%(count)s Π°Ρ€Ρ…Ρ–Π²Π½ΠΈΡ… Π½ΠΈΡ‚ΠΎΠΊ"
180 180
181 181 #: templates/boards/all_threads.html:78 templates/boards/post.html:102
182 182 #, python-format
183 183 #| msgid "%(count)s message"
184 184 #| msgid_plural "%(count)s messages"
185 185 msgid "%(count)s message"
186 186 msgid_plural "%(count)s messages"
187 187 msgstr[0] "%(count)s повідомлСння"
188 188 msgstr[1] "%(count)s повідомлСння"
189 189 msgstr[2] "%(count)s ΠΏΠΎΠ²Ρ–Π΄ΠΎΠΌΠ»Π΅Π½ΡŒ"
190 190
191 191 #: templates/boards/all_threads.html:95 templates/boards/feed.html:30
192 192 #: templates/boards/notifications.html:17 templates/search/search.html:26
193 193 msgid "Previous page"
194 194 msgstr "ΠŸΠΎΠΏΡ”Ρ€Ρ”Π΄Π½Ρ сторінка"
195 195
196 196 #: templates/boards/all_threads.html:109
197 197 #, python-format
198 198 msgid "Skipped %(count)s reply. Open thread to see all replies."
199 199 msgid_plural "Skipped %(count)s replies. Open thread to see all replies."
200 200 msgstr[0] "ΠŸΡ€ΠΎΠΏΡƒΡ‰Π΅Π½ΠΎ %(count)s Π²Ρ–Π΄ΠΏΠΎΠ²Ρ–Π΄ΡŒ. Π ΠΎΠ·Π³ΠΎΡ€Π½Ρ–Ρ‚ΡŒ Π½ΠΈΡ‚ΠΊΡƒ, Ρ‰ΠΎΠ± ΠΏΠΎΠ±Π°Ρ‡ΠΈΡ‚ΠΈ всі Π²Ρ–Π΄ΠΏΠΎΠ²Ρ–Π΄Ρ–."
201 201 msgstr[1] ""
202 202 "ΠŸΡ€ΠΎΠΏΡƒΡ‰Π΅Π½ΠΎ %(count)s Π²Ρ–Π΄ΠΏΠΎΠ²Ρ–Π΄Ρ–. Π ΠΎΠ·Π³ΠΎΡ€Π½Ρ–Ρ‚ΡŒ Π½ΠΈΡ‚ΠΊΡƒ, Ρ‰ΠΎΠ± ΠΏΠΎΠ±Π°Ρ‡ΠΈΡ‚ΠΈ всі Π²Ρ–Π΄ΠΏΠΎΠ²Ρ–Π΄Ρ–."
203 203 msgstr[2] ""
204 204 "ΠŸΡ€ΠΎΠΏΡƒΡ‰Π΅Π½ΠΎ %(count)s Π²Ρ–Π΄ΠΏΠΎΠ²Ρ–Π΄Π΅ΠΉ. Π ΠΎΠ·Π³ΠΎΡ€Π½Ρ–Ρ‚ΡŒ Π½ΠΈΡ‚ΠΊΡƒ, Ρ‰ΠΎΠ± ΠΏΠΎΠ±Π°Ρ‡ΠΈΡ‚ΠΈ всі Π²Ρ–Π΄ΠΏΠΎΠ²Ρ–Π΄Ρ–."
205 205
206 206 #: templates/boards/all_threads.html:127 templates/boards/feed.html:40
207 207 #: templates/boards/notifications.html:27 templates/search/search.html:37
208 208 msgid "Next page"
209 209 msgstr "Наступна сторінка"
210 210
211 211 #: templates/boards/all_threads.html:132
212 212 msgid "No threads exist. Create the first one!"
213 213 msgstr "НСма ΠΏΡ€Π°Π²Π΄ΠΎΠ½ΡŒΠΊΠΈ Π½Π° світі. Π—Π°Ρ‡Π½Ρ–ΠΌΠΎ Ρ—Ρ—!"
214 214
215 215 #: templates/boards/all_threads.html:138
216 216 msgid "Create new thread"
217 217 msgstr "БплСсти Π½ΠΎΠ²Ρƒ Π½ΠΈΡ‚ΠΊΡƒ"
218 218
219 219 #: templates/boards/all_threads.html:143 templates/boards/preview.html:16
220 220 #: templates/boards/thread_normal.html:51
221 221 msgid "Post"
222 222 msgstr "Надіслати"
223 223
224 224 #: templates/boards/all_threads.html:144 templates/boards/preview.html:6
225 225 #: templates/boards/staticpages/help.html:21
226 226 #: templates/boards/thread_normal.html:52
227 227 msgid "Preview"
228 228 msgstr "ΠŸΠΎΠΏΠ΅Ρ€Π΅Π³Π»ΡΠ΄"
229 229
230 230 #: templates/boards/all_threads.html:149
231 231 msgid "Tags must be delimited by spaces. Text or image is required."
232 232 msgstr ""
233 233 "ΠœΡ–Ρ‚ΠΊΠΈ Ρ€ΠΎΠ·ΠΌΠ΅ΠΆΡƒΠ²Π°Ρ‚ΠΈ ΠΏΡ€ΠΎΠ±Ρ–Π»Π°ΠΌΠΈ. ВСкст Ρ‡ΠΈ зобраТСння Ρ” ΠΎΠ±ΠΎΠ²'язковими."
234 234
235 235 #: templates/boards/all_threads.html:152 templates/boards/thread_normal.html:58
236 236 msgid "Text syntax"
237 237 msgstr "Бинтаксис тСксту"
238 238
239 239 #: templates/boards/all_threads.html:166 templates/boards/feed.html:53
240 240 msgid "Pages:"
241 241 msgstr "Π‘Ρ‚ΠΎΡ€Ρ–Π½ΠΊΠΈ:"
242 242
243 243 #: templates/boards/authors.html:6 templates/boards/authors.html.py:12
244 244 msgid "Authors"
245 245 msgstr "Автори"
246 246
247 247 #: templates/boards/authors.html:26
248 248 msgid "Distributed under the"
249 249 msgstr "Π ΠΎΠ·ΠΏΠΎΠ²ΡΡŽΠ΄ΠΆΡƒΡ”Ρ‚ΡŒΡΡ ΠΏΡ–Π΄ Π»Ρ–Ρ†Π΅Π½Π·Ρ–Ρ”ΡŽ"
250 250
251 251 #: templates/boards/authors.html:28
252 252 msgid "license"
253 253 msgstr ""
254 254
255 255 #: templates/boards/authors.html:30
256 256 msgid "Repository"
257 257 msgstr "Π Π΅ΠΏΠΎΠ·ΠΈΡ‚ΠΎΡ€Ρ–ΠΉ"
258 258
259 259 #: templates/boards/base.html:14 templates/boards/base.html.py:41
260 260 msgid "Feed"
261 261 msgstr "Π‘Ρ‚Ρ€Ρ–Ρ‡ΠΊΠ°"
262 262
263 263 #: templates/boards/base.html:31
264 264 msgid "All threads"
265 265 msgstr "Усі Π½ΠΈΡ‚ΠΊΠΈ"
266 266
267 267 #: templates/boards/base.html:37
268 268 msgid "Add tags"
269 269 msgstr "Π”ΠΎΠ΄Π°Ρ‚ΠΈ ΠΌΡ–Ρ‚ΠΊΠΈ"
270 270
271 271 #: templates/boards/base.html:39
272 272 msgid "Tag management"
273 273 msgstr "ΠšΠ΅Ρ€ΡƒΠ²Π°Π½Π½Ρ ΠΌΡ–Ρ‚ΠΊΠ°ΠΌΠΈ"
274 274
275 275 #: templates/boards/base.html:39
276 276 msgid "tags"
277 277 msgstr "ΠΌΡ–Ρ‚ΠΊΠΈ"
278 278
279 279 #: templates/boards/base.html:40
280 280 msgid "search"
281 281 msgstr "ΠΏΠΎΡˆΡƒΠΊ"
282 282
283 283 #: templates/boards/base.html:41 templates/boards/feed.html:11
284 284 msgid "feed"
285 285 msgstr "стрічка"
286 286
287 287 #: templates/boards/base.html:42 templates/boards/random.html:6
288 288 msgid "Random images"
289 289 msgstr "Π’ΠΈΠΏΠ°Π΄ΠΊΠΎΠ²Ρ– зобраТСння"
290 290
291 291 #: templates/boards/base.html:42
292 292 msgid "random"
293 293 msgstr "Π²ΠΈΠΏΠ°Π΄ΠΊΠΎΠ²Ρ–"
294 294
295 295 #: templates/boards/base.html:44
296 296 msgid "favorites"
297 297 msgstr "ΡƒΠ»ΡŽΠ±Π»Π΅Π½Π΅"
298 298
299 299 #: templates/boards/base.html:48 templates/boards/base.html.py:49
300 300 #: templates/boards/notifications.html:8
301 301 msgid "Notifications"
302 302 msgstr "БповіщСння"
303 303
304 304 #: templates/boards/base.html:56 templates/boards/settings.html:8
305 305 msgid "Settings"
306 306 msgstr "ΠΠ°Π»Π°ΡˆΡ‚ΡƒΠ²Π°Π½Π½Ρ"
307 307
308 308 #: templates/boards/base.html:59
309 309 msgid "Loading..."
310 310 msgstr "ЗавантаТСння..."
311 311
312 312 #: templates/boards/base.html:71
313 313 msgid "Admin"
314 314 msgstr "Адміністрування"
315 315
316 316 #: templates/boards/base.html:73
317 317 #, python-format
318 318 msgid "Speed: %(ppd)s posts per day"
319 319 msgstr "Π₯ΡƒΡ‚ΠΊΡ–ΡΡ‚ΡŒ: %(ppd)s ΠΏΠΎΠ²Ρ–Π΄ΠΎΠΌΠ»Π΅Π½ΡŒ Π½Π° дСнь"
320 320
321 321 #: templates/boards/base.html:75
322 322 msgid "Up"
323 323 msgstr "Π”ΠΎΠ³ΠΎΡ€ΠΈ"
324 324
325 325 #: templates/boards/feed.html:45
326 326 msgid "No posts exist. Create the first one!"
327 327 msgstr "Π©Π΅ Π½Π΅ΠΌΠ° ΠΏΠΎΠ²Ρ–Π΄ΠΎΠΌΠ»Π΅Π½ΡŒ. Π—Π°Ρ‡Π½Ρ–ΠΌΠΎ!"
328 328
329 329 #: templates/boards/post.html:33
330 330 msgid "Open"
331 331 msgstr "Π’Ρ–Π΄ΠΊΡ€ΠΈΡ‚ΠΈ"
332 332
333 333 #: templates/boards/post.html:35 templates/boards/post.html.py:46
334 334 msgid "Reply"
335 335 msgstr "Відповісти"
336 336
337 337 #: templates/boards/post.html:41
338 338 msgid " in "
339 339 msgstr " Ρƒ "
340 340
341 341 #: templates/boards/post.html:51
342 342 msgid "Edit"
343 343 msgstr "Π—ΠΌΡ–Π½ΠΈΡ‚ΠΈ"
344 344
345 345 #: templates/boards/post.html:53
346 346 msgid "Edit thread"
347 347 msgstr "Π—ΠΌΡ–Π½ΠΈΡ‚ΠΈ Π½ΠΈΡ‚ΠΊΡƒ"
348 348
349 349 #: templates/boards/post.html:91
350 350 msgid "Replies"
351 351 msgstr "Π’Ρ–Π΄ΠΏΠΎΠ²Ρ–Π΄Ρ–"
352 352
353 353 #: templates/boards/post.html:103
354 354 #, python-format
355 355 msgid "%(count)s image"
356 356 msgid_plural "%(count)s images"
357 357 msgstr[0] "%(count)s зобраТСння"
358 358 msgstr[1] "%(count)s зобраТСння"
359 359 msgstr[2] "%(count)s Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΡŒ"
360 360
361 361 #: templates/boards/rss/post.html:5
362 362 msgid "Post image"
363 363 msgstr "ЗобраТСння повідомлСння"
364 364
365 365 #: templates/boards/settings.html:15
366 366 msgid "You are moderator."
367 367 msgstr "Π’ΠΈ ΠΌΠΎΠ΄Π΅Ρ€Π°Ρ‚ΠΎΡ€."
368 368
369 369 #: templates/boards/settings.html:19
370 370 msgid "Hidden tags:"
371 371 msgstr "ΠŸΡ€ΠΈΡ…ΠΎΠ²Π°Π½Ρ– ΠΌΡ–Ρ‚ΠΊΠΈ:"
372 372
373 373 #: templates/boards/settings.html:25
374 374 msgid "No hidden tags."
375 375 msgstr "НСма ΠΏΡ€ΠΈΡ…ΠΎΠ²Π°Π½ΠΈΡ… ΠΌΡ–Ρ‚ΠΎΠΊ."
376 376
377 377 #: templates/boards/settings.html:34
378 378 msgid "Save"
379 379 msgstr "Π—Π±Π΅Ρ€Π΅Π³Ρ‚ΠΈ"
380 380
381 381 #: templates/boards/staticpages/banned.html:6
382 382 msgid "Banned"
383 383 msgstr "Π—Π°Π±Π»ΠΎΠΊΠΎΠ²Π°Π½ΠΎ"
384 384
385 385 #: templates/boards/staticpages/banned.html:11
386 386 msgid "Your IP address has been banned. Contact the administrator"
387 387 msgstr "Π’Π°ΡˆΡƒ IP-адрСсу Π·Π°Π±Π»ΠΎΠΊΠΎΠ²Π°Π½ΠΎ. Π—Π°Ρ‚Π΅Π»Π΅Ρ„ΠΎΠ½ΡƒΠΉΡ‚Π΅ Π΄ΠΎ спортлото"
388 388
389 389 #: templates/boards/staticpages/help.html:6
390 390 #: templates/boards/staticpages/help.html:10
391 391 msgid "Syntax"
392 392 msgstr "Бинтаксис"
393 393
394 394 #: templates/boards/staticpages/help.html:11
395 395 msgid "Italic text"
396 396 msgstr "ΠšΡƒΡ€ΡΠΈΠ²Π½ΠΈΠΉ тСкст"
397 397
398 398 #: templates/boards/staticpages/help.html:12
399 399 msgid "Bold text"
400 400 msgstr "Напівогрядний тСкст"
401 401
402 402 #: templates/boards/staticpages/help.html:13
403 403 msgid "Spoiler"
404 404 msgstr "Π‘ΠΏΠΎΠΉΠ»Π΅Ρ€"
405 405
406 406 #: templates/boards/staticpages/help.html:14
407 407 msgid "Link to a post"
408 408 msgstr "Посилання Π½Π° повідомлСння"
409 409
410 410 #: templates/boards/staticpages/help.html:15
411 411 msgid "Strikethrough text"
412 412 msgstr "ЗакрСслСний тСкст"
413 413
414 414 #: templates/boards/staticpages/help.html:16
415 415 msgid "Comment"
416 416 msgstr "ΠšΠΎΠΌΠ΅Π½Ρ‚Π°Ρ€"
417 417
418 418 #: templates/boards/staticpages/help.html:17
419 419 #: templates/boards/staticpages/help.html:18
420 420 msgid "Quote"
421 421 msgstr "Π¦ΠΈΡ‚Π°Ρ‚Π°"
422 422
423 423 #: templates/boards/staticpages/help.html:21
424 424 msgid "You can try pasting the text and previewing the result here:"
425 425 msgstr "ΠœΠΎΠΆΠ΅Ρ‚Π΅ спробувати вставити тСкст Ρ– ΠΏΠ΅Ρ€Π΅Π²Ρ–Ρ€ΠΈΡ‚ΠΈ Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚ Ρ‚ΡƒΡ‚:"
426 426
427 427 #: templates/boards/tags.html:17
428 428 msgid "Sections:"
429 429 msgstr "Π ΠΎΠ·Π΄Ρ–Π»ΠΈ:"
430 430
431 431 #: templates/boards/tags.html:30
432 432 msgid "Other tags:"
433 433 msgstr "Π†Π½ΡˆΡ– ΠΌΡ–Ρ‚ΠΊΠΈ:"
434 434
435 435 #: templates/boards/tags.html:43
436 436 msgid "All tags..."
437 437 msgstr "Усі ΠΌΡ–Ρ‚ΠΊΠΈ..."
438 438
439 439 #: templates/boards/thread.html:14
440 440 msgid "Normal"
441 441 msgstr "Π—Π²ΠΈΡ‡Π°ΠΉΠ½ΠΈΠΉ"
442 442
443 443 #: templates/boards/thread.html:15
444 444 msgid "Gallery"
445 445 msgstr "ГалСрСя"
446 446
447 447 #: templates/boards/thread.html:16
448 448 msgid "Tree"
449 449 msgstr "Π’Ρ–Π½ΠΈΠΊ"
450 450
451 451 #: templates/boards/thread.html:35
452 452 msgid "message"
453 453 msgid_plural "messages"
454 454 msgstr[0] "повідомлСння"
455 455 msgstr[1] "повідомлСння"
456 456 msgstr[2] "ΠΏΠΎΠ²Ρ–Π΄ΠΎΠΌΠ»Π΅Π½ΡŒ"
457 457
458 458 #: templates/boards/thread.html:38
459 459 msgid "image"
460 460 msgid_plural "images"
461 461 msgstr[0] "зобраТСння"
462 462 msgstr[1] "зобраТСння"
463 463 msgstr[2] "Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΡŒ"
464 464
465 465 #: templates/boards/thread.html:40
466 466 msgid "Last update: "
467 467 msgstr "ΠžΡΡ‚Π°Π½Π½Ρ” оновлСння: "
468 468
469 469 #: templates/boards/thread_gallery.html:36
470 470 msgid "No images."
471 471 msgstr "НСма Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΡŒ."
472 472
473 473 #: templates/boards/thread_normal.html:30
474 474 msgid "posts to bumplimit"
475 475 msgstr "ΠΏΠΎΠ²Ρ–Π΄ΠΎΠΌΠ»Π΅Π½ΡŒ Π΄ΠΎ бамплямату"
476 476
477 477 #: templates/boards/thread_normal.html:44
478 478 msgid "Reply to thread"
479 479 msgstr "Відповісти Π΄ΠΎ Π½ΠΈΡ‚ΠΊΠΈ"
480 480
481 481 #: templates/boards/thread_normal.html:44
482 482 msgid "to message "
483 483 msgstr "Π½Π° повідомлСння"
484 484
485 485 #: templates/boards/thread_normal.html:59
486 486 msgid "Reset form"
487 487 msgstr "Π‘ΠΊΠΈΠ½ΡƒΡ‚ΠΈ Ρ„ΠΎΡ€ΠΌΡƒ"
488 488
489 489 #: templates/search/search.html:17
490 490 msgid "Ok"
491 491 msgstr "Π€Π°ΠΉΠ½ΠΎ"
492 492
493 493 #: utils.py:120
494 494 #, python-format
495 495 msgid "File must be less than %s but is %s."
496 496 msgstr "Π€Π°ΠΉΠ» ΠΌΡƒΡΠΈΡ‚ΡŒ Π±ΡƒΡ‚ΠΈ мСншС %s, Π°Π»Π΅ ΠΉΠΎΠ³ΠΎ Ρ€ΠΎΠ·ΠΌΡ–Ρ€ %s."
497 497
498 498 msgid "Please wait %(delay)d second before sending message"
499 499 msgid_plural "Please wait %(delay)d seconds before sending message"
500 500 msgstr[0] "Π—Π°Ρ‡Π΅ΠΊΠ°ΠΉΡ‚Π΅, Π±ΡƒΠ΄ΡŒ ласка, %(delay)d сСкунду ΠΏΠ΅Ρ€Π΅Π΄ надсиланням повідомлСння"
501 501 msgstr[1] "Π—Π°Ρ‡Π΅ΠΊΠ°ΠΉΡ‚Π΅, Π±ΡƒΠ΄ΡŒ ласка, %(delay)d сСкунди ΠΏΠ΅Ρ€Π΅Π΄ надсиланням повідомлСння"
502 502 msgstr[2] "Π—Π°Ρ‡Π΅ΠΊΠ°ΠΉΡ‚Π΅, Π±ΡƒΠ΄ΡŒ ласка, %(delay)d сСкунд ΠΏΠ΅Ρ€Π΅Π΄ надсиланням повідомлСння"
503 503
504 504 msgid "New threads"
505 505 msgstr "Нові Π½ΠΈΡ‚ΠΊΠΈ"
506 506
507 507 #, python-format
508 508 msgid "Max file size is %(size)s."
509 509 msgstr "Максимальний Ρ€ΠΎΠ·ΠΌΡ–Ρ€ Ρ„Π°ΠΉΠ»Ρƒ %(size)s."
510 510
511 511 msgid "Size of media:"
512 512 msgstr "Π ΠΎΠ·ΠΌΡ–Ρ€ посСрСдника:"
513 513
514 514 msgid "Statistics"
515 515 msgstr "Бтатистика"
516 516
517 517 msgid "Invalid PoW."
518 518 msgstr "Π₯ΠΈΠ±Π½ΠΈΠΉ PoW."
519 519
520 520 msgid "Stale PoW."
521 521 msgstr "PoW застарів."
522 522
523 523 msgid "Show"
524 524 msgstr "ΠŸΠΎΠΊΠ°Π·ΡƒΠ²Π°Ρ‚ΠΈ"
525 525
526 526 msgid "Hide"
527 527 msgstr "Π₯ΠΎΠ²Π°Ρ‚ΠΈ"
528 528
529 529 msgid "Add to favorites"
530 530 msgstr "Π― Ρ†Π΅ люблю"
531 531
532 532 msgid "Remove from favorites"
533 533 msgstr "Π’ΠΆΠ΅ Π½Π΅ люблю"
534 534
535 535 msgid "Monochrome"
536 536 msgstr "Π‘Π΅Π· Π±Π°Ρ€Π²"
537 537
538 538 msgid "Subsections: "
539 539 msgstr "ΠŸΡ–Π΄Ρ€ΠΎΠ·Π΄Ρ–Π»ΠΈ: "
540 540
541 541 msgid "Change file source"
542 542 msgstr "Π—ΠΌΡ–Π½ΠΈΡ‚ΠΈ Π΄ΠΆΠ΅Ρ€Π΅Π»ΠΎ Ρ„Π°ΠΉΠ»Ρƒ"
543 543
544 544 msgid "interesting"
545 545 msgstr "Ρ†Ρ–ΠΊΠ°Π²Π΅"
546 546
547 547 msgid "images"
548 548 msgstr "ΠΏΡ–Ρ‡ΠΊΡƒΡ€ΠΈ"
549 549
550 550 msgid "Delete post"
551 551 msgstr "Π’ΠΈΠ΄Π°Π»ΠΈΡ‚ΠΈ повідомлСння"
552 552
553 553 msgid "Delete thread"
554 554 msgstr "Π’ΠΈΡ€Π²Π°Ρ‚ΠΈ Π½ΠΈΡ‚ΠΊΡƒ"
555 555
556 556 msgid "Messages per day/week/month:"
557 557 msgstr "ΠŸΠΎΠ²Ρ–Π΄ΠΎΠΌΠ»Π΅Π½ΡŒ Π·Π° дСнь/Ρ‚ΠΈΠΆΠ΄Π΅Π½ΡŒ/Ρ‚ΠΈΠΆΠΌΡ–ΡΡΡ†ΡŒ:"
558 558
559 559 msgid "Subscribe to thread"
560 560 msgstr "Π‘Ρ‚Π΅ΠΆΠΈΡ‚ΠΈ Π·Π° Π½ΠΈΡ‚ΠΊΠΎΡŽ"
561 561
562 562 msgid "Active threads:"
563 563 msgstr "Активні Π½ΠΈΡ‚ΠΊΠΈ:"
564 564
565 565 msgid "No active threads today."
566 566 msgstr "Щось усі Π·Π°ΠΌΠΎΠ²ΠΊΠ»ΠΈ."
567 567
568 568 msgid "Insert URLs on separate lines."
569 569 msgstr "ВставляйтС посилання ΠΎΠΊΡ€Π΅ΠΌΠΈΠΌΠΈ рядками."
570 570
571 571 msgid "You can post no more than %(files)d file."
572 572 msgid_plural "You can post no more than %(files)d files."
573 573 msgstr[0] "Π’ΠΈ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ надіслати Π½Π΅ Π±Ρ–Π»ΡŒΡˆΠ΅ %(files)d Ρ„Π°ΠΉΠ»Ρƒ."
574 574 msgstr[1] "Π’Ρ‹ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ надіслати Π½Π΅ Π±Ρ–Π»ΡŒΡˆΠ΅ %(files)d Ρ„Π°ΠΉΠ»Ρ–Π²."
575 575 msgstr[2] "Π’Ρ‹ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ надіслати Π½Π΅ Π±Ρ–Π»ΡŒΡˆΠ΅ %(files)d Ρ„Π°ΠΉΠ»Ρ–Π²."
576 576
577 577 #, python-format
578 578 msgid "Max file number is %(max_files)s."
579 579 msgstr "Максимальна ΠΊΡ–Π»ΡŒΠΊΡ–ΡΡ‚ΡŒ Ρ„Π°ΠΉΠ»Ρ–Π² %(max_files)s."
580 580
581 581 msgid "Moderation"
582 582 msgstr "ΠœΠΎΠ΄Π΅Ρ€Π°Ρ†Ρ–Ρ"
583 583
584 584 msgid "Check for duplicates"
585 585 msgstr "ΠŸΠ΅Ρ€Π΅Π²Ρ–Ρ€ΡΡ‚ΠΈ Π½Π° Π΄ΡƒΠ±Π»Ρ–ΠΊΠ°Ρ‚ΠΈ"
586 586
587 587 msgid "Some files are already present on the board."
588 588 msgstr "ДСякі Ρ„Π°ΠΉΠ»ΠΈ Π²ΠΆΠ΅ Ρ” Π½Π° Π΄ΠΎΡˆΡ†Ρ–."
589 589
590 590 msgid "Do not download URLs"
591 591 msgstr "НС Π·Π°Π²Π°Π½Ρ‚Π°ΠΆΡƒΠ²Π°Ρ‚ΠΈ посилання"
592 592
593 593 msgid "Ban and delete"
594 594 msgstr "Π—Π°Π±Π»ΠΎΠΊΡƒΠ²Π°Ρ‚ΠΈ ΠΉ Π²ΠΈΠ΄Π°Π»ΠΈΡ‚ΠΈ"
595 595
596 596 msgid "Are you sure?"
597 597 msgstr "Π§ΠΈ Π²ΠΈ ΠΏΠ΅Π²Π½Ρ–?"
598 598
599 599 msgid "Ban"
600 600 msgstr "Π—Π°Π±Π»ΠΎΠΊΡƒΠ²Π°Ρ‚ΠΈ"
601 601
602 602 msgid "URL download mode"
603 603 msgstr "Π Π΅ΠΆΠΈΠΌ завантаТСння посилань"
604 604
605 605 msgid "Download or add URL"
606 606 msgstr "Π—Π°Π²Π°Π½Ρ‚Π°ΠΆΠΈΡ‚ΠΈ Π°Π±ΠΎ Π΄ΠΎΠ΄Π°Ρ‚ΠΈ посилання"
607 607
608 608 msgid "Download or fail"
609 609 msgstr "Π—Π°Π²Π°Π½Ρ‚Π°ΠΆΠΈΡ‚ΠΈ Π°Π±ΠΎ Π²Ρ–Π΄ΠΌΠΎΠ²ΠΈΡ‚ΠΈ"
610 610
611 611 msgid "Insert as URLs"
612 612 msgstr "Вставляти як посилання"
613 613
614 614 msgid "Help"
615 615 msgstr "Π‘ΠΏΡ€Π°Π²ΠΊΠ°"
616 616
617 617 msgid "View available stickers:"
618 618 msgstr "ΠŸΠ΅Ρ€Π΅Π΄ΠΈΠ²ΠΈΡ‚ΠΈΡΡ доступні стікСри:"
619 619
620 620 msgid "Stickers"
621 621 msgstr "Π‘Ρ‚Ρ–ΠΊΠ΅Ρ€ΠΈ"
622 622
623 623 msgid "Available by addresses:"
624 624 msgstr "Доступно Π·Π° адрСсами:"
625
626 msgid "Local stickers"
627 msgstr "Π›ΠΎΠΊΠ°Π»ΡŒΠ½Ρ– стікСри"
628
629 msgid "Global stickers"
630 msgstr "Π“Π»ΠΎΠ±Π°Π»ΡŒΠ½Ρ– стікСри"
631
632 msgid "Remove sticker"
633 msgstr "Π’ΠΈΠ΄Π°Π»ΠΈΡ‚ΠΈ стікСр"
1 NO CONTENT: modified file, binary diff hidden
@@ -1,61 +1,63 b''
1 1 # SOME DESCRIPTIVE TITLE.
2 2 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3 3 # This file is distributed under the same license as the PACKAGE package.
4 4 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
5 5 #
6 6 #, fuzzy
7 7 msgid ""
8 8 msgstr ""
9 9 "Project-Id-Version: PACKAGE VERSION\n"
10 10 "Report-Msgid-Bugs-To: \n"
11 11 "POT-Creation-Date: 2015-09-04 18:47+0300\n"
12 12 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
13 13 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
14 14 "Language-Team: LANGUAGE <LL@li.org>\n"
15 15 "Language: \n"
16 16 "MIME-Version: 1.0\n"
17 17 "Content-Type: text/plain; charset=UTF-8\n"
18 18 "Content-Transfer-Encoding: 8bit\n"
19 19 "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
20 20 "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
21 21
22 22 #: static/js/3party/jquery-ui.min.js:8
23 23 msgid "'"
24 24 msgstr ""
25 25
26 26 #: static/js/refpopup.js:72
27 27 msgid "Loading..."
28 28 msgstr "ЗавантаТСння..."
29 29
30 30 #: static/js/refpopup.js:91
31 31 msgid "Post not found"
32 32 msgstr "ΠŸΠΎΠ²Ρ–Π΄ΠΎΠΌΠ»Π΅Π½Π½Ρ Π½Π΅ Π·Π½Π°ΠΉΠ΄Π΅Π½Π΅"
33 33
34 34 #: static/js/thread_update.js:261
35 35 msgid "message"
36 36 msgid_plural "messages"
37 37 msgstr[0] "повідомлСння"
38 38 msgstr[1] "повідомлСння"
39 39 msgstr[2] "ΠΏΠΎΠ²Ρ–Π΄ΠΎΠΌΠ»Π΅Π½ΡŒ"
40 40
41 41 #: static/js/thread_update.js:262
42 42 msgid "image"
43 43 msgid_plural "images"
44 44 msgstr[0] "зобраТСння"
45 45 msgstr[1] "зобраТСння"
46 46 msgstr[2] "Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΡŒ"
47 47
48 48 #: static/js/thread_update.js:445
49 49 msgid "Sending message..."
50 50 msgstr "ΠŸΠΎΠ²Ρ–Π΄ΠΎΠΌΠ»Π΅Π½Π½Ρ Π½Π°Π΄ΡΠΈΠ»Π°Ρ”Ρ‚ΡŒΡΡ..."
51 51
52 52 #: static/js/thread_update.js:449
53 53 msgid "Server error!"
54 54 msgstr "Π‘Π΅Ρ€Π²Π΅Ρ€ Π½Π΅Π·Π΄ΡƒΠΆΠ°Ρ”! Π—Π°Ρ…ΠΎΠ΄ΡŒΡ‚Π΅ ΠΏΡ–Π·Π½Ρ–ΡˆΠ΅!"
55 55
56 56 msgid "Computing PoW..."
57 57 msgstr "Π ΠΎΠ·Ρ€Π°Ρ…ΠΎΠ²ΡƒΡ”Ρ‚ΡŒΡΡ PoW..."
58 58
59 59 msgid "Duplicates search"
60 60 msgstr "ΠŸΠΎΡˆΡƒΠΊ Π΄ΡƒΠ±Π»Ρ–ΠΊΠ°Ρ‚Ρ–Π²"
61 61
62 msgid "Add local sticker"
63 msgstr "Π”ΠΎΠ΄Π°Ρ‚ΠΈ локальний стікСр"
@@ -1,171 +1,171 b''
1 1 from itertools import zip_longest
2 2
3 3 import boards
4 4 from boards.models import STATUS_ARCHIVE
5 5 from django.core.files.images import get_image_dimensions
6 6 from django.db import models
7 7
8 8 from boards import utils
9 9 from boards.models.attachment.viewers import get_viewers, AbstractViewer, \
10 10 FILE_TYPES_IMAGE
11 11 from boards.utils import get_upload_filename, get_extension, cached_result, \
12 12 get_file_mimetype
13 13
14 14
15 15 class AttachmentManager(models.Manager):
16 16 def create_with_hash(self, file):
17 17 file_hash = utils.get_file_hash(file)
18 18 attachment = self.get_existing_duplicate(file_hash, file)
19 19 if not attachment:
20 20 file_type = get_file_mimetype(file)
21 21 attachment = self.create(file=file, mimetype=file_type,
22 22 hash=file_hash)
23 23
24 24 return attachment
25 25
26 26 def create_from_url(self, url):
27 27 existing = self.filter(url=url)
28 28 if len(existing) > 0:
29 29 attachment = existing[0]
30 30 else:
31 31 attachment = self.create(url=url)
32 32 return attachment
33 33
34 34 def get_random_images(self, count, tags=None):
35 35 images = self.filter(mimetype__in=FILE_TYPES_IMAGE).exclude(
36 36 attachment_posts__thread__status=STATUS_ARCHIVE)
37 37 if tags is not None:
38 38 images = images.filter(attachment_posts__threads__tags__in=tags)
39 39 return images.order_by('?')[:count]
40 40
41 41 def get_existing_duplicate(self, file_hash, file):
42 42 """
43 43 Gets an attachment with the same file if one exists.
44 44 """
45 45 existing = self.filter(hash=file_hash)
46 46 attachment = None
47 47 for existing_attachment in existing:
48 48 existing_file = existing_attachment.file
49 49
50 50 file_chunks = file.chunks()
51 51 existing_file_chunks = existing_file.chunks()
52 52
53 53 if self._compare_chunks(file_chunks, existing_file_chunks):
54 54 attachment = existing_attachment
55 55 return attachment
56 56
57 57 def get_by_alias(self, name):
58 58 try:
59 59 return AttachmentSticker.objects.get(name=name).attachment
60 60 except AttachmentSticker.DoesNotExist:
61 61 return None
62 62
63 63 def _compare_chunks(self, chunks1, chunks2):
64 64 """
65 65 Compares 2 chunks of different sizes (e.g. first chunk array contains
66 66 all data in 1 chunk, and other one -- in a multiple of smaller ones.
67 67 """
68 68 equal = True
69 69
70 70 position1 = 0
71 71 position2 = 0
72 72 chunk1 = None
73 73 chunk2 = None
74 74 chunk1ended = False
75 75 chunk2ended = False
76 76 while True:
77 77 if not chunk1 or len(chunk1) <= position1:
78 78 try:
79 79 chunk1 = chunks1.__next__()
80 80 position1 = 0
81 81 except StopIteration:
82 82 chunk1ended = True
83 83 if not chunk2 or len(chunk2) <= position2:
84 84 try:
85 85 chunk2 = chunks2.__next__()
86 86 position2 = 0
87 87 except StopIteration:
88 88 chunk2ended = True
89 89
90 90 if chunk1ended and chunk2ended:
91 91 # Same size chunksm checked for equality previously
92 92 break
93 93 elif chunk1ended or chunk2ended:
94 94 # Different size chunks, not equal
95 95 equal = False
96 96 break
97 97 elif chunk1[position1] != chunk2[position2]:
98 98 # Different bytes, not equal
99 99 equal = False
100 100 break
101 101 else:
102 102 position1 += 1
103 103 position2 += 1
104 104 return equal
105 105
106 106
107 107 class Attachment(models.Model):
108 108 objects = AttachmentManager()
109 109
110 110 class Meta:
111 111 app_label = 'boards'
112 112 ordering = ('id',)
113 113
114 114 file = models.FileField(upload_to=get_upload_filename, null=True)
115 115 mimetype = models.CharField(max_length=200, null=True)
116 116 hash = models.CharField(max_length=36, null=True)
117 117 url = models.TextField(blank=True, default='')
118 118
119 119 def get_view(self):
120 120 file_viewer = None
121 121 for viewer in get_viewers():
122 122 if viewer.supports(self.mimetype):
123 123 file_viewer = viewer
124 124 break
125 125 if file_viewer is None:
126 126 file_viewer = AbstractViewer
127 127
128 return file_viewer(self.file, self.mimetype, self.hash, self.url).get_view()
128 return file_viewer(self.file, self.mimetype, self.id, self.url).get_view()
129 129
130 130 def __str__(self):
131 131 return self.url or self.file.url
132 132
133 133 def get_random_associated_post(self):
134 134 posts = boards.models.Post.objects.filter(attachments__in=[self])
135 135 return posts.order_by('?').first()
136 136
137 137 @cached_result()
138 138 def get_size(self):
139 139 if self.file:
140 140 if self.mimetype in FILE_TYPES_IMAGE:
141 141 return get_image_dimensions(self.file)
142 142 else:
143 143 return 200, 150
144 144
145 145 def get_thumb_url(self):
146 146 split = self.file.url.rsplit('.', 1)
147 147 w, h = 200, 150
148 148 return '%s.%sx%s.%s' % (split[0], w, h, split[1])
149 149
150 150 @cached_result()
151 151 def get_preview_size(self):
152 152 size = 200, 150
153 153 if self.mimetype in FILE_TYPES_IMAGE:
154 154 preview_path = self.file.path.replace('.', '.200x150.')
155 155 try:
156 156 size = get_image_dimensions(preview_path)
157 157 except Exception:
158 158 pass
159 159
160 160 return size
161 161
162 162 def is_internal(self):
163 163 return self.url is None or len(self.url) == 0
164 164
165 165
166 166 class AttachmentSticker(models.Model):
167 167 attachment = models.ForeignKey('Attachment')
168 168 name = models.TextField(unique=True)
169 169
170 170 def __str__(self):
171 171 return self.name
@@ -1,244 +1,244 b''
1 1 import re
2 2
3 3 from PIL import Image
4 4
5 5 from django.contrib.staticfiles import finders
6 6 from django.contrib.staticfiles.templatetags.staticfiles import static
7 7 from django.core.files.images import get_image_dimensions
8 8 from django.template.defaultfilters import filesizeformat
9 9 from django.core.urlresolvers import reverse
10 10 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
11 11
12 12 from boards.utils import get_domain, cached_result, get_extension
13 13 from boards import settings
14 14
15 15
16 16 FILE_STUB_IMAGE = 'images/file.png'
17 17 FILE_STUB_URL = 'url'
18 18 FILE_FILEFORMAT = 'images/fileformats/{}.png'
19 19
20 20
21 21 FILE_TYPES_VIDEO = (
22 22 'video/webm',
23 23 'video/mp4',
24 24 'video/mpeg',
25 25 'video/ogv',
26 26 )
27 27 FILE_TYPE_SVG = 'image/svg+xml'
28 28 FILE_TYPES_AUDIO = (
29 29 'audio/ogg',
30 30 'audio/mpeg',
31 31 'audio/opus',
32 32 'audio/x-flac',
33 33 'audio/mpeg',
34 34 )
35 35 FILE_TYPES_IMAGE = (
36 36 'image/jpeg',
37 37 'image/jpg',
38 38 'image/png',
39 39 'image/bmp',
40 40 'image/gif',
41 41 )
42 42
43 43 PLAIN_FILE_FORMATS = {
44 44 'zip': 'archive',
45 45 'tar': 'archive',
46 46 'gz': 'archive',
47 47 'mid' : 'midi',
48 48 }
49 49
50 50 URL_PROTOCOLS = {
51 51 'magnet': 'magnet',
52 52 }
53 53
54 54 CSS_CLASS_IMAGE = 'image'
55 55 CSS_CLASS_THUMB = 'thumb'
56 56
57 57 ABSTRACT_VIEW = '<div class="image">'\
58 58 '{}'\
59 59 '<div class="image-metadata"><a href="{}" download >{}, {}</a>'\
60 ' <a class="file-menu" href="#" data-type="{}" data-search-url="{}" data-filename="{}">πŸ” </a></div>'\
60 ' <a class="file-menu" href="#" data-type="{}" data-search-url="{}" data-filename="{}" data-id="{}">πŸ” </a></div>'\
61 61 '</div>'
62 62 URL_VIEW = '<div class="image">' \
63 63 '{}' \
64 64 '<div class="image-metadata">{}</div>' \
65 65 '</div>'
66 66 ABSTRACT_FORMAT_VIEW = '<a href="{}">'\
67 67 '<img class="url-image" src="{}" width="{}" height="{}"/>'\
68 68 '</a>'
69 69 VIDEO_FORMAT_VIEW = '<video width="200" height="150" controls src="{}"></video>'
70 70 AUDIO_FORMAT_VIEW = '<audio controls src="{}"></audio>'
71 71 IMAGE_FORMAT_VIEW = '<a class="{}" href="{full}">' \
72 72 '<img class="post-image-preview"' \
73 73 ' src="{}"' \
74 74 ' alt="{}"' \
75 75 ' width="{}"' \
76 76 ' height="{}"' \
77 77 ' data-width="{}"' \
78 78 ' data-height="{}" />' \
79 79 '</a>'
80 80 SVG_FORMAT_VIEW = '<a class="thumb" href="{}">'\
81 81 '<img class="post-image-preview" width="200" height="150" src="{}" />'\
82 82 '</a>'
83 83 URL_FORMAT_VIEW = '<a href="{}">' \
84 84 '<img class="url-image" src="{}" width="{}" height="{}"/>' \
85 85 '</a>'
86 86
87 87
88 88 def get_viewers():
89 89 return AbstractViewer.__subclasses__()
90 90
91 91
92 92 def get_static_dimensions(filename):
93 93 file_path = finders.find(filename)
94 94 return get_image_dimensions(file_path)
95 95
96 96
97 97 # TODO Move this to utils
98 98 def file_exists(filename):
99 99 return finders.find(filename) is not None
100 100
101 101
102 102 class AbstractViewer:
103 def __init__(self, file, file_type, hash, url):
103 def __init__(self, file, file_type, id, url):
104 104 self.file = file
105 105 self.file_type = file_type
106 self.hash = hash
106 self.id = id
107 107 self.url = url
108 108 self.extension = get_extension(self.file.name)
109 109
110 110 @staticmethod
111 111 def supports(file_type):
112 112 return True
113 113
114 114 def get_view(self):
115 115 search_host = settings.get('External', 'ImageSearchHost')
116 116 if search_host:
117 117 if search_host.endswith('/'):
118 118 search_host = search_host[:-1]
119 119 search_url = search_host + self.file.url
120 120 else:
121 121 search_url = ''
122 122
123 123 return ABSTRACT_VIEW.format(self.get_format_view(), self.file.url,
124 124 self.file_type, filesizeformat(self.file.size),
125 self.file_type, search_url, self.file.name)
125 self.file_type, search_url, self.file.name, self.id)
126 126
127 127 def get_format_view(self):
128 128 image_name = PLAIN_FILE_FORMATS.get(self.extension, self.extension)
129 129 file_name = FILE_FILEFORMAT.format(image_name)
130 130
131 131 if file_exists(file_name):
132 132 image = file_name
133 133 else:
134 134 image = FILE_STUB_IMAGE
135 135
136 136 w, h = get_static_dimensions(image)
137 137
138 138 return ABSTRACT_FORMAT_VIEW.format(self.file.url, static(image), w, h)
139 139
140 140
141 141 class VideoViewer(AbstractViewer):
142 142 @staticmethod
143 143 def supports(file_type):
144 144 return file_type in FILE_TYPES_VIDEO
145 145
146 146 def get_format_view(self):
147 147 return VIDEO_FORMAT_VIEW.format(self.file.url)
148 148
149 149
150 150 class AudioViewer(AbstractViewer):
151 151 @staticmethod
152 152 def supports(file_type):
153 153 return file_type in FILE_TYPES_AUDIO
154 154
155 155 def get_format_view(self):
156 156 return AUDIO_FORMAT_VIEW.format(self.file.url)
157 157
158 158
159 159 class SvgViewer(AbstractViewer):
160 160 @staticmethod
161 161 def supports(file_type):
162 162 return file_type == FILE_TYPE_SVG
163 163
164 164 def get_format_view(self):
165 165 return SVG_FORMAT_VIEW.format(self.file.url, self.file.url)
166 166
167 167
168 168 class ImageViewer(AbstractViewer):
169 169 @staticmethod
170 170 def supports(file_type):
171 171 return file_type in FILE_TYPES_IMAGE
172 172
173 173 def get_format_view(self):
174 174 metadata = '{}, {}'.format(self.file.name.split('.')[-1],
175 175 filesizeformat(self.file.size))
176 176
177 177 try:
178 178 width, height = get_image_dimensions(self.file.path)
179 179 except Exception:
180 180 # If the image is a decompression bomb, treat it as just a regular
181 181 # file
182 182 return super().get_format_view()
183 183
184 184 preview_path = self.file.path.replace('.', '.200x150.')
185 185 try:
186 186 pre_width, pre_height = get_image_dimensions(preview_path)
187 187 except Exception:
188 188 return super().get_format_view()
189 189
190 190 split = self.file.url.rsplit('.', 1)
191 191 w, h = 200, 150
192 192 thumb_url = '%s.%sx%s.%s' % (split[0], w, h, split[1])
193 193
194 194 return IMAGE_FORMAT_VIEW.format(CSS_CLASS_THUMB,
195 195 thumb_url,
196 self.hash,
196 self.id,
197 197 str(pre_width),
198 198 str(pre_height), str(width), str(height),
199 199 full=self.file.url, image_meta=metadata)
200 200
201 201
202 202 class UrlViewer(AbstractViewer):
203 203 @staticmethod
204 204 def supports(file_type):
205 205 return file_type is None
206 206
207 207 def get_view(self):
208 208 return URL_VIEW.format(self.get_format_view(), get_domain(self.url))
209 209
210 210 def get_format_view(self):
211 211 protocol = self.url.split(':')[0]
212 212
213 213 domain = get_domain(self.url)
214 214
215 215 if protocol in URL_PROTOCOLS:
216 216 url_image_name = URL_PROTOCOLS.get(protocol)
217 217 elif domain:
218 218 url_image_name = self._find_image_for_domains(domain) or FILE_STUB_URL
219 219 else:
220 220 url_image_name = FILE_STUB_URL
221 221
222 222 image_path = 'images/{}.png'.format(url_image_name)
223 223 image = static(image_path)
224 224 w, h = get_static_dimensions(image_path)
225 225
226 226 return URL_FORMAT_VIEW.format(self.url, image, w, h)
227 227
228 228 @cached_result()
229 229 def _find_image_for_domains(self, domain):
230 230 """
231 231 Searches for the domain image for every domain level except top.
232 232 E.g. for l3.example.co.uk it will search for l3.example.co.uk, then
233 233 example.co.uk, then co.uk
234 234 """
235 235 levels = domain.split('.')
236 236 while len(levels) > 1:
237 237 domain = '.'.join(levels)
238 238
239 239 filename = 'images/domains/{}.png'.format(domain)
240 240 if file_exists(filename):
241 241 return 'domains/' + domain
242 242 else:
243 243 del levels[0]
244 244
@@ -1,220 +1,230 b''
1 1 /*
2 2 @licstart The following is the entire license notice for the
3 3 JavaScript code in this page.
4 4
5 5
6 6 Copyright (C) 2013 neko259
7 7
8 8 The JavaScript code in this page is free software: you can
9 9 redistribute it and/or modify it under the terms of the GNU
10 10 General Public License (GNU GPL) as published by the Free Software
11 11 Foundation, either version 3 of the License, or (at your option)
12 12 any later version. The code is distributed WITHOUT ANY WARRANTY;
13 13 without even the implied warranty of MERCHANTABILITY or FITNESS
14 14 FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
15 15
16 16 As additional permission under GNU GPL version 3 section 7, you
17 17 may distribute non-source (e.g., minimized or compacted) forms of
18 18 that code without the copy of the GNU GPL normally required by
19 19 section 4, provided you include this license notice and a URL
20 20 through which recipients can access the Corresponding Source.
21 21
22 22 @licend The above is the entire license notice
23 23 for the JavaScript code in this page.
24 24 */
25 25
26 26 var ITEM_VOLUME_LEVEL = 'volumeLevel';
27 27 var IMAGE_TYPES = ['image/png', 'image/jpg', 'image/jpeg', 'image/bmp', 'image/gif'];
28 28
29 29 /**
30 30 * An email is a hidden file to prevent spam bots from posting. It has to be
31 31 * hidden.
32 32 */
33 33 function hideEmailFromForm() {
34 34 $('.form-email').parent().parent().hide();
35 35 }
36 36
37 37 /**
38 38 * Highlight code blocks with code highlighter
39 39 */
40 40 function highlightCode(node) {
41 41 node.find('pre code').each(function(i, e) {
42 42 hljs.highlightBlock(e);
43 43 });
44 44 }
45 45
46 46 function updateFavPosts(data) {
47 47 var includePostBody = $('#fav-panel').is(":visible");
48 48
49 49 var allNewPostCount = 0;
50 50
51 51 if (includePostBody) {
52 52 var favoriteThreadPanel = $('#fav-panel');
53 53 favoriteThreadPanel.empty();
54 54 }
55 55
56 56 $.each($.parseJSON(data), function (_, dict) {
57 57 var newPostCount = dict.new_post_count;
58 58 allNewPostCount += newPostCount;
59 59
60 60 if (includePostBody) {
61 61 var favThreadNode = $('<div class="post"></div>');
62 62 favThreadNode.append($(dict.post_url));
63 63 favThreadNode.append(' ');
64 64 favThreadNode.append($('<span class="title">' + dict.title + '</span>'));
65 65
66 66 if (newPostCount > 0) {
67 67 favThreadNode.append(' (<a href="' + dict.newest_post_link + '">+' + newPostCount + "</a>)");
68 68 }
69 69
70 70 favoriteThreadPanel.append(favThreadNode);
71 71
72 72 addRefLinkPreview(favThreadNode[0]);
73 73 }
74 74 });
75 75
76 76 var newPostCountNode = $('#new-fav-post-count');
77 77 if (allNewPostCount > 0) {
78 78 newPostCountNode.text('(+' + allNewPostCount + ')');
79 79 newPostCountNode.show();
80 80 } else {
81 81 newPostCountNode.hide();
82 82 }
83 83 }
84 84
85 85 function initFavPanel() {
86 86 var favPanelButton = $('#fav-panel-btn');
87 87 if (favPanelButton.length > 0 && typeof SharedWorker != 'undefined') {
88 88 var worker = new SharedWorker($('body').attr('data-update-script'));
89 89 worker.port.onmessage = function(e) {
90 90 updateFavPosts(e.data);
91 91 };
92 92 worker.onerror = function(event){
93 93 throw new Error(event.message + " (" + event.filename + ":" + event.lineno + ")");
94 94 };
95 95 worker.port.start();
96 96
97 97 $(favPanelButton).click(function() {
98 98 var favPanel = $('#fav-panel');
99 99 favPanel.toggle();
100 100
101 101 worker.port.postMessage({ includePostBody: favPanel.is(':visible')});
102 102
103 103 return false;
104 104 });
105 105
106 106 $(document).on('keyup.removepic', function(e) {
107 107 if(e.which === 27) {
108 108 $('#fav-panel').hide();
109 109 }
110 110 });
111 111 }
112 112 }
113 113
114 114 function setVolumeLevel(level) {
115 115 localStorage.setItem(ITEM_VOLUME_LEVEL, level);
116 116 }
117 117
118 118 function getVolumeLevel() {
119 119 var level = localStorage.getItem(ITEM_VOLUME_LEVEL);
120 120 if (level == null) {
121 121 level = 1.0;
122 122 }
123 123 return level
124 124 }
125 125
126 126 function processVolumeUser(node) {
127 127 if (!window.localStorage) return;
128 128 node.prop("volume", getVolumeLevel());
129 129 node.on('volumechange', function(event) {
130 130 setVolumeLevel(event.target.volume);
131 131 $("video,audio").prop("volume", getVolumeLevel());
132 132 });
133 133 }
134 134
135 135 /**
136 136 * Add all scripts than need to work on post, when the post is added to the
137 137 * document.
138 138 */
139 139 function addScriptsToPost(post) {
140 140 addRefLinkPreview(post[0]);
141 141 highlightCode(post);
142 142 processVolumeUser(post.find("video,audio"));
143 143 }
144 144
145 145 /**
146 146 * Fix compatibility issues with some rare browsers
147 147 */
148 148 function compatibilityCrutches() {
149 149 if (window.operamini) {
150 150 $('#form textarea').each(function() { this.placeholder = ''; });
151 151 }
152 152 }
153 153
154 154 function addContextMenu() {
155 155 $.contextMenu({
156 156 selector: '.file-menu',
157 157 trigger: 'left',
158 158
159 159 build: function($trigger, e) {
160 160 var fileSearchUrl = $trigger.data('search-url');
161 161 var isImage = IMAGE_TYPES.indexOf($trigger.data('type')) > -1;
162 162 var hasUrl = fileSearchUrl.length > 0;
163 var id = $trigger.data('id');
163 164 return {
164 165 items: {
165 166 duplicates: {
166 167 name: gettext('Duplicates search'),
167 168 callback: function(key, opts) {
168 169 window.location = '/feed/?image=' + $trigger.data('filename');
169 170 }
170 171 },
171 172 google: {
172 173 name: 'Google',
173 174 visible: isImage && hasUrl,
174 175 callback: function(key, opts) {
175 176 window.location = 'https://www.google.com/searchbyimage?image_url=' + fileSearchUrl;
176 177 }
177 178 },
178 179 iqdb: {
179 180 name: 'IQDB',
180 181 visible: isImage && hasUrl,
181 182 callback: function(key, opts) {
182 183 window.location = 'http://iqdb.org/?url=' + fileSearchUrl;
183 184 }
184 185 },
185 186 tineye: {
186 187 name: 'TinEye',
187 188 visible: isImage && hasUrl,
188 189 callback: function(key, opts) {
189 190 window.location = 'http://tineye.com/search?url=' + fileSearchUrl;
190 191 }
192 },
193 addAlias: {
194 name: gettext('Add local sticker'),
195 callback: function(key, opts) {
196 var alias = prompt(gettext('Input sticker name'));
197 if (alias) {
198 window.location = '/stickers/?action=add&name=' + alias + '&id=' + id;
199 }
200 }
191 201 }
192 },
202 }
193 203 };
194 204 }
195 205 });
196 206 }
197 207
198 208 $( document ).ready(function() {
199 209 hideEmailFromForm();
200 210
201 211 $("a[href='#top']").click(function() {
202 212 $("html, body").animate({ scrollTop: 0 }, "slow");
203 213 return false;
204 214 });
205 215
206 216 addImgPreview();
207 217
208 218 addRefLinkPreview();
209 219
210 220 highlightCode($(document));
211 221
212 222 initFavPanel();
213 223
214 224 var volumeUsers = $("video,audio");
215 225 processVolumeUser(volumeUsers);
216 226
217 227 addContextMenu();
218 228
219 229 compatibilityCrutches();
220 230 });
@@ -1,20 +1,33 b''
1 1 {% extends "boards/base.html" %}
2 2
3 3 {% load i18n %}
4 4 {% load tz %}
5 5
6 6 {% block head %}
7 7 <meta name="robots" content="noindex">
8 8 <title>{% trans "Stickers" %} - {{ site_name }}</title>
9 9 {% endblock %}
10 10
11 11 {% block content %}
12 12 <div id="posts-table">
13 {% for sticker in stickers %}
14 <div class="gallery_image">
15 {{ sticker.attachment.get_view|safe }}
16 <div>{{ sticker.name }}</div>
17 </div>
18 {% endfor %}
13 {% if local_stickers %}
14 <h1>{% trans "Local stickers" %}</h1>
15 {% for sticker in local_stickers %}
16 <div class="gallery_image">
17 {{ sticker.attachment.get_view|safe }}
18 <div>{{ sticker.name }}</div>
19 <div><a href="?action=remove&name={{ sticker.name }}">{% trans "Remove sticker" %}</a></div>
20 </div>
21 {% endfor %}
22 {% endif %}
23 {% if global_stickers %}
24 <h1>{% trans "Global stickers" %}</h1>
25 {% for sticker in global_stickers %}
26 <div class="gallery_image">
27 {{ sticker.attachment.get_view|safe }}
28 <div>{{ sticker.name }}</div>
29 </div>
30 {% endfor %}
31 {% endif %}
19 32 </div>
20 33 {% endblock %}
@@ -1,40 +1,41 b''
1 1 {% extends "boards/base.html" %}
2 2
3 3 {% load i18n %}
4 4 {% load tz %}
5 5
6 6 {% block head %}
7 7 <meta name="robots" content="noindex">
8 8 <title>{% trans 'Settings' %} - {{ site_name }}</title>
9 9 {% endblock %}
10 10
11 11 {% block content %}
12 12 <div id="posts-table">
13 13 <p>
14 14 {% if moderator %}
15 15 {% trans 'You are moderator.' %}
16 16 {% endif %}
17 17 </p>
18 18 {% if hidden_tags %}
19 19 <p>{% trans 'Hidden tags:' %}
20 20 {% for tag in hidden_tags %}
21 21 {{ tag.get_view|safe }}
22 22 {% endfor %}
23 23 </p>
24 24 {% else %}
25 25 <p>{% trans 'No hidden tags.' %}</p>
26 26 {% endif %}
27 <p><a href="{% url 'stickers' %}">{% trans 'Stickers' %}</a></p>
27 28 </div>
28 29
29 30 <div class="post-form-w">
30 31 <div class="post-form">
31 32 <form method="post">{% csrf_token %}
32 33 {{ form.as_div }}
33 34 <div class="form-submit">
34 35 <input type="submit" value="{% trans "Save" %}" />
35 36 </div>
36 37 </form>
37 38 </div>
38 39 </div>
39 40
40 41 {% endblock %}
@@ -1,315 +1,317 b''
1 1 import json
2 2 import logging
3 3
4 4 from django.core import serializers
5 5 from django.db import transaction
6 6 from django.http import HttpResponse, HttpResponseBadRequest
7 7 from django.shortcuts import get_object_or_404
8 8 from django.views.decorators.csrf import csrf_protect
9 9
10 10 from boards.abstracts.settingsmanager import get_settings_manager
11 11 from boards.forms import PostForm, PlainErrorList
12 12 from boards.mdx_neboard import Parser
13 13 from boards.models import Post, Thread, Tag, Attachment, TagAlias
14 14 from boards.models.attachment import AttachmentSticker
15 15 from boards.models.thread import STATUS_ARCHIVE
16 16 from boards.models.user import Notification
17 17 from boards.utils import datetime_to_epoch
18 18 from boards.views.thread import ThreadView
19 19 from boards.models.attachment.viewers import FILE_TYPES_IMAGE
20 20
21 21 __author__ = 'neko259'
22 22
23 23 PARAMETER_TRUNCATED = 'truncated'
24 24 PARAMETER_TAG = 'tag'
25 25 PARAMETER_OFFSET = 'offset'
26 26 PARAMETER_DIFF_TYPE = 'type'
27 27 PARAMETER_POST = 'post'
28 28 PARAMETER_UPDATED = 'updated'
29 29 PARAMETER_LAST_UPDATE = 'last_update'
30 30 PARAMETER_THREAD = 'thread'
31 31 PARAMETER_UIDS = 'uids'
32 32 PARAMETER_SUBSCRIBED = 'subscribed'
33 33
34 34 DIFF_TYPE_HTML = 'html'
35 35 DIFF_TYPE_JSON = 'json'
36 36
37 37 STATUS_OK = 'ok'
38 38 STATUS_ERROR = 'error'
39 39
40 40 logger = logging.getLogger(__name__)
41 41
42 42
43 43 @transaction.atomic
44 44 def api_get_threaddiff(request):
45 45 """
46 46 Gets posts that were changed or added since time
47 47 """
48 48
49 49 thread_id = request.POST.get(PARAMETER_THREAD)
50 50 uids_str = request.POST.get(PARAMETER_UIDS)
51 51
52 52 if not thread_id or not uids_str:
53 53 return HttpResponse(content='Invalid request.')
54 54
55 55 uids = uids_str.strip().split(' ')
56 56
57 57 opening_post = get_object_or_404(Post, id=thread_id)
58 58 thread = opening_post.get_thread()
59 59
60 60 json_data = {
61 61 PARAMETER_UPDATED: [],
62 62 PARAMETER_LAST_UPDATE: None, # TODO Maybe this can be removed already?
63 63 }
64 64 posts = Post.objects.filter(thread=thread).exclude(uid__in=uids)
65 65
66 66 diff_type = request.GET.get(PARAMETER_DIFF_TYPE, DIFF_TYPE_HTML)
67 67
68 68 for post in posts:
69 69 json_data[PARAMETER_UPDATED].append(post.get_post_data(
70 70 format_type=diff_type, request=request))
71 71 json_data[PARAMETER_LAST_UPDATE] = str(thread.last_edit_time)
72 72
73 73 settings_manager = get_settings_manager(request)
74 74 json_data[PARAMETER_SUBSCRIBED] = str(settings_manager.thread_is_fav(opening_post))
75 75
76 76 # If the tag is favorite, update the counter
77 77 settings_manager = get_settings_manager(request)
78 78 favorite = settings_manager.thread_is_fav(opening_post)
79 79 if favorite:
80 80 settings_manager.add_or_read_fav_thread(opening_post)
81 81
82 82 return HttpResponse(content=json.dumps(json_data))
83 83
84 84
85 85 @csrf_protect
86 86 def api_add_post(request, opening_post_id):
87 87 """
88 88 Adds a post and return the JSON response for it
89 89 """
90 90
91 91 opening_post = get_object_or_404(Post, id=opening_post_id)
92 92
93 93 logger.info('Adding post via api...')
94 94
95 95 status = STATUS_OK
96 96 errors = []
97 97
98 98 if request.method == 'POST':
99 99 form = PostForm(request.POST, request.FILES, error_class=PlainErrorList)
100 100 form.session = request.session
101 101
102 102 if form.need_to_ban:
103 103 # Ban user because he is suspected to be a bot
104 104 # _ban_current_user(request)
105 105 status = STATUS_ERROR
106 106 if form.is_valid():
107 107 post = ThreadView().new_post(request, form, opening_post,
108 108 html_response=False)
109 109 if not post:
110 110 status = STATUS_ERROR
111 111 else:
112 112 logger.info('Added post #%d via api.' % post.id)
113 113 else:
114 114 status = STATUS_ERROR
115 115 errors = form.as_json_errors()
116 116
117 117 response = {
118 118 'status': status,
119 119 'errors': errors,
120 120 }
121 121
122 122 return HttpResponse(content=json.dumps(response))
123 123
124 124
125 125 def get_post(request, post_id):
126 126 """
127 127 Gets the html of a post. Used for popups. Post can be truncated if used
128 128 in threads list with 'truncated' get parameter.
129 129 """
130 130
131 131 post = get_object_or_404(Post, id=post_id)
132 132 truncated = PARAMETER_TRUNCATED in request.GET
133 133
134 134 return HttpResponse(content=post.get_view(truncated=truncated, need_op_data=True))
135 135
136 136
137 137 def api_get_threads(request, count):
138 138 """
139 139 Gets the JSON thread opening posts list.
140 140 Parameters that can be used for filtering:
141 141 tag, offset (from which thread to get results)
142 142 """
143 143
144 144 if PARAMETER_TAG in request.GET:
145 145 tag_name = request.GET[PARAMETER_TAG]
146 146 if tag_name is not None:
147 147 tag = get_object_or_404(Tag, name=tag_name)
148 148 threads = tag.get_threads().exclude(status=STATUS_ARCHIVE)
149 149 else:
150 150 threads = Thread.objects.exclude(status=STATUS_ARCHIVE)
151 151
152 152 if PARAMETER_OFFSET in request.GET:
153 153 offset = request.GET[PARAMETER_OFFSET]
154 154 offset = int(offset) if offset is not None else 0
155 155 else:
156 156 offset = 0
157 157
158 158 threads = threads.order_by('-bump_time')
159 159 threads = threads[offset:offset + int(count)]
160 160
161 161 opening_posts = []
162 162 for thread in threads:
163 163 opening_post = thread.get_opening_post()
164 164
165 165 # TODO Add tags, replies and images count
166 166 post_data = opening_post.get_post_data(include_last_update=True)
167 167 post_data['status'] = thread.get_status()
168 168
169 169 opening_posts.append(post_data)
170 170
171 171 return HttpResponse(content=json.dumps(opening_posts))
172 172
173 173
174 174 # TODO Test this
175 175 def api_get_tags(request):
176 176 """
177 177 Gets all tags or user tags.
178 178 """
179 179
180 180 # TODO Get favorite tags for the given user ID
181 181
182 182 tags = TagAlias.objects.all()
183 183
184 184 term = request.GET.get('term')
185 185 if term is not None:
186 186 tags = tags.filter(name__contains=term)
187 187
188 188 tag_names = [tag.name for tag in tags]
189 189
190 190 return HttpResponse(content=json.dumps(tag_names))
191 191
192 192
193 193 def api_get_stickers(request):
194 194 term = request.GET.get('term')
195 195 if not term:
196 196 return HttpResponseBadRequest()
197 197
198 stickers = AttachmentSticker.objects.filter(name__contains=term)
198 global_stickers = AttachmentSticker.objects.filter(name__contains=term)
199 local_stickers = [sticker for sticker in get_settings_manager(request).get_stickers() if term in sticker.name]
200 stickers = list(global_stickers) + local_stickers
199 201
200 202 image_dict = [{'thumb': sticker.attachment.get_thumb_url(),
201 203 'alias': sticker.name}
202 204 for sticker in stickers]
203 205
204 206 return HttpResponse(content=json.dumps(image_dict))
205 207
206 208
207 209 # TODO The result can be cached by the thread last update time
208 210 # TODO Test this
209 211 def api_get_thread_posts(request, opening_post_id):
210 212 """
211 213 Gets the JSON array of thread posts
212 214 """
213 215
214 216 opening_post = get_object_or_404(Post, id=opening_post_id)
215 217 thread = opening_post.get_thread()
216 218 posts = thread.get_replies()
217 219
218 220 json_data = {
219 221 'posts': [],
220 222 'last_update': None,
221 223 }
222 224 json_post_list = []
223 225
224 226 for post in posts:
225 227 json_post_list.append(post.get_post_data())
226 228 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
227 229 json_data['posts'] = json_post_list
228 230
229 231 return HttpResponse(content=json.dumps(json_data))
230 232
231 233
232 234 def api_get_notifications(request, username):
233 235 last_notification_id_str = request.GET.get('last', None)
234 236 last_id = int(last_notification_id_str) if last_notification_id_str is not None else None
235 237
236 238 posts = Notification.objects.get_notification_posts(usernames=[username],
237 239 last=last_id)
238 240
239 241 json_post_list = []
240 242 for post in posts:
241 243 json_post_list.append(post.get_post_data())
242 244 return HttpResponse(content=json.dumps(json_post_list))
243 245
244 246
245 247 def api_get_post(request, post_id):
246 248 """
247 249 Gets the JSON of a post. This can be
248 250 used as and API for external clients.
249 251 """
250 252
251 253 post = get_object_or_404(Post, id=post_id)
252 254
253 255 json = serializers.serialize("json", [post], fields=(
254 256 "pub_time", "_text_rendered", "title", "text", "image",
255 257 "image_width", "image_height", "replies", "tags"
256 258 ))
257 259
258 260 return HttpResponse(content=json)
259 261
260 262
261 263 def api_get_preview(request):
262 264 raw_text = request.POST['raw_text']
263 265
264 266 parser = Parser()
265 267 return HttpResponse(content=parser.parse(parser.preparse(raw_text)))
266 268
267 269
268 270 def api_get_new_posts(request):
269 271 """
270 272 Gets favorite threads and unread posts count.
271 273 """
272 274 posts = list()
273 275
274 276 include_posts = 'include_posts' in request.GET
275 277
276 278 settings_manager = get_settings_manager(request)
277 279 fav_threads = settings_manager.get_fav_threads()
278 280 fav_thread_ops = Post.objects.filter(id__in=fav_threads.keys())\
279 281 .order_by('-pub_time').prefetch_related('thread')
280 282
281 283 ops = [{'op': op, 'last_id': fav_threads[str(op.id)]} for op in fav_thread_ops]
282 284 if include_posts:
283 285 new_post_threads = Thread.objects.get_new_posts(ops)
284 286 if new_post_threads:
285 287 thread_ids = {thread.id: thread for thread in new_post_threads}
286 288 else:
287 289 thread_ids = dict()
288 290
289 291 for op in fav_thread_ops:
290 292 fav_thread_dict = dict()
291 293
292 294 op_thread = op.get_thread()
293 295 if op_thread.id in thread_ids:
294 296 thread = thread_ids[op_thread.id]
295 297 new_post_count = thread.new_post_count
296 298 fav_thread_dict['newest_post_link'] = thread.get_replies()\
297 299 .filter(id__gt=fav_threads[str(op.id)])\
298 300 .first().get_absolute_url(thread=thread)
299 301 else:
300 302 new_post_count = 0
301 303 fav_thread_dict['new_post_count'] = new_post_count
302 304
303 305 fav_thread_dict['id'] = op.id
304 306
305 307 fav_thread_dict['post_url'] = op.get_link_view()
306 308 fav_thread_dict['title'] = op.title
307 309
308 310 posts.append(fav_thread_dict)
309 311 else:
310 312 fav_thread_dict = dict()
311 313 fav_thread_dict['new_post_count'] = \
312 314 Thread.objects.get_new_post_count(ops)
313 315 posts.append(fav_thread_dict)
314 316
315 317 return HttpResponse(content=json.dumps(posts))
@@ -1,25 +1,45 b''
1 from django.shortcuts import render
1 from django.shortcuts import render, redirect
2 2 from django.utils.decorators import method_decorator
3 3 from django.views.decorators.csrf import csrf_protect
4 4
5 from boards.models.attachment import AttachmentSticker
5 from boards.abstracts.settingsmanager import get_settings_manager
6 from boards.models.attachment import AttachmentSticker, Attachment
6 7 from boards.views.base import BaseBoardView
7 8
8 CONTEXT_STICKERS = 'stickers'
9 CONTEXT_GLOBAL_STICKERS = 'global_stickers'
10 CONTEXT_LOCAL_STICKERS = 'local_stickers'
9 11
10 12 TEMPLATE = 'boards/aliases.html'
11 13
12 14
13 15 class AliasesView(BaseBoardView):
14 16 @method_decorator(csrf_protect)
15 17 def get(self, request, category=None):
18 result = self._process_creation(request)
19 if result:
20 return result
21
16 22 params = dict()
17 23
18 24 if category:
19 params[CONTEXT_STICKERS] = AttachmentSticker.objects.filter(
25 params[CONTEXT_GLOBAL_STICKERS] = AttachmentSticker.objects.filter(
20 26 name__startswith=(category + '/'))
21 27 else:
22 params[CONTEXT_STICKERS] = AttachmentSticker.objects.all()
28 params[CONTEXT_GLOBAL_STICKERS] = AttachmentSticker.objects.all()
29 params[CONTEXT_LOCAL_STICKERS] = get_settings_manager(request).get_stickers()
23 30
24 31 return render(request, TEMPLATE, params)
25 32
33 def _process_creation(self, request):
34 action = request.GET.get('action')
35 if action == 'add' and 'name' in request.GET and 'id' in request.GET:
36 name = request.GET['name']
37 id = request.GET['id']
38 attachment = Attachment.objects.get(id=id)
39 get_settings_manager(request).add_attachment_alias(name, attachment)
40
41 return redirect('stickers')
42 if action == 'remove' and 'name' in request.GET:
43 name = request.GET['name']
44 get_settings_manager(request).remove_attachment_alias(name)
45 return redirect('stickers')
General Comments 0
You need to be logged in to leave comments. Login now