from django.forms import ( Form, ModelForm, ModelMultipleChoiceField, MultipleChoiceField, ModelChoiceField, ChoiceField, CharField, HiddenInput, EmailField, IntegerField, DateField, SplitDateTimeField, SplitDateTimeWidget, ) from django.forms import inlineformset_factory from atlas.models import CaseCollection from django.contrib.auth.models import User from generic.models import ( CidUser, CidUserGroup, Examination, ExamCollection, QuestionNote, Supervisor, UserGrades, UserProfile, UserUserGroup, QuestionReview ) from django.contrib.admin.widgets import FilteredSelectMultiple from django.forms.widgets import RadioSelect, 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 UserAnswer as PhysicsUserAnswer, Question from physics.models import Exam as PhysicsExam from physics.models import Question as SbasQuestion from anatomy.models import UserAnswer as AnatomyUserAnswer from anatomy.models import Exam as AnatomyExam from anatomy.models import AnatomyQuestion from anatomy.models import Answer as AnatomyAnswer from rapids.models import UserAnswer as RapidsUserAnswer from rapids.models import Exam as RapidsExam from rapids.models import Rapid from rapids.models import Answer as RapidAnswer from shorts.models import UserAnswer as ShortsUserAnswer from shorts.models import Exam as ShortsExam from shorts.models import Question as ShortsQuestion from longs.models import UserAnswer as LongsUserAnswer from longs.models import Exam as LongsExam from longs.models import Long from sbas.models import UserAnswer as SbasUserAnswer from sbas.models import Exam as SbasExam from sbas.models import Question as SbasQuestion from django.core.exceptions import ValidationError import datetime from django.forms.utils import from_current_timezone, to_current_timezone from crispy_forms.helper import FormHelper from crispy_forms.layout import Submit, Layout, Fieldset, Field, Div from crispy_forms.bootstrap import Accordion, AccordionGroup from dal import autocomplete from autocomplete import widgets as htmx_widgets, Autocomplete, AutocompleteWidget, ModelAutocomplete, register as autocomplete_register from django.utils.safestring import mark_safe from django.urls import reverse from django.contrib.auth.models import User class SplitDateTimeFieldDefaultTime(SplitDateTimeField): def compress(self, data_list): if data_list: # Raise a validation error if time or date is empty # (possible if SplitDateTimeField has required=False). if data_list[0] in self.empty_values: raise ValidationError( self.error_messages["invalid_date"], code="invalid_date" ) if data_list[1] in self.empty_values: data_list[1] = datetime.time(0, 1) # raise ValidationError( # self.error_messages["invalid_time"], code="invalid_time" # ) result = datetime.datetime.combine(*data_list) return from_current_timezone(result) return None class SplitDateTimeFieldDefaultTimeEnd(SplitDateTimeField): def compress(self, data_list): if data_list: # Raise a validation error if time or date is empty # (possible if SplitDateTimeField has required=False). if data_list[0] in self.empty_values: raise ValidationError( self.error_messages["invalid_date"], code="invalid_date" ) if data_list[1] in self.empty_values: data_list[1] = datetime.time(23, 59) # raise ValidationError( # self.error_messages["invalid_time"], code="invalid_time" # ) result = datetime.datetime.combine(*data_list) return from_current_timezone(result) return None class ExamFormMixin: def __init__(self, *args, user, **kwargs) -> None: super(ModelForm, self).__init__(*args, **kwargs) self.helper = FormHelper() self.helper.form_id = "id-exam-form" self.helper.form_class = "exam-form" self.helper.form_method = "post" self.helper.form_action = "submit" self.helper.layout = Layout( Fieldset( "Exam Details", "name", "time_limit", "open_access", "authors_only", "exam_mode", "exam_open_access", "include_history", "active", "publish_results", "archive", Accordion( AccordionGroup("Date restrictions", "restrict_to_dates", "start_date", "end_date", active=False, ) ), ), Div( Submit("submit", "Submit"), css_class="form-group", ), ) # self.helper.add_input(Submit("submit", "Submit")) # if user.is_superuser or user.groups.filter(name="cid_user_manager").exists(): # cid_user_group_queryset = CidUserGroup.objects.filter(archive=False) # user_user_group_queryset = UserUserGroup.objects.filter(archive=False) # else: # cid_user_group_queryset = CidUserGroup.objects.none() # user_user_group_queryset = UserUserGroup.objects.none() # self.fields["cid_user_groups"] = ModelMultipleChoiceField( # required=False, # queryset=cid_user_group_queryset, # ) # self.fields["user_user_groups"] = ModelMultipleChoiceField( # required=False, # queryset=user_user_group_queryset, # ) self.fields["start_date"] = SplitDateTimeFieldDefaultTime( widget=SplitDateTimeWidget( date_attrs={"type": "date", "class": "datepicker"}, time_attrs={"type": "time", "class": "timepicker"}, date_format="%Y-%m-%d", time_format="%H:%M", ), required=False, help_text="The date the exam is due to start (time is optional)", ) self.fields["end_date"] = SplitDateTimeFieldDefaultTimeEnd( widget=SplitDateTimeWidget( date_attrs={"type": "date", "class": "datepicker"}, time_attrs={"type": "time", "class": "timepicker"}, date_format="%Y-%m-%d", time_format="%H:%M", ), required=False, help_text="The date the exam is due to start (time is optional)", ) class Meta: fields = [ "name", "time_limit", "open_access", "authors_only", "exam_mode", "exam_open_access", "include_history", "restrict_to_dates", "start_date", "end_date", "active", "publish_results", "archive", "results_supervisor_visible" # "cid_user_groups", # "user_user_groups", # "author", ] # widgets = { # "start_date": TextInput(attrs={"type": "date"}), # "end_date": TextInput(attrs={"type": "date"}), # } def save(self, commit=True): instance = ModelForm.save(self, False) instance.save() self.save_m2m() return instance class ExamAuthorFormMixin(ModelForm): class Meta: fields = ["author"] def __init__(self, *args, **kwargs): ModelForm.__init__(self, *args, **kwargs) self.fields["author"] = ModelMultipleChoiceField( queryset=User.objects.all(), widget=FilteredSelectMultiple(verbose_name="Authors", is_stacked=False), ) class ExamMarkerFormMixin(ModelForm): class Meta: fields = ["markers"] def __init__(self, *args, **kwargs): ModelForm.__init__(self, *args, **kwargs) self.fields["markers"] = ModelMultipleChoiceField( queryset=User.objects.all(), widget=FilteredSelectMultiple(verbose_name="Markers", is_stacked=False), ) self.fields["markers"].required = False class ExamGroupsFormMixin(ModelForm): class Meta: # model = ExamCollection fields = ("cid_user_groups", "user_user_groups") def __init__(self, *args, user, **kwargs) -> None: super(ModelForm, self).__init__(*args, **kwargs) # TODO: decide how we should handle managing user groups # i.e who should be able to access a list of all users? if user.is_superuser or user.groups.filter(name="cid_user_manager").exists(): cid_user_group_queryset = CidUserGroup.objects.filter(archive=False) user_user_group_queryset = UserUserGroup.objects.filter(archive=False) else: cid_user_group_queryset = CidUserGroup.objects.filter( archive=False, open_access=True ) user_user_group_queryset = UserUserGroup.objects.filter( archive=False, open_access=True ) # cid_user_group_queryset = CidUserGroup.objects.none() # user_user_group_queryset = UserUserGroup.objects.none() self.fields["cid_user_groups"] = ModelMultipleChoiceField( required=False, queryset=cid_user_group_queryset, label="CID User Groups", ) self.fields["user_user_groups"] = ModelMultipleChoiceField( required=False, queryset=user_user_group_queryset, label="User Groups", ) self.fields["cid_user_groups"].widget.attrs["size"] = 8 self.fields["user_user_groups"].widget.attrs["size"] = 8 class ExamCollectionBulkAddGroupsForm(Form): """Form used by the HTMX modal to bulk-add CID/User groups to an ExamCollection. Accepts `user` kwarg in __init__ to limit available groups based on permissions (mirrors behaviour of `ExamGroupsFormMixin`). """ cid_user_groups = ModelMultipleChoiceField(required=False, queryset=CidUserGroup.objects.none(), label="CID User Groups") user_user_groups = ModelMultipleChoiceField(required=False, queryset=UserUserGroup.objects.none(), label="User Groups") EXAM_TYPE_CHOICES = [ ("all", "All"), ("anatomy", "Anatomy"), ("longs", "Longs"), ("rapids", "Rapids"), ("shorts", "Shorts"), ("physics", "Physics"), ("sbas", "SBAs"), ] exam_types = MultipleChoiceField(required=False, choices=EXAM_TYPE_CHOICES, label="Exam types (leave empty for all)") def __init__(self, *args, user=None, **kwargs): # keep compatibility with other forms that pass user as kwarg # This is a plain Form (not a ModelForm) so call Form.__init__ Form.__init__(self, *args, **kwargs) # choose queryset depending on permissions if user and (user.is_superuser or user.groups.filter(name="cid_user_manager").exists()): cid_qs = CidUserGroup.objects.filter(archive=False) user_qs = UserUserGroup.objects.filter(archive=False) else: cid_qs = CidUserGroup.objects.filter(archive=False, open_access=True) user_qs = UserUserGroup.objects.filter(archive=False, open_access=True) self.fields["cid_user_groups"].queryset = cid_qs self.fields["user_user_groups"].queryset = user_qs self.fields["cid_user_groups"].widget.attrs["size"] = 8 self.fields["user_user_groups"].widget.attrs["size"] = 8 # Crispy helper: render fields prettily in templates. We set form_tag=False # because the HTMX partial includes the