Skip to content

Commit

Permalink
🔒 Manage access to concepts/future texts via the API on the API token
Browse files Browse the repository at this point in the history
  • Loading branch information
dariomory authored and joeribekker committed Dec 13, 2022
1 parent 5f85de8 commit 98e925d
Show file tree
Hide file tree
Showing 12 changed files with 230 additions and 101 deletions.
Binary file modified data/models.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions src/sdg/api/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ class TokenAdmin(admin.ModelAdmin, DynamicArrayMixin):
"last_seen",
"created",
"whitelisted_ips",
"api_default_most_recent",
)

readonly_fields = ("key", "last_seen")
ordering = ("organization",)
inlines = (TokenAuthorizationInline,)
Expand Down
22 changes: 22 additions & 0 deletions src/sdg/api/migrations/0008_token_api_default_most_recent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Generated by Django 3.2.16 on 2022-12-13 17:01

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("api", "0007_auto_20220922_1247"),
]

operations = [
migrations.AddField(
model_name="token",
name="api_default_most_recent",
field=models.BooleanField(
default=False,
help_text="Toon standaard de meest recente productbeschrijvingen in de API. Dit kunnen dus ook concept en toekomstige productbeschrijvingen zijn. Dit moet aan staan indien schrijfrechten op de API worden gegeven.",
verbose_name="Standaard meest recent",
),
),
]
27 changes: 17 additions & 10 deletions src/sdg/api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,23 @@ class Token(models.Model):
blank=True,
null=True,
)
whitelisted_ips = ArrayField(
models.CharField(
max_length=15,
validators=[validate_ipv4_address],
),
help_text=_("Whitelisted IP adressen van deze token."),
blank=True,
default=list,
)
api_default_most_recent = models.BooleanField(
_("Standaard meest recent"),
help_text=_(
"Toon standaard de meest recente productbeschrijvingen in de API. Dit kunnen dus ook concept en toekomstige productbeschrijvingen zijn. Dit moet aan staan indien schrijfrechten op de API worden gegeven."
),
default=False,
)

created = models.DateTimeField(
_("aangemaakt"),
auto_now_add=True,
Expand All @@ -56,16 +73,6 @@ class Token(models.Model):
help_text=_("Wanneer het token voor het laatst is gewijzigd."),
)

whitelisted_ips = ArrayField(
models.CharField(
max_length=15,
validators=[validate_ipv4_address],
),
help_text=_("Whitelisted IP adressen van deze token."),
blank=True,
default=list,
)

class Meta:
verbose_name = _("token")
verbose_name_plural = _("tokens")
Expand Down
150 changes: 82 additions & 68 deletions src/sdg/api/serializers/producten.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from rest_framework.fields import SerializerMethodField
from rest_framework.reverse import reverse

from sdg.api.serializers.fields import LabeledUrlListField
Expand Down Expand Up @@ -246,7 +245,6 @@ class ProductSerializer(ProductBaseSerializer):
source="catalogus.lokale_overheid"
)
publicatie_datum = serializers.DateField(
source="active_version.publicatie_datum",
allow_null=True,
help_text="De datum die aangeeft wanneer het product gepubliceerd is/wordt.",
)
Expand All @@ -256,20 +254,20 @@ class ProductSerializer(ProductBaseSerializer):
help_text="Een boolean die aangeeft of de organisatie dit product levert of niet. Als de verantwoordelijke organisatie niet expliciet heeft aangegeven dat een product aanwezig of afwezig is, dan is deze waarde `null`. Als een product afwezig is, dan moet er een toelichting worden gegeven in alle beschikbare talen. Alle andere vertaalde velden kunnen dan leeg blijven.",
)
vertalingen = LocalizedProductSerializer(
source="active_version.vertalingen",
many=True,
# In case there is no active version, there are no available
# translations. Hence, we allow null here (which affect the GET
# operation to actually work).
allow_null=True,
many=True,
help_text="Een lijst met specifieke teksten op basis van taal.",
)
beschikbare_talen = serializers.SerializerMethodField(
method_name="get_talen",
help_text="Alle beschikbare talen.",
)
versie = SerializerMethodField(
method_name="get_versie", help_text="De huidige versie van dit product."
versie = serializers.IntegerField(
help_text="De huidige versie van dit product.",
default=0,
)
doelgroep = serializers.ChoiceField(
source="generiek_product.doelgroep",
Expand Down Expand Up @@ -332,25 +330,48 @@ class Meta:
"view_name": "api:product-detail",
},
}
version_fields = (
"vertalingen",
"publicatie_datum",
"versie",
)

