Skip to content

Commit

Permalink
Merge pull request #1463 from open-zaak/feature/1462-zaken-zoek-on-za…
Browse files Browse the repository at this point in the history
…aktypen

add `zaaktype__in`  for zaak__zoek
  • Loading branch information
annashamray committed Sep 28, 2023
2 parents 49b6444 + 768e55d commit e40dbbc
Show file tree
Hide file tree
Showing 7 changed files with 284 additions and 45 deletions.
9 changes: 9 additions & 0 deletions src/openzaak/components/zaken/api/serializers/zaken.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
)
from openzaak.utils.auth import get_auth
from openzaak.utils.exceptions import DetermineProcessEndDateException
from openzaak.utils.serializer_fields import FKOrServiceUrlField
from openzaak.utils.validators import (
LooseFkIsImmutableValidator,
LooseFkResourceValidator,
Expand Down Expand Up @@ -501,6 +502,14 @@ class ZaakZoekSerializer(serializers.Serializer):
required=False,
help_text=_("Array of unieke resource identifiers (UUID4)"),
)
zaaktype__in = serializers.ListField(
child=FKOrServiceUrlField(),
required=False,
help_text=_("Array van zaaktypen."),
)

class Meta:
model = Zaak


class StatusSerializer(serializers.HyperlinkedModelSerializer):
Expand Down
7 changes: 7 additions & 0 deletions src/openzaak/components/zaken/api/viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,13 @@ def _delete_oios():

super().perform_destroy(instance)

def get_search_input(self):
serializer = self.get_search_input_serializer_class()(
data=self.request.data, context={"request": self.request}
)
serializer.is_valid(raise_exception=True)
return serializer.validated_data


@conditional_retrieve()
class StatusViewSet(
Expand Down
6 changes: 6 additions & 0 deletions src/openzaak/components/zaken/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7470,6 +7470,12 @@ components:
items:
type: string
format: uuid
zaaktype__in:
description: Array van zaaktypen.
type: array
items:
type: string
format: uri
identificatie:
title: Identificatie
description: De unieke identificatie van de ZAAK binnen de organisatie die
Expand Down
8 changes: 8 additions & 0 deletions src/openzaak/components/zaken/swagger2.0.json
Original file line number Diff line number Diff line change
Expand Up @@ -9240,6 +9240,14 @@
"format": "uuid"
}
},
"zaaktype__in": {
"description": "Array van zaaktypen.",
"type": "array",
"items": {
"type": "string",
"format": "uri"
}
},
"identificatie": {
"title": "Identificatie",
"description": "De unieke identificatie van de ZAAK binnen de organisatie die verantwoordelijk is voor de behandeling van de ZAAK.",
Expand Down
78 changes: 78 additions & 0 deletions src/openzaak/components/zaken/tests/test_zaak_zoek.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from django.contrib.gis.geos import Point
from django.test import override_settings, tag

import requests_mock
from rest_framework import status
from rest_framework.test import APITestCase
from vng_api_common.tests import TypeCheckMixin, get_validation_errors, reverse
Expand Down Expand Up @@ -153,3 +154,80 @@ def test_zoek_filter_backend_fields(self):
results = response.json()["results"]
self.assertEqual(len(results), 1)
self.assertEqual(results[0]["identificatie"], zaak2.identificatie)

@override_settings(ALLOWED_HOSTS=["testserver", "testserver.com"])
def test_zoek_zaaktype_in_local(self):
zaak1, zaak2, zaak3 = ZaakFactory.create_batch(3)
url = get_operation_url("zaak__zoek")
data = {
"zaaktype__in": [
f"http://testserver.com{reverse(zaak1.zaaktype)}",
f"http://testserver.com{reverse(zaak2.zaaktype)}",
]
}

response = self.client.post(
url, data, **ZAAK_WRITE_KWARGS, HTTP_HOST="testserver.com",
)

self.assertEqual(response.status_code, status.HTTP_200_OK)

data = response.json()["results"]
data = sorted(data, key=lambda zaak: zaak["identificatie"])

self.assertEqual(len(data), 2)
self.assertEqual(data[0]["url"], f"http://testserver.com{reverse(zaak1)}")
self.assertEqual(data[1]["url"], f"http://testserver.com{reverse(zaak2)}")

@tag("external-urls")
@requests_mock.Mocker()
def test_zoek_zaaktype_in_external(self, m):
external_zaaktype1 = "https://externe.catalogus.nl/api/v1/zaaktypen/b71f72ef-198d-44d8-af64-ae1932df830a"
external_zaaktype2 = "https://externe.catalogus.nl/api/v1/zaaktypen/d530aa07-3e4e-42ff-9be8-3247b3a6e7e3"
zaak1 = ZaakFactory.create(zaaktype=external_zaaktype1)
zaak2 = ZaakFactory.create(zaaktype=external_zaaktype2)
ZaakFactory.create()

url = get_operation_url("zaak__zoek")
data = {"zaaktype__in": [external_zaaktype1, external_zaaktype2]}
m.get(external_zaaktype1, json={"url": external_zaaktype1})
m.get(external_zaaktype2, json={"url": external_zaaktype2})

response = self.client.post(url, data, **ZAAK_WRITE_KWARGS)

self.assertEqual(response.status_code, status.HTTP_200_OK)

data = response.json()["results"]
data = sorted(data, key=lambda zaak: zaak["identificatie"])

self.assertEqual(len(data), 2)
self.assertEqual(data[0]["url"], f"http://testserver{reverse(zaak1)}")
self.assertEqual(data[1]["url"], f"http://testserver{reverse(zaak2)}")

@tag("external-urls")
@requests_mock.Mocker()
@override_settings(ALLOWED_HOSTS=["testserver", "testserver.com"])
def test_zoek_zaaktype_in_local_and_external(self, m):
local_zaaktype = ZaakTypeFactory.create()
local_zaaktype_url = f"http://testserver.com{reverse(local_zaaktype)}"
external_zaaktype = "https://externe.catalogus.nl/api/v1/zaaktypen/d530aa07-3e4e-42ff-9be8-3247b3a6e7e3"
zaak1 = ZaakFactory.create(zaaktype=local_zaaktype)
zaak2 = ZaakFactory.create(zaaktype=external_zaaktype)
ZaakFactory.create()

url = get_operation_url("zaak__zoek")
data = {"zaaktype__in": [local_zaaktype_url, external_zaaktype]}
m.get(external_zaaktype, json={"url": external_zaaktype})

response = self.client.post(
url, data, **ZAAK_WRITE_KWARGS, HTTP_HOST="testserver.com"
)

self.assertEqual(response.status_code, status.HTTP_200_OK)

data = response.json()["results"]
data = sorted(data, key=lambda zaak: zaak["identificatie"])

self.assertEqual(len(data), 2)
self.assertEqual(data[0]["url"], f"http://testserver.com{reverse(zaak1)}")
self.assertEqual(data[1]["url"], f"http://testserver.com{reverse(zaak2)}")
205 changes: 160 additions & 45 deletions src/openzaak/utils/lookups.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
# Copyright (C) 2022 Dimpact
from typing import Tuple

from django.db.models.lookups import Exact as _Exact
from django.core.exceptions import EmptyResultSet
from django.db.models.lookups import Exact as _Exact, In as _In

from django_loose_fk.lookups import get_normalized_value
from django_loose_fk.virtual_models import ProxyMixin
Expand All @@ -11,12 +12,7 @@
from .fields import FkOrServiceUrlField


@FkOrServiceUrlField.register_lookup
class Exact(_Exact):
"""
combine Exact lookups for FkOrUrlField and ServiceUrlField
"""

class FkOrServiceUrlFieldMixin:
def get_cols(self) -> tuple:
""" return tuple of cols for local fk, remote base url and remote relative url"""
target = self.lhs.target
Expand All @@ -28,27 +24,71 @@ def get_cols(self) -> tuple:
target._url_field._relative_field.get_col(db_table),
)

