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