def get_fields(self):
"""
Dynamically bind the source of certain fields based on the version of the product.
"""
fields = super().get_fields()

@staticmethod
def _get_active_version(product: Product, field_name, default=None):
"""Get the value of a field from the product's active version."""
active_version = getattr(product, "active_version", None)
return getattr(active_version, field_name) if active_version else default
for field in self.Meta.version_fields:
fields[field].source = f"{self.version_property_name}.{field}"

def get_versie(self, obj: Product) -> int:
return self._get_active_version(obj, "versie", default=0)
return fields

@property
def version_property_name(self):
"""
Return the appropriate version property based on the meta headers.
If the token type is designed for editors, return the most recent version.
In other cases return the active version.
"""
request = self.context.get("request", object)
auth = getattr(request, "auth", None)

if auth and auth.api_default_most_recent:
return "most_recent_version"

return "active_version"

def get_talen(self, obj: Product) -> list:
return TaalChoices.get_available_languages()

def to_representation(self, instance):
data = super(ProductBaseSerializer, self).to_representation(instance)
active_version = getattr(instance, "active_version", None)
version = getattr(instance, self.version_property_name, None)

if active_version and getattr(instance, "_filter_taal", None):
translations = active_version.vertalingen.all()
if version and getattr(instance, "_filter_taal", None):
translations = version.vertalingen.all()
filtered_translations = [
i for i in translations if i.taal == instance._filter_taal
]
Expand All @@ -369,44 +390,40 @@ def validate(self, attrs):
{"generiekProduct": "Het veld 'product' is verplicht."}
)

most_recent_version = attrs["most_recent_version"]
version = attrs[self.version_property_name]

required_taalen = [taal[0] for taal in TaalChoices]
for version in most_recent_version["vertalingen"]:
if version["taal"] in required_taalen:
required_taalen.remove(version["taal"])

if required_taalen:
languages = [t["taal"] for t in version["vertalingen"]]
required_languages = TaalChoices.get_available_languages()
if any(lang not in languages for lang in required_languages):
raise serializers.ValidationError(
{
"vertalingen": f"Het veld 'taal' is verplicht. Geldige waarden zijn: {required_taalen}"
"vertalingen": f"Het veld 'taal' is verplicht. Geldige waarden zijn: {required_languages}"
}
)

if most_recent_version["vertalingen"]:
if not attrs["product_aanwezig"]:
for vertaling in most_recent_version["vertalingen"]:
if (
not vertaling["product_aanwezig_toelichting"]
or vertaling["product_aanwezig_toelichting"] == ""
):
raise serializers.ValidationError(
{
"productAanwezigToelichting": "Het veld 'productAanwezigToelichting' is verplicht als het product niet aanwezig is."
}
)
if not attrs["product_aanwezig"]:
for translation in version["vertalingen"]:
if (
not translation["product_aanwezig_toelichting"]
or translation["product_aanwezig_toelichting"] == ""
):
raise serializers.ValidationError(
{
"productAanwezigToelichting": "Het veld 'productAanwezigToelichting' is verplicht als het product niet aanwezig is."
}
)

if attrs["product_valt_onder"]:
for vertaling in most_recent_version["vertalingen"]:
if (
not vertaling["product_valt_onder_toelichting"]
or vertaling["product_valt_onder_toelichting"] == ""
):
raise serializers.ValidationError(
{
"productValtOnderToelichting": "Het veld 'productValtOnderToelichting' is verplicht als het product onder een ander product valt."
}
)
if attrs["product_valt_onder"]:
for translation in version["vertalingen"]:
if (
not translation["product_valt_onder_toelichting"]
or translation["product_valt_onder_toelichting"] == ""
):
raise serializers.ValidationError(
{
"productValtOnderToelichting": "Het veld 'productValtOnderToelichting' is verplicht als het product onder een ander product valt."
}
)

