##// END OF EJS Templates
Added signatures to the GET response. Added a view to get a full post response for one post. Don't show post key as it is present in the XML post view. Changed key display format
neko259 -
r837:fbeaaa16 decentral
parent child Browse files
Show More
@@ -1,491 +1,518 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 django.core.cache import cache
8 8 from django.core.urlresolvers import reverse
9 9 from django.db import models, transaction
10 10 from django.template.loader import render_to_string
11 11 from django.utils import timezone
12 12
13 13 from markupfield.fields import MarkupField
14 14
15 from boards.models import PostImage, KeyPair, GlobalId
15 from boards.models import PostImage, KeyPair, GlobalId, Signature
16 16 from boards.models.base import Viewable
17 17 from boards.models.thread import Thread
18 18 from boards import utils
19 19
20 20
21 21 APP_LABEL_BOARDS = 'boards'
22 22
23 23 CACHE_KEY_PPD = 'ppd'
24 24 CACHE_KEY_POST_URL = 'post_url'
25 25
26 26 POSTS_PER_DAY_RANGE = 7
27 27
28 28 BAN_REASON_AUTO = 'Auto'
29 29
30 30 IMAGE_THUMB_SIZE = (200, 150)
31 31
32 32 TITLE_MAX_LENGTH = 200
33 33
34 34 DEFAULT_MARKUP_TYPE = 'bbcode'
35 35
36 36 # TODO This should be removed
37 37 NO_IP = '0.0.0.0'
38 38
39 39 # TODO Real user agent should be saved instead of this
40 40 UNKNOWN_UA = ''
41 41
42 42 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
43 43
44 44 TAG_MODEL = 'model'
45 45 TAG_REQUEST = 'request'
46 46 TAG_RESPONSE = 'response'
47 47 TAG_ID = 'id'
48 48 TAG_STATUS = 'status'
49 49 TAG_MODELS = 'models'
50 50 TAG_TITLE = 'title'
51 51 TAG_TEXT = 'text'
52 52 TAG_THREAD = 'thread'
53 53 TAG_PUB_TIME = 'pub-time'
54 54 TAG_EDIT_TIME = 'edit-time'
55 55 TAG_PREVIOUS = 'previous'
56 56 TAG_NEXT = 'next'
57 TAG_SIGNATURES = 'signatures'
58 TAG_SIGNATURE = 'signature'
57 59
58 60 TYPE_GET = 'get'
59 61
60 62 ATTR_VERSION = 'version'
61 63 ATTR_TYPE = 'type'
62 64 ATTR_NAME = 'name'
63 65 ATTR_REF_ID = 'ref-id'
66 ATTR_MODEL_REF = 'model-ref'
67 ATTR_VALUE = 'value'
64 68
65 69 STATUS_SUCCESS = 'success'
66 70
67 71 logger = logging.getLogger(__name__)
68 72
69 73
70 74 class PostManager(models.Manager):
71 75 def create_post(self, title, text, image=None, thread=None, ip=NO_IP,
72 76 tags=None):
73 77 """
74 78 Creates new post
75 79 """
76 80
77 81 if not tags:
78 82 tags = []
79 83
80 84 posting_time = timezone.now()
81 85 if not thread:
82 86 thread = Thread.objects.create(bump_time=posting_time,
83 87 last_edit_time=posting_time)
84 88 new_thread = True
85 89 else:
86 90 thread.bump()
87 91 thread.last_edit_time = posting_time
88 92 thread.save()
89 93 new_thread = False
90 94
91 95 post = self.create(title=title,
92 96 text=text,
93 97 pub_time=posting_time,
94 98 thread_new=thread,
95 99 poster_ip=ip,
96 100 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
97 101 # last!
98 102 last_edit_time=posting_time)
99 103
100 104 post.set_global_id()
101 105
102 106 if image:
103 107 post_image = PostImage.objects.create(image=image)
104 108 post.images.add(post_image)
105 109 logger.info('Created image #%d for post #%d' % (post_image.id,
106 110 post.id))
107 111
108 112 thread.replies.add(post)
109 113 list(map(thread.add_tag, tags))
110 114
111 115 if new_thread:
112 116 Thread.objects.process_oldest_threads()
113 117 self.connect_replies(post)
114 118
115 119 logger.info('Created post #%d with title %s'
116 120 % (post.id, post.get_title()))
117 121
118 122 return post
119 123
120 124 def delete_post(self, post):
121 125 """
122 126 Deletes post and update or delete its thread
123 127 """
124 128
125 129 post_id = post.id
126 130
127 131 thread = post.get_thread()
128 132
129 133 if post.is_opening():
130 134 thread.delete()
131 135 else:
132 136 thread.last_edit_time = timezone.now()
133 137 thread.save()
134 138
135 139 post.delete()
136 140
137 141 logger.info('Deleted post #%d (%s)' % (post_id, post.get_title()))
138 142
139 143 def delete_posts_by_ip(self, ip):
140 144 """
141 145 Deletes all posts of the author with same IP
142 146 """
143 147
144 148 posts = self.filter(poster_ip=ip)
145 149 for post in posts:
146 150 self.delete_post(post)
147 151
148 152 def connect_replies(self, post):
149 153 """
150 154 Connects replies to a post to show them as a reflink map
151 155 """
152 156
153 157 for reply_number in post.get_replied_ids():
154 158 ref_post = self.filter(id=reply_number)
155 159 if ref_post.count() > 0:
156 160 referenced_post = ref_post[0]
157 161 referenced_post.referenced_posts.add(post)
158 162 referenced_post.last_edit_time = post.pub_time
159 163 referenced_post.build_refmap()
160 164 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
161 165
162 166 referenced_thread = referenced_post.get_thread()
163 167 referenced_thread.last_edit_time = post.pub_time
164 168 referenced_thread.save(update_fields=['last_edit_time'])
165 169
166 170 def get_posts_per_day(self):
167 171 """
168 172 Gets average count of posts per day for the last 7 days
169 173 """
170 174
171 175 day_end = date.today()
172 176 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
173 177
174 178 cache_key = CACHE_KEY_PPD + str(day_end)
175 179 ppd = cache.get(cache_key)
176 180 if ppd:
177 181 return ppd
178 182
179 183 day_time_start = timezone.make_aware(datetime.combine(
180 184 day_start, dtime()), timezone.get_current_timezone())
181 185 day_time_end = timezone.make_aware(datetime.combine(
182 186 day_end, dtime()), timezone.get_current_timezone())
183 187
184 188 posts_per_period = float(self.filter(
185 189 pub_time__lte=day_time_end,
186 190 pub_time__gte=day_time_start).count())
187 191
188 192 ppd = posts_per_period / POSTS_PER_DAY_RANGE
189 193
190 194 cache.set(cache_key, ppd)
191 195 return ppd
192 196
193
194 197 def generate_request_get(self, model_list: list):
195 198 """
196 199 Form a get request from a list of ModelId objects.
197 200 """
198 201
199 202 request = et.Element(TAG_REQUEST)
200 203 request.set(ATTR_TYPE, TYPE_GET)
201 204 request.set(ATTR_VERSION, '1.0')
202 205
203 206 model = et.SubElement(request, TAG_MODEL)
204 207 model.set(ATTR_VERSION, '1.0')
205 208 model.set(ATTR_NAME, 'post')
206 209
207 210 for post in model_list:
208 211 tag_id = et.SubElement(model, TAG_ID)
209 212 post.global_id.to_xml_element(tag_id)
210 213
211 214 return et.tostring(request, 'unicode')
212 215
213 216 def generate_response_get(self, model_list: list):
214 217 response = et.Element(TAG_RESPONSE)
215 218
216 219 status = et.SubElement(response, TAG_STATUS)
217 220 status.text = STATUS_SUCCESS
218 221
219 222 models = et.SubElement(response, TAG_MODELS)
223 signatures = {}
220 224
221 225 ref_id = 1
222 226 for post in model_list:
223 227 model = et.SubElement(models, TAG_MODEL)
224 228 model.set(ATTR_NAME, 'post')
225 229 model.set(ATTR_REF_ID, str(ref_id))
226 ref_id += 1
227 230
228 231 tag_id = et.SubElement(model, TAG_ID)
229 232 post.global_id.to_xml_element(tag_id)
230 233
231 234 title = et.SubElement(model, TAG_TITLE)
232 235 title.text = post.title
233 236
234 237 text = et.SubElement(model, TAG_TEXT)
235 238 text.text = post.text.raw
236 239
237 240 if not post.is_opening():
238 241 thread = et.SubElement(model, TAG_THREAD)
239 242 thread.text = str(post.get_thread().get_opening_post_id())
240 243
241 244 pub_time = et.SubElement(model, TAG_PUB_TIME)
242 245 pub_time.text = str(post.get_pub_time_epoch())
243 246
244 247 edit_time = et.SubElement(model, TAG_EDIT_TIME)
245 248 edit_time.text = str(post.get_edit_time_epoch())
246 249
247 250 previous_ids = post.get_replied_ids()
248 251 if len(previous_ids) > 0:
249 252 previous = et.SubElement(model, TAG_PREVIOUS)
250 253 for id in previous_ids:
251 254 prev_id = et.SubElement(previous, TAG_ID)
252 255 replied_post = Post.objects.get(id=id)
253 256 replied_post.global_id.to_xml_element(prev_id)
254 257
255
256 258 next_ids = post.referenced_posts.order_by('id').all()
257 259 if len(next_ids) > 0:
258 260 next_el = et.SubElement(model, TAG_NEXT)
259 261 for ref_post in next_ids:
260 262 next_id = et.SubElement(next_el, TAG_ID)
261 263 ref_post.global_id.to_xml_element(next_id)
262 264
265 post_signatures = post.signature.all()
266 if post_signatures:
267 signatures[ref_id] = post.signatures
268 else:
269 # TODO Maybe the signature can be computed only once after
270 # the post is added? Need to add some on_save signal queue
271 # and add this there.
272 key = KeyPair.objects.get(public_key=post.global_id.key)
273 signatures[ref_id] = [Signature(
274 key_type=key.key_type,
275 key=key.public_key,
276 signature=key.sign(et.tostring(model, 'unicode')),
277 )]
278 ref_id += 1
279
280 signatures_tag = et.SubElement(response, TAG_SIGNATURES)
281 for ref_id in signatures.keys():
282 signatures = signatures[ref_id]
283
284 for signature in signatures:
285 signature_tag = et.SubElement(signatures_tag, TAG_SIGNATURE)
286 signature_tag.set(ATTR_MODEL_REF, str(ref_id))
287 signature_tag.set(ATTR_TYPE, signature.key_type)
288 signature_tag.set(ATTR_VALUE, signature.signature)
289
263 290 return et.tostring(response, 'unicode')
264 291
265 292
266 293 class Post(models.Model, Viewable):
267 294 """A post is a message."""
268 295
269 296 objects = PostManager()
270 297
271 298 class Meta:
272 299 app_label = APP_LABEL_BOARDS
273 300 ordering = ('id',)
274 301
275 302 title = models.CharField(max_length=TITLE_MAX_LENGTH)
276 303 pub_time = models.DateTimeField()
277 304 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
278 305 escape_html=False)
279 306
280 307 images = models.ManyToManyField(PostImage, null=True, blank=True,
281 308 related_name='ip+', db_index=True)
282 309
283 310 poster_ip = models.GenericIPAddressField()
284 311 poster_user_agent = models.TextField()
285 312
286 313 thread_new = models.ForeignKey('Thread', null=True, default=None,
287 314 db_index=True)
288 315 last_edit_time = models.DateTimeField()
289 316
290 317 # Replies to the post
291 318 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
292 319 null=True,
293 320 blank=True, related_name='rfp+',
294 321 db_index=True)
295 322
296 323 # Replies map. This is built from the referenced posts list to speed up
297 324 # page loading (no need to get all the referenced posts from the database).
298 325 refmap = models.TextField(null=True, blank=True)
299 326
300 327 # Global ID with author key. If the message was downloaded from another
301 328 # server, this indicates the server.
302 329 global_id = models.OneToOneField('GlobalId', null=True, blank=True)
303 330
304 331 # One post can be signed by many nodes that give their trust to it
305 332 signature = models.ManyToManyField('Signature', null=True, blank=True)
306 333
307 334 def __unicode__(self):
308 335 return '#' + str(self.id) + ' ' + self.title + ' (' + \
309 336 self.text.raw[:50] + ')'
310 337
311 338 def get_title(self):
312 339 """
313 340 Gets original post title or part of its text.
314 341 """
315 342
316 343 title = self.title
317 344 if not title:
318 345 title = self.text.rendered
319 346
320 347 return title
321 348
322 349 def build_refmap(self):
323 350 """
324 351 Builds a replies map string from replies list. This is a cache to stop
325 352 the server from recalculating the map on every post show.
326 353 """
327 354 map_string = ''
328 355
329 356 first = True
330 357 for refpost in self.referenced_posts.all():
331 358 if not first:
332 359 map_string += ', '
333 360 map_string += '<a href="%s">&gt;&gt;%s</a>' % (refpost.get_url(),
334 361 refpost.id)
335 362 first = False
336 363
337 364 self.refmap = map_string
338 365
339 366 def get_sorted_referenced_posts(self):
340 367 return self.refmap
341 368
342 369 def is_referenced(self):
343 370 return len(self.refmap) > 0
344 371
345 372 def is_opening(self):
346 373 """
347 374 Checks if this is an opening post or just a reply.
348 375 """
349 376
350 377 return self.get_thread().get_opening_post_id() == self.id
351 378
352 379 @transaction.atomic
353 380 def add_tag(self, tag):
354 381 edit_time = timezone.now()
355 382
356 383 thread = self.get_thread()
357 384 thread.add_tag(tag)
358 385 self.last_edit_time = edit_time
359 386 self.save(update_fields=['last_edit_time'])
360 387
361 388 thread.last_edit_time = edit_time
362 389 thread.save(update_fields=['last_edit_time'])
363 390
364 391 @transaction.atomic
365 392 def remove_tag(self, tag):
366 393 edit_time = timezone.now()
367 394
368 395 thread = self.get_thread()
369 396 thread.remove_tag(tag)
370 397 self.last_edit_time = edit_time
371 398 self.save(update_fields=['last_edit_time'])
372 399
373 400 thread.last_edit_time = edit_time
374 401 thread.save(update_fields=['last_edit_time'])
375 402
376 403 def get_url(self, thread=None):
377 404 """
378 405 Gets full url to the post.
379 406 """
380 407
381 408 cache_key = CACHE_KEY_POST_URL + str(self.id)
382 409 link = cache.get(cache_key)
383 410
384 411 if not link:
385 412 if not thread:
386 413 thread = self.get_thread()
387 414
388 415 opening_id = thread.get_opening_post_id()
389 416
390 417 if self.id != opening_id:
391 418 link = reverse('thread', kwargs={
392 419 'post_id': opening_id}) + '#' + str(self.id)
393 420 else:
394 421 link = reverse('thread', kwargs={'post_id': self.id})
395 422
396 423 cache.set(cache_key, link)
397 424
398 425 return link
399 426
400 427 def get_thread(self):
401 428 """
402 429 Gets post's thread.
403 430 """
404 431
405 432 return self.thread_new
406 433
407 434 def get_referenced_posts(self):
408 435 return self.referenced_posts.only('id', 'thread_new')
409 436
410 437 def get_text(self):
411 438 return self.text
412 439
413 440 def get_view(self, moderator=False, need_open_link=False,
414 441 truncated=False, *args, **kwargs):
415 442 if 'is_opening' in kwargs:
416 443 is_opening = kwargs['is_opening']
417 444 else:
418 445 is_opening = self.is_opening()
419 446
420 447 if 'thread' in kwargs:
421 448 thread = kwargs['thread']
422 449 else:
423 450 thread = self.get_thread()
424 451
425 452 if 'can_bump' in kwargs:
426 453 can_bump = kwargs['can_bump']
427 454 else:
428 455 can_bump = thread.can_bump()
429 456
430 457 if is_opening:
431 458 opening_post_id = self.id
432 459 else:
433 460 opening_post_id = thread.get_opening_post_id()
434 461
435 462 return render_to_string('boards/post.html', {
436 463 'post': self,
437 464 'moderator': moderator,
438 465 'is_opening': is_opening,
439 466 'thread': thread,
440 467 'bumpable': can_bump,
441 468 'need_open_link': need_open_link,
442 469 'truncated': truncated,
443 470 'opening_post_id': opening_post_id,
444 471 })
445 472
446 473 def get_first_image(self):
447 474 return self.images.earliest('id')
448 475
449 476 def delete(self, using=None):
450 477 """
451 478 Deletes all post images and the post itself.
452 479 """
453 480
454 481 self.images.all().delete()
455 482 self.signature.all().delete()
456 483 if self.global_id:
457 484 self.global_id.delete()
458 485
459 486 super(Post, self).delete(using)
460 487
461 488 def set_global_id(self, key_pair=None):
462 489 """
463 490 Sets global id based on the given key pair. If no key pair is given,
464 491 default one is used.
465 492 """
466 493
467 494 if key_pair:
468 495 key = key_pair
469 496 else:
470 497 try:
471 498 key = KeyPair.objects.get(primary=True)
472 499 except KeyPair.DoesNotExist:
473 500 # Do not update the global id because there is no key defined
474 501 return
475 502 global_id = GlobalId(key_type=key.key_type,
476 503 key=key.public_key,
477 504 local_id = self.id)
478 505 global_id.save()
479 506
480 507 self.global_id = global_id
481 508
482 509 self.save(update_fields=['global_id'])
483 510
484 511 def get_pub_time_epoch(self):
485 512 return utils.datetime_to_epoch(self.pub_time)
486 513
487 514 def get_edit_time_epoch(self):
488 515 return utils.datetime_to_epoch(self.last_edit_time)
489 516
490 517 def get_replied_ids(self):
491 518 return re.findall(REGEX_REPLY, self.text.raw)
@@ -1,67 +1,75 b''
1 1 import xml.etree.ElementTree as et
2 2 from django.db import models
3 3
4 4
5 5 ATTR_KEY = 'key'
6 6 ATTR_KEY_TYPE = 'type'
7 7 ATTR_LOCAL_ID = 'local-id'
8 8
9 9
10 10 class GlobalId(models.Model):
11 11 class Meta:
12 12 app_label = 'boards'
13 13
14 14 def __init__(self, *args, **kwargs):
15 15 models.Model.__init__(self, *args, **kwargs)
16 16
17 17 if 'key' in kwargs and 'key_type' in kwargs and 'local_id' in kwargs:
18 18 self.key = kwargs['key']
19 19 self.key_type = kwargs['key_type']
20 20 self.local_id = kwargs['local_id']
21 21
22 22 key = models.TextField()
23 23 key_type = models.TextField()
24 24 local_id = models.IntegerField()
25 25
26 26 def __str__(self):
27 return '%s | %s | %d' % (self.key_type, self.key, self.local_id)
27 return '[%s][%s][%d]' % (self.key_type, self.key, self.local_id)
28 28
29 29 def to_xml_element(self, element: et.Element):
30 30 """
31 31 Exports global id to an XML element.
32 32 """
33 33
34 34 element.set(ATTR_KEY, self.key)
35 35 element.set(ATTR_KEY_TYPE, self.key_type)
36 36 element.set(ATTR_LOCAL_ID, str(self.local_id))
37 37
38 38 @staticmethod
39 39 def from_xml_element(element: et.Element, existing=False):
40 40 """
41 41 Parses XML id tag and gets global id from it.
42 42
43 43 Arguments:
44 44 element -- the XML 'id' element
45 45 existing -- if this is False, a new instance of GlobalId will be
46 46 created. Otherwise, we will search for an existing GlobalId instance
47 47 and throw DoesNotExist if there isn't one.
48 48 """
49 49
50 50 if existing:
51 51 return GlobalId.objects.get(key=element.get(ATTR_KEY),
52 52 key_type=element.get(ATTR_KEY_TYPE),
53 53 local_id=int(element.get(
54 54 ATTR_LOCAL_ID)))
55 55 else:
56 56 return GlobalId(key=element.get(ATTR_KEY),
57 57 key_type=element.get(ATTR_KEY_TYPE),
58 58 local_id=int(element.get(ATTR_LOCAL_ID)))
59 59
60 60
61 61 class Signature(models.Model):
62 62 class Meta:
63 63 app_label = 'boards'
64 64
65 def __init__(self, *args, **kwargs):
66 models.Model.__init__(self, *args, **kwargs)
67
68 if 'key' in kwargs and 'key_type' in kwargs and 'signature' in kwargs:
69 self.key_type = kwargs['key_type']
70 self.key = kwargs['key']
71 self.signature = kwargs['signature']
72
65 73 key_type = models.TextField()
66 74 key = models.TextField()
67 75 signature = models.TextField()
@@ -1,61 +1,61 b''
1 1 import base64
2 2 from ecdsa import SigningKey, VerifyingKey, BadSignatureError
3 3 from django.db import models
4 4
5 5 TYPE_ECDSA = 'ecdsa'
6 6
7 7 APP_LABEL_BOARDS = 'boards'
8 8
9 9
10 10 class KeyPairManager(models.Manager):
11 11 def generate_key(self, key_type=TYPE_ECDSA, primary=False):
12 12 if primary and self.filter(primary=True).exists():
13 13 raise Exception('There can be only one primary key')
14 14
15 15 if key_type == TYPE_ECDSA:
16 16 private = SigningKey.generate()
17 17 public = private.get_verifying_key()
18 18
19 19 private_key_str = base64.b64encode(private.to_string()).decode()
20 20 public_key_str = base64.b64encode(public.to_string()).decode()
21 21
22 22 return self.create(public_key=public_key_str,
23 23 private_key=private_key_str,
24 24 key_type=TYPE_ECDSA, primary=primary)
25 25 else:
26 26 raise Exception('Key type not supported')
27 27
28 28 def verify(self, public_key_str, string, signature, key_type=TYPE_ECDSA):
29 29 if key_type == TYPE_ECDSA:
30 30 public = VerifyingKey.from_string(base64.b64decode(public_key_str))
31 31 signature_byte = base64.b64decode(signature)
32 32 try:
33 33 return public.verify(signature_byte, string.encode())
34 34 except BadSignatureError:
35 35 return False
36 36 else:
37 37 raise Exception('Key type not supported')
38 38
39 39 def has_primary(self):
40 40 return self.filter(primary=True).exists()
41 41
42 42
43 43 class KeyPair(models.Model):
44 44 class Meta:
45 45 app_label = APP_LABEL_BOARDS
46 46
47 47 objects = KeyPairManager()
48 48
49 49 public_key = models.TextField()
50 50 private_key = models.TextField()
51 51 key_type = models.TextField()
52 52 primary = models.BooleanField(default=False)
53 53
54 54 def __str__(self):
55 return '%s | %s' % (self.key_type, self.public_key)
55 return '[%s][%s]' % (self.key_type, self.public_key)
56 56
57 57 def sign(self, string):
58 58 private = SigningKey.from_string(base64.b64decode(
59 59 self.private_key.encode()))
60 signature_byte = private.sign(string.encode())
61 return base64.b64encode(signature_byte)
60 signature_byte = private.sign_deterministic(string.encode())
61 return base64.b64encode(signature_byte).decode()
@@ -1,103 +1,104 b''
1 1 {% load i18n %}
2 2 {% load board %}
3 3 {% load cache %}
4 4
5 5 {% get_current_language as LANGUAGE_CODE %}
6 6
7 7 {% spaceless %}
8 8 {% cache 600 post post.id post.last_edit_time thread.archived bumpable truncated moderator LANGUAGE_CODE need_open_link %}
9 9 {% if thread.archived %}
10 10 <div class="post archive_post" id="{{ post.id }}">
11 11 {% elif bumpable %}
12 12 <div class="post" id="{{ post.id }}">
13 13 {% else %}
14 14 <div class="post dead_post" id="{{ post.id }}">
15 15 {% endif %}
16 16
17 17 <div class="post-info">
18 18 <a class="post_id" href="{% post_object_url post thread=thread %}"
19 19 {% if not truncated and not thread.archived %}
20 20 onclick="javascript:addQuickReply('{{ post.id }}'); return false;"
21 21 title="{% trans 'Quote' %}"
22 22 {% endif %}
23 23 >({{ post.id }}) </a>
24 24 <span class="title">{{ post.title }} </span>
25 25 <span class="pub_time">{{ post.pub_time }}</span>
26 26 {% if thread.archived %}
27 27 β€” {{ thread.bump_time }}
28 28 {% endif %}
29 29 {% if is_opening and need_open_link %}
30 30 {% if thread.archived %}
31 31 [<a class="link" href="{% url 'thread' post.id %}">{% trans "Open" %}</a>]
32 32 {% else %}
33 33 [<a class="link" href="{% url 'thread' post.id %}#form">{% trans "Reply" %}</a>]
34 34 {% endif %}
35 35 {% endif %}
36 36
37 37 {% if post.global_id %}
38 <span class="global-id"> {{ post.global_id }} </span>
38 <a class="global-id" href="
39 {% url 'post_sync_data' post.id %}"> [RAW] </a>
39 40 {% endif %}
40 41
41 42 {% if moderator %}
42 43 <span class="moderator_info">
43 44 [<a href="{% url 'post_admin' post_id=post.id %}"
44 45 >{% trans 'Edit' %}</a>]
45 46 [<a href="{% url 'delete' post_id=post.id %}"
46 47 >{% trans 'Delete' %}</a>]
47 48 ({{ post.poster_ip }})
48 49 [<a href="{% url 'ban' post_id=post.id %}?next={{ request.path }}"
49 50 >{% trans 'Ban IP' %}</a>]
50 51 </span>
51 52 {% endif %}
52 53 </div>
53 54 {% if post.images.exists %}
54 55 {% with post.images.all.0 as image %}
55 56 <div class="image">
56 57 <a
57 58 class="thumb"
58 59 href="{{ image.image.url }}"><img
59 60 src="{{ image.image.url_200x150 }}"
60 61 alt="{{ post.id }}"
61 62 width="{{ image.pre_width }}"
62 63 height="{{ image.pre_height }}"
63 64 data-width="{{ image.width }}"
64 65 data-height="{{ image.height }}"/>
65 66 </a>
66 67 </div>
67 68 {% endwith %}
68 69 {% endif %}
69 70 <div class="message">
70 71 {% autoescape off %}
71 72 {% if truncated %}
72 73 {{ post.text.rendered|truncatewords_html:50 }}
73 74 {% else %}
74 75 {{ post.text.rendered }}
75 76 {% endif %}
76 77 {% endautoescape %}
77 78 {% if post.is_referenced %}
78 79 <div class="refmap">
79 80 {% autoescape off %}
80 81 {% trans "Replies" %}: {{ post.refmap }}
81 82 {% endautoescape %}
82 83 </div>
83 84 {% endif %}
84 85 </div>
85 86 {% endcache %}
86 87 {% if is_opening %}
87 88 {% cache 600 post_thread thread.id thread.last_edit_time LANGUAGE_CODE need_open_link %}
88 89 <div class="metadata">
89 90 {% if is_opening and need_open_link %}
90 91 {{ thread.get_reply_count }} {% trans 'messages' %},
91 92 {{ thread.get_images_count }} {% trans 'images' %}.
92 93 {% endif %}
93 94 <span class="tags">
94 95 {% for tag in thread.get_tags %}
95 96 <a class="tag" href="{% url 'tag' tag.name %}">
96 97 #{{ tag.name }}</a>{% if not forloop.last %},{% endif %}
97 98 {% endfor %}
98 99 </span>
99 100 </div>
100 101 {% endcache %}
101 102 {% endif %}
102 103 </div>
103 104 {% endspaceless %}
@@ -1,95 +1,103 b''
1 from base64 import b64encode
1 2 import logging
2 3
3 4 from django.test import TestCase
4 5 from boards.models import KeyPair, GlobalId, Post
5 6
6 7
7 8 logger = logging.getLogger(__name__)
8 9
9 10
10 11 class KeyTest(TestCase):
11 12 def test_create_key(self):
12 13 key = KeyPair.objects.generate_key('ecdsa')
13 14
14 15 self.assertIsNotNone(key, 'The key was not created.')
15 16
16 17 def test_validation(self):
17 18 key = KeyPair.objects.generate_key(key_type='ecdsa')
18 19 message = 'msg'
19 20 signature = key.sign(message)
20 21 valid = KeyPair.objects.verify(key.public_key, message, signature,
21 22 key_type='ecdsa')
22 23
23 24 self.assertTrue(valid, 'Message verification failed.')
24 25
25 26 def test_primary_constraint(self):
26 27 KeyPair.objects.generate_key(key_type='ecdsa', primary=True)
27 28
28 29 with self.assertRaises(Exception):
29 30 KeyPair.objects.generate_key(key_type='ecdsa', primary=True)
30 31
31 32 def test_model_id_save(self):
32 33 model_id = GlobalId(key_type='test', key='test key', local_id='1')
33 34 model_id.save()
34 35
35 36 def test_request_get(self):
36 37 post = self._create_post_with_key()
37 38
38 39 request = Post.objects.generate_request_get([post])
39 40 logger.debug(request)
40 41
42 key = KeyPair.objects.get(primary=True)
41 43 self.assertTrue('<request type="get" version="1.0">'
42 44 '<model name="post" version="1.0">'
43 '<id key="pubkey" local-id="1" type="test_key_type" />'
45 '<id key="%s" local-id="1" type="%s" />'
44 46 '</model>'
45 '</request>' in request,
47 '</request>' % (
48 key.public_key,
49 key.key_type,
50 ) in request,
46 51 'Wrong XML generated for the GET request.')
47 52
48 53 def test_response_get(self):
49 54 post = self._create_post_with_key()
50 55 reply_post = Post.objects.create_post(title='test_title',
51 56 text='[post]%d[/post]' % post.id,
52 57 thread=post.get_thread())
53 58 reply_reply_post = Post.objects.create_post(title='',
54 59 text='[post]%d[/post]'
55 60 % reply_post.id,
56 61 thread=post.get_thread())
57 62
58 63 response = Post.objects.generate_response_get([reply_post])
59 64 logger.debug(response)
60 65
61 self.assertTrue('<response>'
62 '<status>success</status>'
66 key = KeyPair.objects.get(primary=True)
67 self.assertTrue('<status>success</status>'
63 68 '<models>'
64 69 '<model name="post" ref-id="1">'
65 '<id key="pubkey" local-id="%d" type="test_key_type" />'
70 '<id key="%s" local-id="%d" type="%s" />'
66 71 '<title>test_title</title>'
67 72 '<text>[post]%d[/post]</text>'
68 73 '<thread>%d</thread>'
69 74 '<pub-time>%s</pub-time>'
70 75 '<edit-time>%s</edit-time>'
71 76 '<previous>'
72 '<id key="pubkey" local-id="%d" type="test_key_type" />'
77 '<id key="%s" local-id="%d" type="%s" />'
73 78 '</previous>'
74 79 '<next>'
75 '<id key="pubkey" local-id="%d" type="test_key_type" />'
80 '<id key="%s" local-id="%d" type="%s" />'
76 81 '</next>'
77 82 '</model>'
78 '</models>'
79 '</response>' % (
83 '</models>' % (
84 key.public_key,
80 85 reply_post.id,
86 key.key_type,
81 87 post.id,
82 88 post.id,
83 89 str(reply_post.get_edit_time_epoch()),
84 90 str(reply_post.get_pub_time_epoch()),
91 key.public_key,
85 92 post.id,
93 key.key_type,
94 key.public_key,
86 95 reply_reply_post.id,
96 key.key_type,
87 97 ) in response,
88 98 'Wrong XML generated for the GET response.')
89 99
90 100 def _create_post_with_key(self):
91 key = KeyPair(public_key='pubkey', private_key='privkey',
92 key_type='test_key_type', primary=True)
93 key.save()
101 KeyPair.objects.generate_key(primary=True)
94 102
95 103 return Post.objects.create_post(title='test_title', text='test_text')
@@ -1,55 +1,50 b''
1 1 from boards.models import KeyPair, Post
2 2 from boards.tests.mocks import MockRequest
3 3 from boards.views.sync import respond_get
4 4
5 5 __author__ = 'neko259'
6 6
7 7
8 8 from django.test import TestCase
9 9
10 10
11 11 class SyncTest(TestCase):
12 12 def test_get(self):
13 13 """
14 14 Forms a GET request of a post and checks the response.
15 15 """
16 16
17 key = KeyPair(public_key='pubkey', private_key='privkey',
18 key_type='test_key_type', primary=True)
19 key.save()
20
17 KeyPair.objects.generate_key(primary=True)
21 18 post = Post.objects.create_post(title='test_title', text='test_text')
22 19
23 20 request = MockRequest()
24 21 request.POST['xml'] = (
25 22 '<request type="get" version="1.0">'
26 23 '<model name="post" version="1.0">'
27 24 '<id key="%s" local-id="%d" type="%s" />'
28 25 '</model>'
29 26 '</request>' % (post.global_id.key,
30 27 post.id,
31 28 post.global_id.key_type)
32 29 )
33 30
34 31 self.assertTrue(
35 '<response>'
36 32 '<status>success</status>'
37 33 '<models>'
38 34 '<model name="post" ref-id="1">'
39 35 '<id key="%s" local-id="%d" type="%s" />'
40 36 '<title>%s</title>'
41 37 '<text>%s</text>'
42 38 '<pub-time>%d</pub-time>'
43 39 '<edit-time>%d</edit-time>'
44 40 '</model>'
45 '</models>'
46 '</response>' % (
41 '</models>' % (
47 42 post.global_id.key,
48 43 post.id,
49 44 post.global_id.key_type,
50 45 post.title,
51 46 post.text.raw,
52 47 post.get_pub_time_epoch(),
53 48 post.get_edit_time_epoch(),
54 49 ) in respond_get(request).content.decode(),
55 50 'Wrong response generated for the GET request.') No newline at end of file
@@ -1,83 +1,87 b''
1 1 from django.conf.urls import patterns, url, include
2 2 from django.contrib import admin
3 3 from boards import views
4 4 from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed
5 5 from boards.views import api, tag_threads, all_threads, \
6 6 settings, all_tags
7 7 from boards.views.authors import AuthorsView
8 8 from boards.views.delete_post import DeletePostView
9 9 from boards.views.ban import BanUserView
10 10 from boards.views.search import BoardSearchView
11 11 from boards.views.static import StaticPageView
12 12 from boards.views.post_admin import PostAdminView
13 13 from boards.views.preview import PostPreviewView
14 from boards.views.sync import get_post_sync_data
14 15
15 16 js_info_dict = {
16 17 'packages': ('boards',),
17 18 }
18 19
19 20 urlpatterns = patterns('',
20 21 # /boards/
21 22 url(r'^$', all_threads.AllThreadsView.as_view(), name='index'),
22 23 # /boards/page/
23 24 url(r'^page/(?P<page>\w+)/$', all_threads.AllThreadsView.as_view(),
24 25 name='index'),
25 26
26 27 # /boards/tag/tag_name/
27 28 url(r'^tag/(?P<tag_name>\w+)/$', tag_threads.TagView.as_view(),
28 29 name='tag'),
29 30 # /boards/tag/tag_id/page/
30 31 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/$',
31 32 tag_threads.TagView.as_view(), name='tag'),
32 33
33 34 # /boards/thread/
34 35 url(r'^thread/(?P<post_id>\w+)/$', views.thread.ThreadView.as_view(),
35 36 name='thread'),
36 37 url(r'^thread/(?P<post_id>\w+)/mode/(?P<mode>\w+)/$', views.thread.ThreadView
37 38 .as_view(), name='thread_mode'),
38 39
39 40 # /boards/post_admin/
40 41 url(r'^post_admin/(?P<post_id>\w+)/$', PostAdminView.as_view(),
41 42 name='post_admin'),
42 43
43 44 url(r'^settings/$', settings.SettingsView.as_view(), name='settings'),
44 45 url(r'^tags/$', all_tags.AllTagsView.as_view(), name='tags'),
45 46 url(r'^authors/$', AuthorsView.as_view(), name='authors'),
46 47 url(r'^delete/(?P<post_id>\w+)/$', DeletePostView.as_view(),
47 48 name='delete'),
48 49 url(r'^ban/(?P<post_id>\w+)/$', BanUserView.as_view(), name='ban'),
49 50
50 51 url(r'^banned/$', views.banned.BannedView.as_view(), name='banned'),
51 52 url(r'^staticpage/(?P<name>\w+)/$', StaticPageView.as_view(),
52 53 name='staticpage'),
53 54
54 55 # RSS feeds
55 56 url(r'^rss/$', AllThreadsFeed()),
56 57 url(r'^page/(?P<page>\w+)/rss/$', AllThreadsFeed()),
57 58 url(r'^tag/(?P<tag_name>\w+)/rss/$', TagThreadsFeed()),
58 59 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/rss/$', TagThreadsFeed()),
59 60 url(r'^thread/(?P<post_id>\w+)/rss/$', ThreadPostsFeed()),
60 61
61 62 # i18n
62 63 url(r'^jsi18n/$', 'boards.views.cached_js_catalog', js_info_dict,
63 64 name='js_info_dict'),
64 65
65 66 # API
66 67 url(r'^api/post/(?P<post_id>\w+)/$', api.get_post, name="get_post"),
67 68 url(r'^api/diff_thread/(?P<thread_id>\w+)/(?P<last_update_time>\w+)/$',
68 69 api.api_get_threaddiff, name="get_thread_diff"),
69 70 url(r'^api/threads/(?P<count>\w+)/$', api.api_get_threads,
70 71 name='get_threads'),
71 72 url(r'^api/tags/$', api.api_get_tags, name='get_tags'),
72 73 url(r'^api/thread/(?P<opening_post_id>\w+)/$', api.api_get_thread_posts,
73 74 name='get_thread'),
74 75 url(r'^api/add_post/(?P<opening_post_id>\w+)/$', api.api_add_post,
75 76 name='add_post'),
76 77
77 78 # Search
78 79 url(r'^search/$', BoardSearchView.as_view(), name='search'),
79 80
80 81 # Post preview
81 url(r'^preview/$', PostPreviewView.as_view(), name='preview')
82 url(r'^preview/$', PostPreviewView.as_view(), name='preview'),
83
84 url(r'^post_xml/(?P<post_id>\d+)$', get_post_sync_data,
85 name='post_sync_data'),
82 86
83 87 )
@@ -1,33 +1,49 b''
1 1 import xml.etree.ElementTree as et
2 from django.http import HttpResponse
2 from django.http import HttpResponse, Http404
3 3 from boards.models import GlobalId, Post
4 4
5 5
6 6 def respond_pull(request):
7 7 pass
8 8
9 9
10 10 def respond_get(request):
11 11 """
12 12 Processes a GET request with post ID list and returns the posts XML list.
13 13 Request should contain an 'xml' post attribute with the actual request XML.
14 14 """
15 15
16 16 request_xml = request.POST['xml']
17 17
18 18 posts = []
19 19
20 20 root_tag = et.fromstring(request_xml)
21 21 model_tag = root_tag[0]
22 22 for id_tag in model_tag:
23 23 try:
24 24 global_id = GlobalId.from_xml_element(id_tag, existing=True)
25 25 posts += Post.objects.filter(global_id=global_id)
26 26 except GlobalId.DoesNotExist:
27 27 # This is normal. If we don't have such GlobalId in the system,
28 28 # just ignore this ID and proceed to the next one.
29 29 pass
30 30
31 31 response_xml = Post.objects.generate_response_get(posts)
32 32
33 return HttpResponse(content=response_xml) No newline at end of file
33 return HttpResponse(content=response_xml)
34
35
36 def get_post_sync_data(request, post_id):
37 try:
38 post = Post.objects.get(id=post_id)
39 except Post.DoesNotExist:
40 raise Http404()
41
42 content = 'Global ID: %s\n\nXML: %s' \
43 % (post.global_id, Post.objects.generate_response_get([post]))
44
45
46 return HttpResponse(
47 content_type='text/plain',
48 content=content,
49 ) No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now