This commit is contained in:
Ross
2020-10-15 22:54:10 +01:00
commit c923d52382
35 changed files with 28699 additions and 0 deletions
View File
+56
View File
@@ -0,0 +1,56 @@
from django.contrib import admin
from .models import (
AnatomyQuestion,
Examination,
QuestionType,
Answers,
HalfMarkAnswers,
IncorrectAnswers,
UserAnswer,
Exam,
Modality,
BodyPart,
Structure,
)
from reversion.admin import VersionAdmin
# Register your models here.
class AnatomyAnswersInline(admin.TabularInline):
model = Answers
extra = 1
class HalfMarkAnatomyAnswersInline(admin.TabularInline):
model = HalfMarkAnswers
extra = 1
class IncorrectAnatomyAnswersInline(admin.TabularInline):
model = IncorrectAnswers
extra = 1
class ExamInline(admin.TabularInline):
model = AnatomyQuestion.exams.through
extra = 1
class AnatomyAdmin(VersionAdmin):
inlines = [
AnatomyAnswersInline,
HalfMarkAnatomyAnswersInline,
IncorrectAnatomyAnswersInline,
ExamInline,
]
admin.site.register(AnatomyQuestion, AnatomyAdmin)
admin.site.register(Examination)
admin.site.register(QuestionType)
admin.site.register(UserAnswer)
admin.site.register(Exam)
admin.site.register(Modality)
admin.site.register(BodyPart)
admin.site.register(Structure)
+5
View File
@@ -0,0 +1,5 @@
from django.apps import AppConfig
class AnatomyConfig(AppConfig):
name = 'anatomy'
+16
View File
@@ -0,0 +1,16 @@
from django import forms
from .models import UserAnswer, AnatomyQuestion
class AnatomyAnswerForm(forms.ModelForm):
class Meta:
model = UserAnswer
fields = ("answer",)
class MarkAnatomyQuestionForm(forms.Form):
# correct = forms.CharField(required=False)
# half_correct = forms.CharField(required=False)
# incorrect = forms.CharField(required=False)
marked_answers = forms.CharField(required=False)
+242
View File
@@ -0,0 +1,242 @@
from django.db import models
from django.utils import timezone
from django.core.files.storage import FileSystemStorage
from django.conf import settings
image_storage = FileSystemStorage(
# Physical file location ROOT
location=u"{0}/anatomy/".format(settings.MEDIA_ROOT),
# Url for file
base_url=u"{0}anatomy/".format(settings.MEDIA_URL),
)
def image_directory_path(instance, filename):
# file will be uploaded to MEDIA_ROOT/anatomy/picture/<filename>
return u"picture/{0}".format(filename)
class Examination(models.Model):
examination = models.CharField(max_length=200)
def __str__(self):
return self.examination
class BodyPart(models.Model):
bodypart = models.CharField(max_length=200)
def __str__(self):
return self.bodypart
class Structure(models.Model):
bodypart = models.CharField(max_length=200)
def __str__(self):
return self.bodypart
class Modality(models.Model):
modality = models.CharField(max_length=200)
def __str__(self):
return self.modality
class QuestionType(models.Model):
text = models.CharField(max_length=400)
def __str__(self):
return self.text
class AnatomyQuestion(models.Model):
# author = models.ForeignKey('auth.User', on_delete=models.CASCADE)
# image = models.ImageField()
# feedback = models.TextField(null=True)
question_type = models.ForeignKey(QuestionType,
on_delete=models.SET_NULL,
null=True)
image = models.ImageField(upload_to=image_directory_path,
storage=image_storage)
description = models.CharField(
max_length=400,
help_text=
"Short description of the image e.g. 'Sagittal CT Chest, Abdomen & Pelvis', will be displayed as the title."
)
examination = models.ForeignKey(Examination,
on_delete=models.SET_NULL,
null=True, blank=True)
modality = models.ForeignKey(Modality,
on_delete=models.SET_NULL,
null=True)
body_part = models.ForeignKey(BodyPart,
on_delete=models.SET_NULL,
null=True,
blank=True)
structure = models.ForeignKey(Structure,
on_delete=models.SET_NULL,
null=True,
blank=True)
created_date = models.DateTimeField(default=timezone.now)
def __str__(self):
# Get first answer
a = self.answers.all().first()
# Get associated exams
e = self.exams.all().values_list("name", flat=True)
exams = ", ".join(e)
if a is None:
return "({})".format(exams)
else:
return "{} ({}) [{}, {}]".format(a.answer, exams, self.modality,
self.structure)
def GetUnmarkedAnswersString(self):
unmarked_answers = self.GetUnmarkedAnswers()
if not unmarked_answers:
return "No answers to mark"
return "{} answer unmarked: {}".format(len(unmarked_answers),
", ".join(unmarked_answers))
def GetUnmarkedAnswers(self):
user_answers = set([i.answer for i in self.user_answers.all()])
unmarked_answers = user_answers - self.GetMarkedAnswers()
return unmarked_answers
def GetMarkedAnswers(self):
correct_answers = set([i.answer for i in self.answers.all()])
half_mark_answers = set(
[i.answer for i in self.half_mark_answers.all()])
incorrect_answers = set(
[i.answer for i in self.incorrect_answers.all()])
marked_answers = correct_answers | half_mark_answers | incorrect_answers
return marked_answers
class Answers(models.Model):
question = models.ForeignKey(AnatomyQuestion,
related_name="answers",
on_delete=models.CASCADE)
answer = models.CharField(max_length=500)
def __str__(self):
return self.answer
def clean(self):
if self.answer:
self.answer.strip()
class HalfMarkAnswers(models.Model):
question = models.ForeignKey(AnatomyQuestion,
related_name="half_mark_answers",
on_delete=models.CASCADE)
answer = models.CharField(max_length=500)
def __str__(self):
return self.answer
def clean(self):
if self.answer:
self.answer.strip()
class IncorrectAnswers(models.Model):
question = models.ForeignKey(AnatomyQuestion,
related_name="incorrect_answers",
on_delete=models.CASCADE)
answer = models.CharField(max_length=500)
def __str__(self):
return self.answer
def clean(self):
if self.answer:
self.answer.strip()
class Exam(models.Model):
name = models.CharField(max_length=200)
exam_questions = models.ManyToManyField(AnatomyQuestion,
related_name="exams")
def __str__(self):
return self.name
class UserAnswer(models.Model):
question = models.ForeignKey(AnatomyQuestion,
related_name="user_answers",
on_delete=models.CASCADE)
answer = models.CharField(max_length=500, blank=True)
user = models.ForeignKey(settings.AUTH_USER_MODEL,
null=True,
blank=True,
on_delete=models.SET_NULL)
cid = models.IntegerField(blank=True, null=True)
# Each user answer is associated with a particular exam
exam = models.ForeignKey(Exam,
related_name="exam",
on_delete=models.CASCADE,
null=True)
flagged = models.BooleanField(default=False)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
def __str__(self):
try:
exam = self.exam
except (Exam.DoesNotExist, KeyError) as e:
exam = "None"
return "{}/{}/{}: {}".format(exam, self.cid, self.question.id,
self.answer)
def clean(self):
if self.answer:
self.answer.strip()
class CidUserAnswer(models.Model):
"""User answers by candidate"""
question = models.ForeignKey(AnatomyQuestion,
related_name="cid_user_answers",
on_delete=models.CASCADE)
answer = models.CharField(max_length=500, blank=True)
cid = models.IntegerField(blank=True, null=True)
# Each user answer is associated with a particular exam
exam = models.ForeignKey(Exam,
related_name="cid_user_answers",
on_delete=models.CASCADE,
null=True)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
def __str__(self):
try:
exam = self.exam
except (Exam.DoesNotExist, KeyError) as e:
exam = "None"
return "{}/{}/{}: {}".format(exam, self.cid, self.question.id,
self.answer)
def clean(self):
if self.answer:
self.answer.strip()
+96
View File
@@ -0,0 +1,96 @@
body {
color: gray;
background-color: black;
}
.answer-list {
font-size: 30px;
}
.answer-list .correct {
color: green;
}
.answer-list .half-correct {
color: yellow;
}
.answer-list .incorrect {
color: red;
}
.answer-list .not-marked {
color: lightblue;
}
.key {
font-size: 20;
}
.hide {
display: none;
}
.answered {
color: green;
}
.unanswered {
color: gray;
}
#question-list {
list-style: none;
float: right;
padding: 1px;
border: 1px solid gray;
/*max-height: 80%;*/
height: 100%;
overflow-y: scroll;
}
.flagged {
color: red;
}
.not-flagged {
}
button a {
text-decoration: none;
color: green;
}
#admin-link {
float: right;
}
.marking-list {
float: right;
}
.save {
display: inline-block;
padding: 0.35em 1.2em;
border: 0.1em solid #ffffff;
margin: 0 0.3em 0.3em 0;
border-radius: 0.12em;
box-sizing: border-box;
text-decoration: none;
font-family: "Roboto", sans-serif;
font-weight: 300;
color: #ffffff;
text-align: center;
transition: all 0.2s;
background-color: transparent;
}
.save:hover {
color: #000000;
background-color: #ffffff;
}
#stats-plot {
width: 50%;
float: right;
}
+126
View File
@@ -0,0 +1,126 @@
var dwvapp = [];
var marked_answers = {
"correct": [],
"half-correct": [],
"incorrect": [],
}
$(document).ready(function () {
$(".answer-list li").each(function (index, element) {
console.log(element);
$(element).click(function (e) {
var classes = ['correct', 'half-correct', 'incorrect'];
$(element).each(function () {
this.className = classes[($.inArray(this.className, classes) + 1) % classes.length];
});
prepAnswerData();
});
});
prepAnswerData();
if($(".post-form").length > 0) {
$(".post-form").get(0).addEventListener("submit", function (e) {
if($(".not-marked").length > 0) {
e.preventDefault(); // before the code
alert("Ensure all answers are marked first");
}
});
}
if($(".dwv-container").length) {
$(".dwv-container").append($('<!-- DWV --> <div id="dwv"> <!-- Toolbar --> <div class="toolbar"></div> <input type="range" id="sliceRange" value="0"><!-- Layer Container --> <div class="layerContainer"> <div class="dropBox"></div> <canvas class="imageLayer">Only for HTML5 compatible browsers...</canvas> <div class="infoLayer"> <div class="infotl"></div> <div class="infotc"></div> <div class="infotr"></div> <div class="infocl"></div> <div class="infocr"></div> <div class="infobl"></div> <div class="infobc"></div> <div class="infobr" style="bottom: 64px;"></div></div></div><!-- /layerContainer --> <!-- /dwv -->'));
var app = new dwv.App();
dwvapp = app;
//DEBUG
var listenerWL = function (event) {
console.log("event: " + event.type);
console.log(event);
$(".infotc").text(event.wc);
};
app.addEventListener("wl-width-change", listenerWL);
//app.addEventListener("wl-center-change", listener);
app.init({
"containerDivId": "dwv",
"fitToWindow": true,
"isMobile": true,
"gui": ["tool"],
"filters": ["Threshold", "Sharpen", "Sobel"],
"tools": ["Scroll", "WindowLevel", "ZoomAndPan"], // or try "ZoomAndPan"
});
var range = document.getElementById("sliceRange");
range.min = 0;
app.addEventListener("load-end", function () {
range.max = app.getImage().getGeometry().getSize().getNumberOfSlices() - 1;
if(range.max == 0) {
$(range).hide();
}
});
app.addEventListener("slice-change", function () {
range.value = app.getViewController().getCurrentPosition().k;
});
range.oninput = function () {
var pos = app.getViewController().getCurrentPosition();
pos.k = this.value;
app.getViewController().setCurrentPosition(pos);
}
try {
app.loadURLs([$(".dwv-container").data("url")]);
} catch(error) {
toastr.error(error.message);
}
dwv.gui.appendResetHtml(app);
}
});
function prepAnswerData() {
//$("#id_correct").val($("li.correct").map(function() {
// ans = $(this).text();
// window.marked_answers["correct"].push(ans);
// return ans
//}).get().join('--//--'));
//$("#id_half_correct").val($("li.half-correct").map(function() {
// ans = $(this).text();
// window.marked_answers["half-correct"].push(ans);
// return ans;
//}).get().join('--//--'));
//$("#id_incorrect").val($("li.incorrect").map(function() {
// ans = $(this).text();
// window.marked_answers["incorrect"].push(ans);
// return ans;
//}).get().join('--//--'));
window.marked_answers["correct"] = [];
window.marked_answers["half-correct"] = [];
window.marked_answers["incorrect"] = [];
$("li.correct").map(function () {
ans = $(this).text();
window.marked_answers["correct"].push(ans);
})
$("li.half-correct").map(function () {
ans = $(this).text();
window.marked_answers["half-correct"].push(ans);
})
$("li.incorrect").map(function () {
ans = $(this).text();
window.marked_answers["incorrect"].push(ans);
})
$("#id_marked_answers").val(JSON.stringify(window.marked_answers));
}
+174
View File
@@ -0,0 +1,174 @@
/**
* Application GUI.
*/
// Default window level presets.
dwv.tool.defaultpresets = {
"default (127+/-255)": {"center": 127, "width": 255},
//"default": {"center": 127, "width": 100},
};
dwv.tool.defaultpresets.SC = {
"default (127+/-255)": {"center": 127, "width": 255},
};
// Default window level presets for CT.
dwv.tool.defaultpresets.CT = {
"mediastinum": {"center": 40, "width": 400},
"lung": {"center": -500, "width": 1500},
"bone": {"center": 500, "width": 2000},
};
// decode query
dwv.utils.decodeQuery = dwv.utils.base.decodeQuery;
// Window
dwv.gui.getWindowSize = function() {
d = dwv.gui.base.getWindowSize();
//return {"width": d["width"], "height": d["height"]+100}
h = $(window).height() - ($(document).height()-d["height"]) - 100;
return {"width": d["width"], "height": h}
}
// Progress
dwv.gui.displayProgress = function (/*percent*/) { /*does nothing*/ };
// get element
dwv.gui.getElement = dwv.gui.base.getElement;
// refresh
dwv.gui.refreshElement = dwv.gui.base.refreshElement;
// Slider
dwv.gui.Slider = null;
// Tags table
dwv.gui.DicomTags = null;
// Toolbox
dwv.gui.Toolbox = function (app)
{
this.setup = function (/*list*/)
{
// does nothing
};
this.display = function (/*bool*/)
{
// does nothing
};
this.initialise = function (list)
{
// not wonderful: first one should be scroll is more than one slice
if ( list[0] === false ) {
var inputScroll = app.getElement("scroll-button");
inputScroll.style.display = "none";
var inputZoom = app.getElement("zoom-button");
inputZoom.checked = true;
}
};
};
// Window/level
dwv.gui.WindowLevel = function (app)
{
this.setup = function ()
{
var button = document.createElement("button");
button.className = "wl-button";
button.value = "WindowLevel";
button.onclick = app.onChangeTool;
button.appendChild(document.createTextNode("Window/Level"));
var node = app.getElement("toolbar");
node.appendChild(button);
};
this.display = function (bool)
{
var button = app.getElement("wl-button");
button.disabled = bool;
};
this.initialise = function ()
{
// clear previous
dwv.html.removeNode(app.getElement("presetSelect"));
dwv.html.removeNode(app.getElement("presetLabel"));
// create preset select
var select = dwv.html.createHtmlSelect("presetSelect",
app.getViewController().getWindowLevelPresetsNames(), "wl.presets", true);
select.className = "presetSelect";
select.onchange = app.onChangeWindowLevelPreset;
select.title = "Select w/l preset.";
select.setAttribute("data-inline","true");
var label = document.createElement("label");
label.className = "presetLabel";
label.setAttribute("for", "presetSelect");
label.appendChild(document.createTextNode("Select Preset"));
var node = app.getElement("toolbar");
node.appendChild(label);
node.appendChild(select);
};
};
// Zoom
dwv.gui.ZoomAndPan = function (app)
{
this.setup = function ()
{
var button = document.createElement("button");
button.className = "zoom-button";
button.value = "ZoomAndPan";
button.onclick = app.onChangeTool;
button.appendChild(document.createTextNode("ZoomAndPan"));
var node = app.getElement("toolbar");
node.appendChild(button);
};
this.display = function (bool)
{
var button = app.getElement("zoom-button");
button.disabled = bool;
};
};
// Scroll
dwv.gui.Scroll = function (app)
{
this.setup = function ()
{
var button = document.createElement("button");
button.className = "scroll-button";
button.value = "Scroll";
button.onclick = app.onChangeTool;
button.appendChild(document.createTextNode("Scroll"));
var node = app.getElement("toolbar");
node.appendChild(button);
};
this.display = function (bool)
{
var button = app.getElement("scroll-button");
button.disabled = bool;
};
};
dwv.gui.Slider = dwv.gui.base.Slider;
// NEEDS UI
dwv.gui.Filter = dwv.gui.base.Filter;
// Filter: threshold
dwv.gui.Threshold = dwv.gui.base.Threshold;
// Filter: sharpen
dwv.gui.Sharpen = dwv.gui.base.Sharpen;
// Filter: sobel
dwv.gui.Sobel = dwv.gui.base.Sobel;
//Reset
dwv.gui.appendResetHtml = function (app)
{
var button = document.createElement("button");
button.className = "reset-button";
button.value = "reset";
button.onclick = app.onDisplayReset;
button.appendChild(document.createTextNode("Reset"));
var node = app.getElement("toolbar");
node.appendChild(button);
};
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+9
View File
@@ -0,0 +1,9 @@
{% extends 'blog/base.html' %}
{% block content %}
<h1>Answer</h1>
<form method="POST" class="post-form">{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="save btn btn-default">Save</button>
</form>
{% endblock %}
@@ -0,0 +1,13 @@
{% extends 'anatomy/base.html' %}
{% block content %}
<h1>Question</h1>
<p>{{ question.question_type.first }}</p>
<img src="/media/anatomy/{{ question.image|linebreaksbr }}" />
<form method="POST" class="post-form">{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="save btn btn-default" formaction="/test">Previous</button>
<button type="submit" class="save btn btn-default" formaction="/previous">Next</button>
</form>
{% endblock %}
+32
View File
@@ -0,0 +1,32 @@
{% load static %}
<html>
<head>
<title>Anatomy Quiz</title>
<link rel="stylesheet" href="{% static 'css/anatomy.css' %}">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="{% static 'js/anatomy.js' %}"></script>
<script type="text/javascript" src="{% static 'js/dwv/i18next.min.js' %}"></script>
<script type="text/javascript" src="{% static 'js/dwv/dwv.min.js' %}"></script>
<script type="text/javascript" src="{% static 'js/dwv/appgui.js' %}"></script>
</head>
<body>
<div class="page-header">
{% if request.user.is_staff %}<span id="admin-link"><a
href="{% url 'admin:index' %}">Admin</a></span>{% endif %}
</div>
<div class="content container">
{% block navigation %}
{% endblock %}
<div class="row">
<div class="col-md-8">
{% block content %}
{% endblock %}
</div>
</div>
</div>
</body>
</html>
+79
View File
@@ -0,0 +1,79 @@
{% extends 'anatomy/base.html' %}
{% block content %}
<script>
$(document).ready(function () {
$("#flagged-button").click(function () {
$.ajax({
url: "{% url 'anatomy:flag_question' %}",
data: {
'pk': {{ question.pk }}
},
dataType: 'json',
success: function (data) {
console.log(data.flagged);
if (data.flagged) {
$("#flagged-button").text("Flagged");
$("#flagged-button").attr("class", "flagged");
} else {
$("#flagged-button").text("Not Flagged");
$("#flagged-button").attr("class", "not-flagged");
}
}
});
});
$("#id_answer").keyup(function (event) {
if (event.keyCode == 13) {
$(".btn-default").click();
}
});
});
</script>
<h1>Question {{question_details.current}} of {{question_details.total}}</h1>
<p>{{ question.question_type.first }}</p>
<table>
<tr>
<td>
<div class=dwv-container data-url="{{ question.image.url}}" />
</div>
<form method="POST" class="post-form">{% csrf_token %}
{{ form.as_p }}
{% if question_details.current > 1 %}
<button type="submit" name="previous" class="btn btn-previous">Previous</button>
{% endif %}
{% if question_details.current >= question_details.total %}
<button type="submit" name="save" class="save btn btn-default">Save</button>
{% else %}
<button type="submit" name="next" class="save btn btn-default">Next</button>
{% endif %}
<button id=flagged-button type="button"
class={% if answer.flagged == True %}flagged{% else %}not-flagged{% endif %}>{% if answer.flagged == True %}Flagged{% else %}Not
Flagged{% endif %}</button>
</form>
</td>
<td>
<div class=exam-navigation />
<ul id=question-list>
{% for question in questions %}
<li><a class={% if question.pk in answered_questions %}answered{% else %}unanswered{% endif %}
href="{% url 'exam' pk=exam.pk sk=forloop.counter0 %}">{{ forloop.counter }}{% if question.pk in flagged_questions %}!{% endif %}</a>
</li>
{% endfor %}
</ul>
</div>
</td>
</tr>
</table>
{% endblock %}
+9
View File
@@ -0,0 +1,9 @@
{% extends 'anatomy/base.html' %}
{% block content %}
{% for exam in exams %}
<div class="anatomy">
<h1><a href="{% url 'anatomy:exam_overview' pk=exam.pk %}">{{exam.name}}</a></h1>
</div>
{% endfor %}
{% endblock %}
@@ -0,0 +1,9 @@
{% extends 'anatomy/base.html' %}
{% block content %}
<div class="anatomy">
<h1>{{ exam.name }}</h1>
This exam has {{question_number}} questions.
<p><button><a href="{% url 'anatomy:exam' pk=exam.pk sk=0 %}">Click here to start</a></button></p>
</div>
{% endblock %}
@@ -0,0 +1,40 @@
{% extends 'anatomy/base.html' %}
{% block content %}
<div class="anatomy">
<h2>{{ exam.name }}</h2>
{% for user, value in user_answers_marks.items %}
<h3>Candidate: {{ user_names|get_item:user }}</h3>
<!-- Answers: {{ user_answers_and_marks|get_item:user }}-->
Answers: {% for ans, score in user_answers_and_marks|get_item:user %}
{{ans}} ({{score}}),
{% endfor %}
<br /> Total mark: {{ user_scores|get_item:user }}
{% endfor %}
</div>
<div id="stats-block">
<h2>Stats</h2>
Mean: {{mean}}, Median {{median}}, Mode {{mode}}
<h3>Distribution of results</h3>
<div id="stats-plot">{{plot|safe}}</div>
</div>
<div>
<table>
<tr>
<th>Candidate ID</th>
<th>Score</th>
</tr>
{% for user, value in user_answers_marks.items %}
<tr>
<td>{{user}}</td>
<td>{{user_scores|get_item:user}}</td>
</tr>
{% endfor %}
</table>
</div>
{% endblock %}
{% block navigation %}
<a href="{% url 'anatomy:exam_list' %}">Exams</a> <a href="{% url 'anatomy:mark_overview' pk=exam.pk %}">Marking
overview</a> <a href="{% url 'anatomy:exam_scores_cid' pk=exam.pk %}">Scores</a>
{% endblock %}
+11
View File
@@ -0,0 +1,11 @@
{% extends 'anatomy/base.html' %}
{% block content %}
{% for exam in exams %}
<div class="anatomy">
<h1><a href="{% url 'anatomy:exam_overview' pk=exam.pk %}">Exam {{ forloop.counter }} </a></h1>
{% if request.user.is_staff %}<a href="{% url 'anatomy:mark' pk=exam.pk sk=0 %}">Mark answers</a>{% endif %}
{% if request.user.is_staff %}<a href="{% url 'anatomy:exam_scores_cid' pk=exam.pk %}">Scores</a>{% endif %}
</div>
{% endfor %}
{% endblock %}
+47
View File
@@ -0,0 +1,47 @@
{% extends 'anatomy/base.html' %}
{% block content %}
<h2>Marking question {{question_details.current}} of {{question_details.total}}</h2>
<p>{{ question.question_type }}</p>
<form method="POST" class="post-form">{% csrf_token %}
<div class="marking-list">Marked:
<ul id="marked-answer-list" class="answer-list">
{% for answer in question.answers.all %}
<li class="correct">{{ answer }} </li>
{% endfor %}
{% for answer in question.half_mark_answers.all %}
<li class="half-correct">{{ answer }} </li>
{% endfor %}
{% for answer in question.incorrect_answers.all %}
<li class="incorrect">{{ answer }} </li>
{% endfor %}
</ul>
Unmarked:
<ul id="new-answer-list" class="answer-list">
{% for answer in user_answers %}
<li class="not-marked">{{ answer }} </li>
{% endfor %}
</ul>
<div class="answer-list key">Key: <span class="correct">2 Marks</span>, <span class="half-correct">1
Mark</span>, <span class="incorrect">0 Marks</span></div>
{% if question_details.current > 1 %}
<button type="submit" name="previous" class="save btn btn-default">Previous</button>
{% endif %}
<button type="submit" name="save" class="save btn btn-default">Save</button>
{% if question_details.current >= question_details.total %}
{% else %}
<button type="submit" name="next" class="save btn btn-default">Next</button>
{% endif %}
</div>
<span class=hide>
{{ form.as_p }}
</span>
</form>
<div class=dwv-container data-url="/media/anatomy/{{ question.image}}" />
</div>
{% endblock %}
{% block navigation %}
<a href="{% url 'anatomy:exam_list' %}">Exams</a> <a href="{% url 'anatomy:mark_overview' pk=exam.pk %}">Marking
overview</a> <a href="{% url 'anatomy:exam_scores_cid' pk=exam.pk %}">Scores</a>
{% endblock %}
@@ -0,0 +1,20 @@
{% extends 'anatomy/base.html' %}
{% block content %}
<div class="anatomy">
<h2>{{ exam.name }}</h2>
<ul>
{% for question in questions.all %}
<li><a href="{% url 'anatomy:mark' pk=exam.pk sk=forloop.counter0 %}">Question {{forloop.counter }}:
{{ question }}</a><br /> {{ question.GetUnmarkedAnswersString }}</li>
{% endfor %}
</ul>
<p><button><a href="{% url 'anatomy:mark' pk=exam.pk sk=0 %}">Click here to start marking</a></button></p>
</div>
{% endblock %}
{% block navigation %}
<a href="{% url 'anatomy:exam_list' %}">Exams</a> <a href="{% url 'anatomy:mark_overview' pk=exam.pk %}">Marking
overview</a> <a href="{% url 'anatomy:exam_scores_cid' pk=exam.pk %}">Scores</a>
{% endblock %}
@@ -0,0 +1,14 @@
{% extends 'anatomy/base.html' %}
{% block content %}
<div class="question">
<div class="date">
{{ question.created_date }}
</div>
<h1>{% for answer in question.answers.all %}
{{ answer }},
{% endfor %}</h1>
<p>{{ question.question_type.first|linebreaksbr }}</p>
<img src="/media/anatomy/{{ question.image|linebreaksbr }}" />
</div>
{% endblock %}
@@ -0,0 +1,20 @@
{% extends 'anatomy/base.html' %}
{% block content %}
{% for question in questions %}
<div class="anatomy">
<div class="date">
{{ question.created_date }}
</div>
<h1><a href="{% url 'anatomy:question_detail' pk=question.pk %}">{% for answer in question.answers.all %}
{{ answer }},
{% endfor %}
</a></h1>
<p>{{ question.question_type.first|linebreaksbr }}</p>
<img src="/media/anatomy/{{ question.image|linebreaksbr }}" />
<a href="{% url 'anatomy:answer_question' pk=question.pk %}">ANSWER</a>
</div>
{% endfor %}
{% endblock %}
{$ black navigation %}
+12
View File
@@ -0,0 +1,12 @@
{% extends '../anatomy/base.html' %}
{% block title %}Login{% endblock %}
{% block content %}
<h2>Login</h2>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Login</button>
</form>
{% endblock %}
+3
View File
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
+20
View File
@@ -0,0 +1,20 @@
from django.urls import path, include
from . import views
app_name = "anatomy"
urlpatterns = [
# path('', views.question_list, name='question_list'),
path("", views.index, name="index"),
path("question/<int:pk>/", views.question_detail, name="question_detail"),
path("question/<int:pk>/answer/", views.answer_question, name="answer_question"),
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>/<int:sk>/", views.exam, name="exam"),
path("exam/<int:pk>/", views.exam_overview, name="exam_overview"),
path("exam/<int:pk>/scores", views.exam_scores_cid, name="exam_scores_cid"),
path("exam/<int:pk>/json", views.exam_json, name="exam_json"),
path("submit_answers", views.postExamAnswers, name="exam_answers_submit"),
path("exam/", views.exam_list, name="exam_list"),
path("ajax/exam/flag/", views.flag_question, name="flag_question"),
]
+530
View File
@@ -0,0 +1,530 @@
from django.shortcuts import render, get_object_or_404, redirect
from django.views.decorators.csrf import csrf_exempt
from django import forms
# from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required, user_passes_test
from django.contrib.auth.models import User
from django.http import Http404, JsonResponse
from .forms import AnatomyAnswerForm, MarkAnatomyQuestionForm
from .models import (
AnatomyQuestion,
CidUserAnswer,
Exam,
Answers,
HalfMarkAnswers,
IncorrectAnswers,
)
from collections import defaultdict
import os
import base64
import mimetypes
import json
import statistics
import plotly.express as px
def image_as_base64(image_file):
"""
:param `image_file` for the complete path of image.
:param `format` is format for image, eg: `png` or `jpg`.
"""
# if not os.path.isfile(image_file):
# return None
encoded_string = ""
# with open(image_file, 'rb') as img_f:
# encoded_string = base64.b64encode(img_f.read())
encoded_string = base64.b64encode(image_file.file.read())
mimetype, enc = mimetypes.guess_type(image_file.path)
return "data:image/{};base64,{}".format(mimetype, encoded_string.decode("utf-8"))
from django.template.defaulttags import register
@register.filter
def get_item(dictionary, key):
return dictionary.get(key)
def user_is_admin(user):
if user:
if user.pk == 1:
return True
return False
def question_list(request):
questions = AnatomyQuestion.objects.all()
return render(request, "anatomy/question_list.html", {"questions": questions})
@login_required
def index(request):
exams = Exam.objects.all()
return render(request, "anatomy/index.html", {"exams": exams})
@login_required
def question_detail(request, pk):
question = get_object_or_404(AnatomyQuestion, pk=pk)
return render(request, "anatomy/question_detail.html", {"question": question})
@login_required
def answer_question(request, pk):
question = get_object_or_404(AnatomyQuestion, pk=pk)
answer = question.user_answers.filter(
user=request.user
).first() # .filter(user=User)
if request.method == "POST":
if answer:
form = AnatomyAnswerForm(request.POST, instance=answer)
else:
form = AnatomyAnswerForm(request.POST)
if form.is_valid():
answer = form.save(commit=False)
answer.user = request.user
answer.question = question
# answer.published_date = timezone.now()
answer.save()
return redirect("question_detail", pk=pk)
else:
form = AnatomyAnswerForm(instance=answer)
return render(
request, "anatomy/answer_question.html", {"form": form, "question": question}
)
@login_required
def exam_list(request):
exams = Exam.objects.all()
return render(request, "anatomy/exam_list.html", {"exams": exams})
@login_required
def exam_overview(request, pk):
print(type(pk))
print(Exam.objects.all())
exams = Exam.objects.all()
print("test", Exam.objects.all().get(id=pk))
exam = get_object_or_404(Exam, pk=pk)
print("exam", exam)
questions = exam.exam_questions.all()
question_number = len(questions)
return render(
request,
"anatomy/exam_overview.html",
{"exam": exam, "questions": questions, "question_number": question_number},
)
@login_required
def exam(request, pk, sk):
exam = get_object_or_404(Exam, pk=pk)
questions = exam.exam_questions.all()
try:
question = questions[sk]
except IndexError:
raise Http404("Exam question does not exist")
n = sk
question_details = {
"total": len(questions),
"current": n + 1,
}
# Get data for flagged
# answered_questions = UserAnswer.objects.filter(user=request.user).values_list("question__pk", flat=True)
user_answer_data = UserAnswer.objects.filter(user=request.user).values_list(
"question__pk", "answer", "flagged"
)
# flagged_questions = UserAnswer.objects.filter(user=request.user, flagged=True).values_list("question__pk", flat=True)
# flagged_questions I= UserAnswer.filter(user=request.user, ).values_list("question__pk", flat=True)
answered_questions = set()
flagged_questions = set()
for ans_pk, answer, flagged in user_answer_data:
if len(answer.strip()) > 0:
answered_questions.add(ans_pk)
if flagged:
flagged_questions.add(ans_pk)
# u = question.user_answers
answer = question.user_answers.filter(
user=request.user
).first() # .filter(user=User)
if request.method == "POST":
if answer:
form = AnatomyAnswerForm(request.POST, instance=answer)
else:
form = AnatomyAnswerForm(request.POST)
if form.is_valid():
answer = form.save(commit=False)
answer.user = request.user
answer.question = question
# answer.published_date = timezone.now()
answer.save()
if "next" in request.POST:
return redirect("exam", pk=pk, sk=n + 1)
elif "previous" in request.POST:
return redirect("exam", pk=pk, sk=n - 1)
else:
form = AnatomyAnswerForm(instance=answer)
return render(
request,
"anatomy/exam.html",
{
"exam": exam,
"form": form,
"question": question,
"question_details": question_details,
"questions": questions,
"flagged_questions": flagged_questions,
"answered_questions": answered_questions,
"answer": answer,
},
)
def flag_question(request):
pk = request.GET.get("pk", None)
ans = UserAnswer.objects.filter(user=request.user, question__pk=pk).first()
ans.flagged = not ans.flagged
ans.save()
data = {"flagged": ans.flagged}
return JsonResponse(data)
@csrf_exempt
def postExamAnswers(request):
if request.is_ajax and request.method == "POST":
# horrible but it works
for k in request.POST.dict():
for q in json.loads(k):
# As access is not restricted make sure the data appears valid
if (not isinstance(q["cid"], int)) or (
not isinstance(q["eid"], int) or (not isinstance(q["ans"], str))
):
return JsonResponse({"success": False})
# The model should catch invalid data but this should be less intensive
max_int = 10000000
if q["cid"] > max_int or q["eid"] > max_int:
return JsonResponse({"success": False})
exiting_answers = CidUserAnswer.objects.filter(
question__id=q["qid"], exam__id=q["eid"], cid=q["cid"]
)
if not exiting_answers:
ans = UserAnswer(answer=q["ans"], cid=q["cid"])
ans.question_id = q["qid"]
ans.exam_id = q["eid"]
ans.full_clean()
ans.save()
else:
# Update an existing answer
# should never be more than one (famous last words)
ans = exiting_answers[0]
ans.answer = q["ans"]
ans.save()
# print(UserAnswer.objects.filter(exam__id=q["eid"]))
# print(request.urlencode())
return JsonResponse({"success": True})
# return render(request, "anatomy/exam.html", {"exam" : exam, "question" : question})
@login_required
@user_passes_test(user_is_admin, login_url="/accounts/login")
def mark_overview(request, pk):
exam = get_object_or_404(Exam, pk=pk)
questions = exam.exam_questions.all()
return render(
request, "anatomy/mark_overview.html", {"exam": exam, "questions": questions}
)
@login_required
@user_passes_test(user_is_admin, login_url="/accounts/login")
def mark(request, pk, sk):
exam = get_object_or_404(Exam, pk=pk)
questions = exam.exam_questions.all()
n = sk
question_details = {
"total": len(questions),
"current": n + 1,
}
try:
question = questions[sk]
except IndexError:
raise Http404("Exam question does not exist")
correct_answers = [i.answer for i in question.answers.all()]
half_correct_answers = [i.answer for i in question.half_mark_answers.all()]
incorrect_answers = [i.answer for i in question.incorrect_answers.all()]
user_answers = (
set([i.answer for i in question.user_answers.all()])
- set(correct_answers)
- set(half_correct_answers)
- set(incorrect_answers)
) # .filter(user=User)
user_answers = set([i for i in user_answers if i.strip() != ""])
if request.method == "POST":
form = MarkAnatomyQuestionForm(request.POST)
if form.is_valid():
cd = form.cleaned_data
# correct = cd.get("correct")
# half_correct = cd.get("half_correct")
# incorrect = cd.get("incorrect")
marked_answers = json.loads(cd.get("marked_answers"))
# This is mildly dangerous as we delete all answers
question.answers.all().delete()
question.half_mark_answers.all().delete()
question.incorrect_answers.all().delete()
# This should probably use json (or something else...)
# ajax.....
for ans in marked_answers["correct"]:
if ans.strip() == "":
continue
print("correct", ans)
print(question.pk)
a = Answers()
a.question_id = question.pk
a.answer = ans.strip()
a.save()
# for ans in half_correct.split("--//--"):
for ans in marked_answers["half-correct"]:
if ans.strip() == "":
continue
print("halfcorrect", ans)
a = HalfMarkAnswers()
a.question_id = question.pk
a.answer = ans.strip()
a.save()
for ans in marked_answers["incorrect"]:
if ans.strip() == "":
continue
print("incorrect", ans)
a = IncorrectAnswers()
a.question_id = question.pk
a.answer = ans.strip()
a.save()
# answer = form.save(commit=False)
# answer.user = request.user
# answer.question = question
# answer.published_date = timezone.now()
# answer.save()
if "next" in request.POST:
return redirect("mark", pk=pk, sk=n + 1)
elif "previous" in request.POST:
return redirect("mark", pk=pk, sk=n - 1)
else:
form = MarkAnatomyQuestionForm()
return render(
request,
"anatomy/mark.html",
{
"exam": exam,
"form": form,
"question": question,
"question_details": question_details,
"user_answers": user_answers,
},
)
def exam_json(request, pk):
exam = get_object_or_404(Exam, pk=pk)
questions = exam.exam_questions.all()
exam_questions = defaultdict(dict)
for q in questions:
exam_questions[q.id] = {
"title": "{}".format(q.examination),
"question": str(q.question_type),
"images": [image_as_base64(q.image)],
"type": "anatomy",
}
exam_json = {
"eid": exam.id,
"exam_type": "anatomy",
"exam_name": exam.name,
"exam_mode": True,
"questions": exam_questions,
}
return JsonResponse(exam_json)
@login_required
def exam_scores_cid(request, pk):
exam = get_object_or_404(Exam, pk=pk)
questions = exam.exam_questions.all()
cids = (
UserAnswer.objects.filter(question__in=questions)
.values_list("cid", flat=True)
.distinct()
)
user_answers_and_marks = defaultdict(list)
user_answers_marks = defaultdict(list)
user_answers = defaultdict(list)
user_names = {}
for cid in cids:
# Convoluted (probably...)
user_names[cid] = cid
for q in questions:
s = q.user_answers.filter(cid=cid)
if not s:
user_answers_marks[cid].append(0)
user_answers[cid].append("")
continue
ans = s[0].answer
if ans in q.answers.all().values_list("answer", flat=True):
a = 2
elif ans in q.half_mark_answers.all().values_list("answer", flat=True):
a = 1
else:
a = 0
user_answers[cid].append(ans)
user_answers_marks[cid].append(a)
user_answers_and_marks[cid].append((ans, a))
user_scores = {}
for user in user_answers_marks:
user_scores[user] = sum(user_answers_marks[user])
user_scores_list = list(user_scores.values())
mean = statistics.mean(user_scores_list)
median = statistics.median(user_scores_list)
try:
mode = statistics.mode(user_scores_list)
except statistics.StatisticsError:
mode = "No unique mode"
df = user_scores_list
fig = px.histogram(df, x=0)
fig_html = fig.to_html()
total = len(questions)
return render(
request,
"anatomy/exam_scores.html",
{
"exam": exam,
"questions": questions,
"user_answers": dict(user_answers),
"user_answers_marks": dict(user_answers_marks),
"user_scores": user_scores,
"user_names": user_names,
"user_answers_and_marks": user_answers_and_marks,
"mean": mean,
"median": median,
"mode": mode,
"plot": fig_html,
},
)
@login_required
def exam_scores(request, pk):
exam = get_object_or_404(Exam, pk=pk)
questions = exam.exam_questions.all()
users = (
UserAnswer.objects.filter(question__in=questions).values_list("user").distinct()
)
user_answers_and_marks = defaultdict(list)
user_answers_marks = defaultdict(list)
user_answers = defaultdict(list)
user_names = {}
for u in users:
# Convoluted (probably...)
user_names[u] = User.objects.filter(pk=u[0]).first().get_username()
for q in questions:
s = q.user_answers.filter(user__in=u)
if not s:
user_answers_marks[u].append(0)
user_answers[u].append("")
continue
ans = s[0].answer
if ans in q.answers.all().values_list("answer", flat=True):
a = 2
elif ans in q.half_mark_answers.all().values_list("answer", flat=True):
a = 1
else:
a = 0
user_answers[u].append(ans)
user_answers_marks[u].append(a)
user_answers_and_marks[u].append((ans, a))
user_scores = {}
for user in user_answers_marks:
user_scores[user] = sum(user_answers_marks[user])
# Users with answers
#
# for q in questions:
# answers = q.user_answers.all()
#
# for ans in answers:
# user = ans.user
# if ans.answer in q.answers.all().values_list("answer", flat=True):
# user_answers[user].append(2)
#
total = len(questions)
return render(
request,
"anatomy/exam_scores.html",
{
"exam": exam,
"questions": questions,
"user_answers": dict(user_answers),
"user_answers_marks": dict(user_answers_marks),
"user_scores": user_scores,
"user_names": user_names,
"user_answers_and_marks": user_answers_and_marks,
},
)