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 | 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, |
|
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, |
|
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_ |
|
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 ' |
|
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 ' |
|
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 = |
|
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