Files
penracourses/physics/views.py
T
Ross 7d30e743d5 .
2021-09-30 22:45:59 +01:00

355 lines
10 KiB
Python

from physics.filters import QuestionFilter, UserAnswerFilter
from generic.mixins import SuperuserRequiredMixin
from physics.tables import QuestionTable, UserAnswerTable
from django.shortcuts import render, get_object_or_404, redirect
from django.views.decorators.csrf import csrf_exempt
from django import forms
# from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required, user_passes_test
from django.contrib.auth.models import User
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic.detail import DetailView
from django.views.generic.edit import CreateView, UpdateView, DeleteView
from django.views.generic import ListView
from django.db.models.functions import Lower
from django.core.cache import cache
from django.urls import reverse_lazy, reverse
from django.http import Http404, JsonResponse
from django.http import HttpResponseRedirect, HttpResponse
from django_filters.views import FilterView
from django_tables2.views import SingleTableMixin
from reversion.views import RevisionMixin
from .models import Question, Category, Exam, CidUserAnswer
from collections import defaultdict
import os
import base64
import mimetypes
import json
import statistics
import plotly.express as px
from generic.views import ExamCloneMixin, ExamViews, GenericViewBase
from rest_framework import viewsets, permissions
from rest_framework.pagination import PageNumberPagination
from django.core.exceptions import PermissionDenied
from .forms import ExamForm
class AuthorOrCheckerRequiredMixin(object):
def get_object(self, *args, **kwargs):
obj = super().get_object(*args, **kwargs)
if self.request.user.groups.filter(name="physics_checker").exists():
return obj
if self.request.user not in obj.author.all():
raise PermissionDenied() # or Http404
return obj
def question_list(request):
questions = Question.objects.all()
return render(request, "physics/question_list.html", {"questions": questions})
@login_required
def question_detail(request, pk):
question = get_object_or_404(Question, pk=pk)
return render(request, "physics/question_detail.html", {"question": question})
def active_exams(request):
exams = Exam.objects.all()
print(exams)
active_exams = []
for exam in exams:
if exam.active:
active_exams.append(exam)
return render(request, "physics/available_exam_list.html", {"exams": active_exams})
@login_required
def exam_scores_cid(request, pk):
exam = get_object_or_404(Exam, pk=pk)
questions = exam.exam_questions.all()
cids = (
CidUserAnswer.objects.filter(question__in=questions)
.order_by("cid")
.values_list("cid", flat=True)
.distinct()
)
user_answers_and_marks = defaultdict(list)
user_answers_marks = defaultdict(list)
user_answers = defaultdict(list)
user_names = {}
by_question = defaultdict(list)
unmarked = set()
# Loop through all candidates
for cid in cids:
# Convoluted (probably...)
user_names[cid] = cid
for q in questions:
# Get user answer
s = q.cid_user_answers.filter(cid=cid).first()
if not s:
# skip if no answer
user_answers_marks[cid].append(0)
user_answers[cid].append("")
by_question[q].append(("", 0))
continue
ans = s.get_answers()
answer_scores = s.get_answer_scores()
user_answers[cid].append(ans)
user_answers_marks[cid].append(answer_scores)
user_answers_and_marks[cid].append((ans, answer_scores))
zipped_ans_scores = zip(ans, answer_scores)
by_question[q].append(zipped_ans_scores)
user_scores = {}
for user in user_answers_marks:
user_scores[user] = sum([sum(i) for i in user_answers_marks[user]])
user_scores_list = list(user_scores.values())
if len(user_scores_list) < 1:
mean = 0
median = 0
mode = 0
fig_html = ""
else:
mean = statistics.mean(user_scores_list)
median = statistics.median(user_scores_list)
try:
mode = statistics.mode(user_scores_list)
except statistics.StatisticsError:
mode = "No unique mode"
df = user_scores_list
fig = px.histogram(
df,
x=0,
title="{}: distribution of scores".format(exam),
labels={"0": "Score"},
height=400,
width=600,
)
fig_html = fig.to_html()
max_score = len(questions)
return render(
request,
"physics/exam_scores.html",
{
"cids": cids,
"exam": exam,
"unmarked": unmarked,
"questions": questions,
"by_question": by_question,
"user_answers": dict(user_answers),
"user_answers_marks": dict(user_answers_marks),
"user_scores": user_scores,
"user_scores_list": user_scores_list,
"user_names": user_names,
"user_answers_and_marks": user_answers_and_marks,
"max_score": max_score,
"mean": mean,
"median": median,
"mode": mode,
"plot": fig_html,
},
)
def exam_scores_cid_user(request, pk, sk):
exam = get_object_or_404(Exam, pk=pk)
# TODO:Need some kind of test for cid
cid = sk
questions = exam.exam_questions.all()
answers_and_marks = []
answers_marks = []
answers = []
view_all_results = False
if request.user.groups.filter(name="view_all_results").exists():
view_all_results = True
for q in questions:
# Get user answer
user_answer = q.cid_user_answers.filter(cid=cid).first()
ans = user_answer.get_answers()
answer_scores = user_answer.get_answer_scores()
correct_answers = q.get_answers()
if not exam.publish_results and not view_all_results:
correct_answers = ("*****", "*****", "*****", "*****", "*****")
answer_scores = (0, 0, 0, 0, 0)
answers.append(ans)
answers_marks.append(answer_scores)
merged_ans = zip(q.get_questions(), ans, answer_scores, correct_answers)
answers_and_marks.append((q, merged_ans))
total_score = sum(sum(i) for i in answers_marks)
max_score = len(questions) * 5
return render(
request,
"physics/exam_scores_user.html",
{
"exam": exam,
"cid": cid,
"questions": questions,
"answers": answers,
"answers_marks": answers_marks,
"total_score": total_score,
"max_score": max_score,
"answers_and_marks": answers_and_marks,
},
)
def exam_take(request, pk):
exam = get_object_or_404(Exam, pk=pk)
if not exam.active:
raise Http404("Exam not found")
questions = exam.exam_questions.all()
return render(
request,
"physics/exam_take.html",
{
"exam": exam,
"questions": questions,
},
)
def loadJsonAnswer(answer):
# As access is not restricted make sure the data appears valid
if (not isinstance(answer["cid"], int)) or (not isinstance(answer["eid"], int)):
return False, JsonResponse({"success": False, "error": "invalid"})
# The model should catch invalid data but this should be less intensive
#max_int = 10000000
#if answer["cid"] > max_int or answer["eid"] > max_int:
# return False, JsonResponse({"success": False})
exam = get_object_or_404(Exam, pk=answer["eid"])
#if not exam.active:
# return False, JsonResponse(
# {"success": False, "error": "No active exam: {}".format(answer["eid"])}
# )
questions = defaultdict(dict)
for q, n, a in answer["ans"]:
questions[q][n] = a
pass
for qid in questions:
exiting_answers = CidUserAnswer.objects.filter(
question__id=qid, exam__id=answer["eid"], cid=answer["cid"]
)
a = questions[qid]["a"]
b = questions[qid]["b"]
c = questions[qid]["c"]
d = questions[qid]["d"]
e = questions[qid]["e"]
if not exiting_answers:
ans = CidUserAnswer(a=a, b=b, c=c, d=d, e=e, cid=answer["cid"])
ans.question_id = qid
ans.exam_id = answer["eid"]
ans.full_clean()
ans.save()
else:
# Update an existing answer
# should never be more than one (famous last words)
ans = exiting_answers[0]
ans.a = a
ans.b = b
ans.c = c
ans.d = d
ans.e = e
ans.full_clean()
ans.save()
return True, None
PhysicsExamViews = ExamViews(Exam, Question, "physics", "physics", loadJsonAnswer)
GenericViews = GenericViewBase("physics", Question, CidUserAnswer, Exam)
class ExamCreate(RevisionMixin, LoginRequiredMixin, CreateView):
model = Exam
form_class = ExamForm
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.save()
form.instance.author.add(self.request.user.id)
return super().form_valid(form)
class ExamClone(AuthorOrCheckerRequiredMixin, ExamCloneMixin, ExamCreate):
"""Clone exam view"""
class QuestionView(LoginRequiredMixin, SingleTableMixin, FilterView, AuthorOrCheckerRequiredMixin):
model = Question
table_class = QuestionTable
template_name = "physics/question_view.html"
filterset_class = QuestionFilter
class UserAnswerView(AuthorOrCheckerRequiredMixin, LoginRequiredMixin, DetailView):
model = CidUserAnswer
class UserAnswerTableView(AuthorOrCheckerRequiredMixin, LoginRequiredMixin, SingleTableMixin, FilterView):
model = CidUserAnswer
table_class = UserAnswerTable
template_name = "physics/user_answer_question_view.html"
filterset_class = UserAnswerFilter
class UserAnswerDelete(SuperuserRequiredMixin, DeleteView):
model = CidUserAnswer
template_name = "user_answer_delete.html"
success_url = reverse_lazy("physics:user_answer_table_view")