Compare commits
3
Commits
af3234bacc
...
5a9868a56d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a9868a56d | ||
|
|
edc9b29233 | ||
|
|
5f23134fa6 |
+53
-19
@@ -29,7 +29,6 @@ from loguru import logger
|
|||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class SeriesSchema(ModelSchema):
|
class SeriesSchema(ModelSchema):
|
||||||
case_id: List[int] = []
|
case_id: List[int] = []
|
||||||
|
|
||||||
@@ -59,6 +58,7 @@ def upload_dicom(request, files: List[UploadedFile] = File(...)):
|
|||||||
uploaded = []
|
uploaded = []
|
||||||
duplicate = []
|
duplicate = []
|
||||||
failed = []
|
failed = []
|
||||||
|
duplicate_series = set()
|
||||||
for file in files:
|
for file in files:
|
||||||
# data = file.read()
|
# data = file.read()
|
||||||
try:
|
try:
|
||||||
@@ -67,26 +67,41 @@ def upload_dicom(request, files: List[UploadedFile] = File(...)):
|
|||||||
ud.save()
|
ud.save()
|
||||||
|
|
||||||
uploaded.append((file.name, ud.image_blake3_hash))
|
uploaded.append((file.name, ud.image_blake3_hash))
|
||||||
except DuplicateDicom:
|
except DuplicateDicom as e:
|
||||||
duplicate.append((file.name, ud.image_blake3_hash))
|
duplicate.append((file.name, ud.image_blake3_hash))
|
||||||
|
|
||||||
|
duplicate_series.add(e.duplicate.get_series().get_absolute_url())
|
||||||
|
print(duplicate_series)
|
||||||
pass
|
pass
|
||||||
except InvalidDicomError:
|
except InvalidDicomError:
|
||||||
failed.append(file.name)
|
failed.append(file.name)
|
||||||
|
|
||||||
return {"uploaded": uploaded, "duplicates": duplicate, "failed": failed}
|
return {
|
||||||
|
"uploaded": uploaded,
|
||||||
|
"duplicates": duplicate,
|
||||||
|
"failed": failed,
|
||||||
|
"duplicate_series": list(duplicate_series),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/generate_image_hash", auth=django_auth)
|
@router.post("/generate_image_hash", auth=django_auth)
|
||||||
def generate_image_hash(request, id: int):
|
def generate_image_hash(request, id: int):
|
||||||
s = SeriesImage.objects.get(pk=id)
|
s = SeriesImage.objects.get(pk=id)
|
||||||
s.generate_hashes()
|
s.generate_hashes()
|
||||||
|
|
||||||
return {"md5": s.image_md5_hash, "blake3": s.image_blake3_hash, "is_dicom": s.is_dicom}
|
return {
|
||||||
|
"md5": s.image_md5_hash,
|
||||||
|
"blake3": s.image_blake3_hash,
|
||||||
|
"is_dicom": s.is_dicom,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/clear_dicoms", auth=django_auth)
|
@router.post("/clear_dicoms", auth=django_auth)
|
||||||
def clear_dicoms(request):
|
def clear_dicoms(request):
|
||||||
|
|
||||||
if "selection" in request.POST:
|
if "selection" in request.POST:
|
||||||
dicoms = UncategorisedDicom.objects.filter(series_instance_uid__in=request.POST.getlist("selection"))
|
dicoms = UncategorisedDicom.objects.filter(
|
||||||
|
series_instance_uid__in=request.POST.getlist("selection")
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
dicoms = UncategorisedDicom.objects.filter(user=request.user)
|
dicoms = UncategorisedDicom.objects.filter(user=request.user)
|
||||||
@@ -110,15 +125,17 @@ def uncategorised_dicoms(request):
|
|||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def import_dicoms_helper(request, case_id: int | None = None):
|
def import_dicoms_helper(request, case_id: int | None = None):
|
||||||
# dicoms = UncategorisedDicom.objects.filter(user=request.user)
|
# dicoms = UncategorisedDicom.objects.filter(user=request.user)
|
||||||
if "selection" in request.POST:
|
if "selection" in request.POST:
|
||||||
dicoms = UncategorisedDicom.objects.filter(series_instance_uid__in=request.POST.getlist("selection"))
|
dicoms = UncategorisedDicom.objects.filter(
|
||||||
|
series_instance_uid__in=request.POST.getlist("selection")
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
dicoms = UncategorisedDicom.objects.filter(user=request.user)
|
dicoms = UncategorisedDicom.objects.filter(user=request.user)
|
||||||
|
|
||||||
|
|
||||||
if "order-series" in request.POST:
|
if "order-series" in request.POST:
|
||||||
order = request.POST["order-series"]
|
order = request.POST["order-series"]
|
||||||
else:
|
else:
|
||||||
@@ -136,7 +153,9 @@ def import_dicoms_helper(request, case_id: int | None = None):
|
|||||||
tags = data[series_uid][0][1]
|
tags = data[series_uid][0][1]
|
||||||
|
|
||||||
# Check if series with the id already exists (in which case we just add to htat)
|
# Check if series with the id already exists (in which case we just add to htat)
|
||||||
if Series.objects.filter(series_instance_uid=tags["SeriesInstanceUID"]).exists():
|
if Series.objects.filter(
|
||||||
|
series_instance_uid=tags["SeriesInstanceUID"]
|
||||||
|
).exists():
|
||||||
series = Series.objects.get(series_instance_uid=tags["SeriesInstanceUID"])
|
series = Series.objects.get(series_instance_uid=tags["SeriesInstanceUID"])
|
||||||
else:
|
else:
|
||||||
series = Series(
|
series = Series(
|
||||||
@@ -146,16 +165,16 @@ def import_dicoms_helper(request, case_id: int | None = None):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if tags["StudyDescription"]:
|
if tags["StudyDescription"]:
|
||||||
examination, created = Examination.objects.get_or_create(examination=tags["StudyDescription"])
|
examination, created = Examination.objects.get_or_create(
|
||||||
|
examination=tags["StudyDescription"]
|
||||||
|
)
|
||||||
if created:
|
if created:
|
||||||
examination.modality = modality
|
examination.modality = modality
|
||||||
examination.save()
|
examination.save()
|
||||||
series.examination = examination
|
series.examination = examination
|
||||||
|
|
||||||
|
|
||||||
series.save()
|
series.save()
|
||||||
|
|
||||||
|
|
||||||
match order:
|
match order:
|
||||||
case "order-series-instance-number":
|
case "order-series-instance-number":
|
||||||
series.order_by_dicom("InstanceNumber")
|
series.order_by_dicom("InstanceNumber")
|
||||||
@@ -188,12 +207,18 @@ def import_dicoms_helper(request, case_id: int | None = None):
|
|||||||
return series_list
|
return series_list
|
||||||
|
|
||||||
|
|
||||||
@router.post("/import_dicoms", auth=django_auth, response=List[Tuple[ SeriesSchema, str ]])
|
@router.post(
|
||||||
|
"/import_dicoms", auth=django_auth, response=List[Tuple[SeriesSchema, str]]
|
||||||
|
)
|
||||||
def import_dicoms(request):
|
def import_dicoms(request):
|
||||||
return import_dicoms_helper(request)
|
return import_dicoms_helper(request)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/import_dicoms/{case_id}", auth=django_auth, response=List[Tuple[ SeriesSchema, str ]])
|
@router.post(
|
||||||
|
"/import_dicoms/{case_id}",
|
||||||
|
auth=django_auth,
|
||||||
|
response=List[Tuple[SeriesSchema, str]],
|
||||||
|
)
|
||||||
def import_dicoms_case(request, case_id: int):
|
def import_dicoms_case(request, case_id: int):
|
||||||
return import_dicoms_helper(request, case_id=case_id)
|
return import_dicoms_helper(request, case_id=case_id)
|
||||||
|
|
||||||
@@ -220,6 +245,7 @@ def series_remove_duplicate_images(request, series_id: int):
|
|||||||
|
|
||||||
return len(dupes)
|
return len(dupes)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/series_truncate/{series_id}/{start}/{end}/", auth=django_auth)
|
@router.get("/series_truncate/{series_id}/{start}/{end}/", auth=django_auth)
|
||||||
def series_truncate(request, series_id: int, start: int, end: int):
|
def series_truncate(request, series_id: int, start: int, end: int):
|
||||||
print(start, end)
|
print(start, end)
|
||||||
@@ -261,6 +287,7 @@ def check_image_hash(request, hash: str):
|
|||||||
data = {"status": "success", "id": False}
|
data = {"status": "success", "id": False}
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
@router.post("/check_image_hashes/", auth=django_auth)
|
@router.post("/check_image_hashes/", auth=django_auth)
|
||||||
def check_images_hashes(request, hashes: List[str]):
|
def check_images_hashes(request, hashes: List[str]):
|
||||||
"""Checks a list of image hashes and returns the series id / url if found
|
"""Checks a list of image hashes and returns the series id / url if found
|
||||||
@@ -278,12 +305,18 @@ def check_images_hashes(request, hashes: List[str]):
|
|||||||
data = {
|
data = {
|
||||||
"id": series_image.pk,
|
"id": series_image.pk,
|
||||||
"url": series_image.series.get_absolute_url(),
|
"url": series_image.series.get_absolute_url(),
|
||||||
"type": "series"
|
"type": "series",
|
||||||
}
|
}
|
||||||
except SeriesImage.DoesNotExist:
|
except SeriesImage.DoesNotExist:
|
||||||
try:
|
try:
|
||||||
uncategorised_dicom = UncategorisedDicom.objects.get(image_blake3_hash=hash)
|
uncategorised_dicom = UncategorisedDicom.objects.get(
|
||||||
data = { "id":uncategorised_dicom.pk, "url": reverse("atlas:user_uploads"), "type": "uncategorised"}
|
image_blake3_hash=hash
|
||||||
|
)
|
||||||
|
data = {
|
||||||
|
"id": uncategorised_dicom.pk,
|
||||||
|
"url": reverse("atlas:user_uploads"),
|
||||||
|
"type": "uncategorised",
|
||||||
|
}
|
||||||
except UncategorisedDicom.DoesNotExist:
|
except UncategorisedDicom.DoesNotExist:
|
||||||
data = {"id": False, "url": False}
|
data = {"id": False, "url": False}
|
||||||
|
|
||||||
@@ -291,6 +324,7 @@ def check_images_hashes(request, hashes: List[str]):
|
|||||||
|
|
||||||
return hash_status
|
return hash_status
|
||||||
|
|
||||||
|
|
||||||
# @router.get("/generate_image_hash/{id}", auth=django_auth)
|
# @router.get("/generate_image_hash/{id}", auth=django_auth)
|
||||||
# def generate_image_hash(request, id: int):
|
# def generate_image_hash(request, id: int):
|
||||||
# series_image = SeriesImage.objects.get(pk=id)
|
# series_image = SeriesImage.objects.get(pk=id)
|
||||||
@@ -301,12 +335,13 @@ def check_images_hashes(request, hashes: List[str]):
|
|||||||
#
|
#
|
||||||
# print(series_image)
|
# print(series_image)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/view_dicom_tags/{hash}", auth=django_auth)
|
@router.get("/view_dicom_tags/{hash}", auth=django_auth)
|
||||||
def view_dicom_tags(request, hash: str):
|
def view_dicom_tags(request, hash: str):
|
||||||
|
|
||||||
item = SeriesImage.objects.get(image_blake3_hash=hash)
|
item = SeriesImage.objects.get(image_blake3_hash=hash)
|
||||||
return item.get_dicom_json()
|
return item.get_dicom_json()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/series_split_by_dicom_tag/{series_id}/{dicom_tag}", auth=django_auth)
|
@router.get("/series_split_by_dicom_tag/{series_id}/{dicom_tag}", auth=django_auth)
|
||||||
def series_split_by_tag(request, series_id: int, dicom_tag: str):
|
def series_split_by_tag(request, series_id: int, dicom_tag: str):
|
||||||
series = get_object_or_404(Series, pk=series_id)
|
series = get_object_or_404(Series, pk=series_id)
|
||||||
@@ -353,7 +388,6 @@ def series_split_by_tag(request, series_id: int, dicom_tag:str):
|
|||||||
|
|
||||||
new_series.append(series.pk)
|
new_series.append(series.pk)
|
||||||
|
|
||||||
|
|
||||||
return new_series
|
return new_series
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -143,7 +143,7 @@ class StructureForm(ModelForm):
|
|||||||
class SeriesFindingForm(ModelForm):
|
class SeriesFindingForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SeriesFinding
|
model = SeriesFinding
|
||||||
exclude = ["series", "annotation_json", "viewport_json"]
|
exclude = ["series", "annotation_json", "viewport_json", "current_image_id_index"]
|
||||||
|
|
||||||
widgets = {
|
widgets = {
|
||||||
"findings": autocomplete.ModelSelect2Multiple(
|
"findings": autocomplete.ModelSelect2Multiple(
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.0.2 on 2024-09-23 12:45
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('atlas', '0059_alter_casecollection_markers'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='seriesfinding',
|
||||||
|
name='current_image_id_index',
|
||||||
|
field=models.IntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
+30
-3
@@ -555,6 +555,14 @@ class SeriesImage(SeriesImageBase):
|
|||||||
help_text="Reference to the object that has replaced this one.",
|
help_text="Reference to the object that has replaced this one.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_file_size(self):
|
||||||
|
try:
|
||||||
|
return self.image.size
|
||||||
|
|
||||||
|
# We catch this for when the image does not exist
|
||||||
|
except FileNotFoundError:
|
||||||
|
return 0
|
||||||
|
|
||||||
def get_dicom_data(self):
|
def get_dicom_data(self):
|
||||||
try:
|
try:
|
||||||
with pydicom.dcmread(self.image) as d:
|
with pydicom.dcmread(self.image) as d:
|
||||||
@@ -562,6 +570,9 @@ class SeriesImage(SeriesImageBase):
|
|||||||
except InvalidDicomError:
|
except InvalidDicomError:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
def get_series(self):
|
||||||
|
return self.series
|
||||||
|
|
||||||
# def get_image_dicom_json(self, image_index):
|
# def get_image_dicom_json(self, image_index):
|
||||||
# try:
|
# try:
|
||||||
# with pydicom.dcmread(self.image) as ds:
|
# with pydicom.dcmread(self.image) as ds:
|
||||||
@@ -586,6 +597,7 @@ 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)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
findings = self.findings.all().values_list("name")
|
findings = self.findings.all().values_list("name")
|
||||||
@@ -1066,6 +1078,13 @@ class CaseDetail(models.Model):
|
|||||||
"""Returns the correct question answers as a json string"""
|
"""Returns the correct question answers as a json string"""
|
||||||
return json.dumps(self.question_answers)
|
return json.dumps(self.question_answers)
|
||||||
|
|
||||||
|
def get_user_answers(self, user):
|
||||||
|
"""Returns the users answers as a json string"""
|
||||||
|
try:
|
||||||
|
return UserReportAnswer.objects.get(question=self, user=user)
|
||||||
|
except UserReportAnswer.DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
class CasePrior(models.Model):
|
class CasePrior(models.Model):
|
||||||
case_detail = models.ForeignKey(CaseDetail, on_delete=models.CASCADE)
|
case_detail = models.ForeignKey(CaseDetail, on_delete=models.CASCADE)
|
||||||
prior_case = models.ForeignKey(Case, on_delete=models.CASCADE, related_name="prior_case")
|
prior_case = models.ForeignKey(Case, on_delete=models.CASCADE, related_name="prior_case")
|
||||||
@@ -1258,9 +1277,12 @@ class UncategorisedDicom(models.Model):
|
|||||||
|
|
||||||
basic_dicom_tags = models.JSONField(null=True, blank=True)
|
basic_dicom_tags = models.JSONField(null=True, blank=True)
|
||||||
|
|
||||||
def check_for_duplicates(self, image_hash):
|
def check_for_duplicates(self, image_hash=None|str):
|
||||||
duplicate = None
|
duplicate = None
|
||||||
|
|
||||||
|
if image_hash is None:
|
||||||
|
image_hash = self.image_blake3_hash
|
||||||
|
|
||||||
if obj := UncategorisedDicom.objects.filter(
|
if obj := UncategorisedDicom.objects.filter(
|
||||||
image_blake3_hash=image_hash
|
image_blake3_hash=image_hash
|
||||||
).first():
|
).first():
|
||||||
@@ -1295,7 +1317,8 @@ class UncategorisedDicom(models.Model):
|
|||||||
|
|
||||||
if duplicate is not None:
|
if duplicate is not None:
|
||||||
if duplicate != self:
|
if duplicate != self:
|
||||||
raise DuplicateDicom
|
raise DuplicateDicom(duplicate)
|
||||||
|
|
||||||
|
|
||||||
# Hack for tests
|
# Hack for tests
|
||||||
if image_blake3_hash != "1234":
|
if image_blake3_hash != "1234":
|
||||||
@@ -1334,9 +1357,13 @@ class UncategorisedDicom(models.Model):
|
|||||||
except pydicom.errors.InvalidDicomError:
|
except pydicom.errors.InvalidDicomError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_series(self):
|
||||||
|
return self.series
|
||||||
|
|
||||||
|
|
||||||
class DuplicateDicom(Exception):
|
class DuplicateDicom(Exception):
|
||||||
pass
|
def __init__(self, duplicate):
|
||||||
|
self.duplicate = duplicate
|
||||||
|
|
||||||
|
|
||||||
class Resource(models.Model, AuthorMixin):
|
class Resource(models.Model, AuthorMixin):
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{% if request.user.is_authenticated %}
|
{% if request.user.is_authenticated %}
|
||||||
<br/>Collection: {{collection.name}}-> <a href="{% url 'atlas:collection_detail' pk=collection.pk %}">Overview</a> /
|
<br/>Collection: {{collection.name}}-> <a href="{% url 'atlas:collection_detail' pk=collection.pk %}">Overview</a> /
|
||||||
|
<a href="{% url 'atlas:collection_history' collection.pk %}">History</a> /
|
||||||
<a href="{% url 'atlas:collection_mark_overview' collection.pk %}">Mark</a> /
|
<a href="{% url 'atlas:collection_mark_overview' collection.pk %}">Mark</a> /
|
||||||
<a href="{% url 'atlas:collection_scores_cid' collection.pk %}">Scores</a> /
|
<a href="{% url 'atlas:collection_scores_cid' collection.pk %}">Scores</a> /
|
||||||
<a href="{% url 'atlas:exam_cids' collection.pk %}">Candidates</a> /
|
<a href="{% url 'atlas:exam_cids' collection.pk %}">Candidates</a> /
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{% extends 'atlas/exams.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>{{collection.name}}</h2>
|
||||||
|
|
||||||
|
<h3>Users</h3>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{% for userexam in userexams %}
|
||||||
|
<li>
|
||||||
|
<b><a href="{% url 'atlas:collection_history_user' collection.pk userexam.user_user.pk %}">{{userexam.get_user_name}}</a><b><br/>
|
||||||
|
Completed: {{userexam.completed}}<br/>
|
||||||
|
Started: {{userexam.start_time}}, Ended: {{userexam.end_time}}<br/>
|
||||||
|
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
{% extends 'atlas/exams.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>Collection: {{collection.name}}</h2>
|
||||||
|
|
||||||
|
<h3>User: {{user}}</h3>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{% for casedetail, user_answer in user_answers %}
|
||||||
|
<li class="case">
|
||||||
|
|
||||||
|
<h4>{{forloop.counter}} / Case: {{casedetail.case.title}}</h4>
|
||||||
|
{% if not user_answer %}
|
||||||
|
<span class="case-not-answered">Case not answered.</span>
|
||||||
|
{% else %}
|
||||||
|
<div class="answer-block">
|
||||||
|
{% for value, user_answer, correct_answer, answer_is_correct, automark in user_answer.get_correct_json_answers %}
|
||||||
|
{% if not user_answer %}
|
||||||
|
Not answered
|
||||||
|
{% else %}
|
||||||
|
<div class="{% if answer_is_correct %}
|
||||||
|
correct
|
||||||
|
{% else %}
|
||||||
|
incorrect
|
||||||
|
{% endif %}
|
||||||
|
{% if automark %}
|
||||||
|
automark
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
">
|
||||||
|
<h5>{{value.title}}</h5>
|
||||||
|
{{value.description}}
|
||||||
|
<div
|
||||||
|
>
|
||||||
|
Answer : {{user_answer}}
|
||||||
|
|
||||||
|
{% if not answer_is_correct %}
|
||||||
|
<br/>Correct answer: {{correct_answer}}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block css %}
|
||||||
|
{{ block.super }}
|
||||||
|
<style>
|
||||||
|
.correct {
|
||||||
|
}
|
||||||
|
|
||||||
|
.correct h5::after {
|
||||||
|
content: '✓';
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
.incorrect h5::after {
|
||||||
|
content: '✗';
|
||||||
|
color: orange;
|
||||||
|
}
|
||||||
|
|
||||||
|
.incorrect.automark h5::after {
|
||||||
|
content: '✗';
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-block>div {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.case:has(.case-not-answered) {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
@@ -41,6 +41,7 @@
|
|||||||
<ul id="uploaded-files"></ul>
|
<ul id="uploaded-files"></ul>
|
||||||
<h5>Duplicates</h5>
|
<h5>Duplicates</h5>
|
||||||
<ul id="duplicate-files"></ul>
|
<ul id="duplicate-files"></ul>
|
||||||
|
<ul id="duplicate-series"></ul>
|
||||||
<h5>Failed</h5>
|
<h5>Failed</h5>
|
||||||
<ul id="failed-files"></ul>
|
<ul id="failed-files"></ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -78,6 +79,7 @@
|
|||||||
<script>
|
<script>
|
||||||
window.to_upload = [];
|
window.to_upload = [];
|
||||||
window.upload_count = 1;
|
window.upload_count = 1;
|
||||||
|
window.duplicate_series = new Set()
|
||||||
|
|
||||||
function* chunks(arr, n) {
|
function* chunks(arr, n) {
|
||||||
for (let i = 0; i < arr.length; i += n) {
|
for (let i = 0; i < arr.length; i += n) {
|
||||||
@@ -149,6 +151,7 @@
|
|||||||
item.textContent = JSON.parse(xhr.response)["duplicates"][i];
|
item.textContent = JSON.parse(xhr.response)["duplicates"][i];
|
||||||
$("#duplicate-files").append(item);
|
$("#duplicate-files").append(item);
|
||||||
}
|
}
|
||||||
|
window.duplicate_series.add(...JSON.parse(xhr.response)["duplicate_series"]);
|
||||||
|
|
||||||
for (let i = 0; i < JSON.parse(xhr.response)["failed"].length; i++) {
|
for (let i = 0; i < JSON.parse(xhr.response)["failed"].length; i++) {
|
||||||
let item = document.createElement("li");
|
let item = document.createElement("li");
|
||||||
@@ -162,6 +165,14 @@
|
|||||||
}
|
}
|
||||||
if (current_chunk + 1 == total_chunks) {
|
if (current_chunk + 1 == total_chunks) {
|
||||||
$("#loading").hide();
|
$("#loading").hide();
|
||||||
|
|
||||||
|
window.duplicate_series.forEach((element) => {
|
||||||
|
let item = document.createElement("a");
|
||||||
|
item.href = element;
|
||||||
|
item.textContent = element;
|
||||||
|
$("#duplicate-series").append(item, "<br/>");
|
||||||
|
});
|
||||||
|
//$("#duplicate-series").append([...window.duplicate_series].join("<br/>"));
|
||||||
alert("Uploading complete")
|
alert("Uploading complete")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
{% load crispy_forms_tags %}
|
||||||
|
|
||||||
<div>{{ series.modality}}, {{ series.examination }}, {{ series.plane }}, {{ series.contrast }}</div>
|
<div>{{ series.modality}}, {{ series.examination }}, {{ series.plane }}, {{ series.contrast }}</div>
|
||||||
<div>Description: {{series.description}}</div>
|
<div>Description: {{series.description}}</div>
|
||||||
|
|
||||||
@@ -22,13 +24,7 @@
|
|||||||
<div class="hide" id="hidden-form">
|
<div class="hide" id="hidden-form">
|
||||||
<form method="post" id="series_finding_form">
|
<form method="post" id="series_finding_form">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{series_finding_form.management_form}}
|
{{series_finding_form|crispy}}
|
||||||
<div>
|
|
||||||
<div>Description: {{ series_finding_form.description }}</div>
|
|
||||||
<div>Findings: {{ series_finding_form.findings }}</div>
|
|
||||||
<div>Structures: {{ series_finding_form.structures }}</div>
|
|
||||||
<div>Conditions: {{ series_finding_form.conditions }}</div>
|
|
||||||
</div>
|
|
||||||
<input type="submit" value="Submit">
|
<input type="submit" value="Submit">
|
||||||
<button id="cancel-add-finding-button">Cancel</button>
|
<button id="cancel-add-finding-button">Cancel</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -45,14 +41,28 @@
|
|||||||
{% for finding in series.findings.all %}
|
{% for finding in series.findings.all %}
|
||||||
<div class="finding-box">
|
<div class="finding-box">
|
||||||
<button class="view-finding-button" data-annotationjson={{finding.annotation_json}}
|
<button class="view-finding-button" data-annotationjson={{finding.annotation_json}}
|
||||||
data-viewportjson={{finding.viewport_json}} data-findingid={{finding.id}}>Click to view</button>
|
data-viewportjson={{finding.viewport_json}} data-findingid={{finding.id}} data-currentimageid={{finding.current_image_id_index}}>Click to view</button>
|
||||||
<span class="view-finding-details">
|
<span class="view-finding-details">
|
||||||
Finding(s): {% for f in finding.findings.all %}{{f.get_link}}{% endfor %}<br />
|
Finding(s): {% for f in finding.findings.all %}{{f.get_link}}{% endfor %}<br />
|
||||||
Structure(s): {% for s in finding.structures.all %}{{s.get_link}}{% endfor %}<br />
|
Structure(s): {% for s in finding.structures.all %}{{s.get_link}}{% endfor %}<br />
|
||||||
Condition(s): {% for s in finding.conditions.all %}{{s.get_link}}{% endfor %}<br />
|
Condition(s): {% for s in finding.conditions.all %}{{s.get_link}}{% endfor %}<br />
|
||||||
Description: {{finding.description}}<br />
|
Description: {{finding.description}}<br />
|
||||||
|
|
||||||
|
{% if request.user.is_superuser %}
|
||||||
|
<span _="on click toggle .hidden on next .extra-details">+</span>
|
||||||
|
<div class="hidden extra-details">
|
||||||
|
<h4>Annotation JSON</h4>
|
||||||
|
<pre>{{finding.annotation_json}}</pre>
|
||||||
|
<h4>Viewport JSON</h4>
|
||||||
|
<pre>{{finding.viewport_json}}</pre>
|
||||||
|
<h4>Image ID</h4>
|
||||||
|
<pre>{{finding.current_image_id_index}}</pre>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</span>
|
</span>
|
||||||
<a href="{% url 'atlas:series_edit_finding' pk=series.pk finding_pk=finding.pk %}" class="edit-finding-link">Edit</a>
|
<a href="{% url 'atlas:series_edit_finding' pk=series.pk finding_pk=finding.pk %}" class="edit-finding-link">Edit</a>
|
||||||
|
|
||||||
|
|
||||||
<a href="{% url 'atlas:delete_finding' pk=finding.pk %}" class="delete-finding-link">Delete</a>
|
<a href="{% url 'atlas:delete_finding' pk=finding.pk %}" class="delete-finding-link">Delete</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -102,7 +112,7 @@
|
|||||||
|
|
||||||
|
|
||||||
{% if image.image %}
|
{% if image.image %}
|
||||||
[{{image.image.size|filesizeformat}}]
|
[{{image.get_file_size|filesizeformat}}]
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{{image.image_md5_hash}} ({{image.is_dicom}}) {{image.image_blake3_hash}}
|
{{image.image_md5_hash}} ({{image.is_dicom}}) {{image.image_blake3_hash}}
|
||||||
@@ -195,6 +205,7 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
$("#add-finding-button").click(() => {
|
$("#add-finding-button").click(() => {
|
||||||
|
$("#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);
|
||||||
@@ -202,7 +213,6 @@
|
|||||||
//$("#finding-form").empty().append(
|
//$("#finding-form").empty().append(
|
||||||
// $("#hidden-form form").clone()
|
// $("#hidden-form form").clone()
|
||||||
//);
|
//);
|
||||||
$("#hidden-form").show()
|
|
||||||
|
|
||||||
});
|
});
|
||||||
$("#cancel-add-finding-button").click((e) => {
|
$("#cancel-add-finding-button").click((e) => {
|
||||||
@@ -225,7 +235,9 @@
|
|||||||
dicom_element = $(".cornerstone-element").get(0);
|
dicom_element = $(".cornerstone-element").get(0);
|
||||||
annotationjson = JSON.parse(e.currentTarget.dataset.annotationjson);
|
annotationjson = JSON.parse(e.currentTarget.dataset.annotationjson);
|
||||||
viewport = JSON.parse(e.currentTarget.dataset.viewportjson);
|
viewport = JSON.parse(e.currentTarget.dataset.viewportjson);
|
||||||
loadAnnotationAndViewportOnElement(annotationjson, viewport, dicom_element);
|
current_image_id_index = e.currentTarget.dataset.currentimageid;
|
||||||
|
console.log(e.currentTarget, current_image_id_index)
|
||||||
|
loadAnnotationAndViewportOnElement(annotationjson, viewport, dicom_element, current_image_id_index);
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -249,17 +261,26 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
});
|
});
|
||||||
|
|
||||||
function loadAnnotationAndViewportOnElement(annotation, viewport, dicom_element) {
|
function loadAnnotationAndViewportOnElement(annotation, viewport, dicom_element, current_image_id_index) {
|
||||||
cornerstoneTools.globalImageIdSpecificToolStateManager.clear(dicom_element);
|
cornerstoneTools.globalImageIdSpecificToolStateManager.clear(dicom_element);
|
||||||
cornerstone.getEnabledElement(dicom_element).viewport = viewport
|
cornerstone.getEnabledElement(dicom_element).viewport = viewport
|
||||||
cornerstoneTools.globalImageIdSpecificToolStateManager.restoreToolState(
|
cornerstoneTools.globalImageIdSpecificToolStateManager.restoreToolState(
|
||||||
annotation);
|
annotation);
|
||||||
console.log("annotation", annotation)
|
console.log("annotation", annotation)
|
||||||
console.log("viewport", viewport)
|
console.log("viewport", viewport)
|
||||||
|
|
||||||
//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, Number.isInteger(current_image_id_index))
|
||||||
|
if (Number.isInteger(current_image_id_index)) {
|
||||||
|
console.log("loading stack index")
|
||||||
|
dicomViewer.loadStackIndex(current_image_id_index, dicom_element);
|
||||||
|
} else {
|
||||||
|
console.log("loading next annotation image")
|
||||||
dicomViewer.getNextAnnotationImage(dicom_element);
|
dicomViewer.getNextAnnotationImage(dicom_element);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,6 +297,7 @@
|
|||||||
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,
|
||||||
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),
|
||||||
|
|||||||
@@ -85,6 +85,17 @@ urlpatterns = [
|
|||||||
views.GenericExamViews.exam_cids,
|
views.GenericExamViews.exam_cids,
|
||||||
name="exam_cids",
|
name="exam_cids",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"collection/<int:exam_id>/history",
|
||||||
|
views.collection_history,
|
||||||
|
name="collection_history",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"collection/<int:exam_id>/history/<int:user_pk>/user",
|
||||||
|
views.collection_history_user,
|
||||||
|
name="collection_history_user",
|
||||||
|
),
|
||||||
|
|
||||||
path(
|
path(
|
||||||
"collection/<int:pk>/user_status",
|
"collection/<int:pk>/user_status",
|
||||||
views.GenericExamViews.exam_user_status,
|
views.GenericExamViews.exam_user_status,
|
||||||
|
|||||||
+79
-14
@@ -246,6 +246,7 @@ def question_schema_detail(request, pk: int):
|
|||||||
{"question_schema": question_schema, "example_form": example_form},
|
{"question_schema": question_schema, "example_form": example_form},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
# @user_is_atlas_editor
|
# @user_is_atlas_editor
|
||||||
def question_schema_schemas(request):
|
def question_schema_schemas(request):
|
||||||
@@ -259,6 +260,7 @@ def question_schema_schemas(request):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
# @user_is_atlas_editor
|
# @user_is_atlas_editor
|
||||||
def condition_detail(request, pk):
|
def condition_detail(request, pk):
|
||||||
@@ -857,14 +859,17 @@ class QuestionSchemaCreate(LoginRequiredMixin, CreateView):
|
|||||||
model = QuestionSchema
|
model = QuestionSchema
|
||||||
form_class = QuestionSchemaForm
|
form_class = QuestionSchemaForm
|
||||||
|
|
||||||
|
|
||||||
class QuestionSchemaUpdate(LoginRequiredMixin, UpdateView):
|
class QuestionSchemaUpdate(LoginRequiredMixin, UpdateView):
|
||||||
model = QuestionSchema
|
model = QuestionSchema
|
||||||
form_class = QuestionSchemaForm
|
form_class = QuestionSchemaForm
|
||||||
|
|
||||||
|
|
||||||
class QuestionSchemaDelete(AuthorOrCheckerRequiredMixin, DeleteView):
|
class QuestionSchemaDelete(AuthorOrCheckerRequiredMixin, DeleteView):
|
||||||
model = QuestionSchema
|
model = QuestionSchema
|
||||||
template_name = "confirm_delete.html"
|
template_name = "confirm_delete.html"
|
||||||
|
|
||||||
|
|
||||||
class FindingUpdate(RevisionMixin, LoginRequiredMixin, UpdateView):
|
class FindingUpdate(RevisionMixin, LoginRequiredMixin, UpdateView):
|
||||||
model = Finding
|
model = Finding
|
||||||
form_class = FindingForm
|
form_class = FindingForm
|
||||||
@@ -1272,7 +1277,6 @@ def series_order_dicom(request, pk):
|
|||||||
@login_required
|
@login_required
|
||||||
def image_diff(request):
|
def image_diff(request):
|
||||||
if request.htmx:
|
if request.htmx:
|
||||||
|
|
||||||
image_ids = request.POST.getlist("image-id")
|
image_ids = request.POST.getlist("image-id")
|
||||||
|
|
||||||
if len(image_ids) == 2:
|
if len(image_ids) == 2:
|
||||||
@@ -1381,6 +1385,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"))
|
||||||
|
|
||||||
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)
|
||||||
@@ -1404,6 +1409,7 @@ def create_series_findings(request):
|
|||||||
sf.findings.set(findings)
|
sf.findings.set(findings)
|
||||||
sf.structures.set(structures)
|
sf.structures.set(structures)
|
||||||
sf.conditions.set(conditions)
|
sf.conditions.set(conditions)
|
||||||
|
sf.current_image_id_index = current_image_id_index
|
||||||
sf.save()
|
sf.save()
|
||||||
return JsonResponse({"success": True})
|
return JsonResponse({"success": True})
|
||||||
|
|
||||||
@@ -1596,6 +1602,58 @@ def collection_detail(request, pk):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@user_is_collection_author_or_atlas_editor
|
||||||
|
def collection_history(request, exam_id: int):
|
||||||
|
"""An overview page that shows who has attempted / finised the collection"""
|
||||||
|
collection = get_object_or_404(CaseCollection, pk=exam_id)
|
||||||
|
|
||||||
|
casedetails = CaseDetail.objects.filter(collection=collection).order_by(
|
||||||
|
"sort_order"
|
||||||
|
)
|
||||||
|
|
||||||
|
userexams = collection.get_cid_user_exams()
|
||||||
|
|
||||||
|
print(userexams)
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"atlas/collection_history.html",
|
||||||
|
{
|
||||||
|
"collection": collection,
|
||||||
|
"casesdetails": casedetails,
|
||||||
|
"can_edit": True,
|
||||||
|
"userexams": userexams,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@user_is_collection_author_or_atlas_editor
|
||||||
|
def collection_history_user(request, exam_id: int, user_pk: int):
|
||||||
|
collection = get_object_or_404(CaseCollection, pk=exam_id)
|
||||||
|
|
||||||
|
user = get_object_or_404(User, pk=user_pk)
|
||||||
|
|
||||||
|
casedetails = CaseDetail.objects.filter(
|
||||||
|
collection=collection,
|
||||||
|
).order_by("sort_order")
|
||||||
|
|
||||||
|
print(user)
|
||||||
|
print(casedetails)
|
||||||
|
|
||||||
|
user_answers = [(casedetail, casedetail.get_user_answers(user)) for casedetail in casedetails]
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"atlas/collection_history_user.html",
|
||||||
|
{
|
||||||
|
"collection": collection,
|
||||||
|
"user": user,
|
||||||
|
"casedetails": casedetails,
|
||||||
|
"user_answers": user_answers,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def collection_take_start(request, pk):
|
def collection_take_start(request, pk):
|
||||||
"""The starting page for taking / reviewing a case collection
|
"""The starting page for taking / reviewing a case collection
|
||||||
|
|
||||||
@@ -1630,23 +1688,29 @@ def collection_take_start(request, pk):
|
|||||||
request, "atlas/collection_review_start.html", template_variables
|
request, "atlas/collection_review_start.html", template_variables
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@user_is_collection_author_or_atlas_editor
|
@user_is_collection_author_or_atlas_editor
|
||||||
def collection_case_priors(request, exam_id, case_id):
|
def collection_case_priors(request, exam_id, case_id):
|
||||||
case_detail = CaseDetail.objects.get(case=case_id, collection=exam_id)
|
case_detail = CaseDetail.objects.get(case=case_id, collection=exam_id)
|
||||||
|
|
||||||
|
|
||||||
if request.htmx:
|
if request.htmx:
|
||||||
if "remove" in request.POST:
|
if "remove" in request.POST:
|
||||||
p = CasePrior.objects.get(case_detail=case_detail, prior_case=request.POST["remove"])
|
p = CasePrior.objects.get(
|
||||||
|
case_detail=case_detail, prior_case=request.POST["remove"]
|
||||||
|
)
|
||||||
p.delete()
|
p.delete()
|
||||||
return HttpResponse(f"Case removed")
|
return HttpResponse(f"Case removed")
|
||||||
elif "prior_case_id" in request.POST:
|
elif "prior_case_id" in request.POST:
|
||||||
prior_case = Case.objects.get(pk=request.POST["prior_case_id"])
|
prior_case = Case.objects.get(pk=request.POST["prior_case_id"])
|
||||||
p, created = CasePrior.objects.get_or_create(case_detail=case_detail, prior_case=prior_case)
|
p, created = CasePrior.objects.get_or_create(
|
||||||
|
case_detail=case_detail, prior_case=prior_case
|
||||||
|
)
|
||||||
p.relation_text = request.POST["relation"]
|
p.relation_text = request.POST["relation"]
|
||||||
|
|
||||||
if not p.relation_text:
|
if not p.relation_text:
|
||||||
return HttpResponse("You need to enter text to describe the relationship between the cases")
|
return HttpResponse(
|
||||||
|
"You need to enter text to describe the relationship between the cases"
|
||||||
|
)
|
||||||
|
|
||||||
p.save()
|
p.save()
|
||||||
|
|
||||||
@@ -1674,7 +1738,6 @@ def collection_case_priors(request, exam_id, case_id):
|
|||||||
else:
|
else:
|
||||||
available_priors.append((case, False, ""))
|
available_priors.append((case, False, ""))
|
||||||
|
|
||||||
|
|
||||||
form = PriorCaseForm()
|
form = PriorCaseForm()
|
||||||
|
|
||||||
case_number, case_count = collection.get_index_of_case(
|
case_number, case_count = collection.get_index_of_case(
|
||||||
@@ -1684,7 +1747,6 @@ def collection_case_priors(request, exam_id, case_id):
|
|||||||
previous = collection.get_previous_case(case_detail.case)
|
previous = collection.get_previous_case(case_detail.case)
|
||||||
next = collection.get_next_case(case_detail.case)
|
next = collection.get_next_case(case_detail.case)
|
||||||
|
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
"atlas/collection_case_priors.html",
|
"atlas/collection_case_priors.html",
|
||||||
@@ -1703,6 +1765,7 @@ def collection_case_priors(request, exam_id, case_id):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@user_is_collection_author_or_atlas_editor
|
@user_is_collection_author_or_atlas_editor
|
||||||
def collection_case_details(request, exam_id, case_id):
|
def collection_case_details(request, exam_id, case_id):
|
||||||
case_detail = CaseDetail.objects.get(case=case_id, collection=exam_id)
|
case_detail = CaseDetail.objects.get(case=case_id, collection=exam_id)
|
||||||
@@ -1713,7 +1776,6 @@ def collection_case_details(request, exam_id, case_id):
|
|||||||
raise Http404("Collection not in question mode")
|
raise Http404("Collection not in question mode")
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
|
|
||||||
# Called if the user saves the correct answer form
|
# Called if the user saves the correct answer form
|
||||||
if request.POST.get("submit") == "answer":
|
if request.POST.get("submit") == "answer":
|
||||||
# answers = request.POST
|
# answers = request.POST
|
||||||
@@ -1986,7 +2048,9 @@ def collection_take_overview(
|
|||||||
cid_user_exam.save()
|
cid_user_exam.save()
|
||||||
|
|
||||||
collection.exam_user_status.create(
|
collection.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",
|
||||||
)
|
)
|
||||||
|
|
||||||
return HttpResponse("True")
|
return HttpResponse("True")
|
||||||
@@ -2012,7 +2076,9 @@ def collection_take_overview(
|
|||||||
answer_count = 0
|
answer_count = 0
|
||||||
for cd in case_details:
|
for cd in case_details:
|
||||||
self_review = cid_user_exam.selfreview_set.filter(case=cd.case)
|
self_review = cid_user_exam.selfreview_set.filter(case=cd.case)
|
||||||
if cd in answer_question_map and (answer_question_map[cd].answer or answer_question_map[cd].json_answer):
|
if cd in answer_question_map and (
|
||||||
|
answer_question_map[cd].answer or answer_question_map[cd].json_answer
|
||||||
|
):
|
||||||
question_answer_tuples.append((cd, answer_question_map[cd], self_review))
|
question_answer_tuples.append((cd, answer_question_map[cd], self_review))
|
||||||
answer_count += 1
|
answer_count += 1
|
||||||
else:
|
else:
|
||||||
@@ -2192,8 +2258,6 @@ def collection_case_view_take(
|
|||||||
for series in prior.prior_case.get_series():
|
for series in prior.prior_case.get_series():
|
||||||
series_to_load.append((series, True, prior.relation_text))
|
series_to_load.append((series, True, prior.relation_text))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
previous = case_number > 0
|
previous = case_number > 0
|
||||||
next = case_number < (case_count - 1)
|
next = case_number < (case_count - 1)
|
||||||
|
|
||||||
@@ -2317,6 +2381,7 @@ def use_dates_as_descriptions(request, pk):
|
|||||||
|
|
||||||
return HttpResponse("Done")
|
return HttpResponse("Done")
|
||||||
|
|
||||||
|
|
||||||
def collection_case_dicom_json(request, exam_id, case_id):
|
def collection_case_dicom_json(request, exam_id, case_id):
|
||||||
case_detail = CaseDetail.objects.get(case=case_id, collection=exam_id)
|
case_detail = CaseDetail.objects.get(case=case_id, collection=exam_id)
|
||||||
|
|
||||||
@@ -2715,6 +2780,7 @@ class CaseCollectionAuthorUpdate(RevisionMixin, AuthorRequiredMixin, UpdateView)
|
|||||||
context["collection"] = context["object"]
|
context["collection"] = context["object"]
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class ExamGroupsUpdate(ExamGroupsUpdateBase):
|
class ExamGroupsUpdate(ExamGroupsUpdateBase):
|
||||||
model = CaseCollection
|
model = CaseCollection
|
||||||
form_class = ExamGroupsForm
|
form_class = ExamGroupsForm
|
||||||
@@ -2819,6 +2885,7 @@ def collection_question_schemas(request, exam_id: int):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def collection_reset_answers(request, exam_id: int):
|
def collection_reset_answers(request, exam_id: int):
|
||||||
if request.htmx:
|
if request.htmx:
|
||||||
collection = get_object_or_404(CaseCollection, pk=exam_id)
|
collection = get_object_or_404(CaseCollection, pk=exam_id)
|
||||||
@@ -2834,7 +2901,6 @@ def collection_reset_answers(request, exam_id: int):
|
|||||||
user_answers = case.userreportanswer_set.all()
|
user_answers = case.userreportanswer_set.all()
|
||||||
user_answers.delete()
|
user_answers.delete()
|
||||||
|
|
||||||
|
|
||||||
# User statuses
|
# User statuses
|
||||||
collection.exam_user_status.all().delete()
|
collection.exam_user_status.all().delete()
|
||||||
|
|
||||||
@@ -2842,6 +2908,5 @@ def collection_reset_answers(request, exam_id: int):
|
|||||||
collection.cid_users.all().delete()
|
collection.cid_users.all().delete()
|
||||||
return HttpResponse("Success")
|
return HttpResponse("Success")
|
||||||
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise Http404("Invalid request")
|
raise Http404("Invalid request")
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Generated by Django 5.0.2 on 2024-09-23 12:45
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('generic', '0017_cidusergroup_open_access_userusergroup_open_access_and_more'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='supervisor',
|
||||||
|
name='user',
|
||||||
|
field=models.OneToOneField(blank=True, help_text='If the supervisor has an account on the test system it can be associated here', null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
]
|
||||||
+7
-3
@@ -531,7 +531,7 @@ class SeriesBase(models.Model):
|
|||||||
size = 0
|
size = 0
|
||||||
|
|
||||||
for i in images:
|
for i in images:
|
||||||
size += i.image.size
|
size += i.get_file_size()
|
||||||
|
|
||||||
return size
|
return size
|
||||||
|
|
||||||
@@ -997,7 +997,6 @@ class ExamBase(ExamOrCollectionGenericBase):
|
|||||||
|
|
||||||
return User.objects.filter(pk__in=user_ids)
|
return User.objects.filter(pk__in=user_ids)
|
||||||
|
|
||||||
|
|
||||||
def generate_user_report(self, user):
|
def generate_user_report(self, user):
|
||||||
exam_text = [f"Candidate {user.first_name} [{user.email}]"]
|
exam_text = [f"Candidate {user.first_name} [{user.email}]"]
|
||||||
|
|
||||||
@@ -1478,6 +1477,12 @@ class CidUserExam(models.Model):
|
|||||||
|
|
||||||
return f"{self.exam} / {user}: {start_time} {end_time}"
|
return f"{self.exam} / {user}: {start_time} {end_time}"
|
||||||
|
|
||||||
|
def get_user_name(self) -> str:
|
||||||
|
if self.cid_user is None:
|
||||||
|
return self.user_user.username
|
||||||
|
else:
|
||||||
|
return "CID" + str(self.cid_user.cid)
|
||||||
|
|
||||||
|
|
||||||
CID_GROUP_EXAMS = (
|
CID_GROUP_EXAMS = (
|
||||||
("SBAs", "sba_cid_user_groups"),
|
("SBAs", "sba_cid_user_groups"),
|
||||||
@@ -1504,7 +1509,6 @@ class BaseUserGroup(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class CidUserGroup(BaseUserGroup):
|
class CidUserGroup(BaseUserGroup):
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return str(self.name)
|
return str(self.name)
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -747,7 +747,7 @@ def accounts_bulk_create_check(request):
|
|||||||
supervisor = Supervisor.objects.filter(email=user["supervisor_email"])
|
supervisor = Supervisor.objects.filter(email=user["supervisor_email"])
|
||||||
|
|
||||||
if not supervisor:
|
if not supervisor:
|
||||||
return HttpResponse("Ok, supervisor will be created")
|
return HttpResponse("Ok, supervisor does not exist so will be created")
|
||||||
|
|
||||||
if supervisor.count() > 1:
|
if supervisor.count() > 1:
|
||||||
return HttpResponse(format_html("<span class='error'>{}</span>", "More than one supervisor with that email"))
|
return HttpResponse(format_html("<span class='error'>{}</span>", "More than one supervisor with that email"))
|
||||||
@@ -793,7 +793,7 @@ def accounts_bulk_create(request):
|
|||||||
print(user)
|
print(user)
|
||||||
user_dict = {
|
user_dict = {
|
||||||
"username": user["email"],
|
"username": user["email"],
|
||||||
"first_name": user["email"],
|
"first_name": user["first_name"],
|
||||||
"last_name": user["last_name"],
|
"last_name": user["last_name"],
|
||||||
"email": user["email"],
|
"email": user["email"],
|
||||||
"password": secrets.token_hex(nbytes=16),
|
"password": secrets.token_hex(nbytes=16),
|
||||||
|
|||||||
Reference in New Issue
Block a user