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