Too many changes :(

This commit is contained in:
Ross
2025-07-29 09:20:58 +01:00
parent ad4be1e9a6
commit 4460545442
20 changed files with 361 additions and 81 deletions
+2 -1
View File
@@ -2,11 +2,12 @@ from django.contrib import admin
# Register your models here.
from .models import Question, SampleAnswer, Exam, ExamUserStatus, UserAnswer
from .models import Question, SampleAnswer, Exam, ExamUserStatus, UserAnswer, QuestionImage
admin.site.register(Question)
admin.site.register(SampleAnswer)
admin.site.register(UserAnswer)
admin.site.register(QuestionImage)
#class RapidImageInline(admin.TabularInline):
# model = QuestionImage
+46 -1
View File
@@ -31,6 +31,9 @@ from rapids.models import Abnormality, Examination, Region
from helpers.images import get_image_hash, image_as_base64
from django.utils.html import format_html
from helpers.images import combine_dicom_images_side_by_side
from django.core.files import File
import tempfile
def image_directory_path(instance, filename):
@@ -108,6 +111,43 @@ class Question(QuestionBase):
except pydicom.errors.InvalidDicomError:
pass
def combine_images_side_by_side(self, image1_id, image2_id):
"""
Combines two DICOM images side by side and saves the new image.
Marks the original images as feedback images.
"""
img1 = self.images.get(id=image1_id)
img2 = self.images.get(id=image2_id)
dicom_path1 = os.path.join(settings.MEDIA_ROOT, img1.image.name)
dicom_path2 = os.path.join(settings.MEDIA_ROOT, img2.image.name)
# Combine images (returns a FileDataset)
combined_dataset = combine_dicom_images_side_by_side(dicom_path1, dicom_path2)
# Save new image directly to storage
combined_image_name = f"combined_{img1.filename}_{img2.filename}.dcm"
combined_image_path = os.path.join("shorts/picture", combined_image_name)
full_path = os.path.join(settings.MEDIA_ROOT, combined_image_path)
combined_dataset.save_as(full_path)
with open(full_path, "rb") as f:
new_image = QuestionImage(
question=self,
image=File(f, name=combined_image_path),
feedback_image=False,
is_dicom=True,
)
new_image.save()
# Mark originals as feedback images
img1.feedback_image = True
img1.save()
img2.feedback_image = True
img2.save()
return new_image
def check_user_can_edit(self, user):
if user.is_superuser:
return True
@@ -169,6 +209,11 @@ class Question(QuestionBase):
user_answers = set([i.answer for i in queryset])
return user_answers
def get_examination_string(self):
"""Returns a string of the examinations associated with the question."""
examinations = self.examination.all().values_list("examination", flat=True)
return ", ".join(examinations)
class QuestionImage(models.Model):
@@ -358,7 +403,7 @@ class Exam(ExamBase):
if i.description:
image_titles.append(i.description)
else:
image_titles.append("")
image_titles.append(q.get_examination_string())
exam_questions[q.id] = {
"images": images,
@@ -0,0 +1 @@
<div class="alert alert-warning">{{ error }}</div>
@@ -0,0 +1,19 @@
<span class="image-block">
Image {{ forloop.counter }}{% if image.description %} ({{image.description}}){% endif %}{% if image.feedback_image %} [feedback image]{% endif %}:
<div class="dicom-image shorts-img {% if image.feedback_image %}feedback-img{% endif %}"
data-url="{{ remote_url }}{{ image.image.url}}"></div>
</span>
<script>
document.body.addEventListener("htmx:afterSwap", function(evt) {
const dataUrl = "{{ remote_url }}{{ image.image.url}}";
console.log(dataUrl);
const dicomDiv = document.querySelector('.dicom-image[data-url="' + dataUrl + '"]');
console.log(dicomDiv);
if (dicomDiv) {
setUpDicom(dicomDiv);
}
});
</script>
@@ -18,23 +18,66 @@
</p>
<p class="pre-whitespace"><b>Abnormality:</b> {{ question.get_abnormalities }}</p>
<div class="pre-whitespace multi-image-block"><b>Images:</b>
{% for image in question.images.all %}
<span class="image-block">
Image {{ forloop.counter }}{% if image.description %} ({{image.description}}){% endif %}{% if image.feedback_image %} [feedback image]{% endif %}:
<div class="dicom-image shorts-img {% if image.feedback_image %}feedback-img{% endif %}"
data-url="{{ remote_url }}{{ image.image.url}}"></div>
</span>
{% endfor %}
<form>
{% for image in question.images.all %}
<span class="image-block" style="display: inline-block; vertical-align: top; margin-right: 10px;">
Image {{ forloop.counter }}{% if image.description %} ({{image.description}}){% endif %}{% if image.feedback_image %} [feedback image]{% endif %}:
<div class="dicom-image shorts-img {% if image.feedback_image %}feedback-img{% endif %}"
data-url="{{ remote_url }}{{ image.image.url}}"></div>
<input type="checkbox" class="combine-image-checkbox hide" name="combine-image-checkbox" value="{{ image.pk }}">
</span>
{% endfor %}
<details id="combine-images-details">
<summary>Combine Images</summary>
<button id="combine-images-btn"
class="btn btn-sm btn-outline-primary"
hx-post="{% url 'shorts:combine_images_side_by_side' question.pk %}"
hx-include=".combine-image-checkbox:checked"
hx-target="#combine-images-result"
hx-swap="innerHTML"
hx-indicator="#combine-images-progress">
Combine Selected Images Side by Side
</button>
<div id="combine-images-progress" class="htmx-indicator">
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
Combining images, please wait...
</div>
<div id="combine-images-result"></div>
</details>
<script>
document.addEventListener("DOMContentLoaded", function() {
const checkboxBlocks = document.querySelectorAll(".combine-image-checkbox");
const details = document.getElementById("combine-images-details");
console.log("checkboxBlocks", checkboxBlocks);
function updateVisibility() {
if (details.open) {
checkboxBlocks.forEach(cb => cb.style.display = "inline-block");
// Auto-select if only two checkboxes
if (checkboxBlocks.length === 2) {
checkboxBlocks.forEach(cb => cb.checked = true);
}
} else {
checkboxBlocks.forEach(cb => cb.style.display = "none");
}
}
if (details) {
updateVisibility();
details.addEventListener("toggle", updateVisibility);
}
});
</script>
</form>
</div>
<a href='{% url "shorts:question_findings" question_id=question.pk %}'>Click here to view/add findings</a>
<div>
Exam(s): {% for exam in question.exams.all %}
<a href="{% url 'shorts:exam_overview' pk=exam.pk %}">{{ exam }}</a>,
{% endfor %}
<button class="btn btn-sm" hx-get="{% url 'shorts:question_add_exam' question_id=question.pk %}"
hx-target="#exam-list"
hx-swap="innerHTML">Edit exam(s)</button>
<span id="exam-list"></span>
<button class="btn btn-sm" hx-get="{% url 'shorts:question_add_exam' question_id=question.pk %}"
hx-target="#exam-list"
hx-swap="innerHTML">Edit exam(s)</button>
<span id="exam-list"></span>
</div>
<p class="pre-whitespace"><b>Open Access:</b> {{ question.open_access }}</p>
@@ -56,8 +99,8 @@
{{ answer }}
</span>
</td>
<td>{{answer.score}}</td>
</tr>
<td>{{answer.score}}</td>
</tr>
{% endfor %}
</table>
</details>
@@ -119,7 +162,7 @@
json_toolstates.push(JSON.stringify(toolstate[image]));
} else if ("wadouri:" + image in toolstate) {
json_toolstates.push(JSON.stringify(toolstate["wadouri:" + image]));
}
}
}
console.log("json_toolstates", json_toolstates)
+1
View File
@@ -46,6 +46,7 @@ urlpatterns = [
views.question_add_exam,
name="question_add_exam",
),
path('question/<int:question_id>/combine-images/', views.combine_images_side_by_side, name='combine_images_side_by_side'),
#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(
+13
View File
@@ -1,6 +1,7 @@
import json
from django.shortcuts import render, get_object_or_404, redirect
from django import forms
from django.views.decorators.http import require_POST
# from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required, user_passes_test
@@ -42,6 +43,7 @@ from generic.views import (
)
from generic.mixins import CheckCanEditMixin, SuperuserRequiredMixin
from atlas.models import Finding, Structure, Condition
from rad import settings
from shorts.decorators import user_is_author_or_shorts_checker
from .models import (
@@ -949,3 +951,14 @@ def question_findings(request, question_id, finding_pk=None):
},
)
@require_POST
def combine_images_side_by_side(request, question_id):
image_ids = request.POST.getlist('combine-image-checkbox')
print(image_ids)
if len(image_ids) != 2:
return render(request, "shorts/combine_error.html", {"error": "Please select exactly two images."})
question = get_object_or_404(Question, pk=question_id)
image = question.combine_images_side_by_side(*image_ids)
return render(request, "shorts/combine_result.html", {"image": image,
"remote_url": settings.REMOTE_URL,
})