.
This commit is contained in:
@@ -1,16 +1,16 @@
|
||||
{% extends "sbas/base.html" %}
|
||||
|
||||
{% block js %}
|
||||
{{ form.media }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Add Exam</h2>
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
<table>
|
||||
{{ form.as_table }}
|
||||
</table>
|
||||
<input type="submit" value="Submit">
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% extends "sbas/base.html" %}
|
||||
|
||||
{% block js %}
|
||||
{{ form.media }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Add Exam</h2>
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
<table>
|
||||
{{ form.as_table }}
|
||||
</table>
|
||||
<input type="submit" value="Submit">
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
+77
-77
@@ -1,77 +1,77 @@
|
||||
from django.urls import path, include
|
||||
|
||||
from . import views
|
||||
from django.views.decorators.cache import cache_page
|
||||
|
||||
app_name = "sbas"
|
||||
|
||||
urlpatterns = [
|
||||
# path('', views.question_list, name='question_list'),
|
||||
path("", views.GenericExamViews.index, name="index"),
|
||||
path("question/", views.QuestionView.as_view(), name="question_view"),
|
||||
path("question/<int:pk>/", views.question_detail, name="question_detail"),
|
||||
# path("question/create/", views.QuestionCreate.as_view(), name="anatomy_question_create"),
|
||||
# path("question/<int:pk>/update", views.QuestionUpdate.as_view(), name="anatomy_question_update"),
|
||||
# path("exam/<int:pk>/<int:sk>/mark", views.mark, name="mark"),
|
||||
# path("exam/<int:pk>/mark", views.mark_overview, name="mark_overview"),
|
||||
path(
|
||||
"exam/<int:pk>/<int:sk>/<str:cid>/<str:passcode>/take",
|
||||
views.exam_take,
|
||||
name="exam_take",
|
||||
),
|
||||
path("exam/<int:pk>/start", views.exam_start, name="exam_start"),
|
||||
path(
|
||||
"exam/<int:pk>/<str:cid>/<str:passcode>/finish",
|
||||
views.exam_take_overview,
|
||||
name="exam_take_overview",
|
||||
),
|
||||
path("exam/<int:pk>/", views.GenericExamViews.exam_overview, name="exam_overview"),
|
||||
path(
|
||||
"exam/<int:pk>/scores",
|
||||
views.GenericExamViews.exam_scores_cid,
|
||||
name="exam_scores_cid",
|
||||
),
|
||||
path(
|
||||
"exam/<int:pk>/scores/<int:cid>/<str:passcode>/",
|
||||
views.exam_scores_cid_user,
|
||||
name="exam_scores_cid_user",
|
||||
),
|
||||
path(
|
||||
"exam/<int:pk>/toggle_active",
|
||||
views.GenericExamViews.exam_toggle_active,
|
||||
name="exam_toggle_active",
|
||||
),
|
||||
path(
|
||||
"exam/<int:pk>/toggle_results_published",
|
||||
views.GenericExamViews.exam_toggle_results_published,
|
||||
name="exam_toggle_results_published",
|
||||
),
|
||||
# path("exam/submit", views.GenericExamViews.postExamAnswers, name="exam_answers_submit"),
|
||||
path("exam/", views.GenericExamViews.exam_list, name="exam_list"),
|
||||
path("exam/all", views.GenericExamViews.exam_list_all, name="exam_list_all"),
|
||||
path("exam/<int:exam_id>/clone", views.ExamClone.as_view(), name="exam_clone"),
|
||||
path("exam/<int:exam_id>/cids", views.GenericExamViews.exam_cids, name="exam_cids"),
|
||||
path("exam/available", views.active_exams, name="active_exams"),
|
||||
path("exam/<int:pk>/update", views.ExamUpdate.as_view(), name="exam_update"),
|
||||
path("exam/<int:pk>/delete", views.ExamDelete.as_view(), name="exam_delete"),
|
||||
# path("exam/json/<int:pk>", views.exam_json, name="exam_json"),
|
||||
# path("exam/json/<int:pk>/recreate", views.exam_json_recreate, name="exam_json_recreate"),
|
||||
path(
|
||||
"user_answers/",
|
||||
views.UserAnswerTableView.as_view(),
|
||||
name="user_answer_table_view",
|
||||
),
|
||||
path(
|
||||
"user_answers/<int:pk>", views.UserAnswerView.as_view(), name="user_answer_view"
|
||||
),
|
||||
path(
|
||||
"user_answers/<int:pk>/delete",
|
||||
views.UserAnswerDelete.as_view(),
|
||||
name="user_answer_delete",
|
||||
),
|
||||
path(
|
||||
"user_answers/delete",
|
||||
views.GenericViews.user_answer_delete_multiple,
|
||||
name="user_answer_delete_multiple",
|
||||
),
|
||||
]
|
||||
from django.urls import path, include
|
||||
|
||||
from . import views
|
||||
from django.views.decorators.cache import cache_page
|
||||
|
||||
app_name = "sbas"
|
||||
|
||||
urlpatterns = [
|
||||
# path('', views.question_list, name='question_list'),
|
||||
path("", views.GenericExamViews.index, name="index"),
|
||||
path("question/", views.QuestionView.as_view(), name="question_view"),
|
||||
path("question/<int:pk>/", views.question_detail, name="question_detail"),
|
||||
# path("question/create/", views.QuestionCreate.as_view(), name="anatomy_question_create"),
|
||||
# path("question/<int:pk>/update", views.QuestionUpdate.as_view(), name="anatomy_question_update"),
|
||||
# path("exam/<int:pk>/<int:sk>/mark", views.mark, name="mark"),
|
||||
# path("exam/<int:pk>/mark", views.mark_overview, name="mark_overview"),
|
||||
path(
|
||||
"exam/<int:pk>/<int:sk>/<str:cid>/<str:passcode>/take",
|
||||
views.exam_take,
|
||||
name="exam_take",
|
||||
),
|
||||
path("exam/<int:pk>/start", views.exam_start, name="exam_start"),
|
||||
path(
|
||||
"exam/<int:pk>/<str:cid>/<str:passcode>/finish",
|
||||
views.exam_take_overview,
|
||||
name="exam_take_overview",
|
||||
),
|
||||
path("exam/<int:pk>/", views.GenericExamViews.exam_overview, name="exam_overview"),
|
||||
path(
|
||||
"exam/<int:pk>/scores",
|
||||
views.GenericExamViews.exam_scores_cid,
|
||||
name="exam_scores_cid",
|
||||
),
|
||||
path(
|
||||
"exam/<int:pk>/scores/<int:cid>/<str:passcode>/",
|
||||
views.exam_scores_cid_user,
|
||||
name="exam_scores_cid_user",
|
||||
),
|
||||
path(
|
||||
"exam/<int:pk>/toggle_active",
|
||||
views.GenericExamViews.exam_toggle_active,
|
||||
name="exam_toggle_active",
|
||||
),
|
||||
path(
|
||||
"exam/<int:pk>/toggle_results_published",
|
||||
views.GenericExamViews.exam_toggle_results_published,
|
||||
name="exam_toggle_results_published",
|
||||
),
|
||||
# path("exam/submit", views.GenericExamViews.postExamAnswers, name="exam_answers_submit"),
|
||||
path("exam/", views.GenericExamViews.exam_list, name="exam_list"),
|
||||
path("exam/all", views.GenericExamViews.exam_list_all, name="exam_list_all"),
|
||||
path("exam/<int:exam_id>/clone", views.ExamClone.as_view(), name="exam_clone"),
|
||||
path("exam/<int:exam_id>/cids", views.GenericExamViews.exam_cids, name="exam_cids"),
|
||||
path("exam/available", views.active_exams, name="active_exams"),
|
||||
path("exam/<int:pk>/update", views.ExamUpdate.as_view(), name="exam_update"),
|
||||
path("exam/<int:pk>/delete", views.ExamDelete.as_view(), name="exam_delete"),
|
||||
# path("exam/json/<int:pk>", views.exam_json, name="exam_json"),
|
||||
# path("exam/json/<int:pk>/recreate", views.exam_json_recreate, name="exam_json_recreate"),
|
||||
path(
|
||||
"user_answers/",
|
||||
views.UserAnswerTableView.as_view(),
|
||||
name="user_answer_table_view",
|
||||
),
|
||||
path(
|
||||
"user_answers/<int:pk>", views.UserAnswerView.as_view(), name="user_answer_view"
|
||||
),
|
||||
path(
|
||||
"user_answers/<int:pk>/delete",
|
||||
views.UserAnswerDelete.as_view(),
|
||||
name="user_answer_delete",
|
||||
),
|
||||
path(
|
||||
"user_answers/delete",
|
||||
views.GenericViews.user_answer_delete_multiple,
|
||||
name="user_answer_delete_multiple",
|
||||
),
|
||||
]
|
||||
|
||||
+410
-410
@@ -1,411 +1,411 @@
|
||||
from reversion.views import RevisionMixin
|
||||
from generic.mixins import SuperuserRequiredMixin
|
||||
from django.views.generic.detail import DetailView
|
||||
from generic.models import CidUser
|
||||
from sbas.forms import CidUserAnswerForm, ExamForm
|
||||
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.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 .models import Question, Category, Exam, CidUserAnswer
|
||||
|
||||
from django_tables2 import SingleTableView, SingleTableMixin
|
||||
from django_filters.views import FilterView
|
||||
|
||||
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 .tables import QuestionTable, UserAnswerTable
|
||||
from .filters import QuestionFilter, UserAnswerFilter
|
||||
|
||||
|
||||
class AuthorOrCheckerRequiredMixin(object):
|
||||
def get_object(self, *args, **kwargs):
|
||||
obj = super().get_object(*args, **kwargs)
|
||||
if self.request.user.groups.filter(name="sbas_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, "sbas/question_list.html", {"questions": questions})
|
||||
|
||||
|
||||
@login_required
|
||||
def question_detail(request, pk):
|
||||
question = get_object_or_404(Question, pk=pk)
|
||||
return render(request, "sbas/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, "sbas/available_exam_list.html", {"exams": active_exams})
|
||||
|
||||
|
||||
def exam_scores_cid_user(request, pk, cid, passcode):
|
||||
exam = get_object_or_404(Exam, pk=pk)
|
||||
|
||||
if 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
|
||||
user_answer = q.cid_user_answers.filter(cid=cid).first()
|
||||
|
||||
if user_answer is None:
|
||||
answer_score = 0
|
||||
ans = ""
|
||||
else:
|
||||
answer_score = user_answer.get_answer_score()
|
||||
ans = user_answer.get_answer()
|
||||
|
||||
correct_answer = q.get_correct_answer()
|
||||
|
||||
if not exam.publish_results and not view_all_results:
|
||||
correct_answer = "*****"
|
||||
answer_score = 0
|
||||
answers.append(ans)
|
||||
answers_marks.append(answer_score)
|
||||
|
||||
merged_ans = (ans, answer_score, correct_answer)
|
||||
answers_and_marks.append((q, *merged_ans))
|
||||
|
||||
total_score = sum(answers_marks)
|
||||
|
||||
max_score = len(questions)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"sbas/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_start(request, pk):
|
||||
exam = get_object_or_404(Exam, pk=pk)
|
||||
|
||||
if not exam.active:
|
||||
raise Http404("Exam not found")
|
||||
|
||||
return render(
|
||||
request,
|
||||
"sbas/exam_start.html",
|
||||
{
|
||||
"exam": exam,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def exam_take_overview(request, pk, cid, passcode):
|
||||
exam = get_object_or_404(Exam, pk=pk)
|
||||
|
||||
if not exam.active:
|
||||
raise Http404("Exam not found")
|
||||
|
||||
if not exam.check_cid_user(cid, passcode):
|
||||
raise Http404("Error accessing exam")
|
||||
|
||||
questions = exam.exam_questions.all()
|
||||
|
||||
answers = CidUserAnswer.objects.filter(cid=cid, 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:
|
||||
question_answer_tuples.append((q, answer_question_map[q]))
|
||||
answer_count += 1
|
||||
else:
|
||||
question_answer_tuples.append((q, None))
|
||||
|
||||
c = CidUser.objects.filter(cid=cid).first()
|
||||
cid_user_exam = exam.get_or_create_cid_user_exam(cid_user=c)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"sbas/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, passcode):
|
||||
|
||||
exam = get_object_or_404(Exam, pk=pk)
|
||||
|
||||
if not exam.active:
|
||||
raise Http404("Exam not found")
|
||||
|
||||
if not exam.check_cid_user(cid, passcode):
|
||||
raise Http404("Error accessing exam")
|
||||
|
||||
c = CidUser.objects.filter(cid=cid).first()
|
||||
cid_user_exam = exam.get_or_create_cid_user_exam(cid_user=c)
|
||||
|
||||
question = exam.exam_questions.all()[sk]
|
||||
|
||||
exam_length = len(exam.exam_questions.all())
|
||||
|
||||
pos = exam.get_question_index(question)
|
||||
|
||||
answer = question.cid_user_answers.filter(cid=cid, exam=exam).first()
|
||||
|
||||
if request.method == "POST":
|
||||
if answer:
|
||||
form = CidUserAnswerForm(request.POST, instance=answer)
|
||||
else:
|
||||
form = CidUserAnswerForm(request.POST)
|
||||
if form.is_valid():
|
||||
answer = form.save(commit=False)
|
||||
answer.cid = cid
|
||||
answer.question = question
|
||||
answer.exam = exam
|
||||
# answer.published_date = timezone.now()
|
||||
answer.save()
|
||||
|
||||
cid_user_exam.end_time = timezone.now()
|
||||
cid_user_exam.save()
|
||||
|
||||
kwargs = {"pk": pk, "cid": cid, "passcode": passcode}
|
||||
|
||||
if "next" in request.POST:
|
||||
return redirect("sbas:exam_take", sk=pos + 1, **kwargs)
|
||||
elif "previous" in request.POST:
|
||||
return redirect("sbas:exam_take", sk=pos - 1, **kwargs)
|
||||
elif "finish" in request.POST:
|
||||
return redirect("sbas:exam_take_overview", **kwargs)
|
||||
elif "goto" in request.POST:
|
||||
return redirect(
|
||||
"sbas:exam_take", sk=int(request.POST.get("goto")), **kwargs
|
||||
)
|
||||
else:
|
||||
form = CidUserAnswerForm(instance=answer)
|
||||
|
||||
previous = -1
|
||||
if sk > 0:
|
||||
previous = sk - 1
|
||||
next = sk + 1
|
||||
if sk == exam_length - 1:
|
||||
next = False
|
||||
|
||||
saved_answer = False
|
||||
if answer is not None:
|
||||
saved_answer = answer.answer
|
||||
|
||||
return render(
|
||||
request,
|
||||
"sbas/exam_take.html",
|
||||
{
|
||||
"form": form,
|
||||
"cid": cid,
|
||||
"exam": exam,
|
||||
"question": question,
|
||||
"next": next,
|
||||
"previous": previous,
|
||||
"exam_length": exam_length,
|
||||
"pos": pos,
|
||||
"saved_answer": saved_answer,
|
||||
"passcode": passcode,
|
||||
"cid_user_exam": cid_user_exam,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def loadJsonAnswer(answer):
|
||||
pass
|
||||
|
||||
|
||||
GenericExamViews = ExamViews(
|
||||
Exam, Question, None, CidUserAnswer, "sbas", "sbas", loadJsonAnswer
|
||||
)
|
||||
|
||||
|
||||
class QuestionView(LoginRequiredMixin, SingleTableMixin, FilterView):
|
||||
model = Question
|
||||
table_class = QuestionTable
|
||||
template_name = "sbas/question_view.html"
|
||||
|
||||
filterset_class = QuestionFilter
|
||||
|
||||
|
||||
class UserAnswerView(LoginRequiredMixin, DetailView):
|
||||
model = CidUserAnswer
|
||||
|
||||
|
||||
class UserAnswerTableView(LoginRequiredMixin, SingleTableMixin, FilterView):
|
||||
model = CidUserAnswer
|
||||
table_class = UserAnswerTable
|
||||
template_name = "sbas/user_answer_question_view.html"
|
||||
|
||||
filterset_class = UserAnswerFilter
|
||||
|
||||
|
||||
class UserAnswerDelete(SuperuserRequiredMixin, DeleteView):
|
||||
model = CidUserAnswer
|
||||
template_name = "user_answer_delete.html"
|
||||
success_url = reverse_lazy("sbas:user_answer_table_view")
|
||||
|
||||
|
||||
# @user_passes_test(lambda u: u.is_superuser)
|
||||
# def user_answer_delete_multiple(request):
|
||||
# if request.is_ajax():
|
||||
# print(request.POST["answer_ids"])
|
||||
# answer_ids = json.loads(request.POST["answer_ids"])
|
||||
#
|
||||
# # We could probably delete them all at once....
|
||||
# for id in answer_ids:
|
||||
# CidUserAnswer.objects.get(pk=id).delete()
|
||||
# return JsonResponse({"status": "success"})
|
||||
# return JsonResponse({"status": "error"})
|
||||
|
||||
|
||||
GenericViews = GenericViewBase("sbas", Question, CidUserAnswer, Exam)
|
||||
|
||||
|
||||
class QuestionDelete(AuthorOrCheckerRequiredMixin, DeleteView):
|
||||
model = Question
|
||||
success_url = reverse_lazy("sbas:question_list")
|
||||
|
||||
|
||||
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 ExamUpdate(
|
||||
RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateView
|
||||
):
|
||||
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 ExamViewSet(RevisionMixin, viewsets.ModelViewSet):
|
||||
# # queryset = Exam.objects.all().order_by('name')
|
||||
# serializer_class = ExamSerializer
|
||||
# permission_classes = [permissions.IsAuthenticated]
|
||||
#
|
||||
# def get_queryset(self):
|
||||
# """
|
||||
# This view should return a list exams available to a user.
|
||||
# """
|
||||
# user = self.request.user
|
||||
# if user.groups.filter(name="sbas_checker").exists():
|
||||
# return Exam.objects.all()
|
||||
#
|
||||
# return Exam.objects.filter(author__id=user.id)
|
||||
|
||||
|
||||
class ExamDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView):
|
||||
model = Exam
|
||||
template_name = "exam_confirm_delete.html"
|
||||
success_url = reverse_lazy("sbas:index")
|
||||
|
||||
|
||||
class ExamClone(ExamCloneMixin, ExamCreate):
|
||||
"""Clone exam view"""
|
||||
|
||||
class ExamDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView):
|
||||
model = Exam
|
||||
template_name = "exam_confirm_delete.html"
|
||||
success_url = reverse_lazy("sbas:index")
|
||||
|
||||
|
||||
class ExamUpdate(
|
||||
RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateView
|
||||
):
|
||||
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)
|
||||
from reversion.views import RevisionMixin
|
||||
from generic.mixins import SuperuserRequiredMixin
|
||||
from django.views.generic.detail import DetailView
|
||||
from generic.models import CidUser
|
||||
from sbas.forms import CidUserAnswerForm, ExamForm
|
||||
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.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 .models import Question, Category, Exam, CidUserAnswer
|
||||
|
||||
from django_tables2 import SingleTableView, SingleTableMixin
|
||||
from django_filters.views import FilterView
|
||||
|
||||
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 .tables import QuestionTable, UserAnswerTable
|
||||
from .filters import QuestionFilter, UserAnswerFilter
|
||||
|
||||
|
||||
class AuthorOrCheckerRequiredMixin(object):
|
||||
def get_object(self, *args, **kwargs):
|
||||
obj = super().get_object(*args, **kwargs)
|
||||
if self.request.user.groups.filter(name="sbas_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, "sbas/question_list.html", {"questions": questions})
|
||||
|
||||
|
||||
@login_required
|
||||
def question_detail(request, pk):
|
||||
question = get_object_or_404(Question, pk=pk)
|
||||
return render(request, "sbas/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, "sbas/available_exam_list.html", {"exams": active_exams})
|
||||
|
||||
|
||||
def exam_scores_cid_user(request, pk, cid, passcode):
|
||||
exam = get_object_or_404(Exam, pk=pk)
|
||||
|
||||
if 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
|
||||
user_answer = q.cid_user_answers.filter(cid=cid).first()
|
||||
|
||||
if user_answer is None:
|
||||
answer_score = 0
|
||||
ans = ""
|
||||
else:
|
||||
answer_score = user_answer.get_answer_score()
|
||||
ans = user_answer.get_answer()
|
||||
|
||||
correct_answer = q.get_correct_answer()
|
||||
|
||||
if not exam.publish_results and not view_all_results:
|
||||
correct_answer = "*****"
|
||||
answer_score = 0
|
||||
answers.append(ans)
|
||||
answers_marks.append(answer_score)
|
||||
|
||||
merged_ans = (ans, answer_score, correct_answer)
|
||||
answers_and_marks.append((q, *merged_ans))
|
||||
|
||||
total_score = sum(answers_marks)
|
||||
|
||||
max_score = len(questions)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"sbas/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_start(request, pk):
|
||||
exam = get_object_or_404(Exam, pk=pk)
|
||||
|
||||
if not exam.active:
|
||||
raise Http404("Exam not found")
|
||||
|
||||
return render(
|
||||
request,
|
||||
"sbas/exam_start.html",
|
||||
{
|
||||
"exam": exam,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def exam_take_overview(request, pk, cid, passcode):
|
||||
exam = get_object_or_404(Exam, pk=pk)
|
||||
|
||||
if not exam.active:
|
||||
raise Http404("Exam not found")
|
||||
|
||||
if not exam.check_cid_user(cid, passcode):
|
||||
raise Http404("Error accessing exam")
|
||||
|
||||
questions = exam.exam_questions.all()
|
||||
|
||||
answers = CidUserAnswer.objects.filter(cid=cid, 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:
|
||||
question_answer_tuples.append((q, answer_question_map[q]))
|
||||
answer_count += 1
|
||||
else:
|
||||
question_answer_tuples.append((q, None))
|
||||
|
||||
c = CidUser.objects.filter(cid=cid).first()
|
||||
cid_user_exam = exam.get_or_create_cid_user_exam(cid_user=c)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"sbas/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, passcode):
|
||||
|
||||
exam = get_object_or_404(Exam, pk=pk)
|
||||
|
||||
if not exam.active:
|
||||
raise Http404("Exam not found")
|
||||
|
||||
if not exam.check_cid_user(cid, passcode):
|
||||
raise Http404("Error accessing exam")
|
||||
|
||||
c = CidUser.objects.filter(cid=cid).first()
|
||||
cid_user_exam = exam.get_or_create_cid_user_exam(cid_user=c)
|
||||
|
||||
question = exam.exam_questions.all()[sk]
|
||||
|
||||
exam_length = len(exam.exam_questions.all())
|
||||
|
||||
pos = exam.get_question_index(question)
|
||||
|
||||
answer = question.cid_user_answers.filter(cid=cid, exam=exam).first()
|
||||
|
||||
if request.method == "POST":
|
||||
if answer:
|
||||
form = CidUserAnswerForm(request.POST, instance=answer)
|
||||
else:
|
||||
form = CidUserAnswerForm(request.POST)
|
||||
if form.is_valid():
|
||||
answer = form.save(commit=False)
|
||||
answer.cid = cid
|
||||
answer.question = question
|
||||
answer.exam = exam
|
||||
# answer.published_date = timezone.now()
|
||||
answer.save()
|
||||
|
||||
cid_user_exam.end_time = timezone.now()
|
||||
cid_user_exam.save()
|
||||
|
||||
kwargs = {"pk": pk, "cid": cid, "passcode": passcode}
|
||||
|
||||
if "next" in request.POST:
|
||||
return redirect("sbas:exam_take", sk=pos + 1, **kwargs)
|
||||
elif "previous" in request.POST:
|
||||
return redirect("sbas:exam_take", sk=pos - 1, **kwargs)
|
||||
elif "finish" in request.POST:
|
||||
return redirect("sbas:exam_take_overview", **kwargs)
|
||||
elif "goto" in request.POST:
|
||||
return redirect(
|
||||
"sbas:exam_take", sk=int(request.POST.get("goto")), **kwargs
|
||||
)
|
||||
else:
|
||||
form = CidUserAnswerForm(instance=answer)
|
||||
|
||||
previous = -1
|
||||
if sk > 0:
|
||||
previous = sk - 1
|
||||
next = sk + 1
|
||||
if sk == exam_length - 1:
|
||||
next = False
|
||||
|
||||
saved_answer = False
|
||||
if answer is not None:
|
||||
saved_answer = answer.answer
|
||||
|
||||
return render(
|
||||
request,
|
||||
"sbas/exam_take.html",
|
||||
{
|
||||
"form": form,
|
||||
"cid": cid,
|
||||
"exam": exam,
|
||||
"question": question,
|
||||
"next": next,
|
||||
"previous": previous,
|
||||
"exam_length": exam_length,
|
||||
"pos": pos,
|
||||
"saved_answer": saved_answer,
|
||||
"passcode": passcode,
|
||||
"cid_user_exam": cid_user_exam,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def loadJsonAnswer(answer):
|
||||
pass
|
||||
|
||||
|
||||
GenericExamViews = ExamViews(
|
||||
Exam, Question, None, CidUserAnswer, "sbas", "sbas", loadJsonAnswer
|
||||
)
|
||||
|
||||
|
||||
class QuestionView(LoginRequiredMixin, SingleTableMixin, FilterView):
|
||||
model = Question
|
||||
table_class = QuestionTable
|
||||
template_name = "sbas/question_view.html"
|
||||
|
||||
filterset_class = QuestionFilter
|
||||
|
||||
|
||||
class UserAnswerView(LoginRequiredMixin, DetailView):
|
||||
model = CidUserAnswer
|
||||
|
||||
|
||||
class UserAnswerTableView(LoginRequiredMixin, SingleTableMixin, FilterView):
|
||||
model = CidUserAnswer
|
||||
table_class = UserAnswerTable
|
||||
template_name = "sbas/user_answer_question_view.html"
|
||||
|
||||
filterset_class = UserAnswerFilter
|
||||
|
||||
|
||||
class UserAnswerDelete(SuperuserRequiredMixin, DeleteView):
|
||||
model = CidUserAnswer
|
||||
template_name = "user_answer_delete.html"
|
||||
success_url = reverse_lazy("sbas:user_answer_table_view")
|
||||
|
||||
|
||||
# @user_passes_test(lambda u: u.is_superuser)
|
||||
# def user_answer_delete_multiple(request):
|
||||
# if request.is_ajax():
|
||||
# print(request.POST["answer_ids"])
|
||||
# answer_ids = json.loads(request.POST["answer_ids"])
|
||||
#
|
||||
# # We could probably delete them all at once....
|
||||
# for id in answer_ids:
|
||||
# CidUserAnswer.objects.get(pk=id).delete()
|
||||
# return JsonResponse({"status": "success"})
|
||||
# return JsonResponse({"status": "error"})
|
||||
|
||||
|
||||
GenericViews = GenericViewBase("sbas", Question, CidUserAnswer, Exam)
|
||||
|
||||
|
||||
class QuestionDelete(AuthorOrCheckerRequiredMixin, DeleteView):
|
||||
model = Question
|
||||
success_url = reverse_lazy("sbas:question_list")
|
||||
|
||||
|
||||
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 ExamUpdate(
|
||||
RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateView
|
||||
):
|
||||
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 ExamViewSet(RevisionMixin, viewsets.ModelViewSet):
|
||||
# # queryset = Exam.objects.all().order_by('name')
|
||||
# serializer_class = ExamSerializer
|
||||
# permission_classes = [permissions.IsAuthenticated]
|
||||
#
|
||||
# def get_queryset(self):
|
||||
# """
|
||||
# This view should return a list exams available to a user.
|
||||
# """
|
||||
# user = self.request.user
|
||||
# if user.groups.filter(name="sbas_checker").exists():
|
||||
# return Exam.objects.all()
|
||||
#
|
||||
# return Exam.objects.filter(author__id=user.id)
|
||||
|
||||
|
||||
class ExamDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView):
|
||||
model = Exam
|
||||
template_name = "exam_confirm_delete.html"
|
||||
success_url = reverse_lazy("sbas:index")
|
||||
|
||||
|
||||
class ExamClone(ExamCloneMixin, ExamCreate):
|
||||
"""Clone exam view"""
|
||||
|
||||
class ExamDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView):
|
||||
model = Exam
|
||||
template_name = "exam_confirm_delete.html"
|
||||
success_url = reverse_lazy("sbas:index")
|
||||
|
||||
|
||||
class ExamUpdate(
|
||||
RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateView
|
||||
):
|
||||
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)
|
||||
Reference in New Issue
Block a user