Compare commits
18
Commits
b47bb141e8
...
feb5a85af2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
feb5a85af2 | ||
|
|
292f81ad74 | ||
|
|
349c46c6db | ||
|
|
681a7560ba | ||
|
|
aa90ae5e09 | ||
|
|
f55bb1b18e | ||
|
|
5e78cc4603 | ||
|
|
a9cbb1303a | ||
|
|
24558f92ce | ||
|
|
0f8bae8a14 | ||
|
|
6e658aca93 | ||
|
|
81cd109449 | ||
|
|
d86f990be2 | ||
|
|
b6478c44dc | ||
|
|
12b7fa6629 | ||
|
|
7f4570417e | ||
|
|
0098fa5dfc | ||
|
|
740d013198 |
@@ -2,16 +2,30 @@
|
||||
|
||||
{% block navigation %}
|
||||
{{block.super}}
|
||||
<br/>
|
||||
Exams: {{exam}}->
|
||||
<a href="{% url 'anatomy:exam_overview' pk=exam.pk %}">Overview</a> /
|
||||
{% if exam.exam_mode %}
|
||||
<a href="{% url 'anatomy:mark_overview' pk=exam.pk %}">Mark</a> /
|
||||
<a href="{% url 'anatomy:mark2_overview' pk=exam.pk %}">Mark2</a> /
|
||||
<a href="{% url 'anatomy:exam_scores_all' pk=exam.pk %}">Scores</a> /
|
||||
<a href="{% url 'anatomy:exam_cids' exam_id=exam.pk %}">Candidates</a> /
|
||||
<a href="{% url 'anatomy:exam_stats' exam_id=exam.pk %}">Stats</a> /
|
||||
{% endif %}
|
||||
<a href="{% url 'anatomy:exam_review_start' pk=exam.pk %}">Review</a>
|
||||
{% comment %} <a href="{% url 'anatomy:question_create_exam' pk=exam.pk %}" title="Add a new question directly into the exam.">Add New Question</a> {% endcomment %}
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mt-2 rounded">
|
||||
<div class="container-fluid px-3">
|
||||
<span class="navbar-text me-3 text-white">
|
||||
<strong>Exam:</strong> {{ exam.title|default:exam.name|default:exam }}
|
||||
</span>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#examNav" aria-controls="examNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="examNav">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'anatomy:exam_overview' pk=exam.pk %}">Overview</a></li>
|
||||
{% if exam.exam_mode %}
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'anatomy:mark_overview' pk=exam.pk %}">Mark</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'anatomy:mark2_overview' pk=exam.pk %}">Mark2</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'anatomy:exam_scores_all' pk=exam.pk %}">Scores</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'anatomy:exam_cids' exam_id=exam.pk %}">Candidates</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'anatomy:exam_stats' exam_id=exam.pk %}">Stats</a></li>
|
||||
{% endif %}
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'anatomy:exam_review_start' pk=exam.pk %}">Review</a></li>
|
||||
</ul>
|
||||
|
||||
{% include 'generic/partials/exams/dropdown_menu.html'%}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,28 +1,95 @@
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mt-2 rounded">
|
||||
<div class="container-fluid px-3">
|
||||
<span class="navbar-text me-3 text-white">
|
||||
<strong>Question:</strong>
|
||||
{% if question.stem %}
|
||||
{{ question.stem|striptags|truncatechars:80 }}
|
||||
{% else %}
|
||||
#{{ question.pk }}
|
||||
{% endif %}
|
||||
</span>
|
||||
|
||||
<div class="floating-header">
|
||||
<a href="{% url 'anatomy:anatomy_question_update' question.id %}" title="Edit the Question">Edit</a>
|
||||
<a href="{% url 'anatomy:question_clone' question.id %}" title="Clone the Question">Clone</a>
|
||||
<a href="{% url 'anatomy:question_delete' pk=question.pk %}" title="Delete the Question">Delete</a>
|
||||
<a href="{% url 'anatomy:question_answer_update' pk=question.pk %}" title="Update the question answers">Edit Answers</a>
|
||||
<a href="#"
|
||||
onclick="return window.create_popup_window('{% url 'feedback_create' question_type='anatomy' pk=question.pk %}')">
|
||||
Add Note</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#questionNav" aria-controls="questionNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
{% if request.user.is_superuser %}
|
||||
<a href="{% url 'admin:anatomy_anatomyquestion_change' question.id %}"
|
||||
title="Edit the Question using the admin interface">Admin Edit</a>
|
||||
<a href="{% url 'anatomy:question_user_answers' question.id %}" title="View user answers associated with this question">User answers</a>
|
||||
{% endif %}
|
||||
{% if exam %}
|
||||
<div>
|
||||
|
||||
{% if previous > -1 %}
|
||||
<a href="{% url 'anatomy:exam_question_detail' exam.id previous %}">Previous question</a>
|
||||
<div class="collapse navbar-collapse" id="questionNav">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
{% if exam %}
|
||||
<li class="nav-item">
|
||||
{% if previous > -1 %}
|
||||
<a class="nav-link" href="{% url 'anatomy:exam_question_detail' exam.id previous %}">Previous</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
<li class="nav-item align-self-center">
|
||||
Viewing as part of exam: <a class="nav-link d-inline p-0" href="{% url 'anatomy:exam_overview' exam.id %}">{{ exam }}</a>
|
||||
<span class="navbar-text text-muted ms-2">[{{ pos }}/{{ exam_length }}]</span>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
{% if next %}
|
||||
<a class="nav-link" href="{% url 'anatomy:exam_question_detail' exam.id next %}">Next</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endif %}
|
||||
Viewing question as part of exam: <a href="{% url 'anatomy:exam_overview' exam.id %}">{{exam}}</a> [{{pos}}/{{exam_length}}]
|
||||
{% if next %}
|
||||
<a href="{% url 'anatomy:exam_question_detail' exam.id next %}">Next question</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</ul>
|
||||
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="questionActions" role="button" data-bs-toggle="dropdown" aria-expanded="false">Actions</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end dropdown-menu-dark" aria-labelledby="questionActions">
|
||||
<li>
|
||||
<a class="dropdown-item text-primary" href="{% url 'anatomy:question_detail' question.id %}">
|
||||
<i class="bi bi-eye me-2"></i> View
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item text-primary" href="{% url 'anatomy:anatomy_question_update' question.id %}">
|
||||
<i class="bi bi-pencil-square me-2"></i> Edit
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a class="dropdown-item text-white" href="{% url 'anatomy:question_answer_update' pk=question.pk %}">
|
||||
<i class="bi bi-list-check me-2"></i> Edit Answers
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a class="dropdown-item text-white" href="{% url 'anatomy:question_clone' question.id %}">
|
||||
<i class="bi bi-files me-2"></i> Clone
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a class="dropdown-item text-info" href="#" onclick="return window.create_popup_window('{% url 'feedback_create' question_type='anatomy' pk=question.pk %}')">
|
||||
<i class="bi bi-chat-left-text me-2"></i> Add Note
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
|
||||
<li>
|
||||
<a class="dropdown-item text-danger" href="{% url 'anatomy:question_delete' pk=question.pk %}">
|
||||
<i class="bi bi-trash me-2"></i> Delete
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% if request.user.is_superuser %}
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<a class="dropdown-item text-warning" href="{% url 'admin:anatomy_anatomyquestion_change' question.id %}">
|
||||
<i class="bi bi-tools me-2"></i> Admin Edit
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item text-muted" href="{% url 'anatomy:question_user_answers' question.id %}">
|
||||
<i class="bi bi-people me-2"></i> User answers
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
+5
-22
@@ -831,7 +831,7 @@ class BaseReportAnswerForm(ModelForm):
|
||||
# "answer": "Write your answer in here."
|
||||
# }
|
||||
|
||||
def __init__(self, *args, case_detail, **kwargs):
|
||||
def __init__(self, *args, casedetail, **kwargs):
|
||||
super(BaseReportAnswerForm, self).__init__(*args, **kwargs)
|
||||
self.fields["answer"].required = False
|
||||
|
||||
@@ -847,12 +847,12 @@ class BaseQuestionAnswerForm(ModelForm):
|
||||
# "answer": "Write your answer in here."
|
||||
# }
|
||||
|
||||
def __init__(self, *args, case_detail, **kwargs):
|
||||
def __init__(self, *args, casedetail, **kwargs):
|
||||
super(BaseQuestionAnswerForm, self).__init__(*args, **kwargs)
|
||||
# self.fields["json_answer"].schema = case_detail.question_schema
|
||||
if case_detail.question_schema is not None:
|
||||
# self.fields["json_answer"].schema = casedetail.question_schema
|
||||
if casedetail.question_schema is not None:
|
||||
self.fields["json_answer"] = JSONSchemaField(
|
||||
schema=case_detail.question_schema, options="schema/options.json"
|
||||
schema=casedetail.question_schema, options="schema/options.json"
|
||||
)
|
||||
self.fields["json_answer"].label = ""
|
||||
|
||||
@@ -1153,23 +1153,6 @@ class PriorCaseForm(Form):
|
||||
|
||||
prior_visibility = CharField(max_length=2, required=True)
|
||||
|
||||
# def __init__(self, *args, **kwargs):
|
||||
# self.case_detail = kwargs.pop(
|
||||
# "case_detail"
|
||||
# ) # To get request.user. Do not use kwargs.pop('user', None) due to potential security hole
|
||||
|
||||
# super(PriorCaseForm, self).__init__(*args, **kwargs)
|
||||
|
||||
# prior_cases = self.case_detail.case.get_all_prior_cases()
|
||||
|
||||
# if not prior_cases:
|
||||
# prior_cases = Case.objects.none()
|
||||
|
||||
# self.fields["case"] = ChoiceField(
|
||||
# required=False,
|
||||
# # widget=Select(verbose_name="Series", is_stacked=False),
|
||||
# )
|
||||
|
||||
|
||||
class CaseSeriesForm(forms.ModelForm):
|
||||
def __init__(self, *args, queryset, user=None, is_atlas_editor=False, **kwargs):
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 5.2.7 on 2025-11-17 10:28
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('atlas', '0084_resource_open_access'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='caseprior',
|
||||
old_name='case_detail',
|
||||
new_name='casedetail',
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='caseprior',
|
||||
unique_together={('casedetail', 'prior_case')},
|
||||
),
|
||||
]
|
||||
+10
-3
@@ -1443,6 +1443,13 @@ class CaseDetail(models.Model):
|
||||
def __str__(self) -> str:
|
||||
return f"{self.case} -> {self.collection}"
|
||||
|
||||
|
||||
def get_case_index(self):
|
||||
"""Returns the question index (number) in the collection (0 indexed)"""
|
||||
case_details = list(self.collection.casedetail_set.all().order_by("sort_order"))
|
||||
|
||||
return case_details.index(self)
|
||||
|
||||
def get_question_schema(self):
|
||||
return json.dumps(self.question_schema)
|
||||
|
||||
@@ -1545,7 +1552,7 @@ class CaseDetail(models.Model):
|
||||
return mark_safe(html)
|
||||
|
||||
class CasePrior(models.Model):
|
||||
case_detail = models.ForeignKey(CaseDetail, on_delete=models.CASCADE)
|
||||
casedetail = models.ForeignKey(CaseDetail, on_delete=models.CASCADE)
|
||||
prior_case = models.ForeignKey(Case, on_delete=models.CASCADE, related_name="prior_case")
|
||||
|
||||
relation_text = models.CharField(max_length=255, blank=True, help_text="Text to describe the relationship between the cases")
|
||||
@@ -1564,10 +1571,10 @@ class CasePrior(models.Model):
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ("case_detail", "prior_case")
|
||||
unique_together = ("casedetail", "prior_case")
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.case_detail.case} -> {self.prior_case}"
|
||||
return f"{self.casedetail.case} -> {self.prior_case}"
|
||||
|
||||
class BaseReportAnswer(models.Model):
|
||||
question = models.ForeignKey(CaseDetail, on_delete=models.CASCADE)
|
||||
|
||||
@@ -464,7 +464,7 @@
|
||||
|
||||
|
||||
{% if casedetail %}
|
||||
{% include 'atlas/partials/collection_question_block.html' with case_detail=casedetail can_edit=can_edit %}
|
||||
{% include 'atlas/partials/collection_question_block.html' with can_edit=can_edit %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -3,17 +3,9 @@
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div>
|
||||
{% if previous %}
|
||||
<a href="{% url 'atlas:collection_case_details' collection.id previous.id %}">Previous question</a>
|
||||
{% endif %}
|
||||
Viewing question as part of collection: <a href="{% url 'atlas:collection_detail' collection.id %}">{{collection.name}}</a> [{{case_number|add:1}}/{{collection_length}}]
|
||||
{% if next %}
|
||||
<a href="{% url 'atlas:collection_case_details' collection.id next.id %}">Next question</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include 'atlas/partials/_viewing_case_as_part_of_collection.html' with nav_link_view="atlas:collection_case_details" %}
|
||||
|
||||
<h2>Case: <a href="{% url 'atlas:case_detail' case_detail.case.pk %}">{{case_detail.case.title}}</a></h2>
|
||||
<h2>Case: <a href="{% url 'atlas:case_detail' casedetail.case.pk %}">{{casedetail.case.title}}</a></h2>
|
||||
|
||||
<p>This page allows you to configure how the case is displayed as part of the collection.</p>
|
||||
|
||||
|
||||
@@ -3,66 +3,67 @@
|
||||
|
||||
|
||||
{% block content %}
|
||||
<h2>Setup default display for case</h2>
|
||||
{% include 'atlas/partials/_viewing_case_as_part_of_collection.html' with nav_link_view="atlas:collection_case_displaysetup" %}
|
||||
<h2>Setup default display for case</h2>
|
||||
|
||||
<div id="root" class="dicom-viewer-root" data-images="{{casedetail.case.get_series_images_nested}}"
|
||||
style="box-sizing: border-box; background: #222; width: 100vw; height: 800px; max-width: 1600px;"
|
||||
data-auto-cache-stack="true"
|
||||
{% if casedetail.default_viewerstate %}
|
||||
data-viewerstate={{ casedetail.default_viewerstate }}
|
||||
{% endif %}
|
||||
|
||||
<div id="root" class="dicom-viewer-root" data-images="{{case_detail.case.get_series_images_nested}}"
|
||||
style="box-sizing: border-box; background: #222; width: 100vw; height: 800px; max-width: 1600px;"
|
||||
data-auto-cache-stack="true"
|
||||
{% if case_detail.default_viewerstate %}
|
||||
data-viewerstate={{ case_detail.default_viewerstate }}
|
||||
{% endif %}
|
||||
|
||||
></div>
|
||||
|
||||
<button id="save-viewerstate" type="button" class="btn btn-primary mt-3">
|
||||
Save Viewer State
|
||||
</button>
|
||||
<button id="reset-viewerstate" type="button" class="btn btn-secondary mt-3 ms-2">
|
||||
Reset to Default
|
||||
</button>
|
||||
<div id="viewerstate-save-response" class="mt-2"></div>
|
||||
<button id="save-viewerstate" type="button" class="btn btn-primary mt-3">
|
||||
Save Viewer State
|
||||
</button>
|
||||
<button id="reset-viewerstate" type="button" class="btn btn-secondary mt-3 ms-2">
|
||||
Reset to Default
|
||||
</button>
|
||||
<div id="viewerstate-save-response" class="mt-2"></div>
|
||||
|
||||
<script>
|
||||
document.getElementById('save-viewerstate').addEventListener('click', function() {
|
||||
let viewerState = window.exportViewerState_root();
|
||||
if (!viewerState) {
|
||||
document.getElementById('viewerstate-save-response').innerHTML = "<span class='text-danger'>Viewer state could not be retrieved.</span>";
|
||||
return;
|
||||
}
|
||||
fetch("{% url 'atlas:collection_case_displaysetup' case_detail.collection.pk case_detail.case.pk %}", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRFToken": "{{ csrf_token }}"
|
||||
},
|
||||
body: JSON.stringify({viewerstate: viewerState})
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
document.getElementById('viewerstate-save-response').innerHTML = html;
|
||||
})
|
||||
.catch(error => {
|
||||
document.getElementById('viewerstate-save-response').innerHTML = "<span class='text-danger'>Error saving viewer state.</span>";
|
||||
});
|
||||
});
|
||||
document.getElementById('reset-viewerstate').addEventListener('click', function() {
|
||||
fetch("{% url 'atlas:collection_case_displaysetup' case_detail.collection.pk case_detail.case.pk %}", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRFToken": "{{ csrf_token }}"
|
||||
},
|
||||
body: JSON.stringify({reset_viewerstate: true})
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
document.getElementById('viewerstate-save-response').innerHTML = html;
|
||||
<script>
|
||||
document.getElementById('save-viewerstate').addEventListener('click', function() {
|
||||
let viewerState = window.exportViewerState_root();
|
||||
if (!viewerState) {
|
||||
document.getElementById('viewerstate-save-response').innerHTML = "<span class='text-danger'>Viewer state could not be retrieved.</span>";
|
||||
return;
|
||||
}
|
||||
fetch({% if case_number %}"{% url 'atlas:collection_case_displaysetup' casedetail.collection.pk case_number=case_number %}"{% else %}"{% url 'atlas:collection_case_displaysetup_legacy' casedetail.collection.pk casedetail.case.pk %}"{% endif %}, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRFToken": "{{ csrf_token }}"
|
||||
},
|
||||
body: JSON.stringify({viewerstate: viewerState})
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
document.getElementById('viewerstate-save-response').innerHTML = html;
|
||||
})
|
||||
.catch(error => {
|
||||
document.getElementById('viewerstate-save-response').innerHTML = "<span class='text-danger'>Error saving viewer state.</span>";
|
||||
});
|
||||
});
|
||||
document.getElementById('reset-viewerstate').addEventListener('click', function() {
|
||||
fetch({% if case_number %}"{% url 'atlas:collection_case_displaysetup' casedetail.collection.pk case_number=case_number %}"{% else %}"{% url 'atlas:collection_case_displaysetup_legacy' casedetail.collection.pk casedetail.case.pk %}"{% endif %}, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRFToken": "{{ csrf_token }}"
|
||||
},
|
||||
body: JSON.stringify({reset_viewerstate: true})
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
document.getElementById('viewerstate-save-response').innerHTML = html;
|
||||
// Optionally reload the page or viewer here if needed
|
||||
})
|
||||
.catch(error => {
|
||||
document.getElementById('viewerstate-save-response').innerHTML = "<span class='text-danger'>Error resetting viewer state.</span>";
|
||||
});
|
||||
});
|
||||
</script>
|
||||
})
|
||||
.catch(error => {
|
||||
document.getElementById('viewerstate-save-response').innerHTML = "<span class='text-danger'>Error resetting viewer state.</span>";
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
@@ -4,106 +4,28 @@
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div>
|
||||
{% include 'atlas/partials/_viewing_case_as_part_of_collection.html' with nav_link_view="atlas:collection_case_priors" %}
|
||||
|
||||
{% if previous %}
|
||||
<a href="{% url 'atlas:collection_case_questions' collection.id previous.id %}">Previous question</a>
|
||||
{% endif %}
|
||||
Viewing question as part of collection: <a href="{% url 'atlas:collection_detail' collection.id %}">{{collection.name}}</a> [{{case_number|add:1}}/{{collection_length}}]
|
||||
{% if next %}
|
||||
<a href="{% url 'atlas:collection_case_questions' collection.id next.id %}">Next question</a>
|
||||
{% endif %}
|
||||
<h2 class="mb-3">Managing Priors for Case: <a href="{% url 'atlas:case_detail' casedetail.case.pk %}" class="text-decoration-none">{{casedetail.case.title}}</a></h2>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<p class="mb-1">This page allows you to add series from prior cases to a case in a collection. In order to be available here the cases must be linked on the system.</p>
|
||||
<p class="mb-0">It is necessary to give a description of how the cases are related that will be displayed to the user when viewing the case.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="mb-3">Available priors</h3>
|
||||
|
||||
<h2>Case: <a href="{% url 'atlas:case_detail' case_detail.case.pk %}">{{case_detail.case.title}}</a></h2>
|
||||
|
||||
<p>This page allows you to add series from prior cases to a case in a collection. In order to be available here the cases must be linked on the system.</p>
|
||||
<p>It is necessary to give a description of how the cases are related that will be displayed to the user when viewing the case.</p>
|
||||
|
||||
<h3>Available priors</h3>
|
||||
|
||||
<ul>
|
||||
<div class="row g-3">
|
||||
{% for case, added, relation, visibility in available_priors %}
|
||||
|
||||
<li class='
|
||||
{% if added %}
|
||||
added
|
||||
{% else %}
|
||||
not-added
|
||||
{% endif %}
|
||||
' data-case={{case.id}}>
|
||||
{{case.title}} / {{added}} / {{relation}} / <a href="{% url 'atlas:case_detail' case.id %}">View case</a>
|
||||
<br/>{{case.get_series_blocks|safe}}
|
||||
|
||||
<br/>
|
||||
|
||||
{% if not added %}
|
||||
<form hx-post="{% url 'atlas:collection_case_priors' collection.id case_detail.case.pk %}"
|
||||
hx-target="#htmx-response-{{case.id}}"
|
||||
_="on htmx:afterRequest log 'test' then remove me">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="prior_case_id" value="{{case.id}}" />
|
||||
<input type="text" name="relation" value="{{relation}}" placeholder="relation to case" />
|
||||
<br />
|
||||
<label for="prior_visibility_{{case.id}}">Visibility:</label>
|
||||
<select name="prior_visibility" id="prior_visibility_{{case.id}}">
|
||||
<option value="AL"
|
||||
{% if visibility == 'AL' %}
|
||||
selected
|
||||
{% endif %}
|
||||
>Always</option>
|
||||
<option value="RE"
|
||||
{% if visibility == 'RE' %}
|
||||
selected
|
||||
{% endif %}
|
||||
|
||||
>Review</option>
|
||||
<option value="NO"
|
||||
{% if visibility == 'NO' %}
|
||||
selected
|
||||
{% endif %}
|
||||
>None</option>
|
||||
</select>
|
||||
<br />
|
||||
<button class="btn primary">Add to case</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form hx-post="{% url 'atlas:collection_case_priors' collection.id case_detail.case.pk %}"
|
||||
hx-target="#htmx-response-{{case.id}}"
|
||||
_="on htmx:afterRequest log 'test' then remove me">
|
||||
Case available: {{relation}}
|
||||
<input type="hidden" name="remove" value="{{case.id}}" />
|
||||
<br />
|
||||
<label for="prior_visibility_{{case.id}}">Visibility:</label>
|
||||
<select name="prior_visibility" id="prior_visibility_{{case.id}}" disabled>
|
||||
<option value="AL"
|
||||
{% if visibility == 'AL' %}
|
||||
selected
|
||||
{% endif %}
|
||||
>Always</option>
|
||||
<option value="RE"
|
||||
{% if visibility == 'RE' %}
|
||||
selected
|
||||
{% endif %}
|
||||
|
||||
>Review</option>
|
||||
<option value="NO"
|
||||
{% if visibility == 'NO' %}
|
||||
selected
|
||||
{% endif %}
|
||||
>None</option>
|
||||
</select>
|
||||
<br />
|
||||
<button class="btn primary">Remove from case</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<span id="htmx-response-{{case.id}}"></span>
|
||||
|
||||
</li>
|
||||
|
||||
{% include 'atlas/partials/_prior_card.html' with case=case added=added relation=relation visibility=visibility collection=collection %}
|
||||
{% empty %}
|
||||
<div class="col-12">
|
||||
<div class="alert alert-info">No available prior cases found for the case.</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -4,21 +4,11 @@
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div>
|
||||
{% include 'atlas/partials/_viewing_case_as_part_of_collection.html' with nav_link_view="atlas:collection_case_questions" %}
|
||||
|
||||
{% if previous %}
|
||||
<a href="{% url 'atlas:collection_case_questions' collection.id previous.id %}">Previous question</a>
|
||||
{% endif %}
|
||||
Viewing question as part of collection: <a href="{% url 'atlas:collection_detail' collection.id %}">{{collection.name}}</a> [{{case_number|add:1}}/{{collection_length}}]
|
||||
{% if next %}
|
||||
<a href="{% url 'atlas:collection_case_questions' collection.id next.id %}">Next question</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h2>Case: <a href="{% url 'atlas:case_detail' casedetail.case.pk %}">{{casedetail.case.title}}</a></h2>
|
||||
|
||||
|
||||
<h2>Case: <a href="{% url 'atlas:case_detail' case_detail.case.pk %}">{{case_detail.case.title}}</a></h2>
|
||||
|
||||
{% if case_detail.question_schema is not None and not case_detail.question_answers %}
|
||||
{% if casedetail.question_schema is not None and not casedetail.question_answers %}
|
||||
|
||||
|
||||
<div class="alert alert-danger" role="alert">
|
||||
@@ -42,7 +32,7 @@
|
||||
|
||||
Import schema from:
|
||||
<button class="btn btn-primary btn-sm" type="button" data-bs-toggle="offcanvas" data-bs-target="#offcanvasRight" aria-controls="offcanvasRight"
|
||||
hx-get="{% url 'atlas:collection_question_schemas' case_detail.collection.id %}"
|
||||
hx-get="{% url 'atlas:collection_question_schemas' casedetail.collection.id %}"
|
||||
hx-target=".offcanvas-body"
|
||||
>Collection questions</button>
|
||||
<button class="btn btn-primary btn-sm" type="button" data-bs-toggle="offcanvas" data-bs-target="#offcanvasRight" aria-controls="offcanvasRight"
|
||||
@@ -54,7 +44,7 @@
|
||||
Question Block Title
|
||||
</summary>
|
||||
<p>Title of the question block.</p>
|
||||
<input id="question_block_title_input" type="text" name="title" value="{{case_detail.question_schema.title}}" class="form-control" placeholder="Question title" aria-label="Question title">
|
||||
<input id="question_block_title_input" type="text" name="title" value="{{casedetail.question_schema.title}}" class="form-control" placeholder="Question title" aria-label="Question title">
|
||||
</details>
|
||||
<details><summary>Modify Questions</summary>
|
||||
<div id="questions-container" style="margin-top: 20px;">
|
||||
@@ -123,7 +113,7 @@
|
||||
</form> {% endcomment %}
|
||||
|
||||
|
||||
{% if case_detail.question_schema %}
|
||||
{% if casedetail.question_schema %}
|
||||
<h2>Question display</h2>
|
||||
This shows the form as it will be displayed (with the correct answers). If you wish to edit a correct answer you can change it below.
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
{% endif %}
|
||||
{% if show_history %}
|
||||
<div>
|
||||
History: {{case_detail.get_history_pre|linebreaks}}
|
||||
History: {{casedetail.get_history_pre|linebreaks}}
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -66,9 +66,9 @@
|
||||
<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{% if case_number %}{% url 'atlas:collection_case_dicom_json_review' collection.pk case_number=case_number %}{% else %}{% url 'atlas:collection_case_dicom_json_review_legacy' collection.pk case.pk %}{% endif %}"></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{% if case_number %}{% url 'atlas:collection_case_dicom_json' collection.pk case_number=case_number %}{% else %}{% url 'atlas:collection_case_dicom_json_legacy' collection.pk case.pk %}{% endif %}"></iframe>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
@@ -78,9 +78,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{% if case_number %}{% url 'atlas:collection_case_dicom_json_review' collection.pk case_number=case_number %}{% else %}{% url 'atlas:collection_case_dicom_json_review_legacy' collection.pk case.pk %}{% endif %}" 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{% if case_number %}{% url 'atlas:collection_case_dicom_json' collection.pk case_number=case_number %}{% else %}{% url 'atlas:collection_case_dicom_json_legacy' collection.pk case.pk %}{% endif %}" 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 %}
|
||||
@@ -92,7 +92,7 @@
|
||||
<div id="main_viewer" class="dicom-viewer-root"
|
||||
style="box-sizing: border-box; background: #222; width: 100%; height: 600px;"
|
||||
data-auto-cache-stack="false"
|
||||
data-named-stacks='{{case_detail.get_case_named_stacks}}'
|
||||
data-named-stacks='{{casedetail.get_case_named_stacks}}'
|
||||
></div>
|
||||
</details>
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h2>Case: {{ case.title }}</h2>
|
||||
{{ case_detail.question_schema }}
|
||||
<h2>Case: {{ case.title }}</h2>
|
||||
{{ casedetail.question_schema }}
|
||||
|
||||
|
||||
{{ case.question_answers }}
|
||||
{{ case.question_answers }}
|
||||
|
||||
{% endblock %}
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="vstack gap-3">
|
||||
{% for case_detail, answers in case_map.items %}
|
||||
{% for casedetail, answers in case_map.items %}
|
||||
<div class="border border-dark">
|
||||
<h4>Case: {{case_detail.case.title}}</h4>
|
||||
<h4>Case: {{casedetail.case.title}}</h4>
|
||||
Answers
|
||||
<ul>
|
||||
{% for answer in answers %}
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
{% comment %} <script src="{% static 'js/jquery-3.7.1.min.js' %}"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@json-editor/json-editor@latest/dist/jsoneditor.min.js"></script> {% endcomment %}
|
||||
<ol>
|
||||
{% for case_detail in case_details %}
|
||||
{% for casedetail in casedetails %}
|
||||
|
||||
<li id="case-detail-{{case_detail.pk}}"{% if case_detail.question_schema %} class="cases"{% endif %}>
|
||||
<h2>Case: {{ case_detail.case.title }}</h2>
|
||||
<li id="case-detail-{{casedetail.pk}}"{% if casedetail.question_schema %} class="cases"{% endif %}>
|
||||
<h2>Case: {{ casedetail.case.title }}</h2>
|
||||
|
||||
{% if case_detail.question_schema %}
|
||||
{% if casedetail.question_schema %}
|
||||
<button class="btn btn-sm btn-secondary use-layout">Use schema</button>
|
||||
<details><summary>Schema:</summary>
|
||||
<div class="schema" data-schema='{{ case_detail.get_question_schema }}'>{{ case_detail.question_schema }}</div>
|
||||
<div class="schema" data-schema='{{ casedetail.get_question_schema }}'>{{ casedetail.question_schema }}</div>
|
||||
</details>
|
||||
<div class="form"></div>
|
||||
{% else %}
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
</div>
|
||||
{# Hidden per-case question snippet to inject when case is loaded #}
|
||||
<div class="question-block-snippet" style="display:none;">
|
||||
{% include 'atlas/partials/collection_question_block.html' with case_detail=casedetail %}
|
||||
{% include 'atlas/partials/collection_question_block.html' with casedetail=casedetail %}
|
||||
</div>
|
||||
{% if case.display_sets.all %}
|
||||
<details class="displayset-detail"><summary>Display Sets:</summary>
|
||||
@@ -212,14 +212,14 @@
|
||||
let c = this.closest(".case-item");
|
||||
console.log('clicked local', c)
|
||||
console.log('open local', c.dataset.case)
|
||||
case_details = JSON.parse(c.dataset.casejson);
|
||||
casedetails = JSON.parse(c.dataset.casejson);
|
||||
let viewerstate = JSON.parse(c.dataset.viewerstate) || {};
|
||||
$('#loading-case').show()
|
||||
$("#open-viewer-local").addClass("flash-button");
|
||||
$('#current-case-title').html("<span class='title'>Case:</span> "+c.dataset.title);
|
||||
$("#current-case-history").html("<span class='title'>History:</span> "+case_details['history']);
|
||||
$("#current-case-discussion").html("<span class='title'>Discussion:</span> "+case_details['discussion']);
|
||||
$("#current-case-report").html("<span class='title'>Report:</span> "+case_details['report']);
|
||||
$("#current-case-history").html("<span class='title'>History:</span> "+casedetails['history']);
|
||||
$("#current-case-discussion").html("<span class='title'>Discussion:</span> "+casedetails['discussion']);
|
||||
$("#current-case-report").html("<span class='title'>Report:</span> "+casedetails['report']);
|
||||
|
||||
bc.postMessage({"type": "open",
|
||||
"case" : c.dataset.case, "images": c.dataset.images,
|
||||
@@ -239,15 +239,15 @@
|
||||
let c = this;
|
||||
console.log('clicked local', c)
|
||||
console.log('open local', c.dataset.case)
|
||||
case_details = JSON.parse(c.dataset.casejson);
|
||||
casedetails = JSON.parse(c.dataset.casejson);
|
||||
let viewerstate = JSON.parse(c.dataset.viewerstate) || {};
|
||||
let annotations = JSON.parse(c.dataset.annotations) || {};
|
||||
$('#loading-case').show()
|
||||
$("#open-viewer-local").addClass("flash-button");
|
||||
$('#current-case-title').html("<span class='title'>Case:</span> "+c.dataset.title);
|
||||
$("#current-case-history").html("<span class='title'>History:</span> "+case_details['history']);
|
||||
$("#current-case-discussion").html("<span class='title'>Discussion:</span> "+case_details['discussion']);
|
||||
$("#current-case-report").html("<span class='title'>Report:</span> "+case_details['report']);
|
||||
$("#current-case-history").html("<span class='title'>History:</span> "+casedetails['history']);
|
||||
$("#current-case-discussion").html("<span class='title'>Discussion:</span> "+casedetails['discussion']);
|
||||
$("#current-case-report").html("<span class='title'>Report:</span> "+casedetails['report']);
|
||||
|
||||
bc.postMessage({"type": "open",
|
||||
"case" : c.dataset.case, "images": c.dataset.images,
|
||||
@@ -302,15 +302,15 @@
|
||||
|
||||
let c = $(this).closest(".case-item");
|
||||
|
||||
case_details = c.data('casejson');
|
||||
console.log(case_details);
|
||||
casedetails = c.data('casejson');
|
||||
console.log(casedetails);
|
||||
|
||||
$('#loading-case').show()
|
||||
$("#open-viewer").addClass("flash-button");
|
||||
$('#current-case-title').html("<span class='title'>Case:</span> "+c.data('title'));
|
||||
$("#current-case-history").html("<span class='title'>History:</span> "+case_details['history']);
|
||||
$("#current-case-discussion").html("<span class='title'>Discussion:</span> "+case_details['discussion']);
|
||||
$("#current-case-report").html("<span class='title'>Report:</span> "+case_details['report']);
|
||||
$("#current-case-history").html("<span class='title'>History:</span> "+casedetails['history']);
|
||||
$("#current-case-discussion").html("<span class='title'>Discussion:</span> "+casedetails['discussion']);
|
||||
$("#current-case-report").html("<span class='title'>Report:</span> "+casedetails['report']);
|
||||
|
||||
console.log($(this).data('type'))
|
||||
if ($(this).data('type') == 'case') {
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
<div id="prior-card-{{case.id}}" class="col-12 col-md-6 col-lg-4" data-case="{{case.id}}">
|
||||
<div class="card h-100 shadow-sm {% if added %}border-success{% else %}border-secondary{% endif %}">
|
||||
<div class="card-body d-flex flex-column">
|
||||
{% if error %}
|
||||
<div class="alert alert-danger py-1 mb-2 small">{{ error }}</div>
|
||||
{% endif %}
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<h5 class="card-title mb-0">{{case.title}}</h5>
|
||||
<div>
|
||||
{% if added %}
|
||||
<span class="badge bg-success">Added</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Available</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="card-text small text-muted mb-2">Relation: <span class="fw-medium">{{ relation }}</span></p>
|
||||
|
||||
<div class="mb-3 small">{{case.get_series_blocks|safe}}</div>
|
||||
|
||||
<div class="mt-auto">
|
||||
<div class="d-flex gap-2 mb-2">
|
||||
<a href="{% url 'atlas:case_detail' case.id %}" class="btn btn-sm btn-outline-primary">View case</a>
|
||||
</div>
|
||||
|
||||
{% if not added %}
|
||||
<form class="row g-2"
|
||||
{% if case_number %}
|
||||
hx-post="{% url 'atlas:collection_case_priors' collection.id case_number=case_number %}"
|
||||
{% else %}
|
||||
hx-post="{% url 'atlas:collection_case_priors_legacy' collection.id casedetail.case.pk %}"
|
||||
{% endif %}
|
||||
hx-target="#prior-card-{{case.id}}" hx-swap="outerHTML">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="prior_case_id" value="{{case.id}}" />
|
||||
<div class="col-12">
|
||||
<input class="form-control form-control-sm" type="text" name="relation" value="{{relation}}" placeholder="Relation to case" />
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<select class="form-select form-select-sm" name="prior_visibility" id="prior_visibility_{{case.id}}">
|
||||
<option value="AL" {% if visibility == 'AL' %}selected{% endif %}>Always</option>
|
||||
<option value="RE" {% if visibility == 'RE' %}selected{% endif %}>Review</option>
|
||||
<option value="NO" {% if visibility == 'NO' %}selected{% endif %}>None</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-4 d-grid">
|
||||
<button class="btn btn-sm btn-primary" type="submit">Add</button>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<form
|
||||
{% if case_number %}
|
||||
hx-post="{% url 'atlas:collection_case_priors' collection.id case_number=case_number %}"
|
||||
{% else %}
|
||||
hx-post="{% url 'atlas:collection_case_priors_legacy' collection.id casedetail.case.pk %}"
|
||||
{% endif %}
|
||||
hx-target="#prior-card-{{case.id}}" hx-swap="outerHTML">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="remove" value="{{case.id}}" />
|
||||
<div class="mb-2 small text-muted">Current relation: <strong>{{relation}}</strong></div>
|
||||
<div class="d-flex gap-2">
|
||||
<select class="form-select form-select-sm w-50" name="prior_visibility" id="prior_visibility_{{case.id}}" disabled>
|
||||
<option value="AL" {% if visibility == 'AL' %}selected{% endif %}>Always</option>
|
||||
<option value="RE" {% if visibility == 'RE' %}selected{% endif %}>Review</option>
|
||||
<option value="NO" {% if visibility == 'NO' %}selected{% endif %}>None</option>
|
||||
</select>
|
||||
<button class="btn btn-sm btn-danger" type="submit">Remove</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer bg-transparent">
|
||||
<small class="text-muted">Case ID: {{ case.id }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,24 @@
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
{% if previous %}
|
||||
<a class="btn btn-outline-secondary btn-sm" href="{% url nav_link_view collection.id case_number|add:-1 %}">← Previous</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="h6 mb-0">Viewing as part of a collection</div>
|
||||
<div>
|
||||
<a href="{% url 'atlas:collection_detail' collection.id %}" class="fw-bold">{{collection.name}}</a>
|
||||
<small class="text-muted">[{{case_number|add:1}}/{{collection_length}}]</small>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{% if next %}
|
||||
<a class="btn btn-outline-secondary btn-sm" href="{% url nav_link_view collection.id case_number|add:1 %}">Next →</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mb-1">
|
||||
{% include "atlas/collection_detail.html#casedetails-management-links" %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<div class="mt-2 small text-muted">
|
||||
{% if casedetail.case.series.count == 0 %}
|
||||
<span class="text-warning">No series attached</span>
|
||||
<span class="text-warning">This case has no series / stacks attached</span>
|
||||
{% else %}
|
||||
{{ casedetail.case.series.count }} series
|
||||
{% endif %}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<div class="btn-group btn-group-sm me-2" role="group" aria-label="management-actions">
|
||||
<a class="btn btn-outline-secondary" href="{% url 'atlas:collection_case_displaysetup' casedetail.collection.pk casedetail.case.pk %}" title="Setup default display">
|
||||
<a class="btn btn-outline-secondary" href="{% url 'atlas:collection_case_displaysetup_legacy' casedetail.collection.pk casedetail.case.pk %}" title="Setup default display">
|
||||
<i class="bi bi-display"></i>
|
||||
</a>
|
||||
|
||||
<a class="btn btn-outline-secondary" href="{% url 'atlas:collection_case_details' casedetail.collection.pk casedetail.case.pk %}" title="Case details">
|
||||
<a class="btn btn-outline-secondary" href="{% url 'atlas:collection_case_details_legacy' casedetail.collection.pk casedetail.case.pk %}" title="Case details">
|
||||
<i class="bi bi-info-square"></i>
|
||||
</a>
|
||||
|
||||
{% if collection.collection_type == "QUE" %}
|
||||
<a class="btn btn-outline-secondary" href='{% url "atlas:collection_case_questions" casedetail.collection.pk casedetail.case.pk %}' title="Manage questions">
|
||||
<a class="btn btn-outline-secondary" href='{% url "atlas:collection_case_questions_legacy" casedetail.collection.pk casedetail.case.pk %}' title="Manage questions">
|
||||
{% if casedetail.question_schema %}
|
||||
<i class="bi bi-question-square text-success"></i>
|
||||
{% else %}
|
||||
@@ -18,7 +18,7 @@
|
||||
{% endif %}
|
||||
|
||||
{% if casedetail.case.previous_case %}
|
||||
<a class="btn btn-outline-secondary" href='{% url "atlas:collection_case_priors" casedetail.collection.pk casedetail.case.pk %}' title="Manage priors">
|
||||
<a class="btn btn-outline-secondary" href='{% url "atlas:collection_case_priors_legacy" casedetail.collection.pk casedetail.case.pk %}' title="Manage priors">
|
||||
<i class="bi bi-link-45deg"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{# Partial: render questions and answers as HTML. #}
|
||||
|
||||
{% if case_detail.question_schema %}
|
||||
{% if casedetail.question_schema %}
|
||||
<div class="collection-question-block">
|
||||
<h4>Questions</h4>
|
||||
<dl class="row">
|
||||
{% for name, prop in case_detail.question_schema.properties.items %}
|
||||
{% for name, prop in casedetail.question_schema.properties.items %}
|
||||
<dt class="col-sm-4">{{ prop.title|default:name }}</dt>
|
||||
<dd class="col-sm-8">
|
||||
{% if prop.description %}
|
||||
@@ -18,8 +18,8 @@
|
||||
{% endif %}
|
||||
|
||||
{# Safely fetch the stored/example answer using the project's `get_item` filter. #}
|
||||
{% if case_detail.question_answers %}
|
||||
{% with correct=case_detail.question_answers|get_item:name %}
|
||||
{% if casedetail.question_answers %}
|
||||
{% with correct=casedetail.question_answers|get_item:name %}
|
||||
<div class="mt-2">
|
||||
<strong>Example / Correct:</strong>
|
||||
{% if correct %}
|
||||
@@ -37,7 +37,7 @@
|
||||
{% if user_answer %}
|
||||
{% if user_answer|get_item:name %}
|
||||
{% with ua=user_answer|get_item:name %}
|
||||
{% with correct=case_detail.question_answers|get_item:name %}
|
||||
{% with correct=casedetail.question_answers|get_item:name %}
|
||||
<div class="mt-1">
|
||||
<strong>Your answer:</strong>
|
||||
<span class="ms-2 {% if ua == correct %}text-success{% else %}text-danger{% endif %}">{{ ua }}</span>
|
||||
|
||||
@@ -18,22 +18,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if collection %}
|
||||
<div class="text-end mb-2">
|
||||
<div class="small text-muted">Collection</div>
|
||||
<div class="mb-1">
|
||||
{% include "atlas/collection_detail.html#casedetails-management-links" %}
|
||||
</div>
|
||||
<div class="small">
|
||||
{% if previous %}
|
||||
<a class="me-2" href="{% url 'atlas:collection_case_view' collection.id case_number|add:-1 %}">Previous</a>
|
||||
{% endif %}
|
||||
Viewing question as part of a collection: <a href="{% url 'atlas:collection_detail' collection.id %}">{{ collection.name }}</a>
|
||||
[{{ case_number|add:1 }} / {{ collection_length }}]
|
||||
{% if next %}
|
||||
<a class="ms-2" href="{% url 'atlas:collection_case_view' collection.id case_number|add:1 %}">Next</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if collection %}
|
||||
{% include 'atlas/partials/_viewing_case_as_part_of_collection.html' with nav_link_view="atlas:collection_case_view" %}
|
||||
{% endif %}
|
||||
+58
-6
@@ -168,30 +168,56 @@ urlpatterns = [
|
||||
),
|
||||
path("exam/<int:pk>/groups", views.ExamGroupsUpdate.as_view(), name="exam_groups_edit"),
|
||||
path(
|
||||
"collection/<int:exam_id>/case/<int:case_id>/details",
|
||||
"collection/<int:exam_id>/case/<int:case_number>/details",
|
||||
views.collection_case_details,
|
||||
name="collection_case_details",
|
||||
),
|
||||
# Backwards-compatible redirect from legacy case_id-style URLs
|
||||
path(
|
||||
"collection/<int:exam_id>/case/<int:case_id>/questions",
|
||||
"collection/<int:exam_id>/case/id/<int:case_id>/details",
|
||||
views.redirect_collection_case_details_by_id,
|
||||
name="collection_case_details_legacy",
|
||||
),
|
||||
path(
|
||||
"collection/<int:exam_id>/case/<int:case_number>/questions",
|
||||
views.collection_case_questions,
|
||||
name="collection_case_questions",
|
||||
),
|
||||
path(
|
||||
"collection/<int:exam_id>/case/<int:case_id>/priors",
|
||||
"collection/<int:exam_id>/case/id/<int:case_id>/questions",
|
||||
views.redirect_collection_case_questions_by_id,
|
||||
name="collection_case_questions_legacy",
|
||||
),
|
||||
path(
|
||||
"collection/<int:exam_id>/case/<int:case_number>/priors",
|
||||
views.collection_case_priors,
|
||||
name="collection_case_priors",
|
||||
),
|
||||
path(
|
||||
"collection/<int:exam_id>/case/<int:case_id>/dicom_json",
|
||||
"collection/<int:exam_id>/case/id/<int:case_id>/priors",
|
||||
views.redirect_collection_case_priors_by_id,
|
||||
name="collection_case_priors_legacy",
|
||||
),
|
||||
path(
|
||||
"collection/<int:exam_id>/case/<int:case_number>/dicom_json",
|
||||
views.collection_case_dicom_json,
|
||||
name="collection_case_dicom_json",
|
||||
),
|
||||
path(
|
||||
"collection/<int:exam_id>/case/<int:case_id>/dicom_json/review",
|
||||
"collection/<int:exam_id>/case/id/<int:case_id>/dicom_json",
|
||||
views.redirect_collection_case_dicom_json_by_id,
|
||||
name="collection_case_dicom_json_legacy",
|
||||
),
|
||||
path(
|
||||
"collection/<int:exam_id>/case/<int:case_number>/dicom_json/review",
|
||||
views.collection_case_dicom_json_review,
|
||||
name="collection_case_dicom_json_review",
|
||||
),
|
||||
path(
|
||||
"collection/<int:exam_id>/case/id/<int:case_id>/dicom_json/review",
|
||||
views.redirect_collection_case_dicom_json_review_by_id,
|
||||
name="collection_case_dicom_json_review_legacy",
|
||||
),
|
||||
path(
|
||||
"collection/<int:exam_id>/question_schemas",
|
||||
views.collection_question_schemas,
|
||||
@@ -272,26 +298,52 @@ urlpatterns = [
|
||||
views.collection_case_view,
|
||||
name="collection_case_view",
|
||||
),
|
||||
# legacy path that used case PK; redirect to canonical case_number URL
|
||||
path(
|
||||
"collection/<int:collection_id>/<int:case_id>/display_setup",
|
||||
"collection/<int:pk>/id/<int:case_id>",
|
||||
views.redirect_collection_case_view_by_id,
|
||||
name="collection_case_view_legacy",
|
||||
),
|
||||
path(
|
||||
"collection/<int:collection_id>/<int:case_number>/display_setup",
|
||||
views.collection_case_displaysetup,
|
||||
name="collection_case_displaysetup",
|
||||
),
|
||||
path(
|
||||
"collection/<int:collection_id>/id/<int:case_id>/display_setup",
|
||||
views.redirect_collection_case_displaysetup_by_id,
|
||||
name="collection_case_displaysetup_legacy",
|
||||
),
|
||||
path(
|
||||
"collection/<int:pk>/<int:case_number>/take/<int:cid>/<str:passcode>",
|
||||
views.collection_case_view_take,
|
||||
name="collection_case_view_take",
|
||||
),
|
||||
path(
|
||||
"collection/<int:pk>/id/<int:case_id>/take/<int:cid>/<str:passcode>",
|
||||
views.redirect_collection_case_view_take_by_id,
|
||||
name="collection_case_view_take_legacy",
|
||||
),
|
||||
path(
|
||||
"collection/<int:pk>/<int:case_number>/take/",
|
||||
views.collection_case_view_take_user,
|
||||
name="collection_case_view_take_user",
|
||||
),
|
||||
path(
|
||||
"collection/<int:pk>/id/<int:case_id>/take/",
|
||||
views.redirect_collection_case_view_take_user_by_id,
|
||||
name="collection_case_view_take_user_legacy",
|
||||
),
|
||||
path(
|
||||
"collection/<int:pk>/<int:case_number>/take/answers",
|
||||
views.collection_case_view_take_user_answers,
|
||||
name="collection_case_view_take_user_answers",
|
||||
),
|
||||
path(
|
||||
"collection/<int:pk>/id/<int:case_id>/take/answers",
|
||||
views.redirect_collection_case_view_take_user_answers_by_id,
|
||||
name="collection_case_view_take_user_answers_legacy",
|
||||
),
|
||||
path(
|
||||
"collection/<int:pk>/json_edit",
|
||||
views.GenericExamViews.exam_json_edit,
|
||||
|
||||
+354
-128
@@ -2755,46 +2755,184 @@ def collection_take_start(request, pk, cid=None, passcode=None):
|
||||
)
|
||||
|
||||
|
||||
def _case_number_for_collection_and_caseid(collection_pk, case_id):
|
||||
"""Helper: given a collection PK and a case PK, return the case index (case_number).
|
||||
|
||||
Raises Http404 if the case is not found in the collection.
|
||||
"""
|
||||
collection = get_object_or_404(CaseCollection, pk=collection_pk)
|
||||
try:
|
||||
# attempt to resolve the case object (may raise Http404)
|
||||
case_obj = get_case_for_case_detail(case_id)
|
||||
except Http404:
|
||||
raise
|
||||
try:
|
||||
return collection.get_index_of_case(case_obj)
|
||||
except ValueError:
|
||||
raise Http404(f"Case {case_id} not in collection {collection_pk}")
|
||||
|
||||
|
||||
@login_required
|
||||
def redirect_collection_case_details_by_id(request, exam_id, case_id):
|
||||
case_number = _case_number_for_collection_and_caseid(exam_id, case_id)
|
||||
return redirect("atlas:collection_case_details", exam_id=exam_id, case_number=case_number)
|
||||
|
||||
|
||||
@login_required
|
||||
def redirect_collection_case_questions_by_id(request, exam_id, case_id):
|
||||
case_number = _case_number_for_collection_and_caseid(exam_id, case_id)
|
||||
return redirect("atlas:collection_case_questions", exam_id=exam_id, case_number=case_number)
|
||||
|
||||
|
||||
@login_required
|
||||
def redirect_collection_case_priors_by_id(request, exam_id, case_id):
|
||||
case_number = _case_number_for_collection_and_caseid(exam_id, case_id)
|
||||
return redirect("atlas:collection_case_priors", exam_id=exam_id, case_number=case_number)
|
||||
|
||||
|
||||
@login_required
|
||||
def redirect_collection_case_dicom_json_by_id(request, exam_id, case_id):
|
||||
case_number = _case_number_for_collection_and_caseid(exam_id, case_id)
|
||||
return redirect("atlas:collection_case_dicom_json", exam_id=exam_id, case_number=case_number)
|
||||
|
||||
|
||||
@login_required
|
||||
def redirect_collection_case_dicom_json_review_by_id(request, exam_id, case_id):
|
||||
case_number = _case_number_for_collection_and_caseid(exam_id, case_id)
|
||||
return redirect("atlas:collection_case_dicom_json_review", exam_id=exam_id, case_number=case_number)
|
||||
|
||||
|
||||
@login_required
|
||||
def redirect_collection_case_view_by_id(request, pk, case_id):
|
||||
case_number = _case_number_for_collection_and_caseid(pk, case_id)
|
||||
return redirect("atlas:collection_case_view", pk=pk, case_number=case_number)
|
||||
|
||||
|
||||
@login_required
|
||||
def redirect_collection_case_displaysetup_by_id(request, collection_id, case_id):
|
||||
case_number = _case_number_for_collection_and_caseid(collection_id, case_id)
|
||||
return redirect("atlas:collection_case_displaysetup", collection_id=collection_id, case_number=case_number)
|
||||
|
||||
|
||||
@login_required
|
||||
def redirect_collection_case_view_take_by_id(request, pk, case_id, cid, passcode):
|
||||
case_number = _case_number_for_collection_and_caseid(pk, case_id)
|
||||
return redirect("atlas:collection_case_view_take", pk=pk, case_number=case_number, cid=cid, passcode=passcode)
|
||||
|
||||
|
||||
@login_required
|
||||
def redirect_collection_case_view_take_user_by_id(request, pk, case_id):
|
||||
case_number = _case_number_for_collection_and_caseid(pk, case_id)
|
||||
return redirect("atlas:collection_case_view_take_user", pk=pk, case_number=case_number)
|
||||
|
||||
|
||||
@login_required
|
||||
def redirect_collection_case_view_take_user_answers_by_id(request, pk, case_id):
|
||||
case_number = _case_number_for_collection_and_caseid(pk, case_id)
|
||||
return redirect("atlas:collection_case_view_take_user_answers", pk=pk, case_number=case_number)
|
||||
|
||||
|
||||
|
||||
@user_is_collection_author_or_atlas_editor
|
||||
def collection_case_priors(request, exam_id, case_id):
|
||||
case_detail = CaseDetail.objects.get(case=case_id, collection=exam_id)
|
||||
def collection_case_priors(request, exam_id, case_number):
|
||||
collection = get_object_or_404(CaseCollection, pk=exam_id)
|
||||
try:
|
||||
case_obj = collection.get_case_by_index(case_number)
|
||||
casedetail = CaseDetail.objects.get(case=case_obj, collection=collection)
|
||||
except Exception:
|
||||
raise Http404("Case not found in collection")
|
||||
|
||||
if request.htmx:
|
||||
# Ensure we can render the updated single-card partial and return it so HTMX
|
||||
# can swap the card on the client side.
|
||||
|
||||
if "remove" in request.POST:
|
||||
p = CasePrior.objects.get(
|
||||
case_detail=case_detail, prior_case=request.POST["remove"]
|
||||
prior_pk = request.POST["remove"]
|
||||
try:
|
||||
p = CasePrior.objects.get(casedetail=casedetail, prior_case=prior_pk)
|
||||
p.delete()
|
||||
except CasePrior.DoesNotExist:
|
||||
# Already removed; continue and render the not-added card
|
||||
pass
|
||||
|
||||
prior_case = Case.objects.get(pk=prior_pk)
|
||||
added = False
|
||||
relation = ""
|
||||
visibility = "AL"
|
||||
|
||||
html = render_to_string(
|
||||
"atlas/partials/_prior_card.html",
|
||||
{
|
||||
"case": prior_case,
|
||||
"added": added,
|
||||
"relation": relation,
|
||||
"visibility": visibility,
|
||||
"casedetail": casedetail,
|
||||
"collection": collection,
|
||||
},
|
||||
request=request,
|
||||
)
|
||||
p.delete()
|
||||
return HttpResponse(f"Case removed")
|
||||
return HttpResponse(html)
|
||||
elif "prior_case_id" in request.POST:
|
||||
if not request.POST["relation"]:
|
||||
return HttpResponse(
|
||||
"You need to enter text to describe the relationship between the cases"
|
||||
if not request.POST.get("relation"):
|
||||
# Return the card partial with an inline error so HTMX will replace
|
||||
# the card and display the validation message rather than navigating
|
||||
# to a new page or showing a plain text response.
|
||||
prior_case = Case.objects.get(pk=request.POST.get("prior_case_id"))
|
||||
added = False
|
||||
relation = request.POST.get("relation", "")
|
||||
visibility = request.POST.get("prior_visibility", "AL")
|
||||
html = render_to_string(
|
||||
"atlas/partials/_prior_card.html",
|
||||
{
|
||||
"case": prior_case,
|
||||
"added": added,
|
||||
"relation": relation,
|
||||
"visibility": visibility,
|
||||
"casedetail": casedetail,
|
||||
"collection": collection,
|
||||
"error": "You need to enter text to describe the relationship between the cases",
|
||||
},
|
||||
request=request,
|
||||
)
|
||||
return HttpResponse(html)
|
||||
prior_case = Case.objects.get(pk=request.POST["prior_case_id"])
|
||||
p, created = CasePrior.objects.get_or_create(
|
||||
case_detail=case_detail, prior_case=prior_case
|
||||
casedetail=casedetail, prior_case=prior_case
|
||||
)
|
||||
p.relation_text = request.POST["relation"]
|
||||
|
||||
p.prior_visibility = request.POST["prior_visibility"]
|
||||
|
||||
p.relation_text = request.POST.get("relation", "")
|
||||
p.prior_visibility = request.POST.get("prior_visibility", "AL")
|
||||
p.save()
|
||||
|
||||
return HttpResponse(f"Case added")
|
||||
added = True
|
||||
relation = p.relation_text
|
||||
visibility = p.prior_visibility
|
||||
|
||||
html = render_to_string(
|
||||
"atlas/partials/_prior_card.html",
|
||||
{
|
||||
"case": prior_case,
|
||||
"added": added,
|
||||
"relation": relation,
|
||||
"visibility": visibility,
|
||||
"casedetail": casedetail,
|
||||
"collection": collection,
|
||||
},
|
||||
request=request,
|
||||
)
|
||||
return HttpResponse(html)
|
||||
else:
|
||||
return HttpResponse("False")
|
||||
raise Http404()
|
||||
return HttpResponse("False", status=400)
|
||||
|
||||
collection = case_detail.collection
|
||||
collection = casedetail.collection
|
||||
|
||||
added_priors = CasePrior.objects.filter(case_detail=case_detail)
|
||||
added_priors = CasePrior.objects.filter(casedetail=casedetail)
|
||||
|
||||
added_relation_map = {}
|
||||
for prior in added_priors:
|
||||
added_relation_map[prior.prior_case] = (prior.relation_text, prior.prior_visibility)
|
||||
|
||||
case = case_detail.case
|
||||
case = casedetail.case
|
||||
|
||||
available_cases = case.get_all_prior_cases()
|
||||
|
||||
@@ -2808,22 +2946,22 @@ def collection_case_priors(request, exam_id, case_id):
|
||||
form = PriorCaseForm()
|
||||
|
||||
case_number, case_count = collection.get_index_of_case(
|
||||
case_detail.case, case_count=True
|
||||
casedetail.case, case_count=True
|
||||
)
|
||||
|
||||
previous = collection.get_previous_case(case_detail.case)
|
||||
next = collection.get_next_case(case_detail.case)
|
||||
previous = collection.get_previous_case(casedetail.case)
|
||||
next = collection.get_next_case(casedetail.case)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"atlas/collection_case_priors.html",
|
||||
{
|
||||
"case_detail": case_detail,
|
||||
"casedetail": casedetail,
|
||||
"form": form,
|
||||
#"model_form": model_form,
|
||||
# "example_form": example_form,
|
||||
"collection": collection,
|
||||
"case": case_detail.case,
|
||||
"case": casedetail.case,
|
||||
"previous": previous,
|
||||
"next": next,
|
||||
"collection_length": case_count,
|
||||
@@ -2836,9 +2974,17 @@ def collection_case_priors(request, exam_id, case_id):
|
||||
|
||||
@user_is_collection_author_or_atlas_editor
|
||||
def collection_case_questions(request, exam_id, case_id):
|
||||
case_detail = CaseDetail.objects.get(case=case_id, collection=exam_id)
|
||||
|
||||
collection = case_detail.collection
|
||||
# Support either case_number (index) or case_id (case PK)
|
||||
try:
|
||||
casedetail = CaseDetail.objects.get(case=case_id, collection=exam_id)
|
||||
collection = casedetail.collection
|
||||
except Exception:
|
||||
collection = get_object_or_404(CaseCollection, pk=exam_id)
|
||||
try:
|
||||
case_obj = collection.get_case_by_index(int(case_id))
|
||||
casedetail = CaseDetail.objects.get(case=case_obj, collection=collection)
|
||||
except Exception:
|
||||
raise Http404("Case not found in collection")
|
||||
|
||||
if not collection.collection_type == "QUE":
|
||||
raise Http404("Collection not in question mode")
|
||||
@@ -2847,29 +2993,29 @@ def collection_case_questions(request, exam_id, case_id):
|
||||
# Called if the user saves the correct answer form
|
||||
if request.POST.get("submit") == "answer":
|
||||
# answers = request.POST
|
||||
case_detail.question_answers = json.loads(request.POST.get("json_answer"))
|
||||
case_detail.save()
|
||||
casedetail.question_answers = json.loads(request.POST.get("json_answer"))
|
||||
casedetail.save()
|
||||
example_form = JsonAnswerForm(
|
||||
request.POST, question_schema=case_detail.question_schema
|
||||
request.POST, question_schema=casedetail.question_schema
|
||||
)
|
||||
form = CaseQuestionForm(instance=case_detail)
|
||||
form = CaseQuestionForm(instance=casedetail)
|
||||
# Called if the user saves the main form
|
||||
elif request.POST.get("submit") == "save":
|
||||
form = CaseQuestionForm(request.POST, instance=case_detail)
|
||||
form = CaseQuestionForm(request.POST, instance=casedetail)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
# Add any additional logic or redirection here
|
||||
# example_form = JsonAnswerForm(request.POST, question_schema=case_detail.question_schema)
|
||||
# example_form = JsonAnswerForm(request.POST, question_schema=casedetail.question_schema)
|
||||
post_data = request.POST.copy()
|
||||
if (
|
||||
case_detail.question_schema is not None
|
||||
and case_detail.question_answers is not None
|
||||
casedetail.question_schema is not None
|
||||
and casedetail.question_answers is not None
|
||||
):
|
||||
question_keys = set(case_detail.question_schema["properties"].keys())
|
||||
answer_keys = set(case_detail.question_answers.keys())
|
||||
question_keys = set(casedetail.question_schema["properties"].keys())
|
||||
answer_keys = set(casedetail.question_answers.keys())
|
||||
|
||||
answers = {}
|
||||
for key, value in case_detail.question_answers.items():
|
||||
for key, value in casedetail.question_answers.items():
|
||||
if key in question_keys:
|
||||
answers[key] = value
|
||||
|
||||
@@ -2879,35 +3025,35 @@ def collection_case_questions(request, exam_id, case_id):
|
||||
answers[key] = ""
|
||||
|
||||
post_data["json_answer"] = json.dumps(answers)
|
||||
case_detail.question_answers = answers
|
||||
case_detail.save()
|
||||
casedetail.question_answers = answers
|
||||
casedetail.save()
|
||||
example_form = JsonAnswerForm(
|
||||
post_data, question_schema=case_detail.question_schema
|
||||
post_data, question_schema=casedetail.question_schema
|
||||
)
|
||||
|
||||
form = CaseQuestionForm(instance=case_detail)
|
||||
form = CaseQuestionForm(instance=casedetail)
|
||||
# This shouldn't happen
|
||||
else:
|
||||
assert False
|
||||
form = CaseQuestionForm(request.POST, instance=case_detail)
|
||||
form = CaseQuestionForm(request.POST, instance=casedetail)
|
||||
|
||||
else:
|
||||
post_data = request.POST.copy()
|
||||
post_data["json_answer"] = json.dumps(case_detail.question_answers)
|
||||
post_data["json_answer"] = json.dumps(casedetail.question_answers)
|
||||
example_form = JsonAnswerForm(
|
||||
post_data, question_schema=case_detail.question_schema
|
||||
post_data, question_schema=casedetail.question_schema
|
||||
)
|
||||
|
||||
form = CaseQuestionForm(instance=case_detail)
|
||||
form = CaseQuestionForm(instance=casedetail)
|
||||
pass
|
||||
# post_data = None
|
||||
|
||||
# if case_detail.question_schema is not None and case_detail.question_answers is not None:
|
||||
# question_keys = set(case_detail.question_schema["properties"].keys())
|
||||
# answer_keys = set(case_detail.question_answers.keys())
|
||||
# if casedetail.question_schema is not None and casedetail.question_answers is not None:
|
||||
# question_keys = set(casedetail.question_schema["properties"].keys())
|
||||
# answer_keys = set(casedetail.question_answers.keys())
|
||||
|
||||
# answers = {}
|
||||
# for key, value in case_detail.question_answers.items():
|
||||
# for key, value in casedetail.question_answers.items():
|
||||
# if key in question_keys:
|
||||
# answers[key] = value
|
||||
|
||||
@@ -2918,28 +3064,28 @@ def collection_case_questions(request, exam_id, case_id):
|
||||
|
||||
# post_data = request.POST.copy()
|
||||
# post_data["json_answer"]= json.dumps(answers)
|
||||
# case_detail.question_answers = answers
|
||||
# example_form = JsonAnswerForm(post_data, question_schema=case_detail.question_schema)
|
||||
# casedetail.question_answers = answers
|
||||
# example_form = JsonAnswerForm(post_data, question_schema=casedetail.question_schema)
|
||||
|
||||
# form = CaseQuestionForm(instance=case_detail)
|
||||
# form = CaseQuestionForm(instance=casedetail)
|
||||
|
||||
# blank_form = JsonAnswerForm(question_schema=case_detail.question_schema)
|
||||
# blank_form = JsonAnswerForm(question_schema=casedetail.question_schema)
|
||||
case_number, case_count = collection.get_index_of_case(
|
||||
case_detail.case, case_count=True
|
||||
casedetail.case, case_count=True
|
||||
)
|
||||
|
||||
previous = collection.get_previous_case(case_detail.case)
|
||||
next = collection.get_next_case(case_detail.case)
|
||||
previous = collection.get_previous_case(casedetail.case)
|
||||
next = collection.get_next_case(casedetail.case)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"atlas/collection_case_questions.html",
|
||||
{
|
||||
"case_detail": case_detail,
|
||||
"casedetail": casedetail,
|
||||
"form": form,
|
||||
"example_form": example_form,
|
||||
"collection": collection,
|
||||
"case": case_detail.case,
|
||||
"case": casedetail.case,
|
||||
"previous": previous,
|
||||
"next": next,
|
||||
"collection_length": case_count,
|
||||
@@ -2949,12 +3095,22 @@ def collection_case_questions(request, exam_id, case_id):
|
||||
|
||||
@user_is_collection_author_or_atlas_editor
|
||||
def collection_case_details(request, exam_id, case_id):
|
||||
case_detail = CaseDetail.objects.get(case=case_id, collection=exam_id)
|
||||
|
||||
collection = case_detail.collection
|
||||
# Accept either case_number (index into collection) or case_id (case PK).
|
||||
casedetail = None
|
||||
collection = None
|
||||
try:
|
||||
casedetail = CaseDetail.objects.get(case=case_id, collection=exam_id)
|
||||
collection = casedetail.collection
|
||||
except Exception:
|
||||
collection = get_object_or_404(CaseCollection, pk=exam_id)
|
||||
try:
|
||||
case_obj = collection.get_case_by_index(int(case_id))
|
||||
casedetail = CaseDetail.objects.get(case=case_obj, collection=collection)
|
||||
except Exception:
|
||||
raise Http404("Case not found in collection")
|
||||
|
||||
if request.method == "POST":
|
||||
form = CaseDetailForm(request.POST, instance=case_detail)
|
||||
form = CaseDetailForm(request.POST, instance=casedetail)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
if request.htmx:
|
||||
@@ -2964,23 +3120,23 @@ def collection_case_details(request, exam_id, case_id):
|
||||
if request.htmx:
|
||||
return HttpResponse("Invalid form", status=400)
|
||||
else:
|
||||
form = CaseDetailForm(instance=case_detail)
|
||||
form = CaseDetailForm(instance=casedetail)
|
||||
|
||||
case_number, case_count = collection.get_index_of_case(
|
||||
case_detail.case, case_count=True
|
||||
casedetail.case, case_count=True
|
||||
)
|
||||
|
||||
previous = collection.get_previous_case(case_detail.case)
|
||||
next = collection.get_next_case(case_detail.case)
|
||||
previous = collection.get_previous_case(casedetail.case)
|
||||
next = collection.get_next_case(casedetail.case)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"atlas/collection_case_details.html",
|
||||
{
|
||||
"case_detail": case_detail,
|
||||
"casedetail": casedetail,
|
||||
"form": form,
|
||||
"collection": collection,
|
||||
"case": case_detail.case,
|
||||
"case": casedetail.case,
|
||||
"previous": previous,
|
||||
"next": next,
|
||||
"collection_length": case_count,
|
||||
@@ -2998,14 +3154,14 @@ def collection_mark_overview(request, pk):
|
||||
|
||||
cases = collection.cases.all().order_by("casedetail__sort_order").prefetch_related()
|
||||
|
||||
case_details = CaseDetail.objects.filter(
|
||||
casedetails = CaseDetail.objects.filter(
|
||||
case__in=cases, collection=collection
|
||||
).prefetch_related("case")
|
||||
|
||||
answers = CidReportAnswer.objects.filter(question__in=case_details)
|
||||
answers = CidReportAnswer.objects.filter(question__in=casedetails)
|
||||
|
||||
case_map = {}
|
||||
for cd in case_details:
|
||||
for cd in casedetails:
|
||||
case_map[cd] = []
|
||||
|
||||
for answer in answers:
|
||||
@@ -3017,7 +3173,7 @@ def collection_mark_overview(request, pk):
|
||||
{
|
||||
"collection": collection,
|
||||
"answers": answers,
|
||||
"case_details": case_details,
|
||||
"casedetails": casedetails,
|
||||
"case_map": case_map,
|
||||
},
|
||||
)
|
||||
@@ -3035,9 +3191,9 @@ def collection_mark_question(request, pk, case_number):
|
||||
previous = case_number > 0
|
||||
next = case_number < (case_count - 1)
|
||||
|
||||
case_detail = CaseDetail.objects.filter(case=case, collection=collection)
|
||||
casedetail = CaseDetail.objects.filter(case=case, collection=collection)
|
||||
|
||||
answers = CidReportAnswer.objects.filter(question__in=case_detail)
|
||||
answers = CidReportAnswer.objects.filter(question__in=casedetail)
|
||||
|
||||
answer_forms = []
|
||||
if request.method == "POST":
|
||||
@@ -3051,7 +3207,7 @@ def collection_mark_question(request, pk, case_number):
|
||||
if form.is_valid():
|
||||
answer = form.save(commit=False)
|
||||
# answer.cid = cid
|
||||
# answer.question = case_detail
|
||||
# answer.question = casedetail
|
||||
# answer.published_date = timezone.now()
|
||||
answer.save()
|
||||
|
||||
@@ -3089,7 +3245,7 @@ def collection_mark_question(request, pk, case_number):
|
||||
"collection": collection,
|
||||
"series_list": series_list,
|
||||
"answers": answers,
|
||||
"case_detail": case_detail,
|
||||
"casedetail": casedetail,
|
||||
"case": case,
|
||||
"case_number": case_number,
|
||||
"answer_forms": answer_forms,
|
||||
@@ -3172,13 +3328,13 @@ def collection_take_overview(
|
||||
|
||||
cases = collection.cases.all().order_by("casedetail__sort_order").prefetch_related()
|
||||
|
||||
case_details = CaseDetail.objects.filter(case__in=cases, collection=collection)
|
||||
casedetails = CaseDetail.objects.filter(case__in=cases, collection=collection)
|
||||
|
||||
if cid is not None:
|
||||
answers = CidReportAnswer.objects.filter(cid=cid, question__in=case_details)
|
||||
answers = CidReportAnswer.objects.filter(cid=cid, question__in=casedetails)
|
||||
else:
|
||||
answers = UserReportAnswer.objects.filter(
|
||||
user=request.user, question__in=case_details
|
||||
user=request.user, question__in=casedetails
|
||||
)
|
||||
|
||||
answer_question_map = {}
|
||||
@@ -3187,7 +3343,7 @@ def collection_take_overview(
|
||||
|
||||
question_answer_tuples = []
|
||||
answer_count = 0
|
||||
for cd in case_details:
|
||||
for cd in casedetails:
|
||||
self_review = cid_user_exam.selfreview_set.filter(case=cd.case)
|
||||
if cd in answer_question_map and (
|
||||
answer_question_map[cd].answer or answer_question_map[cd].json_answer
|
||||
@@ -3234,7 +3390,7 @@ def collection_case_view_take_answers(
|
||||
cid=cid, user_user=request.user
|
||||
)
|
||||
|
||||
case_detail = CaseDetail.objects.get(case=case, collection=collection)
|
||||
casedetail = CaseDetail.objects.get(case=case, collection=collection)
|
||||
|
||||
return render(
|
||||
request,
|
||||
@@ -3242,17 +3398,19 @@ def collection_case_view_take_answers(
|
||||
{
|
||||
"collection": collection,
|
||||
"case": case,
|
||||
"case_detail": case_detail,
|
||||
"casedetail": casedetail,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def collection_case_view_take_user(request, pk: int, case_number: int):
|
||||
return collection_case_view_take(request, pk, case_number)
|
||||
def collection_case_view_take_user(request, pk, case_number=None, case_id=None):
|
||||
# Accept either case_number (index) or case_id (case PK) and forward both to
|
||||
# the handler. This allows URLs that use either name to keep working.
|
||||
return collection_case_view_take(request, pk, case_number=case_number, cid=None, passcode=None, case_id=case_id)
|
||||
|
||||
|
||||
def collection_case_view_take(
|
||||
request, pk: int, case_number: int, cid=None, passcode=None
|
||||
request, pk, case_number=None, cid=None, passcode=None, case_id=None
|
||||
):
|
||||
"""
|
||||
Displays the page from which the user takes a case in the collection
|
||||
@@ -3272,6 +3430,21 @@ def collection_case_view_take(
|
||||
"""
|
||||
collection = get_object_or_404(CaseCollection, pk=pk)
|
||||
|
||||
# If caller provided a case_id (case PK) instead of a case_number (index),
|
||||
# translate it into the index used by collection.get_case_by_index().
|
||||
if case_id is not None and case_number is None:
|
||||
try:
|
||||
case_obj = get_case_for_case_detail(case_id)
|
||||
except Http404:
|
||||
raise
|
||||
try:
|
||||
case_number = collection.get_index_of_case(case_obj)
|
||||
except ValueError:
|
||||
raise Http404(f"Case {case_id} not in collection {pk}")
|
||||
# Ensure we have a case_number to work with
|
||||
if case_number is None:
|
||||
raise Http404("No case specified")
|
||||
|
||||
form = None
|
||||
answer: None | CidReportAnswer | UserReportAnswer = None
|
||||
|
||||
@@ -3291,23 +3464,23 @@ def collection_case_view_take(
|
||||
cid_user_exam.start_time = timezone.now()
|
||||
cid_user_exam.save()
|
||||
|
||||
case_detail = CaseDetail.objects.get(case=case, collection=collection)
|
||||
casedetail = CaseDetail.objects.get(case=case, collection=collection)
|
||||
if not collection.review_only():
|
||||
if collection.collection_type == "REP":
|
||||
if cid is not None:
|
||||
answer = case_detail.cidreportanswer_set.filter(cid=cid).first()
|
||||
answer = casedetail.cidreportanswer_set.filter(cid=cid).first()
|
||||
ReportAnswerForm = CidReportAnswerForm
|
||||
else:
|
||||
answer = case_detail.userreportanswer_set.filter(
|
||||
answer = casedetail.userreportanswer_set.filter(
|
||||
user=request.user
|
||||
).first()
|
||||
ReportAnswerForm = UserReportAnswerForm
|
||||
elif collection.collection_type == "QUE":
|
||||
if cid is not None:
|
||||
answer = case_detail.cidreportanswer_set.filter(cid=cid).first()
|
||||
answer = casedetail.cidreportanswer_set.filter(cid=cid).first()
|
||||
ReportAnswerForm = CidQuestionAnswerForm
|
||||
else:
|
||||
answer = case_detail.userreportanswer_set.filter(
|
||||
answer = casedetail.userreportanswer_set.filter(
|
||||
user=request.user
|
||||
).first()
|
||||
ReportAnswerForm = UserQuestionAnswerForm
|
||||
@@ -3325,9 +3498,9 @@ def collection_case_view_take(
|
||||
if not answer and collection.collection_type in ("REP", "QUE"):
|
||||
try:
|
||||
if cid is not None:
|
||||
answer = CidReportAnswer(question=case_detail, cid=cid)
|
||||
answer = CidReportAnswer(question=casedetail, cid=cid)
|
||||
else:
|
||||
answer = UserReportAnswer(question=case_detail, user=request.user)
|
||||
answer = UserReportAnswer(question=casedetail, user=request.user)
|
||||
answer.started_at = timezone.now()
|
||||
answer.save()
|
||||
except Exception:
|
||||
@@ -3340,10 +3513,10 @@ def collection_case_view_take(
|
||||
if not collection.publish_results:
|
||||
if answer:
|
||||
form = ReportAnswerForm(
|
||||
request.POST, instance=answer, case_detail=case_detail
|
||||
request.POST, instance=answer, casedetail=casedetail
|
||||
)
|
||||
else:
|
||||
form = ReportAnswerForm(request.POST, case_detail=case_detail)
|
||||
form = ReportAnswerForm(request.POST, casedetail=casedetail)
|
||||
|
||||
if not cid_user_exam.completed and not (
|
||||
answer is not None and answer.completed
|
||||
@@ -3351,7 +3524,7 @@ def collection_case_view_take(
|
||||
if form.is_valid():
|
||||
answer = form.save(commit=False)
|
||||
answer.set_cid_or_user(cid=cid, user=request.user)
|
||||
answer.question = case_detail
|
||||
answer.question = casedetail
|
||||
# Record submission timestamp
|
||||
answer.submitted_at = timezone.now()
|
||||
# answer.published_date = timezone.now()
|
||||
@@ -3422,7 +3595,7 @@ def collection_case_view_take(
|
||||
)
|
||||
else:
|
||||
if collection.collection_type in ("REP", "QUE"):
|
||||
form = ReportAnswerForm(instance=answer, case_detail=case_detail)
|
||||
form = ReportAnswerForm(instance=answer, casedetail=casedetail)
|
||||
|
||||
series_list = case.series.all().prefetch_related("images", "examination", "plane")
|
||||
|
||||
@@ -3461,7 +3634,7 @@ def collection_case_view_take(
|
||||
|
||||
resources = case.caseresource_set.filter(pre_review=True)
|
||||
|
||||
prior_cases = case_detail.caseprior_set.all()
|
||||
prior_cases = casedetail.caseprior_set.all()
|
||||
|
||||
series_to_load = []
|
||||
|
||||
@@ -3487,7 +3660,7 @@ def collection_case_view_take(
|
||||
"form": form,
|
||||
"collection": collection,
|
||||
"case": case,
|
||||
"case_detail": case_detail,
|
||||
"casedetail": casedetail,
|
||||
"series_list": series_list,
|
||||
"series_to_load": series_to_load,
|
||||
"case_number": case_number,
|
||||
@@ -3511,7 +3684,7 @@ def collection_case_view_take(
|
||||
|
||||
|
||||
@user_is_collection_author_or_atlas_editor
|
||||
def collection_case_view(request, pk, case_number):
|
||||
def collection_case_view(request, pk, case_number=None, case_id=None):
|
||||
"""Edit/management overview of the case (as part of the collection)
|
||||
|
||||
Args:
|
||||
@@ -3527,6 +3700,19 @@ def collection_case_view(request, pk, case_number):
|
||||
"""
|
||||
collection = get_object_or_404(CaseCollection, pk=pk)
|
||||
|
||||
# Support either case_number (index) or case_id (case PK) in the URL/kwargs.
|
||||
if case_id is not None and case_number is None:
|
||||
try:
|
||||
case_obj = get_case_for_case_detail(case_id)
|
||||
except Http404:
|
||||
raise
|
||||
try:
|
||||
case_number = collection.get_index_of_case(case_obj)
|
||||
except ValueError:
|
||||
raise Http404(f"Case {case_id} not in collection {pk}")
|
||||
if case_number is None:
|
||||
raise Http404("No case specified")
|
||||
|
||||
form = None
|
||||
answer = None
|
||||
|
||||
@@ -3538,8 +3724,8 @@ def collection_case_view(request, pk, case_number):
|
||||
|
||||
series_list = case.series.all().prefetch_related("images", "examination", "plane")
|
||||
|
||||
previous = case_number > 0
|
||||
next = case_number < (case_count - 1)
|
||||
previous = collection.get_previous_case(casedetail.case)
|
||||
next = collection.get_next_case(casedetail.case)
|
||||
|
||||
return render(
|
||||
request,
|
||||
@@ -3579,15 +3765,25 @@ def use_dates_as_descriptions(request, pk):
|
||||
def collection_case_dicom_json_review(request, exam_id, case_id):
|
||||
return collection_case_dicom_json(request, exam_id, case_id, review=True)
|
||||
|
||||
|
||||
def collection_case_dicom_json(request, exam_id, case_id, review=False):
|
||||
case_detail = CaseDetail.objects.get(case=case_id, collection=exam_id)
|
||||
# Support either case_number (index) or case_id (case PK)
|
||||
try:
|
||||
casedetail = CaseDetail.objects.get(case=case_id, collection=exam_id)
|
||||
except Exception:
|
||||
collection = get_object_or_404(CaseCollection, pk=exam_id)
|
||||
try:
|
||||
case_obj = collection.get_case_by_index(int(case_id))
|
||||
casedetail = CaseDetail.objects.get(case=case_obj, collection=collection)
|
||||
except Exception:
|
||||
raise Http404("Case not found in collection")
|
||||
|
||||
if review:
|
||||
priors = case_detail.caseprior_set.exclude(prior_visibility="NO")
|
||||
priors = casedetail.caseprior_set.exclude(prior_visibility="NO")
|
||||
else:
|
||||
priors = case_detail.caseprior_set.exclude(prior_visibility__in=["NO", "RE"])
|
||||
priors = casedetail.caseprior_set.exclude(prior_visibility__in=["NO", "RE"])
|
||||
|
||||
return JsonResponse(case_detail.case.get_case_dicom_json(priors=priors))
|
||||
return JsonResponse(casedetail.case.get_case_dicom_json(priors=priors))
|
||||
|
||||
|
||||
def case_dicom_json(request, pk):
|
||||
@@ -3608,12 +3804,12 @@ def delete_collection_cid_answers(request, exam_id, cid):
|
||||
|
||||
cases = collection.cases.all().order_by("casedetail__sort_order").prefetch_related()
|
||||
|
||||
case_details = CaseDetail.objects.filter(
|
||||
casedetails = CaseDetail.objects.filter(
|
||||
case__in=cases, collection=collection
|
||||
).prefetch_related("case")
|
||||
|
||||
cid_user_answers = CidReportAnswer.objects.filter(
|
||||
question__in=case_details, cid=cid
|
||||
question__in=casedetails, cid=cid
|
||||
)
|
||||
|
||||
temp = {}
|
||||
@@ -3630,11 +3826,11 @@ def delete_collection_answers(request, exam_id):
|
||||
|
||||
cases = collection.cases.all().order_by("casedetail__sort_order").prefetch_related()
|
||||
|
||||
case_details = CaseDetail.objects.filter(
|
||||
casedetails = CaseDetail.objects.filter(
|
||||
case__in=cases, collection=collection
|
||||
).prefetch_related("case")
|
||||
|
||||
cid_user_answers = CidReportAnswer.objects.filter(question__in=case_details)
|
||||
cid_user_answers = CidReportAnswer.objects.filter(question__in=casedetails)
|
||||
|
||||
temp = {}
|
||||
for ans in cid_user_answers:
|
||||
@@ -3663,11 +3859,11 @@ def collection_scores_cid(request, pk):
|
||||
|
||||
cases = collection.cases.all().order_by("casedetail__sort_order").prefetch_related()
|
||||
|
||||
case_details = CaseDetail.objects.filter(
|
||||
casedetails = CaseDetail.objects.filter(
|
||||
case__in=cases, collection=collection
|
||||
).prefetch_related("case")
|
||||
|
||||
cid_user_answers = CidReportAnswer.objects.filter(question__in=case_details)
|
||||
cid_user_answers = CidReportAnswer.objects.filter(question__in=casedetails)
|
||||
|
||||
cids = set()
|
||||
|
||||
@@ -4070,7 +4266,7 @@ def collection_question_schemas(request, exam_id: int):
|
||||
|
||||
questions = collection.cases.all().prefetch_related()
|
||||
|
||||
case_details = (
|
||||
casedetails = (
|
||||
CaseDetail.objects.filter(case__in=questions, collection=collection)
|
||||
.prefetch_related("case")
|
||||
.order_by("sort_order")
|
||||
@@ -4081,7 +4277,7 @@ def collection_question_schemas(request, exam_id: int):
|
||||
"atlas/collection_question_schemas.html",
|
||||
{
|
||||
"collection": collection,
|
||||
"case_details": case_details,
|
||||
"casedetails": casedetails,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -4199,10 +4395,10 @@ def collection_reset_answers_user(request, exam_id: int, user_id: int):
|
||||
collection = get_object_or_404(CaseCollection, pk=exam_id)
|
||||
|
||||
# Select all case details (answers are linked to these)
|
||||
case_details = collection.casedetail_set.all().prefetch_related()
|
||||
casedetails = collection.casedetail_set.all().prefetch_related()
|
||||
|
||||
# Delete all answers
|
||||
for case in case_details:
|
||||
for case in casedetails:
|
||||
user_answers = case.userreportanswer_set.filter(user_id=user_id)
|
||||
user_answers.delete()
|
||||
|
||||
@@ -4220,10 +4416,10 @@ def collection_reset_answers_ciduser(request, exam_id: int, cid: int):
|
||||
collection = get_object_or_404(CaseCollection, pk=exam_id)
|
||||
|
||||
# Select all case details (answers are linked to these)
|
||||
case_details = collection.casedetail_set.all().prefetch_related()
|
||||
casedetails = collection.casedetail_set.all().prefetch_related()
|
||||
|
||||
# Delete all answers
|
||||
for case in case_details:
|
||||
for case in casedetails:
|
||||
cid_answers = case.cidreportanswer_set.filter(cid=cid)
|
||||
cid_answers.delete()
|
||||
|
||||
@@ -4241,10 +4437,10 @@ def collection_reset_answers(request, exam_id: int):
|
||||
collection = get_object_or_404(CaseCollection, pk=exam_id)
|
||||
|
||||
# Select all case details (answers are linked to these)
|
||||
case_details = collection.casedetail_set.all().prefetch_related()
|
||||
casedetails = collection.casedetail_set.all().prefetch_related()
|
||||
|
||||
# Delete all answers
|
||||
for case in case_details:
|
||||
for case in casedetails:
|
||||
cid_answers = case.cidreportanswer_set.all()
|
||||
|
||||
cid_answers.delete()
|
||||
@@ -4461,27 +4657,57 @@ def series_bulk_delete(request):
|
||||
|
||||
@user_is_collection_author_or_atlas_editor
|
||||
@csrf_exempt # Only if you have CSRF issues; otherwise, keep CSRF protection
|
||||
def collection_case_displaysetup(request, collection_id, case_id):
|
||||
def collection_case_displaysetup(request, collection_id, case_number=None, case_id=None):
|
||||
"""
|
||||
View to set up the display options for a specific case in a collection.
|
||||
This can include setting which series to display, annotations, etc.
|
||||
"""
|
||||
case_detail = get_object_or_404(CaseDetail, case__pk=case_id, collection__pk=collection_id)
|
||||
# Accept either case_number (index into collection) or case_id (case PK).
|
||||
casedetail = None
|
||||
try:
|
||||
# If a case_id was passed explicitly, try that first
|
||||
if case_id is not None:
|
||||
casedetail = get_object_or_404(CaseDetail, case__pk=case_id, collection__pk=collection_id)
|
||||
else:
|
||||
# Otherwise the route most likely supplied case_number; try resolving by index
|
||||
collection = get_object_or_404(CaseCollection, pk=collection_id)
|
||||
case_obj = collection.get_case_by_index(int(case_number))
|
||||
casedetail = CaseDetail.objects.get(case=case_obj, collection=collection)
|
||||
except Http404:
|
||||
raise
|
||||
except Exception:
|
||||
raise Http404("Case not found in collection")
|
||||
|
||||
if request.method == "POST":
|
||||
data = json.loads(request.body.decode("utf-8"))
|
||||
if data.get("reset_viewerstate"):
|
||||
case_detail.default_viewerstate = None
|
||||
case_detail.save()
|
||||
casedetail.default_viewerstate = None
|
||||
casedetail.save()
|
||||
return HttpResponse("<span class='text-success'>Viewer state reset to default.</span>")
|
||||
viewerstate = data.get("viewerstate")
|
||||
if viewerstate is not None:
|
||||
case_detail.default_viewerstate = viewerstate
|
||||
case_detail.save()
|
||||
casedetail.default_viewerstate = viewerstate
|
||||
casedetail.save()
|
||||
return HttpResponse("<span class='text-success'>Viewer state saved.</span>")
|
||||
else:
|
||||
return HttpResponse("<span class='text-danger'>No viewer state provided.</span>", status=400)
|
||||
|
||||
collection = casedetail.collection
|
||||
|
||||
case_number, case_count = collection.get_index_of_case(
|
||||
casedetail.case, case_count=True
|
||||
)
|
||||
|
||||
previous = collection.get_previous_case(casedetail.case)
|
||||
next = collection.get_next_case(casedetail.case)
|
||||
|
||||
return render(request, "atlas/collection_case_displaysetup.html", {
|
||||
"case_detail": case_detail
|
||||
"casedetail": casedetail,
|
||||
"collection": collection,
|
||||
"case_number": case_number,
|
||||
"case_count": case_count,
|
||||
"previous": previous,
|
||||
"next": next,
|
||||
"collection_length": case_count,
|
||||
|
||||
})
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
<div>
|
||||
<h1 class="h4 mb-1">Exam: {{ exam }}</h1>
|
||||
</div>
|
||||
<div class="mt-2 mt-md-0">
|
||||
{% comment %} <div class="mt-2 mt-md-0">
|
||||
{% include "generic/exam_link_headers.html" %}
|
||||
</div>
|
||||
</div> {% endcomment %}
|
||||
</div>
|
||||
{% include 'exam_notes.html' %}
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="examActions" role="button" data-bs-toggle="dropdown" aria-expanded="false">Actions</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end dropdown-menu-dark" aria-labelledby="examActions">
|
||||
{# Primary action: add question #}
|
||||
<li>
|
||||
<a class="dropdown-item text-primary" href="{% url exam.get_app_name|add:':question_create' %}">
|
||||
<i class="bi bi-plus-lg me-2"></i> Add Question
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{# Edit / Clone group #}
|
||||
<li>
|
||||
<a class="dropdown-item text-white" href="{% url exam.get_app_name|add:':exam_update' exam.pk %}">
|
||||
<i class="bi bi-pencil-square me-2"></i> Edit Exam
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item text-white" href="{% url exam.get_app_name|add:':exam_clone' exam.pk %}">
|
||||
<i class="bi bi-files me-2"></i> Clone
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
|
||||
{# Authors / Markers (informational) #}
|
||||
<li>
|
||||
<a class="dropdown-item text-info" href="{% url exam.get_app_name|add:':exam_authors' exam.id %}">
|
||||
<i class="bi bi-people me-2"></i> Authors
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item text-info" href="{% url exam.get_app_name|add:':exam_markers' exam.id %}">
|
||||
<i class="bi bi-person-badge me-2"></i> Markers
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
|
||||
{# Delete is destructive - show in red #}
|
||||
<li>
|
||||
<a class="dropdown-item text-danger" href="{% url exam.get_app_name|add:':exam_delete' exam.id %}">
|
||||
<i class="bi bi-trash me-2"></i> Delete
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% if request.user.is_superuser %}
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<a class="dropdown-item text-warning" href="{% url 'admin:anatomy_exam_change' exam.pk %}">
|
||||
<i class="bi bi-tools me-2"></i> Admin Edit
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -2,12 +2,27 @@
|
||||
|
||||
{% block navigation %}
|
||||
{{block.super}}
|
||||
<br/>
|
||||
Exams: {{exam}}->
|
||||
<a href="{% url 'longs:exam_overview' pk=exam.pk %}">Overview</a> /
|
||||
<a href="{% url 'longs:mark_overview' pk=exam.pk %}">Mark</a> /
|
||||
<a href="{% url 'longs:exam_scores_all' pk=exam.pk %}">Scores</a> /
|
||||
<a href="{% url 'longs:exam_cids' exam_id=exam.pk %}">Candidates</a> /
|
||||
<a href="{% url 'longs:exam_stats' exam_id=exam.pk %}">Stats</a> /
|
||||
<a href="{% url 'longs:exam_review_start' pk=exam.pk %}">Review</a> /
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mt-2 rounded">
|
||||
<div class="container-fluid px-3">
|
||||
<span class="navbar-text me-3 text-white">
|
||||
<strong>Exam:</strong> {{ exam.title|default:exam.name|default:exam }}
|
||||
</span>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#examNav" aria-controls="examNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="examNav">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'longs:exam_overview' pk=exam.pk %}">Overview</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'longs:mark_overview' pk=exam.pk %}">Mark</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'longs:exam_scores_all' pk=exam.pk %}">Scores</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'longs:exam_cids' exam_id=exam.pk %}">Candidates</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'longs:exam_stats' exam_id=exam.pk %}">Stats</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'longs:exam_review_start' pk=exam.pk %}">Review</a></li>
|
||||
</ul>
|
||||
|
||||
{% include 'generic/partials/exams/dropdown_menu.html'%}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{% endblock %}
|
||||
|
||||
+30
-1
@@ -47,4 +47,33 @@ class ExamMarkerForm(ExamMarkerFormMixin):
|
||||
|
||||
class ExamGroupsForm(ExamGroupsFormMixin):
|
||||
class Meta(ExamGroupsFormMixin.Meta):
|
||||
model = Exam
|
||||
model = Exam
|
||||
|
||||
|
||||
class QuestionForm(ModelForm):
|
||||
class Meta:
|
||||
model = Question
|
||||
fields = [
|
||||
"stem",
|
||||
"a",
|
||||
"a_answer",
|
||||
"a_feedback",
|
||||
"b",
|
||||
"b_answer",
|
||||
"b_feedback",
|
||||
"c",
|
||||
"c_answer",
|
||||
"c_feedback",
|
||||
"d",
|
||||
"d_answer",
|
||||
"d_feedback",
|
||||
"e",
|
||||
"e_answer",
|
||||
"e_feedback",
|
||||
"category",
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# accept an optional user kwarg for parity with other apps
|
||||
self.user = kwargs.pop("user", None)
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -58,35 +58,20 @@
|
||||
- Also supports a manual `.dark-mode` class on body or a parent
|
||||
*/
|
||||
|
||||
:root {
|
||||
--bg: #ffffff;
|
||||
--text: #212529;
|
||||
--muted: #6c757d;
|
||||
--secondary: #6c757d;
|
||||
--card-bg: #ffffff;
|
||||
--card-border: #e9ecef;
|
||||
--primary: #0d6efd;
|
||||
--success: #198754;
|
||||
--danger: #dc3545;
|
||||
--feedback-bg: #f8f9fa;
|
||||
--shadow: 0 1px 2px rgba(0,0,0,0.03);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: #050607;
|
||||
--text: #e6eef6;
|
||||
--muted: #9aa4ad;
|
||||
--card-bg: #0f1720;
|
||||
--card-border: #1f2a33;
|
||||
--primary: #6ea8ff;
|
||||
--success: #36d07a;
|
||||
--danger: #ff6b6b;
|
||||
--feedback-bg: #071322;
|
||||
--shadow: 0 2px 6px rgba(0,0,0,0.6);
|
||||
}
|
||||
body, .exam-take-container { background: var(--bg); color: var(--text); }
|
||||
:root {
|
||||
--bg: #050607;
|
||||
--text: #e6eef6;
|
||||
--muted: #9aa4ad;
|
||||
--card-bg: #0f1720;
|
||||
--card-border: #1f2a33;
|
||||
--primary: #6ea8ff;
|
||||
--success: #36d07a;
|
||||
--danger: #ff6b6b;
|
||||
--feedback-bg: #071322;
|
||||
--shadow: 0 2px 6px rgba(0,0,0,0.6);
|
||||
}
|
||||
body, .exam-take-container { background: var(--bg); color: var(--text); }
|
||||
|
||||
/* Manual override: add `dark-mode` to body or a parent to force dark theme */
|
||||
body.dark-mode, .dark-mode { color: var(--text); background: var(--bg); }
|
||||
|
||||
@@ -1,11 +1,27 @@
|
||||
{% extends 'physics/base.html' %}
|
||||
|
||||
{% block navigation %}
|
||||
{{block.super}}
|
||||
<br/>
|
||||
Exams: {{exam}}-> <a href="{% url 'physics:exam_overview' pk=exam.pk %}">Overview</a> /
|
||||
<a href="{% url 'physics:exam_scores_all' pk=exam.pk %}">Scores</a> /
|
||||
<a href="{% url 'physics:exam_cids' exam_id=exam.pk %}">Candidates</a> /
|
||||
<a href="{% url 'physics:exam_stats' exam_id=exam.pk %}">Stats</a> /
|
||||
<a href="{% url 'physics:exam_review_start' pk=exam.pk %}">Review</a> /
|
||||
{{block.super}}
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mt-2 rounded">
|
||||
<div class="container-fluid px-3">
|
||||
<span class="navbar-text me-3 text-white">
|
||||
<strong>Exam:</strong> {{ exam.title|default:exam.name|default:exam }}
|
||||
</span>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#examNav" aria-controls="examNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="examNav">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'physics:exam_overview' pk=exam.pk %}">Overview</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'physics:exam_scores_all' pk=exam.pk %}">Scores</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'physics:exam_cids' exam_id=exam.pk %}">Candidates</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'physics:exam_stats' exam_id=exam.pk %}">Stats</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'physics:exam_review_start' pk=exam.pk %}">Review</a></li>
|
||||
</ul>
|
||||
|
||||
{% include 'generic/partials/exams/dropdown_menu.html'%}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,40 @@
|
||||
{% extends "physics/base.html" %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-3">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h1>Add question</h1>
|
||||
<form method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
{{ form.media }}
|
||||
|
||||
{% if form.non_field_errors %}
|
||||
<div class="alert alert-danger">{{ form.non_field_errors }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if form.helper %}
|
||||
{% crispy form form.helper %}
|
||||
{% else %}
|
||||
{% for field in form %}
|
||||
<div class="mb-3">
|
||||
{{ field.label_tag }}
|
||||
{{ field }}
|
||||
{% if field.help_text %}
|
||||
<div class="form-text">{{ field.help_text }}</div>
|
||||
{% endif %}
|
||||
{% for error in field.errors %}
|
||||
<div class="text-danger small">{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
<a href="javascript:history.back()" class="btn btn-secondary">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -10,6 +10,8 @@ urlpatterns = []
|
||||
urlpatterns.extend(
|
||||
[
|
||||
path("question/", views.QuestionView.as_view(), name="question_view"),
|
||||
path("question/create/", views.QuestionCreate.as_view(), name="question_create"),
|
||||
path("question/create/exam/<int:pk>", views.QuestionCreate.as_view(), name="question_create_exam"),
|
||||
path("question/<int:pk>/", views.question_detail, name="question_detail"),
|
||||
path("exam/<int:pk>/take_old", views.exam_take_old, name="exam_take_old"),
|
||||
path(
|
||||
|
||||
+57
-1
@@ -56,7 +56,14 @@ from generic.views import (
|
||||
|
||||
from django.core.exceptions import PermissionDenied
|
||||
|
||||
from .forms import ExamGroupsForm, ExamMarkerForm, UserAnswerForm, ExamAuthorForm, ExamForm
|
||||
from .forms import (
|
||||
ExamGroupsForm,
|
||||
ExamMarkerForm,
|
||||
UserAnswerForm,
|
||||
ExamAuthorForm,
|
||||
ExamForm,
|
||||
QuestionForm,
|
||||
)
|
||||
from .decorators import (
|
||||
user_is_author_or_physics_checker,
|
||||
user_is_exam_author_or_physics_checker,
|
||||
@@ -534,6 +541,55 @@ class QuestionView(
|
||||
return context
|
||||
|
||||
|
||||
class QuestionCreateBase(RevisionMixin, LoginRequiredMixin, CreateView):
|
||||
model = Question
|
||||
form_class = QuestionForm
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super(QuestionCreateBase, self).get_form_kwargs()
|
||||
kwargs.update({"user": self.request.user})
|
||||
return kwargs
|
||||
|
||||
def form_valid(self, form):
|
||||
self.object = form.save(commit=False)
|
||||
self.object.save()
|
||||
|
||||
# add the current user as an author
|
||||
form.instance.author.add(self.request.user.id)
|
||||
|
||||
response = super().form_valid(form)
|
||||
return response
|
||||
|
||||
|
||||
class QuestionCreate(QuestionCreateBase):
|
||||
def get_initial(self):
|
||||
if "pk" in self.kwargs:
|
||||
initial = super().get_initial()
|
||||
exam = get_object_or_404(Exam, pk=self.kwargs["pk"])
|
||||
# When creating a question from an exam context we don't have an
|
||||
# 'exams' field on the ModelForm (exams is a reverse relation).
|
||||
# Store nothing in the initial data here; attachment is handled
|
||||
# in form_valid below.
|
||||
return initial
|
||||
|
||||
def form_valid(self, form):
|
||||
# Let the base class save the object and set the author
|
||||
response = super().form_valid(form)
|
||||
|
||||
# If the create URL included an exam pk, add the new question to that exam
|
||||
if "pk" in self.kwargs:
|
||||
try:
|
||||
exam = get_object_or_404(Exam, pk=self.kwargs["pk"])
|
||||
# exam.exam_questions is a ManyToMany through relation; add is fine
|
||||
exam.exam_questions.add(self.object)
|
||||
except Exception:
|
||||
# Don't fail the whole request if attaching to the exam fails;
|
||||
# the question has already been created and author set.
|
||||
pass
|
||||
|
||||
return response
|
||||
|
||||
|
||||
class UserAnswerView(AuthorOrCheckerRequiredMixin, LoginRequiredMixin, DetailView):
|
||||
model = UserAnswer
|
||||
|
||||
|
||||
@@ -2,15 +2,30 @@
|
||||
|
||||
{% block navigation %}
|
||||
{{block.super}}
|
||||
<br/>
|
||||
Exams: {{exam}}->
|
||||
<a href="{% url 'rapids:exam_overview' pk=exam.pk %}">Overview</a> /
|
||||
{% if exam.exam_mode %}
|
||||
<a href="{% url 'rapids:mark_overview' pk=exam.pk %}">Mark</a> /
|
||||
<a href="{% url 'rapids:exam_scores_all' pk=exam.pk %}">Scores</a> /
|
||||
<a href="{% url 'rapids:exam_cids' exam_id=exam.pk %}">Candidates</a> /
|
||||
<a href="{% url 'rapids:exam_stats' exam_id=exam.pk %}">Stats</a> /
|
||||
{% endif %}
|
||||
<a href="{% url 'rapids:question_create_exam' pk=exam.pk %}">Add New Question</a>
|
||||
<a href="{% url 'rapids:exam_review_start' pk=exam.pk %}">Review</a>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mt-2 rounded">
|
||||
<div class="container-fluid px-3">
|
||||
<span class="navbar-text me-3 text-white">
|
||||
<strong>Exam:</strong> {{ exam.title|default:exam.name|default:exam }}
|
||||
</span>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#examNav" aria-controls="examNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="examNav">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'rapids:exam_overview' pk=exam.pk %}">Overview</a></li>
|
||||
{% if exam.exam_mode %}
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'rapids:mark_overview' pk=exam.pk %}">Mark</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'rapids:exam_scores_all' pk=exam.pk %}">Scores</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'rapids:exam_cids' exam_id=exam.pk %}">Candidates</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'rapids:exam_stats' exam_id=exam.pk %}">Stats</a></li>
|
||||
{% endif %}
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'rapids:question_create_exam' pk=exam.pk %}">Add Question</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'rapids:exam_review_start' pk=exam.pk %}">Review</a></li>
|
||||
</ul>
|
||||
|
||||
{% include 'generic/partials/exams/dropdown_menu.html'%}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{% endblock %}
|
||||
|
||||
@@ -2,11 +2,26 @@
|
||||
|
||||
{% block navigation %}
|
||||
{{block.super}}
|
||||
<br/>
|
||||
Exams: {{exam}}->
|
||||
<a href="{% url 'sbas:exam_overview' pk=exam.pk %}">Overview</a> /
|
||||
<a href="{% url 'sbas:exam_scores_all' pk=exam.pk %}">Scores</a> /
|
||||
<a href="{% url 'sbas:exam_cids' exam_id=exam.pk %}">Candidates</a> /
|
||||
<a href="{% url 'sbas:exam_stats' exam_id=exam.pk %}">Stats</a> /
|
||||
<a href="{% url 'sbas:exam_review_start' pk=exam.pk %}">Review</a> /
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mt-2 rounded">
|
||||
<div class="container-fluid px-3">
|
||||
<span class="navbar-text me-3 text-white">
|
||||
<strong>Exam:</strong> {{ exam.title|default:exam.name|default:exam }}
|
||||
</span>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#examNav" aria-controls="examNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="examNav">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'sbas:exam_overview' pk=exam.pk %}">Overview</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'sbas:exam_scores_all' pk=exam.pk %}">Scores</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'sbas:exam_cids' exam_id=exam.pk %}">Candidates</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'sbas:exam_stats' exam_id=exam.pk %}">Stats</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'sbas:exam_review_start' pk=exam.pk %}">Review</a></li>
|
||||
</ul>
|
||||
|
||||
{% include 'generic/partials/exams/dropdown_menu.html'%}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{% endblock %}
|
||||
|
||||
@@ -2,15 +2,30 @@
|
||||
|
||||
{% block navigation %}
|
||||
{{block.super}}
|
||||
<br/>
|
||||
Exams: {{exam}}->
|
||||
<a href="{% url 'shorts:exam_overview' pk=exam.pk %}">Overview</a> /
|
||||
{% if exam.exam_mode %}
|
||||
<a href="{% url 'shorts:mark_overview' pk=exam.pk %}">Mark</a> /
|
||||
<a href="{% url 'shorts:exam_scores_all' pk=exam.pk %}">Scores</a> /
|
||||
<a href="{% url 'shorts:exam_cids' exam_id=exam.pk %}">Candidates</a> /
|
||||
<a href="{% url 'shorts:exam_stats' exam_id=exam.pk %}">Stats</a> /
|
||||
{% endif %}
|
||||
<a href="{% url 'shorts:question_create_exam' pk=exam.pk %}">Add New Question</a>
|
||||
<a href="{% url 'shorts:exam_review_start' pk=exam.pk %}">Review</a>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mt-2 rounded">
|
||||
<div class="container-fluid px-3">
|
||||
<span class="navbar-text me-3 text-white">
|
||||
<strong>Exam:</strong> {{ exam.title|default:exam.name|default:exam }}
|
||||
</span>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#examNav" aria-controls="examNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="examNav">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'shorts:exam_overview' pk=exam.pk %}">Overview</a></li>
|
||||
{% if exam.exam_mode %}
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'shorts:mark_overview' pk=exam.pk %}">Mark</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'shorts:exam_scores_all' pk=exam.pk %}">Scores</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'shorts:exam_cids' exam_id=exam.pk %}">Candidates</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'shorts:exam_stats' exam_id=exam.pk %}">Stats</a></li>
|
||||
{% endif %}
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'shorts:question_create_exam' pk=exam.pk %}">Add Question</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'shorts:exam_review_start' pk=exam.pk %}">Review</a></li>
|
||||
</ul>
|
||||
|
||||
{% include 'generic/partials/exams/dropdown_menu.html'%}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user