##// END OF EJS Templates
Added test for reflinks. Added management command to get posts from other node...
neko259 -
r841:c295c39c decentral
parent child Browse files
Show More
@@ -0,0 +1,37 b''
1 import re
2 import urllib
3 import httplib2
4 from django.core.management import BaseCommand
5 from boards.models import GlobalId
6
7 __author__ = 'neko259'
8
9
10 REGEX_GLOBAL_ID = r'\[(\w+)\]\[(\w+)\]\[(\d+)\]'
11
12
13 class Command(BaseCommand):
14 help = 'Send a sync or get request to the server.' + \
15 'sync_with_server <server_url> [post_global_id]'
16
17 def handle(self, *args, **options):
18 url = args[0]
19 if len(args) > 1:
20 global_id_str = args[1]
21 match = re.match(REGEX_GLOBAL_ID, global_id_str)
22 key_type = match.group(1)
23 key = match.group(2)
24 local_id = match.group(3)
25
26 global_id = GlobalId(key_type=key_type, key=key,
27 local_id=local_id)
28
29 xml = GlobalId.objects.generate_request_get([global_id])
30 data = {'xml': xml}
31 body = urllib.urlencode(data)
32 h = httplib2.Http()
33 response, content = h.request(url, method="POST", body=body)
34
35 # TODO Parse content and get the model list
36 else:
37 raise Exception('Full sync is not supported yet.')
@@ -0,0 +1,3 b''
1 #! /usr/bin/env sh
2
3 python3 manage.py runserver [::]:8000
@@ -0,0 +1,3 b''
1 #! /usr/bin/env sh
2
3 python3 manage.py test
@@ -10,6 +10,7 b' import boards'
10
10
11
11
12 REFLINK_PATTERN = re.compile(r'^\d+$')
12 REFLINK_PATTERN = re.compile(r'^\d+$')
13 GLOBAL_REFLINK_PATTERN = re.compile(r'^(\w+)::([^:]+)::(\d+)$')
13 MULTI_NEWLINES_PATTERN = re.compile(r'(\r?\n){2,}')
14 MULTI_NEWLINES_PATTERN = re.compile(r'(\r?\n){2,}')
14 ONE_NEWLINE = '\n'
15 ONE_NEWLINE = '\n'
15
16
@@ -115,10 +116,28 b' class CodePattern(TextFormatter):'
115
116
116
117
117 def render_reflink(tag_name, value, options, parent, context):
118 def render_reflink(tag_name, value, options, parent, context):
118 if not REFLINK_PATTERN.match(value):
119 post_id = None
119 return '>>%s' % value
120
120
121 post_id = int(value)
121 matches = REFLINK_PATTERN.findall(value)
122 if matches:
123 post_id = int(matches[0][0])
124 else:
125 match = GLOBAL_REFLINK_PATTERN.match(value)
126 if match:
127 key_type = match.group(1)
128 key = match.group(2)
129 local_id = match.group(3)
130
131 try:
132 global_id = boards.models.GlobalId.objects.get(key_type=key_type,
133 key=key, local_id=local_id)
134 for post in boards.models.Post.objects.filter(global_id=global_id).only('id'):
135 post_id = post.id
136 except boards.models.GlobalId.DoesNotExist:
137 pass
138
139 if not post_id:
140 return value
122
141
123 posts = boards.models.Post.objects.filter(id=post_id)
142 posts = boards.models.Post.objects.filter(id=post_id)
124 if posts.exists():
143 if posts.exists():
@@ -17,6 +17,7 b' from boards.models.base import Viewable'
17 from boards.models.thread import Thread
17 from boards.models.thread import Thread
18 from boards import utils
18 from boards import utils
19
19
20 ENCODING_UNICODE = 'unicode'
20
21
21 APP_LABEL_BOARDS = 'boards'
22 APP_LABEL_BOARDS = 'boards'
22
23
@@ -40,6 +41,7 b" NO_IP = '0.0.0.0'"
40 UNKNOWN_UA = ''
41 UNKNOWN_UA = ''
41
42
42 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
43 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
44 REGEX_GLOBAL_REPLY = re.compile(r'\[post\](\w+)::([^:]+)::(\d+)\[/post\]')
43
45
44 TAG_MODEL = 'model'
46 TAG_MODEL = 'model'
45 TAG_REQUEST = 'request'
47 TAG_REQUEST = 'request'
@@ -51,9 +53,6 b" TAG_TITLE = 'title'"
51 TAG_TEXT = 'text'
53 TAG_TEXT = 'text'
52 TAG_THREAD = 'thread'
54 TAG_THREAD = 'thread'
53 TAG_PUB_TIME = 'pub-time'
55 TAG_PUB_TIME = 'pub-time'
54 TAG_EDIT_TIME = 'edit-time'
55 TAG_PREVIOUS = 'previous'
56 TAG_NEXT = 'next'
57 TAG_SIGNATURES = 'signatures'
56 TAG_SIGNATURES = 'signatures'
58 TAG_SIGNATURE = 'signature'
57 TAG_SIGNATURE = 'signature'
59 TAG_CONTENT = 'content'
58 TAG_CONTENT = 'content'
@@ -151,6 +150,7 b' class PostManager(models.Manager):'
151 for post in posts:
150 for post in posts:
152 self.delete_post(post)
151 self.delete_post(post)
153
152
153 # TODO This can be moved into a post
154 def connect_replies(self, post):
154 def connect_replies(self, post):
155 """
155 """
156 Connects replies to a post to show them as a reflink map
156 Connects replies to a post to show them as a reflink map
@@ -196,25 +196,7 b' class PostManager(models.Manager):'
196 cache.set(cache_key, ppd)
196 cache.set(cache_key, ppd)
197 return ppd
197 return ppd
198
198
199 def generate_request_get(self, model_list: list):
199 # TODO Make a separate sync facade?
200 """
201 Form a get request from a list of ModelId objects.
202 """
203
204 request = et.Element(TAG_REQUEST)
205 request.set(ATTR_TYPE, TYPE_GET)
206 request.set(ATTR_VERSION, '1.0')
207
208 model = et.SubElement(request, TAG_MODEL)
209 model.set(ATTR_VERSION, '1.0')
210 model.set(ATTR_NAME, 'post')
211
212 for post in model_list:
213 tag_id = et.SubElement(model, TAG_ID)
214 post.global_id.to_xml_element(tag_id)
215
216 return et.tostring(request, 'unicode')
217
218 def generate_response_get(self, model_list: list):
200 def generate_response_get(self, model_list: list):
219 response = et.Element(TAG_RESPONSE)
201 response = et.Element(TAG_RESPONSE)
220
202
@@ -236,6 +218,7 b' class PostManager(models.Manager):'
236 title.text = post.title
218 title.text = post.title
237
219
238 text = et.SubElement(content_tag, TAG_TEXT)
220 text = et.SubElement(content_tag, TAG_TEXT)
221 # TODO Replace local links by global ones in the text
239 text.text = post.text.raw
222 text.text = post.text.raw
240
223
241 if not post.is_opening():
224 if not post.is_opening():
@@ -260,14 +243,42 b' class PostManager(models.Manager):'
260 signatures = [Signature(
243 signatures = [Signature(
261 key_type=key.key_type,
244 key_type=key.key_type,
262 key=key.public_key,
245 key=key.public_key,
263 signature=key.sign(et.tostring(model, 'unicode')),
246 signature=key.sign(et.tostring(model, ENCODING_UNICODE)),
264 )]
247 )]
265 for signature in signatures:
248 for signature in signatures:
266 signature_tag = et.SubElement(signatures_tag, TAG_SIGNATURE)
249 signature_tag = et.SubElement(signatures_tag, TAG_SIGNATURE)
267 signature_tag.set(ATTR_TYPE, signature.key_type)
250 signature_tag.set(ATTR_TYPE, signature.key_type)
268 signature_tag.set(ATTR_VALUE, signature.signature)
251 signature_tag.set(ATTR_VALUE, signature.signature)
269
252
270 return et.tostring(response, 'unicode')
253 return et.tostring(response, ENCODING_UNICODE)
254
255 def parse_response_get(self, response_xml):
256 tag_root = et.fromstring(response_xml)
257 tag_status = tag_root[0]
258 if 'success' == tag_status.text:
259 tag_models = tag_root[1]
260 for tag_model in tag_models:
261 tag_content = tag_model[0]
262 tag_id = tag_content[1]
263 try:
264 GlobalId.from_xml_element(tag_id, existing=True)
265 # If this post already exists, just continue
266 # TODO Compare post content and update the post if necessary
267 pass
268 except GlobalId.DoesNotExist:
269 global_id = GlobalId.from_xml_element(tag_id)
270
271 title = tag_content.find(TAG_TITLE).text
272 text = tag_content.find(TAG_TEXT).text
273 # TODO Check that the replied posts are already present
274 # before adding new ones
275
276 # TODO Pub time, thread, tags
277
278 post = Post.objects.create(title=title, text=text)
279 else:
280 # TODO Throw an exception?
281 pass
271
282
272
283
273 class Post(models.Model, Viewable):
284 class Post(models.Model, Viewable):
@@ -316,15 +327,7 b' class Post(models.Model, Viewable):'
316 self.text.raw[:50] + ')'
327 self.text.raw[:50] + ')'
317
328
318 def get_title(self):
329 def get_title(self):
319 """
330 return self.title
320 Gets original post title or part of its text.
321 """
322
323 title = self.title
324 if not title:
325 title = self.text.rendered
326
327 return title
328
331
329 def build_refmap(self):
332 def build_refmap(self):
330 """
333 """
@@ -491,8 +494,25 b' class Post(models.Model, Viewable):'
491 def get_pub_time_epoch(self):
494 def get_pub_time_epoch(self):
492 return utils.datetime_to_epoch(self.pub_time)
495 return utils.datetime_to_epoch(self.pub_time)
493
496
494 def get_edit_time_epoch(self):
497 def get_replied_ids(self):
495 return utils.datetime_to_epoch(self.last_edit_time)
498 """
499 Gets ID list of the posts that this post replies.
500 """
496
501
497 def get_replied_ids(self):
502 local_replied = REGEX_REPLY.findall(self.text.raw)
498 return re.findall(REGEX_REPLY, self.text.raw)
503 global_replied = []
504 # TODO Similar code is used in mdx_neboard, maybe it can be extracted
505 # into a method?
506 for match in REGEX_GLOBAL_REPLY.findall(self.text.raw):
507 key_type = match[0]
508 key = match[1]
509 local_id = match[2]
510
511 try:
512 global_id = GlobalId.objects.get(key_type=key_type,
513 key=key, local_id=local_id)
514 for post in Post.objects.filter(global_id=global_id).only('id'):
515 global_replied.append(post.id)
516 except GlobalId.DoesNotExist:
517 pass
518 return local_replied + global_replied
@@ -2,15 +2,48 b' import xml.etree.ElementTree as et'
2 from django.db import models
2 from django.db import models
3
3
4
4
5 TAG_MODEL = 'model'
6 TAG_REQUEST = 'request'
7 TAG_ID = 'id'
8
9 TYPE_GET = 'get'
10
11 ATTR_VERSION = 'version'
12 ATTR_TYPE = 'type'
13 ATTR_NAME = 'name'
14
5 ATTR_KEY = 'key'
15 ATTR_KEY = 'key'
6 ATTR_KEY_TYPE = 'type'
16 ATTR_KEY_TYPE = 'type'
7 ATTR_LOCAL_ID = 'local-id'
17 ATTR_LOCAL_ID = 'local-id'
8
18
9
19
20 class GlobalIdManager(models.Manager):
21 def generate_request_get(self, global_id_list: list):
22 """
23 Form a get request from a list of ModelId objects.
24 """
25
26 request = et.Element(TAG_REQUEST)
27 request.set(ATTR_TYPE, TYPE_GET)
28 request.set(ATTR_VERSION, '1.0')
29
30 model = et.SubElement(request, TAG_MODEL)
31 model.set(ATTR_VERSION, '1.0')
32 model.set(ATTR_NAME, 'post')
33
34 for global_id in global_id_list:
35 tag_id = et.SubElement(model, TAG_ID)
36 global_id.to_xml_element(tag_id)
37
38 return et.tostring(request, 'unicode')
39
40
10 class GlobalId(models.Model):
41 class GlobalId(models.Model):
11 class Meta:
42 class Meta:
12 app_label = 'boards'
43 app_label = 'boards'
13
44
45 objects = GlobalIdManager()
46
14 def __init__(self, *args, **kwargs):
47 def __init__(self, *args, **kwargs):
15 models.Model.__init__(self, *args, **kwargs)
48 models.Model.__init__(self, *args, **kwargs)
16
49
@@ -24,7 +57,7 b' class GlobalId(models.Model):'
24 local_id = models.IntegerField()
57 local_id = models.IntegerField()
25
58
26 def __str__(self):
59 def __str__(self):
27 return '[%s][%s][%d]' % (self.key_type, self.key, self.local_id)
60 return '%s::%s::%d' % (self.key_type, self.key, self.local_id)
28
61
29 def to_xml_element(self, element: et.Element):
62 def to_xml_element(self, element: et.Element):
30 """
63 """
@@ -52,7 +52,7 b' class KeyPair(models.Model):'
52 primary = models.BooleanField(default=False)
52 primary = models.BooleanField(default=False)
53
53
54 def __str__(self):
54 def __str__(self):
55 return '[%s][%s]' % (self.key_type, self.public_key)
55 return '%s::%s' % (self.key_type, self.public_key)
56
56
57 def sign(self, string):
57 def sign(self, string):
58 private = SigningKey.from_string(base64.b64decode(
58 private = SigningKey.from_string(base64.b64decode(
@@ -36,7 +36,7 b' class KeyTest(TestCase):'
36 def test_request_get(self):
36 def test_request_get(self):
37 post = self._create_post_with_key()
37 post = self._create_post_with_key()
38
38
39 request = Post.objects.generate_request_get([post])
39 request = GlobalId.objects.generate_request_get([post.global_id])
40 logger.debug(request)
40 logger.debug(request)
41
41
42 key = KeyPair.objects.get(primary=True)
42 key = KeyPair.objects.get(primary=True)
@@ -1,7 +1,7 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 from boards import settings
3 from boards import settings
4 from boards.models import Tag, Post, Thread
4 from boards.models import Tag, Post, Thread, KeyPair
5
5
6
6
7 class PostTests(TestCase):
7 class PostTests(TestCase):
@@ -109,4 +109,34 b' class PostTests(TestCase):'
109 first_post = posts_in_second_page[0]
109 first_post = posts_in_second_page[0]
110
110
111 self.assertEqual(all_threads[settings.THREADS_PER_PAGE].id,
111 self.assertEqual(all_threads[settings.THREADS_PER_PAGE].id,
112 first_post.id) No newline at end of file
112 first_post.id)
113
114 def test_reflinks(self):
115 """
116 Tests that reflinks are parsed within post and connecting replies
117 to the replied posts.
118
119 Local reflink example: [post]123[/post]
120 Global reflink example: [post]key_type::key::123[/post]
121 """
122
123 key = KeyPair.objects.generate_key(primary=True)
124
125 tag = Tag.objects.create(name='test_tag')
126
127 post = Post.objects.create_post(title='', text='', tags=[tag])
128 post_local_reflink = Post.objects.create_post(title='',
129 text='[post]%d[/post]' % post.id, thread=post.get_thread())
130
131 self.assertTrue(post_local_reflink in post.referenced_posts.all(),
132 'Local reflink not connecting posts.')
133
134 post_global_reflink = Post.objects.create_post(title='',
135 text='[post]%s::%s::%d[/post]' % (
136 post.global_id.key_type, post.global_id.key, post.id),
137 thread=post.get_thread())
138
139 self.assertTrue(post_global_reflink in post.referenced_posts.all(),
140 'Global reflink not connecting posts.')
141
142 # TODO Check that links are parsed into the rendered text
@@ -1,3 +1,4 b''
1 httplib2
1 simplejson
2 simplejson
2 south>=0.8.4
3 south>=0.8.4
3 haystack
4 haystack
@@ -15,6 +15,7 b''
15 * Subscribing to tag via AJAX
15 * Subscribing to tag via AJAX
16 * Add buttons to insert a named link or a named quote to the markup panel
16 * Add buttons to insert a named link or a named quote to the markup panel
17 * Add support for "attention posts" that are shown in the header"
17 * Add support for "attention posts" that are shown in the header"
18 * Use absolute post reflinks in the raw text
18
19
19 = Bugs =
20 = Bugs =
20 * Search sort order is confusing
21 * Search sort order is confusing
General Comments 0
You need to be logged in to leave comments. Login now