##// END OF EJS Templates
Made SyncManager's methods static
neko259 -
r1236:124750d7 decentral
parent child Browse files
Show More
@@ -1,59 +1,59 b''
1 1 import re
2 2 import urllib.parse
3 3 import httplib2
4 4 import xml.etree.ElementTree as ET
5 5
6 6 from django.core.management import BaseCommand
7 7 from boards.models import GlobalId
8 8 from boards.models.post.sync import SyncManager
9 9
10 10 __author__ = 'neko259'
11 11
12 12
13 13 REGEX_GLOBAL_ID = re.compile(r'(\w+)::([\w\+/]+)::(\d+)')
14 14
15 15
16 16 class Command(BaseCommand):
17 17 help = 'Send a sync or get request to the server.' + \
18 18 'sync_with_server <server_url> [post_global_id]'
19 19
20 20 def add_arguments(self, parser):
21 21 parser.add_argument('url', type=str)
22 22 parser.add_argument('global_id', type=str)
23 23
24 24 def handle(self, *args, **options):
25 25 url = options.get('url')
26 26 global_id_str = options.get('global_id')
27 27 if global_id_str:
28 28 match = REGEX_GLOBAL_ID.match(global_id_str)
29 29 if match:
30 30 key_type = match.group(1)
31 31 key = match.group(2)
32 32 local_id = match.group(3)
33 33
34 34 global_id = GlobalId(key_type=key_type, key=key,
35 35 local_id=local_id)
36 36
37 37 xml = GlobalId.objects.generate_request_get([global_id])
38 38 # body = urllib.parse.urlencode(data)
39 39 h = httplib2.Http()
40 40 response, content = h.request(url, method="POST", body=xml)
41 41
42 SyncManager().parse_response_get(content)
42 SyncManager.parse_response_get(content)
43 43 else:
44 44 raise Exception('Invalid global ID')
45 45 else:
46 46 h = httplib2.Http()
47 47 response, content = h.request(url, method="POST")
48 48
49 49 print(content)
50 50
51 51 root = ET.fromstring(content)
52 52 status = root.findall('status')[0].text
53 53 if status == 'success':
54 54 models = root.findall('models')[0]
55 55 for model in models:
56 56 model_content = model[0]
57 57 print(model_content.findall('text')[0].text)
58 58 else:
59 59 raise Exception('Invalid response status')
@@ -1,128 +1,129 b''
1 1 import xml.etree.ElementTree as et
2 2 from django.db import transaction
3 3 from boards.models import KeyPair, GlobalId, Signature, Post
4 4
5 5 ENCODING_UNICODE = 'unicode'
6 6
7 7 TAG_MODEL = 'model'
8 8 TAG_REQUEST = 'request'
9 9 TAG_RESPONSE = 'response'
10 10 TAG_ID = 'id'
11 11 TAG_STATUS = 'status'
12 12 TAG_MODELS = 'models'
13 13 TAG_TITLE = 'title'
14 14 TAG_TEXT = 'text'
15 15 TAG_THREAD = 'thread'
16 16 TAG_PUB_TIME = 'pub-time'
17 17 TAG_SIGNATURES = 'signatures'
18 18 TAG_SIGNATURE = 'signature'
19 19 TAG_CONTENT = 'content'
20 20 TAG_ATTACHMENTS = 'attachments'
21 21 TAG_ATTACHMENT = 'attachment'
22 22
23 23 TYPE_GET = 'get'
24 24
25 25 ATTR_VERSION = 'version'
26 26 ATTR_TYPE = 'type'
27 27 ATTR_NAME = 'name'
28 28 ATTR_VALUE = 'value'
29 29 ATTR_MIMETYPE = 'mimetype'
30 30
31 31 STATUS_SUCCESS = 'success'
32 32
33 33
34 # TODO Make this fully static
35 34 class SyncManager:
36 def generate_response_get(self, model_list: list):
35 @staticmethod
36 def generate_response_get(model_list: list):
37 37 response = et.Element(TAG_RESPONSE)
38 38
39 39 status = et.SubElement(response, TAG_STATUS)
40 40 status.text = STATUS_SUCCESS
41 41
42 42 models = et.SubElement(response, TAG_MODELS)
43 43
44 44 for post in model_list:
45 45 model = et.SubElement(models, TAG_MODEL)
46 46 model.set(ATTR_NAME, 'post')
47 47
48 48 content_tag = et.SubElement(model, TAG_CONTENT)
49 49
50 50 tag_id = et.SubElement(content_tag, TAG_ID)
51 51 post.global_id.to_xml_element(tag_id)
52 52
53 53 title = et.SubElement(content_tag, TAG_TITLE)
54 54 title.text = post.title
55 55
56 56 text = et.SubElement(content_tag, TAG_TEXT)
57 57 text.text = post.get_sync_text()
58 58
59 59 if not post.is_opening():
60 60 thread = et.SubElement(content_tag, TAG_THREAD)
61 61 thread_id = et.SubElement(thread, TAG_ID)
62 62 post.get_thread().get_opening_post().global_id.to_xml_element(thread_id)
63 63 else:
64 64 # TODO Output tags here
65 65 pass
66 66
67 67 pub_time = et.SubElement(content_tag, TAG_PUB_TIME)
68 68 pub_time.text = str(post.get_pub_time_str())
69 69
70 70 signatures_tag = et.SubElement(model, TAG_SIGNATURES)
71 71 post_signatures = post.signature.all()
72 72 if post_signatures:
73 73 signatures = post.signatures
74 74 else:
75 75 # TODO Maybe the signature can be computed only once after
76 76 # the post is added? Need to add some on_save signal queue
77 77 # and add this there.
78 78 key = KeyPair.objects.get(public_key=post.global_id.key)
79 79 signatures = [Signature(
80 80 key_type=key.key_type,
81 81 key=key.public_key,
82 82 signature=key.sign(et.tostring(model, ENCODING_UNICODE)),
83 83 )]
84 84 for signature in signatures:
85 85 signature_tag = et.SubElement(signatures_tag, TAG_SIGNATURE)
86 86 signature_tag.set(ATTR_TYPE, signature.key_type)
87 87 signature_tag.set(ATTR_VALUE, signature.signature)
88 88
89 89 return et.tostring(response, ENCODING_UNICODE)
90 90
91 @staticmethod
91 92 @transaction.atomic
92 def parse_response_get(self, response_xml):
93 def parse_response_get(response_xml):
93 94 tag_root = et.fromstring(response_xml)
94 95 tag_status = tag_root.find(TAG_STATUS)
95 96 if STATUS_SUCCESS == tag_status.text:
96 97 tag_models = tag_root.find(TAG_MODELS)
97 98 for tag_model in tag_models:
98 99 tag_content = tag_model.find(TAG_CONTENT)
99 100 tag_id = tag_content.find(TAG_ID)
100 101 global_id, exists = GlobalId.from_xml_element(tag_id)
101 102
102 103 if exists:
103 104 print('Post with same ID already exists')
104 105 else:
105 106 global_id.save()
106 107
107 108 title = tag_content.find(TAG_TITLE).text
108 109 text = tag_content.find(TAG_TEXT).text
109 110 pub_time = tag_content.find(TAG_PUB_TIME).text
110 111
111 112 thread = tag_content.find(TAG_THREAD)
112 113 if thread:
113 114 opening_post = Post.objects.get(
114 115 id=thread.find(TAG_ID).text)
115 116 else:
116 117 opening_post = None
117 118 # TODO Get tags here
118 119
119 120 # TODO Check that the replied posts are already present
120 121 # before adding new ones
121 122
122 123 post = Post.objects.import_post(
123 124 title=title, text=text, pub_time=pub_time,
124 125 opening_post=opening_post)
125 126 post.global_id = global_id
126 127 else:
127 128 # TODO Throw an exception?
128 129 pass
@@ -1,87 +1,87 b''
1 1 from base64 import b64encode
2 2 import logging
3 3
4 4 from django.test import TestCase
5 5 from boards.models import KeyPair, GlobalId, Post
6 6 from boards.models.post.sync import SyncManager
7 7
8 8 logger = logging.getLogger(__name__)
9 9
10 10
11 11 class KeyTest(TestCase):
12 12 def test_create_key(self):
13 13 key = KeyPair.objects.generate_key('ecdsa')
14 14
15 15 self.assertIsNotNone(key, 'The key was not created.')
16 16
17 17 def test_validation(self):
18 18 key = KeyPair.objects.generate_key(key_type='ecdsa')
19 19 message = 'msg'
20 20 signature = key.sign(message)
21 21 valid = KeyPair.objects.verify(key.public_key, message, signature,
22 22 key_type='ecdsa')
23 23
24 24 self.assertTrue(valid, 'Message verification failed.')
25 25
26 26 def test_primary_constraint(self):
27 27 KeyPair.objects.generate_key(key_type='ecdsa', primary=True)
28 28
29 29 with self.assertRaises(Exception):
30 30 KeyPair.objects.generate_key(key_type='ecdsa', primary=True)
31 31
32 32 def test_model_id_save(self):
33 33 model_id = GlobalId(key_type='test', key='test key', local_id='1')
34 34 model_id.save()
35 35
36 36 def test_request_get(self):
37 37 post = self._create_post_with_key()
38 38
39 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)
43 43 self.assertTrue('<request type="get" version="1.0">'
44 44 '<model name="post" version="1.0">'
45 45 '<id key="%s" local-id="1" type="%s" />'
46 46 '</model>'
47 47 '</request>' % (
48 48 key.public_key,
49 49 key.key_type,
50 50 ) in request,
51 51 'Wrong XML generated for the GET request.')
52 52
53 53 def test_response_get(self):
54 54 post = self._create_post_with_key()
55 55 reply_post = Post.objects.create_post(title='test_title',
56 56 text='[post]%d[/post]' % post.id,
57 57 thread=post.get_thread())
58 58
59 response = SyncManager().generate_response_get([reply_post])
59 response = SyncManager.generate_response_get([reply_post])
60 60 logger.debug(response)
61 61
62 62 key = KeyPair.objects.get(primary=True)
63 63 self.assertTrue('<status>success</status>'
64 64 '<models>'
65 65 '<model name="post">'
66 66 '<content>'
67 67 '<id key="%s" local-id="%d" type="%s" />'
68 68 '<title>test_title</title>'
69 69 '<text>[post]%s[/post]</text>'
70 70 '<thread><id key="%s" local-id="%d" type="%s" /></thread>'
71 71 '<pub-time>%s</pub-time>'
72 72 '</content>' % (
73 73 key.public_key,
74 74 reply_post.id,
75 75 key.key_type,
76 76 str(post.global_id),
77 77 key.public_key,
78 78 post.id,
79 79 key.key_type,
80 80 str(reply_post.get_pub_time_str()),
81 81 ) in response,
82 82 'Wrong XML generated for the GET response.')
83 83
84 84 def _create_post_with_key(self):
85 85 KeyPair.objects.generate_key(primary=True)
86 86
87 87 return Post.objects.create_post(title='test_title', text='test_text')
@@ -1,60 +1,60 b''
1 1 from boards.models import KeyPair, Post
2 2 from boards.models.post.sync import SyncManager
3 3 from boards.tests.mocks import MockRequest
4 4 from boards.views.sync import response_get
5 5
6 6 __author__ = 'neko259'
7 7
8 8
9 9 from django.test import TestCase
10 10
11 11
12 12 class SyncTest(TestCase):
13 13 def test_get(self):
14 14 """
15 15 Forms a GET request of a post and checks the response.
16 16 """
17 17
18 18 KeyPair.objects.generate_key(primary=True)
19 19 post = Post.objects.create_post(title='test_title', text='test_text')
20 20
21 21 request = MockRequest()
22 22 request.body = (
23 23 '<request type="get" version="1.0">'
24 24 '<model name="post" version="1.0">'
25 25 '<id key="%s" local-id="%d" type="%s" />'
26 26 '</model>'
27 27 '</request>' % (post.global_id.key,
28 28 post.id,
29 29 post.global_id.key_type)
30 30 )
31 31
32 32 response = response_get(request).content.decode()
33 33 self.assertTrue(
34 34 '<status>success</status>'
35 35 '<models>'
36 36 '<model name="post">'
37 37 '<content>'
38 38 '<id key="%s" local-id="%d" type="%s" />'
39 39 '<title>%s</title>'
40 40 '<text>%s</text>'
41 41 '<pub-time>%s</pub-time>'
42 42 '</content>' % (
43 43 post.global_id.key,
44 44 post.id,
45 45 post.global_id.key_type,
46 46 post.title,
47 47 post.get_raw_text(),
48 48 post.get_pub_time_str(),
49 49 ) in response_get(request).content.decode(),
50 50 'Wrong response generated for the GET request.')
51 51
52 52 post.delete()
53 53
54 SyncManager().parse_response_get(response)
54 SyncManager.parse_response_get(response)
55 55 self.assertEqual(1, Post.objects.count(),
56 56 'Post was not created from XML response.')
57 57
58 SyncManager().parse_response_get(response)
58 SyncManager.parse_response_get(response)
59 59 self.assertEqual(1, Post.objects.count(),
60 60 'The same post was imported twice.')
@@ -1,258 +1,258 b''
1 1 import json
2 2 import logging
3 3
4 4 import xml.etree.ElementTree as ET
5 5
6 6 from django.db import transaction
7 7 from django.http import HttpResponse
8 8 from django.shortcuts import get_object_or_404
9 9 from django.core import serializers
10 10
11 11 from boards.forms import PostForm, PlainErrorList
12 12 from boards.models import Post, Thread, Tag, GlobalId
13 13 from boards.models.post.sync import SyncManager
14 14 from boards.utils import datetime_to_epoch
15 15 from boards.views.thread import ThreadView
16 16 from boards.models.user import Notification
17 17 from boards.mdx_neboard import Parser
18 18
19 19
20 20 __author__ = 'neko259'
21 21
22 22 PARAMETER_TRUNCATED = 'truncated'
23 23 PARAMETER_TAG = 'tag'
24 24 PARAMETER_OFFSET = 'offset'
25 25 PARAMETER_DIFF_TYPE = 'type'
26 26 PARAMETER_POST = 'post'
27 27 PARAMETER_UPDATED = 'updated'
28 28 PARAMETER_LAST_UPDATE = 'last_update'
29 29 PARAMETER_THREAD = 'thread'
30 30 PARAMETER_UIDS = 'uids'
31 31
32 32 DIFF_TYPE_HTML = 'html'
33 33 DIFF_TYPE_JSON = 'json'
34 34
35 35 STATUS_OK = 'ok'
36 36 STATUS_ERROR = 'error'
37 37
38 38 logger = logging.getLogger(__name__)
39 39
40 40
41 41 @transaction.atomic
42 42 def api_get_threaddiff(request):
43 43 """
44 44 Gets posts that were changed or added since time
45 45 """
46 46
47 47 thread_id = request.POST.get(PARAMETER_THREAD)
48 48 uids_str = request.POST.get(PARAMETER_UIDS).strip()
49 49 uids = uids_str.split(' ')
50 50
51 51 thread = get_object_or_404(Post, id=thread_id).get_thread()
52 52
53 53 json_data = {
54 54 PARAMETER_UPDATED: [],
55 55 PARAMETER_LAST_UPDATE: None, # TODO Maybe this can be removed already?
56 56 }
57 57 posts = Post.objects.filter(threads__in=[thread]).exclude(uid__in=uids)
58 58
59 59 diff_type = request.GET.get(PARAMETER_DIFF_TYPE, DIFF_TYPE_HTML)
60 60
61 61 for post in posts:
62 62 json_data[PARAMETER_UPDATED].append(get_post_data(post.id, diff_type,
63 63 request))
64 64 json_data[PARAMETER_LAST_UPDATE] = str(thread.last_edit_time)
65 65
66 66 return HttpResponse(content=json.dumps(json_data))
67 67
68 68
69 69 def api_add_post(request, opening_post_id):
70 70 """
71 71 Adds a post and return the JSON response for it
72 72 """
73 73
74 74 opening_post = get_object_or_404(Post, id=opening_post_id)
75 75
76 76 logger.info('Adding post via api...')
77 77
78 78 status = STATUS_OK
79 79 errors = []
80 80
81 81 if request.method == 'POST':
82 82 form = PostForm(request.POST, request.FILES, error_class=PlainErrorList)
83 83 form.session = request.session
84 84
85 85 if form.need_to_ban:
86 86 # Ban user because he is suspected to be a bot
87 87 # _ban_current_user(request)
88 88 status = STATUS_ERROR
89 89 if form.is_valid():
90 90 post = ThreadView().new_post(request, form, opening_post,
91 91 html_response=False)
92 92 if not post:
93 93 status = STATUS_ERROR
94 94 else:
95 95 logger.info('Added post #%d via api.' % post.id)
96 96 else:
97 97 status = STATUS_ERROR
98 98 errors = form.as_json_errors()
99 99
100 100 response = {
101 101 'status': status,
102 102 'errors': errors,
103 103 }
104 104
105 105 return HttpResponse(content=json.dumps(response))
106 106
107 107
108 108 def get_post(request, post_id):
109 109 """
110 110 Gets the html of a post. Used for popups. Post can be truncated if used
111 111 in threads list with 'truncated' get parameter.
112 112 """
113 113
114 114 post = get_object_or_404(Post, id=post_id)
115 115 truncated = PARAMETER_TRUNCATED in request.GET
116 116
117 117 return HttpResponse(content=post.get_view(truncated=truncated))
118 118
119 119
120 120 def api_get_threads(request, count):
121 121 """
122 122 Gets the JSON thread opening posts list.
123 123 Parameters that can be used for filtering:
124 124 tag, offset (from which thread to get results)
125 125 """
126 126
127 127 if PARAMETER_TAG in request.GET:
128 128 tag_name = request.GET[PARAMETER_TAG]
129 129 if tag_name is not None:
130 130 tag = get_object_or_404(Tag, name=tag_name)
131 131 threads = tag.get_threads().filter(archived=False)
132 132 else:
133 133 threads = Thread.objects.filter(archived=False)
134 134
135 135 if PARAMETER_OFFSET in request.GET:
136 136 offset = request.GET[PARAMETER_OFFSET]
137 137 offset = int(offset) if offset is not None else 0
138 138 else:
139 139 offset = 0
140 140
141 141 threads = threads.order_by('-bump_time')
142 142 threads = threads[offset:offset + int(count)]
143 143
144 144 opening_posts = []
145 145 for thread in threads:
146 146 opening_post = thread.get_opening_post()
147 147
148 148 # TODO Add tags, replies and images count
149 149 post_data = get_post_data(opening_post.id, include_last_update=True)
150 150 post_data['bumpable'] = thread.can_bump()
151 151 post_data['archived'] = thread.archived
152 152
153 153 opening_posts.append(post_data)
154 154
155 155 return HttpResponse(content=json.dumps(opening_posts))
156 156
157 157
158 158 # TODO Test this
159 159 def api_get_tags(request):
160 160 """
161 161 Gets all tags or user tags.
162 162 """
163 163
164 164 # TODO Get favorite tags for the given user ID
165 165
166 166 tags = Tag.objects.get_not_empty_tags()
167 167
168 168 term = request.GET.get('term')
169 169 if term is not None:
170 170 tags = tags.filter(name__contains=term)
171 171
172 172 tag_names = [tag.name for tag in tags]
173 173
174 174 return HttpResponse(content=json.dumps(tag_names))
175 175
176 176
177 177 # TODO The result can be cached by the thread last update time
178 178 # TODO Test this
179 179 def api_get_thread_posts(request, opening_post_id):
180 180 """
181 181 Gets the JSON array of thread posts
182 182 """
183 183
184 184 opening_post = get_object_or_404(Post, id=opening_post_id)
185 185 thread = opening_post.get_thread()
186 186 posts = thread.get_replies()
187 187
188 188 json_data = {
189 189 'posts': [],
190 190 'last_update': None,
191 191 }
192 192 json_post_list = []
193 193
194 194 for post in posts:
195 195 json_post_list.append(get_post_data(post.id))
196 196 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
197 197 json_data['posts'] = json_post_list
198 198
199 199 return HttpResponse(content=json.dumps(json_data))
200 200
201 201
202 202 def api_get_notifications(request, username):
203 203 last_notification_id_str = request.GET.get('last', None)
204 204 last_id = int(last_notification_id_str) if last_notification_id_str is not None else None
205 205
206 206 posts = Notification.objects.get_notification_posts(username=username,
207 207 last=last_id)
208 208
209 209 json_post_list = []
210 210 for post in posts:
211 211 json_post_list.append(get_post_data(post.id))
212 212 return HttpResponse(content=json.dumps(json_post_list))
213 213
214 214
215 215 def api_get_post(request, post_id):
216 216 """
217 217 Gets the JSON of a post. This can be
218 218 used as and API for external clients.
219 219 """
220 220
221 221 post = get_object_or_404(Post, id=post_id)
222 222
223 223 json = serializers.serialize("json", [post], fields=(
224 224 "pub_time", "_text_rendered", "title", "text", "image",
225 225 "image_width", "image_height", "replies", "tags"
226 226 ))
227 227
228 228 return HttpResponse(content=json)
229 229
230 230
231 231 # TODO Remove this method and use post method directly
232 232 def get_post_data(post_id, format_type=DIFF_TYPE_JSON, request=None,
233 233 include_last_update=False):
234 234 post = get_object_or_404(Post, id=post_id)
235 235 return post.get_post_data(format_type=format_type, request=request,
236 236 include_last_update=include_last_update)
237 237
238 238
239 239 def api_get_preview(request):
240 240 raw_text = request.POST['raw_text']
241 241
242 242 parser = Parser()
243 243 return HttpResponse(content=parser.parse(parser.preparse(raw_text)))
244 244
245 245
246 246 # TODO Make a separate module for sync API methods
247 247 def sync_pull(request):
248 248 """
249 249 Return 'pull' request response for all posts.
250 250 """
251 251 request_xml = request.get('xml')
252 252 if request_xml is None:
253 253 posts = Post.objects.all()
254 254 else:
255 255 pass # TODO Parse the XML and get filters from it
256 256
257 xml = SyncManager().generate_response_get(posts)
257 xml = SyncManager.generate_response_get(posts)
258 258 return HttpResponse(content=xml)
@@ -1,49 +1,49 b''
1 1 import xml.etree.ElementTree as et
2 2 from django.http import HttpResponse, Http404
3 3 from boards.models import GlobalId, Post
4 4 from boards.models.post.sync import SyncManager
5 5
6 6
7 7 def response_pull(request):
8 8 pass
9 9
10 10
11 11 def response_get(request):
12 12 """
13 13 Processes a GET request with post ID list and returns the posts XML list.
14 14 Request should contain an 'xml' post attribute with the actual request XML.
15 15 """
16 16
17 17 request_xml = request.body
18 18
19 19 if request_xml is None:
20 20 return HttpResponse(content='Use the API')
21 21
22 22 posts = []
23 23
24 24 root_tag = et.fromstring(request_xml)
25 25 model_tag = root_tag[0]
26 26 for id_tag in model_tag:
27 27 global_id, exists = GlobalId.from_xml_element(id_tag)
28 28 if exists:
29 29 posts.append(Post.objects.get(global_id=global_id))
30 30
31 response_xml = SyncManager().generate_response_get(posts)
31 response_xml = SyncManager.generate_response_get(posts)
32 32
33 33 return HttpResponse(content=response_xml)
34 34
35 35
36 36 def get_post_sync_data(request, post_id):
37 37 try:
38 38 post = Post.objects.get(id=post_id)
39 39 except Post.DoesNotExist:
40 40 raise Http404()
41 41
42 42 content = 'Global ID: %s\n\nXML: %s' \
43 % (post.global_id, SyncManager().generate_response_get([post]))
43 % (post.global_id, SyncManager.generate_response_get([post]))
44 44
45 45
46 46 return HttpResponse(
47 47 content_type='text/plain',
48 48 content=content,
49 49 ) No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now