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