image findings will now save and reload stack index
This commit is contained in:
+67
-33
@@ -29,7 +29,6 @@ from loguru import logger
|
||||
router = Router()
|
||||
|
||||
|
||||
|
||||
class SeriesSchema(ModelSchema):
|
||||
case_id: List[int] = []
|
||||
|
||||
@@ -59,6 +58,7 @@ def upload_dicom(request, files: List[UploadedFile] = File(...)):
|
||||
uploaded = []
|
||||
duplicate = []
|
||||
failed = []
|
||||
duplicate_series = set()
|
||||
for file in files:
|
||||
# data = file.read()
|
||||
try:
|
||||
@@ -67,26 +67,41 @@ def upload_dicom(request, files: List[UploadedFile] = File(...)):
|
||||
ud.save()
|
||||
|
||||
uploaded.append((file.name, ud.image_blake3_hash))
|
||||
except DuplicateDicom:
|
||||
except DuplicateDicom as e:
|
||||
duplicate.append((file.name, ud.image_blake3_hash))
|
||||
|
||||
duplicate_series.add(e.duplicate.get_series().get_absolute_url())
|
||||
print(duplicate_series)
|
||||
pass
|
||||
except InvalidDicomError:
|
||||
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)
|
||||
def generate_image_hash(request, id:int):
|
||||
def generate_image_hash(request, id: int):
|
||||
s = SeriesImage.objects.get(pk=id)
|
||||
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)
|
||||
def clear_dicoms(request):
|
||||
|
||||
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:
|
||||
dicoms = UncategorisedDicom.objects.filter(user=request.user)
|
||||
@@ -110,15 +125,17 @@ def uncategorised_dicoms(request):
|
||||
|
||||
return data
|
||||
|
||||
|
||||
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:
|
||||
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:
|
||||
dicoms = UncategorisedDicom.objects.filter(user=request.user)
|
||||
|
||||
|
||||
if "order-series" in request.POST:
|
||||
order = request.POST["order-series"]
|
||||
else:
|
||||
@@ -136,7 +153,9 @@ def import_dicoms_helper(request, case_id: int | None = None):
|
||||
tags = data[series_uid][0][1]
|
||||
|
||||
# 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"])
|
||||
else:
|
||||
series = Series(
|
||||
@@ -146,16 +165,16 @@ def import_dicoms_helper(request, case_id: int | None = None):
|
||||
)
|
||||
|
||||
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:
|
||||
examination.modality = modality
|
||||
examination.save()
|
||||
series.examination = examination
|
||||
|
||||
|
||||
series.save()
|
||||
|
||||
|
||||
match order:
|
||||
case "order-series-instance-number":
|
||||
series.order_by_dicom("InstanceNumber")
|
||||
@@ -188,12 +207,18 @@ def import_dicoms_helper(request, case_id: int | None = None):
|
||||
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):
|
||||
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):
|
||||
return import_dicoms_helper(request, case_id=case_id)
|
||||
|
||||
@@ -220,8 +245,9 @@ def series_remove_duplicate_images(request, series_id: int):
|
||||
|
||||
return len(dupes)
|
||||
|
||||
|
||||
@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)
|
||||
series = get_object_or_404(Series, pk=series_id)
|
||||
|
||||
@@ -230,11 +256,11 @@ def series_truncate(request, series_id: int, start: int, end:int):
|
||||
|
||||
images_removed = []
|
||||
for n, image in enumerate(series.get_images()):
|
||||
if n >= start and n <=end:
|
||||
if n >= start and n <= end:
|
||||
continue
|
||||
else:
|
||||
print(n, image)
|
||||
image.removed=True
|
||||
image.removed = True
|
||||
image.image = None
|
||||
image.save()
|
||||
images_removed.append(image.pk)
|
||||
@@ -261,10 +287,11 @@ def check_image_hash(request, hash: str):
|
||||
data = {"status": "success", "id": False}
|
||||
return data
|
||||
|
||||
|
||||
@router.post("/check_image_hashes/", auth=django_auth)
|
||||
def check_images_hashes(request, hashes: List[str]):
|
||||
"""Checks a list of image hashes and returns the series id / url if found
|
||||
|
||||
|
||||
Return format
|
||||
{ "hash_id": {"id": "series_id|false", "url": "series_url|false"}, ...}
|
||||
"""
|
||||
@@ -278,21 +305,28 @@ def check_images_hashes(request, hashes: List[str]):
|
||||
data = {
|
||||
"id": series_image.pk,
|
||||
"url": series_image.series.get_absolute_url(),
|
||||
"type": "series"
|
||||
"type": "series",
|
||||
}
|
||||
except SeriesImage.DoesNotExist:
|
||||
try:
|
||||
uncategorised_dicom = UncategorisedDicom.objects.get(image_blake3_hash=hash)
|
||||
data = { "id":uncategorised_dicom.pk, "url": reverse("atlas:user_uploads"), "type": "uncategorised"}
|
||||
uncategorised_dicom = UncategorisedDicom.objects.get(
|
||||
image_blake3_hash=hash
|
||||
)
|
||||
data = {
|
||||
"id": uncategorised_dicom.pk,
|
||||
"url": reverse("atlas:user_uploads"),
|
||||
"type": "uncategorised",
|
||||
}
|
||||
except UncategorisedDicom.DoesNotExist:
|
||||
data = { "id": False, "url": False}
|
||||
data = {"id": False, "url": False}
|
||||
|
||||
hash_status[hash] = data
|
||||
|
||||
return hash_status
|
||||
|
||||
#@router.get("/generate_image_hash/{id}", auth=django_auth)
|
||||
#def generate_image_hash(request, id: int):
|
||||
|
||||
# @router.get("/generate_image_hash/{id}", auth=django_auth)
|
||||
# def generate_image_hash(request, id: int):
|
||||
# series_image = SeriesImage.objects.get(pk=id)
|
||||
#
|
||||
# series_image.image_blake3_hash = ""
|
||||
@@ -301,21 +335,22 @@ def check_images_hashes(request, hashes: List[str]):
|
||||
#
|
||||
# print(series_image)
|
||||
|
||||
|
||||
@router.get("/view_dicom_tags/{hash}", auth=django_auth)
|
||||
def view_dicom_tags(request, hash: str):
|
||||
|
||||
item = SeriesImage.objects.get(image_blake3_hash=hash)
|
||||
return item.get_dicom_json()
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
if not series.check_user_can_edit(request.user):
|
||||
return {"status": "permission denied"}
|
||||
|
||||
if dicom_tag.startswith("(") and dicom_tag.endswith(")"):
|
||||
dicom_tag = tuple([hex(int(i.strip(),16)) for i in dicom_tag[1:-1].split(",")])
|
||||
dicom_tag = tuple([hex(int(i.strip(), 16)) for i in dicom_tag[1:-1].split(",")])
|
||||
|
||||
image_map = defaultdict(list)
|
||||
|
||||
@@ -324,7 +359,7 @@ def series_split_by_tag(request, series_id: int, dicom_tag:str):
|
||||
print(image)
|
||||
ds = image.get_dicom_data()
|
||||
|
||||
#print("2a", ds[dicom_tag])
|
||||
# print("2a", ds[dicom_tag])
|
||||
|
||||
if dicom_tag in ds:
|
||||
val = ds[dicom_tag].value
|
||||
@@ -339,7 +374,7 @@ def series_split_by_tag(request, series_id: int, dicom_tag:str):
|
||||
new_series = []
|
||||
case = series.case.all()
|
||||
authors = series.author.all()
|
||||
|
||||
|
||||
for n, option in enumerate(image_map):
|
||||
if n == 0:
|
||||
series.images.set(image_map[option])
|
||||
@@ -353,12 +388,11 @@ def series_split_by_tag(request, series_id: int, dicom_tag:str):
|
||||
|
||||
new_series.append(series.pk)
|
||||
|
||||
|
||||
return new_series
|
||||
|
||||
|
||||
@router.get("/split_order_by_dicom_tag/{series_id}/{dicom_tag}", auth=django_auth)
|
||||
def series_order_by_tag(request, series_id: int, dicom_tag:str):
|
||||
def series_order_by_tag(request, series_id: int, dicom_tag: str):
|
||||
series = get_object_or_404(Series, pk=series_id)
|
||||
|
||||
series.order_by_dicom(dicom_tag)
|
||||
|
||||
+1
-1
@@ -143,7 +143,7 @@ class StructureForm(ModelForm):
|
||||
class SeriesFindingForm(ModelForm):
|
||||
class Meta:
|
||||
model = SeriesFinding
|
||||
exclude = ["series", "annotation_json", "viewport_json"]
|
||||
exclude = ["series", "annotation_json", "viewport_json", "current_image_id_index"]
|
||||
|
||||
widgets = {
|
||||
"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),
|
||||
),
|
||||
]
|
||||
+15
-3
@@ -570,6 +570,9 @@ class SeriesImage(SeriesImageBase):
|
||||
except InvalidDicomError:
|
||||
return {}
|
||||
|
||||
def get_series(self):
|
||||
return self.series
|
||||
|
||||
# def get_image_dicom_json(self, image_index):
|
||||
# try:
|
||||
# with pydicom.dcmread(self.image) as ds:
|
||||
@@ -594,6 +597,7 @@ class SeriesFinding(models.Model):
|
||||
conditions = models.ManyToManyField(Condition, blank=True)
|
||||
annotation_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:
|
||||
findings = self.findings.all().values_list("name")
|
||||
@@ -1273,9 +1277,12 @@ class UncategorisedDicom(models.Model):
|
||||
|
||||
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
|
||||
|
||||
if image_hash is None:
|
||||
image_hash = self.image_blake3_hash
|
||||
|
||||
if obj := UncategorisedDicom.objects.filter(
|
||||
image_blake3_hash=image_hash
|
||||
).first():
|
||||
@@ -1310,7 +1317,8 @@ class UncategorisedDicom(models.Model):
|
||||
|
||||
if duplicate is not None:
|
||||
if duplicate != self:
|
||||
raise DuplicateDicom
|
||||
raise DuplicateDicom(duplicate)
|
||||
|
||||
|
||||
# Hack for tests
|
||||
if image_blake3_hash != "1234":
|
||||
@@ -1349,9 +1357,13 @@ class UncategorisedDicom(models.Model):
|
||||
except pydicom.errors.InvalidDicomError:
|
||||
return None
|
||||
|
||||
def get_series(self):
|
||||
return self.series
|
||||
|
||||
|
||||
class DuplicateDicom(Exception):
|
||||
pass
|
||||
def __init__(self, duplicate):
|
||||
self.duplicate = duplicate
|
||||
|
||||
|
||||
class Resource(models.Model, AuthorMixin):
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
<ul id="uploaded-files"></ul>
|
||||
<h5>Duplicates</h5>
|
||||
<ul id="duplicate-files"></ul>
|
||||
<ul id="duplicate-series"></ul>
|
||||
<h5>Failed</h5>
|
||||
<ul id="failed-files"></ul>
|
||||
</div>
|
||||
@@ -78,6 +79,7 @@
|
||||
<script>
|
||||
window.to_upload = [];
|
||||
window.upload_count = 1;
|
||||
window.duplicate_series = new Set()
|
||||
|
||||
function* chunks(arr, n) {
|
||||
for (let i = 0; i < arr.length; i += n) {
|
||||
@@ -149,6 +151,7 @@
|
||||
item.textContent = JSON.parse(xhr.response)["duplicates"][i];
|
||||
$("#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++) {
|
||||
let item = document.createElement("li");
|
||||
@@ -162,6 +165,14 @@
|
||||
}
|
||||
if (current_chunk + 1 == total_chunks) {
|
||||
$("#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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
<div>{{ series.modality}}, {{ series.examination }}, {{ series.plane }}, {{ series.contrast }}</div>
|
||||
<div>Description: {{series.description}}</div>
|
||||
|
||||
@@ -22,13 +24,7 @@
|
||||
<div class="hide" id="hidden-form">
|
||||
<form method="post" id="series_finding_form">
|
||||
{% csrf_token %}
|
||||
{{series_finding_form.management_form}}
|
||||
<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>
|
||||
{{series_finding_form|crispy}}
|
||||
<input type="submit" value="Submit">
|
||||
<button id="cancel-add-finding-button">Cancel</button>
|
||||
</form>
|
||||
@@ -45,14 +41,28 @@
|
||||
{% for finding in series.findings.all %}
|
||||
<div class="finding-box">
|
||||
<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">
|
||||
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 />
|
||||
Condition(s): {% for s in finding.conditions.all %}{{s.get_link}}{% endfor %}<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>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@@ -195,6 +205,7 @@
|
||||
})
|
||||
|
||||
$("#add-finding-button").click(() => {
|
||||
$("#hidden-form").show()
|
||||
dicom_element = $(".cornerstone-element").get(0);
|
||||
cornerstoneTools.globalImageIdSpecificToolStateManager.clear(dicom_element);
|
||||
cornerstone.reset(dicom_element);
|
||||
@@ -202,7 +213,6 @@
|
||||
//$("#finding-form").empty().append(
|
||||
// $("#hidden-form form").clone()
|
||||
//);
|
||||
$("#hidden-form").show()
|
||||
|
||||
});
|
||||
$("#cancel-add-finding-button").click((e) => {
|
||||
@@ -225,7 +235,9 @@
|
||||
dicom_element = $(".cornerstone-element").get(0);
|
||||
annotationjson = JSON.parse(e.currentTarget.dataset.annotationjson);
|
||||
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 %}
|
||||
});
|
||||
|
||||
function loadAnnotationAndViewportOnElement(annotation, viewport, dicom_element) {
|
||||
function loadAnnotationAndViewportOnElement(annotation, viewport, dicom_element, current_image_id_index) {
|
||||
cornerstoneTools.globalImageIdSpecificToolStateManager.clear(dicom_element);
|
||||
cornerstone.getEnabledElement(dicom_element).viewport = viewport
|
||||
cornerstoneTools.globalImageIdSpecificToolStateManager.restoreToolState(
|
||||
annotation);
|
||||
console.log("annotation", annotation)
|
||||
console.log("viewport", viewport)
|
||||
|
||||
//cornerstone.getEnabledElement(dicom_element).toolStateManager.restoreToolState(annotationjson)
|
||||
cornerstone.resize(dicom_element);
|
||||
current_image_id_index = parseInt(current_image_id_index);
|
||||
|
||||
dicomViewer.getNextAnnotationImage(dicom_element);
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -276,6 +297,7 @@
|
||||
description: $('#id_description').val(),
|
||||
series: {{ series.pk }},
|
||||
//annotation_json: JSON.stringify(c.toolStateManager.saveToolState()),
|
||||
current_image_id_index: c.toolStateManager.toolState.stack.data[0].currentImageIdIndex,
|
||||
annotation_json: JSON.stringify(cornerstoneTools.globalImageIdSpecificToolStateManager
|
||||
.saveToolState()),
|
||||
viewport_json: JSON.stringify(c.viewport),
|
||||
|
||||
@@ -1385,6 +1385,7 @@ def create_series_findings(request):
|
||||
description = request.POST.get("description")
|
||||
annotation_json = request.POST.get("annotation_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)
|
||||
findings = Finding.objects.filter(pk__in=findings_ids)
|
||||
@@ -1408,6 +1409,7 @@ def create_series_findings(request):
|
||||
sf.findings.set(findings)
|
||||
sf.structures.set(structures)
|
||||
sf.conditions.set(conditions)
|
||||
sf.current_image_id_index = current_image_id_index
|
||||
sf.save()
|
||||
return JsonResponse({"success": True})
|
||||
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
Reference in New Issue
Block a user