improve surverys

This commit is contained in:
Ross
2026-06-29 09:35:12 +01:00
parent 79ba4db4bf
commit c34dcdfd5e
26 changed files with 911 additions and 39 deletions
+102 -13
View File
@@ -159,6 +159,9 @@ def survey_take(request, pk):
object_id = request.GET.get("object_id") or request.POST.get("object_id")
next_url = request.GET.get("next") or request.POST.get("next") or "/"
mandatory = request.GET.get("mandatory") or request.POST.get("mandatory") or "true"
skip_key = request.GET.get("skip_key") or request.POST.get("skip_key") or ""
# Handle CID auth
cid = request.GET.get("cid") or request.POST.get("cid")
passcode = request.GET.get("passcode") or request.POST.get("passcode")
@@ -189,7 +192,7 @@ def survey_take(request, pk):
object_id=int(object_id) if object_id and str(object_id).isdigit() else None,
pre_or_post=pre_or_post
)
return render(request, "survey/survey_thanks.html", {"next_url": next_url})
return render(request, "survey/survey_thanks.html", {"next_url": next_url, "after_text": survey.after_text})
else:
form = SurveyTakeForm(survey=survey)
@@ -202,6 +205,8 @@ def survey_take(request, pk):
"content_type_id": content_type_id,
"object_id": object_id,
"next": next_url,
"mandatory": mandatory,
"skip_key": skip_key,
})
@login_required
@@ -210,26 +215,63 @@ def survey_results(request, pk):
"""
Scope: Staff/Admin.
Functionality: Renders survey response metrics, analytics, and text answers.
Supports filtering by source context and target collections/exams.
"""
survey = get_object_or_404(Survey, pk=pk)
if not can_manage_survey(request.user, survey):
raise PermissionDenied("You do not have permission to manage this survey.")
responses_count = survey.responses.count()
questions = survey.questions.all()
responses = survey.responses.all()
# Compile list of distinct exams/collections that have used this survey to build the filter dropdown
targets = []
seen_targets = set()
for resp in responses:
if resp.content_type_id and resp.object_id:
key = (resp.content_type_id, resp.object_id)
if key not in seen_targets:
seen_targets.add(key)
try:
obj = resp.content_object
if obj:
targets.append({
"content_type_id": resp.content_type_id,
"object_id": resp.object_id,
"name": str(obj)
})
except Exception:
pass
# Apply GET filters
filter_context = request.GET.get("filter_context")
if filter_context in [SurveyResponse.SurveyContext.PRE, SurveyResponse.SurveyContext.POST, SurveyResponse.SurveyContext.GENERAL]:
responses = responses.filter(pre_or_post=filter_context)
filter_target = request.GET.get("filter_target")
if filter_target:
try:
ct_id, obj_id = filter_target.split(":")
responses = responses.filter(content_type_id=int(ct_id), object_id=int(obj_id))
except (ValueError, TypeError):
pass
responses_count = responses.count()
analytics = []
for q in questions:
answers = q.answers.filter(response__in=responses)
q_data = {
"question": q,
"answers_count": q.answers.count()
"answers_count": answers.count()
}
if q.question_type == SurveyQuestion.QuestionType.TEXT:
q_data["text_answers"] = q.answers.values_list("text_answer", flat=True).exclude(text_answer__isnull=True).exclude(text_answer="")
q_data["text_answers"] = answers.values_list("text_answer", flat=True).exclude(text_answer__isnull=True).exclude(text_answer="")
elif q.question_type == SurveyQuestion.QuestionType.CHOICE:
# Count choices
choice_counts = q.answers.values("choice_answer").annotate(count=Count("id")).order_by("-count")
choice_counts = answers.values("choice_answer").annotate(count=Count("id")).order_by("-count")
q_data["choice_counts"] = []
for cc in choice_counts:
ans_text = cc["choice_answer"]
@@ -243,11 +285,11 @@ def survey_results(request, pk):
elif q.question_type == SurveyQuestion.QuestionType.RATING:
# Stats for rating
avg = q.answers.aggregate(avg=Avg("rating_answer"))["avg"]
avg = answers.aggregate(avg=Avg("rating_answer"))["avg"]
q_data["average_rating"] = round(avg, 2) if avg is not None else "N/A"
# Rating counts
rating_counts = q.answers.values("rating_answer").annotate(count=Count("id"))
rating_counts = answers.values("rating_answer").annotate(count=Count("id"))
counts_dict = {i: 0 for i in range(1, 6)}
for rc in rating_counts:
r = rc["rating_answer"]
@@ -264,12 +306,23 @@ def survey_results(request, pk):
"percentage": pct
})
elif q.question_type == SurveyQuestion.QuestionType.INTEGER:
avg = answers.aggregate(avg=Avg("integer_answer"))["avg"]
q_data["average_integer"] = round(avg, 2) if avg is not None else "N/A"
q_data["integer_answers"] = list(answers.values_list("integer_answer", flat=True).exclude(integer_answer__isnull=True))
elif q.question_type == SurveyQuestion.QuestionType.DATE:
q_data["date_answers"] = list(answers.values_list("date_answer", flat=True).exclude(date_answer__isnull=True))
analytics.append(q_data)
return render(request, "survey/survey_results.html", {
"survey": survey,
"responses_count": responses_count,
"analytics": analytics
"analytics": analytics,
"targets": targets,
"filter_context": filter_context,
"filter_target": filter_target,
})
@login_required
@@ -277,13 +330,28 @@ def survey_results(request, pk):
def survey_export_csv(request, pk):
"""
Scope: Staff/Admin.
Functionality: Exports all responses for a survey as a downloadable CSV.
Functionality: Exports filtered responses for a survey as a downloadable CSV.
"""
survey = get_object_or_404(Survey, pk=pk)
if not can_manage_survey(request.user, survey):
raise PermissionDenied("You do not have permission to manage this survey.")
questions = list(survey.questions.all().order_by("position", "id"))
questions = list(survey.questions.all().order_by("position", "id"))
responses = survey.responses.all()
# Apply GET filters to match CSV export output
filter_context = request.GET.get("filter_context")
if filter_context in [SurveyResponse.SurveyContext.PRE, SurveyResponse.SurveyContext.POST, SurveyResponse.SurveyContext.GENERAL]:
responses = responses.filter(pre_or_post=filter_context)
filter_target = request.GET.get("filter_target")
if filter_target:
try:
ct_id, obj_id = filter_target.split(":")
responses = responses.filter(content_type_id=int(ct_id), object_id=int(obj_id))
except (ValueError, TypeError):
pass
response = HttpResponse(content_type="text/csv")
response["Content-Disposition"] = f'attachment; filename="survey_{survey.pk}_responses.csv"'
@@ -296,7 +364,7 @@ def survey_export_csv(request, pk):
writer.writerow(headers)
# Data rows
for resp in survey.responses.all():
for resp in responses:
user_str = resp.user.username if resp.user else ""
cid_str = str(resp.cid) if resp.cid else ""
ct_str = f"{resp.content_type.app_label}.{resp.content_type.model}" if resp.content_type else ""
@@ -324,6 +392,10 @@ def survey_export_csv(request, pk):
row.append(ans.choice_answer or "")
elif q.question_type == SurveyQuestion.QuestionType.RATING:
row.append(str(ans.rating_answer) if ans.rating_answer is not None else "")
elif q.question_type == SurveyQuestion.QuestionType.INTEGER:
row.append(str(ans.integer_answer) if ans.integer_answer is not None else "")
elif q.question_type == SurveyQuestion.QuestionType.DATE:
row.append(ans.date_answer.strftime("%Y-%m-%d") if ans.date_answer else "")
writer.writerow(row)
@@ -332,9 +404,13 @@ def survey_export_csv(request, pk):
def check_survey_interception(request, exam_or_collection, cid=None, passcode=None):
"""
Checks if a pre_survey is configured for this collection/exam and if the user/cid
has already responded to it. If not, redirects them to take the survey.
has already responded to it or skipped it. If not, redirects them to take the survey.
"""
if exam_or_collection.pre_survey:
session_key = f"skipped_pre_survey_{exam_or_collection.pk}"
if request.session.get(session_key):
return None
user_param = request.user if request.user.is_authenticated else None
cid_param = int(cid) if cid and str(cid).isdigit() else None
ct = ContentType.objects.get_for_model(exam_or_collection)
@@ -351,8 +427,21 @@ def check_survey_interception(request, exam_or_collection, cid=None, passcode=No
if not has_responded:
from django.urls import reverse
survey_url = reverse("survey:survey_take", kwargs={"pk": exam_or_collection.pre_survey.pk})
params = f"?pre_or_post=PRE&content_type_id={ct.id}&object_id={exam_or_collection.pk}&next={request.get_full_path()}"
is_mandatory = getattr(exam_or_collection, "pre_survey_mandatory", False)
mandatory_str = "true" if is_mandatory else "false"
params = f"?pre_or_post=PRE&content_type_id={ct.id}&object_id={exam_or_collection.pk}&next={request.get_full_path()}&mandatory={mandatory_str}&skip_key={session_key}"
if cid and passcode:
params += f"&cid={cid}&passcode={passcode}"
return redirect(f"{survey_url}{params}")
return None
def survey_skip(request):
"""
Scope: Candidates/Users.
Functionality: Sets a session variable to bypass an optional survey, then redirects.
"""
next_url = request.GET.get("next") or "/"
key = request.GET.get("key")
if key:
request.session[key] = True
return redirect(next_url)