return attrs

Expand Down Expand Up @@ -594,8 +611,7 @@ def create(self, validated_data):
bevoegde_organisatie = validated_data.pop("bevoegde_organisatie", [])
locaties = validated_data.pop("locaties", [])

most_recent_version = validated_data.pop("most_recent_version")
vertalingen = most_recent_version.pop("vertalingen", [])
version = validated_data.get(self.version_property_name, {})
publicatie_datum = data.get("publicatie_datum", None)

if publicatie_datum:
Expand Down Expand Up @@ -683,28 +699,26 @@ def create(self, validated_data):
defaults={"publicatie_datum": publicatie_datum},
)

if vertalingen:
for vertaling in vertalingen:

verwijzing_links = []
if "verwijzing_links" in vertaling:
for verwijzing_link in vertaling["verwijzing_links"]:
verwijzing_links.append(list(verwijzing_link.values()))

vertaling["verwijzing_links"] = verwijzing_links
for translation in version.get("vertalingen", []):
verwijzing_links = []
if "verwijzing_links" in translation:
for verwijzing_link in translation["verwijzing_links"]:
verwijzing_links.append(list(verwijzing_link.values()))

if product_versie_created:
translation["verwijzing_links"] = verwijzing_links

LocalizedProduct.objects.create(
**vertaling,
product_versie=product_versie,
)
if product_versie_created:

else:
vertaling["product_versie"] = product_versie
LocalizedProduct.objects.create(
**translation,
product_versie=product_versie,
)

LocalizedProduct.objects.filter(
taal=vertaling["taal"], product_versie=product_versie
).update(**vertaling)
else:
translation["product_versie"] = product_versie
localized_product_qs = LocalizedProduct.objects.filter(
product_versie=product_versie, taal=translation["taal"]
)
localized_product_qs.update(**translation)

return product
22 changes: 21 additions & 1 deletion src/sdg/api/tests/test_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from rest_framework.reverse import reverse
from rest_framework.test import APITestCase

from sdg.api.tests.factories.token import TokenAuthorizationFactory
from sdg.core.constants import TaalChoices
from sdg.core.tests.factories.catalogus import ProductenCatalogusFactory
from sdg.core.tests.factories.logius import OverheidsorganisatieFactory
Expand All @@ -12,7 +13,6 @@
LokaleOverheidFactory,
)
from sdg.producten.models import Product
from sdg.producten.models.product import ProductVersie
from sdg.producten.tests.constants import FUTURE_DATE, NOW_DATE, PAST_DATE
from sdg.producten.tests.factories.localized import LocalizedProductFactory
from sdg.producten.tests.factories.product import (
Expand All @@ -25,6 +25,16 @@
class ProductenCatalogusFilterTests(APITestCase):
url = reverse("api:productencatalogus-list")

def setUp(self):
self.lokale_overheid = LokaleOverheidFactory.create()
self.token_authorization = TokenAuthorizationFactory.create(
lokale_overheid=self.lokale_overheid,
token__api_default_most_recent=True,
)
self.client.defaults.update(
{"HTTP_AUTHORIZATION": f"Token {self.token_authorization.token}"}
)

def test_filter_organisatie(self):
catalog, *_ = ProductenCatalogusFactory.create_batch(5)

Expand Down Expand Up @@ -77,6 +87,16 @@ def test_filter_organisatie_owms_pref_label(self):
class ProductFilterTests(APITestCase):
url = reverse("api:product-list")

def setUp(self):
self.lokale_overheid = LokaleOverheidFactory.create()
self.token_authorization = TokenAuthorizationFactory.create(
lokale_overheid=self.lokale_overheid,
token__api_default_most_recent=True,
)
self.client.defaults.update(
{"HTTP_AUTHORIZATION": f"Token {self.token_authorization.token}"}
)

def test_filter_organisatie(self):
(
product1,
Expand Down
Loading

0 comments on commit 98e925d

Please sign in to comment.