From 60372fa938785f7a512d6ef42a984ee8c79cbcec Mon Sep 17 00:00:00 2001 From: Struan Donald Date: Wed, 11 Sep 2024 17:41:20 +0200 Subject: [PATCH] add a download link to council right of reply section page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This allows a council to download their Right of Reply response as a CSV so they can refer to it later. Fixes ³77 --- ceuk-marking/urls.py | 5 + crowdsourcer/fixtures/ror_responses.json | 48 ++++++- .../crowdsourcer/authority_section_list.html | 13 ++ .../tests/test_right_of_reply_views.py | 41 ++++++ crowdsourcer/views/rightofreply.py | 120 ++++++++++++++++++ 5 files changed, 223 insertions(+), 4 deletions(-) diff --git a/ceuk-marking/urls.py b/ceuk-marking/urls.py index b232e3ee..d3e52930 100644 --- a/ceuk-marking/urls.py +++ b/ceuk-marking/urls.py @@ -88,6 +88,11 @@ rightofreply.AuthorityRORSectionQuestions.as_view(), name="authority_ror", ), + path( + "authorities//ror/download/", + rightofreply.AuthorityRORCSVView.as_view(), + name="authority_ror_download", + ), path( "authority_ror_authorities/", rightofreply.AuthorityRORList.as_view(), diff --git a/crowdsourcer/fixtures/ror_responses.json b/crowdsourcer/fixtures/ror_responses.json index 89f8f25e..191a978c 100644 --- a/crowdsourcer/fixtures/ror_responses.json +++ b/crowdsourcer/fixtures/ror_responses.json @@ -9,7 +9,7 @@ "option": 181, "response_type": 2, "public_notes": "", - "page_number": "0", + "page_number": "", "evidence": "", "private_notes": "", "agree_with_response": true, @@ -29,9 +29,9 @@ "user": 3, "option": 191, "response_type": 2, - "public_notes": "", - "page_number": "0", - "evidence": "", + "public_notes": "http://example.org/", + "page_number": "20", + "evidence": "We do not agree for reasons", "private_notes": "a council objection", "agree_with_response": false, "revision_type": null, @@ -40,5 +40,45 @@ "last_update": "2023-03-15T17:22:10+0000", "multi_option": [] } +}, +{ + "model": "crowdsourcer.response", + "pk": 101, + "fields": { + "authority": 2, + "question": 272, + "user": 2, + "option": 181, + "response_type": 1, + "public_notes": "a public note", + "page_number": "0", + "evidence": "", + "private_notes": "a private note", + "revision_type": null, + "revision_notes": null, + "created": "2023-03-15T17:22:10+0000", + "last_update": "2023-03-15T17:22:10+0000", + "multi_option": [] + } +}, +{ + "model": "crowdsourcer.response", + "pk": 102, + "fields": { + "authority": 2, + "question": 273, + "user": 2, + "option": 6, + "response_type": 1, + "public_notes": "a public note", + "page_number": "0", + "evidence": "", + "private_notes": "a private note", + "revision_type": null, + "revision_notes": null, + "created": "2023-03-15T17:22:10+0000", + "last_update": "2023-03-15T17:22:10+0000", + "multi_option": [] + } } ] diff --git a/crowdsourcer/templates/crowdsourcer/authority_section_list.html b/crowdsourcer/templates/crowdsourcer/authority_section_list.html index edbe88b6..ae83f94d 100644 --- a/crowdsourcer/templates/crowdsourcer/authority_section_list.html +++ b/crowdsourcer/templates/crowdsourcer/authority_section_list.html @@ -40,5 +40,18 @@

Sections

{% endfor %} + + {% if marking_session.label == "Scorecards 2025" %} +
+

Download your Right of Reply response

+

You can download a CSV spreadsheet of the Right of Reply responses you have provided for the 2025 Council Climate Action Scorecards.

+

Please keep these responses private, this is for your own council’s internal use only.

