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
1 change: 1 addition & 0 deletions udata/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,7 @@ def init_app(app):
import udata.features.identicon.api # noqa
import udata.features.territories.api # noqa
import udata.harvest.api # noqa
import udata.core.standard.api # noqa

for module in entrypoints.get_enabled("udata.apis", app).values():
module if inspect.ismodule(module) else import_module(module)
Expand Down
Empty file added udata/core/standard/__init__.py
Empty file.
80 changes: 80 additions & 0 deletions udata/core/standard/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from datetime import datetime

from flask import request
from flask_login import current_user

from udata.api import API, api, errors
from udata.api_fields import patch, patch_and_save

from .models import Standard
from .permissions import StandardEditPermission

ns = api.namespace("standards", "Standards related operations")

common_doc = {"params": {"standard": "The standard ID or slug"}}


@ns.route("/", endpoint="standards")
class StandardListAPI(API):
@api.doc("list_standards")
@api.expect(Standard.__index_parser__)
@api.marshal_with(Standard.__page_fields__)
def get(self):
query = Standard.objects(deleted=None)
return Standard.apply_pagination(Standard.apply_sort_filters(query))

@api.secure
@api.doc("create_standard")
@api.expect(Standard.__write_fields__)
@api.response(400, "Validation error")
@api.marshal_with(Standard.__read_fields__, code=201)
def post(self):
standard = patch(Standard(), request)

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

standard.save()

return patch_and_save(standard, request), 201


@ns.route("/<standard:standard>/", endpoint="standard", doc=common_doc)
class StandardAPI(API):
@api.doc("get_standard")
@api.marshal_with(Standard.__read_fields__)
def get(self, standard):
"""Fetch a given standard"""
if not StandardEditPermission(standard).can():
if standard.private:
api.abort(404)
elif standard.deleted:
api.abort(410, "This standard has been deleted")
return standard

@api.secure
@api.doc("update_standard")
@api.expect(Standard.__write_fields__)
@api.marshal_with(Standard.__read_fields__)
@api.response(400, errors.VALIDATION_ERROR)
def put(self, standard):
"""Update a given standard"""
request_deleted = request.json.get("deleted", True)
if standard.deleted and request_deleted is not None:
api.abort(410, "This standard has been deleted")
StandardEditPermission(standard).test()

# This is a patch but old API acted like PATCH on PUT requests.
return patch_and_save(standard, request)

@api.secure
@api.doc("delete_standard")
@api.response(204, "Standard deleted")
def delete(self, standard):
"""Delete a given standard"""
if standard.deleted:
api.abort(410, "This standard has been deleted")
StandardEditPermission(standard).test()
standard.deleted = datetime.utcnow()
standard.save()
return "", 204
15 changes: 15 additions & 0 deletions udata/core/standard/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from udata.i18n import lazy_gettext as _

STANDARD_TYPES = {
"tableschema": "Table Schema",
"jsonschema": "JSON Schema",
"datapackage": "Data Package",
"other": _("Other"),
}

STATUS_TYPES = {
"adopted": _("Adopted"),
"published": _("Published"),
"construction": _("In construction"),
"investigation": _("Investigated"),
}
112 changes: 112 additions & 0 deletions udata/core/standard/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import udata.core.contact_point.api_fields as contact_api_fields
from udata.api_fields import field, function_field, generate_fields
from udata.core.owned import Owned
from udata.i18n import lazy_gettext as _
from udata.mongo import db
from udata.mongo.errors import FieldValidationError
from udata.uris import endpoint_for
from udata.utils import hash_url

from .constants import STANDARD_TYPES, STATUS_TYPES


def check_url_does_not_exists(url, **_kwargs):
"""Ensure a reuse URL is not yet registered"""
if url and Standard.url_exists(url):
raise FieldValidationError(_("This URL is already registered"), field="url")


@generate_fields()
class Standard(db.Datetimed, Owned, db.Document):
name = field(
db.StringField(required=True),
sortable=True,
show_as_ref=True,
)
slug = field(
db.SlugField(max_length=255, required=True, populate_from="name", update=True, follow=True),
readonly=True,
auditable=False,
)
description = field(
db.StringField(required=True),
markdown=True,
)
type = field(
db.StringField(required=True, choices=list(STANDARD_TYPES)),
filterable={},
)
url = field(
db.URLField(required=True),
description="The URL to query the schema",
checks=[check_url_does_not_exists],
)
status = field(
db.StringField(required=True, choices=list(STATUS_TYPES)),
filterable={},
)
tags = field(
db.TagListField(),
filterable={
"key": "tag",
},
)
extras = field(db.ExtrasField(), auditable=False)
deleted = field(
db.DateTimeField(),
auditable=False,
)
urlhash = db.StringField(required=True, unique=True)
archived = field(
db.DateTimeField(),
)
contact_points = field(
db.ListField(
field(
db.ReferenceField("ContactPoint", reverse_delete_rule=db.PULL),
nested_fields=contact_api_fields.contact_point_fields,
allow_null=True,
),
),
filterable={
"key": "contact_point",
},
)

verbose_name = _("standard")

meta = {
"indexes": [
"$name",
"created_at",
"last_modified",
"urlhash",
]
}

def __str__(self):
return self.name or ""

@function_field(description="Link to the API endpoint for this report")
def self_api_url(self):
return endpoint_for("api.report", report=self, _external=True)

@classmethod
def url_exists(cls, url):
urlhash = hash_url(url)
return cls.objects(urlhash=urlhash).count() > 0

@classmethod
def get(cls, id_or_slug):
obj = cls.objects(slug=id_or_slug).first()
return obj or cls.objects.get_or_404(id=id_or_slug)

@property
def type_label(self):
return STANDARD_TYPES[self.type]

def clean(self):
super(Standard, self).clean()
"""Auto populate urlhash from url"""
if not self.urlhash or "url" in self._get_changed_fields():
self.urlhash = hash_url(self.url)
19 changes: 19 additions & 0 deletions udata/core/standard/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from udata.auth import Permission, UserNeed
from udata.core.organization.permissions import (
OrganizationAdminNeed,
OrganizationEditorNeed,
)
from udata.core.standard.models import Standard


class StandardEditPermission(Permission):
def __init__(self, standard: Standard) -> None:
needs = []

if standard.organization:
needs.append(OrganizationAdminNeed(standard.organization.id))
needs.append(OrganizationEditorNeed(standard.organization.id))
elif standard.owner:
needs.append(UserNeed(standard.owner.fs_uniquifier))

super(StandardEditPermission, self).__init__(*needs)
1 change: 1 addition & 0 deletions udata/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from udata.core.spam.models import * # noqa
from udata.core.reports.models import * # noqa
from udata.core.dataservices.models import * # noqa
from udata.core.standard.models import * # noqa

from udata.features.transfer.models import * # noqa
from udata.features.territories.models import * # noqa
Expand Down
5 changes: 5 additions & 0 deletions udata/routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,10 @@ class ReportConverter(ModelConverter):
model = models.Report


class StandardConverter(ModelConverter):
model = models.Standard


class TerritoryConverter(PathConverter):
DEFAULT_PREFIX = "fr" # TODO: make it a setting parameter

Expand Down Expand Up @@ -248,5 +252,6 @@ def init_app(app):
app.url_map.converters["territory"] = TerritoryConverter
app.url_map.converters["contact_point"] = ContactPointConverter
app.url_map.converters["report"] = ReportConverter
app.url_map.converters["standard"] = StandardConverter

app.jinja_env.globals["endpoint_for"] = endpoint_for