##// END OF EJS Templates
Convert \r\n and \r to \n in the post text used in sync
neko259 -
r1504:ce9e0d38 decentral
parent child Browse files
Show More
@@ -1,454 +1,455 b''
1 1 import logging
2 2 import re
3 3 import uuid
4 4
5 5 from django.core.exceptions import ObjectDoesNotExist
6 6 from django.core.urlresolvers import reverse
7 7 from django.db import models
8 8 from django.db.models import TextField, QuerySet
9 9 from django.template.defaultfilters import truncatewords, striptags
10 10 from django.template.loader import render_to_string
11 11 from django.utils import timezone
12 12 from django.dispatch import receiver
13 13 from django.db.models.signals import pre_save, post_save
14 14
15 15 from boards import settings
16 16 from boards.abstracts.tripcode import Tripcode
17 17 from boards.mdx_neboard import get_parser
18 18 from boards.models import PostImage, Attachment, KeyPair, GlobalId
19 19 from boards.models.base import Viewable
20 20 from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
21 21 from boards.models.post.manager import PostManager
22 22 from boards.models.user import Notification
23 23
24 24 CSS_CLS_HIDDEN_POST = 'hidden_post'
25 25 CSS_CLS_DEAD_POST = 'dead_post'
26 26 CSS_CLS_ARCHIVE_POST = 'archive_post'
27 27 CSS_CLS_POST = 'post'
28 28 CSS_CLS_MONOCHROME = 'monochrome'
29 29
30 30 TITLE_MAX_WORDS = 10
31 31
32 32 APP_LABEL_BOARDS = 'boards'
33 33
34 34 BAN_REASON_AUTO = 'Auto'
35 35
36 36 IMAGE_THUMB_SIZE = (200, 150)
37 37
38 38 TITLE_MAX_LENGTH = 200
39 39
40 40 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
41 41 REGEX_GLOBAL_REPLY = re.compile(r'\[post\](\w+)::([^:]+)::(\d+)\[/post\]')
42 42 REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?')
43 43 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
44 44
45 45 PARAMETER_TRUNCATED = 'truncated'
46 46 PARAMETER_TAG = 'tag'
47 47 PARAMETER_OFFSET = 'offset'
48 48 PARAMETER_DIFF_TYPE = 'type'
49 49 PARAMETER_CSS_CLASS = 'css_class'
50 50 PARAMETER_THREAD = 'thread'
51 51 PARAMETER_IS_OPENING = 'is_opening'
52 52 PARAMETER_POST = 'post'
53 53 PARAMETER_OP_ID = 'opening_post_id'
54 54 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
55 55 PARAMETER_REPLY_LINK = 'reply_link'
56 56 PARAMETER_NEED_OP_DATA = 'need_op_data'
57 57
58 58 POST_VIEW_PARAMS = (
59 59 'need_op_data',
60 60 'reply_link',
61 61 'need_open_link',
62 62 'truncated',
63 63 'mode_tree',
64 64 'perms',
65 65 'tree_depth',
66 66 )
67 67
68 68
69 69 class Post(models.Model, Viewable):
70 70 """A post is a message."""
71 71
72 72 objects = PostManager()
73 73
74 74 class Meta:
75 75 app_label = APP_LABEL_BOARDS
76 76 ordering = ('id',)
77 77
78 78 title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
79 79 pub_time = models.DateTimeField()
80 80 text = TextField(blank=True, null=True)
81 81 _text_rendered = TextField(blank=True, null=True, editable=False)
82 82
83 83 images = models.ManyToManyField(PostImage, null=True, blank=True,
84 84 related_name='post_images', db_index=True)
85 85 attachments = models.ManyToManyField(Attachment, null=True, blank=True,
86 86 related_name='attachment_posts')
87 87
88 88 poster_ip = models.GenericIPAddressField()
89 89
90 90 # TODO This field can be removed cause UID is used for update now
91 91 last_edit_time = models.DateTimeField()
92 92
93 93 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
94 94 null=True,
95 95 blank=True, related_name='refposts',
96 96 db_index=True)
97 97 refmap = models.TextField(null=True, blank=True)
98 98 threads = models.ManyToManyField('Thread', db_index=True,
99 99 related_name='multi_replies')
100 100 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
101 101
102 102 url = models.TextField()
103 103 uid = models.TextField(db_index=True)
104 104
105 105 # Global ID with author key. If the message was downloaded from another
106 106 # server, this indicates the server.
107 107 global_id = models.OneToOneField('GlobalId', null=True, blank=True)
108 108
109 109 tripcode = models.CharField(max_length=50, blank=True, default='')
110 110 opening = models.BooleanField(db_index=True)
111 111 hidden = models.BooleanField(default=False)
112 112
113 113 def __str__(self):
114 114 return 'P#{}/{}'.format(self.id, self.get_title())
115 115
116 116 def get_referenced_posts(self):
117 117 threads = self.get_threads().all()
118 118 return self.referenced_posts.filter(threads__in=threads)\
119 119 .order_by('pub_time').distinct().all()
120 120
121 121 def get_title(self) -> str:
122 122 return self.title
123 123
124 124 def get_title_or_text(self):
125 125 title = self.get_title()
126 126 if not title:
127 127 title = truncatewords(striptags(self.get_text()), TITLE_MAX_WORDS)
128 128
129 129 return title
130 130
131 131 def build_refmap(self) -> None:
132 132 """
133 133 Builds a replies map string from replies list. This is a cache to stop
134 134 the server from recalculating the map on every post show.
135 135 """
136 136
137 137 post_urls = [refpost.get_link_view()
138 138 for refpost in self.referenced_posts.all()]
139 139
140 140 self.refmap = ', '.join(post_urls)
141 141
142 142 def is_referenced(self) -> bool:
143 143 return self.refmap and len(self.refmap) > 0
144 144
145 145 def is_opening(self) -> bool:
146 146 """
147 147 Checks if this is an opening post or just a reply.
148 148 """
149 149
150 150 return self.opening
151 151
152 152 def get_absolute_url(self, thread=None):
153 153 url = None
154 154
155 155 if thread is None:
156 156 thread = self.get_thread()
157 157
158 158 # Url is cached only for the "main" thread. When getting url
159 159 # for other threads, do it manually.
160 160 if self.url:
161 161 url = self.url
162 162
163 163 if url is None:
164 164 opening = self.is_opening()
165 165 opening_id = self.id if opening else thread.get_opening_post_id()
166 166 url = reverse('thread', kwargs={'post_id': opening_id})
167 167 if not opening:
168 168 url += '#' + str(self.id)
169 169
170 170 return url
171 171
172 172 def get_thread(self):
173 173 return self.thread
174 174
175 175 def get_thread_id(self):
176 176 return self.thread_id
177 177 def get_threads(self) -> QuerySet:
178 178 """
179 179 Gets post's thread.
180 180 """
181 181
182 182 return self.threads
183 183
184 184 def get_view(self, *args, **kwargs) -> str:
185 185 """
186 186 Renders post's HTML view. Some of the post params can be passed over
187 187 kwargs for the means of caching (if we view the thread, some params
188 188 are same for every post and don't need to be computed over and over.
189 189 """
190 190
191 191 thread = self.get_thread()
192 192
193 193 css_classes = [CSS_CLS_POST]
194 194 if thread.is_archived():
195 195 css_classes.append(CSS_CLS_ARCHIVE_POST)
196 196 elif not thread.can_bump():
197 197 css_classes.append(CSS_CLS_DEAD_POST)
198 198 if self.is_hidden():
199 199 css_classes.append(CSS_CLS_HIDDEN_POST)
200 200 if thread.is_monochrome():
201 201 css_classes.append(CSS_CLS_MONOCHROME)
202 202
203 203 params = dict()
204 204 for param in POST_VIEW_PARAMS:
205 205 if param in kwargs:
206 206 params[param] = kwargs[param]
207 207
208 208 params.update({
209 209 PARAMETER_POST: self,
210 210 PARAMETER_IS_OPENING: self.is_opening(),
211 211 PARAMETER_THREAD: thread,
212 212 PARAMETER_CSS_CLASS: ' '.join(css_classes),
213 213 })
214 214
215 215 return render_to_string('boards/post.html', params)
216 216
217 217 def get_search_view(self, *args, **kwargs):
218 218 return self.get_view(need_op_data=True, *args, **kwargs)
219 219
220 220 def get_first_image(self) -> PostImage:
221 221 return self.images.earliest('id')
222 222
223 223 def delete(self, using=None):
224 224 """
225 225 Deletes all post images and the post itself.
226 226 """
227 227
228 228 for image in self.images.all():
229 229 image_refs_count = image.post_images.count()
230 230 if image_refs_count == 1:
231 231 image.delete()
232 232
233 233 for attachment in self.attachments.all():
234 234 attachment_refs_count = attachment.attachment_posts.count()
235 235 if attachment_refs_count == 1:
236 236 attachment.delete()
237 237
238 238 if self.global_id:
239 239 self.global_id.delete()
240 240
241 241 thread = self.get_thread()
242 242 thread.last_edit_time = timezone.now()
243 243 thread.save()
244 244
245 245 super(Post, self).delete(using)
246 246
247 247 logging.getLogger('boards.post.delete').info(
248 248 'Deleted post {}'.format(self))
249 249
250 250 def set_global_id(self, key_pair=None):
251 251 """
252 252 Sets global id based on the given key pair. If no key pair is given,
253 253 default one is used.
254 254 """
255 255
256 256 if key_pair:
257 257 key = key_pair
258 258 else:
259 259 try:
260 260 key = KeyPair.objects.get(primary=True)
261 261 except KeyPair.DoesNotExist:
262 262 # Do not update the global id because there is no key defined
263 263 return
264 264 global_id = GlobalId(key_type=key.key_type,
265 265 key=key.public_key,
266 266 local_id=self.id)
267 267 global_id.save()
268 268
269 269 self.global_id = global_id
270 270
271 271 self.save(update_fields=['global_id'])
272 272
273 273 def get_pub_time_str(self):
274 274 return str(self.pub_time)
275 275
276 276 def get_replied_ids(self):
277 277 """
278 278 Gets ID list of the posts that this post replies.
279 279 """
280 280
281 281 raw_text = self.get_raw_text()
282 282
283 283 local_replied = REGEX_REPLY.findall(raw_text)
284 284 global_replied = []
285 285 for match in REGEX_GLOBAL_REPLY.findall(raw_text):
286 286 key_type = match[0]
287 287 key = match[1]
288 288 local_id = match[2]
289 289
290 290 try:
291 291 global_id = GlobalId.objects.get(key_type=key_type,
292 292 key=key, local_id=local_id)
293 293 for post in Post.objects.filter(global_id=global_id).only('id'):
294 294 global_replied.append(post.id)
295 295 except GlobalId.DoesNotExist:
296 296 pass
297 297 return local_replied + global_replied
298 298
299 299 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
300 300 include_last_update=False) -> str:
301 301 """
302 302 Gets post HTML or JSON data that can be rendered on a page or used by
303 303 API.
304 304 """
305 305
306 306 return get_exporter(format_type).export(self, request,
307 307 include_last_update)
308 308
309 309 def notify_clients(self, recursive=True):
310 310 """
311 311 Sends post HTML data to the thread web socket.
312 312 """
313 313
314 314 if not settings.get_bool('External', 'WebsocketsEnabled'):
315 315 return
316 316
317 317 thread_ids = list()
318 318 for thread in self.get_threads().all():
319 319 thread_ids.append(thread.id)
320 320
321 321 thread.notify_clients()
322 322
323 323 if recursive:
324 324 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
325 325 post_id = reply_number.group(1)
326 326
327 327 try:
328 328 ref_post = Post.objects.get(id=post_id)
329 329
330 330 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
331 331 # If post is in this thread, its thread was already notified.
332 332 # Otherwise, notify its thread separately.
333 333 ref_post.notify_clients(recursive=False)
334 334 except ObjectDoesNotExist:
335 335 pass
336 336
337 337 def build_url(self):
338 338 self.url = self.get_absolute_url()
339 339 self.save(update_fields=['url'])
340 340
341 341 def save(self, force_insert=False, force_update=False, using=None,
342 342 update_fields=None):
343 343 new_post = self.id is None
344 344
345 345 self.uid = str(uuid.uuid4())
346 346 if update_fields is not None and 'uid' not in update_fields:
347 347 update_fields += ['uid']
348 348
349 349 if not new_post:
350 350 for thread in self.get_threads().all():
351 351 thread.last_edit_time = self.last_edit_time
352 352
353 353 thread.save(update_fields=['last_edit_time', 'status'])
354 354
355 355 super().save(force_insert, force_update, using, update_fields)
356 356
357 357 if self.url is None:
358 358 self.build_url()
359 359
360 360 def get_text(self) -> str:
361 361 return self._text_rendered
362 362
363 363 def get_raw_text(self) -> str:
364 364 return self.text
365 365
366 366 def get_sync_text(self) -> str:
367 367 """
368 368 Returns text applicable for sync. It has absolute post reflinks.
369 369 """
370 370
371 371 replacements = dict()
372 372 for post_id in REGEX_REPLY.findall(self.get_raw_text()):
373 373 absolute_post_id = str(Post.objects.get(id=post_id).global_id)
374 374 replacements[post_id] = absolute_post_id
375 375
376 376 text = self.get_raw_text()
377 377 for key in replacements:
378 378 text = text.replace('[post]{}[/post]'.format(key),
379 379 '[post]{}[/post]'.format(replacements[key]))
380 text = text.replace('\r\n', '\n').replace('\r', '\n')
380 381
381 382 return text
382 383
383 384 def get_absolute_id(self) -> str:
384 385 """
385 386 If the post has many threads, shows its main thread OP id in the post
386 387 ID.
387 388 """
388 389
389 390 if self.get_threads().count() > 1:
390 391 return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id)
391 392 else:
392 393 return str(self.id)
393 394
394 395
395 396 def connect_threads(self, opening_posts):
396 397 for opening_post in opening_posts:
397 398 threads = opening_post.get_threads().all()
398 399 for thread in threads:
399 400 if thread.can_bump():
400 401 thread.update_bump_status()
401 402
402 403 thread.last_edit_time = self.last_edit_time
403 404 thread.save(update_fields=['last_edit_time', 'status'])
404 405 self.threads.add(opening_post.get_thread())
405 406
406 407 def get_tripcode(self):
407 408 if self.tripcode:
408 409 return Tripcode(self.tripcode)
409 410
410 411 def get_link_view(self):
411 412 """
412 413 Gets view of a reflink to the post.
413 414 """
414 415 result = '<a href="{}">&gt;&gt;{}</a>'.format(self.get_absolute_url(),
415 416 self.id)
416 417 if self.is_opening():
417 418 result = '<b>{}</b>'.format(result)
418 419
419 420 return result
420 421
421 422 def is_hidden(self) -> bool:
422 423 return self.hidden
423 424
424 425 def set_hidden(self, hidden):
425 426 self.hidden = hidden
426 427
427 428
428 429 # SIGNALS (Maybe move to other module?)
429 430 @receiver(post_save, sender=Post)
430 431 def connect_replies(instance, **kwargs):
431 432 for reply_number in re.finditer(REGEX_REPLY, instance.get_raw_text()):
432 433 post_id = reply_number.group(1)
433 434
434 435 try:
435 436 referenced_post = Post.objects.get(id=post_id)
436 437
437 438 referenced_post.referenced_posts.add(instance)
438 439 referenced_post.last_edit_time = instance.pub_time
439 440 referenced_post.build_refmap()
440 441 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
441 442 except ObjectDoesNotExist:
442 443 pass
443 444
444 445
445 446 @receiver(post_save, sender=Post)
446 447 def connect_notifications(instance, **kwargs):
447 448 for reply_number in re.finditer(REGEX_NOTIFICATION, instance.get_raw_text()):
448 449 user_name = reply_number.group(1).lower()
449 450 Notification.objects.get_or_create(name=user_name, post=instance)
450 451
451 452
452 453 @receiver(pre_save, sender=Post)
453 454 def preparse_text(instance, **kwargs):
454 455 instance._text_rendered = get_parser().parse(instance.get_raw_text())
@@ -1,201 +1,201 b''
1 1 import xml.etree.ElementTree as et
2 2 from django.db import transaction
3 3 from boards.models import KeyPair, GlobalId, Signature, Post, Tag
4 4
5 5 ENCODING_UNICODE = 'unicode'
6 6
7 7 TAG_MODEL = 'model'
8 8 TAG_REQUEST = 'request'
9 9 TAG_RESPONSE = 'response'
10 10 TAG_ID = 'id'
11 11 TAG_STATUS = 'status'
12 12 TAG_MODELS = 'models'
13 13 TAG_TITLE = 'title'
14 14 TAG_TEXT = 'text'
15 15 TAG_THREAD = 'thread'
16 16 TAG_PUB_TIME = 'pub-time'
17 17 TAG_SIGNATURES = 'signatures'
18 18 TAG_SIGNATURE = 'signature'
19 19 TAG_CONTENT = 'content'
20 20 TAG_ATTACHMENTS = 'attachments'
21 21 TAG_ATTACHMENT = 'attachment'
22 22 TAG_TAGS = 'tags'
23 23 TAG_TAG = 'tag'
24 24
25 25 TYPE_GET = 'get'
26 26
27 27 ATTR_VERSION = 'version'
28 28 ATTR_TYPE = 'type'
29 29 ATTR_NAME = 'name'
30 30 ATTR_VALUE = 'value'
31 31 ATTR_MIMETYPE = 'mimetype'
32 32 ATTR_KEY = 'key'
33 33
34 34 STATUS_SUCCESS = 'success'
35 35
36 36
37 37 class SyncManager:
38 38 @staticmethod
39 39 def generate_response_get(model_list: list):
40 40 response = et.Element(TAG_RESPONSE)
41 41
42 42 status = et.SubElement(response, TAG_STATUS)
43 43 status.text = STATUS_SUCCESS
44 44
45 45 models = et.SubElement(response, TAG_MODELS)
46 46
47 47 for post in model_list:
48 48 model = et.SubElement(models, TAG_MODEL)
49 49 model.set(ATTR_NAME, 'post')
50 50
51 51 content_tag = et.SubElement(model, TAG_CONTENT)
52 52
53 53 tag_id = et.SubElement(content_tag, TAG_ID)
54 54 post.global_id.to_xml_element(tag_id)
55 55
56 56 title = et.SubElement(content_tag, TAG_TITLE)
57 57 title.text = post.title
58 58
59 59 text = et.SubElement(content_tag, TAG_TEXT)
60 60 text.text = post.get_sync_text()
61 61
62 62 thread = post.get_thread()
63 63 if post.is_opening():
64 64 tag_tags = et.SubElement(content_tag, TAG_TAGS)
65 65 for tag in thread.get_tags():
66 66 tag_tag = et.SubElement(tag_tags, TAG_TAG)
67 67 tag_tag.text = tag.name
68 68 else:
69 69 tag_thread = et.SubElement(content_tag, TAG_THREAD)
70 70 thread_id = et.SubElement(tag_thread, TAG_ID)
71 71 thread.get_opening_post().global_id.to_xml_element(thread_id)
72 72
73 73 pub_time = et.SubElement(content_tag, TAG_PUB_TIME)
74 74 pub_time.text = str(post.get_pub_time_str())
75 75
76 76 signatures_tag = et.SubElement(model, TAG_SIGNATURES)
77 77 post_signatures = post.global_id.signature_set.all()
78 78 if post_signatures:
79 79 signatures = post_signatures
80 80 # TODO Adding signature to a post is not yet added. For now this
81 81 # block is useless
82 82 else:
83 83 # TODO Maybe the signature can be computed only once after
84 84 # the post is added? Need to add some on_save signal queue
85 85 # and add this there.
86 86 key = KeyPair.objects.get(public_key=post.global_id.key)
87 87 signature = Signature(
88 88 key_type=key.key_type,
89 89 key=key.public_key,
90 signature=key.sign(et.tostring(content_tag, ENCODING_UNICODE)),
90 signature=key.sign(et.tostring(content_tag, encoding=ENCODING_UNICODE)),
91 91 global_id=post.global_id,
92 92 )
93 93 signature.save()
94 94 signatures = [signature]
95 95 for signature in signatures:
96 96 signature_tag = et.SubElement(signatures_tag, TAG_SIGNATURE)
97 97 signature_tag.set(ATTR_TYPE, signature.key_type)
98 98 signature_tag.set(ATTR_VALUE, signature.signature)
99 99 signature_tag.set(ATTR_KEY, signature.key)
100 100
101 101 return et.tostring(response, ENCODING_UNICODE)
102 102
103 103 @staticmethod
104 104 @transaction.atomic
105 105 def parse_response_get(response_xml):
106 106 tag_root = et.fromstring(response_xml)
107 107 tag_status = tag_root.find(TAG_STATUS)
108 108 if STATUS_SUCCESS == tag_status.text:
109 109 tag_models = tag_root.find(TAG_MODELS)
110 110 for tag_model in tag_models:
111 111 tag_content = tag_model.find(TAG_CONTENT)
112 112
113 113 signatures = SyncManager._verify_model(tag_content, tag_model)
114 114
115 115 tag_id = tag_content.find(TAG_ID)
116 116 global_id, exists = GlobalId.from_xml_element(tag_id)
117 117
118 118 if exists:
119 119 print('Post with same ID already exists')
120 120 else:
121 121 global_id.save()
122 122 for signature in signatures:
123 123 signature.global_id = global_id
124 124 signature.save()
125 125
126 126 title = tag_content.find(TAG_TITLE).text
127 127 text = tag_content.find(TAG_TEXT).text
128 128 pub_time = tag_content.find(TAG_PUB_TIME).text
129 129
130 130 thread = tag_content.find(TAG_THREAD)
131 131 tags = []
132 132 if thread:
133 133 thread_id = thread.find(TAG_ID)
134 134 op_global_id, exists = GlobalId.from_xml_element(thread_id)
135 135 if exists:
136 136 opening_post = Post.objects.get(global_id=op_global_id)
137 137 else:
138 138 raise Exception('Load the OP first')
139 139 else:
140 140 opening_post = None
141 141 tag_tags = tag_content.find(TAG_TAGS)
142 142 for tag_tag in tag_tags:
143 143 tag, created = Tag.objects.get_or_create(
144 144 name=tag_tag.text)
145 145 tags.append(tag)
146 146
147 147 # TODO Check that the replied posts are already present
148 148 # before adding new ones
149 149
150 150 # TODO Get images
151 151
152 152 post = Post.objects.import_post(
153 153 title=title, text=text, pub_time=pub_time,
154 154 opening_post=opening_post, tags=tags,
155 155 global_id=global_id)
156 156 else:
157 157 # TODO Throw an exception?
158 158 pass
159 159
160 160 @staticmethod
161 161 def generate_response_pull():
162 162 response = et.Element(TAG_RESPONSE)
163 163
164 164 status = et.SubElement(response, TAG_STATUS)
165 165 status.text = STATUS_SUCCESS
166 166
167 167 models = et.SubElement(response, TAG_MODELS)
168 168
169 169 for post in Post.objects.all():
170 170 tag_id = et.SubElement(models, TAG_ID)
171 171 post.global_id.to_xml_element(tag_id)
172 172
173 173 return et.tostring(response, ENCODING_UNICODE)
174 174
175 175 @staticmethod
176 176 def _verify_model(tag_content, tag_model):
177 177 """
178 178 Verifies all signatures for a single model.
179 179 """
180 180
181 181 signatures = []
182 182
183 183 tag_signatures = tag_model.find(TAG_SIGNATURES)
184 184 for tag_signature in tag_signatures:
185 185 signature_type = tag_signature.get(ATTR_TYPE)
186 186 signature_value = tag_signature.get(ATTR_VALUE)
187 187 signature_key = tag_signature.get(ATTR_KEY)
188 188
189 189 signature = Signature(key_type=signature_type,
190 190 key=signature_key,
191 191 signature=signature_value)
192 192
193 193 content = et.tostring(tag_content, ENCODING_UNICODE)
194 194
195 195 if not KeyPair.objects.verify(
196 196 signature, content):
197 197 raise Exception('Invalid model signature for {}'.format(content))
198 198
199 199 signatures.append(signature)
200 200
201 201 return signatures
@@ -1,101 +1,102 b''
1 1 from boards.models import KeyPair, Post, Tag
2 2 from boards.models.post.sync import SyncManager
3 3 from boards.tests.mocks import MockRequest
4 4 from boards.views.sync import response_get
5 5
6 6 __author__ = 'neko259'
7 7
8 8
9 9 from django.test import TestCase
10 10
11 11
12 12 class SyncTest(TestCase):
13 13 def test_get(self):
14 14 """
15 15 Forms a GET request of a post and checks the response.
16 16 """
17 17
18 18 key = KeyPair.objects.generate_key(primary=True)
19 19 tag = Tag.objects.create(name='tag1')
20 post = Post.objects.create_post(title='test_title', text='test_text',
20 post = Post.objects.create_post(title='test_title',
21 text='test_text\rline two',
21 22 tags=[tag])
22 23
23 24 request = MockRequest()
24 25 request.body = (
25 26 '<request type="get" version="1.0">'
26 27 '<model name="post" version="1.0">'
27 28 '<id key="%s" local-id="%d" type="%s" />'
28 29 '</model>'
29 30 '</request>' % (post.global_id.key,
30 31 post.id,
31 32 post.global_id.key_type)
32 33 )
33 34
34 35 response = response_get(request).content.decode()
35 36 self.assertTrue(
36 37 '<status>success</status>'
37 38 '<models>'
38 39 '<model name="post">'
39 40 '<content>'
40 41 '<id key="%s" local-id="%d" type="%s" />'
41 42 '<title>%s</title>'
42 43 '<text>%s</text>'
43 44 '<tags><tag>%s</tag></tags>'
44 45 '<pub-time>%s</pub-time>'
45 46 '</content>' % (
46 47 post.global_id.key,
47 48 post.global_id.local_id,
48 49 post.global_id.key_type,
49 50 post.title,
50 post.get_raw_text(),
51 post.get_sync_text(),
51 52 post.get_thread().get_tags().first().name,
52 53 post.get_pub_time_str(),
53 54 ) in response,
54 55 'Wrong response generated for the GET request.')
55 56
56 57 post.delete()
57 58 key.delete()
58 59
59 60 KeyPair.objects.generate_key(primary=True)
60 61
61 62 SyncManager.parse_response_get(response)
62 63 self.assertEqual(1, Post.objects.count(),
63 64 'Post was not created from XML response.')
64 65
65 66 parsed_post = Post.objects.first()
66 67 self.assertEqual('tag1',
67 68 parsed_post.get_thread().get_tags().first().name,
68 69 'Invalid tag was parsed.')
69 70
70 71 SyncManager.parse_response_get(response)
71 72 self.assertEqual(1, Post.objects.count(),
72 73 'The same post was imported twice.')
73 74
74 75 self.assertEqual(1, parsed_post.global_id.signature_set.count(),
75 76 'Signature was not saved.')
76 77
77 78 post = parsed_post
78 79
79 80 # Trying to sync the same once more
80 81 response = response_get(request).content.decode()
81 82
82 83 self.assertTrue(
83 84 '<status>success</status>'
84 85 '<models>'
85 86 '<model name="post">'
86 87 '<content>'
87 88 '<id key="%s" local-id="%d" type="%s" />'
88 89 '<title>%s</title>'
89 90 '<text>%s</text>'
90 91 '<tags><tag>%s</tag></tags>'
91 92 '<pub-time>%s</pub-time>'
92 93 '</content>' % (
93 94 post.global_id.key,
94 95 post.global_id.local_id,
95 96 post.global_id.key_type,
96 97 post.title,
97 post.get_raw_text(),
98 post.get_sync_text(),
98 99 post.get_thread().get_tags().first().name,
99 100 post.get_pub_time_str(),
100 101 ) in response,
101 102 'Wrong response generated for the GET request.')
General Comments 0
You need to be logged in to leave comments. Login now