diff --git a/anatomy/tests/test_exams.py b/anatomy/tests/test_exams.py index 5da38f73..480fbb36 100644 --- a/anatomy/tests/test_exams.py +++ b/anatomy/tests/test_exams.py @@ -73,7 +73,6 @@ def create_question(exam): dir=settings.MEDIA_ROOT, suffix=".jpg", delete=False ) - question: Question = Question.objects.create( question_type=QuestionType.objects.first(), structure=Structure.objects.first(), @@ -162,14 +161,13 @@ def test_exams(db, client): "eid": f"anatomy/{exam.pk}", "cid": cid_user_1001.cid, "start_time": "1659618141.731", - "answers": [[]] + "answers": [[]], } # Test submission # Start with 0 answers res = client.post(reverse("global_exam_answers_submit"), data=post_json) - print(res.status_code) json_res = json.loads(res.content) assert json_res["success"] == True @@ -179,10 +177,63 @@ def test_exams(db, client): "eid": f"anatomy/{exam.pk+10}", "cid": cid_user_1001.cid, "start_time": "1659618141.731", - "answers": [[]] + "answers": [[]], } res = client.post(reverse("global_exam_answers_submit"), data=post_json) json_res = json.loads(res.content) assert json_res["success"] == False - # Test that we ca + # Test invalid cid + post_json = { + "eid": f"anatomy/{exam.pk+10}", + "cid": 1, + "start_time": "1659618141.731", + "answers": [[]], + } + res = client.post(reverse("global_exam_answers_submit"), data=post_json) + json_res = json.loads(res.content) + assert json_res["success"] == False + assert json_res["error"] == "Invalid data" + + valid_answers = [] + + for questions in exam_json["questions"]: + for qid in questions: + valid_answers.append( + { + "eid": exam_json["eid"], + "cid": cid_user_1001.cid, + "passcode": cid_user_1001.passcode, + "qid": qid, + "qidn": "1", + "ans": f"answer {qid}", + } + ) + + post_json = { + "eid": exam_json["eid"], + "cid": cid_user_1001.cid, + "start_time": "1659618141.731", + "answers": json.dumps(valid_answers), + } + + # Test submission + # give three answers + print("post_json", post_json) + res = client.post(reverse("global_exam_answers_submit"), data=post_json) + + json_res = json.loads(res.content) + print(json_res) + + assert json_res["success"] == True + assert json_res["question_count"] == 3 + + # We can then check the answers have been submitted (and can be viewed by the user) + cid_scores_res = client.get( + reverse(f"cid_scores", kwargs={"cid": 1001, "passcode": "EFGH"}) + ) + + cid_scores_soup = BeautifulSoup(cid_scores_res.content, "html.parser") + assigned_exams = cid_scores_soup.find(id="exam-assigned").find_all("li") + print(assigned_exams) + diff --git a/generic/views.py b/generic/views.py index 42c496a9..00de22b5 100644 --- a/generic/views.py +++ b/generic/views.py @@ -2470,3 +2470,7 @@ class ExamDeleteBase(RevisionMixin, DeleteView): def get_success_url(self) -> str: # return super().get_success_url()(self): return reverse_lazy(f"{self.model.app_name}:index") + + +def exam_inactive(request, context): + return render(request, "exam_inactive.html", context) \ No newline at end of file diff --git a/physics/views.py b/physics/views.py index bf6262b2..4716a105 100644 --- a/physics/views.py +++ b/physics/views.py @@ -50,6 +50,7 @@ from generic.views import ( ExamUpdateBase, ExamViews, GenericViewBase, + exam_inactive, ) from rest_framework import viewsets, permissions @@ -168,7 +169,7 @@ def exam_take_old(request, pk): exam = get_object_or_404(Exam, pk=pk) if not exam.active: - raise Http404("Exam not found") + raise exam_inactive(request) questions = exam.exam_questions.all() @@ -186,7 +187,7 @@ def exam_start(request, pk): exam = get_object_or_404(Exam, pk=pk) if not exam.active: - raise Http404("Exam not found") + return exam_inactive(request, context={"exam": exam}) return render( request, @@ -200,12 +201,13 @@ def exam_start(request, pk): def exam_take_overview(request, pk, cid=None, passcode=None): exam = get_object_or_404(Exam, pk=pk) - if not exam.active: - raise Http404("Exam not found") - if cid is not None and not exam.check_cid_user(cid, passcode): raise Http404("Error accessing exam") + if not exam.active: + return exam_inactive(request, context={"exam": exam}) + + questions = exam.exam_questions.all() if cid is not None: @@ -253,7 +255,7 @@ def exam_take(request, pk, sk, cid=None, passcode=None): exam = get_object_or_404(Exam, pk=pk) if not exam.active: - raise Http404("Exam not found") + return exam_inactive(request, context={"exam": exam}) if cid is not None and not exam.check_cid_user(cid, passcode): raise Http404("Error accessing exam") diff --git a/rad/urls.py b/rad/urls.py index 962f2708..aedb1169 100644 --- a/rad/urls.py +++ b/rad/urls.py @@ -77,7 +77,7 @@ urlpatterns = [ path("accounts/profile//", views.account_profile, name="account_profile"), path("", TemplateView.as_view(template_name="index.html"), name="home"), path("cid/results//", views.cid_results, name="cid_results"), - path("cid//", views.cid_scores, name="cid_scores"), + path("cid//", views.cid_scores, name="cid_scores"), path("cid//", views.cid_scores_admin, name="cid_scores_admin"), path("user/scores", views.user_scores, name="user_scores"), path("cid/", views.cid_selector, name="cid_selector"), diff --git a/rad/views.py b/rad/views.py index cd6a04c2..95e996a6 100644 --- a/rad/views.py +++ b/rad/views.py @@ -186,11 +186,9 @@ def user_scores(request): }, ) -def cid_scores(request, pk, passcode): +def cid_scores(request, cid, passcode): # exam = get_object_or_404(Exam, pk=pk) - cid = pk - cid_user = CidUser.objects.filter(cid=cid).first() print(cid_user) if not cid_user or cid_user.passcode != passcode: @@ -278,6 +276,7 @@ def active_exams_unbased(request, cid=None, passcode=None): @csrf_exempt def exam_submit(request): + print(request) if request.is_ajax and request.method == "POST": print(request.POST) exam_type, exam_id = request.POST.get("eid").split("/") @@ -478,7 +477,7 @@ def view_feedback(request): # HTTP Error 400 def page_not_found(request, exception): - response = render(request, "404.html", {}) + response = render(request, "404.html") response.status_code = 404 @@ -486,7 +485,7 @@ def page_not_found(request, exception): def page_forbidden(request, exception): - response = render(request, "403.html", {}) + response = render(request, "403.html") response.status_code = 403 @@ -494,7 +493,7 @@ def page_forbidden(request, exception): def server_error(request): - response = render(request, "500.html", {}) + response = render(request, "500.html") response.status_code = 500 diff --git a/static/css/anatomy.css b/static/css/anatomy.css index 8b96e14c..b8f7971c 100644 --- a/static/css/anatomy.css +++ b/static/css/anatomy.css @@ -280,6 +280,12 @@ button a { border: 1px dashed white; } +.series-drop { + position: sticky; + height: 600px; + bottom: -500px; +} + .submit-button { position: sticky; bottom: 0; @@ -837,4 +843,19 @@ summary h5 { bottom: 0px; background-color: black; opacity: 80%; +} + +.small-url-button { + padding: 0px; +} + +button a, button a:link, button a:visited, button a:hover { + text-decoration: none; + color: inherit; +} + +details.filter { + border: 1px solid gray; + padding: 10px; + margin: 20px; } \ No newline at end of file diff --git a/static/django-htmx.js b/static/django-htmx.js new file mode 100644 index 00000000..aaf8eecc --- /dev/null +++ b/static/django-htmx.js @@ -0,0 +1,22 @@ +{ + const data = document.currentScript.dataset; + const isDebug = data.debug === "True"; + + if (isDebug) { + document.addEventListener("htmx:beforeOnLoad", function (event) { + const xhr = event.detail.xhr; + if (xhr.status == 500 || xhr.status == 404) { + // Tell htmx to stop processing this response + event.stopPropagation(); + + document.children[0].innerHTML = xhr.response; + + // Run Django’s inline script + // (1, eval) wtf - see https://stackoverflow.com/questions/9107240/1-evalthis-vs-evalthis-in-javascript + (1, eval)(document.scripts[0].innerText); + // Need to directly call Django’s onload function since browser won’t + window.onload(); + } + }); + } +} diff --git a/static/js/anatomy.js b/static/js/anatomy.js index ede0d47d..0493c7c2 100644 --- a/static/js/anatomy.js +++ b/static/js/anatomy.js @@ -579,4 +579,5 @@ function delete_multiple(url, csrf_token) { } } -window.delete_multiple = delete_multiple \ No newline at end of file +window.delete_multiple = delete_multiple + diff --git a/static/js/upload_form_helpers.js b/static/js/upload_form_helpers.js new file mode 100644 index 00000000..8fff2006 --- /dev/null +++ b/static/js/upload_form_helpers.js @@ -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('') + 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 = $(``).get(0) + $(el).parent().parent().prepend(element); + + + ocr2(url, el); + checkHash(hash, el); + + if ($("#drop-filenames").length > 0) { + $("#drop-filenames").append( + `${n}: ${el.files[0].name}` + ) + } + } else { + imageId = `wadouri:${url}`; + imageId2 = imageId + + const element = $(`
`).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( + `${n}: ${el.files[0].name}
` + ) + + 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.
Click to view
`, "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(); + //} + + } \ No newline at end of file diff --git a/templates/404.html b/templates/404.html index 8d6dd12f..754a63f9 100644 --- a/templates/404.html +++ b/templates/404.html @@ -3,6 +3,8 @@ {% block content %}

