##// END OF EJS Templates
Merge tip
Bohdan Horbeshko -
r2146:69a1b01a merge lite
parent child Browse files
Show More
@@ -1,53 +1,54 b''
1 bc8fce57a613175450b8b6d933cdd85f22c04658 1.1
1 bc8fce57a613175450b8b6d933cdd85f22c04658 1.1
2 784258eb652c563c288ca7652c33f52cd4733d83 1.1-stable
2 784258eb652c563c288ca7652c33f52cd4733d83 1.1-stable
3 1b53a22467a8fccc798935d7a26efe78e4bc7b25 1.2-stable
3 1b53a22467a8fccc798935d7a26efe78e4bc7b25 1.2-stable
4 1713fb7543386089e364c39703b79e57d3d851f0 1.3
4 1713fb7543386089e364c39703b79e57d3d851f0 1.3
5 80f183ebbe132ea8433eacae9431360f31fe7083 1.4
5 80f183ebbe132ea8433eacae9431360f31fe7083 1.4
6 4330ff5a2bf6c543d8aaae8a43de1dc062f3bd13 1.4.1
6 4330ff5a2bf6c543d8aaae8a43de1dc062f3bd13 1.4.1
7 8531d7b001392289a6b761f38c73a257606552ad 1.5
7 8531d7b001392289a6b761f38c73a257606552ad 1.5
8 78e843c8b04b5a81cee5aa24601e305fae75da24 1.5.1
8 78e843c8b04b5a81cee5aa24601e305fae75da24 1.5.1
9 4f92838730ed9aa1d17651bbcdca19a097fd0c37 1.6
9 4f92838730ed9aa1d17651bbcdca19a097fd0c37 1.6
10 4bac2f37ea463337ddd27f98e7985407a74de504 1.7
10 4bac2f37ea463337ddd27f98e7985407a74de504 1.7
11 1c4febea92c6503ae557fba73b2768659ae90d24 1.7.1
11 1c4febea92c6503ae557fba73b2768659ae90d24 1.7.1
12 56a4a4578fc454ee455e33dd74a2cc82234bcb59 1.7.2
12 56a4a4578fc454ee455e33dd74a2cc82234bcb59 1.7.2
13 34d6f3d5deb22be56b6c1512ec654bd7f6e03bcc 1.7.3
13 34d6f3d5deb22be56b6c1512ec654bd7f6e03bcc 1.7.3
14 f5cca33d29c673b67d43f310bebc4e3a21c6d04c 1.7.4
14 f5cca33d29c673b67d43f310bebc4e3a21c6d04c 1.7.4
15 7f7c33ba6e3f3797ca866c5ed5d358a2393f1371 1.8
15 7f7c33ba6e3f3797ca866c5ed5d358a2393f1371 1.8
16 a6b9dd9547bdc17b681502efcceb17aa5c09adf4 1.8.1
16 a6b9dd9547bdc17b681502efcceb17aa5c09adf4 1.8.1
17 8318fa1615d1946e4519f5735ae880909521990d 2.0
17 8318fa1615d1946e4519f5735ae880909521990d 2.0
18 e23590ee7e2067a3f0fc3cbcfd66404b47127feb 2.1
18 e23590ee7e2067a3f0fc3cbcfd66404b47127feb 2.1
19 4d998aba79e4abf0a2e78e93baaa2c2800b1c49c 2.2
19 4d998aba79e4abf0a2e78e93baaa2c2800b1c49c 2.2
20 07fdef4ac33a859250d03f17c594089792bca615 2.2.1
20 07fdef4ac33a859250d03f17c594089792bca615 2.2.1
21 bcc74d45f060ecd3ff06ff448165aea0d026cb3e 2.2.2
21 bcc74d45f060ecd3ff06ff448165aea0d026cb3e 2.2.2
22 b0e629ff24eb47a449ecfb455dc6cc600d18c9e2 2.2.3
22 b0e629ff24eb47a449ecfb455dc6cc600d18c9e2 2.2.3
23 1b52ba60f17fd7c90912c14d9d17e880b7952d01 2.2.4
23 1b52ba60f17fd7c90912c14d9d17e880b7952d01 2.2.4
24 957e2fec91468f739b0fc2b9936d564505048c68 2.3.0
24 957e2fec91468f739b0fc2b9936d564505048c68 2.3.0
25 bb91141c6ea5c822ccbe2d46c3c48bdab683b77d 2.4.0
25 bb91141c6ea5c822ccbe2d46c3c48bdab683b77d 2.4.0
26 97eb184637e5691b288eaf6b03e8971f3364c239 2.5.0
26 97eb184637e5691b288eaf6b03e8971f3364c239 2.5.0
27 119fafc5381b933bf30d97be0b278349f6135075 2.5.1
27 119fafc5381b933bf30d97be0b278349f6135075 2.5.1
28 d528d76d3242cced614fa11bb63f3d342e4e1d09 2.5.2
28 d528d76d3242cced614fa11bb63f3d342e4e1d09 2.5.2
29 1b631781ced34fbdeec032e7674bc4e131724699 2.6.0
29 1b631781ced34fbdeec032e7674bc4e131724699 2.6.0
30 0f2ef17dc0de678ada279bf7eedf6c5585f1fd7a 2.6.1
30 0f2ef17dc0de678ada279bf7eedf6c5585f1fd7a 2.6.1
31 d53fc814a424d7fd90f23025c87b87baa164450e 2.7.0
31 d53fc814a424d7fd90f23025c87b87baa164450e 2.7.0
32 836d8bb9fcd930b952b9a02029442c71c2441983 2.8.0
32 836d8bb9fcd930b952b9a02029442c71c2441983 2.8.0
33 dfb6c481b1a2c33705de9a9b5304bc924c46b202 2.8.1
33 dfb6c481b1a2c33705de9a9b5304bc924c46b202 2.8.1
34 4a5bec08ccfb47a27f9e98698f12dd5b7246623b 2.8.2
34 4a5bec08ccfb47a27f9e98698f12dd5b7246623b 2.8.2
35 604935b98f5b5e4a5e903594f048046e1fbb3519 2.8.3
35 604935b98f5b5e4a5e903594f048046e1fbb3519 2.8.3
36 c48ffdc671566069ed0f33644da1229277f3cd18 2.9.0
36 c48ffdc671566069ed0f33644da1229277f3cd18 2.9.0
37 d66dc192d4e089ba85325afeef5229b73cb0fde4 2.10.0
37 d66dc192d4e089ba85325afeef5229b73cb0fde4 2.10.0
38 1c22a38cca9ae3bee13d6f263792c0629d0061f6 2.10.1
38 1c22a38cca9ae3bee13d6f263792c0629d0061f6 2.10.1
39 3076e0d03339f3b41dcc71fb6af2b4169920846c 2.11.0
39 3076e0d03339f3b41dcc71fb6af2b4169920846c 2.11.0
40 9cffa58fae74952b8ffe70328af88a5df17059c1 2.12.0
40 9cffa58fae74952b8ffe70328af88a5df17059c1 2.12.0
41 f5caa9e46201ed5b3f1e31655fb4d57bc1b89ab1 3.0.0
41 f5caa9e46201ed5b3f1e31655fb4d57bc1b89ab1 3.0.0
42 df2ee5df6e73363c8d8fd8f22b87e1a2b21544d4 3.1.0
42 df2ee5df6e73363c8d8fd8f22b87e1a2b21544d4 3.1.0
43 3504151c4799f4e33fb7ff846b119d5693c74145 3.2.0
43 3504151c4799f4e33fb7ff846b119d5693c74145 3.2.0
44 a03f50d9723e618d011fde7dcc7288bc6861346e 3.2.1
44 a03f50d9723e618d011fde7dcc7288bc6861346e 3.2.1
45 507a67acbf2e8dc287ba796fa7e5700f8f725bac 3.2.2
45 507a67acbf2e8dc287ba796fa7e5700f8f725bac 3.2.2
46 1376f5fc44354b4dff69631ad187d57690c0d460 3.3.0
46 1376f5fc44354b4dff69631ad187d57690c0d460 3.3.0
47 21e5d408a1a59aec0e3a97cb206d70c8ce34e9b8 3.3.1
47 21e5d408a1a59aec0e3a97cb206d70c8ce34e9b8 3.3.1
48 f2d19a1cde13d82a3803a7d73a4f9c114ed00e7e 3.3.2
48 f2d19a1cde13d82a3803a7d73a4f9c114ed00e7e 3.3.2
49 bb195ee1fe07d68b6fccfdde1dbe7c4dd430e15d 3.3.3
49 bb195ee1fe07d68b6fccfdde1dbe7c4dd430e15d 3.3.3
50 19785af352684884f94566785ba1b13b3ddc5216 3.4.0
50 19785af352684884f94566785ba1b13b3ddc5216 3.4.0
51 757b4ada4ca121db4296b13ec0df491645db8fd0 3.5.0
51 757b4ada4ca121db4296b13ec0df491645db8fd0 3.5.0
52 3da1a2d02072eec5419956265dcd8c7f47155c12 4.0.0
52 3da1a2d02072eec5419956265dcd8c7f47155c12 4.0.0
53 da8f0f9d5099ee8b22aa317b91beb06543011f1d 4.1.0
53 da8f0f9d5099ee8b22aa317b91beb06543011f1d 4.1.0
54 9619ecc0f79b705c94b3ac873896809eaf7779a6 4.1.1
@@ -1,31 +1,66 b''
1 import xml.etree.ElementTree as et
1 import xml.etree.ElementTree as et
2
2
3 from boards.models import Post
3 from boards.models import Post, Tag
4
4
5 TAG_THREAD = 'thread'
5 TAG_THREAD = 'thread'
6 TAG_TAGS = 'tags'
7 TAG_TAG = 'tag'
8 TAG_TIME_FROM = 'timestamp_from'
6
9
7
10
8 class PostFilter:
11 class PostFilter:
9 def __init__(self, content=None):
12 def __init__(self, content=None):
10 self.content = content
13 self.content = content
11
14
12 def filter(self, posts):
15 def filter(self, posts):
13 return posts
16 return posts
14
17
15 def add_filter(self, model_tag, value):
18 def add_filter(self, model_tag, value):
16 return model_tag
19 return model_tag
17
20
18
21
19 class ThreadFilter(PostFilter):
22 class ThreadFilter(PostFilter):
20 def filter(self, posts):
23 def filter(self, posts):
21 op_id = self.content.text
24 op_id = self.content.text
22
25
23 op = Post.objects.filter(opening=True, id=op_id).first()
26 op = Post.objects.filter(opening=True, id=op_id).first()
24 if op:
27 if op:
25 return posts.filter(thread=op.get_thread())
28 return posts.filter(thread=op.get_thread())
26 else:
29 else:
27 return posts.none()
30 return posts.none()
28
31
29 def add_filter(self, model_tag, value):
32 def add_filter(self, model_tag, value):
30 thread_tag = et.SubElement(model_tag, TAG_THREAD)
33 thread_tag = et.SubElement(model_tag, TAG_THREAD)
31 thread_tag.text = str(value)
34 thread_tag.text = str(value)
35
36
37 class TagsFilter(PostFilter):
38 def filter(self, posts):
39 tags = []
40 for tag_tag in self.content:
41 try:
42 tags.append(Tag.objects.get(name=tag_tag.text))
43 except Tag.DoesNotExist:
44 pass
45
46 if tags:
47 return posts.filter(thread__tags__in=tags)
48 else:
49 return posts.none()
50
51 def add_filter(self, model_tag, value):
52 tags_tag = et.SubElement(model_tag, TAG_TAGS)
53 for tag_name in value:
54 tag_tag = et.SubElement(tags_tag, TAG_TAG)
55 tag_tag.text = tag_name
56
57
58 class TimestampFromFilter(PostFilter):
59 def filter(self, posts):
60 from_time = self.content.text
61 return posts.filter(pub_time__gt=from_time)
62
63 def add_filter(self, model_tag, value):
64 tags_from_time = et.SubElement(model_tag, TAG_TIME_FROM)
65 tags_from_time.text = value
66
@@ -1,49 +1,49 b''
1 [Version]
1 [Version]
2 Version = 4.1.0 2017
2 Version = 4.1.1 2017
3 SiteName = Neboard DEV
3 SiteName = Neboard DEV
4
4
5 [Cache]
5 [Cache]
6 # Timeout for caching, if cache is used
6 # Timeout for caching, if cache is used
7 CacheTimeout = 600
7 CacheTimeout = 600
8
8
9 [Forms]
9 [Forms]
10 # Max post length in characters
10 # Max post length in characters
11 MaxTextLength = 30000
11 MaxTextLength = 30000
12 MaxFileSize = 8000000
12 MaxFileSize = 8000000
13 LimitFirstPosting = true
13 LimitFirstPosting = true
14 LimitPostingSpeed = false
14 LimitPostingSpeed = false
15 PowDifficulty = 0
15 PowDifficulty = 0
16 # Delay in seconds
16 # Delay in seconds
17 PostingDelay = 30
17 PostingDelay = 30
18 Autoban = false
18 Autoban = false
19 DefaultTag = test
19 DefaultTag = test
20 MaxFileCount = 1
20 MaxFileCount = 1
21 AdditionalSpoilerSpaces = false
21 AdditionalSpoilerSpaces = false
22
22
23 [Messages]
23 [Messages]
24 # Thread bumplimit
24 # Thread bumplimit
25 MaxPostsPerThread = 10
25 MaxPostsPerThread = 10
26 ThreadArchiveDays = 300
26 ThreadArchiveDays = 300
27 AnonymousMode = false
27 AnonymousMode = false
28
28
29 [View]
29 [View]
30 DefaultTheme = md
30 DefaultTheme = md
31 DefaultImageViewer = simple
31 DefaultImageViewer = simple
32 LastRepliesCount = 3
32 LastRepliesCount = 3
33 ThreadsPerPage = 3
33 ThreadsPerPage = 3
34 PostsPerPage = 10
34 PostsPerPage = 10
35 ImagesPerPageGallery = 20
35 ImagesPerPageGallery = 20
36 MaxFavoriteThreads = 20
36 MaxFavoriteThreads = 20
37 MaxLandingThreads = 20
37 MaxLandingThreads = 20
38 Themes=md:Mystic Dark,md_centered:Mystic Dark (centered),sw:Snow White,pg:Photon Grey,ad:Amanita Dark,iw:Inocibe White
38 Themes=md:Mystic Dark,md_centered:Mystic Dark (centered),sw:Snow White,pg:Photon Grey,ad:Amanita Dark,iw:Inocibe White
39 ImageViewers=simple:Simple,popup:Popup
39 ImageViewers=simple:Simple,popup:Popup
40
40
41 [Storage]
41 [Storage]
42 # Enable archiving threads instead of deletion when the thread limit is reached
42 # Enable archiving threads instead of deletion when the thread limit is reached
43 ArchiveThreads = true
43 ArchiveThreads = true
44
44
45 [RSS]
45 [RSS]
46 MaxItems = 20
46 MaxItems = 20
47
47
48 [External]
48 [External]
49 ImageSearchHost=
49 ImageSearchHost=
@@ -1,99 +1,115 b''
1 import re
1 import re
2 import logging
2 import logging
3 import xml.etree.ElementTree as ET
3 import xml.etree.ElementTree as ET
4
4
5 import httplib2
5 import httplib2
6 from django.core.management import BaseCommand
6 from django.core.management import BaseCommand
7
7
8 from boards.models import GlobalId
8 from boards.models import GlobalId
9 from boards.models.post.sync import SyncManager, TAG_ID, TAG_VERSION
9 from boards.models.post.sync import SyncManager, TAG_ID, TAG_VERSION
10
10
11 __author__ = 'neko259'
11 __author__ = 'neko259'
12
12
13
13
14 REGEX_GLOBAL_ID = re.compile(r'(\w+)::([\w\+/]+)::(\d+)')
14 REGEX_GLOBAL_ID = re.compile(r'(\w+)::([\w\+/]+)::(\d+)')
15
15
16
16
17 class Command(BaseCommand):
17 class Command(BaseCommand):
18 help = 'Send a sync or get request to the server.'
18 help = 'Send a sync or get request to the server.'
19
19
20 def add_arguments(self, parser):
20 def add_arguments(self, parser):
21 parser.add_argument('url', type=str, help='Server root url')
21 parser.add_argument('url', type=str, help='Server root url')
22 parser.add_argument('--global-id', type=str, default='',
22 parser.add_argument('--global-id', type=str, default='',
23 help='Post global ID')
23 help='Post global ID')
24 parser.add_argument('--split-query', type=int, default=1,
24 parser.add_argument('--split-query', type=int, default=1,
25 help='Split GET query into separate by the given'
25 help='Split GET query into separate by the given'
26 ' number of posts in one')
26 ' number of posts in one')
27 parser.add_argument('--thread', type=int,
27 parser.add_argument('--thread', type=int,
28 help='Get posts of one specific thread')
28 help='Get posts of one specific thread')
29 parser.add_argument('--tags', type=str,
30 help='Get posts of the tags, comma-separated')
31 parser.add_argument('--time-from', type=str,
32 help='Get posts from the given timestamp')
29
33
30 def handle(self, *args, **options):
34 def handle(self, *args, **options):
31 logger = logging.getLogger('boards.sync')
35 logger = logging.getLogger('boards.sync')
32
36
33 url = options.get('url')
37 url = options.get('url')
34
38
35 list_url = url + 'api/sync/list/'
39 list_url = url + 'api/sync/list/'
36 get_url = url + 'api/sync/get/'
40 get_url = url + 'api/sync/get/'
37 file_url = url[:-1]
41 file_url = url[:-1]
38
42
39 global_id_str = options.get('global_id')
43 global_id_str = options.get('global_id')
40 if global_id_str:
44 if global_id_str:
41 match = REGEX_GLOBAL_ID.match(global_id_str)
45 match = REGEX_GLOBAL_ID.match(global_id_str)
42 if match:
46 if match:
43 key_type = match.group(1)
47 key_type = match.group(1)
44 key = match.group(2)
48 key = match.group(2)
45 local_id = match.group(3)
49 local_id = match.group(3)
46
50
47 global_id = GlobalId(key_type=key_type, key=key,
51 global_id = GlobalId(key_type=key_type, key=key,
48 local_id=local_id)
52 local_id=local_id)
49
53
50 xml = SyncManager.generate_request_get([global_id])
54 xml = SyncManager.generate_request_get([global_id])
51 h = httplib2.Http()
55 h = httplib2.Http()
52 response, content = h.request(get_url, method="POST", body=xml)
56 response, content = h.request(get_url, method="POST", body=xml)
53
57
54 SyncManager.parse_response_get(content, file_url)
58 SyncManager.parse_response_get(content, file_url)
55 else:
59 else:
56 raise Exception('Invalid global ID')
60 raise Exception('Invalid global ID')
57 else:
61 else:
58 logger.info('Running LIST request...')
62 logger.info('Running LIST request...')
59 h = httplib2.Http()
63 h = httplib2.Http()
64
65 tags = []
66 tags_str = options.get('tags')
67 if tags_str:
68 tags = tags_str.split(',')
69
60 xml = SyncManager.generate_request_list(
70 xml = SyncManager.generate_request_list(
61 opening_post=options.get('thread'))
71 opening_post=options.get('thread'), tags=tags,
72 timestamp_from=options.get('time_from')).encode()
62 response, content = h.request(list_url, method="POST", body=xml)
73 response, content = h.request(list_url, method="POST", body=xml)
74 if response.status != 200:
75 raise Exception('Server returned error {}'.format(response.status))
76
63 logger.info('Processing response...')
77 logger.info('Processing response...')
64
78
65 root = ET.fromstring(content)
79 root = ET.fromstring(content)
66 status = root.findall('status')[0].text
80 status = root.findall('status')[0].text
67 if status == 'success':
81 if status == 'success':
68 ids_to_sync = list()
82 ids_to_sync = list()
69
83
70 models = root.findall('models')[0]
84 models = root.findall('models')[0]
71 for model in models:
85 for model in models:
72 tag_id = model.find(TAG_ID)
86 tag_id = model.find(TAG_ID)
73 global_id, exists = GlobalId.from_xml_element(tag_id)
87 global_id, exists = GlobalId.from_xml_element(tag_id)
74 tag_version = model.find(TAG_VERSION)
88 tag_version = model.find(TAG_VERSION)
75 if tag_version is not None:
89 if tag_version is not None:
76 version = int(tag_version.text) or 1
90 version = int(tag_version.text) or 1
77 else:
91 else:
78 version = 1
92 version = 1
79 if not exists or global_id.post.version < version:
93 if not exists or global_id.post.version < version:
80 logger.debug('Processed (+) post {}'.format(global_id))
94 logger.debug('Processed (+) post {}'.format(global_id))
81 ids_to_sync.append(global_id)
95 ids_to_sync.append(global_id)
82 else:
96 else:
83 logger.debug('* Processed (-) post {}'.format(global_id))
97 logger.debug('* Processed (-) post {}'.format(global_id))
84 logger.info('Starting sync...')
98 logger.info('Starting sync...')
85
99
86 if len(ids_to_sync) > 0:
100 if len(ids_to_sync) > 0:
87 limit = options.get('split_query', len(ids_to_sync))
101 limit = options.get('split_query', len(ids_to_sync))
88 for offset in range(0, len(ids_to_sync), limit):
102 for offset in range(0, len(ids_to_sync), limit):
89 xml = SyncManager.generate_request_get(ids_to_sync[offset:offset + limit])
103 xml = SyncManager.generate_request_get(ids_to_sync[offset:offset + limit])
90 h = httplib2.Http()
104 h = httplib2.Http()
91 logger.info('Running GET request...')
105 logger.info('Running GET request...')
92 response, content = h.request(get_url, method="POST", body=xml)
106 response, content = h.request(get_url, method="POST", body=xml)
93 logger.info('Processing response...')
107 logger.info('Processing response...')
94
108
95 SyncManager.parse_response_get(content, file_url)
109 SyncManager.parse_response_get(content, file_url)
110
111 logger.info('Sync completed successfully')
96 else:
112 else:
97 logger.info('Nothing to get, everything synced')
113 logger.info('Nothing to get, everything synced')
98 else:
114 else:
99 raise Exception('Invalid response status')
115 raise Exception('Invalid response status')
@@ -1,345 +1,391 b''
1 import xml.etree.ElementTree as et
1 import xml.etree.ElementTree as et
2 import logging
2 import logging
3 from xml.etree import ElementTree
3 from xml.etree import ElementTree
4
4
5 from boards.abstracts.exceptions import SyncException
5 from boards.abstracts.exceptions import SyncException
6 from boards.abstracts.sync_filters import ThreadFilter
6 from boards.abstracts.sync_filters import ThreadFilter, TagsFilter,\
7 TimestampFromFilter
7 from boards.models import KeyPair, GlobalId, Signature, Post, Tag
8 from boards.models import KeyPair, GlobalId, Signature, Post, Tag
8 from boards.models.attachment.downloaders import download
9 from boards.models.attachment.downloaders import download
9 from boards.models.signature import TAG_REQUEST, ATTR_TYPE, TYPE_GET, \
10 from boards.models.signature import TAG_REQUEST, ATTR_TYPE, TYPE_GET, \
10 ATTR_VERSION, TAG_MODEL, ATTR_NAME, TAG_ID, TYPE_LIST
11 ATTR_VERSION, TAG_MODEL, ATTR_NAME, TAG_ID, TYPE_LIST
11 from boards.utils import get_file_mimetype, get_file_hash
12 from boards.utils import get_file_mimetype, get_file_hash
12 from django.db import transaction
13 from django.db import transaction
13 from django import forms
14 from django import forms
14
15
15 EXCEPTION_NODE = 'Sync node returned an error: {}.'
16 EXCEPTION_NODE = 'Sync node returned an error: {}.'
16 EXCEPTION_DOWNLOAD = 'File was not downloaded.'
17 EXCEPTION_DOWNLOAD = 'File was not downloaded.'
17 EXCEPTION_HASH = 'File hash does not match attachment hash.'
18 EXCEPTION_HASH = 'File hash does not match attachment hash.'
18 EXCEPTION_SIGNATURE = 'Invalid model signature for {}.'
19 EXCEPTION_SIGNATURE = 'Invalid model signature for {}.'
19 EXCEPTION_AUTHOR_SIGNATURE = 'Model {} has no author signature.'
20 EXCEPTION_AUTHOR_SIGNATURE = 'Model {} has no author signature.'
21 EXCEPTION_THREAD = 'No thread exists for post {}'
20 ENCODING_UNICODE = 'unicode'
22 ENCODING_UNICODE = 'unicode'
21
23
22 TAG_MODEL = 'model'
24 TAG_MODEL = 'model'
23 TAG_REQUEST = 'request'
25 TAG_REQUEST = 'request'
24 TAG_RESPONSE = 'response'
26 TAG_RESPONSE = 'response'
25 TAG_ID = 'id'
27 TAG_ID = 'id'
26 TAG_STATUS = 'status'
28 TAG_STATUS = 'status'
27 TAG_MODELS = 'models'
29 TAG_MODELS = 'models'
28 TAG_TITLE = 'title'
30 TAG_TITLE = 'title'
29 TAG_TEXT = 'text'
31 TAG_TEXT = 'text'
30 TAG_THREAD = 'thread'
32 TAG_THREAD = 'thread'
31 TAG_PUB_TIME = 'pub-time'
33 TAG_PUB_TIME = 'pub-time'
32 TAG_SIGNATURES = 'signatures'
34 TAG_SIGNATURES = 'signatures'
33 TAG_SIGNATURE = 'signature'
35 TAG_SIGNATURE = 'signature'
34 TAG_CONTENT = 'content'
36 TAG_CONTENT = 'content'
35 TAG_ATTACHMENTS = 'attachments'
37 TAG_ATTACHMENTS = 'attachments'
36 TAG_ATTACHMENT = 'attachment'
38 TAG_ATTACHMENT = 'attachment'
37 TAG_TAGS = 'tags'
39 TAG_TAGS = 'tags'
38 TAG_TAG = 'tag'
40 TAG_TAG = 'tag'
39 TAG_ATTACHMENT_REFS = 'attachment-refs'
41 TAG_ATTACHMENT_REFS = 'attachment-refs'
40 TAG_ATTACHMENT_REF = 'attachment-ref'
42 TAG_ATTACHMENT_REF = 'attachment-ref'
41 TAG_TRIPCODE = 'tripcode'
43 TAG_TRIPCODE = 'tripcode'
42 TAG_VERSION = 'version'
44 TAG_VERSION = 'version'
43
45
44 TYPE_GET = 'get'
46 TYPE_GET = 'get'
45
47
46 ATTR_VERSION = 'version'
48 ATTR_VERSION = 'version'
47 ATTR_TYPE = 'type'
49 ATTR_TYPE = 'type'
48 ATTR_NAME = 'name'
50 ATTR_NAME = 'name'
49 ATTR_VALUE = 'value'
51 ATTR_VALUE = 'value'
50 ATTR_MIMETYPE = 'mimetype'
52 ATTR_MIMETYPE = 'mimetype'
51 ATTR_KEY = 'key'
53 ATTR_KEY = 'key'
52 ATTR_REF = 'ref'
54 ATTR_REF = 'ref'
53 ATTR_URL = 'url'
55 ATTR_URL = 'url'
54 ATTR_ID_TYPE = 'id-type'
56 ATTR_ID_TYPE = 'id-type'
55
57
56 ID_TYPE_MD5 = 'md5'
58 ID_TYPE_MD5 = 'md5'
57 ID_TYPE_URL = 'url'
59 ID_TYPE_URL = 'url'
58
60
59 STATUS_SUCCESS = 'success'
61 STATUS_SUCCESS = 'success'
60
62
61
63
62 logger = logging.getLogger('boards.sync')
64 logger = logging.getLogger('boards.sync')
63
65
64
66
65 class SyncManager:
67 class SyncManager:
66 @staticmethod
68 @staticmethod
67 def generate_response_get(model_list: list):
69 def generate_response_get(model_list: list):
68 response = et.Element(TAG_RESPONSE)
70 response = et.Element(TAG_RESPONSE)
69
71
70 status = et.SubElement(response, TAG_STATUS)
72 status = et.SubElement(response, TAG_STATUS)
71 status.text = STATUS_SUCCESS
73 status.text = STATUS_SUCCESS
72
74
73 models = et.SubElement(response, TAG_MODELS)
75 models = et.SubElement(response, TAG_MODELS)
74
76
75 for post in model_list:
77 for post in model_list:
76 model = et.SubElement(models, TAG_MODEL)
78 model = et.SubElement(models, TAG_MODEL)
77 model.set(ATTR_NAME, 'post')
79 model.set(ATTR_NAME, 'post')
78
80
79 global_id = post.global_id
81 global_id = post.global_id
80
82
81 attachments = post.attachments.all()
83 attachments = post.attachments.all()
82 if global_id.content:
84 if global_id.content:
83 model.append(et.fromstring(global_id.content))
85 model.append(et.fromstring(global_id.content))
84 if len(attachments) > 0:
86 if len(attachments) > 0:
85 internal_attachments = False
87 internal_attachments = False
86 for attachment in attachments:
88 for attachment in attachments:
87 if attachment.is_internal():
89 if attachment.is_internal():
88 internal_attachments = True
90 internal_attachments = True
89 break
91 break
90
92
91 if internal_attachments:
93 if internal_attachments:
92 attachment_refs = et.SubElement(model, TAG_ATTACHMENT_REFS)
94 attachment_refs = et.SubElement(model, TAG_ATTACHMENT_REFS)
93 for file in attachments:
95 for file in attachments:
94 SyncManager._attachment_to_xml(
96 SyncManager._attachment_to_xml(
95 None, attachment_refs, file)
97 None, attachment_refs, file)
96 else:
98 else:
97 content_tag = et.SubElement(model, TAG_CONTENT)
99 content_tag = et.SubElement(model, TAG_CONTENT)
98
100
99 tag_id = et.SubElement(content_tag, TAG_ID)
101 tag_id = et.SubElement(content_tag, TAG_ID)
100 global_id.to_xml_element(tag_id)
102 global_id.to_xml_element(tag_id)
101
103
102 title = et.SubElement(content_tag, TAG_TITLE)
104 title = et.SubElement(content_tag, TAG_TITLE)
103 title.text = post.title
105 title.text = post.title
104
106
105 text = et.SubElement(content_tag, TAG_TEXT)
107 text = et.SubElement(content_tag, TAG_TEXT)
106 text.text = post.get_sync_text()
108 text.text = post.get_sync_text()
107
109
108 thread = post.get_thread()
110 thread = post.get_thread()
109 if post.is_opening():
111 if post.is_opening():
110 tag_tags = et.SubElement(content_tag, TAG_TAGS)
112 tag_tags = et.SubElement(content_tag, TAG_TAGS)
111 for tag in thread.get_tags():
113 for tag in thread.get_tags():
112 tag_tag = et.SubElement(tag_tags, TAG_TAG)
114 tag_tag = et.SubElement(tag_tags, TAG_TAG)
113 tag_tag.text = tag.name
115 tag_tag.text = tag.name
114 else:
116 else:
115 tag_thread = et.SubElement(content_tag, TAG_THREAD)
117 tag_thread = et.SubElement(content_tag, TAG_THREAD)
116 thread_id = et.SubElement(tag_thread, TAG_ID)
118 thread_id = et.SubElement(tag_thread, TAG_ID)
117 thread.get_opening_post().global_id.to_xml_element(thread_id)
119 thread.get_opening_post().global_id.to_xml_element(thread_id)
118
120
119 pub_time = et.SubElement(content_tag, TAG_PUB_TIME)
121 pub_time = et.SubElement(content_tag, TAG_PUB_TIME)
120 pub_time.text = str(post.get_pub_time_str())
122 pub_time.text = str(post.get_pub_time_str())
121
123
122 if post.tripcode:
124 if post.tripcode:
123 tripcode = et.SubElement(content_tag, TAG_TRIPCODE)
125 tripcode = et.SubElement(content_tag, TAG_TRIPCODE)
124 tripcode.text = post.tripcode
126 tripcode.text = post.tripcode
125
127
126 if len(attachments) > 0:
128 if len(attachments) > 0:
127 attachments_tag = et.SubElement(content_tag, TAG_ATTACHMENTS)
129 attachments_tag = et.SubElement(content_tag, TAG_ATTACHMENTS)
128
130
129 internal_attachments = False
131 internal_attachments = False
130 for attachment in attachments:
132 for attachment in attachments:
131 if attachment.is_internal():
133 if attachment.is_internal():
132 internal_attachments = True
134 internal_attachments = True
133 break
135 break
134
136
135 if internal_attachments:
137 if internal_attachments:
136 attachment_refs = et.SubElement(model, TAG_ATTACHMENT_REFS)
138 attachment_refs = et.SubElement(model, TAG_ATTACHMENT_REFS)
137 else:
139 else:
138 attachment_refs = None
140 attachment_refs = None
139
141
140 for file in attachments:
142 for file in attachments:
141 SyncManager._attachment_to_xml(
143 SyncManager._attachment_to_xml(
142 attachments_tag, attachment_refs, file)
144 attachments_tag, attachment_refs, file)
143 version_tag = et.SubElement(content_tag, TAG_VERSION)
145 version_tag = et.SubElement(content_tag, TAG_VERSION)
144 version_tag.text = str(post.version)
146 version_tag.text = str(post.version)
145
147
146 global_id.content = et.tostring(content_tag, ENCODING_UNICODE)
148 global_id.content = et.tostring(content_tag, ENCODING_UNICODE)
147 global_id.save()
149 global_id.save()
148
150
149 signatures_tag = et.SubElement(model, TAG_SIGNATURES)
151 signatures_tag = et.SubElement(model, TAG_SIGNATURES)
150 post_signatures = global_id.signature_set.all()
152 post_signatures = global_id.signature_set.all()
151 if post_signatures:
153 if post_signatures:
152 signatures = post_signatures
154 signatures = post_signatures
153 else:
155 else:
154 key = KeyPair.objects.get(public_key=global_id.key)
156 key = KeyPair.objects.get(public_key=global_id.key)
155 signature = Signature(
157 signature = Signature(
156 key_type=key.key_type,
158 key_type=key.key_type,
157 key=key.public_key,
159 key=key.public_key,
158 signature=key.sign(global_id.content),
160 signature=key.sign(global_id.content),
159 global_id=global_id,
161 global_id=global_id,
160 )
162 )
161 signature.save()
163 signature.save()
162 signatures = [signature]
164 signatures = [signature]
163 for signature in signatures:
165 for signature in signatures:
164 signature_tag = et.SubElement(signatures_tag, TAG_SIGNATURE)
166 signature_tag = et.SubElement(signatures_tag, TAG_SIGNATURE)
165 signature_tag.set(ATTR_TYPE, signature.key_type)
167 signature_tag.set(ATTR_TYPE, signature.key_type)
166 signature_tag.set(ATTR_VALUE, signature.signature)
168 signature_tag.set(ATTR_VALUE, signature.signature)
167 signature_tag.set(ATTR_KEY, signature.key)
169 signature_tag.set(ATTR_KEY, signature.key)
168
170
169 return et.tostring(response, ENCODING_UNICODE)
171 return et.tostring(response, ENCODING_UNICODE)
170
172
171 @staticmethod
173 @staticmethod
172 def parse_response_get(response_xml, hostname):
174 def parse_response_get(response_xml, hostname):
173 tag_root = et.fromstring(response_xml)
175 tag_root = et.fromstring(response_xml)
174 tag_status = tag_root.find(TAG_STATUS)
176 tag_status = tag_root.find(TAG_STATUS)
175 if STATUS_SUCCESS == tag_status.text:
177 if STATUS_SUCCESS == tag_status.text:
176 tag_models = tag_root.find(TAG_MODELS)
178 tag_models = tag_root.find(TAG_MODELS)
177 for tag_model in tag_models:
179 for tag_model in tag_models:
178 SyncManager.parse_post(tag_model, hostname)
180 SyncManager.parse_post(tag_model, hostname)
179 else:
181 else:
180 raise SyncException(EXCEPTION_NODE.format(tag_status.text))
182 raise SyncException(EXCEPTION_NODE.format(tag_status.text))
181
183
182 @staticmethod
184 @staticmethod
183 @transaction.atomic
185 @transaction.atomic
184 def parse_post(tag_model, hostname):
186 def parse_post(tag_model, hostname):
185 tag_content = tag_model.find(TAG_CONTENT)
187 tag_content = tag_model.find(TAG_CONTENT)
186
188
187 content_str = et.tostring(tag_content, ENCODING_UNICODE)
189 content_str = et.tostring(tag_content, ENCODING_UNICODE)
188
190
189 tag_id = tag_content.find(TAG_ID)
191 tag_id = tag_content.find(TAG_ID)
190 global_id, exists = GlobalId.from_xml_element(tag_id)
192 global_id, exists = GlobalId.from_xml_element(tag_id)
191 signatures = SyncManager._verify_model(global_id, content_str, tag_model)
193 signatures = SyncManager._verify_model(global_id, content_str, tag_model)
192
194
193 version = int(tag_content.find(TAG_VERSION).text)
195 version = int(tag_content.find(TAG_VERSION).text)
194 is_old = exists and global_id.post.version < version
196 is_old = exists and global_id.post.version < version
195 if exists and not is_old:
197 if exists and not is_old:
196 print('Post with same ID exists and is up to date.')
198 logger.debug('Post {} exists and is up to date.'.format(global_id))
197 else:
199 else:
198 global_id.content = content_str
200 global_id.content = content_str
199 global_id.save()
201 global_id.save()
200 for signature in signatures:
202 for signature in signatures:
201 signature.global_id = global_id
203 signature.global_id = global_id
202 signature.save()
204 signature.save()
203
205
204 title = tag_content.find(TAG_TITLE).text or ''
206 title = tag_content.find(TAG_TITLE).text or ''
205 text = tag_content.find(TAG_TEXT).text or ''
207 text = tag_content.find(TAG_TEXT).text or ''
206 pub_time = tag_content.find(TAG_PUB_TIME).text
208 pub_time = tag_content.find(TAG_PUB_TIME).text
207 tripcode_tag = tag_content.find(TAG_TRIPCODE)
209 tripcode_tag = tag_content.find(TAG_TRIPCODE)
208 if tripcode_tag is not None:
210 if tripcode_tag is not None:
209 tripcode = tripcode_tag.text or ''
211 tripcode = tripcode_tag.text or ''
210 else:
212 else:
211 tripcode = ''
213 tripcode = ''
212
214
213 thread = tag_content.find(TAG_THREAD)
215 thread = tag_content.find(TAG_THREAD)
214 tags = []
216 tags = []
215 if thread:
217 if thread:
216 thread_id = thread.find(TAG_ID)
218 thread_id = thread.find(TAG_ID)
217 op_global_id, exists = GlobalId.from_xml_element(thread_id)
219 op_global_id, exists = GlobalId.from_xml_element(thread_id)
218 if exists:
220 if exists:
219 opening_post = Post.objects.get(global_id=op_global_id)
221 opening_post = Post.objects.get(global_id=op_global_id)
220 else:
222 else:
221 logger.debug('No thread exists for post {}'.format(global_id))
223 raise Exception(EXCEPTION_THREAD.format(global_id))
222 else:
224 else:
223 opening_post = None
225 opening_post = None
224 tag_tags = tag_content.find(TAG_TAGS)
226 tag_tags = tag_content.find(TAG_TAGS)
225 for tag_tag in tag_tags:
227 for tag_tag in tag_tags:
226 tag, created = Tag.objects.get_or_create(
228 tag, created = Tag.objects.get_or_create(
227 name=tag_tag.text)
229 name=tag_tag.text)
228 tags.append(tag)
230 tags.append(tag)
229
231
230 # TODO Check that the replied posts are already present
232 # TODO Check that the replied posts are already present
231 # before adding new ones
233 # before adding new ones
232
234
233 files = []
235 files = []
234 urls = []
236 urls = []
235 tag_attachments = tag_content.find(TAG_ATTACHMENTS) or list()
237 tag_attachments = tag_content.find(TAG_ATTACHMENTS) or list()
236 tag_refs = tag_model.find(TAG_ATTACHMENT_REFS)
238 tag_refs = tag_model.find(TAG_ATTACHMENT_REFS)
237 for attachment in tag_attachments:
239 for attachment in tag_attachments:
238 if attachment.get(ATTR_ID_TYPE) == ID_TYPE_URL:
240 if attachment.get(ATTR_ID_TYPE) == ID_TYPE_URL:
239 urls.append(attachment.text)
241 urls.append(attachment.text)
240 else:
242 else:
241 tag_ref = tag_refs.find("{}[@ref='{}']".format(
243 tag_ref = tag_refs.find("{}[@ref='{}']".format(
242 TAG_ATTACHMENT_REF, attachment.text))
244 TAG_ATTACHMENT_REF, attachment.text))
243 url = tag_ref.get(ATTR_URL)
245 url = tag_ref.get(ATTR_URL)
244 try:
246 try:
245 attached_file = download(hostname + url, validate=False)
247 attached_file = download(hostname + url, validate=False)
246
248
247 if attached_file is None:
249 if attached_file is None:
248 raise SyncException(EXCEPTION_DOWNLOAD)
250 raise SyncException(EXCEPTION_DOWNLOAD)
249
251
250 hash = get_file_hash(attached_file)
252 hash = get_file_hash(attached_file)
251 if hash != attachment.text:
253 if hash != attachment.text:
252 raise SyncException(EXCEPTION_HASH)
254 raise SyncException(EXCEPTION_HASH)
253
255
254 files.append(attached_file)
256 files.append(attached_file)
255 except forms.ValidationError:
257 except forms.ValidationError:
256 urls.append(hostname+url)
258 urls.append(hostname+url)
257
259
258
260
259 if is_old:
261 if is_old:
260 post = global_id.post
262 post = global_id.post
261 Post.objects.update_post(
263 Post.objects.update_post(
262 post, title=title, text=text, pub_time=pub_time,
264 post, title=title, text=text, pub_time=pub_time,
263 tags=tags, files=files, file_urls=urls,
265 tags=tags, files=files, file_urls=urls,
264 tripcode=tripcode, version=version)
266 tripcode=tripcode, version=version)
265 logger.debug('Parsed updated post {}'.format(global_id))
267 logger.debug('Parsed updated post {}'.format(global_id))
266 else:
268 else:
267 Post.objects.import_post(
269 Post.objects.import_post(
268 title=title, text=text, pub_time=pub_time,
270 title=title, text=text, pub_time=pub_time,
269 opening_post=opening_post, tags=tags,
271 opening_post=opening_post, tags=tags,
270 global_id=global_id, files=files,
272 global_id=global_id, files=files,
271 file_urls=urls, tripcode=tripcode,
273 file_urls=urls, tripcode=tripcode,
272 version=version)
274 version=version)
273 logger.debug('Parsed new post {}'.format(global_id))
275 logger.debug('Parsed new post {}'.format(global_id))
274
276
275 @staticmethod
277 @staticmethod
276 def generate_response_list(filters):
278 def generate_response_list(filters):
277 response = et.Element(TAG_RESPONSE)
279 response = et.Element(TAG_RESPONSE)
278
280
279 status = et.SubElement(response, TAG_STATUS)
281 status = et.SubElement(response, TAG_STATUS)
280 status.text = STATUS_SUCCESS
282 status.text = STATUS_SUCCESS
281
283
282 models = et.SubElement(response, TAG_MODELS)
284 models = et.SubElement(response, TAG_MODELS)
283
285
284 posts = Post.objects.prefetch_related('global_id')
286 posts = Post.objects.prefetch_related('global_id')
285 for post_filter in filters:
287 for post_filter in filters:
286 posts = post_filter.filter(posts)
288 posts = post_filter.filter(posts)
287
289
288 for post in posts:
290 for post in posts:
289 tag_model = et.SubElement(models, TAG_MODEL)
291 tag_model = et.SubElement(models, TAG_MODEL)
290 tag_id = et.SubElement(tag_model, TAG_ID)
292 tag_id = et.SubElement(tag_model, TAG_ID)
291 post.global_id.to_xml_element(tag_id)
293 post.global_id.to_xml_element(tag_id)
292 tag_version = et.SubElement(tag_model, TAG_VERSION)
294 tag_version = et.SubElement(tag_model, TAG_VERSION)
293 tag_version.text = str(post.version)
295 tag_version.text = str(post.version)
294
296
295 return et.tostring(response, ENCODING_UNICODE)
297 return et.tostring(response, ENCODING_UNICODE)
296
298
297 @staticmethod
299 @staticmethod
298 def _verify_model(global_id, content_str, tag_model):
300 def _verify_model(global_id, content_str, tag_model):
299 """
301 """
300 Verifies all signatures for a single model.
302 Verifies all signatures for a single model.
301 """
303 """
302
304
303 signatures = []
305 signatures = []
304
306
305 tag_signatures = tag_model.find(TAG_SIGNATURES)
307 tag_signatures = tag_model.find(TAG_SIGNATURES)
306 has_author_signature = False
308 has_author_signature = False
307 for tag_signature in tag_signatures:
309 for tag_signature in tag_signatures:
308 signature_type = tag_signature.get(ATTR_TYPE)
310 signature_type = tag_signature.get(ATTR_TYPE)
309 signature_value = tag_signature.get(ATTR_VALUE)
311 signature_value = tag_signature.get(ATTR_VALUE)
310 signature_key = tag_signature.get(ATTR_KEY)
312 signature_key = tag_signature.get(ATTR_KEY)
311
313
312 if global_id.key_type == signature_type and\
314 if global_id.key_type == signature_type and\
313 global_id.key == signature_key:
315 global_id.key == signature_key:
314 has_author_signature = True
316 has_author_signature = True
315
317
316 signature = Signature(key_type=signature_type,
318 signature = Signature(key_type=signature_type,
317 key=signature_key,
319 key=signature_key,
318 signature=signature_value)
320 signature=signature_value)
319
321
320 if not KeyPair.objects.verify(signature, content_str):
322 if not KeyPair.objects.verify(signature, content_str):
321 raise SyncException(EXCEPTION_SIGNATURE.format(content_str))
323 raise SyncException(EXCEPTION_SIGNATURE.format(content_str))
322
324
323 signatures.append(signature)
325 signatures.append(signature)
324 if not has_author_signature:
326 if not has_author_signature:
325 raise SyncException(EXCEPTION_AUTHOR_SIGNATURE.format(content_str))
327 raise SyncException(EXCEPTION_AUTHOR_SIGNATURE.format(content_str))
326
328
327 return signatures
329 return signatures
328
330
329 @staticmethod
331 @staticmethod
330 def _attachment_to_xml(tag_attachments, tag_refs, attachment):
332 def _attachment_to_xml(tag_attachments, tag_refs, attachment):
331 if tag_attachments is not None:
333 if tag_attachments is not None:
332 attachment_tag = et.SubElement(tag_attachments, TAG_ATTACHMENT)
334 attachment_tag = et.SubElement(tag_attachments, TAG_ATTACHMENT)
333 if attachment.is_internal():
335 if attachment.is_internal():
334 mimetype = get_file_mimetype(attachment.file.file)
336 mimetype = get_file_mimetype(attachment.file.file)
335 attachment_tag.set(ATTR_MIMETYPE, mimetype)
337 attachment_tag.set(ATTR_MIMETYPE, mimetype)
336 attachment_tag.set(ATTR_ID_TYPE, ID_TYPE_MD5)
338 attachment_tag.set(ATTR_ID_TYPE, ID_TYPE_MD5)
337 attachment_tag.text = attachment.hash
339 attachment_tag.text = attachment.hash
338 else:
340 else:
339 attachment_tag.set(ATTR_ID_TYPE, ID_TYPE_URL)
341 attachment_tag.set(ATTR_ID_TYPE, ID_TYPE_URL)
340 attachment_tag.text = attachment.url
342 attachment_tag.text = attachment.url
341
343
342 if tag_refs is not None:
344 if tag_refs is not None and attachment.is_internal():
343 attachment_ref = et.SubElement(tag_refs, TAG_ATTACHMENT_REF)
345 attachment_ref = et.SubElement(tag_refs, TAG_ATTACHMENT_REF)
344 attachment_ref.set(ATTR_REF, attachment.hash)
346 attachment_ref.set(ATTR_REF, attachment.hash)
345 attachment_ref.set(ATTR_URL, attachment.file.url)
347 attachment_ref.set(ATTR_URL, attachment.file.url)
348
349 @staticmethod
350 def generate_request_get(global_id_list: list):
351 """
352 Form a get request from a list of ModelId objects.
353 """
354
355 request = et.Element(TAG_REQUEST)
356 request.set(ATTR_TYPE, TYPE_GET)
357 request.set(ATTR_VERSION, '1.0')
358
359 model = et.SubElement(request, TAG_MODEL)
360 model.set(ATTR_VERSION, '1.0')
361 model.set(ATTR_NAME, 'post')
362
363 for global_id in global_id_list:
364 tag_id = et.SubElement(model, TAG_ID)
365 global_id.to_xml_element(tag_id)
366
367 return et.tostring(request, 'unicode')
368
369 @staticmethod
370 def generate_request_list(opening_post=None, tags=list(),
371 timestamp_from=None):
372 """
373 Form a pull request from a list of ModelId objects.
374 """
375
376 request = et.Element(TAG_REQUEST)
377 request.set(ATTR_TYPE, TYPE_LIST)
378 request.set(ATTR_VERSION, '1.0')
379
380 model = et.SubElement(request, TAG_MODEL)
381 model.set(ATTR_VERSION, '1.0')
382 model.set(ATTR_NAME, 'post')
383
384 if opening_post:
385 ThreadFilter().add_filter(model, opening_post)
386 if tags:
387 TagsFilter().add_filter(model, tags)
388 if timestamp_from:
389 TimestampFromFilter().add_filter(model, timestamp_from)
390
391 return et.tostring(request, 'unicode')
@@ -1,214 +1,253 b''
1 from django.test import TestCase
1 from django.test import TestCase
2
2
3 from boards.models import KeyPair, Post, Tag
3 from boards.models import KeyPair, Post, Tag
4 from boards.models.post.sync import SyncManager
4 from boards.models.post.sync import SyncManager
5 from boards.tests.mocks import MockRequest
5 from boards.tests.mocks import MockRequest
6 from boards.views.sync import response_get, response_list
6 from boards.views.sync import response_get, response_list
7
7
8 __author__ = 'neko259'
8 __author__ = 'neko259'
9
9
10
10
11 class SyncTest(TestCase):
11 class SyncTest(TestCase):
12 def test_get(self):
12 def test_get(self):
13 """
13 """
14 Forms a GET request of a post and checks the response.
14 Forms a GET request of a post and checks the response.
15 """
15 """
16
16
17 key = KeyPair.objects.generate_key(primary=True)
17 key = KeyPair.objects.generate_key(primary=True)
18 tag = Tag.objects.create(name='tag1')
18 tag = Tag.objects.create(name='tag1')
19 post = Post.objects.create_post(title='test_title',
19 post = Post.objects.create_post(title='test_title',
20 text='test_text\rline two',
20 text='test_text\rline two',
21 tags=[tag])
21 tags=[tag])
22
22
23 request = MockRequest()
23 request = MockRequest()
24 request.body = (
24 request.body = (
25 '<request type="get" version="1.0">'
25 '<request type="get" version="1.0">'
26 '<model name="post" version="1.0">'
26 '<model name="post" version="1.0">'
27 '<id key="%s" local-id="%d" type="%s" />'
27 '<id key="%s" local-id="%d" type="%s" />'
28 '</model>'
28 '</model>'
29 '</request>' % (post.global_id.key,
29 '</request>' % (post.global_id.key,
30 post.id,
30 post.id,
31 post.global_id.key_type)
31 post.global_id.key_type)
32 )
32 )
33
33
34 response = response_get(request).content.decode()
34 response = response_get(request).content.decode()
35 self.assertTrue(
35 self.assertTrue(
36 '<status>success</status>'
36 '<status>success</status>'
37 '<models>'
37 '<models>'
38 '<model name="post">'
38 '<model name="post">'
39 '<content>'
39 '<content>'
40 '<id key="%s" local-id="%d" type="%s" />'
40 '<id key="%s" local-id="%d" type="%s" />'
41 '<title>%s</title>'
41 '<title>%s</title>'
42 '<text>%s</text>'
42 '<text>%s</text>'
43 '<tags><tag>%s</tag></tags>'
43 '<tags><tag>%s</tag></tags>'
44 '<pub-time>%s</pub-time>'
44 '<pub-time>%s</pub-time>'
45 '<version>%s</version>'
45 '<version>%s</version>'
46 '</content>' % (
46 '</content>' % (
47 post.global_id.key,
47 post.global_id.key,
48 post.global_id.local_id,
48 post.global_id.local_id,
49 post.global_id.key_type,
49 post.global_id.key_type,
50 post.title,
50 post.title,
51 post.get_sync_text(),
51 post.get_sync_text(),
52 post.get_thread().get_tags().first().name,
52 post.get_thread().get_tags().first().name,
53 post.get_pub_time_str(),
53 post.get_pub_time_str(),
54 post.version,
54 post.version,
55 ) in response,
55 ) in response,
56 'Wrong response generated for the GET request.')
56 'Wrong response generated for the GET request.')
57
57
58 post.delete()
58 post.delete()
59 key.delete()
59 key.delete()
60
60
61 KeyPair.objects.generate_key(primary=True)
61 KeyPair.objects.generate_key(primary=True)
62
62
63 SyncManager.parse_response_get(response, None)
63 SyncManager.parse_response_get(response, None)
64 self.assertEqual(1, Post.objects.count(),
64 self.assertEqual(1, Post.objects.count(),
65 'Post was not created from XML response.')
65 'Post was not created from XML response.')
66
66
67 parsed_post = Post.objects.first()
67 parsed_post = Post.objects.first()
68 self.assertEqual('tag1',
68 self.assertEqual('tag1',
69 parsed_post.get_thread().get_tags().first().name,
69 parsed_post.get_thread().get_tags().first().name,
70 'Invalid tag was parsed.')
70 'Invalid tag was parsed.')
71
71
72 SyncManager.parse_response_get(response, None)
72 SyncManager.parse_response_get(response, None)
73 self.assertEqual(1, Post.objects.count(),
73 self.assertEqual(1, Post.objects.count(),
74 'The same post was imported twice.')
74 'The same post was imported twice.')
75
75
76 self.assertEqual(1, parsed_post.global_id.signature_set.count(),
76 self.assertEqual(1, parsed_post.global_id.signature_set.count(),
77 'Signature was not saved.')
77 'Signature was not saved.')
78
78
79 post = parsed_post
79 post = parsed_post
80
80
81 # Trying to sync the same once more
81 # Trying to sync the same once more
82 response = response_get(request).content.decode()
82 response = response_get(request).content.decode()
83
83
84 self.assertTrue(
84 self.assertTrue(
85 '<status>success</status>'
85 '<status>success</status>'
86 '<models>'
86 '<models>'
87 '<model name="post">'
87 '<model name="post">'
88 '<content>'
88 '<content>'
89 '<id key="%s" local-id="%d" type="%s" />'
89 '<id key="%s" local-id="%d" type="%s" />'
90 '<title>%s</title>'
90 '<title>%s</title>'
91 '<text>%s</text>'
91 '<text>%s</text>'
92 '<tags><tag>%s</tag></tags>'
92 '<tags><tag>%s</tag></tags>'
93 '<pub-time>%s</pub-time>'
93 '<pub-time>%s</pub-time>'
94 '<version>%s</version>'
94 '<version>%s</version>'
95 '</content>' % (
95 '</content>' % (
96 post.global_id.key,
96 post.global_id.key,
97 post.global_id.local_id,
97 post.global_id.local_id,
98 post.global_id.key_type,
98 post.global_id.key_type,
99 post.title,
99 post.title,
100 post.get_sync_text(),
100 post.get_sync_text(),
101 post.get_thread().get_tags().first().name,
101 post.get_thread().get_tags().first().name,
102 post.get_pub_time_str(),
102 post.get_pub_time_str(),
103 post.version,
103 post.version,
104 ) in response,
104 ) in response,
105 'Wrong response generated for the GET request.')
105 'Wrong response generated for the GET request.')
106
106
107 def test_list_all(self):
107 def test_list_all(self):
108 key = KeyPair.objects.generate_key(primary=True)
108 key = KeyPair.objects.generate_key(primary=True)
109 tag = Tag.objects.create(name='tag1')
109 tag = Tag.objects.create(name='tag1')
110 post = Post.objects.create_post(title='test_title',
110 post = Post.objects.create_post(title='test_title',
111 text='test_text\rline two',
111 text='test_text\rline two',
112 tags=[tag])
112 tags=[tag])
113 post2 = Post.objects.create_post(title='test title 2',
113 post2 = Post.objects.create_post(title='test title 2',
114 text='test text 2',
114 text='test text 2',
115 tags=[tag])
115 tags=[tag])
116
116
117 request_all = MockRequest()
117 request_all = MockRequest()
118 request_all.body = (
118 request_all.body = (
119 '<request type="list" version="1.0">'
119 '<request type="list" version="1.0">'
120 '<model name="post" version="1.0">'
120 '<model name="post" version="1.0">'
121 '</model>'
121 '</model>'
122 '</request>'
122 '</request>'
123 )
123 )
124
124
125 response_all = response_list(request_all).content.decode()
125 response_all = response_list(request_all).content.decode()
126 self.assertTrue(
126 self.assertTrue(
127 '<status>success</status>'
127 '<status>success</status>'
128 '<models>'
128 '<models>'
129 '<model>'
129 '<model>'
130 '<id key="{}" local-id="{}" type="{}" />'
130 '<id key="{}" local-id="{}" type="{}" />'
131 '<version>{}</version>'
131 '<version>{}</version>'
132 '</model>'
132 '</model>'
133 '<model>'
133 '<model>'
134 '<id key="{}" local-id="{}" type="{}" />'
134 '<id key="{}" local-id="{}" type="{}" />'
135 '<version>{}</version>'
135 '<version>{}</version>'
136 '</model>'
136 '</model>'
137 '</models>'.format(
137 '</models>'.format(
138 post.global_id.key,
138 post.global_id.key,
139 post.global_id.local_id,
139 post.global_id.local_id,
140 post.global_id.key_type,
140 post.global_id.key_type,
141 post.version,
141 post.version,
142 post2.global_id.key,
142 post2.global_id.key,
143 post2.global_id.local_id,
143 post2.global_id.local_id,
144 post2.global_id.key_type,
144 post2.global_id.key_type,
145 post2.version,
145 post2.version,
146 ) in response_all,
146 ) in response_all,
147 'Wrong response generated for the LIST request for all posts.')
147 'Wrong response generated for the LIST request for all posts.')
148
148
149 def test_list_existing_thread(self):
149 def test_list_existing_thread(self):
150 key = KeyPair.objects.generate_key(primary=True)
150 key = KeyPair.objects.generate_key(primary=True)
151 tag = Tag.objects.create(name='tag1')
151 tag = Tag.objects.create(name='tag1')
152 post = Post.objects.create_post(title='test_title',
152 post = Post.objects.create_post(title='test_title',
153 text='test_text\rline two',
153 text='test_text\rline two',
154 tags=[tag])
154 tags=[tag])
155 post2 = Post.objects.create_post(title='test title 2',
155 post2 = Post.objects.create_post(title='test title 2',
156 text='test text 2',
156 text='test text 2',
157 tags=[tag])
157 tags=[tag])
158
158
159 request_thread = MockRequest()
159 request_thread = MockRequest()
160 request_thread.body = (
160 request_thread.body = (
161 '<request type="list" version="1.0">'
161 '<request type="list" version="1.0">'
162 '<model name="post" version="1.0">'
162 '<model name="post" version="1.0">'
163 '<thread>{}</thread>'
163 '<thread>{}</thread>'
164 '</model>'
164 '</model>'
165 '</request>'.format(
165 '</request>'.format(
166 post.id,
166 post.id,
167 )
167 )
168 )
168 )
169
169
170 response_thread = response_list(request_thread).content.decode()
170 response_thread = response_list(request_thread).content.decode()
171 self.assertTrue(
171 self.assertTrue(
172 '<status>success</status>'
172 '<status>success</status>'
173 '<models>'
173 '<models>'
174 '<model>'
174 '<model>'
175 '<id key="{}" local-id="{}" type="{}" />'
175 '<id key="{}" local-id="{}" type="{}" />'
176 '<version>{}</version>'
176 '<version>{}</version>'
177 '</model>'
177 '</model>'
178 '</models>'.format(
178 '</models>'.format(
179 post.global_id.key,
179 post.global_id.key,
180 post.global_id.local_id,
180 post.global_id.local_id,
181 post.global_id.key_type,
181 post.global_id.key_type,
182 post.version,
182 post.version,
183 ) in response_thread,
183 ) in response_thread,
184 'Wrong response generated for the LIST request for posts of '
184 'Wrong response generated for the LIST request for posts of '
185 'existing thread.')
185 'existing thread.')
186
186
187 def test_list_non_existing_thread(self):
187 def test_list_non_existing_thread(self):
188 key = KeyPair.objects.generate_key(primary=True)
188 key = KeyPair.objects.generate_key(primary=True)
189 tag = Tag.objects.create(name='tag1')
189 tag = Tag.objects.create(name='tag1')
190 post = Post.objects.create_post(title='test_title',
190 post = Post.objects.create_post(title='test_title',
191 text='test_text\rline two',
191 text='test_text\rline two',
192 tags=[tag])
192 tags=[tag])
193 post2 = Post.objects.create_post(title='test title 2',
193 post2 = Post.objects.create_post(title='test title 2',
194 text='test text 2',
194 text='test text 2',
195 tags=[tag])
195 tags=[tag])
196
196
197 request_thread = MockRequest()
197 request_thread = MockRequest()
198 request_thread.body = (
198 request_thread.body = (
199 '<request type="list" version="1.0">'
199 '<request type="list" version="1.0">'
200 '<model name="post" version="1.0">'
200 '<model name="post" version="1.0">'
201 '<thread>{}</thread>'
201 '<thread>{}</thread>'
202 '</model>'
202 '</model>'
203 '</request>'.format(
203 '</request>'.format(
204 0,
204 0,
205 )
205 )
206 )
206 )
207
207
208 response_thread = response_list(request_thread).content.decode()
208 response_thread = response_list(request_thread).content.decode()
209 self.assertTrue(
209 self.assertTrue(
210 '<status>success</status>'
210 '<status>success</status>'
211 '<models />'
211 '<models />'
212 in response_thread,
212 in response_thread,
213 'Wrong response generated for the LIST request for posts of '
213 'Wrong response generated for the LIST request for posts of '
214 'non-existing thread.')
214 'non-existing thread.')
215
216 def test_list_pub_time(self):
217 key = KeyPair.objects.generate_key(primary=True)
218 tag = Tag.objects.create(name='tag1')
219 post = Post.objects.create_post(title='test_title',
220 text='test_text\rline two',
221 tags=[tag])
222 post2 = Post.objects.create_post(title='test title 2',
223 text='test text 2',
224 tags=[tag])
225
226 request_thread = MockRequest()
227 request_thread.body = (
228 '<request type="list" version="1.0">'
229 '<model name="post" version="1.0">'
230 '<timestamp_from>{}</timestamp_from>'
231 '</model>'
232 '</request>'.format(
233 post.pub_time,
234 )
235 )
236
237 response_thread = response_list(request_thread).content.decode()
238 self.assertTrue(
239 '<status>success</status>'
240 '<models>'
241 '<model>'
242 '<id key="{}" local-id="{}" type="{}" />'
243 '<version>{}</version>'
244 '</model>'
245 '</models>'.format(
246 post2.global_id.key,
247 post2.global_id.local_id,
248 post2.global_id.key_type,
249 post2.version,
250 ) in response_thread,
251 'Wrong response generated for the LIST request for posts of '
252 'existing thread.')
253
@@ -1,157 +1,157 b''
1 """
1 """
2 This module contains helper functions and helper classes.
2 This module contains helper functions and helper classes.
3 """
3 """
4 import hashlib
4 import hashlib
5 import uuid
5 import uuid
6
6
7 from boards.abstracts.constants import FILE_DIRECTORY
7 from boards.abstracts.constants import FILE_DIRECTORY
8 from random import random
8 from random import random
9 import time
9 import time
10 import hmac
10 import hmac
11
11
12 from django.core.cache import cache
12 from django.core.cache import cache
13 from django.db.models import Model
13 from django.db.models import Model
14 from django import forms
14 from django import forms
15 from django.template.defaultfilters import filesizeformat
15 from django.template.defaultfilters import filesizeformat
16 from django.utils import timezone
16 from django.utils import timezone
17 from django.utils.translation import ugettext_lazy as _
17 from django.utils.translation import ugettext_lazy as _
18 import magic
18 import magic
19 import os
19 import os
20
20
21 import boards
21 import boards
22 from boards.settings import get_bool
22 from boards.settings import get_bool
23 from neboard import settings
23 from neboard import settings
24
24
25 CACHE_KEY_DELIMITER = '_'
25 CACHE_KEY_DELIMITER = '_'
26
26
27 HTTP_FORWARDED = 'HTTP_X_FORWARDED_FOR'
27 HTTP_FORWARDED = 'HTTP_X_FORWARDED_FOR'
28 META_REMOTE_ADDR = 'REMOTE_ADDR'
28 META_REMOTE_ADDR = 'REMOTE_ADDR'
29
29
30 SETTING_MESSAGES = 'Messages'
30 SETTING_MESSAGES = 'Messages'
31 SETTING_ANON_MODE = 'AnonymousMode'
31 SETTING_ANON_MODE = 'AnonymousMode'
32
32
33 ANON_IP = '127.0.0.1'
33 ANON_IP = '127.0.0.1'
34
34
35 FILE_EXTENSION_DELIMITER = '.'
35 FILE_EXTENSION_DELIMITER = '.'
36
36
37
37
38 def is_anonymous_mode():
38 def is_anonymous_mode():
39 return get_bool(SETTING_MESSAGES, SETTING_ANON_MODE)
39 return get_bool(SETTING_MESSAGES, SETTING_ANON_MODE)
40
40
41
41
42 def get_client_ip(request):
42 def get_client_ip(request):
43 if is_anonymous_mode():
43 if is_anonymous_mode():
44 ip = ANON_IP
44 ip = ANON_IP
45 else:
45 else:
46 x_forwarded_for = request.META.get(HTTP_FORWARDED)
46 x_forwarded_for = request.META.get(HTTP_FORWARDED)
47 if x_forwarded_for:
47 if x_forwarded_for:
48 ip = x_forwarded_for.split(',')[-1].strip()
48 ip = x_forwarded_for.split(',')[-1].strip()
49 else:
49 else:
50 ip = request.META.get(META_REMOTE_ADDR)
50 ip = request.META.get(META_REMOTE_ADDR)
51 return ip
51 return ip
52
52
53
53
54 # TODO The output format is not epoch because it includes microseconds
54 # TODO The output format is not epoch because it includes microseconds
55 def datetime_to_epoch(datetime):
55 def datetime_to_epoch(datetime):
56 return int(time.mktime(timezone.localtime(
56 return int(time.mktime(timezone.localtime(
57 datetime,timezone.get_current_timezone()).timetuple())
57 datetime,timezone.get_current_timezone()).timetuple())
58 * 1000000 + datetime.microsecond)
58 * 1000000 + datetime.microsecond)
59
59
60
60
61 def get_websocket_token(user_id='', timestamp=''):
61 def get_websocket_token(user_id='', timestamp=''):
62 """
62 """
63 Create token to validate information provided by new connection.
63 Create token to validate information provided by new connection.
64 """
64 """
65
65
66 sign = hmac.new(settings.CENTRIFUGE_PROJECT_SECRET.encode())
66 sign = hmac.new(settings.CENTRIFUGE_PROJECT_SECRET.encode())
67 sign.update(settings.CENTRIFUGE_PROJECT_ID.encode())
67 sign.update(settings.CENTRIFUGE_PROJECT_ID.encode())
68 sign.update(user_id.encode())
68 sign.update(user_id.encode())
69 sign.update(timestamp.encode())
69 sign.update(timestamp.encode())
70 token = sign.hexdigest()
70 token = sign.hexdigest()
71
71
72 return token
72 return token
73
73
74
74
75 # TODO Test this carefully
75 # TODO Test this carefully
76 def cached_result(key_method=None):
76 def cached_result(key_method=None):
77 """
77 """
78 Caches method result in the Django's cache system, persisted by object name,
78 Caches method result in the Django's cache system, persisted by object name,
79 object name, model id if object is a Django model, args and kwargs if any.
79 object name, model id if object is a Django model, args and kwargs if any.
80 """
80 """
81 def _cached_result(function):
81 def _cached_result(function):
82 def inner_func(obj, *args, **kwargs):
82 def inner_func(obj, *args, **kwargs):
83 cache_key_params = [obj.__class__.__name__, function.__name__]
83 cache_key_params = [obj.__class__.__name__, function.__name__]
84
84
85 cache_key_params += args
85 cache_key_params += args
86 for key, value in kwargs:
86 for key, value in kwargs:
87 cache_key_params.append(key + ':' + value)
87 cache_key_params.append(key + ':' + value)
88
88
89 if isinstance(obj, Model):
89 if isinstance(obj, Model):
90 cache_key_params.append(str(obj.id))
90 cache_key_params.append(str(obj.id))
91
91
92 if key_method is not None:
92 if key_method is not None:
93 cache_key_params += [str(arg) for arg in key_method(obj)]
93 cache_key_params += [str(arg) for arg in key_method(obj)]
94
94
95 cache_key = CACHE_KEY_DELIMITER.join(cache_key_params)
95 cache_key = CACHE_KEY_DELIMITER.join(cache_key_params)
96
96
97 persisted_result = cache.get(cache_key)
97 persisted_result = cache.get(cache_key)
98 if persisted_result is not None:
98 if persisted_result is not None:
99 result = persisted_result
99 result = persisted_result
100 else:
100 else:
101 result = function(obj, *args, **kwargs)
101 result = function(obj, *args, **kwargs)
102 if result is not None:
102 if result is not None:
103 cache.set(cache_key, result)
103 cache.set(cache_key, result)
104
104
105 return result
105 return result
106
106
107 return inner_func
107 return inner_func
108 return _cached_result
108 return _cached_result
109
109
110
110
111 def get_file_hash(file) -> str:
111 def get_file_hash(file) -> str:
112 md5 = hashlib.md5()
112 md5 = hashlib.md5()
113 for chunk in file.chunks():
113 for chunk in file.chunks():
114 md5.update(chunk)
114 md5.update(chunk)
115 return md5.hexdigest()
115 return md5.hexdigest()
116
116
117
117
118 def validate_file_size(size: int):
118 def validate_file_size(size: int):
119 max_size = boards.settings.get_int('Forms', 'MaxFileSize')
119 max_size = boards.settings.get_int('Forms', 'MaxFileSize')
120 if size > max_size:
120 if max_size > 0 and size > max_size:
121 raise forms.ValidationError(
121 raise forms.ValidationError(
122 _('File must be less than %s but is %s.')
122 _('File must be less than %s but is %s.')
123 % (filesizeformat(max_size), filesizeformat(size)))
123 % (filesizeformat(max_size), filesizeformat(size)))
124
124
125
125
126 def get_extension(filename):
126 def get_extension(filename):
127 return filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
127 return filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
128
128
129
129
130 def get_upload_filename(model_instance, old_filename):
130 def get_upload_filename(model_instance, old_filename):
131 extension = get_extension(old_filename)
131 extension = get_extension(old_filename)
132 new_name = '{}.{}'.format(uuid.uuid4(), extension)
132 new_name = '{}.{}'.format(uuid.uuid4(), extension)
133
133
134 return os.path.join(FILE_DIRECTORY, new_name)
134 return os.path.join(FILE_DIRECTORY, new_name)
135
135
136
136
137 def get_file_mimetype(file) -> str:
137 def get_file_mimetype(file) -> str:
138 file_type = magic.from_buffer(file.chunks().__next__(), mime=True)
138 file_type = magic.from_buffer(file.chunks().__next__(), mime=True)
139 if file_type is None:
139 if file_type is None:
140 file_type = 'application/octet-stream'
140 file_type = 'application/octet-stream'
141 elif type(file_type) == bytes:
141 elif type(file_type) == bytes:
142 file_type = file_type.decode()
142 file_type = file_type.decode()
143 return file_type
143 return file_type
144
144
145
145
146 def get_domain(url: str) -> str:
146 def get_domain(url: str) -> str:
147 """
147 """
148 Gets domain from an URL with random number of domain levels.
148 Gets domain from an URL with random number of domain levels.
149 """
149 """
150 domain_parts = url.split('/')
150 domain_parts = url.split('/')
151 if len(domain_parts) >= 2:
151 if len(domain_parts) >= 2:
152 full_domain = domain_parts[2]
152 full_domain = domain_parts[2]
153 else:
153 else:
154 full_domain = ''
154 full_domain = ''
155
155
156 return full_domain
156 return full_domain
157
157
@@ -1,42 +1,46 b''
1 from datetime import datetime
1 from datetime import datetime
2 from datetime import timedelta
2 from datetime import timedelta
3
3
4 from django.db.models import Count
4 from django.db.models import Count
5 from django.shortcuts import render
5 from django.shortcuts import render
6 from django.utils.decorators import method_decorator
6 from django.utils.decorators import method_decorator
7 from django.views.decorators.csrf import csrf_protect
7 from django.views.decorators.csrf import csrf_protect
8
8
9 from boards import settings
9 from boards import settings
10 from boards.models import Post
10 from boards.models import Post
11 from boards.models import Tag, Attachment, STATUS_ACTIVE
11 from boards.models import Tag, Attachment, STATUS_ACTIVE
12 from boards.views.base import BaseBoardView
12 from boards.views.base import BaseBoardView
13
13
14 PARAM_SECTION_STR = 'section_str'
14 PARAM_SECTION_STR = 'section_str'
15 PARAM_LATEST_THREADS = 'latest_threads'
15 PARAM_LATEST_THREADS = 'latest_threads'
16 PARAM_IMAGES = 'images'
16 PARAM_IMAGES = 'images'
17
17
18 TEMPLATE = 'boards/landing.html'
18 TEMPLATE = 'boards/landing.html'
19
19
20 RANDOM_IMAGE_COUNT = 3
20 RANDOM_IMAGE_COUNT = 3
21
21
22
22
23 class LandingView(BaseBoardView):
23 class LandingView(BaseBoardView):
24 @method_decorator(csrf_protect)
24 @method_decorator(csrf_protect)
25 def get(self, request):
25 def get(self, request):
26 params = dict()
26 params = dict()
27
27
28 params[PARAM_SECTION_STR] = Tag.objects.get_tag_url_list(
28 params[PARAM_SECTION_STR] = Tag.objects.get_tag_url_list(
29 Tag.objects.filter(required=True))
29 Tag.objects.filter(required=True))
30
30
31 today = datetime.now() - timedelta(1)
31 today = datetime.now() - timedelta(1)
32 max_landing_threads = settings.get_int('View', 'MaxFavoriteThreads')
33 ops = Post.objects.filter(thread__replies__pub_time__gt=today, opening=True, thread__status=STATUS_ACTIVE)\
32 ops = Post.objects.filter(thread__replies__pub_time__gt=today, opening=True, thread__status=STATUS_ACTIVE)\
34 .annotate(today_post_count=Count('thread__replies'))\
33 .annotate(today_post_count=Count('thread__replies'))\
35 .order_by('-pub_time')[:max_landing_threads]
34 .order_by('-pub_time')
35
36 max_landing_threads = settings.get_int('View', 'MaxFavoriteThreads')
37 if max_landing_threads > 0:
38 ops = ops[:max_landing_threads]
39
36 params[PARAM_LATEST_THREADS] = ops
40 params[PARAM_LATEST_THREADS] = ops
37
41
38 params[PARAM_IMAGES] = Attachment.objects.get_random_images(
42 params[PARAM_IMAGES] = Attachment.objects.get_random_images(
39 RANDOM_IMAGE_COUNT)
43 RANDOM_IMAGE_COUNT)
40
44
41 return render(request, TEMPLATE, params)
45 return render(request, TEMPLATE, params)
42
46
@@ -1,44 +1,45 b''
1 from django.shortcuts import render
1 from django.shortcuts import render
2 from django.views.generic import View
2 from django.views.generic import View
3 from django.db.models import Q
3
4
4 from boards.abstracts.paginator import get_paginator
5 from boards.abstracts.paginator import get_paginator
5 from boards.forms import SearchForm, PlainErrorList
6 from boards.forms import SearchForm, PlainErrorList
6 from boards.models import Post
7 from boards.models import Post
7
8
8
9
9 MIN_QUERY_LENGTH = 3
10 MIN_QUERY_LENGTH = 3
10 RESULTS_PER_PAGE = 10
11 RESULTS_PER_PAGE = 10
11
12
12 FORM_QUERY = 'query'
13 FORM_QUERY = 'query'
13
14
14 CONTEXT_QUERY = 'query'
15 CONTEXT_QUERY = 'query'
15 CONTEXT_FORM = 'form'
16 CONTEXT_FORM = 'form'
16 CONTEXT_PAGE = 'page'
17 CONTEXT_PAGE = 'page'
17
18
18 REQUEST_PAGE = 'page'
19 REQUEST_PAGE = 'page'
19
20
20 __author__ = 'neko259'
21 __author__ = 'neko259'
21
22
22 TEMPLATE = 'search/search.html'
23 TEMPLATE = 'search/search.html'
23
24
24
25
25 class BoardSearchView(View):
26 class BoardSearchView(View):
26 def get(self, request):
27 def get(self, request):
27 params = dict()
28 params = dict()
28
29
29 form = SearchForm(request.GET, error_class=PlainErrorList)
30 form = SearchForm(request.GET, error_class=PlainErrorList)
30 params[CONTEXT_FORM] = form
31 params[CONTEXT_FORM] = form
31
32
32 if form.is_valid():
33 if form.is_valid():
33 query = form.cleaned_data[FORM_QUERY]
34 query = form.cleaned_data[FORM_QUERY]
34 if len(query) >= MIN_QUERY_LENGTH:
35 if len(query) >= MIN_QUERY_LENGTH:
35 results = Post.objects.filter(text__icontains=query)\
36 results = Post.objects.filter(Q(text__icontains=query) |
36 .order_by('-id')
37 Q(title__icontains=query)).order_by('-id')
37 paginator = get_paginator(results, RESULTS_PER_PAGE)
38 paginator = get_paginator(results, RESULTS_PER_PAGE)
38
39
39 page = int(request.GET.get(REQUEST_PAGE, '1'))
40 page = int(request.GET.get(REQUEST_PAGE, '1'))
40
41
41 params[CONTEXT_PAGE] = paginator.page(page)
42 params[CONTEXT_PAGE] = paginator.page(page)
42 params[CONTEXT_QUERY] = query
43 params[CONTEXT_QUERY] = query
43
44
44 return render(request, TEMPLATE, params)
45 return render(request, TEMPLATE, params)
@@ -1,79 +1,83 b''
1 import logging
1 import logging
2
2
3 import xml.etree.ElementTree as et
3 import xml.etree.ElementTree as et
4
4
5 from django.http import HttpResponse, Http404
5 from django.http import HttpResponse, Http404
6
6
7 from boards.abstracts.sync_filters import ThreadFilter, TAG_THREAD
7 from boards.abstracts.sync_filters import ThreadFilter, TagsFilter,\
8 TimestampFromFilter,\
9 TAG_THREAD, TAG_TAGS, TAG_TIME_FROM
8 from boards.models import GlobalId, Post
10 from boards.models import GlobalId, Post
9 from boards.models.post.sync import SyncManager
11 from boards.models.post.sync import SyncManager
10
12
11
13
12 logger = logging.getLogger('boards.sync')
14 logger = logging.getLogger('boards.sync')
13
15
14
16
15 FILTERS = {
17 FILTERS = {
16 TAG_THREAD: ThreadFilter,
18 TAG_THREAD: ThreadFilter,
19 TAG_TAGS: TagsFilter,
20 TAG_TIME_FROM: TimestampFromFilter,
17 }
21 }
18
22
19
23
20 def response_list(request):
24 def response_list(request):
21 request_xml = request.body
25 request_xml = request.body
22
26
23 filters = []
27 filters = []
24
28
25 if request_xml is None or len(request_xml) == 0:
29 if request_xml is None or len(request_xml) == 0:
26 return HttpResponse(content='Use the API')
30 return HttpResponse(content='Use the API')
27 else:
31 else:
28 root_tag = et.fromstring(request_xml)
32 root_tag = et.fromstring(request_xml)
29 model_tag = root_tag[0]
33 model_tag = root_tag[0]
30
34
31 for tag_filter in model_tag:
35 for tag_filter in model_tag:
32 filter_name = tag_filter.tag
36 filter_name = tag_filter.tag
33 model_filter = FILTERS.get(filter_name)(tag_filter)
37 model_filter = FILTERS.get(filter_name)
34 if not model_filter:
38 if not model_filter:
35 logger.warning('Unavailable filter: {}'.format(filter_name))
39 logger.warning('Unavailable filter: {}'.format(filter_name))
36 filters.append(model_filter)
40 filters.append(model_filter(tag_filter))
37
41
38 response_xml = SyncManager.generate_response_list(filters)
42 response_xml = SyncManager.generate_response_list(filters)
39
43
40 return HttpResponse(content=response_xml)
44 return HttpResponse(content=response_xml)
41
45
42
46
43 def response_get(request):
47 def response_get(request):
44 """
48 """
45 Processes a GET request with post ID list and returns the posts XML list.
49 Processes a GET request with post ID list and returns the posts XML list.
46 Request should contain an 'xml' post attribute with the actual request XML.
50 Request should contain an 'xml' post attribute with the actual request XML.
47 """
51 """
48
52
49 request_xml = request.body
53 request_xml = request.body
50
54
51 if request_xml is None or len(request_xml) == 0:
55 if request_xml is None or len(request_xml) == 0:
52 return HttpResponse(content='Use the API')
56 return HttpResponse(content='Use the API')
53
57
54 posts = []
58 posts = []
55
59
56 root_tag = et.fromstring(request_xml)
60 root_tag = et.fromstring(request_xml)
57 model_tag = root_tag[0]
61 model_tag = root_tag[0]
58 for id_tag in model_tag:
62 for id_tag in model_tag:
59 global_id, exists = GlobalId.from_xml_element(id_tag)
63 global_id, exists = GlobalId.from_xml_element(id_tag)
60 if exists:
64 if exists:
61 posts.append(Post.objects.get(global_id=global_id))
65 posts.append(Post.objects.get(global_id=global_id))
62
66
63 response_xml = SyncManager.generate_response_get(posts)
67 response_xml = SyncManager.generate_response_get(posts)
64
68
65 return HttpResponse(content=response_xml)
69 return HttpResponse(content=response_xml)
66
70
67
71
68 def get_post_sync_data(request, post_id):
72 def get_post_sync_data(request, post_id):
69 try:
73 try:
70 post = Post.objects.get(id=post_id)
74 post = Post.objects.get(id=post_id)
71 except Post.DoesNotExist:
75 except Post.DoesNotExist:
72 raise Http404()
76 raise Http404()
73
77
74 xml_str = SyncManager.generate_response_get([post])
78 xml_str = SyncManager.generate_response_get([post])
75
79
76 return HttpResponse(
80 return HttpResponse(
77 content_type='text/xml; charset=utf-8',
81 content_type='text/xml; charset=utf-8',
78 content=xml_str,
82 content=xml_str,
79 )
83 )
General Comments 0
You need to be logged in to leave comments. Login now