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