404 error

-Page not found.... +{{ reason }} + +

a {{resolved}}

{% endblock %} \ No newline at end of file diff --git a/templates/cid_scores.html b/templates/cid_scores.html index c4ad1a40..13e15755 100644 --- a/templates/cid_scores.html +++ b/templates/cid_scores.html @@ -4,43 +4,47 @@

CID: {{ cid }}

Assigned exams

-

The following exams will be available to take when active. +

+ The following exams will be available to take when active. {% for exam_type, exams in available_exams %}

{{exam_type}}

-
    + {% endfor %} {% if all_exams %} -

    Exam results

    -
    - The following exam results been found. Click to view answers (and scores when the results are published): - {% for exam_type, exams in all_exams %} - {% if exams %} -

    {{exam_type|title}}

    -
      - {% for exam in exams %} -
    • {{exam.name}} {% if exam.active %}[Active]{% endif %} {% if exam.publish_results %}[Results Published]{% endif %}
    • - {% endfor %} -
    - {% endif %} - {% endfor %}
    -{% endif %} +

    Exam results

    +
    + The following exam results been found. Click to view answers (and scores when the results are published): + {% for exam_type, exams in all_exams %} + {% if exams %} +

    {{exam_type|title}}

    +
      + {% for exam in exams %} +
    • {{exam.name}} {% if exam.active %}[Active]{% endif %} {% if exam.publish_results %}[Results Published]{% endif %}
    • + {% endfor %} +
    + {% endif %} + {% endfor %} +
    + {% endif %} {% if case_collections %} -
    -

    Case Collection

    - The following Case Collections have been found. - {% for collection in case_collections %} -
  • {{collection.name}} {% if collection.active %}[Active]{% endif %} {% if collection.publish_results %}[Results Published]{% endif %}
  • - - {% endfor %} +
    +

    Case Collection

    + The following Case Collections have been found. +
      + {% for collection in case_collections %} +
    • {{collection.name}} {% if collection.active %}[Active]{% endif %} {% if collection.publish_results %}[Results Published]{% endif %}
    • + + {% endfor %} +
    - {% endif %} + {% endif %}
    {% endblock %} diff --git a/templates/exam_inactive.html b/templates/exam_inactive.html new file mode 100644 index 00000000..29c1c23e --- /dev/null +++ b/templates/exam_inactive.html @@ -0,0 +1,8 @@ +{% extends 'base.html' %} + +{% block content %} +
    +

    {{exam.name}}

    +Exam is currently inactive. +
    +{% endblock %} \ No newline at end of file diff --git a/templates/registration/login.html b/templates/registration/login.html index c0ea7e61..a47c071d 100644 --- a/templates/registration/login.html +++ b/templates/registration/login.html @@ -13,4 +13,6 @@ {% if request.user.is_staff %}

    Change password

    {% endif %} + +

    This page is for user account logins. If you have a CID / Passcode click here {% endblock %}