diff --git a/caps/api/serializers.py b/caps/api/serializers.py index 5a8fbe8e..3c84b6db 100644 --- a/caps/api/serializers.py +++ b/caps/api/serializers.py @@ -1,6 +1,6 @@ from rest_framework import reverse, serializers -from caps.models import Council, Promise, SavedSearch +from caps.models import Council, PlanDocument, Promise, SavedSearch class CouncilSerializer(serializers.HyperlinkedModelSerializer): @@ -16,6 +16,9 @@ class CouncilSerializer(serializers.HyperlinkedModelSerializer): carbon_reduction_statements = serializers.HyperlinkedIdentityField( view_name="council-commitments", lookup_field="authority_code" ) + climate_documents = serializers.HyperlinkedIdentityField( + view_name="council-documents", lookup_field="authority_code" + ) class Meta: model = Council @@ -29,6 +32,7 @@ class Meta: "authority_code", "plan_count", "document_count", + "climate_documents", "plans_last_update", "carbon_reduction_commitment", "carbon_neutral_date", @@ -74,6 +78,41 @@ def get_council(self, obj): return result +class PlanDocumentSerializer(serializers.HyperlinkedModelSerializer): + council = serializers.SerializerMethodField() + document_type = serializers.SerializerMethodField() + cached_url = serializers.CharField(source="file") + + class Meta: + model = PlanDocument + fields = [ + "council", + "title", + "document_type", + "file_type", + "url", + "cached_url", + "updated_at", + ] + + def get_council(self, obj): + code = obj.council.authority_code + # do this is a string otherwise you get an array as the result + result = "{}".format( + reverse.reverse( + "council-detail", args=[code], request=self.context["request"] + ), + ) + return result + + def get_document_type(self, obj): + type_code = obj.get_document_type_display() + if type_code is None: + type_code = "unknown" + + return type_code + + class SearchTermSerializer(serializers.HyperlinkedModelSerializer): times_seen = serializers.IntegerField() diff --git a/caps/api/views.py b/caps/api/views.py index 107284c9..91491ba7 100644 --- a/caps/api/views.py +++ b/caps/api/views.py @@ -11,6 +11,7 @@ from caps.api.serializers import ( CouncilSerializer, + PlanDocumentSerializer, PromiseSerializer, SearchTermSerializer, ) @@ -195,6 +196,36 @@ def get_queryset(self): ) +class CouncilDocumentsViewSet(viewsets.ReadOnlyModelViewSet): + """ + Information about a council's climate documents. + + Lists all documents we have for a council. + + * council - link to the council + * title - title of the document + * document_type - the type of document, if known, e.g Action Plan + * file_type - what type of file, e.g PDF + * url - link to document on the council's website + * cached_url - link to our cached copy of the file + * updated_at - date the information was last update + """ + + queryset = PlanDocument.objects.order_by("updated_at").all() + serializer_class = PlanDocumentSerializer + pagination_class = None + + def get_queryset(self): + return ( + PlanDocument.objects.filter( + council__authority_code=self.kwargs["authority_code"] + ) + .select_related("council") + .order_by("updated_at") + .all() + ) + + class SearchTermViewSet(viewsets.ReadOnlyModelViewSet): """ List of search terms that returned results, ordered by most popular terms diff --git a/caps/tests/test_api.py b/caps/tests/test_api.py index f9d7f84c..26b291fa 100644 --- a/caps/tests/test_api.py +++ b/caps/tests/test_api.py @@ -34,6 +34,7 @@ def test_basic_council_api(self): "carbon_neutral_date": None, "carbon_reduction_commitment": False, "carbon_reduction_statements": "http://testserver/api/councils/BOS/commitments", + "climate_documents": "http://testserver/api/councils/BOS/documents", "declared_emergency": None, "gss_code": "E14000111", "country": "England", @@ -66,6 +67,7 @@ def test_council_api_plan_count(self): "carbon_neutral_date": None, "carbon_reduction_commitment": False, "carbon_reduction_statements": "http://testserver/api/councils/BOS/commitments", + "climate_documents": "http://testserver/api/councils/BOS/documents", "declared_emergency": None, "document_count": 1, "gss_code": "E14000111", @@ -118,6 +120,7 @@ def test_multiple_councils(self): "carbon_neutral_date": None, "carbon_reduction_commitment": False, "carbon_reduction_statements": "http://testserver/api/councils/BOS/commitments", + "climate_documents": "http://testserver/api/councils/BOS/documents", "declared_emergency": None, }, { @@ -134,6 +137,7 @@ def test_multiple_councils(self): "carbon_neutral_date": None, "carbon_reduction_commitment": False, "carbon_reduction_statements": "http://testserver/api/councils/EBS/commitments", + "climate_documents": "http://testserver/api/councils/EBS/documents", "declared_emergency": None, }, { @@ -150,6 +154,7 @@ def test_multiple_councils(self): "carbon_neutral_date": None, "carbon_reduction_commitment": False, "carbon_reduction_statements": "http://testserver/api/councils/WBS/commitments", + "climate_documents": "http://testserver/api/councils/WBS/documents", "declared_emergency": None, }, ], @@ -204,6 +209,7 @@ def test_filtering(self): "carbon_neutral_date": None, "carbon_reduction_commitment": False, "carbon_reduction_statements": "http://testserver/api/councils/EBS/commitments", + "climate_documents": "http://testserver/api/councils/EBS/documents", "declared_emergency": None, } ], @@ -230,6 +236,7 @@ def test_filtering(self): "carbon_neutral_date": None, "carbon_reduction_commitment": False, "carbon_reduction_statements": "http://testserver/api/councils/EBS/commitments", + "climate_documents": "http://testserver/api/councils/EBS/documents", "declared_emergency": None, }, { @@ -246,6 +253,7 @@ def test_filtering(self): "carbon_neutral_date": None, "carbon_reduction_commitment": False, "carbon_reduction_statements": "http://testserver/api/councils/WBS/commitments", + "climate_documents": "http://testserver/api/councils/WBS/documents", "declared_emergency": None, }, ], @@ -270,12 +278,54 @@ def test_filtering(self): "carbon_neutral_date": None, "carbon_reduction_commitment": False, "carbon_reduction_statements": "http://testserver/api/councils/NBS/commitments", + "climate_documents": "http://testserver/api/councils/NBS/documents", "declared_emergency": None, } ], ) +class DocumentApiTest(APITestCase): + council = None + + def setUp(self): + self.council = Council.objects.create( + name="Borsetshire", + slug="borsetshire", + country=Council.ENGLAND, + authority_code="BOS", + gss_code="E14000111", + ) + + def test_council_with_plan(self): + plan = PlanDocument.objects.create( + council=self.council, + url="https://borsetshire.gov.uk/climate_plan.pdf", + document_type=PlanDocument.ACTION_PLAN, + file_type="pdf", + scope=PlanDocument.COUNCIL_ONLY, + title="An Action Plan", + updated_at="2024-01-01", + ) + + response = self.client.get("/api/councils/BOS/documents") + + self.assertEquals( + json.loads(response.content), + [ + { + "council": "http://testserver/api/councils/BOS/", + "title": "An Action Plan", + "document_type": "Action plan", + "file_type": "pdf", + "url": "https://borsetshire.gov.uk/climate_plan.pdf", + "cached_url": "", + "updated_at": plan.updated_at.isoformat(), + } + ], + ) + + class PromisesAPITest(APITestCase): def setUp(self): self.council_bors = Council.objects.create( @@ -325,6 +375,7 @@ def test_council_with_commitment(self): "carbon_neutral_date": 2035, "carbon_reduction_commitment": True, "carbon_reduction_statements": "http://testserver/api/councils/BOS/commitments", + "climate_documents": "http://testserver/api/councils/BOS/documents", "declared_emergency": None, }, { @@ -341,6 +392,7 @@ def test_council_with_commitment(self): "carbon_neutral_date": None, "carbon_reduction_commitment": False, "carbon_reduction_statements": "http://testserver/api/councils/WBS/commitments", + "climate_documents": "http://testserver/api/councils/WBS/documents", "declared_emergency": None, }, ], diff --git a/caps/urls.py b/caps/urls.py index bef35cb2..191fa311 100755 --- a/caps/urls.py +++ b/caps/urls.py @@ -1,13 +1,13 @@ -from django.urls import include, path -from django.contrib import admin +import haystack.generic_views from django.conf import settings +from django.contrib import admin +from django.urls import include, path from django.views.generic.base import RedirectView -import haystack.generic_views -from caps.forms import HighlightedSearchForm -import caps.views as views import caps.api.views as api_views +import caps.views as views from caps.api import routers +from caps.forms import HighlightedSearchForm router = routers.Router() router.register(r"councils", api_views.CouncilViewSet, basename="council") @@ -55,6 +55,11 @@ api_views.CouncilCommitmentsViewSet.as_view({"get": "list"}), name="council-commitments", ), + path( + "api/councils//documents", + api_views.CouncilDocumentsViewSet.as_view({"get": "list"}), + name="council-documents", + ), path("content//", views.MarkdownView.as_view(), name="content"), # used for testing page in debug mode path("404/", views.NotFoundPageView.as_view(), name="404"),