Show More
@@ -51,3 +51,4 b' 19785af352684884f94566785ba1b13b3ddc5216' | |||||
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,8 +1,11 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: | |
@@ -29,3 +32,35 b' class ThreadFilter(PostFilter):' | |||||
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,5 +1,5 b'' | |||||
1 | [Version] |
|
1 | [Version] | |
2 |
Version = 4.1. |
|
2 | Version = 4.1.1 2017 | |
3 | SiteName = Neboard DEV |
|
3 | SiteName = Neboard DEV | |
4 |
|
4 | |||
5 | [Cache] |
|
5 | [Cache] |
@@ -26,6 +26,10 b' class Command(BaseCommand):' | |||||
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') | |
@@ -57,9 +61,19 b' class Command(BaseCommand):' | |||||
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) | |
@@ -93,6 +107,8 b' class Command(BaseCommand):' | |||||
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: |
@@ -3,7 +3,8 b' 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, \ | |
@@ -17,6 +18,7 b" EXCEPTION_DOWNLOAD = 'File was not downl" | |||||
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' | |
@@ -193,7 +195,7 b' class SyncManager:' | |||||
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 |
|
|
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() | |
@@ -218,7 +220,7 b' class SyncManager:' | |||||
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) | |
@@ -339,7 +341,51 b' class SyncManager:' | |||||
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') |
@@ -212,3 +212,42 b' class SyncTest(TestCase):' | |||||
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 |
@@ -117,7 +117,7 b' def get_file_hash(file) -> str:' | |||||
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))) |
@@ -29,10 +29,14 b' class LandingView(BaseBoardView):' | |||||
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') |
|
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( |
@@ -1,5 +1,6 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 | |
@@ -32,8 +33,8 b' class BoardSearchView(View):' | |||||
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')) |
@@ -4,7 +4,9 b' 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, T |
|
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 | |||
@@ -14,6 +16,8 b" logger = logging.getLogger('boards.sync'" | |||||
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 | |||
@@ -30,10 +34,10 b' def response_list(request):' | |||||
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) |
|
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 |
General Comments 0
You need to be logged in to leave comments.
Login now