This commit is contained in:
Ross
2022-03-23 22:06:33 +00:00
parent a99750e579
commit 9f54a9c108
32 changed files with 12173 additions and 12153 deletions
+188 -188
View File
@@ -1,189 +1,189 @@
from django.forms import (
Form,
ModelForm,
ModelMultipleChoiceField,
ModelChoiceField,
ChoiceField,
CharField,
HiddenInput,
)
from django.forms import inlineformset_factory
from generic.models import CidUser, CidUserGroup, Examination, QuestionNote
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.forms.widgets import RadioSelect, TextInput, Textarea
from rapids.models import Rapid
from anatomy.models import AnatomyQuestion
from longs.models import Long
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.contenttypes.models import ContentType
from physics.models import CidUserAnswer as PhysicsCidUserAnswer, Question
from physics.models import Exam as PhysicsExam
from physics.models import Question as SbasQuestion
from anatomy.models import CidUserAnswer as AnatomyCidUserAnswer
from anatomy.models import Exam as AnatomyExam
from anatomy.models import AnatomyQuestion
from anatomy.models import Answer as AnatomyAnswer
from rapids.models import CidUserAnswer as RapidsCidUserAnswer
from rapids.models import Exam as RapidsExam
from rapids.models import Rapid
from rapids.models import Answer as RapidAnswer
from longs.models import CidUserAnswer as LongsCidUserAnswer
from longs.models import Exam as LongsExam
from longs.models import Long
from sbas.models import CidUserAnswer as SbasCidUserAnswer
from sbas.models import Exam as SbasExam
from sbas.models import Question as SbasQuestion
class ExaminationForm(ModelForm):
class Meta:
model = Examination
fields = ["examination"]
class QuestionNoteForm(ModelForm):
class Meta:
model = QuestionNote
fields = ["content_type", "object_id", "author", "note_type", "note"]
widgets = {"content_type": HiddenInput(), "object_id": HiddenInput()}
def __init__(self, *args, **kwargs):
question_type = kwargs.pop("question_type")
pk = kwargs.pop("pk")
user = kwargs.pop("user")
# if question_type == "rapid":
# question = Rapid
# content_type = ContentType.objects.get(model="rapid")
# elif question_type == "anatomy":
# question = AnatomyQuestion
# content_type = ContentType.objects.get(model="anatomyquestion")
# elif question_type == "long":
# question = Long
# content_type = ContentType.objects.get(model="long")
# else:
# raise PermissionError()
# get_object_or_404(question, pk=pk)
# We get the 'initial' keyword argument or initialize it
# as a dict if it didn't exist.
# if kwargs.get("instance"):
ModelForm.__init__(self, *args, **kwargs)
super(QuestionNoteForm, self).__init__(*args, **kwargs)
# initial = kwargs.setdefault("initial", {})
## The widget for a ModelMultipleChoiceField expects
## a list of primary key for the selected data.
##initial["content_type"] = question
##initial["content_id"] = pk
# self.fields["content_type"].initial = content_type
# self.fields["object_id"].initial = pk
class CidUserForm(ModelForm):
def __init__(self, *args, **kwargs):
if kwargs.get("instance"):
# We get the 'initial' keyword argument or initialize it
# as a dict if it didn't exist.
initial = kwargs.setdefault("initial", {})
# The widget for a ModelMultipleChoiceField expects
# a list of primary key for the selected data.
initial["physics_exams"] = [t.pk for t in kwargs["instance"].physics_exams.all()]
initial["anatomy_exams"] = [t.pk for t in kwargs["instance"].anatomy_exams.all()]
initial["sba_exams"] = [t.pk for t in kwargs["instance"].sba_exams.all()]
initial["rapid_exams"] = [t.pk for t in kwargs["instance"].rapid_exams.all()]
initial["longs_exams"] = [t.pk for t in kwargs["instance"].longs_exams.all()]
ModelForm.__init__(self, *args, **kwargs)
super(CidUserForm, self).__init__(*args, **kwargs)
self.fields["physics_exams"] = ModelMultipleChoiceField(
required=False,
queryset=PhysicsExam.objects.filter(archive=False),
widget=FilteredSelectMultiple(verbose_name="Physics Exams", is_stacked=False),
)
self.fields["anatomy_exams"] = ModelMultipleChoiceField(
required=False,
queryset=AnatomyExam.objects.filter(archive=False),
widget=FilteredSelectMultiple(verbose_name="Anatomy Exams", is_stacked=False),
)
self.fields["sba_exams"] = ModelMultipleChoiceField(
required=False,
queryset=SbasExam.objects.filter(archive=False),
widget=FilteredSelectMultiple(verbose_name="SBA Exams", is_stacked=False),
)
self.fields["rapid_exams"] = ModelMultipleChoiceField(
required=False,
queryset=RapidsExam.objects.filter(archive=False),
widget=FilteredSelectMultiple(verbose_name="Rapid Exams", is_stacked=False),
)
self.fields["longs_exams"] = ModelMultipleChoiceField(
required=False,
queryset=LongsExam.objects.filter(archive=False),
widget=FilteredSelectMultiple(verbose_name="Long Exams", is_stacked=False),
)
def save(self, commit=True):
# Get the unsaved Long instance
instance = ModelForm.save(self, False)
# Prepare a 'save_m2m' method for the form,
old_save_m2m = self.save_m2m
def save_m2m():
old_save_m2m()
instance.physics_exams.clear()
for physics_exams in self.cleaned_data["physics_exams"]:
instance.physics_exams.add(physics_exams.pk)
instance.anatomy_exams.clear()
for anatomy_exams in self.cleaned_data["anatomy_exams"]:
instance.anatomy_exams.add(anatomy_exams.pk)
instance.sba_exams.clear()
for sba_exams in self.cleaned_data["sba_exams"]:
instance.sba_exams.add(sba_exams.pk)
instance.rapid_exams.clear()
for rapid_exams in self.cleaned_data["rapid_exams"]:
instance.rapid_exams.add(rapid_exams.pk)
instance.longs_exams.clear()
for longs_exams in self.cleaned_data["longs_exams"]:
instance.longs_exams.add(longs_exams.pk)
self.save_m2m = save_m2m
# Do we need to save all changes now?
# if commit:
instance.save()
self.save_m2m()
return instance
class Meta:
model = CidUser
fields = [
"passcode",
"active",
"internal_candidate",
"name",
"email",
"supervisor_email",
"login_email_sent",
"results_email_sent",
"group",
]
class CidUserGroupForm(ModelForm):
class Meta:
model = CidUserGroup
from django.forms import (
Form,
ModelForm,
ModelMultipleChoiceField,
ModelChoiceField,
ChoiceField,
CharField,
HiddenInput,
)
from django.forms import inlineformset_factory
from generic.models import CidUser, CidUserGroup, Examination, QuestionNote
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.forms.widgets import RadioSelect, TextInput, Textarea
from rapids.models import Rapid
from anatomy.models import AnatomyQuestion
from longs.models import Long
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.contenttypes.models import ContentType
from physics.models import CidUserAnswer as PhysicsCidUserAnswer, Question
from physics.models import Exam as PhysicsExam
from physics.models import Question as SbasQuestion
from anatomy.models import CidUserAnswer as AnatomyCidUserAnswer
from anatomy.models import Exam as AnatomyExam
from anatomy.models import AnatomyQuestion
from anatomy.models import Answer as AnatomyAnswer
from rapids.models import CidUserAnswer as RapidsCidUserAnswer
from rapids.models import Exam as RapidsExam
from rapids.models import Rapid
from rapids.models import Answer as RapidAnswer
from longs.models import CidUserAnswer as LongsCidUserAnswer
from longs.models import Exam as LongsExam
from longs.models import Long
from sbas.models import CidUserAnswer as SbasCidUserAnswer
from sbas.models import Exam as SbasExam
from sbas.models import Question as SbasQuestion
class ExaminationForm(ModelForm):
class Meta:
model = Examination
fields = ["examination"]
class QuestionNoteForm(ModelForm):
class Meta:
model = QuestionNote
fields = ["content_type", "object_id", "author", "note_type", "note"]
widgets = {"content_type": HiddenInput(), "object_id": HiddenInput()}
def __init__(self, *args, **kwargs):
question_type = kwargs.pop("question_type")
pk = kwargs.pop("pk")
user = kwargs.pop("user")
# if question_type == "rapid":
# question = Rapid
# content_type = ContentType.objects.get(model="rapid")
# elif question_type == "anatomy":
# question = AnatomyQuestion
# content_type = ContentType.objects.get(model="anatomyquestion")
# elif question_type == "long":
# question = Long
# content_type = ContentType.objects.get(model="long")
# else:
# raise PermissionError()
# get_object_or_404(question, pk=pk)
# We get the 'initial' keyword argument or initialize it
# as a dict if it didn't exist.
# if kwargs.get("instance"):
ModelForm.__init__(self, *args, **kwargs)
super(QuestionNoteForm, self).__init__(*args, **kwargs)
# initial = kwargs.setdefault("initial", {})
## The widget for a ModelMultipleChoiceField expects
## a list of primary key for the selected data.
##initial["content_type"] = question
##initial["content_id"] = pk
# self.fields["content_type"].initial = content_type
# self.fields["object_id"].initial = pk
class CidUserForm(ModelForm):
def __init__(self, *args, **kwargs):
if kwargs.get("instance"):
# We get the 'initial' keyword argument or initialize it
# as a dict if it didn't exist.
initial = kwargs.setdefault("initial", {})
# The widget for a ModelMultipleChoiceField expects
# a list of primary key for the selected data.
initial["physics_exams"] = [t.pk for t in kwargs["instance"].physics_exams.all()]
initial["anatomy_exams"] = [t.pk for t in kwargs["instance"].anatomy_exams.all()]
initial["sba_exams"] = [t.pk for t in kwargs["instance"].sba_exams.all()]
initial["rapid_exams"] = [t.pk for t in kwargs["instance"].rapid_exams.all()]
initial["longs_exams"] = [t.pk for t in kwargs["instance"].longs_exams.all()]
ModelForm.__init__(self, *args, **kwargs)
super(CidUserForm, self).__init__(*args, **kwargs)
self.fields["physics_exams"] = ModelMultipleChoiceField(
required=False,
queryset=PhysicsExam.objects.filter(archive=False),
widget=FilteredSelectMultiple(verbose_name="Physics Exams", is_stacked=False),
)
self.fields["anatomy_exams"] = ModelMultipleChoiceField(
required=False,
queryset=AnatomyExam.objects.filter(archive=False),
widget=FilteredSelectMultiple(verbose_name="Anatomy Exams", is_stacked=False),
)
self.fields["sba_exams"] = ModelMultipleChoiceField(
required=False,
queryset=SbasExam.objects.filter(archive=False),
widget=FilteredSelectMultiple(verbose_name="SBA Exams", is_stacked=False),
)
self.fields["rapid_exams"] = ModelMultipleChoiceField(
required=False,
queryset=RapidsExam.objects.filter(archive=False),
widget=FilteredSelectMultiple(verbose_name="Rapid Exams", is_stacked=False),
)
self.fields["longs_exams"] = ModelMultipleChoiceField(
required=False,
queryset=LongsExam.objects.filter(archive=False),
widget=FilteredSelectMultiple(verbose_name="Long Exams", is_stacked=False),
)
def save(self, commit=True):
# Get the unsaved Long instance
instance = ModelForm.save(self, False)
# Prepare a 'save_m2m' method for the form,
old_save_m2m = self.save_m2m
def save_m2m():
old_save_m2m()
instance.physics_exams.clear()
for physics_exams in self.cleaned_data["physics_exams"]:
instance.physics_exams.add(physics_exams.pk)
instance.anatomy_exams.clear()
for anatomy_exams in self.cleaned_data["anatomy_exams"]:
instance.anatomy_exams.add(anatomy_exams.pk)
instance.sba_exams.clear()
for sba_exams in self.cleaned_data["sba_exams"]:
instance.sba_exams.add(sba_exams.pk)
instance.rapid_exams.clear()
for rapid_exams in self.cleaned_data["rapid_exams"]:
instance.rapid_exams.add(rapid_exams.pk)
instance.longs_exams.clear()
for longs_exams in self.cleaned_data["longs_exams"]:
instance.longs_exams.add(longs_exams.pk)
self.save_m2m = save_m2m
# Do we need to save all changes now?
# if commit:
instance.save()
self.save_m2m()
return instance
class Meta:
model = CidUser
fields = [
"passcode",
"active",
"internal_candidate",
"name",
"email",
"supervisor_email",
"login_email_sent",
"results_email_sent",
"group",
]
class CidUserGroupForm(ModelForm):
class Meta:
model = CidUserGroup
fields = ["name", "archive"]
+19 -19
View File
@@ -1,19 +1,19 @@
# Generated by Django 3.2.10 on 2022-03-01 18:32
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('generic', '0024_auto_20211231_1044'),
]
operations = [
migrations.AlterField(
model_name='ciduser',
name='group',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='generic.cidusergroup'),
),
]
# Generated by Django 3.2.10 on 2022-03-01 18:32
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('generic', '0024_auto_20211231_1044'),
]
operations = [
migrations.AlterField(
model_name='ciduser',
name='group',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='generic.cidusergroup'),
),
]
+469 -469
View File
@@ -1,469 +1,469 @@
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db import models
from django.http import HttpRequest
from django.utils import timezone
from smtplib import SMTPException
from django.urls import reverse
from django.views.generic.edit import CreateView
from reversion.views import RevisionMixin
import tagulous
import tagulous.models
from sortedm2m.fields import SortedManyToManyField
from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.core.mail import send_mail
from django.core.mail import EmailMessage
class Plane(models.Model):
plane = models.CharField(max_length=200, unique=True)
def __str__(self):
return self.plane
class Meta:
ordering = ("plane",)
class Contrast(models.Model):
contrast = models.CharField(max_length=200, unique=True)
def __str__(self):
return self.contrast
class Meta:
ordering = ("contrast",)
class Examination(models.Model):
examination = models.CharField(max_length=200, unique=True)
def __str__(self):
return self.examination
class Meta:
ordering = ("examination",)
class Site(models.Model):
site = models.CharField(max_length=200)
initials = models.CharField(max_length=200)
def __str__(self):
return self.site
class Condition(tagulous.models.TagModel):
pass
class Sign(tagulous.models.TagModel):
pass
class ExamBase(models.Model):
name = models.CharField(max_length=200, help_text="Name of the exam")
# exam_questions = SortedManyToManyField(Long, related_name="exams", blank="true")
active = models.BooleanField(
help_text="If an exam should be available to take", default=False
)
exam_mode = models.BooleanField(
help_text="If an exam should be taken in exam mode (users results will be submited to the server for marking)",
default=False,
)
publish_results = models.BooleanField(
help_text="If an exams results should be available", default=False
)
recreate_json = models.BooleanField(
help_text="If the json cache needs updating", default=False
)
json_creation_time = models.DateTimeField(blank=True, default=None, null=True)
exam_json_id = models.IntegerField(
default=1, help_text="auto incrementing field when json recreated"
)
archive = models.BooleanField(
help_text="Archived exams will remain on the test system but will not be displayed by default",
default=False,
)
open_access = models.BooleanField(
help_text="If the exam is freely accessible (to view and edit on the test system)",
default=False,
)
stats_mean = models.FloatField(default=0)
stats_mode = models.CharField(default=0, max_length=25)
stats_median = models.FloatField(default=0)
stats_candidates = models.FloatField(default=0)
stats_min = models.FloatField(default=0)
stats_max = models.FloatField(default=0)
stats_max_possible = models.FloatField(default=0)
stats_graph = models.TextField(default=0)
user_scores = models.JSONField(default=dict)
notes = GenericRelation("generic.CidUserExam")
# time_limit = models.IntegerField(
# help_text="Exam time limit (in seconds). Default is 2100 secondse (35 minutes)",
# default=2100,
# )
class Meta:
abstract = True
def get_exam_stats(self):
text = f"""{self.name}
{"-"*len(self.name)}
- Candidates: {int(self.stats_candidates)}
- Mean: {self.stats_mean:.2f} Median: {self.stats_median} Mode: {self.stats_mode} [Min: {self.stats_min} / Max: {self.stats_max}]
"""
return text
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse("{}:exam_overview".format(self.app_name), kwargs={"pk": self.pk})
def get_take_url(self):
return f"{settings.REMOTE_URL}/rts"
def get_exam_name(self):
return self.name
def get_json_url(self):
return reverse("{}:exam_json".format(self.app_name), args=(self.pk,))
def get_question_index(self, question):
return list(self.exam_questions.all()).index(question)
def get_authors(self):
"""Returns a comma seperated text list of authors"""
authors = ", ".join([i.username for i in self.author.all()])
return authors
def get_author_objects(self):
"""Returns a comma seperated text list of authors"""
authors = [i for i in self.author.all()]
return authors
def check_cid_user(self, cid, passcode, request=None):
if request is not None and request.user.is_superuser:
return True
if self.valid_users.exists():
user = self.valid_users.filter(cid=cid).first()
if not user or user.passcode != passcode:
return False
return True
def get_or_create_cid_user_exam(self, cid_user, start_time=None):
content_type = ContentType.objects.get_for_model(self)
c = CidUserExam.objects.filter(
content_type=content_type, object_id=self.pk, cid_user=cid_user
).first()
print(c)
if c:
return c
if start_time is None:
start_time = timezone.now()
new = CidUserExam(
content_type=content_type,
object_id=self.pk,
cid_user=cid_user,
start_time=start_time,
)
new.save()
return new
def get_cid_user_exams(self, cid_user=None):
content_type = ContentType.objects.get_for_model(self)
if cid_user is None:
return CidUserExam.objects.filter(
content_type=content_type,
object_id=self.pk,
)
else:
return CidUserExam.objects.filter(
content_type=content_type, object_id=self.pk, cid_user=cid_user
)
def get_cid_user_score(self, cid_user):
c = str(cid_user)
if c in self.user_scores:
return self.user_scores[c]
else:
return False
class NoteType(models.Model):
note_type = models.CharField(max_length=200)
def __str__(self):
return self.note_type
class QuestionNote(models.Model):
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
question = GenericForeignKey("content_type", "object_id")
# author = models.ForeignKey(User,
author = models.CharField(max_length=200, blank=True)
user_author = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, blank=True, null=True
)
note = models.TextField(blank=True)
created_on = models.DateTimeField(auto_now_add=True)
complete = models.BooleanField(default=False)
note_type = models.ForeignKey(
NoteType, on_delete=models.CASCADE, null=True, blank=True
)
# def get_absolute_url(self):
# self.pk = self.rapid_id
# return reverse('rapids:question_detail', kwargs={'pk': self.pk})
def __str__(self):
return "{}: {} [{}] {} / {}".format(
self.content_type,
self.get_author_str(),
self.created_on,
self.note_type,
self.note,
)
def get_absolute_url(self):
# TODO: return to question if logged in
return "/"
def get_object_url(self):
return self.question.get_absolute_url()
def get_author_str(self):
if self.user_author is not None:
return self.user_author.username
else:
return self.author
def get_short_str(self):
return f"{self.note_type} - {self.note}"
class CidUser(models.Model):
EXAM_TYPES = (
("Physics", "physics_exams"),
("Rapids", "rapid_exams"),
("SBAs", "sba_exams"),
("Anatomy", "anatomy_exams"),
("Longs", "longs_exams"),
)
cid = models.IntegerField(blank=True, null=True, unique=True)
passcode = models.CharField(blank=True, max_length=50)
active = models.BooleanField(default=True)
internal_candidate = models.BooleanField(default=False)
name = models.CharField(blank=True, max_length=255)
email = models.EmailField(blank=True)
supervisor_email = models.EmailField(blank=True)
login_email_sent = models.BooleanField(default=False)
results_email_sent = models.BooleanField(default=False)
group = models.ForeignKey("CidUserGroup", on_delete=models.SET_NULL, null=True, blank=True)
def __str__(self) -> str:
return str(self.cid)
def get_absolute_url(self):
return reverse("generic:manage_cids")
def check_passcode(self, passcode) -> bool:
return self.passcode == passcode
def get_cid_exams(self):
available_exams = []
for n, t in self.EXAM_TYPES:
exam_rel = getattr(self, t)
if exam_rel.exists():
exams = exam_rel.filter(exam_mode=True, archive=False).order_by("name")
available_exams.append((n, exams))
return available_exams
def generate_exam_report(self):
exam_list = self.get_cid_exams()
name = ""
if self.name:
name = f"{self.name} "
exam_text = [f"Candidate {self.cid} {name}[{self.email}]"]
for exam_type, exams in exam_list:
exam_text.append(f"{'='*len(exam_type)}\n{exam_type}\n{'='*len(exam_type)}")
for exam in exams:
try:
user_score = exam.user_scores[str(self.cid)]
except KeyError:
user_score = "Score not generated"
t = exam.get_exam_stats()
try:
percentage = (
f" ({user_score / int(exam.stats_max_possible) * 100:.2f}%)"
)
except TypeError:
percentage = ""
exam_text.append(
f"""{t}
# Score: {user_score} / {int(exam.stats_max_possible)}{percentage}
"""
)
msg = "\n\n".join(exam_text)
return msg
html_msg = f""""""
def email_results(self, resend=False, additional_emails=[]):
if not self.internal_candidate:
return False, "Not internal candidate."
if self.results_email_sent and not resend:
return False, "Already sent."
# Get a list of taken exams
msg = self.generate_exam_report()
emails = [self.email, self.supervisor_email]
if additional_emails:
emails.extend(additional_emails)
try:
send_mail(
f"{self.group} results",
msg,
"test@xkjq.uk",
emails,
fail_silently=False,
# html_message=html_msg,
)
except SMTPException as e:
return False, e
self.results_email_sent = True
self.save()
return True, ""
def email_details(self, resend=False):
if self.login_email_sent and not resend:
return False, "Already sent."
login_url = reverse("cid_selector")
msg = f"""
See below for your details for: {self.group}
---------
CID: {self.cid}
Passcode: {self.passcode}
---------
You will need these to access to the test system.
Direct link to login:
{settings.REMOTE_URL}{login_url}?cid={self.cid}&passcode={self.passcode}
The above details are unique to you. Please do not share as otherwise others will be able to access and submit your results.
Please note this inbox is not monitored. Direct all enquiries to plh-tr.radiology-academy@nhs.net
"""
try:
# send_mail(
# "Candidate Details",
# msg,
# "test@xkjq.uk",
# [self.email],
# fail_silently=False,
# )
email = EmailMessage(
"Candidate Details",
msg,
"test@xkjq.uk",
[self.email],
["ross.kruger@nhs.net"],
)
email.send(fail_silently=False)
except SMTPException as e:
return False, e
self.login_email_sent = True
self.save()
return True, ""
def get_email_results_url(self):
return reverse("generic:candidate_email_results", kwargs={"cid": self.cid})
def get_email_results_resend_url(self):
return reverse("generic:candidate_email_results_resend", kwargs={"cid": self.cid})
class CidUserExam(models.Model):
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
exam = GenericForeignKey("content_type", "object_id")
start_time = models.DateTimeField(blank=True, null=True)
end_time = models.DateTimeField(blank=True, null=True)
cid_user = models.ForeignKey(CidUser, on_delete=models.CASCADE)
manual_submission = models.BooleanField(default=False)
class CidUserGroup(models.Model):
name = models.CharField(blank=True, max_length=50)
archive = models.BooleanField(default=False)
def __str__(self) -> str:
return str(self.name)
def GetGroupUsers(self):
cid_users = self.ciduser_set.all()
s = ", ".join([f"{user.id} - {user.name}" for user in cid_users])
return s
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db import models
from django.http import HttpRequest
from django.utils import timezone
from smtplib import SMTPException
from django.urls import reverse
from django.views.generic.edit import CreateView
from reversion.views import RevisionMixin
import tagulous
import tagulous.models
from sortedm2m.fields import SortedManyToManyField
from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.core.mail import send_mail
from django.core.mail import EmailMessage
class Plane(models.Model):
plane = models.CharField(max_length=200, unique=True)
def __str__(self):
return self.plane
class Meta:
ordering = ("plane",)
class Contrast(models.Model):
contrast = models.CharField(max_length=200, unique=True)
def __str__(self):
return self.contrast
class Meta:
ordering = ("contrast",)
class Examination(models.Model):
examination = models.CharField(max_length=200, unique=True)
def __str__(self):
return self.examination
class Meta:
ordering = ("examination",)
class Site(models.Model):
site = models.CharField(max_length=200)
initials = models.CharField(max_length=200)
def __str__(self):
return self.site
class Condition(tagulous.models.TagModel):
pass
class Sign(tagulous.models.TagModel):
pass
class ExamBase(models.Model):
name = models.CharField(max_length=200, help_text="Name of the exam")
# exam_questions = SortedManyToManyField(Long, related_name="exams", blank="true")
active = models.BooleanField(
help_text="If an exam should be available to take", default=False
)
exam_mode = models.BooleanField(
help_text="If an exam should be taken in exam mode (users results will be submited to the server for marking)",
default=False,
)
publish_results = models.BooleanField(
help_text="If an exams results should be available", default=False
)
recreate_json = models.BooleanField(
help_text="If the json cache needs updating", default=False
)
json_creation_time = models.DateTimeField(blank=True, default=None, null=True)
exam_json_id = models.IntegerField(
default=1, help_text="auto incrementing field when json recreated"
)
archive = models.BooleanField(
help_text="Archived exams will remain on the test system but will not be displayed by default",
default=False,
)
open_access = models.BooleanField(
help_text="If the exam is freely accessible (to view and edit on the test system)",
default=False,
)
stats_mean = models.FloatField(default=0)
stats_mode = models.CharField(default=0, max_length=25)
stats_median = models.FloatField(default=0)
stats_candidates = models.FloatField(default=0)
stats_min = models.FloatField(default=0)
stats_max = models.FloatField(default=0)
stats_max_possible = models.FloatField(default=0)
stats_graph = models.TextField(default=0)
user_scores = models.JSONField(default=dict)
notes = GenericRelation("generic.CidUserExam")
# time_limit = models.IntegerField(
# help_text="Exam time limit (in seconds). Default is 2100 secondse (35 minutes)",
# default=2100,
# )
class Meta:
abstract = True
def get_exam_stats(self):
text = f"""{self.name}
{"-"*len(self.name)}
- Candidates: {int(self.stats_candidates)}
- Mean: {self.stats_mean:.2f} Median: {self.stats_median} Mode: {self.stats_mode} [Min: {self.stats_min} / Max: {self.stats_max}]
"""
return text
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse("{}:exam_overview".format(self.app_name), kwargs={"pk": self.pk})
def get_take_url(self):
return f"{settings.REMOTE_URL}/rts"
def get_exam_name(self):
return self.name
def get_json_url(self):
return reverse("{}:exam_json".format(self.app_name), args=(self.pk,))
def get_question_index(self, question):
return list(self.exam_questions.all()).index(question)
def get_authors(self):
"""Returns a comma seperated text list of authors"""
authors = ", ".join([i.username for i in self.author.all()])
return authors
def get_author_objects(self):
"""Returns a comma seperated text list of authors"""
authors = [i for i in self.author.all()]
return authors
def check_cid_user(self, cid, passcode, request=None):
if request is not None and request.user.is_superuser:
return True
if self.valid_users.exists():
user = self.valid_users.filter(cid=cid).first()
if not user or user.passcode != passcode:
return False
return True
def get_or_create_cid_user_exam(self, cid_user, start_time=None):
content_type = ContentType.objects.get_for_model(self)
c = CidUserExam.objects.filter(
content_type=content_type, object_id=self.pk, cid_user=cid_user
).first()
print(c)
if c:
return c
if start_time is None:
start_time = timezone.now()
new = CidUserExam(
content_type=content_type,
object_id=self.pk,
cid_user=cid_user,
start_time=start_time,
)
new.save()
return new
def get_cid_user_exams(self, cid_user=None):
content_type = ContentType.objects.get_for_model(self)
if cid_user is None:
return CidUserExam.objects.filter(
content_type=content_type,
object_id=self.pk,
)
else:
return CidUserExam.objects.filter(
content_type=content_type, object_id=self.pk, cid_user=cid_user
)
def get_cid_user_score(self, cid_user):
c = str(cid_user)
if c in self.user_scores:
return self.user_scores[c]
else:
return False
class NoteType(models.Model):
note_type = models.CharField(max_length=200)
def __str__(self):
return self.note_type
class QuestionNote(models.Model):
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
question = GenericForeignKey("content_type", "object_id")
# author = models.ForeignKey(User,
author = models.CharField(max_length=200, blank=True)
user_author = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, blank=True, null=True
)
note = models.TextField(blank=True)
created_on = models.DateTimeField(auto_now_add=True)
complete = models.BooleanField(default=False)
note_type = models.ForeignKey(
NoteType, on_delete=models.CASCADE, null=True, blank=True
)
# def get_absolute_url(self):
# self.pk = self.rapid_id
# return reverse('rapids:question_detail', kwargs={'pk': self.pk})
def __str__(self):
return "{}: {} [{}] {} / {}".format(
self.content_type,
self.get_author_str(),
self.created_on,
self.note_type,
self.note,
)
def get_absolute_url(self):
# TODO: return to question if logged in
return "/"
def get_object_url(self):
return self.question.get_absolute_url()
def get_author_str(self):
if self.user_author is not None:
return self.user_author.username
else:
return self.author
def get_short_str(self):
return f"{self.note_type} - {self.note}"
class CidUser(models.Model):
EXAM_TYPES = (
("Physics", "physics_exams"),
("Rapids", "rapid_exams"),
("SBAs", "sba_exams"),
("Anatomy", "anatomy_exams"),
("Longs", "longs_exams"),
)
cid = models.IntegerField(blank=True, null=True, unique=True)
passcode = models.CharField(blank=True, max_length=50)
active = models.BooleanField(default=True)
internal_candidate = models.BooleanField(default=False)
name = models.CharField(blank=True, max_length=255)
email = models.EmailField(blank=True)
supervisor_email = models.EmailField(blank=True)
login_email_sent = models.BooleanField(default=False)
results_email_sent = models.BooleanField(default=False)
group = models.ForeignKey("CidUserGroup", on_delete=models.SET_NULL, null=True, blank=True)
def __str__(self) -> str:
return str(self.cid)
def get_absolute_url(self):
return reverse("generic:manage_cids")
def check_passcode(self, passcode) -> bool:
return self.passcode == passcode
def get_cid_exams(self):
available_exams = []
for n, t in self.EXAM_TYPES:
exam_rel = getattr(self, t)
if exam_rel.exists():
exams = exam_rel.filter(exam_mode=True, archive=False).order_by("name")
available_exams.append((n, exams))
return available_exams
def generate_exam_report(self):
exam_list = self.get_cid_exams()
name = ""
if self.name:
name = f"{self.name} "
exam_text = [f"Candidate {self.cid} {name}[{self.email}]"]
for exam_type, exams in exam_list:
exam_text.append(f"{'='*len(exam_type)}\n{exam_type}\n{'='*len(exam_type)}")
for exam in exams:
try:
user_score = exam.user_scores[str(self.cid)]
except KeyError:
user_score = "Score not generated"
t = exam.get_exam_stats()
try:
percentage = (
f" ({user_score / int(exam.stats_max_possible) * 100:.2f}%)"
)
except TypeError:
percentage = ""
exam_text.append(
f"""{t}
# Score: {user_score} / {int(exam.stats_max_possible)}{percentage}
"""
)
msg = "\n\n".join(exam_text)
return msg
html_msg = f""""""
def email_results(self, resend=False, additional_emails=[]):
if not self.internal_candidate:
return False, "Not internal candidate."
if self.results_email_sent and not resend:
return False, "Already sent."
# Get a list of taken exams
msg = self.generate_exam_report()
emails = [self.email, self.supervisor_email]
if additional_emails:
emails.extend(additional_emails)
try:
send_mail(
f"{self.group} results",
msg,
"test@xkjq.uk",
emails,
fail_silently=False,
# html_message=html_msg,
)
except SMTPException as e:
return False, e
self.results_email_sent = True
self.save()
return True, ""
def email_details(self, resend=False):
if self.login_email_sent and not resend:
return False, "Already sent."
login_url = reverse("cid_selector")
msg = f"""
See below for your details for: {self.group}
---------
CID: {self.cid}
Passcode: {self.passcode}
---------
You will need these to access to the test system.
Direct link to login:
{settings.REMOTE_URL}{login_url}?cid={self.cid}&passcode={self.passcode}
The above details are unique to you. Please do not share as otherwise others will be able to access and submit your results.
Please note this inbox is not monitored. Direct all enquiries to plh-tr.radiology-academy@nhs.net
"""
try:
# send_mail(
# "Candidate Details",
# msg,
# "test@xkjq.uk",
# [self.email],
# fail_silently=False,
# )
email = EmailMessage(
"Candidate Details",
msg,
"test@xkjq.uk",
[self.email],
["ross.kruger@nhs.net"],
)
email.send(fail_silently=False)
except SMTPException as e:
return False, e
self.login_email_sent = True
self.save()
return True, ""
def get_email_results_url(self):
return reverse("generic:candidate_email_results", kwargs={"cid": self.cid})
def get_email_results_resend_url(self):
return reverse("generic:candidate_email_results_resend", kwargs={"cid": self.cid})
class CidUserExam(models.Model):
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
exam = GenericForeignKey("content_type", "object_id")
start_time = models.DateTimeField(blank=True, null=True)
end_time = models.DateTimeField(blank=True, null=True)
cid_user = models.ForeignKey(CidUser, on_delete=models.CASCADE)
manual_submission = models.BooleanField(default=False)
class CidUserGroup(models.Model):
name = models.CharField(blank=True, max_length=50)
archive = models.BooleanField(default=False)
def __str__(self) -> str:
return str(self.name)
def GetGroupUsers(self):
cid_users = self.ciduser_set.all()
s = ", ".join([f"{user.id} - {user.name}" for user in cid_users])
return s
+1650 -1650
View File
File diff suppressed because it is too large Load Diff