Skip to content

Commit 42792b4

Browse files
committed
add a page to allow bulk updating of responses to a question
This is mostly to allow people to download a CSV of responses, edit that and re-upload it to correct the responses. It will only update an existing response if it has changed.
1 parent 64b8df1 commit 42792b4

File tree

8 files changed

+381
-3
lines changed

8 files changed

+381
-3
lines changed

ceuk-marking/urls.py

+5
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,11 @@
328328
questions.SectionList.as_view(),
329329
name="question_sections",
330330
),
331+
path(
332+
"questions/update/<section_name>/<question>/",
333+
questions.QuestionBulkUpdateView.as_view(),
334+
name="question_bulk_update",
335+
),
331336
]
332337

333338
urlpatterns = [

crowdsourcer/fixtures/responses.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"response_type": 1,
1111
"public_notes": "public notrs",
1212
"page_number": "0",
13-
"evidence": "",
13+
"evidence": null,
1414
"private_notes": "private notes",
1515
"revision_type": null,
1616
"revision_notes": null,

crowdsourcer/forms.py

+72
Original file line numberDiff line numberDiff line change
@@ -584,3 +584,75 @@ def __init__(self, properties: {}, **kwargs):
584584
QuestionFormset = modelformset_factory(
585585
Question, fields=["question_type", "weighting"], extra=0, can_delete=False
586586
)
587+
588+
589+
class QuestionBulkUploadForm(Form):
590+
question = CharField(widget=HiddenInput)
591+
stage = ChoiceField(required=True, choices=[])
592+
updated_responses = FileField()
593+
594+
def clean(self):
595+
data = self.cleaned_data.get("updated_responses")
596+
597+
try:
598+
df = pd.read_csv(
599+
data,
600+
usecols=[
601+
"authority",
602+
"answer",
603+
"score",
604+
"public_notes",
605+
"page_number",
606+
"evidence",
607+
"private_notes",
608+
],
609+
)
610+
except ValueError as v:
611+
raise ValidationError(f"Problem processing csv file: {v}")
612+
613+
self.responses_df = df
614+
615+
try:
616+
question = Question.objects.get(id=self.cleaned_data["question"])
617+
except Question.DoesNotExist:
618+
raise ValidationError(f"Bad question id: {self.cleaned_data['question']}")
619+
620+
is_multi = question.question_type == "multiple_choice"
621+
622+
file_errors = []
623+
for _, row in self.responses_df.iterrows():
624+
desc = row["answer"].strip()
625+
try:
626+
PublicAuthority.objects.get(
627+
name=row["authority"],
628+
marking_session=self.session,
629+
)
630+
except PublicAuthority.DoesNotExist:
631+
file_errors.append(f"No such authority: {row['authority']}")
632+
continue
633+
634+
if desc == "-":
635+
continue
636+
637+
if not is_multi:
638+
answers = [desc]
639+
else:
640+
answers = desc.split("|")
641+
642+
for answer in answers:
643+
try:
644+
Option.objects.get(question=question, description=answer)
645+
except Option.DoesNotExist:
646+
file_errors.append(
647+
f"No such answer for {row['authority']}: {answer}"
648+
)
649+
continue
650+
651+
if len(file_errors) > 0:
652+
raise ValidationError(file_errors)
653+
654+
def __init__(self, question_id, stage_choices, session, **kwargs):
655+
super().__init__(**kwargs)
656+
self.session = session
657+
self.initial["question"] = question_id
658+
self.fields["stage"].choices = stage_choices
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{% extends 'crowdsourcer/base.html' %}
2+
3+
{% load crowdsourcer_tags django_bootstrap5 %}
4+
5+
{% block content %}
6+
{% if show_login %}
7+
<h1 class="mb-4">Sign in</h1>
8+
<a href="{% url 'login' %}">Sign in</a>
9+
{% else %}
10+
<h1 class="mb-4">Update Responses for {{ section.title }} {{ question.number_and_part }}</h1>
11+
<h4 class="mb-4">{{ question.description }}</h4>
12+
13+
14+
<form enctype="multipart/form-data" action="" method="post">
15+
{% csrf_token %}
16+
{% bootstrap_form form %}
17+
<input type="submit" value="Update">
18+
</form>
19+
20+
{% endif %}
21+
{% endblock %}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
authority,answer,score,public_notes,page_number,evidence,private_notes
2+
Aberdeen City Council,Yes,1,uploaded public notes,99,uploaded evidence,uploaded private notes
3+
Aberdeenshire Council,No,0,,,,
4+
Adur District Council,-,-,-,-,-,-
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
authority,answer,score,public_notes,page_number,evidence,private_notes
2+
Aberdeen City Council,Yes,1,uploaded public notes,99,uploaded evidence,uploaded private notes
3+
Aberdeenshire Council,Yes,0,public notrs,0,,private notes
4+
Adur District Council,-,-,-,-,-,-
+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import pathlib
2+
3+
from django.contrib.auth.models import Permission, User
4+
from django.test import TestCase
5+
from django.urls import reverse
6+
7+
from crowdsourcer.models import Question, Response
8+
9+
10+
class BaseTestCase(TestCase):
11+
fixtures = [
12+
"authorities.json",
13+
"basics.json",
14+
"users.json",
15+
"questions.json",
16+
"options.json",
17+
"assignments.json",
18+
"responses.json",
19+
]
20+
21+
def setUp(self):
22+
p = Permission.objects.get(codename="can_manage_users")
23+
u = User.objects.get(username="volunteer_admin")
24+
u.user_permissions.add(p)
25+
26+
self.client.force_login(u)
27+
self.user = u
28+
29+
30+
class TestBulkUpload(BaseTestCase):
31+
def test_one_update_one_new(self):
32+
url = reverse("question_bulk_update", args=("Transport", "1"))
33+
response = self.client.get(url)
34+
35+
q = Question.objects.get(
36+
section__title="Transport",
37+
section__marking_session__label="Default",
38+
number=1,
39+
)
40+
41+
all_r = Response.objects.filter(question=q, response_type__type="First Mark")
42+
self.assertEqual(all_r.count(), 1)
43+
44+
r = Response.objects.get(question=q, authority__name="Aberdeenshire Council")
45+
46+
self.assertEqual(r.option.description, "Yes")
47+
self.assertEqual(r.page_number, "0")
48+
49+
upload_file = (
50+
pathlib.Path(__file__).parent.resolve()
51+
/ "data"
52+
/ "test_question_upload.csv"
53+
)
54+
55+
with open(upload_file, "rb") as fp:
56+
response = self.client.post(
57+
url,
58+
data={
59+
"question": 281,
60+
"updated_responses": fp,
61+
"stage": "First Mark",
62+
},
63+
)
64+
65+
self.assertRedirects(response, "/Default" + url)
66+
self.assertEqual(all_r.count(), 2)
67+
68+
r = Response.objects.get(question=q, authority__name="Aberdeen City Council")
69+
70+
self.assertEqual(r.option.description, "Yes")
71+
self.assertEqual(r.page_number, "99")
72+
73+
r = Response.objects.get(question=q, authority__name="Aberdeenshire Council")
74+
75+
self.assertEqual(r.option.description, "No")
76+
self.assertEqual(r.page_number, None)
77+
78+
def test_one_new_one_unchanged(self):
79+
url = reverse("question_bulk_update", args=("Transport", "1"))
80+
response = self.client.get(url)
81+
82+
q = Question.objects.get(
83+
section__title="Transport",
84+
section__marking_session__label="Default",
85+
number=1,
86+
)
87+
88+
all_r = Response.objects.filter(question=q, response_type__type="First Mark")
89+
self.assertEqual(all_r.count(), 1)
90+
91+
r = Response.objects.get(question=q, authority__name="Aberdeenshire Council")
92+
93+
last_update = r.last_update
94+
self.assertEqual(r.option.description, "Yes")
95+
self.assertEqual(r.page_number, "0")
96+
97+
upload_file = (
98+
pathlib.Path(__file__).parent.resolve()
99+
/ "data"
100+
/ "test_question_upload_one_unchanged.csv"
101+
)
102+
103+
with open(upload_file, "rb") as fp:
104+
response = self.client.post(
105+
url,
106+
data={
107+
"question": 281,
108+
"updated_responses": fp,
109+
"stage": "First Mark",
110+
},
111+
)
112+
113+
self.assertRedirects(response, "/Default" + url)
114+
self.assertEqual(all_r.count(), 2)
115+
116+
r = Response.objects.get(question=q, authority__name="Aberdeen City Council")
117+
118+
self.assertEqual(r.option.description, "Yes")
119+
self.assertEqual(r.page_number, "99")
120+
121+
r = Response.objects.get(question=q, authority__name="Aberdeenshire Council")
122+
123+
self.assertEqual(r.option.description, "Yes")
124+
self.assertEqual(r.page_number, "0")
125+
self.assertEqual(last_update, r.last_update)

0 commit comments

Comments
 (0)