This commit is contained in:
Ross
2022-03-23 22:06:33 +00:00
parent a99750e579
commit 9f54a9c108
32 changed files with 12173 additions and 12153 deletions
+215 -215
View File
@@ -1,215 +1,215 @@
from django.forms import ( from django.forms import (
Form, Form,
ModelForm, ModelForm,
ModelMultipleChoiceField, ModelMultipleChoiceField,
ModelChoiceField, ModelChoiceField,
ChoiceField, ChoiceField,
CharField, CharField,
) )
from django.forms import inlineformset_factory from django.forms import inlineformset_factory
from .models import ( from .models import (
Answer, Answer,
CidUserAnswer, CidUserAnswer,
AnatomyQuestion, AnatomyQuestion,
# Examination, # Examination,
BodyPart, BodyPart,
Structure, Structure,
Region, Region,
Modality, Modality,
QuestionType, QuestionType,
Exam, Exam,
) )
from generic.models import Examination from generic.models import Examination
from django.contrib.admin.widgets import FilteredSelectMultiple from django.contrib.admin.widgets import FilteredSelectMultiple
from django.forms.widgets import RadioSelect, TextInput, Textarea from django.forms.widgets import RadioSelect, TextInput, Textarea
from dal import autocomplete from dal import autocomplete
class AnatomyAnswerForm(ModelForm): class AnatomyAnswerForm(ModelForm):
class Meta: class Meta:
model = CidUserAnswer model = CidUserAnswer
fields = ("answer",) fields = ("answer",)
class MarkAnatomyQuestionForm(Form): class MarkAnatomyQuestionForm(Form):
# correct = forms.CharField(required=False) # correct = forms.CharField(required=False)
# half_correct = forms.CharField(required=False) # half_correct = forms.CharField(required=False)
# incorrect = forms.CharField(required=False) # incorrect = forms.CharField(required=False)
marked_answers = CharField(required=False) marked_answers = CharField(required=False)
class AnatomyQuestionForm(ModelForm): class AnatomyQuestionForm(ModelForm):
# exams = ModelMultipleChoiceField(required=False, queryset=Exam.objects.all()) # exams = ModelMultipleChoiceField(required=False, queryset=Exam.objects.all())
class Media: class Media:
# Django also includes a few javascript files necessary # Django also includes a few javascript files necessary
# for the operation of this form element. You need to # for the operation of this form element. You need to
# include <script src="/admin/jsi18n"></script> # include <script src="/admin/jsi18n"></script>
# in the template. # in the template.
css = { css = {
"all": ["css/widgets.css"], "all": ["css/widgets.css"],
} }
# Adding this javascript is crucial # Adding this javascript is crucial
js = ["jsi18n.js", "tesseract.min.js"] js = ["jsi18n.js", "tesseract.min.js"]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.user = kwargs.pop( self.user = kwargs.pop(
"user" "user"
) # To get request.user. Do not use kwargs.pop('user', None) due to potential security hole ) # To get request.user. Do not use kwargs.pop('user', None) due to potential security hole
if kwargs.get("instance"): if kwargs.get("instance"):
# We get the 'initial' keyword argument or initialize it # We get the 'initial' keyword argument or initialize it
# as a dict if it didn't exist. # as a dict if it didn't exist.
initial = kwargs.setdefault("initial", {}) initial = kwargs.setdefault("initial", {})
# The widget for a ModelMultipleChoiceField expects # The widget for a ModelMultipleChoiceField expects
# a list of primary key for the selected data. # a list of primary key for the selected data.
initial["exams"] = [t.pk for t in kwargs["instance"].exams.all()] initial["exams"] = [t.pk for t in kwargs["instance"].exams.all()]
ModelForm.__init__(self, *args, **kwargs) ModelForm.__init__(self, *args, **kwargs)
super(AnatomyQuestionForm, self).__init__(*args, **kwargs) super(AnatomyQuestionForm, self).__init__(*args, **kwargs)
# self.fields['question'].widget.attrs = {'class': 'question-form', 'rows': 10, 'cols': 100} # self.fields['question'].widget.attrs = {'class': 'question-form', 'rows': 10, 'cols': 100}
# self.fields['feedback'].widget.attrs = {'class': 'feedback-form', 'rows': 10, 'cols': 100} # self.fields['feedback'].widget.attrs = {'class': 'feedback-form', 'rows': 10, 'cols': 100}
self.fields["question_type"] = ModelChoiceField( self.fields["question_type"] = ModelChoiceField(
required=True, required=True,
queryset=QuestionType.objects.all(), queryset=QuestionType.objects.all(),
initial=1, initial=1,
) )
self.fields["region"] = ModelChoiceField( self.fields["region"] = ModelChoiceField(
required=False, required=False,
queryset=Region.objects.all(), queryset=Region.objects.all(),
) )
self.fields["modality"] = ModelChoiceField( self.fields["modality"] = ModelChoiceField(
required=True, required=True,
queryset=Modality.objects.all(), queryset=Modality.objects.all(),
) )
self.fields["examination"] = ModelChoiceField( self.fields["examination"] = ModelChoiceField(
required=False, required=False,
queryset=Examination.objects.all(), queryset=Examination.objects.all(),
) )
self.fields["body_part"] = ModelChoiceField( self.fields["body_part"] = ModelChoiceField(
required=False, required=False,
queryset=BodyPart.objects.all(), queryset=BodyPart.objects.all(),
) )
if self.user.groups.filter(name="anatomy_checker").exists(): if self.user.groups.filter(name="anatomy_checker").exists():
exam_queryset = Exam.objects.all() exam_queryset = Exam.objects.all()
else: else:
exam_queryset = Exam.objects.filter( exam_queryset = Exam.objects.filter(
author__id=self.user.id author__id=self.user.id
) | Exam.objects.filter(open_access=True) ) | Exam.objects.filter(open_access=True)
self.fields["exams"] = ModelMultipleChoiceField( self.fields["exams"] = ModelMultipleChoiceField(
required=False, required=False,
queryset=exam_queryset, queryset=exam_queryset,
widget=FilteredSelectMultiple(verbose_name="Exams", is_stacked=False), widget=FilteredSelectMultiple(verbose_name="Exams", is_stacked=False),
) )
def save(self, commit=True): def save(self, commit=True):
# Get the unsaved Pizza instance # Get the unsaved Pizza instance
instance = ModelForm.save(self, False) instance = ModelForm.save(self, False)
instance.save() instance.save()
old_exams = instance.exams.all() old_exams = instance.exams.all()
new_exams = self.cleaned_data["exams"] new_exams = self.cleaned_data["exams"]
for exam in old_exams: for exam in old_exams:
if exam not in new_exams: if exam not in new_exams:
exam.exam_questions.remove(instance) exam.exam_questions.remove(instance)
for exam in new_exams: for exam in new_exams:
exam.exam_questions.add(instance) exam.exam_questions.add(instance)
self.save_m2m() self.save_m2m()
return instance return instance
class Meta: class Meta:
model = AnatomyQuestion model = AnatomyQuestion
fields = [ fields = [
"question_type", "question_type",
"image", "image",
"region", "region",
"modality", "modality",
"structure", "structure",
"examination", "examination",
"body_part", "body_part",
"description", "description",
"open_access", "open_access",
] ]
widgets = { widgets = {
"structure": autocomplete.ModelSelect2(url="anatomy:structure-autocomplete") "structure": autocomplete.ModelSelect2(url="anatomy:structure-autocomplete")
} }
AnswerFormSet = inlineformset_factory( AnswerFormSet = inlineformset_factory(
AnatomyQuestion, AnatomyQuestion,
Answer, Answer,
fields=["answer", "status"], fields=["answer", "status"],
widgets={ widgets={
"answer": Textarea( "answer": Textarea(
attrs={"required": "true", "minlength": 2, "maxlength": 500, "rows": 3} attrs={"required": "true", "minlength": 2, "maxlength": 500, "rows": 3}
) )
}, },
exclude=[], exclude=[],
can_delete=True, can_delete=True,
extra=1, extra=1,
max_num=10, max_num=10,
) )
AnswerUpdateFormSet = inlineformset_factory( AnswerUpdateFormSet = inlineformset_factory(
AnatomyQuestion, AnatomyQuestion,
Answer, Answer,
fields=["answer", "status"], fields=["answer", "status"],
widgets={ widgets={
"answer": Textarea( "answer": Textarea(
attrs={"required": "true", "minlength": 2, "maxlength": 500, "rows": 3} attrs={"required": "true", "minlength": 2, "maxlength": 500, "rows": 3}
) )
}, },
exclude=[], exclude=[],
can_delete=True, can_delete=True,
extra=0, extra=0,
max_num=10, max_num=10,
) )
class ExaminationForm(ModelForm): class ExaminationForm(ModelForm):
class Meta: class Meta:
model = Examination model = Examination
fields = ["examination"] fields = ["examination"]
class StructureForm(ModelForm): class StructureForm(ModelForm):
class Meta: class Meta:
model = Structure model = Structure
fields = ["structure"] fields = ["structure"]
class BodyPartForm(ModelForm): class BodyPartForm(ModelForm):
class Meta: class Meta:
model = BodyPart model = BodyPart
fields = ["bodypart"] fields = ["bodypart"]
class ExamForm(ModelForm): class ExamForm(ModelForm):
class Meta: class Meta:
model = Exam model = Exam
fields = [ fields = [
"name", "name",
"time_limit", "time_limit",
"open_access", "open_access",
"exam_mode", "exam_mode",
"active", "active",
"publish_results", "publish_results",
"archive", "archive",
] ]
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+19 -19
View File
@@ -1,20 +1,20 @@
{% extends 'anatomy/exams.html' %} {% extends 'anatomy/exams.html' %}
{% block content %} {% block content %}
<div class="anatomy"> <div class="anatomy">
<h2>Marking exam: {{ exam.name }}</h2> <h2>Marking exam: {{ exam.name }}</h2>
You can start marking from a particular question by clicking on it below. You can start marking from a particular question by clicking on it below.
<div> <div>
<button class="show-all-button">Show all</button><button class="show-unmarked-button">Show unmarked</button> <button class="show-all-button">Show all</button><button class="show-unmarked-button">Show unmarked</button>
</div> </div>
<div id="stark-marking-button"><a href="{% url 'anatomy:mark' exam_pk=exam.pk sk=0 %}"><button>Click here to start marking</button></a></div> <div id="stark-marking-button"><a href="{% url 'anatomy:mark' exam_pk=exam.pk sk=0 %}"><button>Click here to start marking</button></a></div>
<ul id="question-mark-list"> <ul id="question-mark-list">
{% for question, unmarked_count, unmarked_count2 in question_unmarked_map %} {% for question, unmarked_count, unmarked_count2 in question_unmarked_map %}
<li data-markcount={{unmarked_count}} {% if unmarked_count %}class="unmarked" {% endif %}><a href="{% url 'anatomy:mark' exam_pk=exam.pk sk=forloop.counter0 %}">Question {{forloop.counter }}: <li data-markcount={{unmarked_count}} {% if unmarked_count %}class="unmarked" {% endif %}><a href="{% url 'anatomy:mark' exam_pk=exam.pk sk=forloop.counter0 %}">Question {{forloop.counter }}:
{{ question }}</a><br />Unmarked answers: {{ unmarked_count }}</li> {{ question }}</a><br />Unmarked answers: {{ unmarked_count }}</li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
{% endblock %} {% endblock %}
+1111 -1111
View File
File diff suppressed because it is too large Load Diff
+112 -112
View File
@@ -1,112 +1,112 @@
from django.contrib import admin from django.contrib import admin
from .models import ( from .models import (
Case, Case,
CaseDetail, CaseDetail,
CaseCollection, CaseCollection,
Differential, Differential,
Series, Series,
SeriesImage, SeriesImage,
Condition, Condition,
Subspecialty, Subspecialty,
Finding, Finding,
SeriesFinding, SeriesFinding,
Structure, Structure,
PathologicalProcess, PathologicalProcess,
Presentation, Presentation,
) )
import tagulous.admin import tagulous.admin
from django.forms import ModelForm from django.forms import ModelForm
from reversion.admin import VersionAdmin from reversion.admin import VersionAdmin
from django.forms.widgets import RadioSelect from django.forms.widgets import RadioSelect
from tinymce.widgets import TinyMCE from tinymce.widgets import TinyMCE
# from generic.models import Examination, Sign, Site, Condition # from generic.models import Examination, Sign, Site, Condition
from generic.models import Examination from generic.models import Examination
# Register your models here. # Register your models here.
# admin.site.register(Examination) # admin.site.register(Examination)
# admin.site.register(Site) # admin.site.register(Site)
admin.site.register(SeriesImage) admin.site.register(SeriesImage)
admin.site.register(Condition) admin.site.register(Condition)
admin.site.register(Subspecialty) admin.site.register(Subspecialty)
admin.site.register(Finding) admin.site.register(Finding)
admin.site.register(Structure) admin.site.register(Structure)
admin.site.register(SeriesFinding) admin.site.register(SeriesFinding)
admin.site.register(Differential) admin.site.register(Differential)
admin.site.register(PathologicalProcess) admin.site.register(PathologicalProcess)
admin.site.register(Presentation) admin.site.register(Presentation)
admin.site.register(CaseDetail) admin.site.register(CaseDetail)
class DifferentialInline(admin.TabularInline): class DifferentialInline(admin.TabularInline):
model = Differential model = Differential
class CasesInline(admin.TabularInline): class CasesInline(admin.TabularInline):
model = CaseDetail model = CaseDetail
class CaseCollectionAdminForm(ModelForm): class CaseCollectionAdminForm(ModelForm):
class Meta: class Meta:
model = CaseCollection model = CaseCollection
exclude = [] exclude = []
class CaseCollectionAdmin(VersionAdmin): class CaseCollectionAdmin(VersionAdmin):
form = CaseCollectionAdminForm form = CaseCollectionAdminForm
save_on_top = True save_on_top = True
save_as = True save_as = True
view_on_site = True view_on_site = True
inlines = (CasesInline,) inlines = (CasesInline,)
admin.site.register(CaseCollection, CaseCollectionAdmin) admin.site.register(CaseCollection, CaseCollectionAdmin)
class AtlasAdminForm(ModelForm): class AtlasAdminForm(ModelForm):
class Meta: class Meta:
model = Case model = Case
fields = [ fields = [
"title", "title",
"description", "description",
"history", "history",
"discussion", "discussion",
"condition", "condition",
"differential", "differential",
"subspecialty", "subspecialty",
"author", "author",
"series", "series",
] ]
widgets = { widgets = {
# "normal": RadioSelect( # "normal": RadioSelect(
# choices=[(True, 'Yes'), # choices=[(True, 'Yes'),
# (False, 'No')]) # (False, 'No')])
"history": TinyMCE(attrs={"cols": 80, "rows": 30}), "history": TinyMCE(attrs={"cols": 80, "rows": 30}),
"discussion": TinyMCE(attrs={"cols": 80, "rows": 30}), "discussion": TinyMCE(attrs={"cols": 80, "rows": 30}),
} }
class AtlasAdmin(VersionAdmin): class AtlasAdmin(VersionAdmin):
form = AtlasAdminForm form = AtlasAdminForm
save_on_top = True save_on_top = True
save_as = True save_as = True
view_on_site = True view_on_site = True
inlines = (DifferentialInline,) inlines = (DifferentialInline,)
admin.site.register(Case, AtlasAdmin) admin.site.register(Case, AtlasAdmin)
class SeriesAdmin(VersionAdmin): class SeriesAdmin(VersionAdmin):
pass pass
admin.site.register(Series, SeriesAdmin) admin.site.register(Series, SeriesAdmin)
+36 -36
View File
@@ -1,36 +1,36 @@
# Generated by Django 3.2.10 on 2022-03-21 16:02 # Generated by Django 3.2.10 on 2022-03-21 16:02
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('atlas', '0024_alter_case_previous_case'), ('atlas', '0024_alter_case_previous_case'),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='CaseCollection', name='CaseCollection',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, unique=True)), ('name', models.CharField(max_length=255, unique=True)),
('open_access', models.BooleanField(default=True, help_text='If a collection should be freely available to browse')), ('open_access', models.BooleanField(default=True, help_text='If a collection should be freely available to browse')),
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
name='CaseDetail', name='CaseDetail',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('sort_order', models.IntegerField()), ('sort_order', models.IntegerField()),
('case', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='atlas.case')), ('case', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='atlas.case')),
('collection', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='atlas.casecollection')), ('collection', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='atlas.casecollection')),
], ],
), ),
migrations.AddField( migrations.AddField(
model_name='casecollection', model_name='casecollection',
name='cases', name='cases',
field=models.ManyToManyField(through='atlas.CaseDetail', to='atlas.Case'), field=models.ManyToManyField(through='atlas.CaseDetail', to='atlas.Case'),
), ),
] ]
+23 -23
View File
@@ -1,23 +1,23 @@
# Generated by Django 3.2.10 on 2022-03-21 16:12 # Generated by Django 3.2.10 on 2022-03-21 16:12
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('atlas', '0025_auto_20220321_1602'), ('atlas', '0025_auto_20220321_1602'),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='case', model_name='case',
name='open_access', name='open_access',
field=models.BooleanField(default=True, help_text='If a case should be freely available to browse'), field=models.BooleanField(default=True, help_text='If a case should be freely available to browse'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='series', model_name='series',
name='open_access', name='open_access',
field=models.BooleanField(default=True, help_text='If a series should be freely available to browse'), field=models.BooleanField(default=True, help_text='If a series should be freely available to browse'),
), ),
] ]
+575 -568
View File
File diff suppressed because it is too large Load Diff
+125 -125
View File
@@ -1,125 +1,125 @@
<div class="atlas {% if case.scrapped %}atlas-scrapped{% endif %}"> <div class="atlas {% if case.scrapped %}atlas-scrapped{% endif %}">
<div class="date"> <div class="date">
{{ case.created_date|date:"d/m/Y" }} {{ case.created_date|date:"d/m/Y" }}
</div> </div>
<div class="id"> <div class="id">
ID: {{ case.id }} ID: {{ case.id }}
</div> </div>
<p class="pre-whitespace"><b>Title:</b> {{ case.title }}</p> <p class="pre-whitespace"><b>Title:</b> {{ case.title }}</p>
<p class="pre-whitespace"><b>Description:</b> {{ case.description }}</p> <p class="pre-whitespace"><b>Description:</b> {{ case.description }}</p>
<p> <p>
<b>Presentation:</b> <b>Presentation:</b>
<ul> <ul>
{% for presentation in case.presentation.all %} {% for presentation in case.presentation.all %}
<li>{{presentation.get_link}}</li> <li>{{presentation.get_link}}</li>
{% endfor %} {% endfor %}
</ul> </ul>
</p> </p>
<p class="pre-whitespace"><b>History:</b> {{ case.history }}</p> <p class="pre-whitespace"><b>History:</b> {{ case.history }}</p>
<p class="pre-whitespace"><b>Discussion:</b> {{ case.discussion }}</p> <p class="pre-whitespace"><b>Discussion:</b> {{ case.discussion }}</p>
<p class="pre-whitespace"><b>Report:</b> {{ case.report }}</p> <p class="pre-whitespace"><b>Report:</b> {{ case.report }}</p>
<div class="pre-whitespace multi-image-block"><b>Series:</b> <div class="pre-whitespace multi-image-block"><b>Series:</b>
{% for series in case.series.all %} {% for series in case.series.all %}
<span class="series-block"> <span class="series-block">
<span> <span>
Series {{ forloop.counter }}: Series {{ forloop.counter }}:
<a href="{% url 'atlas:series_detail' pk=series.pk %}"> <a href="{% url 'atlas:series_detail' pk=series.pk %}">
{{series.get_block}} {{series.get_block}}
</a> </a>
<a href="#" <a href="#"
onclick="return window.create_popup_window('/atlas/series/{{series.pk}}', 'Series')">Popup</a> onclick="return window.create_popup_window('/atlas/series/{{series.pk}}', 'Series')">Popup</a>
</span> </span>
</span> </span>
{% endfor %} {% endfor %}
<a href="{% url 'atlas:series_id_create' pk=case.pk %}">Add new series</a><br /> <a href="{% url 'atlas:series_id_create' pk=case.pk %}">Add new series</a><br />
</div> </div>
{% comment %} <p><b>Scrapped:</b> {{ case.scrapped }} <a {% comment %} <p><b>Scrapped:</b> {{ case.scrapped }} <a
href="{% url 'atlas:case_scrap' pk=case.pk %}">(toggle)</a> {% endcomment %} href="{% url 'atlas:case_scrap' pk=case.pk %}">(toggle)</a> {% endcomment %}
<div class="atlas-answer"> <div class="atlas-answer">
<p class="pre-whitespace"> <p class="pre-whitespace">
Condition: Condition:
<ul> <ul>
{% for con in case.condition.all %} {% for con in case.condition.all %}
<li>{{con.get_link}}</li> <li>{{con.get_link}}</li>
{% endfor %} {% endfor %}
</ul> </ul>
<details> <details>
<summary><b>Findings</b></summary> <summary><b>Findings</b></summary>
{% if case.series.all %} {% if case.series.all %}
See individual series for more details.<br/> See individual series for more details.<br/>
{% for series in case.series.all %} {% for series in case.series.all %}
{% for finding in series.findings.all %} {% for finding in series.findings.all %}
<div class="finding-box"> <div class="finding-box">
<span> <span>
<a href="{{series.get_absolute_url}}">Series {{series.pk}}</a>: {{series.get_examination_full}} |&nbsp; <a href="{{series.get_absolute_url}}">Series {{series.pk}}</a>: {{series.get_examination_full}} |&nbsp;
</span> </span>
{% if finding.findings.all %} {% if finding.findings.all %}
<span> <span>
Findings: {% for f in finding.findings.all %} Findings: {% for f in finding.findings.all %}
{{f.get_link}},&nbsp; {{f.get_link}},&nbsp;
{% endfor %} {% endfor %}
</span> </span>
{% endif %} {% endif %}
{% if finding.structures.all %} {% if finding.structures.all %}
<span> <span>
Structure: {% for s in finding.structures.all %} Structure: {% for s in finding.structures.all %}
{{s.get_link}},&nbsp; {{s.get_link}},&nbsp;
{% endfor %} {% endfor %}
</span> </span>
{% endif %} {% endif %}
{% if finding.description %} {% if finding.description %}
<span> <span>
Description: {{finding.description}} Description: {{finding.description}}
</span> </span>
{% endif %} {% endif %}
</div> </div>
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}
{% else %} {% else %}
No findings associated with case. No findings associated with case.
{% endif %} {% endif %}
</details> </details>
<details open> <details open>
<summary><b>Differentials</b></summary> <summary><b>Differentials</b></summary>
<ul> <ul>
{% for diff in case.differentialcase.all %} {% for diff in case.differentialcase.all %}
<li>{{diff.condition.get_link}} - {{diff.text}}</li> <li>{{diff.condition.get_link}} - {{diff.text}}</li>
{% endfor %} {% endfor %}
</ul> </ul>
</details> </details>
</p> </p>
</div> </div>
Subspecialty: Subspecialty:
<ul> <ul>
{% for sub in case.subspecialty.all %} {% for sub in case.subspecialty.all %}
<li>{{sub.get_link}}</li> <li>{{sub.get_link}}</li>
{% endfor %} {% endfor %}
</ul> </ul>
<b>Pathological Process:</b> <b>Pathological Process:</b>
<ul> <ul>
{% for p in case.pathological_process.all %} {% for p in case.pathological_process.all %}
<li>{{p.get_link}}</li> <li>{{p.get_link}}</li>
{% endfor %} {% endfor %}
</ul> </ul>
</p> </p>
<p class="pre-whitespace"><b>Previous case:</b> {{ case.previous_case }}</p> <p class="pre-whitespace"><b>Previous case:</b> {{ case.previous_case }}</p>
<p class="pre-whitespace"><b>Next case:</b> {{ case.next_case }}</p> <p class="pre-whitespace"><b>Next case:</b> {{ case.next_case }}</p>
<div> <div>
{% include 'question_notes.html' %} {% include 'question_notes.html' %}
</div> </div>
<p><b>Checked by:</b> {% for verified in case.verified.all %} <a <p><b>Checked by:</b> {% for verified in case.verified.all %} <a
href="{% url 'atlas:verified_detail' pk=verified.pk %}">{{verified}}</a>, {% endfor %}</p> href="{% url 'atlas:verified_detail' pk=verified.pk %}">{{verified}}</a>, {% endfor %}</p>
<p><b>Author(s):</b> {% for author in case.author.all %} <a <p><b>Author(s):</b> {% for author in case.author.all %} <a
href="{% url 'atlas:author_detail' pk=author.pk %}">{{author}}</a>, {% endfor %}</p> href="{% url 'atlas:author_detail' pk=author.pk %}">{{author}}</a>, {% endfor %}</p>
+52 -40
View File
@@ -1,40 +1,52 @@
{% extends 'atlas/base.html' %} {% extends 'atlas/base.html' %}
{% block content %} {% block content %}
<h2>Case {{case_number|add:1}}</h2> <h2>Case {{case_number|add:1}}</h2>
<div class="pre-whitespace multi-image-block"> <div class="pre-whitespace multi-image-block">
{% for series in series_list %} {% for series in series_list %}
<span class="series-block"> <span class="series-block">
<span> <span>
Series {{ forloop.counter }}: Series {{ forloop.counter }}:
<a href="{% url 'atlas:series_detail' pk=series.pk %}"> <a href="{% url 'atlas:series_detail' pk=series.pk %}">
{{series.get_block}} {{series.get_block}}
</a> </a>
<a href="#" <a href="#"
onclick="return window.create_popup_window('/atlas/series/{{series.pk}}', 'Series')">Popup</a> onclick="return window.create_popup_window('/atlas/series/{{series.pk}}', 'Series')">Popup</a>
</span> </span>
</span> </span>
{% endfor %} {% endfor %}
</div> </div>
{% for series in series_list %} <div>
<div> <div id="single-dicom-viewer" class="dicom-viewer" data-images="{{ series.get_image_url_array }}" data-annotations=''>
<div id="single-dicom-viewer" class="dicom-viewer" data-images="{{ series.get_image_url_array }}" data-annotations=''> </div>
</div>
</div>
</div>
<div>
{% endfor %} {% if previous %}
<div> <a href="{% url 'atlas:collection_case_view' pk=collection.pk case_number=case_number|add:-1 %}">Previous</a>
{% if previous %} {% endif %}
<a href="{% url 'atlas:collection_case_view' pk=collection.pk case_number=case_number|add:-1 %}">Previous</a> {% if next %}
{% endif %} <a href="{% url 'atlas:collection_case_view' pk=collection.pk case_number=case_number|add:1 %}">Next</a>
{% if next %} {% endif %}
<a href="{% url 'atlas:collection_case_view' pk=collection.pk case_number=case_number|add:1 %}">Next</a> </div>
{% endif %} <br/>
</div> Return to <a href='{{collection.get_absolute_url}}'>collection</a>
<br/> {% endblock %}
Return to <a href='{{collection.get_absolute_url}}'>collection</a> {% block js %}
{% endblock %} <script type="text/javascript">
window.images = {
{% for series in series_list %}
{{forloop.counter0}} : ["{{ series.get_image_url_array_not_json }}"],
{% endfor %}
}
$(document).ready(function () {
loadDicomViewer(window.images[0])
})
</script>
{% endblock js %}
@@ -1,12 +1,12 @@
{% extends 'atlas/base.html' %} {% extends 'atlas/base.html' %}
{% block content %} {% block content %}
<h2>{{collection.name}} <h2>{{collection.name}}
<ul> <ul>
{% for case in collection.cases.all %} {% for case in collection.cases.all %}
<li><a href="{% url 'atlas:collection_case_view' pk=collection.pk case_number=forloop.counter0 %}">Case {{forloop.counter}}</a></li> <li><a href="{% url 'atlas:collection_case_view' pk=collection.pk case_number=forloop.counter0 %}">Case {{forloop.counter}}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endblock %} {% endblock %}
@@ -1,9 +1,9 @@
{% extends 'atlas/base.html' %} {% extends 'atlas/base.html' %}
{% block content %} {% block content %}
<ul> <ul>
{% for collection in collections %} {% for collection in collections %}
<li><a href='{{collection.get_absolute_url}}'>{{collection.name}}</a></li> <li><a href='{{collection.get_absolute_url}}'>{{collection.name}}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endblock %} {% endblock %}
@@ -1,172 +1,172 @@
<div>{{ series.modality}}, {{ series.examination }}, {{ series.plane }}, {{ series.contrast }}</div> <div>{{ series.modality}}, {{ series.examination }}, {{ series.plane }}, {{ series.contrast }}</div>
<div id="single-dicom-viewer" class="dicom-viewer" data-images="{{ series.get_image_url_array }}" data-annotations=''> <div id="single-dicom-viewer" class="dicom-viewer" data-images="{{ series.get_image_url_array }}" data-annotations=''>
</div> </div>
{% if editing_finding < 1 %} {% if editing_finding < 1 %}
<button id="add-finding-button">Add finding</button> <button id="add-finding-button">Add finding</button>
{% endif %} {% endif %}
<button id="reset-viewport-button">Reset viewport</button> <button id="reset-viewport-button">Reset viewport</button>
<div id="finding-form"> <div id="finding-form">
<div class="hide" id="hidden-form"> <div class="hide" id="hidden-form">
<form method="post" id="series_finding_form"> <form method="post" id="series_finding_form">
{% csrf_token %} {% csrf_token %}
{{series_finding_form.management_form}} {{series_finding_form.management_form}}
<div> <div>
<div>Description: {{ series_finding_form.description }}</div> <div>Description: {{ series_finding_form.description }}</div>
<div>Findings: {{ series_finding_form.findings }}</div> <div>Findings: {{ series_finding_form.findings }}</div>
<div>Structures: {{ series_finding_form.structures }}</div> <div>Structures: {{ series_finding_form.structures }}</div>
</div> </div>
<input type="submit" value="Submit"> <input type="submit" value="Submit">
<button id="cancel-add-finding-button">Cancel</button> <button id="cancel-add-finding-button">Cancel</button>
</form> </form>
</div> </div>
</div> </div>
<details open> <details open>
<summary>Findings</summary> <summary>Findings</summary>
{% for finding in series.findings.all %} {% for finding in series.findings.all %}
<div class="finding-box"> <div class="finding-box">
<button class="view-finding-button" data-annotationjson={{finding.annotation_json}} <button class="view-finding-button" data-annotationjson={{finding.annotation_json}}
data-viewportjson={{finding.viewport_json}} data-findingid={{finding.id}}>Click to view</button> data-viewportjson={{finding.viewport_json}} data-findingid={{finding.id}}>Click to view</button>
<span class="view-finding-details"> <span class="view-finding-details">
Finding(s): {% for f in finding.findings.all %}{{f.get_link}}{% endfor %}<br /> Finding(s): {% for f in finding.findings.all %}{{f.get_link}}{% endfor %}<br />
Structure(s): {% for s in finding.structures.all %}{{s.get_link}}{% endfor %}<br /> Structure(s): {% for s in finding.structures.all %}{{s.get_link}}{% endfor %}<br />
Description: {{finding.description}}<br /> Description: {{finding.description}}<br />
</span> </span>
<a href="{% url 'atlas:series_edit_finding' pk=series.pk finding_pk=finding.pk %}" class="edit-finding-link">Edit</a> <a href="{% url 'atlas:series_edit_finding' pk=series.pk finding_pk=finding.pk %}" class="edit-finding-link">Edit</a>
<a href="{% url 'atlas:delete_finding' pk=finding.pk %}" class="delete-finding-link">Delete</a> <a href="{% url 'atlas:delete_finding' pk=finding.pk %}" class="delete-finding-link">Delete</a>
</div> </div>
{% endfor %} {% endfor %}
</details> </details>
<div>Author: {{ series.get_author_display }}</div> <div>Author: {{ series.get_author_display }}</div>
<details> <details>
<summary>Image info</summary> <summary>Image info</summary>
<div> <div>
<a href="{% url 'atlas:series_order_dicom' pk=series.pk %}" title="orders dicom by slice location">Order dicoms <a href="{% url 'atlas:series_order_dicom' pk=series.pk %}" title="orders dicom by slice location">Order dicoms
by slice location</a> by slice location</a>
<a href="{% url 'atlas:series_order_dicom_instance' pk=series.pk %}" <a href="{% url 'atlas:series_order_dicom_instance' pk=series.pk %}"
title="orders dicom by instance number">Order dicoms by instance number</a> title="orders dicom by instance number">Order dicoms by instance number</a>
<a href="{% url 'atlas:series_order_dicom_SeriesInstanceUID' pk=series.pk %}" <a href="{% url 'atlas:series_order_dicom_SeriesInstanceUID' pk=series.pk %}"
title="orders dicom by instance number">Order dicoms by SeriesInstanceUID</a> title="orders dicom by instance number">Order dicoms by SeriesInstanceUID</a>
<a href="{% url 'atlas:series_order_upload_filename' pk=series.pk %}" <a href="{% url 'atlas:series_order_upload_filename' pk=series.pk %}"
title="orders dicom by uploaded filename">Order by uploaded filename</a> title="orders dicom by uploaded filename">Order by uploaded filename</a>
</div> </div>
{% for image in series.images.all %} {% for image in series.images.all %}
{{image.image.url}}, pos: {{image.position}}, {{image.upload_filename}} [{{image.image.size|filesizeformat}}]<br /> {{image.image.url}}, pos: {{image.position}}, {{image.upload_filename}} [{{image.image.size|filesizeformat}}]<br />
{% comment %} {{image.get_dicom_info|safe}}<br /> {% endcomment %} {% comment %} {{image.get_dicom_info|safe}}<br /> {% endcomment %}
{% endfor %} {% endfor %}
<p>Total image size: {{series.get_total_image_size|filesizeformat}}</p> <p>Total image size: {{series.get_total_image_size|filesizeformat}}</p>
<p><a href="{% url 'atlas:series_download' pk=series.pk %}">Download images</a></p> <p><a href="{% url 'atlas:series_download' pk=series.pk %}">Download images</a></p>
</details> </details>
<script> <script>
$(document).ready(function () { $(document).ready(function () {
$("#add-finding-button").click(() => { $("#add-finding-button").click(() => {
dicom_element = $(".cornerstone-element").get(0); dicom_element = $(".cornerstone-element").get(0);
cornerstoneTools.globalImageIdSpecificToolStateManager.clear(dicom_element); cornerstoneTools.globalImageIdSpecificToolStateManager.clear(dicom_element);
cornerstone.reset(dicom_element); cornerstone.reset(dicom_element);
cornerstone.resize(dicom_element, true); cornerstone.resize(dicom_element, true);
//$("#finding-form").empty().append( //$("#finding-form").empty().append(
// $("#hidden-form form").clone() // $("#hidden-form form").clone()
//); //);
$("#hidden-form").show() $("#hidden-form").show()
}); });
$("#cancel-add-finding-button").click((e) => { $("#cancel-add-finding-button").click((e) => {
$("#hidden-form").hide() $("#hidden-form").hide()
e.preventDefault(); e.preventDefault();
}); });
$("#reset-viewport-button").click(() => { $("#reset-viewport-button").click(() => {
dicom_element = $(".cornerstone-element").get(0); dicom_element = $(".cornerstone-element").get(0);
//annotationjson = JSON.parse(e.currentTarget.dataset.annotationjson); //annotationjson = JSON.parse(e.currentTarget.dataset.annotationjson);
cornerstoneTools.globalImageIdSpecificToolStateManager.clear(dicom_element); cornerstoneTools.globalImageIdSpecificToolStateManager.clear(dicom_element);
cornerstone.reset(dicom_element); cornerstone.reset(dicom_element);
cornerstone.resize(dicom_element, true); cornerstone.resize(dicom_element, true);
}); });
$(".view-finding-button").each((n, el) => { $(".view-finding-button").each((n, el) => {
$(el).click((e) => { $(el).click((e) => {
dicom_element = $(".cornerstone-element").get(0); dicom_element = $(".cornerstone-element").get(0);
annotationjson = JSON.parse(e.currentTarget.dataset.annotationjson); annotationjson = JSON.parse(e.currentTarget.dataset.annotationjson);
viewport = JSON.parse(e.currentTarget.dataset.viewportjson); viewport = JSON.parse(e.currentTarget.dataset.viewportjson);
loadAnnotationAndViewportOnElement(annotationjson, viewport, dicom_element); loadAnnotationAndViewportOnElement(annotationjson, viewport, dicom_element);
}); });
}); });
{% if editing_finding > 0 %} {% if editing_finding > 0 %}
$("#hidden-form").show() $("#hidden-form").show()
setTimeout(function() { setTimeout(function() {
$(".view-finding-button[data-findingid={{editing_finding}}]").trigger("click"); $(".view-finding-button[data-findingid={{editing_finding}}]").trigger("click");
}, 1000) }, 1000)
{% endif %} {% endif %}
}); });
function loadAnnotationAndViewportOnElement(annotation, viewport, dicom_element) { function loadAnnotationAndViewportOnElement(annotation, viewport, dicom_element) {
cornerstoneTools.globalImageIdSpecificToolStateManager.clear(dicom_element); cornerstoneTools.globalImageIdSpecificToolStateManager.clear(dicom_element);
cornerstone.getEnabledElement(dicom_element).viewport = viewport cornerstone.getEnabledElement(dicom_element).viewport = viewport
cornerstoneTools.globalImageIdSpecificToolStateManager.restoreToolState( cornerstoneTools.globalImageIdSpecificToolStateManager.restoreToolState(
annotation); annotation);
console.log(annotation) console.log(annotation)
//cornerstone.getEnabledElement(dicom_element).toolStateManager.restoreToolState(annotationjson) //cornerstone.getEnabledElement(dicom_element).toolStateManager.restoreToolState(annotationjson)
cornerstone.resize(dicom_element); cornerstone.resize(dicom_element);
dicomViewer.getNextAnnotationImage(dicom_element); dicomViewer.getNextAnnotationImage(dicom_element);
} }
$(document).on('submit', '#series_finding_form', function (e) { $(document).on('submit', '#series_finding_form', function (e) {
dicom_element = $(".cornerstone-element").get(0); dicom_element = $(".cornerstone-element").get(0);
c = cornerstone.getEnabledElement(dicom_element); c = cornerstone.getEnabledElement(dicom_element);
e.preventDefault(); e.preventDefault();
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: '{% url "atlas:add_finding" %}', url: '{% url "atlas:add_finding" %}',
data: { data: {
series_finding_id: {{ editing_finding }}, series_finding_id: {{ editing_finding }},
description: $('#id_description').val(), description: $('#id_description').val(),
series: {{ series.pk }}, series: {{ series.pk }},
//annotation_json: JSON.stringify(c.toolStateManager.saveToolState()), //annotation_json: JSON.stringify(c.toolStateManager.saveToolState()),
annotation_json: JSON.stringify(cornerstoneTools.globalImageIdSpecificToolStateManager annotation_json: JSON.stringify(cornerstoneTools.globalImageIdSpecificToolStateManager
.saveToolState()), .saveToolState()),
viewport_json: JSON.stringify(c.viewport), viewport_json: JSON.stringify(c.viewport),
findings: JSON.stringify($('#finding-form select[name="findings"]').find(":selected") findings: JSON.stringify($('#finding-form select[name="findings"]').find(":selected")
.map((i, el) => { .map((i, el) => {
return $(el).val() return $(el).val()
}).toArray()), }).toArray()),
structures: JSON.stringify($('#finding-form select[name="structures"]').find( structures: JSON.stringify($('#finding-form select[name="structures"]').find(
":selected") ":selected")
.map((i, el) => { .map((i, el) => {
return $(el).val() return $(el).val()
}).toArray()), }).toArray()),
csrfmiddlewaretoken: $('input[name=csrfmiddlewaretoken]').val(), csrfmiddlewaretoken: $('input[name=csrfmiddlewaretoken]').val(),
action: 'post' action: 'post'
}, },
success: function (json) { success: function (json) {
$("#finding-form").empty(); $("#finding-form").empty();
toastr.info('Finding added.'); toastr.info('Finding added.');
location.href='{{series.get_absolute_url}}'; location.href='{{series.get_absolute_url}}';
}, },
error: function (xhr, errmsg, err) { error: function (xhr, errmsg, err) {
console.log(xhr.status + ": " + xhr console.log(xhr.status + ": " + xhr
.responseText); // provide a bit more info about the error to the console .responseText); // provide a bit more info about the error to the console
} }
}); });
}); });
</script> </script>
+151 -151
View File
@@ -1,151 +1,151 @@
from django.urls import path, include from django.urls import path, include
from atlas.models import Condition, Finding, Presentation, Structure from atlas.models import Condition, Finding, Presentation, Structure
from . import views from . import views
app_name = "atlas" app_name = "atlas"
urlpatterns = [ urlpatterns = [
path("", views.CaseView.as_view(), name="index"), path("", views.CaseView.as_view(), name="index"),
path("author/<int:pk>/", views.author_detail, name="author_detail"), path("author/<int:pk>/", views.author_detail, name="author_detail"),
path("author/", views.author_list, name="author_list"), path("author/", views.author_list, name="author_list"),
path("case/", views.CaseView.as_view(), name="case_view"), path("case/", views.CaseView.as_view(), name="case_view"),
path("collection/", views.collection_index_view, name="collection_index_view"), path("collection/", views.collection_index_view, name="collection_index_view"),
path("collections/<int:pk>", views.collection_detail_view, name="collection_detail_view"), path("collections/<int:pk>", views.collection_detail_view, name="collection_detail_view"),
path("collections/<int:pk>/<int:case_number>", views.collection_case_view, name="collection_case_view"), path("collections/<int:pk>/<int:case_number>", views.collection_case_view, name="collection_case_view"),
path("condition/", views.ConditionView.as_view(), name="condition_view"), path("condition/", views.ConditionView.as_view(), name="condition_view"),
path("categories/", views.categories_list, name="categories_list"), path("categories/", views.categories_list, name="categories_list"),
path("condition/<int:pk>", views.condition_detail, name="condition_detail"), path("condition/<int:pk>", views.condition_detail, name="condition_detail"),
path( path(
"condition/<int:pk>/delete", "condition/<int:pk>/delete",
views.ConditionDelete.as_view(), views.ConditionDelete.as_view(),
name="condition_delete", name="condition_delete",
), ),
path( path(
"condition/<int:pk>/update", "condition/<int:pk>/update",
views.ConditionUpdate.as_view(), views.ConditionUpdate.as_view(),
name="condition_update", name="condition_update",
), ),
path("subspecialty/", views.SubspecialtyView.as_view(), name="subspecialty_view"), path("subspecialty/", views.SubspecialtyView.as_view(), name="subspecialty_view"),
path("subspecialty/<int:pk>", views.subspecialty_detail, name="subspecialty_detail"), path("subspecialty/<int:pk>", views.subspecialty_detail, name="subspecialty_detail"),
path("condition/create", views.ConditionCreate.as_view(), name="condition_create"), path("condition/create", views.ConditionCreate.as_view(), name="condition_create"),
path("finding/", views.FindingView.as_view(), name="finding_view"), path("finding/", views.FindingView.as_view(), name="finding_view"),
path("finding/<int:pk>", views.finding_detail, name="finding_detail"), path("finding/<int:pk>", views.finding_detail, name="finding_detail"),
path( path(
"finding/<int:pk>/delete", views.FindingDelete.as_view(), name="finding_delete" "finding/<int:pk>/delete", views.FindingDelete.as_view(), name="finding_delete"
), ),
path( path(
"finding/<int:pk>/update", views.FindingUpdate.as_view(), name="finding_update" "finding/<int:pk>/update", views.FindingUpdate.as_view(), name="finding_update"
), ),
path("finding/create", views.FindingCreate.as_view(), name="finding_create"), path("finding/create", views.FindingCreate.as_view(), name="finding_create"),
path("structure/", views.StructureView.as_view(), name="structure_view"), path("structure/", views.StructureView.as_view(), name="structure_view"),
path("structure/<int:pk>", views.structure_detail, name="structure_detail"), path("structure/<int:pk>", views.structure_detail, name="structure_detail"),
path( path(
"structure/<int:pk>/delete", "structure/<int:pk>/delete",
views.StructureDelete.as_view(), views.StructureDelete.as_view(),
name="structure_delete", name="structure_delete",
), ),
path( path(
"structure/<int:pk>/update", "structure/<int:pk>/update",
views.StructureUpdate.as_view(), views.StructureUpdate.as_view(),
name="structure_update", name="structure_update",
), ),
path("structure/create", views.StructureCreate.as_view(), name="structure_create"), path("structure/create", views.StructureCreate.as_view(), name="structure_create"),
path("series/<int:pk>", views.series_detail, name="series_detail"), path("series/<int:pk>", views.series_detail, name="series_detail"),
path("series/", views.SeriesView.as_view(), name="series_view"), path("series/", views.SeriesView.as_view(), name="series_view"),
path("series/<int:pk>", views.series_detail, name="series_detail"), path("series/<int:pk>", views.series_detail, name="series_detail"),
path( path(
"series/<int:pk>/<int:finding_pk>", "series/<int:pk>/<int:finding_pk>",
views.series_detail, views.series_detail,
name="series_edit_finding", name="series_edit_finding",
), ),
path( path(
"series/<int:pk>/order_dicom", "series/<int:pk>/order_dicom",
views.series_order_dicom, views.series_order_dicom,
name="series_order_dicom", name="series_order_dicom",
), ),
path( path(
"series/<int:pk>/download", "series/<int:pk>/download",
views.SeriesImagesZipView.as_view(), views.SeriesImagesZipView.as_view(),
name="series_download", name="series_download",
), ),
path( path(
"series/<int:pk>/order_dicom_instance", "series/<int:pk>/order_dicom_instance",
views.series_order_dicom_instance, views.series_order_dicom_instance,
name="series_order_dicom_instance", name="series_order_dicom_instance",
), ),
path( path(
"series/<int:pk>/order_dicom_SeriesInstanceUID", "series/<int:pk>/order_dicom_SeriesInstanceUID",
views.series_order_dicom_SeriesInstanceUID, views.series_order_dicom_SeriesInstanceUID,
name="series_order_dicom_SeriesInstanceUID", name="series_order_dicom_SeriesInstanceUID",
), ),
path( path(
"series/<int:pk>/order_upload_filename", "series/<int:pk>/order_upload_filename",
views.series_order_upload_filename, views.series_order_upload_filename,
name="series_order_upload_filename", name="series_order_upload_filename",
), ),
path( path(
"series/<int:pk>/delete", "series/<int:pk>/delete",
views.SeriesDelete.as_view(), views.SeriesDelete.as_view(),
name="series_delete", name="series_delete",
), ),
# path("unchecked/", views.unchecked_list, name="unchecked_list"), # path("unchecked/", views.unchecked_list, name="unchecked_list"),
# path("verified/<int:pk>/", views.verified_detail, name="verified_detail"), # path("verified/<int:pk>/", views.verified_detail, name="verified_detail"),
path("case/<int:pk>/", views.case_detail, name="case_detail"), path("case/<int:pk>/", views.case_detail, name="case_detail"),
path("case/<int:pk>/clone", views.AtlasClone.as_view(), name="case_clone"), path("case/<int:pk>/clone", views.AtlasClone.as_view(), name="case_clone"),
# path("verified/", views.verified, name="verified"), # path("verified/", views.verified, name="verified"),
# path("all_questions/", views.all_questions, name="all_questions"), # path("all_questions/", views.all_questions, name="all_questions"),
path("case/<int:pk>/scrap", views.atlas_scrap, name="case_scrap"), path("case/<int:pk>/scrap", views.atlas_scrap, name="case_scrap"),
path("case/<int:pk>/delete", views.CaseDelete.as_view(), name="case_delete"), path("case/<int:pk>/delete", views.CaseDelete.as_view(), name="case_delete"),
path("case/create/", views.AtlasCreate.as_view(), name="create"), path("case/create/", views.AtlasCreate.as_view(), name="create"),
path("series/create", views.SeriesCreate.as_view(), name="series_create"), path("series/create", views.SeriesCreate.as_view(), name="series_create"),
path( path(
"series/<int:pk>/create", "series/<int:pk>/create",
views.SeriesCreate.as_view(), views.SeriesCreate.as_view(),
name="series_id_create", name="series_id_create",
), ),
path( path(
"case/<int:pk>/update", "case/<int:pk>/update",
views.AtlasUpdate.as_view(), views.AtlasUpdate.as_view(),
name="case_update", name="case_update",
), ),
path( path(
"series/<int:pk>/update", "series/<int:pk>/update",
views.SeriesUpdate.as_view(), views.SeriesUpdate.as_view(),
name="series_update", name="series_update",
), ),
path( path(
"series/add_finding", "series/add_finding",
views.create_series_findings, views.create_series_findings,
name="add_finding", name="add_finding",
), ),
path( path(
"series/delete_finding/<int:pk>", "series/delete_finding/<int:pk>",
views.SeriesFindingDelete.as_view(), views.SeriesFindingDelete.as_view(),
name="delete_finding", name="delete_finding",
), ),
path( path(
"condition-autocomplete", "condition-autocomplete",
views.ConditionAutocomplete.as_view(model=Condition, create_field="name"), views.ConditionAutocomplete.as_view(model=Condition, create_field="name"),
name="condition-autocomplete", name="condition-autocomplete",
), ),
path( path(
"presentation-autocomplete", "presentation-autocomplete",
views.PresentationAutocomplete.as_view(model=Presentation, create_field="name"), views.PresentationAutocomplete.as_view(model=Presentation, create_field="name"),
name="presentation-autocomplete", name="presentation-autocomplete",
), ),
path( path(
"finding-autocomplete", "finding-autocomplete",
views.FindingAutocomplete.as_view(model=Finding, create_field="name"), views.FindingAutocomplete.as_view(model=Finding, create_field="name"),
name="finding-autocomplete", name="finding-autocomplete",
), ),
path( path(
"structure-autocomplete", "structure-autocomplete",
views.StructureAutocomplete.as_view(model=Structure, create_field="name"), views.StructureAutocomplete.as_view(model=Structure, create_field="name"),
name="structure-autocomplete", name="structure-autocomplete",
), ),
path("presentation/", views.PresentationView.as_view(), name="presentation_view"), path("presentation/", views.PresentationView.as_view(), name="presentation_view"),
path("presentation/<int:pk>", views.presentation_detail, name="presentation_detail"), path("presentation/<int:pk>", views.presentation_detail, name="presentation_detail"),
path("process/", views.PathologicalProcessView.as_view(), name="pathological_process_view"), path("process/", views.PathologicalProcessView.as_view(), name="pathological_process_view"),
path("process/<int:pk>", views.pathological_process_detail, name="pathological_process_detail"), path("process/<int:pk>", views.pathological_process_detail, name="pathological_process_detail"),
] ]
+918 -918
View File
File diff suppressed because it is too large Load Diff
+188 -188
View File
@@ -1,189 +1,189 @@
from django.forms import ( from django.forms import (
Form, Form,
ModelForm, ModelForm,
ModelMultipleChoiceField, ModelMultipleChoiceField,
ModelChoiceField, ModelChoiceField,
ChoiceField, ChoiceField,
CharField, CharField,
HiddenInput, HiddenInput,
) )
from django.forms import inlineformset_factory from django.forms import inlineformset_factory
from generic.models import CidUser, CidUserGroup, Examination, QuestionNote from generic.models import CidUser, CidUserGroup, Examination, QuestionNote
from django.contrib.admin.widgets import FilteredSelectMultiple from django.contrib.admin.widgets import FilteredSelectMultiple
from django.forms.widgets import RadioSelect, TextInput, Textarea from django.forms.widgets import RadioSelect, TextInput, Textarea
from rapids.models import Rapid from rapids.models import Rapid
from anatomy.models import AnatomyQuestion from anatomy.models import AnatomyQuestion
from longs.models import Long from longs.models import Long
from django.shortcuts import render, get_object_or_404, redirect from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from physics.models import CidUserAnswer as PhysicsCidUserAnswer, Question from physics.models import CidUserAnswer as PhysicsCidUserAnswer, Question
from physics.models import Exam as PhysicsExam from physics.models import Exam as PhysicsExam
from physics.models import Question as SbasQuestion from physics.models import Question as SbasQuestion
from anatomy.models import CidUserAnswer as AnatomyCidUserAnswer from anatomy.models import CidUserAnswer as AnatomyCidUserAnswer
from anatomy.models import Exam as AnatomyExam from anatomy.models import Exam as AnatomyExam
from anatomy.models import AnatomyQuestion from anatomy.models import AnatomyQuestion
from anatomy.models import Answer as AnatomyAnswer from anatomy.models import Answer as AnatomyAnswer
from rapids.models import CidUserAnswer as RapidsCidUserAnswer from rapids.models import CidUserAnswer as RapidsCidUserAnswer
from rapids.models import Exam as RapidsExam from rapids.models import Exam as RapidsExam
from rapids.models import Rapid from rapids.models import Rapid
from rapids.models import Answer as RapidAnswer from rapids.models import Answer as RapidAnswer
from longs.models import CidUserAnswer as LongsCidUserAnswer from longs.models import CidUserAnswer as LongsCidUserAnswer
from longs.models import Exam as LongsExam from longs.models import Exam as LongsExam
from longs.models import Long from longs.models import Long
from sbas.models import CidUserAnswer as SbasCidUserAnswer from sbas.models import CidUserAnswer as SbasCidUserAnswer
from sbas.models import Exam as SbasExam from sbas.models import Exam as SbasExam
from sbas.models import Question as SbasQuestion from sbas.models import Question as SbasQuestion
class ExaminationForm(ModelForm): class ExaminationForm(ModelForm):
class Meta: class Meta:
model = Examination model = Examination
fields = ["examination"] fields = ["examination"]
class QuestionNoteForm(ModelForm): class QuestionNoteForm(ModelForm):
class Meta: class Meta:
model = QuestionNote model = QuestionNote
fields = ["content_type", "object_id", "author", "note_type", "note"] fields = ["content_type", "object_id", "author", "note_type", "note"]
widgets = {"content_type": HiddenInput(), "object_id": HiddenInput()} widgets = {"content_type": HiddenInput(), "object_id": HiddenInput()}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
question_type = kwargs.pop("question_type") question_type = kwargs.pop("question_type")
pk = kwargs.pop("pk") pk = kwargs.pop("pk")
user = kwargs.pop("user") user = kwargs.pop("user")
# if question_type == "rapid": # if question_type == "rapid":
# question = Rapid # question = Rapid
# content_type = ContentType.objects.get(model="rapid") # content_type = ContentType.objects.get(model="rapid")
# elif question_type == "anatomy": # elif question_type == "anatomy":
# question = AnatomyQuestion # question = AnatomyQuestion
# content_type = ContentType.objects.get(model="anatomyquestion") # content_type = ContentType.objects.get(model="anatomyquestion")
# elif question_type == "long": # elif question_type == "long":
# question = Long # question = Long
# content_type = ContentType.objects.get(model="long") # content_type = ContentType.objects.get(model="long")
# else: # else:
# raise PermissionError() # raise PermissionError()
# get_object_or_404(question, pk=pk) # get_object_or_404(question, pk=pk)
# We get the 'initial' keyword argument or initialize it # We get the 'initial' keyword argument or initialize it
# as a dict if it didn't exist. # as a dict if it didn't exist.
# if kwargs.get("instance"): # if kwargs.get("instance"):
ModelForm.__init__(self, *args, **kwargs) ModelForm.__init__(self, *args, **kwargs)
super(QuestionNoteForm, self).__init__(*args, **kwargs) super(QuestionNoteForm, self).__init__(*args, **kwargs)
# initial = kwargs.setdefault("initial", {}) # initial = kwargs.setdefault("initial", {})
## The widget for a ModelMultipleChoiceField expects ## The widget for a ModelMultipleChoiceField expects
## a list of primary key for the selected data. ## a list of primary key for the selected data.
##initial["content_type"] = question ##initial["content_type"] = question
##initial["content_id"] = pk ##initial["content_id"] = pk
# self.fields["content_type"].initial = content_type # self.fields["content_type"].initial = content_type
# self.fields["object_id"].initial = pk # self.fields["object_id"].initial = pk
class CidUserForm(ModelForm): class CidUserForm(ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
if kwargs.get("instance"): if kwargs.get("instance"):
# We get the 'initial' keyword argument or initialize it # We get the 'initial' keyword argument or initialize it
# as a dict if it didn't exist. # as a dict if it didn't exist.
initial = kwargs.setdefault("initial", {}) initial = kwargs.setdefault("initial", {})
# The widget for a ModelMultipleChoiceField expects # The widget for a ModelMultipleChoiceField expects
# a list of primary key for the selected data. # a list of primary key for the selected data.
initial["physics_exams"] = [t.pk for t in kwargs["instance"].physics_exams.all()] initial["physics_exams"] = [t.pk for t in kwargs["instance"].physics_exams.all()]
initial["anatomy_exams"] = [t.pk for t in kwargs["instance"].anatomy_exams.all()] initial["anatomy_exams"] = [t.pk for t in kwargs["instance"].anatomy_exams.all()]
initial["sba_exams"] = [t.pk for t in kwargs["instance"].sba_exams.all()] initial["sba_exams"] = [t.pk for t in kwargs["instance"].sba_exams.all()]
initial["rapid_exams"] = [t.pk for t in kwargs["instance"].rapid_exams.all()] initial["rapid_exams"] = [t.pk for t in kwargs["instance"].rapid_exams.all()]
initial["longs_exams"] = [t.pk for t in kwargs["instance"].longs_exams.all()] initial["longs_exams"] = [t.pk for t in kwargs["instance"].longs_exams.all()]
ModelForm.__init__(self, *args, **kwargs) ModelForm.__init__(self, *args, **kwargs)
super(CidUserForm, self).__init__(*args, **kwargs) super(CidUserForm, self).__init__(*args, **kwargs)
self.fields["physics_exams"] = ModelMultipleChoiceField( self.fields["physics_exams"] = ModelMultipleChoiceField(
required=False, required=False,
queryset=PhysicsExam.objects.filter(archive=False), queryset=PhysicsExam.objects.filter(archive=False),
widget=FilteredSelectMultiple(verbose_name="Physics Exams", is_stacked=False), widget=FilteredSelectMultiple(verbose_name="Physics Exams", is_stacked=False),
) )
self.fields["anatomy_exams"] = ModelMultipleChoiceField( self.fields["anatomy_exams"] = ModelMultipleChoiceField(
required=False, required=False,
queryset=AnatomyExam.objects.filter(archive=False), queryset=AnatomyExam.objects.filter(archive=False),
widget=FilteredSelectMultiple(verbose_name="Anatomy Exams", is_stacked=False), widget=FilteredSelectMultiple(verbose_name="Anatomy Exams", is_stacked=False),
) )
self.fields["sba_exams"] = ModelMultipleChoiceField( self.fields["sba_exams"] = ModelMultipleChoiceField(
required=False, required=False,
queryset=SbasExam.objects.filter(archive=False), queryset=SbasExam.objects.filter(archive=False),
widget=FilteredSelectMultiple(verbose_name="SBA Exams", is_stacked=False), widget=FilteredSelectMultiple(verbose_name="SBA Exams", is_stacked=False),
) )
self.fields["rapid_exams"] = ModelMultipleChoiceField( self.fields["rapid_exams"] = ModelMultipleChoiceField(
required=False, required=False,
queryset=RapidsExam.objects.filter(archive=False), queryset=RapidsExam.objects.filter(archive=False),
widget=FilteredSelectMultiple(verbose_name="Rapid Exams", is_stacked=False), widget=FilteredSelectMultiple(verbose_name="Rapid Exams", is_stacked=False),
) )
self.fields["longs_exams"] = ModelMultipleChoiceField( self.fields["longs_exams"] = ModelMultipleChoiceField(
required=False, required=False,
queryset=LongsExam.objects.filter(archive=False), queryset=LongsExam.objects.filter(archive=False),
widget=FilteredSelectMultiple(verbose_name="Long Exams", is_stacked=False), widget=FilteredSelectMultiple(verbose_name="Long Exams", is_stacked=False),
) )
def save(self, commit=True): def save(self, commit=True):
# Get the unsaved Long instance # Get the unsaved Long instance
instance = ModelForm.save(self, False) instance = ModelForm.save(self, False)
# Prepare a 'save_m2m' method for the form, # Prepare a 'save_m2m' method for the form,
old_save_m2m = self.save_m2m old_save_m2m = self.save_m2m
def save_m2m(): def save_m2m():
old_save_m2m() old_save_m2m()
instance.physics_exams.clear() instance.physics_exams.clear()
for physics_exams in self.cleaned_data["physics_exams"]: for physics_exams in self.cleaned_data["physics_exams"]:
instance.physics_exams.add(physics_exams.pk) instance.physics_exams.add(physics_exams.pk)
instance.anatomy_exams.clear() instance.anatomy_exams.clear()
for anatomy_exams in self.cleaned_data["anatomy_exams"]: for anatomy_exams in self.cleaned_data["anatomy_exams"]:
instance.anatomy_exams.add(anatomy_exams.pk) instance.anatomy_exams.add(anatomy_exams.pk)
instance.sba_exams.clear() instance.sba_exams.clear()
for sba_exams in self.cleaned_data["sba_exams"]: for sba_exams in self.cleaned_data["sba_exams"]:
instance.sba_exams.add(sba_exams.pk) instance.sba_exams.add(sba_exams.pk)
instance.rapid_exams.clear() instance.rapid_exams.clear()
for rapid_exams in self.cleaned_data["rapid_exams"]: for rapid_exams in self.cleaned_data["rapid_exams"]:
instance.rapid_exams.add(rapid_exams.pk) instance.rapid_exams.add(rapid_exams.pk)
instance.longs_exams.clear() instance.longs_exams.clear()
for longs_exams in self.cleaned_data["longs_exams"]: for longs_exams in self.cleaned_data["longs_exams"]:
instance.longs_exams.add(longs_exams.pk) instance.longs_exams.add(longs_exams.pk)
self.save_m2m = save_m2m self.save_m2m = save_m2m
# Do we need to save all changes now? # Do we need to save all changes now?
# if commit: # if commit:
instance.save() instance.save()
self.save_m2m() self.save_m2m()
return instance return instance
class Meta: class Meta:
model = CidUser model = CidUser
fields = [ fields = [
"passcode", "passcode",
"active", "active",
"internal_candidate", "internal_candidate",
"name", "name",
"email", "email",
"supervisor_email", "supervisor_email",
"login_email_sent", "login_email_sent",
"results_email_sent", "results_email_sent",
"group", "group",
] ]
class CidUserGroupForm(ModelForm): class CidUserGroupForm(ModelForm):
class Meta: class Meta:
model = CidUserGroup model = CidUserGroup
fields = ["name", "archive"] fields = ["name", "archive"]
+19 -19
View File
@@ -1,19 +1,19 @@
# Generated by Django 3.2.10 on 2022-03-01 18:32 # Generated by Django 3.2.10 on 2022-03-01 18:32
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('generic', '0024_auto_20211231_1044'), ('generic', '0024_auto_20211231_1044'),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='ciduser', model_name='ciduser',
name='group', name='group',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='generic.cidusergroup'), field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='generic.cidusergroup'),
), ),
] ]
+469 -469
View File
@@ -1,469 +1,469 @@
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.db import models from django.db import models
from django.http import HttpRequest from django.http import HttpRequest
from django.utils import timezone from django.utils import timezone
from smtplib import SMTPException from smtplib import SMTPException
from django.urls import reverse from django.urls import reverse
from django.views.generic.edit import CreateView from django.views.generic.edit import CreateView
from reversion.views import RevisionMixin from reversion.views import RevisionMixin
import tagulous import tagulous
import tagulous.models import tagulous.models
from sortedm2m.fields import SortedManyToManyField from sortedm2m.fields import SortedManyToManyField
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.mail import send_mail from django.core.mail import send_mail
from django.core.mail import EmailMessage from django.core.mail import EmailMessage
class Plane(models.Model): class Plane(models.Model):
plane = models.CharField(max_length=200, unique=True) plane = models.CharField(max_length=200, unique=True)
def __str__(self): def __str__(self):
return self.plane return self.plane
class Meta: class Meta:
ordering = ("plane",) ordering = ("plane",)
class Contrast(models.Model): class Contrast(models.Model):
contrast = models.CharField(max_length=200, unique=True) contrast = models.CharField(max_length=200, unique=True)
def __str__(self): def __str__(self):
return self.contrast return self.contrast
class Meta: class Meta:
ordering = ("contrast",) ordering = ("contrast",)
class Examination(models.Model): class Examination(models.Model):
examination = models.CharField(max_length=200, unique=True) examination = models.CharField(max_length=200, unique=True)
def __str__(self): def __str__(self):
return self.examination return self.examination
class Meta: class Meta:
ordering = ("examination",) ordering = ("examination",)
class Site(models.Model): class Site(models.Model):
site = models.CharField(max_length=200) site = models.CharField(max_length=200)
initials = models.CharField(max_length=200) initials = models.CharField(max_length=200)
def __str__(self): def __str__(self):
return self.site return self.site
class Condition(tagulous.models.TagModel): class Condition(tagulous.models.TagModel):
pass pass
class Sign(tagulous.models.TagModel): class Sign(tagulous.models.TagModel):
pass pass
class ExamBase(models.Model): class ExamBase(models.Model):
name = models.CharField(max_length=200, help_text="Name of the exam") name = models.CharField(max_length=200, help_text="Name of the exam")
# exam_questions = SortedManyToManyField(Long, related_name="exams", blank="true") # exam_questions = SortedManyToManyField(Long, related_name="exams", blank="true")
active = models.BooleanField( active = models.BooleanField(
help_text="If an exam should be available to take", default=False help_text="If an exam should be available to take", default=False
) )
exam_mode = models.BooleanField( exam_mode = models.BooleanField(
help_text="If an exam should be taken in exam mode (users results will be submited to the server for marking)", help_text="If an exam should be taken in exam mode (users results will be submited to the server for marking)",
default=False, default=False,
) )
publish_results = models.BooleanField( publish_results = models.BooleanField(
help_text="If an exams results should be available", default=False help_text="If an exams results should be available", default=False
) )
recreate_json = models.BooleanField( recreate_json = models.BooleanField(
help_text="If the json cache needs updating", default=False help_text="If the json cache needs updating", default=False
) )
json_creation_time = models.DateTimeField(blank=True, default=None, null=True) json_creation_time = models.DateTimeField(blank=True, default=None, null=True)
exam_json_id = models.IntegerField( exam_json_id = models.IntegerField(
default=1, help_text="auto incrementing field when json recreated" default=1, help_text="auto incrementing field when json recreated"
) )
archive = models.BooleanField( archive = models.BooleanField(
help_text="Archived exams will remain on the test system but will not be displayed by default", help_text="Archived exams will remain on the test system but will not be displayed by default",
default=False, default=False,
) )
open_access = models.BooleanField( open_access = models.BooleanField(
help_text="If the exam is freely accessible (to view and edit on the test system)", help_text="If the exam is freely accessible (to view and edit on the test system)",
default=False, default=False,
) )
stats_mean = models.FloatField(default=0) stats_mean = models.FloatField(default=0)
stats_mode = models.CharField(default=0, max_length=25) stats_mode = models.CharField(default=0, max_length=25)
stats_median = models.FloatField(default=0) stats_median = models.FloatField(default=0)
stats_candidates = models.FloatField(default=0) stats_candidates = models.FloatField(default=0)
stats_min = models.FloatField(default=0) stats_min = models.FloatField(default=0)
stats_max = models.FloatField(default=0) stats_max = models.FloatField(default=0)
stats_max_possible = models.FloatField(default=0) stats_max_possible = models.FloatField(default=0)
stats_graph = models.TextField(default=0) stats_graph = models.TextField(default=0)
user_scores = models.JSONField(default=dict) user_scores = models.JSONField(default=dict)
notes = GenericRelation("generic.CidUserExam") notes = GenericRelation("generic.CidUserExam")
# time_limit = models.IntegerField( # time_limit = models.IntegerField(
# help_text="Exam time limit (in seconds). Default is 2100 secondse (35 minutes)", # help_text="Exam time limit (in seconds). Default is 2100 secondse (35 minutes)",
# default=2100, # default=2100,
# ) # )
class Meta: class Meta:
abstract = True abstract = True
def get_exam_stats(self): def get_exam_stats(self):
text = f"""{self.name} text = f"""{self.name}
{"-"*len(self.name)} {"-"*len(self.name)}
- Candidates: {int(self.stats_candidates)} - Candidates: {int(self.stats_candidates)}
- Mean: {self.stats_mean:.2f} Median: {self.stats_median} Mode: {self.stats_mode} [Min: {self.stats_min} / Max: {self.stats_max}] - Mean: {self.stats_mean:.2f} Median: {self.stats_median} Mode: {self.stats_mode} [Min: {self.stats_min} / Max: {self.stats_max}]
""" """
return text return text
def __str__(self): def __str__(self):
return self.name return self.name
def get_absolute_url(self): def get_absolute_url(self):
return reverse("{}:exam_overview".format(self.app_name), kwargs={"pk": self.pk}) return reverse("{}:exam_overview".format(self.app_name), kwargs={"pk": self.pk})
def get_take_url(self): def get_take_url(self):
return f"{settings.REMOTE_URL}/rts" return f"{settings.REMOTE_URL}/rts"
def get_exam_name(self): def get_exam_name(self):
return self.name return self.name
def get_json_url(self): def get_json_url(self):
return reverse("{}:exam_json".format(self.app_name), args=(self.pk,)) return reverse("{}:exam_json".format(self.app_name), args=(self.pk,))
def get_question_index(self, question): def get_question_index(self, question):
return list(self.exam_questions.all()).index(question) return list(self.exam_questions.all()).index(question)
def get_authors(self): def get_authors(self):
"""Returns a comma seperated text list of authors""" """Returns a comma seperated text list of authors"""
authors = ", ".join([i.username for i in self.author.all()]) authors = ", ".join([i.username for i in self.author.all()])
return authors return authors
def get_author_objects(self): def get_author_objects(self):
"""Returns a comma seperated text list of authors""" """Returns a comma seperated text list of authors"""
authors = [i for i in self.author.all()] authors = [i for i in self.author.all()]
return authors return authors
def check_cid_user(self, cid, passcode, request=None): def check_cid_user(self, cid, passcode, request=None):
if request is not None and request.user.is_superuser: if request is not None and request.user.is_superuser:
return True return True
if self.valid_users.exists(): if self.valid_users.exists():
user = self.valid_users.filter(cid=cid).first() user = self.valid_users.filter(cid=cid).first()
if not user or user.passcode != passcode: if not user or user.passcode != passcode:
return False return False
return True return True
def get_or_create_cid_user_exam(self, cid_user, start_time=None): def get_or_create_cid_user_exam(self, cid_user, start_time=None):
content_type = ContentType.objects.get_for_model(self) content_type = ContentType.objects.get_for_model(self)
c = CidUserExam.objects.filter( c = CidUserExam.objects.filter(
content_type=content_type, object_id=self.pk, cid_user=cid_user content_type=content_type, object_id=self.pk, cid_user=cid_user
).first() ).first()
print(c) print(c)
if c: if c:
return c return c
if start_time is None: if start_time is None:
start_time = timezone.now() start_time = timezone.now()
new = CidUserExam( new = CidUserExam(
content_type=content_type, content_type=content_type,
object_id=self.pk, object_id=self.pk,
cid_user=cid_user, cid_user=cid_user,
start_time=start_time, start_time=start_time,
) )
new.save() new.save()
return new return new
def get_cid_user_exams(self, cid_user=None): def get_cid_user_exams(self, cid_user=None):
content_type = ContentType.objects.get_for_model(self) content_type = ContentType.objects.get_for_model(self)
if cid_user is None: if cid_user is None:
return CidUserExam.objects.filter( return CidUserExam.objects.filter(
content_type=content_type, content_type=content_type,
object_id=self.pk, object_id=self.pk,
) )
else: else:
return CidUserExam.objects.filter( return CidUserExam.objects.filter(
content_type=content_type, object_id=self.pk, cid_user=cid_user content_type=content_type, object_id=self.pk, cid_user=cid_user
) )
def get_cid_user_score(self, cid_user): def get_cid_user_score(self, cid_user):
c = str(cid_user) c = str(cid_user)
if c in self.user_scores: if c in self.user_scores:
return self.user_scores[c] return self.user_scores[c]
else: else:
return False return False
class NoteType(models.Model): class NoteType(models.Model):
note_type = models.CharField(max_length=200) note_type = models.CharField(max_length=200)
def __str__(self): def __str__(self):
return self.note_type return self.note_type
class QuestionNote(models.Model): class QuestionNote(models.Model):
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField() object_id = models.PositiveIntegerField()
question = GenericForeignKey("content_type", "object_id") question = GenericForeignKey("content_type", "object_id")
# author = models.ForeignKey(User, # author = models.ForeignKey(User,
author = models.CharField(max_length=200, blank=True) author = models.CharField(max_length=200, blank=True)
user_author = models.ForeignKey( user_author = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, blank=True, null=True settings.AUTH_USER_MODEL, on_delete=models.CASCADE, blank=True, null=True
) )
note = models.TextField(blank=True) note = models.TextField(blank=True)
created_on = models.DateTimeField(auto_now_add=True) created_on = models.DateTimeField(auto_now_add=True)
complete = models.BooleanField(default=False) complete = models.BooleanField(default=False)
note_type = models.ForeignKey( note_type = models.ForeignKey(
NoteType, on_delete=models.CASCADE, null=True, blank=True NoteType, on_delete=models.CASCADE, null=True, blank=True
) )
# def get_absolute_url(self): # def get_absolute_url(self):
# self.pk = self.rapid_id # self.pk = self.rapid_id
# return reverse('rapids:question_detail', kwargs={'pk': self.pk}) # return reverse('rapids:question_detail', kwargs={'pk': self.pk})
def __str__(self): def __str__(self):
return "{}: {} [{}] {} / {}".format( return "{}: {} [{}] {} / {}".format(
self.content_type, self.content_type,
self.get_author_str(), self.get_author_str(),
self.created_on, self.created_on,
self.note_type, self.note_type,
self.note, self.note,
) )
def get_absolute_url(self): def get_absolute_url(self):
# TODO: return to question if logged in # TODO: return to question if logged in
return "/" return "/"
def get_object_url(self): def get_object_url(self):
return self.question.get_absolute_url() return self.question.get_absolute_url()
def get_author_str(self): def get_author_str(self):
if self.user_author is not None: if self.user_author is not None:
return self.user_author.username return self.user_author.username
else: else:
return self.author return self.author
def get_short_str(self): def get_short_str(self):
return f"{self.note_type} - {self.note}" return f"{self.note_type} - {self.note}"
class CidUser(models.Model): class CidUser(models.Model):
EXAM_TYPES = ( EXAM_TYPES = (
("Physics", "physics_exams"), ("Physics", "physics_exams"),
("Rapids", "rapid_exams"), ("Rapids", "rapid_exams"),
("SBAs", "sba_exams"), ("SBAs", "sba_exams"),
("Anatomy", "anatomy_exams"), ("Anatomy", "anatomy_exams"),
("Longs", "longs_exams"), ("Longs", "longs_exams"),
) )
cid = models.IntegerField(blank=True, null=True, unique=True) cid = models.IntegerField(blank=True, null=True, unique=True)
passcode = models.CharField(blank=True, max_length=50) passcode = models.CharField(blank=True, max_length=50)
active = models.BooleanField(default=True) active = models.BooleanField(default=True)
internal_candidate = models.BooleanField(default=False) internal_candidate = models.BooleanField(default=False)
name = models.CharField(blank=True, max_length=255) name = models.CharField(blank=True, max_length=255)
email = models.EmailField(blank=True) email = models.EmailField(blank=True)
supervisor_email = models.EmailField(blank=True) supervisor_email = models.EmailField(blank=True)
login_email_sent = models.BooleanField(default=False) login_email_sent = models.BooleanField(default=False)
results_email_sent = models.BooleanField(default=False) results_email_sent = models.BooleanField(default=False)
group = models.ForeignKey("CidUserGroup", on_delete=models.SET_NULL, null=True, blank=True) group = models.ForeignKey("CidUserGroup", on_delete=models.SET_NULL, null=True, blank=True)
def __str__(self) -> str: def __str__(self) -> str:
return str(self.cid) return str(self.cid)
def get_absolute_url(self): def get_absolute_url(self):
return reverse("generic:manage_cids") return reverse("generic:manage_cids")
def check_passcode(self, passcode) -> bool: def check_passcode(self, passcode) -> bool:
return self.passcode == passcode return self.passcode == passcode
def get_cid_exams(self): def get_cid_exams(self):
available_exams = [] available_exams = []
for n, t in self.EXAM_TYPES: for n, t in self.EXAM_TYPES:
exam_rel = getattr(self, t) exam_rel = getattr(self, t)
if exam_rel.exists(): if exam_rel.exists():
exams = exam_rel.filter(exam_mode=True, archive=False).order_by("name") exams = exam_rel.filter(exam_mode=True, archive=False).order_by("name")
available_exams.append((n, exams)) available_exams.append((n, exams))
return available_exams return available_exams
def generate_exam_report(self): def generate_exam_report(self):
exam_list = self.get_cid_exams() exam_list = self.get_cid_exams()
name = "" name = ""
if self.name: if self.name:
name = f"{self.name} " name = f"{self.name} "
exam_text = [f"Candidate {self.cid} {name}[{self.email}]"] exam_text = [f"Candidate {self.cid} {name}[{self.email}]"]
for exam_type, exams in exam_list: for exam_type, exams in exam_list:
exam_text.append(f"{'='*len(exam_type)}\n{exam_type}\n{'='*len(exam_type)}") exam_text.append(f"{'='*len(exam_type)}\n{exam_type}\n{'='*len(exam_type)}")
for exam in exams: for exam in exams:
try: try:
user_score = exam.user_scores[str(self.cid)] user_score = exam.user_scores[str(self.cid)]
except KeyError: except KeyError:
user_score = "Score not generated" user_score = "Score not generated"
t = exam.get_exam_stats() t = exam.get_exam_stats()
try: try:
percentage = ( percentage = (
f" ({user_score / int(exam.stats_max_possible) * 100:.2f}%)" f" ({user_score / int(exam.stats_max_possible) * 100:.2f}%)"
) )
except TypeError: except TypeError:
percentage = "" percentage = ""
exam_text.append( exam_text.append(
f"""{t} f"""{t}
# Score: {user_score} / {int(exam.stats_max_possible)}{percentage} # Score: {user_score} / {int(exam.stats_max_possible)}{percentage}
""" """
) )
msg = "\n\n".join(exam_text) msg = "\n\n".join(exam_text)
return msg return msg
html_msg = f"""""" html_msg = f""""""
def email_results(self, resend=False, additional_emails=[]): def email_results(self, resend=False, additional_emails=[]):
if not self.internal_candidate: if not self.internal_candidate:
return False, "Not internal candidate." return False, "Not internal candidate."
if self.results_email_sent and not resend: if self.results_email_sent and not resend:
return False, "Already sent." return False, "Already sent."
# Get a list of taken exams # Get a list of taken exams
msg = self.generate_exam_report() msg = self.generate_exam_report()
emails = [self.email, self.supervisor_email] emails = [self.email, self.supervisor_email]
if additional_emails: if additional_emails:
emails.extend(additional_emails) emails.extend(additional_emails)
try: try:
send_mail( send_mail(
f"{self.group} results", f"{self.group} results",
msg, msg,
"test@xkjq.uk", "test@xkjq.uk",
emails, emails,
fail_silently=False, fail_silently=False,
# html_message=html_msg, # html_message=html_msg,
) )
except SMTPException as e: except SMTPException as e:
return False, e return False, e
self.results_email_sent = True self.results_email_sent = True
self.save() self.save()
return True, "" return True, ""
def email_details(self, resend=False): def email_details(self, resend=False):
if self.login_email_sent and not resend: if self.login_email_sent and not resend:
return False, "Already sent." return False, "Already sent."
login_url = reverse("cid_selector") login_url = reverse("cid_selector")
msg = f""" msg = f"""
See below for your details for: {self.group} See below for your details for: {self.group}
--------- ---------
CID: {self.cid} CID: {self.cid}
Passcode: {self.passcode} Passcode: {self.passcode}
--------- ---------
You will need these to access to the test system. You will need these to access to the test system.
Direct link to login: Direct link to login:
{settings.REMOTE_URL}{login_url}?cid={self.cid}&passcode={self.passcode} {settings.REMOTE_URL}{login_url}?cid={self.cid}&passcode={self.passcode}
The above details are unique to you. Please do not share as otherwise others will be able to access and submit your results. The above details are unique to you. Please do not share as otherwise others will be able to access and submit your results.
Please note this inbox is not monitored. Direct all enquiries to plh-tr.radiology-academy@nhs.net Please note this inbox is not monitored. Direct all enquiries to plh-tr.radiology-academy@nhs.net
""" """
try: try:
# send_mail( # send_mail(
# "Candidate Details", # "Candidate Details",
# msg, # msg,
# "test@xkjq.uk", # "test@xkjq.uk",
# [self.email], # [self.email],
# fail_silently=False, # fail_silently=False,
# ) # )
email = EmailMessage( email = EmailMessage(
"Candidate Details", "Candidate Details",
msg, msg,
"test@xkjq.uk", "test@xkjq.uk",
[self.email], [self.email],
["ross.kruger@nhs.net"], ["ross.kruger@nhs.net"],
) )
email.send(fail_silently=False) email.send(fail_silently=False)
except SMTPException as e: except SMTPException as e:
return False, e return False, e
self.login_email_sent = True self.login_email_sent = True
self.save() self.save()
return True, "" return True, ""
def get_email_results_url(self): def get_email_results_url(self):
return reverse("generic:candidate_email_results", kwargs={"cid": self.cid}) return reverse("generic:candidate_email_results", kwargs={"cid": self.cid})
def get_email_results_resend_url(self): def get_email_results_resend_url(self):
return reverse("generic:candidate_email_results_resend", kwargs={"cid": self.cid}) return reverse("generic:candidate_email_results_resend", kwargs={"cid": self.cid})
class CidUserExam(models.Model): class CidUserExam(models.Model):
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField() object_id = models.PositiveIntegerField()
exam = GenericForeignKey("content_type", "object_id") exam = GenericForeignKey("content_type", "object_id")
start_time = models.DateTimeField(blank=True, null=True) start_time = models.DateTimeField(blank=True, null=True)
end_time = models.DateTimeField(blank=True, null=True) end_time = models.DateTimeField(blank=True, null=True)
cid_user = models.ForeignKey(CidUser, on_delete=models.CASCADE) cid_user = models.ForeignKey(CidUser, on_delete=models.CASCADE)
manual_submission = models.BooleanField(default=False) manual_submission = models.BooleanField(default=False)
class CidUserGroup(models.Model): class CidUserGroup(models.Model):
name = models.CharField(blank=True, max_length=50) name = models.CharField(blank=True, max_length=50)
archive = models.BooleanField(default=False) archive = models.BooleanField(default=False)
def __str__(self) -> str: def __str__(self) -> str:
return str(self.name) return str(self.name)
def GetGroupUsers(self): def GetGroupUsers(self):
cid_users = self.ciduser_set.all() cid_users = self.ciduser_set.all()
s = ", ".join([f"{user.id} - {user.name}" for user in cid_users]) s = ", ".join([f"{user.id} - {user.name}" for user in cid_users])
return s return s
+1650 -1650
View File
File diff suppressed because it is too large Load Diff
+280 -280
View File
@@ -1,280 +1,280 @@
from django.forms import ( from django.forms import (
Form, Form,
ModelForm, ModelForm,
ModelMultipleChoiceField, ModelMultipleChoiceField,
ModelChoiceField, ModelChoiceField,
ChoiceField, ChoiceField,
CharField, CharField,
) )
from django.forms import inlineformset_factory from django.forms import inlineformset_factory
from longs.models import ( from longs.models import (
# Examination, # Examination,
AnswerMarks, AnswerMarks,
Long, Long,
LongSeries, LongSeries,
LongSeriesImage, LongSeriesImage,
LongCreationDefault, LongCreationDefault,
CidUserAnswer, CidUserAnswer,
Exam, Exam,
) )
from anatomy.models import Modality from anatomy.models import Modality
from generic.models import Examination, Condition, Sign from generic.models import Examination, Condition, Sign
# from generic.models import Examination, Site, Condition, Sign # from generic.models import Examination, Site, Condition, Sign
from django.contrib.admin.widgets import FilteredSelectMultiple from django.contrib.admin.widgets import FilteredSelectMultiple
from django.forms.widgets import RadioSelect, TextInput, Textarea from django.forms.widgets import RadioSelect, TextInput, Textarea
from tinymce.widgets import TinyMCE from tinymce.widgets import TinyMCE
class LongAnswerForm(ModelForm): class LongAnswerForm(ModelForm):
class Meta: class Meta:
model = CidUserAnswer model = CidUserAnswer
fields = ( fields = (
"answer_observations", "answer_observations",
"answer_interpretation", "answer_interpretation",
"answer_principle_diagnosis", "answer_principle_diagnosis",
"answer_differential_diagnosis", "answer_differential_diagnosis",
"answer_management", "answer_management",
) )
class MarkLongQuestionSingleForm(ModelForm): class MarkLongQuestionSingleForm(ModelForm):
class Meta: class Meta:
model = CidUserAnswer model = CidUserAnswer
fields = ["score"] fields = ["score"]
class MarkLongQuestionDoubleForm(ModelForm): class MarkLongQuestionDoubleForm(ModelForm):
class Meta: class Meta:
model = AnswerMarks model = AnswerMarks
fields = ["score", "mark_reason", "candidate_feedback"] fields = ["score", "mark_reason", "candidate_feedback"]
class ExaminationForm(ModelForm): class ExaminationForm(ModelForm):
class Meta: class Meta:
model = Examination model = Examination
fields = ["examination"] fields = ["examination"]
class LongCreationDefaultForm(ModelForm): class LongCreationDefaultForm(ModelForm):
class Meta: class Meta:
model = LongCreationDefault model = LongCreationDefault
# fields = ["site"] # fields = ["site"]
exclude = ["author"] exclude = ["author"]
class LongSeriesForm(ModelForm): class LongSeriesForm(ModelForm):
class Media: class Media:
# Django also includes a few javascript files necessary # Django also includes a few javascript files necessary
# for the operation of this form element. You need to # for the operation of this form element. You need to
# include <script src="/admin/jsi18n"></script> # include <script src="/admin/jsi18n"></script>
# in the template. # in the template.
css = { css = {
"all": ["css/widgets.css"], "all": ["css/widgets.css"],
} }
# Adding this javascript is crucial # Adding this javascript is crucial
js = ["jsi18n.js", "tesseract.min.js"] js = ["jsi18n.js", "tesseract.min.js"]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.user = kwargs.pop( self.user = kwargs.pop(
"user" "user"
) # To get request.user. Do not use kwargs.pop('user', None) due to potential security hole ) # To get request.user. Do not use kwargs.pop('user', None) due to potential security hole
if kwargs.get("instance"): if kwargs.get("instance"):
# We get the 'initial' keyword argument or initialize it # We get the 'initial' keyword argument or initialize it
# as a dict if it didn't exist. # as a dict if it didn't exist.
initial = kwargs.setdefault("initial", {}) initial = kwargs.setdefault("initial", {})
# The widget for a ModelMultipleChoiceField expects # The widget for a ModelMultipleChoiceField expects
# a list of primary key for the selected data. # a list of primary key for the selected data.
initial["long"] = [t.pk for t in kwargs["instance"].long.all()] initial["long"] = [t.pk for t in kwargs["instance"].long.all()]
super(LongSeriesForm, self).__init__(*args, **kwargs) super(LongSeriesForm, self).__init__(*args, **kwargs)
# self.fields["examination"] = ModelChoiceField( # self.fields["examination"] = ModelChoiceField(
# queryset=Examination.objects.all(), # queryset=Examination.objects.all(),
# widget=FilteredSelectMultiple(verbose_name="Examination", is_stacked=False), # widget=FilteredSelectMultiple(verbose_name="Examination", is_stacked=False),
# ) # )
self.fields["modality"] = ModelChoiceField( self.fields["modality"] = ModelChoiceField(
required=True, required=True,
queryset=Modality.objects.all(), queryset=Modality.objects.all(),
) )
ModelForm.__init__(self, *args, **kwargs) ModelForm.__init__(self, *args, **kwargs)
if self.user.groups.filter(name="long_checker").exists(): if self.user.groups.filter(name="long_checker").exists():
long_queryset = Long.objects.all() long_queryset = Long.objects.all()
else: else:
long_queryset = Long.objects.filter( long_queryset = Long.objects.filter(
author__id=self.user.id author__id=self.user.id
) | Long.objects.filter(open_access=True) ) | Long.objects.filter(open_access=True)
self.fields["long"] = ModelMultipleChoiceField( self.fields["long"] = ModelMultipleChoiceField(
required=False, required=False,
queryset=long_queryset, queryset=long_queryset,
widget=FilteredSelectMultiple(verbose_name="Long", is_stacked=False), widget=FilteredSelectMultiple(verbose_name="Long", is_stacked=False),
) )
def save(self, commit=True): def save(self, commit=True):
# Get the unsaved Pizza instance # Get the unsaved Pizza instance
instance = ModelForm.save(self, False) instance = ModelForm.save(self, False)
# Prepare a 'save_m2m' method for the form, # Prepare a 'save_m2m' method for the form,
old_save_m2m = self.save_m2m old_save_m2m = self.save_m2m
def save_m2m(): def save_m2m():
old_save_m2m() old_save_m2m()
# This is where we actually link the pizza with toppings # This is where we actually link the pizza with toppings
instance.long.clear() instance.long.clear()
for long in self.cleaned_data["long"]: for long in self.cleaned_data["long"]:
instance.long.add(long) instance.long.add(long)
self.save_m2m = save_m2m self.save_m2m = save_m2m
# Do we need to save all changes now? # Do we need to save all changes now?
# if commit: # if commit:
instance.save() instance.save()
self.save_m2m() self.save_m2m()
return instance return instance
class Meta: class Meta:
model = LongSeries model = LongSeries
exclude = ["author"] exclude = ["author"]
class LongForm(ModelForm): class LongForm(ModelForm):
# exams = ModelMultipleChoiceField(required=False, queryset=Exam.objects.all(),widget=FilteredSelectMultiple(verbose_name="Exams", is_stacked=False)) # exams = ModelMultipleChoiceField(required=False, queryset=Exam.objects.all(),widget=FilteredSelectMultiple(verbose_name="Exams", is_stacked=False))
class Media: class Media:
# Django also includes a few javascript files necessary # Django also includes a few javascript files necessary
# for the operation of this form element. You need to # for the operation of this form element. You need to
# include <script src="/admin/jsi18n"></script> # include <script src="/admin/jsi18n"></script>
# in the template. # in the template.
css = { css = {
"all": ["css/widgets.css"], "all": ["css/widgets.css"],
} }
# Adding this javascript is crucial # Adding this javascript is crucial
js = ["jsi18n.js", "tesseract.min.js"] js = ["jsi18n.js", "tesseract.min.js"]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.user = kwargs.pop( self.user = kwargs.pop(
"user" "user"
) # To get request.user. Do not use kwargs.pop('user', None) due to potential security hole ) # To get request.user. Do not use kwargs.pop('user', None) due to potential security hole
if kwargs.get("instance"): if kwargs.get("instance"):
# We get the 'initial' keyword argument or initialize it # We get the 'initial' keyword argument or initialize it
# as a dict if it didn't exist. # as a dict if it didn't exist.
initial = kwargs.setdefault("initial", {}) initial = kwargs.setdefault("initial", {})
# The widget for a ModelMultipleChoiceField expects # The widget for a ModelMultipleChoiceField expects
# a list of primary key for the selected data. # a list of primary key for the selected data.
initial["exams"] = [t.pk for t in kwargs["instance"].exams.all()] initial["exams"] = [t.pk for t in kwargs["instance"].exams.all()]
ModelForm.__init__(self, *args, **kwargs) ModelForm.__init__(self, *args, **kwargs)
super(LongForm, self).__init__(*args, **kwargs) super(LongForm, self).__init__(*args, **kwargs)
if self.user.groups.filter(name="long_checker").exists(): if self.user.groups.filter(name="long_checker").exists():
exam_queryset = Exam.objects.all() exam_queryset = Exam.objects.all()
else: else:
exam_queryset = Exam.objects.filter( exam_queryset = Exam.objects.filter(
author__id=self.user.id author__id=self.user.id
) | Exam.objects.filter(open_access=True) ) | Exam.objects.filter(open_access=True)
self.fields["exams"] = ModelMultipleChoiceField( self.fields["exams"] = ModelMultipleChoiceField(
required=False, required=False,
queryset=exam_queryset, queryset=exam_queryset,
widget=FilteredSelectMultiple(verbose_name="Exams", is_stacked=False), widget=FilteredSelectMultiple(verbose_name="Exams", is_stacked=False),
) )
class Meta: class Meta:
model = Long model = Long
# fields = ['due_back'] # fields = ['due_back']
# fields = '__all__' # fields = '__all__'
fields = [ fields = [
# "examination", # "examination",
# "site", # "site",
"description", "description",
"history", "history",
"feedback", "feedback",
"condition", "condition",
"sign", "sign",
"model_observations", "model_observations",
"model_interpretation", "model_interpretation",
"model_principle_diagnosis", "model_principle_diagnosis",
"model_differential_diagnosis", "model_differential_diagnosis",
"model_management", "model_management",
"mark_scheme", "mark_scheme",
"open_access", "open_access",
] ]
# fields = ['question', 'feedback', 'subspecialty', 'references'] # fields = ['question', 'feedback', 'subspecialty', 'references']
widgets = { widgets = {
# "normal": RadioSelect( # "normal": RadioSelect(
# choices=[(True, 'Yes'), # choices=[(True, 'Yes'),
# (False, 'No')]) # (False, 'No')])
"feedback": TinyMCE(attrs={"cols": 80, "rows": 20}), "feedback": TinyMCE(attrs={"cols": 80, "rows": 20}),
"mark_scheme": TinyMCE(attrs={"cols": 80, "rows": 30}), "mark_scheme": TinyMCE(attrs={"cols": 80, "rows": 30}),
"description": Textarea(attrs={"cols": 80, "rows": 5}), "description": Textarea(attrs={"cols": 80, "rows": 5}),
"history": Textarea(attrs={"cols": 80, "rows": 5}), "history": Textarea(attrs={"cols": 80, "rows": 5}),
"model_observations": Textarea(attrs={"cols": 80, "rows": 5}), "model_observations": Textarea(attrs={"cols": 80, "rows": 5}),
"model_interpretation": Textarea(attrs={"cols": 80, "rows": 5}), "model_interpretation": Textarea(attrs={"cols": 80, "rows": 5}),
"model_principle_diagnosis": Textarea(attrs={"cols": 80, "rows": 5}), "model_principle_diagnosis": Textarea(attrs={"cols": 80, "rows": 5}),
"model_differential_diagnosis": Textarea(attrs={"cols": 80, "rows": 5}), "model_differential_diagnosis": Textarea(attrs={"cols": 80, "rows": 5}),
"model_management": Textarea(attrs={"cols": 80, "rows": 5}), "model_management": Textarea(attrs={"cols": 80, "rows": 5}),
} }
def save(self, commit=True): def save(self, commit=True):
instance = ModelForm.save(self, False) instance = ModelForm.save(self, False)
instance.save() instance.save()
old_exams = instance.exams.all() old_exams = instance.exams.all()
new_exams = self.cleaned_data["exams"] new_exams = self.cleaned_data["exams"]
for exam in old_exams: for exam in old_exams:
if exam not in new_exams: if exam not in new_exams:
exam.exam_questions.remove(instance) exam.exam_questions.remove(instance)
for exam in new_exams: for exam in new_exams:
exam.exam_questions.add(instance) exam.exam_questions.add(instance)
self.save_m2m() self.save_m2m()
return instance return instance
SeriesFormSet = inlineformset_factory( SeriesFormSet = inlineformset_factory(
Long, Long,
LongSeries.long.through, LongSeries.long.through,
exclude=[], exclude=[],
can_delete=True, can_delete=True,
extra=0, extra=0,
max_num=10, max_num=10,
) )
LongSeriesImageFormSet = inlineformset_factory( LongSeriesImageFormSet = inlineformset_factory(
LongSeries, LongSeries,
LongSeriesImage, LongSeriesImage,
exclude=[], exclude=[],
can_delete=True, can_delete=True,
extra=0, extra=0,
max_num=2000, max_num=2000,
field_classes="testing", field_classes="testing",
) )
class ExamForm(ModelForm): class ExamForm(ModelForm):
class Meta: class Meta:
model = Exam model = Exam
fields = [ fields = [
"name", "name",
"time_limit", "time_limit",
"open_access", "open_access",
"exam_mode", "exam_mode",
"active", "active",
"publish_results", "publish_results",
"double_mark", "double_mark",
"archive", "archive",
] ]
+32 -32
View File
@@ -1,33 +1,33 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block title %} {% block title %}
Longs Longs
{% endblock %} {% endblock %}
{% block css %} {% block css %}
{% endblock %} {% endblock %}
{% block js %} {% block js %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% endblock %} {% endblock %}
{% block navigation %} {% block navigation %}
Longs: Longs:
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<a href="{% url 'longs:exam_list' %}">Exams</a> / <a href="{% url 'longs:exam_list' %}">Exams</a> /
<a href="{% url 'longs:exam_create' %}" title="Create a new exam">Create Exam</a> / <a href="{% url 'longs:exam_create' %}" title="Create a new exam">Create Exam</a> /
<a href="{% url 'longs:long_view' %}">Cases</a> / <a href="{% url 'longs:long_view' %}">Cases</a> /
<a href="{% url 'longs:long_series_view' %}">Series</a> / <a href="{% url 'longs:long_series_view' %}">Series</a> /
<a href="{% url 'longs:long_create' %}" title="Create a new long case">Create Case</a> / <a href="{% url 'longs:long_create' %}" title="Create a new long case">Create Case</a> /
<a href="{% url 'longs:long_series_create' %}" title="Create a new image series">Create Series</a> <a href="{% url 'longs:long_series_create' %}" title="Create a new image series">Create Series</a>
{% endif %} {% endif %}
{% if request.user.is_superuser %} {% if request.user.is_superuser %}
/ <a href="{% url 'longs:user_answer_table_view' %}" title="User answers">Answers</a> / <a href="{% url 'longs:user_answer_table_view' %}" title="User answers">Answers</a>
{% endif %} {% endif %}
{% comment %} </br> {% comment %} </br>
Questions by: Questions by:
<span id="authors-link"><a href="{% url 'longs:author_list' %}">author</a></span> {% endcomment %} <span id="authors-link"><a href="{% url 'longs:author_list' %}">author</a></span> {% endcomment %}
{% endblock %} {% endblock %}
+1339 -1339
View File
File diff suppressed because it is too large Load Diff
+268 -268
View File
@@ -1,268 +1,268 @@
""" """
Django settings for rad project. Django settings for rad project.
Generated by 'django-admin startproject' using Django 1.11.14. Generated by 'django-admin startproject' using Django 1.11.14.
For more information on this file, see For more information on this file, see
https://docs.djangoproject.com/en/1.11/topics/settings/ https://docs.djangoproject.com/en/1.11/topics/settings/
For the full list of settings and their values, see For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.11/ref/settings/ https://docs.djangoproject.com/en/1.11/ref/settings/
""" """
import os import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
with open("/etc/secret_key") as f: with open("/etc/secret_key") as f:
SECRET_KEY = f.read().strip() SECRET_KEY = f.read().strip()
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = True
ALLOWED_HOSTS = [ ALLOWED_HOSTS = [
"localhost", "localhost",
"127.0.0.1", "127.0.0.1",
"161.35.163.87", "161.35.163.87",
"penracourses.org.uk", "penracourses.org.uk",
"www.penracourses.org.uk", "www.penracourses.org.uk",
] ]
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
"generic", "generic",
"anatomy", "anatomy",
"physics", "physics",
"rapids", "rapids",
"longs", "longs",
"sbas", "sbas",
"wally", "wally",
"atlas", "atlas",
"reversion", "reversion",
"django_tables2", "django_tables2",
"django_filters", "django_filters",
"easy_thumbnails", "easy_thumbnails",
"filer", "filer",
"mptt", "mptt",
"sortedm2m", "sortedm2m",
"dal", "dal",
"dal_select2", "dal_select2",
"dal_queryset_sequence", "dal_queryset_sequence",
"django.contrib.admin", "django.contrib.admin",
"django.contrib.auth", "django.contrib.auth",
"django.contrib.contenttypes", "django.contrib.contenttypes",
"django.contrib.sessions", "django.contrib.sessions",
"django.contrib.messages", "django.contrib.messages",
"django.contrib.staticfiles", "django.contrib.staticfiles",
"debug_toolbar", "debug_toolbar",
"tagulous", "tagulous",
"dbbackup", "dbbackup",
"rest_framework", "rest_framework",
"tinymce", "tinymce",
] ]
MIDDLEWARE = [ MIDDLEWARE = [
"corsheaders.middleware.CorsMiddleware", "corsheaders.middleware.CorsMiddleware",
"django.middleware.security.SecurityMiddleware", "django.middleware.security.SecurityMiddleware",
"django.middleware.csrf.CsrfViewMiddleware", "django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware", "django.middleware.common.CommonMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware", "django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware",
"reversion.middleware.RevisionMiddleware", "reversion.middleware.RevisionMiddleware",
"debug_toolbar.middleware.DebugToolbarMiddleware", "debug_toolbar.middleware.DebugToolbarMiddleware",
] ]
ROOT_URLCONF = "rad.urls" ROOT_URLCONF = "rad.urls"
TEMPLATES = [ TEMPLATES = [
{ {
"BACKEND": "django.template.backends.django.DjangoTemplates", "BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [os.path.join(BASE_DIR, "templates")], "DIRS": [os.path.join(BASE_DIR, "templates")],
"APP_DIRS": True, "APP_DIRS": True,
"OPTIONS": { "OPTIONS": {
"context_processors": [ "context_processors": [
"django.template.context_processors.debug", "django.template.context_processors.debug",
"django.template.context_processors.request", "django.template.context_processors.request",
"django.contrib.auth.context_processors.auth", "django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages", "django.contrib.messages.context_processors.messages",
"django.template.context_processors.static", "django.template.context_processors.static",
], ],
}, },
}, },
] ]
WSGI_APPLICATION = "rad.wsgi.application" WSGI_APPLICATION = "rad.wsgi.application"
# Database # Database
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases # https://docs.djangoproject.com/en/1.11/ref/settings/#databases
DATABASES = { DATABASES = {
"default": { "default": {
"ENGINE": "django.db.backends.postgresql", "ENGINE": "django.db.backends.postgresql",
"NAME": "rad", "NAME": "rad",
"USER": "django", "USER": "django",
"PASSWORD": "f7bf31dc9bda1256ea827953480d1917", "PASSWORD": "f7bf31dc9bda1256ea827953480d1917",
# "HOST": "xkjq.uk", # "HOST": "xkjq.uk",
"HOST": "161.35.163.87", "HOST": "161.35.163.87",
"PORT": "5432", "PORT": "5432",
} }
} }
# Password validation # Password validation
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [ AUTH_PASSWORD_VALIDATORS = [
{ {
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
}, },
{ {
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
}, },
{ {
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
}, },
{ {
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
}, },
] ]
SERIALIZATION_MODULES = { SERIALIZATION_MODULES = {
"xml": "tagulous.serializers.xml_serializer", "xml": "tagulous.serializers.xml_serializer",
"json": "tagulous.serializers.json", "json": "tagulous.serializers.json",
"python": "tagulous.serializers.python", "python": "tagulous.serializers.python",
"yaml": "tagulous.serializers.pyyaml", "yaml": "tagulous.serializers.pyyaml",
} }
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/1.11/topics/i18n/ # https://docs.djangoproject.com/en/1.11/topics/i18n/
LANGUAGE_CODE = "en-us" LANGUAGE_CODE = "en-us"
TIME_ZONE = "Europe/London" TIME_ZONE = "Europe/London"
USE_I18N = True USE_I18N = True
USE_L10N = True USE_L10N = True
USE_TZ = True USE_TZ = True
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.11/howto/static-files/ # https://docs.djangoproject.com/en/1.11/howto/static-files/
SECURE_SSL_REDIRECT = True SECURE_SSL_REDIRECT = True
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
STATIC_URL = "/static/" STATIC_URL = "/static/"
STATIC_ROOT = "static/" STATIC_ROOT = "static/"
REMOTE_URL = "https://www.penracourses.org.uk" REMOTE_URL = "https://www.penracourses.org.uk"
# #
# if DEBUG: # if DEBUG:
# STATIC_URL = REMOTE_URL + STATIC_URL # STATIC_URL = REMOTE_URL + STATIC_URL
MEDIA_URL = "/media/" MEDIA_URL = "/media/"
MEDIA_ROOT = "/home/django/rad/media/" MEDIA_ROOT = "/home/django/rad/media/"
# if DEBUG: # if DEBUG:
# MEDIA_URL = REMOTE_URL + MEDIA_URL # MEDIA_URL = REMOTE_URL + MEDIA_URL
# Redirect to home URL after login (Default redirects to /accounts/profile/) # Redirect to home URL after login (Default redirects to /accounts/profile/)
LOGIN_REDIRECT_URL = "/" LOGIN_REDIRECT_URL = "/"
LOGOUT_REDIRECT_URL = "/" LOGOUT_REDIRECT_URL = "/"
TAGULOUS_AUTOCOMPLETE_JS = ( TAGULOUS_AUTOCOMPLETE_JS = (
"tagulous/lib/jquery.js", "tagulous/lib/jquery.js",
"tagulous/lib/select2-3/select2.min.js", "tagulous/lib/select2-3/select2.min.js",
"tagulous/tagulous.js", "tagulous/tagulous.js",
"tagulous/adaptor/select2-3.js", "tagulous/adaptor/select2-3.js",
) )
TAGULOUS_AUTOCOMPLETE_CSS = {"all": ["tagulous/lib/select2-3/select2.css"]} TAGULOUS_AUTOCOMPLETE_CSS = {"all": ["tagulous/lib/select2-3/select2.css"]}
INTERNAL_IPS = ["localhost", "127.0.0.1"] INTERNAL_IPS = ["localhost", "127.0.0.1"]
LOGGING = { LOGGING = {
"version": 1, "version": 1,
"disable_existing_loggers": False, "disable_existing_loggers": False,
"incremental": True, "incremental": True,
"root": { "root": {
"level": "DEBUG", "level": "DEBUG",
}, },
} }
CORS_ORIGIN_ALLOW_ALL = True CORS_ORIGIN_ALLOW_ALL = True
CACHES = { CACHES = {
"default": { "default": {
"BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache", "BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache",
"LOCATION": "127.0.0.1:11211", "LOCATION": "127.0.0.1:11211",
#'OPTIONS': { #'OPTIONS': {
# 'server_max_value_length': 1024 * 1024 * 10 # 'server_max_value_length': 1024 * 1024 * 10
# } # }
}, },
"filesystem": { "filesystem": {
"BACKEND": "django.core.cache.backends.filebased.FileBasedCache", "BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
"LOCATION": "/var/tmp/django_cache", "LOCATION": "/var/tmp/django_cache",
}, },
} }
THUMBNAIL_ALIASES = { THUMBNAIL_ALIASES = {
"": { "": {
"exam-list": {"size": (50, 50), "crop": True}, "exam-list": {"size": (50, 50), "crop": True},
}, },
} }
DATA_UPLOAD_MAX_NUMBER_FIELDS = 50000 DATA_UPLOAD_MAX_NUMBER_FIELDS = 50000
THUMBNAIL_SOURCE_GENERATORS = ( THUMBNAIL_SOURCE_GENERATORS = (
"easy_thumbnails.source_generators.pil_image", "easy_thumbnails.source_generators.pil_image",
"helpers.images.pil_dicom_image", "helpers.images.pil_dicom_image",
) )
DBBACKUP_STORAGE = "django.core.files.storage.FileSystemStorage" DBBACKUP_STORAGE = "django.core.files.storage.FileSystemStorage"
DBBACKUP_STORAGE_OPTIONS = {"location": os.path.join(BASE_DIR, "../backups")} DBBACKUP_STORAGE_OPTIONS = {"location": os.path.join(BASE_DIR, "../backups")}
DEFAULT_FILE_STORAGE = ( DEFAULT_FILE_STORAGE = (
"django_hashedfilenamestorage.storage.HashedFilenameFileSystemStorage" "django_hashedfilenamestorage.storage.HashedFilenameFileSystemStorage"
) )
TINYMCE_DEFAULT_CONFIG = { TINYMCE_DEFAULT_CONFIG = {
"theme": "silver", "theme": "silver",
"height": 500, "height": 500,
"menubar": False, "menubar": False,
"plugins": "advlist,autolink,lists,link,image,charmap,print,preview,anchor," "plugins": "advlist,autolink,lists,link,image,charmap,print,preview,anchor,"
"searchreplace,visualblocks,code,fullscreen,insertdatetime,media,table,paste," "searchreplace,visualblocks,code,fullscreen,insertdatetime,media,table,paste,"
"code,help,wordcount", "code,help,wordcount",
"toolbar": "undo redo | formatselect | code" "toolbar": "undo redo | formatselect | code"
"bold italic backcolor | alignleft aligncenter " "bold italic backcolor | alignleft aligncenter "
"alignright alignjustify | bullist numlist outdent indent | " "alignright alignjustify | bullist numlist outdent indent | "
"removeformat | code | help", "removeformat | code | help",
"skin": "oxide-dark", "skin": "oxide-dark",
"menubar": "tools", "menubar": "tools",
"content_css": "dark", "content_css": "dark",
} }
DEFAULT_AUTO_FIELD = "django.db.models.AutoField" DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = "smtp.office365.com " EMAIL_HOST = "smtp.office365.com "
EMAIL_HOST_USER = "Penra.courses@nhs.net" EMAIL_HOST_USER = "Penra.courses@nhs.net"
EMAIL_HOST_PASSWORD = '"got3<}%o"~\J",\Di3<' EMAIL_HOST_PASSWORD = '"got3<}%o"~\J",\Di3<'
EMAIL_PORT = 587 EMAIL_PORT = 587
EMAIL_USE_TLS = True EMAIL_USE_TLS = True
EMAIL_USE_SSl = False EMAIL_USE_SSl = False
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
try: try:
from .settings_local import * from .settings_local import *
except ImportError: except ImportError:
pass pass
+241 -241
View File
@@ -1,241 +1,241 @@
from django.forms import ( from django.forms import (
Form, Form,
ModelForm, ModelForm,
ModelMultipleChoiceField, ModelMultipleChoiceField,
ModelChoiceField, ModelChoiceField,
ChoiceField, ChoiceField,
CharField, CharField,
) )
from django.forms import inlineformset_factory from django.forms import inlineformset_factory
from rapids.models import ( from rapids.models import (
Abnormality, Abnormality,
Examination, Examination,
Rapid, Rapid,
RapidCreationDefault, RapidCreationDefault,
RapidImage, RapidImage,
Region, Region,
Answer, Answer,
CidUserAnswer, CidUserAnswer,
Exam, Exam,
) )
from django.contrib.admin.widgets import FilteredSelectMultiple from django.contrib.admin.widgets import FilteredSelectMultiple
from django.forms.widgets import RadioSelect, TextInput, Textarea from django.forms.widgets import RadioSelect, TextInput, Textarea
class RapidAnswerForm(ModelForm): class RapidAnswerForm(ModelForm):
class Meta: class Meta:
model = CidUserAnswer model = CidUserAnswer
fields = ("answer",) fields = ("answer",)
class MarkRapidQuestionForm(Form): class MarkRapidQuestionForm(Form):
# correct = forms.CharField(required=False) # correct = forms.CharField(required=False)
# half_correct = forms.CharField(required=False) # half_correct = forms.CharField(required=False)
# incorrect = forms.CharField(required=False) # incorrect = forms.CharField(required=False)
marked_answers = CharField(required=False) marked_answers = CharField(required=False)
class ExaminationForm(ModelForm): class ExaminationForm(ModelForm):
class Meta: class Meta:
model = Examination model = Examination
fields = ["examination"] fields = ["examination"]
class RegionForm(ModelForm): class RegionForm(ModelForm):
class Meta: class Meta:
model = Region model = Region
fields = ["name"] fields = ["name"]
class AbnormalityForm(ModelForm): class AbnormalityForm(ModelForm):
class Meta: class Meta:
model = Abnormality model = Abnormality
fields = ["name"] fields = ["name"]
class RapidCreationDefaultForm(ModelForm): class RapidCreationDefaultForm(ModelForm):
class Meta: class Meta:
model = RapidCreationDefault model = RapidCreationDefault
# fields = ["site"] # fields = ["site"]
exclude = ["author"] exclude = ["author"]
class RapidForm(ModelForm): class RapidForm(ModelForm):
# exams = ModelMultipleChoiceField(required=False, queryset=Exam.objects.all(),widget=FilteredSelectMultiple(verbose_name="Exams", is_stacked=False)) # exams = ModelMultipleChoiceField(required=False, queryset=Exam.objects.all(),widget=FilteredSelectMultiple(verbose_name="Exams", is_stacked=False))
class Media: class Media:
# Django also includes a few javascript files necessary # Django also includes a few javascript files necessary
# for the operation of this form element. You need to # for the operation of this form element. You need to
# include <script src="/admin/jsi18n"></script> # include <script src="/admin/jsi18n"></script>
# in the template. # in the template.
css = { css = {
"all": ["css/widgets.css"], "all": ["css/widgets.css"],
} }
# Adding this javascript is crucial # Adding this javascript is crucial
js = ["jsi18n.js", "tesseract.min.js"] js = ["jsi18n.js", "tesseract.min.js"]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.user = kwargs.pop( self.user = kwargs.pop(
"user" "user"
) # To get request.user. Do not use kwargs.pop('user', None) due to potential security hole ) # To get request.user. Do not use kwargs.pop('user', None) due to potential security hole
if kwargs.get("instance"): if kwargs.get("instance"):
# We get the 'initial' keyword argument or initialize it # We get the 'initial' keyword argument or initialize it
# as a dict if it didn't exist. # as a dict if it didn't exist.
initial = kwargs.setdefault("initial", {}) initial = kwargs.setdefault("initial", {})
# The widget for a ModelMultipleChoiceField expects # The widget for a ModelMultipleChoiceField expects
# a list of primary key for the selected data. # a list of primary key for the selected data.
initial["exams"] = [t.pk for t in kwargs["instance"].exams.all()] initial["exams"] = [t.pk for t in kwargs["instance"].exams.all()]
ModelForm.__init__(self, *args, **kwargs) ModelForm.__init__(self, *args, **kwargs)
super(RapidForm, self).__init__(*args, **kwargs) super(RapidForm, self).__init__(*args, **kwargs)
# self.fields['question'].widget.attrs = {'class': 'question-form', 'rows': 10, 'cols': 100} # self.fields['question'].widget.attrs = {'class': 'question-form', 'rows': 10, 'cols': 100}
# self.fields['feedback'].widget.attrs = {'class': 'feedback-form', 'rows': 10, 'cols': 100} # self.fields['feedback'].widget.attrs = {'class': 'feedback-form', 'rows': 10, 'cols': 100}
self.fields["abnormality"] = ModelMultipleChoiceField( self.fields["abnormality"] = ModelMultipleChoiceField(
required=False, required=False,
queryset=Abnormality.objects.all(), queryset=Abnormality.objects.all(),
widget=FilteredSelectMultiple(verbose_name="Abnormality", is_stacked=False), widget=FilteredSelectMultiple(verbose_name="Abnormality", is_stacked=False),
) )
self.fields["region"] = ModelMultipleChoiceField( self.fields["region"] = ModelMultipleChoiceField(
required=False, required=False,
queryset=Region.objects.all(), queryset=Region.objects.all(),
widget=FilteredSelectMultiple(verbose_name="Region", is_stacked=False), widget=FilteredSelectMultiple(verbose_name="Region", is_stacked=False),
) )
self.fields["examination"] = ModelMultipleChoiceField( self.fields["examination"] = ModelMultipleChoiceField(
queryset=Examination.objects.all(), queryset=Examination.objects.all(),
widget=FilteredSelectMultiple(verbose_name="Examination", is_stacked=False), widget=FilteredSelectMultiple(verbose_name="Examination", is_stacked=False),
) )
self.fields["laterality"] = ChoiceField( self.fields["laterality"] = ChoiceField(
choices=Rapid.LATERALITY_CHOICES, required=False, widget=RadioSelect() choices=Rapid.LATERALITY_CHOICES, required=False, widget=RadioSelect()
) )
if self.user.groups.filter(name="rapid_checker").exists(): if self.user.groups.filter(name="rapid_checker").exists():
exam_queryset = Exam.objects.all() exam_queryset = Exam.objects.all()
else: else:
exam_queryset = Exam.objects.filter( exam_queryset = Exam.objects.filter(
author__id=self.user.id author__id=self.user.id
) | Exam.objects.filter(open_access=True) ) | Exam.objects.filter(open_access=True)
self.fields["exams"] = ModelMultipleChoiceField( self.fields["exams"] = ModelMultipleChoiceField(
required=False, required=False,
queryset=exam_queryset, queryset=exam_queryset,
widget=FilteredSelectMultiple(verbose_name="Exams", is_stacked=False), widget=FilteredSelectMultiple(verbose_name="Exams", is_stacked=False),
) )
def save(self, commit=True): def save(self, commit=True):
instance = ModelForm.save(self, False) instance = ModelForm.save(self, False)
instance.save() instance.save()
old_exams = instance.exams.all() old_exams = instance.exams.all()
new_exams = self.cleaned_data["exams"] new_exams = self.cleaned_data["exams"]
for exam in old_exams: for exam in old_exams:
if exam not in new_exams: if exam not in new_exams:
exam.exam_questions.remove(instance) exam.exam_questions.remove(instance)
for exam in new_exams: for exam in new_exams:
exam.exam_questions.add(instance) exam.exam_questions.add(instance)
#exam.save() #exam.save()
# # Prepare a 'save_m2m' method for the form, # # Prepare a 'save_m2m' method for the form,
# old_save_m2m = self.save_m2m # old_save_m2m = self.save_m2m
# #
# def save_m2m(): # def save_m2m():
# old_save_m2m() # old_save_m2m()
# # This is where we actually link the pizza with toppings # # This is where we actually link the pizza with toppings
# instance.exams.clear() # instance.exams.clear()
# for exam in self.cleaned_data["exams"]: # for exam in self.cleaned_data["exams"]:
# instance.exams.add(exam) # instance.exams.add(exam)
# #
# self.save_m2m = save_m2m # self.save_m2m = save_m2m
# #
# # Do we need to save all changes now? # # Do we need to save all changes now?
# # if commit: # # if commit:
# instance.save() # instance.save()
self.save_m2m() self.save_m2m()
return instance return instance
class Meta: class Meta:
model = Rapid model = Rapid
# fields = ['due_back'] # fields = ['due_back']
# fields = '__all__' # fields = '__all__'
fields = [ fields = [
"normal", "normal",
"abnormality", "abnormality",
"region", "region",
"laterality", "laterality",
"examination", "examination",
# "site", # "site",
"feedback", "feedback",
"open_access", "open_access",
] ]
# fields = ['question', 'feedback', 'subspecialty', 'references'] # fields = ['question', 'feedback', 'subspecialty', 'references']
widgets = { widgets = {
# "normal": RadioSelect( # "normal": RadioSelect(
# choices=[(True, 'Yes'), # choices=[(True, 'Yes'),
# (False, 'No')]) # (False, 'No')])
} }
ImageFormSet = inlineformset_factory( ImageFormSet = inlineformset_factory(
Rapid, Rapid,
RapidImage, RapidImage,
fields=["image", "feedback_image"], fields=["image", "feedback_image"],
exclude=[], exclude=[],
can_delete=True, can_delete=True,
extra=1, extra=1,
max_num=10, max_num=10,
field_classes="testing", field_classes="testing",
) )
AnswerFormSet = inlineformset_factory( AnswerFormSet = inlineformset_factory(
Rapid, Rapid,
Answer, Answer,
fields=["answer", "status"], fields=["answer", "status"],
widgets={ widgets={
"answer": Textarea( "answer": Textarea(
attrs={"required": "false", "minlength": 2, "maxlength": 500, "rows": 3} attrs={"required": "false", "minlength": 2, "maxlength": 500, "rows": 3}
) )
}, },
exclude=[], exclude=[],
can_delete=True, can_delete=True,
extra=0, extra=0,
max_num=10, max_num=10,
) )
AnswerUpdateFormSet = inlineformset_factory( AnswerUpdateFormSet = inlineformset_factory(
Rapid, Rapid,
Answer, Answer,
fields=["answer", "status"], fields=["answer", "status"],
widgets={ widgets={
"answer": Textarea( "answer": Textarea(
attrs={"required": "false", "minlength": 2, "maxlength": 500, "rows": 3} attrs={"required": "false", "minlength": 2, "maxlength": 500, "rows": 3}
) )
}, },
exclude=[], exclude=[],
can_delete=True, can_delete=True,
extra=0, extra=0,
max_num=10, max_num=10,
) )
# This should be made generic? # This should be made generic?
class ExamForm(ModelForm): class ExamForm(ModelForm):
class Meta: class Meta:
model = Exam model = Exam
fields = [ fields = [
"name", "name",
"time_limit", "time_limit",
"open_access", "open_access",
"exam_mode", "exam_mode",
"active", "active",
"publish_results", "publish_results",
"archive", "archive",
] ]
+19 -19
View File
@@ -1,20 +1,20 @@
{% extends 'rapids/exams.html' %} {% extends 'rapids/exams.html' %}
{% block content %} {% block content %}
<div class="rapids"> <div class="rapids">
<h2>Marking exam: {{ exam.name }}</h2> <h2>Marking exam: {{ exam.name }}</h2>
You can start marking from a particular question by clicking on it below. You can start marking from a particular question by clicking on it below.
<div> <div>
<button class="show-all-button">Show all</button><button class="show-unmarked-button">Show unmarked</button> <button class="show-all-button">Show all</button><button class="show-unmarked-button">Show unmarked</button>
</div> </div>
<div id="stark-marking-button"><a href="{% url 'rapids:mark' exam_pk=exam.pk sk=0 %}"><button>Click here to start marking</button></a></div> <div id="stark-marking-button"><a href="{% url 'rapids:mark' exam_pk=exam.pk sk=0 %}"><button>Click here to start marking</button></a></div>
<ul id="question-mark-list"> <ul id="question-mark-list">
{% for question, unmarked_count, unmarked_count2 in question_unmarked_map %} {% for question, unmarked_count, unmarked_count2 in question_unmarked_map %}
<li data-markcount={{unmarked_count}} {% if unmarked_count %}class="unmarked" {% endif %}><a href="{% url 'rapids:mark' exam_pk=exam.pk sk=forloop.counter0 %}">Question {{forloop.counter }}: <li data-markcount={{unmarked_count}} {% if unmarked_count %}class="unmarked" {% endif %}><a href="{% url 'rapids:mark' exam_pk=exam.pk sk=forloop.counter0 %}">Question {{forloop.counter }}:
{{ question }}</a><br />Unmarked answers: {{unmarked_count}}</li> {{ question }}</a><br />Unmarked answers: {{unmarked_count}}</li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
{% endblock %} {% endblock %}
+1290 -1290
View File
File diff suppressed because it is too large Load Diff
+16 -16
View File
@@ -1,16 +1,16 @@
{% extends "sbas/base.html" %} {% extends "sbas/base.html" %}
{% block js %} {% block js %}
{{ form.media }} {{ form.media }}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<h2>Add Exam</h2> <h2>Add Exam</h2>
<form action="" method="post"> <form action="" method="post">
{% csrf_token %} {% csrf_token %}
<table> <table>
{{ form.as_table }} {{ form.as_table }}
</table> </table>
<input type="submit" value="Submit"> <input type="submit" value="Submit">
</form> </form>
{% endblock %} {% endblock %}
+77 -77
View File
@@ -1,77 +1,77 @@
from django.urls import path, include from django.urls import path, include
from . import views from . import views
from django.views.decorators.cache import cache_page from django.views.decorators.cache import cache_page
app_name = "sbas" app_name = "sbas"
urlpatterns = [ urlpatterns = [
# path('', views.question_list, name='question_list'), # path('', views.question_list, name='question_list'),
path("", views.GenericExamViews.index, name="index"), path("", views.GenericExamViews.index, name="index"),
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/<int:pk>/", views.question_detail, name="question_detail"),
# path("question/create/", views.QuestionCreate.as_view(), name="anatomy_question_create"), # 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("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>/<int:sk>/mark", views.mark, name="mark"),
# path("exam/<int:pk>/mark", views.mark_overview, name="mark_overview"), # path("exam/<int:pk>/mark", views.mark_overview, name="mark_overview"),
path( path(
"exam/<int:pk>/<int:sk>/<str:cid>/<str:passcode>/take", "exam/<int:pk>/<int:sk>/<str:cid>/<str:passcode>/take",
views.exam_take, views.exam_take,
name="exam_take", name="exam_take",
), ),
path("exam/<int:pk>/start", views.exam_start, name="exam_start"), path("exam/<int:pk>/start", views.exam_start, name="exam_start"),
path( path(
"exam/<int:pk>/<str:cid>/<str:passcode>/finish", "exam/<int:pk>/<str:cid>/<str:passcode>/finish",
views.exam_take_overview, views.exam_take_overview,
name="exam_take_overview", name="exam_take_overview",
), ),
path("exam/<int:pk>/", views.GenericExamViews.exam_overview, name="exam_overview"), path("exam/<int:pk>/", views.GenericExamViews.exam_overview, name="exam_overview"),
path( path(
"exam/<int:pk>/scores", "exam/<int:pk>/scores",
views.GenericExamViews.exam_scores_cid, views.GenericExamViews.exam_scores_cid,
name="exam_scores_cid", name="exam_scores_cid",
), ),
path( path(
"exam/<int:pk>/scores/<int:cid>/<str:passcode>/", "exam/<int:pk>/scores/<int:cid>/<str:passcode>/",
views.exam_scores_cid_user, views.exam_scores_cid_user,
name="exam_scores_cid_user", name="exam_scores_cid_user",
), ),
path( path(
"exam/<int:pk>/toggle_active", "exam/<int:pk>/toggle_active",
views.GenericExamViews.exam_toggle_active, views.GenericExamViews.exam_toggle_active,
name="exam_toggle_active", name="exam_toggle_active",
), ),
path( path(
"exam/<int:pk>/toggle_results_published", "exam/<int:pk>/toggle_results_published",
views.GenericExamViews.exam_toggle_results_published, views.GenericExamViews.exam_toggle_results_published,
name="exam_toggle_results_published", name="exam_toggle_results_published",
), ),
# path("exam/submit", views.GenericExamViews.postExamAnswers, name="exam_answers_submit"), # path("exam/submit", views.GenericExamViews.postExamAnswers, name="exam_answers_submit"),
path("exam/", views.GenericExamViews.exam_list, name="exam_list"), path("exam/", views.GenericExamViews.exam_list, name="exam_list"),
path("exam/all", views.GenericExamViews.exam_list_all, name="exam_list_all"), path("exam/all", views.GenericExamViews.exam_list_all, name="exam_list_all"),
path("exam/<int:exam_id>/clone", views.ExamClone.as_view(), name="exam_clone"), path("exam/<int:exam_id>/clone", views.ExamClone.as_view(), name="exam_clone"),
path("exam/<int:exam_id>/cids", views.GenericExamViews.exam_cids, name="exam_cids"), path("exam/<int:exam_id>/cids", views.GenericExamViews.exam_cids, name="exam_cids"),
path("exam/available", views.active_exams, name="active_exams"), path("exam/available", views.active_exams, name="active_exams"),
path("exam/<int:pk>/update", views.ExamUpdate.as_view(), name="exam_update"), 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/<int:pk>/delete", views.ExamDelete.as_view(), name="exam_delete"),
# path("exam/json/<int:pk>", views.exam_json, name="exam_json"), # 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("exam/json/<int:pk>/recreate", views.exam_json_recreate, name="exam_json_recreate"),
path( path(
"user_answers/", "user_answers/",
views.UserAnswerTableView.as_view(), views.UserAnswerTableView.as_view(),
name="user_answer_table_view", name="user_answer_table_view",
), ),
path( path(
"user_answers/<int:pk>", views.UserAnswerView.as_view(), name="user_answer_view" "user_answers/<int:pk>", views.UserAnswerView.as_view(), name="user_answer_view"
), ),
path( path(
"user_answers/<int:pk>/delete", "user_answers/<int:pk>/delete",
views.UserAnswerDelete.as_view(), views.UserAnswerDelete.as_view(),
name="user_answer_delete", name="user_answer_delete",
), ),
path( path(
"user_answers/delete", "user_answers/delete",
views.GenericViews.user_answer_delete_multiple, views.GenericViews.user_answer_delete_multiple,
name="user_answer_delete_multiple", name="user_answer_delete_multiple",
), ),
] ]
+410 -410
View File
@@ -1,411 +1,411 @@
from reversion.views import RevisionMixin from reversion.views import RevisionMixin
from generic.mixins import SuperuserRequiredMixin from generic.mixins import SuperuserRequiredMixin
from django.views.generic.detail import DetailView from django.views.generic.detail import DetailView
from generic.models import CidUser from generic.models import CidUser
from sbas.forms import CidUserAnswerForm, ExamForm from sbas.forms import CidUserAnswerForm, ExamForm
from django.shortcuts import render, get_object_or_404, redirect from django.shortcuts import render, get_object_or_404, redirect
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django import forms from django import forms
# from django.contrib.auth.models import User # from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required, user_passes_test from django.contrib.auth.decorators import login_required, user_passes_test
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic.edit import CreateView, UpdateView, DeleteView from django.views.generic.edit import CreateView, UpdateView, DeleteView
from django.views.generic import ListView from django.views.generic import ListView
from django.db.models.functions import Lower from django.db.models.functions import Lower
from django.core.cache import cache from django.core.cache import cache
from django.urls import reverse_lazy, reverse from django.urls import reverse_lazy, reverse
from django.http import Http404, JsonResponse from django.http import Http404, JsonResponse
from django.http import HttpResponseRedirect, HttpResponse from django.http import HttpResponseRedirect, HttpResponse
from .models import Question, Category, Exam, CidUserAnswer from .models import Question, Category, Exam, CidUserAnswer
from django_tables2 import SingleTableView, SingleTableMixin from django_tables2 import SingleTableView, SingleTableMixin
from django_filters.views import FilterView from django_filters.views import FilterView
from collections import defaultdict from collections import defaultdict
import os import os
import base64 import base64
import mimetypes import mimetypes
import json import json
import statistics import statistics
import plotly.express as px import plotly.express as px
from generic.views import ExamCloneMixin, ExamViews, GenericViewBase from generic.views import ExamCloneMixin, ExamViews, GenericViewBase
from rest_framework import viewsets, permissions from rest_framework import viewsets, permissions
from rest_framework.pagination import PageNumberPagination from rest_framework.pagination import PageNumberPagination
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from .tables import QuestionTable, UserAnswerTable from .tables import QuestionTable, UserAnswerTable
from .filters import QuestionFilter, UserAnswerFilter from .filters import QuestionFilter, UserAnswerFilter
class AuthorOrCheckerRequiredMixin(object): class AuthorOrCheckerRequiredMixin(object):
def get_object(self, *args, **kwargs): def get_object(self, *args, **kwargs):
obj = super().get_object(*args, **kwargs) obj = super().get_object(*args, **kwargs)
if self.request.user.groups.filter(name="sbas_checker").exists(): if self.request.user.groups.filter(name="sbas_checker").exists():
return obj return obj
if self.request.user not in obj.author.all(): if self.request.user not in obj.author.all():
raise PermissionDenied() # or Http404 raise PermissionDenied() # or Http404
return obj return obj
def question_list(request): def question_list(request):
questions = Question.objects.all() questions = Question.objects.all()
return render(request, "sbas/question_list.html", {"questions": questions}) return render(request, "sbas/question_list.html", {"questions": questions})
@login_required @login_required
def question_detail(request, pk): def question_detail(request, pk):
question = get_object_or_404(Question, pk=pk) question = get_object_or_404(Question, pk=pk)
return render(request, "sbas/question_detail.html", {"question": question}) return render(request, "sbas/question_detail.html", {"question": question})
def active_exams(request): def active_exams(request):
exams = Exam.objects.all() exams = Exam.objects.all()
print(exams) print(exams)
active_exams = [] active_exams = []
for exam in exams: for exam in exams:
if exam.active: if exam.active:
active_exams.append(exam) active_exams.append(exam)
return render(request, "sbas/available_exam_list.html", {"exams": active_exams}) return render(request, "sbas/available_exam_list.html", {"exams": active_exams})
def exam_scores_cid_user(request, pk, cid, passcode): def exam_scores_cid_user(request, pk, cid, passcode):
exam = get_object_or_404(Exam, pk=pk) exam = get_object_or_404(Exam, pk=pk)
if not exam.check_cid_user(cid, passcode, request): if not exam.check_cid_user(cid, passcode, request):
raise Http404("Error accessing exam") raise Http404("Error accessing exam")
questions = exam.exam_questions.all() questions = exam.exam_questions.all()
answers_and_marks = [] answers_and_marks = []
answers_marks = [] answers_marks = []
answers = [] answers = []
view_all_results = False view_all_results = False
if request.user.groups.filter(name="view_all_results").exists(): if request.user.groups.filter(name="view_all_results").exists():
view_all_results = True view_all_results = True
for q in questions: for q in questions:
# Get user answer # Get user answer
user_answer = q.cid_user_answers.filter(cid=cid).first() user_answer = q.cid_user_answers.filter(cid=cid).first()
if user_answer is None: if user_answer is None:
answer_score = 0 answer_score = 0
ans = "" ans = ""
else: else:
answer_score = user_answer.get_answer_score() answer_score = user_answer.get_answer_score()
ans = user_answer.get_answer() ans = user_answer.get_answer()
correct_answer = q.get_correct_answer() correct_answer = q.get_correct_answer()
if not exam.publish_results and not view_all_results: if not exam.publish_results and not view_all_results:
correct_answer = "*****" correct_answer = "*****"
answer_score = 0 answer_score = 0
answers.append(ans) answers.append(ans)
answers_marks.append(answer_score) answers_marks.append(answer_score)
merged_ans = (ans, answer_score, correct_answer) merged_ans = (ans, answer_score, correct_answer)
answers_and_marks.append((q, *merged_ans)) answers_and_marks.append((q, *merged_ans))
total_score = sum(answers_marks) total_score = sum(answers_marks)
max_score = len(questions) max_score = len(questions)
return render( return render(
request, request,
"sbas/exam_scores_user.html", "sbas/exam_scores_user.html",
{ {
"exam": exam, "exam": exam,
"cid": cid, "cid": cid,
"passcode": passcode, "passcode": passcode,
"questions": questions, "questions": questions,
"answers": answers, "answers": answers,
"answers_marks": answers_marks, "answers_marks": answers_marks,
"total_score": total_score, "total_score": total_score,
"max_score": max_score, "max_score": max_score,
"answers_and_marks": answers_and_marks, "answers_and_marks": answers_and_marks,
"view_all_results": view_all_results, "view_all_results": view_all_results,
}, },
) )
def exam_start(request, pk): def exam_start(request, pk):
exam = get_object_or_404(Exam, pk=pk) exam = get_object_or_404(Exam, pk=pk)
if not exam.active: if not exam.active:
raise Http404("Exam not found") raise Http404("Exam not found")
return render( return render(
request, request,
"sbas/exam_start.html", "sbas/exam_start.html",
{ {
"exam": exam, "exam": exam,
}, },
) )
def exam_take_overview(request, pk, cid, passcode): def exam_take_overview(request, pk, cid, passcode):
exam = get_object_or_404(Exam, pk=pk) exam = get_object_or_404(Exam, pk=pk)
if not exam.active: if not exam.active:
raise Http404("Exam not found") raise Http404("Exam not found")
if not exam.check_cid_user(cid, passcode): if not exam.check_cid_user(cid, passcode):
raise Http404("Error accessing exam") raise Http404("Error accessing exam")
questions = exam.exam_questions.all() questions = exam.exam_questions.all()
answers = CidUserAnswer.objects.filter(cid=cid, exam=exam) answers = CidUserAnswer.objects.filter(cid=cid, exam=exam)
answer_question_map = {} answer_question_map = {}
for ans in answers: for ans in answers:
answer_question_map[ans.question] = ans answer_question_map[ans.question] = ans
question_answer_tuples = [] question_answer_tuples = []
answer_count = 0 answer_count = 0
for q in questions: for q in questions:
if q in answer_question_map and answer_question_map[q].answer: if q in answer_question_map and answer_question_map[q].answer:
question_answer_tuples.append((q, answer_question_map[q])) question_answer_tuples.append((q, answer_question_map[q]))
answer_count += 1 answer_count += 1
else: else:
question_answer_tuples.append((q, None)) question_answer_tuples.append((q, None))
c = CidUser.objects.filter(cid=cid).first() c = CidUser.objects.filter(cid=cid).first()
cid_user_exam = exam.get_or_create_cid_user_exam(cid_user=c) cid_user_exam = exam.get_or_create_cid_user_exam(cid_user=c)
return render( return render(
request, request,
"sbas/exam_take_overview.html", "sbas/exam_take_overview.html",
{ {
"exam": exam, "exam": exam,
"cid": cid, "cid": cid,
"question_answer_tuples": question_answer_tuples, "question_answer_tuples": question_answer_tuples,
"answer_count": answer_count, "answer_count": answer_count,
"exam_length": len(questions), "exam_length": len(questions),
"passcode": passcode, "passcode": passcode,
"cid_user_exam": cid_user_exam, "cid_user_exam": cid_user_exam,
}, },
) )
def exam_take(request, pk, sk, cid, passcode): def exam_take(request, pk, sk, cid, passcode):
exam = get_object_or_404(Exam, pk=pk) exam = get_object_or_404(Exam, pk=pk)
if not exam.active: if not exam.active:
raise Http404("Exam not found") raise Http404("Exam not found")
if not exam.check_cid_user(cid, passcode): if not exam.check_cid_user(cid, passcode):
raise Http404("Error accessing exam") raise Http404("Error accessing exam")
c = CidUser.objects.filter(cid=cid).first() c = CidUser.objects.filter(cid=cid).first()
cid_user_exam = exam.get_or_create_cid_user_exam(cid_user=c) cid_user_exam = exam.get_or_create_cid_user_exam(cid_user=c)
question = exam.exam_questions.all()[sk] question = exam.exam_questions.all()[sk]
exam_length = len(exam.exam_questions.all()) exam_length = len(exam.exam_questions.all())
pos = exam.get_question_index(question) pos = exam.get_question_index(question)
answer = question.cid_user_answers.filter(cid=cid, exam=exam).first() answer = question.cid_user_answers.filter(cid=cid, exam=exam).first()
if request.method == "POST": if request.method == "POST":
if answer: if answer:
form = CidUserAnswerForm(request.POST, instance=answer) form = CidUserAnswerForm(request.POST, instance=answer)
else: else:
form = CidUserAnswerForm(request.POST) form = CidUserAnswerForm(request.POST)
if form.is_valid(): if form.is_valid():
answer = form.save(commit=False) answer = form.save(commit=False)
answer.cid = cid answer.cid = cid
answer.question = question answer.question = question
answer.exam = exam answer.exam = exam
# answer.published_date = timezone.now() # answer.published_date = timezone.now()
answer.save() answer.save()
cid_user_exam.end_time = timezone.now() cid_user_exam.end_time = timezone.now()
cid_user_exam.save() cid_user_exam.save()
kwargs = {"pk": pk, "cid": cid, "passcode": passcode} kwargs = {"pk": pk, "cid": cid, "passcode": passcode}
if "next" in request.POST: if "next" in request.POST:
return redirect("sbas:exam_take", sk=pos + 1, **kwargs) return redirect("sbas:exam_take", sk=pos + 1, **kwargs)
elif "previous" in request.POST: elif "previous" in request.POST:
return redirect("sbas:exam_take", sk=pos - 1, **kwargs) return redirect("sbas:exam_take", sk=pos - 1, **kwargs)
elif "finish" in request.POST: elif "finish" in request.POST:
return redirect("sbas:exam_take_overview", **kwargs) return redirect("sbas:exam_take_overview", **kwargs)
elif "goto" in request.POST: elif "goto" in request.POST:
return redirect( return redirect(
"sbas:exam_take", sk=int(request.POST.get("goto")), **kwargs "sbas:exam_take", sk=int(request.POST.get("goto")), **kwargs
) )
else: else:
form = CidUserAnswerForm(instance=answer) form = CidUserAnswerForm(instance=answer)
previous = -1 previous = -1
if sk > 0: if sk > 0:
previous = sk - 1 previous = sk - 1
next = sk + 1 next = sk + 1
if sk == exam_length - 1: if sk == exam_length - 1:
next = False next = False
saved_answer = False saved_answer = False
if answer is not None: if answer is not None:
saved_answer = answer.answer saved_answer = answer.answer
return render( return render(
request, request,
"sbas/exam_take.html", "sbas/exam_take.html",
{ {
"form": form, "form": form,
"cid": cid, "cid": cid,
"exam": exam, "exam": exam,
"question": question, "question": question,
"next": next, "next": next,
"previous": previous, "previous": previous,
"exam_length": exam_length, "exam_length": exam_length,
"pos": pos, "pos": pos,
"saved_answer": saved_answer, "saved_answer": saved_answer,
"passcode": passcode, "passcode": passcode,
"cid_user_exam": cid_user_exam, "cid_user_exam": cid_user_exam,
}, },
) )
def loadJsonAnswer(answer): def loadJsonAnswer(answer):
pass pass
GenericExamViews = ExamViews( GenericExamViews = ExamViews(
Exam, Question, None, CidUserAnswer, "sbas", "sbas", loadJsonAnswer Exam, Question, None, CidUserAnswer, "sbas", "sbas", loadJsonAnswer
) )
class QuestionView(LoginRequiredMixin, SingleTableMixin, FilterView): class QuestionView(LoginRequiredMixin, SingleTableMixin, FilterView):
model = Question model = Question
table_class = QuestionTable table_class = QuestionTable
template_name = "sbas/question_view.html" template_name = "sbas/question_view.html"
filterset_class = QuestionFilter filterset_class = QuestionFilter
class UserAnswerView(LoginRequiredMixin, DetailView): class UserAnswerView(LoginRequiredMixin, DetailView):
model = CidUserAnswer model = CidUserAnswer
class UserAnswerTableView(LoginRequiredMixin, SingleTableMixin, FilterView): class UserAnswerTableView(LoginRequiredMixin, SingleTableMixin, FilterView):
model = CidUserAnswer model = CidUserAnswer
table_class = UserAnswerTable table_class = UserAnswerTable
template_name = "sbas/user_answer_question_view.html" template_name = "sbas/user_answer_question_view.html"
filterset_class = UserAnswerFilter filterset_class = UserAnswerFilter
class UserAnswerDelete(SuperuserRequiredMixin, DeleteView): class UserAnswerDelete(SuperuserRequiredMixin, DeleteView):
model = CidUserAnswer model = CidUserAnswer
template_name = "user_answer_delete.html" template_name = "user_answer_delete.html"
success_url = reverse_lazy("sbas:user_answer_table_view") success_url = reverse_lazy("sbas:user_answer_table_view")
# @user_passes_test(lambda u: u.is_superuser) # @user_passes_test(lambda u: u.is_superuser)
# def user_answer_delete_multiple(request): # def user_answer_delete_multiple(request):
# if request.is_ajax(): # if request.is_ajax():
# print(request.POST["answer_ids"]) # print(request.POST["answer_ids"])
# answer_ids = json.loads(request.POST["answer_ids"]) # answer_ids = json.loads(request.POST["answer_ids"])
# #
# # We could probably delete them all at once.... # # We could probably delete them all at once....
# for id in answer_ids: # for id in answer_ids:
# CidUserAnswer.objects.get(pk=id).delete() # CidUserAnswer.objects.get(pk=id).delete()
# return JsonResponse({"status": "success"}) # return JsonResponse({"status": "success"})
# return JsonResponse({"status": "error"}) # return JsonResponse({"status": "error"})
GenericViews = GenericViewBase("sbas", Question, CidUserAnswer, Exam) GenericViews = GenericViewBase("sbas", Question, CidUserAnswer, Exam)
class QuestionDelete(AuthorOrCheckerRequiredMixin, DeleteView): class QuestionDelete(AuthorOrCheckerRequiredMixin, DeleteView):
model = Question model = Question
success_url = reverse_lazy("sbas:question_list") success_url = reverse_lazy("sbas:question_list")
class ExamCreate(RevisionMixin, LoginRequiredMixin, CreateView): class ExamCreate(RevisionMixin, LoginRequiredMixin, CreateView):
model = Exam model = Exam
form_class = ExamForm form_class = ExamForm
def form_valid(self, form): def form_valid(self, form):
self.object = form.save(commit=False) self.object = form.save(commit=False)
self.object.save() self.object.save()
form.instance.author.add(self.request.user.id) form.instance.author.add(self.request.user.id)
return super().form_valid(form) return super().form_valid(form)
class ExamUpdate( class ExamUpdate(
RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateView RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateView
): ):
model = Exam model = Exam
form_class = ExamForm form_class = ExamForm
def form_valid(self, form): def form_valid(self, form):
self.object = form.save(commit=False) self.object = form.save(commit=False)
self.object.save() self.object.save()
form.instance.author.add(self.request.user.id) form.instance.author.add(self.request.user.id)
return super().form_valid(form) return super().form_valid(form)
# class ExamViewSet(RevisionMixin, viewsets.ModelViewSet): # class ExamViewSet(RevisionMixin, viewsets.ModelViewSet):
# # queryset = Exam.objects.all().order_by('name') # # queryset = Exam.objects.all().order_by('name')
# serializer_class = ExamSerializer # serializer_class = ExamSerializer
# permission_classes = [permissions.IsAuthenticated] # permission_classes = [permissions.IsAuthenticated]
# #
# def get_queryset(self): # def get_queryset(self):
# """ # """
# This view should return a list exams available to a user. # This view should return a list exams available to a user.
# """ # """
# user = self.request.user # user = self.request.user
# if user.groups.filter(name="sbas_checker").exists(): # if user.groups.filter(name="sbas_checker").exists():
# return Exam.objects.all() # return Exam.objects.all()
# #
# return Exam.objects.filter(author__id=user.id) # return Exam.objects.filter(author__id=user.id)
class ExamDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView): class ExamDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView):
model = Exam model = Exam
template_name = "exam_confirm_delete.html" template_name = "exam_confirm_delete.html"
success_url = reverse_lazy("sbas:index") success_url = reverse_lazy("sbas:index")
class ExamClone(ExamCloneMixin, ExamCreate): class ExamClone(ExamCloneMixin, ExamCreate):
"""Clone exam view""" """Clone exam view"""
class ExamDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView): class ExamDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView):
model = Exam model = Exam
template_name = "exam_confirm_delete.html" template_name = "exam_confirm_delete.html"
success_url = reverse_lazy("sbas:index") success_url = reverse_lazy("sbas:index")
class ExamUpdate( class ExamUpdate(
RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateView RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateView
): ):
model = Exam model = Exam
form_class = ExamForm form_class = ExamForm
def form_valid(self, form): def form_valid(self, form):
self.object = form.save(commit=False) self.object = form.save(commit=False)
self.object.save() self.object.save()
form.instance.author.add(self.request.user.id) form.instance.author.add(self.request.user.id)
return super().form_valid(form) return super().form_valid(form)
+830 -830
View File
File diff suppressed because it is too large Load Diff
+161 -161
View File
@@ -1,162 +1,162 @@
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function () { $(document).ready(function () {
// send request to change the is_private state on customSwitches toggle // send request to change the is_private state on customSwitches toggle
$("#exam-active-switch, .exam-active-switch").on("change", function (evt) { $("#exam-active-switch, .exam-active-switch").on("change", function (evt) {
$.ajax({ $.ajax({
url: evt.currentTarget.dataset.posturl, url: evt.currentTarget.dataset.posturl,
data: { data: {
csrfmiddlewaretoken: "{{ csrf_token }}", csrfmiddlewaretoken: "{{ csrf_token }}",
active: this.checked // true if checked else false active: this.checked // true if checked else false
}, },
type: "POST", type: "POST",
dataType: "json", dataType: "json",
}) })
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly // $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
.done(function (data) { .done(function (data) {
console.log(data); console.log(data);
if (data.status == "success") { if (data.status == "success") {
toastr.info(`Exam: ${data.name} [${data.id}] toastr.info(`Exam: ${data.name} [${data.id}]
Active state changed to: ${data.active}.`) Active state changed to: ${data.active}.`)
} }
// show some message according to the response. // show some message according to the response.
// For eg. A message box showing that the status has been changed // For eg. A message box showing that the status has been changed
}) })
.fail(function (data) { .fail(function (data) {
console.log(data) console.log(data)
toastr.error(`Failed to change state<br/>${data.responseJSON.status}`); toastr.error(`Failed to change state<br/>${data.responseJSON.status}`);
}) })
.always(function () { .always(function () {
console.log('[Done]'); console.log('[Done]');
}) })
}) })
$("#exam-publish-results-switch, .exam-publish-results-switch").on("change", function (evt) { $("#exam-publish-results-switch, .exam-publish-results-switch").on("change", function (evt) {
$.ajax({ $.ajax({
url: evt.currentTarget.dataset.posturl, url: evt.currentTarget.dataset.posturl,
data: { data: {
csrfmiddlewaretoken: "{{ csrf_token }}", csrfmiddlewaretoken: "{{ csrf_token }}",
publish_results: this.checked // true if checked else false publish_results: this.checked // true if checked else false
}, },
type: "POST", type: "POST",
dataType: "json", dataType: "json",
}) })
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly // $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
.done(function (data) { .done(function (data) {
console.log(data); console.log(data);
if (data.status == "success") { if (data.status == "success") {
toastr.info(`Exam: ${data.name} [${data.id}] toastr.info(`Exam: ${data.name} [${data.id}]
Published state changed to: ${data.publish_results}.`) Published state changed to: ${data.publish_results}.`)
} }
// show some message according to the response. // show some message according to the response.
// For eg. A message box showing that the status has been changed // For eg. A message box showing that the status has been changed
}) })
.fail(function (data) { .fail(function (data) {
toastr.error(`Failed to change state`); toastr.error(`Failed to change state`);
}) })
.always(function () { .always(function () {
console.log('[Done]'); console.log('[Done]');
}) })
}) })
$("#button-open-access").click(function (evt) { $("#button-open-access").click(function (evt) {
$.ajax({ $.ajax({
url: evt.currentTarget.dataset.posturl, url: evt.currentTarget.dataset.posturl,
data: { data: {
csrfmiddlewaretoken: "{{ csrf_token }}", csrfmiddlewaretoken: "{{ csrf_token }}",
set_open_access: true, set_open_access: true,
}, },
type: "POST", type: "POST",
dataType: "json", dataType: "json",
}) })
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly // $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
.done(function (data) { .done(function (data) {
console.log(data); console.log(data);
if (data.status == "success") { if (data.status == "success") {
toastr.info('Question access state changed.'); toastr.info('Question access state changed.');
} else { } else {
toastr.warning("Error"); toastr.warning("Error");
toastr.info(data.status); toastr.info(data.status);
} }
}) })
.fail((e) => { .fail((e) => {
toastr.warning(`Error, ${e}`); toastr.warning(`Error, ${e}`);
}) })
.always(function () { .always(function () {
console.log('[Done]'); console.log('[Done]');
}) })
}) })
$("#button-closed-access").click(function (evt) { $("#button-closed-access").click(function (evt) {
$.ajax({ $.ajax({
url: evt.currentTarget.dataset.posturl, url: evt.currentTarget.dataset.posturl,
data: { data: {
csrfmiddlewaretoken: "{{ csrf_token }}", csrfmiddlewaretoken: "{{ csrf_token }}",
set_open_access: false, set_open_access: false,
}, },
type: "POST", type: "POST",
dataType: "json", dataType: "json",
}) })
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly // $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
.done(function (data) { .done(function (data) {
console.log(data); console.log(data);
if (data.status == "success") { if (data.status == "success") {
toastr.info('Question access state changed.'); toastr.info('Question access state changed.');
} else { } else {
toastr.warning("Error"); toastr.warning("Error");
toastr.info(data.status); toastr.info(data.status);
} }
}) })
.fail((e) => { .fail((e) => {
toastr.warning(`Error, ${e}`); toastr.warning(`Error, ${e}`);
}) })
.always(function () { .always(function () {
console.log('[Done]'); console.log('[Done]');
}) })
}) })
$("#button-edit-order").click(function (evt) { $("#button-edit-order").click(function (evt) {
$(this).remove(); $(this).remove();
sortable('.sortable'); sortable('.sortable');
$("#full-question-list li").each((n, el) => { $("#full-question-list li").each((n, el) => {
$(el).append($( $(el).append($(
"<span class='exam-question-delete flex-col'><button>DELETE</button></span>" "<span class='exam-question-delete flex-col'><button>DELETE</button></span>"
).click(() => { ).click(() => {
el.remove(); el.remove();
})); }));
}) })
$("#full-question-list").append($( $("#full-question-list").append($(
"<button title='click and drag questions to change order'>Save exam order</button>" "<button title='click and drag questions to change order'>Save exam order</button>"
).click(() => { ).click(() => {
new_order = []; new_order = [];
$("#full-question-list li").each((n, el) => { $("#full-question-list li").each((n, el) => {
new_order.push(el.dataset.question_pk) new_order.push(el.dataset.question_pk)
}) })
$.ajax({ $.ajax({
url: evt.currentTarget.dataset.posturl, url: evt.currentTarget.dataset.posturl,
data: { data: {
csrfmiddlewaretoken: "{{ csrf_token }}", csrfmiddlewaretoken: "{{ csrf_token }}",
set_exam_order: JSON.stringify(new_order), set_exam_order: JSON.stringify(new_order),
}, },
type: "POST", type: "POST",
dataType: "json", dataType: "json",
}) })
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly // $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
.done(function (data) { .done(function (data) {
console.log(data); console.log(data);
if (data.status == "success") { if (data.status == "success") {
toastr.info('Exam order changed.') toastr.info('Exam order changed.')
} }
}) })
.always(function () { .always(function () {
console.log('[Done]'); console.log('[Done]');
}) })
})); }));
}); });
}); });
</script> </script>