diff --git a/ietf/meeting/forms.py b/ietf/meeting/forms.py index 640336da7..99ee5ba95 100644 --- a/ietf/meeting/forms.py +++ b/ietf/meeting/forms.py @@ -1,6 +1,11 @@ import datetime +import re from django import forms +from django.forms.fields import Field +from django.utils.encoding import force_text +from django.utils import six + from ietf.group.models import Group from ietf.ietfauth.utils import has_role from ietf.meeting.models import Meeting, Schedule, TimeSlot, Session, SchedTimeSessAssignment, countries, timezones @@ -9,6 +14,93 @@ from ietf.meeting.models import Meeting, Schedule, TimeSlot, Session, SchedTimeS countries.insert(0, ('', '-'*9 )) timezones.insert(0, ('', '-'*9 )) +# ------------------------------------------------- +# DurationField from Django 1.8 +# ------------------------------------------------- +def duration_string(duration): + days = duration.days + seconds = duration.seconds + microseconds = duration.microseconds + + minutes = seconds // 60 + seconds = seconds % 60 + + hours = minutes // 60 + minutes = minutes % 60 + + string = '{:02d}:{:02d}:{:02d}'.format(hours, minutes, seconds) + if days: + string = '{} '.format(days) + string + if microseconds: + string += '.{:06d}'.format(microseconds) + + return string + +standard_duration_re = re.compile( + r'^' + r'(?:(?P-?\d+) (days?, )?)?' + r'((?:(?P\d+):)(?=\d+:\d+))?' + r'(?:(?P\d+):)?' + r'(?P\d+)' + r'(?:\.(?P\d{1,6})\d{0,6})?' + r'$' +) + +# Support the sections of ISO 8601 date representation that are accepted by +# timedelta +iso8601_duration_re = re.compile( + r'^P' + r'(?:(?P\d+(.\d+)?)D)?' + r'(?:T' + r'(?:(?P\d+(.\d+)?)H)?' + r'(?:(?P\d+(.\d+)?)M)?' + r'(?:(?P\d+(.\d+)?)S)?' + r')?' + r'$' +) + +def parse_duration(value): + """Parses a duration string and returns a datetime.timedelta. + + The preferred format for durations in Django is '%d %H:%M:%S.%f'. + + Also supports ISO 8601 representation. + """ + match = standard_duration_re.match(value) + if not match: + match = iso8601_duration_re.match(value) + if match: + kw = match.groupdict() + if kw.get('microseconds'): + kw['microseconds'] = kw['microseconds'].ljust(6, '0') + kw = {k: float(v) for k, v in six.iteritems(kw) if v is not None} + return datetime.timedelta(**kw) + +class DurationField(Field): + default_error_messages = { + 'invalid': 'Enter a valid duration.', + } + + def prepare_value(self, value): + if isinstance(value, datetime.timedelta): + return duration_string(value) + return value + + def to_python(self, value): + if value in self.empty_values: + return None + if isinstance(value, datetime.timedelta): + return value + value = parse_duration(force_text(value)) + if value is None: + raise ValidationError(self.error_messages['invalid'], code='invalid') + return value + + +# ------------------------------------------------- +# Helpers +# ------------------------------------------------- + class GroupModelChoiceField(forms.ModelChoiceField): ''' Custom ModelChoiceField, changes the label to a more readable format @@ -16,9 +108,15 @@ class GroupModelChoiceField(forms.ModelChoiceField): def label_from_instance(self, obj): return obj.acronym +# ------------------------------------------------- +# Forms +# ------------------------------------------------- + class InterimRequestForm(forms.Form): group = GroupModelChoiceField(queryset = Group.objects.filter(type__in=('wg','rg'),state='active').order_by('acronym')) date = forms.DateField() + time = forms.TimeField() + duration = DurationField() face_to_face = forms.BooleanField(required=False) city = forms.CharField(max_length=255,required=False) country = forms.ChoiceField(choices=countries,required=False) @@ -41,6 +139,8 @@ class InterimRequestForm(forms.Form): agenda = self.cleaned_data.get('agenda') agenda_note = self.cleaned_data.get('agenda_note') date = self.cleaned_data.get('date') + time = self.cleaned_data.get('time') + duration = self.cleaned_data.get('duration') group = self.cleaned_data.get('group') city = self.cleaned_data.get('city') country = self.cleaned_data.get('country') @@ -51,8 +151,10 @@ class InterimRequestForm(forms.Form): meeting = Meeting.objects.create(number=number,type_id='interim',date=date,city=city, country=country,agenda_note=agenda_note,time_zone=timezone) schedule = Schedule.objects.create(meeting=meeting, owner=self.person, visible=True, public=True) - slot = TimeSlot.objects.create(meeting=meeting, type_id="session", duration=30 * 60, - time=datetime.datetime.combine(datetime.date.today(), datetime.time(9, 30))) + meeting.agenda = schedule + meeting.save() + slot = TimeSlot.objects.create(meeting=meeting, type_id="session", duration=duration, + time=datetime.datetime.combine(date, time)) session = Session.objects.create(meeting=meeting, group=group, requested_by=self.person, diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py index 181eae696..c6c899be3 100644 --- a/ietf/meeting/tests_views.py +++ b/ietf/meeting/tests_views.py @@ -389,6 +389,9 @@ class InterimTests(TestCase): def test_interim_request_submit(self): make_meeting_test_data() date = datetime.date.today() + datetime.timedelta(days=30) + time = datetime.datetime.now().time().replace(microsecond=0,second=0) + dt = datetime.datetime.combine(date, time) + duration = datetime.timedelta(hours=3) group = Group.objects.get(acronym='mars') city = 'San Francisco' country = 'US' @@ -399,6 +402,8 @@ class InterimTests(TestCase): self.client.login(username="secretary", password="secretary+password") data = {'group':group.pk, 'date':date.strftime("%Y-%m-%d"), + 'time':time.strftime('%H:%M'), + 'duration':'03:00:00', 'city':city, 'country':country, 'timezone':timezone, @@ -421,6 +426,9 @@ class InterimTests(TestCase): self.assertEqual(meeting.agenda_note,agenda_note) session = meeting.session_set.first() self.assertEqual(session.remote_instructions,remote_instructions) + timeslot = session.official_timeslotassignment().timeslot + self.assertEqual(timeslot.time,dt) + self.assertEqual(timeslot.duration,duration)