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
19 changes: 19 additions & 0 deletions froide/georegion/factories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from django.contrib.gis.geos import MultiPolygon

import factory
from factory.django import DjangoModelFactory

from froide.georegion.models import GeoRegion
from froide.helper.text_utils import slugify


class GeoRegionFactory(DjangoModelFactory):
class Meta:
model = GeoRegion

name = factory.Sequence(lambda n: "GeoRegion {0}".format(n))
slug = factory.LazyAttribute(lambda o: slugify(o.name))
kind = "country"
geom = MultiPolygon()
depth = 1
path = factory.Sequence(lambda n: f"{n:04d}")
38 changes: 7 additions & 31 deletions froide/publicbody/api_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from django.db.models import Q

from django_filters import rest_framework as filters
from elasticsearch_dsl.query import Q as ESQ
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.settings import api_settings
Expand All @@ -15,6 +14,7 @@
)
from froide.helper.search import SearchQuerySetWrapper
from froide.helper.search.api_views import ESQueryMixin
from froide.publicbody.filters import PublicBodyFilterSet

from .documents import PublicBodyDocument
from .models import Category, Classification, FoiLaw, Jurisdiction, PublicBody
Expand Down Expand Up @@ -233,6 +233,9 @@ class PublicBodyViewSet(
}
filter_backends = (filters.DjangoFilterBackend,)
filterset_class = PublicBodyFilter
search_model = PublicBody
search_document = PublicBodyDocument
searchfilterset_class = PublicBodyFilterSet

# OpenRefine needs JSONP responses
# This is OK because authentication is not considered
Expand Down Expand Up @@ -299,36 +302,8 @@ def get_serializer_context(self):
return ctx

def get_searchqueryset(self):
query = self.request.GET.get("q", "")

sqs = SearchQuerySetWrapper(PublicBodyDocument.search(), PublicBody)

if len(query) > 2:
sqs = sqs.set_query(
ESQ("multi_match", query=query, fields=["name_auto", "content"])
)

model_filters = {
"jurisdiction": Jurisdiction,
"classification": Classification,
"categories": Category,
"regions": GeoRegion,
}
for key, model in model_filters.items():
pks = self.request.GET.getlist(key)
if pks:
try:
obj = model.objects.filter(pk__in=pks)
sqs = sqs.filter(**{key: [o.pk for o in obj]})
except ValueError:
# Make result set empty, no 0 pk present
sqs = sqs.filter(key, **{key: 0})

other_filters = {"regions_kind": "regions_kind"}
for key, search_key in other_filters.items():
values = self.request.GET.getlist(key)
if values:
sqs = sqs.filter(**{search_key: values})
sqs = self.search_document.search()
sqs = SearchQuerySetWrapper(sqs, self.search_model)

sqs = sqs.add_aggregation(
[
Expand All @@ -338,4 +313,5 @@ def get_searchqueryset(self):
"regions",
]
)

return sqs
20 changes: 19 additions & 1 deletion froide/publicbody/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import django_filters

from froide.georegion.models import GeoRegion
from froide.helper.search.filters import BaseSearchFilterSet
from froide.helper.widgets import BootstrapSelect

Expand Down Expand Up @@ -44,9 +45,18 @@ class PublicBodyFilterSet(BaseSearchFilterSet):
method="filter_classification",
)

regions = django_filters.ModelMultipleChoiceFilter(
queryset=GeoRegion.objects.all(),
method="filter_regions",
)

regions_kind = django_filters.CharFilter(
method="filter_regions_kind",
)

class Meta:
model = PublicBody
fields = ["q", "jurisdiction", "category", "classification"]
fields = ["q", "jurisdiction", "category", "classification", "regions"]

def filter_jurisdiction(self, qs, name, value):
return self.apply_filter(qs, name, jurisdiction=value.id)
Expand All @@ -56,3 +66,11 @@ def filter_category(self, qs, name, value):

def filter_classification(self, qs, name, value):
return self.apply_filter(qs, name, classification=value.id)

def filter_regions(self, qs, name, value):
if value:
return self.apply_filter(qs, name, regions=[v.id for v in value])
return qs

def filter_regions_kind(self, qs, name, value):
return self.apply_filter(qs, name, regions_kind=value)
196 changes: 195 additions & 1 deletion froide/publicbody/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from froide.account.factories import UserFactory
from froide.foirequest.tests.factories import make_world, rebuild_index
from froide.georegion.factories import GeoRegionFactory
from froide.georegion.models import GeoRegion
from froide.helper.csv_utils import export_csv_bytes

Expand All @@ -22,7 +23,12 @@
PublicBodyChangeProposalFactory,
PublicBodyFactory,
)
from .models import FoiLaw, Jurisdiction, PublicBody, PublicBodyChangeProposal
from .models import (
FoiLaw,
Jurisdiction,
PublicBody,
PublicBodyChangeProposal,
)


class PublicBodyTest(TestCase):
Expand Down Expand Up @@ -286,6 +292,194 @@ def test_autocomplete(self):
self.assertEqual(obj["objects"], [])


@pytest.fixture
def publicbody_data(db):
# Jurisdictions
berlin = JurisdictionFactory(name="Berlin", slug="berlin")
hamburg = JurisdictionFactory(name="Hamburg", slug="hamburg")

