Files
penracourses/physics/views.py
T
2023-05-15 11:12:52 +01:00

478 lines
14 KiB
Python

from django.contrib.contenttypes.models import ContentType
from django.utils import timezone
from generic.models import CidUser
from physics.decorators import user_is_author_or_physics_checker
from physics.filters import QuestionFilter, CidUserAnswerFilter
from generic.mixins import SuperuserRequiredMixin
from physics.tables import QuestionTable, CidUserAnswerTable
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, HttpResponseBadRequest, 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 (
AuthorRequiredMixin,
ExamCloneMixin,
ExamCreateBase,
ExamDeleteBase,
ExamUpdateBase,
ExamViews,
GenericViewBase,
exam_inactive,
)
from rest_framework import viewsets, permissions
from rest_framework.pagination import PageNumberPagination
from django.core.exceptions import PermissionDenied
from .forms import CidUserAnswerForm, ExamAuthorForm, ExamForm
from .decorators import (
user_is_author_or_physics_checker,
user_is_exam_author_or_physics_checker,
)
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
@user_is_author_or_physics_checker
def question_detail(request, pk):
question = get_object_or_404(Question, pk=pk)
return render(request, "physics/question_detail.html", {"question": question})
@login_required
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})
def exam_scores_cid_user(request, pk, cid=None, passcode=None):
exam = get_object_or_404(Exam, pk=pk)
if not exam.exam_mode:
raise Http404("Packet not in exam mode")
if cid is not None and not exam.check_cid_user(cid, passcode, request):
raise Http404("Error accessing exam")
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
if cid is not None:
user_answer = q.cid_user_answers.filter(cid=cid, exam__id=pk).first()
else:
user_answer = q.cid_user_answers.filter(
user=request.user, exam__id=pk
).first()
if user_answer is None:
answer_scores = (0, 0, 0, 0, 0)
ans = ("", "", "", "", "")
else:
ans = user_answer.get_answers()
answer_scores = user_answer.get_answer_score()
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,
"passcode": passcode,
"questions": questions,
"answers": answers,
"answers_marks": answers_marks,
"total_score": total_score,
"max_score": max_score,
"answers_and_marks": answers_and_marks,
"view_all_results": view_all_results,
},
)
def exam_take_old(request, pk):
exam = get_object_or_404(Exam, pk=pk)
if not exam.active:
raise exam_inactive(request)
questions = exam.exam_questions.all()
return render(
request,
"physics/exam_take_old.html",
{
"exam": exam,
"questions": questions,
},
)
def exam_start(request, pk):
exam = get_object_or_404(Exam, pk=pk)
if not exam.active:
return exam_inactive(request, context={"exam": exam})
return render(
request,
"physics/exam_start.html",
{
"exam": exam,
},
)
def exam_take_overview(request, pk, cid=None, passcode=None):
exam = get_object_or_404(Exam, pk=pk)
if cid is not None and not exam.check_cid_user(cid, passcode):
raise Http404("Error accessing exam")
if not exam.active:
return exam_inactive(request, context={"exam": exam})
questions = exam.exam_questions.all()
if cid is not None:
answers = CidUserAnswer.objects.filter(cid=cid, exam=exam)
else:
answers = CidUserAnswer.objects.filter(user=request.user, exam=exam)
answer_question_map = {}
for ans in answers:
answer_question_map[ans.question] = ans
question_answer_tuples = []
answer_count = 0
for q in questions:
# if q in answer_question_map and answer_question_map[q].answer:
if q in answer_question_map: # might need to improve this
question_answer_tuples.append((q, answer_question_map[q]))
answer_count += 1
else:
question_answer_tuples.append((q, None))
if cid is not None:
c = CidUser.objects.filter(cid=cid).first()
cid_user_exam = exam.get_or_create_cid_user_exam(cid_user=c)
else:
cid_user_exam = exam.get_or_create_cid_user_exam(user_user=request.user)
return render(
request,
"physics/exam_take_overview.html",
{
"exam": exam,
"cid": cid,
"question_answer_tuples": question_answer_tuples,
"answer_count": answer_count,
"exam_length": len(questions),
"passcode": passcode,
"cid_user_exam": cid_user_exam,
},
)
def exam_take(request, pk, sk, cid=None, passcode=None):
exam = get_object_or_404(Exam, pk=pk)
if not exam.active:
return exam_inactive(request, context={"exam": exam})
if cid is not None and not exam.check_cid_user(cid, passcode):
raise Http404("Error accessing exam")
if cid is not None:
c = CidUser.objects.filter(cid=cid).first()
cid_user_exam = exam.get_or_create_cid_user_exam(cid_user=c)
else:
cid_user_exam = exam.get_or_create_cid_user_exam(user_user=request.user)
question = exam.exam_questions.all()[sk]
exam_length = len(exam.exam_questions.all())
pos = exam.get_question_index(question)
if cid is not None:
answer = question.cid_user_answers.filter(cid=cid, exam=exam).first()
else:
answer = question.cid_user_answers.filter(user=request.user, exam=exam).first()
previous = -1
if sk > 0:
previous = sk - 1
next = sk + 1
if sk == exam_length - 1:
next = False
if request.method == "POST":
if answer:
form = CidUserAnswerForm(request.POST, instance=answer)
else:
form = CidUserAnswerForm(request.POST)
if form.is_valid() and not exam.publish_results:
answer = form.save(commit=False)
if cid is not None:
answer.cid = cid
else:
answer.user = request.user
answer.question = question
answer.exam = exam
# answer.published_date = timezone.now()
answer.save()
cid_user_exam.end_time = timezone.now()
cid_user_exam.save()
if cid is not None:
kwargs = {"pk": pk, "cid": cid, "passcode": passcode}
take_url = "physics:exam_take"
finish_url = "physics:exam_take_overview"
else:
kwargs = {"pk": pk}
take_url = "physics:exam_take_user"
finish_url = "physics:exam_take_overview_user"
if "next" in request.POST:
if not next:
return HttpResponseBadRequest("Invalid request")
return redirect(take_url, sk=pos + 1, **kwargs)
elif "previous" in request.POST:
return redirect(take_url, sk=pos - 1, **kwargs)
elif "finish" in request.POST:
return redirect(finish_url, **kwargs)
elif "goto" in request.POST:
return redirect(take_url, sk=int(request.POST.get("goto")), **kwargs)
else:
form = CidUserAnswerForm(instance=answer)
saved_answer = False
if answer is not None:
saved_answer = [answer.a, answer.b, answer.c, answer.d, answer.e]
return render(
request,
"physics/exam_take.html",
{
"form": form,
"cid": cid,
"exam": exam,
"question": question,
"next": next,
"previous": previous,
"exam_length": exam_length,
"pos": pos,
"passcode": passcode,
"cid_user_exam": cid_user_exam,
},
)
# 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 = 2147483647
# if answer["cid"] > max_int or answer["eid"] > max_int:
# return False, JsonResponse({"success": False, "error": "invalid candidate id"})
#
# 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
GenericExamViews = ExamViews(Exam, Question, None, CidUserAnswer, "physics", "physics")
GenericViews = GenericViewBase("physics", Question, CidUserAnswer, Exam)
class ExamCreate(ExamCreateBase):
model = Exam
form_class = ExamForm
class ExamUpdate(ExamUpdateBase, AuthorOrCheckerRequiredMixin):
model = Exam
form_class = ExamForm
class ExamDelete(AuthorOrCheckerRequiredMixin, ExamDeleteBase):
model = Exam
class ExamClone(AuthorOrCheckerRequiredMixin, ExamCloneMixin, ExamCreate):
"""Clone exam view"""
class ExamAuthorUpdate(
RevisionMixin, LoginRequiredMixin, AuthorRequiredMixin, UpdateView
):
model = Exam
form_class = ExamAuthorForm
class QuestionView(
LoginRequiredMixin, SingleTableMixin, FilterView, AuthorOrCheckerRequiredMixin
):
model = Question
table_class = QuestionTable
template_name = "question_table_view.html"
filterset_class = QuestionFilter
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["app_name"] = "physics"
return context
class CidUserAnswerView(AuthorOrCheckerRequiredMixin, LoginRequiredMixin, DetailView):
model = CidUserAnswer
class CidUserAnswerTableView(
AuthorOrCheckerRequiredMixin, LoginRequiredMixin, SingleTableMixin, FilterView
):
model = CidUserAnswer
table_class = CidUserAnswerTable
template_name = "physics/user_answer_question_view.html"
filterset_class = CidUserAnswerFilter
class CidUserAnswerDelete(SuperuserRequiredMixin, DeleteView):
model = CidUserAnswer
template_name = "user_answer_delete.html"
success_url = reverse_lazy("physics:user_answer_table_view")