Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion udata/core/topic/apiv2.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from flask import request, url_for
from flask_security import current_user

from udata import search
from udata.api import API, apiv2
from udata.core.discussions.models import Discussion
from udata.core.topic.api_fields import (
Expand All @@ -18,7 +19,8 @@
from udata.core.topic.models import Topic, TopicElement
from udata.core.topic.parsers import TopicApiParser, TopicElementsParser
from udata.core.topic.permissions import TopicEditPermission
from udata.utils import get_by
from udata.core.topic.search import TopicSearch
from udata.utils import get_by, multi_to_dict

DEFAULT_SORTING = "-created_at"

Expand All @@ -28,6 +30,7 @@

topic_parser = TopicApiParser()
elements_parser = TopicElementsParser()
search_parser = TopicSearch.as_request_parser()

common_doc = {"params": {"topic": "The topic ID"}}

Expand Down Expand Up @@ -312,3 +315,16 @@ def put(self, topic, element_id):
topic.update(**data)

return element


@ns.route("/search/", endpoint="topic_search")
class TopicSearchAPI(API):
"""Topics collection search endpoint"""

@apiv2.doc("search_topics")
@apiv2.expect(search_parser)
@apiv2.marshal_with(topic_page_fields)
def get(self):
"""Search all topics"""
search_parser.parse_args()
return search.query(TopicSearch, **multi_to_dict(request.args))
116 changes: 116 additions & 0 deletions udata/core/topic/search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import datetime

from udata.core.spatial.constants import ADMIN_LEVEL_MAX
from udata.core.spatial.models import GeoZone, admin_levels
from udata.core.topic.models import Topic
from udata.core.topic.parsers import TopicApiParser
from udata.models import Organization, User
from udata.search import (
BoolFilter,
Filter,
ListFilter,
ModelSearchAdapter,
ModelTermsFilter,
register,
)
from udata.utils import to_iso_datetime

__all__ = ("TopicSearch",)

DEFAULT_SORTING = "-created_at"


@register
class TopicSearch(ModelSearchAdapter):
model = Topic
search_url = "topics/"

sorts = {"created": "created_at", "last_modified": "last_modified"}

filters = {
"tag": ListFilter(),
"organization": ModelTermsFilter(model=Organization),
"owner": ModelTermsFilter(model=User),
"geozone": ModelTermsFilter(model=GeoZone),
"granularity": Filter(),
"featured": BoolFilter(),
}

@classmethod
def is_indexable(cls, topic: Topic) -> bool:
return not topic.private

@classmethod
def mongo_search(cls, args):
topics = Topic.objects.visible()
topics = TopicApiParser.parse_filters(topics, args)

sort = (
cls.parse_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"])

@classmethod
def serialize(cls, topic: Topic) -> dict:
organization = None
owner = None
if topic.organization:
org = Organization.objects(id=topic.organization.id).first()
organization = {
"id": str(org.id),
"name": org.name,
"public_service": 1 if org.public_service else 0,
"followers": org.metrics.get("followers", 0),
}
elif topic.owner:
owner = User.objects(id=topic.owner.id).first()
extras = {}
for key, value in topic.extras.items():
extras[key] = to_iso_datetime(value) if isinstance(value, datetime.datetime) else value

def get_elements_titles(elements: list, max_elements: int = 1000):
titles = []
for element in elements[:max_elements]:
if element.title:
titles.append(element.title)
elif element.element and getattr(element.element, "title", None):
titles.append(element.element.title)
return " ".join(titles)

document = {
"id": str(topic.id),
"name": topic.name,
"description": topic.description,
"created_at": to_iso_datetime(topic.created_at),
"last_modified": to_iso_datetime(topic.last_modified),
"featured": topic.featured,
"organization": organization,
"owner": str(owner.id) if owner else None,
"tags": topic.tags,
"extras": extras,
"elements_titles": get_elements_titles(topic.elements),
}

if topic.spatial is not None:
zone_ids = [z.id for z in topic.spatial.zones]
zones = GeoZone.objects(id__in=zone_ids)
geozones = []
coverage_level = ADMIN_LEVEL_MAX
for zone in zones:
geozones.append(
{
"id": zone.id,
"name": zone.name,
}
)
coverage_level = min(coverage_level, admin_levels[zone.level])
document.update(
{
"geozones": geozones,
"granularity": topic.spatial.granularity,
}
)

return document
1 change: 1 addition & 0 deletions udata/search/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,4 @@ def init_app(app):
import udata.core.dataset.search # noqa
import udata.core.reuse.search # noqa
import udata.core.organization.search # noqa
import udata.core.topic.search # noqa
45 changes: 45 additions & 0 deletions udata/tests/topic/test_search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from udata.core.organization import constants as org_constants
from udata.core.organization.factories import OrganizationFactory
from udata.core.spatial.factories import SpatialCoverageFactory
from udata.core.topic.factories import TopicFactory
from udata.core.topic.search import TopicSearch
from udata.tests.api import APITestCase
from udata.utils import to_iso_datetime


class TestTopicSearch(APITestCase):
def test_adapter_serialize(self):
org = OrganizationFactory(name="orga")
org.add_badge(org_constants.CERTIFIED)
org.add_badge(org_constants.PUBLIC_SERVICE)
assert org.public_service is True
spatial = SpatialCoverageFactory()
topic = TopicFactory(private=False, organization=org, spatial=spatial)

assert TopicSearch.is_indexable(topic)
serialized = TopicSearch.serialize(topic)
assert serialized == {
"id": str(topic.id),
"name": topic.name,
"description": topic.description,
"created_at": to_iso_datetime(topic.created_at),
"last_modified": to_iso_datetime(topic.last_modified),
"featured": topic.featured,
"organization": {
"id": str(topic.organization.id),
"name": "orga",
"public_service": 1,
"followers": 0,
},
"owner": None,
"tags": topic.tags,
"extras": topic.extras,
"elements_titles": " ".join([element.title for element in topic.elements]),
"geozones": [
{
"id": spatial.zones[0].id,
"name": spatial.zones[0].name,
}
],
"granularity": spatial.granularity,
}