Skip to content

Commit 79987f7

Browse files
committed
add a download link to council right of reply section page
This allows a council to download their Right of Reply response as a CSV so they can refer to it later. Fixes ³77
1 parent b6ce363 commit 79987f7

File tree

5 files changed

+222
-4
lines changed

5 files changed

+222
-4
lines changed

ceuk-marking/urls.py

+5
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,11 @@
8888
rightofreply.AuthorityRORSectionQuestions.as_view(),
8989
name="authority_ror",
9090
),
91+
path(
92+
"authorities/<name>/ror/download/",
93+
rightofreply.AuthorityRORCSVView.as_view(),
94+
name="authority_ror_download",
95+
),
9196
path(
9297
"authority_ror_authorities/",
9398
rightofreply.AuthorityRORList.as_view(),

crowdsourcer/fixtures/ror_responses.json

+44-4
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"option": 181,
1010
"response_type": 2,
1111
"public_notes": "",
12-
"page_number": "0",
12+
"page_number": "",
1313
"evidence": "",
1414
"private_notes": "",
1515
"agree_with_response": true,
@@ -29,9 +29,9 @@
2929
"user": 3,
3030
"option": 191,
3131
"response_type": 2,
32-
"public_notes": "",
33-
"page_number": "0",
34-
"evidence": "",
32+
"public_notes": "http://example.org/",
33+
"page_number": "20",
34+
"evidence": "We do not agree for reasons",
3535
"private_notes": "a council objection",
3636
"agree_with_response": false,
3737
"revision_type": null,
@@ -40,5 +40,45 @@
4040
"last_update": "2023-03-15T17:22:10+0000",
4141
"multi_option": []
4242
}
43+
},
44+
{
45+
"model": "crowdsourcer.response",
46+
"pk": 101,
47+
"fields": {
48+
"authority": 2,
49+
"question": 272,
50+
"user": 2,
51+
"option": 181,
52+
"response_type": 1,
53+
"public_notes": "a public note",
54+
"page_number": "0",
55+
"evidence": "",
56+
"private_notes": "a private note",
57+
"revision_type": null,
58+
"revision_notes": null,
59+
"created": "2023-03-15T17:22:10+0000",
60+
"last_update": "2023-03-15T17:22:10+0000",
61+
"multi_option": []
62+
}
63+
},
64+
{
65+
"model": "crowdsourcer.response",
66+
"pk": 102,
67+
"fields": {
68+
"authority": 2,
69+
"question": 273,
70+
"user": 2,
71+
"option": 6,
72+
"response_type": 1,
73+
"public_notes": "a public note",
74+
"page_number": "0",
75+
"evidence": "",
76+
"private_notes": "a private note",
77+
"revision_type": null,
78+
"revision_notes": null,
79+
"created": "2023-03-15T17:22:10+0000",
80+
"last_update": "2023-03-15T17:22:10+0000",
81+
"multi_option": []
82+
}
4383
}
4484
]

crowdsourcer/templates/crowdsourcer/authority_section_list.html

+12
Original file line numberDiff line numberDiff line change
@@ -49,5 +49,17 @@ <h2 class="mb-3 h4">Help us by providing optional feedback</h2>
4949
</div>
5050
{% endif %}
5151

52+
{% if marking_session.label == "Scorecards 2025" %}
53+
<div class="my-5" style="max-width: 40rem">
54+
<h2 class="mb-3 h4">Download your Right of Reply response</h2>
55+
<p>You can download a CSV spreadsheet of the Right of Reply responses you have provided for the 2025 Council Climate Action Scorecards.</p>
56+
<p>Please keep these responses private, this is for your own council’s internal use only.</p>
57+
<a href="{% session_url 'authority_ror_download' authority_name %}" class="btn btn-primary d-inline-flex align-items-center">
58+
{% include 'crowdsourcer/icons/download.svg' with classes="me-2" %}
59+
Download responses CSV
60+
</a>
61+
</div>
62+
{% endif %}
63+
5264
{% endif %}
5365
{% endblock %}

crowdsourcer/tests/test_right_of_reply_views.py

+41
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
import io
2+
13
from django.contrib.auth.models import User
24
from django.test import TestCase
35
from django.urls import reverse
46

