##// END OF EJS Templates
Use global thread ID instead of local one.
neko259 -
r936:ff78113e decentral
parent child Browse files
Show More
@@ -1,621 +1,622 b''
1 1 from datetime import datetime, timedelta, date
2 2 from datetime import time as dtime
3 3 import logging
4 4 import re
5 5 import xml.etree.ElementTree as et
6 6
7 7 from adjacent import Client
8 8 from django.core.cache import cache
9 9 from django.core.urlresolvers import reverse
10 10 from django.db import models, transaction
11 11 from django.db.models import TextField
12 12 from django.template.loader import render_to_string
13 13 from django.utils import timezone
14 14
15 15 from boards.models import PostImage, KeyPair, GlobalId, Signature
16 16 from boards import settings
17 17 from boards.mdx_neboard import bbcode_extended
18 18 from boards.models import PostImage
19 19 from boards.models.base import Viewable
20 20 from boards.models.thread import Thread
21 21 from boards import utils
22 22 from boards.utils import datetime_to_epoch
23 23
24 24 ENCODING_UNICODE = 'unicode'
25 25
26 26 WS_NOTIFICATION_TYPE_NEW_POST = 'new_post'
27 27 WS_NOTIFICATION_TYPE = 'notification_type'
28 28
29 29 WS_CHANNEL_THREAD = "thread:"
30 30
31 31 APP_LABEL_BOARDS = 'boards'
32 32
33 33 CACHE_KEY_PPD = 'ppd'
34 34 CACHE_KEY_POST_URL = 'post_url'
35 35
36 36 POSTS_PER_DAY_RANGE = 7
37 37
38 38 BAN_REASON_AUTO = 'Auto'
39 39
40 40 IMAGE_THUMB_SIZE = (200, 150)
41 41
42 42 TITLE_MAX_LENGTH = 200
43 43
44 44 # TODO This should be removed
45 45 NO_IP = '0.0.0.0'
46 46
47 47 # TODO Real user agent should be saved instead of this
48 48 UNKNOWN_UA = ''
49 49
50 50 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
51 51 REGEX_GLOBAL_REPLY = re.compile(r'\[post\](\w+)::([^:]+)::(\d+)\[/post\]')
52 52
53 53 TAG_MODEL = 'model'
54 54 TAG_REQUEST = 'request'
55 55 TAG_RESPONSE = 'response'
56 56 TAG_ID = 'id'
57 57 TAG_STATUS = 'status'
58 58 TAG_MODELS = 'models'
59 59 TAG_TITLE = 'title'
60 60 TAG_TEXT = 'text'
61 61 TAG_THREAD = 'thread'
62 62 TAG_PUB_TIME = 'pub-time'
63 63 TAG_SIGNATURES = 'signatures'
64 64 TAG_SIGNATURE = 'signature'
65 65 TAG_CONTENT = 'content'
66 66 TAG_ATTACHMENTS = 'attachments'
67 67 TAG_ATTACHMENT = 'attachment'
68 68
69 69 TYPE_GET = 'get'
70 70
71 71 ATTR_VERSION = 'version'
72 72 ATTR_TYPE = 'type'
73 73 ATTR_NAME = 'name'
74 74 ATTR_VALUE = 'value'
75 75 ATTR_MIMETYPE = 'mimetype'
76 76
77 77 STATUS_SUCCESS = 'success'
78 78
79 79 PARAMETER_TRUNCATED = 'truncated'
80 80 PARAMETER_TAG = 'tag'
81 81 PARAMETER_OFFSET = 'offset'
82 82 PARAMETER_DIFF_TYPE = 'type'
83 83 PARAMETER_BUMPABLE = 'bumpable'
84 84 PARAMETER_THREAD = 'thread'
85 85 PARAMETER_IS_OPENING = 'is_opening'
86 86 PARAMETER_MODERATOR = 'moderator'
87 87 PARAMETER_POST = 'post'
88 88 PARAMETER_OP_ID = 'opening_post_id'
89 89 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
90 90
91 91 DIFF_TYPE_HTML = 'html'
92 92 DIFF_TYPE_JSON = 'json'
93 93
94 94 PREPARSE_PATTERNS = {
95 95 r'>>(\d+)': r'[post]\1[/post]', # Reflink ">>123"
96 96 r'^>([^>].+)': r'[quote]\1[/quote]', # Quote ">text"
97 97 r'^//(.+)': r'[comment]\1[/comment]', # Comment "//text"
98 98 }
99 99
100 100
101 101 class PostManager(models.Manager):
102 102 @transaction.atomic
103 103 def create_post(self, title: str, text: str, image=None, thread=None,
104 104 ip=NO_IP, tags: list=None):
105 105 """
106 106 Creates new post
107 107 """
108 108
109 109 if not tags:
110 110 tags = []
111 111
112 112 posting_time = timezone.now()
113 113 if not thread:
114 114 thread = Thread.objects.create(bump_time=posting_time,
115 115 last_edit_time=posting_time)
116 116 new_thread = True
117 117 else:
118 118 new_thread = False
119 119
120 120 pre_text = self._preparse_text(text)
121 121
122 122 post = self.create(title=title,
123 123 text=pre_text,
124 124 pub_time=posting_time,
125 125 thread_new=thread,
126 126 poster_ip=ip,
127 127 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
128 128 # last!
129 129 last_edit_time=posting_time)
130 130
131 131 post.set_global_id()
132 132
133 133 logger = logging.getLogger('boards.post.create')
134 134
135 135 logger.info('Created post {} by {}'.format(
136 136 post, post.poster_ip))
137 137
138 138 if image:
139 139 post_image = PostImage.objects.create(image=image)
140 140 post.images.add(post_image)
141 141 logger.info('Created image #{} for post #{}'.format(
142 142 post_image.id, post.id))
143 143
144 144 thread.replies.add(post)
145 145 list(map(thread.add_tag, tags))
146 146
147 147 if new_thread:
148 148 Thread.objects.process_oldest_threads()
149 149 else:
150 150 thread.bump()
151 151 thread.last_edit_time = posting_time
152 152 thread.save()
153 153
154 154 self.connect_replies(post)
155 155
156 156 return post
157 157
158 158 def delete_posts_by_ip(self, ip):
159 159 """
160 160 Deletes all posts of the author with same IP
161 161 """
162 162
163 163 posts = self.filter(poster_ip=ip)
164 164 for post in posts:
165 165 post.delete()
166 166
167 167 # TODO This can be moved into a post
168 168 def connect_replies(self, post):
169 169 """
170 170 Connects replies to a post to show them as a reflink map
171 171 """
172 172
173 173 for reply_number in post.get_replied_ids():
174 174 ref_post = self.filter(id=reply_number)
175 175 if ref_post.count() > 0:
176 176 referenced_post = ref_post[0]
177 177 referenced_post.referenced_posts.add(post)
178 178 referenced_post.last_edit_time = post.pub_time
179 179 referenced_post.build_refmap()
180 180 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
181 181
182 182 referenced_thread = referenced_post.get_thread()
183 183 referenced_thread.last_edit_time = post.pub_time
184 184 referenced_thread.save(update_fields=['last_edit_time'])
185 185
186 186 def get_posts_per_day(self):
187 187 """
188 188 Gets average count of posts per day for the last 7 days
189 189 """
190 190
191 191 day_end = date.today()
192 192 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
193 193
194 194 cache_key = CACHE_KEY_PPD + str(day_end)
195 195 ppd = cache.get(cache_key)
196 196 if ppd:
197 197 return ppd
198 198
199 199 day_time_start = timezone.make_aware(datetime.combine(
200 200 day_start, dtime()), timezone.get_current_timezone())
201 201 day_time_end = timezone.make_aware(datetime.combine(
202 202 day_end, dtime()), timezone.get_current_timezone())
203 203
204 204 posts_per_period = float(self.filter(
205 205 pub_time__lte=day_time_end,
206 206 pub_time__gte=day_time_start).count())
207 207
208 208 ppd = posts_per_period / POSTS_PER_DAY_RANGE
209 209
210 210 cache.set(cache_key, ppd)
211 211 return ppd
212 212
213 213 # TODO Make a separate sync facade?
214 214 def generate_response_get(self, model_list: list):
215 215 response = et.Element(TAG_RESPONSE)
216 216
217 217 status = et.SubElement(response, TAG_STATUS)
218 218 status.text = STATUS_SUCCESS
219 219
220 220 models = et.SubElement(response, TAG_MODELS)
221 221
222 222 for post in model_list:
223 223 model = et.SubElement(models, TAG_MODEL)
224 224 model.set(ATTR_NAME, 'post')
225 225
226 226 content_tag = et.SubElement(model, TAG_CONTENT)
227 227
228 228 tag_id = et.SubElement(content_tag, TAG_ID)
229 229 post.global_id.to_xml_element(tag_id)
230 230
231 231 title = et.SubElement(content_tag, TAG_TITLE)
232 232 title.text = post.title
233 233
234 234 text = et.SubElement(content_tag, TAG_TEXT)
235 235 # TODO Replace local links by global ones in the text
236 236 text.text = post.get_raw_text()
237 237
238 238 if not post.is_opening():
239 239 thread = et.SubElement(content_tag, TAG_THREAD)
240 thread.text = str(post.get_thread().get_opening_post_id())
240 thread_id = et.SubElement(thread, TAG_ID)
241 post.get_thread().get_opening_post().global_id.to_xml_element(thread_id)
241 242 else:
242 243 # TODO Output tags here
243 244 pass
244 245
245 246 pub_time = et.SubElement(content_tag, TAG_PUB_TIME)
246 247 pub_time.text = str(post.get_pub_time_epoch())
247 248
248 249 signatures_tag = et.SubElement(model, TAG_SIGNATURES)
249 250 post_signatures = post.signature.all()
250 251 if post_signatures:
251 252 signatures = post.signatures
252 253 else:
253 254 # TODO Maybe the signature can be computed only once after
254 255 # the post is added? Need to add some on_save signal queue
255 256 # and add this there.
256 257 key = KeyPair.objects.get(public_key=post.global_id.key)
257 258 signatures = [Signature(
258 259 key_type=key.key_type,
259 260 key=key.public_key,
260 261 signature=key.sign(et.tostring(model, ENCODING_UNICODE)),
261 262 )]
262 263 for signature in signatures:
263 264 signature_tag = et.SubElement(signatures_tag, TAG_SIGNATURE)
264 265 signature_tag.set(ATTR_TYPE, signature.key_type)
265 266 signature_tag.set(ATTR_VALUE, signature.signature)
266 267
267 268 return et.tostring(response, ENCODING_UNICODE)
268 269
269 270 def parse_response_get(self, response_xml):
270 271 tag_root = et.fromstring(response_xml)
271 272 tag_status = tag_root[0]
272 273 if 'success' == tag_status.text:
273 274 tag_models = tag_root[1]
274 275 for tag_model in tag_models:
275 276 tag_content = tag_model[0]
276 277 tag_id = tag_content[1]
277 278 try:
278 279 GlobalId.from_xml_element(tag_id, existing=True)
279 280 # If this post already exists, just continue
280 281 # TODO Compare post content and update the post if necessary
281 282 pass
282 283 except GlobalId.DoesNotExist:
283 284 global_id = GlobalId.from_xml_element(tag_id)
284 285
285 286 title = tag_content.find(TAG_TITLE).text
286 287 text = tag_content.find(TAG_TEXT).text
287 288 # TODO Check that the replied posts are already present
288 289 # before adding new ones
289 290
290 291 # TODO Pub time, thread, tags
291 292
292 293 post = Post.objects.create(title=title, text=text)
293 294 else:
294 295 # TODO Throw an exception?
295 296 pass
296 297
297 298 def _preparse_text(self, text):
298 299 """
299 300 Preparses text to change patterns like '>>' to a proper bbcode
300 301 tags.
301 302 """
302 303
303 304 for key, value in PREPARSE_PATTERNS.items():
304 305 text = re.sub(key, value, text, flags=re.MULTILINE)
305 306
306 307 return text
307 308
308 309
309 310 class Post(models.Model, Viewable):
310 311 """A post is a message."""
311 312
312 313 objects = PostManager()
313 314
314 315 class Meta:
315 316 app_label = APP_LABEL_BOARDS
316 317 ordering = ('id',)
317 318
318 319 title = models.CharField(max_length=TITLE_MAX_LENGTH)
319 320 pub_time = models.DateTimeField()
320 321 text = TextField(blank=True, null=True)
321 322 _text_rendered = TextField(blank=True, null=True, editable=False)
322 323
323 324 images = models.ManyToManyField(PostImage, null=True, blank=True,
324 325 related_name='ip+', db_index=True)
325 326
326 327 poster_ip = models.GenericIPAddressField()
327 328 poster_user_agent = models.TextField()
328 329
329 330 thread_new = models.ForeignKey('Thread', null=True, default=None,
330 331 db_index=True)
331 332 last_edit_time = models.DateTimeField()
332 333
333 334 # Replies to the post
334 335 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
335 336 null=True,
336 337 blank=True, related_name='rfp+',
337 338 db_index=True)
338 339
339 340 # Replies map. This is built from the referenced posts list to speed up
340 341 # page loading (no need to get all the referenced posts from the database).
341 342 refmap = models.TextField(null=True, blank=True)
342 343
343 344 # Global ID with author key. If the message was downloaded from another
344 345 # server, this indicates the server.
345 346 global_id = models.OneToOneField('GlobalId', null=True, blank=True)
346 347
347 348 # One post can be signed by many nodes that give their trust to it
348 349 signature = models.ManyToManyField('Signature', null=True, blank=True)
349 350
350 351 def __str__(self):
351 352 return 'P#{}/{}'.format(self.id, self.title)
352 353
353 354 def get_title(self) -> str:
354 355 """
355 356 Gets original post title or part of its text.
356 357 """
357 358
358 359 title = self.title
359 360 if not title:
360 361 title = self.get_text()
361 362
362 363 return title
363 364
364 365 def build_refmap(self) -> None:
365 366 """
366 367 Builds a replies map string from replies list. This is a cache to stop
367 368 the server from recalculating the map on every post show.
368 369 """
369 370 map_string = ''
370 371
371 372 first = True
372 373 for refpost in self.referenced_posts.all():
373 374 if not first:
374 375 map_string += ', '
375 376 map_string += '<a href="%s">&gt;&gt;%s</a>' % (refpost.get_url(),
376 377 refpost.id)
377 378 first = False
378 379
379 380 self.refmap = map_string
380 381
381 382 def get_sorted_referenced_posts(self):
382 383 return self.refmap
383 384
384 385 def is_referenced(self) -> bool:
385 386 if not self.refmap:
386 387 return False
387 388 else:
388 389 return len(self.refmap) > 0
389 390
390 391 def is_opening(self) -> bool:
391 392 """
392 393 Checks if this is an opening post or just a reply.
393 394 """
394 395
395 396 return self.get_thread().get_opening_post_id() == self.id
396 397
397 398 @transaction.atomic
398 399 def add_tag(self, tag):
399 400 edit_time = timezone.now()
400 401
401 402 thread = self.get_thread()
402 403 thread.add_tag(tag)
403 404 self.last_edit_time = edit_time
404 405 self.save(update_fields=['last_edit_time'])
405 406
406 407 thread.last_edit_time = edit_time
407 408 thread.save(update_fields=['last_edit_time'])
408 409
409 410 def get_url(self, thread=None):
410 411 """
411 412 Gets full url to the post.
412 413 """
413 414
414 415 cache_key = CACHE_KEY_POST_URL + str(self.id)
415 416 link = cache.get(cache_key)
416 417
417 418 if not link:
418 419 if not thread:
419 420 thread = self.get_thread()
420 421
421 422 opening_id = thread.get_opening_post_id()
422 423
423 424 if self.id != opening_id:
424 425 link = reverse('thread', kwargs={
425 426 'post_id': opening_id}) + '#' + str(self.id)
426 427 else:
427 428 link = reverse('thread', kwargs={'post_id': self.id})
428 429
429 430 cache.set(cache_key, link)
430 431
431 432 return link
432 433
433 434 def get_thread(self) -> Thread:
434 435 """
435 436 Gets post's thread.
436 437 """
437 438
438 439 return self.thread_new
439 440
440 441 def get_referenced_posts(self):
441 442 return self.referenced_posts.only('id', 'thread_new')
442 443
443 444 def get_view(self, moderator=False, need_open_link=False,
444 445 truncated=False, *args, **kwargs):
445 446 """
446 447 Renders post's HTML view. Some of the post params can be passed over
447 448 kwargs for the means of caching (if we view the thread, some params
448 449 are same for every post and don't need to be computed over and over.
449 450 """
450 451
451 452 is_opening = kwargs.get(PARAMETER_IS_OPENING, self.is_opening())
452 453 thread = kwargs.get(PARAMETER_THREAD, self.get_thread())
453 454 can_bump = kwargs.get(PARAMETER_BUMPABLE, thread.can_bump())
454 455
455 456 if is_opening:
456 457 opening_post_id = self.id
457 458 else:
458 459 opening_post_id = thread.get_opening_post_id()
459 460
460 461 return render_to_string('boards/post.html', {
461 462 PARAMETER_POST: self,
462 463 PARAMETER_MODERATOR: moderator,
463 464 PARAMETER_IS_OPENING: is_opening,
464 465 PARAMETER_THREAD: thread,
465 466 PARAMETER_BUMPABLE: can_bump,
466 467 PARAMETER_NEED_OPEN_LINK: need_open_link,
467 468 PARAMETER_TRUNCATED: truncated,
468 469 PARAMETER_OP_ID: opening_post_id,
469 470 })
470 471
471 472 def get_search_view(self, *args, **kwargs):
472 473 return self.get_view(args, kwargs)
473 474
474 475 def get_first_image(self) -> PostImage:
475 476 return self.images.earliest('id')
476 477
477 478 def delete(self, using=None):
478 479 """
479 480 Deletes all post images and the post itself. If the post is opening,
480 481 thread with all posts is deleted.
481 482 """
482 483
483 484 self.images.all().delete()
484 485 self.signature.all().delete()
485 486 if self.global_id:
486 487 self.global_id.delete()
487 488
488 489 if self.is_opening():
489 490 self.get_thread().delete()
490 491 else:
491 492 thread = self.get_thread()
492 493 thread.last_edit_time = timezone.now()
493 494 thread.save()
494 495
495 496 super(Post, self).delete(using)
496 497 logging.getLogger('boards.post.delete').info(
497 498 'Deleted post {}'.format(self))
498 499
499 500 def set_global_id(self, key_pair=None):
500 501 """
501 502 Sets global id based on the given key pair. If no key pair is given,
502 503 default one is used.
503 504 """
504 505
505 506 if key_pair:
506 507 key = key_pair
507 508 else:
508 509 try:
509 510 key = KeyPair.objects.get(primary=True)
510 511 except KeyPair.DoesNotExist:
511 512 # Do not update the global id because there is no key defined
512 513 return
513 514 global_id = GlobalId(key_type=key.key_type,
514 515 key=key.public_key,
515 516 local_id = self.id)
516 517 global_id.save()
517 518
518 519 self.global_id = global_id
519 520
520 521 self.save(update_fields=['global_id'])
521 522
522 523 def get_pub_time_epoch(self):
523 524 return utils.datetime_to_epoch(self.pub_time)
524 525
525 526 def get_replied_ids(self):
526 527 """
527 528 Gets ID list of the posts that this post replies.
528 529 """
529 530
530 531 raw_text = self.get_raw_text()
531 532
532 533 local_replied = REGEX_REPLY.findall(raw_text)
533 534 global_replied = []
534 535 for match in REGEX_GLOBAL_REPLY.findall(raw_text):
535 536 key_type = match[0]
536 537 key = match[1]
537 538 local_id = match[2]
538 539
539 540 try:
540 541 global_id = GlobalId.objects.get(key_type=key_type,
541 542 key=key, local_id=local_id)
542 543 for post in Post.objects.filter(global_id=global_id).only('id'):
543 544 global_replied.append(post.id)
544 545 except GlobalId.DoesNotExist:
545 546 pass
546 547 return local_replied + global_replied
547 548
548 549
549 550 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
550 551 include_last_update=False):
551 552 """
552 553 Gets post HTML or JSON data that can be rendered on a page or used by
553 554 API.
554 555 """
555 556
556 557 if format_type == DIFF_TYPE_HTML:
557 558 params = dict()
558 559 params['post'] = self
559 560 if PARAMETER_TRUNCATED in request.GET:
560 561 params[PARAMETER_TRUNCATED] = True
561 562
562 563 return render_to_string('boards/api_post.html', params)
563 564 elif format_type == DIFF_TYPE_JSON:
564 565 post_json = {
565 566 'id': self.id,
566 567 'title': self.title,
567 568 'text': self._text_rendered,
568 569 }
569 570 if self.images.exists():
570 571 post_image = self.get_first_image()
571 572 post_json['image'] = post_image.image.url
572 573 post_json['image_preview'] = post_image.image.url_200x150
573 574 if include_last_update:
574 575 post_json['bump_time'] = datetime_to_epoch(
575 576 self.thread_new.bump_time)
576 577 return post_json
577 578
578 579 def send_to_websocket(self, request, recursive=True):
579 580 """
580 581 Sends post HTML data to the thread web socket.
581 582 """
582 583
583 584 if not settings.WEBSOCKETS_ENABLED:
584 585 return
585 586
586 587 client = Client()
587 588
588 589 thread = self.get_thread()
589 590 thread_id = thread.id
590 591 channel_name = WS_CHANNEL_THREAD + str(thread.get_opening_post_id())
591 592 client.publish(channel_name, {
592 593 WS_NOTIFICATION_TYPE: WS_NOTIFICATION_TYPE_NEW_POST,
593 594 })
594 595 client.send()
595 596
596 597 logger = logging.getLogger('boards.post.websocket')
597 598
598 599 logger.info('Sent notification from post #{} to channel {}'.format(
599 600 self.id, channel_name))
600 601
601 602 if recursive:
602 603 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
603 604 post_id = reply_number.group(1)
604 605 ref_post = Post.objects.filter(id=post_id)[0]
605 606
606 607 # If post is in this thread, its thread was already notified.
607 608 # Otherwise, notify its thread separately.
608 609 if ref_post.thread_new_id != thread_id:
609 610 ref_post.send_to_websocket(request, recursive=False)
610 611
611 612 def save(self, force_insert=False, force_update=False, using=None,
612 613 update_fields=None):
613 614 self._text_rendered = bbcode_extended(self.get_raw_text())
614 615
615 616 super().save(force_insert, force_update, using, update_fields)
616 617
617 618 def get_text(self) -> str:
618 619 return self._text_rendered
619 620
620 621 def get_raw_text(self) -> str:
621 622 return self.text
@@ -1,85 +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
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 59 response = Post.objects.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]%d[/post]</text>'
70 '<thread>%d</thread>'
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 post.id,
77 key.public_key,
77 78 post.id,
79 key.key_type,
78 80 str(reply_post.get_pub_time_epoch()),
79 81 ) in response,
80 82 'Wrong XML generated for the GET response.')
81 83
82 84 def _create_post_with_key(self):
83 85 KeyPair.objects.generate_key(primary=True)
84 86
85 87 return Post.objects.create_post(title='test_title', text='test_text')
@@ -1,58 +1,58 b''
1 1 <?xml version="1.1" encoding="UTF-8" ?>
2 2 <response>
3 3 <!--
4 4 Valid statuses are 'success' and 'error'.
5 5 -->
6 6 <status>success</status>
7 7 <models>
8 8 <model name="post">
9 9 <!--
10 10 Content tag is the data that is signed by signatures and must
11 11 not be changed for the post from other node.
12 12 -->
13 13 <content>
14 14 <id key="id1" type="ecdsa" local-id="1" />
15 15 <title>13</title>
16 16 <text>Thirteen</text>
17 <thread>id1/12</thread>
17 <thread><id key="id1" type="ecdsa" local-id="2" /></thread>
18 18 <pub-time>12</pub-time>
19 19 <!--
20 20 Images are saved as attachments and included in the
21 21 signature.
22 22 -->
23 23 <attachments>
24 24 <attachment mimetype="image/png" name="12345.png">
25 25 TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sIGJ1dCBieSB0
26 26 aGlzIHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbmltYWxzLCB3aGljaCBpcyBhIGx1
27 27 c3Qgb2YgdGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJzZXZlcmFuY2Ugb2YgZGVsaWdodCBpbiB0
28 28 aGUgY29udGludWVkIGFuZCBpbmRlZmF0aWdhYmxlIGdlbmVyYXRpb24gb2Yga25vd2xlZGdl
29 29 LCBleGNlZWRzIHRoZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4=
30 30 </attachment>
31 31 </attachments>
32 32 </content>
33 33 <!--
34 34 There can be several signatures for one model. At least one
35 35 signature must be made with the key used in global ID.
36 36 -->
37 37 <signatures>
38 38 <signature key="id1" type="ecdsa" value="dhefhtreh" />
39 39 <signature key="id45" type="ecdsa" value="dsgfgdhefhtreh" />
40 40 </signatures>
41 41 </model>
42 42 <model name="post">
43 43 <content>
44 44 <id key="id1" type="ecdsa" local-id="id2" />
45 45 <title>13</title>
46 46 <text>Thirteen</text>
47 47 <pub-time>12</pub-time>
48 48 <edit-time>13</edit-time>
49 49 <tags>
50 50 <tag>tag1</tag>
51 51 </tags>
52 52 </content>
53 53 <signatures>
54 54 <signature key="id2" type="ecdsa" value="dehdfh" />
55 55 </signatures>
56 56 </model>
57 57 </models>
58 58 </response>
General Comments 0
You need to be logged in to leave comments. Login now