+ + {% include 'crowdsourcer/icons/download.svg' with classes="me-2" %} + Download responses CSV + +
+ {% endif %} + {% endif %} {% endblock %} diff --git a/crowdsourcer/tests/test_right_of_reply_views.py b/crowdsourcer/tests/test_right_of_reply_views.py index c130066c..13ee81b3 100644 --- a/crowdsourcer/tests/test_right_of_reply_views.py +++ b/crowdsourcer/tests/test_right_of_reply_views.py @@ -1,7 +1,11 @@ +import io + from django.contrib.auth.models import User from django.test import TestCase from django.urls import reverse +import pandas as pd + from crowdsourcer.models import ( Assigned, MarkingSession, @@ -554,3 +558,40 @@ def test_view_other_session(self): progress = response.context["progress"] self.assertEqual(len(progress.keys()), 2) + + +class TestCSVDownloadView(BaseTestCase): + def test_download(self): + url = reverse("authority_ror_download", args=("Aberdeenshire Council",)) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + content = response.content.decode("utf-8") + # the dtype bit stops pandas doing annoying conversions and ending up + # with page numers as floats etc + df = pd.read_csv(io.StringIO(content), dtype="object") + # avoid nan results + df = df.fillna("") + + self.assertEqual(df.shape[0], 2) + b_and_h_q4 = df.iloc[0] + b_and_h_q5 = df.iloc[1] + + self.assertEqual(b_and_h_q4.question_no, "4") + self.assertEqual( + b_and_h_q4.first_mark_response, + "The council has completed an exercise to measure how much, approximately, it will cost them to retrofit all homes (to EPC C or higher, or equivalent) and there is a target date of 2030.", + ) + self.assertEqual(b_and_h_q4.agree_with_mark, "Yes") + self.assertEqual(b_and_h_q4.council_page_number, "") + self.assertEqual(b_and_h_q4.council_evidence, "") + + self.assertEqual(b_and_h_q5.question_no, "5") + self.assertEqual( + b_and_h_q5.first_mark_response, + "The council convenes or is a member of a local retrofit partnership", + ) + self.assertEqual(b_and_h_q5.council_evidence, "http://example.org/") + self.assertEqual(b_and_h_q5.agree_with_mark, "No") + self.assertEqual(b_and_h_q5.council_page_number, "20") + self.assertEqual(b_and_h_q5.council_notes, "We do not agree for reasons") diff --git a/crowdsourcer/views/rightofreply.py b/crowdsourcer/views/rightofreply.py index a1d29203..1e6bbaf9 100644 --- a/crowdsourcer/views/rightofreply.py +++ b/crowdsourcer/views/rightofreply.py @@ -1,6 +1,9 @@ +import csv import logging +from collections import defaultdict from django.core.exceptions import PermissionDenied +from django.http import HttpResponse from django.shortcuts import redirect from django.urls import reverse from django.views.generic import ListView @@ -205,3 +208,120 @@ def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["ror_user"] = True return context + + +class AuthorityRORCSVView(ListView): + context_object_name = "responses" + + def get_queryset(self): + user = self.request.user + + rt = ResponseType.objects.get(type="Right of Reply") + if user.is_superuser: + authority_name = self.kwargs["name"] + authority = PublicAuthority.objects.get(name=authority_name) + else: + authority = self.request.user.marker.authority + + self.authority = authority + + if authority is not None: + return ( + Response.objects.filter( + question__section__marking_session=self.request.current_session, + response_type=rt, + authority=authority, + ) + .select_related("question", "question__section") + .order_by( + "question__section__title", + "question__number", + "question__number_part", + ) + ) + + return None + + def get_first_mark_responses(self): + rt = ResponseType.objects.get(type="First Mark") + responses = ( + Response.objects.filter( + question__section__marking_session=self.request.current_session, + response_type=rt, + authority=self.authority, + ) + .select_related("question", "question__section") + .order_by( + "question__section__title", + "question__number", + "question__number_part", + ) + ) + + by_section = defaultdict(dict) + + for r in responses: + by_section[r.question.section.title][ + r.question.number_and_part + ] = r.option.description + + return by_section + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + rows = [] + rows.append( + [ + "section", + "question_no", + "question", + "first_mark_response", + "agree_with_mark", + "council_response", + "council_evidence", + "council_page_number", + "council_notes", + ] + ) + + first_mark_responses = self.get_first_mark_responses() + + for response in context["responses"]: + first_mark_response = "" + if first_mark_responses.get( + response.question.section.title + ) and first_mark_responses[response.question.section.title].get( + response.question.number_and_part + ): + first_mark_response = first_mark_responses[ + response.question.section.title + ][response.question.number_and_part] + rows.append( + [ + response.question.section.title, + response.question.number_and_part, + response.question.description, + first_mark_response, + "Yes" if response.agree_with_response else "No", + response.option, + ",".join(response.evidence_links), + response.page_number, + response.evidence, + ] + ) + + context["authority"] = self.authority.name + context["rows"] = rows + + return context + + def render_to_response(self, context, **response_kwargs): + filename = f"{self.request.current_session.label}_{context['authority']}_Right_of_Reply.csv" + response = HttpResponse( + content_type="text/csv", + headers={"Content-Disposition": 'attachment; filename="' + filename + '"'}, + ) + writer = csv.writer(response) + for row in context["rows"]: + writer.writerow(row) + return response