Skip to content

Commit 2f60c0b

Browse files
committed
allow first markers to see and copy across last years answers
If there are previous questions available then display the answers and allow the First Markers to copy these across. This might not work for the options if they have changed but that's ok because if they've changed then they should be checking them and not copying them. Fixes #133
1 parent 7aa703e commit 2f60c0b

File tree

8 files changed

+297
-20
lines changed

8 files changed

+297
-20
lines changed

crowdsourcer/fixtures/questions.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -599,7 +599,8 @@
599599
2,
600600
3,
601601
4
602-
]
602+
],
603+
"previous_question": 281
603604
}
604605
}
605606
]

crowdsourcer/forms.py

+1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ def __init__(self, *args, **kwargs):
5454

5555
self.authority_obj = self.initial.get("authority", None)
5656
self.question_obj = self.initial.get("question", None)
57+
self.previous_response = self.initial.get("previous_response", None)
5758

5859
self.fields["option"].queryset = Option.objects.filter(
5960
question=self.question_obj

crowdsourcer/templates/crowdsourcer/authority_questions.html

+1-18
Original file line numberDiff line numberDiff line change
@@ -69,24 +69,7 @@ <h3 class="mb-4 mb-md-5 text-success">
6969
</details>
7070
</div>
7171
<div class="col-md-7 order-md-1">
72-
<label class="form-label" for="{{ q_form.option.if_for_label }}">Answer</label>
73-
{% if q_form.question_obj.question_type == "multiple_choice" %}
74-
{% bootstrap_field q_form.multi_option show_label="skip" %}
75-
{% else %}
76-
{% bootstrap_field q_form.option show_label="skip" %}
77-
{% endif %}
78-
79-
{% bootstrap_field q_form.page_number %}
80-
81-
{% bootstrap_field q_form.evidence %}
82-
83-
{% bootstrap_field q_form.public_notes %}
84-
85-
{% bootstrap_field q_form.private_notes %}
86-
87-
{{ q_form.authority }}
88-
{{ q_form.question }}
89-
{{ q_form.id }}
72+
{% include 'crowdsourcer/includes/first_mark_response_form_fields.html' %}
9073
</div>
9174
</div>
9275
</fieldset>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
{% extends 'crowdsourcer/base.html' %}
2+
3+
{% load django_bootstrap5 %}
4+
{% load neighbourhood_filters %}
5+
6+
{% block content %}
7+
<h1 class="mb-3 mb-md-4">
8+
{% if authority.website %}
9+
<a href="{{ authority.website }}">{{ authority_name }}</a>:
10+
{% else %}
11+
{{ authority_name }}:
12+
{% endif %}
13+
{{section_title}}
14+
</h1>
15+
16+
<div class="sticky-top py-3 bg-white border-bottom" style="margin-bottom: -1px;">
17+
<div class="dropdown">
18+
<button class="btn btn-outline-secondary dropdown-toggle" id="navbarDropdown" data-bs-toggle="dropdown" aria-expanded="false">
19+
Skip to question
20+
</button>
21+
<ul class="dropdown-menu" aria-labelledby="navbarDropdown">
22+
{% for q_form in form %}
23+
<li><a class="dropdown-item d-flex" style="max-width: 30em; white-space: normal;" href="#q{{q_form.question_obj.number_and_part}}">
24+
<span style="width: 2em; flex: 0 0 auto;">Q{{q_form.question_obj.number_and_part}}</span>
25+
<span class="text-muted fs-7 ms-3">{{ q_form.question_obj.description }}</span>
26+
</a></li>
27+
{% endfor %}
28+
</ul>
29+
</div>
30+
</div>
31+
32+
{% if message %}
33+
<h3 class="mb-4 mb-md-5 text-success">
34+
{{ message }}
35+
</h3>
36+
{% endif %}
37+
38+
<form method="POST">
39+
{% csrf_token %}
40+
{% if form.total_error_count > 0 %}
41+
<div class="mb-4">
42+
<div class="col-md-7 text-danger">
43+
There were some errors which are highlighted in red below.
44+
</div>
45+
</div>
46+
{% endif %}
47+
{{ form.management_form }}
48+
{% for q_form in form %}
49+
<fieldset class="py-4 py-md-5 border-top">
50+
<legend id="q{{ q_form.question_obj.number_and_part }}" class="mb-3">
51+
<h2 class="h4 mb-0 mw-40em">
52+
{{ q_form.question_obj.number_and_part }}. {{ q_form.question_obj.description }}
53+
{% if q_form.question_obj.how_marked == "foi" %}(FOI){% endif %}
54+
{% if q_form.question_obj.how_marked == "national_data" %}(National Data){% endif %}
55+
</h2>
56+
</legend>
57+
<div class="d-sm-flex mx-n3 text-muted">
58+
<details class="mt-3 mt-sm-0 mx-3 mw-30em">
59+
<summary class="fw-bold mb-2">Criteria</summary>
60+
{% autoescape off %}
61+
{{ q_form.question_obj.criteria|linebreaks }}
62+
{% endautoescape %}
63+
</details>
64+
<details class="mt-3 mt-sm-0 mx-3 mw-30em">
65+
<summary class="fw-bold mb-2">Clarifications</summary>
66+
{% autoescape off %}
67+
{{ q_form.question_obj.clarifications|linebreaks }}
68+
{% endautoescape %}
69+
</details>
70+
</div>
71+
<div class="row gx-lg-5">
72+
<div class="col-lg-6 mt-6">
73+
74+
<h3 class="h5 text-muted mb-3 mb-lg-4">Previous Response</h3>
75+
76+
<h4 class="form-label fs-6">Marker’s answer</h4>
77+
<div class="read-only-answer mb-3 mb-md-4">
78+
{% if q_form.previous_response.multi_option.values %}
79+
<p>
80+
{% for option in q_form.previous_response.multi_option.values %}
81+
{{ option.description }},
82+
{% empty %}
83+
(none)
84+
{% endfor %}
85+
</p>
86+
{% else %}
87+
{{ q_form.previous_response.option|default:"(none)"|linebreaks }}
88+
{% endif %}
89+
</div>
90+
91+
{% if q_form.question_obj.how_marked == 'foi' %}
92+
<h4 class="form-label fs-6">FOI request</h4>
93+
<div class="read-only-answer mb-3 mb-md-4">
94+
{{ q_form.previous_response.evidence|urlize_external }}
95+
</div>
96+
{% else %}
97+
<h4 class="form-label fs-6">Marker’s evidence of criteria met</h4>
98+
<div class="read-only-answer mb-3 mb-md-4">
99+
{{ q_form.previous_response.evidence|default:"(none)"|linebreaks }}
100+
</div>
101+
{% endif %}
102+
103+
{% if q_form.question_obj.how_marked != 'foi' %}
104+
<h4 class="form-label fs-6">Links to evidence</h4>
105+
<div class="read-only-answer mb-3 mb-md-4">
106+
{{ q_form.previous_response.public_notes|default:"(none)"|urlize_external|linebreaks }}
107+
</div>
108+
109+
<h4 class="form-label fs-6">Page number</h4>
110+
<div class="read-only-answer mb-3 mb-md-4">
111+
{{ q_form.previous_response.page_number|default:"(none)" }}<br>
112+
</div>
113+
{% endif %}
114+
115+
<h4 class="form-label fs-6">Marker’s additional notes</h4>
116+
<div class="read-only-answer mb-3 mb-md-4">
117+
{{ q_form.previous_response.private_notes|default:"(none)"|linebreaks }}
118+
</div>
119+
120+
</div>
121+
<div class="col-lg-6 mt-6 js-new-responses">
122+
123+
<h3 class="h5 text-muted mb-3 mb-lg-4">New response</h3>
124+
125+
<div class="mb-3">
126+
<button class="btn btn-sm btn-outline-primary d-flex align-items-center js-copy-response-from-previous" type="button">
127+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="me-2" viewBox="0 0 16 16">
128+
<path d="M9.5 2.672a.5.5 0 1 0 1 0V.843a.5.5 0 0 0-1 0v1.829Zm4.5.035A.5.5 0 0 0 13.293 2L12 3.293a.5.5 0 1 0 .707.707L14 2.707ZM7.293 4A.5.5 0 1 0 8 3.293L6.707 2A.5.5 0 0 0 6 2.707L7.293 4Zm-.621 2.5a.5.5 0 1 0 0-1H4.843a.5.5 0 1 0 0 1h1.829Zm8.485 0a.5.5 0 1 0 0-1h-1.829a.5.5 0 0 0 0 1h1.829ZM13.293 10A.5.5 0 1 0 14 9.293L12.707 8a.5.5 0 1 0-.707.707L13.293 10ZM9.5 11.157a.5.5 0 0 0 1 0V9.328a.5.5 0 0 0-1 0v1.829Zm1.854-5.097a.5.5 0 0 0 0-.706l-.708-.708a.5.5 0 0 0-.707 0L8.646 5.94a.5.5 0 0 0 0 .707l.708.708a.5.5 0 0 0 .707 0l1.293-1.293Zm-3 3a.5.5 0 0 0 0-.706l-.708-.708a.5.5 0 0 0-.707 0L.646 13.94a.5.5 0 0 0 0 .707l.708.708a.5.5 0 0 0 .707 0L8.354 9.06Z"/>
129+
</svg>
130+
Copy from Previous Response
131+
</button>
132+
</div>
133+
134+
<script type="application/json" class="js-previous-json">
135+
{
136+
{% if q_form.previous_response.multi_option.values %}
137+
"multi_option": [
138+
{% for option in q_form.previous_response.multi_option.values %}
139+
"{{ option.description }}"{% if not forloop.last %},{% endif %}
140+
{% endfor %}
141+
],
142+
{% else %}
143+
"option": "{{ q_form.previous_response.option.description }}",
144+
{% endif %}
145+
{% if q_form.question_obj.how_marked == "foi" %}
146+
"evidence": "",
147+
"public_notes": "{{ q_form.previous_response.evidence|default_if_none:''|escapejs }}",
148+
{% else %}
149+
"evidence": "{{ q_form.previous_response.evidence|default_if_none:''|escapejs }}",
150+
"public_notes": "{{ q_form.previous_response.public_notes|default_if_none:''|escapejs }}",
151+
{% endif %}
152+
"page_number": "{{ q_form.previous_response.page_number|default_if_none:''|escapejs }}",
153+
"private_notes": "{{ q_form.previous_response.private_notes|default_if_none:''|escapejs }}"
154+
}
155+
</script>
156+
157+
{% include 'crowdsourcer/includes/first_mark_response_form_fields.html' %}
158+
159+
</div>
160+
</div>
161+
</fieldset>
162+
{% endfor %}
163+
164+
<div class="sticky-bottom py-3 bg-white border-top" style="margin-top: -1px;">
165+
<input type="submit" class="btn btn-primary" value="Save answers">
166+
</div>
167+
</form>
168+
169+
<script>
170+
document.querySelectorAll('.js-copy-response-from-previous').forEach(function(el, i){
171+
el.addEventListener('click', function(e){
172+
e.preventDefault();
173+
var new_responses = el.closest('.js-new-responses');
174+
var previous_responses = JSON.parse(new_responses.querySelector('.js-previous-json').textContent);
175+
176+
["page_number", "evidence", "public_notes", "private_notes"].forEach(function(slug, i){
177+
new_responses.querySelector('[name$="' + slug + '"]').value = previous_responses[slug];
178+
});
179+
180+
/*
181+
Have to do this by comparing the labels as there is a new set of options so the ids
182+
will not match. This will fail if the text is not the same but then it should.
183+
*/
184+
if(previous_responses.multi_option){
185+
new_responses.querySelectorAll('[name$="multi_option"]').forEach(function(checkbox){
186+
checkbox.checked = ( previous_responses.multi_option.indexOf(checkbox.label) > -1 );
187+
});
188+
} else {
189+
new_responses.querySelectorAll('[name$="option"] option').forEach(function(option){
190+
option.selected = option.label = previous_responses.option;
191+
});
192+
}
193+
});
194+
});
195+
</script>
196+
197+
{% endblock %}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{% load django_bootstrap5 %}
2+
3+
<label class="form-label" for="{{ q_form.option.if_for_label }}">Answer</label>
4+
{% if q_form.question_obj.section.title == "Transport" and q_form.question_obj.number == 11 and authority.questiongroup.description == "District" %}
5+
<p class="form-text">
6+
<strong>Note:</strong> District councils are not responsible for transport planning, so you should ignore any road projects in this council’s response.
7+
</p>
8+
{% endif %}
9+
{% if q_form.question_obj.question_type == "multiple_choice" %}
10+
{% bootstrap_field q_form.multi_option show_label="skip" %}
11+
{% else %}
12+
{% bootstrap_field q_form.option show_label="skip" %}
13+
{% endif %}
14+
15+
{% bootstrap_field q_form.page_number %}
16+
17+
{% bootstrap_field q_form.evidence %}
18+
19+
{% bootstrap_field q_form.public_notes %}
20+
21+
{% bootstrap_field q_form.private_notes %}
22+
23+
{% if q_form.question_obj.how_marked == "foi" %}
24+
{% bootstrap_field q_form.foi_answer_in_ror %}
25+
{% endif %}
26+
27+
{{ q_form.authority }}
28+
{{ q_form.question }}
29+
{{ q_form.id }}

crowdsourcer/tests/test_views.py

+26
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,8 @@ def test_save(self):
335335
response = self.client.get(url)
336336
self.assertEqual(response.status_code, 200)
337337

338+
self.assertFalse(response.context["has_previous_questions"])
339+
338340
response = self.client.post(
339341
url,
340342
data={
@@ -754,6 +756,30 @@ def test_multi_save_fix(self):
754756
self.assertNotEquals(last_update, answers[1].last_update)
755757

756758

759+
class TestSaveWithPreviousQuestionsView(BaseTestCase):
760+
fixtures = [
761+
"authorities.json",
762+
"basics.json",
763+
"users.json",
764+
"questions.json",
765+
"options.json",
766+
"assignments.json",
767+
]
768+
769+
def test_save(self):
770+
u = User.objects.get(username="other_marker")
771+
self.client.force_login(u)
772+
773+
url = reverse(
774+
"session_urls:authority_question_edit",
775+
args=("Second Session", "Aberdeenshire Council", "Transport"),
776+
)
777+
response = self.client.get(url)
778+
self.assertEqual(response.status_code, 200)
779+
780+
self.assertTrue(response.context["has_previous_questions"])
781+
782+
757783
class TestAllAuthorityProgressView(BaseTestCase):
758784
def test_non_admin_denied(self):
759785
response = self.client.get(reverse("all_authority_progress"))

crowdsourcer/views/base.py

+2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ class BaseQuestionView(TemplateView):
2626
log_start = "marking form"
2727
title_start = ""
2828
how_marked_in = ["volunteer", "national_volunteer"]
29+
has_previous_questions = False
2930

3031
def setup(self, request, *args, **kwargs):
3132
super().setup(request, *args, **kwargs)
@@ -160,6 +161,7 @@ def get_context_data(self, **kwargs):
160161
context[
161162
"page_title"
162163
] = f"{self.title_start}{context['authority_name']}: {context['section_title']}"
164+
context["has_previous_questions"] = self.has_previous_questions
163165

164166
return context
165167

crowdsourcer/views/marking.py

+39-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
MarkingSession,
1010
PublicAuthority,
1111
Question,
12+
Response,
1213
ResponseType,
1314
)
1415
from crowdsourcer.views.base import BaseQuestionView, BaseSectionAuthorityList
@@ -215,6 +216,12 @@ class SectionAuthorityList(BaseSectionAuthorityList):
215216
class AuthoritySectionQuestions(BaseQuestionView):
216217
template_name = "crowdsourcer/authority_questions.html"
217218

219+
def get_template_names(self):
220+
if self.has_previous_questions:
221+
return ["crowdsourcer/authority_questions_with_previous.html"]
222+
else:
223+
return [self.template_name]
224+
218225
def get_initial_obj(self):
219226
if self.kwargs["name"] == "Isles of Scilly":
220227
self.how_marked_in = [
@@ -224,7 +231,38 @@ def get_initial_obj(self):
224231
"national_data",
225232
]
226233

227-
return super().get_initial_obj()
234+
initial = super().get_initial_obj()
235+
236+
is_previous = Question.objects.filter(
237+
section__marking_session=self.request.current_session,
238+
section__title=self.kwargs["section_title"],
239+
questiongroup=self.authority.questiongroup,
240+
how_marked__in=self.how_marked_in,
241+
previous_question__isnull=False,
242+
).exists()
243+
244+
if is_previous:
245+
self.has_previous_questions = True
246+
question_list = self.questions.values_list(
247+
"previous_question_id", flat=True
248+
)
249+
prev_responses = Response.objects.filter(
250+
authority=self.authority,
251+
question__in=question_list,
252+
response_type=self.rt,
253+
).select_related("question")
254+
255+
response_map = {}
256+
for r in prev_responses:
257+
response_map[r.question.id] = r
258+
259+
for q in self.questions:
260+
data = initial[q.id]
261+
data["previous_response"] = response_map.get(q.previous_question_id)
262+
263+
initial[q.id] = data
264+
265+
return initial
228266

229267
def process_form(self, form):
230268
cleaned_data = form.cleaned_data

0 commit comments

Comments
 (0)