Source code for AKSubmission.forms
"""
Submission-specific forms
"""
import itertools
import re
from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from AKModel.availability.forms import AvailabilitiesFormMixin
from AKModel.availability.models import Availability
from AKModel.models import AK, AKOwner, AKCategory, AKRequirement, AKSlot, AKOrgaMessage
[docs]
class AKForm(AvailabilitiesFormMixin, forms.ModelForm):
"""
Base form to add and edit AKs
Contains suitable widgets for the different data types, restricts querysets (e.g., of requirements) to entries
belonging to the event this AK belongs to.
Prepares initial slot creation (by accepting multiple input formats and a list of slots to generate),
automatically generate short names and wiki links if necessary
Will be modified/used by :class:`AKSubmissionForm` (that allows to add slots and excludes links)
and :class:`AKWishForm`
"""
required_css_class = 'required'
split_string = re.compile('[,;]')
class Meta:
model = AK
fields = ['name',
'short_name',
'protocol_link',
'owners',
'description',
'category',
'reso',
'present',
'requirements',
'conflicts',
'prerequisites',
'notes',
'event'
]
widgets = {
'requirements': forms.CheckboxSelectMultiple,
'event': forms.HiddenInput,
}
[docs]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.initial = {**self.initial, **kwargs['initial']}
# Use better multiple select input for owners, conflicts and prerequisites
if "owners" in self.fields:
self.fields["owners"].widget.attrs = {'class': 'chosen-select'}
self.fields["conflicts"].widget.attrs = {'class': 'chosen-select'}
self.fields["prerequisites"].widget.attrs = {'class': 'chosen-select'}
self.fields['category'].queryset = AKCategory.objects.filter(event=self.initial.get('event'))
self.fields['requirements'].queryset = AKRequirement.objects.filter(event=self.initial.get('event'))
# Don't ask for requirements if there are no requirements configured for this event
if self.fields['requirements'].queryset.count() == 0:
self.fields.pop('requirements')
self.fields['prerequisites'].queryset = AK.objects.filter(event=self.initial.get('event')).exclude(
pk=self.instance.pk)
self.fields['conflicts'].queryset = AK.objects.filter(event=self.initial.get('event')).exclude(
pk=self.instance.pk)
if "owners" in self.fields:
self.fields['owners'].queryset = AKOwner.objects.filter(event=self.initial.get('event'))
@staticmethod
def _clean_duration(duration):
"""
Clean/convert input format for the duration(s) of the slot(s)
Handle different duration formats (h:mm and decimal comma instead of point)
:param duration: raw input, either with ":", "," or "."
:return: normalized duration (point-separated hour float)
"""
if ":" in duration:
h, m = duration.split(":")
duration = int(h) + int(m) / 60
if "," in str(duration):
duration = float(duration.replace(",", "."))
try:
duration = float(duration)
except ValueError as exc:
raise ValidationError(
_('"%(duration)s" is not a valid duration'),
code='invalid',
params={'duration': duration},
) from exc
return duration
[docs]
def clean(self):
"""
Normalize/clean inputs
Generate a (not yet used) short name if field was left blank,
create a list of normalized slot durations
:return: cleaned inputs
"""
cleaned_data = super().clean()
# Generate short name if not given
short_name = self.cleaned_data["short_name"]
if len(short_name) == 0:
short_name = self.cleaned_data['name']
# First try to split AK name at positions with semantic value (e.g., where the full name is separated
# by a ':'), if not possible, do a hard cut at the maximum specified length
short_name = short_name.partition(':')[0]
short_name = short_name.partition(' - ')[0]
short_name = short_name.partition(' (')[0]
short_name = short_name[:AK._meta.get_field('short_name').max_length]
# Check whether this short name already exists...
for i in itertools.count(1):
# ...and either use it...
if not AK.objects.filter(short_name=short_name, event=self.cleaned_data["event"]).exists():
break
# ... or postfix a number starting at 1 and growing until an unused short name is found
digits = len(str(i))
short_name = f'{short_name[:-(digits + 1)]}-{i}'
cleaned_data["short_name"] = short_name
# Get durations from raw durations field
if "durations" in cleaned_data:
cleaned_data["durations"] = [self._clean_duration(d) for d in self.cleaned_data["durations"].split()]
return cleaned_data
[docs]
class AKSubmissionForm(AKForm):
"""
Form for Submitting new AKs
Is a special variant of :class:`AKForm` that does not allow to manually edit wiki and protocol links and enforces
the generation of at least one slot.
"""
class Meta(AKForm.Meta):
# Exclude fields again that were previously included in the parent class
exclude = ['link', 'protocol_link'] #pylint: disable=modelform-uses-exclude
[docs]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Add field for durations (cleaning will be handled by parent class)
self.fields["durations"] = forms.CharField(
widget=forms.Textarea,
label=_("Duration(s)"),
help_text=_(
"Enter at least one planned duration (in hours). "
"If your AK should have multiple slots, use multiple lines"),
initial=self.initial.get('event').default_slot
)
[docs]
def clean_availabilities(self):
"""
Automatically improve availabilities entered.
If the user did not specify availabilities assume the full event duration is possible
:return: cleaned availabilities
(either user input or one availability for the full length of the event if user input was empty)
"""
availabilities = super().clean_availabilities()
if len(availabilities) == 0:
availabilities.append(Availability.with_event_length(event=self.cleaned_data["event"]))
return availabilities
[docs]
class AKWishForm(AKForm):
"""
Form for submitting or editing wishes
Is a special variant of :class:`AKForm` that does not allow to specify owner(s) or
manually edit wiki and protocol links
"""
class Meta(AKForm.Meta):
# Exclude fields again that were previously included in the parent class
exclude = ['owners', 'link', 'protocol_link'] #pylint: disable=modelform-uses-exclude
[docs]
class AKOwnerForm(forms.ModelForm):
"""
Form to create/edit AK owners
"""
required_css_class = 'required'
class Meta:
model = AKOwner
fields = ['name', 'institution', 'link', 'event']
widgets = {
'event': forms.HiddenInput
}
[docs]
class AKDurationForm(forms.ModelForm):
"""
Form to add an additional slot to a given AK
"""
class Meta:
model = AKSlot
fields = ['duration', 'ak', 'event']
widgets = {
'ak': forms.HiddenInput,
'event': forms.HiddenInput
}
[docs]
class AKOrgaMessageForm(forms.ModelForm):
"""
Form to create a confidential message to the organizers belonging to a given AK
"""
class Meta:
model = AKOrgaMessage
fields = ['ak', 'text', 'event']
widgets = {
'ak': forms.HiddenInput,
'event': forms.HiddenInput,
'text': forms.Textarea,
}