import os import codecs import datetime from django import forms from django.db.models import Q from django.forms import BaseInlineFormSet from ietf.doc.models import Document, DocAlias, State, NewRevisionDocEvent from ietf.doc.utils import get_document_content from ietf.group.models import Group from ietf.ietfauth.utils import has_role from ietf.meeting.models import Session, Meeting, Schedule, countries, timezones from ietf.meeting.helpers import get_next_interim_number, make_materials_directories from ietf.meeting.helpers import is_meeting_approved, get_next_agenda_name from ietf.message.models import Message from ietf.person.models import Person from ietf.utils.fields import DatepickerDateField, DurationField # need to insert empty option for use in ChoiceField # countries.insert(0, ('', '-'*9 )) countries.insert(0, ('', '')) timezones.insert(0, ('', '-' * 9)) # ------------------------------------------------- # Helpers # ------------------------------------------------- class GroupModelChoiceField(forms.ModelChoiceField): ''' Custom ModelChoiceField, changes the label to a more readable format ''' def label_from_instance(self, obj): return obj.acronym class CustomDurationField(DurationField): '''Custom DurationField to display as HH:MM (no seconds)''' def prepare_value(self, value): if isinstance(value, datetime.timedelta): return duration_string(value) return value def duration_string(duration): '''Custom duration_string to return HH:MM (no seconds)''' days = duration.days seconds = duration.seconds microseconds = duration.microseconds minutes = seconds // 60 seconds = seconds % 60 hours = minutes // 60 minutes = minutes % 60 string = '{:02d}:{:02d}'.format(hours, minutes) if days: string = '{} '.format(days) + string if microseconds: string += '.{:06d}'.format(microseconds) return string # ------------------------------------------------- # Forms # ------------------------------------------------- class InterimSessionInlineFormSet(BaseInlineFormSet): def __init__(self, *args, **kwargs): super(InterimSessionInlineFormSet, self).__init__(*args, **kwargs) if 'data' in kwargs: self.meeting_type = kwargs['data']['meeting_type'] def clean(self): '''Custom clean method to verify dates are consecutive for multi-day meetings''' super(InterimSessionInlineFormSet, self).clean() if self.meeting_type == 'multi-day': dates = [] for form in self.forms: date = form.cleaned_data.get('date') if date: dates.append(date) if len(dates) < 2: return self.cleaned_data dates.sort() last_date = dates[0] for date in dates[1:]: if last_date + datetime.timedelta(days=1) != date: raise forms.ValidationError('For Multi-Day meetings, days must be consecutive') last_date = date return self.cleaned_data class InterimMeetingModelForm(forms.ModelForm): group = GroupModelChoiceField(queryset=Group.objects.filter(type__in=('wg', 'rg'), state__in=('active', 'proposed', 'bof')).order_by('acronym'), required=False) in_person = forms.BooleanField(required=False) meeting_type = forms.ChoiceField(choices=( ("single", "Single"), ("multi-day", "Multi-Day"), ('series', 'Series')), required=False, initial='single', widget=forms.RadioSelect) approved = forms.BooleanField(required=False) city = forms.CharField(max_length=255, required=False) country = forms.ChoiceField(choices=countries, required=False) time_zone = forms.ChoiceField(choices=timezones) class Meta: model = Meeting fields = ('group', 'in_person', 'meeting_type', 'approved', 'city', 'country', 'time_zone') def __init__(self, request, *args, **kwargs): super(InterimMeetingModelForm, self).__init__(*args, **kwargs) self.user = request.user self.person = self.user.person self.is_edit = bool(self.instance.pk) self.fields['group'].widget.attrs['class'] = "select2-field" self.fields['time_zone'].initial = 'UTC' self.fields['approved'].initial = True self.set_group_options() if self.is_edit: self.fields['group'].initial = self.instance.session_set.first().group self.fields['group'].widget.attrs['disabled'] = True if self.instance.city or self.instance.country: self.fields['in_person'].initial = True if is_meeting_approved(self.instance): self.fields['approved'].initial = True else: self.fields['approved'].initial = False self.fields['approved'].widget.attrs['disabled'] = True def clean(self): super(InterimMeetingModelForm, self).clean() cleaned_data = self.cleaned_data if not cleaned_data.get('group'): raise forms.ValidationError("You must select a group") return self.cleaned_data def is_virtual(self): if not self.is_bound or self.data.get('in_person'): return False else: return True def set_group_options(self): '''Set group options based on user accessing the form''' if has_role(self.user, "Secretariat"): return # don't reduce group options q_objects = Q() if has_role(self.user, "Area Director"): q_objects.add(Q(type="wg", state__in=("active", "proposed", "bof")), Q.OR) if has_role(self.user, "IRTF Chair"): q_objects.add(Q(type="rg", state__in=("active", "proposed")), Q.OR) if has_role(self.user, "WG Chair"): q_objects.add(Q(type="wg", state__in=("active", "proposed", "bof"), role__person=self.person, role__name="chair"), Q.OR) if has_role(self.user, "RG Chair"): q_objects.add(Q(type="rg", state__in=("active", "proposed"), role__person=self.person, role__name="chair"), Q.OR) queryset = Group.objects.filter(q_objects).distinct().order_by('acronym') self.fields['group'].queryset = queryset # if there's only one possibility make it the default if len(queryset) == 1: self.fields['group'].initial = queryset[0] def save(self, *args, **kwargs): '''Save must handle fields not included in the form: date,number,type_id''' date = kwargs.pop('date') group = self.cleaned_data.get('group') meeting = super(InterimMeetingModelForm, self).save(commit=False) if not meeting.type_id: meeting.type_id = 'interim' if not meeting.number: meeting.number = get_next_interim_number(group.acronym, date) meeting.date = date if kwargs.get('commit', True): # create schedule with meeting meeting.save() # pre-save so we have meeting.pk for schedule if not meeting.agenda: meeting.agenda = Schedule.objects.create( meeting=meeting, owner=Person.objects.get(name='(System)')) meeting.save() # save with agenda # create directories make_materials_directories(meeting) return meeting class InterimSessionModelForm(forms.ModelForm): date = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={"autoclose": "1"}, label='Date', required=False) time = forms.TimeField(widget=forms.TimeInput(format='%H:%M'), required=True) requested_duration = CustomDurationField(required=True) end_time = forms.TimeField(required=False) remote_instructions = forms.CharField(max_length=1024, required=True) agenda = forms.CharField(required=False, widget=forms.Textarea, strip=False) agenda_note = forms.CharField(max_length=255, required=False) class Meta: model = Session fields = ('date', 'time', 'requested_duration', 'end_time', 'remote_instructions', 'agenda', 'agenda_note') def __init__(self, *args, **kwargs): if 'user' in kwargs: self.user = kwargs.pop('user') if 'group' in kwargs: self.group = kwargs.pop('group') if 'is_approved_or_virtual' in kwargs: self.is_approved_or_virtual = kwargs.pop('is_approved_or_virtual') super(InterimSessionModelForm, self).__init__(*args, **kwargs) self.is_edit = bool(self.instance.pk) # setup fields that aren't intrinsic to the Session object if self.is_edit: self.initial['date'] = self.instance.official_timeslotassignment().timeslot.time self.initial['time'] = self.instance.official_timeslotassignment().timeslot.time if self.instance.agenda(): doc = self.instance.agenda() path = os.path.join(doc.get_file_path(), doc.filename_with_rev()) self.initial['agenda'] = get_document_content(os.path.basename(path), path, markup=False) def clean_date(self): '''Date field validator. We can't use required on the input because it is a datepicker widget''' date = self.cleaned_data.get('date') if not date: raise forms.ValidationError('Required field') return date def save(self, *args, **kwargs): """NOTE: as the baseform of an inlineformset self.save(commit=True) never gets called""" session = super(InterimSessionModelForm, self).save(commit=kwargs.get('commit', True)) if self.is_approved_or_virtual: session.status_id = 'scheda' else: session.status_id = 'apprw' session.group = self.group session.type_id = 'session' if not self.instance.pk: session.requested_by = self.user.person return session def save_agenda(self): if self.instance.agenda(): doc = self.instance.agenda() doc.rev = str(int(doc.rev) + 1).zfill(2) doc.save() else: filename = get_next_agenda_name(meeting=self.instance.meeting) doc = Document.objects.create( type_id='agenda', group=self.group, name=filename, rev='00', external_url='{}-00.txt'.format(filename)) doc.set_state(State.objects.get(type__slug=doc.type.slug, slug='active')) DocAlias.objects.create(name=doc.name, document=doc) self.instance.sessionpresentation_set.create(document=doc, rev=doc.rev) NewRevisionDocEvent.objects.create( type='new_revision', by=self.user.person, doc=doc, rev=doc.rev, desc='New revision available') # write file path = os.path.join(self.instance.meeting.get_materials_path(), 'agenda', doc.filename_with_rev()) directory = os.path.dirname(path) if not os.path.exists(directory): os.makedirs(directory) with codecs.open(path, "w", encoding='utf-8') as file: file.write(self.cleaned_data['agenda']) class InterimAnnounceForm(forms.ModelForm): class Meta: model = Message fields = ('to', 'frm', 'cc', 'bcc', 'reply_to', 'subject', 'body') def save(self, *args, **kwargs): user = kwargs.pop('user') message = super(InterimAnnounceForm, self).save(commit=False) message.by = user.person message.save() return message class InterimCancelForm(forms.Form): group = forms.CharField(max_length=255, required=False) date = forms.DateField(required=False) comments = forms.CharField(required=False, widget=forms.Textarea(attrs={'placeholder': 'enter optional comments here'}), strip=False) def __init__(self, *args, **kwargs): super(InterimCancelForm, self).__init__(*args, **kwargs) self.fields['group'].widget.attrs['disabled'] = True self.fields['date'].widget.attrs['disabled'] = True