.
This commit is contained in:
+215
-215
@@ -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",
|
||||
]
|
||||
|
||||
+830
-830
File diff suppressed because it is too large
Load Diff
+527
-526
File diff suppressed because it is too large
Load Diff
@@ -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
File diff suppressed because it is too large
Load Diff
+112
-112
@@ -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)
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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}} |
|
||||
</span>
|
||||
{% if finding.findings.all %}
|
||||
<span>
|
||||
Findings: {% for f in finding.findings.all %}
|
||||
{{f.get_link}},
|
||||
{% endfor %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if finding.structures.all %}
|
||||
<span>
|
||||
Structure: {% for s in finding.structures.all %}
|
||||
{{s.get_link}},
|
||||
{% 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}} |
|
||||
</span>
|
||||
{% if finding.findings.all %}
|
||||
<span>
|
||||
Findings: {% for f in finding.findings.all %}
|
||||
{{f.get_link}},
|
||||
{% endfor %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if finding.structures.all %}
|
||||
<span>
|
||||
Structure: {% for s in finding.structures.all %}
|
||||
{{s.get_link}},
|
||||
{% 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>
|
||||
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+188
-188
@@ -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"]
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+280
-280
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+268
-268
@@ -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
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+161
-161
@@ -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>
|
||||
Reference in New Issue
Block a user