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