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