Skip to content
Open
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
4 changes: 3 additions & 1 deletion udata/api_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,8 @@ def constructor_write(**kwargs):
f"EmbeddedDocumentField `{key}` requires a `nested_fields` param to serialize/deserialize or a `@generate_fields()` definition."
)

elif isinstance(field, mongo_fields.MultiPolygonField):
constructor = restx_fields.Raw
else:
raise ValueError(f"Unsupported MongoEngine field type {field.__class__}")

Expand Down Expand Up @@ -804,7 +806,7 @@ def patch(obj: _T, request) -> _T:
model_attribute, mongo_fields.ReferenceField | mongo_fields.LazyReferenceField
):
value = wrap_primary_key(key, model_attribute, value)
elif isinstance(
elif value and isinstance(
model_attribute,
(
mongoengine.fields.GenericReferenceField,
Expand Down
8 changes: 5 additions & 3 deletions udata/core/spatial/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from werkzeug.local import LocalProxy
from werkzeug.utils import cached_property

from udata.api_fields import field, generate_fields
from udata.app import cache
from udata.core.metrics.models import WithMetrics
from udata.i18n import _, get_locale, language
Expand Down Expand Up @@ -135,12 +136,13 @@ def get_spatial_admin_levels():
admin_levels = LocalProxy(get_spatial_admin_levels)


@generate_fields()
class SpatialCoverage(EmbeddedDocument):
"""Represent a spatial coverage as a list of territories and/or a geometry."""

geom = MultiPolygonField()
zones = ListField(ReferenceField(GeoZone))
granularity = StringField(default="other")
geom = field(MultiPolygonField())
zones = field(ListField(ReferenceField(GeoZone)))
granularity = field(StringField(default="other"))

@property
def granularity_label(self):
Expand Down
88 changes: 0 additions & 88 deletions udata/core/topic/api_fields.py

This file was deleted.

119 changes: 90 additions & 29 deletions udata/core/topic/apiv2.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,58 @@
import logging

import mongoengine
from flask import abort, request
from flask import abort, request, url_for
from flask_security import current_user

from udata import search
from udata.api import API, api, apiv2
from udata.api import API, api, apiv2, fields
from udata.api_fields import patch, patch_and_save
from udata.core.discussions.models import Discussion
from udata.core.topic.api_fields import (
element_fields,
element_page_fields,
topic_fields,
topic_input_fields,
topic_page_fields,
topic_search_page_fields,
)
from udata.core.topic.forms import TopicElementForm, TopicForm
from udata.core.topic import DEFAULT_PAGE_SIZE
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.core.topic.search import TopicSearch
from udata.mongo.errors import FieldValidationError

apiv2.inherit("ModelReference", api.model_reference)
apiv2.inherit("TopicElement (read)", TopicElement.__read_fields__)

topic_fields = apiv2.clone(
"Topic",
Topic.__read_fields__,
{
"elements": fields.Raw(
attribute=lambda o: {
"rel": "subsection",
"href": url_for(
"apiv2.topic_elements",
topic=o.id,
page=1,
page_size=DEFAULT_PAGE_SIZE,
_external=True,
),
"type": "GET",
"total": o.elements.count(),
},
description="Link to the topic elements",
),
},
)

topic_page_fields = apiv2.model("TopicPage", fields.pager(topic_fields))
topic_search_page_fields = apiv2.model("TopicSearchPage", fields.search_pager(topic_fields))
apiv2.inherit("TopicElementPage", TopicElement.__page_fields__)

topic_input_fields = apiv2.clone(
"TopicInput",
Topic.__write_fields__,
{
"elements": fields.List(
fields.Nested(TopicElement.__read_fields__), description="The topic elements"
),
},
)

DEFAULT_SORTING = "-created_at"

Expand Down Expand Up @@ -75,8 +106,28 @@ def get(self):
@apiv2.response(400, "Validation error")
def post(self):
"""Create a topic"""
form = apiv2.validate(TopicForm)
return form.save(), 201
# Elements are a reverse relationship (TopicElement → Topic), not a field
# on Topic itself, so patch() can't handle them. We extract them from the
# payload and manage them manually after saving the topic.
# TODO: patch() could support virtual fields for reverse relationships to
# avoid this manual handling.
data = request.json.copy()
elements_data = data.pop("elements", None)

topic = patch(Topic(), data)

if not topic.owner and not topic.organization:
topic.owner = current_user._get_current_object()

topic.save()

if elements_data is not None:
for element_data in elements_data:
element = patch(TopicElement(), element_data)
element.topic = topic
element.save()

return topic, 201


@ns.route("/<topic:topic>/", endpoint="topic", doc=common_doc)
Expand All @@ -98,8 +149,20 @@ def put(self, topic):
"""Update a given topic"""
if not TopicEditPermission(topic).can():
apiv2.abort(403, "Forbidden")
form = apiv2.validate(TopicForm, topic)
return form.save()

data = request.json.copy()
elements_data = data.pop("elements", None)

patch_and_save(topic, data)

if elements_data is not None:
TopicElement.objects(topic=topic).delete()
for element_data in elements_data:
element = patch(TopicElement(), element_data)
element.topic = topic
element.save()

return topic

@apiv2.secure
@apiv2.doc("delete_topic")
Expand All @@ -119,7 +182,7 @@ def delete(self, topic):
class TopicElementsAPI(API):
@apiv2.doc("topic_elements")
@apiv2.expect(elements_parser.parser)
@apiv2.marshal_with(element_page_fields)
@apiv2.marshal_with(TopicElement.__page_fields__)
def get(self, topic):
"""Get a given topic's elements with pagination."""
args = elements_parser.parse()
Expand All @@ -132,7 +195,7 @@ def get(self, topic):
@apiv2.secure
@apiv2.doc("topic_elements_create")
@apiv2.expect([api.model_reference])
@apiv2.marshal_list_with(element_fields)
@apiv2.marshal_list_with(TopicElement.__read_fields__)
@apiv2.response(400, "Expecting a list")
@apiv2.response(404, "Topic not found")
@apiv2.response(403, "Forbidden")
Expand All @@ -148,19 +211,19 @@ def post(self, topic):
errors = []
elements = []
for element_data in data:
form = TopicElementForm.from_json(element_data, meta={"csrf": False})
if not form.validate():
errors.append(form.errors)
else:
element = TopicElement()
form.populate_obj(element)
try:
element = patch(TopicElement(), element_data)
element.topic = topic
element.save()
elements.append(element)
except FieldValidationError as e:
errors.append({e.field: [str(e)]})

if errors:
apiv2.abort(400, errors=errors)

for element in elements:
element.save()

topic.save()

return elements, 201
Expand Down Expand Up @@ -206,8 +269,8 @@ def delete(self, topic, element_id):

@apiv2.secure
@apiv2.doc("topic_element_update")
@apiv2.expect(element_fields)
@apiv2.marshal_with(element_fields)
@apiv2.expect(TopicElement.__read_fields__)
@apiv2.marshal_with(TopicElement.__read_fields__)
@apiv2.response(404, "Topic not found")
@apiv2.response(404, "Element not found in topic")
@apiv2.response(204, "Success")
Expand All @@ -217,8 +280,6 @@ def put(self, topic, element_id):
apiv2.abort(403, "Forbidden")

element = TopicElement.objects.get_or_404(pk=element_id)
form = apiv2.validate(TopicElementForm, element)
form.populate_obj(element)
element.save()
patch_and_save(element, request)

return element
Loading