fix a number of things

This commit is contained in:
Ross
2023-08-07 13:42:22 +01:00
parent b36fa370f1
commit c3e36bfb12
16 changed files with 302 additions and 34 deletions
View File
+4 -2
View File
@@ -583,8 +583,10 @@ class SelfReviewForm(ModelForm):
widgets = {"user_exam": HiddenInput(), "case": HiddenInput()}
def __init__(self, *args, **kwargs):
user_exam_id = kwargs.pop("user_exam_id")
case_id = kwargs.pop("case_id")
if "user_exam_id" in kwargs:
user_exam_id = kwargs.pop("user_exam_id")
if "case_id" in kwargs:
case_id = kwargs.pop("case_id")
ModelForm.__init__(self, *args, **kwargs)
super(SelfReviewForm, self).__init__(*args, **kwargs)
@@ -0,0 +1,22 @@
# Generated by Django 4.1.4 on 2023-08-07 08:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('atlas', '0007_selfreview'),
]
operations = [
migrations.RemoveField(
model_name='case',
name='scrapped',
),
migrations.AddField(
model_name='case',
name='archive',
field=models.BooleanField(default=False, help_text='Case has been archived, this will remain on the system but will not be shown'),
),
]
+120 -9
View File
@@ -13,6 +13,7 @@ from django.utils import timezone
import pydicom
import pydicom.multival
from django.core.files.storage import FileSystemStorage
from django.conf import settings
@@ -272,8 +273,9 @@ class Case(models.Model):
related_name="atlas_edited_cases",
)
scrapped = models.BooleanField(
default=False, help_text="Question has been scrapped and will not be shown"
archive = models.BooleanField(
default=False,
help_text="Case has been archived, this will remain on the system but will not be shown",
)
open_access = models.BooleanField(
@@ -325,6 +327,30 @@ class Case(models.Model):
examinations = [series.get_examination() for series in self.series.all()]
return f"{self.pk}:{self.title} {'/'.join([str(c) for c in self.condition.all()])} [{', '.join(examinations)}]"
def get_case_dicom_json(self):
series_json = []
for series in self.series.all():
series_json.append(series.get_series_dicom_json())
print(series_json)
return {
"studies": [
{
"StudyInstanceUID": "1.3.6.1.4.1.14519.5.2.1.6279.6001.298806137288633453246975630178",
"StudyDate": "20000101",
"StudyTime": "",
"PatientName": "",
"PatientID": "LIDC-IDRI-0001",
"AccessionNumber": "",
"PatientAge": "",
"PatientSex": "",
"series": series_json,
}
]
}
pass
class SeriesImage(SeriesImageBase):
image = models.FileField(upload_to=image_directory_path)
@@ -332,6 +358,50 @@ class SeriesImage(SeriesImageBase):
"Series", related_name="images", on_delete=models.SET_NULL, null=True
)
def get_image_dicom_json(self):
try:
with pydicom.dcmread(self.image) as ds:
to_keep = [
"Columns",
"Rows",
"InstanceNumber",
"SOPClassUID",
"PhotometricInterpretation",
"BitsAllocated",
"BitsStored",
"PixelRepresentation",
"SamplesPerPixel",
"PixelSpacing",
"HighBit",
"ImageOrientationPatient",
"ImagePositionPatient",
"FrameOfReferenceUID",
"ImageType",
"Modality",
"SOPInstanceUID",
"SeriesInstanceUID",
"StudyInstanceUID",
"WindowCenter",
"WindowWidth",
"SeriesDate",
]
d = {}
for key in to_keep:
val = ds[key].value
if type(val) == pydicom.multival.MultiValue:
d[key] = list(val)
else:
d[key] = val
return {"metadata": d, "url": self.image.url}
except FileNotFoundError:
return []
class SeriesFinding(models.Model):
series = models.ForeignKey(
@@ -411,6 +481,23 @@ class Series(SeriesBase):
"<a href='{}'>{}</a>", self.get_absolute_url(), self.get_full_str()
)
def get_series_dicom_json(self):
instances = []
image: SeriesImage
for image in self.images.all():
instances.append(image.get_image_dicom_json())
series_json = {
"SeriesInstanceUID": "1.3.6.1.4.1.14519.5.2.1.6279.6001.179049373636438705059720603192",
"SeriesNumber": 3000566,
"Modality": "CT",
"SliceThickness": 2.5,
"instances": instances,
}
return series_json
class CaseCollection(ExamCollectionGenericBase):
app_name = "atlas"
@@ -522,10 +609,14 @@ class CaseCollection(ExamCollectionGenericBase):
return False
def get_case_by_index(self, case_index:int, case_count=False) -> Case | Tuple[Case, int]:
def get_case_by_index(
self, case_index: int, case_count=False
) -> Case | Tuple[Case, int]:
# Seems to be a bug when case_number is 0 or 1 the same (first) object gets returned
# forces a list fixes (but is likely inefficient)
cases = list(self.cases.all().order_by("casedetail__sort_order").prefetch_related())
cases = list(
self.cases.all().order_by("casedetail__sort_order").prefetch_related()
)
try:
case = cases[case_index]
@@ -534,10 +625,20 @@ class CaseCollection(ExamCollectionGenericBase):
raise Http404(s)
if case_count:
return case, len(cases)
return case, len(cases)
else:
return case
def get_case_take_url(self, case):
cases = list(
self.cases.all().order_by("casedetail__sort_order").prefetch_related()
)
return reverse(
"atlas:collection_case_view_take_user",
kwargs={"pk": self.pk, "case_number": cases.index(case)},
)
class CaseDetail(models.Model):
case = models.ForeignKey(Case, on_delete=models.CASCADE)
@@ -617,9 +718,7 @@ class SelfReview(models.Model):
review_update_date = models.DateTimeField(auto_now=True)
def get_absolute_url(self):
return reverse(
"atlas:collection_take_overview_user", kwargs={"pk": self.user_exam.exam.pk}
)
return self.user_exam.exam.get_case_take_url(self.case)
def get_display_block(self):
html = """<div class='self-feedback-block'>
@@ -628,15 +727,27 @@ class SelfReview(models.Model):
Findings: <span class="findings">{findings}</span><br/>
Interpretation: <span class="interpretation">{interpretation}</span><br/>
<span class="date">Date: {date}</span><br/>
<span class="date">Date: {date}</span><br/><a href="{edit_url}">Edit</a>
</div>
"""
edit_url = reverse(
"atlas:self_review_update",
kwargs={
"user_exam_id": self.user_exam.pk,
"case_id": self.case.pk,
"pk": self.pk,
},
)
# edit_url = "TEST"
return format_html(
html,
comments=self.comments,
findings=self.findings,
interpretation=self.interpretation,
date=self.review_date,
id=self.pk,
edit_url=edit_url,
)
@@ -1,6 +1,7 @@
{% extends 'atlas/base.html' %}
{% block content %}
<span class="collection-name-blend">Collection: {{collection}}</span>
<h2>Case {{case_number|add:1}}
{% if show_title %}
@@ -75,6 +76,10 @@
</p>
{% endif %}
{% if collection.publish_results or cid_user_exam.completed %}
<a href="{% url 'atlas:add_self_review' cid_user_exam.id case.id %}"><button>Add self review</button></a>
{% endif %}
<form method="POST" class="post-form">{% csrf_token %}
{% if collection.collection_type == "REP" %}
<div class="form-contents">
@@ -84,21 +89,22 @@
</div>
<p>
{% if completed %}
<h4>Answer score: {{answer.score}}</h4>
Answer feedback: {{answer.feedback|safe}}
<br/>
{% if self_review %}
<h4>Self Feedback</h4>
{% if collection.self_review %}
{% if self_review %}
<h4>Self Feedback</h4>
{% for review in self_review %}
{{review.get_display_block}}
{% for review in self_review %}
{{review.get_display_block}}
{% endfor %}
{% endfor %}
{% endif %}
{% else %}
<h4>Answer score: {{answer.score}}</h4>
Answer feedback: {{answer.feedback|safe}}
<br/>
{% endif %}
{% endif %}
</p>
@@ -117,10 +123,6 @@
<br />
<button type="submit" name="finish" class="save btn btn-default">Overview</button>
<button type="submit" id="goto-button" value="test" name="goto" class="hide">goto</button>
{% if collection.publish_results or cid_user_exam.completed %}
<a href="{% url 'atlas:add_self_review' cid_user_exam.id case.id %}">Add self review</a>
{% endif %}
</form>
<style>
label {
@@ -15,8 +15,8 @@
{{answer_count}} out of {{collection_length}} cases answered. Click to go to case.
<div class="sba-finish-list">
<ul>
{% for question, answer in question_answer_tuples %}
<li> <span {% if not collection.review_only and not answer %}class="unanswered"{% endif %}>
{% for question, answer, self_review in question_answer_tuples %}
<li> <span class="{% if not collection.review_only and not answer %}unanswered{% endif %}">
<a href="
{% if cid is not None %}
{% url 'atlas:collection_case_view_take' pk=collection.id case_number=forloop.counter0 cid=cid passcode=passcode %}
@@ -24,7 +24,7 @@
{% url 'atlas:collection_case_view_take_user' pk=collection.id case_number=forloop.counter0 %}
{% endif %}
">
Case: {{forloop.counter}}
Case: {{forloop.counter}}{% if self_review %} <span class="self-review-complete" title="You have reviewed this case."> <i class="bi-card-checklist"></i></span>{% endif %}
</a>
<br/>
{% if not collection.review_only %}
@@ -22,6 +22,9 @@
<button>Start</button>
{% endif %}
</a>
{% if cid_exam.completed %}
<a href="{% url 'atlas:collection_take_overview_user' pk=collection.pk %}"><button>Overview</button></a>
{% endif %}
{% else %}
Enter your CID and passcode in the below boxes.<br />
<p><input id="cid-box" type="text" value="Candidate ID"></p>
+4
View File
@@ -1,4 +1,8 @@
{% extends 'atlas/base.html' %}
{% block content %}
<h2>Atlas</h2>
<a href="{% url 'atlas:user_collections' %}">Available collections</a>
{% endblock %}
@@ -33,4 +33,5 @@
</div> {% endcomment %}
<input type="submit" value="Submit">
</form>
{% if review %}<br/><a href="{% url 'atlas:self_review_delete' user_exam.id case.id review.id %}"><button>Delete review</button></a>{% endif %}
{% endblock %}
@@ -0,0 +1,16 @@
{% extends 'atlas/base.html' %}
{% block content %}
<h2>Collections</h2>
<ul>
{% for collection in collections %}
<li>
<a href="{% url 'atlas:collection_take_start' collection.pk %}">{{collection}} {{collelction.is_complete}}</a>
</li>
{% endfor %}
</ul>
{% endblock %}
+19 -2
View File
@@ -8,21 +8,32 @@ from . import views
app_name = "atlas"
urlpatterns = [
path("", views.CaseView.as_view(), name="index"),
path("", views.index, name="index"),
path(
"create/",
RedirectView.as_view(pattern_name="atlas:case_create", permanent=False),
name="create",
),
path(
"add_self_review/<int:user_exam_id>/<int:case_id>",
"self_review/<int:user_exam_id>/<int:case_id>/add",
views.AddSelfReview.as_view(),
name="add_self_review",
),
path(
"self_review/<int:user_exam_id>/<int:case_id>/update/<int:pk>",
views.SelfReviewUpdate.as_view(),
name="self_review_update",
),
path(
"self_review/<int:user_exam_id>/<int:case_id>/delete/<int:pk>",
views.SelfReviewDelete.as_view(),
name="self_review_delete",
),
path("author/<int:pk>/", views.author_detail, name="author_detail"),
path("author/", views.author_list, name="author_list"),
path("case/", views.CaseView.as_view(), name="case_view"),
path("collection/", views.CollectionView.as_view(), name="collection_view"),
path("collection/user", views.user_collections, name="user_collections"),
path(
"collection/create",
views.CaseCollectionCreate.as_view(),
@@ -125,6 +136,12 @@ urlpatterns = [
views.GenericExamViews.exam_json_edit,
name="exam_json_edit",
),
path(
"case/<int:pk>/dicom_json",
views.case_dicom_json,
name="case_dicom_json",
),
path("condition/", views.ConditionView.as_view(), name="condition_view"),
path("categories/", views.categories_list, name="categories_list"),
path("condition/<int:pk>", views.condition_detail, name="condition_detail"),
+85 -4
View File
@@ -290,6 +290,13 @@ def author_list(request):
return render(request, "atlas/author_list.html", {"authors": authors})
def index(request):
return render(request, "atlas/index.html", {})
def user_collections(request):
collections = request.user.user_casecollection_exams.all()
return render(request, "atlas/user_collections.html", {"collections": collections})
class CaseDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView):
model = Case
@@ -1325,12 +1332,13 @@ def collection_take_overview(
question_answer_tuples = []
answer_count = 0
for q in case_details:
if q in answer_question_map and answer_question_map[q].answer:
question_answer_tuples.append((q, answer_question_map[q]))
for cd in case_details:
self_review = cid_user_exam.selfreview_set.filter(case=cd.case)
if cd in answer_question_map and answer_question_map[cd].answer:
question_answer_tuples.append((cd, answer_question_map[cd], self_review))
answer_count += 1
else:
question_answer_tuples.append((q, None))
question_answer_tuples.append((cd, None, self_review))
return render(
request,
@@ -1347,6 +1355,7 @@ def collection_take_overview(
)
def collection_case_view_take_user(request, pk: int, case_number: int):
return collection_case_view_take(request, pk, case_number)
@@ -1543,6 +1552,10 @@ def collection_case_view(request, pk, case_number):
},
)
def case_dicom_json(request, pk):
case = get_object_or_404(Case, pk=pk)
return JsonResponse(case.get_case_dicom_json())
@user_is_collection_author_or_atlas_editor
def delete_collection_cid_answers(request, exam_id, cid):
@@ -1761,6 +1774,70 @@ class CollectionCloneMixin:
class CollectionClone(CollectionCloneMixin, CaseCollectionCreate):
"""Clone exam view"""
class SelfReviewDelete(DeleteView):
model = SelfReview
template_name = "confirm_delete.html"
#success_url = reverse_lazy("atlas:finding_view")
def get_initial(self):
user_exam_id = self.kwargs["user_exam_id"]
case_id = self.kwargs["case_id"]
review_id = self.kwargs["pk"]
self.user_exam = get_object_or_404(CidUserExam, pk=user_exam_id)
self.case = get_object_or_404(Case, pk=case_id)
self.review = get_object_or_404(SelfReview, pk=review_id)
return {"user_exam": user_exam_id, "case": case_id, "review": review_id}
def get_context_data(self, **kwargs):
# This can also be accessed via view.**** in the template
context = super(SelfReviewDelete, self).get_context_data(**kwargs)
context["user_exam"] = self.user_exam
context["case"] = self.case
context["review"] = self.review
return context
def get_success_url(self) -> str:
return self.user_exam.exam.get_case_take_url(self.case)
class SelfReviewUpdate(
UpdateView
):
model = SelfReview
form_class = SelfReviewForm
def get_initial(self):
user_exam_id = self.kwargs["user_exam_id"]
case_id = self.kwargs["case_id"]
review_id = self.kwargs["pk"]
self.user_exam = get_object_or_404(CidUserExam, pk=user_exam_id)
self.case = get_object_or_404(Case, pk=case_id)
self.review = get_object_or_404(SelfReview, pk=review_id)
return {"user_exam": user_exam_id, "case": case_id, "review": review_id}
def get_context_data(self, **kwargs):
# This can also be accessed via view.**** in the template
context = super(SelfReviewUpdate, self).get_context_data(**kwargs)
context["user_exam"] = self.user_exam
context["case"] = self.case
context["review"] = self.review
return context
def get_success_url(self) -> str:
return self.user_exam.exam.get_case_take_url(self.case)
#def get_object(self):
# return self.review
# return
#def get_initial(self):
# user_exam_id = self.kwargs["user_exam_id"]
# case_id = self.kwargs["case_id"]
# self.user_exam = get_object_or_404(CidUserExam, pk=user_exam_id)
# self.case = get_object_or_404(Case, pk=case_id)
# return {"user_exam": user_exam_id, "case": case_id}
class AddSelfReview(CreateView):
model = SelfReview
form_class = SelfReviewForm
@@ -1789,6 +1866,10 @@ class AddSelfReview(CreateView):
context["case"] = self.case
return context
def get_success_url(self) -> str:
return self.user_exam.exam.get_case_take_url(self.case)
#return reverse("atlas:collection_take_overview_user", kwargs={"pk": self.user_exam.collection.id })
# def form_valid(self, form):
# model = form.save(commit=False)
#
+1
View File
@@ -8,6 +8,7 @@ services:
volumes:
- ./rad/:/usr/src/rad/
- ../rts/:/usr/src/rad/rts
- ../Viewers/platform/app/dist/:/usr/src/rad/viewer
- ./backups:/usr/src/backups
- ./media:/usr/src/media
- ./static:/usr/src/static
+5
View File
@@ -1013,4 +1013,9 @@ h1, h2, h3, h4, h5, h6 {
.self-feedback-block.data {
opacity: 60%;
font-size: small;
}
.collection-name-blend {
opacity: 70%;
font-size: small;
}
+1
View File
@@ -164,6 +164,7 @@ if settings.DEBUG:
#urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += static("/rts/", document_root="rts")
urlpatterns += static("/viewer/", document_root="viewer")
import debug_toolbar
urlpatterns = [
+2
View File
@@ -17,6 +17,8 @@
<link href="{% static 'css/widgets.css' %}" type="text/css" media="all" rel="stylesheet">
<link rel="stylesheet" href="{% static 'css/dicomViewer.css' %}">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.css" integrity="sha512-6S2HWzVFxruDlZxI3sXOZZ4/eJ8AcxkQH1+JjSe/ONCEqR9L4Ysq5JdT5ipqtzU7WHalNwzwBv+iE51gNHJNqQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
<link href="/static/css/widgets.css" type="text/css" media="all" rel="stylesheet">
<link href="/static/admin/css/vendor/select2/select2.min.css" type="text/css" media="screen" rel="stylesheet">