def get_prep_lookup(self):
def split_lhs(
self, compiler, connection
) -> Tuple[str, tuple, str, tuple, str, tuple]:

fk_lhs, base_url_lhs, relative_url_lhs = self.get_cols()

fk_lhs_sql, fk_params = self.process_lhs(compiler, connection, lhs=fk_lhs)
base_lhs_sql, base_lhs_params = self.process_lhs(
compiler, connection, lhs=base_url_lhs
)
relative_lhs_sql, relative_lhs_params = self.process_lhs(
compiler, connection, lhs=relative_url_lhs
)

return (
fk_lhs_sql,
fk_params,
base_lhs_sql,
base_lhs_params,
relative_lhs_sql,
relative_lhs_params,
)

def get_prep_lookup(self) -> list:
if not self.rhs_is_direct_value():
return super().get_prep_lookup()

if isinstance(self.rhs, ProxyMixin):
self.rhs = self.rhs._loose_fk_data["url"]
fk_lhs, base_url_lhs, relative_url_lhs = self.get_cols()
rhs_values = (
self.rhs if self.get_db_prep_lookup_value_is_iterable else [self.rhs]
)

if isinstance(self.rhs, str):
# dealing with a remote composite URL - return list
fk_lhs, base_url_lhs, relative_url_lhs = self.get_cols()
prepared_values = []
for rhs_value in rhs_values:
if isinstance(rhs_value, ProxyMixin):
rhs_value = rhs_value._loose_fk_data["url"]

base_value, relative_value = decompose_value(self.rhs)
base_normalized_value = get_normalized_value(base_value)[0]
relative__normalized_value = get_normalized_value(relative_value)[0]
return [
base_url_lhs.field.get_prep_value(base_normalized_value),
relative_url_lhs.field.get_prep_value(relative__normalized_value),
]
if isinstance(rhs_value, str):
# dealing with a remote composite URL - return list
base_value, relative_value = decompose_value(rhs_value)
base_normalized_value = get_normalized_value(base_value)[0]
relative__normalized_value = get_normalized_value(relative_value)[0]
prepared_value = [
base_url_lhs.field.get_prep_value(base_normalized_value),
relative_url_lhs.field.get_prep_value(relative__normalized_value),
]

