##// END OF EJS Templates
Merged with default branch
neko259 -
r1227:af86e9af merge decentral
parent child Browse files
Show More
@@ -0,0 +1,19 b''
1 # -*- coding: utf-8 -*-
2 from __future__ import unicode_literals
3
4 from django.db import models, migrations
5
6
7 class Migration(migrations.Migration):
8
9 dependencies = [
10 ('boards', '0018_banner'),
11 ]
12
13 operations = [
14 migrations.AlterField(
15 model_name='post',
16 name='referenced_posts',
17 field=models.ManyToManyField(db_index=True, to='boards.Post', null=True, related_name='refposts', blank=True),
18 ),
19 ]
@@ -0,0 +1,15 b''
1 # -*- coding: utf-8 -*-
2 from __future__ import unicode_literals
3
4 from django.db import models, migrations
5
6
7 class Migration(migrations.Migration):
8
9 dependencies = [
10 ('boards', '0019_merge'),
11 ('boards', '0019_auto_20150519_1323'),
12 ]
13
14 operations = [
15 ]
@@ -0,0 +1,19 b''
1 {% extends "boards/thread.html" %}
2
3 {% load i18n %}
4 {% load static from staticfiles %}
5 {% load board %}
6 {% load tz %}
7
8 {% block thread_content %}
9 {% get_current_language as LANGUAGE_CODE %}
10 {% get_current_timezone as TIME_ZONE %}
11
12 <div class="thread">
13 {% for post in thread.get_top_level_replies %}
14 {% post_view post moderator=moderator mode_tree=True %}
15 {% endfor %}
16 </div>
17
18 <script src="{% static 'js/thread.js' %}"></script>
19 {% endblock %}
@@ -0,0 +1,12 b''
1 from boards.views.thread import ThreadView
2
3 TEMPLATE_TREE = 'boards/thread_tree.html'
4
5
6 class TreeThreadView(ThreadView):
7
8 def get_template(self):
9 return TEMPLATE_TREE
10
11 def get_mode(self):
12 return 'tree'
@@ -29,3 +29,7 b' d528d76d3242cced614fa11bb63f3d342e4e1d09'
29 1b631781ced34fbdeec032e7674bc4e131724699 2.6.0
29 1b631781ced34fbdeec032e7674bc4e131724699 2.6.0
30 0f2ef17dc0de678ada279bf7eedf6c5585f1fd7a 2.6.1
30 0f2ef17dc0de678ada279bf7eedf6c5585f1fd7a 2.6.1
31 d53fc814a424d7fd90f23025c87b87baa164450e 2.7.0
31 d53fc814a424d7fd90f23025c87b87baa164450e 2.7.0
32 836d8bb9fcd930b952b9a02029442c71c2441983 2.8.0
33 dfb6c481b1a2c33705de9a9b5304bc924c46b202 2.8.1
34 4a5bec08ccfb47a27f9e98698f12dd5b7246623b 2.8.2
35 604935b98f5b5e4a5e903594f048046e1fbb3519 2.8.3
@@ -2,7 +2,7 b''
2
2
3 from django.core.paginator import Paginator
3 from django.core.paginator import Paginator
4
4
5 PAGINATOR_LOOKAROUND_SIZE = 3
5 PAGINATOR_LOOKAROUND_SIZE = 2
6
6
7
7
8 def get_paginator(*args, **kwargs):
8 def get_paginator(*args, **kwargs):
@@ -14,9 +14,48 b' class DividedPaginator(Paginator):'
14 lookaround_size = PAGINATOR_LOOKAROUND_SIZE
14 lookaround_size = PAGINATOR_LOOKAROUND_SIZE
15 current_page = 0
15 current_page = 0
16
16
17 def center_range(self):
17 def _left_range(self):
18 return self.page_range[:self.lookaround_size]
19
20 def _right_range(self):
21 pages = self.num_pages-self.lookaround_size
22 if pages <= 0:
23 return []
24 else:
25 return self.page_range[pages:]
26
27 def _center_range(self):
18 index = self.page_range.index(self.current_page)
28 index = self.page_range.index(self.current_page)
19
29
20 start = max(0, index - self.lookaround_size)
30 start = max(self.lookaround_size, index - self.lookaround_size)
21 end = min(len(self.page_range), index + self.lookaround_size + 1)
31 end = min(self.num_pages - self.lookaround_size, index + self.lookaround_size + 1)
22 return self.page_range[start:end] No newline at end of file
32 return self.page_range[start:end]
33
34 def get_divided_range(self):
35 dr = list()
36
37 dr += self._left_range()
38 dr += self._center_range()
39 dr += self._right_range()
40
41 # Remove duplicates
42 dr = list(set(dr))
43 dr.sort()
44
45 return dr
46
47 def get_dividers(self):
48 dividers = []
49
50 prev_page = 1
51 for page in self.get_divided_range():
52 if page - prev_page > 1:
53 dividers.append(page)
54
55 # There can be no more than 2 dividers, so don't bother going
56 # further
57 if len(dividers) > 2:
58 break
59 prev_page = page
60
61 return dividers
@@ -71,7 +71,7 b' class SettingsManager:'
71 tag_names = self.get_setting(SETTING_FAVORITE_TAGS)
71 tag_names = self.get_setting(SETTING_FAVORITE_TAGS)
72 tags = []
72 tags = []
73 if tag_names:
73 if tag_names:
74 tags = Tag.objects.filter(name__in=tag_names)
74 tags = list(Tag.objects.filter(name__in=tag_names))
75 return tags
75 return tags
76
76
77 def add_fav_tag(self, tag):
77 def add_fav_tag(self, tag):
@@ -95,9 +95,7 b' class SettingsManager:'
95 tag_names = self.get_setting(SETTING_HIDDEN_TAGS)
95 tag_names = self.get_setting(SETTING_HIDDEN_TAGS)
96 tags = []
96 tags = []
97 if tag_names:
97 if tag_names:
98 for tag_name in tag_names:
98 tags = list(Tag.objects.filter(name__in=tag_names))
99 tag = get_object_or_404(Tag, name=tag_name)
100 tags.append(tag)
101
99
102 return tags
100 return tags
103
101
@@ -1,6 +1,6 b''
1 [Version]
1 [Version]
2 Version = 2.7.0 Chani
2 Version = 2.8.3 Charlie
3 SiteName = Neboard
3 SiteName = Neboard DEV
4
4
5 [Cache]
5 [Cache]
6 # Timeout for caching, if cache is used
6 # Timeout for caching, if cache is used
@@ -15,9 +15,12 b' from boards.models import Tag, Post'
15 from neboard import settings
15 from neboard import settings
16 import boards.settings as board_settings
16 import boards.settings as board_settings
17
17
18 HEADER_CONTENT_LENGTH = 'content-length'
19 HEADER_CONTENT_TYPE = 'content-type'
18
20
19 CONTENT_TYPE_IMAGE = (
21 CONTENT_TYPE_IMAGE = (
20 'image/jpeg',
22 'image/jpeg',
23 'image/jpg',
21 'image/png',
24 'image/png',
22 'image/gif',
25 'image/gif',
23 'image/bmp',
26 'image/bmp',
@@ -298,9 +301,9 b' class PostForm(NeboardForm):'
298 try:
301 try:
299 # Verify content headers
302 # Verify content headers
300 response_head = requests.head(url, verify=False)
303 response_head = requests.head(url, verify=False)
301 content_type = response_head.headers['content-type'].split(';')[0]
304 content_type = response_head.headers[HEADER_CONTENT_TYPE].split(';')[0]
302 if content_type in CONTENT_TYPE_IMAGE:
305 if content_type in CONTENT_TYPE_IMAGE:
303 length_header = response_head.headers.get('content-length')
306 length_header = response_head.headers.get(HEADER_CONTENT_LENGTH)
304 if length_header:
307 if length_header:
305 length = int(length_header)
308 length = int(length_header)
306 self.validate_image_size(length)
309 self.validate_image_size(length)
1 NO CONTENT: modified file, binary diff hidden
NO CONTENT: modified file, binary diff hidden
@@ -7,7 +7,7 b' msgid ""'
7 msgstr ""
7 msgstr ""
8 "Project-Id-Version: PACKAGE VERSION\n"
8 "Project-Id-Version: PACKAGE VERSION\n"
9 "Report-Msgid-Bugs-To: \n"
9 "Report-Msgid-Bugs-To: \n"
10 "POT-Creation-Date: 2015-05-14 23:38+0300\n"
10 "POT-Creation-Date: 2015-05-19 17:51+0300\n"
11 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
11 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
12 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
12 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
13 "Language-Team: LANGUAGE <LL@li.org>\n"
13 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -189,7 +189,7 b' msgid "Create new thread"'
189 msgstr "Создать новую тему"
189 msgstr "Создать новую тему"
190
190
191 #: templates/boards/all_threads.html:118 templates/boards/preview.html:16
191 #: templates/boards/all_threads.html:118 templates/boards/preview.html:16
192 #: templates/boards/thread_normal.html:43
192 #: templates/boards/thread_normal.html:38
193 msgid "Post"
193 msgid "Post"
194 msgstr "Отправить"
194 msgstr "Отправить"
195
195
@@ -199,7 +199,7 b' msgstr ""'
199 "Метки должны быть разделены пробелами. Текст или изображение обязательны."
199 "Метки должны быть разделены пробелами. Текст или изображение обязательны."
200
200
201 #: templates/boards/all_threads.html:126
201 #: templates/boards/all_threads.html:126
202 #: templates/boards/thread_normal.html:48
202 #: templates/boards/thread_normal.html:43
203 msgid "Text syntax"
203 msgid "Text syntax"
204 msgstr "Синтаксис текста"
204 msgstr "Синтаксис текста"
205
205
@@ -244,7 +244,6 b' msgid "tags"'
244 msgstr "метки"
244 msgstr "метки"
245
245
246 #: templates/boards/base.html:40
246 #: templates/boards/base.html:40
247 #| msgid "Search"
248 msgid "search"
247 msgid "search"
249 msgstr "поиск"
248 msgstr "поиск"
250
249
@@ -298,15 +297,15 b' msgstr "\xd0\x98\xd0\xb7\xd0\xbc\xd0\xb5\xd0\xbd\xd0\xb8\xd1\x82\xd1\x8c"'
298 msgid "Edit thread"
297 msgid "Edit thread"
299 msgstr "Изменить тему"
298 msgstr "Изменить тему"
300
299
301 #: templates/boards/post.html:77
300 #: templates/boards/post.html:84
302 msgid "Replies"
301 msgid "Replies"
303 msgstr "Ответы"
302 msgstr "Ответы"
304
303
305 #: templates/boards/post.html:89 templates/boards/thread.html:26
304 #: templates/boards/post.html:97 templates/boards/thread.html:37
306 msgid "messages"
305 msgid "messages"
307 msgstr "сообщений"
306 msgstr "сообщений"
308
307
309 #: templates/boards/post.html:90 templates/boards/thread.html:27
308 #: templates/boards/post.html:98 templates/boards/thread.html:38
310 msgid "images"
309 msgid "images"
311 msgstr "изображений"
310 msgstr "изображений"
312
311
@@ -388,37 +387,42 b' msgstr "\xd0\x9c\xd0\xb5\xd1\x82\xd0\xba\xd0\xb8 \xd0\xbd\xd0\xb5 \xd0\xbd\xd0\xb0\xd0\xb9\xd0\xb4\xd0\xb5\xd0\xbd\xd1\x8b."'
388 msgid "All tags"
387 msgid "All tags"
389 msgstr "Все метки"
388 msgstr "Все метки"
390
389
391 #: templates/boards/thread.html:28
390 #: templates/boards/thread.html:15
391 #| msgid "Normal mode"
392 msgid "Normal"
393 msgstr "Нормальный"
394
395 #: templates/boards/thread.html:16
396 #| msgid "Gallery mode"
397 msgid "Gallery"
398 msgstr "Галерея"
399
400 #: templates/boards/thread.html:17
401 #| msgid "Tree mode"
402 msgid "Tree"
403 msgstr "Дерево"
404
405 #: templates/boards/thread.html:39
392 msgid "Last update: "
406 msgid "Last update: "
393 msgstr "Последнее обновление: "
407 msgstr "Последнее обновление: "
394
408
395 #: templates/boards/thread_gallery.html:19
409 #: templates/boards/thread_gallery.html:36
396 #: templates/boards/thread_normal.html:13
397 msgid "Normal mode"
398 msgstr "Нормальный режим"
399
400 #: templates/boards/thread_gallery.html:20
401 #: templates/boards/thread_normal.html:14
402 msgid "Gallery mode"
403 msgstr "Режим галереи"
404
405 #: templates/boards/thread_gallery.html:41
406 msgid "No images."
410 msgid "No images."
407 msgstr "Нет изображений."
411 msgstr "Нет изображений."
408
412
409 #: templates/boards/thread_normal.html:22
413 #: templates/boards/thread_normal.html:17
410 msgid "posts to bumplimit"
414 msgid "posts to bumplimit"
411 msgstr "сообщений до бамплимита"
415 msgstr "сообщений до бамплимита"
412
416
413 #: templates/boards/thread_normal.html:36
417 #: templates/boards/thread_normal.html:31
414 msgid "Reply to thread"
418 msgid "Reply to thread"
415 msgstr "Ответить в тему"
419 msgstr "Ответить в тему"
416
420
417 #: templates/boards/thread_normal.html:49
421 #: templates/boards/thread_normal.html:44
418 msgid "Close form"
422 msgid "Close form"
419 msgstr "Закрыть форму"
423 msgstr "Закрыть форму"
420
424
421 #: templates/boards/thread_normal.html:63
425 #: templates/boards/thread_normal.html:58
422 msgid "Update"
426 msgid "Update"
423 msgstr "Обновить"
427 msgstr "Обновить"
424
428
@@ -17,6 +17,5 b' class Command(BaseCommand):'
17 primary=first_key)
17 primary=first_key)
18 print(key)
18 print(key)
19
19
20 if first_key:
20 for post in Post.objects.filter(global_id=None):
21 for post in Post.objects.filter(global_id=None):
21 post.set_global_id()
22 post.set_global_id() No newline at end of file
@@ -160,6 +160,17 b' def render_notification(tag_name, value,'
160 reverse('notifications', kwargs={'username': username}), username)
160 reverse('notifications', kwargs={'username': username}), username)
161
161
162
162
163 def render_tag(tag_name, value, options, parent, context):
164 tag_name = value.lower()
165
166 try:
167 url = boards.models.Tag.objects.get(name=tag_name).get_view()
168 except ObjectDoesNotExist:
169 url = tag_name
170
171 return url
172
173
163 formatters = [
174 formatters = [
164 QuotePattern,
175 QuotePattern,
165 SpoilerPattern,
176 SpoilerPattern,
@@ -188,6 +199,7 b' class Parser:'
188 self.parser.add_formatter('post', render_reflink, strip=True)
199 self.parser.add_formatter('post', render_reflink, strip=True)
189 self.parser.add_formatter('quote', render_quote, strip=True)
200 self.parser.add_formatter('quote', render_quote, strip=True)
190 self.parser.add_formatter('user', render_notification, strip=True)
201 self.parser.add_formatter('user', render_notification, strip=True)
202 self.parser.add_formatter('tag', render_tag, strip=True)
191 self.parser.add_simple_formatter(
203 self.parser.add_simple_formatter(
192 'comment', '<span class="comment">//%(value)s</span>')
204 'comment', '<span class="comment">//%(value)s</span>')
193 self.parser.add_simple_formatter(
205 self.parser.add_simple_formatter(
@@ -7,7 +7,7 b' import uuid'
7 from django.core.exceptions import ObjectDoesNotExist
7 from django.core.exceptions import ObjectDoesNotExist
8 from django.core.urlresolvers import reverse
8 from django.core.urlresolvers import reverse
9 from django.db import models, transaction
9 from django.db import models, transaction
10 from django.db.models import TextField
10 from django.db.models import TextField, QuerySet
11 from django.template.loader import render_to_string
11 from django.template.loader import render_to_string
12 from django.utils import timezone
12 from django.utils import timezone
13
13
@@ -44,7 +44,6 b" REGEX_GLOBAL_REPLY = re.compile(r'\\[post"
44 REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?')
44 REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?')
45 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
45 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
46
46
47
48 PARAMETER_TRUNCATED = 'truncated'
47 PARAMETER_TRUNCATED = 'truncated'
49 PARAMETER_TAG = 'tag'
48 PARAMETER_TAG = 'tag'
50 PARAMETER_OFFSET = 'offset'
49 PARAMETER_OFFSET = 'offset'
@@ -59,7 +58,14 b" PARAMETER_NEED_OPEN_LINK = 'need_open_li"
59 PARAMETER_REPLY_LINK = 'reply_link'
58 PARAMETER_REPLY_LINK = 'reply_link'
60 PARAMETER_NEED_OP_DATA = 'need_op_data'
59 PARAMETER_NEED_OP_DATA = 'need_op_data'
61
60
62 DIFF_TYPE_HTML = 'html'
61 POST_VIEW_PARAMS = (
62 'need_op_data',
63 'reply_link',
64 'moderator',
65 'need_open_link',
66 'truncated',
67 'mode_tree',
68 )
63
69
64 REFMAP_STR = '<a href="{}">&gt;&gt;{}</a>'
70 REFMAP_STR = '<a href="{}">&gt;&gt;{}</a>'
65
71
@@ -67,7 +73,7 b' REFMAP_STR = \'<a href="{}">&gt;&gt;{}</a'
67 class PostManager(models.Manager):
73 class PostManager(models.Manager):
68 @transaction.atomic
74 @transaction.atomic
69 def create_post(self, title: str, text: str, image=None, thread=None,
75 def create_post(self, title: str, text: str, image=None, thread=None,
70 ip=NO_IP, tags: list=None, threads: list=None):
76 ip=NO_IP, tags: list=None, opening_posts: list=None):
71 """
77 """
72 Creates new post
78 Creates new post
73 """
79 """
@@ -80,13 +86,14 b' class PostManager(models.Manager):'
80
86
81 if not tags:
87 if not tags:
82 tags = []
88 tags = []
83 if not threads:
89 if not opening_posts:
84 threads = []
90 opening_posts = []
85
91
86 posting_time = timezone.now()
92 posting_time = timezone.now()
87 if not thread:
93 if not thread:
88 thread = boards.models.thread.Thread.objects.create(
94 thread = boards.models.thread.Thread.objects.create(
89 bump_time=posting_time, last_edit_time=posting_time)
95 bump_time=posting_time, last_edit_time=posting_time)
96 list(map(thread.tags.add, tags))
90 new_thread = True
97 new_thread = True
91 else:
98 else:
92 new_thread = False
99 new_thread = False
@@ -110,8 +117,6 b' class PostManager(models.Manager):'
110 if image:
117 if image:
111 post.images.add(PostImage.objects.create_with_hash(image))
118 post.images.add(PostImage.objects.create_with_hash(image))
112
119
113 list(map(thread.add_tag, tags))
114
115 if new_thread:
120 if new_thread:
116 boards.models.thread.Thread.objects.process_oldest_threads()
121 boards.models.thread.Thread.objects.process_oldest_threads()
117 else:
122 else:
@@ -119,8 +124,9 b' class PostManager(models.Manager):'
119 thread.bump()
124 thread.bump()
120 thread.save()
125 thread.save()
121
126
127 post.build_url()
122 post.connect_replies()
128 post.connect_replies()
123 post.connect_threads(threads)
129 post.connect_threads(opening_posts)
124 post.connect_notifications()
130 post.connect_notifications()
125
131
126 post.build_url()
132 post.build_url()
@@ -183,7 +189,7 b' class Post(models.Model, Viewable):'
183
189
184 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
190 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
185 null=True,
191 null=True,
186 blank=True, related_name='rfp+',
192 blank=True, related_name='refposts',
187 db_index=True)
193 db_index=True)
188 refmap = models.TextField(null=True, blank=True)
194 refmap = models.TextField(null=True, blank=True)
189 threads = models.ManyToManyField('Thread', db_index=True)
195 threads = models.ManyToManyField('Thread', db_index=True)
@@ -202,6 +208,11 b' class Post(models.Model, Viewable):'
202 def __str__(self):
208 def __str__(self):
203 return 'P#{}/{}'.format(self.id, self.title)
209 return 'P#{}/{}'.format(self.id, self.title)
204
210
211 def get_referenced_posts(self):
212 threads = self.get_threads().all()
213 return self.referenced_posts.filter(threads__in=threads)\
214 .order_by('pub_time').distinct().all()
215
205 def get_title(self) -> str:
216 def get_title(self) -> str:
206 """
217 """
207 Gets original post title or part of its text.
218 Gets original post title or part of its text.
@@ -235,20 +246,26 b' class Post(models.Model, Viewable):'
235 return self.get_thread().get_opening_post_id() == self.id
246 return self.get_thread().get_opening_post_id() == self.id
236
247
237 def get_absolute_url(self):
248 def get_absolute_url(self):
238 return self.url
249 if self.url:
250 return self.url
251 else:
252 opening_id = self.get_thread().get_opening_post_id()
253 post_url = reverse('thread', kwargs={'post_id': opening_id})
254 if self.id != opening_id:
255 post_url += '#' + str(self.id)
256 return post_url
239
257
240 def get_thread(self):
258 def get_thread(self):
241 return self.thread
259 return self.thread
242
260
243 def get_threads(self) -> list:
261 def get_threads(self) -> QuerySet:
244 """
262 """
245 Gets post's thread.
263 Gets post's thread.
246 """
264 """
247
265
248 return self.threads
266 return self.threads
249
267
250 def get_view(self, moderator=False, need_open_link=False,
268 def get_view(self, *args, **kwargs) -> str:
251 truncated=False, reply_link=False, *args, **kwargs) -> str:
252 """
269 """
253 Renders post's HTML view. Some of the post params can be passed over
270 Renders post's HTML view. Some of the post params can be passed over
254 kwargs for the means of caching (if we view the thread, some params
271 kwargs for the means of caching (if we view the thread, some params
@@ -269,19 +286,21 b' class Post(models.Model, Viewable):'
269 elif not thread.can_bump():
286 elif not thread.can_bump():
270 css_class += ' dead_post'
287 css_class += ' dead_post'
271
288
272 return render_to_string('boards/post.html', {
289 params = dict()
290 for param in POST_VIEW_PARAMS:
291 if param in kwargs:
292 params[param] = kwargs[param]
293
294 params.update({
273 PARAMETER_POST: self,
295 PARAMETER_POST: self,
274 PARAMETER_MODERATOR: moderator,
275 PARAMETER_IS_OPENING: is_opening,
296 PARAMETER_IS_OPENING: is_opening,
276 PARAMETER_THREAD: thread,
297 PARAMETER_THREAD: thread,
277 PARAMETER_CSS_CLASS: css_class,
298 PARAMETER_CSS_CLASS: css_class,
278 PARAMETER_NEED_OPEN_LINK: need_open_link,
279 PARAMETER_TRUNCATED: truncated,
280 PARAMETER_OP_ID: opening_post_id,
299 PARAMETER_OP_ID: opening_post_id,
281 PARAMETER_REPLY_LINK: reply_link,
282 PARAMETER_NEED_OP_DATA: kwargs.get(PARAMETER_NEED_OP_DATA)
283 })
300 })
284
301
302 return render_to_string('boards/post.html', params)
303
285 def get_search_view(self, *args, **kwargs):
304 def get_search_view(self, *args, **kwargs):
286 return self.get_view(need_op_data=True, *args, **kwargs)
305 return self.get_view(need_op_data=True, *args, **kwargs)
287
306
@@ -399,12 +418,7 b' class Post(models.Model, Viewable):'
399 pass
418 pass
400
419
401 def build_url(self):
420 def build_url(self):
402 thread = self.get_thread()
421 self.url = self.get_absolute_url()
403 opening_id = thread.get_opening_post_id()
404 post_url = reverse('thread', kwargs={'post_id': opening_id})
405 if self.id != opening_id:
406 post_url += '#' + str(self.id)
407 self.url = post_url
408 self.save(update_fields=['url'])
422 self.save(update_fields=['url'])
409
423
410 def save(self, force_insert=False, force_update=False, using=None,
424 def save(self, force_insert=False, force_update=False, using=None,
@@ -417,11 +431,9 b' class Post(models.Model, Viewable):'
417
431
418 if self.id:
432 if self.id:
419 for thread in self.get_threads().all():
433 for thread in self.get_threads().all():
420 if thread.can_bump():
421 thread.update_bump_status(exclude_posts=[self])
422 thread.last_edit_time = self.last_edit_time
434 thread.last_edit_time = self.last_edit_time
423
435
424 thread.save(update_fields=['last_edit_time', 'bumpable'])
436 thread.save(update_fields=['last_edit_time'])
425
437
426 super().save(force_insert, force_update, using, update_fields)
438 super().save(force_insert, force_update, using, update_fields)
427
439
@@ -466,11 +478,6 b' class Post(models.Model, Viewable):'
466 pass
478 pass
467
479
468 def connect_threads(self, opening_posts):
480 def connect_threads(self, opening_posts):
469 """
470 If the referenced post is an OP in another thread,
471 make this post multi-thread.
472 """
473
474 for opening_post in opening_posts:
481 for opening_post in opening_posts:
475 threads = opening_post.get_threads().all()
482 threads = opening_post.get_threads().all()
476 for thread in threads:
483 for thread in threads:
@@ -479,5 +486,4 b' class Post(models.Model, Viewable):'
479
486
480 thread.last_edit_time = self.last_edit_time
487 thread.last_edit_time = self.last_edit_time
481 thread.save(update_fields=['last_edit_time', 'bumpable'])
488 thread.save(update_fields=['last_edit_time', 'bumpable'])
482
489 self.threads.add(opening_post.get_thread())
483 self.threads.add(thread)
@@ -30,6 +30,7 b" ATTR_MIMETYPE = 'mimetype'"
30 STATUS_SUCCESS = 'success'
30 STATUS_SUCCESS = 'success'
31
31
32
32
33 # TODO Make this fully static
33 class SyncManager:
34 class SyncManager:
34 def generate_response_get(self, model_list: list):
35 def generate_response_get(self, model_list: list):
35 response = et.Element(TAG_RESPONSE)
36 response = et.Element(TAG_RESPONSE)
@@ -1,7 +1,7 b''
1 import logging
1 import logging
2 from adjacent import Client
2 from adjacent import Client
3
3
4 from django.db.models import Count, Sum
4 from django.db.models import Count, Sum, QuerySet
5 from django.utils import timezone
5 from django.utils import timezone
6 from django.db import models
6 from django.db import models
7
7
@@ -72,7 +72,7 b' class Thread(models.Model):'
72 bumpable = models.BooleanField(default=True)
72 bumpable = models.BooleanField(default=True)
73 max_posts = models.IntegerField(default=get_thread_max_posts)
73 max_posts = models.IntegerField(default=get_thread_max_posts)
74
74
75 def get_tags(self) -> list:
75 def get_tags(self) -> QuerySet:
76 """
76 """
77 Gets a sorted tag list.
77 Gets a sorted tag list.
78 """
78 """
@@ -118,7 +118,7 b' class Thread(models.Model):'
118
118
119 return self.bumpable and not self.archived
119 return self.bumpable and not self.archived
120
120
121 def get_last_replies(self) -> list:
121 def get_last_replies(self) -> QuerySet:
122 """
122 """
123 Gets several last replies, not including opening post
123 Gets several last replies, not including opening post
124 """
124 """
@@ -145,7 +145,7 b' class Thread(models.Model):'
145 reply_count - 1)
145 reply_count - 1)
146 return reply_count - last_replies_count - 1
146 return reply_count - last_replies_count - 1
147
147
148 def get_replies(self, view_fields_only=False) -> list:
148 def get_replies(self, view_fields_only=False) -> QuerySet:
149 """
149 """
150 Gets sorted thread posts
150 Gets sorted thread posts
151 """
151 """
@@ -156,7 +156,10 b' class Thread(models.Model):'
156 query = query.defer('poster_ip')
156 query = query.defer('poster_ip')
157 return query.all()
157 return query.all()
158
158
159 def get_replies_with_images(self, view_fields_only=False) -> list:
159 def get_top_level_replies(self) -> QuerySet:
160 return self.get_replies().exclude(refposts__threads__in=[self])
161
162 def get_replies_with_images(self, view_fields_only=False) -> QuerySet:
160 """
163 """
161 Gets replies that have at least one image attached
164 Gets replies that have at least one image attached
162 """
165 """
@@ -164,14 +167,6 b' class Thread(models.Model):'
164 return self.get_replies(view_fields_only).annotate(images_count=Count(
167 return self.get_replies(view_fields_only).annotate(images_count=Count(
165 'images')).filter(images_count__gt=0)
168 'images')).filter(images_count__gt=0)
166
169
167 # TODO Do we still need this?
168 def add_tag(self, tag: Tag):
169 """
170 Connects thread to a tag and tag to a thread
171 """
172
173 self.tags.add(tag)
174
175 def get_opening_post(self, only_id=False) -> Post:
170 def get_opening_post(self, only_id=False) -> Post:
176 """
171 """
177 Gets the first post of the thread
172 Gets the first post of the thread
@@ -199,16 +194,6 b' class Thread(models.Model):'
199
194
200 return self.get_opening_post().pub_time
195 return self.get_opening_post().pub_time
201
196
202 def delete(self, using=None):
203 """
204 Deletes thread with all replies.
205 """
206
207 for reply in self.get_replies().all():
208 reply.delete()
209
210 super(Thread, self).delete(using)
211
212 def __str__(self):
197 def __str__(self):
213 return 'T#{}/{}'.format(self.id, self.get_opening_post_id())
198 return 'T#{}/{}'.format(self.id, self.get_opening_post_id())
214
199
@@ -216,13 +201,15 b' class Thread(models.Model):'
216 return boards.models.Tag.objects.get_tag_url_list(self.get_tags())
201 return boards.models.Tag.objects.get_tag_url_list(self.get_tags())
217
202
218 def update_posts_time(self, exclude_posts=None):
203 def update_posts_time(self, exclude_posts=None):
204 last_edit_time = self.last_edit_time
205
219 for post in self.post_set.all():
206 for post in self.post_set.all():
220 if exclude_posts is not None and post not in exclude_posts:
207 if exclude_posts is None or post not in exclude_posts:
221 # Manual update is required because uids are generated on save
208 # Manual update is required because uids are generated on save
222 post.last_edit_time = self.last_edit_time
209 post.last_edit_time = last_edit_time
223 post.save(update_fields=['last_edit_time'])
210 post.save(update_fields=['last_edit_time'])
224
211
225 post.threads.update(last_edit_time=self.last_edit_time)
212 post.get_threads().update(last_edit_time=last_edit_time)
226
213
227 def notify_clients(self):
214 def notify_clients(self):
228 if not settings.get_bool('External', 'WebsocketsEnabled'):
215 if not settings.get_bool('External', 'WebsocketsEnabled'):
@@ -99,3 +99,7 b' textarea, input {'
99 .post-image-full {
99 .post-image-full {
100 width: 100%;
100 width: 100%;
101 }
101 }
102
103 #preview-text {
104 display: none;
105 }
@@ -232,11 +232,11 b' blockquote {'
232 }
232 }
233
233
234 .dead_post {
234 .dead_post {
235 background-color: #442222;
235 border-left: solid 5px #982C2C;
236 }
236 }
237
237
238 .archive_post {
238 .archive_post {
239 background-color: #000;
239 border-left: solid 5px #B7B7B7;
240 }
240 }
241
241
242 .mark_btn {
242 .mark_btn {
@@ -534,3 +534,22 b' ul {'
534 .post-button-form > button:hover {
534 .post-button-form > button:hover {
535 text-decoration: underline;
535 text-decoration: underline;
536 }
536 }
537
538 .tree_reply > .post {
539 margin-left: 1ex;
540 margin-top: 1ex;
541 border-left: solid 1px #777;
542 border-right: solid 1px #777;
543 }
544
545 #preview-text {
546 border: solid 1px white;
547 margin: 1ex 0 1ex 0;
548 padding: 1ex;
549 }
550
551 button {
552 border: 1px solid white;
553 margin-bottom: .5ex;
554 margin-top: .5ex;
555 }
@@ -28,7 +28,7 b' html {'
28 }
28 }
29
29
30 .link, a {
30 .link, a {
31 color: rgb(255, 102, 0);
31 color: #ff7000;
32 }
32 }
33
33
34 .block {
34 .block {
@@ -187,7 +187,11 b' blockquote {'
187 }
187 }
188
188
189 .dead_post {
189 .dead_post {
190 background-color: #ecc;
190 border-top: solid #d5494f;
191 }
192
193 .archive_post {
194 border-top: solid #575e9f;
191 }
195 }
192
196
193 .quote {
197 .quote {
@@ -366,3 +370,9 b' input[type="submit"]:hover {'
366 .highlight {
370 .highlight {
367 background-color: #F9E8A5;
371 background-color: #F9E8A5;
368 }
372 }
373
374 #preview-text {
375 border: solid 1px black;
376 margin: 1ex 0 1ex 0;
377 padding: 1ex;
378 }
@@ -401,3 +401,9 b' li {'
401 .dead_post {
401 .dead_post {
402 border-right: 1ex solid #666;
402 border-right: 1ex solid #666;
403 }
403 }
404
405 #preview-text {
406 border: solid 1px white;
407 margin: 1ex 0 1ex 0;
408 padding: 1ex;
409 }
1 NO CONTENT: modified file, binary diff hidden
NO CONTENT: modified file, binary diff hidden
@@ -24,4 +24,20 b" var form = $('#form');"
24 if (event.which == 13 && event.ctrlKey) {
24 if (event.which == 13 && event.ctrlKey) {
25 form.submit();
25 form.submit();
26 }
26 }
27 }); No newline at end of file
27 });
28
29 $('#preview-button').click(function() {
30 var data = {
31 raw_text: $('textarea').val()
32 }
33
34 var diffUrl = '/api/preview/';
35
36 $.post(diffUrl,
37 data,
38 function(data) {
39 var previewTextBlock = $('#preview-text');
40 previewTextBlock.html(data);
41 previewTextBlock.show();
42 })
43 })
@@ -104,10 +104,11 b' function getThreadDiff() {'
104 }
104 }
105
105
106 var data = {
106 var data = {
107 uids: uids
107 uids: uids,
108 thread: threadId
108 }
109 }
109
110
110 var diffUrl = '/api/diff_thread?thread=' + threadId;
111 var diffUrl = '/api/diff_thread/';
111
112
112 $.post(diffUrl,
113 $.post(diffUrl,
113 data,
114 data,
@@ -309,6 +310,7 b' function resetForm(form) {'
309 form.find('input:radio, input:checkbox')
310 form.find('input:radio, input:checkbox')
310 .removeAttr('checked').removeAttr('selected');
311 .removeAttr('checked').removeAttr('selected');
311 $('.file_wrap').find('.file-thumb').remove();
312 $('.file_wrap').find('.file-thumb').remove();
313 $('#preview-text').hide();
312 }
314 }
313
315
314 /**
316 /**
@@ -373,6 +375,9 b' function processNewPost(post) {'
373 showAsErrors($('form'), gettext('Sending message...'));
375 showAsErrors($('form'), gettext('Sending message...'));
374 },
376 },
375 success: updateOnPost,
377 success: updateOnPost,
378 error: function() {
379 showAsErrors($('form'), gettext('Server error!'));
380 },
376 url: '/api/add_post/' + threadId + '/'
381 url: '/api/add_post/' + threadId + '/'
377 };
382 };
378
383
@@ -122,12 +122,10 b''
122 <div>
122 <div>
123 {% trans 'Tags must be delimited by spaces. Text or image is required.' %}
123 {% trans 'Tags must be delimited by spaces. Text or image is required.' %}
124 </div>
124 </div>
125 <div>
125 <div><button id="preview-button">{% trans 'Preview' %}</button></div>
126 <a href="{% url "staticpage" name="help" %}">{% trans 'Text syntax' %}</a>
126 <div id="preview-text"></div>
127 </div>
127 <div><a href="{% url "staticpage" name="help" %}">{% trans 'Text syntax' %}</a></div>
128 <div>
128 <div><a href="{% url "tags" "required" %}">{% trans 'Tags' %}</a></div>
129 <a href="{% url "tags" "required" %}">{% trans 'Tags' %}</a>
130 </div>
131 </div>
129 </div>
132 </div>
130 </div>
133
131
@@ -141,36 +139,27 b''
141 <span class="metapanel">
139 <span class="metapanel">
142 <b><a href="{% url "authors" %}">{{ site_name }}</a> {{ version }}</b>
140 <b><a href="{% url "authors" %}">{{ site_name }}</a> {{ version }}</b>
143 {% trans "Pages:" %}
141 {% trans "Pages:" %}
144 <a href="
145 {% if tag %}
146 {% url "tag" tag_name=tag.name page=paginator.page_range|first %}
147 {% else %}
148 {% url "index" page=paginator.page_range|first %}
149 {% endif %}
150 ">&lt;&lt;</a>
151 [
142 [
152 {% for page in paginator.center_range %}
143 {% with dividers=paginator.get_dividers %}
153 <a
144 {% for page in paginator.get_divided_range %}
154 {% ifequal page current_page.number %}
145 {% if page in dividers %}
155 class="current_page"
146 …,
156 {% endifequal %}
147 {% endif %}
157 href="
148 <a
158 {% if tag %}
149 {% ifequal page current_page.number %}
159 {% url "tag" tag_name=tag.name page=page %}
150 class="current_page"
160 {% else %}
151 {% endifequal %}
161 {% url "index" page=page %}
152 href="
162 {% endif %}
153 {% if tag %}
163 ">{{ page }}</a>
154 {% url "tag" tag_name=tag.name %}?page={{ page }}
164 {% if not forloop.last %},{% endif %}
155 {% else %}
165 {% endfor %}
156 {% url "index" %}?page={{ page }}
157 {% endif %}
158 ">{{ page }}</a>
159 {% if not forloop.last %},{% endif %}
160 {% endfor %}
161 {% endwith %}
166 ]
162 ]
167 <a href="
168 {% if tag %}
169 {% url "tag" tag_name=tag.name page=paginator.page_range|last %}
170 {% else %}
171 {% url "index" page=paginator.page_range|last %}
172 {% endif %}
173 ">&gt;&gt;</a>
174 [<a href="rss/">RSS</a>]
163 [<a href="rss/">RSS</a>]
175 </span>
164 </span>
176
165
@@ -51,24 +51,21 b''
51 <span class="metapanel">
51 <span class="metapanel">
52 <b><a href="{% url "authors" %}">{{ site_name }}</a> {{ version }}</b>
52 <b><a href="{% url "authors" %}">{{ site_name }}</a> {{ version }}</b>
53 {% trans "Pages:" %}
53 {% trans "Pages:" %}
54 <a href="
55 {% url "feed" page=paginator.page_range|first %}
56 ">&lt;&lt;</a>
57 [
54 [
58 {% for page in paginator.center_range %}
55 {% with dividers=paginator.get_dividers %}
59 <a
56 {% for page in paginator.get_divided_range %}
60 {% ifequal page current_page.number %}
57 {% if page in dividers %}
61 class="current_page"
58 …,
62 {% endifequal %}
59 {% endif %}
63 href="
60 <a
64 {% url "feed" page=page %}
61 {% ifequal page current_page.number %}
65 ">{{ page }}</a>
62 class="current_page"
66 {% if not forloop.last %},{% endif %}
63 {% endifequal %}
67 {% endfor %}
64 href="{% url "feed" %}?page={{ page }}">{{ page }}</a>
65 {% if not forloop.last %},{% endif %}
66 {% endfor %}
67 {% endwith %}
68 ]
68 ]
69 <a href="
70 {% url "feed" page=paginator.page_range|last %}
71 ">&gt;&gt;</a>
72 </span>
69 </span>
73
70
74 {% endblock %}
71 {% endblock %}
@@ -73,11 +73,19 b''
73 {% endif %}
73 {% endif %}
74 {% endautoescape %}
74 {% endautoescape %}
75 {% if post.is_referenced %}
75 {% if post.is_referenced %}
76 <div class="refmap">
76 {% if mode_tree %}
77 {% autoescape off %}
77 <div class="tree_reply">
78 {% trans "Replies" %}: {{ post.refmap }}
78 {% for refpost in post.get_referenced_posts %}
79 {% endautoescape %}
79 {% post_view refpost mode_tree=True %}
80 </div>
80 {% endfor %}
81 </div>
82 {% else %}
83 <div class="refmap">
84 {% autoescape off %}
85 {% trans "Replies" %}: {{ post.refmap }}
86 {% endautoescape %}
87 </div>
88 {% endif %}
81 {% endif %}
89 {% endif %}
82 </div>
90 </div>
83 {% comment %}
91 {% comment %}
@@ -16,6 +16,7 b''
16 <p>[comment]<span class="comment">{% trans 'Comment' %}</span>[/comment]</p>
16 <p>[comment]<span class="comment">{% trans 'Comment' %}</span>[/comment]</p>
17 <p>[quote]<span class="quote">&gt;{% trans 'Quote' %}</span>[/quote]</p>
17 <p>[quote]<span class="quote">&gt;{% trans 'Quote' %}</span>[/quote]</p>
18 <p>[quote source=src]<div class="multiquote"><div class="quote-header">src</div><div class="quote-text">{% trans 'Quote' %}</div></div><br />[/quote]</p>
18 <p>[quote source=src]<div class="multiquote"><div class="quote-header">src</div><div class="quote-text">{% trans 'Quote' %}</div></div><br />[/quote]</p>
19 <p>[tag]<a class="tag">tag</a>[/tag]</p>
19 <br/>
20 <br/>
20 <p>{% trans 'You can try pasting the text and previewing the result here:' %} <a href="{% url 'preview' %}">{% trans 'Preview' %}</a></p>
21 <p>{% trans 'You can try pasting the text and previewing the result here:' %} <a href="{% url 'preview' %}">{% trans 'Preview' %}</a></p>
21 {% endblock %}
22 {% endblock %}
@@ -10,6 +10,17 b''
10 - {{ site_name }}</title>
10 - {{ site_name }}</title>
11 {% endblock %}
11 {% endblock %}
12
12
13 {% block content %}
14 <div class="image-mode-tab">
15 <a {% ifequal mode 'normal' %}class="current_mode"{% endifequal %} href="{% url 'thread' opening_post.id %}">{% trans 'Normal' %}</a>,
16 <a {% ifequal mode 'gallery' %}class="current_mode"{% endifequal %} href="{% url 'thread_gallery' opening_post.id %}">{% trans 'Gallery' %}</a>,
17 <a {% ifequal mode 'tree' %}class="current_mode"{% endifequal %} href="{% url 'thread_tree' opening_post.id %}">{% trans 'Tree' %}</a>
18 </div>
19
20 {% block thread_content %}
21 {% endblock %}
22 {% endblock %}
23
13 {% block metapanel %}
24 {% block metapanel %}
14
25
15 <span class="metapanel"
26 <span class="metapanel"
@@ -11,15 +11,10 b''
11 - {{ site_name }}</title>
11 - {{ site_name }}</title>
12 {% endblock %}
12 {% endblock %}
13
13
14 {% block content %}
14 {% block thread_content %}
15 {% get_current_language as LANGUAGE_CODE %}
15 {% get_current_language as LANGUAGE_CODE %}
16 {% get_current_timezone as TIME_ZONE %}
16 {% get_current_timezone as TIME_ZONE %}
17
17
18 <div class="image-mode-tab">
19 <a href="{% url 'thread' thread.get_opening_post.id %}">{% trans 'Normal mode' %}</a>,
20 <a class="current_mode" href="{% url 'thread_gallery' thread.get_opening_post.id %}">{% trans 'Gallery mode' %}</a>
21 </div>
22
23 <div id="posts-table">
18 <div id="posts-table">
24 {% if posts %}
19 {% if posts %}
25 {% for post in posts %}
20 {% for post in posts %}
@@ -5,15 +5,10 b''
5 {% load board %}
5 {% load board %}
6 {% load tz %}
6 {% load tz %}
7
7
8 {% block content %}
8 {% block thread_content %}
9 {% get_current_language as LANGUAGE_CODE %}
9 {% get_current_language as LANGUAGE_CODE %}
10 {% get_current_timezone as TIME_ZONE %}
10 {% get_current_timezone as TIME_ZONE %}
11
11
12 <div class="image-mode-tab">
13 <a class="current_mode" href="{% url 'thread' opening_post.id %}">{% trans 'Normal mode' %}</a>,
14 <a href="{% url 'thread_gallery' opening_post.id %}">{% trans 'Gallery mode' %}</a>
15 </div>
16
17 {% if bumpable and thread.has_post_limit %}
12 {% if bumpable and thread.has_post_limit %}
18 <div class="bar-bg">
13 <div class="bar-bg">
19 <div class="bar-value" style="width:{{ bumplimit_progress }}%" id="bumplimit_progress">
14 <div class="bar-value" style="width:{{ bumplimit_progress }}%" id="bumplimit_progress">
@@ -44,6 +39,8 b''
44 </div>
39 </div>
45 </form>
40 </form>
46 </div>
41 </div>
42 <div><button id="preview-button">{% trans 'Preview' %}</button></div>
43 <div id="preview-text"></div>
47 <div><a href="{% url "staticpage" name="help" %}">
44 <div><a href="{% url "staticpage" name="help" %}">
48 {% trans 'Text syntax' %}</a></div>
45 {% trans 'Text syntax' %}</a></div>
49 <div><a id="form-close-button" href="#" onClick="resetFormPosition(); return false;">{% trans 'Close form' %}</a></div>
46 <div><a id="form-close-button" href="#" onClick="resetFormPosition(); return false;">{% trans 'Close form' %}</a></div>
@@ -36,7 +36,7 b' def image_actions(*args, **kwargs):'
36 image_link = 'http://' + args[1] + image_link # TODO https?
36 image_link = 'http://' + args[1] + image_link # TODO https?
37
37
38 return ', '.join([IMG_ACTION_URL.format(
38 return ', '.join([IMG_ACTION_URL.format(
39 action['link'] % image_link, action['name'])for action in actions])
39 action['link'] % image_link, action['name']) for action in actions])
40
40
41
41
42 @register.simple_tag(name='post_view')
42 @register.simple_tag(name='post_view')
@@ -15,7 +15,7 b' class ApiTest(TestCase):'
15 tags=[tag])
15 tags=[tag])
16
16
17 req = MockRequest()
17 req = MockRequest()
18 req.GET['thread'] = opening_post.id
18 req.POST['thread'] = opening_post.id
19 req.POST['uids'] = opening_post.uid
19 req.POST['uids'] = opening_post.uid
20 # Check the exact timestamp post was added
20 # Check the exact timestamp post was added
21 empty_response = api.api_get_threaddiff(req)
21 empty_response = api.api_get_threaddiff(req)
@@ -29,7 +29,7 b' class ApiTest(TestCase):'
29 text='[post]%d[/post]\ntext' % opening_post.id,
29 text='[post]%d[/post]\ntext' % opening_post.id,
30 thread=opening_post.get_thread())
30 thread=opening_post.get_thread())
31 req = MockRequest()
31 req = MockRequest()
32 req.GET['thread'] = opening_post.id
32 req.POST['thread'] = opening_post.id
33 req.POST['uids'] = ' '.join(uids)
33 req.POST['uids'] = ' '.join(uids)
34 # Check the timestamp before post was added
34 # Check the timestamp before post was added
35 response = api.api_get_threaddiff(req)
35 response = api.api_get_threaddiff(req)
@@ -40,7 +40,7 b' class ApiTest(TestCase):'
40 # Reload post to get the new UID
40 # Reload post to get the new UID
41 opening_post = Post.objects.get(id=opening_post.id)
41 opening_post = Post.objects.get(id=opening_post.id)
42 req = MockRequest()
42 req = MockRequest()
43 req.GET['thread'] = opening_post.id
43 req.POST['thread'] = opening_post.id
44 req.POST['uids'] = ' '.join([opening_post.uid, reply.uid])
44 req.POST['uids'] = ' '.join([opening_post.uid, reply.uid])
45 empty_response = api.api_get_threaddiff(req)
45 empty_response = api.api_get_threaddiff(req)
46 diff = simplejson.loads(empty_response.content)
46 diff = simplejson.loads(empty_response.content)
@@ -3,7 +3,7 b' import logging'
3
3
4 from django.test import TestCase
4 from django.test import TestCase
5 from boards.models import KeyPair, GlobalId, Post
5 from boards.models import KeyPair, GlobalId, Post
6
6 from boards.models.post.sync import SyncManager
7
7
8 logger = logging.getLogger(__name__)
8 logger = logging.getLogger(__name__)
9
9
@@ -56,7 +56,7 b' class KeyTest(TestCase):'
56 text='[post]%d[/post]' % post.id,
56 text='[post]%d[/post]' % post.id,
57 thread=post.get_thread())
57 thread=post.get_thread())
58
58
59 response = Post.objects.generate_response_get([reply_post])
59 response = SyncManager().generate_response_get([reply_post])
60 logger.debug(response)
60 logger.debug(response)
61
61
62 key = KeyPair.objects.get(primary=True)
62 key = KeyPair.objects.get(primary=True)
@@ -1,5 +1,6 b''
1 from django.core.paginator import Paginator
1 from django.core.paginator import Paginator
2 from django.test import TestCase
2 from django.test import TestCase
3
3 from boards import settings
4 from boards import settings
4 from boards.models import Tag, Post, Thread, KeyPair
5 from boards.models import Tag, Post, Thread, KeyPair
5
6
@@ -145,8 +146,8 b' class PostTests(TestCase):'
145 tags=[tag])
146 tags=[tag])
146 thread = opening_post.get_thread()
147 thread = opening_post.get_thread()
147
148
148 reply1 = Post.objects.create_post(title='title', text='text', thread=thread)
149 Post.objects.create_post(title='title', text='text', thread=thread)
149 reply2 = Post.objects.create_post(title='title', text='text', thread=thread)
150 Post.objects.create_post(title='title', text='text', thread=thread)
150
151
151 replies = thread.get_replies()
152 replies = thread.get_replies()
152 self.assertTrue(len(replies) > 0, 'No replies found for thread.')
153 self.assertTrue(len(replies) > 0, 'No replies found for thread.')
@@ -154,3 +155,45 b' class PostTests(TestCase):'
154 replies = thread.get_replies(view_fields_only=True)
155 replies = thread.get_replies(view_fields_only=True)
155 self.assertTrue(len(replies) > 0,
156 self.assertTrue(len(replies) > 0,
156 'No replies found for thread with view fields only.')
157 'No replies found for thread with view fields only.')
158
159 def test_bumplimit(self):
160 """
161 Tests that the thread bumpable status is changed and post uids and
162 last update times are updated across all post threads.
163 """
164
165 op1 = Post.objects.create_post(title='title', text='text')
166 op2 = Post.objects.create_post(title='title', text='text')
167
168 thread1 = op1.get_thread()
169 thread1.max_posts = 5
170 thread1.save()
171
172 uid_1 = op1.uid
173 uid_2 = op2.uid
174
175 # Create multi reply
176 Post.objects.create_post(
177 title='title', text='text', thread=thread1,
178 opening_posts=[op1, op2])
179 thread_update_time_2 = op2.get_thread().last_edit_time
180 for i in range(6):
181 Post.objects.create_post(title='title', text='text',
182 thread=thread1)
183
184 self.assertFalse(op1.get_thread().can_bump(),
185 'Thread is bumpable when it should not be.')
186 self.assertTrue(op2.get_thread().can_bump(),
187 'Thread is not bumpable when it should be.')
188 self.assertNotEqual(
189 uid_1, Post.objects.get(id=op1.id).uid,
190 'UID of the first OP should be changed but it is not.')
191 self.assertEqual(
192 uid_2, Post.objects.get(id=op2.id).uid,
193 'UID of the first OP should not be changed but it is.')
194
195 self.assertNotEqual(
196 thread_update_time_2,
197 Thread.objects.get(id=op2.get_thread().id).last_edit_time,
198 'Thread last update time should change when the other thread '
199 'changes status.')
@@ -1,6 +1,6 b''
1 from boards.models import KeyPair, Post
1 from boards.models import KeyPair, Post
2 from boards.tests.mocks import MockRequest
2 from boards.tests.mocks import MockRequest
3 from boards.views.sync import respond_get
3 from boards.views.sync import response_get
4
4
5 __author__ = 'neko259'
5 __author__ = 'neko259'
6
6
@@ -18,7 +18,7 b' class SyncTest(TestCase):'
18 post = Post.objects.create_post(title='test_title', text='test_text')
18 post = Post.objects.create_post(title='test_title', text='test_text')
19
19
20 request = MockRequest()
20 request = MockRequest()
21 request.POST['xml'] = (
21 request.body = (
22 '<request type="get" version="1.0">'
22 '<request type="get" version="1.0">'
23 '<model name="post" version="1.0">'
23 '<model name="post" version="1.0">'
24 '<id key="%s" local-id="%d" type="%s" />'
24 '<id key="%s" local-id="%d" type="%s" />'
@@ -44,5 +44,5 b' class SyncTest(TestCase):'
44 post.title,
44 post.title,
45 post.get_raw_text(),
45 post.get_raw_text(),
46 post.get_pub_time_epoch(),
46 post.get_pub_time_epoch(),
47 ) in respond_get(request).content.decode(),
47 ) in response_get(request).content.decode(),
48 'Wrong response generated for the GET request.')
48 'Wrong response generated for the GET request.')
@@ -20,26 +20,20 b' js_info_dict = {'
20 urlpatterns = patterns('',
20 urlpatterns = patterns('',
21 # /boards/
21 # /boards/
22 url(r'^$', all_threads.AllThreadsView.as_view(), name='index'),
22 url(r'^$', all_threads.AllThreadsView.as_view(), name='index'),
23 # /boards/page/
24 url(r'^page/(?P<page>\w+)/$', all_threads.AllThreadsView.as_view(),
25 name='index'),
26
23
27 # /boards/tag/tag_name/
24 # /boards/tag/tag_name/
28 url(r'^tag/(?P<tag_name>\w+)/$', tag_threads.TagView.as_view(),
25 url(r'^tag/(?P<tag_name>\w+)/$', tag_threads.TagView.as_view(),
29 name='tag'),
26 name='tag'),
30 # /boards/tag/tag_id/page/
31 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/$',
32 tag_threads.TagView.as_view(), name='tag'),
33
27
34 # /boards/thread/
28 # /boards/thread/
35 url(r'^thread/(?P<post_id>\d+)/$', views.thread.normal.NormalThreadView.as_view(),
29 url(r'^thread/(?P<post_id>\d+)/$', views.thread.NormalThreadView.as_view(),
36 name='thread'),
30 name='thread'),
37 url(r'^thread/(?P<post_id>\d+)/mode/gallery/$', views.thread.gallery.GalleryThreadView.as_view(),
31 url(r'^thread/(?P<post_id>\d+)/mode/gallery/$', views.thread.GalleryThreadView.as_view(),
38 name='thread_gallery'),
32 name='thread_gallery'),
33 url(r'^thread/(?P<post_id>\d+)/mode/tree/$', views.thread.TreeThreadView.as_view(),
34 name='thread_tree'),
39 # /feed/
35 # /feed/
40 url(r'^feed/$', views.feed.FeedView.as_view(), name='feed'),
36 url(r'^feed/$', views.feed.FeedView.as_view(), name='feed'),
41 url(r'^feed/page/(?P<page>\w+)/$', views.feed.FeedView.as_view(),
42 name='feed'),
43
37
44 url(r'^settings/$', settings.SettingsView.as_view(), name='settings'),
38 url(r'^settings/$', settings.SettingsView.as_view(), name='settings'),
45 url(r'^tags/(?P<query>\w+)?/?$', all_tags.AllTagsView.as_view(), name='tags'),
39 url(r'^tags/(?P<query>\w+)?/?$', all_tags.AllTagsView.as_view(), name='tags'),
@@ -62,8 +56,7 b" urlpatterns = patterns('',"
62
56
63 # API
57 # API
64 url(r'^api/post/(?P<post_id>\d+)/$', api.get_post, name="get_post"),
58 url(r'^api/post/(?P<post_id>\d+)/$', api.get_post, name="get_post"),
65 url(r'^api/diff_thread$',
59 url(r'^api/diff_thread/$', api.api_get_threaddiff, name="get_thread_diff"),
66 api.api_get_threaddiff, name="get_thread_diff"),
67 url(r'^api/threads/(?P<count>\w+)/$', api.api_get_threads,
60 url(r'^api/threads/(?P<count>\w+)/$', api.api_get_threads,
68 name='get_threads'),
61 name='get_threads'),
69 url(r'^api/tags/$', api.api_get_tags, name='get_tags'),
62 url(r'^api/tags/$', api.api_get_tags, name='get_tags'),
@@ -73,6 +66,7 b" urlpatterns = patterns('',"
73 name='add_post'),
66 name='add_post'),
74 url(r'^api/notifications/(?P<username>\w+)/$', api.api_get_notifications,
67 url(r'^api/notifications/(?P<username>\w+)/$', api.api_get_notifications,
75 name='api_notifications'),
68 name='api_notifications'),
69 url(r'^api/preview/$', api.api_get_preview, name='preview'),
76
70
77 # Sync protocol API
71 # Sync protocol API
78 url(r'^api/sync/pull/$', api.sync_pull, name='api_sync_pull'),
72 url(r'^api/sync/pull/$', api.sync_pull, name='api_sync_pull'),
@@ -43,7 +43,9 b' class AllThreadsView(PostMixin, BaseBoar'
43 self.settings_manager = None
43 self.settings_manager = None
44 super(AllThreadsView, self).__init__()
44 super(AllThreadsView, self).__init__()
45
45
46 def get(self, request, page=DEFAULT_PAGE, form: ThreadForm=None):
46 def get(self, request, form: ThreadForm=None):
47 page = request.GET.get('page', DEFAULT_PAGE)
48
47 params = self.get_context_data(request=request)
49 params = self.get_context_data(request=request)
48
50
49 if not form:
51 if not form:
@@ -67,7 +69,7 b' class AllThreadsView(PostMixin, BaseBoar'
67
69
68 return render(request, TEMPLATE, params)
70 return render(request, TEMPLATE, params)
69
71
70 def post(self, request, page=DEFAULT_PAGE):
72 def post(self, request):
71 form = ThreadForm(request.POST, request.FILES,
73 form = ThreadForm(request.POST, request.FILES,
72 error_class=PlainErrorList)
74 error_class=PlainErrorList)
73 form.session = request.session
75 form.session = request.session
@@ -78,7 +80,7 b' class AllThreadsView(PostMixin, BaseBoar'
78 # Ban user because he is suspected to be a bot
80 # Ban user because he is suspected to be a bot
79 self._ban_current_user(request)
81 self._ban_current_user(request)
80
82
81 return self.get(request, page, form)
83 return self.get(request, form)
82
84
83 def get_page_context(self, paginator, params, page):
85 def get_page_context(self, paginator, params, page):
84 """
86 """
@@ -95,14 +97,12 b' class AllThreadsView(PostMixin, BaseBoar'
95 params[PARAMETER_NEXT_LINK] = self.get_next_page_link(current_page)
97 params[PARAMETER_NEXT_LINK] = self.get_next_page_link(current_page)
96
98
97 def get_previous_page_link(self, current_page):
99 def get_previous_page_link(self, current_page):
98 return reverse('index', kwargs={
100 return reverse('index') + '?page=' \
99 'page': current_page.previous_page_number(),
101 + str(current_page.previous_page_number())
100 })
101
102
102 def get_next_page_link(self, current_page):
103 def get_next_page_link(self, current_page):
103 return reverse('index', kwargs={
104 return reverse('index') + '?page=' \
104 'page': current_page.next_page_number(),
105 + str(current_page.next_page_number())
105 })
106
106
107 @staticmethod
107 @staticmethod
108 def parse_tags_string(tag_strings):
108 def parse_tags_string(tag_strings):
@@ -151,7 +151,7 b' class AllThreadsView(PostMixin, BaseBoar'
151 tags = self.parse_tags_string(tag_strings)
151 tags = self.parse_tags_string(tag_strings)
152
152
153 post = Post.objects.create_post(title=title, text=text, image=image,
153 post = Post.objects.create_post(title=title, text=text, image=image,
154 ip=ip, tags=tags, threads=threads)
154 ip=ip, tags=tags, opening_posts=threads)
155
155
156 # This is required to update the threads to which posts we have replied
156 # This is required to update the threads to which posts we have replied
157 # when creating this one
157 # when creating this one
@@ -14,6 +14,7 b' from boards.models.post.sync import Sync'
14 from boards.utils import datetime_to_epoch
14 from boards.utils import datetime_to_epoch
15 from boards.views.thread import ThreadView
15 from boards.views.thread import ThreadView
16 from boards.models.user import Notification
16 from boards.models.user import Notification
17 from boards.mdx_neboard import Parser
17
18
18
19
19 __author__ = 'neko259'
20 __author__ = 'neko259'
@@ -43,7 +44,7 b' def api_get_threaddiff(request):'
43 Gets posts that were changed or added since time
44 Gets posts that were changed or added since time
44 """
45 """
45
46
46 thread_id = request.GET.get(PARAMETER_THREAD)
47 thread_id = request.POST.get(PARAMETER_THREAD)
47 uids_str = request.POST.get(PARAMETER_UIDS).strip()
48 uids_str = request.POST.get(PARAMETER_UIDS).strip()
48 uids = uids_str.split(' ')
49 uids = uids_str.split(' ')
49
50
@@ -58,7 +59,8 b' def api_get_threaddiff(request):'
58 diff_type = request.GET.get(PARAMETER_DIFF_TYPE, DIFF_TYPE_HTML)
59 diff_type = request.GET.get(PARAMETER_DIFF_TYPE, DIFF_TYPE_HTML)
59
60
60 for post in posts:
61 for post in posts:
61 json_data[PARAMETER_UPDATED].append(get_post_data(post.id, diff_type, request))
62 json_data[PARAMETER_UPDATED].append(get_post_data(post.id, diff_type,
63 request))
62 json_data[PARAMETER_LAST_UPDATE] = str(thread.last_edit_time)
64 json_data[PARAMETER_LAST_UPDATE] = str(thread.last_edit_time)
63
65
64 return HttpResponse(content=json.dumps(json_data))
66 return HttpResponse(content=json.dumps(json_data))
@@ -234,6 +236,13 b' def get_post_data(post_id, format_type=D'
234 include_last_update=include_last_update)
236 include_last_update=include_last_update)
235
237
236
238
239 def api_get_preview(request):
240 raw_text = request.POST['raw_text']
241
242 parser = Parser()
243 return HttpResponse(content=parser.parse(parser.preparse(raw_text)))
244
245
237 # TODO Make a separate module for sync API methods
246 # TODO Make a separate module for sync API methods
238 def sync_pull(request):
247 def sync_pull(request):
239 """
248 """
@@ -28,7 +28,9 b' DEFAULT_PAGE = 1'
28
28
29 class FeedView(PostMixin, BaseBoardView):
29 class FeedView(PostMixin, BaseBoardView):
30
30
31 def get(self, request, page=DEFAULT_PAGE):
31 def get(self, request):
32 page = request.GET.get('page', DEFAULT_PAGE)
33
32 params = self.get_context_data(request=request)
34 params = self.get_context_data(request=request)
33
35
34 settings_manager = get_settings_manager(request)
36 settings_manager = get_settings_manager(request)
@@ -61,11 +63,9 b' class FeedView(PostMixin, BaseBoardView)'
61 params[PARAMETER_NEXT_LINK] = self.get_next_page_link(current_page)
63 params[PARAMETER_NEXT_LINK] = self.get_next_page_link(current_page)
62
64
63 def get_previous_page_link(self, current_page):
65 def get_previous_page_link(self, current_page):
64 return reverse('feed', kwargs={
66 return reverse('feed') + '?page={}'.format(
65 'page': current_page.previous_page_number(),
67 current_page.previous_page_number())
66 })
67
68
68 def get_next_page_link(self, current_page):
69 def get_next_page_link(self, current_page):
69 return reverse('feed', kwargs={
70 return reverse('feed') + '?page={}'.format(
70 'page': current_page.next_page_number(),
71 current_page.next_page_number())
71 })
@@ -52,22 +52,20 b' class TagView(AllThreadsView, Dispatcher'
52 def get_previous_page_link(self, current_page):
52 def get_previous_page_link(self, current_page):
53 return reverse('tag', kwargs={
53 return reverse('tag', kwargs={
54 'tag_name': self.tag_name,
54 'tag_name': self.tag_name,
55 'page': current_page.previous_page_number(),
55 }) + '?page=' + str(current_page.previous_page_number())
56 })
57
56
58 def get_next_page_link(self, current_page):
57 def get_next_page_link(self, current_page):
59 return reverse('tag', kwargs={
58 return reverse('tag', kwargs={
60 'tag_name': self.tag_name,
59 'tag_name': self.tag_name,
61 'page': current_page.next_page_number(),
60 }) + '?page=' + str(current_page.next_page_number())
62 })
63
61
64 def get(self, request, tag_name, page=DEFAULT_PAGE, form=None):
62 def get(self, request, tag_name, form=None):
65 self.tag_name = tag_name
63 self.tag_name = tag_name
66
64
67 return super(TagView, self).get(request, page, form)
65 return super(TagView, self).get(request, form)
68
66
69
67
70 def post(self, request, tag_name, page=DEFAULT_PAGE):
68 def post(self, request, tag_name):
71 self.tag_name = tag_name
69 self.tag_name = tag_name
72
70
73 if 'method' in request.POST:
71 if 'method' in request.POST:
@@ -1,3 +1,4 b''
1 from boards.views.thread.thread import ThreadView
1 from boards.views.thread.thread import ThreadView
2 from boards.views.thread.normal import NormalThreadView
2 from boards.views.thread.normal import NormalThreadView
3 from boards.views.thread.gallery import GalleryThreadView
3 from boards.views.thread.gallery import GalleryThreadView
4 from boards.views.thread.tree import TreeThreadView
@@ -17,3 +17,6 b' class GalleryThreadView(ThreadView):'
17 view_fields_only=True)
17 view_fields_only=True)
18
18
19 return params
19 return params
20
21 def get_mode(self):
22 return 'gallery'
@@ -1,9 +1,7 b''
1 from boards import settings
2 from boards.views.thread import ThreadView
1 from boards.views.thread import ThreadView
3
2
4 TEMPLATE_NORMAL = 'boards/thread_normal.html'
3 TEMPLATE_NORMAL = 'boards/thread_normal.html'
5
4
6 CONTEXT_OP = 'opening_post'
7 CONTEXT_BUMPLIMIT_PRG = 'bumplimit_progress'
5 CONTEXT_BUMPLIMIT_PRG = 'bumplimit_progress'
8 CONTEXT_POSTS_LEFT = 'posts_left'
6 CONTEXT_POSTS_LEFT = 'posts_left'
9 CONTEXT_BUMPABLE = 'bumpable'
7 CONTEXT_BUMPABLE = 'bumpable'
@@ -14,6 +12,9 b' class NormalThreadView(ThreadView):'
14 def get_template(self):
12 def get_template(self):
15 return TEMPLATE_NORMAL
13 return TEMPLATE_NORMAL
16
14
15 def get_mode(self):
16 return 'normal'
17
17 def get_data(self, thread):
18 def get_data(self, thread):
18 params = dict()
19 params = dict()
19
20
@@ -26,6 +27,4 b' class NormalThreadView(ThreadView):'
26 params[CONTEXT_BUMPLIMIT_PRG] = str(
27 params[CONTEXT_BUMPLIMIT_PRG] = str(
27 float(left_posts) / max_posts * 100)
28 float(left_posts) / max_posts * 100)
28
29
29 params[CONTEXT_OP] = thread.get_opening_post()
30
31 return params
30 return params
@@ -21,6 +21,8 b" CONTEXT_WS_PROJECT = 'ws_project'"
21 CONTEXT_WS_HOST = 'ws_host'
21 CONTEXT_WS_HOST = 'ws_host'
22 CONTEXT_WS_PORT = 'ws_port'
22 CONTEXT_WS_PORT = 'ws_port'
23 CONTEXT_WS_TIME = 'ws_token_time'
23 CONTEXT_WS_TIME = 'ws_token_time'
24 CONTEXT_MODE = 'mode'
25 CONTEXT_OP = 'opening_post'
24
26
25 FORM_TITLE = 'title'
27 FORM_TITLE = 'title'
26 FORM_TEXT = 'text'
28 FORM_TEXT = 'text'
@@ -51,6 +53,8 b' class ThreadView(BaseBoardView, PostMixi'
51 params[CONTEXT_FORM] = form
53 params[CONTEXT_FORM] = form
52 params[CONTEXT_LASTUPDATE] = str(thread_to_show.last_edit_time)
54 params[CONTEXT_LASTUPDATE] = str(thread_to_show.last_edit_time)
53 params[CONTEXT_THREAD] = thread_to_show
55 params[CONTEXT_THREAD] = thread_to_show
56 params[CONTEXT_MODE] = self.get_mode()
57 params[CONTEXT_OP] = opening_post
54
58
55 if settings.get_bool('External', 'WebsocketsEnabled'):
59 if settings.get_bool('External', 'WebsocketsEnabled'):
56 token_time = format(timezone.now(), u'U')
60 token_time = format(timezone.now(), u'U')
@@ -107,7 +111,7 b' class ThreadView(BaseBoardView, PostMixi'
107
111
108 post = Post.objects.create_post(title=title, text=text, image=image,
112 post = Post.objects.create_post(title=title, text=text, image=image,
109 thread=post_thread, ip=ip,
113 thread=post_thread, ip=ip,
110 threads=threads)
114 opening_posts=threads)
111 post.notify_clients()
115 post.notify_clients()
112
116
113 if html_response:
117 if html_response:
@@ -116,16 +120,19 b' class ThreadView(BaseBoardView, PostMixi'
116 else:
120 else:
117 return post
121 return post
118
122
119 def get_data(self, thread):
123 def get_data(self, thread) -> dict:
120 """
124 """
121 Returns context params for the view.
125 Returns context params for the view.
122 """
126 """
123
127
124 pass
128 return dict()
125
129
126 def get_template(self):
130 def get_template(self) -> str:
127 """
131 """
128 Gets template to show the thread mode on.
132 Gets template to show the thread mode on.
129 """
133 """
130
134
131 pass
135 pass
136
137 def get_mode(self) -> str:
138 pass
@@ -83,7 +83,6 b' STATICFILES_DIRS = ('
83 STATICFILES_FINDERS = (
83 STATICFILES_FINDERS = (
84 'django.contrib.staticfiles.finders.FileSystemFinder',
84 'django.contrib.staticfiles.finders.FileSystemFinder',
85 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
85 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
86 'compressor.finders.CompressorFinder',
87 )
86 )
88
87
89 if DEBUG:
88 if DEBUG:
@@ -12,7 +12,7 b' Site: http://neboard.me/'
12 1. Install all dependencies over pip or system-wide
12 1. Install all dependencies over pip or system-wide
13 2. Setup a database in `neboard/settings.py`
13 2. Setup a database in `neboard/settings.py`
14 3. Run `./manage.py migrate` to apply all south migrations
14 3. Run `./manage.py migrate` to apply all south migrations
15 4. Apply config changes to `boards/settings/config.ini`. You can see the default settings in `boards/settings/default_config.ini`
15 4. Apply config changes to `boards/config/config.ini`. You can see the default settings in `boards/config/default_config.ini`
16
16
17 # RUNNING #
17 # RUNNING #
18
18
@@ -34,4 +34,4 b' You can also just clone the mercurial pr'
34
34
35 # CONCLUSION #
35 # CONCLUSION #
36
36
37 Enjoy our software and thank you! No newline at end of file
37 Enjoy our software and thank you!
@@ -2,8 +2,9 b' httplib2'
2 simplejson
2 simplejson
3 requests
3 requests
4 adjacent
4 adjacent
5 haystack
5 django-haystack
6 pillow
6 pillow
7 django>=1.8
7 django>=1.8
8 bbcode
8 bbcode
9 ecdsa No newline at end of file
9 django-debug-toolbar
10 pytz
General Comments 0
You need to be logged in to leave comments. Login now