##// END OF EJS Templates
Attach synced file as link if it exceeds local filesize limit
bodqhrohro -
r2142:ee12465d default
parent child Browse files
Show More
@@ -1,331 +1,337 b''
1 import xml.etree.ElementTree as et
1 import xml.etree.ElementTree as et
2 import logging
2 import logging
3
3
4 from boards.abstracts.exceptions import SyncException
4 from boards.abstracts.exceptions import SyncException
5 from boards.models import KeyPair, GlobalId, Signature, Post, Tag
5 from boards.models import KeyPair, GlobalId, Signature, Post, Tag
6 from boards.models.attachment.downloaders import download
6 from boards.models.attachment.downloaders import download
7 from boards.utils import get_file_mimetype, get_file_hash
7 from boards.utils import get_file_mimetype, get_file_hash
8 from django.db import transaction
8 from django.db import transaction
9 from django import forms
9
10
10 EXCEPTION_NODE = 'Sync node returned an error: {}.'
11 EXCEPTION_NODE = 'Sync node returned an error: {}.'
11 EXCEPTION_DOWNLOAD = 'File was not downloaded.'
12 EXCEPTION_DOWNLOAD = 'File was not downloaded.'
12 EXCEPTION_HASH = 'File hash does not match attachment hash.'
13 EXCEPTION_HASH = 'File hash does not match attachment hash.'
13 EXCEPTION_SIGNATURE = 'Invalid model signature for {}.'
14 EXCEPTION_SIGNATURE = 'Invalid model signature for {}.'
14 EXCEPTION_AUTHOR_SIGNATURE = 'Model {} has no author signature.'
15 EXCEPTION_AUTHOR_SIGNATURE = 'Model {} has no author signature.'
15 ENCODING_UNICODE = 'unicode'
16 ENCODING_UNICODE = 'unicode'
16
17
17 TAG_MODEL = 'model'
18 TAG_MODEL = 'model'
18 TAG_REQUEST = 'request'
19 TAG_REQUEST = 'request'
19 TAG_RESPONSE = 'response'
20 TAG_RESPONSE = 'response'
20 TAG_ID = 'id'
21 TAG_ID = 'id'
21 TAG_STATUS = 'status'
22 TAG_STATUS = 'status'
22 TAG_MODELS = 'models'
23 TAG_MODELS = 'models'
23 TAG_TITLE = 'title'
24 TAG_TITLE = 'title'
24 TAG_TEXT = 'text'
25 TAG_TEXT = 'text'
25 TAG_THREAD = 'thread'
26 TAG_THREAD = 'thread'
26 TAG_PUB_TIME = 'pub-time'
27 TAG_PUB_TIME = 'pub-time'
27 TAG_SIGNATURES = 'signatures'
28 TAG_SIGNATURES = 'signatures'
28 TAG_SIGNATURE = 'signature'
29 TAG_SIGNATURE = 'signature'
29 TAG_CONTENT = 'content'
30 TAG_CONTENT = 'content'
30 TAG_ATTACHMENTS = 'attachments'
31 TAG_ATTACHMENTS = 'attachments'
31 TAG_ATTACHMENT = 'attachment'
32 TAG_ATTACHMENT = 'attachment'
32 TAG_TAGS = 'tags'
33 TAG_TAGS = 'tags'
33 TAG_TAG = 'tag'
34 TAG_TAG = 'tag'
34 TAG_ATTACHMENT_REFS = 'attachment-refs'
35 TAG_ATTACHMENT_REFS = 'attachment-refs'
35 TAG_ATTACHMENT_REF = 'attachment-ref'
36 TAG_ATTACHMENT_REF = 'attachment-ref'
36 TAG_TRIPCODE = 'tripcode'
37 TAG_TRIPCODE = 'tripcode'
37 TAG_VERSION = 'version'
38 TAG_VERSION = 'version'
38
39
39 TYPE_GET = 'get'
40 TYPE_GET = 'get'
40
41
41 ATTR_VERSION = 'version'
42 ATTR_VERSION = 'version'
42 ATTR_TYPE = 'type'
43 ATTR_TYPE = 'type'
43 ATTR_NAME = 'name'
44 ATTR_NAME = 'name'
44 ATTR_VALUE = 'value'
45 ATTR_VALUE = 'value'
45 ATTR_MIMETYPE = 'mimetype'
46 ATTR_MIMETYPE = 'mimetype'
46 ATTR_KEY = 'key'
47 ATTR_KEY = 'key'
47 ATTR_REF = 'ref'
48 ATTR_REF = 'ref'
48 ATTR_URL = 'url'
49 ATTR_URL = 'url'
49 ATTR_ID_TYPE = 'id-type'
50 ATTR_ID_TYPE = 'id-type'
50
51
51 ID_TYPE_MD5 = 'md5'
52 ID_TYPE_MD5 = 'md5'
52 ID_TYPE_URL = 'url'
53 ID_TYPE_URL = 'url'
53
54
54 STATUS_SUCCESS = 'success'
55 STATUS_SUCCESS = 'success'
55
56
56
57
57 logger = logging.getLogger('boards.sync')
58 logger = logging.getLogger('boards.sync')
58
59
59
60
60 class SyncManager:
61 class SyncManager:
61 @staticmethod
62 @staticmethod
62 def generate_response_get(model_list: list):
63 def generate_response_get(model_list: list):
63 response = et.Element(TAG_RESPONSE)
64 response = et.Element(TAG_RESPONSE)
64
65
65 status = et.SubElement(response, TAG_STATUS)
66 status = et.SubElement(response, TAG_STATUS)
66 status.text = STATUS_SUCCESS
67 status.text = STATUS_SUCCESS
67
68
68 models = et.SubElement(response, TAG_MODELS)
69 models = et.SubElement(response, TAG_MODELS)
69
70
70 for post in model_list:
71 for post in model_list:
71 model = et.SubElement(models, TAG_MODEL)
72 model = et.SubElement(models, TAG_MODEL)
72 model.set(ATTR_NAME, 'post')
73 model.set(ATTR_NAME, 'post')
73
74
74 global_id = post.global_id
75 global_id = post.global_id
75
76
76 attachments = post.attachments.all()
77 attachments = post.attachments.all()
77 if global_id.content:
78 if global_id.content:
78 model.append(et.fromstring(global_id.content))
79 model.append(et.fromstring(global_id.content))
79 if len(attachments) > 0:
80 if len(attachments) > 0:
80 internal_attachments = False
81 internal_attachments = False
81 for attachment in attachments:
82 for attachment in attachments:
82 if attachment.is_internal():
83 if attachment.is_internal():
83 internal_attachments = True
84 internal_attachments = True
84 break
85 break
85
86
86 if internal_attachments:
87 if internal_attachments:
87 attachment_refs = et.SubElement(model, TAG_ATTACHMENT_REFS)
88 attachment_refs = et.SubElement(model, TAG_ATTACHMENT_REFS)
88 for file in attachments:
89 for file in attachments:
89 SyncManager._attachment_to_xml(
90 SyncManager._attachment_to_xml(
90 None, attachment_refs, file)
91 None, attachment_refs, file)
91 else:
92 else:
92 content_tag = et.SubElement(model, TAG_CONTENT)
93 content_tag = et.SubElement(model, TAG_CONTENT)
93
94
94 tag_id = et.SubElement(content_tag, TAG_ID)
95 tag_id = et.SubElement(content_tag, TAG_ID)
95 global_id.to_xml_element(tag_id)
96 global_id.to_xml_element(tag_id)
96
97
97 title = et.SubElement(content_tag, TAG_TITLE)
98 title = et.SubElement(content_tag, TAG_TITLE)
98 title.text = post.title
99 title.text = post.title
99
100
100 text = et.SubElement(content_tag, TAG_TEXT)
101 text = et.SubElement(content_tag, TAG_TEXT)
101 text.text = post.get_sync_text()
102 text.text = post.get_sync_text()
102
103
103 thread = post.get_thread()
104 thread = post.get_thread()
104 if post.is_opening():
105 if post.is_opening():
105 tag_tags = et.SubElement(content_tag, TAG_TAGS)
106 tag_tags = et.SubElement(content_tag, TAG_TAGS)
106 for tag in thread.get_tags():
107 for tag in thread.get_tags():
107 tag_tag = et.SubElement(tag_tags, TAG_TAG)
108 tag_tag = et.SubElement(tag_tags, TAG_TAG)
108 tag_tag.text = tag.name
109 tag_tag.text = tag.name
109 else:
110 else:
110 tag_thread = et.SubElement(content_tag, TAG_THREAD)
111 tag_thread = et.SubElement(content_tag, TAG_THREAD)
111 thread_id = et.SubElement(tag_thread, TAG_ID)
112 thread_id = et.SubElement(tag_thread, TAG_ID)
112 thread.get_opening_post().global_id.to_xml_element(thread_id)
113 thread.get_opening_post().global_id.to_xml_element(thread_id)
113
114
114 pub_time = et.SubElement(content_tag, TAG_PUB_TIME)
115 pub_time = et.SubElement(content_tag, TAG_PUB_TIME)
115 pub_time.text = str(post.get_pub_time_str())
116 pub_time.text = str(post.get_pub_time_str())
116
117
117 if post.tripcode:
118 if post.tripcode:
118 tripcode = et.SubElement(content_tag, TAG_TRIPCODE)
119 tripcode = et.SubElement(content_tag, TAG_TRIPCODE)
119 tripcode.text = post.tripcode
120 tripcode.text = post.tripcode
120
121
121 if len(attachments) > 0:
122 if len(attachments) > 0:
122 attachments_tag = et.SubElement(content_tag, TAG_ATTACHMENTS)
123 attachments_tag = et.SubElement(content_tag, TAG_ATTACHMENTS)
123
124
124 internal_attachments = False
125 internal_attachments = False
125 for attachment in attachments:
126 for attachment in attachments:
126 if attachment.is_internal():
127 if attachment.is_internal():
127 internal_attachments = True
128 internal_attachments = True
128 break
129 break
129
130
130 if internal_attachments:
131 if internal_attachments:
131 attachment_refs = et.SubElement(model, TAG_ATTACHMENT_REFS)
132 attachment_refs = et.SubElement(model, TAG_ATTACHMENT_REFS)
132 else:
133 else:
133 attachment_refs = None
134 attachment_refs = None
134
135
135 for file in attachments:
136 for file in attachments:
136 SyncManager._attachment_to_xml(
137 SyncManager._attachment_to_xml(
137 attachments_tag, attachment_refs, file)
138 attachments_tag, attachment_refs, file)
138 version_tag = et.SubElement(content_tag, TAG_VERSION)
139 version_tag = et.SubElement(content_tag, TAG_VERSION)
139 version_tag.text = str(post.version)
140 version_tag.text = str(post.version)
140
141
141 global_id.content = et.tostring(content_tag, ENCODING_UNICODE)
142 global_id.content = et.tostring(content_tag, ENCODING_UNICODE)
142 global_id.save()
143 global_id.save()
143
144
144 signatures_tag = et.SubElement(model, TAG_SIGNATURES)
145 signatures_tag = et.SubElement(model, TAG_SIGNATURES)
145 post_signatures = global_id.signature_set.all()
146 post_signatures = global_id.signature_set.all()
146 if post_signatures:
147 if post_signatures:
147 signatures = post_signatures
148 signatures = post_signatures
148 else:
149 else:
149 key = KeyPair.objects.get(public_key=global_id.key)
150 key = KeyPair.objects.get(public_key=global_id.key)
150 signature = Signature(
151 signature = Signature(
151 key_type=key.key_type,
152 key_type=key.key_type,
152 key=key.public_key,
153 key=key.public_key,
153 signature=key.sign(global_id.content),
154 signature=key.sign(global_id.content),
154 global_id=global_id,
155 global_id=global_id,
155 )
156 )
156 signature.save()
157 signature.save()
157 signatures = [signature]
158 signatures = [signature]
158 for signature in signatures:
159 for signature in signatures:
159 signature_tag = et.SubElement(signatures_tag, TAG_SIGNATURE)
160 signature_tag = et.SubElement(signatures_tag, TAG_SIGNATURE)
160 signature_tag.set(ATTR_TYPE, signature.key_type)
161 signature_tag.set(ATTR_TYPE, signature.key_type)
161 signature_tag.set(ATTR_VALUE, signature.signature)
162 signature_tag.set(ATTR_VALUE, signature.signature)
162 signature_tag.set(ATTR_KEY, signature.key)
163 signature_tag.set(ATTR_KEY, signature.key)
163
164
164 return et.tostring(response, ENCODING_UNICODE)
165 return et.tostring(response, ENCODING_UNICODE)
165
166
166 @staticmethod
167 @staticmethod
167 def parse_response_get(response_xml, hostname):
168 def parse_response_get(response_xml, hostname):
168 tag_root = et.fromstring(response_xml)
169 tag_root = et.fromstring(response_xml)
169 tag_status = tag_root.find(TAG_STATUS)
170 tag_status = tag_root.find(TAG_STATUS)
170 if STATUS_SUCCESS == tag_status.text:
171 if STATUS_SUCCESS == tag_status.text:
171 tag_models = tag_root.find(TAG_MODELS)
172 tag_models = tag_root.find(TAG_MODELS)
172 for tag_model in tag_models:
173 for tag_model in tag_models:
173 SyncManager.parse_post(tag_model, hostname)
174 SyncManager.parse_post(tag_model, hostname)
174 else:
175 else:
175 raise SyncException(EXCEPTION_NODE.format(tag_status.text))
176 raise SyncException(EXCEPTION_NODE.format(tag_status.text))
176
177
177 @staticmethod
178 @staticmethod
178 @transaction.atomic
179 @transaction.atomic
179 def parse_post(tag_model, hostname):
180 def parse_post(tag_model, hostname):
180 tag_content = tag_model.find(TAG_CONTENT)
181 tag_content = tag_model.find(TAG_CONTENT)
181
182
182 content_str = et.tostring(tag_content, ENCODING_UNICODE)
183 content_str = et.tostring(tag_content, ENCODING_UNICODE)
183
184
184 tag_id = tag_content.find(TAG_ID)
185 tag_id = tag_content.find(TAG_ID)
185 global_id, exists = GlobalId.from_xml_element(tag_id)
186 global_id, exists = GlobalId.from_xml_element(tag_id)
186 signatures = SyncManager._verify_model(global_id, content_str, tag_model)
187 signatures = SyncManager._verify_model(global_id, content_str, tag_model)
187
188
188 version = int(tag_content.find(TAG_VERSION).text)
189 version = int(tag_content.find(TAG_VERSION).text)
189 is_old = exists and global_id.post.version < version
190 is_old = exists and global_id.post.version < version
190 if exists and not is_old:
191 if exists and not is_old:
191 print('Post with same ID exists and is up to date.')
192 print('Post with same ID exists and is up to date.')
192 else:
193 else:
193 global_id.content = content_str
194 global_id.content = content_str
194 global_id.save()
195 global_id.save()
195 for signature in signatures:
196 for signature in signatures:
196 signature.global_id = global_id
197 signature.global_id = global_id
197 signature.save()
198 signature.save()
198
199
199 title = tag_content.find(TAG_TITLE).text or ''
200 title = tag_content.find(TAG_TITLE).text or ''
200 text = tag_content.find(TAG_TEXT).text or ''
201 text = tag_content.find(TAG_TEXT).text or ''
201 pub_time = tag_content.find(TAG_PUB_TIME).text
202 pub_time = tag_content.find(TAG_PUB_TIME).text
202 tripcode_tag = tag_content.find(TAG_TRIPCODE)
203 tripcode_tag = tag_content.find(TAG_TRIPCODE)
203 if tripcode_tag is not None:
204 if tripcode_tag is not None:
204 tripcode = tripcode_tag.text or ''
205 tripcode = tripcode_tag.text or ''
205 else:
206 else:
206 tripcode = ''
207 tripcode = ''
207
208
208 thread = tag_content.find(TAG_THREAD)
209 thread = tag_content.find(TAG_THREAD)
209 tags = []
210 tags = []
210 if thread:
211 if thread:
211 thread_id = thread.find(TAG_ID)
212 thread_id = thread.find(TAG_ID)
212 op_global_id, exists = GlobalId.from_xml_element(thread_id)
213 op_global_id, exists = GlobalId.from_xml_element(thread_id)
213 if exists:
214 if exists:
214 opening_post = Post.objects.get(global_id=op_global_id)
215 opening_post = Post.objects.get(global_id=op_global_id)
215 else:
216 else:
216 logger.debug('No thread exists for post {}'.format(global_id))
217 logger.debug('No thread exists for post {}'.format(global_id))
217 else:
218 else:
218 opening_post = None
219 opening_post = None
219 tag_tags = tag_content.find(TAG_TAGS)
220 tag_tags = tag_content.find(TAG_TAGS)
220 for tag_tag in tag_tags:
221 for tag_tag in tag_tags:
221 tag, created = Tag.objects.get_or_create(
222 tag, created = Tag.objects.get_or_create(
222 name=tag_tag.text)
223 name=tag_tag.text)
223 tags.append(tag)
224 tags.append(tag)
224
225
225 # TODO Check that the replied posts are already present
226 # TODO Check that the replied posts are already present
226 # before adding new ones
227 # before adding new ones
227
228
228 files = []
229 files = []
229 urls = []
230 urls = []
230 tag_attachments = tag_content.find(TAG_ATTACHMENTS) or list()
231 tag_attachments = tag_content.find(TAG_ATTACHMENTS) or list()
231 tag_refs = tag_model.find(TAG_ATTACHMENT_REFS)
232 tag_refs = tag_model.find(TAG_ATTACHMENT_REFS)
232 for attachment in tag_attachments:
233 for attachment in tag_attachments:
233 if attachment.get(ATTR_ID_TYPE) == ID_TYPE_URL:
234 if attachment.get(ATTR_ID_TYPE) == ID_TYPE_URL:
234 urls.append(attachment.text)
235 urls.append(attachment.text)
235 else:
236 else:
236 tag_ref = tag_refs.find("{}[@ref='{}']".format(
237 tag_ref = tag_refs.find("{}[@ref='{}']".format(
237 TAG_ATTACHMENT_REF, attachment.text))
238 TAG_ATTACHMENT_REF, attachment.text))
238 url = tag_ref.get(ATTR_URL)
239 url = tag_ref.get(ATTR_URL)
239 attached_file = download(hostname + url)
240 try:
240 if attached_file is None:
241 attached_file = download(hostname + url)
241 raise SyncException(EXCEPTION_DOWNLOAD)
242
243 if attached_file is None:
244 raise SyncException(EXCEPTION_DOWNLOAD)
242
245
243 hash = get_file_hash(attached_file)
246 hash = get_file_hash(attached_file)
244 if hash != attachment.text:
247 if hash != attachment.text:
245 raise SyncException(EXCEPTION_HASH)
248 raise SyncException(EXCEPTION_HASH)
246
249
247 files.append(attached_file)
250 files.append(attached_file)
251 except forms.ValidationError:
252 urls.append(hostname+url)
253
248
254
249 if is_old:
255 if is_old:
250 post = global_id.post
256 post = global_id.post
251 Post.objects.update_post(
257 Post.objects.update_post(
252 post, title=title, text=text, pub_time=pub_time,
258 post, title=title, text=text, pub_time=pub_time,
253 tags=tags, files=files, file_urls=urls,
259 tags=tags, files=files, file_urls=urls,
254 tripcode=tripcode, version=version)
260 tripcode=tripcode, version=version)
255 logger.debug('Parsed updated post {}'.format(global_id))
261 logger.debug('Parsed updated post {}'.format(global_id))
256 else:
262 else:
257 Post.objects.import_post(
263 Post.objects.import_post(
258 title=title, text=text, pub_time=pub_time,
264 title=title, text=text, pub_time=pub_time,
259 opening_post=opening_post, tags=tags,
265 opening_post=opening_post, tags=tags,
260 global_id=global_id, files=files,
266 global_id=global_id, files=files,
261 file_urls=urls, tripcode=tripcode,
267 file_urls=urls, tripcode=tripcode,
262 version=version)
268 version=version)
263 logger.debug('Parsed new post {}'.format(global_id))
269 logger.debug('Parsed new post {}'.format(global_id))
264
270
265 @staticmethod
271 @staticmethod
266 def generate_response_list():
272 def generate_response_list():
267 response = et.Element(TAG_RESPONSE)
273 response = et.Element(TAG_RESPONSE)
268
274
269 status = et.SubElement(response, TAG_STATUS)
275 status = et.SubElement(response, TAG_STATUS)
270 status.text = STATUS_SUCCESS
276 status.text = STATUS_SUCCESS
271
277
272 models = et.SubElement(response, TAG_MODELS)
278 models = et.SubElement(response, TAG_MODELS)
273
279
274 for post in Post.objects.prefetch_related('global_id').all():
280 for post in Post.objects.prefetch_related('global_id').all():
275 tag_model = et.SubElement(models, TAG_MODEL)
281 tag_model = et.SubElement(models, TAG_MODEL)
276 tag_id = et.SubElement(tag_model, TAG_ID)
282 tag_id = et.SubElement(tag_model, TAG_ID)
277 post.global_id.to_xml_element(tag_id)
283 post.global_id.to_xml_element(tag_id)
278 tag_version = et.SubElement(tag_model, TAG_VERSION)
284 tag_version = et.SubElement(tag_model, TAG_VERSION)
279 tag_version.text = str(post.version)
285 tag_version.text = str(post.version)
280
286
281 return et.tostring(response, ENCODING_UNICODE)
287 return et.tostring(response, ENCODING_UNICODE)
282
288
283 @staticmethod
289 @staticmethod
284 def _verify_model(global_id, content_str, tag_model):
290 def _verify_model(global_id, content_str, tag_model):
285 """
291 """
286 Verifies all signatures for a single model.
292 Verifies all signatures for a single model.
287 """
293 """
288
294
289 signatures = []
295 signatures = []
290
296
291 tag_signatures = tag_model.find(TAG_SIGNATURES)
297 tag_signatures = tag_model.find(TAG_SIGNATURES)
292 has_author_signature = False
298 has_author_signature = False
293 for tag_signature in tag_signatures:
299 for tag_signature in tag_signatures:
294 signature_type = tag_signature.get(ATTR_TYPE)
300 signature_type = tag_signature.get(ATTR_TYPE)
295 signature_value = tag_signature.get(ATTR_VALUE)
301 signature_value = tag_signature.get(ATTR_VALUE)
296 signature_key = tag_signature.get(ATTR_KEY)
302 signature_key = tag_signature.get(ATTR_KEY)
297
303
298 if global_id.key_type == signature_type and\
304 if global_id.key_type == signature_type and\
299 global_id.key == signature_key:
305 global_id.key == signature_key:
300 has_author_signature = True
306 has_author_signature = True
301
307
302 signature = Signature(key_type=signature_type,
308 signature = Signature(key_type=signature_type,
303 key=signature_key,
309 key=signature_key,
304 signature=signature_value)
310 signature=signature_value)
305
311
306 if not KeyPair.objects.verify(signature, content_str):
312 if not KeyPair.objects.verify(signature, content_str):
307 raise SyncException(EXCEPTION_SIGNATURE.format(content_str))
313 raise SyncException(EXCEPTION_SIGNATURE.format(content_str))
308
314
309 signatures.append(signature)
315 signatures.append(signature)
310 if not has_author_signature:
316 if not has_author_signature:
311 raise SyncException(EXCEPTION_AUTHOR_SIGNATURE.format(content_str))
317 raise SyncException(EXCEPTION_AUTHOR_SIGNATURE.format(content_str))
312
318
313 return signatures
319 return signatures
314
320
315 @staticmethod
321 @staticmethod
316 def _attachment_to_xml(tag_attachments, tag_refs, attachment):
322 def _attachment_to_xml(tag_attachments, tag_refs, attachment):
317 if tag_attachments is not None:
323 if tag_attachments is not None:
318 attachment_tag = et.SubElement(tag_attachments, TAG_ATTACHMENT)
324 attachment_tag = et.SubElement(tag_attachments, TAG_ATTACHMENT)
319 if attachment.is_internal():
325 if attachment.is_internal():
320 mimetype = get_file_mimetype(attachment.file.file)
326 mimetype = get_file_mimetype(attachment.file.file)
321 attachment_tag.set(ATTR_MIMETYPE, mimetype)
327 attachment_tag.set(ATTR_MIMETYPE, mimetype)
322 attachment_tag.set(ATTR_ID_TYPE, ID_TYPE_MD5)
328 attachment_tag.set(ATTR_ID_TYPE, ID_TYPE_MD5)
323 attachment_tag.text = attachment.hash
329 attachment_tag.text = attachment.hash
324 else:
330 else:
325 attachment_tag.set(ATTR_ID_TYPE, ID_TYPE_URL)
331 attachment_tag.set(ATTR_ID_TYPE, ID_TYPE_URL)
326 attachment_tag.text = attachment.url
332 attachment_tag.text = attachment.url
327
333
328 if tag_refs is not None:
334 if tag_refs is not None:
329 attachment_ref = et.SubElement(tag_refs, TAG_ATTACHMENT_REF)
335 attachment_ref = et.SubElement(tag_refs, TAG_ATTACHMENT_REF)
330 attachment_ref.set(ATTR_REF, attachment.hash)
336 attachment_ref.set(ATTR_REF, attachment.hash)
331 attachment_ref.set(ATTR_URL, attachment.file.url)
337 attachment_ref.set(ATTR_URL, attachment.file.url)
General Comments 0
You need to be logged in to leave comments. Login now