improving physics / sba exams

This commit is contained in:
Ross
2023-12-11 13:57:02 +00:00
parent 66ce7b42ac
commit 472fe6ee96
13 changed files with 280 additions and 231 deletions
+46
View File
@@ -581,3 +581,49 @@ function delete_multiple(url, csrf_token) {
}
window.delete_multiple = delete_multiple
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();
}
window.initializeClock = initializeClock
+18 -1
View File
@@ -1,4 +1,5 @@
from collections import defaultdict
import datetime
import json
import os
from typing import Self, Tuple
@@ -712,6 +713,20 @@ class ExamBase(ExamCollectionGenericBase):
"{}:exam_json_cid".format(self.app_name), args=(self.pk, cid, passcode)
)
def get_time_limit(self):
"""Returns a human readable time limit"""
h, m, s = str(datetime.timedelta(seconds=self.time_limit)).split(":")
time_limit = ""
if s != "0":
time_limit = f" {s} second(s)"
if m != "0":
time_limit = f"{m} minute(s) {time_limit}"
if h != "0":
time_limit = f"{h} hour(s) {time_limit}"
return time_limit.strip()
def get_question_index(self, question):
return list(self.exam_questions.all()).index(question)
@@ -955,10 +970,12 @@ class CidUser(models.Model):
def check_passcode(self, passcode) -> bool:
return self.passcode == passcode
def get_cid_exams(self):
def get_cid_exams(self, include_case_collections=True):
available_exams = []
for n, t in EXAM_TYPES:
if not include_case_collections and n == "casecollection":
continue
exam_rel = getattr(self, t)
if exam_rel.exists():
exams = exam_rel.filter(exam_mode=True, archive=False).order_by("name")
@@ -11,6 +11,10 @@
<p><input id="cid-box" type="text" value="Candidate ID"></p>
<p><input id="passcode-box" type="text" value="Passcode"></p>
{% if exam.time_limit %}
This exam has a time limit of {{ exam.get_time_limit }}. The time will start when you click the Start Exam button below.<br/>
{% endif %}
<button>Start exam</button>
<script type="text/javascript">
+32 -104
View File
@@ -8,28 +8,7 @@
CID: {{cid}}
{% endif %}
</span>
<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>
{% include "exam_clock.html" %}
<div class="no-select">
<h2>{{exam.name}}: Question [<span id="question-number">{{pos|add:1}}</span>/<span id="exam-length">{{exam_length}}</span>]</h2>
{% if exam.publish_results %}
@@ -54,7 +33,11 @@
{{ form.a.errors }}
<label for="{{ form.a.id_for_label }}" class="flex-8 question-text">{{question.a|safe}}</label>
<span class="flex-1">{{ form.a }}<span class="postinput"></span></span>
</div>
{% if exam.publish_results and question.a_feedback %}
<span class="feedback">Feedback: {{question.a_feedback}}</span>
{% endif %}
</li>
<li data-ans="b">
<div class="fieldWrapper flex-container">
@@ -88,22 +71,18 @@
</div>
{% if previous > -1 %}
<button type="submit" name="previous" class="save btn btn-default">Previous</button>
<button type="submit" name="previous" class="save btn btn-default" title="Click to save your answer(s) and go to the previous question">Previous</button>
{% endif %}
{% if next %}
<button type="submit" name="next" class="save btn btn-default">Next</button>
<button type="submit" name="next" class="save btn btn-default" title="Click to save your answer(s) and go to the next quesiton">Next</button>
{% else %}
{% if not exam.publish_results %}
<button type="submit" name="save" class="save btn btn-default">Save</button>
<button type="submit" name="save" class="save btn btn-default" title="Click to save your current answer(s)">Save</button>
{% endif %}
{% endif %}
{% comment %} {% if exam.publish_results and question.feedback %}
<h3>Feedback</h3>
<p>{{question.feedback|safe}}</p>
{% endif %} {% endcomment %}
<br />
<button type="submit" name="finish" class="save btn btn-default">Overview</button>
<button type="submit" name="finish" class="save btn btn-default" title="Click to go to the overview page (your answers will be saved).">Overview</button>
<button type="submit" id="goto-button" value="test" name="goto" class="hide">goto</button>
</form>
</div>
@@ -113,6 +92,11 @@
<div id="menu-list">
</div>
<details><summary>Help</summary>
<p>Each question contains a list of 5 different statements. These are either True or False</p>
<p>Click on each statement to toggle between True and False. The current state can be seen to the right of the statement. </p>
<p>Your answers are saved when navigating between questions (or if the save button is clicked on the final question). An overview of all the questions can be seen by clicking on the overview button (or by clicking <a target="_blank" href="{% url 'physics:exam_scores_cid_user' pk=exam.pk cid=cid passcode=passcode %}">here</a>).</p>
</details>
{% endblock %}
{% block js %}
@@ -151,8 +135,10 @@
{% endif %}
for (let i = 0; i < {{ exam_length }}; i++) {
$("#menu-list").append($(
`<button class="question-menu-item" name="goto-${i}" data-qn="${i}">${i+1}</button>`));
qbutton = $(`<button class="question-menu-item" name="goto-${i}" data-qn="${i}">${i+1}</button>`)
if (i == {{pos}}) {qbutton.addClass("current-question")}
$("#menu-list").append(qbutton);
}
$("button.question-menu-item").on("click", (e) => {
@@ -164,48 +150,6 @@
})
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();
}
@@ -232,14 +176,18 @@
border: 1px solid purple;
}
.answer-true::after {
.answer-true::before {
content: "[Correct answer: True]";
color: darkgray;
float: right;
opacity: 50%;
}
.answer-false::after {
.answer-false::before {
content: "[Correct answer: False]";
color: darkgray;
float: right;
opacity: 50%;
}
.answer-correct {
@@ -249,6 +197,11 @@
border: 1px dashed darkred;
}
.physics-answer-list li {
margin-top: 5px;
margin-bottom: 5px;
}
.physics-answer-list input:checked+.postinput::after {
margin-left: 20px;
content: "True"
@@ -258,35 +211,10 @@
content: "False"
}
#clockdiv{
font-family: sans-serif;
display: inline-block;
font-weight: 100;
text-align: center;
font-size: 12px;
display: none;
color: darkgray;
.feedback {
padding: 20px;
display: block;
}
#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>
{% endblock %}
@@ -1,11 +1,14 @@
{% extends 'base.html' %}
{% block content %}
<span id="user-id">
{% if request.user.is_authenticated %}
User: {{request.user}}
{% else %}
CID: {{cid}}
{% endif %}
</span>
{% include "exam_clock.html" %}
<h2>Exam: {{exam.name}}</h2>
@@ -21,26 +24,43 @@
</div>
{% endif %}
<div><p>Questions</p></div>
<div class="overview-text">{{answer_count}} out of {{exam_length}} questions answered. Click to go to question.</div>
{% if not exam.publish_results and answer_count != exam_length %}
<div id="unanswered-questions-alert" class="alert alert-warning" role="alert">
You have unanswered questions.
</div>
{% endif %}
<div class="overview-text">{{answer_count}} out of {{exam_length}} questions answered. Unanswered questions are shown in red. <p>Click to go to a question.</p></div>
<div class="physics-finish-list">
{% for question, answer in question_answer_tuples %}
{% if request.user.is_authenticated %}
<a href="{% url 'physics:exam_take_user' pk=exam.id sk=forloop.counter0 %}"><button {% if not answer %}class="unanswered"{% endif %}>{{forloop.counter}}: {{answer.answer}}</button></a>
<a href="{% url 'physics:exam_take_user' pk=exam.id sk=forloop.counter0 %}"><button {% if not answer %}class="unanswered" title="You have not answered this question"{% endif %}>{{forloop.counter}}: {{answer.answer}}</button></a>
{% else %}
<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" title="You have not answered this question"{% endif %}>{{forloop.counter}}: {{answer.answer}}</button></a>
{% endif %}
{% endfor %}
</div>
<div id="time-details">
Start time: {{cid_user_exam.start_time}}<br/>
Last change time: {{cid_user_exam.end_time}}
</div>
{% endblock %}
{% block js %}
<script>
$(document).ready(() => {
{% if not exam.publish_results %}
let time_limit = '{{exam.time_limit}}'
if (time_limit != "None") {
let end_time = new Date({{cid_user_exam.start_time|date:"U"}}*1000+parseInt('{{exam.time_limit}}')*1000);
initializeClock("clockdiv", end_time);
}
{% endif %}
})
</script>
{% endblock %}
+52 -3
View File
@@ -73,7 +73,8 @@ a:link {
font-size: 20;
}
.hide, .hidden {
.hide,
.hidden {
display: none;
}
@@ -83,7 +84,12 @@ a:link {
}
.unanswered {
color: gray;
color: darkred;
border-color: darkred;
}
.unanswered:hover {
background-color: rgb(166, 0, 0);
}
#question-list {
@@ -1057,7 +1063,50 @@ table .peninsula-trainee::before {
user-select: none;
}
.form-control, .form-control:focus, .form-control option {
.form-control,
.form-control:focus,
.form-control option {
background-color: unset;
color: unset
}
#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;
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;
}
#time-details {
opacity: 50%;
font-size: small;
}
.current-question {
background-color: #52057b;
color: grey;
}
+1 -1
View File
@@ -245,7 +245,7 @@ def cid_scores(request, cid, passcode):
case_collections = cid_user.casecollection_exams.all()
available_exams = cid_user.get_cid_exams()
available_exams = cid_user.get_cid_exams(include_case_collections=False)
return render(
request,
+4
View File
@@ -11,6 +11,10 @@
<p><input id="cid-box" type="text" value="Candidate ID"></p>
<p><input id="passcode-box" type="text" value="Passcode"></p>
{% if exam.time_limit %}
This exam has a time limit of {{ exam.get_time_limit }}. The time will start when you click the Start Exam button below.<br/>
{% endif %}
<button>Start exam</button>
<script type="text/javascript">
+20 -99
View File
@@ -8,28 +8,7 @@
CID: {{cid}}
{% endif %}
</span>
<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>
{% include "exam_clock.html" %}
<div class="no-select">
<h2>{{exam.name}}: Question [<span id="question-number">{{pos|add:1}}</span>/<span id="exam-length">{{exam_length}}</span>]</h2>
{% if exam.publish_results %}
@@ -58,13 +37,13 @@
</div>
{% if previous > -1 %}
<button type="submit" name="previous" class="save btn btn-default">Previous</button>
<button type="submit" name="previous" class="save btn btn-default" title="Click to save your answer(s) and go to the previous question">Previous</button>
{% endif %}
{% if next %}
<button type="submit" name="next" class="save btn btn-default">Next</button>
<button type="submit" name="next" class="save btn btn-default" title="Click to save your answer(s) and go to the next quesiton">Next</button>
{% else %}
{% if not exam.publish_results %}
<button type="submit" name="save" class="save btn btn-default">Save</button>
<button type="submit" name="save" class="save btn btn-default" title="Click to save your current answer(s)">Save</button>
{% endif %}
{% endif %}
@@ -73,7 +52,7 @@
<p>{{question.feedback|safe}}</p>
{% endif %}
<br />
<button type="submit" name="finish" class="save btn btn-default">Overview</button>
<button type="submit" name="finish" class="save btn btn-default" title="Click to go to the overview page (your answers will be saved).">Overview</button>
<button type="submit" id="goto-button" value="test" name="goto" class="hide">goto</button>
</form>
</div>
@@ -83,6 +62,11 @@
<div id="menu-list">
</div>
<details><summary>Help</summary>
<p>Each question contains a list of 5 different statements. One of these is the single BEST answer.</p>
<p>Click on the correct statement to select it. Once selected it will be highlighted. </p>
<p>Your answers are saved when navigating between questions (or if the save button is clicked on the final question). An overview of all the questions can be seen by clicking on the overview button (or by clicking <a target="_blank" href="{% url 'sbas:exam_scores_cid_user' pk=exam.pk cid=cid passcode=passcode %}">here</a>).</p>
</details>
{% endblock %}
{% block js %}
@@ -113,8 +97,10 @@
$("ul.sba-answer-list li[data-ans='{{saved_answer}}']").addClass("selected"); {% endif %}
for (let i = 0; i < {{ exam_length }}; i++) {
$("#menu-list").append($(
`<button class="question-menu-item" name="goto-${i}" data-qn="${i}">${i+1}</button>`));
qbutton = $(`<button class="question-menu-item" name="goto-${i}" data-qn="${i}">${i+1}</button>`)
if (i == {{pos}}) {qbutton.addClass("current-question")}
$("#menu-list").append(qbutton);
}
$("button.question-menu-item").on("click", (e) => {
@@ -124,50 +110,6 @@
});
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) {
console.log("Start clock", 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();
}
/* beautify ignore:end */
})
</script>
{% endblock %}
@@ -180,36 +122,15 @@
.selected {
border: 1px solid purple;
}
#clockdiv{
font-family: sans-serif;
display: inline-block;
font-weight: 100;
text-align: center;
font-size: 12px;
display: none;
color: darkgray;
li:hover::before {
content: "Click to select";
float: right
}
#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;
li:hover.selected::before {
content: "Chosen answer";
float: right
}
#timeup {
display: none;
color: red;
font-weight: 100;
text-align: center;
font-size: 12px;
}
</style>
{% endblock %}
+24 -4
View File
@@ -1,12 +1,15 @@
{% extends 'base.html' %}
{% block content %}
<span id="user-id">
{% if request.user.is_authenticated %}
User: {{request.user}}
{% else %}
CID: {{cid}}
{% endif %}
</span>
{% include "exam_clock.html" %}
<h2>Exam: {{exam.name}}</h2>
{% if exam.publish_results %}
@@ -21,26 +24,43 @@
</div>
{% endif %}
<div><p>Questions</p></div>
<div class="overview-text">{{answer_count}} out of {{exam_length}} questions answered. Click to go to question.</div>
{% if not exam.publish_results and answer_count != exam_length %}
<div id="unanswered-questions-alert" class="alert alert-warning" role="alert">
You have unanswered questions.
</div>
{% endif %}
<div class="overview-text">{{answer_count}} out of {{exam_length}} questions answered. Unanswered questions are shown in red. <p>Click to go to a question.</p></div>
<div class="sba-finish-list">
{% for question, answer in question_answer_tuples %}
{% if request.user.is_authenticated %}
<a href="{% url 'sbas:exam_take_user' pk=exam.id sk=forloop.counter0 %}"><button {% if not answer %}class="unanswered"{% endif %}>{{forloop.counter}}: {{answer.answer}}</button></a>
<a href="{% url 'sbas:exam_take_user' pk=exam.id sk=forloop.counter0 %}"><button {% if not answer %}class="unanswered" title="You have not answered this question"{% endif %}>{{forloop.counter}}: {{answer.answer}}</button></a>
{% else %}
<a href="{% url 'sbas: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 'sbas:exam_take' pk=exam.id sk=forloop.counter0 cid=cid passcode=passcode %}"><button {% if not answer %}class="unanswered" title="You have not answered this question"{% endif %}>{{forloop.counter}}: {{answer.answer}}</button></a>
{% endif %}
{% endfor %}
</div>
<div id="time-details">
Start time: {{cid_user_exam.start_time}}<br/>
Last change time: {{cid_user_exam.end_time}}
</div>
{% endblock %}
{% block js %}
<script>
$(document).ready(() => {
{% if not exam.publish_results %}
let time_limit = '{{exam.time_limit}}'
if (time_limit != "None") {
let end_time = new Date({{cid_user_exam.start_time|date:"U"}}*1000+parseInt('{{exam.time_limit}}')*1000);
initializeClock("clockdiv", end_time);
}
{% endif %}
})
</script>
{% endblock %}
+4 -2
View File
@@ -4,8 +4,10 @@
<div class="">
<h2>Exam / Results checker</h2>
<p>Please enter your CID (Candidate ID) and passcode</p>
<p>CID: <input type="text" name="cid" id="cid-box" autofocus></p>
<p>Passcode: <input id="passcode-box" type="text"></p>
<table>
<tr><td>CID:</td><td><input type="text" name="cid" id="cid-box" autofocus placeholder="Please enter your CID number"></td></tr>
<tr><td>Passcode:</td><td><input id="passcode-box" type="text" placeholder="Please enter your passcode"></td></tr>
</table>
<button id="cid-selector-go-button">Login</button>
</div>
<details>
+18
View File
@@ -0,0 +1,18 @@
<div id="timeup">
Allocated time over.
</div>
<div id="clockdiv">
Time remaining:
<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>
+22 -2
View File
@@ -15,19 +15,38 @@
<a href="{% url 'longs:index'%}">Longs</a>
<a href="{% url 'sbas:index'%}">SBAs</a> {% endcomment %}
{% endif %}
{% if request.user.is_staff %}<a href="{% url 'admin:index'%}">Admin</a>{% endif %}
</div>
</p>
{% if request.user.is_authenticated %}
{% if request.user.userprofile.peninsula_trainee %}
<div id="user-details">
Please check your details are correct.<br/>
Name: {{request.user.first_name}} {{request.user.last_name}}<br/>
Email: {{request.user.email}}<br/>
Grade: {{request.user.userprofile.grade}}<br/>
Supervisor: {{request.user.userprofile.supervisor}}
</div>
{% endif %}
<p>
<a href="{% url 'user_scores'%}">Your Exams</a>
<a href="{% url 'user_scores'%}"><button>Exams</button></a>
</p>
{% endif %}
<p><a href="http://www.penracourses.org.uk/rts">RTS is available here</a></p>
{% if not request.user.is_authenticated %}
<p><a href="{% url 'cid_selector' %}">CID users can log in here</a></p>
{% endif %}
<div>
{% if request.user.is_staff %}
Manage users <a href="{% url 'accounts_list'%}">here</a> and candidates <a href="{% url 'generic:manage_cids'%}">here</a>
@@ -35,6 +54,7 @@
<p>Manage <a href="{% url 'generic:examination_view' %}">Examinations</a>
</p>
<a href="{% url 'admin:index'%}">Admin</a>
{% endif %}
</div>
{% endblock %}