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