7+
import pandas as pd
8+
59
from crowdsourcer.models import (
610
Assigned,
711
MarkingSession,
@@ -554,3 +558,40 @@ def test_view_other_session(self):
554558
progress = response.context["progress"]
555559

556560
self.assertEqual(len(progress.keys()), 2)
561+
562+
563+
class TestCSVDownloadView(BaseTestCase):
564+
def test_download(self):
565+
url = reverse("authority_ror_download", args=("Aberdeenshire Council",))
566+
response = self.client.get(url)
567+
self.assertEqual(response.status_code, 200)
568+
569+
content = response.content.decode("utf-8")
570+
# the dtype bit stops pandas doing annoying conversions and ending up
571+
# with page numers as floats etc
572+
df = pd.read_csv(io.StringIO(content), dtype="object")
573+
# avoid nan results
574+
df = df.fillna("")
575+
576+
self.assertEqual(df.shape[0], 2)
577+
b_and_h_q4 = df.iloc[0]
578+
b_and_h_q5 = df.iloc[1]
579+
580+
self.assertEqual(b_and_h_q4.question_no, "4")
581+
self.assertEqual(
582+
b_and_h_q4.first_mark_response,
583+
"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.",
584+
)
585+
self.assertEqual(b_and_h_q4.agree_with_mark, "Yes")
586+
self.assertEqual(b_and_h_q4.council_page_number, "")
587+
self.assertEqual(b_and_h_q4.council_evidence, "")
588+
589+
self.assertEqual(b_and_h_q5.question_no, "5")
590+
self.assertEqual(
591+
b_and_h_q5.first_mark_response,
592+
"The council convenes or is a member of a local retrofit partnership",
593+
)
594+
self.assertEqual(b_and_h_q5.council_evidence, "http://example.org/")
595+
self.assertEqual(b_and_h_q5.agree_with_mark, "No")
596+
self.assertEqual(b_and_h_q5.council_page_number, "20")
597+
self.assertEqual(b_and_h_q5.council_notes, "We do not agree for reasons")

crowdsourcer/views/rightofreply.py

+120
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import csv
12
import logging
3+
from collections import defaultdict
24

35
from django.core.exceptions import PermissionDenied
6+
from django.http import HttpResponse
47
from django.shortcuts import redirect
58
from django.urls import reverse
69
from django.views.generic import ListView
@@ -209,3 +212,120 @@ def get_context_data(self, **kwargs):
209212
context = super().get_context_data(**kwargs)
210213
context["ror_user"] = True
211214
return context
215+
216+
217+
class AuthorityRORCSVView(ListView):
218+
context_object_name = "responses"
219+
220+
def get_queryset(self):
221+
user = self.request.user
222+
223+
rt = ResponseType.objects.get(type="Right of Reply")
224+
if user.is_superuser:
225+
authority_name = self.kwargs["name"]
226+
authority = PublicAuthority.objects.get(name=authority_name)
227+
else:
228+
authority = self.request.user.marker.authority
229+
230+
self.authority = authority
231+
232+
if authority is not None:
233+
return (
234+
Response.objects.filter(
235+
question__section__marking_session=self.request.current_session,
236+
response_type=rt,
237+
authority=authority,
238+
)
239+
.select_related("question", "question__section")
240+
.order_by(
241+
"question__section__title",
242+
"question__number",
243+
"question__number_part",
244+
)
245+
)
246+
247+
return None
248+
249+
def get_first_mark_responses(self):
250+
rt = ResponseType.objects.get(type="First Mark")
251+
responses = (
252+
Response.objects.filter(
253+
question__section__marking_session=self.request.current_session,
254+
response_type=rt,
255+
authority=self.authority,
256+
)
257+
.select_related("question", "question__section")
258+
.order_by(
259+
"question__section__title",
260+
"question__number",
261+
"question__number_part",
262+
)
263+
)
264+
265+
by_section = defaultdict(dict)
266+
267+
for r in responses:
268+
by_section[r.question.section.title][
269+
r.question.number_and_part
270+
] = r.option.description
271+
272+
return by_section
273+
274+
def get_context_data(self, **kwargs):
275+
context = super().get_context_data(**kwargs)
276+
rows = []
277+
rows.append(
278+
[
279+
"section",
280+
"question_no",
281+
"question",
282+
"first_mark_response",
283+
"agree_with_mark",
284+
"council_response",
285+
"council_evidence",
286+
"council_page_number",
287+
"council_notes",
288+
]
289+
)
290+
291+
first_mark_responses = self.get_first_mark_responses()
292+
293+
for response in context["responses"]:
294+
first_mark_response = ""
295+
if first_mark_responses.get(
296+
response.question.section.title
297+
) and first_mark_responses[response.question.section.title].get(
298+
response.question.number_and_part
299+
):
300+
first_mark_response = first_mark_responses[
301+
response.question.section.title
302+
][response.question.number_and_part]
303+
rows.append(
304+
[
305+
response.question.section.title,
306+
response.question.number_and_part,
307+
response.question.description,
308+
first_mark_response,
309+
"Yes" if response.agree_with_response else "No",
310+
response.option,
311+
",".join(response.evidence_links),
312+
response.page_number,
313+
response.evidence,
314+
]
315+
)
316+
317+
context["authority"] = self.authority.name
318+
context["rows"] = rows
319+
320+
return context
321+
322+
def render_to_response(self, context, **response_kwargs):
323+
filename = f"{self.request.current_session.label}_{context['authority']}_Right_of_Reply.csv"
324+
response = HttpResponse(
325+
content_type="text/csv",
326+
headers={"Content-Disposition": 'attachment; filename="' + filename + '"'},
327+
)
328+
writer = csv.writer(response)
329+
for row in context["rows"]:
330+
writer.writerow(row)
331+
return response

0 commit comments

Comments
 (0)