Too many changes :(
This commit is contained in:
+2
-1
@@ -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
@@ -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)
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
Reference in New Issue
Block a user