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