# Classifications
ministry = ClassificationFactory(name="Ministry", slug="ministry")
agency = ClassificationFactory(name="Agency", slug="agency")

# Categories
environment = CategoryFactory(name="Environment", slug="environment")
traffic = CategoryFactory(name="Traffic", slug="traffic")

# Regions
mitte = GeoRegionFactory(name="Mitte", kind="district")
altona = GeoRegionFactory(name="Altona", kind="district")

# PublicBodies
pb1 = PublicBodyFactory(
name="Umweltministerium Berlin",
jurisdiction=berlin,
classification=ministry,
description="Zuständig für Umweltschutz",
)
pb1.categories.add(environment)
pb1.regions.add(mitte)

pb2 = PublicBodyFactory(
name="Verkehrsministerium Berlin",
jurisdiction=berlin,
classification=ministry,
description="Zuständig für Verkehr",
)
pb2.categories.add(traffic)

pb3 = PublicBodyFactory(
name="Umweltbehörde Hamburg",
jurisdiction=hamburg,
classification=agency,
description="Hamburger Umweltbehörde",
)
pb3.categories.add(environment)
pb3.regions.add(altona)

pb4 = PublicBodyFactory(
name="Bezirksamt Mitte",
jurisdiction=berlin,
classification=agency,
)
pb4.regions.add(mitte)

rebuild_index()

yield {
"jurisdictions": {"berlin": berlin, "hamburg": hamburg},
"classifications": {"ministry": ministry, "agency": agency},
"categories": {"environment": environment, "traffic": traffic},
"regions": {"mitte": mitte, "altona": altona},
"publicbodies": {"pb1": pb1, "pb2": pb2, "pb3": pb3, "pb4": pb4},
}


@pytest.mark.django_db
@pytest.mark.parametrize(
"query_params,expected_count,expected_names",
[
# Text search
({"q": "Umwelt"}, 2, ["Umweltministerium Berlin", "Umweltbehörde Hamburg"]),
# Jurisdiction filter
(
{"jurisdiction": "berlin"},
3,
[
"Umweltministerium Berlin",
"Verkehrsministerium Berlin",
"Bezirksamt Mitte",
],
),
# Classification filter
(
{"classification": "ministry"},
2,
["Umweltministerium Berlin", "Verkehrsministerium Berlin"],
),
# Category filter
(
{"categories": "environment"},
2,
["Umweltministerium Berlin", "Umweltbehörde Hamburg"],
),
# GeoRegion filter
({"regions": "mitte"}, 2, ["Umweltministerium Berlin", "Bezirksamt Mitte"]),
# GeoRegion kind filter
(
{"regions_kind": "district"},
3,
["Umweltministerium Berlin", "Bezirksamt Mitte", "Umweltbehörde Hamburg"],
),
# Combination text + jurisdiction
({"q": "Umwelt", "jurisdiction": "berlin"}, 1, ["Umweltministerium Berlin"]),
# Combination jurisdiction + classification
(
{"jurisdiction": "berlin", "classification": "ministry"},
2,
["Umweltministerium Berlin", "Verkehrsministerium Berlin"],
),
# Combination category + classification
(
{"categories": "environment", "classification": "ministry"},
1,
["Umweltministerium Berlin"],
),
# No results
({"q": "NichtVorhanden"}, 0, []),
# Empty search (all results)
(
{},
4,
[
"Umweltministerium Berlin",
"Verkehrsministerium Berlin",
"Umweltbehörde Hamburg",
"Bezirksamt Mitte",
],
),
],
ids=[
"text_search",
"jurisdiction_filter",
"classification_filter",
"category_filter",
"georegion_filter",
"georegion_kind_filter",
"text_and_jurisdiction",
"jurisdiction_and_classification",
"category_and_classification",
"no_results",
"empty_search",
],
)
def test_publicbody_search_filters(
client, publicbody_data, query_params, expected_count, expected_names
):
"""Test different combinations of search and filters for the publicbody search API."""

test_params = query_params.copy()

# Replace slugs with actual IDs.
if "jurisdiction" in test_params:
slug = test_params["jurisdiction"]
test_params["jurisdiction"] = publicbody_data["jurisdictions"][slug].pk

if "classification" in test_params:
slug = test_params["classification"]
test_params["classification"] = publicbody_data["classifications"][slug].pk

if "categories" in test_params:
slug = test_params["categories"]
test_params["categories"] = publicbody_data["categories"][slug].pk

if "regions" in test_params:
slug = test_params["regions"]
test_params["regions"] = publicbody_data["regions"][slug].pk

url = reverse("api:publicbody-search")
response = client.get(url, test_params)

assert response.status_code == 200
data = response.json()

# Check counts.
assert len(data["objects"]) == expected_count
assert data["meta"]["total_count"] == expected_count

# Check names.
result_names = {pb["name"] for pb in data["objects"]}
assert result_names == set(expected_names)

# Check facets.
assert "facets" in data
facets = data["facets"]["fields"]
assert "jurisdiction" in facets
assert "classification" in facets
assert "categories" in facets
assert "regions" in facets


@pytest.mark.django_db
def test_accept_change_proposal():
pb = PublicBodyFactory.create()
Expand Down
Loading