This commit is contained in:
Ross
2022-07-27 10:06:56 +01:00
parent 8f5c878bab
commit ef478459d8
14 changed files with 701 additions and 645 deletions
+2 -1
View File
@@ -579,4 +579,5 @@ function delete_multiple(url, csrf_token) {
}
}
window.delete_multiple = delete_multiple
window.delete_multiple = delete_multiple
+373
View File
@@ -0,0 +1,373 @@
let active_file_inputs = new Set();
function add_input_form() {
var form_idx = $('#id_images-TOTAL_FORMS').val();
$('#image_form_set').append($($('#empty_form').html().replace(/__prefix__/g, form_idx)).on("change",
input_change));
$('#id_images-TOTAL_FORMS').val(parseInt(form_idx) + 1);
}
async function blobToBase64(blob) {
return new Promise((resolve, _) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.readAsDataURL(blob);
});
}
function extendInputs(form_id, n) {
// Makes sure we have n inputs available
// returns available inputs
inputs = getUnusedInputs($(`#${form_id} input[type=file][id^=id_images-]`));
//fileInput = document.getElementById("id_images-0-image");
// Make sure we have enough input targets
input_diff = (n - inputs.length)
if (input_diff > 0) {
for (let j = 0; j < input_diff; j++) {
add_input_form()
}
// Need to make sure we include the new ones...
inputs = getUnusedInputs($(`#${form_id} input[type=file][id^=id_images-]`));
}
return inputs;
}
function getCurrentFormFiles(form_id) {
inputs = $(`#${form_id} input[type=file]`);
let files = new Set();
for (let j = 0; j < inputs.length; j++) {
i = inputs.get(j);
if (i.files.length > 0) {
files.add(i.files[0]);
}
}
return files;
}
function getUnusedInputs(inputs) {
new_inputs = [];
for (let i = 0; i < inputs.length; i++) {
input = inputs.get(i);
if (!input.value) {
new_inputs.push(input);
}
}
return $(new_inputs);
}
function addFile(f, feedback) {
let dT = new DataTransfer();
dT.clearData();
dT.items.add(f);
for (let i = 0; i < inputs.length; i++) {
el = inputs.get(i);
if (el.files.length == 0) {
el.files = dT.files;
//ocr(el)
if (feedback) {
arr = el.name.split("-");
arr.splice(2, 1, "feedback_image");
feedback_el_name = arr.join("-");
$(`[name=${feedback_el_name}]`).prop("checked", true);
}
$(el).change();
return false;
}
}
}
function ocr2(url, input) {
console.log("OCR", input)
Tesseract.recognize(
url,
'eng',
//{ logger: m => console.log(m) }
).then(({
data: {
text
}
}) => {
l = text.toLowerCase();
uploading_el = $(`img[data-input-id='${input.id}'], div[data-input-id='${input.id}']`)
uploading_el.removeClass("image-ident-loading");
console.log("found text", l);
if (l.includes("accesion") || l.includes("number") || l.search(
/(ref|rk9|rh8|ra9|rbz)\d+/g) > -1) {
console.log("Match found ", input);
$(input).addClass("image-ident-warning");
uploading_el.addClass("image-ident-warning");
} else {
$(input).addClass("image-ident-ok");
}
})
}
function input_change(evt) {
file = evt.target.files[0];
$(evt.target).removeClass("image-ident-warning");
$(evt.target).removeClass("image-ident-ok");
el = evt.target;
readFileAndProcess2(el);
}
function showEditPopup(url) {
var win = window.open(url, "Edit",
'height=500,width=800,resizable=yes,scrollbars=yes');
return false;
}
function showAddPopup(triggeringLink) {
var name = triggeringLink.id.replace(/^add_/, '');
href = triggeringLink.href;
var win = window.open(href, name, 'height=500,width=800,resizable=yes,scrollbars=yes');
win.focus();
return false;
}
function closePopup(win, newID, newRepr, id) {
$(id + "_to").append('<option value=' + newID + ' title=' + newRepr + ' >' + newRepr + '</option>')
win.close();
}
async function readFileAndProcess2(el) {
//file_set = $("#image_form_set input[type=file]");
//file_set.each(async (n, el) => {
if (el.files.length > 0) {
filename = el.files[0].name;
// Probably no need to await here anymore
//await readFile(el.files[0], el);
$(el).parent().parent().find(".temp-thumb").remove();
let url = URL.createObjectURL(el.files[0])
n = el.id.split("-")[1];
$(`#drop-filenames span[data-input-id='${el.id}']`).remove();
let extra_class = " image-ident-loading";
if ($(el).hasClass("image-ident-warning")) {
extra_class = " image-ident-warning";
} else if ($(el).hasClass("image-ident-ok")) {
extra_class = " image-ident-ok";
}
//let extra_class = "";
let imageId;
if (el.files[0].type.startsWith("image")) {
let hash = CryptoJS.MD5(el.files[0]).toString();
base = await blobToBase64(el.files[0]);
imageId = "base64://" + base.split(",")[1];
imageId2 = base;
const element = $(`<img class='temp-thumb' src=${imageId}></div>`).get(0)
$(el).parent().parent().prepend(element);
ocr2(url, el);
checkHash(hash, el);
if ($("#drop-filenames").length > 0) {
$("#drop-filenames").append(
`<span data-input-id='${el.id}'><span>${n}: ${el.files[0].name}</span><img class='uploading${extra_class}' data-input-id='${el.id}' src=${url}></span>`
)
}
} else {
imageId = `wadouri:${url}`;
imageId2 = imageId
const element = $(`<div class='temp-thumb' src=${imageId2}></div>`).get(0)
$(el).parent().parent().prepend(element);
cornerstone.enable(element);
cornerstone.loadAndCacheImage(imageId).then(function (image) {
cornerstone.displayImage(element, image);
cornerstone.resize(element)
if (hash_url) {
let pixel_data = image.getPixelData().toString()
let hash = CryptoJS.MD5(pixel_data).toString();
checkHash(hash, el);
}
console.log("image", image)
extractDicomDetails(image.data.byteArray)
});
if ($("#drop-filenames").length > 0) {
$("#drop-filenames").append(
`<span data-input-id='${el.id}'><span>${n}: ${el.files[0].name}</span><div class='uploading${extra_class}' data-input-id='${el.id}' src=${url}></div></span>`
)
const element2 = $(`#drop-filenames div[data-input-id='${el.id}']`).get(0);
cornerstone.enable(element2);
cornerstone.loadAndCacheImage(imageId).then(function (image) {
cornerstone.displayImage(element2, image);
$(element2).addClass("temp-thumb-large");
cornerstone.resize(element2)
setTimeout(function () {
ocr2($(element2).find("canvas").get(0).toDataURL(), el);
$(element2).removeClass("temp-thumb-large");
cornerstone.resize(element2)
}, 500);
});
}
}
active_file_inputs.delete(el)
console.log("active", active_file_inputs)
// Only load once all queued files have been processed
if (active_file_inputs.size < 1) {
//load_viewer = true;
loadViewer();
}
}
//})
}
function checkHash(hash, input, url) {
$.ajax({
url: hash_url,
data: {
csrfmiddlewaretoken: csrf_token,
hash: hash
},
type: "POST",
dataType: "json",
})
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
.done(function (data) {
console.log(data);
if (data.status == "success") {
console.log("success");
console.log(data.id)
if (data.id) {
//toastr.info(`id ${data.id}`)
uploading_el = $(`img[data-input-id='${input.id}'], div[data-input-id='${input.id}']`)
$(input).addClass("image-duplicate");
uploading_el.addClass("image-duplicate");
toastr.warning(`Image (${input.files[0].name}) already exists.<br /><a href='${data.url}'>Click to view</a><br /><button type="button" class="btn clear">Dismiss</button>`, "Duplicate image", {
"closeButton": false,
"debug": false,
"newestOnTop": false,
"progressBar": false,
"positionClass": "toast-top-full-width",
"preventDuplicates": false,
"onclick": null,
"showDuration": "0",
"hideDuration": "0",
"timeOut": 0,
"extendedTimeOut": 0,
"showEasing": "swing",
"hideEasing": "linear",
"showMethod": "fadeIn",
"hideMethod": "fadeOut",
"tapToDismiss": false
})
}
}
})
.always(function () {
console.log('[Done]');
})
}
function updateImagePositions() {
console.log("UP pos");
file_set = $("#image_form_set ul");
n = 0;
for (let i = 0; i < file_set.length; i++) {
ul = file_set.get(i);
console.log(ul)
file_element = $(ul).find("input[type=file]").get(0);
// If a (new) file is being uploaded we save it's name prior to it being hashed
if (file_element.files.length > 0) {
file_name = file_element.files[0].name
//$(ul).find("input[type=text]").val(file_name);
$(ul).find("input[name*='filename']").val(file_name);
}
if (file_element.files.length > 0 || $(ul).find(":contains('Currently:')")) {
n++;
$(ul).find("input[type=number]").val(n);
}
}
}
function loadViewer() {
console.log("LOAD VIEWER")
// temp fix before starting viewer collapsed
$("#single-dicom-viewer").show()
//file_set = $("#image_form_set input[type=file]");
//n = 0;
//for (let i = 0; i < file_set.length; i++) {
// el = file_set.get(i)
// if (el.files.length > 0) {
// n++;
// }
//}
image_set = $("#image_form_set .temp-thumb").get() //.map(function() { return $(this).attr("src")}).get();
console.log(image_set);
//console.log()
//if (n == image_set.length) {
const event = new CustomEvent('loadDicomViewer', {
"detail": image_set
});
window.dispatchEvent(event);
sortable('.sortable');
//sortable('.sortable')[0].addEventListener('sortstop', function(e) {
updateImagePositions();
//}
}
-3
View File
@@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.
View File
+61
View File
@@ -0,0 +1,61 @@
import json
from re import A
import pytest
# from django.contrib.auth.models import User
from django.urls import reverse
from rich.pretty import pprint
from bs4 import BeautifulSoup
from anatomy.views import GenericExamViews as AnatomyExamViews
from anatomy.models import Exam
from generic.models import CidUser, CidUserGroup
APP_NAME = "anatomy"
def create_cid_user_and_groups(db):
group1 = CidUserGroup.objects.create(name="Group1")
group2= CidUserGroup.objects.create(name="Group2")
cid_user_1000 = CidUser.objects.create(cid=1000, passcode="ABCD", group=group1)
cid_user_1001 = CidUser.objects.create(cid=1001, passcode="EFGH", group=group1)
cid_user_2001 = CidUser.objects.create(cid=2001, passcode="EFGH", group=group2)
assert cid_user_1000.cid == 1000
assert cid_user_1001.cid == 1001
def create_exam(db):
exam: Exam = AnatomyExamViews.Exam.objects.create(name="Cid Exam", exam_mode=True, active=True)
group1: CidUserGroup = CidUserGroup.objects.get(name="Group1")
exam.cid_user_groups.add(group1)
exam.valid_cid_users.add(*group1.ciduser_set.all())
assert exam in group1.anatomy_cid_user_groups.all()
assert exam
def test_exams(db, client):
create_cid_user_and_groups(db)
create_exam(db)
active_exams = client.get(reverse(f"{APP_NAME}:active_exams"))
assert active_exams.status_code == 200
assert len(json.load(active_exams.content)) == 0
cid_user_1001 = CidUser.objects.get(cid=1001)
for cid_user in [cid_user_1001]
active_exams_cid = client.get(reverse(f"{APP_NAME}:active_exams_cid", ))
print(active_exams_cid.status_code)
print(active_exams_cid.content)
+5 -2
View File
@@ -149,7 +149,7 @@ class SeriesForm(ModelForm):
"all": ["css/widgets.css"],
}
# Adding this javascript is crucial
js = ["jsi18n.js", "tesseract.min.js"]
js = ["jsi18n.js", "tesseract.min.js", "js/upload_form_helpers.js"]
def __init__(self, *args, **kwargs):
self.user = kwargs.pop(
@@ -239,7 +239,7 @@ class CaseForm(ModelForm):
"all": ["css/widgets.css"],
}
# Adding this javascript is crucial
js = ["jsi18n.js", "tesseract.min.js"]
js = ["jsi18n.js", "tesseract.min.js", "js/upload_form_helpers.js"]
def __init__(self, *args, **kwargs):
logging.info("LOG")
@@ -371,6 +371,7 @@ CaseDifferentialFormSet = inlineformset_factory(
class CaseSeriesForm(ModelForm):
def __init__(self, *args, user, **kwargs):
super(CaseSeriesForm, self).__init__(*args, **kwargs)
@@ -386,6 +387,8 @@ class CaseSeriesForm(ModelForm):
)
class CaseCollectionCaseForm(ModelForm):
def __init__(self, *args, user, **kwargs):
super(CaseCollectionCaseForm, self).__init__(*args, **kwargs)
+92 -159
View File
@@ -6,15 +6,10 @@
<!--<script type="text/javascript" src="/admin/jsi18n/"></script>-->
<script type="text/javascript">
let crsf_token = "{{csrf_token}}";
let hash_url = false;
document.addEventListener('drop', function (e) { e.preventDefault(); }, false);
function add_input_form() {
var form_idx = $('#id_images-TOTAL_FORMS').val();
$('#image_form_set').append($('#empty_form').html().replace(/__prefix__/g, form_idx));
$('#id_images-TOTAL_FORMS').val(parseInt(form_idx) + 1);
}
$(document).ready(function () {
$("#add_examination").appendTo($("label[for='id_examination']"));
@@ -44,71 +39,93 @@
dropContainer.ondrop = function (evt) {
$(evt.target).removeClass("drop-target-active");
file_drop_number = evt.dataTransfer.files.length;
if (evt.dataTransfer.files.length < 1) {
toastr.warning(`Drop failed :( try again?`);
if (file_drop_number < 1) {
toastr.warning(`Drop failed (no files), try again.`);
} else {
toastr.info(`Adding ${evt.dataTransfer.files.length} files.`);
toastr.info(`Loading ${file_drop_number} dropped image(s).`);
}
// Get all input elements
inputs = $("#atlas-form input[type=file][id^=id_images-]");
//fileInput = document.getElementById("id_images-0-image");
inputs = extendInputs("atlas-form", file_drop_number);
// Make sure we have enough input targets
input_diff = (evt.dataTransfer.files.length - inputs.length)
console.log("drop inputs", inputs, evt);
if (input_diff > 0) {
for (let j = 0; j < input_diff; j++) {
add_input_form()
}
// Need to make sure we include the new ones...
inputs = $("#atlas-form input[type=file][id^=id_images-]");
}
// Loop through each dropped file and try to assign to an
// input element
[...evt.dataTransfer.files].forEach((f) => {
let dT = new DataTransfer();
dT.clearData();
dT.items.add(f);
for (let i = 0; i < inputs.length; i++) {
el = inputs.get(i)
if (el.files.length == 0) {
el.files = dT.files;
//ocr(el)
return false
}
feedback = false;
if (evt.target.id == "feedback-drop-target") {
feedback = true;
}
addFile(f, feedback)
})
// // If you want to use some of the dropped files
// const dT = new DataTransfer();
// dT.items.add(evt.dataTransfer.files[0]);
// dT.items.add(evt.dataTransfer.files[3]);
// fileInput.files = dT.files;
loadDicomViewerAndFileImages();
evt.preventDefault();
evt.stopPropagation();
};
//dropContainer.ondrop = function (evt) {
// $(evt.target).removeClass("drop-target-active");
// if (evt.dataTransfer.files.length < 1) {
// toastr.warning(`Drop failed :( try again?`);
// } else {
// toastr.info(`Adding ${evt.dataTransfer.files.length} files.`);
// }
// // Get all input elements
// inputs = $("#atlas-form input[type=file][id^=id_images-]");
// //fileInput = document.getElementById("id_images-0-image");
// // Make sure we have enough input targets
// input_diff = (evt.dataTransfer.files.length - inputs.length)
// if (input_diff > 0) {
// for (let j = 0; j < input_diff; j++) {
// add_input_form()
// }
// // Need to make sure we include the new ones...
// inputs = $("#atlas-form input[type=file][id^=id_images-]");
// }
// // Loop through each dropped file and try to assign to an
// // input element
// [...evt.dataTransfer.files].forEach((f) => {
// let dT = new DataTransfer();
// dT.clearData();
// dT.items.add(f);
// for (let i = 0; i < inputs.length; i++) {
// el = inputs.get(i)
// if (el.files.length == 0) {
// el.files = dT.files;
// //ocr(el)
// return false
// }
// }
// })
// // // If you want to use some of the dropped files
// // const dT = new DataTransfer();
// // dT.items.add(evt.dataTransfer.files[0]);
// // dT.items.add(evt.dataTransfer.files[3]);
// // fileInput.files = dT.files;
// loadDicomViewerAndFileImages();
// evt.preventDefault();
// evt.stopPropagation();
//};
$('#add_more_images').click(() => { add_input_form() });
$("input[type=file]").on('change', function () {
$(this).removeClass("image-ident-warning");
$(this).removeClass("image-ident-ok");
console.log("input change1");
console.log("input change", this);
//ocr(this);
//LoadDicomViewer();
});
$("input[type=file]").on('change', input_change);
@@ -134,63 +151,6 @@
function ocr(input) {
// Tesseract.recognize(f, "eng",{ logger: m => console.log(m) }
// ).then(({ data: { text } }) => {
// console.log(text);
// l = text.toLowerCase();
// if (l.includes("accesion") || l.includes("number") || l.search(/(REF|RK9|RH8|RA9)\d+/g)) {
// console.log("SHIT", this);
// }
// })
console.log('{% static "worker.min.js" %}');
console.log('{{ STATIC_URL }}/tesseract-core.wasm.js %}');
const worker = Tesseract.createWorker({
workerPath: '{% static "worker.min.js" %}',
langPath: '{% static "" %}',
//langPath: '{{ STATIC_URL }}',
corePath: '{% static "tesseract-core.wasm.js" %}',
});
const url = URL.createObjectURL(input.files[0]);
let img = new Image;
img.src = url;
img.onload = function () {
width = img.width;
// const rectangle = { left: 0, top: 0, width: width, height: 10 }
(async () => {
await worker.load();
await worker.loadLanguage('eng');
await worker.initialize('eng');
// const { data: { text } } = await worker.recognize(input.files[0], { rectangle });
const { data: { text } } = await worker.recognize(input.files[0]);
//console.log(text);
l = text.toLowerCase();
$(`img[data-input-id='${input.id}'`).removeClass("image-ident-loading");
console.log(l);
if (l.includes("accesion") || l.includes("number") || l.search(/(ref|rk9|rh8|ra9|rbz)\d+/g) > -1) {
$(input).addClass("image-ident-warning");
$(`img[data-input-id='${input.id}'`).addClass("image-ident-warning");
} else {
$(input).addClass("image-ident-ok");
}
await worker.terminate();
})();
}
}
async function loadDicomViewerAndFileImages(callback) {
$("#single-dicom-viewer").empty()
//images = []
@@ -263,55 +223,29 @@
}
function loadViewer(images) {
//dicomViewer.loadCornerstone($("#single-dicom-viewer"), null, images, annotations);
file_set = $("#image_form_set input[type=file]");
n = 0;
for (let i = 0; i < file_set.length; i++) {
el = file_set.get(i)
if (el.files.length > 0) {
n++;
}
}
image_set = $("#image_form_set img");
if (n == image_set.length) {
const event = new CustomEvent('loadDicomViewer', { "detail": image_set });
window.dispatchEvent(event);
sortable('.sortable');
//sortable('.sortable')[0].addEventListener('sortstop', function(e) {
updateImagePositions();
}
}
function updateImagePositions() {
console.log("UP pos");
file_set = $("#image_form_set ul");
n = 0;
for (let i = 0; i < file_set.length; i++) {
ul = file_set.get(i);
console.log(ul)
file_element = $(ul).find("input[type=file]").get(0);
// If a (new) file is being uploaded we save it's name prior to it being hashed
if (file_element.files.length > 0) {
file_name = file_element.files[0].name
$(ul).find("input[type=text]").val(file_name);
}
if (file_element.files.length > 0 || $(ul).find(":contains('Currently:')")) {
n++;
$(ul).find("input[type=number]").val(n);
}
}
}
// function loadViewer(images) {
// //dicomViewer.loadCornerstone($("#single-dicom-viewer"), null, images, annotations);
// file_set = $("#image_form_set input[type=file]");
//
// n = 0;
// for (let i = 0; i < file_set.length; i++) {
// el = file_set.get(i)
// if (el.files.length > 0) {
// n++;
// }
// }
//
// image_set = $("#image_form_set img");
// if (n == image_set.length) {
// const event = new CustomEvent('loadDicomViewer', { "detail": image_set });
//
// window.dispatchEvent(event);
// sortable('.sortable');
// //sortable('.sortable')[0].addEventListener('sortstop', function(e) {
// updateImagePositions();
// }
//
// }
function extractDicomDetails(byteArray) {
// We need to setup a try/catch block because parseDicom will throw an exception
@@ -486,13 +420,12 @@
</table>
<h3>Images:</h3>
<input type="button" value="Add More Images (manually)" id="add_more_images">
<div id="drop-container" class="series-drop">Drop images here (or use the buttons below). Dropping images here will overwrite
existing images in the series. When drag and dropping make sure to drag the first image or the order will not be
maintained. Note: dicom images are often not exported in order (although their order can be automatically
detected once uploaded)
<div id="single-dicom-viewer" class="dicom-viewer" data-images="" data-annotations=''></div>
<div id="drop-filenames"></div>
{% comment %} <div id="drop-filenames"></div> {% endcomment %}
</div>
<div id="image_form_set" class="sortable">
{% for form in image_formset %}
+9
View File
@@ -0,0 +1,9 @@
DEBUG=1
SECRET_KEY=w(s0&(_eb058wvmg@44_repv8)r9@5p8fx*g_@c)1dm&d*ew^u
DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]
DB_NAME=rad
DB_HOST=db
DB_USER=django
DB_PASSWORD=postgres
DB_PORT=5432
+27
View File
@@ -0,0 +1,27 @@
version: '3.8'
services:
web:
build: ./rad
command: python manage.py runserver 0.0.0.0:8000
volumes:
- ./rad/:/usr/src/rad/
- ../rts/:/usr/src/rad/rts
- ./backups:/usr/src/backups
- ./media:/usr/src/media
- ./static:/usr/src/static
ports:
- 8000:8000
env_file:
- ./.env.dev
db:
image: postgres:14.2-alpine
volumes:
- postgres_data:/var/lib/postgresql/data/
environment:
- POSTGRES_USER=django
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=rad
volumes:
postgres_data:
+1 -1
View File
@@ -1587,7 +1587,7 @@ class ExamViews(View, LoginRequiredMixin):
answer_score = 0
ans = "Not answered"
else:
if user_answer.normal:
if exam.app_name == "rapids" and user_answer.normal:
ans = "Normal"
else:
ans = user_answer.answer
+56 -41
View File
@@ -5,47 +5,62 @@ from . import views
app_name = "physics"
urlpatterns = [
# path("question/", views.QuestionView.as_view(), name="question_view"),
path("question/", views.QuestionView.as_view(), name="question_view"),
path("question/<int:pk>/", views.question_detail, name="question_detail"),
# path("question/create/", views.QuestionCreate.as_view(), name="anatomy_question_create"),
# path("question/<int:pk>/update", views.QuestionUpdate.as_view(), name="anatomy_question_update"),
# path("exam/<int:pk>/<int:sk>/mark", views.mark, name="mark"),
# path("exam/<int:pk>/mark", views.mark_overview, name="mark_overview"),
path("exam/<int:pk>/take_old", views.exam_take_old, name="exam_take_old"),
path(
"exam/<int:pk>/<int:sk>/<str:cid>/<str:passcode>/take",
views.exam_take,
name="exam_take",
),
path("exam/<int:pk>/start", views.exam_start, name="exam_start"),
path(
"exam/<int:pk>/<str:cid>/<str:passcode>/finish",
views.exam_take_overview,
name="exam_take_overview",
),
path("exam/<int:pk>/authors", views.ExamAuthorUpdate.as_view(), name="exam_authors"),
path("exam/<int:exam_id>/clone", views.ExamClone.as_view(), name="exam_clone"),
path("exam/<int:pk>/update", views.ExamUpdate.as_view(), name="exam_update"),
path("exam/<int:pk>/delete", views.ExamDelete.as_view(), name="exam_delete"),
path("exam/available", views.active_exams, name="active_exams"),
# path("exam/json/<int:pk>", views.exam_json, name="exam_json"),
# path("exam/json/<int:pk>/recreate", views.exam_json_recreate, name="exam_json_recreate"),
path(
"user_answers/",
views.UserAnswerTableView.as_view(),
name="user_answer_table_view",
),
path(
"user_answers/<int:pk>", views.UserAnswerView.as_view(), name="user_answer_view"
),
path(
"user_answers/<int:pk>/delete",
views.UserAnswerDelete.as_view(),
name="user_answer_delete",
),
]
urlpatterns = []
urlpatterns.extend(
[
# path("question/", views.QuestionView.as_view(), name="question_view"),
path("question/", views.QuestionView.as_view(), name="question_view"),
path("question/<int:pk>/", views.question_detail, name="question_detail"),
# path("question/create/", views.QuestionCreate.as_view(), name="anatomy_question_create"),
# path("question/<int:pk>/update", views.QuestionUpdate.as_view(), name="anatomy_question_update"),
# path("exam/<int:pk>/<int:sk>/mark", views.mark, name="mark"),
# path("exam/<int:pk>/mark", views.mark_overview, name="mark_overview"),
path("exam/<int:pk>/take_old", views.exam_take_old, name="exam_take_old"),
path(
"exam/<int:pk>/<int:sk>/<str:cid>/<str:passcode>/take",
views.exam_take,
name="exam_take",
),
path("exam/<int:pk>/start", views.exam_start, name="exam_start"),
path(
"exam/<int:pk>/<str:cid>/<str:passcode>/finish",
views.exam_take_overview,
name="exam_take_overview",
),
path(
"exam/<int:pk>/authors",
views.ExamAuthorUpdate.as_view(),
name="exam_authors",
),
path("exam/<int:exam_id>/clone", views.ExamClone.as_view(), name="exam_clone"),
path("exam/<int:pk>/update", views.ExamUpdate.as_view(), name="exam_update"),
path("exam/<int:pk>/delete", views.ExamDelete.as_view(), name="exam_delete"),
path("exam/available", views.active_exams, name="active_exams"),
# path("exam/json/<int:pk>", views.exam_json, name="exam_json"),
# path("exam/json/<int:pk>/recreate", views.exam_json_recreate, name="exam_json_recreate"),
path(
"user_answers/",
views.UserAnswerTableView.as_view(),
name="user_answer_table_view",
),
path(
"user_answers/<int:pk>",
views.UserAnswerView.as_view(),
name="user_answer_view",
),
path(
"user_answers/<int:pk>/delete",
views.UserAnswerDelete.as_view(),
name="user_answer_delete",
),
path(
"exam/<int:pk>/scores/<int:cid>/<str:passcode>/",
views.exam_scores_cid_user,
name="exam_scores_cid_user",
),
]
)
urlpatterns.extend(generic_view_urls(views.GenericViews))
urlpatterns.extend(generic_exam_urls(views.GenericExamViews))
+1 -1
View File
@@ -76,7 +76,7 @@ class RapidForm(ModelForm):
"all": ["css/widgets.css"],
}
# Adding this javascript is crucial
js = ["jsi18n.js", "tesseract.min.js"]
js = ["jsi18n.js", "tesseract.min.js", "js/upload_form_helpers.js"]
def __init__(self, *args, **kwargs):
self.user = kwargs.pop(
+73 -436
View File
@@ -6,11 +6,12 @@
<script type="text/javascript">
let normal = false;
let csrf_token = "{{csrf_token}}";
let hash_url = "{% url 'rapids:rapid_question_by_hash' %}";
//let monitor_directory_handler = false;
//let monitor_interval_id = false;
// set to hold the files that are still being processed
let active_file_inputs = new Set();
let load_viewer = false;
// async function listAllFilesAndDirs(dirHandle, files_only) {
@@ -39,84 +40,13 @@
// return files;
// }
function showEditPopup(url) {
var win = window.open(url, "Edit",
'height=500,width=800,resizable=yes,scrollbars=yes');
return false;
}
function showAddPopup(triggeringLink) {
var name = triggeringLink.id.replace(/^add_/, '');
href = triggeringLink.href;
var win = window.open(href, name, 'height=500,width=800,resizable=yes,scrollbars=yes');
win.focus();
return false;
}
function closePopup(win, newID, newRepr, id) {
$(id + "_to").append('<option value=' + newID + ' title=' + newRepr + ' >' + newRepr + '</option>')
win.close();
}
document.addEventListener('drop', function (e) {
e.preventDefault();
}, false);
function getUnusedInputs(inputs) {
new_inputs = [];
for (let i = 0; i < inputs.length; i++) {
input = inputs.get(i);
if (!input.value) {
new_inputs.push(input);
}
}
return $(new_inputs);
}
function getCurrentFiles() {
inputs = $("#rapid-form input[type=file]");
let files = new Set();
for (let j = 0; j < inputs.length; j++) {
i = inputs.get(j);
if (i.files.length > 0) {
files.add(i.files[0]);
}
}
return files;
}
function extendInputs(n) {
// Makes sure we have n inputs available
// returns available inputs
inputs = getUnusedInputs($("#rapid-form input[type=file][id^=id_images-]"));
//fileInput = document.getElementById("id_images-0-image");
// Make sure we have enough input targets
input_diff = (n - inputs.length)
if (input_diff > 0) {
for (let j = 0; j < input_diff; j++) {
add_input_form()
}
// Need to make sure we include the new ones...
inputs = getUnusedInputs($("#rapid-form input[type=file][id^=id_images-]"));
}
return inputs;
}
function add_input_form() {
var form_idx = $('#id_images-TOTAL_FORMS').val();
$('#image_form_set').append($($('#empty_form').html().replace(/__prefix__/g, form_idx)).on("change",
input_change));
$('#id_images-TOTAL_FORMS').val(parseInt(form_idx) + 1);
}
function add_answers_input_form() {
var form_idx = $('#id_answers-TOTAL_FORMS').val();
@@ -125,62 +55,6 @@
$('#id_answers-TOTAL_FORMS').val(parseInt(form_idx) + 1);
}
function input_change(evt) {
file = evt.target.files[0];
$(evt.target).removeClass("image-ident-warning");
$(evt.target).removeClass("image-ident-ok");
el = evt.target;
//readFileAndProcess();
readFileAndProcess2(el);
// Check if we have selected a examination type
if (!$("#id_examination_to option").length) {
// Yes we do read the file twice (here and where loading it in the viewer)
var reader = new FileReader();
reader.onload = function (file) {
var arrayBuffer = reader.result;
// Here we have the file data as an ArrayBuffer. dicomParser requires as input a
// Uint8Array so we create that here
var byteArray = new Uint8Array(arrayBuffer);
extractDicomStudyDescription(byteArray)
}
reader.readAsArrayBuffer(file);
}
}
function addFile(f, feedback) {
let dT = new DataTransfer();
dT.clearData();
dT.items.add(f);
for (let i = 0; i < inputs.length; i++) {
el = inputs.get(i);
if (el.files.length == 0) {
el.files = dT.files;
//ocr(el)
if (feedback) {
arr = el.name.split("-");
arr.splice(2, 1, "feedback_image");
feedback_el_name = arr.join("-");
$(`[name=${feedback_el_name}]`).prop("checked", true);
}
$(el).change();
return false;
}
}
}
$(document).ready(function () {
@@ -223,7 +97,7 @@
}
// Get all input elements
inputs = extendInputs(file_drop_number);
inputs = extendInputs("rapid-form", file_drop_number);
console.log("drop inputs", inputs, evt);
@@ -250,7 +124,7 @@
// listAllFilesAndDirs(monitor_directory_handler).then(async function (
// items) {
// files_to_add = new Set();
// current_files = getCurrentFiles();
// current_files = getCurrentFormFiles();
// current_files_tuple = new Set()
// for (let elem of current_files) {
// current_files_tuple.add(
@@ -368,321 +242,83 @@
});
function ocr2(url, input) {
console.log("OCR", url, input)
Tesseract.recognize(
url,
'eng',
{ logger: m => console.log(m) }
).then(({ data: { text } }) => {
l = text.toLowerCase();
uploading_el = $(`img[data-input-id='${input.id}'], div[data-input-id='${input.id}']`)
uploading_el.removeClass("image-ident-loading");
console.log("found text", l);
if (l.includes("accesion") || l.includes("number") || l.search(
/(ref|rk9|rh8|ra9|rbz)\d+/g) > -1) {
console.log("Match found ", input);
$(input).addClass("image-ident-warning");
uploading_el.addClass("image-ident-warning");
} else {
$(input).addClass("image-ident-ok");
}
})
}
async function blobToBase64(blob) {
return new Promise((resolve, _) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.readAsDataURL(blob);
});
}
// async function readFileAndProcess(callback) {
// //$("#single-dicom-viewer").empty()
// //images = []
// //Function that returns a promise to read the file
// const reader = (file) => {
// return new Promise((resolve, reject) => {
// const fileReader = new FileReader();
// fileReader.onload = () => resolve(fileReader);
// fileReader.readAsDataURL(file);
// });
// }
//
// // Add file to the process list
// active_file_inputs.add(el)
// const readFile = (file, el) => {
// reader(file).then(reader => {
// console.log("12345", reader)
// $(el).parent().parent().find(".temp-thumb").remove();
//
// if (reader.result.startsWith("data:application/octet-stream;base64")) {
// const element = $(`<div class='temp-thumb' src=${reader.result}></div>`).get(0)
// $(el).parent().parent().prepend(element);
// const imageId = cornerstoneWADOImageLoader.wadouri.fileManager.add(
// file
// );
// cornerstone.enable(element);
// cornerstone.loadAndCacheImage(imageId).then(function (image) {
// cornerstone.displayImage(element, image);
// cornerstone.resize(element)
//
// });
//
// } else {
// var image = new Image();
// image.title = file.name;
// image.src = reader.result;
// image.className = "temp-thumb";
// $(el).parent().parent().prepend(image);
// //images.push(reader.result);
// }
//
// active_file_inputs.delete(el)
// console.log("active", active_file_inputs)
// // Only load once all queued files have been processed
// if (active_file_inputs.size < 1) {
// loadViewer();
// }
// });
// }
//
// file_set = $("#image_form_set input[type=file]");
//
// file_set.each(async (n, el) => {
// if (el.files.length > 0) {
// filename = el.files[0].name;
// // Probably no need to await here anymore
// await readFile(el.files[0], el);
// }
// })
// }
async function readFileAndProcess2(el) {
//file_set = $("#image_form_set input[type=file]");
//file_set.each(async (n, el) => {
if (el.files.length > 0) {
filename = el.files[0].name;
// Probably no need to await here anymore
//await readFile(el.files[0], el);
// const viewLoadChecker = setInterval(function () {
// if (load_viewer) {
// load_viewer = false;
// loadViewer();
// }
// }, 2000);
console.log(el.files[0])
function extractDicomDetails(byteArray) {
if (!$("#id_examination_to option").length) {
$(el).parent().parent().find(".temp-thumb").remove();
let url = URL.createObjectURL(el.files[0])
n = el.id.split("-")[1];
$(`#drop-filenames span[data-input-id='${el.id}']`).remove();
let extra_class = " image-ident-loading";
if ($(el).hasClass("image-ident-warning")) {
extra_class = " image-ident-warning";
} else if ($(el).hasClass("image-ident-ok")) {
extra_class = " image-ident-ok";
}
//let extra_class = "";
let imageId;
if (el.files[0].type.startsWith("image")) {
let hash = CryptoJS.MD5(el.files[0]).toString();
console.log("HASH", hash)
base = await blobToBase64(el.files[0]);
imageId = "base64://" + base.split(",")[1];
imageId2 = base;
const element = $(`<img class='temp-thumb' src=${imageId}></div>`).get(0)
$(el).parent().parent().prepend(element);
ocr2(url, el);
checkHash(hash, el);
$("#drop-filenames").append(
`<span data-input-id='${el.id}'><span>${n}: ${el.files[0].name}</span><img class='uploading${extra_class}' data-input-id='${el.id}' src=${url}></span>`
)
} else {
imageId = `wadouri:${url}`;
imageId2 = imageId
console.log(imageId)
const element = $(`<div class='temp-thumb' src=${imageId2}></div>`).get(0)
$(el).parent().parent().prepend(element);
cornerstone.enable(element);
cornerstone.loadAndCacheImage(imageId).then(function (image) {
cornerstone.displayImage(element, image);
cornerstone.resize(element)
let pixel_data = image.getPixelData().toString()
console.log(element, "pixel data", pixel_data)
let hash = CryptoJS.MD5(pixel_data).toString();
console.log(element, "HASH", hash)
checkHash(hash, el);
});
$("#drop-filenames").append(
`<span data-input-id='${el.id}'><span>${n}: ${el.files[0].name}</span><div class='uploading${extra_class}' data-input-id='${el.id}' src=${url}></div></span>`
)
const element2 = $(`#drop-filenames div[data-input-id='${el.id}']`).get(0);
cornerstone.enable(element2);
cornerstone.loadAndCacheImage(imageId).then(function (image) {
cornerstone.displayImage(element2, image);
$(element2).addClass("temp-thumb-large");
cornerstone.resize(element2)
setTimeout(function () {
ocr2($(element2).find("canvas").get(0).toDataURL(), el);
$(element2).removeClass("temp-thumb-large");
cornerstone.resize(element2)
}, 500);
});
}
active_file_inputs.delete(el)
console.log("active", active_file_inputs)
// Only load once all queued files have been processed
if (active_file_inputs.size < 1) {
load_viewer = true;
//loadViewer();
}
}
//})
}
async function readFileAndProcess(callback) {
//$("#single-dicom-viewer").empty()
//images = []
//Function that returns a promise to read the file
const reader = (file) => {
return new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onload = () => resolve(fileReader);
fileReader.readAsDataURL(file);
});
}
// Add file to the process list
active_file_inputs.add(el)
const readFile = (file, el) => {
reader(file).then(reader => {
console.log("12345", reader)
$(el).parent().parent().find(".temp-thumb").remove();
if (reader.result.startsWith("data:application/octet-stream;base64")) {
const element = $(`<div class='temp-thumb' src=${reader.result}></div>`).get(0)
$(el).parent().parent().prepend(element);
const imageId = cornerstoneWADOImageLoader.wadouri.fileManager.add(
file
);
cornerstone.enable(element);
cornerstone.loadAndCacheImage(imageId).then(function (image) {
cornerstone.displayImage(element, image);
cornerstone.resize(element)
});
} else {
var image = new Image();
image.title = file.name;
image.src = reader.result;
image.className = "temp-thumb";
$(el).parent().parent().prepend(image);
//images.push(reader.result);
}
active_file_inputs.delete(el)
console.log("active", active_file_inputs)
// Only load once all queued files have been processed
if (active_file_inputs.size < 1) {
loadViewer();
}
});
}
file_set = $("#image_form_set input[type=file]");
file_set.each(async (n, el) => {
if (el.files.length > 0) {
filename = el.files[0].name;
// Probably no need to await here anymore
await readFile(el.files[0], el);
}
})
}
const viewLoadChecker = setInterval(function () {
if (load_viewer) {
load_viewer = false;
loadViewer();
}
}, 2000);
function loadViewer() {
console.log("LOAD VIEWER")
// temp fix before starting viewer collapsed
$("#single-dicom-viewer").show()
//file_set = $("#image_form_set input[type=file]");
//n = 0;
//for (let i = 0; i < file_set.length; i++) {
// el = file_set.get(i)
// if (el.files.length > 0) {
// n++;
// }
//}
image_set = $("#image_form_set .temp-thumb").get()//.map(function() { return $(this).attr("src")}).get();
console.log(image_set);
//console.log()
//if (n == image_set.length) {
const event = new CustomEvent('loadDicomViewer', {
"detail": image_set
});
window.dispatchEvent(event);
sortable('.sortable');
//sortable('.sortable')[0].addEventListener('sortstop', function(e) {
updateImagePositions();
//}
}
function updateImagePositions() {
console.log("UP pos");
file_set = $("#image_form_set ul");
n = 0;
for (let i = 0; i < file_set.length; i++) {
ul = file_set.get(i);
console.log(ul)
file_element = $(ul).find("input[type=file]").get(0);
// If a (new) file is being uploaded we save it's name prior to it being hashed
if (file_element.files.length > 0) {
file_name = file_element.files[0].name
//$(ul).find("input[type=text]").val(file_name);
$(ul).find("input[name*='filename']").val(file_name);
}
if (file_element.files.length > 0 || $(ul).find(":contains('Currently:')")) {
n++;
$(ul).find("input[type=number]").val(n);
}
}
}
function checkHash(hash, input) {
$.ajax({
url: "{% url 'rapids:rapid_question_by_hash' %}",
data: {
csrfmiddlewaretoken: '{{ csrf_token }}',
hash: hash
},
type: "POST",
dataType: "json",
})
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
.done(function (data) {
console.log(data);
if (data.status == "success") {
console.log("success");
console.log(data.id)
if (data.id) {
//toastr.info(`id ${data.id}`)
uploading_el = $(`img[data-input-id='${input.id}'], div[data-input-id='${input.id}']`)
$(input).addClass("image-duplicate");
uploading_el.addClass("image-duplicate");
toastr.warning(`Image (${input.files[0].name}) already exists.<br /><a href='${data.url}'>Click to view</a><br /><button type="button" class="btn clear">Dismiss</button>`, "Duplicate image", {
"closeButton": false,
"debug": false,
"newestOnTop": false,
"progressBar": false,
"positionClass": "toast-top-full-width",
"preventDuplicates": false,
"onclick": null,
"showDuration": "0",
"hideDuration": "0",
"timeOut": 0,
"extendedTimeOut": 0,
"showEasing": "swing",
"hideEasing": "linear",
"showMethod": "fadeIn",
"hideMethod": "fadeOut",
"tapToDismiss": false
})
}
}
})
.always(function () {
console.log('[Done]');
})
}
function extractDicomStudyDescription(byteArray) {
// We need to setup a try/catch block because parseDicom will throw an exception
// if you attempt to parse a non dicom part 10 file (or one that is corrupted)
try {
@@ -717,6 +353,7 @@
} catch (err) {
console.log(err);
}
}
}
</script>
+1 -1
View File
@@ -179,4 +179,4 @@ def test_index(client, create_superuser, exam_views, django_user_model):
print(json_response)
assert len(json_response) == 2
assert len(json_response) == 2