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) 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