##// END OF EJS Templates
Added more parameters to the post xml output
neko259 -
r828:b99c5139 decentral
parent child Browse files
Show More
@@ -1,447 +1,467 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 15 from boards.models import PostImage, KeyPair, GlobalId
16 16 from boards.models.base import Viewable
17 17 from boards.models.thread import Thread
18 from boards import utils
18 19
19 20
20 21 APP_LABEL_BOARDS = 'boards'
21 22
22 23 CACHE_KEY_PPD = 'ppd'
23 24 CACHE_KEY_POST_URL = 'post_url'
24 25
25 26 POSTS_PER_DAY_RANGE = 7
26 27
27 28 BAN_REASON_AUTO = 'Auto'
28 29
29 30 IMAGE_THUMB_SIZE = (200, 150)
30 31
31 32 TITLE_MAX_LENGTH = 200
32 33
33 34 DEFAULT_MARKUP_TYPE = 'bbcode'
34 35
35 36 # TODO This should be removed
36 37 NO_IP = '0.0.0.0'
37 38
38 39 # TODO Real user agent should be saved instead of this
39 40 UNKNOWN_UA = ''
40 41
41 42 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
42 43
43 44 TAG_MODEL = 'model'
44 45 TAG_REQUEST = 'request'
45 46 TAG_RESPONSE = 'response'
46 47 TAG_ID = 'id'
47 48 TAG_STATUS = 'status'
48 49 TAG_MODELS = 'models'
49 50 TAG_TITLE = 'title'
50 51 TAG_TEXT = 'text'
52 TAG_THREAD = 'thread'
53 TAG_PUB_TIME = 'pub-time'
54 TAG_EDIT_TIME = 'edit-time'
51 55
52 56 TYPE_GET = 'get'
53 57
54 58 ATTR_VERSION = 'version'
55 59 ATTR_TYPE = 'type'
56 60 ATTR_NAME = 'name'
57 61 ATTR_REF_ID = 'ref-id'
58 62
59 63 STATUS_SUCCESS = 'success'
60 64
61 65 logger = logging.getLogger(__name__)
62 66
63 67
64 68 class PostManager(models.Manager):
65 69 def create_post(self, title, text, image=None, thread=None, ip=NO_IP,
66 70 tags=None):
67 71 """
68 72 Creates new post
69 73 """
70 74
71 75 if not tags:
72 76 tags = []
73 77
74 78 posting_time = timezone.now()
75 79 if not thread:
76 80 thread = Thread.objects.create(bump_time=posting_time,
77 81 last_edit_time=posting_time)
78 82 new_thread = True
79 83 else:
80 84 thread.bump()
81 85 thread.last_edit_time = posting_time
82 86 thread.save()
83 87 new_thread = False
84 88
85 89 post = self.create(title=title,
86 90 text=text,
87 91 pub_time=posting_time,
88 92 thread_new=thread,
89 93 poster_ip=ip,
90 94 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
91 95 # last!
92 96 last_edit_time=posting_time)
93 97
94 98 post.set_global_id()
95 99
96 100 if image:
97 101 post_image = PostImage.objects.create(image=image)
98 102 post.images.add(post_image)
99 103 logger.info('Created image #%d for post #%d' % (post_image.id,
100 104 post.id))
101 105
102 106 thread.replies.add(post)
103 107 list(map(thread.add_tag, tags))
104 108
105 109 if new_thread:
106 110 Thread.objects.process_oldest_threads()
107 111 self.connect_replies(post)
108 112
109 113 logger.info('Created post #%d with title %s' % (post.id,
110 114 post.get_title()))
111 115
112 116 return post
113 117
114 118 def delete_post(self, post):
115 119 """
116 120 Deletes post and update or delete its thread
117 121 """
118 122
119 123 post_id = post.id
120 124
121 125 thread = post.get_thread()
122 126
123 127 if post.is_opening():
124 128 thread.delete()
125 129 else:
126 130 thread.last_edit_time = timezone.now()
127 131 thread.save()
128 132
129 133 post.delete()
130 134
131 135 logger.info('Deleted post #%d (%s)' % (post_id, post.get_title()))
132 136
133 137 def delete_posts_by_ip(self, ip):
134 138 """
135 139 Deletes all posts of the author with same IP
136 140 """
137 141
138 142 posts = self.filter(poster_ip=ip)
139 143 for post in posts:
140 144 self.delete_post(post)
141 145
142 146 def connect_replies(self, post):
143 147 """
144 148 Connects replies to a post to show them as a reflink map
145 149 """
146 150
147 151 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
148 152 post_id = reply_number.group(1)
149 153 ref_post = self.filter(id=post_id)
150 154 if ref_post.count() > 0:
151 155 referenced_post = ref_post[0]
152 156 referenced_post.referenced_posts.add(post)
153 157 referenced_post.last_edit_time = post.pub_time
154 158 referenced_post.build_refmap()
155 159 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
156 160
157 161 referenced_thread = referenced_post.get_thread()
158 162 referenced_thread.last_edit_time = post.pub_time
159 163 referenced_thread.save(update_fields=['last_edit_time'])
160 164
161 165 def get_posts_per_day(self):
162 166 """
163 167 Gets average count of posts per day for the last 7 days
164 168 """
165 169
166 170 day_end = date.today()
167 171 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
168 172
169 173 cache_key = CACHE_KEY_PPD + str(day_end)
170 174 ppd = cache.get(cache_key)
171 175 if ppd:
172 176 return ppd
173 177
174 178 day_time_start = timezone.make_aware(datetime.combine(
175 179 day_start, dtime()), timezone.get_current_timezone())
176 180 day_time_end = timezone.make_aware(datetime.combine(
177 181 day_end, dtime()), timezone.get_current_timezone())
178 182
179 183 posts_per_period = float(self.filter(
180 184 pub_time__lte=day_time_end,
181 185 pub_time__gte=day_time_start).count())
182 186
183 187 ppd = posts_per_period / POSTS_PER_DAY_RANGE
184 188
185 189 cache.set(cache_key, ppd)
186 190 return ppd
187 191
188 192
189 193 def generate_request_get(self, model_list: list):
190 194 """
191 195 Form a get request from a list of ModelId objects.
192 196 """
193 197
194 198 request = et.Element(TAG_REQUEST)
195 199 request.set(ATTR_TYPE, TYPE_GET)
196 200 request.set(ATTR_VERSION, '1.0')
197 201
198 202 model = et.SubElement(request, TAG_MODEL)
199 203 model.set(ATTR_VERSION, '1.0')
200 204 model.set(ATTR_NAME, 'post')
201 205
202 206 for post in model_list:
203 207 tag_id = et.SubElement(model, TAG_ID)
204 208 post.global_id.to_xml_element(tag_id)
205 209
206 210 return et.tostring(request, 'unicode')
207 211
208 212 def generate_response_get(self, model_list: list):
209 213 response = et.Element(TAG_RESPONSE)
210 214
211 215 status = et.SubElement(response, TAG_STATUS)
212 216 status.text = STATUS_SUCCESS
213 217
214 218 models = et.SubElement(response, TAG_MODELS)
215 219
216 220 ref_id = 1
217 221 for post in model_list:
218 222 model = et.SubElement(models, TAG_MODEL)
219 223 model.set(ATTR_NAME, 'post')
220 224 model.set(ATTR_REF_ID, str(ref_id))
221 225 ref_id += 1
222 226
223 227 tag_id = et.SubElement(model, TAG_ID)
224 228 post.global_id.to_xml_element(tag_id)
225 229
226 230 title = et.SubElement(model, TAG_TITLE)
227 231 title.text = post.title
228 232
229 233 text = et.SubElement(model, TAG_TEXT)
230 text.text = post.text.rendered
234 text.text = post.text.raw
235
236 if not post.is_opening():
237 thread = et.SubElement(model, TAG_THREAD)
238 thread.text = post.get_opening_post_id()
239
240 pub_time = et.SubElement(model, TAG_PUB_TIME)
241 pub_time.text = str(post.get_pub_time_epoch())
242
243 edit_time = et.SubElement(model, TAG_EDIT_TIME)
244 edit_time.text = str(post.get_edit_time_epoch())
231 245
232 246 return et.tostring(response, 'unicode')
233 247
234 248
235 249 class Post(models.Model, Viewable):
236 250 """A post is a message."""
237 251
238 252 objects = PostManager()
239 253
240 254 class Meta:
241 255 app_label = APP_LABEL_BOARDS
242 256 ordering = ('id',)
243 257
244 258 title = models.CharField(max_length=TITLE_MAX_LENGTH)
245 259 pub_time = models.DateTimeField()
246 260 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
247 261 escape_html=False)
248 262
249 263 images = models.ManyToManyField(PostImage, null=True, blank=True,
250 264 related_name='ip+', db_index=True)
251 265
252 266 poster_ip = models.GenericIPAddressField()
253 267 poster_user_agent = models.TextField()
254 268
255 269 thread_new = models.ForeignKey('Thread', null=True, default=None,
256 270 db_index=True)
257 271 last_edit_time = models.DateTimeField()
258 272
259 273 # Replies to the post
260 274 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
261 275 null=True,
262 276 blank=True, related_name='rfp+',
263 277 db_index=True)
264 278
265 279 # Replies map. This is built from the referenced posts list to speed up
266 280 # page loading (no need to get all the referenced posts from the database).
267 281 refmap = models.TextField(null=True, blank=True)
268 282
269 283 # Global ID with author key. If the message was downloaded from another
270 284 # server, this indicates the server.
271 285 global_id = models.OneToOneField('GlobalId', null=True, blank=True)
272 286
273 287 # One post can be signed by many nodes that give their trust to it
274 288 signature = models.ManyToManyField('Signature', null=True, blank=True)
275 289
276 290 def __unicode__(self):
277 291 return '#' + str(self.id) + ' ' + self.title + ' (' + \
278 292 self.text.raw[:50] + ')'
279 293
280 294 def get_title(self):
281 295 """
282 296 Gets original post title or part of its text.
283 297 """
284 298
285 299 title = self.title
286 300 if not title:
287 301 title = self.text.rendered
288 302
289 303 return title
290 304
291 305 def build_refmap(self):
292 306 """
293 307 Builds a replies map string from replies list. This is a cache to stop
294 308 the server from recalculating the map on every post show.
295 309 """
296 310 map_string = ''
297 311
298 312 first = True
299 313 for refpost in self.referenced_posts.all():
300 314 if not first:
301 315 map_string += ', '
302 316 map_string += '<a href="%s">&gt;&gt;%s</a>' % (refpost.get_url(),
303 317 refpost.id)
304 318 first = False
305 319
306 320 self.refmap = map_string
307 321
308 322 def get_sorted_referenced_posts(self):
309 323 return self.refmap
310 324
311 325 def is_referenced(self):
312 326 return len(self.refmap) > 0
313 327
314 328 def is_opening(self):
315 329 """
316 330 Checks if this is an opening post or just a reply.
317 331 """
318 332
319 333 return self.get_thread().get_opening_post_id() == self.id
320 334
321 335 @transaction.atomic
322 336 def add_tag(self, tag):
323 337 edit_time = timezone.now()
324 338
325 339 thread = self.get_thread()
326 340 thread.add_tag(tag)
327 341 self.last_edit_time = edit_time
328 342 self.save(update_fields=['last_edit_time'])
329 343
330 344 thread.last_edit_time = edit_time
331 345 thread.save(update_fields=['last_edit_time'])
332 346
333 347 @transaction.atomic
334 348 def remove_tag(self, tag):
335 349 edit_time = timezone.now()
336 350
337 351 thread = self.get_thread()
338 352 thread.remove_tag(tag)
339 353 self.last_edit_time = edit_time
340 354 self.save(update_fields=['last_edit_time'])
341 355
342 356 thread.last_edit_time = edit_time
343 357 thread.save(update_fields=['last_edit_time'])
344 358
345 359 def get_url(self, thread=None):
346 360 """
347 361 Gets full url to the post.
348 362 """
349 363
350 364 cache_key = CACHE_KEY_POST_URL + str(self.id)
351 365 link = cache.get(cache_key)
352 366
353 367 if not link:
354 368 if not thread:
355 369 thread = self.get_thread()
356 370
357 371 opening_id = thread.get_opening_post_id()
358 372
359 373 if self.id != opening_id:
360 374 link = reverse('thread', kwargs={
361 375 'post_id': opening_id}) + '#' + str(self.id)
362 376 else:
363 377 link = reverse('thread', kwargs={'post_id': self.id})
364 378
365 379 cache.set(cache_key, link)
366 380
367 381 return link
368 382
369 383 def get_thread(self):
370 384 """
371 385 Gets post's thread.
372 386 """
373 387
374 388 return self.thread_new
375 389
376 390 def get_referenced_posts(self):
377 391 return self.referenced_posts.only('id', 'thread_new')
378 392
379 393 def get_text(self):
380 394 return self.text
381 395
382 396 def get_view(self, moderator=False, need_open_link=False,
383 397 truncated=False, *args, **kwargs):
384 398 if 'is_opening' in kwargs:
385 399 is_opening = kwargs['is_opening']
386 400 else:
387 401 is_opening = self.is_opening()
388 402
389 403 if 'thread' in kwargs:
390 404 thread = kwargs['thread']
391 405 else:
392 406 thread = self.get_thread()
393 407
394 408 if 'can_bump' in kwargs:
395 409 can_bump = kwargs['can_bump']
396 410 else:
397 411 can_bump = thread.can_bump()
398 412
399 413 if is_opening:
400 414 opening_post_id = self.id
401 415 else:
402 416 opening_post_id = thread.get_opening_post_id()
403 417
404 418 return render_to_string('boards/post.html', {
405 419 'post': self,
406 420 'moderator': moderator,
407 421 'is_opening': is_opening,
408 422 'thread': thread,
409 423 'bumpable': can_bump,
410 424 'need_open_link': need_open_link,
411 425 'truncated': truncated,
412 426 'opening_post_id': opening_post_id,
413 427 })
414 428
415 429 def get_first_image(self):
416 430 return self.images.earliest('id')
417 431
418 432 def delete(self, using=None):
419 433 """
420 434 Deletes all post images and the post itself.
421 435 """
422 436
423 437 self.images.all().delete()
424 438
425 439 super(Post, self).delete(using)
426 440
427 441 def set_global_id(self, key_pair=None):
428 442 """
429 443 Sets global id based on the given key pair. If no key pair is given,
430 444 default one is used.
431 445 """
432 446
433 447 if key_pair:
434 448 key = key_pair
435 449 else:
436 450 try:
437 451 key = KeyPair.objects.get(primary=True)
438 452 except KeyPair.DoesNotExist:
439 453 # Do not update the global id because there is no key defined
440 454 return
441 455 global_id = GlobalId(key_type=key.key_type,
442 456 key=key.public_key,
443 457 local_id = self.id)
444 458
445 459 self.global_id = global_id
446 460
447 461 self.save(update_fields=['global_id'])
462
463 def get_pub_time_epoch(self):
464 return utils.datetime_to_epoch(self.pub_time)
465
466 def get_edit_time_epoch(self):
467 return utils.datetime_to_epoch(self.last_edit_time)
@@ -1,69 +1,80 b''
1 1 import logging
2 2
3 3 from django.test import TestCase
4 4 from boards.models import KeyPair, GlobalId, Post
5 5
6 6
7 7 logger = logging.getLogger(__name__)
8 8
9 9
10 10 class KeyTest(TestCase):
11 11 def test_create_key(self):
12 12 key = KeyPair.objects.generate_key('ecdsa')
13 13
14 14 self.assertIsNotNone(key, 'The key was not created.')
15 15
16 16 def test_validation(self):
17 17 key = KeyPair.objects.generate_key(key_type='ecdsa')
18 18 message = 'msg'
19 19 signature = key.sign(message)
20 20 valid = KeyPair.objects.verify(key.public_key, message, signature,
21 21 key_type='ecdsa')
22 22
23 23 self.assertTrue(valid, 'Message verification failed.')
24 24
25 25 def test_primary_constraint(self):
26 26 KeyPair.objects.generate_key(key_type='ecdsa', primary=True)
27 27
28 28 try:
29 29 KeyPair.objects.generate_key(key_type='ecdsa', primary=True)
30 30 self.fail('Exception should be thrown indicating there can be only'
31 31 ' one primary key.')
32 32 except Exception:
33 33 pass
34 34
35 35 def test_model_id_save(self):
36 36 model_id = GlobalId(key_type='test', key='test key', local_id='1')
37 37 model_id.save()
38 38
39 39 def test_request_get(self):
40 40 post = self._create_post_with_key()
41 41
42 42 request = Post.objects.generate_request_get([post])
43 43 logger.debug(request)
44 44
45 self.assertTrue('<request type="get" version="1.0"><model '
46 'name="post" version="1.0"><id key="pubkey" '
47 'local-id="1" type="test_key_type" /></model></request>' in
48 request,
45 self.assertTrue('<request type="get" version="1.0">'
46 '<model name="post" version="1.0">'
47 '<id key="pubkey" local-id="1" type="test_key_type" />'
48 '</model>'
49 '</request>' in request,
49 50 'Wrong XML generated for the GET request.')
50 51
51 52 def test_response_get(self):
52 53 post = self._create_post_with_key()
53 54
54 55 response = Post.objects.generate_response_get([post])
55 56 logger.debug(response)
56 57
57 self.assertTrue('<response><status>success</status><models><model '
58 'name="post" ref-id="1"><id key="pubkey" local-id="1"'
59 ' type="test_key_type" /><title>test_title</title>'
58 self.assertTrue('<response>'
59 '<status>success</status>'
60 '<models>'
61 '<model name="post" ref-id="1">'
62 '<id key="pubkey" local-id="1" type="test_key_type" />'
63 '<title>test_title</title>'
60 64 '<text>test_text</text>'
61 '</model></models></response>' in response,
65 '<pub-time>%s</pub-time>'
66 '<edit-time>%s</edit-time>'
67 '</model>'
68 '</models>'
69 '</response>' % (
70 str(post.get_edit_time_epoch()),
71 str(post.get_pub_time_epoch())
72 ) in response,
62 73 'Wrong XML generated for the GET response.')
63 74
64 75 def _create_post_with_key(self):
65 76 key = KeyPair(public_key='pubkey', private_key='privkey',
66 77 key_type='test_key_type', primary=True)
67 78 key.save()
68 79
69 80 return Post.objects.create_post(title='test_title', text='test_text')
General Comments 0
You need to be logged in to leave comments. Login now