##// END OF EJS Templates
Merge tip
Bohdan Horbeshko -
r2144:1765cedc merge lite
parent child Browse files
Show More
@@ -0,0 +1,31 b''
1 import xml.etree.ElementTree as et
2
3 from boards.models import Post
4
5 TAG_THREAD = 'thread'
6
7
8 class PostFilter:
9 def __init__(self, content=None):
10 self.content = content
11
12 def filter(self, posts):
13 return posts
14
15 def add_filter(self, model_tag, value):
16 return model_tag
17
18
19 class ThreadFilter(PostFilter):
20 def filter(self, posts):
21 op_id = self.content.text
22
23 op = Post.objects.filter(opening=True, id=op_id).first()
24 if op:
25 return posts.filter(thread=op.get_thread())
26 else:
27 return posts.none()
28
29 def add_filter(self, model_tag, value):
30 thread_tag = et.SubElement(model_tag, TAG_THREAD)
31 thread_tag.text = str(value)
@@ -0,0 +1,24 b''
1 # -*- coding: utf-8 -*-
2 # Generated by Django 1.10.5 on 2017-01-23 14:20
3 from __future__ import unicode_literals
4
5 from django.db import migrations, models
6
7
8 class Migration(migrations.Migration):
9
10 dependencies = [
11 ('boards', '0055_auto_20161229_1132'),
12 ]
13
14 operations = [
15 migrations.AlterModelOptions(
16 name='attachment',
17 options={'ordering': ('id',)},
18 ),
19 migrations.AlterField(
20 model_name='post',
21 name='uid',
22 field=models.TextField(),
23 ),
24 ]
@@ -18,6 +18,7 b' PostingDelay = 30'
18 18 Autoban = false
19 19 DefaultTag = test
20 20 MaxFileCount = 1
21 AdditionalSpoilerSpaces = false
21 22
22 23 [Messages]
23 24 # Thread bumplimit
@@ -24,6 +24,8 b' class Command(BaseCommand):'
24 24 parser.add_argument('--split-query', type=int, default=1,
25 25 help='Split GET query into separate by the given'
26 26 ' number of posts in one')
27 parser.add_argument('--thread', type=int,
28 help='Get posts of one specific thread')
27 29
28 30 def handle(self, *args, **options):
29 31 logger = logging.getLogger('boards.sync')
@@ -45,7 +47,7 b' class Command(BaseCommand):'
45 47 global_id = GlobalId(key_type=key_type, key=key,
46 48 local_id=local_id)
47 49
48 xml = GlobalId.objects.generate_request_get([global_id])
50 xml = SyncManager.generate_request_get([global_id])
49 51 h = httplib2.Http()
50 52 response, content = h.request(get_url, method="POST", body=xml)
51 53
@@ -55,7 +57,8 b' class Command(BaseCommand):'
55 57 else:
56 58 logger.info('Running LIST request...')
57 59 h = httplib2.Http()
58 xml = GlobalId.objects.generate_request_list()
60 xml = SyncManager.generate_request_list(
61 opening_post=options.get('thread'))
59 62 response, content = h.request(list_url, method="POST", body=xml)
60 63 logger.info('Processing response...')
61 64
@@ -83,7 +86,7 b' class Command(BaseCommand):'
83 86 if len(ids_to_sync) > 0:
84 87 limit = options.get('split_query', len(ids_to_sync))
85 88 for offset in range(0, len(ids_to_sync), limit):
86 xml = GlobalId.objects.generate_request_get(ids_to_sync[offset:offset+limit])
89 xml = SyncManager.generate_request_get(ids_to_sync[offset:offset + limit])
87 90 h = httplib2.Http()
88 91 logger.info('Running GET request...')
89 92 response, content = h.request(get_url, method="POST", body=xml)
@@ -10,6 +10,7 b' from django.core.exceptions import Objec'
10 10 from django.core.urlresolvers import reverse
11 11
12 12 import boards
13 from boards import settings
13 14
14 15
15 16 __author__ = 'neko259'
@@ -24,6 +25,7 b' LINE_BREAK_HTML = \'<div class="br"></div'
24 25 SPOILER_SPACE = '&nbsp;'
25 26
26 27 MAX_SPOILER_MULTIPLIER = 2
28 MAX_SPOILER_SPACE_COUNT = 20
27 29
28 30
29 31 class TextFormatter():
@@ -202,11 +204,15 b' def render_tag(tag_name, value, options,'
202 204
203 205
204 206 def render_spoiler(tag_name, value, options, parent, context):
205 text_len = len(value)
206 space_count = random.randint(0, text_len * MAX_SPOILER_MULTIPLIER)
207 side_spaces = SPOILER_SPACE * (space_count // 2)
208 return '<span class="spoiler">{}{}{}</span>'.format(side_spaces, value,
209 side_spaces)
207 if settings.get_bool('Forms', 'AdditionalSpoilerSpaces'):
208 text_len = len(value)
209 space_count = min(random.randint(0, text_len * MAX_SPOILER_MULTIPLIER),
210 MAX_SPOILER_SPACE_COUNT)
211 side_spaces = SPOILER_SPACE * (space_count // 2)
212 else:
213 side_spaces = ''
214 return '<span class="spoiler">{}{}{}</span>'.format(side_spaces,
215 value, side_spaces)
210 216
211 217
212 218 formatters = [
@@ -30,15 +30,15 b' class Downloader:'
30 30 return True
31 31
32 32 @staticmethod
33 def download(url: str):
33 def download(url: str, validate):
34 34 # Verify content headers
35 35 response_head = requests.head(url, verify=False)
36 36 content_type = response_head.headers[HEADER_CONTENT_TYPE].split(';')[0]
37 if content_type in TYPE_URL_ONLY:
37 if validate and content_type in TYPE_URL_ONLY:
38 38 return None
39 39
40 40 length_header = response_head.headers.get(HEADER_CONTENT_LENGTH)
41 if length_header:
41 if validate and length_header:
42 42 length = int(length_header)
43 43 validate_file_size(length)
44 44 # Get the actual content into memory
@@ -63,7 +63,7 b' class Downloader:'
63 63
64 64 class YouTubeDownloader(Downloader):
65 65 @staticmethod
66 def download(url: str):
66 def download(url: str, validate):
67 67 yt = YouTube()
68 68 yt.from_url(url)
69 69 videos = yt.filter(YOUTUBE_VIDEO_FORMAT)
@@ -82,7 +82,7 b' class NothingDownloader(Downloader):'
82 82 return REGEX_MAGNET.match(url)
83 83
84 84 @staticmethod
85 def download(url: str):
85 def download(url: str, validate):
86 86 return None
87 87
88 88
@@ -93,9 +93,9 b' DOWNLOADERS = ('
93 93 )
94 94
95 95
96 def download(url):
96 def download(url, validate=True):
97 97 for downloader in DOWNLOADERS:
98 98 if downloader.handles(url):
99 return downloader.download(url)
99 return downloader.download(url, validate=validate)
100 100 raise Exception('No downloader supports this URL.')
101 101
@@ -90,7 +90,7 b' class Post(models.Model, Viewable):'
90 90 thread = models.ForeignKey('Thread', db_index=True, related_name='replies')
91 91
92 92 url = models.TextField()
93 uid = models.TextField(db_index=True)
93 uid = models.TextField()
94 94
95 95 # Global ID with author key. If the message was downloaded from another
96 96 # server, this indicates the server.
@@ -1,9 +1,13 b''
1 1 import xml.etree.ElementTree as et
2 2 import logging
3 from xml.etree import ElementTree
3 4
4 5 from boards.abstracts.exceptions import SyncException
6 from boards.abstracts.sync_filters import ThreadFilter
5 7 from boards.models import KeyPair, GlobalId, Signature, Post, Tag
6 8 from boards.models.attachment.downloaders import download
9 from boards.models.signature import TAG_REQUEST, ATTR_TYPE, TYPE_GET, \
10 ATTR_VERSION, TAG_MODEL, ATTR_NAME, TAG_ID, TYPE_LIST
7 11 from boards.utils import get_file_mimetype, get_file_hash
8 12 from django.db import transaction
9 13 from django import forms
@@ -238,7 +242,7 b' class SyncManager:'
238 242 TAG_ATTACHMENT_REF, attachment.text))
239 243 url = tag_ref.get(ATTR_URL)
240 244 try:
241 attached_file = download(hostname + url)
245 attached_file = download(hostname + url, validate=False)
242 246
243 247 if attached_file is None:
244 248 raise SyncException(EXCEPTION_DOWNLOAD)
@@ -269,7 +273,7 b' class SyncManager:'
269 273 logger.debug('Parsed new post {}'.format(global_id))
270 274
271 275 @staticmethod
272 def generate_response_list():
276 def generate_response_list(filters):
273 277 response = et.Element(TAG_RESPONSE)
274 278
275 279 status = et.SubElement(response, TAG_STATUS)
@@ -277,7 +281,11 b' class SyncManager:'
277 281
278 282 models = et.SubElement(response, TAG_MODELS)
279 283
280 for post in Post.objects.prefetch_related('global_id').all():
284 posts = Post.objects.prefetch_related('global_id')
285 for post_filter in filters:
286 posts = post_filter.filter(posts)
287
288 for post in posts:
281 289 tag_model = et.SubElement(models, TAG_MODEL)
282 290 tag_id = et.SubElement(tag_model, TAG_ID)
283 291 post.global_id.to_xml_element(tag_id)
@@ -334,4 +342,4 b' class SyncManager:'
334 342 if tag_refs is not None:
335 343 attachment_ref = et.SubElement(tag_refs, TAG_ATTACHMENT_REF)
336 344 attachment_ref.set(ATTR_REF, attachment.hash)
337 attachment_ref.set(ATTR_URL, attachment.file.url)
345 attachment_ref.set(ATTR_URL, attachment.file.url)
@@ -1,8 +1,9 b''
1 1 import xml.etree.ElementTree as et
2
2 3 from django.db import models
4
3 5 from boards.models import KeyPair
4 6
5
6 7 TAG_MODEL = 'model'
7 8 TAG_REQUEST = 'request'
8 9 TAG_ID = 'id'
@@ -20,40 +21,6 b" ATTR_LOCAL_ID = 'local-id'"
20 21
21 22
22 23 class GlobalIdManager(models.Manager):
23 def generate_request_get(self, global_id_list: list):
24 """
25 Form a get request from a list of ModelId objects.
26 """
27
28 request = et.Element(TAG_REQUEST)
29 request.set(ATTR_TYPE, TYPE_GET)
30 request.set(ATTR_VERSION, '1.0')
31
32 model = et.SubElement(request, TAG_MODEL)
33 model.set(ATTR_VERSION, '1.0')
34 model.set(ATTR_NAME, 'post')
35
36 for global_id in global_id_list:
37 tag_id = et.SubElement(model, TAG_ID)
38 global_id.to_xml_element(tag_id)
39
40 return et.tostring(request, 'unicode')
41
42 def generate_request_list(self):
43 """
44 Form a pull request from a list of ModelId objects.
45 """
46
47 request = et.Element(TAG_REQUEST)
48 request.set(ATTR_TYPE, TYPE_LIST)
49 request.set(ATTR_VERSION, '1.0')
50
51 model = et.SubElement(request, TAG_MODEL)
52 model.set(ATTR_VERSION, '1.0')
53 model.set(ATTR_NAME, 'post')
54
55 return et.tostring(request, 'unicode')
56
57 24 def global_id_exists(self, global_id):
58 25 """
59 26 Checks if the same global id already exists in the system.
@@ -23,47 +23,50 b' THUMB_SIZES = ((200, 150),)'
23 23
24 24 @receiver(post_save, sender=Post)
25 25 def connect_replies(instance, **kwargs):
26 for reply_number in re.finditer(REGEX_REPLY, instance.get_raw_text()):
27 post_id = reply_number.group(1)
26 if not kwargs['update_fields']:
27 for reply_number in re.finditer(REGEX_REPLY, instance.get_raw_text()):
28 post_id = reply_number.group(1)
28 29
29 try:
30 referenced_post = Post.objects.get(id=post_id)
30 try:
31 referenced_post = Post.objects.get(id=post_id)
31 32
32 if not referenced_post.referenced_posts.filter(
33 id=instance.id).exists():
34 referenced_post.referenced_posts.add(instance)
35 referenced_post.last_edit_time = instance.pub_time
36 referenced_post.build_refmap()
37 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
38 except Post.DoesNotExist:
39 pass
33 if not referenced_post.referenced_posts.filter(
34 id=instance.id).exists():
35 referenced_post.referenced_posts.add(instance)
36 referenced_post.last_edit_time = instance.pub_time
37 referenced_post.build_refmap()
38 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
39 except Post.DoesNotExist:
40 pass
40 41
41 42
42 43 @receiver(post_save, sender=Post)
43 44 @receiver(post_import_deps, sender=Post)
44 45 def connect_global_replies(instance, **kwargs):
45 for reply_number in re.finditer(REGEX_GLOBAL_REPLY, instance.get_raw_text()):
46 key_type = reply_number.group(1)
47 key = reply_number.group(2)
48 local_id = reply_number.group(3)
46 if not kwargs['update_fields']:
47 for reply_number in re.finditer(REGEX_GLOBAL_REPLY, instance.get_raw_text()):
48 key_type = reply_number.group(1)
49 key = reply_number.group(2)
50 local_id = reply_number.group(3)
49 51
50 try:
51 global_id = GlobalId.objects.get(key_type=key_type, key=key,
52 local_id=local_id)
53 referenced_post = Post.objects.get(global_id=global_id)
54 referenced_post.referenced_posts.add(instance)
55 referenced_post.last_edit_time = instance.pub_time
56 referenced_post.build_refmap()
57 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
58 except (GlobalId.DoesNotExist, Post.DoesNotExist):
59 pass
52 try:
53 global_id = GlobalId.objects.get(key_type=key_type, key=key,
54 local_id=local_id)
55 referenced_post = Post.objects.get(global_id=global_id)
56 referenced_post.referenced_posts.add(instance)
57 referenced_post.last_edit_time = instance.pub_time
58 referenced_post.build_refmap()
59 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
60 except (GlobalId.DoesNotExist, Post.DoesNotExist):
61 pass
60 62
61 63
62 64 @receiver(post_save, sender=Post)
63 65 def connect_notifications(instance, **kwargs):
64 for reply_number in re.finditer(REGEX_NOTIFICATION, instance.get_raw_text()):
65 user_name = reply_number.group(1).lower()
66 Notification.objects.get_or_create(name=user_name, post=instance)
66 if not kwargs['update_fields']:
67 for reply_number in re.finditer(REGEX_NOTIFICATION, instance.get_raw_text()):
68 user_name = reply_number.group(1).lower()
69 Notification.objects.get_or_create(name=user_name, post=instance)
67 70
68 71
69 72 @receiver(pre_save, sender=Post)
@@ -57,40 +57,35 b' function showFormAfter(blockToInsertAfte'
57 57 }
58 58
59 59 function addQuickReply(postId) {
60 // If we click "reply" on the same post, it means "cancel"
61 if (getForm().prev().attr('id') == postId) {
62 resetFormPosition();
63 } else {
64 var blockToInsert = null;
65 var textAreaJq = getPostTextarea();
66 var postLinkRaw = '[post]' + postId + '[/post]'
67 var textToAdd = '';
60 var blockToInsert = null;
61 var textAreaJq = getPostTextarea();
62 var postLinkRaw = '[post]' + postId + '[/post]'
63 var textToAdd = '';
68 64
69 if (postId != null) {
70 var post = $('#' + postId);
65 if (postId != null) {
66 var post = $('#' + postId);
71 67
72 // If this is not OP, add reflink to the post. If there already is
73 // the same reflink, don't add it again.
74 var postText = textAreaJq.val();
75 if (!post.is(':first-child') && postText.indexOf(postLinkRaw) < 0) {
76 // Insert line break if none is present.
77 if (postText.length > 0 && !postText.endsWith('\n') && !postText.endsWith('\r')) {
78 textToAdd += '\n';
79 }
80 textToAdd += postLinkRaw + '\n';
68 // If this is not OP, add reflink to the post. If there already is
69 // the same reflink, don't add it again.
70 var postText = textAreaJq.val();
71 if (!post.is(':first-child') && postText.indexOf(postLinkRaw) < 0) {
72 // Insert line break if none is present.
73 if (postText.length > 0 && !postText.endsWith('\n') && !postText.endsWith('\r')) {
74 textToAdd += '\n';
81 75 }
76 textToAdd += postLinkRaw + '\n';
77 }
82 78
83 textAreaJq.val(textAreaJq.val()+ textToAdd);
84 blockToInsert = post;
85 } else {
86 blockToInsert = $('.thread');
87 }
88 showFormAfter(blockToInsert);
89
90 textAreaJq.focus();
79 textAreaJq.val(textAreaJq.val()+ textToAdd);
80 blockToInsert = post;
81 } else {
82 blockToInsert = $('.thread');
83 }
84 showFormAfter(blockToInsert);
91 85
92 moveCaretToEnd(textAreaJq);
93 }
86 textAreaJq.focus();
87
88 moveCaretToEnd(textAreaJq);
94 89 }
95 90
96 91 function addQuickQuote() {
@@ -98,7 +93,7 b' function addQuickQuote() {'
98 93
99 94 var quoteButton = $("#quote-button");
100 95 var postId = quoteButton.attr('data-post-id');
101 if (postId != null && getForm().prev().attr('id') != postId) {
96 if (postId != null) {
102 97 addQuickReply(postId);
103 98 }
104 99
@@ -1,4 +1,3 b''
1 from base64 import b64encode
2 1 import logging
3 2
4 3 from django.test import TestCase
@@ -38,7 +37,7 b' class KeyTest(TestCase):'
38 37 def test_request_get(self):
39 38 post = self._create_post_with_key()
40 39
41 request = GlobalId.objects.generate_request_get([post.global_id])
40 request = SyncManager.generate_request_get([post.global_id])
42 41 logger.debug(request)
43 42
44 43 key = KeyPair.objects.get(primary=True)
@@ -3,7 +3,7 b' from django.test import TestCase'
3 3 from boards.models import KeyPair, Post, Tag
4 4 from boards.models.post.sync import SyncManager
5 5 from boards.tests.mocks import MockRequest
6 from boards.views.sync import response_get
6 from boards.views.sync import response_get, response_list
7 7
8 8 __author__ = 'neko259'
9 9
@@ -103,3 +103,112 b' class SyncTest(TestCase):'
103 103 post.version,
104 104 ) in response,
105 105 'Wrong response generated for the GET request.')
106
107 def test_list_all(self):
108 key = KeyPair.objects.generate_key(primary=True)
109 tag = Tag.objects.create(name='tag1')
110 post = Post.objects.create_post(title='test_title',
111 text='test_text\rline two',
112 tags=[tag])
113 post2 = Post.objects.create_post(title='test title 2',
114 text='test text 2',
115 tags=[tag])
116
117 request_all = MockRequest()
118 request_all.body = (
119 '<request type="list" version="1.0">'
120 '<model name="post" version="1.0">'
121 '</model>'
122 '</request>'
123 )
124
125 response_all = response_list(request_all).content.decode()
126 self.assertTrue(
127 '<status>success</status>'
128 '<models>'
129 '<model>'
130 '<id key="{}" local-id="{}" type="{}" />'
131 '<version>{}</version>'
132 '</model>'
133 '<model>'
134 '<id key="{}" local-id="{}" type="{}" />'
135 '<version>{}</version>'
136 '</model>'
137 '</models>'.format(
138 post.global_id.key,
139 post.global_id.local_id,
140 post.global_id.key_type,
141 post.version,
142 post2.global_id.key,
143 post2.global_id.local_id,
144 post2.global_id.key_type,
145 post2.version,
146 ) in response_all,
147 'Wrong response generated for the LIST request for all posts.')
148
149 def test_list_existing_thread(self):
150 key = KeyPair.objects.generate_key(primary=True)
151 tag = Tag.objects.create(name='tag1')
152 post = Post.objects.create_post(title='test_title',
153 text='test_text\rline two',
154 tags=[tag])
155 post2 = Post.objects.create_post(title='test title 2',
156 text='test text 2',
157 tags=[tag])
158
159 request_thread = MockRequest()
160 request_thread.body = (
161 '<request type="list" version="1.0">'
162 '<model name="post" version="1.0">'
163 '<thread>{}</thread>'
164 '</model>'
165 '</request>'.format(
166 post.id,
167 )
168 )
169
170 response_thread = response_list(request_thread).content.decode()
171 self.assertTrue(
172 '<status>success</status>'
173 '<models>'
174 '<model>'
175 '<id key="{}" local-id="{}" type="{}" />'
176 '<version>{}</version>'
177 '</model>'
178 '</models>'.format(
179 post.global_id.key,
180 post.global_id.local_id,
181 post.global_id.key_type,
182 post.version,
183 ) in response_thread,
184 'Wrong response generated for the LIST request for posts of '
185 'existing thread.')
186
187 def test_list_non_existing_thread(self):
188 key = KeyPair.objects.generate_key(primary=True)
189 tag = Tag.objects.create(name='tag1')
190 post = Post.objects.create_post(title='test_title',
191 text='test_text\rline two',
192 tags=[tag])
193 post2 = Post.objects.create_post(title='test title 2',
194 text='test text 2',
195 tags=[tag])
196
197 request_thread = MockRequest()
198 request_thread.body = (
199 '<request type="list" version="1.0">'
200 '<model name="post" version="1.0">'
201 '<thread>{}</thread>'
202 '</model>'
203 '</request>'.format(
204 0,
205 )
206 )
207
208 response_thread = response_list(request_thread).content.decode()
209 self.assertTrue(
210 '<status>success</status>'
211 '<models />'
212 in response_thread,
213 'Wrong response generated for the LIST request for posts of '
214 'non-existing thread.')
@@ -1,17 +1,41 b''
1 import logging
2
1 3 import xml.etree.ElementTree as et
2 4
3 5 from django.http import HttpResponse, Http404
6
7 from boards.abstracts.sync_filters import ThreadFilter, TAG_THREAD
4 8 from boards.models import GlobalId, Post
5 9 from boards.models.post.sync import SyncManager
6 10
7 11
12 logger = logging.getLogger('boards.sync')
13
14
15 FILTERS = {
16 TAG_THREAD: ThreadFilter,
17 }
18
19
8 20 def response_list(request):
9 21 request_xml = request.body
10 22
23 filters = []
24
11 25 if request_xml is None or len(request_xml) == 0:
12 26 return HttpResponse(content='Use the API')
27 else:
28 root_tag = et.fromstring(request_xml)
29 model_tag = root_tag[0]
13 30
14 response_xml = SyncManager.generate_response_list()
31 for tag_filter in model_tag:
32 filter_name = tag_filter.tag
33 model_filter = FILTERS.get(filter_name)(tag_filter)
34 if not model_filter:
35 logger.warning('Unavailable filter: {}'.format(filter_name))
36 filters.append(model_filter)
37
38 response_xml = SyncManager.generate_response_list(filters)
15 39
16 40 return HttpResponse(content=response_xml)
17 41
@@ -33,11 +33,11 b' 3. Setup a database in `neboard/settings'
33 33
34 34 Depending on configured database and search engine, you need to install corresponding dependencies manually.
35 35
36 Default database is *sqlite*, default search engine is *simple*.
36 Default database is *sqlite*. If you want to change the database backend, refer to the django documentation for the correct settings. Please note that sqlite accepts only one connection at a time, so you won't be able to run 2 servers or a server and a sync at the same time.
37 37
38 38 4. Setup SECRET_KEY to a secret value in `neboard/settings.py
39 39 5. Run `./manage.py migrate` to apply all migrations
40 6. Apply config changes to `boards/config/config.ini`. You can see the default settings in `boards/config/default_config.ini`
40 6. Apply config changes to `boards/config/settings.ini`. You can see the default settings in `boards/config/default_config.ini`(do not delete or overwrite it).
41 41 7. If you want to use decetral engine, run `./manage.py generate_keypair` to generate keys
42 42
43 43 # RUNNING #
General Comments 0
You need to be logged in to leave comments. Login now