# local URl
return get_normalized_value(self.rhs)[0]
else:
# local urls = return their pk
prepared_value = get_normalized_value(rhs_value)[0]

prepared_values.append(prepared_value)

return (
prepared_values[0]
if not self.get_db_prep_lookup_value_is_iterable
else prepared_values
)


@FkOrServiceUrlField.register_lookup
class Exact(FkOrServiceUrlFieldMixin, _Exact):
"""
combine Exact lookups for FkOrUrlField and ServiceUrlField
"""

def get_db_prep_lookup(self, value, connection):
# composite field
Expand All @@ -69,29 +109,6 @@ def get_db_prep_lookup(self, value, connection):

return super().get_db_prep_lookup(value, connection)

def split_lhs(
self, compiler, connection
) -> Tuple[str, tuple, str, tuple, str, tuple]:

fk_lhs, base_url_lhs, relative_url_lhs = self.get_cols()

fk_lhs_sql, fk_params = self.process_lhs(compiler, connection, lhs=fk_lhs)
base_lhs_sql, base_lhs_params = self.process_lhs(
compiler, connection, lhs=base_url_lhs
)
relative_lhs_sql, relative_lhs_params = self.process_lhs(
compiler, connection, lhs=relative_url_lhs
)

return (
fk_lhs_sql,
fk_params,
base_lhs_sql,
base_lhs_params,
relative_lhs_sql,
relative_lhs_params,
)

def as_sql(self, compiler, connection):
# process lhs
(
Expand Down Expand Up @@ -120,3 +137,101 @@ def as_sql(self, compiler, connection):
sql = f"{fk_lhs_sql} {rhs_sql}"

return sql, params


@FkOrServiceUrlField.register_lookup
class In(FkOrServiceUrlFieldMixin, _In):
"""
Split the IN query into two IN queries, per datatype.
Creates an IN query for the url field values, and an IN query for the FK
field values, joined together by an OR.
This realization will add additional DB query for every external url item in rhs list
"""

lookup_name = "in"

def process_rhs(self, compiler, connection):
"""
separate list of values into two lists because we will use different expressions for them
"""
if self.rhs_is_direct_value():
target = self.lhs.target
db_table = target.model._meta.db_table

remote_rhs = [obj for obj in self.rhs if isinstance(obj, list)]
local_rhs = [obj for obj in self.rhs if obj not in remote_rhs]

if remote_rhs:
url_lhs = target._url_field.get_col(db_table)

_remote_lookup = _In(url_lhs, remote_rhs)
url_rhs_sql, url_rhs_params = _remote_lookup.process_rhs(
compiler, connection
)
else:
url_rhs_sql, url_rhs_params = None, ()

# filter out the remote objects
if local_rhs:
fk_lhs = target._fk_field.get_col(db_table)

_local_lookup = _In(fk_lhs, local_rhs)
fk_rhs_sql, fk_rhs_params = _local_lookup.process_rhs(
compiler, connection
)
else:
fk_rhs_sql, fk_rhs_params = None, ()

else:
# we're dealing with something that can be expressed as SQL -> it's local only!
url_rhs_sql, url_rhs_params = None, ()
fk_rhs_sql, fk_rhs_params = super().process_rhs(compiler, connection)

return url_rhs_sql, url_rhs_params, fk_rhs_sql, fk_rhs_params

def as_sql(self, compiler, connection):
# process lhs
(
fk_lhs_sql,
fk_lhs_params,
base_lhs_sql,
base_lhs_params,
relative_lhs_sql,
relative_lhs_params,
) = self.split_lhs(compiler, connection)

# process rhs
url_rhs_sql, url_rhs_params, fk_rhs_sql, fk_params = self.process_rhs(
compiler, connection
)

# combine
if not fk_rhs_sql and not url_rhs_sql:
raise EmptyResultSet()

if fk_rhs_sql:
fk_rhs_sql = self.get_rhs_op(connection, fk_rhs_sql)
fk_sql = "%s %s" % (fk_lhs_sql, fk_rhs_sql)
else:
fk_sql = None

if url_rhs_sql:
url_rhs_sql = "IN (" + ", ".join(["(%s, %s)"] * len(url_rhs_params)) + ")"
url_sql = f"({base_lhs_sql}, {relative_lhs_sql}) {url_rhs_sql}"
# flatten param list
url_params = sum(url_rhs_params, [])
else:
url_sql = None
url_params = []

if not fk_sql:
return url_sql, url_params

if not url_sql:
return fk_sql, fk_params

params = url_params + list(fk_params)
sql = "(%s OR %s)" % (url_sql, fk_sql)

return sql, params

0 comments on commit e40dbbc

Please sign in to comment.