Add question time limit to CaseCollection model and update related template for countdown display

This commit is contained in:
Ross
2025-10-13 09:45:12 +01:00
parent 682e8160b5
commit 6f99cf0d53
5 changed files with 104 additions and 95 deletions
+1
View File
@@ -215,6 +215,7 @@ class CaseCollectionForm(ModelForm):
"feedback_once_collection_complete",
"collection_type",
"viewer_mode",
"question_time_limit",
),
Fieldset("Valid User Groups", *user_fields) if user_fields else None,
Div(
@@ -0,0 +1,18 @@
# Generated by Django 5.1.4 on 2025-10-13 08:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('atlas', '0075_casedetail_override_history_and_more'),
]
operations = [
migrations.AddField(
model_name='casecollection',
name='question_time_limit',
field=models.PositiveIntegerField(help_text='Time limit for answering questions in seconds.', null=True),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.1.4 on 2025-10-13 08:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('atlas', '0076_casecollection_question_time_limit'),
]
operations = [
migrations.AlterField(
model_name='casecollection',
name='question_time_limit',
field=models.PositiveIntegerField(blank=True, help_text='Time limit for answering questions in seconds.', null=True),
),
]
+6
View File
@@ -942,6 +942,12 @@ class CaseCollection(ExamOrCollectionGenericBase):
feedback_once_collection_complete = models.BooleanField(default=True, help_text="If true feedback is only given once the collection is complete. If false feedback is given after each case.")
question_time_limit = models.PositiveIntegerField(
blank=True,
null=True,
help_text="Time limit for answering questions in seconds."
)
class COLLECTION_TYPE_CHOICES(models.TextChoices):
REVIEW = (
"REV",
@@ -16,6 +16,13 @@
</h2>
{% if not question_completed and collection.question_time_limit is not None %}
<div id="question-timer-block" style="margin-top:8px; margin-bottom:8px;" title="This question has a time limit of {{ collection.question_time_limit }} seconds. The timer will start when the page loads.">
<strong>Time remaining:</strong>
<span id="question-timer" aria-live="polite">&nbsp;</span>
</div>
{% endif %}
{% comment %} <details>
<summary class="opacity-50">Help <i class="bi bi-info-circle"></i></summary>
</details> {% endcomment %}
@@ -49,13 +56,13 @@
{% if collection.show_ohif_viewer %}
<div>
{% if question_completed %}
<iframe id="viewer" style="width: 100%; height: 100%; border: none; padding: 0px; min-height: 700px" src="/ohif/viewer/dicomjson?url=https://www.penracourses.org.uk{% url 'atlas:collection_case_dicom_json_review' collection.pk case.pk %}"></iframe>
<iframe id="viewer" style="width: 100%; height: 100%; border: none; padding: 0px; min-height: 700px" src="/ohif/viewer/dicomjson?url=https://www.penracourses.org.uk{% url 'atlas:collection_case_dicom_json_review' collection.pk case.pk %}"></iframe>
{% else %}
<iframe id="viewer" style="width: 100%; height: 100%; border: none; padding: 0px; min-height: 700px" src="/ohif/viewer/dicomjson?url=https://www.penracourses.org.uk{% url 'atlas:collection_case_dicom_json' collection.pk case.pk %}"></iframe>
<iframe id="viewer" style="width: 100%; height: 100%; border: none; padding: 0px; min-height: 700px" src="/ohif/viewer/dicomjson?url=https://www.penracourses.org.uk{% url 'atlas:collection_case_dicom_json' collection.pk case.pk %}"></iframe>
{% endif %}
</div>
{% endif %}
@@ -63,9 +70,9 @@
{% if collection.show_ohif_viewer_link %}
<div>
{% if question_completed %}
<a target="_blank" href="/ohif/viewer/dicomjson?url=https://www.penracourses.org.uk{% url 'atlas:collection_case_dicom_json_review' collection.pk case.pk %}" title="Click to open the case in the advanced viewer. This will open in a new tab/window."><button class="viewer"><i class="bi bi-collection"></i> Launch advanced viewer.</button></a>
<a target="_blank" href="/ohif/viewer/dicomjson?url=https://www.penracourses.org.uk{% url 'atlas:collection_case_dicom_json_review' collection.pk case.pk %}" title="Click to open the case in the advanced viewer. This will open in a new tab/window."><button class="viewer"><i class="bi bi-collection"></i> Launch advanced viewer.</button></a>
{% else %}
<a target="_blank" href="/ohif/viewer/dicomjson?url=https://www.penracourses.org.uk{% url 'atlas:collection_case_dicom_json' collection.pk case.pk %}" title="Click to open the case in the advanced viewer. This will open in a new tab/window."><button class="viewer"><i class="bi bi-collection"></i> Launch advanced viewer.</button></a>
<a target="_blank" href="/ohif/viewer/dicomjson?url=https://www.penracourses.org.uk{% url 'atlas:collection_case_dicom_json' collection.pk case.pk %}" title="Click to open the case in the advanced viewer. This will open in a new tab/window."><button class="viewer"><i class="bi bi-collection"></i> Launch advanced viewer.</button></a>
{% endif %}
</div>
{% endif %}
@@ -325,107 +332,66 @@
{{ form.media }}
{% comment %} <script src="https://cdn.jsdelivr.net/npm/@json-editor/json-editor@latest/dist/jsoneditor.min.js"></script> {% endcomment %}
<script type="text/javascript">
window.images = {
{% comment %} {% for series in series_list %}
{{ forloop.counter0 }}: ["{{ series.get_image_url_array_not_json }}"],
{% endfor %} {% endcomment %}
{% for series, prior, relation in series_to_load %}
{{ forloop.counter0 }}: ["{{ series.get_image_url_array_not_json }}"],
{% endfor %}
}
// Question time limit countdown + auto-submit
$(function () {
try {
var timeLimit = {{ collection.question_time_limit|default:'null' }};
var questionCompleted = {{ question_completed|yesno:"true,false" }};
console.debug('Timer init:', {timeLimit: timeLimit, questionCompleted: questionCompleted});
$(document).ready(function () {
setTimeout(() => {
window.loadDicomViewer(window.images[0])
}, 500);
})
if (!timeLimit || questionCompleted) {
return;
}
{% comment %} $('document').ready(function() {
var $timer = $('#question-timer');
var $form = $('form.post-form');
// Get value from either a json string or url pointing to a json file
function process(value) {
var isjson=true;
var result;
if ($timer.length === 0 || $form.length === 0) {
console.debug('Timer: required elements not found, aborting timer init');
return;
}
try {
result = JSON.parse(value);
} catch(e) {
isjson=false;
}
var remaining = parseInt(timeLimit, 10);
if (isjson) {
return result;
} else {
return $.getJSON(value)
.then(function (response) {
return response;
});
}
}
function formatTime(s) {
var mins = Math.floor(s / 60);
var secs = s % 60;
return mins + ':' + (secs < 10 ? '0' + secs : secs);
}
$('.editor_holder').each(function() {
// Get the DOM Element
var element = $(this).get(0);
console.log("el", element)
$timer.text(formatTime(remaining));
var options_text = $(this).attr('options')
var schema_text = $(this).attr('schema')
var intervalId = setInterval(function () {
remaining -= 1;
if (remaining <= 0) {
clearInterval(intervalId);
$timer.text('0:00');
var $next = $form.find('button[name="next"]');
var $finish = $form.find('button[name="finish"]');
var schema = process(schema_text);
var options = process(options_text);
if ($next.length) {
$next.prop('disabled', false);
$next.click();
} else if ($finish.length) {
$finish.prop('disabled', false);
$finish.click();
} else {
$form.submit();
}
} else {
$timer.text(formatTime(remaining));
}
}, 1000);
var name = $(this).attr('name');
var hidden_identifier = 'input[name=' + name + ']';
var initial = $(hidden_identifier).val();
// Check if editor is within form
var form = $(this).closest('form')
console.log("form", form)
//Wait for any ajax requests to complete
$.when(schema, options).done(function(schemaresult, optionsresult) {
optionsresult.form_name_root = name;
// Pass initial value though to editor
if (initial) {
optionsresult.startval = JSON.parse(initial);
$form.on('submit', function () {
clearInterval(intervalId);
});
} catch (e) {
console.error('Error initializing timer:', e);
}
});
optionsresult.schema = schemaresult;
// console.log(options);
var editor = new JSONEditor(element, optionsresult);
console.log("editor", editor)
if (form) {
$(form).submit(function(e) {
console.log("submitting")
// Set the hidden field value to the editors value
$(hidden_identifier).val(JSON.stringify(editor.getValue()));
// Disable the editor so it's values wont be submitted
//editor.disable();
// Validate the editor's current value against the schema
const errors = editor.validate();
if (errors.length) {
// errors is an array of objects, each with a `path`, `property`, and `message` parameter
// `property` is the schema keyword that triggered the validation error (e.g. "minLength")
// `path` is a dot separated path into the JSON object (e.g. "root.path.to.field")
console.log(errors);
}
else {
console.log("valid");
}
console.log(editor.getValue());
//e.preventDefault();
})
} else {
console.log("No form found")
}
})
});
})
{% endcomment %}
</script>
{% endblock js %}