datatracker/ietf/meeting/forms.py
Henrik Levkowetz f2b883d2bb Merged in ^/trunk@17617.
- Legacy-Id: 17618
2020-04-14 17:11:51 +00:00

360 lines
15 KiB
Python

# Copyright The IETF Trust 2016-2020, All Rights Reserved
# -*- coding: utf-8 -*-
import io
import os
import datetime
from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db.models import Q
from django.forms import BaseInlineFormSet
import debug # pyflakes:ignore
from ietf.doc.models import Document, DocAlias, State, NewRevisionDocEvent
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_interim_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, MultiEmailField
from ietf.utils.validators import ( validate_file_size, validate_mime_type,
validate_file_extension, validate_no_html_frame)
# 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
dates.sort()
last_date = dates[0]
for date in dates[1:]:
if date - last_date != datetime.timedelta(days=1):
raise forms.ValidationError('For Multi-Day meetings, days must be consecutive')
last_date = date
self.days = len(dates)
return # formset doesn't have cleaned_data
class InterimMeetingModelForm(forms.ModelForm):
# TODO: Should area groups get to schedule Interims?
group = GroupModelChoiceField(queryset=Group.objects.filter(type__in=('wg', 'rg', 'ag'), 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_interim_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
meeting.days = 1
if kwargs.get('commit', True):
# create schedule with meeting
meeting.save() # pre-save so we have meeting.pk for schedule
if not meeting.schedule:
meeting.schedule = Schedule.objects.create(
meeting=meeting,
owner=Person.objects.get(name='(System)'))
meeting.save() # save with schedule
# 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 'requires_approval' in kwargs:
self.requires_approval = kwargs.pop('requires_approval')
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()
content = doc.text_or_error()
self.initial['agenda'] = content
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 clean_requested_duration(self):
min_minutes = settings.INTERIM_SESSION_MINIMUM_MINUTES
max_minutes = settings.INTERIM_SESSION_MAXIMUM_MINUTES
duration = self.cleaned_data.get('requested_duration')
if not duration or duration < datetime.timedelta(minutes=min_minutes) or duration > datetime.timedelta(minutes=max_minutes):
raise forms.ValidationError('Provide a duration, %s-%smin.' % (min_minutes, max_minutes))
return duration
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))
session.group = self.group
session.type_id = 'regular'
return session
def save_agenda(self):
if self.instance.agenda():
doc = self.instance.agenda()
doc.rev = str(int(doc.rev) + 1).zfill(2)
e = NewRevisionDocEvent.objects.create(
type='new_revision',
by=self.user.person,
doc=doc,
rev=doc.rev,
desc='New revision available')
doc.save_with_history([e])
else:
filename = get_next_agenda_name(meeting=self.instance.meeting)
doc = Document.objects.create(
type_id='agenda',
group=self.group,
name=filename,
rev='00',
# FIXME: if these are always computed, they shouldn't be in uploaded_filename - just compute them when needed
# FIXME: What about agendas in html or markdown format?
uploaded_filename='{}-00.txt'.format(filename))
doc.set_state(State.objects.get(type__slug=doc.type.slug, slug='active'))
DocAlias.objects.create(name=doc.name).docs.add(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 io.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
class FileUploadForm(forms.Form):
file = forms.FileField(label='File to upload')
def __init__(self, *args, **kwargs):
doc_type = kwargs.pop('doc_type')
assert doc_type in settings.MEETING_VALID_UPLOAD_EXTENSIONS
self.doc_type = doc_type
self.extensions = settings.MEETING_VALID_UPLOAD_EXTENSIONS[doc_type]
self.mime_types = settings.MEETING_VALID_UPLOAD_MIME_TYPES[doc_type]
super(FileUploadForm, self).__init__(*args, **kwargs)
label = '%s file to upload. ' % (self.doc_type.capitalize(), )
if self.mime_types:
label += 'Note that you can only upload files with these formats: %s.' % (', '.join(self.mime_types, ))
self.fields['file'].label=label
def clean_file(self):
file = self.cleaned_data['file']
validate_file_size(file)
ext = validate_file_extension(file, self.extensions)
mime_type, encoding = validate_mime_type(file, self.mime_types)
if not hasattr(self, 'file_encoding'):
self.file_encoding = {}
self.file_encoding[file.name] = encoding or None
if self.mime_types:
if not file.content_type in settings.MEETING_VALID_UPLOAD_MIME_FOR_OBSERVED_MIME[mime_type]:
raise ValidationError('Upload Content-Type (%s) is different from the observed mime-type (%s)' % (file.content_type, mime_type))
if mime_type in settings.MEETING_VALID_MIME_TYPE_EXTENSIONS:
if not ext in settings.MEETING_VALID_MIME_TYPE_EXTENSIONS[mime_type]:
raise ValidationError('Upload Content-Type (%s) does not match the extension (%s)' % (file.content_type, ext))
if mime_type in ['text/html', ] or ext in settings.MEETING_VALID_MIME_TYPE_EXTENSIONS['text/html']:
# We'll do html sanitization later, but for frames, we fail here,
# as the sanitized version will most likely be useless.
validate_no_html_frame(file)
return file
class RequestMinutesForm(forms.Form):
to = MultiEmailField()
cc = MultiEmailField(required=False)
subject = forms.CharField()
body = forms.CharField(widget=forms.Textarea,strip=False)