Compare commits
17
Commits
9c61068026
...
3f5f39c41c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f5f39c41c | ||
|
|
92663bc620 | ||
|
|
359a399cc8 | ||
|
|
883b11a689 | ||
|
|
2e22846d23 | ||
|
|
ef8d8f3ef1 | ||
|
|
51029d1b83 | ||
|
|
1d04f9ae9f | ||
|
|
48e6339ecb | ||
|
|
3616d1b9c5 | ||
|
|
41dd5b7086 | ||
|
|
707a98629d | ||
|
|
8833fb6105 | ||
|
|
e6d8317fc5 | ||
|
|
d1249f14d8 | ||
|
|
6f96f79303 | ||
|
|
ea42f7f0c7 |
+15
-4
@@ -33,6 +33,9 @@ from dal import autocomplete
|
|||||||
|
|
||||||
from tinymce.widgets import TinyMCE
|
from tinymce.widgets import TinyMCE
|
||||||
|
|
||||||
|
from crispy_forms.helper import FormHelper
|
||||||
|
from crispy_forms.layout import Submit
|
||||||
|
|
||||||
class AnatomyAnswerForm(ModelForm):
|
class AnatomyAnswerForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = UserAnswer
|
model = UserAnswer
|
||||||
@@ -76,6 +79,14 @@ class AnatomyQuestionForm(ModelForm):
|
|||||||
ModelForm.__init__(self, *args, **kwargs)
|
ModelForm.__init__(self, *args, **kwargs)
|
||||||
|
|
||||||
super(AnatomyQuestionForm, self).__init__(*args, **kwargs)
|
super(AnatomyQuestionForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.helper = FormHelper()
|
||||||
|
self.helper.form_id = 'id-anatomy-question-form'
|
||||||
|
#self.helper.form_class = 'blueForms'
|
||||||
|
#self.helper.form_method = 'post'
|
||||||
|
#self.helper.form_action = ''
|
||||||
|
|
||||||
|
#self.helper.add_input(Submit('submit', 'Submit'))
|
||||||
# self.fields['question'].widget.attrs = {'class': 'question-form', 'rows': 10, 'cols': 100}
|
# self.fields['question'].widget.attrs = {'class': 'question-form', 'rows': 10, 'cols': 100}
|
||||||
# self.fields['feedback'].widget.attrs = {'class': 'feedback-form', 'rows': 10, 'cols': 100}
|
# self.fields['feedback'].widget.attrs = {'class': 'feedback-form', 'rows': 10, 'cols': 100}
|
||||||
self.fields["question_type"] = ModelChoiceField(
|
self.fields["question_type"] = ModelChoiceField(
|
||||||
@@ -145,15 +156,15 @@ class AnatomyQuestionForm(ModelForm):
|
|||||||
fields = [
|
fields = [
|
||||||
"question_type",
|
"question_type",
|
||||||
"image",
|
"image",
|
||||||
"answer_help",
|
"description",
|
||||||
"answer_suggest_incorrect",
|
|
||||||
"region",
|
|
||||||
"modality",
|
"modality",
|
||||||
|
"region",
|
||||||
"structure",
|
"structure",
|
||||||
"feedback",
|
"feedback",
|
||||||
"examination",
|
"examination",
|
||||||
"body_part",
|
"body_part",
|
||||||
"description",
|
"answer_help",
|
||||||
|
"answer_suggest_incorrect",
|
||||||
"open_access",
|
"open_access",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Generated by Django 5.0.2 on 2024-10-07 10:05
|
||||||
|
|
||||||
|
import anatomy.models
|
||||||
|
import django.contrib.postgres.fields
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('anatomy', '0020_alter_exam_markers'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='anatomyquestion',
|
||||||
|
name='answer_help',
|
||||||
|
field=models.TextField(blank=True, default='', help_text='Helpful information for marking, this will not be displayed to the user taking the exam.', null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='anatomyquestion',
|
||||||
|
name='answer_suggest_incorrect',
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=255, null=True), blank=True, default=list, help_text='An array that defines text that if found in the answer is likely incorrect. This is used to aid marking', size=None),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='anatomyquestion',
|
||||||
|
name='image',
|
||||||
|
field=models.ImageField(help_text="The image to use for the question. Ideally use unmarked images and annotate (arrow) them on the test system. If you wish to reuse an image that is already uploaded 'clone' the question that contains it.", upload_to=anatomy.models.image_directory_path),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='anatomyquestion',
|
||||||
|
name='question_type',
|
||||||
|
field=models.ForeignKey(default=1, help_text='Type of question, this is the question text that will be displayed to the user.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='anatomy.questiontype'),
|
||||||
|
),
|
||||||
|
]
|
||||||
+10
-4
@@ -72,12 +72,12 @@ class QuestionType(models.Model):
|
|||||||
@reversion.register
|
@reversion.register
|
||||||
class AnatomyQuestion(QuestionBase):
|
class AnatomyQuestion(QuestionBase):
|
||||||
question_type = models.ForeignKey(
|
question_type = models.ForeignKey(
|
||||||
QuestionType, on_delete=models.SET_NULL, null=True, default=1
|
QuestionType, on_delete=models.SET_NULL, null=True, default=1, help_text="Type of question, this is the question text that will be displayed to the user."
|
||||||
)
|
)
|
||||||
|
|
||||||
image = models.ImageField(
|
image = models.ImageField(
|
||||||
upload_to=image_directory_path,
|
upload_to=image_directory_path,
|
||||||
help_text="The image to use for the question. Ideally use use unmarked images and annotate (arrow) them on the test system. If you wish to reuse an image that is already uploaded 'clone' the question that contains it.",
|
help_text="The image to use for the question. Ideally use unmarked images and annotate (arrow) them on the test system. If you wish to reuse an image that is already uploaded 'clone' the question that contains it.",
|
||||||
)
|
)
|
||||||
|
|
||||||
image_annotations = models.TextField(
|
image_annotations = models.TextField(
|
||||||
@@ -90,9 +90,9 @@ class AnatomyQuestion(QuestionBase):
|
|||||||
help_text="Short description of the image e.g. 'Sagittal CT Chest, Abdomen & Pelvis', will be displayed as the title.",
|
help_text="Short description of the image e.g. 'Sagittal CT Chest, Abdomen & Pelvis', will be displayed as the title.",
|
||||||
)
|
)
|
||||||
|
|
||||||
answer_help = models.TextField(default="", blank=True, null=True, help_text="Helpful information for marking")
|
answer_help = models.TextField(default="", blank=True, null=True, help_text="Helpful information for marking, this will not be displayed to the user taking the exam.")
|
||||||
|
|
||||||
answer_suggest_incorrect = ArrayField(models.CharField(max_length=255, null=True), default=list, blank=True)
|
answer_suggest_incorrect = ArrayField(models.CharField(max_length=255, null=True), default=list, blank=True, help_text="An array that defines text that if found in the answer is likely incorrect. This is used to aid marking")
|
||||||
|
|
||||||
examination = models.ForeignKey(
|
examination = models.ForeignKey(
|
||||||
Examination, on_delete=models.SET_NULL, null=True, blank=True
|
Examination, on_delete=models.SET_NULL, null=True, blank=True
|
||||||
@@ -329,6 +329,12 @@ class Answer(models.Model):
|
|||||||
self.answer = self.answer.strip()
|
self.answer = self.answer.strip()
|
||||||
self.answer_compare = get_answer_compare(self.answer)
|
self.answer_compare = get_answer_compare(self.answer)
|
||||||
|
|
||||||
|
def can_edit(self, user):
|
||||||
|
"""Check if user can edit the answer
|
||||||
|
|
||||||
|
This just mirrors the result from the attached question"""
|
||||||
|
return self.question.can_edit(user)
|
||||||
|
|
||||||
# def get_compare_string(self):
|
# def get_compare_string(self):
|
||||||
# s = self.answer.lower().strip()
|
# s = self.answer.lower().strip()
|
||||||
# s = s.translate(str.maketrans('', '', string.punctuation))
|
# s = s.translate(str.maketrans('', '', string.punctuation))
|
||||||
|
|||||||
@@ -585,13 +585,14 @@ export function loadCornerstone(main_element, db, images, annotations_to_load, l
|
|||||||
//});
|
//});
|
||||||
} else {
|
} else {
|
||||||
let url;
|
let url;
|
||||||
// This doesn't seem to have any benefit
|
// Cornerstone needs the whole url to load? (not relative)
|
||||||
//if (data_url.startsWith("http")) {
|
if (data_url.startsWith("http")) {
|
||||||
// url = data_url;
|
url = data_url;
|
||||||
//} else {
|
} else {
|
||||||
// url = window.location.href.replace(/\/\#\/?$/, '') + "/" + data_url
|
url = window.location.href.replace(/\/\#\/?$/, '').replace("index.html", "") + "/" + data_url
|
||||||
//}
|
}
|
||||||
url = data_url;
|
//url = data_url;
|
||||||
|
|
||||||
|
|
||||||
if (url.endsWith("dcm")) {
|
if (url.endsWith("dcm")) {
|
||||||
url = "wadouri:" + url;
|
url = "wadouri:" + url;
|
||||||
@@ -618,7 +619,11 @@ export function loadCornerstone(main_element, db, images, annotations_to_load, l
|
|||||||
|
|
||||||
//console.log("LOAD and cache, then", image);
|
//console.log("LOAD and cache, then", image);
|
||||||
loadCornerstoneMainImage(element, image, stack, db, load_as_stack);
|
loadCornerstoneMainImage(element, image, stack, db, load_as_stack);
|
||||||
checkAnonStatus(image.data)
|
|
||||||
|
// We can only check dicom data if it exists (e.g. not on jpegs)
|
||||||
|
if (image.data != undefined) {
|
||||||
|
checkAnonStatus(image.data)
|
||||||
|
}
|
||||||
|
|
||||||
let load_annotation_image = true;
|
let load_annotation_image = true;
|
||||||
if (load_annotation_image) {
|
if (load_annotation_image) {
|
||||||
@@ -1079,6 +1084,10 @@ function onNewImage(e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function checkAnonStatus(dataSet) {
|
function checkAnonStatus(dataSet) {
|
||||||
|
// dataSet will be undefined if the image is not a dicom
|
||||||
|
if (dataSet == undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let accession_number = dataSet.string('x00080050').toLowerCase();
|
let accession_number = dataSet.string('x00080050').toLowerCase();
|
||||||
let patient_id = dataSet.string('x00100020').toLowerCase();
|
let patient_id = dataSet.string('x00100020').toLowerCase();
|
||||||
|
|
||||||
@@ -1215,6 +1224,16 @@ export function loadStackIndex(new_index, dicom_element) {
|
|||||||
//c = cornerstone.getEnabledElement(dicom_element)
|
//c = cornerstone.getEnabledElement(dicom_element)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function loadImageById(imageId, dicom_element) {
|
||||||
|
// Update stack index
|
||||||
|
let c = cornerstone.getEnabledElement(dicom_element);
|
||||||
|
let new_index = c.toolStateManager.toolState.stack.data[0].imageIds.indexOf(imageId);
|
||||||
|
c.toolStateManager.toolState.stack.data[0].currentImageIdIndex = new_index;
|
||||||
|
cornerstone.loadImage(imageId).then(b => {
|
||||||
|
cornerstone.displayImage(dicom_element, b);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function urltoFile(url, filename, mimeType) {
|
function urltoFile(url, filename, mimeType) {
|
||||||
return fetch(url)
|
return fetch(url)
|
||||||
.then(function (res) {
|
.then(function (res) {
|
||||||
@@ -1277,34 +1296,34 @@ export function getNextAnnotationImage(element) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function manualScrollDicom(n, dicom_element) {
|
function manualScrollDicom(n, dicom_element) {
|
||||||
// There must be a better way to do this...
|
// There must be a better way to do this...
|
||||||
//dicom_element = document.getElementById("dicom-image");
|
//dicom_element = document.getElementById("dicom-image");
|
||||||
let c = cornerstone.getEnabledElement(dicom_element);
|
let c = cornerstone.getEnabledElement(dicom_element);
|
||||||
|
|
||||||
let max = c.toolStateManager.toolState.stack.data[0].imageIds.length;
|
let max = c.toolStateManager.toolState.stack.data[0].imageIds.length;
|
||||||
|
|
||||||
if (max < 2) {
|
if (max < 2) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let current_index =
|
let current_index =
|
||||||
c.toolStateManager.toolState.stack.data[0].currentImageIdIndex;
|
c.toolStateManager.toolState.stack.data[0].currentImageIdIndex;
|
||||||
|
|
||||||
let new_index = current_index + n
|
let new_index = current_index + n
|
||||||
|
|
||||||
let corrected_index = Math.min(Math.max(new_index, 0), max)
|
|
||||||
|
|
||||||
// TODO: if loop
|
let corrected_index = Math.min(Math.max(new_index, 0), max)
|
||||||
//if (new_index >= max) {
|
|
||||||
// corrected_index = new_index - max;
|
|
||||||
//} else if (new_index < 0) {
|
|
||||||
// corrected_index = new_index + max;
|
|
||||||
//}
|
|
||||||
|
|
||||||
c.toolStateManager.toolState.stack.data[0].currentImageIdIndex = corrected_index;
|
// TODO: if loop
|
||||||
let id = c.toolStateManager.toolState.stack.data[0].imageIds[corrected_index];
|
//if (new_index >= max) {
|
||||||
cornerstone.loadImage(id).then((b) => {
|
// corrected_index = new_index - max;
|
||||||
cornerstone.displayImage(dicom_element, b);
|
//} else if (new_index < 0) {
|
||||||
});
|
// corrected_index = new_index + max;
|
||||||
// c = cornerstone.getEnabledElement(dicom_element)
|
//}
|
||||||
|
|
||||||
|
c.toolStateManager.toolState.stack.data[0].currentImageIdIndex = corrected_index;
|
||||||
|
let id = c.toolStateManager.toolState.stack.data[0].imageIds[corrected_index];
|
||||||
|
cornerstone.loadImage(id).then((b) => {
|
||||||
|
cornerstone.displayImage(dicom_element, b);
|
||||||
|
});
|
||||||
|
// c = cornerstone.getEnabledElement(dicom_element)
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
{% extends "anatomy/base.html" %}
|
{% extends "anatomy/base.html" %}
|
||||||
|
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
Anatomy
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
<!--<script type="text/javascript" src="/admin/jsi18n/"></script>-->
|
<!--<script type="text/javascript" src="/admin/jsi18n/"></script>-->
|
||||||
@@ -220,6 +226,12 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<h2>{{object|yesno:"Update,Create"}} Question</h2>
|
<h2>{{object|yesno:"Update,Create"}} Question</h2>
|
||||||
|
<details class="help-text">
|
||||||
|
<summary><i class="bi bi-info-circle"></i> Help</summary>
|
||||||
|
<p>This form can be used to create an Anatomy question.</p>
|
||||||
|
|
||||||
|
<p>Question type can be selected from a predefined list. If you require a more please contact ross.kruger@nhs.net.</p>
|
||||||
|
</details>
|
||||||
<form action="" method="post" enctype="multipart/form-data" id="anatomyquestion-form">
|
<form action="" method="post" enctype="multipart/form-data" id="anatomyquestion-form">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<a href="/anatomy/examination/create" id="add_examination" class="add-popup"
|
<a href="/anatomy/examination/create" id="add_examination" class="add-popup"
|
||||||
@@ -227,9 +239,7 @@
|
|||||||
<a href="/anatomy/body_part/create" id="add_body_part" class="add-popup"
|
<a href="/anatomy/body_part/create" id="add_body_part" class="add-popup"
|
||||||
onclick="return showAddPopup(this);"><img src="{% static '/img/icon-addlink.svg' %}"></a>
|
onclick="return showAddPopup(this);"><img src="{% static '/img/icon-addlink.svg' %}"></a>
|
||||||
|
|
||||||
<table>
|
{{ form|crispy }}
|
||||||
{{ form.as_table }}
|
|
||||||
</table>
|
|
||||||
<div id="drop-container" class="drop-target">Drop image here
|
<div id="drop-container" class="drop-target">Drop image here
|
||||||
<div id="drop-filenames"></div>
|
<div id="drop-filenames"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,38 +2,58 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
<!-- testing -->
|
|
||||||
<!--<div id="dicom-image" data-url="http://localhost:8000/static/abdoct.jpg"
|
|
||||||
data-annotations='{{question.image_annotations}}' data-edit_annotation=true></div>-->
|
|
||||||
{% include 'anatomy/question_link_header.html' %}
|
{% include 'anatomy/question_link_header.html' %}
|
||||||
<button id="save-annotations">Save Annotations</button>
|
<div id="dicom-image" class="dicom-image" data-url="{{ remote_url }}{{ question.image.url}}"
|
||||||
<div id="dicom-image" class="dicom-image" data-url="https://www.penracourses.org.uk{{ question.image.url}}"
|
data-annotations='{{question.image_annotations}}' data-edit_annotation=true>
|
||||||
data-annotations='{{question.image_annotations}}' data-edit_annotation=true></div>
|
<details class="help-text">
|
||||||
|
<summary><i class="bi bi-info-circle"></i> Help</summary>
|
||||||
|
<p>Annotate the image using the right mouse button to click and drag an arrow. To remove an arrow drag it out of the image boundaries. To save the changes click the "Save Annotations" button.</p>
|
||||||
|
|
||||||
|
<p>There is no limit to the number of arrows that can be added to the image.</p>
|
||||||
|
</details>
|
||||||
|
<button id="save-annotations">Save Annotations</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
<div class="question">
|
<div class="question">
|
||||||
<div class="date">
|
<div class="date">
|
||||||
Created: {{ question.created_date|date:"d/m/Y" }}
|
Created: {{ question.created_date|date:"d/m/Y" }}
|
||||||
</div>
|
</div>
|
||||||
<h1>{{ question.get_primary_answer }}</h1>
|
<h2>Question type: {{question.question_type}}</h2>
|
||||||
<h2>{{question.question_type}}</h2>
|
<h3>Primary answer: {{ question.get_primary_answer }}</h3>
|
||||||
<details>
|
<div>
|
||||||
<summary>
|
<details>
|
||||||
Answers:
|
<summary title="Click to view the question answers">
|
||||||
</summary>
|
Answers:
|
||||||
<table>
|
</summary>
|
||||||
<tr><th>Answer</th><th>Score</th></tr>
|
<table>
|
||||||
{% for answer in question.answers.all|dictsortreversed:"status" %}
|
<tr><th>Answer</th><th>Score</th></tr>
|
||||||
<tr>
|
{% for answer in question.answers.all|dictsortreversed:"status" %}
|
||||||
<td>
|
<tr>
|
||||||
<span {% if answer.proposed %}class="proposed-answer" data-aid="{{answer.pk}}" data-question-type="anatomy"
|
<td {% if answer.proposed %}class="proposed-answer" data-aid="{{answer.pk}}" data-question-type="anatomy"
|
||||||
{% endif %}>
|
{% endif %}>
|
||||||
{{ answer }}
|
<span >
|
||||||
</span>
|
{{ answer }}
|
||||||
<td>
|
</span>
|
||||||
<td>{{answer.status}}</td>
|
<td>
|
||||||
</tr>
|
<td>{{answer.status}}</td>
|
||||||
{% endfor %}
|
{% if answer.proposed %}
|
||||||
</table>
|
<td>
|
||||||
</details>
|
<button class="btn btn-sm accept-button" data-aid={{answer.id}}
|
||||||
|
hx-get="{% url 'anatomy:confirm_answer' answer.id %}"
|
||||||
|
title="Click to accept the proposed answer">Accept</button>
|
||||||
|
<button class="btn btn-sm delete-button" data-aid={{answer.id}}
|
||||||
|
hx-get="{% url 'anatomy:delete_answer' answer.id %}"
|
||||||
|
title="Click to delete the proposed answer"
|
||||||
|
>Delete</button>
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
Answer help: {{ question.answer_help|safe }}
|
Answer help: {{ question.answer_help|safe }}
|
||||||
</div>
|
</div>
|
||||||
@@ -47,6 +67,10 @@
|
|||||||
Exams: {% for exam in question.exams.all %}
|
Exams: {% for exam in question.exams.all %}
|
||||||
<a href="{% url 'anatomy:exam_overview' pk=exam.pk %}">{{ exam }}</a>
|
<a href="{% url 'anatomy:exam_overview' pk=exam.pk %}">{{ exam }}</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
<button class="btn btn-sm" hx-get="{% url 'anatomy:question_add_exam' question_id=question.pk %}"
|
||||||
|
hx-target="#exam-list"
|
||||||
|
hx-swap="innerHTML">Edit exam(s)</button>
|
||||||
|
<span id="exam-list"></span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
Modality: {{ question.modality }}
|
Modality: {{ question.modality }}
|
||||||
@@ -143,4 +167,17 @@
|
|||||||
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block css %}
|
||||||
|
<style>
|
||||||
|
.proposed-answer::before {
|
||||||
|
content: "\F505";
|
||||||
|
font-family: bootstrap-icons;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question div {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock css %}
|
||||||
@@ -47,6 +47,13 @@ urlpatterns = [
|
|||||||
views.QuestionDelete.as_view(),
|
views.QuestionDelete.as_view(),
|
||||||
name="question_delete",
|
name="question_delete",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"question/<int:question_id>/add_exam",
|
||||||
|
views.question_add_exam,
|
||||||
|
name="question_add_exam",
|
||||||
|
),
|
||||||
|
path("answer/<int:answer_id>/confirm", views.confirm_answer, name="confirm_answer"),
|
||||||
|
path("answer/<int:answer_id>/delete", views.delete_answer, name="delete_answer"),
|
||||||
path("exam/<int:exam_pk>/<int:sk>/mark", views.mark, name="mark"),
|
path("exam/<int:exam_pk>/<int:sk>/mark", views.mark, name="mark"),
|
||||||
path("exam/<int:exam_pk>/<int:sk>/mark/all", views.mark_all, name="mark_all"),
|
path("exam/<int:exam_pk>/<int:sk>/mark/all", views.mark_all, name="mark_all"),
|
||||||
path(
|
path(
|
||||||
|
|||||||
+88
-1
@@ -26,6 +26,11 @@ from django.http import HttpResponseRedirect, HttpResponse
|
|||||||
|
|
||||||
from dal import autocomplete
|
from dal import autocomplete
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils.html import format_html_join
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
|
|
||||||
from .forms import (
|
from .forms import (
|
||||||
AnatomyAnswerForm,
|
AnatomyAnswerForm,
|
||||||
AnatomyQuestionAnswerForm,
|
AnatomyQuestionAnswerForm,
|
||||||
@@ -494,6 +499,8 @@ class AnatomyQuestionCreateBase(RevisionMixin, LoginRequiredMixin, CreateView):
|
|||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(AnatomyQuestionCreateBase, self).get_context_data(**kwargs)
|
context = super(AnatomyQuestionCreateBase, self).get_context_data(**kwargs)
|
||||||
|
if "object" in context:
|
||||||
|
context["question"] = context["object"]
|
||||||
if self.request.POST:
|
if self.request.POST:
|
||||||
context["answer_formset"] = AnswerFormSet(
|
context["answer_formset"] = AnswerFormSet(
|
||||||
self.request.POST, self.request.FILES
|
self.request.POST, self.request.FILES
|
||||||
@@ -526,6 +533,9 @@ class AnatomyQuestionCreateBase(RevisionMixin, LoginRequiredMixin, CreateView):
|
|||||||
else:
|
else:
|
||||||
return super().form_invalid(form)
|
return super().form_invalid(form)
|
||||||
|
|
||||||
|
def get_success_url(self) -> str:
|
||||||
|
return redirect("anatomy:question_detail", pk=self.object.pk)
|
||||||
|
|
||||||
|
|
||||||
class AnatomyQuestionCreate(AnatomyQuestionCreateBase):
|
class AnatomyQuestionCreate(AnatomyQuestionCreateBase):
|
||||||
|
|
||||||
@@ -540,7 +550,6 @@ class AnatomyQuestionCreate(AnatomyQuestionCreateBase):
|
|||||||
|
|
||||||
return initial
|
return initial
|
||||||
|
|
||||||
|
|
||||||
# # There has to be a better way...
|
# # There has to be a better way...
|
||||||
# try:
|
# try:
|
||||||
# s = (i.pk for i in self.request.user.AnatomyQuestion_default.site.all())
|
# s = (i.pk for i in self.request.user.AnatomyQuestion_default.site.all())
|
||||||
@@ -844,3 +853,81 @@ GenericViews = GenericViewBase("anatomy", AnatomyQuestion, UserAnswer, Exam)
|
|||||||
class ExamClone(ExamCloneMixin, ExamCreate):
|
class ExamClone(ExamCloneMixin, ExamCreate):
|
||||||
"""Clone exam view"""
|
"""Clone exam view"""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def confirm_answer(request, answer_id: int):
|
||||||
|
if request.htmx:
|
||||||
|
answer = get_object_or_404(Answer, pk=answer_id)
|
||||||
|
|
||||||
|
# Check if the user has permission to confirm the answer
|
||||||
|
if not answer.can_edit(request.user):
|
||||||
|
return HttpResponse("Invalid permissions", status=403)
|
||||||
|
|
||||||
|
answer.clean()
|
||||||
|
answer.proposed = False
|
||||||
|
answer.save()
|
||||||
|
return HttpResponse("Answer accepted")
|
||||||
|
|
||||||
|
raise PermissionDenied()
|
||||||
|
|
||||||
|
def delete_answer(request, answer_id: int):
|
||||||
|
if request.htmx:
|
||||||
|
answer = get_object_or_404(Answer, pk=answer_id)
|
||||||
|
|
||||||
|
# Check if the user has permission to confirm the answer
|
||||||
|
if not answer.can_edit(request.user):
|
||||||
|
return HttpResponse("Invalid permissions", status=403)
|
||||||
|
|
||||||
|
answer.delete()
|
||||||
|
return HttpResponse("Answer deleted")
|
||||||
|
raise PermissionDenied()
|
||||||
|
|
||||||
|
def question_add_exam(request, question_id: int):
|
||||||
|
if request.htmx:
|
||||||
|
question = get_object_or_404(AnatomyQuestion, pk=question_id)
|
||||||
|
|
||||||
|
if not question.can_edit(request.user):
|
||||||
|
return HttpResponse("Invalid permissions", status=403)
|
||||||
|
|
||||||
|
|
||||||
|
if request.POST:
|
||||||
|
exam_id = request.POST.get("exam_id", None)
|
||||||
|
|
||||||
|
if exam_id is None:
|
||||||
|
return HttpResponse("No exam id provided", status=400)
|
||||||
|
|
||||||
|
exam = get_object_or_404(Exam, pk=exam_id)
|
||||||
|
|
||||||
|
if request.POST.get("remove", False) == "true":
|
||||||
|
question.exams.remove(exam)
|
||||||
|
return HttpResponse("Question removed from exam.")
|
||||||
|
else:
|
||||||
|
question.exams.add(exam)
|
||||||
|
|
||||||
|
return HttpResponse("Question added to exam.")
|
||||||
|
else:
|
||||||
|
# Return a list of exams that we can add to the question
|
||||||
|
|
||||||
|
exams = Exam.objects.filter(author=request.user) | Exam.objects.filter(open_access=True)
|
||||||
|
exams = exams.difference(question.exams.all())
|
||||||
|
|
||||||
|
if not exams:
|
||||||
|
return HttpResponse("No exams available to add")
|
||||||
|
|
||||||
|
html = "Exams to add to question: <br>"
|
||||||
|
for exam in exams:
|
||||||
|
html = html + (f"<span id='htmx-exam-list'><form><input name='exam_id' value='{exam.id}' type='hidden'><button hx-post=\"{request.path}\""
|
||||||
|
f">{exam.name}: {exam.id}</button></form>"
|
||||||
|
)
|
||||||
|
html = html + "<br>Exams to remove from question: <br>"
|
||||||
|
for exam in question.exams.all():
|
||||||
|
html = html + (f"<span id='htmx-exam-list'><form><input name='exam_id' value='{exam.id}' type='hidden'><input name='remove' value='true' type='hidden'><button hx-post=\"{request.path}\""
|
||||||
|
f"hx-confirm='Are you sure you want to remove this exam from the question?'"
|
||||||
|
f">{exam.name}: {exam.id}</button></form>"
|
||||||
|
)
|
||||||
|
html = html + "<button _='on click remove #htmx-exam-list'>Cancel</button>"
|
||||||
|
|
||||||
|
return HttpResponse(mark_safe(html))
|
||||||
|
|
||||||
|
|
||||||
|
raise PermissionDenied()
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.0.2 on 2024-10-07 08:28
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('atlas', '0060_seriesfinding_current_image_id_index'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='seriesfinding',
|
||||||
|
name='current_image_id_index',
|
||||||
|
field=models.TextField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
+2
-1
@@ -597,7 +597,8 @@ class SeriesFinding(models.Model):
|
|||||||
conditions = models.ManyToManyField(Condition, blank=True)
|
conditions = models.ManyToManyField(Condition, blank=True)
|
||||||
annotation_json = models.TextField(null=True, blank=True)
|
annotation_json = models.TextField(null=True, blank=True)
|
||||||
viewport_json = models.TextField(null=True, blank=True)
|
viewport_json = models.TextField(null=True, blank=True)
|
||||||
current_image_id_index = models.IntegerField(null=True, blank=True)
|
# This has been migrated to install the image id (not index)
|
||||||
|
current_image_id_index = models.TextField(null=True, blank=True)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
findings = self.findings.all().values_list("name")
|
findings = self.findings.all().values_list("name")
|
||||||
|
|||||||
@@ -215,9 +215,10 @@
|
|||||||
$("#add-finding-button").click(() => {
|
$("#add-finding-button").click(() => {
|
||||||
$("#hidden-form").show()
|
$("#hidden-form").show()
|
||||||
dicom_element = $(".cornerstone-element").get(0);
|
dicom_element = $(".cornerstone-element").get(0);
|
||||||
cornerstoneTools.globalImageIdSpecificToolStateManager.clear(dicom_element);
|
//cornerstoneTools.globalImageIdSpecificToolStateManager.clear(dicom_element);
|
||||||
cornerstone.reset(dicom_element);
|
//cornerstone.reset(dicom_element);
|
||||||
cornerstone.resize(dicom_element, true);
|
////cornerstone.resize(dicom_element, true);
|
||||||
|
$("#add-finding-button").hide()
|
||||||
//$("#finding-form").empty().append(
|
//$("#finding-form").empty().append(
|
||||||
// $("#hidden-form form").clone()
|
// $("#hidden-form form").clone()
|
||||||
//);
|
//);
|
||||||
@@ -225,6 +226,7 @@
|
|||||||
});
|
});
|
||||||
$("#cancel-add-finding-button").click((e) => {
|
$("#cancel-add-finding-button").click((e) => {
|
||||||
$("#hidden-form").hide()
|
$("#hidden-form").hide()
|
||||||
|
$("#add-finding-button").show()
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -279,15 +281,23 @@
|
|||||||
|
|
||||||
//cornerstone.getEnabledElement(dicom_element).toolStateManager.restoreToolState(annotationjson)
|
//cornerstone.getEnabledElement(dicom_element).toolStateManager.restoreToolState(annotationjson)
|
||||||
cornerstone.resize(dicom_element);
|
cornerstone.resize(dicom_element);
|
||||||
current_image_id_index = parseInt(current_image_id_index);
|
console.log("current_image_id_index", current_image_id_index)
|
||||||
|
//current_image_id_index = parseInt(current_image_id_index);
|
||||||
|
|
||||||
console.log(current_image_id_index, Number.isInteger(current_image_id_index))
|
console.log(current_image_id_index, Number.isInteger(current_image_id_index))
|
||||||
if (Number.isInteger(current_image_id_index)) {
|
if (Number.isInteger(current_image_id_index)) {
|
||||||
console.log("loading stack index")
|
console.log("loading stack index")
|
||||||
dicomViewer.loadStackIndex(current_image_id_index, dicom_element);
|
dicomViewer.loadStackIndex(current_image_id_index, dicom_element);
|
||||||
} else {
|
} else {
|
||||||
console.log("loading next annotation image")
|
try {
|
||||||
dicomViewer.getNextAnnotationImage(dicom_element);
|
console.log("current_image_id_index", current_image_id_index)
|
||||||
|
dicomViewer.loadImageById(current_image_id_index, dicom_element);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
console.log("loading image by id failed")
|
||||||
|
console.log("loading next annotation image")
|
||||||
|
dicomViewer.getNextAnnotationImage(dicom_element);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -305,7 +315,10 @@
|
|||||||
description: $('#id_description').val(),
|
description: $('#id_description').val(),
|
||||||
series: {{ series.pk }},
|
series: {{ series.pk }},
|
||||||
//annotation_json: JSON.stringify(c.toolStateManager.saveToolState()),
|
//annotation_json: JSON.stringify(c.toolStateManager.saveToolState()),
|
||||||
current_image_id_index: c.toolStateManager.toolState.stack.data[0].currentImageIdIndex,
|
// This is the stack number
|
||||||
|
//current_image_id_index: c.toolStateManager.toolState.stack.data[0].currentImageIdIndex,
|
||||||
|
// but we want to use the current image id (so if the stack is reordered it still attaches to the correct image)
|
||||||
|
current_image_id_index: c.toolStateManager.toolState.stack.data[0].imageIds[c.toolStateManager.toolState.stack.data[0].currentImageIdIndex],
|
||||||
annotation_json: JSON.stringify(cornerstoneTools.globalImageIdSpecificToolStateManager
|
annotation_json: JSON.stringify(cornerstoneTools.globalImageIdSpecificToolStateManager
|
||||||
.saveToolState()),
|
.saveToolState()),
|
||||||
viewport_json: JSON.stringify(c.viewport),
|
viewport_json: JSON.stringify(c.viewport),
|
||||||
|
|||||||
+14
-1
@@ -1,6 +1,9 @@
|
|||||||
import json
|
import json
|
||||||
import pprint
|
import pprint
|
||||||
|
from typing import Any
|
||||||
from dal import autocomplete
|
from dal import autocomplete
|
||||||
|
from django.db.models.base import Model as Model
|
||||||
|
from django.db.models.query import QuerySet
|
||||||
from django.shortcuts import render, get_object_or_404, redirect
|
from django.shortcuts import render, get_object_or_404, redirect
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@@ -563,12 +566,21 @@ class CaseDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView):
|
|||||||
template_name = "confirm_delete.html"
|
template_name = "confirm_delete.html"
|
||||||
|
|
||||||
|
|
||||||
|
# TODO fix permissions
|
||||||
class SeriesFindingDelete(RevisionMixin, PermissionRequiredMixin, DeleteView):
|
class SeriesFindingDelete(RevisionMixin, PermissionRequiredMixin, DeleteView):
|
||||||
permission_required = "atlas.delete_seriesfinding"
|
permission_required = "atlas.delete_seriesfinding"
|
||||||
model = SeriesFinding
|
model = SeriesFinding
|
||||||
template_name = "confirm_delete.html"
|
template_name = "confirm_delete.html"
|
||||||
# success_url = reverse_lazy("atlas:case_view")
|
# success_url = reverse_lazy("atlas:case_view")
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
series_finding = super().get_object()
|
||||||
|
series = series_finding.series
|
||||||
|
if series.check_user_can_edit(self.request.user):
|
||||||
|
return series_finding
|
||||||
|
else:
|
||||||
|
raise PermissionDenied
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
pk = self.kwargs["pk"]
|
pk = self.kwargs["pk"]
|
||||||
series_id = get_object_or_404(SeriesFinding, pk=pk).series.id
|
series_id = get_object_or_404(SeriesFinding, pk=pk).series.id
|
||||||
@@ -1375,6 +1387,7 @@ class SeriesImagesZipView(SeriesImagesZipViewBase, SuperuserRequiredMixin):
|
|||||||
series_object = Series
|
series_object = Series
|
||||||
|
|
||||||
|
|
||||||
|
# At the moment only the series owner / author can delete findings but anyone can create
|
||||||
def create_series_findings(request):
|
def create_series_findings(request):
|
||||||
# posts = Post.objects.all()
|
# posts = Post.objects.all()
|
||||||
response_data = {}
|
response_data = {}
|
||||||
@@ -1388,7 +1401,7 @@ def create_series_findings(request):
|
|||||||
description = request.POST.get("description")
|
description = request.POST.get("description")
|
||||||
annotation_json = request.POST.get("annotation_json")
|
annotation_json = request.POST.get("annotation_json")
|
||||||
viewport_json = request.POST.get("viewport_json")
|
viewport_json = request.POST.get("viewport_json")
|
||||||
current_image_id_index = int(request.POST.get("current_image_id_index"))
|
current_image_id_index = request.POST.get("current_image_id_index")
|
||||||
|
|
||||||
series = Series.objects.get(pk=series_id)
|
series = Series.objects.get(pk=series_id)
|
||||||
findings = Finding.objects.filter(pk__in=findings_ids)
|
findings = Finding.objects.filter(pk__in=findings_ids)
|
||||||
|
|||||||
@@ -78,9 +78,6 @@ class FirstImageColumn(tables.Column):
|
|||||||
|
|
||||||
|
|
||||||
class CidUserTable(tables.Table):
|
class CidUserTable(tables.Table):
|
||||||
# edit = tables.LinkColumn(
|
|
||||||
# "anatomy:anatomy_question_update", text="Edit", args=[A("pk")], orderable=False
|
|
||||||
# )
|
|
||||||
cid = tables.LinkColumn(
|
cid = tables.LinkColumn(
|
||||||
"cid_scores", args=[A("cid"), A("passcode")], orderable=True
|
"cid_scores", args=[A("cid"), A("passcode")], orderable=True
|
||||||
)
|
)
|
||||||
@@ -129,9 +126,6 @@ class CidUserTable(tables.Table):
|
|||||||
|
|
||||||
|
|
||||||
class CidUserExamTable(tables.Table):
|
class CidUserExamTable(tables.Table):
|
||||||
# edit = tables.LinkColumn(
|
|
||||||
# "anatomy:anatomy_question_update", text="Edit", args=[A("pk")], orderable=False
|
|
||||||
# )
|
|
||||||
cid = tables.LinkColumn(
|
cid = tables.LinkColumn(
|
||||||
"cid_scores", args=[A("cid"), A("passcode")], orderable=True
|
"cid_scores", args=[A("cid"), A("passcode")], orderable=True
|
||||||
)
|
)
|
||||||
@@ -189,9 +183,6 @@ class CidUserExamTable(tables.Table):
|
|||||||
|
|
||||||
|
|
||||||
class UserUserTable(tables.Table):
|
class UserUserTable(tables.Table):
|
||||||
# edit = tables.LinkColumn(
|
|
||||||
# "anatomy:anatomy_question_update", text="Edit", args=[A("pk")], orderable=False
|
|
||||||
# )
|
|
||||||
|
|
||||||
user = tables.Column(
|
user = tables.Column(
|
||||||
linkify={"viewname": "account_profile", "args": [A("username")]},
|
linkify={"viewname": "account_profile", "args": [A("username")]},
|
||||||
|
|||||||
+200
-110
@@ -48,7 +48,12 @@ from atlas.models import CaseCollection, CaseDetail
|
|||||||
from generic.decorators import user_is_cid_user_manager
|
from generic.decorators import user_is_cid_user_manager
|
||||||
from generic.filters import CidUserFilter, ExaminationFilter, SupervisorFilter
|
from generic.filters import CidUserFilter, ExaminationFilter, SupervisorFilter
|
||||||
|
|
||||||
from generic.tables import CidUserExamTable, CidUserTable, ExaminationTable, SupervisorTable
|
from generic.tables import (
|
||||||
|
CidUserExamTable,
|
||||||
|
CidUserTable,
|
||||||
|
ExaminationTable,
|
||||||
|
SupervisorTable,
|
||||||
|
)
|
||||||
from generic.mixins import CheckCanEditMixin, SuperuserRequiredMixin
|
from generic.mixins import CheckCanEditMixin, SuperuserRequiredMixin
|
||||||
|
|
||||||
import zipfile
|
import zipfile
|
||||||
@@ -79,6 +84,7 @@ from .models import (
|
|||||||
ExamCollection,
|
ExamCollection,
|
||||||
ExamUserStatus,
|
ExamUserStatus,
|
||||||
Examination,
|
Examination,
|
||||||
|
QuestionBase,
|
||||||
QuestionNote,
|
QuestionNote,
|
||||||
Supervisor,
|
Supervisor,
|
||||||
UserGrades,
|
UserGrades,
|
||||||
@@ -87,15 +93,31 @@ from .models import (
|
|||||||
get_next_cid,
|
get_next_cid,
|
||||||
)
|
)
|
||||||
|
|
||||||
from rapids.models import Rapid as RapidQuestion, ExamQuestionDetail as RapidsExamQuestionDetail
|
from rapids.models import (
|
||||||
|
Rapid as RapidQuestion,
|
||||||
|
ExamQuestionDetail as RapidsExamQuestionDetail,
|
||||||
|
)
|
||||||
from rapids.models import Exam as RapidsExam
|
from rapids.models import Exam as RapidsExam
|
||||||
from longs.models import Long as LongQuestion, LongSeries, ExamQuestionDetail as LongsExamQuestionDetail
|
from longs.models import (
|
||||||
|
Long as LongQuestion,
|
||||||
|
LongSeries,
|
||||||
|
ExamQuestionDetail as LongsExamQuestionDetail,
|
||||||
|
)
|
||||||
from longs.models import Exam as LongsExam
|
from longs.models import Exam as LongsExam
|
||||||
from anatomy.models import AnatomyQuestion as AnatomyQuestion, ExamQuestionDetail as AnatomyExamQuestionDetail
|
from anatomy.models import (
|
||||||
|
AnatomyQuestion as AnatomyQuestion,
|
||||||
|
ExamQuestionDetail as AnatomyExamQuestionDetail,
|
||||||
|
)
|
||||||
from anatomy.models import Exam as AnatomyExam
|
from anatomy.models import Exam as AnatomyExam
|
||||||
from sbas.models import Question as SbasQuestion, ExamQuestionDetail as SbasExamQuestionDetail
|
from sbas.models import (
|
||||||
|
Question as SbasQuestion,
|
||||||
|
ExamQuestionDetail as SbasExamQuestionDetail,
|
||||||
|
)
|
||||||
from sbas.models import Exam as SbasExam
|
from sbas.models import Exam as SbasExam
|
||||||
from physics.models import Question as PhysicsQuestion, ExamQuestionDetail as PhysicsExamQuestionDetail
|
from physics.models import (
|
||||||
|
Question as PhysicsQuestion,
|
||||||
|
ExamQuestionDetail as PhysicsExamQuestionDetail,
|
||||||
|
)
|
||||||
from physics.models import Exam as PhysicsExam
|
from physics.models import Exam as PhysicsExam
|
||||||
|
|
||||||
from django.db.models import Case, When
|
from django.db.models import Case, When
|
||||||
@@ -112,13 +134,13 @@ import plotly.express as px
|
|||||||
from django.db.models import Prefetch
|
from django.db.models import Prefetch
|
||||||
|
|
||||||
|
|
||||||
|
class RedirectMixin:
|
||||||
class RedirectMixin():
|
|
||||||
def get_success_url(self) -> str:
|
def get_success_url(self) -> str:
|
||||||
if "redirect" in self.request.GET:
|
if "redirect" in self.request.GET:
|
||||||
return self.request.GET["redirect"]
|
return self.request.GET["redirect"]
|
||||||
return super().get_success_url()
|
return super().get_success_url()
|
||||||
|
|
||||||
|
|
||||||
class AuthorRequiredMixin(object):
|
class AuthorRequiredMixin(object):
|
||||||
def get_object(self, *args, **kwargs):
|
def get_object(self, *args, **kwargs):
|
||||||
obj = super().get_object(*args, **kwargs)
|
obj = super().get_object(*args, **kwargs)
|
||||||
@@ -141,6 +163,7 @@ class CidManagerRequiredMixin(UserPassesTestMixin):
|
|||||||
# return obj
|
# return obj
|
||||||
# raise PermissionDenied() # or Http404
|
# raise PermissionDenied() # or Http404
|
||||||
|
|
||||||
|
|
||||||
def get_exam_model_from_app_name(app_name: str) -> ExamBase:
|
def get_exam_model_from_app_name(app_name: str) -> ExamBase:
|
||||||
EXAM_MAP = {
|
EXAM_MAP = {
|
||||||
"physics": PhysicsExam,
|
"physics": PhysicsExam,
|
||||||
@@ -471,7 +494,7 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
if (exam.open_access and exam.active) or user in exam.get_author_objects():
|
if (exam.open_access and exam.active) or user in exam.get_author_objects():
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if marker and user in exam.markers.all():
|
if marker and user in exam.markers.all():
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if exam.authors_only:
|
if exam.authors_only:
|
||||||
@@ -535,7 +558,7 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
if exam_id is not None:
|
if exam_id is not None:
|
||||||
exam = get_object_or_404(self.Exam, pk=exam_id)
|
exam = get_object_or_404(self.Exam, pk=exam_id)
|
||||||
# Remove open_access check for the momement
|
# Remove open_access check for the momement
|
||||||
#if exam.open_access or user in exam.get_author_objects():
|
# if exam.open_access or user in exam.get_author_objects():
|
||||||
# return True
|
# return True
|
||||||
if user in exam.get_author_objects():
|
if user in exam.get_author_objects():
|
||||||
return True
|
return True
|
||||||
@@ -687,13 +710,21 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
def exam_list(self, request, all=False):
|
def exam_list(self, request, all=False):
|
||||||
if not self.check_user_access(request.user):
|
if not self.check_user_access(request.user):
|
||||||
# raise PermissionDenied
|
# raise PermissionDenied
|
||||||
exam_list = self.Exam.objects.filter(author__id=request.user.id, exam_mode=True).order_by(
|
exam_list = self.Exam.objects.filter(
|
||||||
"name"
|
author__id=request.user.id, exam_mode=True
|
||||||
)
|
).order_by("name")
|
||||||
|
|
||||||
exam_list = exam_list | self.Exam.objects.filter(markers__id=request.user.id, exam_mode=True).order_by("name")
|
exam_list = exam_list | self.Exam.objects.filter(
|
||||||
|
markers__id=request.user.id, exam_mode=True
|
||||||
|
).order_by("name")
|
||||||
else:
|
else:
|
||||||
exam_list = self.Exam.objects.prefetch_related("valid_user_users", "valid_cid_users").filter(exam_mode=True).order_by("name")
|
exam_list = (
|
||||||
|
self.Exam.objects.prefetch_related(
|
||||||
|
"valid_user_users", "valid_cid_users"
|
||||||
|
)
|
||||||
|
.filter(exam_mode=True)
|
||||||
|
.order_by("name")
|
||||||
|
)
|
||||||
|
|
||||||
if not all:
|
if not all:
|
||||||
exam_list = exam_list.filter(archive=False).order_by("name")
|
exam_list = exam_list.filter(archive=False).order_by("name")
|
||||||
@@ -743,7 +774,7 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
|
|
||||||
status = {}
|
status = {}
|
||||||
for user in users:
|
for user in users:
|
||||||
#user = User.objects.get(pk=u)
|
# user = User.objects.get(pk=u)
|
||||||
user_exam = exam.get_or_create_cid_user_exam(user_user=user)
|
user_exam = exam.get_or_create_cid_user_exam(user_user=user)
|
||||||
if user_exam.results_emailed_status:
|
if user_exam.results_emailed_status:
|
||||||
status[user] = json.loads(user_exam.results_emailed_status)
|
status[user] = json.loads(user_exam.results_emailed_status)
|
||||||
@@ -796,7 +827,7 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
|
|
||||||
email_results = {}
|
email_results = {}
|
||||||
for user in users:
|
for user in users:
|
||||||
#user = User.objects.get(pk=u)
|
# user = User.objects.get(pk=u)
|
||||||
user_exam = exam.get_or_create_cid_user_exam(user_user=user)
|
user_exam = exam.get_or_create_cid_user_exam(user_user=user)
|
||||||
|
|
||||||
if unsent_only and user_exam.results_emailed_status:
|
if unsent_only and user_exam.results_emailed_status:
|
||||||
@@ -904,7 +935,7 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
queryset=self.Answer.objects.filter(
|
queryset=self.Answer.objects.filter(
|
||||||
proposed=False, status=self.Answer.MarkOptions.CORRECT
|
proposed=False, status=self.Answer.MarkOptions.CORRECT
|
||||||
),
|
),
|
||||||
to_attr="prefetched_primary_answer"
|
to_attr="prefetched_primary_answer",
|
||||||
# queryset=self.Answer.objects.filter(),
|
# queryset=self.Answer.objects.filter(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -913,21 +944,20 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
elif self.app_name == "longs":
|
elif self.app_name == "longs":
|
||||||
questions = (
|
questions = (
|
||||||
exam.exam_questions.prefetch_related(
|
exam.exam_questions.prefetch_related(
|
||||||
#Prefetch(
|
# Prefetch(
|
||||||
# "series",
|
# "series",
|
||||||
# queryset=LongSeries.objects.select_related(
|
# queryset=LongSeries.objects.select_related(
|
||||||
# "modality", "examination", "plane", "contrast"
|
# "modality", "examination", "plane", "contrast"
|
||||||
# ).prefetch_related(Prefetch("images")),
|
# ).prefetch_related(Prefetch("images")),
|
||||||
#),
|
# ),
|
||||||
"author",
|
"author",
|
||||||
#"examquestiondetail",
|
# "examquestiondetail",
|
||||||
#"longsseries",
|
# "longsseries",
|
||||||
"series",
|
"series",
|
||||||
#"seriesdetail",
|
# "seriesdetail",
|
||||||
"series__images",
|
"series__images",
|
||||||
"series__plane",
|
"series__plane",
|
||||||
"series__examination"
|
"series__examination",
|
||||||
|
|
||||||
# to_attr="prefetched_primary_answer"
|
# to_attr="prefetched_primary_answer"
|
||||||
# queryset=self.Answer.objects.filter(),
|
# queryset=self.Answer.objects.filter(),
|
||||||
# "series",
|
# "series",
|
||||||
@@ -948,7 +978,7 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
queryset=self.Answer.objects.filter(
|
queryset=self.Answer.objects.filter(
|
||||||
proposed=False, status=self.Answer.MarkOptions.CORRECT
|
proposed=False, status=self.Answer.MarkOptions.CORRECT
|
||||||
),
|
),
|
||||||
to_attr="prefetched_primary_answer"
|
to_attr="prefetched_primary_answer",
|
||||||
# queryset=self.Answer.objects.filter(),
|
# queryset=self.Answer.objects.filter(),
|
||||||
),
|
),
|
||||||
"images",
|
"images",
|
||||||
@@ -966,25 +996,29 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
# )
|
# )
|
||||||
elif self.app_name == "physics":
|
elif self.app_name == "physics":
|
||||||
questions = (
|
questions = (
|
||||||
exam.exam_questions.select_related("category").all()
|
exam.exam_questions.select_related("category")
|
||||||
|
.all()
|
||||||
.order_by("examquestiondetail__sort_order")
|
.order_by("examquestiondetail__sort_order")
|
||||||
# .prefetch_related("images", "abnormality", "region", "examination")
|
# .prefetch_related("images", "abnormality", "region", "examination")
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
questions = exam.exam_questions.select_related().all().order_by("examquestiondetail__sort_order")
|
questions = (
|
||||||
|
exam.exam_questions.select_related()
|
||||||
|
.all()
|
||||||
|
.order_by("examquestiondetail__sort_order")
|
||||||
|
)
|
||||||
|
|
||||||
question_number = len(questions)
|
question_number = len(questions)
|
||||||
|
|
||||||
cid_candidates = exam.valid_cid_users.count()
|
cid_candidates = exam.valid_cid_users.count()
|
||||||
users_candidates = exam.valid_user_users.count()
|
users_candidates = exam.valid_user_users.count()
|
||||||
|
|
||||||
#question_orders = [q.examquestiondetail.sort_order for q in questions]
|
# question_orders = [q.examquestiondetail.sort_order for q in questions]
|
||||||
|
|
||||||
# Exam order can be missing when, this can be checked with
|
# Exam order can be missing when, this can be checked with
|
||||||
# exam.exam_questions.select_related().all().order_by("examquestiondetail__sort_order").values_list("examquestiondetail__sort_order")
|
# exam.exam_questions.select_related().all().order_by("examquestiondetail__sort_order").values_list("examquestiondetail__sort_order")
|
||||||
# TODO decide if / how this should be flagged to a user (updating teh exam order will fix)
|
# TODO decide if / how this should be flagged to a user (updating teh exam order will fix)
|
||||||
|
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
"{}/exam_overview.html".format(self.app_name),
|
"{}/exam_overview.html".format(self.app_name),
|
||||||
@@ -1049,10 +1083,12 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
if not request.user.is_superuser:
|
if not request.user.is_superuser:
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
|
|
||||||
cid_users = exam.valid_cid_users.all()#.order_by("cid")#.prefetch_related("*")
|
cid_users = (
|
||||||
|
exam.valid_cid_users.all()
|
||||||
|
) # .order_by("cid")#.prefetch_related("*")
|
||||||
cid_user_count = len(cid_users)
|
cid_user_count = len(cid_users)
|
||||||
|
|
||||||
user_users = exam.valid_user_users.all()#.order_by("username")
|
user_users = exam.valid_user_users.all() # .order_by("username")
|
||||||
user_user_count = len(user_users)
|
user_user_count = len(user_users)
|
||||||
|
|
||||||
user_exam_data = exam.cid_users.all().prefetch_related("cid_user", "user_user")
|
user_exam_data = exam.cid_users.all().prefetch_related("cid_user", "user_user")
|
||||||
@@ -1072,7 +1108,7 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
|
|
||||||
return render(request, "exam_cids.html", context)
|
return render(request, "exam_cids.html", context)
|
||||||
|
|
||||||
#def exam_groups_edit(self, request, exam_id):
|
# def exam_groups_edit(self, request, exam_id):
|
||||||
# exam = get_object_or_404(self.Exam, pk=exam_id)
|
# exam = get_object_or_404(self.Exam, pk=exam_id)
|
||||||
|
|
||||||
# if not request.user.groups.filter(name="cid_user_manager").exists():
|
# if not request.user.groups.filter(name="cid_user_manager").exists():
|
||||||
@@ -1080,8 +1116,7 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
# if request.user not in exam.author.all():
|
# if request.user not in exam.author.all():
|
||||||
# raise PermissionDenied
|
# raise PermissionDenied
|
||||||
|
|
||||||
|
#
|
||||||
#
|
|
||||||
|
|
||||||
# context = {
|
# context = {
|
||||||
# "exam": exam,
|
# "exam": exam,
|
||||||
@@ -1208,7 +1243,9 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
if cid is not None:
|
if cid is not None:
|
||||||
statuses = exam.exam_user_status.filter(cid_user_exam__cid_user__cid=cid)
|
statuses = exam.exam_user_status.filter(cid_user_exam__cid_user__cid=cid)
|
||||||
elif user_id is not None:
|
elif user_id is not None:
|
||||||
statuses = exam.exam_user_status.filter(cid_user_exam__user_user__id=user_id)
|
statuses = exam.exam_user_status.filter(
|
||||||
|
cid_user_exam__user_user__id=user_id
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
statuses = exam.exam_user_status.all()
|
statuses = exam.exam_user_status.all()
|
||||||
|
|
||||||
@@ -1332,7 +1369,8 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
)
|
)
|
||||||
case "longs":
|
case "longs":
|
||||||
question_detail = LongsExamQuestionDetail.objects.get(
|
question_detail = LongsExamQuestionDetail.objects.get(
|
||||||
question=question, exam=exam)
|
question=question, exam=exam
|
||||||
|
)
|
||||||
case "physics":
|
case "physics":
|
||||||
question_detail = PhysicsExamQuestionDetail.objects.get(
|
question_detail = PhysicsExamQuestionDetail.objects.get(
|
||||||
question=question, exam=exam
|
question=question, exam=exam
|
||||||
@@ -1341,7 +1379,7 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
question_detail = SbasExamQuestionDetail.objects.get(
|
question_detail = SbasExamQuestionDetail.objects.get(
|
||||||
question=question, exam=exam
|
question=question, exam=exam
|
||||||
)
|
)
|
||||||
case _:
|
case _:
|
||||||
data = {"status": "error"}
|
data = {"status": "error"}
|
||||||
return JsonResponse(data, status=200)
|
return JsonResponse(data, status=200)
|
||||||
question_detail.sort_order = n
|
question_detail.sort_order = n
|
||||||
@@ -1408,7 +1446,7 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
queryset=self.Answer.objects.filter(
|
queryset=self.Answer.objects.filter(
|
||||||
proposed=False, status=self.Answer.MarkOptions.CORRECT
|
proposed=False, status=self.Answer.MarkOptions.CORRECT
|
||||||
),
|
),
|
||||||
to_attr="prefetched_primary_answer"
|
to_attr="prefetched_primary_answer",
|
||||||
# queryset=self.Answer.objects.filter(),
|
# queryset=self.Answer.objects.filter(),
|
||||||
),
|
),
|
||||||
# Prefetch(
|
# Prefetch(
|
||||||
@@ -1490,15 +1528,17 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@method_decorator(login_required)
|
@method_decorator(login_required)
|
||||||
def exam_question_user_answer(self, request, pk: int, sk: int, c_or_u: str, user_or_cid: str):
|
def exam_question_user_answer(
|
||||||
|
self, request, pk: int, sk: int, c_or_u: str, user_or_cid: str
|
||||||
|
):
|
||||||
exam: ExamBase = get_object_or_404(self.Exam, pk=pk)
|
exam: ExamBase = get_object_or_404(self.Exam, pk=pk)
|
||||||
|
|
||||||
if c_or_u == "u":
|
if c_or_u == "u":
|
||||||
user = get_object_or_404(User,pk=user_or_cid)
|
user = get_object_or_404(User, pk=user_or_cid)
|
||||||
answer = exam.get_question_user_user_answer(sk, user)
|
answer = exam.get_question_user_user_answer(sk, user)
|
||||||
elif c_or_u == "c":
|
elif c_or_u == "c":
|
||||||
#cid_user = CidUser.objects.filter(cid=user_or_cid)
|
# cid_user = CidUser.objects.filter(cid=user_or_cid)
|
||||||
answer =exam.get_question_cid_user_answer(sk, user_or_cid)
|
answer = exam.get_question_cid_user_answer(sk, user_or_cid)
|
||||||
else:
|
else:
|
||||||
raise Http404
|
raise Http404
|
||||||
|
|
||||||
@@ -1510,7 +1550,7 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
exam: ExamBase = get_object_or_404(self.Exam, pk=pk)
|
exam: ExamBase = get_object_or_404(self.Exam, pk=pk)
|
||||||
|
|
||||||
cid_user = CidUser.objects.filter(cid=cid)
|
cid_user = CidUser.objects.filter(cid=cid)
|
||||||
answer =exam.get_question_cid_user_answer(sk, cid_user)
|
answer = exam.get_question_cid_user_answer(sk, cid_user)
|
||||||
|
|
||||||
print(answer)
|
print(answer)
|
||||||
|
|
||||||
@@ -1522,12 +1562,12 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
if not self.check_user_access(request.user, pk, marker=True):
|
if not self.check_user_access(request.user, pk, marker=True):
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
|
|
||||||
#question = exam.get_questions()[sk]
|
# question = exam.get_questions()[sk]
|
||||||
question = exam.get_question_by_index(sk)
|
question = exam.get_question_by_index(sk)
|
||||||
|
|
||||||
exam_length = len(exam.exam_questions.all())
|
exam_length = len(exam.exam_questions.all())
|
||||||
|
|
||||||
#pos = exam.get_question_index(question) + 1
|
# pos = exam.get_question_index(question) + 1
|
||||||
pos = sk + 1
|
pos = sk + 1
|
||||||
|
|
||||||
previous = -1
|
previous = -1
|
||||||
@@ -1559,6 +1599,7 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
"pos": pos,
|
"pos": pos,
|
||||||
"view_feedback": view_feedback,
|
"view_feedback": view_feedback,
|
||||||
"can_edit": can_edit,
|
"can_edit": can_edit,
|
||||||
|
"remote_url": settings.REMOTE_URL,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1800,7 +1841,9 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
cid_user=c, start_time=t
|
cid_user=c, start_time=t
|
||||||
)
|
)
|
||||||
exam.exam_user_status.create(
|
exam.exam_user_status.create(
|
||||||
cid_user_exam=cid_user_exam, status="submitted", extra="manual submission"
|
cid_user_exam=cid_user_exam,
|
||||||
|
status="submitted",
|
||||||
|
extra="manual submission",
|
||||||
)
|
)
|
||||||
|
|
||||||
cid_user_exam.end_time = timezone.now()
|
cid_user_exam.end_time = timezone.now()
|
||||||
@@ -1830,7 +1873,9 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
user_id = None
|
user_id = None
|
||||||
else:
|
else:
|
||||||
user_id = request.user.pk
|
user_id = request.user.pk
|
||||||
if exam.exam_mode and not exam.check_cid_user(cid, passcode, request.user, user_id):
|
if exam.exam_mode and not exam.check_cid_user(
|
||||||
|
cid, passcode, request.user, user_id
|
||||||
|
):
|
||||||
raise Http404("No available exam")
|
raise Http404("No available exam")
|
||||||
|
|
||||||
# exam_json_cache = cache.get("{}_exam_json_{}".format(self.app_name, pk))
|
# exam_json_cache = cache.get("{}_exam_json_{}".format(self.app_name, pk))
|
||||||
@@ -2009,7 +2054,7 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
|
|
||||||
if not exam.check_cid_user(cid, passcode, request.user):
|
if not exam.check_cid_user(cid, passcode, request.user):
|
||||||
if not self.check_user_access(request.user, pk):
|
if not self.check_user_access(request.user, pk):
|
||||||
#raise PermissionDenied
|
# raise PermissionDenied
|
||||||
raise Http404("Error accessing exam")
|
raise Http404("Error accessing exam")
|
||||||
|
|
||||||
if user is not None:
|
if user is not None:
|
||||||
@@ -2059,7 +2104,6 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
if self.app_name == "longs":
|
if self.app_name == "longs":
|
||||||
feedback = user_answer.candidate_feedback
|
feedback = user_answer.candidate_feedback
|
||||||
|
|
||||||
|
|
||||||
match self.app_name:
|
match self.app_name:
|
||||||
case "sbas":
|
case "sbas":
|
||||||
correct_answer = q.get_correct_answer()
|
correct_answer = q.get_correct_answer()
|
||||||
@@ -2083,7 +2127,9 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
answers_marks.append(answer_score)
|
answers_marks.append(answer_score)
|
||||||
|
|
||||||
print(q.get_questions(), ans, answer_score, correct_answer)
|
print(q.get_questions(), ans, answer_score, correct_answer)
|
||||||
merged_ans = zip(q.get_questions(), ans, answer_score, correct_answer)
|
merged_ans = zip(
|
||||||
|
q.get_questions(), ans, answer_score, correct_answer
|
||||||
|
)
|
||||||
answers_and_marks.append((q, merged_ans))
|
answers_and_marks.append((q, merged_ans))
|
||||||
case _:
|
case _:
|
||||||
correct_answer = q.get_primary_answer()
|
correct_answer = q.get_primary_answer()
|
||||||
@@ -2095,7 +2141,9 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
answers_marks.append(answer_score)
|
answers_marks.append(answer_score)
|
||||||
|
|
||||||
if self.app_name == "longs":
|
if self.app_name == "longs":
|
||||||
answers_and_marks.append((ans, answer_score, correct_answer, feedback))
|
answers_and_marks.append(
|
||||||
|
(ans, answer_score, correct_answer, feedback)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
answers_and_marks.append((ans, answer_score, correct_answer))
|
answers_and_marks.append((ans, answer_score, correct_answer))
|
||||||
|
|
||||||
@@ -2109,7 +2157,9 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
answered = [i for i in answers_marks if type(i) == int]
|
answered = [i for i in answers_marks if type(i) == int]
|
||||||
total_score = sum(answered)
|
total_score = sum(answered)
|
||||||
unmarked_number = len(answers_marks) - len(answered)
|
unmarked_number = len(answers_marks) - len(answered)
|
||||||
total_score = "{} ({} unmarked)".format(total_score, unmarked_number)
|
total_score = "{} ({} unmarked)".format(
|
||||||
|
total_score, unmarked_number
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
total_score = sum(answers_marks)
|
total_score = sum(answers_marks)
|
||||||
|
|
||||||
@@ -2171,7 +2221,7 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
|
|
||||||
# user_answers_and_marks = defaultdict(list)
|
# user_answers_and_marks = defaultdict(list)
|
||||||
user_answers_marks = defaultdict(list)
|
user_answers_marks = defaultdict(list)
|
||||||
#user_answers = defaultdict(list)
|
# user_answers = defaultdict(list)
|
||||||
# user_names = {}
|
# user_names = {}
|
||||||
# cid_passcodes = {}
|
# cid_passcodes = {}
|
||||||
|
|
||||||
@@ -2245,7 +2295,7 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
if answer_score == "unmarked":
|
if answer_score == "unmarked":
|
||||||
index = exam.get_question_index(q)
|
index = exam.get_question_index(q)
|
||||||
unmarked.add(index)
|
unmarked.add(index)
|
||||||
#user_answers[cid].append(ans)
|
# user_answers[cid].append(ans)
|
||||||
user_answers_marks[cid].append(answer_score)
|
user_answers_marks[cid].append(answer_score)
|
||||||
|
|
||||||
if self.app_name == "rapids":
|
if self.app_name == "rapids":
|
||||||
@@ -2258,9 +2308,6 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
zipped_ans_scores = zip(ans, answer_score)
|
zipped_ans_scores = zip(ans, answer_score)
|
||||||
by_question[q][cid] = zipped_ans_scores
|
by_question[q][cid] = zipped_ans_scores
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
user_scores = {}
|
user_scores = {}
|
||||||
user_scores_normalised = {}
|
user_scores_normalised = {}
|
||||||
user_answer_count = {}
|
user_answer_count = {}
|
||||||
@@ -2273,11 +2320,10 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
# If either question or user do not exist we give a default
|
# If either question or user do not exist we give a default
|
||||||
# not answered == score of 3
|
# not answered == score of 3
|
||||||
if user not in by_question[question]:
|
if user not in by_question[question]:
|
||||||
#print("NOT in")
|
# print("NOT in")
|
||||||
by_question[question][user] = ("Not answered", 3.0)
|
by_question[question][user] = ("Not answered", 3.0)
|
||||||
user_answers_marks[user].append(3.0)
|
user_answers_marks[user].append(3.0)
|
||||||
|
|
||||||
|
|
||||||
if self.app_name in ("rapids", "anatomy", "sbas", "longs"):
|
if self.app_name in ("rapids", "anatomy", "sbas", "longs"):
|
||||||
user_scores[user] = sum(
|
user_scores[user] = sum(
|
||||||
[i for i in user_answers_marks[user] if i != "unmarked"]
|
[i for i in user_answers_marks[user] if i != "unmarked"]
|
||||||
@@ -2312,7 +2358,7 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
mode = 0
|
mode = 0
|
||||||
fig_html = ""
|
fig_html = ""
|
||||||
else:
|
else:
|
||||||
#Exclude very low scores from statistics
|
# Exclude very low scores from statistics
|
||||||
lower_score_bound = max_score / 5
|
lower_score_bound = max_score / 5
|
||||||
bounded_scores = [i for i in user_scores_list if i > lower_score_bound]
|
bounded_scores = [i for i in user_scores_list if i > lower_score_bound]
|
||||||
if bounded_scores:
|
if bounded_scores:
|
||||||
@@ -2399,9 +2445,9 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
}
|
}
|
||||||
|
|
||||||
if self.app_name == "rapids":
|
if self.app_name == "rapids":
|
||||||
template_variables[
|
template_variables["user_answers_callstates"] = (
|
||||||
"user_answers_callstates"
|
user_answers_callstates_counted
|
||||||
] = user_answers_callstates_counted
|
)
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
@@ -2418,9 +2464,7 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
if not exam.exam_mode:
|
if not exam.exam_mode:
|
||||||
raise Http404("Packet not in exam mode")
|
raise Http404("Packet not in exam mode")
|
||||||
|
|
||||||
template_variables = {
|
template_variables = {"exam": exam}
|
||||||
"exam": exam
|
|
||||||
}
|
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
@@ -2475,7 +2519,7 @@ class GenericViewBase:
|
|||||||
|
|
||||||
@method_decorator(login_required)
|
@method_decorator(login_required)
|
||||||
def question_detail(self, request, pk):
|
def question_detail(self, request, pk):
|
||||||
question = get_object_or_404(self.question_object, pk=pk)
|
question: QuestionBase = get_object_or_404(self.question_object, pk=pk)
|
||||||
|
|
||||||
if not question.open_access:
|
if not question.open_access:
|
||||||
if (
|
if (
|
||||||
@@ -2498,7 +2542,12 @@ class GenericViewBase:
|
|||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
f"{self.app_name}/question_detail.html",
|
f"{self.app_name}/question_detail.html",
|
||||||
{"question": question, "view_feedback": view_feedback},
|
{
|
||||||
|
"question": question,
|
||||||
|
"view_feedback": view_feedback,
|
||||||
|
"remote_url": settings.REMOTE_URL,
|
||||||
|
"can_edit": question.can_edit(request.user),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: improve permissions on these
|
# TODO: improve permissions on these
|
||||||
@@ -2513,15 +2562,22 @@ class GenericViewBase:
|
|||||||
question = get_object_or_404(self.question_object, pk=pk)
|
question = get_object_or_404(self.question_object, pk=pk)
|
||||||
|
|
||||||
if answer_compare is None:
|
if answer_compare is None:
|
||||||
answers = self.cid_user_answer_object.objects.filter(question__id=question.pk).prefetch_related("question", "exam", "user")
|
answers = self.cid_user_answer_object.objects.filter(
|
||||||
|
question__id=question.pk
|
||||||
|
).prefetch_related("question", "exam", "user")
|
||||||
else:
|
else:
|
||||||
answers = self.cid_user_answer_object.objects.filter(question__id=question.pk, answer_compare=answer_compare).prefetch_related("question", "exam", "user")
|
answers = self.cid_user_answer_object.objects.filter(
|
||||||
|
question__id=question.pk, answer_compare=answer_compare
|
||||||
|
).prefetch_related("question", "exam", "user")
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
f"{self.app_name}/question_user_answers.html",
|
f"{self.app_name}/question_user_answers.html",
|
||||||
{"question": question, "answers": answers, "answer_compare": answer_compare},
|
{
|
||||||
|
"question": question,
|
||||||
|
"answers": answers,
|
||||||
|
"answer_compare": answer_compare,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@method_decorator(login_required)
|
@method_decorator(login_required)
|
||||||
@@ -2547,11 +2603,10 @@ class GenericViewBase:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ExamCloneMixin():
|
class ExamCloneMixin:
|
||||||
def get_template_names(self) -> list[str]:
|
def get_template_names(self) -> list[str]:
|
||||||
return "exam_clone_form.html"
|
return "exam_clone_form.html"
|
||||||
#return super().get_template_names()
|
# return super().get_template_names()
|
||||||
|
|
||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
old_object = get_object_or_404(self.model, pk=self.kwargs["exam_id"])
|
old_object = get_object_or_404(self.model, pk=self.kwargs["exam_id"])
|
||||||
@@ -2616,6 +2671,8 @@ class ExaminationAutocomplete(autocomplete.Select2QuerySetView):
|
|||||||
return Examination.objects.none()
|
return Examination.objects.none()
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
|
||||||
class SupervisorAutocomplete(autocomplete.Select2QuerySetView):
|
class SupervisorAutocomplete(autocomplete.Select2QuerySetView):
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
# TODO: we should probably filter this to only
|
# TODO: we should probably filter this to only
|
||||||
@@ -2745,6 +2802,7 @@ def cid_group_view_all(request):
|
|||||||
{"groups": groups, "view_all": True},
|
{"groups": groups, "view_all": True},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@user_is_cid_user_manager
|
@user_is_cid_user_manager
|
||||||
def user_group_add_candidates_to_exams(request, group_id):
|
def user_group_add_candidates_to_exams(request, group_id):
|
||||||
if request.htmx:
|
if request.htmx:
|
||||||
@@ -2763,13 +2821,12 @@ def user_group_add_candidates_to_exams(request, group_id):
|
|||||||
else:
|
else:
|
||||||
exam.valid_user_users.add(*group.users.all())
|
exam.valid_user_users.add(*group.users.all())
|
||||||
return HttpResponse(f"Candidates added")
|
return HttpResponse(f"Candidates added")
|
||||||
#exam.save()
|
# exam.save()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
raise PermissionDenied() # or Http404
|
raise PermissionDenied() # or Http404
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@user_is_cid_user_manager
|
@user_is_cid_user_manager
|
||||||
def user_group_view_detail(request, group_id):
|
def user_group_view_detail(request, group_id):
|
||||||
group = get_object_or_404(UserUserGroup, pk=group_id)
|
group = get_object_or_404(UserUserGroup, pk=group_id)
|
||||||
@@ -2816,6 +2873,7 @@ def get_user_selection_from_request(request):
|
|||||||
user_models = User.objects.filter(id__in=selected_users)
|
user_models = User.objects.filter(id__in=selected_users)
|
||||||
return user_models
|
return user_models
|
||||||
|
|
||||||
|
|
||||||
@user_is_cid_user_manager
|
@user_is_cid_user_manager
|
||||||
def user_not_trainee(request, user_id):
|
def user_not_trainee(request, user_id):
|
||||||
user = get_object_or_404(User, pk=user_id)
|
user = get_object_or_404(User, pk=user_id)
|
||||||
@@ -2823,7 +2881,10 @@ def user_not_trainee(request, user_id):
|
|||||||
user.userprofile.peninsula_trainee = False
|
user.userprofile.peninsula_trainee = False
|
||||||
user.userprofile.save()
|
user.userprofile.save()
|
||||||
|
|
||||||
return HttpResponse(f"{user.username} is no longer a trainee", content_type="text/plain")
|
return HttpResponse(
|
||||||
|
f"{user.username} is no longer a trainee", content_type="text/plain"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@user_is_cid_user_manager
|
@user_is_cid_user_manager
|
||||||
def users_bulk_edit(request):
|
def users_bulk_edit(request):
|
||||||
@@ -2862,8 +2923,7 @@ def users_bulk_edit(request):
|
|||||||
# TODO: work out how to actually use hyperscript
|
# TODO: work out how to actually use hyperscript
|
||||||
html = (
|
html = (
|
||||||
html
|
html
|
||||||
+
|
+ f"""<button class='change-grade-button' id='add-grade--remove'
|
||||||
f"""<button class='change-grade-button' id='add-grade--remove'
|
|
||||||
hx-post="{reverse('generic:users_bulk_edit')}"
|
hx-post="{reverse('generic:users_bulk_edit')}"
|
||||||
hx-include="[name='selection']"
|
hx-include="[name='selection']"
|
||||||
hx-confirm="This will remove grades of all selected users, are you sure you wish to continue?"
|
hx-confirm="This will remove grades of all selected users, are you sure you wish to continue?"
|
||||||
@@ -3064,6 +3124,7 @@ def candidate_email_results_resend(request, cid, resend=True):
|
|||||||
def create_cid_email(request):
|
def create_cid_email(request):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@user_is_cid_user_manager
|
@user_is_cid_user_manager
|
||||||
def cid_details(request, cid: int):
|
def cid_details(request, cid: int):
|
||||||
print(cid)
|
print(cid)
|
||||||
@@ -3076,6 +3137,7 @@ def cid_details(request, cid: int):
|
|||||||
|
|
||||||
raise PermissionDenied() # or Http404
|
raise PermissionDenied() # or Http404
|
||||||
|
|
||||||
|
|
||||||
@user_is_cid_user_manager
|
@user_is_cid_user_manager
|
||||||
def manage_cid_users(request):
|
def manage_cid_users(request):
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
@@ -3307,11 +3369,13 @@ class UserUserGroupUpdate(RevisionMixin, CidManagerRequiredMixin, UpdateView):
|
|||||||
context["group_type"] = "user"
|
context["group_type"] = "user"
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class UserUserGroupDelete(RevisionMixin, CidManagerRequiredMixin, DeleteView):
|
class UserUserGroupDelete(RevisionMixin, CidManagerRequiredMixin, DeleteView):
|
||||||
model = UserUserGroup
|
model = UserUserGroup
|
||||||
template_name = "confirm_delete.html"
|
template_name = "confirm_delete.html"
|
||||||
success_url = reverse_lazy("generic:user_group_view")
|
success_url = reverse_lazy("generic:user_group_view")
|
||||||
|
|
||||||
|
|
||||||
class UserGroupExamUpdate(CidManagerRequiredMixin, UpdateView):
|
class UserGroupExamUpdate(CidManagerRequiredMixin, UpdateView):
|
||||||
model = UserUserGroup
|
model = UserUserGroup
|
||||||
form_class = UserGroupExamForm
|
form_class = UserGroupExamForm
|
||||||
@@ -3321,6 +3385,7 @@ class UserGroupExamUpdate(CidManagerRequiredMixin, UpdateView):
|
|||||||
context["group_type"] = "user"
|
context["group_type"] = "user"
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class CidGroupExamUpdate(CidManagerRequiredMixin, UpdateView):
|
class CidGroupExamUpdate(CidManagerRequiredMixin, UpdateView):
|
||||||
model = CidUserGroup
|
model = CidUserGroup
|
||||||
form_class = CidGroupExamForm
|
form_class = CidGroupExamForm
|
||||||
@@ -3330,6 +3395,7 @@ class CidGroupExamUpdate(CidManagerRequiredMixin, UpdateView):
|
|||||||
context["group_type"] = "cid"
|
context["group_type"] = "cid"
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class ExamCreateBase(RevisionMixin, LoginRequiredMixin, CreateView):
|
class ExamCreateBase(RevisionMixin, LoginRequiredMixin, CreateView):
|
||||||
template_name = "exam_create_form.html"
|
template_name = "exam_create_form.html"
|
||||||
|
|
||||||
@@ -3390,26 +3456,30 @@ def exam_inactive(request, context):
|
|||||||
#
|
#
|
||||||
# success_url = reverse_lazy("accounts_list")
|
# success_url = reverse_lazy("accounts_list")
|
||||||
|
|
||||||
@user_is_cid_user_manager
|
|
||||||
def trainees(request, grade: None|str=None):
|
|
||||||
|
|
||||||
trainees = UserProfile.objects.filter(peninsula_trainee=True, user__is_active=True).order_by("grade__name", Lower("user__last_name")).prefetch_related("supervisor", "grade", "user")
|
@user_is_cid_user_manager
|
||||||
|
def trainees(request, grade: None | str = None):
|
||||||
|
|
||||||
|
trainees = (
|
||||||
|
UserProfile.objects.filter(peninsula_trainee=True, user__is_active=True)
|
||||||
|
.order_by("grade__name", Lower("user__last_name"))
|
||||||
|
.prefetch_related("supervisor", "grade", "user")
|
||||||
|
)
|
||||||
|
|
||||||
if grade is not None:
|
if grade is not None:
|
||||||
trainees = trainees.filter(grade__name=grade)
|
trainees = trainees.filter(grade__name=grade)
|
||||||
|
|
||||||
context = {
|
context = {"trainees": trainees, "grade": grade}
|
||||||
"trainees" : trainees,
|
|
||||||
"grade": grade
|
|
||||||
}
|
|
||||||
|
|
||||||
return render(request, "generic/trainees.html", context)
|
return render(request, "generic/trainees.html", context)
|
||||||
|
|
||||||
|
|
||||||
def create_trainee(request, context=None):
|
def create_trainee(request, context=None):
|
||||||
return create_user(request, context, trainee=True)
|
return create_user(request, context, trainee=True)
|
||||||
|
|
||||||
|
|
||||||
@user_is_cid_user_manager
|
@user_is_cid_user_manager
|
||||||
def create_user(request, context=None, trainee: bool=False):
|
def create_user(request, context=None, trainee: bool = False):
|
||||||
if trainee:
|
if trainee:
|
||||||
form_template = "generic/trainee_creation_form.html"
|
form_template = "generic/trainee_creation_form.html"
|
||||||
else:
|
else:
|
||||||
@@ -3499,9 +3569,10 @@ class SupervisorCreate(CidManagerRequiredMixin, CreateView):
|
|||||||
# success_url = reverse_lazy("generic:supervisor_detail")
|
# success_url = reverse_lazy("generic:supervisor_detail")
|
||||||
|
|
||||||
|
|
||||||
#class SupervisorList(CidManagerRequiredMixin, ListView):
|
# class SupervisorList(CidManagerRequiredMixin, ListView):
|
||||||
# model = Supervisor
|
# model = Supervisor
|
||||||
|
|
||||||
|
|
||||||
class SupervisorList(CidManagerRequiredMixin, SingleTableMixin, FilterView):
|
class SupervisorList(CidManagerRequiredMixin, SingleTableMixin, FilterView):
|
||||||
model = Supervisor
|
model = Supervisor
|
||||||
table_class = SupervisorTable
|
table_class = SupervisorTable
|
||||||
@@ -3509,20 +3580,25 @@ class SupervisorList(CidManagerRequiredMixin, SingleTableMixin, FilterView):
|
|||||||
|
|
||||||
filterset_class = SupervisorFilter
|
filterset_class = SupervisorFilter
|
||||||
|
|
||||||
|
|
||||||
class ExamCollectionList(ListView):
|
class ExamCollectionList(ListView):
|
||||||
model = ExamCollection
|
model = ExamCollection
|
||||||
|
|
||||||
|
|
||||||
class ExamCollectionDetail(DetailView, AuthorRequiredMixin):
|
class ExamCollectionDetail(DetailView, AuthorRequiredMixin):
|
||||||
model = ExamCollection
|
model = ExamCollection
|
||||||
|
|
||||||
|
|
||||||
class ExamCollectionEdit(UpdateView, AuthorRequiredMixin):
|
class ExamCollectionEdit(UpdateView, AuthorRequiredMixin):
|
||||||
model = ExamCollection
|
model = ExamCollection
|
||||||
form_class = ExamCollectionForm
|
form_class = ExamCollectionForm
|
||||||
|
|
||||||
|
|
||||||
class ExamCollectionCreate(CreateView, AuthorRequiredMixin):
|
class ExamCollectionCreate(CreateView, AuthorRequiredMixin):
|
||||||
model = ExamCollection
|
model = ExamCollection
|
||||||
form_class = ExamCollectionForm
|
form_class = ExamCollectionForm
|
||||||
|
|
||||||
|
|
||||||
class ExamCollectionClone(CreateView, AuthorRequiredMixin):
|
class ExamCollectionClone(CreateView, AuthorRequiredMixin):
|
||||||
model = ExamCollection
|
model = ExamCollection
|
||||||
template_name = "generic/examcollection_clone_form.html"
|
template_name = "generic/examcollection_clone_form.html"
|
||||||
@@ -3532,23 +3608,22 @@ class ExamCollectionClone(CreateView, AuthorRequiredMixin):
|
|||||||
old_object = get_object_or_404(self.model, pk=self.kwargs["pk"])
|
old_object = get_object_or_404(self.model, pk=self.kwargs["pk"])
|
||||||
self.initial_object = old_object
|
self.initial_object = old_object
|
||||||
|
|
||||||
|
|
||||||
initial_data = model_to_dict(old_object, exclude=["id", "date"])
|
initial_data = model_to_dict(old_object, exclude=["id", "date"])
|
||||||
|
|
||||||
## We manually transfer the forign keys / m2m relationships
|
## We manually transfer the forign keys / m2m relationships
|
||||||
#questions = old_object.exam_questions.all().values_list("id", flat=True)
|
# questions = old_object.exam_questions.all().values_list("id", flat=True)
|
||||||
authors = old_object.author.all().values_list("id", flat=True)
|
authors = old_object.author.all().values_list("id", flat=True)
|
||||||
|
|
||||||
## clear the associated groups
|
## clear the associated groups
|
||||||
#initial_data["valid_user_users"] = []
|
# initial_data["valid_user_users"] = []
|
||||||
#initial_data["valid_cid_users"] = []
|
# initial_data["valid_cid_users"] = []
|
||||||
#initial_data["cid_user_groups"] = []
|
# initial_data["cid_user_groups"] = []
|
||||||
#initial_data["user_user_groups"] = []
|
# initial_data["user_user_groups"] = []
|
||||||
|
|
||||||
#initial_data["active"] = False
|
# initial_data["active"] = False
|
||||||
#initial_data["publish_results"] = False
|
# initial_data["publish_results"] = False
|
||||||
|
|
||||||
#self.exam_questions = list(questions)
|
# self.exam_questions = list(questions)
|
||||||
self.author = list(authors)
|
self.author = list(authors)
|
||||||
|
|
||||||
return initial_data
|
return initial_data
|
||||||
@@ -3556,7 +3631,7 @@ class ExamCollectionClone(CreateView, AuthorRequiredMixin):
|
|||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
object = form.save()
|
object = form.save()
|
||||||
# Reapply these otherwise they get lost?
|
# Reapply these otherwise they get lost?
|
||||||
#object.exam_questions.set(self.exam_questions)
|
# object.exam_questions.set(self.exam_questions)
|
||||||
|
|
||||||
GROUP_TYPES = (
|
GROUP_TYPES = (
|
||||||
("Rapid Exams", "rapids_exams", RapidsExam),
|
("Rapid Exams", "rapids_exams", RapidsExam),
|
||||||
@@ -3570,11 +3645,13 @@ class ExamCollectionClone(CreateView, AuthorRequiredMixin):
|
|||||||
object.save()
|
object.save()
|
||||||
return HttpResponseRedirect(object.get_absolute_url())
|
return HttpResponseRedirect(object.get_absolute_url())
|
||||||
|
|
||||||
|
|
||||||
class ExamCollectionDelete(DeleteView, AuthorRequiredMixin):
|
class ExamCollectionDelete(DeleteView, AuthorRequiredMixin):
|
||||||
model = ExamCollection
|
model = ExamCollection
|
||||||
template_name = "confirm_delete.html"
|
template_name = "confirm_delete.html"
|
||||||
success_url = reverse_lazy("generic:examcollection_list")
|
success_url = reverse_lazy("generic:examcollection_list")
|
||||||
|
|
||||||
|
|
||||||
class ExaminationView(SuperuserRequiredMixin, SingleTableMixin, FilterView):
|
class ExaminationView(SuperuserRequiredMixin, SingleTableMixin, FilterView):
|
||||||
model = Examination
|
model = Examination
|
||||||
table_class = ExaminationTable
|
table_class = ExaminationTable
|
||||||
@@ -3596,7 +3673,8 @@ class ExaminationUpdate(UpdateView, SuperuserRequiredMixin):
|
|||||||
|
|
||||||
class SeriesImagesZipViewBase(SuperuserRequiredMixin, View):
|
class SeriesImagesZipViewBase(SuperuserRequiredMixin, View):
|
||||||
"""Download all images from an image series"""
|
"""Download all images from an image series"""
|
||||||
http_method_names = ['get']
|
|
||||||
|
http_method_names = ["get"]
|
||||||
series_object = None
|
series_object = None
|
||||||
|
|
||||||
def get_files(self):
|
def get_files(self):
|
||||||
@@ -3605,12 +3683,14 @@ class SeriesImagesZipViewBase(SuperuserRequiredMixin, View):
|
|||||||
return [i.image.file for i in series.get_images()]
|
return [i.image.file for i in series.get_images()]
|
||||||
|
|
||||||
def get_archive_name(self, request):
|
def get_archive_name(self, request):
|
||||||
#series = Series.objects.get(pk=self.kwargs["pk"])
|
# series = Series.objects.get(pk=self.kwargs["pk"])
|
||||||
return f"Series {self.kwargs['pk']}.zip"
|
return f"Series {self.kwargs['pk']}.zip"
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
temp_file = ContentFile(b"", name=self.get_archive_name(request))
|
temp_file = ContentFile(b"", name=self.get_archive_name(request))
|
||||||
with zipfile.ZipFile(temp_file, mode='w', compression=zipfile.ZIP_DEFLATED) as zip_file:
|
with zipfile.ZipFile(
|
||||||
|
temp_file, mode="w", compression=zipfile.ZIP_DEFLATED
|
||||||
|
) as zip_file:
|
||||||
files = self.get_files()
|
files = self.get_files()
|
||||||
for file_ in files:
|
for file_ in files:
|
||||||
path = os.path.split(file_.name)[-1]
|
path = os.path.split(file_.name)[-1]
|
||||||
@@ -3619,24 +3699,35 @@ class SeriesImagesZipViewBase(SuperuserRequiredMixin, View):
|
|||||||
file_size = temp_file.tell()
|
file_size = temp_file.tell()
|
||||||
temp_file.seek(0)
|
temp_file.seek(0)
|
||||||
|
|
||||||
response = HttpResponse(temp_file, content_type='application/zip')
|
response = HttpResponse(temp_file, content_type="application/zip")
|
||||||
response['Content-Disposition'] = 'attachment; filename=%s' % self.get_archive_name(request)
|
response["Content-Disposition"] = (
|
||||||
response['Content-Length'] = file_size
|
"attachment; filename=%s" % self.get_archive_name(request)
|
||||||
|
)
|
||||||
|
response["Content-Length"] = file_size
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
class ExamGroupsUpdateBase(
|
class ExamGroupsUpdateBase(
|
||||||
RevisionMixin, CheckCanEditMixin, LoginRequiredMixin, AuthorRequiredMixin, UpdateView
|
RevisionMixin,
|
||||||
|
CheckCanEditMixin,
|
||||||
|
LoginRequiredMixin,
|
||||||
|
AuthorRequiredMixin,
|
||||||
|
UpdateView,
|
||||||
):
|
):
|
||||||
template_name = "generic/exam_groups_edit.html"
|
template_name = "generic/exam_groups_edit.html"
|
||||||
|
|
||||||
def get_success_url(self) -> str:
|
def get_success_url(self) -> str:
|
||||||
return reverse_lazy(f"{self.object.get_app_name()}:exam_cids", kwargs={"exam_id": self.object.pk})
|
return reverse_lazy(
|
||||||
|
f"{self.object.get_app_name()}:exam_cids",
|
||||||
|
kwargs={"exam_id": self.object.pk},
|
||||||
|
)
|
||||||
|
|
||||||
def get_form_kwargs(self):
|
def get_form_kwargs(self):
|
||||||
kwargs = super(ExamGroupsUpdateBase, self).get_form_kwargs()
|
kwargs = super(ExamGroupsUpdateBase, self).get_form_kwargs()
|
||||||
kwargs.update({"user": self.request.user})
|
kwargs.update({"user": self.request.user})
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
class UpdateQuestionMixin(RedirectMixin, RevisionMixin, UpdateView):
|
class UpdateQuestionMixin(RedirectMixin, RevisionMixin, UpdateView):
|
||||||
|
|
||||||
def get_form_kwargs(self):
|
def get_form_kwargs(self):
|
||||||
@@ -3660,4 +3751,3 @@ class UpdateQuestionMixin(RedirectMixin, RevisionMixin, UpdateView):
|
|||||||
if self.request.user in obj.get_author_objects():
|
if self.request.user in obj.get_author_objects():
|
||||||
return context
|
return context
|
||||||
raise PermissionDenied() # or Http404
|
raise PermissionDenied() # or Http404
|
||||||
|
|
||||||
|
|||||||
@@ -268,7 +268,7 @@ button a {
|
|||||||
|
|
||||||
#save-annotations {
|
#save-annotations {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
bottom: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#save-annotations.save-rapid-annotations {
|
#save-annotations.save-rapid-annotations {
|
||||||
@@ -1287,4 +1287,29 @@ tr:has(> td > a) {
|
|||||||
/* For crispy... */
|
/* For crispy... */
|
||||||
.select2-selection {
|
.select2-selection {
|
||||||
padding-bottom: 30px;
|
padding-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-text, .help-text summary {
|
||||||
|
cursor: help !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
details.help-text > summary {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .selector, .selector-available, .selector-chosen {
|
||||||
|
display: inline-block;
|
||||||
|
} */
|
||||||
|
|
||||||
|
#div_id_exams {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dicom-image .help-text{
|
||||||
|
position: absolute;
|
||||||
|
z-index: 9999999;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dicom-image .help-text:hover{
|
||||||
|
background-color: rgba(0, 0, 0, 0.546);
|
||||||
}
|
}
|
||||||
@@ -24,9 +24,6 @@ urlpatterns = [
|
|||||||
views.QuestionDelete.as_view(),
|
views.QuestionDelete.as_view(),
|
||||||
name="question_delete",
|
name="question_delete",
|
||||||
),
|
),
|
||||||
# path("question/<int:pk>/update", views.QuestionUpdate.as_view(), name="anatomy_question_update"),
|
|
||||||
# path("exam/<int:pk>/<int:sk>/mark", views.mark, name="mark"),
|
|
||||||
# path("exam/<int:pk>/mark", views.mark_overview, name="mark_overview"),
|
|
||||||
path(
|
path(
|
||||||
"exam/<int:pk>/<int:sk>/<str:cid>/<str:passcode>/take",
|
"exam/<int:pk>/<int:sk>/<str:cid>/<str:passcode>/take",
|
||||||
views.exam_take,
|
views.exam_take,
|
||||||
|
|||||||
Reference in New Issue
Block a user