##// END OF EJS Templates
Fixed tag parsing
neko259 -
r2104:2e6f7c21 default
parent child Browse files
Show More
@@ -1,395 +1,395
1 import logging
1 import logging
2 import xml.etree.ElementTree as et
2 import xml.etree.ElementTree as et
3
3
4 from django.db import transaction
4 from django.db import transaction
5 from django.utils.dateparse import parse_datetime
5 from django.utils.dateparse import parse_datetime
6
6
7 from boards.abstracts.exceptions import SyncException
7 from boards.abstracts.exceptions import SyncException
8 from boards.abstracts.sync_filters import ThreadFilter, TagsFilter, \
8 from boards.abstracts.sync_filters import ThreadFilter, TagsFilter, \
9 TimestampFromFilter
9 TimestampFromFilter
10 from boards.models import KeyPair, GlobalId, Signature, Post, Tag
10 from boards.models import KeyPair, GlobalId, Signature, Post, Tag
11 from boards.models.attachment.downloaders import download
11 from boards.models.attachment.downloaders import download
12 from boards.models.signature import TAG_REQUEST, ATTR_TYPE, TYPE_GET, \
12 from boards.models.signature import TAG_REQUEST, ATTR_TYPE, TYPE_GET, \
13 ATTR_VERSION, TAG_MODEL, ATTR_NAME, TAG_ID, TYPE_LIST
13 ATTR_VERSION, TAG_MODEL, ATTR_NAME, TAG_ID, TYPE_LIST
14 from boards.utils import get_file_mimetype, get_file_hash
14 from boards.utils import get_file_mimetype, get_file_hash
15
15
16 EXCEPTION_NODE = 'Sync node returned an error: {}.'
16 EXCEPTION_NODE = 'Sync node returned an error: {}.'
17 EXCEPTION_DOWNLOAD = 'File was not downloaded.'
17 EXCEPTION_DOWNLOAD = 'File was not downloaded.'
18 EXCEPTION_HASH = 'File hash does not match attachment hash.'
18 EXCEPTION_HASH = 'File hash does not match attachment hash.'
19 EXCEPTION_SIGNATURE = 'Invalid model signature for {}.'
19 EXCEPTION_SIGNATURE = 'Invalid model signature for {}.'
20 EXCEPTION_AUTHOR_SIGNATURE = 'Model {} has no author signature.'
20 EXCEPTION_AUTHOR_SIGNATURE = 'Model {} has no author signature.'
21 EXCEPTION_THREAD = 'No thread exists for post {}'
21 EXCEPTION_THREAD = 'No thread exists for post {}'
22 ENCODING_UNICODE = 'unicode'
22 ENCODING_UNICODE = 'unicode'
23
23
24 TAG_MODEL = 'model'
24 TAG_MODEL = 'model'
25 TAG_REQUEST = 'request'
25 TAG_REQUEST = 'request'
26 TAG_RESPONSE = 'response'
26 TAG_RESPONSE = 'response'
27 TAG_ID = 'id'
27 TAG_ID = 'id'
28 TAG_STATUS = 'status'
28 TAG_STATUS = 'status'
29 TAG_MODELS = 'models'
29 TAG_MODELS = 'models'
30 TAG_TITLE = 'title'
30 TAG_TITLE = 'title'
31 TAG_TEXT = 'text'
31 TAG_TEXT = 'text'
32 TAG_THREAD = 'thread'
32 TAG_THREAD = 'thread'
33 TAG_PUB_TIME = 'pub-time'
33 TAG_PUB_TIME = 'pub-time'
34 TAG_UPDATE_TIME = 'update-time'
34 TAG_UPDATE_TIME = 'update-time'
35 TAG_SIGNATURES = 'signatures'
35 TAG_SIGNATURES = 'signatures'
36 TAG_SIGNATURE = 'signature'
36 TAG_SIGNATURE = 'signature'
37 TAG_CONTENT = 'content'
37 TAG_CONTENT = 'content'
38 TAG_ATTACHMENTS = 'attachments'
38 TAG_ATTACHMENTS = 'attachments'
39 TAG_ATTACHMENT = 'attachment'
39 TAG_ATTACHMENT = 'attachment'
40 TAG_TAGS = 'tags'
40 TAG_TAGS = 'tags'
41 TAG_TAG = 'tag'
41 TAG_TAG = 'tag'
42 TAG_ATTACHMENT_REFS = 'attachment-refs'
42 TAG_ATTACHMENT_REFS = 'attachment-refs'
43 TAG_ATTACHMENT_REF = 'attachment-ref'
43 TAG_ATTACHMENT_REF = 'attachment-ref'
44 TAG_TRIPCODE = 'tripcode'
44 TAG_TRIPCODE = 'tripcode'
45 TAG_VERSION = 'version'
45 TAG_VERSION = 'version'
46
46
47 TYPE_GET = 'get'
47 TYPE_GET = 'get'
48
48
49 ATTR_VERSION = 'version'
49 ATTR_VERSION = 'version'
50 ATTR_TYPE = 'type'
50 ATTR_TYPE = 'type'
51 ATTR_NAME = 'name'
51 ATTR_NAME = 'name'
52 ATTR_VALUE = 'value'
52 ATTR_VALUE = 'value'
53 ATTR_MIMETYPE = 'mimetype'
53 ATTR_MIMETYPE = 'mimetype'
54 ATTR_KEY = 'key'
54 ATTR_KEY = 'key'
55 ATTR_REF = 'ref'
55 ATTR_REF = 'ref'
56 ATTR_URL = 'url'
56 ATTR_URL = 'url'
57 ATTR_ID_TYPE = 'id-type'
57 ATTR_ID_TYPE = 'id-type'
58
58
59 ID_TYPE_MD5 = 'md5'
59 ID_TYPE_MD5 = 'md5'
60 ID_TYPE_URL = 'url'
60 ID_TYPE_URL = 'url'
61
61
62 STATUS_SUCCESS = 'success'
62 STATUS_SUCCESS = 'success'
63
63
64 CURRENT_MODEL_VERSION = '1.1'
64 CURRENT_MODEL_VERSION = '1.1'
65
65
66
66
67 logger = logging.getLogger('boards.sync')
67 logger = logging.getLogger('boards.sync')
68
68
69
69
70 class SyncManager:
70 class SyncManager:
71 @staticmethod
71 @staticmethod
72 def generate_response_get(model_list: list):
72 def generate_response_get(model_list: list):
73 response = et.Element(TAG_RESPONSE)
73 response = et.Element(TAG_RESPONSE)
74
74
75 status = et.SubElement(response, TAG_STATUS)
75 status = et.SubElement(response, TAG_STATUS)
76 status.text = STATUS_SUCCESS
76 status.text = STATUS_SUCCESS
77
77
78 models = et.SubElement(response, TAG_MODELS)
78 models = et.SubElement(response, TAG_MODELS)
79
79
80 for post in model_list:
80 for post in model_list:
81 model = et.SubElement(models, TAG_MODEL)
81 model = et.SubElement(models, TAG_MODEL)
82 model.set(ATTR_NAME, 'post')
82 model.set(ATTR_NAME, 'post')
83
83
84 global_id = post.global_id
84 global_id = post.global_id
85
85
86 attachments = post.attachments.all()
86 attachments = post.attachments.all()
87 if global_id.content:
87 if global_id.content:
88 model.append(et.fromstring(global_id.content))
88 model.append(et.fromstring(global_id.content))
89 if len(attachments) > 0:
89 if len(attachments) > 0:
90 internal_attachments = False
90 internal_attachments = False
91 for attachment in attachments:
91 for attachment in attachments:
92 if attachment.is_internal():
92 if attachment.is_internal():
93 internal_attachments = True
93 internal_attachments = True
94 break
94 break
95
95
96 if internal_attachments:
96 if internal_attachments:
97 attachment_refs = et.SubElement(model, TAG_ATTACHMENT_REFS)
97 attachment_refs = et.SubElement(model, TAG_ATTACHMENT_REFS)
98 for file in attachments:
98 for file in attachments:
99 SyncManager._attachment_to_xml(
99 SyncManager._attachment_to_xml(
100 None, attachment_refs, file)
100 None, attachment_refs, file)
101 else:
101 else:
102 content_tag = et.SubElement(model, TAG_CONTENT)
102 content_tag = et.SubElement(model, TAG_CONTENT)
103
103
104 tag_id = et.SubElement(content_tag, TAG_ID)
104 tag_id = et.SubElement(content_tag, TAG_ID)
105 global_id.to_xml_element(tag_id)
105 global_id.to_xml_element(tag_id)
106
106
107 title = et.SubElement(content_tag, TAG_TITLE)
107 title = et.SubElement(content_tag, TAG_TITLE)
108 title.text = post.title
108 title.text = post.title
109
109
110 text = et.SubElement(content_tag, TAG_TEXT)
110 text = et.SubElement(content_tag, TAG_TEXT)
111 text.text = post.get_sync_text()
111 text.text = post.get_sync_text()
112
112
113 thread = post.get_thread()
113 thread = post.get_thread()
114 if post.is_opening():
114 if post.is_opening():
115 tag_tags = et.SubElement(content_tag, TAG_TAGS)
115 tag_tags = et.SubElement(content_tag, TAG_TAGS)
116 for tag in thread.get_tags():
116 for tag in thread.get_tags():
117 tag_tag = et.SubElement(tag_tags, TAG_TAG)
117 tag_tag = et.SubElement(tag_tags, TAG_TAG)
118 tag_tag.text = tag.get_name()
118 tag_tag.text = tag.get_name()
119 else:
119 else:
120 tag_thread = et.SubElement(content_tag, TAG_THREAD)
120 tag_thread = et.SubElement(content_tag, TAG_THREAD)
121 thread_id = et.SubElement(tag_thread, TAG_ID)
121 thread_id = et.SubElement(tag_thread, TAG_ID)
122 thread.get_opening_post().global_id.to_xml_element(thread_id)
122 thread.get_opening_post().global_id.to_xml_element(thread_id)
123
123
124 pub_time = et.SubElement(content_tag, TAG_PUB_TIME)
124 pub_time = et.SubElement(content_tag, TAG_PUB_TIME)
125 pub_time.text = str(post.get_pub_time_str())
125 pub_time.text = str(post.get_pub_time_str())
126
126
127 update_time = et.SubElement(content_tag, TAG_UPDATE_TIME)
127 update_time = et.SubElement(content_tag, TAG_UPDATE_TIME)
128 update_time.text = str(post.last_edit_time)
128 update_time.text = str(post.last_edit_time)
129
129
130 if post.tripcode:
130 if post.tripcode:
131 tripcode = et.SubElement(content_tag, TAG_TRIPCODE)
131 tripcode = et.SubElement(content_tag, TAG_TRIPCODE)
132 tripcode.text = post.tripcode
132 tripcode.text = post.tripcode
133
133
134 if len(attachments) > 0:
134 if len(attachments) > 0:
135 attachments_tag = et.SubElement(content_tag, TAG_ATTACHMENTS)
135 attachments_tag = et.SubElement(content_tag, TAG_ATTACHMENTS)
136
136
137 internal_attachments = False
137 internal_attachments = False
138 for attachment in attachments:
138 for attachment in attachments:
139 if attachment.is_internal():
139 if attachment.is_internal():
140 internal_attachments = True
140 internal_attachments = True
141 break
141 break
142
142
143 if internal_attachments:
143 if internal_attachments:
144 attachment_refs = et.SubElement(model, TAG_ATTACHMENT_REFS)
144 attachment_refs = et.SubElement(model, TAG_ATTACHMENT_REFS)
145 else:
145 else:
146 attachment_refs = None
146 attachment_refs = None
147
147
148 for file in attachments:
148 for file in attachments:
149 SyncManager._attachment_to_xml(
149 SyncManager._attachment_to_xml(
150 attachments_tag, attachment_refs, file)
150 attachments_tag, attachment_refs, file)
151
151
152 global_id.content = et.tostring(content_tag, ENCODING_UNICODE)
152 global_id.content = et.tostring(content_tag, ENCODING_UNICODE)
153 global_id.save()
153 global_id.save()
154
154
155 signatures_tag = et.SubElement(model, TAG_SIGNATURES)
155 signatures_tag = et.SubElement(model, TAG_SIGNATURES)
156 post_signatures = global_id.signature_set.all()
156 post_signatures = global_id.signature_set.all()
157 if post_signatures:
157 if post_signatures:
158 signatures = post_signatures
158 signatures = post_signatures
159 else:
159 else:
160 key = KeyPair.objects.get(public_key=global_id.key)
160 key = KeyPair.objects.get(public_key=global_id.key)
161 signature = Signature(
161 signature = Signature(
162 key_type=key.key_type,
162 key_type=key.key_type,
163 key=key.public_key,
163 key=key.public_key,
164 signature=key.sign(global_id.content),
164 signature=key.sign(global_id.content),
165 global_id=global_id,
165 global_id=global_id,
166 )
166 )
167 signature.save()
167 signature.save()
168 signatures = [signature]
168 signatures = [signature]
169 for signature in signatures:
169 for signature in signatures:
170 signature_tag = et.SubElement(signatures_tag, TAG_SIGNATURE)
170 signature_tag = et.SubElement(signatures_tag, TAG_SIGNATURE)
171 signature_tag.set(ATTR_TYPE, signature.key_type)
171 signature_tag.set(ATTR_TYPE, signature.key_type)
172 signature_tag.set(ATTR_VALUE, signature.signature)
172 signature_tag.set(ATTR_VALUE, signature.signature)
173 signature_tag.set(ATTR_KEY, signature.key)
173 signature_tag.set(ATTR_KEY, signature.key)
174
174
175 return et.tostring(response, ENCODING_UNICODE)
175 return et.tostring(response, ENCODING_UNICODE)
176
176
177 @staticmethod
177 @staticmethod
178 def parse_response_get(response_xml, hostname):
178 def parse_response_get(response_xml, hostname):
179 tag_root = et.fromstring(response_xml)
179 tag_root = et.fromstring(response_xml)
180 tag_status = tag_root.find(TAG_STATUS)
180 tag_status = tag_root.find(TAG_STATUS)
181 if STATUS_SUCCESS == tag_status.text:
181 if STATUS_SUCCESS == tag_status.text:
182 tag_models = tag_root.find(TAG_MODELS)
182 tag_models = tag_root.find(TAG_MODELS)
183 for tag_model in tag_models:
183 for tag_model in tag_models:
184 SyncManager.parse_post(tag_model, hostname)
184 SyncManager.parse_post(tag_model, hostname)
185 else:
185 else:
186 raise SyncException(EXCEPTION_NODE.format(tag_status.text))
186 raise SyncException(EXCEPTION_NODE.format(tag_status.text))
187
187
188 @staticmethod
188 @staticmethod
189 @transaction.atomic
189 @transaction.atomic
190 def parse_post(tag_model, hostname):
190 def parse_post(tag_model, hostname):
191 tag_content = tag_model.find(TAG_CONTENT)
191 tag_content = tag_model.find(TAG_CONTENT)
192
192
193 content_str = et.tostring(tag_content, ENCODING_UNICODE)
193 content_str = et.tostring(tag_content, ENCODING_UNICODE)
194
194
195 tag_id = tag_content.find(TAG_ID)
195 tag_id = tag_content.find(TAG_ID)
196 global_id, exists = GlobalId.from_xml_element(tag_id)
196 global_id, exists = GlobalId.from_xml_element(tag_id)
197 signatures = SyncManager._verify_model(global_id, content_str, tag_model)
197 signatures = SyncManager._verify_model(global_id, content_str, tag_model)
198
198
199 pub_time = tag_content.find(TAG_PUB_TIME).text
199 pub_time = tag_content.find(TAG_PUB_TIME).text
200
200
201 tag_update_time = tag_content.find(TAG_UPDATE_TIME)
201 tag_update_time = tag_content.find(TAG_UPDATE_TIME)
202 if tag_update_time:
202 if tag_update_time:
203 update_time = tag_content.find(TAG_UPDATE_TIME).text
203 update_time = tag_content.find(TAG_UPDATE_TIME).text
204 else:
204 else:
205 update_time = pub_time
205 update_time = pub_time
206
206
207 is_old = exists and global_id.post.last_edit_time < parse_datetime(update_time)
207 is_old = exists and global_id.post.last_edit_time < parse_datetime(update_time)
208 if exists and not is_old:
208 if exists and not is_old:
209 logger.debug('Post {} exists and is up to date.'.format(global_id))
209 logger.debug('Post {} exists and is up to date.'.format(global_id))
210 else:
210 else:
211 global_id.content = content_str
211 global_id.content = content_str
212 global_id.save()
212 global_id.save()
213 for signature in signatures:
213 for signature in signatures:
214 signature.global_id = global_id
214 signature.global_id = global_id
215 signature.save()
215 signature.save()
216
216
217 title = tag_content.find(TAG_TITLE).text or ''
217 title = tag_content.find(TAG_TITLE).text or ''
218 text = tag_content.find(TAG_TEXT).text or ''
218 text = tag_content.find(TAG_TEXT).text or ''
219 tripcode_tag = tag_content.find(TAG_TRIPCODE)
219 tripcode_tag = tag_content.find(TAG_TRIPCODE)
220 if tripcode_tag is not None:
220 if tripcode_tag is not None:
221 tripcode = tripcode_tag.text or ''
221 tripcode = tripcode_tag.text or ''
222 else:
222 else:
223 tripcode = ''
223 tripcode = ''
224
224
225 thread = tag_content.find(TAG_THREAD)
225 thread = tag_content.find(TAG_THREAD)
226 tags = []
226 tags = []
227 if thread:
227 if thread:
228 thread_id = thread.find(TAG_ID)
228 thread_id = thread.find(TAG_ID)
229 op_global_id, exists = GlobalId.from_xml_element(thread_id)
229 op_global_id, exists = GlobalId.from_xml_element(thread_id)
230 if exists:
230 if exists:
231 opening_post = Post.objects.get(global_id=op_global_id)
231 opening_post = Post.objects.get(global_id=op_global_id)
232 else:
232 else:
233 raise Exception(EXCEPTION_THREAD.format(global_id))
233 raise Exception(EXCEPTION_THREAD.format(global_id))
234 else:
234 else:
235 opening_post = None
235 opening_post = None
236 tag_tags = tag_content.find(TAG_TAGS)
236 tag_tags = tag_content.find(TAG_TAGS)
237 for tag_tag in tag_tags:
237 for tag_tag in tag_tags:
238 tag, created = Tag.objects.get_or_create(
238 tag, created = Tag.objects.get_or_create_with_alias(
239 aliases__name=tag_tag.text)
239 name=tag_tag.text)
240 tags.append(tag)
240 tags.append(tag)
241
241
242 # TODO Check that the replied posts are already present
242 # TODO Check that the replied posts are already present
243 # before adding new ones
243 # before adding new ones
244
244
245 files = []
245 files = []
246 urls = []
246 urls = []
247 tag_attachments = tag_content.find(TAG_ATTACHMENTS) or list()
247 tag_attachments = tag_content.find(TAG_ATTACHMENTS) or list()
248 tag_refs = tag_model.find(TAG_ATTACHMENT_REFS)
248 tag_refs = tag_model.find(TAG_ATTACHMENT_REFS)
249 for attachment in tag_attachments:
249 for attachment in tag_attachments:
250 if attachment.get(ATTR_ID_TYPE) == ID_TYPE_URL:
250 if attachment.get(ATTR_ID_TYPE) == ID_TYPE_URL:
251 urls.append(attachment.text)
251 urls.append(attachment.text)
252 else:
252 else:
253 tag_ref = tag_refs.find("{}[@ref='{}']".format(
253 tag_ref = tag_refs.find("{}[@ref='{}']".format(
254 TAG_ATTACHMENT_REF, attachment.text))
254 TAG_ATTACHMENT_REF, attachment.text))
255 url = tag_ref.get(ATTR_URL)
255 url = tag_ref.get(ATTR_URL)
256 attached_file = download(hostname + url, validate=False)
256 attached_file = download(hostname + url, validate=False)
257 if attached_file is None:
257 if attached_file is None:
258 raise SyncException(EXCEPTION_DOWNLOAD)
258 raise SyncException(EXCEPTION_DOWNLOAD)
259
259
260 hash = get_file_hash(attached_file)
260 hash = get_file_hash(attached_file)
261 if hash != attachment.text:
261 if hash != attachment.text:
262 raise SyncException(EXCEPTION_HASH)
262 raise SyncException(EXCEPTION_HASH)
263
263
264 files.append(attached_file)
264 files.append(attached_file)
265
265
266 if is_old:
266 if is_old:
267 post = global_id.post
267 post = global_id.post
268 Post.objects.update_post(
268 Post.objects.update_post(
269 post, title=title, text=text, pub_time=pub_time,
269 post, title=title, text=text, pub_time=pub_time,
270 tags=tags, files=files, file_urls=urls,
270 tags=tags, files=files, file_urls=urls,
271 tripcode=tripcode, version=version, last_edit_time=update_time)
271 tripcode=tripcode, version=version, last_edit_time=update_time)
272 logger.debug('Parsed updated post {}'.format(global_id))
272 logger.debug('Parsed updated post {}'.format(global_id))
273 else:
273 else:
274 Post.objects.import_post(
274 Post.objects.import_post(
275 title=title, text=text, pub_time=pub_time,
275 title=title, text=text, pub_time=pub_time,
276 opening_post=opening_post, tags=tags,
276 opening_post=opening_post, tags=tags,
277 global_id=global_id, files=files,
277 global_id=global_id, files=files,
278 file_urls=urls, tripcode=tripcode, last_edit_time=update_time)
278 file_urls=urls, tripcode=tripcode, last_edit_time=update_time)
279 logger.debug('Parsed new post {}'.format(global_id))
279 logger.debug('Parsed new post {}'.format(global_id))
280
280
281 @staticmethod
281 @staticmethod
282 def generate_response_list(filters):
282 def generate_response_list(filters):
283 response = et.Element(TAG_RESPONSE)
283 response = et.Element(TAG_RESPONSE)
284
284
285 status = et.SubElement(response, TAG_STATUS)
285 status = et.SubElement(response, TAG_STATUS)
286 status.text = STATUS_SUCCESS
286 status.text = STATUS_SUCCESS
287
287
288 models = et.SubElement(response, TAG_MODELS)
288 models = et.SubElement(response, TAG_MODELS)
289
289
290 posts = Post.objects.prefetch_related('global_id')
290 posts = Post.objects.prefetch_related('global_id')
291 for post_filter in filters:
291 for post_filter in filters:
292 posts = post_filter.filter(posts)
292 posts = post_filter.filter(posts)
293
293
294 for post in posts:
294 for post in posts:
295 tag_model = et.SubElement(models, TAG_MODEL)
295 tag_model = et.SubElement(models, TAG_MODEL)
296 tag_id = et.SubElement(tag_model, TAG_ID)
296 tag_id = et.SubElement(tag_model, TAG_ID)
297 post.global_id.to_xml_element(tag_id)
297 post.global_id.to_xml_element(tag_id)
298 update_time = et.SubElement(tag_model, TAG_UPDATE_TIME)
298 update_time = et.SubElement(tag_model, TAG_UPDATE_TIME)
299 update_time.text = str(post.last_edit_time)
299 update_time.text = str(post.last_edit_time)
300
300
301 return et.tostring(response, ENCODING_UNICODE)
301 return et.tostring(response, ENCODING_UNICODE)
302
302
303 @staticmethod
303 @staticmethod
304 def _verify_model(global_id, content_str, tag_model):
304 def _verify_model(global_id, content_str, tag_model):
305 """
305 """
306 Verifies all signatures for a single model.
306 Verifies all signatures for a single model.
307 """
307 """
308
308
309 signatures = []
309 signatures = []
310
310
311 tag_signatures = tag_model.find(TAG_SIGNATURES)
311 tag_signatures = tag_model.find(TAG_SIGNATURES)
312 has_author_signature = False
312 has_author_signature = False
313 for tag_signature in tag_signatures:
313 for tag_signature in tag_signatures:
314 signature_type = tag_signature.get(ATTR_TYPE)
314 signature_type = tag_signature.get(ATTR_TYPE)
315 signature_value = tag_signature.get(ATTR_VALUE)
315 signature_value = tag_signature.get(ATTR_VALUE)
316 signature_key = tag_signature.get(ATTR_KEY)
316 signature_key = tag_signature.get(ATTR_KEY)
317
317
318 if global_id.key_type == signature_type and\
318 if global_id.key_type == signature_type and\
319 global_id.key == signature_key:
319 global_id.key == signature_key:
320 has_author_signature = True
320 has_author_signature = True
321
321
322 signature = Signature(key_type=signature_type,
322 signature = Signature(key_type=signature_type,
323 key=signature_key,
323 key=signature_key,
324 signature=signature_value)
324 signature=signature_value)
325
325
326 if not KeyPair.objects.verify(signature, content_str):
326 if not KeyPair.objects.verify(signature, content_str):
327 raise SyncException(EXCEPTION_SIGNATURE.format(content_str))
327 raise SyncException(EXCEPTION_SIGNATURE.format(content_str))
328
328
329 signatures.append(signature)
329 signatures.append(signature)
330 if not has_author_signature:
330 if not has_author_signature:
331 raise SyncException(EXCEPTION_AUTHOR_SIGNATURE.format(content_str))
331 raise SyncException(EXCEPTION_AUTHOR_SIGNATURE.format(content_str))
332
332
333 return signatures
333 return signatures
334
334
335 @staticmethod
335 @staticmethod
336 def _attachment_to_xml(tag_attachments, tag_refs, attachment):
336 def _attachment_to_xml(tag_attachments, tag_refs, attachment):
337 if tag_attachments is not None:
337 if tag_attachments is not None:
338 attachment_tag = et.SubElement(tag_attachments, TAG_ATTACHMENT)
338 attachment_tag = et.SubElement(tag_attachments, TAG_ATTACHMENT)
339 if attachment.is_internal():
339 if attachment.is_internal():
340 mimetype = get_file_mimetype(attachment.file.file)
340 mimetype = get_file_mimetype(attachment.file.file)
341 attachment_tag.set(ATTR_MIMETYPE, mimetype)
341 attachment_tag.set(ATTR_MIMETYPE, mimetype)
342 attachment_tag.set(ATTR_ID_TYPE, ID_TYPE_MD5)
342 attachment_tag.set(ATTR_ID_TYPE, ID_TYPE_MD5)
343 attachment_tag.text = attachment.hash
343 attachment_tag.text = attachment.hash
344 else:
344 else:
345 attachment_tag.set(ATTR_ID_TYPE, ID_TYPE_URL)
345 attachment_tag.set(ATTR_ID_TYPE, ID_TYPE_URL)
346 attachment_tag.text = attachment.url
346 attachment_tag.text = attachment.url
347
347
348 if tag_refs is not None and attachment.is_internal():
348 if tag_refs is not None and attachment.is_internal():
349 attachment_ref = et.SubElement(tag_refs, TAG_ATTACHMENT_REF)
349 attachment_ref = et.SubElement(tag_refs, TAG_ATTACHMENT_REF)
350 attachment_ref.set(ATTR_REF, attachment.hash)
350 attachment_ref.set(ATTR_REF, attachment.hash)
351 attachment_ref.set(ATTR_URL, attachment.file.url)
351 attachment_ref.set(ATTR_URL, attachment.file.url)
352
352
353 @staticmethod
353 @staticmethod
354 def generate_request_get(global_id_list: list):
354 def generate_request_get(global_id_list: list):
355 """
355 """
356 Form a get request from a list of ModelId objects.
356 Form a get request from a list of ModelId objects.
357 """
357 """
358
358
359 request = et.Element(TAG_REQUEST)
359 request = et.Element(TAG_REQUEST)
360 request.set(ATTR_TYPE, TYPE_GET)
360 request.set(ATTR_TYPE, TYPE_GET)
361 request.set(ATTR_VERSION, '1.0')
361 request.set(ATTR_VERSION, '1.0')
362
362
363 model = et.SubElement(request, TAG_MODEL)
363 model = et.SubElement(request, TAG_MODEL)
364 model.set(ATTR_VERSION, '1.0')
364 model.set(ATTR_VERSION, '1.0')
365 model.set(ATTR_NAME, 'post')
365 model.set(ATTR_NAME, 'post')
366
366
367 for global_id in global_id_list:
367 for global_id in global_id_list:
368 tag_id = et.SubElement(model, TAG_ID)
368 tag_id = et.SubElement(model, TAG_ID)
369 global_id.to_xml_element(tag_id)
369 global_id.to_xml_element(tag_id)
370
370
371 return et.tostring(request, 'unicode')
371 return et.tostring(request, 'unicode')
372
372
373 @staticmethod
373 @staticmethod
374 def generate_request_list(opening_post=None, tags=list(),
374 def generate_request_list(opening_post=None, tags=list(),
375 timestamp_from=None):
375 timestamp_from=None):
376 """
376 """
377 Form a pull request from a list of ModelId objects.
377 Form a pull request from a list of ModelId objects.
378 """
378 """
379
379
380 request = et.Element(TAG_REQUEST)
380 request = et.Element(TAG_REQUEST)
381 request.set(ATTR_TYPE, TYPE_LIST)
381 request.set(ATTR_TYPE, TYPE_LIST)
382 request.set(ATTR_VERSION, '1.0')
382 request.set(ATTR_VERSION, '1.0')
383
383
384 model = et.SubElement(request, TAG_MODEL)
384 model = et.SubElement(request, TAG_MODEL)
385 model.set(ATTR_VERSION, CURRENT_MODEL_VERSION)
385 model.set(ATTR_VERSION, CURRENT_MODEL_VERSION)
386 model.set(ATTR_NAME, 'post')
386 model.set(ATTR_NAME, 'post')
387
387
388 if opening_post:
388 if opening_post:
389 ThreadFilter().add_filter(model, opening_post)
389 ThreadFilter().add_filter(model, opening_post)
390 if tags:
390 if tags:
391 TagsFilter().add_filter(model, tags)
391 TagsFilter().add_filter(model, tags)
392 if timestamp_from:
392 if timestamp_from:
393 TimestampFromFilter().add_filter(model, timestamp_from)
393 TimestampFromFilter().add_filter(model, timestamp_from)
394
394
395 return et.tostring(request, 'unicode')
395 return et.tostring(request, 'unicode')
@@ -1,252 +1,253
1 from django.test import TestCase
1 from django.test import TestCase
2
2
3 from boards.models import KeyPair, Post, Tag
3 from boards.models import KeyPair, Post, Tag
4 from boards.models.post.sync import SyncManager
4 from boards.models.post.sync import SyncManager
5 from boards.tests.mocks import MockRequest
5 from boards.tests.mocks import MockRequest
6 from boards.views.sync import response_get, response_list
6 from boards.views.sync import response_get, response_list
7
7
8 __author__ = 'neko259'
8 __author__ = 'neko259'
9
9
10
10
11 class SyncTest(TestCase):
11 class SyncTest(TestCase):
12 def test_get(self):
12 def test_get(self):
13 """
13 """
14 Forms a GET request of a post and checks the response.
14 Forms a GET request of a post and checks the response.
15 """
15 """
16
16
17 key = KeyPair.objects.generate_key(primary=True)
17 key = KeyPair.objects.generate_key(primary=True)
18 tag, created = Tag.objects.get_or_create_with_alias(name='tag1')
18 tag, created = Tag.objects.get_or_create_with_alias(name='tag1')
19 post = Post.objects.create_post(title='test_title',
19 post = Post.objects.create_post(title='test_title',
20 text='test_text\rline two',
20 text='test_text\rline two',
21 tags=[tag])
21 tags=[tag])
22
22
23 request = MockRequest()
23 request = MockRequest()
24 request.body = (
24 request.body = (
25 '<request type="get" version="1.0">'
25 '<request type="get" version="1.0">'
26 '<model name="post" version="1.0">'
26 '<model name="post" version="1.0">'
27 '<id key="%s" local-id="%d" type="%s" />'
27 '<id key="%s" local-id="%d" type="%s" />'
28 '</model>'
28 '</model>'
29 '</request>' % (post.global_id.key,
29 '</request>' % (post.global_id.key,
30 post.id,
30 post.id,
31 post.global_id.key_type)
31 post.global_id.key_type)
32 )
32 )
33
33
34 response = response_get(request).content.decode()
34 response = response_get(request).content.decode()
35 self.assertTrue(
35 self.assertTrue(
36 '<status>success</status>'
36 '<status>success</status>'
37 '<models>'
37 '<models>'
38 '<model name="post">'
38 '<model name="post">'
39 '<content>'
39 '<content>'
40 '<id key="{}" local-id="{}" type="{}" />'
40 '<id key="{}" local-id="{}" type="{}" />'
41 '<title>{}</title>'
41 '<title>{}</title>'
42 '<text>{}</text>'
42 '<text>{}</text>'
43 '<tags><tag>{}</tag></tags>'
43 '<tags><tag>{}</tag></tags>'
44 '<pub-time>{}</pub-time>'
44 '<pub-time>{}</pub-time>'
45 '<update-time>{}</update-time>'
45 '<update-time>{}</update-time>'
46 '</content>'.format(
46 '</content>'.format(
47 post.global_id.key,
47 post.global_id.key,
48 post.global_id.local_id,
48 post.global_id.local_id,
49 post.global_id.key_type,
49 post.global_id.key_type,
50 post.title,
50 post.title,
51 post.get_sync_text(),
51 post.get_sync_text(),
52 post.get_thread().get_tags().first().get_name(),
52 post.get_thread().get_tags().first().get_name(),
53 post.get_pub_time_str(),
53 post.get_pub_time_str(),
54 post.last_edit_time,
54 post.last_edit_time,
55 ) in response,
55 ) in response,
56 'Wrong response generated for the GET request.')
56 'Wrong response generated for the GET request.')
57
57
58 post.delete()
58 post.delete()
59 key.delete()
59 key.delete()
60
60
61 KeyPair.objects.generate_key(primary=True)
61 KeyPair.objects.generate_key(primary=True)
62
62
63 SyncManager.parse_response_get(response, None)
63 SyncManager.parse_response_get(response, None)
64 self.assertEqual(1, Post.objects.count(),
64 self.assertEqual(1, Post.objects.count(),
65 'Post was not created from XML response.')
65 'Post was not created from XML response.')
66
66
67 parsed_post = Post.objects.first()
67 parsed_post = Post.objects.first()
68 self.assertEqual('tag1',
68 self.assertEqual('tag1',
69 parsed_post.get_thread().get_tags().first().get_name(),
69 parsed_post.get_thread().get_tags().first().get_name(),
70 'Invalid tag was parsed.')
70 'Invalid tag was parsed.')
71
71
72 SyncManager.parse_response_get(response, None)
72 SyncManager.parse_response_get(response, None)
73 self.assertEqual(1, Post.objects.count(),
73 self.assertEqual(1, Post.objects.count(),
74 'The same post was imported twice.')
74 'The same post was imported twice.')
75
75
76 self.assertEqual(1, parsed_post.global_id.signature_set.count(),
76 self.assertEqual(1, parsed_post.global_id.signature_set.count(),
77 'Signature was not saved.')
77 'Signature was not saved.')
78
78
79 post = parsed_post
79 post = parsed_post
80
80
81 # Trying to sync the same once more
81 # Trying to sync the same once more
82 response = response_get(request).content.decode()
82 response = response_get(request).content.decode()
83
83
84 self.assertTrue(
84 self.assertTrue(
85 '<status>success</status>'
85 '<status>success</status>'
86 '<models>'
86 '<models>'
87 '<model name="post">'
87 '<model name="post">'
88 '<content>'
88 '<content>'
89 '<id key="{}" local-id="{}" type="{}" />'
89 '<id key="{}" local-id="{}" type="{}" />'
90 '<title>{}</title>'
90 '<title>{}</title>'
91 '<text>{}</text>'
91 '<text>{}</text>'
92 '<tags><tag>{}</tag></tags>'
92 '<tags><tag>{}</tag></tags>'
93 '<pub-time>{}</pub-time>'
93 '<pub-time>{}</pub-time>'
94 '<update-time>{}</update-time>'
94 '<update-time>{}</update-time>'
95 '</content>'.format(
95 '</content>'.format(
96 post.global_id.key,
96 post.global_id.key,
97 post.global_id.local_id,
97 post.global_id.local_id,
98 post.global_id.key_type,
98 post.global_id.key_type,
99 post.title,
99 post.title,
100 post.get_sync_text(),
100 post.get_sync_text(),
101 post.get_thread().get_tags().first().get_name(),
101 post.get_thread().get_tags().first().get_name(),
102 post.get_pub_time_str(),
102 post.get_pub_time_str(),
103 post.last_edit_time,
103 ) in response,
104 ) in response,
104 'Wrong response generated for the GET request.')
105 'Wrong response generated for the GET request.')
105
106
106 def test_list_all(self):
107 def test_list_all(self):
107 key = KeyPair.objects.generate_key(primary=True)
108 key = KeyPair.objects.generate_key(primary=True)
108 tag, created = Tag.objects.get_or_create_with_alias(name='tag1')
109 tag, created = Tag.objects.get_or_create_with_alias(name='tag1')
109 post = Post.objects.create_post(title='test_title',
110 post = Post.objects.create_post(title='test_title',
110 text='test_text\rline two',
111 text='test_text\rline two',
111 tags=[tag])
112 tags=[tag])
112 post2 = Post.objects.create_post(title='test title 2',
113 post2 = Post.objects.create_post(title='test title 2',
113 text='test text 2',
114 text='test text 2',
114 tags=[tag])
115 tags=[tag])
115
116
116 request_all = MockRequest()
117 request_all = MockRequest()
117 request_all.body = (
118 request_all.body = (
118 '<request type="list" version="1.0">'
119 '<request type="list" version="1.0">'
119 '<model name="post" version="1.0">'
120 '<model name="post" version="1.0">'
120 '</model>'
121 '</model>'
121 '</request>'
122 '</request>'
122 )
123 )
123
124
124 response_all = response_list(request_all).content.decode()
125 response_all = response_list(request_all).content.decode()
125 self.assertTrue(
126 self.assertTrue(
126 '<status>success</status>'
127 '<status>success</status>'
127 '<models>'
128 '<models>'
128 '<model>'
129 '<model>'
129 '<id key="{}" local-id="{}" type="{}" />'
130 '<id key="{}" local-id="{}" type="{}" />'
130 '<update-time>{}</update-time>'
131 '<update-time>{}</update-time>'
131 '</model>'
132 '</model>'
132 '<model>'
133 '<model>'
133 '<id key="{}" local-id="{}" type="{}" />'
134 '<id key="{}" local-id="{}" type="{}" />'
134 '<update-time>{}</update-time>'
135 '<update-time>{}</update-time>'
135 '</model>'
136 '</model>'
136 '</models>'.format(
137 '</models>'.format(
137 post.global_id.key,
138 post.global_id.key,
138 post.global_id.local_id,
139 post.global_id.local_id,
139 post.global_id.key_type,
140 post.global_id.key_type,
140 post.last_edit_time,
141 post.last_edit_time,
141 post2.global_id.key,
142 post2.global_id.key,
142 post2.global_id.local_id,
143 post2.global_id.local_id,
143 post2.global_id.key_type,
144 post2.global_id.key_type,
144 post2.last_edit_time,
145 post2.last_edit_time,
145 ) in response_all,
146 ) in response_all,
146 'Wrong response generated for the LIST request for all posts.')
147 'Wrong response generated for the LIST request for all posts.')
147
148
148 def test_list_existing_thread(self):
149 def test_list_existing_thread(self):
149 key = KeyPair.objects.generate_key(primary=True)
150 key = KeyPair.objects.generate_key(primary=True)
150 tag, created = Tag.objects.get_or_create_with_alias(name='tag1')
151 tag, created = Tag.objects.get_or_create_with_alias(name='tag1')
151 post = Post.objects.create_post(title='test_title',
152 post = Post.objects.create_post(title='test_title',
152 text='test_text\rline two',
153 text='test_text\rline two',
153 tags=[tag])
154 tags=[tag])
154 post2 = Post.objects.create_post(title='test title 2',
155 post2 = Post.objects.create_post(title='test title 2',
155 text='test text 2',
156 text='test text 2',
156 tags=[tag])
157 tags=[tag])
157
158
158 request_thread = MockRequest()
159 request_thread = MockRequest()
159 request_thread.body = (
160 request_thread.body = (
160 '<request type="list" version="1.0">'
161 '<request type="list" version="1.0">'
161 '<model name="post" version="1.0">'
162 '<model name="post" version="1.0">'
162 '<thread>{}</thread>'
163 '<thread>{}</thread>'
163 '</model>'
164 '</model>'
164 '</request>'.format(
165 '</request>'.format(
165 post.id,
166 post.id,
166 )
167 )
167 )
168 )
168
169
169 response_thread = response_list(request_thread).content.decode()
170 response_thread = response_list(request_thread).content.decode()
170 self.assertTrue(
171 self.assertTrue(
171 '<status>success</status>'
172 '<status>success</status>'
172 '<models>'
173 '<models>'
173 '<model>'
174 '<model>'
174 '<id key="{}" local-id="{}" type="{}" />'
175 '<id key="{}" local-id="{}" type="{}" />'
175 '<update-time>{}</update-time>'
176 '<update-time>{}</update-time>'
176 '</model>'
177 '</model>'
177 '</models>'.format(
178 '</models>'.format(
178 post.global_id.key,
179 post.global_id.key,
179 post.global_id.local_id,
180 post.global_id.local_id,
180 post.global_id.key_type,
181 post.global_id.key_type,
181 post.last_edit_time,
182 post.last_edit_time,
182 ) in response_thread,
183 ) in response_thread,
183 'Wrong response generated for the LIST request for posts of '
184 'Wrong response generated for the LIST request for posts of '
184 'existing thread.')
185 'existing thread.')
185
186
186 def test_list_non_existing_thread(self):
187 def test_list_non_existing_thread(self):
187 key = KeyPair.objects.generate_key(primary=True)
188 key = KeyPair.objects.generate_key(primary=True)
188 tag, created = Tag.objects.get_or_create_with_alias(name='tag1')
189 tag, created = Tag.objects.get_or_create_with_alias(name='tag1')
189 post = Post.objects.create_post(title='test_title',
190 post = Post.objects.create_post(title='test_title',
190 text='test_text\rline two',
191 text='test_text\rline two',
191 tags=[tag])
192 tags=[tag])
192 post2 = Post.objects.create_post(title='test title 2',
193 post2 = Post.objects.create_post(title='test title 2',
193 text='test text 2',
194 text='test text 2',
194 tags=[tag])
195 tags=[tag])
195
196
196 request_thread = MockRequest()
197 request_thread = MockRequest()
197 request_thread.body = (
198 request_thread.body = (
198 '<request type="list" version="1.0">'
199 '<request type="list" version="1.0">'
199 '<model name="post" version="1.0">'
200 '<model name="post" version="1.0">'
200 '<thread>{}</thread>'
201 '<thread>{}</thread>'
201 '</model>'
202 '</model>'
202 '</request>'.format(
203 '</request>'.format(
203 0,
204 0,
204 )
205 )
205 )
206 )
206
207
207 response_thread = response_list(request_thread).content.decode()
208 response_thread = response_list(request_thread).content.decode()
208 self.assertTrue(
209 self.assertTrue(
209 '<status>success</status>'
210 '<status>success</status>'
210 '<models />'
211 '<models />'
211 in response_thread,
212 in response_thread,
212 'Wrong response generated for the LIST request for posts of '
213 'Wrong response generated for the LIST request for posts of '
213 'non-existing thread.')
214 'non-existing thread.')
214
215
215 def test_list_pub_time(self):
216 def test_list_pub_time(self):
216 key = KeyPair.objects.generate_key(primary=True)
217 key = KeyPair.objects.generate_key(primary=True)
217 tag, created = Tag.objects.get_or_create_with_alias(name='tag1')
218 tag, created = Tag.objects.get_or_create_with_alias(name='tag1')
218 post = Post.objects.create_post(title='test_title',
219 post = Post.objects.create_post(title='test_title',
219 text='test_text\rline two',
220 text='test_text\rline two',
220 tags=[tag])
221 tags=[tag])
221 post2 = Post.objects.create_post(title='test title 2',
222 post2 = Post.objects.create_post(title='test title 2',
222 text='test text 2',
223 text='test text 2',
223 tags=[tag])
224 tags=[tag])
224
225
225 request_thread = MockRequest()
226 request_thread = MockRequest()
226 request_thread.body = (
227 request_thread.body = (
227 '<request type="list" version="1.0">'
228 '<request type="list" version="1.0">'
228 '<model name="post" version="1.0">'
229 '<model name="post" version="1.0">'
229 '<timestamp_from>{}</timestamp_from>'
230 '<timestamp_from>{}</timestamp_from>'
230 '</model>'
231 '</model>'
231 '</request>'.format(
232 '</request>'.format(
232 post.pub_time,
233 post.pub_time,
233 )
234 )
234 )
235 )
235
236
236 response_thread = response_list(request_thread).content.decode()
237 response_thread = response_list(request_thread).content.decode()
237 self.assertTrue(
238 self.assertTrue(
238 '<status>success</status>'
239 '<status>success</status>'
239 '<models>'
240 '<models>'
240 '<model>'
241 '<model>'
241 '<id key="{}" local-id="{}" type="{}" />'
242 '<id key="{}" local-id="{}" type="{}" />'
242 '<update-time>{}</update_time>'
243 '<update-time>{}</update_time>'
243 '</model>'
244 '</model>'
244 '</models>'.format(
245 '</models>'.format(
245 post2.global_id.key,
246 post2.global_id.key,
246 post2.global_id.local_id,
247 post2.global_id.local_id,
247 post2.global_id.key_type,
248 post2.global_id.key_type,
248 post2.last_edit_time,
249 post2.last_edit_time,
249 ) in response_thread,
250 ) in response_thread,
250 'Wrong response generated for the LIST request for posts of '
251 'Wrong response generated for the LIST request for posts of '
251 'existing thread.')
252 'existing thread.')
252
253
General Comments 0
You need to be logged in to leave comments. Login now