From 6214be993915d15422c6d37bc9f739835a3de73b Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 20 Dec 2021 19:16:17 +0000 Subject: [PATCH] . --- anatomy/views.py | 3 +- generic/models.py | 41 ++++++- generic/views.py | 13 ++ physics/templates/physics/exam_take.html | 115 +++++++++++++++++- ...am_finish.html => exam_take_overview.html} | 2 + physics/templates/physics/exams.html | 4 +- physics/urls.py | 10 +- physics/views.py | 24 +++- ...am_finish.html => exam_take_overview.html} | 2 + sbas/urls.py | 4 +- sbas/views.py | 26 +++- 11 files changed, 219 insertions(+), 25 deletions(-) rename physics/templates/physics/{exam_finish.html => exam_take_overview.html} (91%) rename sbas/templates/sbas/{exam_finish.html => exam_take_overview.html} (93%) diff --git a/anatomy/views.py b/anatomy/views.py index 3ac27502..7a02a7ec 100644 --- a/anatomy/views.py +++ b/anatomy/views.py @@ -48,7 +48,7 @@ from .models import ( # HalfMarkAnswers, # IncorrectAnswers, ) -from generic.models import Examination +from generic.models import CidUser, Examination from generic.views import ExamCloneMixin, ExamViews, GenericViewBase from reversion.views import RevisionMixin @@ -243,6 +243,7 @@ def loadJsonAnswer(answer): exam = get_object_or_404(Exam, pk=eid) + if not exam.check_cid_user(answer["cid"], answer["passcode"]): return False, JsonResponse( {"success": False, "error": "invalid cid / passcode"} diff --git a/generic/models.py b/generic/models.py index 344120a1..03912fbf 100644 --- a/generic/models.py +++ b/generic/models.py @@ -1,6 +1,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin from django.db import models from django.http import HttpRequest +from django.utils import timezone from smtplib import SMTPException from django.urls import reverse @@ -13,7 +14,7 @@ import tagulous.models from sortedm2m.fields import SortedManyToManyField from django.conf import settings -from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation from django.contrib.contenttypes.models import ContentType from django.core.mail import send_mail @@ -114,6 +115,8 @@ class ExamBase(models.Model): stats_graph = models.TextField(default=0) + notes = GenericRelation("generic.CidUserExam") + # time_limit = models.IntegerField( # help_text="Exam time limit (in seconds). Default is 2100 secondse (35 minutes)", # default=2100, @@ -163,6 +166,38 @@ class ExamBase(models.Model): return True + def get_or_create_cid_user_exam(self, cid_user, start_time=None): + content_type = ContentType.objects.get_for_model(self) + c = CidUserExam.objects.filter( + content_type=content_type, object_id=self.pk, cid_user=cid_user + ).first() + print(c) + if c: + return c + + if start_time is None: + start_time = timezone.now() + new = CidUserExam( + content_type=content_type, + object_id=self.pk, + cid_user=cid_user, + start_time=start_time, + ) + new.save() + return new + + def get_cid_user_exams(self, cid_user=None): + content_type = ContentType.objects.get_for_model(self) + if cid_user is None: + return CidUserExam.objects.filter( + content_type=content_type, + object_id=self.pk, + ) + else: + return CidUserExam.objects.filter( + content_type=content_type, object_id=self.pk, cid_user=cid_user + ) + class NoteType(models.Model): note_type = models.CharField(max_length=200) @@ -248,7 +283,7 @@ class CidUser(models.Model): return str(self.cid) def get_absolute_url(self): - return reverse('generic:manage_cids') + return reverse("generic:manage_cids") def check_passcode(self, passcode) -> bool: return self.passcode == passcode @@ -332,7 +367,7 @@ class CidUser(models.Model): class CidUserExam(models.Model): content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) object_id = models.PositiveIntegerField() - question = GenericForeignKey("content_type", "object_id") + exam = GenericForeignKey("content_type", "object_id") start_time = models.DateTimeField(blank=True, null=True) end_time = models.DateTimeField(blank=True, null=True) diff --git a/generic/views.py b/generic/views.py index 58b0eb1f..a43e3046 100644 --- a/generic/views.py +++ b/generic/views.py @@ -7,6 +7,7 @@ from django.forms.models import model_to_dict from django.shortcuts import render, get_object_or_404, redirect from django.contrib.auth.decorators import login_required, user_passes_test +from django.utils import timezone from django.views.decorators.csrf import csrf_exempt from django.core.exceptions import PermissionDenied, FieldError @@ -850,6 +851,18 @@ class ExamViews(View, LoginRequiredMixin): # print(UserAnswer.objects.filter(exam__id=q["eid"])) # print(request.urlencode()) + exam_type, exam_id = request.POST.get("eid").split("/") + + c = CidUser.objects.filter(cid=request.POST.get("cid")).first() + exam = self.Exam.objects.get(pk=exam_id) + t = timezone.make_aware(datetime.fromtimestamp(float(request.POST.get("start_time")))) + cid_user_exam = exam.get_or_create_cid_user_exam( + cid_user=c, start_time=t + ) + + cid_user_exam.end_time = timezone.now() + cid_user_exam.save() + return JsonResponse({"success": True, "question_count": n}) return JsonResponse({"success": False, "error": "Invalid data"}) diff --git a/physics/templates/physics/exam_take.html b/physics/templates/physics/exam_take.html index 800c0a2a..406474fe 100755 --- a/physics/templates/physics/exam_take.html +++ b/physics/templates/physics/exam_take.html @@ -2,6 +2,28 @@ {% block content %} CID: {{cid}} +
+ Allocated time over. +
+
+ Time remaining: + {% comment %}
+ +
Days
+
{% endcomment %} +
+ + Hours +
+
+ + Minutes +
+
+ + Seconds +
+

{{exam.name}}: Question [{{pos|add:1}}/{{exam_length}}]

{% if exam.publish_results %} @@ -100,6 +122,13 @@ // $(el).addClass("selected") // }) //}) + + let time_limit = '{{exam_time_limit}}' + if (time_limit != "None") { + let end_time = new Date({{cid_user_exam.start_time|date:"U"}}*1000+{{exam.time_limit}}*1000); + initializeClock("clockdiv", end_time); + } + {% else %} $(`ol.physics-answer-list input`).each((n, el) => { el.disabled = true; }); {{question.get_answers_js}}.forEach((el, n) => { @@ -134,7 +163,54 @@ $("#goto-button").click(); }); /* beautify ignore:end */ + }) + + function getTimeRemaining(endtime) { + const total = Date.parse(endtime) - Date.parse(new Date()); + const seconds = Math.floor((total / 1000) % 60); + const minutes = Math.floor((total / 1000 / 60) % 60); + const hours = Math.floor((total / (1000 * 60 * 60)) % 24); + //const days = Math.floor(total / (1000 * 60 * 60 * 24)); + + return { + total, + //days, + hours, + minutes, + seconds + }; + } + + function initializeClock(id, endtime) { + const clock = document.getElementById(id); + clock.style.display = "block" + const daysSpan = clock.querySelector('.days'); + const hoursSpan = clock.querySelector('.hours'); + const minutesSpan = clock.querySelector('.minutes'); + const secondsSpan = clock.querySelector('.seconds'); + + function updateClock() { + const t = getTimeRemaining(endtime); + + //daysSpan.innerHTML = t.days; + hoursSpan.innerHTML = ('0' + t.hours).slice(-2); + minutesSpan.innerHTML = ('0' + t.minutes).slice(-2); + secondsSpan.innerHTML = ('0' + t.seconds).slice(-2); + + if (t.total <= 0) { + clearInterval(timeinterval); + timeup.style.display = "block" + clock.style.display = "none" + } + } + + const timeinterval = setInterval(updateClock, 1000); + updateClock(); + } + + + {% endblock %} {% block css %} @@ -160,13 +236,13 @@ .answer-true::after { content: "[Correct answer: True]"; - color: purple; - + color: darkgray; + } .answer-false::after { content: "[Correct answer: False]"; - color: purple; - + color: darkgray; + } .answer-correct { border: 1px dotted darkgreen; @@ -183,5 +259,36 @@ margin-left: 20px; content: "False" } + + #clockdiv{ + font-family: sans-serif; + display: inline-block; + font-weight: 100; + text-align: center; + font-size: 12px; + display: none; + color: darkgray; + } + + #clockdiv > div{ + padding: 1px; + border-radius: 3px; + {% comment %} background: purple; {% endcomment %} + display: inline-block; + } + #clockdiv div > span.num{ + color: white; + padding: 1px; + border-radius: 3px; + display: inline-block; + } + + #timeup { + display: none; + color: red; + font-weight: 100; + text-align: center; + font-size: 12px; + } {% endblock %} diff --git a/physics/templates/physics/exam_finish.html b/physics/templates/physics/exam_take_overview.html similarity index 91% rename from physics/templates/physics/exam_finish.html rename to physics/templates/physics/exam_take_overview.html index ffb8df5a..d2591700 100644 --- a/physics/templates/physics/exam_finish.html +++ b/physics/templates/physics/exam_take_overview.html @@ -18,6 +18,8 @@ {% endfor %}
+ Start time: {{cid_user_exam.start_time}}
+ Last change time: {{cid_user_exam.end_time}} {% endblock %} diff --git a/physics/templates/physics/exams.html b/physics/templates/physics/exams.html index 65b9c8e2..a5345c2b 100644 --- a/physics/templates/physics/exams.html +++ b/physics/templates/physics/exams.html @@ -3,5 +3,7 @@ {% block navigation %} {{block.super}}
-Exams: {{exam.name}}-> Overview / Scores +Exams: {{exam.name}}-> Overview / +Scores / +Candidates {% endblock %} \ No newline at end of file diff --git a/physics/urls.py b/physics/urls.py index 30115f69..6a5f2d02 100644 --- a/physics/urls.py +++ b/physics/urls.py @@ -22,11 +22,15 @@ urlpatterns = [ path("exam//start", views.exam_start, name="exam_start"), path( "exam////finish", - views.exam_finish, - name="exam_finish", + views.exam_take_overview, + name="exam_take_overview", ), path("exam//", views.GenericExamViews.exam_overview, name="exam_overview"), - path("exam//scores", views.GenericExamViews.exam_scores_cid, name="exam_scores_cid"), + path( + "exam//scores", + views.GenericExamViews.exam_scores_cid, + name="exam_scores_cid", + ), path( "exam//scores///", views.exam_scores_cid_user, diff --git a/physics/views.py b/physics/views.py index 1862f67c..b2bec5af 100644 --- a/physics/views.py +++ b/physics/views.py @@ -1,3 +1,6 @@ +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, UserAnswerFilter from generic.mixins import SuperuserRequiredMixin @@ -178,9 +181,12 @@ def exam_start(request, pk): ) -def exam_finish(request, pk, cid, passcode): +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") @@ -202,12 +208,12 @@ def exam_finish(request, pk, cid, passcode): else: question_answer_tuples.append((q, None)) - if not exam.active: - raise Http404("Exam not found") + c = CidUser.objects.filter(cid=cid).first() + cid_user_exam = exam.get_or_create_cid_user_exam(cid_user=c) return render( request, - "physics/exam_finish.html", + "physics/exam_take_overview.html", { "exam": exam, "cid": cid, @@ -215,6 +221,7 @@ def exam_finish(request, pk, cid, passcode): "answer_count": answer_count, "exam_length": len(questions), "passcode": passcode, + "cid_user_exam": cid_user_exam, }, ) @@ -229,6 +236,9 @@ def exam_take(request, pk, sk, cid, passcode): 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()) @@ -250,9 +260,12 @@ def exam_take(request, pk, sk, cid, passcode): # 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} take_url = "physics:exam_take" - finish_url = "physics:exam_finish" + finish_url = "physics:exam_take_overview" if "next" in request.POST: return redirect(take_url, sk=pos + 1, **kwargs) @@ -289,6 +302,7 @@ def exam_take(request, pk, sk, cid, passcode): "exam_length": exam_length, "pos": pos, "passcode": passcode, + "cid_user_exam": cid_user_exam, }, ) diff --git a/sbas/templates/sbas/exam_finish.html b/sbas/templates/sbas/exam_take_overview.html similarity index 93% rename from sbas/templates/sbas/exam_finish.html rename to sbas/templates/sbas/exam_take_overview.html index 4507ac95..9693043d 100644 --- a/sbas/templates/sbas/exam_finish.html +++ b/sbas/templates/sbas/exam_take_overview.html @@ -13,6 +13,8 @@ {% endfor %} + Start time: {{cid_user_exam.start_time}} + {% endblock %} {% block js %} diff --git a/sbas/urls.py b/sbas/urls.py index 555eed3c..ceb5615d 100644 --- a/sbas/urls.py +++ b/sbas/urls.py @@ -22,8 +22,8 @@ urlpatterns = [ path("exam//start", views.exam_start, name="exam_start"), path( "exam////finish", - views.exam_finish, - name="exam_finish", + views.exam_take_overview, + name="exam_take_overview", ), path("exam//", views.GenericExamViews.exam_overview, name="exam_overview"), path( diff --git a/sbas/views.py b/sbas/views.py index 8f2e1693..fe3589d5 100644 --- a/sbas/views.py +++ b/sbas/views.py @@ -1,6 +1,7 @@ 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 @@ -156,9 +157,12 @@ def exam_start(request, pk): ) -def exam_finish(request, pk, cid, passcode): +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") @@ -179,12 +183,12 @@ def exam_finish(request, pk, cid, passcode): else: question_answer_tuples.append((q, None)) - if not exam.active: - raise Http404("Exam not found") + 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_finish.html", + "sbas/exam_take_overview.html", { "exam": exam, "cid": cid, @@ -192,6 +196,7 @@ def exam_finish(request, pk, cid, passcode): "answer_count": answer_count, "exam_length": len(questions), "passcode": passcode, + "cid_user_exam": cid_user_exam, }, ) @@ -206,6 +211,9 @@ def exam_take(request, pk, sk, cid, passcode): 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()) @@ -227,6 +235,9 @@ def exam_take(request, pk, sk, cid, passcode): # 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: @@ -234,7 +245,7 @@ def exam_take(request, pk, sk, cid, passcode): elif "previous" in request.POST: return redirect("sbas:exam_take", sk=pos - 1, **kwargs) elif "finish" in request.POST: - return redirect("sbas:exam_finish", **kwargs) + return redirect("sbas:exam_take_overview", **kwargs) elif "goto" in request.POST: return redirect( "sbas:exam_take", sk=int(request.POST.get("goto")), **kwargs @@ -267,6 +278,7 @@ def exam_take(request, pk, sk, cid, passcode): "pos": pos, "saved_answer": saved_answer, "passcode": passcode, + "cid_user_exam": cid_user_exam, }, ) @@ -321,6 +333,7 @@ class UserAnswerDelete(SuperuserRequiredMixin, DeleteView): GenericViews = GenericViewBase("sbas", Question, CidUserAnswer, Exam) + class QuestionDelete(AuthorOrCheckerRequiredMixin, DeleteView): model = Question success_url = reverse_lazy("sbas:question_list") @@ -352,7 +365,7 @@ class ExamUpdate( return super().form_valid(form) -#class ExamViewSet(RevisionMixin, viewsets.ModelViewSet): +# class ExamViewSet(RevisionMixin, viewsets.ModelViewSet): # # queryset = Exam.objects.all().order_by('name') # serializer_class = ExamSerializer # permission_classes = [permissions.IsAuthenticated] @@ -373,5 +386,6 @@ class ExamDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView): template_name = "exam_confirm_delete.html" success_url = reverse_lazy("sbas:index") + class ExamClone(ExamCloneMixin, ExamCreate): """Clone exam view"""