##// END OF EJS Templates
Show posts per
neko259 -
r407:33667f74 default
parent child Browse files
Show More
1 NO CONTENT: modified file, binary diff hidden
@@ -1,371 +1,376 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 "POT-Creation-Date: 2013-11-09 21:41+0200\n"
10 "POT-Creation-Date: 2013-11-24 16:57+0200\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 #: authors.py:5
22 22 msgid "author"
23 23 msgstr "автор"
24 24
25 25 #: authors.py:6
26 26 msgid "developer"
27 27 msgstr "разработчик"
28 28
29 29 #: authors.py:7
30 30 msgid "javascript developer"
31 31 msgstr "разработчик javascript"
32 32
33 33 #: authors.py:8
34 34 msgid "designer"
35 35 msgstr "дизайнер"
36 36
37 #: forms.py:47 templates/boards/posting_general.html:184
38 #: templates/boards/thread.html:103
37 #: forms.py:48 templates/boards/posting_general.html:206
38 #: templates/boards/thread.html:101
39 39 msgid "Title"
40 40 msgstr "Заголовок"
41 41
42 #: forms.py:49 templates/boards/posting_general.html:199
43 #: templates/boards/thread.html:118
42 #: forms.py:50 templates/boards/posting_general.html:221
43 #: templates/boards/thread.html:116
44 44 msgid "Text"
45 45 msgstr "Текст"
46 46
47 #: forms.py:50 templates/boards/posting_general.html:204
48 #: templates/boards/thread.html:123
47 #: forms.py:51 templates/boards/posting_general.html:226
48 #: templates/boards/thread.html:121
49 49 msgid "Image"
50 50 msgstr "Изображение"
51 51
52 #: forms.py:53 templates/boards/posting_general.html:214
53 #: templates/boards/thread.html:128
52 #: forms.py:54 templates/boards/posting_general.html:236
53 #: templates/boards/thread.html:126
54 54 msgid "e-mail"
55 55 msgstr ""
56 56
57 #: forms.py:64
57 #: forms.py:65
58 58 #, python-format
59 59 msgid "Title must have less than %s characters"
60 60 msgstr "Заголовок должен иметь меньше %s символов"
61 61
62 #: forms.py:73
62 #: forms.py:74
63 63 #, python-format
64 64 msgid "Text must have less than %s characters"
65 65 msgstr "Текст должен быть короче %s символов"
66 66
67 #: forms.py:84
67 #: forms.py:85
68 68 #, python-format
69 69 msgid "Image must be less than %s bytes"
70 70 msgstr "Изображение должно быть менее %s байт"
71 71
72 #: forms.py:111
72 #: forms.py:112
73 73 msgid "Either text or image must be entered."
74 74 msgstr "Текст или картинка должны быть введены."
75 75
76 #: forms.py:124
76 #: forms.py:125
77 77 #, python-format
78 78 msgid "Wait %s seconds after last posting"
79 79 msgstr "Подождите %s секунд после последнего постинга"
80 80
81 #: forms.py:138 templates/boards/post.html:39
82 #: templates/boards/posting_general.html:209 templates/boards/tags.html:7
81 #: forms.py:139 templates/boards/post.html:60
82 #: templates/boards/posting_general.html:231 templates/boards/tags.html:6
83 83 #: templates/boards/rss/post.html:10
84 84 msgid "Tags"
85 85 msgstr "Теги"
86 86
87 #: forms.py:146
87 #: forms.py:147
88 88 msgid "Inappropriate characters in tags."
89 89 msgstr "Недопустимые символы в тегах."
90 90
91 #: forms.py:174 forms.py:195
91 #: forms.py:175 forms.py:196
92 92 msgid "Captcha validation failed"
93 93 msgstr "Проверка капчи провалена"
94 94
95 #: forms.py:201
95 #: forms.py:202
96 96 msgid "Theme"
97 97 msgstr "Тема"
98 98
99 #: forms.py:206
99 #: forms.py:207
100 100 msgid "Enable moderation panel"
101 101 msgstr "Включить панель модерации"
102 102
103 #: forms.py:221
103 #: forms.py:222
104 104 msgid "No such user found"
105 105 msgstr "Данный пользователь не найден"
106 106
107 #: forms.py:235
107 #: forms.py:236
108 108 #, python-format
109 109 msgid "Wait %s minutes after last login"
110 110 msgstr "Подождите %s минут после последнего входа"
111 111
112 112 #: templates/boards/404.html:6
113 113 msgid "Not found"
114 114 msgstr "Не найдено"
115 115
116 116 #: templates/boards/404.html:12
117 117 msgid "This page does not exist"
118 118 msgstr "Этой страницы не существует"
119 119
120 120 #: templates/boards/authors.html:6 templates/boards/authors.html.py:12
121 121 msgid "Authors"
122 122 msgstr "Авторы"
123 123
124 124 #: templates/boards/authors.html:25
125 125 msgid "Distributed under the"
126 126 msgstr "Распространяется под"
127 127
128 128 #: templates/boards/authors.html:27
129 129 msgid "license"
130 130 msgstr "лицензией"
131 131
132 132 #: templates/boards/authors.html:29
133 133 msgid "Repository"
134 134 msgstr "Репозиторий"
135 135
136 136 #: templates/boards/base.html:13
137 137 msgid "Feed"
138 138 msgstr "Лента"
139 139
140 #: templates/boards/base.html:34
140 #: templates/boards/base.html:35
141 141 msgid "All threads"
142 142 msgstr "Все темы"
143 143
144 #: templates/boards/base.html:39
144 #: templates/boards/base.html:40
145 145 msgid "Tag management"
146 146 msgstr "Управление тегами"
147 147
148 #: templates/boards/base.html:41
148 #: templates/boards/base.html:42
149 149 msgid "Settings"
150 150 msgstr "Настройки"
151 151
152 #: templates/boards/base.html:48 templates/boards/login.html:6
152 #: templates/boards/base.html:49 templates/boards/login.html:6
153 153 #: templates/boards/login.html.py:21
154 154 msgid "Login"
155 155 msgstr "Вход"
156 156
157 #: templates/boards/base.html:49
157 #: templates/boards/base.html:50
158 #, python-format
159 msgid "Speed: %(posts_per_day)s posts per day"
160 msgstr "Скорость: %(posts_per_day)s сообщений в день"
161
162 #: templates/boards/base.html:51
158 163 msgid "Up"
159 164 msgstr "Вверх"
160 165
161 166 #: templates/boards/login.html:15
162 167 msgid "User ID"
163 168 msgstr "ID пользователя"
164 169
165 170 #: templates/boards/login.html:24
166 171 msgid "Insert your user id above"
167 172 msgstr "Вставьте свой ID пользователя выше"
168 173
169 #: templates/boards/post.html:10 templates/boards/rss/post.html:5
170 msgid "Post image"
171 msgstr "Изображение сообщения"
172
173 #: templates/boards/post.html:26 templates/boards/posting_general.html:79
174 #: templates/boards/thread.html:60
174 #: templates/boards/post.html:34 templates/boards/posting_general.html:100
175 #: templates/boards/thread.html:59
175 176 msgid "Delete"
176 177 msgstr "Удалить"
177 178
178 #: templates/boards/post.html:29 templates/boards/posting_general.html:82
179 #: templates/boards/thread.html:63
179 #: templates/boards/post.html:37 templates/boards/posting_general.html:104
180 #: templates/boards/thread.html:62
180 181 msgid "Ban IP"
181 182 msgstr "Заблокировать IP"
182 183
183 #: templates/boards/posting_general.html:44
184 #: templates/boards/post.html:50 templates/boards/posting_general.html:113
185 #: templates/boards/posting_general.html:169 templates/boards/thread.html:71
186 msgid "Replies"
187 msgstr "Ответы"
188
189 #: templates/boards/posting_general.html:63
184 190 msgid "Previous page"
185 191 msgstr "Предыдущая страница"
186 192
187 #: templates/boards/posting_general.html:74
193 #: templates/boards/posting_general.html:94
188 194 msgid "Reply"
189 195 msgstr "Ответ"
190 196
191 #: templates/boards/posting_general.html:91
192 #: templates/boards/posting_general.html:147 templates/boards/thread.html:72
193 msgid "Replies"
194 msgstr "Ответы"
195
196 #: templates/boards/posting_general.html:100 templates/boards/thread.html:156
197 #: templates/boards/posting_general.html:122 templates/boards/thread.html:154
197 198 msgid "replies"
198 199 msgstr "ответов"
199 200
200 #: templates/boards/posting_general.html:101 templates/boards/thread.html:157
201 #: templates/boards/posting_general.html:123 templates/boards/thread.html:155
201 202 msgid "images"
202 203 msgstr "изображений"
203 204
204 #: templates/boards/posting_general.html:170
205 #: templates/boards/posting_general.html:192
205 206 msgid "Next page"
206 207 msgstr "Следующая страница"
207 208
208 #: templates/boards/posting_general.html:175
209 #: templates/boards/posting_general.html:197
209 210 msgid "No threads exist. Create the first one!"
210 211 msgstr "Нет тем. Создайте первую!"
211 212
212 #: templates/boards/posting_general.html:181
213 #: templates/boards/posting_general.html:203
213 214 msgid "Create new thread"
214 215 msgstr "Создать новую тему"
215 216
216 #: templates/boards/posting_general.html:189 templates/boards/thread.html:108
217 #: templates/boards/posting_general.html:211 templates/boards/thread.html:106
217 218 msgid "Formatting"
218 219 msgstr "Форматирование"
219 220
220 #: templates/boards/posting_general.html:191 templates/boards/thread.html:110
221 #: templates/boards/posting_general.html:213 templates/boards/thread.html:108
221 222 msgid "quote"
222 223 msgstr "цитата"
223 224
224 #: templates/boards/posting_general.html:192 templates/boards/thread.html:111
225 #: templates/boards/posting_general.html:214 templates/boards/thread.html:109
225 226 msgid "italic"
226 227 msgstr "курсив"
227 228
228 #: templates/boards/posting_general.html:193 templates/boards/thread.html:112
229 #: templates/boards/posting_general.html:215 templates/boards/thread.html:110
229 230 msgid "bold"
230 231 msgstr "полужирный"
231 232
232 #: templates/boards/posting_general.html:194 templates/boards/thread.html:113
233 #: templates/boards/posting_general.html:216 templates/boards/thread.html:111
233 234 msgid "spoiler"
234 235 msgstr "спойлер"
235 236
236 #: templates/boards/posting_general.html:195 templates/boards/thread.html:114
237 #: templates/boards/posting_general.html:217 templates/boards/thread.html:112
237 238 msgid "comment"
238 239 msgstr "комментарий"
239 240
240 #: templates/boards/posting_general.html:227 templates/boards/thread.html:142
241 #: templates/boards/posting_general.html:249 templates/boards/thread.html:140
241 242 msgid "Post"
242 243 msgstr "Отправить"
243 244
244 #: templates/boards/posting_general.html:229
245 #: templates/boards/posting_general.html:251
245 246 msgid "Tags must be delimited by spaces. Text or image is required."
246 247 msgstr ""
247 248 "Теги должны быть разделены пробелами. Текст или изображение обязательны."
248 249
249 #: templates/boards/posting_general.html:232 templates/boards/thread.html:144
250 #: templates/boards/posting_general.html:254 templates/boards/thread.html:142
250 251 msgid "Text syntax"
251 252 msgstr "Синтаксис текста"
252 253
253 #: templates/boards/posting_general.html:242
254 #: templates/boards/posting_general.html:264
254 255 msgid "Pages:"
255 256 msgstr "Страницы: "
256 257
257 258 #: templates/boards/settings.html:14
258 259 msgid "User:"
259 260 msgstr "Пользователь:"
260 261
261 262 #: templates/boards/settings.html:16
262 263 msgid "You are moderator."
263 264 msgstr "Вы модератор."
264 265
265 266 #: templates/boards/settings.html:19
266 267 msgid "Posts:"
267 268 msgstr "Сообщений:"
268 269
269 270 #: templates/boards/settings.html:20
270 271 msgid "First access:"
271 272 msgstr "Первый доступ:"
272 273
273 274 #: templates/boards/settings.html:22
274 275 msgid "Last access:"
275 276 msgstr "Последний доступ: "
276 277
277 278 #: templates/boards/settings.html:31
278 279 msgid "Save"
279 280 msgstr "Сохранить"
280 281
281 282 #: templates/boards/tags.html:24
282 283 msgid "threads"
283 284 msgstr "тем"
284 285
285 286 #: templates/boards/tags.html:37
286 287 msgid "No tags found."
287 288 msgstr "Теги не найдены."
288 289
289 #: templates/boards/thread.html:25
290 #: templates/boards/thread.html:24
290 291 msgid "posts to bumplimit"
291 292 msgstr "сообщений до бамплимита"
292 293
293 #: templates/boards/thread.html:100
294 #: templates/boards/thread.html:98
294 295 msgid "Reply to thread"
295 296 msgstr "Ответить в тему"
296 297
297 #: templates/boards/thread.html:158
298 #: templates/boards/thread.html:156
298 299 msgid "Last update: "
299 300 msgstr "Последнее обновление: "
300 301
302 #: templates/boards/rss/post.html:5
303 msgid "Post image"
304 msgstr "Изображение сообщения"
305
301 306 #: templates/boards/staticpages/banned.html:6
302 307 msgid "Banned"
303 308 msgstr "Заблокирован"
304 309
305 310 #: templates/boards/staticpages/banned.html:11
306 311 msgid "Your IP address has been banned. Contact the administrator"
307 312 msgstr "Ваш IP адрес был заблокирован. Свяжитесь с администратором"
308 313
309 314 #: templates/boards/staticpages/help.html:6
310 315 #: templates/boards/staticpages/help.html:10
311 316 msgid "Syntax"
312 317 msgstr "Синтаксис"
313 318
314 319 #: templates/boards/staticpages/help.html:11
315 320 msgid "2 line breaks for a new line."
316 321 msgstr "2 перевода строки создают новый абзац."
317 322
318 323 #: templates/boards/staticpages/help.html:12
319 324 msgid "Italic text"
320 325 msgstr "Курсивный текст"
321 326
322 327 #: templates/boards/staticpages/help.html:13
323 328 msgid "Bold text"
324 329 msgstr "Полужирный текст"
325 330
326 331 #: templates/boards/staticpages/help.html:14
327 332 msgid "Spoiler"
328 333 msgstr "Спойлер"
329 334
330 335 #: templates/boards/staticpages/help.html:15
331 336 msgid "Comment"
332 337 msgstr "Комментарий"
333 338
334 339 #: templates/boards/staticpages/help.html:16
335 340 msgid "Quote"
336 341 msgstr "Цитата"
337 342
338 343 #: templates/boards/staticpages/help.html:17
339 344 msgid "Link to a post"
340 345 msgstr "Ссылка на сообщение"
341 346
342 347 #: templates/boards/staticpages/help.html:18
343 348 msgid "Strikethrough text"
344 349 msgstr "Зачеркнутый текст"
345 350
346 351 #~ msgid "Tag: "
347 352 #~ msgstr "Тег: "
348 353
349 354 #~ msgid "Remove"
350 355 #~ msgstr "Удалить"
351 356
352 357 #~ msgid "Add"
353 358 #~ msgstr "Добавить"
354 359
355 360 #~ msgid "Basic markdown syntax."
356 361 #~ msgstr "Базовый синтаксис markdown."
357 362
358 363 #~ msgid "Example: "
359 364 #~ msgstr "Пример: "
360 365
361 366 #~ msgid "tags"
362 367 #~ msgstr "тегов"
363 368
364 369 #~ msgid "Get!"
365 370 #~ msgstr "Гет!"
366 371
367 372 #~ msgid "View"
368 373 #~ msgstr "Просмотр"
369 374
370 375 #~ msgid "gets"
371 376 #~ msgstr "гетов"
@@ -1,295 +1,308 b''
1 from datetime import datetime, timedelta
2 from datetime import time as dtime
1 3 import os
2 4 from random import random
3 5 import time
4 6 import math
5 7 import re
6 8
7 9 from django.db import models
8 10 from django.http import Http404
9 11 from django.utils import timezone
10 12 from markupfield.fields import MarkupField
11 13
12 14 from neboard import settings
13 15 from boards import settings as boards_settings
14 16 from boards import thumbs
15 17
16 18 BAN_REASON_AUTO = 'Auto'
17 19
18 20 IMAGE_THUMB_SIZE = (200, 150)
19 21
20 22 TITLE_MAX_LENGTH = 50
21 23
22 24 DEFAULT_MARKUP_TYPE = 'markdown'
23 25
24 26 NO_PARENT = -1
25 27 NO_IP = '0.0.0.0'
26 28 UNKNOWN_UA = ''
27 29 ALL_PAGES = -1
28 30 IMAGES_DIRECTORY = 'images/'
29 31 FILE_EXTENSION_DELIMITER = '.'
30 32
31 33 SETTING_MODERATE = "moderate"
32 34
33 35 REGEX_REPLY = re.compile('>>(\d+)')
34 36
35 37
36 38 class PostManager(models.Manager):
37 39
38 40 def create_post(self, title, text, image=None, thread=None,
39 41 ip=NO_IP, tags=None, user=None):
40 42 posting_time = timezone.now()
41 43 if not thread:
42 44 thread = Thread.objects.create(bump_time=posting_time,
43 45 last_edit_time=posting_time)
44 46 else:
45 47 thread.bump()
46 48 thread.last_edit_time = posting_time
47 49 thread.save()
48 50
49 51 post = self.create(title=title,
50 52 text=text,
51 53 pub_time=posting_time,
52 54 thread_new=thread,
53 55 image=image,
54 56 poster_ip=ip,
55 57 poster_user_agent=UNKNOWN_UA,
56 58 last_edit_time=posting_time,
57 59 user=user)
58 60
59 61 thread.replies.add(post)
60 62 if tags:
61 63 linked_tags = []
62 64 for tag in tags:
63 65 tag_linked_tags = tag.get_linked_tags()
64 66 if len(tag_linked_tags) > 0:
65 67 linked_tags.extend(tag_linked_tags)
66 68
67 69 tags.extend(linked_tags)
68 70 map(thread.add_tag, tags)
69 71
70 72 self._delete_old_threads()
71 73 self.connect_replies(post)
72 74
73 75 return post
74 76
75 77 def delete_post(self, post):
76 78 thread = post.thread_new
77 79 thread.last_edit_time = timezone.now()
78 80 thread.save()
79 81
80 82 post.delete()
81 83
82 84 def delete_posts_by_ip(self, ip):
83 85 posts = self.filter(poster_ip=ip)
84 86 map(self.delete_post, posts)
85 87
86 88 # TODO Move this method to thread manager
87 89 def get_threads(self, tag=None, page=ALL_PAGES,
88 90 order_by='-bump_time'):
89 91 if tag:
90 92 threads = tag.threads
91 93
92 94 if not threads.exists():
93 95 raise Http404
94 96 else:
95 97 threads = Thread.objects.all()
96 98
97 99 threads = threads.order_by(order_by)
98 100
99 101 if page != ALL_PAGES:
100 102 thread_count = threads.count()
101 103
102 104 if page < self._get_page_count(thread_count):
103 105 start_thread = page * settings.THREADS_PER_PAGE
104 106 end_thread = min(start_thread + settings.THREADS_PER_PAGE,
105 107 thread_count)
106 108 threads = threads[start_thread:end_thread]
107 109
108 110 return threads
109 111
110 112 # TODO Move this method to thread manager
111 113 def get_thread_page_count(self, tag=None):
112 114 if tag:
113 115 threads = Thread.objects.filter(tags=tag)
114 116 else:
115 117 threads = Thread.objects.all()
116 118
117 119 return self._get_page_count(threads.count())
118 120
119 121 # TODO Move this method to thread manager
120 122 def _delete_old_threads(self):
121 123 """
122 124 Preserves maximum thread count. If there are too many threads,
123 125 delete the old ones.
124 126 """
125 127
126 128 # TODO Move old threads to the archive instead of deleting them.
127 129 # Maybe make some 'old' field in the model to indicate the thread
128 130 # must not be shown and be able for replying.
129 131
130 132 threads = Thread.objects.all()
131 133 thread_count = threads.count()
132 134
133 135 if thread_count > settings.MAX_THREAD_COUNT:
134 136 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
135 137 old_threads = threads[thread_count - num_threads_to_delete:]
136 138
137 139 map(Thread.delete_with_posts, old_threads)
138 140
139 141 def connect_replies(self, post):
140 142 """Connect replies to a post to show them as a refmap"""
141 143
142 144 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
143 145 post_id = reply_number.group(1)
144 146 ref_post = self.filter(id=post_id)
145 147 if ref_post.count() > 0:
146 148 referenced_post = ref_post[0]
147 149 referenced_post.referenced_posts.add(post)
148 150 referenced_post.last_edit_time = post.pub_time
149 151 referenced_post.save()
150 152
151 153 def _get_page_count(self, thread_count):
152 154 return int(math.ceil(thread_count / float(settings.THREADS_PER_PAGE)))
153 155
156 def get_posts_per_day(self):
157 """Get count of posts for the current day"""
158
159 today = datetime.now().date()
160 tomorrow = today + timedelta(1)
161 today_start = datetime.combine(today, dtime())
162 today_end = datetime.combine(tomorrow, dtime())
163
164 return self.filter(pub_time__lte=today_end, pub_time__gte=today_start)\
165 .count()
166
154 167
155 168 class Post(models.Model):
156 169 """A post is a message."""
157 170
158 171 objects = PostManager()
159 172
160 173 class Meta:
161 174 app_label = 'boards'
162 175
163 176 def _update_image_filename(self, filename):
164 177 """Get unique image filename"""
165 178
166 179 path = IMAGES_DIRECTORY
167 180 new_name = str(int(time.mktime(time.gmtime())))
168 181 new_name += str(int(random() * 1000))
169 182 new_name += FILE_EXTENSION_DELIMITER
170 183 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
171 184
172 185 return os.path.join(path, new_name)
173 186
174 187 title = models.CharField(max_length=TITLE_MAX_LENGTH)
175 188 pub_time = models.DateTimeField()
176 189 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
177 190 escape_html=False)
178 191
179 192 image_width = models.IntegerField(default=0)
180 193 image_height = models.IntegerField(default=0)
181 194
182 195 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
183 196 blank=True, sizes=(IMAGE_THUMB_SIZE,),
184 197 width_field='image_width',
185 198 height_field='image_height')
186 199
187 200 poster_ip = models.GenericIPAddressField()
188 201 poster_user_agent = models.TextField()
189 202
190 203 thread = models.ForeignKey('Post', null=True, default=None)
191 204 thread_new = models.ForeignKey('Thread', null=True, default=None)
192 205 last_edit_time = models.DateTimeField()
193 206 user = models.ForeignKey('User', null=True, default=None)
194 207
195 208 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
196 209 null=True,
197 210 blank=True, related_name='rfp+')
198 211
199 212 def __unicode__(self):
200 213 return '#' + str(self.id) + ' ' + self.title + ' (' + \
201 214 self.text.raw[:50] + ')'
202 215
203 216 def get_title(self):
204 217 title = self.title
205 218 if len(title) == 0:
206 219 title = self.text.raw[:20]
207 220
208 221 return title
209 222
210 223 def get_sorted_referenced_posts(self):
211 224 return self.referenced_posts.order_by('id')
212 225
213 226 def is_referenced(self):
214 227 return self.referenced_posts.all().exists()
215 228
216 229 def is_opening(self):
217 230 return self.thread_new.get_replies()[0] == self
218 231
219 232
220 233 class Thread(models.Model):
221 234
222 235 class Meta:
223 236 app_label = 'boards'
224 237
225 238 tags = models.ManyToManyField('Tag')
226 239 bump_time = models.DateTimeField()
227 240 last_edit_time = models.DateTimeField()
228 241 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
229 242 blank=True, related_name='tre+')
230 243
231 244 def get_tags(self):
232 245 """Get a sorted tag list"""
233 246
234 247 return self.tags.order_by('name')
235 248
236 249 def bump(self):
237 250 """Bump (move to up) thread"""
238 251
239 252 if self.can_bump():
240 253 self.bump_time = timezone.now()
241 254
242 255 def get_reply_count(self):
243 256 return self.replies.count()
244 257
245 258 def get_images_count(self):
246 259 return self.replies.filter(image_width__gt=0).count()
247 260
248 261 def can_bump(self):
249 262 """Check if the thread can be bumped by replying"""
250 263
251 264 post_count = self.get_reply_count()
252 265
253 266 return post_count <= settings.MAX_POSTS_PER_THREAD
254 267
255 268 def delete_with_posts(self):
256 269 """Completely delete thread"""
257 270
258 271 if self.replies.count() > 0:
259 272 map(Post.objects.delete_post, self.replies.all())
260 273
261 274 self.delete()
262 275
263 276 def get_last_replies(self):
264 277 """Get last replies, not including opening post"""
265 278
266 279 if settings.LAST_REPLIES_COUNT > 0:
267 280 reply_count = self.get_reply_count()
268 281
269 282 if reply_count > 0:
270 283 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
271 284 reply_count - 1)
272 285 last_replies = self.replies.all().order_by('pub_time')[
273 286 reply_count - reply_count_to_show:]
274 287
275 288 return last_replies
276 289
277 290 def get_replies(self):
278 291 """Get sorted thread posts"""
279 292
280 293 return self.replies.all().order_by('pub_time')
281 294
282 295 def add_tag(self, tag):
283 296 """Connect thread to a tag and tag to a thread"""
284 297
285 298 self.tags.add(tag)
286 299 tag.threads.add(self)
287 300
288 301 def get_opening_post(self):
289 302 return self.get_replies()[0]
290 303
291 304 def __unicode__(self):
292 305 return str(self.get_replies()[0].id)
293 306
294 307 def get_pub_time(self):
295 308 return self.get_opening_post().pub_time No newline at end of file
@@ -1,58 +1,59 b''
1 1 {% load staticfiles %}
2 2 {% load i18n %}
3 3 {% load static from staticfiles %}
4 4
5 5 <!DOCTYPE html>
6 6 <html>
7 7 <head>
8 8 <link rel="stylesheet" type="text/css"
9 9 href="{% static 'css/base.css' %}" media="all"/>
10 10 <link rel="stylesheet" type="text/css"
11 11 href="{% static theme_css %}" media="all"/>
12 12 <link rel="alternate" type="application/rss+xml" href="rss/" title=
13 13 "{% trans 'Feed' %}"/>
14 14
15 15 <link rel="icon" type="image/png"
16 16 href="{% static 'favicon.png' %}">
17 17
18 18 <meta name="viewport" content="width=device-width, initial-scale=1"/>
19 19 <meta charset="utf-8"/>
20 20
21 21 {% block head %}{% endblock %}
22 22 </head>
23 23 <body>
24 24 <script src="{% static 'js/jquery-2.0.1.min.js' %}"></script>
25 25 <script src="{% static 'js/jquery-ui-1.10.3.custom.min.js' %}"></script>
26 26 <script src="{% static 'js/jquery.mousewheel.js' %}"></script>
27 27 <script src="{% url 'django.views.i18n.javascript_catalog' %}"></script>
28 28 <script src="{% static 'js/panel.js' %}"></script>
29 29 <script src="{% static 'js/popup.js' %}"></script>
30 30 <script src="{% static 'js/image.js' %}"></script>
31 31 <script src="{% static 'js/refpopup.js' %}"></script>
32 32 <script src="{% static 'js/main.js' %}"></script>
33 33
34 34 <div class="navigation_panel">
35 35 <a class="link" href="{% url 'index' %}">{% trans "All threads" %}</a>
36 36 {% for tag in tags %}
37 37 <a class="tag" href="{% url 'tag' tag_name=tag.name %}"
38 38 >#{{ tag.name }}</a>,
39 39 {% endfor %}
40 40 <a class="tag" href="{% url 'tags' %}" title="{% trans 'Tag management' %}"
41 41 >[...]</a>
42 42 <a class="link" href="{% url 'settings' %}">{% trans 'Settings' %}</a>
43 43 </div>
44 44
45 45 {% block content %}{% endblock %}
46 46
47 47 <div class="navigation_panel">
48 48 {% block metapanel %}{% endblock %}
49 49 [<a href="{% url "login" %}">{% trans 'Login' %}</a>]
50 {% blocktrans %}Speed: {{ posts_per_day }} posts per day{% endblocktrans %}
50 51 <a class="link" href="#top">{% trans 'Up' %}</a>
51 52 </div>
52 53
53 54 <div class="footer">
54 55 <!-- Put your banners here -->
55 56 </div>
56 57
57 58 </body>
58 59 </html>
@@ -1,565 +1,566 b''
1 1 import hashlib
2 2 import json
3 3 import string
4 4 import time
5 5 from datetime import datetime
6 6 import re
7 7
8 8 from django.core import serializers
9 9 from django.core.urlresolvers import reverse
10 10 from django.http import HttpResponseRedirect
11 11 from django.http.response import HttpResponse
12 12 from django.template import RequestContext
13 13 from django.shortcuts import render, redirect, get_object_or_404
14 14 from django.utils import timezone
15 15 from django.db import transaction
16 16
17 17 from boards import forms
18 18 import boards
19 19 from boards import utils
20 20 from boards.forms import ThreadForm, PostForm, SettingsForm, PlainErrorList, \
21 21 ThreadCaptchaForm, PostCaptchaForm, LoginForm, ModeratorSettingsForm
22 22 from boards.models import Post, Tag, Ban, User
23 23 from boards.models.post import SETTING_MODERATE, REGEX_REPLY
24 24 from boards.models.user import RANK_USER
25 25 from boards import authors
26 26 from boards.utils import get_client_ip
27 27 import neboard
28 28
29 29
30 30 BAN_REASON_SPAM = 'Autoban: spam bot'
31 31
32 32
33 33 def index(request, page=0):
34 34 context = _init_default_context(request)
35 35
36 36 if utils.need_include_captcha(request):
37 37 threadFormClass = ThreadCaptchaForm
38 38 kwargs = {'request': request}
39 39 else:
40 40 threadFormClass = ThreadForm
41 41 kwargs = {}
42 42
43 43 if request.method == 'POST':
44 44 form = threadFormClass(request.POST, request.FILES,
45 45 error_class=PlainErrorList, **kwargs)
46 46 form.session = request.session
47 47
48 48 if form.is_valid():
49 49 return _new_post(request, form)
50 50 if form.need_to_ban:
51 51 # Ban user because he is suspected to be a bot
52 52 _ban_current_user(request)
53 53 else:
54 54 form = threadFormClass(error_class=PlainErrorList, **kwargs)
55 55
56 56 threads = []
57 57 for thread_to_show in Post.objects.get_threads(page=int(page)):
58 58 threads.append({
59 59 'thread': thread_to_show,
60 60 'op': thread_to_show.get_replies()[0],
61 61 'bumpable': thread_to_show.can_bump(),
62 62 'last_replies': thread_to_show.get_last_replies(),
63 63 })
64 64
65 65 # TODO Make this generic for tag and threads list pages
66 66 context['threads'] = None if len(threads) == 0 else threads
67 67 context['form'] = form
68 68
69 69 page_count = Post.objects.get_thread_page_count()
70 70 context['pages'] = range(page_count)
71 71 page = int(page)
72 72 if page < page_count - 1:
73 73 context['next_page'] = str(page + 1)
74 74 if page > 0:
75 75 context['prev_page'] = str(page - 1)
76 76
77 77 return render(request, 'boards/posting_general.html',
78 78 context)
79 79
80 80
81 81 @transaction.atomic
82 82 def _new_post(request, form, opening_post=None):
83 83 """Add a new post (in thread or as a reply)."""
84 84
85 85 ip = get_client_ip(request)
86 86 is_banned = Ban.objects.filter(ip=ip).exists()
87 87
88 88 if is_banned:
89 89 return redirect(you_are_banned)
90 90
91 91 data = form.cleaned_data
92 92
93 93 title = data['title']
94 94 text = data['text']
95 95
96 96 text = _remove_invalid_links(text)
97 97
98 98 if 'image' in data.keys():
99 99 image = data['image']
100 100 else:
101 101 image = None
102 102
103 103 tags = []
104 104
105 105 if not opening_post:
106 106 tag_strings = data['tags']
107 107
108 108 if tag_strings:
109 109 tag_strings = tag_strings.split(' ')
110 110 for tag_name in tag_strings:
111 111 tag_name = string.lower(tag_name.strip())
112 112 if len(tag_name) > 0:
113 113 tag, created = Tag.objects.get_or_create(name=tag_name)
114 114 tags.append(tag)
115 115 post_thread = None
116 116 else:
117 117 post_thread = opening_post.thread_new
118 118
119 119 post = Post.objects.create_post(title=title, text=text, ip=ip,
120 120 thread=post_thread, image=image,
121 121 tags=tags, user=_get_user(request))
122 122
123 123 thread_to_show = (opening_post.id if opening_post else post.id)
124 124
125 125 if opening_post:
126 126 return redirect(reverse(thread, kwargs={'post_id': thread_to_show}) +
127 127 '#' + str(post.id))
128 128 else:
129 129 return redirect(thread, post_id=thread_to_show)
130 130
131 131
132 132 def tag(request, tag_name, page=0):
133 133 """
134 134 Get all tag threads. Threads are split in pages, so some page is
135 135 requested. Default page is 0.
136 136 """
137 137
138 138 tag = get_object_or_404(Tag, name=tag_name)
139 139 threads = []
140 140 for thread_to_show in Post.objects.get_threads(page=int(page), tag=tag):
141 141 threads.append({
142 142 'thread': thread_to_show,
143 143 'op': thread_to_show.get_replies()[0],
144 144 'bumpable': thread_to_show.can_bump(),
145 145 'last_replies': thread_to_show.get_last_replies(),
146 146 })
147 147
148 148 if request.method == 'POST':
149 149 form = ThreadForm(request.POST, request.FILES,
150 150 error_class=PlainErrorList)
151 151 form.session = request.session
152 152
153 153 if form.is_valid():
154 154 return _new_post(request, form)
155 155 if form.need_to_ban:
156 156 # Ban user because he is suspected to be a bot
157 157 _ban_current_user(request)
158 158 else:
159 159 form = forms.ThreadForm(initial={'tags': tag_name},
160 160 error_class=PlainErrorList)
161 161
162 162 context = _init_default_context(request)
163 163 context['threads'] = None if len(threads) == 0 else threads
164 164 context['tag'] = tag
165 165
166 166 page_count = Post.objects.get_thread_page_count(tag=tag)
167 167 context['pages'] = range(page_count)
168 168 page = int(page)
169 169 if page < page_count - 1:
170 170 context['next_page'] = str(page + 1)
171 171 if page > 0:
172 172 context['prev_page'] = str(page - 1)
173 173
174 174 context['form'] = form
175 175
176 176 return render(request, 'boards/posting_general.html',
177 177 context)
178 178
179 179
180 180 def thread(request, post_id):
181 181 """Get all thread posts"""
182 182
183 183 if utils.need_include_captcha(request):
184 184 postFormClass = PostCaptchaForm
185 185 kwargs = {'request': request}
186 186 else:
187 187 postFormClass = PostForm
188 188 kwargs = {}
189 189
190 190 if request.method == 'POST':
191 191 form = postFormClass(request.POST, request.FILES,
192 192 error_class=PlainErrorList, **kwargs)
193 193 form.session = request.session
194 194
195 195 opening_post = get_object_or_404(Post, id=post_id)
196 196 if form.is_valid():
197 197 return _new_post(request, form, opening_post)
198 198 if form.need_to_ban:
199 199 # Ban user because he is suspected to be a bot
200 200 _ban_current_user(request)
201 201 else:
202 202 form = postFormClass(error_class=PlainErrorList, **kwargs)
203 203
204 204 thread_to_show = get_object_or_404(Post, id=post_id).thread_new
205 205
206 206 context = _init_default_context(request)
207 207
208 208 posts = thread_to_show.get_replies()
209 209 context['form'] = form
210 210 context['bumpable'] = thread_to_show.can_bump()
211 211 if context['bumpable']:
212 212 context['posts_left'] = neboard.settings.MAX_POSTS_PER_THREAD - posts\
213 213 .count()
214 214 context['bumplimit_progress'] = str(
215 215 float(context['posts_left']) /
216 216 neboard.settings.MAX_POSTS_PER_THREAD * 100)
217 217 context["last_update"] = _datetime_to_epoch(thread_to_show.last_edit_time)
218 218 context["thread"] = thread_to_show
219 219
220 220 return render(request, 'boards/thread.html', context)
221 221
222 222
223 223 def login(request):
224 224 """Log in with user id"""
225 225
226 226 context = _init_default_context(request)
227 227
228 228 if request.method == 'POST':
229 229 form = LoginForm(request.POST, request.FILES,
230 230 error_class=PlainErrorList)
231 231 form.session = request.session
232 232
233 233 if form.is_valid():
234 234 user = User.objects.get(user_id=form.cleaned_data['user_id'])
235 235 request.session['user_id'] = user.id
236 236 return redirect(index)
237 237
238 238 else:
239 239 form = LoginForm()
240 240
241 241 context['form'] = form
242 242
243 243 return render(request, 'boards/login.html', context)
244 244
245 245
246 246 def settings(request):
247 247 """User's settings"""
248 248
249 249 context = _init_default_context(request)
250 250 user = _get_user(request)
251 251 is_moderator = user.is_moderator()
252 252
253 253 if request.method == 'POST':
254 254 with transaction.commit_on_success():
255 255 if is_moderator:
256 256 form = ModeratorSettingsForm(request.POST,
257 257 error_class=PlainErrorList)
258 258 else:
259 259 form = SettingsForm(request.POST, error_class=PlainErrorList)
260 260
261 261 if form.is_valid():
262 262 selected_theme = form.cleaned_data['theme']
263 263
264 264 user.save_setting('theme', selected_theme)
265 265
266 266 if is_moderator:
267 267 moderate = form.cleaned_data['moderate']
268 268 user.save_setting(SETTING_MODERATE, moderate)
269 269
270 270 return redirect(settings)
271 271 else:
272 272 selected_theme = _get_theme(request)
273 273
274 274 if is_moderator:
275 275 form = ModeratorSettingsForm(initial={'theme': selected_theme,
276 276 'moderate': context['moderator']},
277 277 error_class=PlainErrorList)
278 278 else:
279 279 form = SettingsForm(initial={'theme': selected_theme},
280 280 error_class=PlainErrorList)
281 281
282 282 context['form'] = form
283 283
284 284 return render(request, 'boards/settings.html', context)
285 285
286 286
287 287 def all_tags(request):
288 288 """All tags list"""
289 289
290 290 context = _init_default_context(request)
291 291 context['all_tags'] = Tag.objects.get_not_empty_tags()
292 292
293 293 return render(request, 'boards/tags.html', context)
294 294
295 295
296 296 def jump_to_post(request, post_id):
297 297 """Determine thread in which the requested post is and open it's page"""
298 298
299 299 post = get_object_or_404(Post, id=post_id)
300 300
301 301 if not post.thread:
302 302 return redirect(thread, post_id=post.id)
303 303 else:
304 304 return redirect(reverse(thread, kwargs={'post_id': post.thread.id})
305 305 + '#' + str(post.id))
306 306
307 307
308 308 def authors(request):
309 309 """Show authors list"""
310 310
311 311 context = _init_default_context(request)
312 312 context['authors'] = boards.authors.authors
313 313
314 314 return render(request, 'boards/authors.html', context)
315 315
316 316
317 317 @transaction.atomic
318 318 def delete(request, post_id):
319 319 """Delete post"""
320 320
321 321 user = _get_user(request)
322 322 post = get_object_or_404(Post, id=post_id)
323 323
324 324 if user.is_moderator():
325 325 # TODO Show confirmation page before deletion
326 326 Post.objects.delete_post(post)
327 327
328 328 if not post.thread:
329 329 return _redirect_to_next(request)
330 330 else:
331 331 return redirect(thread, post_id=post.thread.id)
332 332
333 333
334 334 @transaction.atomic
335 335 def ban(request, post_id):
336 336 """Ban user"""
337 337
338 338 user = _get_user(request)
339 339 post = get_object_or_404(Post, id=post_id)
340 340
341 341 if user.is_moderator():
342 342 # TODO Show confirmation page before ban
343 343 ban, created = Ban.objects.get_or_create(ip=post.poster_ip)
344 344 if created:
345 345 ban.reason = 'Banned for post ' + str(post_id)
346 346 ban.save()
347 347
348 348 return _redirect_to_next(request)
349 349
350 350
351 351 def you_are_banned(request):
352 352 """Show the page that notifies that user is banned"""
353 353
354 354 context = _init_default_context(request)
355 355
356 356 ban = get_object_or_404(Ban, ip=utils.get_client_ip(request))
357 357 context['ban_reason'] = ban.reason
358 358 return render(request, 'boards/staticpages/banned.html', context)
359 359
360 360
361 361 def page_404(request):
362 362 """Show page 404 (not found error)"""
363 363
364 364 context = _init_default_context(request)
365 365 return render(request, 'boards/404.html', context)
366 366
367 367
368 368 @transaction.atomic
369 369 def tag_subscribe(request, tag_name):
370 370 """Add tag to favorites"""
371 371
372 372 user = _get_user(request)
373 373 tag = get_object_or_404(Tag, name=tag_name)
374 374
375 375 if not tag in user.fav_tags.all():
376 376 user.add_tag(tag)
377 377
378 378 return _redirect_to_next(request)
379 379
380 380
381 381 @transaction.atomic
382 382 def tag_unsubscribe(request, tag_name):
383 383 """Remove tag from favorites"""
384 384
385 385 user = _get_user(request)
386 386 tag = get_object_or_404(Tag, name=tag_name)
387 387
388 388 if tag in user.fav_tags.all():
389 389 user.remove_tag(tag)
390 390
391 391 return _redirect_to_next(request)
392 392
393 393
394 394 def static_page(request, name):
395 395 """Show a static page that needs only tags list and a CSS"""
396 396
397 397 context = _init_default_context(request)
398 398 return render(request, 'boards/staticpages/' + name + '.html', context)
399 399
400 400
401 401 def api_get_post(request, post_id):
402 402 """
403 403 Get the JSON of a post. This can be
404 404 used as and API for external clients.
405 405 """
406 406
407 407 post = get_object_or_404(Post, id=post_id)
408 408
409 409 json = serializers.serialize("json", [post], fields=(
410 410 "pub_time", "_text_rendered", "title", "text", "image",
411 411 "image_width", "image_height", "replies", "tags"
412 412 ))
413 413
414 414 return HttpResponse(content=json)
415 415
416 416
417 417 @transaction.atomic
418 418 def api_get_threaddiff(request, thread_id, last_update_time):
419 419 """Get posts that were changed or added since time"""
420 420
421 421 thread = get_object_or_404(Post, id=thread_id).thread_new
422 422
423 423 filter_time = datetime.fromtimestamp(float(last_update_time) / 1000000,
424 424 timezone.get_current_timezone())
425 425
426 426 json_data = {
427 427 'added': [],
428 428 'updated': [],
429 429 'last_update': None,
430 430 }
431 431 added_posts = Post.objects.filter(thread_new=thread,
432 432 pub_time__gt=filter_time)\
433 433 .order_by('pub_time')
434 434 updated_posts = Post.objects.filter(thread_new=thread,
435 435 pub_time__lte=filter_time,
436 436 last_edit_time__gt=filter_time)
437 437 for post in added_posts:
438 438 json_data['added'].append(get_post(request, post.id).content.strip())
439 439 for post in updated_posts:
440 440 json_data['updated'].append(get_post(request, post.id).content.strip())
441 441 json_data['last_update'] = _datetime_to_epoch(thread.last_edit_time)
442 442
443 443 return HttpResponse(content=json.dumps(json_data))
444 444
445 445
446 446 def get_post(request, post_id):
447 447 """Get the html of a post. Used for popups."""
448 448
449 449 post = get_object_or_404(Post, id=post_id)
450 450 thread = post.thread_new
451 451
452 452 context = RequestContext(request)
453 453 context["post"] = post
454 454 context["can_bump"] = thread.can_bump()
455 455 if "truncated" in request.GET:
456 456 context["truncated"] = True
457 457
458 458 return render(request, 'boards/post.html', context)
459 459
460 460
461 461 def _get_theme(request, user=None):
462 462 """Get user's CSS theme"""
463 463
464 464 if not user:
465 465 user = _get_user(request)
466 466 theme = user.get_setting('theme')
467 467 if not theme:
468 468 theme = neboard.settings.DEFAULT_THEME
469 469
470 470 return theme
471 471
472 472
473 473 def _init_default_context(request):
474 474 """Create context with default values that are used in most views"""
475 475
476 476 context = RequestContext(request)
477 477
478 478 user = _get_user(request)
479 479 context['user'] = user
480 480 context['tags'] = user.get_sorted_fav_tags()
481 context['posts_per_day'] = Post.objects.get_posts_per_day()
481 482
482 483 theme = _get_theme(request, user)
483 484 context['theme'] = theme
484 485 context['theme_css'] = 'css/' + theme + '/base_page.css'
485 486
486 487 # This shows the moderator panel
487 488 moderate = user.get_setting(SETTING_MODERATE)
488 489 if moderate == 'True':
489 490 context['moderator'] = user.is_moderator()
490 491 else:
491 492 context['moderator'] = False
492 493
493 494 return context
494 495
495 496
496 497 def _get_user(request):
497 498 """
498 499 Get current user from the session. If the user does not exist, create
499 500 a new one.
500 501 """
501 502
502 503 session = request.session
503 504 if not 'user_id' in session:
504 505 request.session.save()
505 506
506 507 md5 = hashlib.md5()
507 508 md5.update(session.session_key)
508 509 new_id = md5.hexdigest()
509 510
510 511 time_now = timezone.now()
511 512 user = User.objects.create(user_id=new_id, rank=RANK_USER,
512 513 registration_time=time_now)
513 514
514 515 session['user_id'] = user.id
515 516 else:
516 517 user = User.objects.get(id=session['user_id'])
517 518
518 519 return user
519 520
520 521
521 522 def _redirect_to_next(request):
522 523 """
523 524 If a 'next' parameter was specified, redirect to the next page. This is
524 525 used when the user is required to return to some page after the current
525 526 view has finished its work.
526 527 """
527 528
528 529 if 'next' in request.GET:
529 530 next_page = request.GET['next']
530 531 return HttpResponseRedirect(next_page)
531 532 else:
532 533 return redirect(index)
533 534
534 535
535 536 @transaction.atomic
536 537 def _ban_current_user(request):
537 538 """Add current user to the IP ban list"""
538 539
539 540 ip = utils.get_client_ip(request)
540 541 ban, created = Ban.objects.get_or_create(ip=ip)
541 542 if created:
542 543 ban.can_read = False
543 544 ban.reason = BAN_REASON_SPAM
544 545 ban.save()
545 546
546 547
547 548 def _remove_invalid_links(text):
548 549 """
549 550 Replace invalid links in posts so that they won't be parsed.
550 551 Invalid links are links to non-existent posts
551 552 """
552 553
553 554 for reply_number in re.finditer(REGEX_REPLY, text):
554 555 post_id = reply_number.group(1)
555 556 post = Post.objects.filter(id=post_id)
556 557 if not post.exists():
557 558 text = string.replace(text, '>>' + post_id, post_id)
558 559
559 560 return text
560 561
561 562
562 563 def _datetime_to_epoch(datetime):
563 564 return int(time.mktime(timezone.localtime(
564 565 datetime,timezone.get_current_timezone()).timetuple())
565 566 * 1000000 + datetime.microsecond)
@@ -1,48 +1,48 b''
1 1 = Features =
2 2 [DONE] Connecting tags to each other
3 3 [DONE] Connect posts to the replies (in messages), get rid of the JS reply map
4 4 [DONE] Better django admin pages to simplify admin operations
5 5 [DONE] Regen script to update all posts
6 6 [DONE] Remove jump links from refmaps
7 7 [DONE] Ban reasons. Split bans into 2 types "read-only" and "read
8 8 denied". Use second only for autoban for spam
9 9 [DONE] Clean up tests and make them run ALWAYS
10 10 [DONE] Use transactions in tests
11 11 [DONE] Thread autoupdate (JS + API)
12 [IN PROGRESS] Split up post model into post and thread,
12 [DONE] Split up post model into post and thread,
13 13 and move everything that is used only in 1st post to thread model.
14 [DONE] Show board speed in the lower panel (posts per day)
14 15
15 16 [NOT STARTED] Tree view (JS)
16 17 [NOT STARTED] Adding tags to images filename
17 18 [NOT STARTED] Federative network for s2s communication
18 19 [NOT STARTED] XMPP gate
19 20 [NOT STARTED] Bitmessage gate
20 21 [NOT STARTED] Notification engine
21 22 [NOT STARTED] Javascript disabling engine
22 23 [NOT STARTED] Group tags by first letter in all tags list
23 [NOT STARTED] Show board speed in the lower panel (posts per day)
24 24 [NOT STARTED] Character counter in the post field
25 25 [NOT STARTED] Save image thumbnails size to the separate field
26 26 [NOT STARTED] Whitelist functionality. Permin autoban of an address
27 27 [NOT STARTED] Statistics module. Count views (optional, may result in bad
28 28 performance), posts per day/week/month, users (or IPs)
29 29 [NOT STARTED] Quote button next to "reply" for posts in thread to include full
30 30 post or its part (delimited by N characters) into quote of the new post.
31 31 [NOT STARTED] Ban confirmation page with reason
32 32 [NOT STARTED] Post deletion confirmation page
33 33 [NOT STARTED] Moderating page. Tags editing and adding
34 34 [NOT STARTED] Get thread graph image using pygraphviz
35 35 [NOT STARTED] Creating post via AJAX without reloading page
36 36 [NOT STARTED] Subscribing to tag via AJAX
37 37
38 38 = Bugs =
39 39 [DONE] Fix bug with creating threads from tag view
40 40 [DONE] Quote characters within quote causes quote parsing to fail
41 41
42 42 [IN PROGRESS] Replies, images, last update time in bottom panel doesn't change when
43 43 thread updates
44 44
45 45 = Testing =
46 46 [NOT STARTED] Make tests for every view
47 47 [NOT STARTED] Make tests for every model
48 48 [NOT STARTED] Make tests for every form
General Comments 0
You need to be logged in to leave comments. Login now