##// END OF EJS Templates
Process updated posts from sync server
neko259 -
r1586:931a7e94 default
parent child Browse files
Show More
@@ -1,22 +1,20 b''
1 from django.core.management import BaseCommand
1 from django.core.management import BaseCommand
2 from django.db import transaction
2 from django.db import transaction
3
3
4 from boards.models import GlobalId
4 from boards.models import GlobalId
5
5
6 __author__ = 'neko259'
6 __author__ = 'neko259'
7
7
8
8
9 class Command(BaseCommand):
9 class Command(BaseCommand):
10 help = 'Removes local global ID cache'
10 help = 'Removes local global ID cache'
11
11
12 @transaction.atomic
12 @transaction.atomic
13 def handle(self, *args, **options):
13 def handle(self, *args, **options):
14 count = 0
14 count = 0
15 for global_id in GlobalId.objects.exclude(content__isnull=True).exclude(
15 for global_id in GlobalId.objects.exclude(content__isnull=True).exclude(
16 content=''):
16 content=''):
17 if global_id.is_local():
17 if global_id.is_local():
18 global_id.content = None
18 global_id.clear_cache()
19 global_id.save()
20 global_id.signature_set.all().delete()
21 count += 1
19 count += 1
22 print('Invalidated {} caches.'.format(count))
20 print('Invalidated {} caches.'.format(count))
@@ -1,88 +1,90 b''
1 import re
1 import re
2 import xml.etree.ElementTree as ET
2 import xml.etree.ElementTree as ET
3
3
4 import httplib2
4 import httplib2
5 from django.core.management import BaseCommand
5 from django.core.management import BaseCommand
6
6
7 from boards.models import GlobalId
7 from boards.models import GlobalId
8 from boards.models.post.sync import SyncManager, TAG_ID, TAG_VERSION
8 from boards.models.post.sync import SyncManager, TAG_ID, TAG_VERSION
9
9
10 __author__ = 'neko259'
10 __author__ = 'neko259'
11
11
12
12
13 REGEX_GLOBAL_ID = re.compile(r'(\w+)::([\w\+/]+)::(\d+)')
13 REGEX_GLOBAL_ID = re.compile(r'(\w+)::([\w\+/]+)::(\d+)')
14
14
15
15
16 class Command(BaseCommand):
16 class Command(BaseCommand):
17 help = 'Send a sync or get request to the server.'
17 help = 'Send a sync or get request to the server.'
18
18
19 def add_arguments(self, parser):
19 def add_arguments(self, parser):
20 parser.add_argument('url', type=str, help='Server root url')
20 parser.add_argument('url', type=str, help='Server root url')
21 parser.add_argument('--global-id', type=str, default='',
21 parser.add_argument('--global-id', type=str, default='',
22 help='Post global ID')
22 help='Post global ID')
23 parser.add_argument('--split-query', type=int,
23 parser.add_argument('--split-query', type=int,
24 help='Split GET query into separate by the given'
24 help='Split GET query into separate by the given'
25 ' number of posts in one')
25 ' number of posts in one')
26
26
27 def handle(self, *args, **options):
27 def handle(self, *args, **options):
28 url = options.get('url')
28 url = options.get('url')
29
29
30 list_url = url + 'api/sync/list/'
30 list_url = url + 'api/sync/list/'
31 get_url = url + 'api/sync/get/'
31 get_url = url + 'api/sync/get/'
32 file_url = url[:-1]
32 file_url = url[:-1]
33
33
34 global_id_str = options.get('global_id')
34 global_id_str = options.get('global_id')
35 if global_id_str:
35 if global_id_str:
36 match = REGEX_GLOBAL_ID.match(global_id_str)
36 match = REGEX_GLOBAL_ID.match(global_id_str)
37 if match:
37 if match:
38 key_type = match.group(1)
38 key_type = match.group(1)
39 key = match.group(2)
39 key = match.group(2)
40 local_id = match.group(3)
40 local_id = match.group(3)
41
41
42 global_id = GlobalId(key_type=key_type, key=key,
42 global_id = GlobalId(key_type=key_type, key=key,
43 local_id=local_id)
43 local_id=local_id)
44
44
45 xml = GlobalId.objects.generate_request_get([global_id])
45 xml = GlobalId.objects.generate_request_get([global_id])
46 h = httplib2.Http()
46 h = httplib2.Http()
47 response, content = h.request(get_url, method="POST", body=xml)
47 response, content = h.request(get_url, method="POST", body=xml)
48
48
49 SyncManager.parse_response_get(content, file_url)
49 SyncManager.parse_response_get(content, file_url)
50 else:
50 else:
51 raise Exception('Invalid global ID')
51 raise Exception('Invalid global ID')
52 else:
52 else:
53 print('Running LIST request')
53 print('Running LIST request...')
54 h = httplib2.Http()
54 h = httplib2.Http()
55 xml = GlobalId.objects.generate_request_list()
55 xml = GlobalId.objects.generate_request_list()
56 response, content = h.request(list_url, method="POST", body=xml)
56 response, content = h.request(list_url, method="POST", body=xml)
57 print('Processing response...')
57
58
58 root = ET.fromstring(content)
59 root = ET.fromstring(content)
59 status = root.findall('status')[0].text
60 status = root.findall('status')[0].text
60 if status == 'success':
61 if status == 'success':
61 ids_to_sync = list()
62 ids_to_sync = list()
62
63
63 models = root.findall('models')[0]
64 models = root.findall('models')[0]
64 for model in models:
65 for model in models:
65 tag_id = model.find(TAG_ID)
66 tag_id = model.find(TAG_ID)
66 global_id, exists = GlobalId.from_xml_element(tag_id)
67 global_id, exists = GlobalId.from_xml_element(tag_id)
67 tag_version = model.find(TAG_VERSION)
68 tag_version = model.find(TAG_VERSION)
68 if tag_version is not None:
69 if tag_version is not None:
69 version = int(tag_version.text) or 1
70 version = int(tag_version.text) or 1
70 else:
71 else:
71 version = 1
72 version = 1
72 if not exists or global_id.post.version < version:
73 if not exists or global_id.post.version < version:
73 ids_to_sync.append(global_id)
74 ids_to_sync.append(global_id)
74 print('Starting sync...')
75 print('Starting sync...')
75
76
76 if len(ids_to_sync) > 0:
77 if len(ids_to_sync) > 0:
77 limit = options.get('split_query', len(ids_to_sync))
78 limit = options.get('split_query', len(ids_to_sync))
78 for offset in range(0, len(ids_to_sync), limit):
79 for offset in range(0, len(ids_to_sync), limit):
79 xml = GlobalId.objects.generate_request_get(ids_to_sync[offset:offset+limit])
80 xml = GlobalId.objects.generate_request_get(ids_to_sync[offset:offset+limit])
80 h = httplib2.Http()
81 h = httplib2.Http()
81 print('Running GET request')
82 print('Running GET request...')
82 response, content = h.request(get_url, method="POST", body=xml)
83 response, content = h.request(get_url, method="POST", body=xml)
84 print('Processing response...')
83
85
84 SyncManager.parse_response_get(content, file_url)
86 SyncManager.parse_response_get(content, file_url)
85 else:
87 else:
86 print('Nothing to get, everything synced')
88 print('Nothing to get, everything synced')
87 else:
89 else:
88 raise Exception('Invalid response status')
90 raise Exception('Invalid response status')
@@ -1,396 +1,397 b''
1 import uuid
1 import uuid
2
2
3 import re
3 import re
4 from boards import settings
4 from boards import settings
5 from boards.abstracts.tripcode import Tripcode
5 from boards.abstracts.tripcode import Tripcode
6 from boards.models import PostImage, Attachment, KeyPair, GlobalId
6 from boards.models import PostImage, Attachment, KeyPair, GlobalId
7 from boards.models.base import Viewable
7 from boards.models.base import Viewable
8 from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
8 from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
9 from boards.models.post.manager import PostManager
9 from boards.models.post.manager import PostManager
10 from boards.utils import datetime_to_epoch
10 from boards.utils import datetime_to_epoch
11 from django.core.exceptions import ObjectDoesNotExist
11 from django.core.exceptions import ObjectDoesNotExist
12 from django.core.urlresolvers import reverse
12 from django.core.urlresolvers import reverse
13 from django.db import models
13 from django.db import models
14 from django.db.models import TextField, QuerySet, F
14 from django.db.models import TextField, QuerySet, F
15 from django.template.defaultfilters import truncatewords, striptags
15 from django.template.defaultfilters import truncatewords, striptags
16 from django.template.loader import render_to_string
16 from django.template.loader import render_to_string
17
17
18 CSS_CLS_HIDDEN_POST = 'hidden_post'
18 CSS_CLS_HIDDEN_POST = 'hidden_post'
19 CSS_CLS_DEAD_POST = 'dead_post'
19 CSS_CLS_DEAD_POST = 'dead_post'
20 CSS_CLS_ARCHIVE_POST = 'archive_post'
20 CSS_CLS_ARCHIVE_POST = 'archive_post'
21 CSS_CLS_POST = 'post'
21 CSS_CLS_POST = 'post'
22 CSS_CLS_MONOCHROME = 'monochrome'
22 CSS_CLS_MONOCHROME = 'monochrome'
23
23
24 TITLE_MAX_WORDS = 10
24 TITLE_MAX_WORDS = 10
25
25
26 APP_LABEL_BOARDS = 'boards'
26 APP_LABEL_BOARDS = 'boards'
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 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
34 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
35 REGEX_GLOBAL_REPLY = re.compile(r'\[post\](\w+)::([^:]+)::(\d+)\[/post\]')
35 REGEX_GLOBAL_REPLY = re.compile(r'\[post\](\w+)::([^:]+)::(\d+)\[/post\]')
36 REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?')
36 REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?')
37 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
37 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
38
38
39 PARAMETER_TRUNCATED = 'truncated'
39 PARAMETER_TRUNCATED = 'truncated'
40 PARAMETER_TAG = 'tag'
40 PARAMETER_TAG = 'tag'
41 PARAMETER_OFFSET = 'offset'
41 PARAMETER_OFFSET = 'offset'
42 PARAMETER_DIFF_TYPE = 'type'
42 PARAMETER_DIFF_TYPE = 'type'
43 PARAMETER_CSS_CLASS = 'css_class'
43 PARAMETER_CSS_CLASS = 'css_class'
44 PARAMETER_THREAD = 'thread'
44 PARAMETER_THREAD = 'thread'
45 PARAMETER_IS_OPENING = 'is_opening'
45 PARAMETER_IS_OPENING = 'is_opening'
46 PARAMETER_POST = 'post'
46 PARAMETER_POST = 'post'
47 PARAMETER_OP_ID = 'opening_post_id'
47 PARAMETER_OP_ID = 'opening_post_id'
48 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
48 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
49 PARAMETER_REPLY_LINK = 'reply_link'
49 PARAMETER_REPLY_LINK = 'reply_link'
50 PARAMETER_NEED_OP_DATA = 'need_op_data'
50 PARAMETER_NEED_OP_DATA = 'need_op_data'
51
51
52 POST_VIEW_PARAMS = (
52 POST_VIEW_PARAMS = (
53 'need_op_data',
53 'need_op_data',
54 'reply_link',
54 'reply_link',
55 'need_open_link',
55 'need_open_link',
56 'truncated',
56 'truncated',
57 'mode_tree',
57 'mode_tree',
58 'perms',
58 'perms',
59 'tree_depth',
59 'tree_depth',
60 )
60 )
61
61
62
62
63 class Post(models.Model, Viewable):
63 class Post(models.Model, Viewable):
64 """A post is a message."""
64 """A post is a message."""
65
65
66 objects = PostManager()
66 objects = PostManager()
67
67
68 class Meta:
68 class Meta:
69 app_label = APP_LABEL_BOARDS
69 app_label = APP_LABEL_BOARDS
70 ordering = ('id',)
70 ordering = ('id',)
71
71
72 title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
72 title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
73 pub_time = models.DateTimeField()
73 pub_time = models.DateTimeField()
74 text = TextField(blank=True, null=True)
74 text = TextField(blank=True, null=True)
75 _text_rendered = TextField(blank=True, null=True, editable=False)
75 _text_rendered = TextField(blank=True, null=True, editable=False)
76
76
77 images = models.ManyToManyField(PostImage, null=True, blank=True,
77 images = models.ManyToManyField(PostImage, null=True, blank=True,
78 related_name='post_images', db_index=True)
78 related_name='post_images', db_index=True)
79 attachments = models.ManyToManyField(Attachment, null=True, blank=True,
79 attachments = models.ManyToManyField(Attachment, null=True, blank=True,
80 related_name='attachment_posts')
80 related_name='attachment_posts')
81
81
82 poster_ip = models.GenericIPAddressField()
82 poster_ip = models.GenericIPAddressField()
83
83
84 # TODO This field can be removed cause UID is used for update now
84 # TODO This field can be removed cause UID is used for update now
85 last_edit_time = models.DateTimeField()
85 last_edit_time = models.DateTimeField()
86
86
87 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
87 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
88 null=True,
88 null=True,
89 blank=True, related_name='refposts',
89 blank=True, related_name='refposts',
90 db_index=True)
90 db_index=True)
91 refmap = models.TextField(null=True, blank=True)
91 refmap = models.TextField(null=True, blank=True)
92 threads = models.ManyToManyField('Thread', db_index=True,
92 threads = models.ManyToManyField('Thread', db_index=True,
93 related_name='multi_replies')
93 related_name='multi_replies')
94 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
94 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
95
95
96 url = models.TextField()
96 url = models.TextField()
97 uid = models.TextField(db_index=True)
97 uid = models.TextField(db_index=True)
98
98
99 # Global ID with author key. If the message was downloaded from another
99 # Global ID with author key. If the message was downloaded from another
100 # server, this indicates the server.
100 # server, this indicates the server.
101 global_id = models.OneToOneField(GlobalId, null=True, blank=True,
101 global_id = models.OneToOneField(GlobalId, null=True, blank=True,
102 on_delete=models.CASCADE)
102 on_delete=models.CASCADE)
103
103
104 tripcode = models.CharField(max_length=50, blank=True, default='')
104 tripcode = models.CharField(max_length=50, blank=True, default='')
105 opening = models.BooleanField(db_index=True)
105 opening = models.BooleanField(db_index=True)
106 hidden = models.BooleanField(default=False)
106 hidden = models.BooleanField(default=False)
107 version = models.IntegerField(default=1)
107 version = models.IntegerField(default=1)
108
108
109 def __str__(self):
109 def __str__(self):
110 return 'P#{}/{}'.format(self.id, self.get_title())
110 return 'P#{}/{}'.format(self.id, self.get_title())
111
111
112 def get_title(self) -> str:
112 def get_title(self) -> str:
113 return self.title
113 return self.title
114
114
115 def get_title_or_text(self):
115 def get_title_or_text(self):
116 title = self.get_title()
116 title = self.get_title()
117 if not title:
117 if not title:
118 title = truncatewords(striptags(self.get_text()), TITLE_MAX_WORDS)
118 title = truncatewords(striptags(self.get_text()), TITLE_MAX_WORDS)
119
119
120 return title
120 return title
121
121
122 def build_refmap(self) -> None:
122 def build_refmap(self) -> None:
123 """
123 """
124 Builds a replies map string from replies list. This is a cache to stop
124 Builds a replies map string from replies list. This is a cache to stop
125 the server from recalculating the map on every post show.
125 the server from recalculating the map on every post show.
126 """
126 """
127
127
128 post_urls = [refpost.get_link_view()
128 post_urls = [refpost.get_link_view()
129 for refpost in self.referenced_posts.all()]
129 for refpost in self.referenced_posts.all()]
130
130
131 self.refmap = ', '.join(post_urls)
131 self.refmap = ', '.join(post_urls)
132
132
133 def is_referenced(self) -> bool:
133 def is_referenced(self) -> bool:
134 return self.refmap and len(self.refmap) > 0
134 return self.refmap and len(self.refmap) > 0
135
135
136 def is_opening(self) -> bool:
136 def is_opening(self) -> bool:
137 """
137 """
138 Checks if this is an opening post or just a reply.
138 Checks if this is an opening post or just a reply.
139 """
139 """
140
140
141 return self.opening
141 return self.opening
142
142
143 def get_absolute_url(self, thread=None):
143 def get_absolute_url(self, thread=None):
144 url = None
144 url = None
145
145
146 if thread is None:
146 if thread is None:
147 thread = self.get_thread()
147 thread = self.get_thread()
148
148
149 # Url is cached only for the "main" thread. When getting url
149 # Url is cached only for the "main" thread. When getting url
150 # for other threads, do it manually.
150 # for other threads, do it manually.
151 if self.url:
151 if self.url:
152 url = self.url
152 url = self.url
153
153
154 if url is None:
154 if url is None:
155 opening = self.is_opening()
155 opening = self.is_opening()
156 opening_id = self.id if opening else thread.get_opening_post_id()
156 opening_id = self.id if opening else thread.get_opening_post_id()
157 url = reverse('thread', kwargs={'post_id': opening_id})
157 url = reverse('thread', kwargs={'post_id': opening_id})
158 if not opening:
158 if not opening:
159 url += '#' + str(self.id)
159 url += '#' + str(self.id)
160
160
161 return url
161 return url
162
162
163 def get_thread(self):
163 def get_thread(self):
164 return self.thread
164 return self.thread
165
165
166 def get_thread_id(self):
166 def get_thread_id(self):
167 return self.thread_id
167 return self.thread_id
168
168
169 def get_threads(self) -> QuerySet:
169 def get_threads(self) -> QuerySet:
170 """
170 """
171 Gets post's thread.
171 Gets post's thread.
172 """
172 """
173
173
174 return self.threads
174 return self.threads
175
175
176 def _get_cache_key(self):
176 def _get_cache_key(self):
177 return [datetime_to_epoch(self.last_edit_time)]
177 return [datetime_to_epoch(self.last_edit_time)]
178
178
179 def get_view(self, *args, **kwargs) -> str:
179 def get_view(self, *args, **kwargs) -> str:
180 """
180 """
181 Renders post's HTML view. Some of the post params can be passed over
181 Renders post's HTML view. Some of the post params can be passed over
182 kwargs for the means of caching (if we view the thread, some params
182 kwargs for the means of caching (if we view the thread, some params
183 are same for every post and don't need to be computed over and over.
183 are same for every post and don't need to be computed over and over.
184 """
184 """
185
185
186 thread = self.get_thread()
186 thread = self.get_thread()
187
187
188 css_classes = [CSS_CLS_POST]
188 css_classes = [CSS_CLS_POST]
189 if thread.is_archived():
189 if thread.is_archived():
190 css_classes.append(CSS_CLS_ARCHIVE_POST)
190 css_classes.append(CSS_CLS_ARCHIVE_POST)
191 elif not thread.can_bump():
191 elif not thread.can_bump():
192 css_classes.append(CSS_CLS_DEAD_POST)
192 css_classes.append(CSS_CLS_DEAD_POST)
193 if self.is_hidden():
193 if self.is_hidden():
194 css_classes.append(CSS_CLS_HIDDEN_POST)
194 css_classes.append(CSS_CLS_HIDDEN_POST)
195 if thread.is_monochrome():
195 if thread.is_monochrome():
196 css_classes.append(CSS_CLS_MONOCHROME)
196 css_classes.append(CSS_CLS_MONOCHROME)
197
197
198 params = dict()
198 params = dict()
199 for param in POST_VIEW_PARAMS:
199 for param in POST_VIEW_PARAMS:
200 if param in kwargs:
200 if param in kwargs:
201 params[param] = kwargs[param]
201 params[param] = kwargs[param]
202
202
203 params.update({
203 params.update({
204 PARAMETER_POST: self,
204 PARAMETER_POST: self,
205 PARAMETER_IS_OPENING: self.is_opening(),
205 PARAMETER_IS_OPENING: self.is_opening(),
206 PARAMETER_THREAD: thread,
206 PARAMETER_THREAD: thread,
207 PARAMETER_CSS_CLASS: ' '.join(css_classes),
207 PARAMETER_CSS_CLASS: ' '.join(css_classes),
208 })
208 })
209
209
210 return render_to_string('boards/post.html', params)
210 return render_to_string('boards/post.html', params)
211
211
212 def get_search_view(self, *args, **kwargs):
212 def get_search_view(self, *args, **kwargs):
213 return self.get_view(need_op_data=True, *args, **kwargs)
213 return self.get_view(need_op_data=True, *args, **kwargs)
214
214
215 def get_first_image(self) -> PostImage:
215 def get_first_image(self) -> PostImage:
216 return self.images.earliest('id')
216 return self.images.earliest('id')
217
217
218 def set_global_id(self, key_pair=None):
218 def set_global_id(self, key_pair=None):
219 """
219 """
220 Sets global id based on the given key pair. If no key pair is given,
220 Sets global id based on the given key pair. If no key pair is given,
221 default one is used.
221 default one is used.
222 """
222 """
223
223
224 if key_pair:
224 if key_pair:
225 key = key_pair
225 key = key_pair
226 else:
226 else:
227 try:
227 try:
228 key = KeyPair.objects.get(primary=True)
228 key = KeyPair.objects.get(primary=True)
229 except KeyPair.DoesNotExist:
229 except KeyPair.DoesNotExist:
230 # Do not update the global id because there is no key defined
230 # Do not update the global id because there is no key defined
231 return
231 return
232 global_id = GlobalId(key_type=key.key_type,
232 global_id = GlobalId(key_type=key.key_type,
233 key=key.public_key,
233 key=key.public_key,
234 local_id=self.id)
234 local_id=self.id)
235 global_id.save()
235 global_id.save()
236
236
237 self.global_id = global_id
237 self.global_id = global_id
238
238
239 self.save(update_fields=['global_id'])
239 self.save(update_fields=['global_id'])
240
240
241 def get_pub_time_str(self):
241 def get_pub_time_str(self):
242 return str(self.pub_time)
242 return str(self.pub_time)
243
243
244 def get_replied_ids(self):
244 def get_replied_ids(self):
245 """
245 """
246 Gets ID list of the posts that this post replies.
246 Gets ID list of the posts that this post replies.
247 """
247 """
248
248
249 raw_text = self.get_raw_text()
249 raw_text = self.get_raw_text()
250
250
251 local_replied = REGEX_REPLY.findall(raw_text)
251 local_replied = REGEX_REPLY.findall(raw_text)
252 global_replied = []
252 global_replied = []
253 for match in REGEX_GLOBAL_REPLY.findall(raw_text):
253 for match in REGEX_GLOBAL_REPLY.findall(raw_text):
254 key_type = match[0]
254 key_type = match[0]
255 key = match[1]
255 key = match[1]
256 local_id = match[2]
256 local_id = match[2]
257
257
258 try:
258 try:
259 global_id = GlobalId.objects.get(key_type=key_type,
259 global_id = GlobalId.objects.get(key_type=key_type,
260 key=key, local_id=local_id)
260 key=key, local_id=local_id)
261 for post in Post.objects.filter(global_id=global_id).only('id'):
261 for post in Post.objects.filter(global_id=global_id).only('id'):
262 global_replied.append(post.id)
262 global_replied.append(post.id)
263 except GlobalId.DoesNotExist:
263 except GlobalId.DoesNotExist:
264 pass
264 pass
265 return local_replied + global_replied
265 return local_replied + global_replied
266
266
267 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
267 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
268 include_last_update=False) -> str:
268 include_last_update=False) -> str:
269 """
269 """
270 Gets post HTML or JSON data that can be rendered on a page or used by
270 Gets post HTML or JSON data that can be rendered on a page or used by
271 API.
271 API.
272 """
272 """
273
273
274 return get_exporter(format_type).export(self, request,
274 return get_exporter(format_type).export(self, request,
275 include_last_update)
275 include_last_update)
276
276
277 def notify_clients(self, recursive=True):
277 def notify_clients(self, recursive=True):
278 """
278 """
279 Sends post HTML data to the thread web socket.
279 Sends post HTML data to the thread web socket.
280 """
280 """
281
281
282 if not settings.get_bool('External', 'WebsocketsEnabled'):
282 if not settings.get_bool('External', 'WebsocketsEnabled'):
283 return
283 return
284
284
285 thread_ids = list()
285 thread_ids = list()
286 for thread in self.get_threads().all():
286 for thread in self.get_threads().all():
287 thread_ids.append(thread.id)
287 thread_ids.append(thread.id)
288
288
289 thread.notify_clients()
289 thread.notify_clients()
290
290
291 if recursive:
291 if recursive:
292 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
292 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
293 post_id = reply_number.group(1)
293 post_id = reply_number.group(1)
294
294
295 try:
295 try:
296 ref_post = Post.objects.get(id=post_id)
296 ref_post = Post.objects.get(id=post_id)
297
297
298 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
298 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
299 # If post is in this thread, its thread was already notified.
299 # If post is in this thread, its thread was already notified.
300 # Otherwise, notify its thread separately.
300 # Otherwise, notify its thread separately.
301 ref_post.notify_clients(recursive=False)
301 ref_post.notify_clients(recursive=False)
302 except ObjectDoesNotExist:
302 except ObjectDoesNotExist:
303 pass
303 pass
304
304
305 def build_url(self):
305 def build_url(self):
306 self.url = self.get_absolute_url()
306 self.url = self.get_absolute_url()
307 self.save(update_fields=['url'])
307 self.save(update_fields=['url'])
308
308
309 def save(self, force_insert=False, force_update=False, using=None,
309 def save(self, force_insert=False, force_update=False, using=None,
310 update_fields=None):
310 update_fields=None):
311 new_post = self.id is None
311 new_post = self.id is None
312
312
313 self.uid = str(uuid.uuid4())
313 self.uid = str(uuid.uuid4())
314 if update_fields is not None and 'uid' not in update_fields:
314 if update_fields is not None and 'uid' not in update_fields:
315 update_fields += ['uid']
315 update_fields += ['uid']
316
316
317 if not new_post:
317 if not new_post:
318 for thread in self.get_threads().all():
318 for thread in self.get_threads().all():
319 thread.last_edit_time = self.last_edit_time
319 thread.last_edit_time = self.last_edit_time
320
320
321 thread.save(update_fields=['last_edit_time', 'status'])
321 thread.save(update_fields=['last_edit_time', 'status'])
322
322
323 super().save(force_insert, force_update, using, update_fields)
323 super().save(force_insert, force_update, using, update_fields)
324
324
325 if self.url is None:
325 if self.url is None:
326 self.build_url()
326 self.build_url()
327
327
328 def get_text(self) -> str:
328 def get_text(self) -> str:
329 return self._text_rendered
329 return self._text_rendered
330
330
331 def get_raw_text(self) -> str:
331 def get_raw_text(self) -> str:
332 return self.text
332 return self.text
333
333
334 def get_sync_text(self) -> str:
334 def get_sync_text(self) -> str:
335 """
335 """
336 Returns text applicable for sync. It has absolute post reflinks.
336 Returns text applicable for sync. It has absolute post reflinks.
337 """
337 """
338
338
339 replacements = dict()
339 replacements = dict()
340 for post_id in REGEX_REPLY.findall(self.get_raw_text()):
340 for post_id in REGEX_REPLY.findall(self.get_raw_text()):
341 try:
341 try:
342 absolute_post_id = str(Post.objects.get(id=post_id).global_id)
342 absolute_post_id = str(Post.objects.get(id=post_id).global_id)
343 replacements[post_id] = absolute_post_id
343 replacements[post_id] = absolute_post_id
344 except Post.DoesNotExist:
344 except Post.DoesNotExist:
345 pass
345 pass
346
346
347 text = self.get_raw_text() or ''
347 text = self.get_raw_text() or ''
348 for key in replacements:
348 for key in replacements:
349 text = text.replace('[post]{}[/post]'.format(key),
349 text = text.replace('[post]{}[/post]'.format(key),
350 '[post]{}[/post]'.format(replacements[key]))
350 '[post]{}[/post]'.format(replacements[key]))
351 text = text.replace('\r\n', '\n').replace('\r', '\n')
351 text = text.replace('\r\n', '\n').replace('\r', '\n')
352
352
353 return text
353 return text
354
354
355 def connect_threads(self, opening_posts):
355 def connect_threads(self, opening_posts):
356 for opening_post in opening_posts:
356 for opening_post in opening_posts:
357 threads = opening_post.get_threads().all()
357 threads = opening_post.get_threads().all()
358 for thread in threads:
358 for thread in threads:
359 if thread.can_bump():
359 if thread.can_bump():
360 thread.update_bump_status()
360 thread.update_bump_status()
361
361
362 thread.last_edit_time = self.last_edit_time
362 thread.last_edit_time = self.last_edit_time
363 thread.save(update_fields=['last_edit_time', 'status'])
363 thread.save(update_fields=['last_edit_time', 'status'])
364 self.threads.add(opening_post.get_thread())
364 self.threads.add(opening_post.get_thread())
365
365
366 def get_tripcode(self):
366 def get_tripcode(self):
367 if self.tripcode:
367 if self.tripcode:
368 return Tripcode(self.tripcode)
368 return Tripcode(self.tripcode)
369
369
370 def get_link_view(self):
370 def get_link_view(self):
371 """
371 """
372 Gets view of a reflink to the post.
372 Gets view of a reflink to the post.
373 """
373 """
374 result = '<a href="{}">&gt;&gt;{}</a>'.format(self.get_absolute_url(),
374 result = '<a href="{}">&gt;&gt;{}</a>'.format(self.get_absolute_url(),
375 self.id)
375 self.id)
376 if self.is_opening():
376 if self.is_opening():
377 result = '<b>{}</b>'.format(result)
377 result = '<b>{}</b>'.format(result)
378
378
379 return result
379 return result
380
380
381 def is_hidden(self) -> bool:
381 def is_hidden(self) -> bool:
382 return self.hidden
382 return self.hidden
383
383
384 def set_hidden(self, hidden):
384 def set_hidden(self, hidden):
385 self.hidden = hidden
385 self.hidden = hidden
386
386
387 def increment_version(self):
387 def increment_version(self):
388 self.version = F('version') + 1
388 self.version = F('version') + 1
389
389
390 def clear_cache(self):
390 def clear_cache(self):
391 """
392 Clears sync data (content cache, signatures etc).
393 """
391 global_id = self.global_id
394 global_id = self.global_id
392 if global_id is not None and global_id.is_local()\
395 if global_id is not None and global_id.is_local()\
393 and global_id.content is not None:
396 and global_id.content is not None:
394 global_id.content = None
397 global_id.clear_cache()
395 global_id.save()
396 global_id.signature_set.all().delete()
@@ -1,161 +1,184 b''
1 import logging
1 import logging
2
2
3 from datetime import datetime, timedelta, date
3 from datetime import datetime, timedelta, date
4 from datetime import time as dtime
4 from datetime import time as dtime
5
5
6 from django.db import models, transaction
6 from django.db import models, transaction
7 from django.utils import timezone
7 from django.utils import timezone
8
8
9 import boards
9 import boards
10
10
11 from boards.models.user import Ban
11 from boards.models.user import Ban
12 from boards.mdx_neboard import Parser
12 from boards.mdx_neboard import Parser
13 from boards.models import PostImage, Attachment
13 from boards.models import PostImage, Attachment
14 from boards import utils
14 from boards import utils
15
15
16 __author__ = 'neko259'
16 __author__ = 'neko259'
17
17
18 IMAGE_TYPES = (
18 IMAGE_TYPES = (
19 'jpeg',
19 'jpeg',
20 'jpg',
20 'jpg',
21 'png',
21 'png',
22 'bmp',
22 'bmp',
23 'gif',
23 'gif',
24 )
24 )
25
25
26 POSTS_PER_DAY_RANGE = 7
26 POSTS_PER_DAY_RANGE = 7
27 NO_IP = '0.0.0.0'
27 NO_IP = '0.0.0.0'
28
28
29
29
30 class PostManager(models.Manager):
30 class PostManager(models.Manager):
31 @transaction.atomic
31 @transaction.atomic
32 def create_post(self, title: str, text: str, file=None, thread=None,
32 def create_post(self, title: str, text: str, file=None, thread=None,
33 ip=NO_IP, tags: list=None, opening_posts: list=None,
33 ip=NO_IP, tags: list=None, opening_posts: list=None,
34 tripcode='', monochrome=False, images=[]):
34 tripcode='', monochrome=False, images=[]):
35 """
35 """
36 Creates new post
36 Creates new post
37 """
37 """
38
38
39 if thread is not None and thread.is_archived():
39 if thread is not None and thread.is_archived():
40 raise Exception('Cannot post into an archived thread')
40 raise Exception('Cannot post into an archived thread')
41
41
42 if not utils.is_anonymous_mode():
42 if not utils.is_anonymous_mode():
43 is_banned = Ban.objects.filter(ip=ip).exists()
43 is_banned = Ban.objects.filter(ip=ip).exists()
44 else:
44 else:
45 is_banned = False
45 is_banned = False
46
46
47 # TODO Raise specific exception and catch it in the views
47 # TODO Raise specific exception and catch it in the views
48 if is_banned:
48 if is_banned:
49 raise Exception("This user is banned")
49 raise Exception("This user is banned")
50
50
51 if not tags:
51 if not tags:
52 tags = []
52 tags = []
53 if not opening_posts:
53 if not opening_posts:
54 opening_posts = []
54 opening_posts = []
55
55
56 posting_time = timezone.now()
56 posting_time = timezone.now()
57 new_thread = False
57 new_thread = False
58 if not thread:
58 if not thread:
59 thread = boards.models.thread.Thread.objects.create(
59 thread = boards.models.thread.Thread.objects.create(
60 bump_time=posting_time, last_edit_time=posting_time,
60 bump_time=posting_time, last_edit_time=posting_time,
61 monochrome=monochrome)
61 monochrome=monochrome)
62 list(map(thread.tags.add, tags))
62 list(map(thread.tags.add, tags))
63 boards.models.thread.Thread.objects.process_oldest_threads()
63 boards.models.thread.Thread.objects.process_oldest_threads()
64 new_thread = True
64 new_thread = True
65
65
66 pre_text = Parser().preparse(text)
66 pre_text = Parser().preparse(text)
67
67
68 post = self.create(title=title,
68 post = self.create(title=title,
69 text=pre_text,
69 text=pre_text,
70 pub_time=posting_time,
70 pub_time=posting_time,
71 poster_ip=ip,
71 poster_ip=ip,
72 thread=thread,
72 thread=thread,
73 last_edit_time=posting_time,
73 last_edit_time=posting_time,
74 tripcode=tripcode,
74 tripcode=tripcode,
75 opening=new_thread)
75 opening=new_thread)
76 post.threads.add(thread)
76 post.threads.add(thread)
77
77
78 logger = logging.getLogger('boards.post.create')
78 logger = logging.getLogger('boards.post.create')
79
79
80 logger.info('Created post [{}] with text [{}] by {}'.format(post,
80 logger.info('Created post [{}] with text [{}] by {}'.format(post,
81 post.get_text(),post.poster_ip))
81 post.get_text(),post.poster_ip))
82
82
83 if file:
83 if file:
84 self._add_file_to_post(file, post)
84 self._add_file_to_post(file, post)
85 for image in images:
85 for image in images:
86 post.images.add(image)
86 post.images.add(image)
87
87
88 post.connect_threads(opening_posts)
88 post.connect_threads(opening_posts)
89 post.set_global_id()
89 post.set_global_id()
90
90
91 # Thread needs to be bumped only when the post is already created
91 # Thread needs to be bumped only when the post is already created
92 if not new_thread:
92 if not new_thread:
93 thread.last_edit_time = posting_time
93 thread.last_edit_time = posting_time
94 thread.bump()
94 thread.bump()
95 thread.save()
95 thread.save()
96
96
97 return post
97 return post
98
98
99 def delete_posts_by_ip(self, ip):
99 def delete_posts_by_ip(self, ip):
100 """
100 """
101 Deletes all posts of the author with same IP
101 Deletes all posts of the author with same IP
102 """
102 """
103
103
104 posts = self.filter(poster_ip=ip)
104 posts = self.filter(poster_ip=ip)
105 for post in posts:
105 for post in posts:
106 post.delete()
106 post.delete()
107
107
108 @utils.cached_result()
108 @utils.cached_result()
109 def get_posts_per_day(self) -> float:
109 def get_posts_per_day(self) -> float:
110 """
110 """
111 Gets average count of posts per day for the last 7 days
111 Gets average count of posts per day for the last 7 days
112 """
112 """
113
113
114 day_end = date.today()
114 day_end = date.today()
115 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
115 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
116
116
117 day_time_start = timezone.make_aware(datetime.combine(
117 day_time_start = timezone.make_aware(datetime.combine(
118 day_start, dtime()), timezone.get_current_timezone())
118 day_start, dtime()), timezone.get_current_timezone())
119 day_time_end = timezone.make_aware(datetime.combine(
119 day_time_end = timezone.make_aware(datetime.combine(
120 day_end, dtime()), timezone.get_current_timezone())
120 day_end, dtime()), timezone.get_current_timezone())
121
121
122 posts_per_period = float(self.filter(
122 posts_per_period = float(self.filter(
123 pub_time__lte=day_time_end,
123 pub_time__lte=day_time_end,
124 pub_time__gte=day_time_start).count())
124 pub_time__gte=day_time_start).count())
125
125
126 ppd = posts_per_period / POSTS_PER_DAY_RANGE
126 ppd = posts_per_period / POSTS_PER_DAY_RANGE
127
127
128 return ppd
128 return ppd
129
129
130 @transaction.atomic
130 @transaction.atomic
131 def import_post(self, title: str, text: str, pub_time: str, global_id,
131 def import_post(self, title: str, text: str, pub_time: str, global_id,
132 opening_post=None, tags=list(), files=list(),
132 opening_post=None, tags=list(), files=list(),
133 tripcode=None):
133 tripcode=None, version=1):
134 is_opening = opening_post is None
134 is_opening = opening_post is None
135 if is_opening:
135 if is_opening:
136 thread = boards.models.thread.Thread.objects.create(
136 thread = boards.models.thread.Thread.objects.create(
137 bump_time=pub_time, last_edit_time=pub_time)
137 bump_time=pub_time, last_edit_time=pub_time)
138 list(map(thread.tags.add, tags))
138 list(map(thread.tags.add, tags))
139 else:
139 else:
140 thread = opening_post.get_thread()
140 thread = opening_post.get_thread()
141
141
142 post = self.create(title=title, text=text,
142 post = self.create(title=title,
143 text=text,
143 pub_time=pub_time,
144 pub_time=pub_time,
144 poster_ip=NO_IP,
145 poster_ip=NO_IP,
145 last_edit_time=pub_time,
146 last_edit_time=pub_time,
146 global_id=global_id,
147 global_id=global_id,
147 opening=is_opening,
148 opening=is_opening,
148 thread=thread, tripcode=tripcode)
149 thread=thread,
150 tripcode=tripcode,
151 version=version)
149
152
150 # TODO Add files
151 for file in files:
153 for file in files:
152 self._add_file_to_post(file, post)
154 self._add_file_to_post(file, post)
153
155
154 post.threads.add(thread)
156 post.threads.add(thread)
155
157
158 @transaction.atomic
159 def update_post(self, post, title: str, text: str, pub_time: str,
160 tags=list(), files=list(), tripcode=None, version=1):
161 post.title = title
162 post.text = text
163 post.pub_time = pub_time
164 post.tripcode = tripcode
165 post.version = version
166 post.save()
167
168 post.clear_cache()
169
170 post.images.clear()
171 post.attachments.clear()
172 for file in files:
173 self._add_file_to_post(file, post)
174
175 thread = post.get_thread()
176 thread.tags.clear()
177 list(map(thread.tags.add, tags))
178
156 def _add_file_to_post(self, file, post):
179 def _add_file_to_post(self, file, post):
157 file_type = file.name.split('.')[-1].lower()
180 file_type = file.name.split('.')[-1].lower()
158 if file_type in IMAGE_TYPES:
181 if file_type in IMAGE_TYPES:
159 post.images.add(PostImage.objects.create_with_hash(file))
182 post.images.add(PostImage.objects.create_with_hash(file))
160 else:
183 else:
161 post.attachments.add(Attachment.objects.create_with_hash(file))
184 post.attachments.add(Attachment.objects.create_with_hash(file))
@@ -1,299 +1,310 b''
1 import xml.etree.ElementTree as et
1 import xml.etree.ElementTree as et
2
2
3 from boards.models.attachment.downloaders import download
3 from boards.models.attachment.downloaders import download
4 from boards.utils import get_file_mimetype, get_file_hash
4 from boards.utils import get_file_mimetype, get_file_hash
5 from django.db import transaction
5 from django.db import transaction
6 from boards.models import KeyPair, GlobalId, Signature, Post, Tag
6 from boards.models import KeyPair, GlobalId, Signature, Post, Tag
7
7
8 EXCEPTION_NODE = 'Sync node returned an error: {}.'
8 EXCEPTION_NODE = 'Sync node returned an error: {}.'
9 EXCEPTION_OP = 'Load the OP first.'
9 EXCEPTION_OP = 'Load the OP first.'
10 EXCEPTION_DOWNLOAD = 'File was not downloaded.'
10 EXCEPTION_DOWNLOAD = 'File was not downloaded.'
11 EXCEPTION_HASH = 'File hash does not match attachment hash.'
11 EXCEPTION_HASH = 'File hash does not match attachment hash.'
12 EXCEPTION_SIGNATURE = 'Invalid model signature for {}.'
12 EXCEPTION_SIGNATURE = 'Invalid model signature for {}.'
13 EXCEPTION_AUTHOR_SIGNATURE = 'Model {} has no author signature.'
13 EXCEPTION_AUTHOR_SIGNATURE = 'Model {} has no author signature.'
14 ENCODING_UNICODE = 'unicode'
14 ENCODING_UNICODE = 'unicode'
15
15
16 TAG_MODEL = 'model'
16 TAG_MODEL = 'model'
17 TAG_REQUEST = 'request'
17 TAG_REQUEST = 'request'
18 TAG_RESPONSE = 'response'
18 TAG_RESPONSE = 'response'
19 TAG_ID = 'id'
19 TAG_ID = 'id'
20 TAG_STATUS = 'status'
20 TAG_STATUS = 'status'
21 TAG_MODELS = 'models'
21 TAG_MODELS = 'models'
22 TAG_TITLE = 'title'
22 TAG_TITLE = 'title'
23 TAG_TEXT = 'text'
23 TAG_TEXT = 'text'
24 TAG_THREAD = 'thread'
24 TAG_THREAD = 'thread'
25 TAG_PUB_TIME = 'pub-time'
25 TAG_PUB_TIME = 'pub-time'
26 TAG_SIGNATURES = 'signatures'
26 TAG_SIGNATURES = 'signatures'
27 TAG_SIGNATURE = 'signature'
27 TAG_SIGNATURE = 'signature'
28 TAG_CONTENT = 'content'
28 TAG_CONTENT = 'content'
29 TAG_ATTACHMENTS = 'attachments'
29 TAG_ATTACHMENTS = 'attachments'
30 TAG_ATTACHMENT = 'attachment'
30 TAG_ATTACHMENT = 'attachment'
31 TAG_TAGS = 'tags'
31 TAG_TAGS = 'tags'
32 TAG_TAG = 'tag'
32 TAG_TAG = 'tag'
33 TAG_ATTACHMENT_REFS = 'attachment-refs'
33 TAG_ATTACHMENT_REFS = 'attachment-refs'
34 TAG_ATTACHMENT_REF = 'attachment-ref'
34 TAG_ATTACHMENT_REF = 'attachment-ref'
35 TAG_TRIPCODE = 'tripcode'
35 TAG_TRIPCODE = 'tripcode'
36 TAG_VERSION = 'version'
36 TAG_VERSION = 'version'
37
37
38 TYPE_GET = 'get'
38 TYPE_GET = 'get'
39
39
40 ATTR_VERSION = 'version'
40 ATTR_VERSION = 'version'
41 ATTR_TYPE = 'type'
41 ATTR_TYPE = 'type'
42 ATTR_NAME = 'name'
42 ATTR_NAME = 'name'
43 ATTR_VALUE = 'value'
43 ATTR_VALUE = 'value'
44 ATTR_MIMETYPE = 'mimetype'
44 ATTR_MIMETYPE = 'mimetype'
45 ATTR_KEY = 'key'
45 ATTR_KEY = 'key'
46 ATTR_REF = 'ref'
46 ATTR_REF = 'ref'
47 ATTR_URL = 'url'
47 ATTR_URL = 'url'
48 ATTR_ID_TYPE = 'id-type'
48 ATTR_ID_TYPE = 'id-type'
49
49
50 ID_TYPE_MD5 = 'md5'
50 ID_TYPE_MD5 = 'md5'
51
51
52 STATUS_SUCCESS = 'success'
52 STATUS_SUCCESS = 'success'
53
53
54
54
55 class SyncException(Exception):
55 class SyncException(Exception):
56 pass
56 pass
57
57
58
58
59 class SyncManager:
59 class SyncManager:
60 @staticmethod
60 @staticmethod
61 def generate_response_get(model_list: list):
61 def generate_response_get(model_list: list):
62 response = et.Element(TAG_RESPONSE)
62 response = et.Element(TAG_RESPONSE)
63
63
64 status = et.SubElement(response, TAG_STATUS)
64 status = et.SubElement(response, TAG_STATUS)
65 status.text = STATUS_SUCCESS
65 status.text = STATUS_SUCCESS
66
66
67 models = et.SubElement(response, TAG_MODELS)
67 models = et.SubElement(response, TAG_MODELS)
68
68
69 for post in model_list:
69 for post in model_list:
70 model = et.SubElement(models, TAG_MODEL)
70 model = et.SubElement(models, TAG_MODEL)
71 model.set(ATTR_NAME, 'post')
71 model.set(ATTR_NAME, 'post')
72
72
73 global_id = post.global_id
73 global_id = post.global_id
74
74
75 images = post.images.all()
75 images = post.images.all()
76 attachments = post.attachments.all()
76 attachments = post.attachments.all()
77 if global_id.content:
77 if global_id.content:
78 model.append(et.fromstring(global_id.content))
78 model.append(et.fromstring(global_id.content))
79 if len(images) > 0 or len(attachments) > 0:
79 if len(images) > 0 or len(attachments) > 0:
80 attachment_refs = et.SubElement(model, TAG_ATTACHMENT_REFS)
80 attachment_refs = et.SubElement(model, TAG_ATTACHMENT_REFS)
81 for image in images:
81 for image in images:
82 SyncManager._attachment_to_xml(
82 SyncManager._attachment_to_xml(
83 None, attachment_refs, image.image.file,
83 None, attachment_refs, image.image.file,
84 image.hash, image.image.url)
84 image.hash, image.image.url)
85 for file in attachments:
85 for file in attachments:
86 SyncManager._attachment_to_xml(
86 SyncManager._attachment_to_xml(
87 None, attachment_refs, file.file.file,
87 None, attachment_refs, file.file.file,
88 file.hash, file.file.url)
88 file.hash, file.file.url)
89 else:
89 else:
90 content_tag = et.SubElement(model, TAG_CONTENT)
90 content_tag = et.SubElement(model, TAG_CONTENT)
91
91
92 tag_id = et.SubElement(content_tag, TAG_ID)
92 tag_id = et.SubElement(content_tag, TAG_ID)
93 global_id.to_xml_element(tag_id)
93 global_id.to_xml_element(tag_id)
94
94
95 title = et.SubElement(content_tag, TAG_TITLE)
95 title = et.SubElement(content_tag, TAG_TITLE)
96 title.text = post.title
96 title.text = post.title
97
97
98 text = et.SubElement(content_tag, TAG_TEXT)
98 text = et.SubElement(content_tag, TAG_TEXT)
99 text.text = post.get_sync_text()
99 text.text = post.get_sync_text()
100
100
101 thread = post.get_thread()
101 thread = post.get_thread()
102 if post.is_opening():
102 if post.is_opening():
103 tag_tags = et.SubElement(content_tag, TAG_TAGS)
103 tag_tags = et.SubElement(content_tag, TAG_TAGS)
104 for tag in thread.get_tags():
104 for tag in thread.get_tags():
105 tag_tag = et.SubElement(tag_tags, TAG_TAG)
105 tag_tag = et.SubElement(tag_tags, TAG_TAG)
106 tag_tag.text = tag.name
106 tag_tag.text = tag.name
107 else:
107 else:
108 tag_thread = et.SubElement(content_tag, TAG_THREAD)
108 tag_thread = et.SubElement(content_tag, TAG_THREAD)
109 thread_id = et.SubElement(tag_thread, TAG_ID)
109 thread_id = et.SubElement(tag_thread, TAG_ID)
110 thread.get_opening_post().global_id.to_xml_element(thread_id)
110 thread.get_opening_post().global_id.to_xml_element(thread_id)
111
111
112 pub_time = et.SubElement(content_tag, TAG_PUB_TIME)
112 pub_time = et.SubElement(content_tag, TAG_PUB_TIME)
113 pub_time.text = str(post.get_pub_time_str())
113 pub_time.text = str(post.get_pub_time_str())
114
114
115 if post.tripcode:
115 if post.tripcode:
116 tripcode = et.SubElement(content_tag, TAG_TRIPCODE)
116 tripcode = et.SubElement(content_tag, TAG_TRIPCODE)
117 tripcode.text = post.tripcode
117 tripcode.text = post.tripcode
118
118
119 if len(images) > 0 or len(attachments) > 0:
119 if len(images) > 0 or len(attachments) > 0:
120 attachments_tag = et.SubElement(content_tag, TAG_ATTACHMENTS)
120 attachments_tag = et.SubElement(content_tag, TAG_ATTACHMENTS)
121 attachment_refs = et.SubElement(model, TAG_ATTACHMENT_REFS)
121 attachment_refs = et.SubElement(model, TAG_ATTACHMENT_REFS)
122
122
123 for image in images:
123 for image in images:
124 SyncManager._attachment_to_xml(
124 SyncManager._attachment_to_xml(
125 attachments_tag, attachment_refs, image.image.file,
125 attachments_tag, attachment_refs, image.image.file,
126 image.hash, image.image.url)
126 image.hash, image.image.url)
127 for file in attachments:
127 for file in attachments:
128 SyncManager._attachment_to_xml(
128 SyncManager._attachment_to_xml(
129 attachments_tag, attachment_refs, file.file.file,
129 attachments_tag, attachment_refs, file.file.file,
130 file.hash, file.file.url)
130 file.hash, file.file.url)
131 version_tag = et.SubElement(content_tag, TAG_VERSION)
131 version_tag = et.SubElement(content_tag, TAG_VERSION)
132 version_tag.text = str(post.version)
132 version_tag.text = str(post.version)
133
133
134 global_id.content = et.tostring(content_tag, ENCODING_UNICODE)
134 global_id.content = et.tostring(content_tag, ENCODING_UNICODE)
135 global_id.save()
135 global_id.save()
136
136
137 signatures_tag = et.SubElement(model, TAG_SIGNATURES)
137 signatures_tag = et.SubElement(model, TAG_SIGNATURES)
138 post_signatures = global_id.signature_set.all()
138 post_signatures = global_id.signature_set.all()
139 if post_signatures:
139 if post_signatures:
140 signatures = post_signatures
140 signatures = post_signatures
141 else:
141 else:
142 key = KeyPair.objects.get(public_key=global_id.key)
142 key = KeyPair.objects.get(public_key=global_id.key)
143 signature = Signature(
143 signature = Signature(
144 key_type=key.key_type,
144 key_type=key.key_type,
145 key=key.public_key,
145 key=key.public_key,
146 signature=key.sign(global_id.content),
146 signature=key.sign(global_id.content),
147 global_id=global_id,
147 global_id=global_id,
148 )
148 )
149 signature.save()
149 signature.save()
150 signatures = [signature]
150 signatures = [signature]
151 for signature in signatures:
151 for signature in signatures:
152 signature_tag = et.SubElement(signatures_tag, TAG_SIGNATURE)
152 signature_tag = et.SubElement(signatures_tag, TAG_SIGNATURE)
153 signature_tag.set(ATTR_TYPE, signature.key_type)
153 signature_tag.set(ATTR_TYPE, signature.key_type)
154 signature_tag.set(ATTR_VALUE, signature.signature)
154 signature_tag.set(ATTR_VALUE, signature.signature)
155 signature_tag.set(ATTR_KEY, signature.key)
155 signature_tag.set(ATTR_KEY, signature.key)
156
156
157 return et.tostring(response, ENCODING_UNICODE)
157 return et.tostring(response, ENCODING_UNICODE)
158
158
159 @staticmethod
159 @staticmethod
160 @transaction.atomic
160 @transaction.atomic
161 def parse_response_get(response_xml, hostname):
161 def parse_response_get(response_xml, hostname):
162 tag_root = et.fromstring(response_xml)
162 tag_root = et.fromstring(response_xml)
163 tag_status = tag_root.find(TAG_STATUS)
163 tag_status = tag_root.find(TAG_STATUS)
164 if STATUS_SUCCESS == tag_status.text:
164 if STATUS_SUCCESS == tag_status.text:
165 tag_models = tag_root.find(TAG_MODELS)
165 tag_models = tag_root.find(TAG_MODELS)
166 for tag_model in tag_models:
166 for tag_model in tag_models:
167 tag_content = tag_model.find(TAG_CONTENT)
167 tag_content = tag_model.find(TAG_CONTENT)
168
168
169 content_str = et.tostring(tag_content, ENCODING_UNICODE)
169 content_str = et.tostring(tag_content, ENCODING_UNICODE)
170
170
171 tag_id = tag_content.find(TAG_ID)
171 tag_id = tag_content.find(TAG_ID)
172 global_id, exists = GlobalId.from_xml_element(tag_id)
172 global_id, exists = GlobalId.from_xml_element(tag_id)
173 signatures = SyncManager._verify_model(global_id, content_str, tag_model)
173 signatures = SyncManager._verify_model(global_id, content_str, tag_model)
174
174
175 if exists:
175 version = int(tag_content.find(TAG_VERSION).text)
176 print('Post with same ID already exists')
176 is_old = exists and global_id.post.version < version
177 if exists and not is_old:
178 print('Post with same ID exists and is up to date.')
177 else:
179 else:
178 global_id.content = content_str
180 global_id.content = content_str
179 global_id.save()
181 global_id.save()
180 for signature in signatures:
182 for signature in signatures:
181 signature.global_id = global_id
183 signature.global_id = global_id
182 signature.save()
184 signature.save()
183
185
184 title = tag_content.find(TAG_TITLE).text or ''
186 title = tag_content.find(TAG_TITLE).text or ''
185 text = tag_content.find(TAG_TEXT).text or ''
187 text = tag_content.find(TAG_TEXT).text or ''
186 pub_time = tag_content.find(TAG_PUB_TIME).text
188 pub_time = tag_content.find(TAG_PUB_TIME).text
187 tripcode_tag = tag_content.find(TAG_TRIPCODE)
189 tripcode_tag = tag_content.find(TAG_TRIPCODE)
188 if tripcode_tag is not None:
190 if tripcode_tag is not None:
189 tripcode = tripcode_tag.text or ''
191 tripcode = tripcode_tag.text or ''
190 else:
192 else:
191 tripcode = ''
193 tripcode = ''
192
194
193 thread = tag_content.find(TAG_THREAD)
195 thread = tag_content.find(TAG_THREAD)
194 tags = []
196 tags = []
195 if thread:
197 if thread:
196 thread_id = thread.find(TAG_ID)
198 thread_id = thread.find(TAG_ID)
197 op_global_id, exists = GlobalId.from_xml_element(thread_id)
199 op_global_id, exists = GlobalId.from_xml_element(thread_id)
198 if exists:
200 if exists:
199 opening_post = Post.objects.get(global_id=op_global_id)
201 opening_post = Post.objects.get(global_id=op_global_id)
200 else:
202 else:
201 raise SyncException(EXCEPTION_OP)
203 raise SyncException(EXCEPTION_OP)
202 else:
204 else:
203 opening_post = None
205 opening_post = None
204 tag_tags = tag_content.find(TAG_TAGS)
206 tag_tags = tag_content.find(TAG_TAGS)
205 for tag_tag in tag_tags:
207 for tag_tag in tag_tags:
206 tag, created = Tag.objects.get_or_create(
208 tag, created = Tag.objects.get_or_create(
207 name=tag_tag.text)
209 name=tag_tag.text)
208 tags.append(tag)
210 tags.append(tag)
209
211
210 # TODO Check that the replied posts are already present
212 # TODO Check that the replied posts are already present
211 # before adding new ones
213 # before adding new ones
212
214
213 files = []
215 files = []
214 tag_attachments = tag_content.find(TAG_ATTACHMENTS) or list()
216 tag_attachments = tag_content.find(TAG_ATTACHMENTS) or list()
215 tag_refs = tag_model.find(TAG_ATTACHMENT_REFS)
217 tag_refs = tag_model.find(TAG_ATTACHMENT_REFS)
216 for attachment in tag_attachments:
218 for attachment in tag_attachments:
217 tag_ref = tag_refs.find("{}[@ref='{}']".format(
219 tag_ref = tag_refs.find("{}[@ref='{}']".format(
218 TAG_ATTACHMENT_REF, attachment.text))
220 TAG_ATTACHMENT_REF, attachment.text))
219 url = tag_ref.get(ATTR_URL)
221 url = tag_ref.get(ATTR_URL)
220 attached_file = download(hostname + url)
222 attached_file = download(hostname + url)
221 if attached_file is None:
223 if attached_file is None:
222 raise SyncException(EXCEPTION_DOWNLOAD)
224 raise SyncException(EXCEPTION_DOWNLOAD)
223
225
224 hash = get_file_hash(attached_file)
226 hash = get_file_hash(attached_file)
225 if hash != attachment.text:
227 if hash != attachment.text:
226 raise SyncException(EXCEPTION_HASH)
228 raise SyncException(EXCEPTION_HASH)
227
229
228 files.append(attached_file)
230 files.append(attached_file)
229
231
232 if is_old:
233 post = global_id.post
234 Post.objects.update_post(
235 post, title=title, text=text, pub_time=pub_time,
236 tags=tags, files=files, tripcode=tripcode,
237 version=version)
238 print('Parsed updated post {}'.format(global_id))
239 else:
230 Post.objects.import_post(
240 Post.objects.import_post(
231 title=title, text=text, pub_time=pub_time,
241 title=title, text=text, pub_time=pub_time,
232 opening_post=opening_post, tags=tags,
242 opening_post=opening_post, tags=tags,
233 global_id=global_id, files=files, tripcode=tripcode)
243 global_id=global_id, files=files, tripcode=tripcode,
234 print('Parsed post {}'.format(global_id))
244 version=version)
245 print('Parsed new post {}'.format(global_id))
235 else:
246 else:
236 raise SyncException(EXCEPTION_NODE.format(tag_status.text))
247 raise SyncException(EXCEPTION_NODE.format(tag_status.text))
237
248
238 @staticmethod
249 @staticmethod
239 def generate_response_list():
250 def generate_response_list():
240 response = et.Element(TAG_RESPONSE)
251 response = et.Element(TAG_RESPONSE)
241
252
242 status = et.SubElement(response, TAG_STATUS)
253 status = et.SubElement(response, TAG_STATUS)
243 status.text = STATUS_SUCCESS
254 status.text = STATUS_SUCCESS
244
255
245 models = et.SubElement(response, TAG_MODELS)
256 models = et.SubElement(response, TAG_MODELS)
246
257
247 for post in Post.objects.prefetch_related('global_id').all():
258 for post in Post.objects.prefetch_related('global_id').all():
248 tag_model = et.SubElement(models, TAG_MODEL)
259 tag_model = et.SubElement(models, TAG_MODEL)
249 tag_id = et.SubElement(tag_model, TAG_ID)
260 tag_id = et.SubElement(tag_model, TAG_ID)
250 post.global_id.to_xml_element(tag_id)
261 post.global_id.to_xml_element(tag_id)
251 tag_version = et.SubElement(tag_model, TAG_VERSION)
262 tag_version = et.SubElement(tag_model, TAG_VERSION)
252 tag_version.text = str(post.version)
263 tag_version.text = str(post.version)
253
264
254 return et.tostring(response, ENCODING_UNICODE)
265 return et.tostring(response, ENCODING_UNICODE)
255
266
256 @staticmethod
267 @staticmethod
257 def _verify_model(global_id, content_str, tag_model):
268 def _verify_model(global_id, content_str, tag_model):
258 """
269 """
259 Verifies all signatures for a single model.
270 Verifies all signatures for a single model.
260 """
271 """
261
272
262 signatures = []
273 signatures = []
263
274
264 tag_signatures = tag_model.find(TAG_SIGNATURES)
275 tag_signatures = tag_model.find(TAG_SIGNATURES)
265 has_author_signature = False
276 has_author_signature = False
266 for tag_signature in tag_signatures:
277 for tag_signature in tag_signatures:
267 signature_type = tag_signature.get(ATTR_TYPE)
278 signature_type = tag_signature.get(ATTR_TYPE)
268 signature_value = tag_signature.get(ATTR_VALUE)
279 signature_value = tag_signature.get(ATTR_VALUE)
269 signature_key = tag_signature.get(ATTR_KEY)
280 signature_key = tag_signature.get(ATTR_KEY)
270
281
271 if global_id.key_type == signature_type and\
282 if global_id.key_type == signature_type and\
272 global_id.key == signature_key:
283 global_id.key == signature_key:
273 has_author_signature = True
284 has_author_signature = True
274
285
275 signature = Signature(key_type=signature_type,
286 signature = Signature(key_type=signature_type,
276 key=signature_key,
287 key=signature_key,
277 signature=signature_value)
288 signature=signature_value)
278
289
279 if not KeyPair.objects.verify(signature, content_str):
290 if not KeyPair.objects.verify(signature, content_str):
280 raise SyncException(EXCEPTION_SIGNATURE.format(content_str))
291 raise SyncException(EXCEPTION_SIGNATURE.format(content_str))
281
292
282 signatures.append(signature)
293 signatures.append(signature)
283 if not has_author_signature:
294 if not has_author_signature:
284 raise SyncException(EXCEPTION_AUTHOR_SIGNATURE.format(content_str))
295 raise SyncException(EXCEPTION_AUTHOR_SIGNATURE.format(content_str))
285
296
286 return signatures
297 return signatures
287
298
288 @staticmethod
299 @staticmethod
289 def _attachment_to_xml(tag_attachments, tag_refs, file, hash, url):
300 def _attachment_to_xml(tag_attachments, tag_refs, file, hash, url):
290 if tag_attachments is not None:
301 if tag_attachments is not None:
291 mimetype = get_file_mimetype(file)
302 mimetype = get_file_mimetype(file)
292 attachment = et.SubElement(tag_attachments, TAG_ATTACHMENT)
303 attachment = et.SubElement(tag_attachments, TAG_ATTACHMENT)
293 attachment.set(ATTR_MIMETYPE, mimetype)
304 attachment.set(ATTR_MIMETYPE, mimetype)
294 attachment.set(ATTR_ID_TYPE, ID_TYPE_MD5)
305 attachment.set(ATTR_ID_TYPE, ID_TYPE_MD5)
295 attachment.text = hash
306 attachment.text = hash
296
307
297 attachment_ref = et.SubElement(tag_refs, TAG_ATTACHMENT_REF)
308 attachment_ref = et.SubElement(tag_refs, TAG_ATTACHMENT_REF)
298 attachment_ref.set(ATTR_REF, hash)
309 attachment_ref.set(ATTR_REF, hash)
299 attachment_ref.set(ATTR_URL, url)
310 attachment_ref.set(ATTR_URL, url)
@@ -1,150 +1,158 b''
1 import xml.etree.ElementTree as et
1 import xml.etree.ElementTree as et
2 from django.db import models
2 from django.db import models
3 from boards.models import KeyPair
3 from boards.models import KeyPair
4
4
5
5
6 TAG_MODEL = 'model'
6 TAG_MODEL = 'model'
7 TAG_REQUEST = 'request'
7 TAG_REQUEST = 'request'
8 TAG_ID = 'id'
8 TAG_ID = 'id'
9
9
10 TYPE_GET = 'get'
10 TYPE_GET = 'get'
11 TYPE_LIST = 'list'
11 TYPE_LIST = 'list'
12
12
13 ATTR_VERSION = 'version'
13 ATTR_VERSION = 'version'
14 ATTR_TYPE = 'type'
14 ATTR_TYPE = 'type'
15 ATTR_NAME = 'name'
15 ATTR_NAME = 'name'
16
16
17 ATTR_KEY = 'key'
17 ATTR_KEY = 'key'
18 ATTR_KEY_TYPE = 'type'
18 ATTR_KEY_TYPE = 'type'
19 ATTR_LOCAL_ID = 'local-id'
19 ATTR_LOCAL_ID = 'local-id'
20
20
21
21
22 class GlobalIdManager(models.Manager):
22 class GlobalIdManager(models.Manager):
23 def generate_request_get(self, global_id_list: list):
23 def generate_request_get(self, global_id_list: list):
24 """
24 """
25 Form a get request from a list of ModelId objects.
25 Form a get request from a list of ModelId objects.
26 """
26 """
27
27
28 request = et.Element(TAG_REQUEST)
28 request = et.Element(TAG_REQUEST)
29 request.set(ATTR_TYPE, TYPE_GET)
29 request.set(ATTR_TYPE, TYPE_GET)
30 request.set(ATTR_VERSION, '1.0')
30 request.set(ATTR_VERSION, '1.0')
31
31
32 model = et.SubElement(request, TAG_MODEL)
32 model = et.SubElement(request, TAG_MODEL)
33 model.set(ATTR_VERSION, '1.0')
33 model.set(ATTR_VERSION, '1.0')
34 model.set(ATTR_NAME, 'post')
34 model.set(ATTR_NAME, 'post')
35
35
36 for global_id in global_id_list:
36 for global_id in global_id_list:
37 tag_id = et.SubElement(model, TAG_ID)
37 tag_id = et.SubElement(model, TAG_ID)
38 global_id.to_xml_element(tag_id)
38 global_id.to_xml_element(tag_id)
39
39
40 return et.tostring(request, 'unicode')
40 return et.tostring(request, 'unicode')
41
41
42 def generate_request_list(self):
42 def generate_request_list(self):
43 """
43 """
44 Form a pull request from a list of ModelId objects.
44 Form a pull request from a list of ModelId objects.
45 """
45 """
46
46
47 request = et.Element(TAG_REQUEST)
47 request = et.Element(TAG_REQUEST)
48 request.set(ATTR_TYPE, TYPE_LIST)
48 request.set(ATTR_TYPE, TYPE_LIST)
49 request.set(ATTR_VERSION, '1.0')
49 request.set(ATTR_VERSION, '1.0')
50
50
51 model = et.SubElement(request, TAG_MODEL)
51 model = et.SubElement(request, TAG_MODEL)
52 model.set(ATTR_VERSION, '1.0')
52 model.set(ATTR_VERSION, '1.0')
53 model.set(ATTR_NAME, 'post')
53 model.set(ATTR_NAME, 'post')
54
54
55 return et.tostring(request, 'unicode')
55 return et.tostring(request, 'unicode')
56
56
57 def global_id_exists(self, global_id):
57 def global_id_exists(self, global_id):
58 """
58 """
59 Checks if the same global id already exists in the system.
59 Checks if the same global id already exists in the system.
60 """
60 """
61
61
62 return self.filter(key=global_id.key,
62 return self.filter(key=global_id.key,
63 key_type=global_id.key_type,
63 key_type=global_id.key_type,
64 local_id=global_id.local_id).exists()
64 local_id=global_id.local_id).exists()
65
65
66
66
67 class GlobalId(models.Model):
67 class GlobalId(models.Model):
68 """
68 """
69 Global model ID and cache.
69 Global model ID and cache.
70 Key, key type and local ID make a single global identificator of the model.
70 Key, key type and local ID make a single global identificator of the model.
71 Content is an XML cache of the model that can be passed along between nodes
71 Content is an XML cache of the model that can be passed along between nodes
72 without manual serialization each time.
72 without manual serialization each time.
73 """
73 """
74 class Meta:
74 class Meta:
75 app_label = 'boards'
75 app_label = 'boards'
76
76
77 objects = GlobalIdManager()
77 objects = GlobalIdManager()
78
78
79 def __init__(self, *args, **kwargs):
79 def __init__(self, *args, **kwargs):
80 models.Model.__init__(self, *args, **kwargs)
80 models.Model.__init__(self, *args, **kwargs)
81
81
82 if 'key' in kwargs and 'key_type' in kwargs and 'local_id' in kwargs:
82 if 'key' in kwargs and 'key_type' in kwargs and 'local_id' in kwargs:
83 self.key = kwargs['key']
83 self.key = kwargs['key']
84 self.key_type = kwargs['key_type']
84 self.key_type = kwargs['key_type']
85 self.local_id = kwargs['local_id']
85 self.local_id = kwargs['local_id']
86
86
87 key = models.TextField()
87 key = models.TextField()
88 key_type = models.TextField()
88 key_type = models.TextField()
89 local_id = models.IntegerField()
89 local_id = models.IntegerField()
90 content = models.TextField(blank=True, null=True)
90 content = models.TextField(blank=True, null=True)
91
91
92 def __str__(self):
92 def __str__(self):
93 return '%s::%s::%d' % (self.key_type, self.key, self.local_id)
93 return '%s::%s::%d' % (self.key_type, self.key, self.local_id)
94
94
95 def to_xml_element(self, element: et.Element):
95 def to_xml_element(self, element: et.Element):
96 """
96 """
97 Exports global id to an XML element.
97 Exports global id to an XML element.
98 """
98 """
99
99
100 element.set(ATTR_KEY, self.key)
100 element.set(ATTR_KEY, self.key)
101 element.set(ATTR_KEY_TYPE, self.key_type)
101 element.set(ATTR_KEY_TYPE, self.key_type)
102 element.set(ATTR_LOCAL_ID, str(self.local_id))
102 element.set(ATTR_LOCAL_ID, str(self.local_id))
103
103
104 @staticmethod
104 @staticmethod
105 def from_xml_element(element: et.Element):
105 def from_xml_element(element: et.Element):
106 """
106 """
107 Parses XML id tag and gets global id from it.
107 Parses XML id tag and gets global id from it.
108
108
109 Arguments:
109 Arguments:
110 element -- the XML 'id' element
110 element -- the XML 'id' element
111
111
112 Returns:
112 Returns:
113 global_id -- id itself
113 global_id -- id itself
114 exists -- True if the global id was taken from database, False if it
114 exists -- True if the global id was taken from database, False if it
115 did not exist and was created.
115 did not exist and was created.
116 """
116 """
117
117
118 try:
118 try:
119 return GlobalId.objects.get(key=element.get(ATTR_KEY),
119 return GlobalId.objects.get(key=element.get(ATTR_KEY),
120 key_type=element.get(ATTR_KEY_TYPE),
120 key_type=element.get(ATTR_KEY_TYPE),
121 local_id=int(element.get(
121 local_id=int(element.get(
122 ATTR_LOCAL_ID))), True
122 ATTR_LOCAL_ID))), True
123 except GlobalId.DoesNotExist:
123 except GlobalId.DoesNotExist:
124 return GlobalId(key=element.get(ATTR_KEY),
124 return GlobalId(key=element.get(ATTR_KEY),
125 key_type=element.get(ATTR_KEY_TYPE),
125 key_type=element.get(ATTR_KEY_TYPE),
126 local_id=int(element.get(ATTR_LOCAL_ID))), False
126 local_id=int(element.get(ATTR_LOCAL_ID))), False
127
127
128 def is_local(self):
128 def is_local(self):
129 """Checks fo the ID is local model's"""
129 """Checks fo the ID is local model's"""
130 return KeyPair.objects.filter(
130 return KeyPair.objects.filter(
131 key_type=self.key_type, public_key=self.key).exists()
131 key_type=self.key_type, public_key=self.key).exists()
132
132
133 def clear_cache(self):
134 """
135 Removes content cache and signatures.
136 """
137 self.content = None
138 self.save()
139 self.signature_set.all().delete()
140
133
141
134 class Signature(models.Model):
142 class Signature(models.Model):
135 class Meta:
143 class Meta:
136 app_label = 'boards'
144 app_label = 'boards'
137
145
138 def __init__(self, *args, **kwargs):
146 def __init__(self, *args, **kwargs):
139 models.Model.__init__(self, *args, **kwargs)
147 models.Model.__init__(self, *args, **kwargs)
140
148
141 if 'key' in kwargs and 'key_type' in kwargs and 'signature' in kwargs:
149 if 'key' in kwargs and 'key_type' in kwargs and 'signature' in kwargs:
142 self.key_type = kwargs['key_type']
150 self.key_type = kwargs['key_type']
143 self.key = kwargs['key']
151 self.key = kwargs['key']
144 self.signature = kwargs['signature']
152 self.signature = kwargs['signature']
145
153
146 key_type = models.TextField()
154 key_type = models.TextField()
147 key = models.TextField()
155 key = models.TextField()
148 signature = models.TextField()
156 signature = models.TextField()
149
157
150 global_id = models.ForeignKey('GlobalId')
158 global_id = models.ForeignKey('GlobalId')
@@ -1,106 +1,105 b''
1 from django.test import TestCase
2
1 from boards.models import KeyPair, Post, Tag
3 from boards.models import KeyPair, Post, Tag
2 from boards.models.post.sync import SyncManager
4 from boards.models.post.sync import SyncManager
3 from boards.tests.mocks import MockRequest
5 from boards.tests.mocks import MockRequest
4 from boards.views.sync import response_get
6 from boards.views.sync import response_get
5
7
6 __author__ = 'neko259'
8 __author__ = 'neko259'
7
9
8
10
9 from django.test import TestCase
10
11
12 class SyncTest(TestCase):
11 class SyncTest(TestCase):
13 def test_get(self):
12 def test_get(self):
14 """
13 """
15 Forms a GET request of a post and checks the response.
14 Forms a GET request of a post and checks the response.
16 """
15 """
17
16
18 key = KeyPair.objects.generate_key(primary=True)
17 key = KeyPair.objects.generate_key(primary=True)
19 tag = Tag.objects.create(name='tag1')
18 tag = Tag.objects.create(name='tag1')
20 post = Post.objects.create_post(title='test_title',
19 post = Post.objects.create_post(title='test_title',
21 text='test_text\rline two',
20 text='test_text\rline two',
22 tags=[tag])
21 tags=[tag])
23
22
24 request = MockRequest()
23 request = MockRequest()
25 request.body = (
24 request.body = (
26 '<request type="get" version="1.0">'
25 '<request type="get" version="1.0">'
27 '<model name="post" version="1.0">'
26 '<model name="post" version="1.0">'
28 '<id key="%s" local-id="%d" type="%s" />'
27 '<id key="%s" local-id="%d" type="%s" />'
29 '</model>'
28 '</model>'
30 '</request>' % (post.global_id.key,
29 '</request>' % (post.global_id.key,
31 post.id,
30 post.id,
32 post.global_id.key_type)
31 post.global_id.key_type)
33 )
32 )
34
33
35 response = response_get(request).content.decode()
34 response = response_get(request).content.decode()
36 self.assertTrue(
35 self.assertTrue(
37 '<status>success</status>'
36 '<status>success</status>'
38 '<models>'
37 '<models>'
39 '<model name="post">'
38 '<model name="post">'
40 '<content>'
39 '<content>'
41 '<id key="%s" local-id="%d" type="%s" />'
40 '<id key="%s" local-id="%d" type="%s" />'
42 '<title>%s</title>'
41 '<title>%s</title>'
43 '<text>%s</text>'
42 '<text>%s</text>'
44 '<tags><tag>%s</tag></tags>'
43 '<tags><tag>%s</tag></tags>'
45 '<pub-time>%s</pub-time>'
44 '<pub-time>%s</pub-time>'
46 '<version>%s</version>'
45 '<version>%s</version>'
47 '</content>' % (
46 '</content>' % (
48 post.global_id.key,
47 post.global_id.key,
49 post.global_id.local_id,
48 post.global_id.local_id,
50 post.global_id.key_type,
49 post.global_id.key_type,
51 post.title,
50 post.title,
52 post.get_sync_text(),
51 post.get_sync_text(),
53 post.get_thread().get_tags().first().name,
52 post.get_thread().get_tags().first().name,
54 post.get_pub_time_str(),
53 post.get_pub_time_str(),
55 post.version,
54 post.version,
56 ) in response,
55 ) in response,
57 'Wrong response generated for the GET request.')
56 'Wrong response generated for the GET request.')
58
57
59 post.delete()
58 post.delete()
60 key.delete()
59 key.delete()
61
60
62 KeyPair.objects.generate_key(primary=True)
61 KeyPair.objects.generate_key(primary=True)
63
62
64 SyncManager.parse_response_get(response, None)
63 SyncManager.parse_response_get(response, None)
65 self.assertEqual(1, Post.objects.count(),
64 self.assertEqual(1, Post.objects.count(),
66 'Post was not created from XML response.')
65 'Post was not created from XML response.')
67
66
68 parsed_post = Post.objects.first()
67 parsed_post = Post.objects.first()
69 self.assertEqual('tag1',
68 self.assertEqual('tag1',
70 parsed_post.get_thread().get_tags().first().name,
69 parsed_post.get_thread().get_tags().first().name,
71 'Invalid tag was parsed.')
70 'Invalid tag was parsed.')
72
71
73 SyncManager.parse_response_get(response, None)
72 SyncManager.parse_response_get(response, None)
74 self.assertEqual(1, Post.objects.count(),
73 self.assertEqual(1, Post.objects.count(),
75 'The same post was imported twice.')
74 'The same post was imported twice.')
76
75
77 self.assertEqual(1, parsed_post.global_id.signature_set.count(),
76 self.assertEqual(1, parsed_post.global_id.signature_set.count(),
78 'Signature was not saved.')
77 'Signature was not saved.')
79
78
80 post = parsed_post
79 post = parsed_post
81
80
82 # Trying to sync the same once more
81 # Trying to sync the same once more
83 response = response_get(request).content.decode()
82 response = response_get(request).content.decode()
84
83
85 self.assertTrue(
84 self.assertTrue(
86 '<status>success</status>'
85 '<status>success</status>'
87 '<models>'
86 '<models>'
88 '<model name="post">'
87 '<model name="post">'
89 '<content>'
88 '<content>'
90 '<id key="%s" local-id="%d" type="%s" />'
89 '<id key="%s" local-id="%d" type="%s" />'
91 '<title>%s</title>'
90 '<title>%s</title>'
92 '<text>%s</text>'
91 '<text>%s</text>'
93 '<tags><tag>%s</tag></tags>'
92 '<tags><tag>%s</tag></tags>'
94 '<pub-time>%s</pub-time>'
93 '<pub-time>%s</pub-time>'
95 '<version>%s</version>'
94 '<version>%s</version>'
96 '</content>' % (
95 '</content>' % (
97 post.global_id.key,
96 post.global_id.key,
98 post.global_id.local_id,
97 post.global_id.local_id,
99 post.global_id.key_type,
98 post.global_id.key_type,
100 post.title,
99 post.title,
101 post.get_sync_text(),
100 post.get_sync_text(),
102 post.get_thread().get_tags().first().name,
101 post.get_thread().get_tags().first().name,
103 post.get_pub_time_str(),
102 post.get_pub_time_str(),
104 post.version,
103 post.version,
105 ) in response,
104 ) in response,
106 'Wrong response generated for the GET request.')
105 'Wrong response generated for the GET request.')
General Comments 0
You need to be logged in to leave comments. Login now