From d324409aacfa3d486e58f36cf63e4c2525b549a1 Mon Sep 17 00:00:00 2001 From: Stanislas P <57526019+quaxsze@users.noreply.github.com> Date: Tue, 17 Oct 2023 08:38:11 -0300 Subject: [PATCH] Topics upgrades : add a created_at field and allow to filter on tags or name (#2904) --- CHANGELOG.md | 3 ++ udata/core/topic/api.py | 45 ++++++++++++++++++++++++------ udata/core/topic/models.py | 15 +++++++++- udata/tests/api/test_topics_api.py | 16 +++++++++-- 4 files changed, 67 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5805bd51a1..d42661e2c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ - Topics creation, update and deletion are now opened to all users [#2898](https://github.com/opendatateam/udata/pull/2898) - Topics are now `db.Owned` and searchable by `id` in dataset search [#2901](https://github.com/opendatateam/udata/pull/2901) - Remove `deleted` api field that does not exist [#2903](https://github.com/opendatateam/udata/pull/2903) + - Add `created_at`field to topic's model [#2904](https://github.com/opendatateam/udata/pull/2904) + - Topics can now be filtered by `tag` field [#2904](https://github.com/opendatateam/udata/pull/2904) + - Topics can now be queried by test search in `name` field with `q` argument [#2904](https://github.com/opendatateam/udata/pull/2904) - Fix site title and keywords never get updated [#2900](https://github.com/opendatateam/udata/pull/2900) - Reuse's extras are now exposed by API [#2905](https://github.com/opendatateam/udata/pull/2905) - Add German to udata translations [2899](https://github.com/opendatateam/udata/pull/2899)[2909](https://github.com/opendatateam/udata/pull/2909) diff --git a/udata/core/topic/api.py b/udata/core/topic/api.py index 7c79a610f1..62e83d4d65 100644 --- a/udata/core/topic/api.py +++ b/udata/core/topic/api.py @@ -1,6 +1,5 @@ from udata.api import api, fields, API - - +from udata.api.parsers import ModelApiParser from udata.core.dataset.api_fields import dataset_fields from udata.core.organization.api_fields import org_ref_fields from udata.core.reuse.api_fields import reuse_fields @@ -9,6 +8,8 @@ from .models import Topic from .forms import TopicForm +DEFAULT_SORTING = '-created_at' + ns = api.namespace('topics', 'Topics related operations') topic_fields = api.model('Topic', { @@ -28,8 +29,6 @@ 'private': fields.Boolean(description='Is the topic private'), 'created_at': fields.ISODateTime( description='The topic creation date', readonly=True), - 'last_modified': fields.ISODateTime( - description='The topic last modification date', readonly=True), 'organization': fields.Nested( org_ref_fields, allow_null=True, description='The publishing organization', readonly=True), @@ -48,20 +47,48 @@ topic_page_fields = api.model('TopicPage', fields.pager(topic_fields)) -parser = api.page_parser() + +class TopicApiParser(ModelApiParser): + sorts = { + 'name': 'name', + 'created': 'created_at' + } + + def __init__(self): + super().__init__() + self.parser.add_argument('tag', type=str, location='args') + + @staticmethod + def parse_filters(topics, args): + if args.get('q'): + # Following code splits the 'q' argument by spaces to surround + # every word in it with quotes before rebuild it. + # This allows the search_text method to tokenise with an AND + # between tokens whereas an OR is used without it. + phrase_query = ' '.join([f'"{elem}"' for elem in args['q'].split(' ')]) + topics = topics.search_text(phrase_query) + if args.get('tag'): + topics = topics.filter(tags=args['tag']) + return topics + + +topic_parser = TopicApiParser() @ns.route('/', endpoint='topics') class TopicsAPI(API): @api.doc('list_topics') - @api.expect(parser) + @api.expect(topic_parser.parser) @api.marshal_with(topic_page_fields) def get(self): '''List all topics''' - args = parser.parse_args() - return (Topic.objects.order_by('-created') - .paginate(args['page'], args['page_size'])) + args = topic_parser.parse() + topics = Topic.objects() + topics = topic_parser.parse_filters(topics, args) + sort = args['sort'] or ('$text_score' if args['q'] else None) or DEFAULT_SORTING + return (topics.order_by(sort) + .paginate(args['page'], args['page_size'])) @api.doc('create_topic') @api.expect(topic_fields) diff --git a/udata/core/topic/models.py b/udata/core/topic/models.py index 62d76197c6..c43dec0e81 100644 --- a/udata/core/topic/models.py +++ b/udata/core/topic/models.py @@ -1,5 +1,6 @@ +from datetime import datetime from flask import url_for - +from mongoengine.fields import DateTimeField from mongoengine.signals import pre_save from udata.models import db from udata.search import reindex @@ -27,6 +28,18 @@ class Topic(db.Document, db.Owned): private = db.BooleanField() extras = db.ExtrasField() + created_at = DateTimeField(default=datetime.utcnow, required=True) + + meta = { + 'indexes': [ + '$name', + 'created_at', + 'slug' + ] + db.Owned.meta['indexes'], + 'ordering': ['-created_at'], + 'auto_create_index_on_save': True + } + def __str__(self): return self.name diff --git a/udata/tests/api/test_topics_api.py b/udata/tests/api/test_topics_api.py index b25365503f..470c160721 100644 --- a/udata/tests/api/test_topics_api.py +++ b/udata/tests/api/test_topics_api.py @@ -13,11 +13,23 @@ class TopicsAPITest(APITestCase): def test_topic_api_list(self): '''It should fetch a topic list from the API''' - topics = TopicFactory.create_batch(3) + TopicFactory.create_batch(3) + tag_topic = TopicFactory(tags=['energy']) + name_topic = TopicFactory(name='topic-for-query') response = self.get(url_for('api.topics')) self.assert200(response) - self.assertEqual(len(response.json['data']), len(topics)) + self.assertEqual(len(response.json['data']), 5) + + response = self.get(url_for('api.topics', q='topic-for')) + self.assert200(response) + self.assertEqual(len(response.json['data']), 1) + self.assertEqual(response.json['data'][0]['id'], str(name_topic.id)) + + response = self.get(url_for('api.topics', tag='energy')) + self.assert200(response) + self.assertEqual(len(response.json['data']), 1) + self.assertEqual(response.json['data'][0]['id'], str(tag_topic.id)) def test_topic_api_get(self): '''It should fetch a topic from the API'''