Skip to content

Commit 27d10a4

Browse files
committed
add JSON response view for saving individual responses
For autosaving responses as we got to help avoid data loss.
1 parent ff7f1de commit 27d10a4

File tree

3 files changed

+154
-2
lines changed

3 files changed

+154
-2
lines changed

ceuk-marking/urls.py

+5
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@
6767
marking.SectionAuthorityList.as_view(),
6868
name="section_authorities",
6969
),
70+
path(
71+
"authorities/<name>/section/<section_title>/questions/<question>/",
72+
marking.AuthoritySectionJSONQuestion.as_view(),
73+
name="authority_json_question_edit",
74+
),
7075
path(
7176
"authorities/<name>/section/<section_title>/questions/",
7277
marking.AuthoritySectionQuestions.as_view(),

crowdsourcer/views/base.py

+140-1
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
from django.core.exceptions import PermissionDenied
55
from django.db.models import Count, F, FloatField, OuterRef, Subquery
66
from django.db.models.functions import Cast
7+
from django.http import JsonResponse
78
from django.views.generic import ListView, TemplateView
89

9-
from crowdsourcer.forms import ResponseFormset
10+
from crowdsourcer.forms import ResponseForm, ResponseFormset
1011
from crowdsourcer.models import (
1112
Assigned,
1213
PublicAuthority,
@@ -167,6 +168,144 @@ def get_context_data(self, **kwargs):
167168
return context
168169

169170

171+
class BaseResponseJSONView(TemplateView):
172+
model = Response
173+
form = ResponseForm
174+
response_type = "First Mark"
175+
log_start = "marking form"
176+
title_start = ""
177+
how_marked_in = ["volunteer", "national_volunteer"]
178+
179+
def setup(self, request, *args, **kwargs):
180+
super().setup(request, *args, **kwargs)
181+
try:
182+
self.rt = ResponseType.objects.get(type=self.response_type)
183+
except ResponseType.DoesNotExist:
184+
self.rt = None
185+
186+
def check_local_permissions(self):
187+
return True
188+
189+
def check_permissions(self):
190+
if self.request.user.is_anonymous:
191+
raise PermissionDenied
192+
193+
if self.check_local_permissions() is False:
194+
raise PermissionDenied
195+
196+
if not Assigned.is_user_assigned(
197+
self.request.user,
198+
authority=self.kwargs["name"],
199+
section=self.kwargs["section_title"],
200+
marking_session=self.request.current_session,
201+
current_stage=self.rt,
202+
):
203+
raise PermissionDenied
204+
205+
def get_initial_obj(self):
206+
self.authority = PublicAuthority.objects.get(name=self.kwargs["name"])
207+
self.question = Question.objects.get(id=self.kwargs["question"])
208+
instance = None
209+
initial = {
210+
"authority": self.authority,
211+
"question": self.question,
212+
}
213+
try:
214+
instance = Response.objects.get(
215+
authority=self.authority, question=self.question, response_type=self.rt
216+
)
217+
logger.debug(
218+
f"FOUND initial object for {self.authority}, {self.question}, {self.rt}"
219+
)
220+
for f in [
221+
"evidence",
222+
"id",
223+
"multi_option",
224+
"option",
225+
"page_number",
226+
"private_notes",
227+
"public_notes",
228+
]:
229+
initial[f] = getattr(instance, f)
230+
logger.debug(f"initial data is {initial}")
231+
except Response.DoesNotExist:
232+
logger.debug(
233+
f"did NOT find initial object for {self.authority}, {self.question}, {self.rt}"
234+
)
235+
pass
236+
237+
return {"initial": initial, "instance": instance}
238+
239+
def get_form(self):
240+
initial = self.get_initial_obj()
241+
if self.request.POST:
242+
data = self.request.POST
243+
form = self.form(
244+
data, instance=initial["instance"], initial=initial["initial"]
245+
)
246+
else:
247+
form = self.form(instance=initial["instance"], initial=initial["initial"])
248+
return form
249+
250+
def get(self, *args, **kwargs):
251+
return None
252+
253+
def session_form_hash(self):
254+
return f"form-submission+{self.__class__.__name__}"
255+
256+
def get_post_hash(self):
257+
excluded = {
258+
"csrfmiddlewaretoken",
259+
}
260+
post_hash = hash(
261+
tuple(
262+
sorted(
263+
(k, v) for k, v in self.request.POST.items() if k not in excluded
264+
)
265+
)
266+
)
267+
268+
return post_hash
269+
270+
def post(self, *args, **kwargs):
271+
self.check_permissions()
272+
section_title = self.kwargs.get("section_title", "")
273+
authority = self.kwargs.get("name", "")
274+
logger.debug(
275+
f"{self.log_start} JSON post from {self.request.user.email} for {authority}/{section_title}"
276+
)
277+
logger.debug(f"post data is {self.request.POST}")
278+
279+
form = self.get_form()
280+
logger.debug("got form")
281+
if form.is_valid():
282+
logger.debug("form IS VALID")
283+
post_hash = self.get_post_hash()
284+
if self.check_form_not_resubmitted(post_hash):
285+
logger.debug("form GOOD, saving")
286+
form.instance.response_type = self.rt
287+
form.instance.user = self.request.user
288+
form.save()
289+
self.request.session[self.session_form_hash()] = post_hash
290+
else:
291+
logger.debug("form RESUBMITTED, not saving")
292+
else:
293+
logger.debug(f"form NOT VALID, errors are {form.errors}")
294+
return JsonResponse({"success": 0, "errors": form.errors})
295+
296+
print(form.instance)
297+
return JsonResponse({"success": 1})
298+
299+
# there are occassional issues with the same form being resubmitted twice the first time
300+
# someone saves a result which means you get two responses saved for the same question which
301+
# leads to issues when exporting the data so add in some basic checking that this isn't a
302+
# repeat submission.
303+
def check_form_not_resubmitted(self, post_hash):
304+
previous_post_hash = self.request.session.get(self.session_form_hash())
305+
306+
return post_hash != previous_post_hash
307+
308+
170309
class BaseSectionAuthorityList(ListView):
171310
template_name = "crowdsourcer/section_authority_list.html"
172311
model = Section

crowdsourcer/views/marking.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@
1212
Response,
1313
ResponseType,
1414
)
15-
from crowdsourcer.views.base import BaseQuestionView, BaseSectionAuthorityList
15+
from crowdsourcer.views.base import (
16+
BaseQuestionView,
17+
BaseResponseJSONView,
18+
BaseSectionAuthorityList,
19+
)
1620

1721
logger = logging.getLogger(__name__)
1822

@@ -279,3 +283,7 @@ def process_form(self, form):
279283
logger.debug(
280284
f"option is {cleaned_data.get('option', None)}, multi is {cleaned_data.get('multi_option', None)}"
281285
)
286+
287+
288+
class AuthoritySectionJSONQuestion(BaseResponseJSONView):
289+
pass

0 commit comments

Comments
 (0)