Skip to content

Commit 82ad5b3

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 e074608 commit 82ad5b3

File tree

5 files changed

+217
-4
lines changed

5 files changed

+217
-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

+7
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,12 @@ <h1 class="mb-4">Sections</h1>
4040
{% endfor %}
4141
</tbody>
4242
</table>
43+
44+
{% if marking_session.label == "Scorecards 2025" %}
45+
<a href="{% session_url 'authority_ror_download' authority_name %}">Download your Right of Reply response</a>
46+
<p>
47+
Clicking here enables you to download a .csv copy of your Right of Reply responses you have provided for the 2025 Council Climate Action Scorecards. Please keep these responses private, for your own council internal use only.
48+
</p>
49+
{% endif %}
4350
{% endif %}
4451
{% 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
@@ -205,3 +208,120 @@ def get_context_data(self, **kwargs):
205208
context = super().get_context_data(**kwargs)
206209
context["ror_user"] = True
207210
return context
211+
212+
213+
class AuthorityRORCSVView(ListView):
214+
context_object_name = "responses"
215+
216+
def get_queryset(self):
217+
user = self.request.user
218+
219+
rt = ResponseType.objects.get(type="Right of Reply")
220+
if user.is_superuser:
221+
authority_name = self.kwargs["name"]
222+
authority = PublicAuthority.objects.get(name=authority_name)
223+
else:
224+
authority = self.request.user.marker.authority
225+
226+
self.authority = authority
227+
228+
if authority is not None:
229+
return (
230+
Response.objects.filter(
231+
question__section__marking_session=self.request.current_session,
232+
response_type=rt,
233+
authority=authority,
234+
)
235+
.select_related("question", "question__section")
236+
.order_by(
237+
"question__section__title",
238+
"question__number",
239+
"question__number_part",
240+
)
241+
)
242+
243+
return None
244+
245+
def get_first_mark_responses(self):
246+
rt = ResponseType.objects.get(type="First Mark")
247+
responses = (
248+
Response.objects.filter(
249+
question__section__marking_session=self.request.current_session,
250+
response_type=rt,
251+
authority=self.authority,
252+
)
253+
.select_related("question", "question__section")
254+
.order_by(
255+
"question__section__title",
256+
"question__number",
257+
"question__number_part",
258+
)
259+
)
260+
261+
by_section = defaultdict(dict)
262+
263+
for r in responses:
264+
by_section[r.question.section.title][
265+
r.question.number_and_part
266+
] = r.option.description
267+
268+
return by_section
269+
270+
def get_context_data(self, **kwargs):
271+
context = super().get_context_data(**kwargs)
272+
rows = []
273+
rows.append(
274+
[
275+
"section",
276+
"question_no",
277+
"question",
278+
"first_mark_response",
279+
"agree_with_mark",
280+
"council_response",
281+
"council_evidence",
282+
"council_page_number",
283+
"council_notes",
284+
]
285+
)
286+
287+
first_mark_responses = self.get_first_mark_responses()
288+
289+
for response in context["responses"]:
290+
first_mark_response = ""
291+
if first_mark_responses.get(
292+
response.question.section.title
293+
) and first_mark_responses[response.question.section.title].get(
294+
response.question.number_and_part
295+
):
296+
first_mark_response = first_mark_responses[
297+
response.question.section.title
298+
][response.question.number_and_part]
299+
rows.append(
300+
[
301+
response.question.section.title,
302+
response.question.number_and_part,
303+
response.question.description,
304+
first_mark_response,
305+
"Yes" if response.agree_with_response else "No",
306+
response.option,
307+
",".join(response.evidence_links),
308+
response.page_number,
309+
response.evidence,
310+
]
311+
)
312+
313+
context["authority"] = self.authority.name
314+
context["rows"] = rows
315+
316+
return context
317+
318+
def render_to_response(self, context, **response_kwargs):
319+
filename = f"{self.request.current_session.label}_{context['authority']}_Right_of_Reply.csv"
320+
response = HttpResponse(
321+
content_type="text/csv",
322+
headers={"Content-Disposition": 'attachment; filename="' + filename + '"'},
323+
)
324+
writer = csv.writer(response)
325+
for row in context["rows"]:
326+
writer.writerow(row)
327+
return response

0 commit comments

Comments
 (0)