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.') |
@@ -10,6 +10,7 b' import boards' | |||
|
10 | 10 | |
|
11 | 11 | |
|
12 | 12 | REFLINK_PATTERN = re.compile(r'^\d+$') |
|
13 | GLOBAL_REFLINK_PATTERN = re.compile(r'^(\w+)::([^:]+)::(\d+)$') | |
|
13 | 14 | MULTI_NEWLINES_PATTERN = re.compile(r'(\r?\n){2,}') |
|
14 | 15 | ONE_NEWLINE = '\n' |
|
15 | 16 | |
@@ -115,10 +116,28 b' class CodePattern(TextFormatter):' | |||
|
115 | 116 | |
|
116 | 117 | |
|
117 | 118 | def render_reflink(tag_name, value, options, parent, context): |
|
118 | if not REFLINK_PATTERN.match(value): | |
|
119 | return '>>%s' % value | |
|
119 | post_id = None | |
|
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 | 142 | posts = boards.models.Post.objects.filter(id=post_id) |
|
124 | 143 | if posts.exists(): |
@@ -17,6 +17,7 b' from boards.models.base import Viewable' | |||
|
17 | 17 | from boards.models.thread import Thread |
|
18 | 18 | from boards import utils |
|
19 | 19 | |
|
20 | ENCODING_UNICODE = 'unicode' | |
|
20 | 21 | |
|
21 | 22 | APP_LABEL_BOARDS = 'boards' |
|
22 | 23 | |
@@ -40,6 +41,7 b" NO_IP = '0.0.0.0'" | |||
|
40 | 41 | UNKNOWN_UA = '' |
|
41 | 42 | |
|
42 | 43 | REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]') |
|
44 | REGEX_GLOBAL_REPLY = re.compile(r'\[post\](\w+)::([^:]+)::(\d+)\[/post\]') | |
|
43 | 45 | |
|
44 | 46 | TAG_MODEL = 'model' |
|
45 | 47 | TAG_REQUEST = 'request' |
@@ -51,9 +53,6 b" TAG_TITLE = 'title'" | |||
|
51 | 53 | TAG_TEXT = 'text' |
|
52 | 54 | TAG_THREAD = 'thread' |
|
53 | 55 | TAG_PUB_TIME = 'pub-time' |
|
54 | TAG_EDIT_TIME = 'edit-time' | |
|
55 | TAG_PREVIOUS = 'previous' | |
|
56 | TAG_NEXT = 'next' | |
|
57 | 56 | TAG_SIGNATURES = 'signatures' |
|
58 | 57 | TAG_SIGNATURE = 'signature' |
|
59 | 58 | TAG_CONTENT = 'content' |
@@ -151,6 +150,7 b' class PostManager(models.Manager):' | |||
|
151 | 150 | for post in posts: |
|
152 | 151 | self.delete_post(post) |
|
153 | 152 | |
|
153 | # TODO This can be moved into a post | |
|
154 | 154 | def connect_replies(self, post): |
|
155 | 155 | """ |
|
156 | 156 | Connects replies to a post to show them as a reflink map |
@@ -196,25 +196,7 b' class PostManager(models.Manager):' | |||
|
196 | 196 | cache.set(cache_key, ppd) |
|
197 | 197 | return ppd |
|
198 | 198 | |
|
199 | def generate_request_get(self, model_list: list): | |
|
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 | ||
|
199 | # TODO Make a separate sync facade? | |
|
218 | 200 | def generate_response_get(self, model_list: list): |
|
219 | 201 | response = et.Element(TAG_RESPONSE) |
|
220 | 202 | |
@@ -236,6 +218,7 b' class PostManager(models.Manager):' | |||
|
236 | 218 | title.text = post.title |
|
237 | 219 | |
|
238 | 220 | text = et.SubElement(content_tag, TAG_TEXT) |
|
221 | # TODO Replace local links by global ones in the text | |
|
239 | 222 | text.text = post.text.raw |
|
240 | 223 | |
|
241 | 224 | if not post.is_opening(): |
@@ -260,14 +243,42 b' class PostManager(models.Manager):' | |||
|
260 | 243 | signatures = [Signature( |
|
261 | 244 | key_type=key.key_type, |
|
262 | 245 | key=key.public_key, |
|
263 |
signature=key.sign(et.tostring(model, |
|
|
246 | signature=key.sign(et.tostring(model, ENCODING_UNICODE)), | |
|
264 | 247 | )] |
|
265 | 248 | for signature in signatures: |
|
266 | 249 | signature_tag = et.SubElement(signatures_tag, TAG_SIGNATURE) |
|
267 | 250 | signature_tag.set(ATTR_TYPE, signature.key_type) |
|
268 | 251 | signature_tag.set(ATTR_VALUE, signature.signature) |
|
269 | 252 | |
|
270 |
return et.tostring(response, |
|
|
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 | 284 | class Post(models.Model, Viewable): |
@@ -316,15 +327,7 b' class Post(models.Model, Viewable):' | |||
|
316 | 327 | self.text.raw[:50] + ')' |
|
317 | 328 | |
|
318 | 329 | def get_title(self): |
|
319 | """ | |
|
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 | |
|
330 | return self.title | |
|
328 | 331 | |
|
329 | 332 | def build_refmap(self): |
|
330 | 333 | """ |
@@ -491,8 +494,25 b' class Post(models.Model, Viewable):' | |||
|
491 | 494 | def get_pub_time_epoch(self): |
|
492 | 495 | return utils.datetime_to_epoch(self.pub_time) |
|
493 | 496 | |
|
494 |
def get_ |
|
|
495 | return utils.datetime_to_epoch(self.last_edit_time) | |
|
497 | def get_replied_ids(self): | |
|
498 | """ | |
|
499 | Gets ID list of the posts that this post replies. | |
|
500 | """ | |
|
496 | 501 | |
|
497 | def get_replied_ids(self): | |
|
498 | return re.findall(REGEX_REPLY, self.text.raw) | |
|
502 | local_replied = REGEX_REPLY.findall(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 | 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 | 15 | ATTR_KEY = 'key' |
|
6 | 16 | ATTR_KEY_TYPE = 'type' |
|
7 | 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 | 41 | class GlobalId(models.Model): |
|
11 | 42 | class Meta: |
|
12 | 43 | app_label = 'boards' |
|
13 | 44 | |
|
45 | objects = GlobalIdManager() | |
|
46 | ||
|
14 | 47 | def __init__(self, *args, **kwargs): |
|
15 | 48 | models.Model.__init__(self, *args, **kwargs) |
|
16 | 49 | |
@@ -24,7 +57,7 b' class GlobalId(models.Model):' | |||
|
24 | 57 | local_id = models.IntegerField() |
|
25 | 58 | |
|
26 | 59 | def __str__(self): |
|
27 |
return ' |
|
|
60 | return '%s::%s::%d' % (self.key_type, self.key, self.local_id) | |
|
28 | 61 | |
|
29 | 62 | def to_xml_element(self, element: et.Element): |
|
30 | 63 | """ |
@@ -52,7 +52,7 b' class KeyPair(models.Model):' | |||
|
52 | 52 | primary = models.BooleanField(default=False) |
|
53 | 53 | |
|
54 | 54 | def __str__(self): |
|
55 |
return ' |
|
|
55 | return '%s::%s' % (self.key_type, self.public_key) | |
|
56 | 56 | |
|
57 | 57 | def sign(self, string): |
|
58 | 58 | private = SigningKey.from_string(base64.b64decode( |
@@ -36,7 +36,7 b' class KeyTest(TestCase):' | |||
|
36 | 36 | def test_request_get(self): |
|
37 | 37 | post = self._create_post_with_key() |
|
38 | 38 | |
|
39 |
request = |
|
|
39 | request = GlobalId.objects.generate_request_get([post.global_id]) | |
|
40 | 40 | logger.debug(request) |
|
41 | 41 | |
|
42 | 42 | key = KeyPair.objects.get(primary=True) |
@@ -1,7 +1,7 b'' | |||
|
1 | 1 | from django.core.paginator import Paginator |
|
2 | 2 | from django.test import TestCase |
|
3 | 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 | 7 | class PostTests(TestCase): |
@@ -109,4 +109,34 b' class PostTests(TestCase):' | |||
|
109 | 109 | first_post = posts_in_second_page[0] |
|
110 | 110 | |
|
111 | 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 |
@@ -15,6 +15,7 b'' | |||
|
15 | 15 | * Subscribing to tag via AJAX |
|
16 | 16 | * Add buttons to insert a named link or a named quote to the markup panel |
|
17 | 17 | * Add support for "attention posts" that are shown in the header" |
|
18 | * Use absolute post reflinks in the raw text | |
|
18 | 19 | |
|
19 | 20 | = Bugs = |
|
20 | 21 | * Search sort order is confusing |
General Comments 0
You need to be logged in to leave comments.
Login now