##// END OF EJS Templates
Updated sync method for requesting and getting a post
neko259 -
r1177:a55da940 decentral
parent child Browse files
Show More
@@ -0,0 +1,116 b''
1 import xml.etree.ElementTree as et
2 from boards.models import KeyPair, GlobalId, Signature, Post
3
4 ENCODING_UNICODE = 'unicode'
5
6 TAG_MODEL = 'model'
7 TAG_REQUEST = 'request'
8 TAG_RESPONSE = 'response'
9 TAG_ID = 'id'
10 TAG_STATUS = 'status'
11 TAG_MODELS = 'models'
12 TAG_TITLE = 'title'
13 TAG_TEXT = 'text'
14 TAG_THREAD = 'thread'
15 TAG_PUB_TIME = 'pub-time'
16 TAG_SIGNATURES = 'signatures'
17 TAG_SIGNATURE = 'signature'
18 TAG_CONTENT = 'content'
19 TAG_ATTACHMENTS = 'attachments'
20 TAG_ATTACHMENT = 'attachment'
21
22 TYPE_GET = 'get'
23
24 ATTR_VERSION = 'version'
25 ATTR_TYPE = 'type'
26 ATTR_NAME = 'name'
27 ATTR_VALUE = 'value'
28 ATTR_MIMETYPE = 'mimetype'
29
30 STATUS_SUCCESS = 'success'
31
32
33 class SyncManager:
34 def generate_response_get(self, model_list: list):
35 response = et.Element(TAG_RESPONSE)
36
37 status = et.SubElement(response, TAG_STATUS)
38 status.text = STATUS_SUCCESS
39
40 models = et.SubElement(response, TAG_MODELS)
41
42 for post in model_list:
43 model = et.SubElement(models, TAG_MODEL)
44 model.set(ATTR_NAME, 'post')
45
46 content_tag = et.SubElement(model, TAG_CONTENT)
47
48 tag_id = et.SubElement(content_tag, TAG_ID)
49 post.global_id.to_xml_element(tag_id)
50
51 title = et.SubElement(content_tag, TAG_TITLE)
52 title.text = post.title
53
54 text = et.SubElement(content_tag, TAG_TEXT)
55 # TODO Replace local links by global ones in the text
56 text.text = post.get_raw_text()
57
58 if not post.is_opening():
59 thread = et.SubElement(content_tag, TAG_THREAD)
60 thread_id = et.SubElement(thread, TAG_ID)
61 post.get_thread().get_opening_post().global_id.to_xml_element(thread_id)
62 else:
63 # TODO Output tags here
64 pass
65
66 pub_time = et.SubElement(content_tag, TAG_PUB_TIME)
67 pub_time.text = str(post.get_pub_time_epoch())
68
69 signatures_tag = et.SubElement(model, TAG_SIGNATURES)
70 post_signatures = post.signature.all()
71 if post_signatures:
72 signatures = post.signatures
73 else:
74 # TODO Maybe the signature can be computed only once after
75 # the post is added? Need to add some on_save signal queue
76 # and add this there.
77 key = KeyPair.objects.get(public_key=post.global_id.key)
78 signatures = [Signature(
79 key_type=key.key_type,
80 key=key.public_key,
81 signature=key.sign(et.tostring(model, ENCODING_UNICODE)),
82 )]
83 for signature in signatures:
84 signature_tag = et.SubElement(signatures_tag, TAG_SIGNATURE)
85 signature_tag.set(ATTR_TYPE, signature.key_type)
86 signature_tag.set(ATTR_VALUE, signature.signature)
87
88 return et.tostring(response, ENCODING_UNICODE)
89
90 def parse_response_get(self, response_xml):
91 tag_root = et.fromstring(response_xml)
92 tag_status = tag_root.find(TAG_STATUS)
93 if STATUS_SUCCESS == tag_status.text:
94 tag_models = tag_root.find(TAG_MODELS)
95 for tag_model in tag_models:
96 tag_content = tag_model.find(TAG_CONTENT)
97 tag_id = tag_content.find(TAG_ID)
98 try:
99 GlobalId.from_xml_element(tag_id, existing=True)
100 print('Post with same ID already exists')
101 except GlobalId.DoesNotExist:
102 global_id = GlobalId.from_xml_element(tag_id)
103
104 title = tag_content.find(TAG_TITLE).text
105 text = tag_content.find(TAG_TEXT).text
106 # TODO Check that the replied posts are already present
107 # before adding new ones
108
109 # TODO Pub time, thread, tags
110
111 print(title)
112 print(text)
113 # post = Post.objects.create(title=title, text=text)
114 else:
115 # TODO Throw an exception?
116 pass
@@ -1,61 +1,59 b''
1 1 import re
2 2 import urllib.parse
3 3 import httplib2
4 4 import xml.etree.ElementTree as ET
5 5
6 6 from django.core.management import BaseCommand
7 7 from boards.models import GlobalId
8 from boards.models.post.sync import SyncManager
8 9
9 10 __author__ = 'neko259'
10 11
11 12
12 13 REGEX_GLOBAL_ID = re.compile(r'(\w+)::([\w\+/]+)::(\d+)')
13 14
14 15
15 16 class Command(BaseCommand):
16 17 help = 'Send a sync or get request to the server.' + \
17 18 'sync_with_server <server_url> [post_global_id]'
18 19
19 20 def add_arguments(self, parser):
20 21 parser.add_argument('url', type=str)
21 #parser.add_argument('global_id', type=str) # TODO Implement this
22 parser.add_argument('global_id', type=str)
22 23
23 24 def handle(self, *args, **options):
24 25 url = options.get('url')
25 26 global_id_str = options.get('global_id')
26 27 if global_id_str:
27 28 match = REGEX_GLOBAL_ID.match(global_id_str)
28 29 if match:
29 30 key_type = match.group(1)
30 31 key = match.group(2)
31 32 local_id = match.group(3)
32 33
33 34 global_id = GlobalId(key_type=key_type, key=key,
34 35 local_id=local_id)
35 36
36 37 xml = GlobalId.objects.generate_request_get([global_id])
37 data = {'xml': xml}
38 body = urllib.parse.urlencode(data)
38 # body = urllib.parse.urlencode(data)
39 39 h = httplib2.Http()
40 response, content = h.request(url, method="POST", body=body)
40 response, content = h.request(url, method="POST", body=xml)
41 41
42 # TODO Parse content and get the model list
43
44 print(content)
42 SyncManager().parse_response_get(content)
45 43 else:
46 44 raise Exception('Invalid global ID')
47 45 else:
48 46 h = httplib2.Http()
49 47 response, content = h.request(url, method="POST")
50 48
51 49 print(content)
52 50
53 51 root = ET.fromstring(content)
54 52 status = root.findall('status')[0].text
55 53 if status == 'success':
56 54 models = root.findall('models')[0]
57 55 for model in models:
58 56 model_content = model[0]
59 57 print(model_content.findall('text')[0].text)
60 58 else:
61 59 raise Exception('Invalid response status')
@@ -1,607 +1,483 b''
1 1 from datetime import datetime, timedelta, date
2 2 from datetime import time as dtime
3 3 import logging
4 4 import re
5 5 import uuid
6 import xml.etree.ElementTree as et
7 6
8 7 from django.core.exceptions import ObjectDoesNotExist
9 8 from django.core.urlresolvers import reverse
10 9 from django.db import models, transaction
11 10 from django.db.models import TextField
12 11 from django.template.loader import render_to_string
13 12 from django.utils import timezone
14 13
15 14 from boards.mdx_neboard import Parser
16 from boards.models import KeyPair, GlobalId, Signature
15 from boards.models import KeyPair, GlobalId
17 16 from boards import settings
18 17 from boards.models import PostImage
19 18 from boards.models.base import Viewable
20 19 from boards import utils
21 20 from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
22 21 from boards.models.user import Notification, Ban
23 22 import boards.models.thread
24 23
25
26 ENCODING_UNICODE = 'unicode'
27
28 24 WS_NOTIFICATION_TYPE_NEW_POST = 'new_post'
29 25 WS_NOTIFICATION_TYPE = 'notification_type'
30 26
31 27 WS_CHANNEL_THREAD = "thread:"
32 28
33 29 APP_LABEL_BOARDS = 'boards'
34 30
35 31 POSTS_PER_DAY_RANGE = 7
36 32
37 33 BAN_REASON_AUTO = 'Auto'
38 34
39 35 IMAGE_THUMB_SIZE = (200, 150)
40 36
41 37 TITLE_MAX_LENGTH = 200
42 38
43 39 # TODO This should be removed
44 40 NO_IP = '0.0.0.0'
45 41
46 42 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
47 43 REGEX_GLOBAL_REPLY = re.compile(r'\[post\](\w+)::([^:]+)::(\d+)\[/post\]')
48 44 REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?')
49 45 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
50 46
51 TAG_MODEL = 'model'
52 TAG_REQUEST = 'request'
53 TAG_RESPONSE = 'response'
54 TAG_ID = 'id'
55 TAG_STATUS = 'status'
56 TAG_MODELS = 'models'
57 TAG_TITLE = 'title'
58 TAG_TEXT = 'text'
59 TAG_THREAD = 'thread'
60 TAG_PUB_TIME = 'pub-time'
61 TAG_SIGNATURES = 'signatures'
62 TAG_SIGNATURE = 'signature'
63 TAG_CONTENT = 'content'
64 TAG_ATTACHMENTS = 'attachments'
65 TAG_ATTACHMENT = 'attachment'
66
67 TYPE_GET = 'get'
68
69 ATTR_VERSION = 'version'
70 ATTR_TYPE = 'type'
71 ATTR_NAME = 'name'
72 ATTR_VALUE = 'value'
73 ATTR_MIMETYPE = 'mimetype'
74
75 STATUS_SUCCESS = 'success'
76 47
77 48 PARAMETER_TRUNCATED = 'truncated'
78 49 PARAMETER_TAG = 'tag'
79 50 PARAMETER_OFFSET = 'offset'
80 51 PARAMETER_DIFF_TYPE = 'type'
81 52 PARAMETER_CSS_CLASS = 'css_class'
82 53 PARAMETER_THREAD = 'thread'
83 54 PARAMETER_IS_OPENING = 'is_opening'
84 55 PARAMETER_MODERATOR = 'moderator'
85 56 PARAMETER_POST = 'post'
86 57 PARAMETER_OP_ID = 'opening_post_id'
87 58 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
88 59 PARAMETER_REPLY_LINK = 'reply_link'
89 60 PARAMETER_NEED_OP_DATA = 'need_op_data'
90 61
91 62 DIFF_TYPE_HTML = 'html'
92 DIFF_TYPE_JSON = 'json'
93 63
94 64 REFMAP_STR = '<a href="{}">&gt;&gt;{}</a>'
95 65
96 66
97 67 class PostManager(models.Manager):
98 68 @transaction.atomic
99 69 def create_post(self, title: str, text: str, image=None, thread=None,
100 70 ip=NO_IP, tags: list=None, threads: list=None):
101 71 """
102 72 Creates new post
103 73 """
104 74
105 75 is_banned = Ban.objects.filter(ip=ip).exists()
106 76
107 77 # TODO Raise specific exception and catch it in the views
108 78 if is_banned:
109 79 raise Exception("This user is banned")
110 80
111 81 if not tags:
112 82 tags = []
113 83 if not threads:
114 84 threads = []
115 85
116 86 posting_time = timezone.now()
117 87 if not thread:
118 88 thread = boards.models.thread.Thread.objects.create(
119 89 bump_time=posting_time, last_edit_time=posting_time)
120 90 new_thread = True
121 91 else:
122 92 new_thread = False
123 93
124 94 pre_text = Parser().preparse(text)
125 95
126 96 post = self.create(title=title,
127 97 text=pre_text,
128 98 pub_time=posting_time,
129 99 poster_ip=ip,
130 100 thread=thread,
131 101 last_edit_time=posting_time)
132 102 post.threads.add(thread)
133 103
134 104 post.set_global_id()
135 105
136 106 logger = logging.getLogger('boards.post.create')
137 107
138 108 logger.info('Created post {} by {}'.format(post, post.poster_ip))
139 109
140 110 if image:
141 111 post.images.add(PostImage.objects.create_with_hash(image))
142 112
143 113 list(map(thread.add_tag, tags))
144 114
145 115 if new_thread:
146 116 boards.models.thread.Thread.objects.process_oldest_threads()
147 117 else:
148 118 thread.last_edit_time = posting_time
149 119 thread.bump()
150 120 thread.save()
151 121
152 122 post.connect_replies()
153 123 post.connect_threads(threads)
154 124 post.connect_notifications()
155 125
156 126 post.build_url()
157 127
158 128 return post
159 129
160 130 def delete_posts_by_ip(self, ip):
161 131 """
162 132 Deletes all posts of the author with same IP
163 133 """
164 134
165 135 posts = self.filter(poster_ip=ip)
166 136 for post in posts:
167 137 post.delete()
168 138
169 139 @utils.cached_result()
170 140 def get_posts_per_day(self) -> float:
171 141 """
172 142 Gets average count of posts per day for the last 7 days
173 143 """
174 144
175 145 day_end = date.today()
176 146 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
177 147
178 148 day_time_start = timezone.make_aware(datetime.combine(
179 149 day_start, dtime()), timezone.get_current_timezone())
180 150 day_time_end = timezone.make_aware(datetime.combine(
181 151 day_end, dtime()), timezone.get_current_timezone())
182 152
183 153 posts_per_period = float(self.filter(
184 154 pub_time__lte=day_time_end,
185 155 pub_time__gte=day_time_start).count())
186 156
187 157 ppd = posts_per_period / POSTS_PER_DAY_RANGE
188 158
189 159 return ppd
190 160
191 161
192 # TODO Make a separate sync facade?
193 def generate_response_get(self, model_list: list):
194 response = et.Element(TAG_RESPONSE)
195
196 status = et.SubElement(response, TAG_STATUS)
197 status.text = STATUS_SUCCESS
198
199 models = et.SubElement(response, TAG_MODELS)
200
201 for post in model_list:
202 model = et.SubElement(models, TAG_MODEL)
203 model.set(ATTR_NAME, 'post')
204
205 content_tag = et.SubElement(model, TAG_CONTENT)
206
207 tag_id = et.SubElement(content_tag, TAG_ID)
208 post.global_id.to_xml_element(tag_id)
209
210 title = et.SubElement(content_tag, TAG_TITLE)
211 title.text = post.title
212
213 text = et.SubElement(content_tag, TAG_TEXT)
214 # TODO Replace local links by global ones in the text
215 text.text = post.get_raw_text()
216
217 if not post.is_opening():
218 thread = et.SubElement(content_tag, TAG_THREAD)
219 thread_id = et.SubElement(thread, TAG_ID)
220 post.get_thread().get_opening_post().global_id.to_xml_element(thread_id)
221 else:
222 # TODO Output tags here
223 pass
224
225 pub_time = et.SubElement(content_tag, TAG_PUB_TIME)
226 pub_time.text = str(post.get_pub_time_epoch())
227
228 signatures_tag = et.SubElement(model, TAG_SIGNATURES)
229 post_signatures = post.signature.all()
230 if post_signatures:
231 signatures = post.signatures
232 else:
233 # TODO Maybe the signature can be computed only once after
234 # the post is added? Need to add some on_save signal queue
235 # and add this there.
236 key = KeyPair.objects.get(public_key=post.global_id.key)
237 signatures = [Signature(
238 key_type=key.key_type,
239 key=key.public_key,
240 signature=key.sign(et.tostring(model, ENCODING_UNICODE)),
241 )]
242 for signature in signatures:
243 signature_tag = et.SubElement(signatures_tag, TAG_SIGNATURE)
244 signature_tag.set(ATTR_TYPE, signature.key_type)
245 signature_tag.set(ATTR_VALUE, signature.signature)
246
247 return et.tostring(response, ENCODING_UNICODE)
248
249 def parse_response_get(self, response_xml):
250 tag_root = et.fromstring(response_xml)
251 tag_status = tag_root[0]
252 if 'success' == tag_status.text:
253 tag_models = tag_root[1]
254 for tag_model in tag_models:
255 tag_content = tag_model[0]
256 tag_id = tag_content[1]
257 try:
258 GlobalId.from_xml_element(tag_id, existing=True)
259 # If this post already exists, just continue
260 # TODO Compare post content and update the post if necessary
261 pass
262 except GlobalId.DoesNotExist:
263 global_id = GlobalId.from_xml_element(tag_id)
264
265 title = tag_content.find(TAG_TITLE).text
266 text = tag_content.find(TAG_TEXT).text
267 # TODO Check that the replied posts are already present
268 # before adding new ones
269
270 # TODO Pub time, thread, tags
271
272 post = Post.objects.create(title=title, text=text)
273 else:
274 # TODO Throw an exception?
275 pass
276
277 # TODO Make a separate parser module and move preparser there
278 def _preparse_text(self, text: str) -> str:
279 """
280 Preparses text to change patterns like '>>' to a proper bbcode
281 tags.
282 """
283
284 162 class Post(models.Model, Viewable):
285 163 """A post is a message."""
286 164
287 165 objects = PostManager()
288 166
289 167 class Meta:
290 168 app_label = APP_LABEL_BOARDS
291 169 ordering = ('id',)
292 170
293 171 title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
294 172 pub_time = models.DateTimeField()
295 173 text = TextField(blank=True, null=True)
296 174 _text_rendered = TextField(blank=True, null=True, editable=False)
297 175
298 176 images = models.ManyToManyField(PostImage, null=True, blank=True,
299 177 related_name='ip+', db_index=True)
300 178
301 179 poster_ip = models.GenericIPAddressField()
302 180
303 181 # TODO This field can be removed cause UID is used for update now
304 182 last_edit_time = models.DateTimeField()
305 183
306 184 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
307 185 null=True,
308 186 blank=True, related_name='rfp+',
309 187 db_index=True)
310 188 refmap = models.TextField(null=True, blank=True)
311 189 threads = models.ManyToManyField('Thread', db_index=True)
312 190 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
313 191
314 192 url = models.TextField()
315 193 uid = models.TextField(db_index=True)
316 194
317 195 # Global ID with author key. If the message was downloaded from another
318 196 # server, this indicates the server.
319 197 global_id = models.OneToOneField('GlobalId', null=True, blank=True)
320 198
321 199 # One post can be signed by many nodes that give their trust to it
322 200 signature = models.ManyToManyField('Signature', null=True, blank=True)
323 201
324 202 def __str__(self):
325 203 return 'P#{}/{}'.format(self.id, self.title)
326 204
327 205 def get_title(self) -> str:
328 206 """
329 207 Gets original post title or part of its text.
330 208 """
331 209
332 210 title = self.title
333 211 if not title:
334 212 title = self.get_text()
335 213
336 214 return title
337 215
338 216 def build_refmap(self) -> None:
339 217 """
340 218 Builds a replies map string from replies list. This is a cache to stop
341 219 the server from recalculating the map on every post show.
342 220 """
343 221
344 222 post_urls = [REFMAP_STR.format(refpost.get_absolute_url(), refpost.id)
345 223 for refpost in self.referenced_posts.all()]
346 224
347 225 self.refmap = ', '.join(post_urls)
348 226
349 227 def is_referenced(self) -> bool:
350 228 return self.refmap and len(self.refmap) > 0
351 229
352 230 def is_opening(self) -> bool:
353 231 """
354 232 Checks if this is an opening post or just a reply.
355 233 """
356 234
357 235 return self.get_thread().get_opening_post_id() == self.id
358 236
359 237 def get_absolute_url(self):
360 238 return self.url
361 239
362 240 def get_thread(self):
363 241 return self.thread
364 242
365 243 def get_threads(self) -> list:
366 244 """
367 245 Gets post's thread.
368 246 """
369 247
370 248 return self.threads
371 249
372 250 def get_view(self, moderator=False, need_open_link=False,
373 251 truncated=False, reply_link=False, *args, **kwargs) -> str:
374 252 """
375 253 Renders post's HTML view. Some of the post params can be passed over
376 254 kwargs for the means of caching (if we view the thread, some params
377 255 are same for every post and don't need to be computed over and over.
378 256 """
379 257
380 258 thread = self.get_thread()
381 259 is_opening = kwargs.get(PARAMETER_IS_OPENING, self.is_opening())
382 260
383 261 if is_opening:
384 262 opening_post_id = self.id
385 263 else:
386 264 opening_post_id = thread.get_opening_post_id()
387 265
388 266 css_class = 'post'
389 267 if thread.archived:
390 268 css_class += ' archive_post'
391 269 elif not thread.can_bump():
392 270 css_class += ' dead_post'
393 271
394 272 return render_to_string('boards/post.html', {
395 273 PARAMETER_POST: self,
396 274 PARAMETER_MODERATOR: moderator,
397 275 PARAMETER_IS_OPENING: is_opening,
398 276 PARAMETER_THREAD: thread,
399 277 PARAMETER_CSS_CLASS: css_class,
400 278 PARAMETER_NEED_OPEN_LINK: need_open_link,
401 279 PARAMETER_TRUNCATED: truncated,
402 280 PARAMETER_OP_ID: opening_post_id,
403 281 PARAMETER_REPLY_LINK: reply_link,
404 282 PARAMETER_NEED_OP_DATA: kwargs.get(PARAMETER_NEED_OP_DATA)
405 283 })
406 284
407 285 def get_search_view(self, *args, **kwargs):
408 286 return self.get_view(need_op_data=True, *args, **kwargs)
409 287
410 288 def get_first_image(self) -> PostImage:
411 289 return self.images.earliest('id')
412 290
413 291 def delete(self, using=None):
414 292 """
415 293 Deletes all post images and the post itself.
416 294 """
417 295
418 296 for image in self.images.all():
419 297 image_refs_count = Post.objects.filter(images__in=[image]).count()
420 298 if image_refs_count == 1:
421 299 image.delete()
422 300
423 301 self.signature.all().delete()
424 302 if self.global_id:
425 303 self.global_id.delete()
426 304
427 305 thread = self.get_thread()
428 306 thread.last_edit_time = timezone.now()
429 307 thread.save()
430 308
431 309 super(Post, self).delete(using)
432 310
433 311 logging.getLogger('boards.post.delete').info(
434 312 'Deleted post {}'.format(self))
435 313
436 # TODO Implement this with OOP, e.g. use the factory and HtmlPostData class
437 314 def set_global_id(self, key_pair=None):
438 315 """
439 316 Sets global id based on the given key pair. If no key pair is given,
440 317 default one is used.
441 318 """
442 319
443 320 if key_pair:
444 321 key = key_pair
445 322 else:
446 323 try:
447 324 key = KeyPair.objects.get(primary=True)
448 325 except KeyPair.DoesNotExist:
449 326 # Do not update the global id because there is no key defined
450 327 return
451 328 global_id = GlobalId(key_type=key.key_type,
452 329 key=key.public_key,
453 330 local_id = self.id)
454 331 global_id.save()
455 332
456 333 self.global_id = global_id
457 334
458 335 self.save(update_fields=['global_id'])
459 336
460 337 def get_pub_time_epoch(self):
461 338 return utils.datetime_to_epoch(self.pub_time)
462 339
463 340 def get_replied_ids(self):
464 341 """
465 342 Gets ID list of the posts that this post replies.
466 343 """
467 344
468 345 raw_text = self.get_raw_text()
469 346
470 347 local_replied = REGEX_REPLY.findall(raw_text)
471 348 global_replied = []
472 349 for match in REGEX_GLOBAL_REPLY.findall(raw_text):
473 350 key_type = match[0]
474 351 key = match[1]
475 352 local_id = match[2]
476 353
477 354 try:
478 355 global_id = GlobalId.objects.get(key_type=key_type,
479 356 key=key, local_id=local_id)
480 357 for post in Post.objects.filter(global_id=global_id).only('id'):
481 358 global_replied.append(post.id)
482 359 except GlobalId.DoesNotExist:
483 360 pass
484 361 return local_replied + global_replied
485 362
486
487 363 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
488 364 include_last_update=False) -> str:
489 365 """
490 366 Gets post HTML or JSON data that can be rendered on a page or used by
491 367 API.
492 368 """
493 369
494 370 return get_exporter(format_type).export(self, request,
495 371 include_last_update)
496 372
497 373 def notify_clients(self, recursive=True):
498 374 """
499 375 Sends post HTML data to the thread web socket.
500 376 """
501 377
502 378 if not settings.get_bool('External', 'WebsocketsEnabled'):
503 379 return
504 380
505 381 thread_ids = list()
506 382 for thread in self.get_threads().all():
507 383 thread_ids.append(thread.id)
508 384
509 385 thread.notify_clients()
510 386
511 387 if recursive:
512 388 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
513 389 post_id = reply_number.group(1)
514 390
515 391 try:
516 392 ref_post = Post.objects.get(id=post_id)
517 393
518 394 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
519 395 # If post is in this thread, its thread was already notified.
520 396 # Otherwise, notify its thread separately.
521 397 ref_post.notify_clients(recursive=False)
522 398 except ObjectDoesNotExist:
523 399 pass
524 400
525 401 def build_url(self):
526 402 thread = self.get_thread()
527 403 opening_id = thread.get_opening_post_id()
528 404 post_url = reverse('thread', kwargs={'post_id': opening_id})
529 405 if self.id != opening_id:
530 406 post_url += '#' + str(self.id)
531 407 self.url = post_url
532 408 self.save(update_fields=['url'])
533 409
534 410 def save(self, force_insert=False, force_update=False, using=None,
535 411 update_fields=None):
536 412 self._text_rendered = Parser().parse(self.get_raw_text())
537 413
538 414 self.uid = str(uuid.uuid4())
539 415 if update_fields is not None and 'uid' not in update_fields:
540 416 update_fields += ['uid']
541 417
542 418 if self.id:
543 419 for thread in self.get_threads().all():
544 420 if thread.can_bump():
545 421 thread.update_bump_status(exclude_posts=[self])
546 422 thread.last_edit_time = self.last_edit_time
547 423
548 424 thread.save(update_fields=['last_edit_time', 'bumpable'])
549 425
550 426 super().save(force_insert, force_update, using, update_fields)
551 427
552 428 def get_text(self) -> str:
553 429 return self._text_rendered
554 430
555 431 def get_raw_text(self) -> str:
556 432 return self.text
557 433
558 434 def get_absolute_id(self) -> str:
559 435 """
560 436 If the post has many threads, shows its main thread OP id in the post
561 437 ID.
562 438 """
563 439
564 440 if self.get_threads().count() > 1:
565 441 return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id)
566 442 else:
567 443 return str(self.id)
568 444
569 445 def connect_notifications(self):
570 446 for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()):
571 447 user_name = reply_number.group(1).lower()
572 448 Notification.objects.get_or_create(name=user_name, post=self)
573 449
574 450 def connect_replies(self):
575 451 """
576 452 Connects replies to a post to show them as a reflink map
577 453 """
578 454
579 455 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
580 456 post_id = reply_number.group(1)
581 457
582 458 try:
583 459 referenced_post = Post.objects.get(id=post_id)
584 460
585 461 referenced_post.referenced_posts.add(self)
586 462 referenced_post.last_edit_time = self.pub_time
587 463 referenced_post.build_refmap()
588 464 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
589 465 except ObjectDoesNotExist:
590 466 pass
591 467
592 468 def connect_threads(self, opening_posts):
593 469 """
594 470 If the referenced post is an OP in another thread,
595 471 make this post multi-thread.
596 472 """
597 473
598 474 for opening_post in opening_posts:
599 475 threads = opening_post.get_threads().all()
600 476 for thread in threads:
601 477 if thread.can_bump():
602 478 thread.update_bump_status()
603 479
604 480 thread.last_edit_time = self.last_edit_time
605 481 thread.save(update_fields=['last_edit_time', 'bumpable'])
606 482
607 483 self.threads.add(thread)
@@ -1,93 +1,94 b''
1 1 from django.conf.urls import patterns, url
2 2 from django.views.i18n import javascript_catalog
3 3
4 4 from boards import views
5 5 from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed
6 6 from boards.views import api, tag_threads, all_threads, \
7 7 settings, all_tags, feed
8 8 from boards.views.authors import AuthorsView
9 9 from boards.views.notifications import NotificationView
10 10 from boards.views.search import BoardSearchView
11 11 from boards.views.static import StaticPageView
12 12 from boards.views.preview import PostPreviewView
13 from boards.views.sync import get_post_sync_data
13 from boards.views.sync import get_post_sync_data, response_get
14 14
15 15
16 16 js_info_dict = {
17 17 'packages': ('boards',),
18 18 }
19 19
20 20 urlpatterns = patterns('',
21 21 # /boards/
22 22 url(r'^$', all_threads.AllThreadsView.as_view(), name='index'),
23 23 # /boards/page/
24 24 url(r'^page/(?P<page>\w+)/$', all_threads.AllThreadsView.as_view(),
25 25 name='index'),
26 26
27 27 # /boards/tag/tag_name/
28 28 url(r'^tag/(?P<tag_name>\w+)/$', tag_threads.TagView.as_view(),
29 29 name='tag'),
30 30 # /boards/tag/tag_id/page/
31 31 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/$',
32 32 tag_threads.TagView.as_view(), name='tag'),
33 33
34 34 # /boards/thread/
35 35 url(r'^thread/(?P<post_id>\d+)/$', views.thread.normal.NormalThreadView.as_view(),
36 36 name='thread'),
37 37 url(r'^thread/(?P<post_id>\d+)/mode/gallery/$', views.thread.gallery.GalleryThreadView.as_view(),
38 38 name='thread_gallery'),
39 39 # /feed/
40 40 url(r'^feed/$', views.feed.FeedView.as_view(), name='feed'),
41 41 url(r'^feed/page/(?P<page>\w+)/$', views.feed.FeedView.as_view(),
42 42 name='feed'),
43 43
44 44 url(r'^settings/$', settings.SettingsView.as_view(), name='settings'),
45 45 url(r'^tags/(?P<query>\w+)?/?$', all_tags.AllTagsView.as_view(), name='tags'),
46 46 url(r'^authors/$', AuthorsView.as_view(), name='authors'),
47 47
48 48 url(r'^banned/$', views.banned.BannedView.as_view(), name='banned'),
49 49 url(r'^staticpage/(?P<name>\w+)/$', StaticPageView.as_view(),
50 50 name='staticpage'),
51 51
52 52 # RSS feeds
53 53 url(r'^rss/$', AllThreadsFeed()),
54 54 url(r'^page/(?P<page>\d+)/rss/$', AllThreadsFeed()),
55 55 url(r'^tag/(?P<tag_name>\w+)/rss/$', TagThreadsFeed()),
56 56 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/rss/$', TagThreadsFeed()),
57 57 url(r'^thread/(?P<post_id>\d+)/rss/$', ThreadPostsFeed()),
58 58
59 59 # i18n
60 60 url(r'^jsi18n/$', javascript_catalog, js_info_dict,
61 61 name='js_info_dict'),
62 62
63 63 # API
64 64 url(r'^api/post/(?P<post_id>\d+)/$', api.get_post, name="get_post"),
65 65 url(r'^api/diff_thread$',
66 66 api.api_get_threaddiff, name="get_thread_diff"),
67 67 url(r'^api/threads/(?P<count>\w+)/$', api.api_get_threads,
68 68 name='get_threads'),
69 69 url(r'^api/tags/$', api.api_get_tags, name='get_tags'),
70 70 url(r'^api/thread/(?P<opening_post_id>\w+)/$', api.api_get_thread_posts,
71 71 name='get_thread'),
72 72 url(r'^api/add_post/(?P<opening_post_id>\w+)/$', api.api_add_post,
73 73 name='add_post'),
74 74 url(r'^api/notifications/(?P<username>\w+)/$', api.api_get_notifications,
75 75 name='api_notifications'),
76 76
77 77 # Sync protocol API
78 78 url(r'^api/sync/pull/$', api.sync_pull, name='api_sync_pull'),
79 url(r'^api/sync/get/$', response_get, name='api_sync_pull'),
79 80 # TODO 'get' request
80 81
81 82 # Search
82 83 url(r'^search/$', BoardSearchView.as_view(), name='search'),
83 84
84 85 # Notifications
85 86 url(r'^notifications/(?P<username>\w+)$', NotificationView.as_view(), name='notifications'),
86 87
87 88 # Post preview
88 89 url(r'^preview/$', PostPreviewView.as_view(), name='preview'),
89 90
90 91 url(r'^post_xml/(?P<post_id>\d+)$', get_post_sync_data,
91 92 name='post_sync_data'),
92 93
93 94 )
@@ -1,246 +1,249 b''
1 1 import json
2 2 import logging
3 3
4 import xml.etree.ElementTree as ET
5
4 6 from django.db import transaction
5 7 from django.http import HttpResponse
6 8 from django.shortcuts import get_object_or_404
7 9 from django.core import serializers
8 10
9 11 from boards.forms import PostForm, PlainErrorList
10 from boards.models import Post, Thread, Tag
12 from boards.models import Post, Thread, Tag, GlobalId
13 from boards.models.post.sync import SyncManager
11 14 from boards.utils import datetime_to_epoch
12 15 from boards.views.thread import ThreadView
13 16 from boards.models.user import Notification
14 17
15 18
16 19 __author__ = 'neko259'
17 20
18 21 PARAMETER_TRUNCATED = 'truncated'
19 22 PARAMETER_TAG = 'tag'
20 23 PARAMETER_OFFSET = 'offset'
21 24 PARAMETER_DIFF_TYPE = 'type'
22 25 PARAMETER_POST = 'post'
23 26 PARAMETER_UPDATED = 'updated'
24 27 PARAMETER_LAST_UPDATE = 'last_update'
25 28 PARAMETER_THREAD = 'thread'
26 29 PARAMETER_UIDS = 'uids'
27 30
28 31 DIFF_TYPE_HTML = 'html'
29 32 DIFF_TYPE_JSON = 'json'
30 33
31 34 STATUS_OK = 'ok'
32 35 STATUS_ERROR = 'error'
33 36
34 37 logger = logging.getLogger(__name__)
35 38
36 39
37 40 @transaction.atomic
38 41 def api_get_threaddiff(request):
39 42 """
40 43 Gets posts that were changed or added since time
41 44 """
42 45
43 46 thread_id = request.GET.get(PARAMETER_THREAD)
44 47 uids_str = request.POST.get(PARAMETER_UIDS).strip()
45 48 uids = uids_str.split(' ')
46 49
47 50 thread = get_object_or_404(Post, id=thread_id).get_thread()
48 51
49 52 json_data = {
50 53 PARAMETER_UPDATED: [],
51 54 PARAMETER_LAST_UPDATE: None, # TODO Maybe this can be removed already?
52 55 }
53 56 posts = Post.objects.filter(threads__in=[thread]).exclude(uid__in=uids)
54 57
55 58 diff_type = request.GET.get(PARAMETER_DIFF_TYPE, DIFF_TYPE_HTML)
56 59
57 60 for post in posts:
58 61 json_data[PARAMETER_UPDATED].append(get_post_data(post.id, diff_type, request))
59 62 json_data[PARAMETER_LAST_UPDATE] = str(thread.last_edit_time)
60 63
61 64 return HttpResponse(content=json.dumps(json_data))
62 65
63 66
64 67 def api_add_post(request, opening_post_id):
65 68 """
66 69 Adds a post and return the JSON response for it
67 70 """
68 71
69 72 opening_post = get_object_or_404(Post, id=opening_post_id)
70 73
71 74 logger.info('Adding post via api...')
72 75
73 76 status = STATUS_OK
74 77 errors = []
75 78
76 79 if request.method == 'POST':
77 80 form = PostForm(request.POST, request.FILES, error_class=PlainErrorList)
78 81 form.session = request.session
79 82
80 83 if form.need_to_ban:
81 84 # Ban user because he is suspected to be a bot
82 85 # _ban_current_user(request)
83 86 status = STATUS_ERROR
84 87 if form.is_valid():
85 88 post = ThreadView().new_post(request, form, opening_post,
86 89 html_response=False)
87 90 if not post:
88 91 status = STATUS_ERROR
89 92 else:
90 93 logger.info('Added post #%d via api.' % post.id)
91 94 else:
92 95 status = STATUS_ERROR
93 96 errors = form.as_json_errors()
94 97
95 98 response = {
96 99 'status': status,
97 100 'errors': errors,
98 101 }
99 102
100 103 return HttpResponse(content=json.dumps(response))
101 104
102 105
103 106 def get_post(request, post_id):
104 107 """
105 108 Gets the html of a post. Used for popups. Post can be truncated if used
106 109 in threads list with 'truncated' get parameter.
107 110 """
108 111
109 112 post = get_object_or_404(Post, id=post_id)
110 113 truncated = PARAMETER_TRUNCATED in request.GET
111 114
112 115 return HttpResponse(content=post.get_view(truncated=truncated))
113 116
114 117
115 118 def api_get_threads(request, count):
116 119 """
117 120 Gets the JSON thread opening posts list.
118 121 Parameters that can be used for filtering:
119 122 tag, offset (from which thread to get results)
120 123 """
121 124
122 125 if PARAMETER_TAG in request.GET:
123 126 tag_name = request.GET[PARAMETER_TAG]
124 127 if tag_name is not None:
125 128 tag = get_object_or_404(Tag, name=tag_name)
126 129 threads = tag.get_threads().filter(archived=False)
127 130 else:
128 131 threads = Thread.objects.filter(archived=False)
129 132
130 133 if PARAMETER_OFFSET in request.GET:
131 134 offset = request.GET[PARAMETER_OFFSET]
132 135 offset = int(offset) if offset is not None else 0
133 136 else:
134 137 offset = 0
135 138
136 139 threads = threads.order_by('-bump_time')
137 140 threads = threads[offset:offset + int(count)]
138 141
139 142 opening_posts = []
140 143 for thread in threads:
141 144 opening_post = thread.get_opening_post()
142 145
143 146 # TODO Add tags, replies and images count
144 147 post_data = get_post_data(opening_post.id, include_last_update=True)
145 148 post_data['bumpable'] = thread.can_bump()
146 149 post_data['archived'] = thread.archived
147 150
148 151 opening_posts.append(post_data)
149 152
150 153 return HttpResponse(content=json.dumps(opening_posts))
151 154
152 155
153 156 # TODO Test this
154 157 def api_get_tags(request):
155 158 """
156 159 Gets all tags or user tags.
157 160 """
158 161
159 162 # TODO Get favorite tags for the given user ID
160 163
161 164 tags = Tag.objects.get_not_empty_tags()
162 165
163 166 term = request.GET.get('term')
164 167 if term is not None:
165 168 tags = tags.filter(name__contains=term)
166 169
167 170 tag_names = [tag.name for tag in tags]
168 171
169 172 return HttpResponse(content=json.dumps(tag_names))
170 173
171 174
172 175 # TODO The result can be cached by the thread last update time
173 176 # TODO Test this
174 177 def api_get_thread_posts(request, opening_post_id):
175 178 """
176 179 Gets the JSON array of thread posts
177 180 """
178 181
179 182 opening_post = get_object_or_404(Post, id=opening_post_id)
180 183 thread = opening_post.get_thread()
181 184 posts = thread.get_replies()
182 185
183 186 json_data = {
184 187 'posts': [],
185 188 'last_update': None,
186 189 }
187 190 json_post_list = []
188 191
189 192 for post in posts:
190 193 json_post_list.append(get_post_data(post.id))
191 194 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
192 195 json_data['posts'] = json_post_list
193 196
194 197 return HttpResponse(content=json.dumps(json_data))
195 198
196 199
197 200 def api_get_notifications(request, username):
198 201 last_notification_id_str = request.GET.get('last', None)
199 202 last_id = int(last_notification_id_str) if last_notification_id_str is not None else None
200 203
201 204 posts = Notification.objects.get_notification_posts(username=username,
202 205 last=last_id)
203 206
204 207 json_post_list = []
205 208 for post in posts:
206 209 json_post_list.append(get_post_data(post.id))
207 210 return HttpResponse(content=json.dumps(json_post_list))
208 211
209 212
210 213 def api_get_post(request, post_id):
211 214 """
212 215 Gets the JSON of a post. This can be
213 216 used as and API for external clients.
214 217 """
215 218
216 219 post = get_object_or_404(Post, id=post_id)
217 220
218 221 json = serializers.serialize("json", [post], fields=(
219 222 "pub_time", "_text_rendered", "title", "text", "image",
220 223 "image_width", "image_height", "replies", "tags"
221 224 ))
222 225
223 226 return HttpResponse(content=json)
224 227
225 228
226 229 # TODO Remove this method and use post method directly
227 230 def get_post_data(post_id, format_type=DIFF_TYPE_JSON, request=None,
228 231 include_last_update=False):
229 232 post = get_object_or_404(Post, id=post_id)
230 233 return post.get_post_data(format_type=format_type, request=request,
231 234 include_last_update=include_last_update)
232 235
233 236
234 237 # TODO Make a separate module for sync API methods
235 238 def sync_pull(request):
236 239 """
237 Return 'get' request response for all posts.
240 Return 'pull' request response for all posts.
238 241 """
239 242 request_xml = request.get('xml')
240 243 if request_xml is None:
241 244 posts = Post.objects.all()
242 245 else:
243 246 pass # TODO Parse the XML and get filters from it
244 247
245 xml = Post.objects.generate_response_get(posts)
248 xml = SyncManager().generate_response_get(posts)
246 249 return HttpResponse(content=xml)
@@ -1,49 +1,53 b''
1 1 import xml.etree.ElementTree as et
2 2 from django.http import HttpResponse, Http404
3 3 from boards.models import GlobalId, Post
4 from boards.models.post.sync import SyncManager
4 5
5 6
6 def respond_pull(request):
7 def response_pull(request):
7 8 pass
8 9
9 10
10 def respond_get(request):
11 def response_get(request):
11 12 """
12 13 Processes a GET request with post ID list and returns the posts XML list.
13 14 Request should contain an 'xml' post attribute with the actual request XML.
14 15 """
15 16
16 request_xml = request.POST['xml']
17 request_xml = request.body
18
19 if request_xml is None:
20 return HttpResponse(content='Use the API')
17 21
18 22 posts = []
19 23
20 24 root_tag = et.fromstring(request_xml)
21 25 model_tag = root_tag[0]
22 26 for id_tag in model_tag:
23 27 try:
24 28 global_id = GlobalId.from_xml_element(id_tag, existing=True)
25 posts += Post.objects.filter(global_id=global_id)
29 posts.append(Post.objects.get(global_id=global_id))
26 30 except GlobalId.DoesNotExist:
27 31 # This is normal. If we don't have such GlobalId in the system,
28 32 # just ignore this ID and proceed to the next one.
29 33 pass
30 34
31 response_xml = Post.objects.generate_response_get(posts)
35 response_xml = SyncManager().generate_response_get(posts)
32 36
33 37 return HttpResponse(content=response_xml)
34 38
35 39
36 40 def get_post_sync_data(request, post_id):
37 41 try:
38 42 post = Post.objects.get(id=post_id)
39 43 except Post.DoesNotExist:
40 44 raise Http404()
41 45
42 46 content = 'Global ID: %s\n\nXML: %s' \
43 % (post.global_id, Post.objects.generate_response_get([post]))
47 % (post.global_id, SyncManager().generate_response_get([post]))
44 48
45 49
46 50 return HttpResponse(
47 51 content_type='text/plain',
48 52 content=content,
49 53 ) No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now