Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Topics upgrades : add a created_at field and allow to filter on tags or name #2904

Merged
merged 12 commits into from
Oct 17, 2023
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
45 changes: 36 additions & 9 deletions udata/core/topic/api.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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', {
Expand All @@ -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),
Expand All @@ -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)
Expand Down
15 changes: 14 additions & 1 deletion udata/core/topic/models.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -27,6 +28,18 @@ class Topic(db.Document, db.Owned):
private = db.BooleanField()
extras = db.ExtrasField()

created_at = DateTimeField(default=datetime.utcnow, required=True)
maudetes marked this conversation as resolved.
Show resolved Hide resolved

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

Expand Down
16 changes: 14 additions & 2 deletions udata/tests/api/test_topics_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'''
Expand Down