##// END OF EJS Templates
Allow filtering sync by tags
neko259 -
r1844:031556bd default
parent child Browse files
Show More
@@ -1,31 +1,55 b''
1 1 import xml.etree.ElementTree as et
2 2
3 from boards.models import Post
3 from boards.models import Post, Tag
4 4
5 5 TAG_THREAD = 'thread'
6 TAG_TAGS = 'tags'
7 TAG_TAG = 'tag'
6 8
7 9
8 10 class PostFilter:
9 11 def __init__(self, content=None):
10 12 self.content = content
11 13
12 14 def filter(self, posts):
13 15 return posts
14 16
15 17 def add_filter(self, model_tag, value):
16 18 return model_tag
17 19
18 20
19 21 class ThreadFilter(PostFilter):
20 22 def filter(self, posts):
21 23 op_id = self.content.text
22 24
23 25 op = Post.objects.filter(opening=True, id=op_id).first()
24 26 if op:
25 27 return posts.filter(thread=op.get_thread())
26 28 else:
27 29 return posts.none()
28 30
29 31 def add_filter(self, model_tag, value):
30 32 thread_tag = et.SubElement(model_tag, TAG_THREAD)
31 33 thread_tag.text = str(value)
34
35
36 class TagsFilter(PostFilter):
37 def filter(self, posts):
38 tags = []
39 for tag_tag in self.content:
40 try:
41 tags.append(Tag.objects.get(name=tag_tag.text))
42 except Tag.DoesNotExist:
43 pass
44
45 if tags:
46 return posts.filter(thread__tags__in=tags)
47 else:
48 return posts.none()
49
50 def add_filter(self, model_tag, value):
51 tags_tag = et.SubElement(model_tag, TAG_TAGS)
52 for tag_name in value:
53 tag_tag = et.SubElement(tags_tag, TAG_TAG)
54 tag_tag.text = tag_name
55
@@ -1,99 +1,104 b''
1 1 import re
2 2 import logging
3 3 import xml.etree.ElementTree as ET
4 4
5 5 import httplib2
6 6 from django.core.management import BaseCommand
7 7
8 8 from boards.models import GlobalId
9 9 from boards.models.post.sync import SyncManager, TAG_ID, TAG_VERSION
10 10
11 11 __author__ = 'neko259'
12 12
13 13
14 14 REGEX_GLOBAL_ID = re.compile(r'(\w+)::([\w\+/]+)::(\d+)')
15 15
16 16
17 17 class Command(BaseCommand):
18 18 help = 'Send a sync or get request to the server.'
19 19
20 20 def add_arguments(self, parser):
21 21 parser.add_argument('url', type=str, help='Server root url')
22 22 parser.add_argument('--global-id', type=str, default='',
23 23 help='Post global ID')
24 24 parser.add_argument('--split-query', type=int, default=1,
25 25 help='Split GET query into separate by the given'
26 26 ' number of posts in one')
27 27 parser.add_argument('--thread', type=int,
28 28 help='Get posts of one specific thread')
29 parser.add_argument('--tags', type=str,
30 help='Get posts of the tags, comma-separated')
29 31
30 32 def handle(self, *args, **options):
31 33 logger = logging.getLogger('boards.sync')
32 34
33 35 url = options.get('url')
34 36
35 37 list_url = url + 'api/sync/list/'
36 38 get_url = url + 'api/sync/get/'
37 39 file_url = url[:-1]
38 40
39 41 global_id_str = options.get('global_id')
40 42 if global_id_str:
41 43 match = REGEX_GLOBAL_ID.match(global_id_str)
42 44 if match:
43 45 key_type = match.group(1)
44 46 key = match.group(2)
45 47 local_id = match.group(3)
46 48
47 49 global_id = GlobalId(key_type=key_type, key=key,
48 50 local_id=local_id)
49 51
50 52 xml = SyncManager.generate_request_get([global_id])
51 53 h = httplib2.Http()
52 54 response, content = h.request(get_url, method="POST", body=xml)
53 55
54 56 SyncManager.parse_response_get(content, file_url)
55 57 else:
56 58 raise Exception('Invalid global ID')
57 59 else:
58 60 logger.info('Running LIST request...')
59 61 h = httplib2.Http()
60 62 xml = SyncManager.generate_request_list(
61 opening_post=options.get('thread'))
63 opening_post=options.get('thread'), tags=options.get('tags').split(',')).encode()
62 64 response, content = h.request(list_url, method="POST", body=xml)
65 if response.status != 200:
66 raise Exception('Server returned error {}'.format(response.status))
67
63 68 logger.info('Processing response...')
64 69
65 70 root = ET.fromstring(content)
66 71 status = root.findall('status')[0].text
67 72 if status == 'success':
68 73 ids_to_sync = list()
69 74
70 75 models = root.findall('models')[0]
71 76 for model in models:
72 77 tag_id = model.find(TAG_ID)
73 78 global_id, exists = GlobalId.from_xml_element(tag_id)
74 79 tag_version = model.find(TAG_VERSION)
75 80 if tag_version is not None:
76 81 version = int(tag_version.text) or 1
77 82 else:
78 83 version = 1
79 84 if not exists or global_id.post.version < version:
80 85 logger.debug('Processed (+) post {}'.format(global_id))
81 86 ids_to_sync.append(global_id)
82 87 else:
83 88 logger.debug('* Processed (-) post {}'.format(global_id))
84 89 logger.info('Starting sync...')
85 90
86 91 if len(ids_to_sync) > 0:
87 92 limit = options.get('split_query', len(ids_to_sync))
88 93 for offset in range(0, len(ids_to_sync), limit):
89 94 xml = SyncManager.generate_request_get(ids_to_sync[offset:offset + limit])
90 95 h = httplib2.Http()
91 96 logger.info('Running GET request...')
92 97 response, content = h.request(get_url, method="POST", body=xml)
93 98 logger.info('Processing response...')
94 99
95 100 SyncManager.parse_response_get(content, file_url)
96 101 else:
97 102 logger.info('Nothing to get, everything synced')
98 103 else:
99 104 raise Exception('Invalid response status')
@@ -1,379 +1,381 b''
1 1 import xml.etree.ElementTree as et
2 2 import logging
3 3 from xml.etree import ElementTree
4 4
5 5 from boards.abstracts.exceptions import SyncException
6 from boards.abstracts.sync_filters import ThreadFilter
6 from boards.abstracts.sync_filters import ThreadFilter, TagsFilter
7 7 from boards.models import KeyPair, GlobalId, Signature, Post, Tag
8 8 from boards.models.attachment.downloaders import download
9 9 from boards.models.signature import TAG_REQUEST, ATTR_TYPE, TYPE_GET, \
10 10 ATTR_VERSION, TAG_MODEL, ATTR_NAME, TAG_ID, TYPE_LIST
11 11 from boards.utils import get_file_mimetype, get_file_hash
12 12 from django.db import transaction
13 13
14 14 EXCEPTION_NODE = 'Sync node returned an error: {}.'
15 15 EXCEPTION_DOWNLOAD = 'File was not downloaded.'
16 16 EXCEPTION_HASH = 'File hash does not match attachment hash.'
17 17 EXCEPTION_SIGNATURE = 'Invalid model signature for {}.'
18 18 EXCEPTION_AUTHOR_SIGNATURE = 'Model {} has no author signature.'
19 19 EXCEPTION_THREAD = 'No thread exists for post {}'
20 20 ENCODING_UNICODE = 'unicode'
21 21
22 22 TAG_MODEL = 'model'
23 23 TAG_REQUEST = 'request'
24 24 TAG_RESPONSE = 'response'
25 25 TAG_ID = 'id'
26 26 TAG_STATUS = 'status'
27 27 TAG_MODELS = 'models'
28 28 TAG_TITLE = 'title'
29 29 TAG_TEXT = 'text'
30 30 TAG_THREAD = 'thread'
31 31 TAG_PUB_TIME = 'pub-time'
32 32 TAG_SIGNATURES = 'signatures'
33 33 TAG_SIGNATURE = 'signature'
34 34 TAG_CONTENT = 'content'
35 35 TAG_ATTACHMENTS = 'attachments'
36 36 TAG_ATTACHMENT = 'attachment'
37 37 TAG_TAGS = 'tags'
38 38 TAG_TAG = 'tag'
39 39 TAG_ATTACHMENT_REFS = 'attachment-refs'
40 40 TAG_ATTACHMENT_REF = 'attachment-ref'
41 41 TAG_TRIPCODE = 'tripcode'
42 42 TAG_VERSION = 'version'
43 43
44 44 TYPE_GET = 'get'
45 45
46 46 ATTR_VERSION = 'version'
47 47 ATTR_TYPE = 'type'
48 48 ATTR_NAME = 'name'
49 49 ATTR_VALUE = 'value'
50 50 ATTR_MIMETYPE = 'mimetype'
51 51 ATTR_KEY = 'key'
52 52 ATTR_REF = 'ref'
53 53 ATTR_URL = 'url'
54 54 ATTR_ID_TYPE = 'id-type'
55 55
56 56 ID_TYPE_MD5 = 'md5'
57 57 ID_TYPE_URL = 'url'
58 58
59 59 STATUS_SUCCESS = 'success'
60 60
61 61
62 62 logger = logging.getLogger('boards.sync')
63 63
64 64
65 65 class SyncManager:
66 66 @staticmethod
67 67 def generate_response_get(model_list: list):
68 68 response = et.Element(TAG_RESPONSE)
69 69
70 70 status = et.SubElement(response, TAG_STATUS)
71 71 status.text = STATUS_SUCCESS
72 72
73 73 models = et.SubElement(response, TAG_MODELS)
74 74
75 75 for post in model_list:
76 76 model = et.SubElement(models, TAG_MODEL)
77 77 model.set(ATTR_NAME, 'post')
78 78
79 79 global_id = post.global_id
80 80
81 81 attachments = post.attachments.all()
82 82 if global_id.content:
83 83 model.append(et.fromstring(global_id.content))
84 84 if len(attachments) > 0:
85 85 internal_attachments = False
86 86 for attachment in attachments:
87 87 if attachment.is_internal():
88 88 internal_attachments = True
89 89 break
90 90
91 91 if internal_attachments:
92 92 attachment_refs = et.SubElement(model, TAG_ATTACHMENT_REFS)
93 93 for file in attachments:
94 94 SyncManager._attachment_to_xml(
95 95 None, attachment_refs, file)
96 96 else:
97 97 content_tag = et.SubElement(model, TAG_CONTENT)
98 98
99 99 tag_id = et.SubElement(content_tag, TAG_ID)
100 100 global_id.to_xml_element(tag_id)
101 101
102 102 title = et.SubElement(content_tag, TAG_TITLE)
103 103 title.text = post.title
104 104
105 105 text = et.SubElement(content_tag, TAG_TEXT)
106 106 text.text = post.get_sync_text()
107 107
108 108 thread = post.get_thread()
109 109 if post.is_opening():
110 110 tag_tags = et.SubElement(content_tag, TAG_TAGS)
111 111 for tag in thread.get_tags():
112 112 tag_tag = et.SubElement(tag_tags, TAG_TAG)
113 113 tag_tag.text = tag.name
114 114 else:
115 115 tag_thread = et.SubElement(content_tag, TAG_THREAD)
116 116 thread_id = et.SubElement(tag_thread, TAG_ID)
117 117 thread.get_opening_post().global_id.to_xml_element(thread_id)
118 118
119 119 pub_time = et.SubElement(content_tag, TAG_PUB_TIME)
120 120 pub_time.text = str(post.get_pub_time_str())
121 121
122 122 if post.tripcode:
123 123 tripcode = et.SubElement(content_tag, TAG_TRIPCODE)
124 124 tripcode.text = post.tripcode
125 125
126 126 if len(attachments) > 0:
127 127 attachments_tag = et.SubElement(content_tag, TAG_ATTACHMENTS)
128 128
129 129 internal_attachments = False
130 130 for attachment in attachments:
131 131 if attachment.is_internal():
132 132 internal_attachments = True
133 133 break
134 134
135 135 if internal_attachments:
136 136 attachment_refs = et.SubElement(model, TAG_ATTACHMENT_REFS)
137 137 else:
138 138 attachment_refs = None
139 139
140 140 for file in attachments:
141 141 SyncManager._attachment_to_xml(
142 142 attachments_tag, attachment_refs, file)
143 143 version_tag = et.SubElement(content_tag, TAG_VERSION)
144 144 version_tag.text = str(post.version)
145 145
146 146 global_id.content = et.tostring(content_tag, ENCODING_UNICODE)
147 147 global_id.save()
148 148
149 149 signatures_tag = et.SubElement(model, TAG_SIGNATURES)
150 150 post_signatures = global_id.signature_set.all()
151 151 if post_signatures:
152 152 signatures = post_signatures
153 153 else:
154 154 key = KeyPair.objects.get(public_key=global_id.key)
155 155 signature = Signature(
156 156 key_type=key.key_type,
157 157 key=key.public_key,
158 158 signature=key.sign(global_id.content),
159 159 global_id=global_id,
160 160 )
161 161 signature.save()
162 162 signatures = [signature]
163 163 for signature in signatures:
164 164 signature_tag = et.SubElement(signatures_tag, TAG_SIGNATURE)
165 165 signature_tag.set(ATTR_TYPE, signature.key_type)
166 166 signature_tag.set(ATTR_VALUE, signature.signature)
167 167 signature_tag.set(ATTR_KEY, signature.key)
168 168
169 169 return et.tostring(response, ENCODING_UNICODE)
170 170
171 171 @staticmethod
172 172 def parse_response_get(response_xml, hostname):
173 173 tag_root = et.fromstring(response_xml)
174 174 tag_status = tag_root.find(TAG_STATUS)
175 175 if STATUS_SUCCESS == tag_status.text:
176 176 tag_models = tag_root.find(TAG_MODELS)
177 177 for tag_model in tag_models:
178 178 SyncManager.parse_post(tag_model, hostname)
179 179 else:
180 180 raise SyncException(EXCEPTION_NODE.format(tag_status.text))
181 181
182 182 @staticmethod
183 183 @transaction.atomic
184 184 def parse_post(tag_model, hostname):
185 185 tag_content = tag_model.find(TAG_CONTENT)
186 186
187 187 content_str = et.tostring(tag_content, ENCODING_UNICODE)
188 188
189 189 tag_id = tag_content.find(TAG_ID)
190 190 global_id, exists = GlobalId.from_xml_element(tag_id)
191 191 signatures = SyncManager._verify_model(global_id, content_str, tag_model)
192 192
193 193 version = int(tag_content.find(TAG_VERSION).text)
194 194 is_old = exists and global_id.post.version < version
195 195 if exists and not is_old:
196 196 print('Post with same ID exists and is up to date.')
197 197 else:
198 198 global_id.content = content_str
199 199 global_id.save()
200 200 for signature in signatures:
201 201 signature.global_id = global_id
202 202 signature.save()
203 203
204 204 title = tag_content.find(TAG_TITLE).text or ''
205 205 text = tag_content.find(TAG_TEXT).text or ''
206 206 pub_time = tag_content.find(TAG_PUB_TIME).text
207 207 tripcode_tag = tag_content.find(TAG_TRIPCODE)
208 208 if tripcode_tag is not None:
209 209 tripcode = tripcode_tag.text or ''
210 210 else:
211 211 tripcode = ''
212 212
213 213 thread = tag_content.find(TAG_THREAD)
214 214 tags = []
215 215 if thread:
216 216 thread_id = thread.find(TAG_ID)
217 217 op_global_id, exists = GlobalId.from_xml_element(thread_id)
218 218 if exists:
219 219 opening_post = Post.objects.get(global_id=op_global_id)
220 220 else:
221 221 raise Exception(EXCEPTION_THREAD.format(global_id))
222 222 else:
223 223 opening_post = None
224 224 tag_tags = tag_content.find(TAG_TAGS)
225 225 for tag_tag in tag_tags:
226 226 tag, created = Tag.objects.get_or_create(
227 227 name=tag_tag.text)
228 228 tags.append(tag)
229 229
230 230 # TODO Check that the replied posts are already present
231 231 # before adding new ones
232 232
233 233 files = []
234 234 urls = []
235 235 tag_attachments = tag_content.find(TAG_ATTACHMENTS) or list()
236 236 tag_refs = tag_model.find(TAG_ATTACHMENT_REFS)
237 237 for attachment in tag_attachments:
238 238 if attachment.get(ATTR_ID_TYPE) == ID_TYPE_URL:
239 239 urls.append(attachment.text)
240 240 else:
241 241 tag_ref = tag_refs.find("{}[@ref='{}']".format(
242 242 TAG_ATTACHMENT_REF, attachment.text))
243 243 url = tag_ref.get(ATTR_URL)
244 244 attached_file = download(hostname + url, validate=False)
245 245 if attached_file is None:
246 246 raise SyncException(EXCEPTION_DOWNLOAD)
247 247
248 248 hash = get_file_hash(attached_file)
249 249 if hash != attachment.text:
250 250 raise SyncException(EXCEPTION_HASH)
251 251
252 252 files.append(attached_file)
253 253
254 254 if is_old:
255 255 post = global_id.post
256 256 Post.objects.update_post(
257 257 post, title=title, text=text, pub_time=pub_time,
258 258 tags=tags, files=files, file_urls=urls,
259 259 tripcode=tripcode, version=version)
260 260 logger.debug('Parsed updated post {}'.format(global_id))
261 261 else:
262 262 Post.objects.import_post(
263 263 title=title, text=text, pub_time=pub_time,
264 264 opening_post=opening_post, tags=tags,
265 265 global_id=global_id, files=files,
266 266 file_urls=urls, tripcode=tripcode,
267 267 version=version)
268 268 logger.debug('Parsed new post {}'.format(global_id))
269 269
270 270 @staticmethod
271 271 def generate_response_list(filters):
272 272 response = et.Element(TAG_RESPONSE)
273 273
274 274 status = et.SubElement(response, TAG_STATUS)
275 275 status.text = STATUS_SUCCESS
276 276
277 277 models = et.SubElement(response, TAG_MODELS)
278 278
279 279 posts = Post.objects.prefetch_related('global_id')
280 280 for post_filter in filters:
281 281 posts = post_filter.filter(posts)
282 282
283 283 for post in posts:
284 284 tag_model = et.SubElement(models, TAG_MODEL)
285 285 tag_id = et.SubElement(tag_model, TAG_ID)
286 286 post.global_id.to_xml_element(tag_id)
287 287 tag_version = et.SubElement(tag_model, TAG_VERSION)
288 288 tag_version.text = str(post.version)
289 289
290 290 return et.tostring(response, ENCODING_UNICODE)
291 291
292 292 @staticmethod
293 293 def _verify_model(global_id, content_str, tag_model):
294 294 """
295 295 Verifies all signatures for a single model.
296 296 """
297 297
298 298 signatures = []
299 299
300 300 tag_signatures = tag_model.find(TAG_SIGNATURES)
301 301 has_author_signature = False
302 302 for tag_signature in tag_signatures:
303 303 signature_type = tag_signature.get(ATTR_TYPE)
304 304 signature_value = tag_signature.get(ATTR_VALUE)
305 305 signature_key = tag_signature.get(ATTR_KEY)
306 306
307 307 if global_id.key_type == signature_type and\
308 308 global_id.key == signature_key:
309 309 has_author_signature = True
310 310
311 311 signature = Signature(key_type=signature_type,
312 312 key=signature_key,
313 313 signature=signature_value)
314 314
315 315 if not KeyPair.objects.verify(signature, content_str):
316 316 raise SyncException(EXCEPTION_SIGNATURE.format(content_str))
317 317
318 318 signatures.append(signature)
319 319 if not has_author_signature:
320 320 raise SyncException(EXCEPTION_AUTHOR_SIGNATURE.format(content_str))
321 321
322 322 return signatures
323 323
324 324 @staticmethod
325 325 def _attachment_to_xml(tag_attachments, tag_refs, attachment):
326 326 if tag_attachments is not None:
327 327 attachment_tag = et.SubElement(tag_attachments, TAG_ATTACHMENT)
328 328 if attachment.is_internal():
329 329 mimetype = get_file_mimetype(attachment.file.file)
330 330 attachment_tag.set(ATTR_MIMETYPE, mimetype)
331 331 attachment_tag.set(ATTR_ID_TYPE, ID_TYPE_MD5)
332 332 attachment_tag.text = attachment.hash
333 333 else:
334 334 attachment_tag.set(ATTR_ID_TYPE, ID_TYPE_URL)
335 335 attachment_tag.text = attachment.url
336 336
337 337 if tag_refs is not None and attachment.is_internal():
338 338 attachment_ref = et.SubElement(tag_refs, TAG_ATTACHMENT_REF)
339 339 attachment_ref.set(ATTR_REF, attachment.hash)
340 340 attachment_ref.set(ATTR_URL, attachment.file.url)
341 341
342 342 @staticmethod
343 343 def generate_request_get(global_id_list: list):
344 344 """
345 345 Form a get request from a list of ModelId objects.
346 346 """
347 347
348 348 request = et.Element(TAG_REQUEST)
349 349 request.set(ATTR_TYPE, TYPE_GET)
350 350 request.set(ATTR_VERSION, '1.0')
351 351
352 352 model = et.SubElement(request, TAG_MODEL)
353 353 model.set(ATTR_VERSION, '1.0')
354 354 model.set(ATTR_NAME, 'post')
355 355
356 356 for global_id in global_id_list:
357 357 tag_id = et.SubElement(model, TAG_ID)
358 358 global_id.to_xml_element(tag_id)
359 359
360 360 return et.tostring(request, 'unicode')
361 361
362 362 @staticmethod
363 def generate_request_list(opening_post=None):
363 def generate_request_list(opening_post=None, tags=list()):
364 364 """
365 365 Form a pull request from a list of ModelId objects.
366 366 """
367 367
368 368 request = et.Element(TAG_REQUEST)
369 369 request.set(ATTR_TYPE, TYPE_LIST)
370 370 request.set(ATTR_VERSION, '1.0')
371 371
372 372 model = et.SubElement(request, TAG_MODEL)
373 373 model.set(ATTR_VERSION, '1.0')
374 374 model.set(ATTR_NAME, 'post')
375 375
376 376 if opening_post:
377 377 ThreadFilter().add_filter(model, opening_post)
378 if tags:
379 TagsFilter().add_filter(model, tags)
378 380
379 381 return et.tostring(request, 'unicode')
@@ -1,79 +1,81 b''
1 1 import logging
2 2
3 3 import xml.etree.ElementTree as et
4 4
5 5 from django.http import HttpResponse, Http404
6 6
7 from boards.abstracts.sync_filters import ThreadFilter, TAG_THREAD
7 from boards.abstracts.sync_filters import ThreadFilter, TagsFilter,\
8 TAG_THREAD, TAG_TAGS
8 9 from boards.models import GlobalId, Post
9 10 from boards.models.post.sync import SyncManager
10 11
11 12
12 13 logger = logging.getLogger('boards.sync')
13 14
14 15
15 16 FILTERS = {
16 17 TAG_THREAD: ThreadFilter,
18 TAG_TAGS: TagsFilter,
17 19 }
18 20
19 21
20 22 def response_list(request):
21 23 request_xml = request.body
22 24
23 25 filters = []
24 26
25 27 if request_xml is None or len(request_xml) == 0:
26 28 return HttpResponse(content='Use the API')
27 29 else:
28 30 root_tag = et.fromstring(request_xml)
29 31 model_tag = root_tag[0]
30 32
31 33 for tag_filter in model_tag:
32 34 filter_name = tag_filter.tag
33 model_filter = FILTERS.get(filter_name)(tag_filter)
35 model_filter = FILTERS.get(filter_name)
34 36 if not model_filter:
35 37 logger.warning('Unavailable filter: {}'.format(filter_name))
36 filters.append(model_filter)
38 filters.append(model_filter(tag_filter))
37 39
38 40 response_xml = SyncManager.generate_response_list(filters)
39 41
40 42 return HttpResponse(content=response_xml)
41 43
42 44
43 45 def response_get(request):
44 46 """
45 47 Processes a GET request with post ID list and returns the posts XML list.
46 48 Request should contain an 'xml' post attribute with the actual request XML.
47 49 """
48 50
49 51 request_xml = request.body
50 52
51 53 if request_xml is None or len(request_xml) == 0:
52 54 return HttpResponse(content='Use the API')
53 55
54 56 posts = []
55 57
56 58 root_tag = et.fromstring(request_xml)
57 59 model_tag = root_tag[0]
58 60 for id_tag in model_tag:
59 61 global_id, exists = GlobalId.from_xml_element(id_tag)
60 62 if exists:
61 63 posts.append(Post.objects.get(global_id=global_id))
62 64
63 65 response_xml = SyncManager.generate_response_get(posts)
64 66
65 67 return HttpResponse(content=response_xml)
66 68
67 69
68 70 def get_post_sync_data(request, post_id):
69 71 try:
70 72 post = Post.objects.get(id=post_id)
71 73 except Post.DoesNotExist:
72 74 raise Http404()
73 75
74 76 xml_str = SyncManager.generate_response_get([post])
75 77
76 78 return HttpResponse(
77 79 content_type='text/xml; charset=utf-8',
78 80 content=xml_str,
79 81 )
General Comments 0
You need to be logged in to leave comments. Login now