.
This commit is contained in:
+2
-1
@@ -48,7 +48,7 @@ from .models import (
|
|||||||
# HalfMarkAnswers,
|
# HalfMarkAnswers,
|
||||||
# IncorrectAnswers,
|
# IncorrectAnswers,
|
||||||
)
|
)
|
||||||
from generic.models import Examination
|
from generic.models import CidUser, Examination
|
||||||
from generic.views import ExamCloneMixin, ExamViews, GenericViewBase
|
from generic.views import ExamCloneMixin, ExamViews, GenericViewBase
|
||||||
from reversion.views import RevisionMixin
|
from reversion.views import RevisionMixin
|
||||||
|
|
||||||
@@ -243,6 +243,7 @@ def loadJsonAnswer(answer):
|
|||||||
|
|
||||||
exam = get_object_or_404(Exam, pk=eid)
|
exam = get_object_or_404(Exam, pk=eid)
|
||||||
|
|
||||||
|
|
||||||
if not exam.check_cid_user(answer["cid"], answer["passcode"]):
|
if not exam.check_cid_user(answer["cid"], answer["passcode"]):
|
||||||
return False, JsonResponse(
|
return False, JsonResponse(
|
||||||
{"success": False, "error": "invalid cid / passcode"}
|
{"success": False, "error": "invalid cid / passcode"}
|
||||||
|
|||||||
+38
-3
@@ -1,6 +1,7 @@
|
|||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
|
from django.utils import timezone
|
||||||
from smtplib import SMTPException
|
from smtplib import SMTPException
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@@ -13,7 +14,7 @@ import tagulous.models
|
|||||||
from sortedm2m.fields import SortedManyToManyField
|
from sortedm2m.fields import SortedManyToManyField
|
||||||
from django.conf import settings
|
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.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
from django.core.mail import send_mail
|
from django.core.mail import send_mail
|
||||||
@@ -114,6 +115,8 @@ class ExamBase(models.Model):
|
|||||||
|
|
||||||
stats_graph = models.TextField(default=0)
|
stats_graph = models.TextField(default=0)
|
||||||
|
|
||||||
|
notes = GenericRelation("generic.CidUserExam")
|
||||||
|
|
||||||
# time_limit = models.IntegerField(
|
# time_limit = models.IntegerField(
|
||||||
# help_text="Exam time limit (in seconds). Default is 2100 secondse (35 minutes)",
|
# help_text="Exam time limit (in seconds). Default is 2100 secondse (35 minutes)",
|
||||||
# default=2100,
|
# default=2100,
|
||||||
@@ -163,6 +166,38 @@ class ExamBase(models.Model):
|
|||||||
|
|
||||||
return True
|
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):
|
class NoteType(models.Model):
|
||||||
note_type = models.CharField(max_length=200)
|
note_type = models.CharField(max_length=200)
|
||||||
@@ -248,7 +283,7 @@ class CidUser(models.Model):
|
|||||||
return str(self.cid)
|
return str(self.cid)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('generic:manage_cids')
|
return reverse("generic:manage_cids")
|
||||||
|
|
||||||
def check_passcode(self, passcode) -> bool:
|
def check_passcode(self, passcode) -> bool:
|
||||||
return self.passcode == passcode
|
return self.passcode == passcode
|
||||||
@@ -332,7 +367,7 @@ class CidUser(models.Model):
|
|||||||
class CidUserExam(models.Model):
|
class CidUserExam(models.Model):
|
||||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
||||||
object_id = models.PositiveIntegerField()
|
object_id = models.PositiveIntegerField()
|
||||||
question = GenericForeignKey("content_type", "object_id")
|
exam = GenericForeignKey("content_type", "object_id")
|
||||||
|
|
||||||
start_time = models.DateTimeField(blank=True, null=True)
|
start_time = models.DateTimeField(blank=True, null=True)
|
||||||
end_time = models.DateTimeField(blank=True, null=True)
|
end_time = models.DateTimeField(blank=True, null=True)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from django.forms.models import model_to_dict
|
|||||||
from django.shortcuts import render, get_object_or_404, redirect
|
from django.shortcuts import render, get_object_or_404, redirect
|
||||||
|
|
||||||
from django.contrib.auth.decorators import login_required, user_passes_test
|
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.views.decorators.csrf import csrf_exempt
|
||||||
from django.core.exceptions import PermissionDenied, FieldError
|
from django.core.exceptions import PermissionDenied, FieldError
|
||||||
|
|
||||||
@@ -850,6 +851,18 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
# print(UserAnswer.objects.filter(exam__id=q["eid"]))
|
# print(UserAnswer.objects.filter(exam__id=q["eid"]))
|
||||||
# print(request.urlencode())
|
# 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": True, "question_count": n})
|
||||||
return JsonResponse({"success": False, "error": "Invalid data"})
|
return JsonResponse({"success": False, "error": "Invalid data"})
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,28 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
CID: {{cid}}
|
CID: {{cid}}
|
||||||
|
<div id="timeup">
|
||||||
|
Allocated time over.
|
||||||
|
</div>
|
||||||
|
<div id="clockdiv">
|
||||||
|
Time remaining:
|
||||||
|
{% comment %} <div>
|
||||||
|
<span class="days"></span>
|
||||||
|
<div class="smalltext">Days</div>
|
||||||
|
</div> {% endcomment %}
|
||||||
|
<div>
|
||||||
|
<span class="hours num"></span>
|
||||||
|
<span class="smalltext">Hours</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="minutes num"></span>
|
||||||
|
<span class="smalltext">Minutes</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="seconds num"></span>
|
||||||
|
<span class="smalltext">Seconds</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="no-select">
|
<div class="no-select">
|
||||||
<h2>{{exam.name}}: Question [{{pos|add:1}}/{{exam_length}}]</h2>
|
<h2>{{exam.name}}: Question [{{pos|add:1}}/{{exam_length}}]</h2>
|
||||||
{% if exam.publish_results %}
|
{% if exam.publish_results %}
|
||||||
@@ -100,6 +122,13 @@
|
|||||||
// $(el).addClass("selected")
|
// $(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 %}
|
{% else %}
|
||||||
$(`ol.physics-answer-list input`).each((n, el) => { el.disabled = true; });
|
$(`ol.physics-answer-list input`).each((n, el) => { el.disabled = true; });
|
||||||
{{question.get_answers_js}}.forEach((el, n) => {
|
{{question.get_answers_js}}.forEach((el, n) => {
|
||||||
@@ -134,7 +163,54 @@
|
|||||||
$("#goto-button").click();
|
$("#goto-button").click();
|
||||||
});
|
});
|
||||||
/* beautify ignore:end */
|
/* 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block css %}
|
{% block css %}
|
||||||
@@ -160,13 +236,13 @@
|
|||||||
|
|
||||||
.answer-true::after {
|
.answer-true::after {
|
||||||
content: "[Correct answer: True]";
|
content: "[Correct answer: True]";
|
||||||
color: purple;
|
color: darkgray;
|
||||||
|
|
||||||
}
|
}
|
||||||
.answer-false::after {
|
.answer-false::after {
|
||||||
content: "[Correct answer: False]";
|
content: "[Correct answer: False]";
|
||||||
color: purple;
|
color: darkgray;
|
||||||
|
|
||||||
}
|
}
|
||||||
.answer-correct {
|
.answer-correct {
|
||||||
border: 1px dotted darkgreen;
|
border: 1px dotted darkgreen;
|
||||||
@@ -183,5 +259,36 @@
|
|||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
content: "False"
|
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;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
+2
@@ -18,6 +18,8 @@
|
|||||||
<a href="{% url 'physics:exam_take' pk=exam.id sk=forloop.counter0 cid=cid passcode=passcode %}"><button {% if not answer %}class="unanswered"{% endif %}>{{forloop.counter}}: {{answer.answer}}</button></a>
|
<a href="{% url 'physics:exam_take' pk=exam.id sk=forloop.counter0 cid=cid passcode=passcode %}"><button {% if not answer %}class="unanswered"{% endif %}>{{forloop.counter}}: {{answer.answer}}</button></a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
Start time: {{cid_user_exam.start_time}}<br/>
|
||||||
|
Last change time: {{cid_user_exam.end_time}}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -3,5 +3,7 @@
|
|||||||
{% block navigation %}
|
{% block navigation %}
|
||||||
{{block.super}}
|
{{block.super}}
|
||||||
<br/>
|
<br/>
|
||||||
Exams: {{exam.name}}-> <a href="{% url 'physics:exam_overview' pk=exam.pk %}">Overview</a> / <a href="{% url 'physics:exam_scores_cid' pk=exam.pk %}">Scores</a>
|
Exams: {{exam.name}}-> <a href="{% url 'physics:exam_overview' pk=exam.pk %}">Overview</a> /
|
||||||
|
<a href="{% url 'physics:exam_scores_cid' pk=exam.pk %}">Scores</a> /
|
||||||
|
<a href="{% url 'physics:exam_cids' exam_id=exam.pk %}">Candidates</a>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
+7
-3
@@ -22,11 +22,15 @@ urlpatterns = [
|
|||||||
path("exam/<int:pk>/start", views.exam_start, name="exam_start"),
|
path("exam/<int:pk>/start", views.exam_start, name="exam_start"),
|
||||||
path(
|
path(
|
||||||
"exam/<int:pk>/<str:cid>/<str:passcode>/finish",
|
"exam/<int:pk>/<str:cid>/<str:passcode>/finish",
|
||||||
views.exam_finish,
|
views.exam_take_overview,
|
||||||
name="exam_finish",
|
name="exam_take_overview",
|
||||||
),
|
),
|
||||||
path("exam/<int:pk>/", views.GenericExamViews.exam_overview, name="exam_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",
|
||||||
|
views.GenericExamViews.exam_scores_cid,
|
||||||
|
name="exam_scores_cid",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"exam/<int:pk>/scores/<int:cid>/<str:passcode>/",
|
"exam/<int:pk>/scores/<int:cid>/<str:passcode>/",
|
||||||
views.exam_scores_cid_user,
|
views.exam_scores_cid_user,
|
||||||
|
|||||||
+19
-5
@@ -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.decorators import user_is_author_or_physics_checker
|
||||||
from physics.filters import QuestionFilter, UserAnswerFilter
|
from physics.filters import QuestionFilter, UserAnswerFilter
|
||||||
from generic.mixins import SuperuserRequiredMixin
|
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)
|
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):
|
if not exam.check_cid_user(cid, passcode):
|
||||||
raise Http404("Error accessing exam")
|
raise Http404("Error accessing exam")
|
||||||
|
|
||||||
@@ -202,12 +208,12 @@ def exam_finish(request, pk, cid, passcode):
|
|||||||
else:
|
else:
|
||||||
question_answer_tuples.append((q, None))
|
question_answer_tuples.append((q, None))
|
||||||
|
|
||||||
if not exam.active:
|
c = CidUser.objects.filter(cid=cid).first()
|
||||||
raise Http404("Exam not found")
|
cid_user_exam = exam.get_or_create_cid_user_exam(cid_user=c)
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
"physics/exam_finish.html",
|
"physics/exam_take_overview.html",
|
||||||
{
|
{
|
||||||
"exam": exam,
|
"exam": exam,
|
||||||
"cid": cid,
|
"cid": cid,
|
||||||
@@ -215,6 +221,7 @@ def exam_finish(request, pk, cid, passcode):
|
|||||||
"answer_count": answer_count,
|
"answer_count": answer_count,
|
||||||
"exam_length": len(questions),
|
"exam_length": len(questions),
|
||||||
"passcode": passcode,
|
"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):
|
if not exam.check_cid_user(cid, passcode):
|
||||||
raise Http404("Error accessing exam")
|
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]
|
question = exam.exam_questions.all()[sk]
|
||||||
|
|
||||||
exam_length = len(exam.exam_questions.all())
|
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.published_date = timezone.now()
|
||||||
answer.save()
|
answer.save()
|
||||||
|
|
||||||
|
cid_user_exam.end_time = timezone.now()
|
||||||
|
cid_user_exam.save()
|
||||||
|
|
||||||
kwargs = {"pk": pk, "cid": cid, "passcode": passcode}
|
kwargs = {"pk": pk, "cid": cid, "passcode": passcode}
|
||||||
take_url = "physics:exam_take"
|
take_url = "physics:exam_take"
|
||||||
finish_url = "physics:exam_finish"
|
finish_url = "physics:exam_take_overview"
|
||||||
|
|
||||||
if "next" in request.POST:
|
if "next" in request.POST:
|
||||||
return redirect(take_url, sk=pos + 1, **kwargs)
|
return redirect(take_url, sk=pos + 1, **kwargs)
|
||||||
@@ -289,6 +302,7 @@ def exam_take(request, pk, sk, cid, passcode):
|
|||||||
"exam_length": exam_length,
|
"exam_length": exam_length,
|
||||||
"pos": pos,
|
"pos": pos,
|
||||||
"passcode": passcode,
|
"passcode": passcode,
|
||||||
|
"cid_user_exam": cid_user_exam,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
Start time: {{cid_user_exam.start_time}}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
+2
-2
@@ -22,8 +22,8 @@ urlpatterns = [
|
|||||||
path("exam/<int:pk>/start", views.exam_start, name="exam_start"),
|
path("exam/<int:pk>/start", views.exam_start, name="exam_start"),
|
||||||
path(
|
path(
|
||||||
"exam/<int:pk>/<str:cid>/<str:passcode>/finish",
|
"exam/<int:pk>/<str:cid>/<str:passcode>/finish",
|
||||||
views.exam_finish,
|
views.exam_take_overview,
|
||||||
name="exam_finish",
|
name="exam_take_overview",
|
||||||
),
|
),
|
||||||
path("exam/<int:pk>/", views.GenericExamViews.exam_overview, name="exam_overview"),
|
path("exam/<int:pk>/", views.GenericExamViews.exam_overview, name="exam_overview"),
|
||||||
path(
|
path(
|
||||||
|
|||||||
+20
-6
@@ -1,6 +1,7 @@
|
|||||||
from reversion.views import RevisionMixin
|
from reversion.views import RevisionMixin
|
||||||
from generic.mixins import SuperuserRequiredMixin
|
from generic.mixins import SuperuserRequiredMixin
|
||||||
from django.views.generic.detail import DetailView
|
from django.views.generic.detail import DetailView
|
||||||
|
from generic.models import CidUser
|
||||||
from sbas.forms import CidUserAnswerForm, ExamForm
|
from sbas.forms import CidUserAnswerForm, ExamForm
|
||||||
from django.shortcuts import render, get_object_or_404, redirect
|
from django.shortcuts import render, get_object_or_404, redirect
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
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)
|
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):
|
if not exam.check_cid_user(cid, passcode):
|
||||||
raise Http404("Error accessing exam")
|
raise Http404("Error accessing exam")
|
||||||
|
|
||||||
@@ -179,12 +183,12 @@ def exam_finish(request, pk, cid, passcode):
|
|||||||
else:
|
else:
|
||||||
question_answer_tuples.append((q, None))
|
question_answer_tuples.append((q, None))
|
||||||
|
|
||||||
if not exam.active:
|
c = CidUser.objects.filter(cid=cid).first()
|
||||||
raise Http404("Exam not found")
|
cid_user_exam = exam.get_or_create_cid_user_exam(cid_user=c)
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
"sbas/exam_finish.html",
|
"sbas/exam_take_overview.html",
|
||||||
{
|
{
|
||||||
"exam": exam,
|
"exam": exam,
|
||||||
"cid": cid,
|
"cid": cid,
|
||||||
@@ -192,6 +196,7 @@ def exam_finish(request, pk, cid, passcode):
|
|||||||
"answer_count": answer_count,
|
"answer_count": answer_count,
|
||||||
"exam_length": len(questions),
|
"exam_length": len(questions),
|
||||||
"passcode": passcode,
|
"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):
|
if not exam.check_cid_user(cid, passcode):
|
||||||
raise Http404("Error accessing exam")
|
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]
|
question = exam.exam_questions.all()[sk]
|
||||||
|
|
||||||
exam_length = len(exam.exam_questions.all())
|
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.published_date = timezone.now()
|
||||||
answer.save()
|
answer.save()
|
||||||
|
|
||||||
|
cid_user_exam.end_time = timezone.now()
|
||||||
|
cid_user_exam.save()
|
||||||
|
|
||||||
kwargs = {"pk": pk, "cid": cid, "passcode": passcode}
|
kwargs = {"pk": pk, "cid": cid, "passcode": passcode}
|
||||||
|
|
||||||
if "next" in request.POST:
|
if "next" in request.POST:
|
||||||
@@ -234,7 +245,7 @@ def exam_take(request, pk, sk, cid, passcode):
|
|||||||
elif "previous" in request.POST:
|
elif "previous" in request.POST:
|
||||||
return redirect("sbas:exam_take", sk=pos - 1, **kwargs)
|
return redirect("sbas:exam_take", sk=pos - 1, **kwargs)
|
||||||
elif "finish" in request.POST:
|
elif "finish" in request.POST:
|
||||||
return redirect("sbas:exam_finish", **kwargs)
|
return redirect("sbas:exam_take_overview", **kwargs)
|
||||||
elif "goto" in request.POST:
|
elif "goto" in request.POST:
|
||||||
return redirect(
|
return redirect(
|
||||||
"sbas:exam_take", sk=int(request.POST.get("goto")), **kwargs
|
"sbas:exam_take", sk=int(request.POST.get("goto")), **kwargs
|
||||||
@@ -267,6 +278,7 @@ def exam_take(request, pk, sk, cid, passcode):
|
|||||||
"pos": pos,
|
"pos": pos,
|
||||||
"saved_answer": saved_answer,
|
"saved_answer": saved_answer,
|
||||||
"passcode": passcode,
|
"passcode": passcode,
|
||||||
|
"cid_user_exam": cid_user_exam,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -321,6 +333,7 @@ class UserAnswerDelete(SuperuserRequiredMixin, DeleteView):
|
|||||||
|
|
||||||
GenericViews = GenericViewBase("sbas", Question, CidUserAnswer, Exam)
|
GenericViews = GenericViewBase("sbas", Question, CidUserAnswer, Exam)
|
||||||
|
|
||||||
|
|
||||||
class QuestionDelete(AuthorOrCheckerRequiredMixin, DeleteView):
|
class QuestionDelete(AuthorOrCheckerRequiredMixin, DeleteView):
|
||||||
model = Question
|
model = Question
|
||||||
success_url = reverse_lazy("sbas:question_list")
|
success_url = reverse_lazy("sbas:question_list")
|
||||||
@@ -352,7 +365,7 @@ class ExamUpdate(
|
|||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
#class ExamViewSet(RevisionMixin, viewsets.ModelViewSet):
|
# class ExamViewSet(RevisionMixin, viewsets.ModelViewSet):
|
||||||
# # queryset = Exam.objects.all().order_by('name')
|
# # queryset = Exam.objects.all().order_by('name')
|
||||||
# serializer_class = ExamSerializer
|
# serializer_class = ExamSerializer
|
||||||
# permission_classes = [permissions.IsAuthenticated]
|
# permission_classes = [permissions.IsAuthenticated]
|
||||||
@@ -373,5 +386,6 @@ class ExamDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView):
|
|||||||
template_name = "exam_confirm_delete.html"
|
template_name = "exam_confirm_delete.html"
|
||||||
success_url = reverse_lazy("sbas:index")
|
success_url = reverse_lazy("sbas:index")
|
||||||
|
|
||||||
|
|
||||||
class ExamClone(ExamCloneMixin, ExamCreate):
|
class ExamClone(ExamCloneMixin, ExamCreate):
|
||||||
"""Clone exam view"""
|
"""Clone exam view"""
|
||||||
|
|||||||
Reference in New Issue
Block a user