diff --git a/ietf/doc/templatetags/ietf_filters.py b/ietf/doc/templatetags/ietf_filters.py index e7ba11b4d..e4e1ceb05 100644 --- a/ietf/doc/templatetags/ietf_filters.py +++ b/ietf/doc/templatetags/ietf_filters.py @@ -610,3 +610,9 @@ def session_end_time(session): timeslot = session.official_timeslotassignment().timeslot return timeslot.time + timeslot.duration +@register.filter +def format_timedelta(timedelta): + s = timedelta.seconds + hours, remainder = divmod(s, 3600) + minutes, seconds = divmod(remainder, 60) + return '{hours:02d}:{minutes:02d}'.format(hours=hours,minutes=minutes) diff --git a/ietf/meeting/forms.py b/ietf/meeting/forms.py index 994d25646..2ed7902ec 100644 --- a/ietf/meeting/forms.py +++ b/ietf/meeting/forms.py @@ -9,22 +9,27 @@ from django.utils.encoding import force_text from django.utils import six 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, countries, timezones -from ietf.meeting.helpers import assign_interim_session +from ietf.meeting.models import Session, Meeting, Schedule, countries, timezones +from ietf.meeting.helpers import get_next_interim_number, assign_interim_session +from ietf.meeting.helpers import is_meeting_approved from ietf.message.models import Message +from ietf.person.models import Person from ietf.secr.utils.meeting import get_upload_root from ietf.utils.fields import DatepickerDateField # need to insert empty option for use in ChoiceField -#countries.insert(0, ('', '-'*9 )) +# countries.insert(0, ('', '-'*9 )) countries.insert(0, ('', '')) -timezones.insert(0, ('', '-'*9 )) +timezones.insert(0, ('', '-' * 9)) # ------------------------------------------------- # DurationField from Django 1.8 # ------------------------------------------------- + + def duration_string(duration): days = duration.days seconds = duration.seconds @@ -36,7 +41,8 @@ def duration_string(duration): hours = minutes // 60 minutes = minutes % 60 - string = '{:02d}:{:02d}:{:02d}'.format(hours, minutes, seconds) + # string = '{:02d}:{:02d}:{:02d}'.format(hours, minutes, seconds) + string = '{:02d}:{:02d}'.format(hours, minutes) if days: string = '{} '.format(days) + string if microseconds: @@ -44,6 +50,10 @@ def duration_string(duration): return string +custom_duration_re = re.compile( + r'^(?P\d+):(?P\d+)$' +) + standard_duration_re = re.compile( r'^' r'(?:(?P-?\d+) (days?, )?)?' @@ -67,6 +77,7 @@ iso8601_duration_re = re.compile( r'$' ) + def parse_duration(value): """Parses a duration string and returns a datetime.timedelta. @@ -74,7 +85,9 @@ def parse_duration(value): Also supports ISO 8601 representation. """ - match = standard_duration_re.match(value) + match = custom_duration_re.match(value) + if not match: + match = standard_duration_re.match(value) if not match: match = iso8601_duration_re.match(value) if match: @@ -84,6 +97,7 @@ def parse_duration(value): 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.', @@ -109,6 +123,7 @@ class DurationField(Field): # Helpers # ------------------------------------------------- + class GroupModelChoiceField(forms.ModelChoiceField): ''' Custom ModelChoiceField, changes the label to a more readable format @@ -116,43 +131,42 @@ class GroupModelChoiceField(forms.ModelChoiceField): def label_from_instance(self, obj): return obj.acronym -''' -class BaseSessionFormSet(BaseFormSet): - def save_new_objects(self, commit=True): - self.new_objects = [] - for form in self.extra_forms: - if not form.has_changed(): - continue - # If someone has marked an add form for deletion, don't save the - # object. - if self.can_delete and self._should_delete_form(form): - continue - self.new_objects.append(self.save_new(form, commit=commit)) - if not commit: - self.saved_forms.append(form) - return self.new_objects - - def save(self,commit=True): - #return self.save_existing_objects(commit) + self.save_new_objects(commit) - return self.save_new_objects(commit) -''' - # ------------------------------------------------- # Forms # ------------------------------------------------- -class InterimRequestForm(forms.Form): - group = GroupModelChoiceField(queryset = Group.objects.filter(type__in=('wg','rg'),state='active').order_by('acronym')) + +class InterimMeetingModelForm(forms.ModelForm): + group = GroupModelChoiceField(queryset=Group.objects.filter(type__in=('wg', 'rg'), state='active').order_by('acronym')) 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) + 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(InterimRequestForm, self).__init__(*args, **kwargs) + super(InterimMeetingModelForm, self).__init__(*args, **kwargs) self.user = request.user self.person = self.user.person - self.fields["group"].widget.attrs["class"] = "select2-field" + self.is_edit = bool(self.instance.pk) + self.fields['group'].widget.attrs['class'] = "select2-field" + self.fields['time_zone'].initial = 'UTC' 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 def set_group_options(self): '''Set group options based on user accessing the form''' @@ -160,40 +174,132 @@ class InterimRequestForm(forms.Form): if has_role(self.user, "Secretariat"): return # don't reduce group options if has_role(self.user, "Area Director"): - queryset = Group.objects.filter(type="wg", state__in=("active","proposed")).order_by('acronym') + queryset = Group.objects.filter(type="wg", state__in=("active", "proposed")).order_by('acronym') elif has_role(self.user, "IRTF Chair"): - queryset = Group.objects.filter(type="rg", state__in=("active","proposed")).order_by('acronym') + queryset = Group.objects.filter(type="rg", state__in=("active", "proposed")).order_by('acronym') elif has_role(self.user, "WG Chair"): - queryset = Group.objects.filter(type="wg", state__in=("active","proposed"), role__person=self.person, role__name="chair").distinct().order_by('acronym') + queryset = Group.objects.filter(type="wg", state__in=("active", "proposed"), role__person=self.person, role__name="chair").distinct().order_by('acronym') elif has_role(self.user, "RG Chair"): - queryset = Group.objects.filter(type="rg", state__in=("active","proposed"), role__person=self.person, role__name="chair").distinct().order_by('acronym') + queryset = Group.objects.filter(type="rg", state__in=("active", "proposed"), role__person=self.person, role__name="chair").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, date) + meeting.date = date + if kwargs.get('commit', True): + # create schedule with meeting + if not meeting.agenda: + meeting.agenda = Schedule.objects.create( + meeting=meeting, + owner=Person.objects.get(name='(System)')) + meeting.save() + + 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=False) + time_utc = forms.TimeField(required=False) + requested_duration = DurationField(required=False) + end_time = forms.TimeField(required=False) + end_time_utc = forms.TimeField(required=False) + remote_instructions = forms.CharField(max_length=1024, required=False) + agenda = forms.CharField(required=False, widget=forms.Textarea) + agenda_note = forms.CharField(max_length=255, required=False) + + class Meta: + model = Session + fields = ('date', 'time', 'time_utc', 'requested_duration', 'end_time', + 'end_time_utc', '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' in kwargs: + self.is_approved = kwargs.pop('is_approved') + 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 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: + 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 = 'agenda-interim-{group}-{date}-{time}'.format( + group=self.group.acronym, + date=self.cleaned_data['date'].strftime("%Y-%m-%d-"), + time=self.cleaned_data['time'].strftime("%H%M")) + doc = Document.objects.create( + type_id='agenda', + group=self.group, + name=filename, + rev='00') + doc.set_state(State.objects.get(type=doc.type, 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(get_upload_root(self.instance.meeting), 'agenda', doc.filename_with_rev()) + directory = os.path.dirname(path) + if not os.path.exists(directory): + os.makedirs(directory) + with open(path, "w") as file: + file.write(self.cleaned_data['agenda']) + + class InterimSessionForm(forms.Form): - # unset: date,time,duration - date = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={"autoclose": "1" }, label='Date', required=False) + date = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={"autoclose": "1"}, label='Date', required=False) time = forms.TimeField(required=False) time_utc = forms.TimeField(required=False) duration = DurationField(required=False) end_time = forms.TimeField(required=False) end_time_utc = forms.TimeField(required=False) - remote_instructions = forms.CharField(max_length=1024,required=False) - agenda = forms.CharField(required=False,widget=forms.Textarea) - agenda_note = forms.CharField(max_length=255,required=False) - city = forms.CharField(max_length=255,required=False) - country = forms.ChoiceField(choices=countries,required=False) - timezone = forms.ChoiceField(choices=timezones) - - def __init__(self, *args, **kwargs): - super(InterimSessionForm, self).__init__(*args, **kwargs) - self.fields['timezone'].initial = 'UTC' - - def _save_agenda(self, session): - pass + remote_instructions = forms.CharField(max_length=1024, required=False) + agenda = forms.CharField(required=False, widget=forms.Textarea) + agenda_note = forms.CharField(max_length=255, required=False) def save(self, request, group, meeting, is_approved): person = request.user.person @@ -208,7 +314,8 @@ class InterimSessionForm(forms.Form): status_id = 'scheda' else: status_id = 'apprw' - session = Session.objects.create(meeting=meeting, + session = Session.objects.create( + meeting=meeting, group=group, requested_by=person, requested_duration=duration, @@ -216,22 +323,23 @@ class InterimSessionForm(forms.Form): type_id='session', remote_instructions=remote_instructions, agenda_note=agenda_note,) - assign_interim_session(session,time) - + assign_interim_session(session, time) + if agenda: # create objects - filename = 'agenda-interim-%s-%s' % (group.acronym,time.strftime("%Y-%m-%d-%H%M")) - doc = Document.objects.create(type_id='agenda',group=group,name=filename,rev='00') - doc.set_state(State.objects.get(type=doc.type,slug='active')) + filename = 'agenda-interim-%s-%s' % (group.acronym, time.strftime("%Y-%m-%d-%H%M")) + doc = Document.objects.create(type_id='agenda', group=group, name=filename, rev='00') + doc.set_state(State.objects.get(type=doc.type, slug='active')) DocAlias.objects.create(name=doc.name, document=doc) - session.sessionpresentation_set.create(document=doc,rev=doc.rev) - NewRevisionDocEvent.objects.create(type='new_revision', + session.sessionpresentation_set.create(document=doc, rev=doc.rev) + NewRevisionDocEvent.objects.create( + type='new_revision', by=request.user.person, doc=doc, rev=doc.rev, desc='New revision available') # write file - path = os.path.join(get_upload_root(meeting),'agenda',doc.filename_with_rev()) + path = os.path.join(get_upload_root(meeting), 'agenda', doc.filename_with_rev()) directory = os.path.dirname(path) if not os.path.exists(directory): os.makedirs(directory) @@ -240,19 +348,17 @@ class InterimSessionForm(forms.Form): return session + class InterimAnnounceForm(forms.ModelForm): class Meta: model = Message - fields = ('to','frm','cc','bcc','reply_to','subject','body') + fields = ('to', 'frm', 'cc', 'bcc', 'reply_to', 'subject', 'body') - #def __init__(self): - # super(InterimAnnounceForm, self).__init__(*args,**kwargs) - def save(self, *args, **kwargs): user = kwargs.pop('user') message = super(InterimAnnounceForm, self).save(commit=False) message.by = user.person message.save() - return message \ No newline at end of file + return message diff --git a/ietf/meeting/helpers.py b/ietf/meeting/helpers.py index df7f12f49..ca9544950 100644 --- a/ietf/meeting/helpers.py +++ b/ietf/meeting/helpers.py @@ -16,6 +16,7 @@ from django.template.loader import render_to_string import debug # pyflakes:ignore from ietf.doc.models import Document +from ietf.doc.utils import get_document_content from ietf.group.models import Group from ietf.ietfauth.utils import has_role, user_is_person from ietf.liaisons.utils import get_person_for_user @@ -290,67 +291,31 @@ def session_constraint_expire(request,session): # ------------------------------------------------- # Interim Meeting Helpers # ------------------------------------------------- -def get_announcement_initial(meeting): - '''Returns a dictionary suitable to initialize an InterimAnnouncementForm (Message ModelForm)''' - group = meeting.session_set.first().group - in_person = bool(meeting.city) - initial = {} - initial['to'] = settings.INTERIM_ANNOUNCE_TO_EMAIL - initial['cc'] = group.list_email - initial['frm'] = settings.INTERIM_ANNOUNCE_FROM_EMAIL - if in_person: - desc = 'Interim' + + +def assign_interim_session(form): + """Helper function to create a timeslot and assign the interim session""" + time = datetime.datetime.combine( + form.cleaned_data['date'], + form.cleaned_data['time']) + session = form.instance + if session.official_timeslotassignment(): + slot = session.official_timeslotassignment().timeslot + slot.time = time + slot.save() else: - desc = 'Virtual' - initial['subject'] = '%s (%s) WG %s Meeting: %s' % (group.name,group.acronym,desc,meeting.date) - body = render_to_string('meeting/interim_announcement.txt', locals()) - initial['body'] = body - return initial + slot = TimeSlot.objects.create( + meeting=session.meeting, + type_id="session", + duration=session.requested_duration, + time=time) + SchedTimeSessAssignment.objects.create( + timeslot=slot, + session=session, + schedule=session.meeting.agenda) -def get_earliest_session(session_formset): - '''Return earliest InterimSessionForm from formset''' - earliest = session_formset[0] - if len(session_formset) == 1: - return earliest - for form in session_formset[1:]: - date = form.cleaned_data.get('date') - if date and date < earliest.cleaned_data.get('date'): - earliest = form - return earliest -def get_next_interim_number(group,date): - sequence = Meeting.objects.filter(number__startswith='interim-%s-%s' % (date.year,group.acronym)).count() + 1 - return 'interim-%s-%s-%s' % (date.year,group.acronym,sequence) - -def create_interim_meeting(group,date,city='',country='',timezone='UTC',person=None): - '''Helper function to create interim meeting and associated schedule''' - if not person: - person = Person.objects.get(name="(System)") - number = get_next_interim_number(group,date) - meeting = Meeting.objects.create(number=number,type_id='interim',date=date,city=city, - country=country,time_zone=timezone) - schedule = Schedule.objects.create(meeting=meeting, owner=person, visible=True, public=True) - meeting.agenda = schedule - meeting.save() - return meeting - -def create_interim_meeting_from_forms(request_form,session_form): - '''Create an Interim meeting, given an InterimRequestForm and InterimSessionForm''' - group = request_form.cleaned_data.get('group') - date = session_form.cleaned_data.get('date') - city = session_form.cleaned_data.get('city') - country = session_form.cleaned_data.get('country') - timezone = session_form.cleaned_data.get('timezone') - person = request_form.person - return create_interim_meeting(group=group,date=date,city=city,country=country,timezone=timezone,person=person) - -def assign_interim_session(session,time): - '''Helper function to create a timeslot and assign the interim session''' - slot = TimeSlot.objects.create(meeting=session.meeting, type_id="session", - duration=session.requested_duration, time=time) - SchedTimeSessAssignment.objects.create(timeslot=slot, session=session, schedule=session.meeting.agenda) - -def can_approve_interim_request(meeting,user): +def can_approve_interim_request(meeting, user): '''Returns True if the user has permissions to approve an interim meeting request''' if meeting.type.slug != 'interim': return False @@ -361,26 +326,29 @@ def can_approve_interim_request(meeting,user): if not session: return False group = session.group - if group.type.slug == 'wg' and group.parent.role_set.filter(name='ad',person=person): + if group.type.slug == 'wg' and group.parent.role_set.filter(name='ad', person=person): return True - if group.type.slug == 'rg' and group.parent.role_set.filter(name='chair',person=person): + if group.type.slug == 'rg' and group.parent.role_set.filter(name='chair', person=person): return True return False -def can_edit_interim_request(meeting,user): + +def can_edit_interim_request(meeting, user): '''Returns True if the user can edit the interim meeting request''' - - if can_approve_interim_request(meeting,user): + + if can_approve_interim_request(meeting, user): return True - + return False + def can_request_interim_meeting(user): - if has_role(user, ('Secretariat','Area Director','WG Chair','IRTF Chair', 'RG Chair')): + if has_role(user, ('Secretariat', 'Area Director', 'WG Chair', 'IRTF Chair', 'RG Chair')): return True return False -def can_view_interim_request(meeting,user): + +def can_view_interim_request(meeting, user): '''Returns True if the user can see the pending interim request in the pending interim view''' if meeting.type.slug != 'interim': return False @@ -395,6 +363,129 @@ def can_view_interim_request(meeting,user): return True if has_role(user, 'IRTF Chair') and group.type.slug == 'rg': return True - if group.role_set.filter(name='chair',person=person): + if group.role_set.filter(name='chair', person=person): return True return False + + +def create_interim_meeting(group, date, city='', country='', timezone='UTC', + person=None): + """Helper function to create interim meeting and associated schedule""" + if not person: + person = Person.objects.get(name='(System)') + number = get_next_interim_number(group, date) + meeting = Meeting.objects.create( + number=number, + type_id='interim', + date=date, + city=city, + country=country, + time_zone=timezone) + schedule = Schedule.objects.create( + meeting=meeting, + owner=person, + visible=True, + public=True) + meeting.agenda = schedule + meeting.save() + return meeting + + +def get_announcement_initial(meeting): + '''Returns a dictionary suitable to initialize an InterimAnnouncementForm (Message ModelForm)''' + group = meeting.session_set.first().group + in_person = bool(meeting.city) + initial = {} + initial['to'] = settings.INTERIM_ANNOUNCE_TO_EMAIL + initial['cc'] = group.list_email + initial['frm'] = settings.INTERIM_ANNOUNCE_FROM_EMAIL + if in_person: + desc = 'Interim' + else: + desc = 'Virtual' + initial['subject'] = '%s (%s) WG %s Meeting: %s' % (group.name, group.acronym, desc, meeting.date) + body = render_to_string('meeting/interim_announcement.txt', locals()) + initial['body'] = body + return initial + + +def get_earliest_session_date(formset): + '''Return earliest date from InterimSession Formset''' + return sorted([f.cleaned_data['date'] for f in formset.forms if f.cleaned_data.get('date')])[0] + + +def get_interim_initial(meeting): + '''Returns a dictionary suitable to initialize a InterimRequestForm''' + initial = {} + initial['group'] = meeting.session_set.first().group + if meeting.city: + initial['in_person'] = True + else: + initial['in_person'] = False + if meeting.session_set.count() > 1: + initial['meeting_type'] = 'multi-day' + else: + initial['meeting_type'] = 'single' + if meeting.session_set.first().status.slug == 'apprw': + initial['approved'] = False + else: + initial['approved'] = True + return initial + + +def get_interim_session_initial(meeting): + '''Returns a list of dictionaries suitable to initialize a InterimSessionForm''' + initials = [] + for session in meeting.session_set.all(): + initial = {} + initial['date'] = session.official_timeslotassignment().timeslot.time + initial['time'] = session.official_timeslotassignment().timeslot.time + initial['duration'] = session.requested_duration + initial['remote_instructions'] = session.remote_instructions + initial['agenda_note'] = session.agenda_note + doc = session.agenda() + if doc: + path = os.path.join(doc.get_file_path(), doc.filename_with_rev()) + initial['agenda'] = get_document_content(os.path.basename(path), path, markup=False) + initials.append(initial) + + return initials + + +def is_meeting_approved(meeting): + """Returns True if the meeting is approved""" + if meeting.session_set.first().status.slug == 'apprw': + return False + else: + return True + + +def get_next_interim_number(group, date): + """Returns a unique number to use for the next interim meeting for + *group*""" + meetings = Meeting.objects.filter( + number__startswith='interim-{year}-{group}'.format( + year=date.year, + group=group.acronym)) + if meetings: + sequences = [int(m.number.split('-')[-1]) for m in meetings] + last_sequence = sorted(sequences)[-1] + else: + last_sequence = 0 + return 'interim-{year}-{group}-{sequence}'.format( + year=date.year, + group=group.acronym, + sequence=last_sequence + 1) + + +def sessions_post_save(forms): + """Helper function to perform various post save operations on each form of a + InterimSessionModelForm formset""" + + for form in forms: + if not form.has_changed(): + continue + if ('date' in form.changed_data) or ('time' in form.changed_data): + assign_interim_session(form) + if 'agenda' in form.changed_data: + form.save_agenda() diff --git a/ietf/meeting/models.py b/ietf/meeting/models.py index 2d84f5ace..131ea89b7 100644 --- a/ietf/meeting/models.py +++ b/ietf/meeting/models.py @@ -995,7 +995,7 @@ class Session(models.Model): return Constraint.objects.filter(target=self.group, meeting=self.meeting).order_by('name__name') def timeslotassignment_for_agenda(self, schedule): - return self.timeslotassignments.filter(schedule=schedule)[0] + return self.timeslotassignments.filter(schedule=schedule).first() def official_timeslotassignment(self): return self.timeslotassignment_for_agenda(self.meeting.agenda) diff --git a/ietf/meeting/test_data.py b/ietf/meeting/test_data.py index 657e59c93..ead5ac9e1 100644 --- a/ietf/meeting/test_data.py +++ b/ietf/meeting/test_data.py @@ -16,7 +16,16 @@ def make_interim_meeting(group,date,status='sched'): attendees=10, requested_by=system_person, requested_duration=20, status_id=status, scheduled=datetime.datetime.now(),type_id="session") - assign_interim_session(session,time) + #assign_interim_session(session,time) + slot = TimeSlot.objects.create( + meeting=meeting, + type_id="session", + duration=session.requested_duration, + time=time) + SchedTimeSessAssignment.objects.create( + timeslot=slot, + session=session, + schedule=session.meeting.agenda) return meeting def make_meeting_test_data(): diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py index ad76489fe..c92eda084 100644 --- a/ietf/meeting/tests_views.py +++ b/ietf/meeting/tests_views.py @@ -6,6 +6,7 @@ import urlparse from django.core.urlresolvers import reverse as urlreverse from django.conf import settings from django.contrib.auth.models import User +from django.db import transaction from pyquery import PyQuery @@ -468,6 +469,46 @@ class InterimTests(TestCase): self.assertEqual(Group.objects.filter(type__in=('wg','rg'),state='active').count(), len(q("#id_group option")) -1 ) # -1 for options placeholder + def test_temp(self): + from django.forms.models import modelform_factory, inlineformset_factory + from ietf.meeting.forms import InterimSessionModelForm + from django.utils.functional import curry + + make_meeting_test_data() + group = Group.objects.get(acronym='mars') + 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) + remote_instructions = 'Use webex' + agenda = 'Intro. Slides. Discuss.' + agenda_note = 'On second level' + self.client.login(username="secretary", password="secretary+password") + data = {'group':group.pk, + 'meeting_type':'single', + 'city':'', + 'country':'', + 'time_zone':'UTC', + 'session_set-0-date':date.strftime("%Y-%m-%d"), + 'session_set-0-time':time.strftime('%H:%M'), + 'session_set-0-requested_duration':'03:00:00', + 'session_set-0-remote_instructions':remote_instructions, + 'session_set-0-agenda':agenda, + 'session_set-0-agenda_note':agenda_note, + 'session_set-TOTAL_FORMS':1, + 'session_set-INITIAL_FORMS':0, + 'session_set-MIN_NUM_FORMS':0, + 'session_set-MAX_NUM_FORMS':1000} + + user = User.objects.get(username='secretary') + is_approved = False + meeting = Meeting.objects.order_by('id').last() + SessionFormset = inlineformset_factory(Meeting, Session, form=InterimSessionModelForm, can_delete=False, extra=2) + SessionFormset.form = staticmethod(curry(InterimSessionModelForm, user=user,group=group,is_approved=is_approved)) + formset = SessionFormset(instance=meeting, data=data) + #assert False, (formset.management_form) + formset.save() + def test_interim_request_single(self): make_meeting_test_data() group = Group.objects.get(acronym='mars') @@ -481,17 +522,19 @@ class InterimTests(TestCase): self.client.login(username="secretary", password="secretary+password") data = {'group':group.pk, 'meeting_type':'single', - 'form-0-date':date.strftime("%Y-%m-%d"), - 'form-0-time':time.strftime('%H:%M'), - 'form-0-duration':'03:00:00', - 'form-0-city':'', - 'form-0-country':'', - 'form-0-timezone':'UTC', - 'form-0-remote_instructions':remote_instructions, - 'form-0-agenda':agenda, - 'form-0-agenda_note':agenda_note, - 'form-TOTAL_FORMS':1, - 'form-INITIAL_FORMS':0} + 'city':'', + 'country':'', + 'time_zone':'UTC', + 'session_set-0-date':date.strftime("%Y-%m-%d"), + 'session_set-0-time':time.strftime('%H:%M'), + 'session_set-0-requested_duration':'03:00:00', + 'session_set-0-remote_instructions':remote_instructions, + 'session_set-0-agenda':agenda, + 'session_set-0-agenda_note':agenda_note, + 'session_set-TOTAL_FORMS':1, + 'session_set-INITIAL_FORMS':0, + 'session_set-MIN_NUM_FORMS':0, + 'session_set-MAX_NUM_FORMS':1000} r = self.client.post(urlreverse("ietf.meeting.views.interim_request"),data) @@ -524,24 +567,24 @@ class InterimTests(TestCase): duration = datetime.timedelta(hours=3) city = 'San Francisco' country = 'US' - timezone = 'US/Pacific' + time_zone = 'US/Pacific' remote_instructions = 'Use webex' agenda = 'Intro. Slides. Discuss.' agenda_note = 'On second level' self.client.login(username="secretary", password="secretary+password") data = {'group':group.pk, 'meeting_type':'single', - 'form-0-date':date.strftime("%Y-%m-%d"), - 'form-0-time':time.strftime('%H:%M'), - 'form-0-duration':'03:00:00', - 'form-0-city':city, - 'form-0-country':country, - 'form-0-timezone':timezone, - 'form-0-remote_instructions':remote_instructions, - 'form-0-agenda':agenda, - 'form-0-agenda_note':agenda_note, - 'form-TOTAL_FORMS':1, - 'form-INITIAL_FORMS':0} + 'city':city, + 'country':country, + 'time_zone':time_zone, + 'session_set-0-date':date.strftime("%Y-%m-%d"), + 'session_set-0-time':time.strftime('%H:%M'), + 'session_set-0-requested_duration':'03:00:00', + 'session_set-0-remote_instructions':remote_instructions, + 'session_set-0-agenda':agenda, + 'session_set-0-agenda_note':agenda_note, + 'session_set-TOTAL_FORMS':1, + 'session_set-INITIAL_FORMS':0} r = self.client.post(urlreverse("ietf.meeting.views.interim_request"),data) @@ -552,7 +595,7 @@ class InterimTests(TestCase): self.assertEqual(meeting.number,'interim-%s-%s-%s' % (date.year,group.acronym,1)) self.assertEqual(meeting.city,city) self.assertEqual(meeting.country,country) - self.assertEqual(meeting.time_zone,timezone) + self.assertEqual(meeting.time_zone,time_zone) session = meeting.session_set.first() self.assertEqual(session.remote_instructions,remote_instructions) self.assertEqual(session.agenda_note,agenda_note) @@ -571,33 +614,30 @@ class InterimTests(TestCase): group = Group.objects.get(acronym='mars') city = 'San Francisco' country = 'US' - timezone = 'US/Pacific' + time_zone = 'US/Pacific' remote_instructions = 'Use webex' agenda = 'Intro. Slides. Discuss.' agenda_note = 'On second level' self.client.login(username="secretary", password="secretary+password") data = {'group':group.pk, 'meeting_type':'multi-day', - 'form-0-date':date.strftime("%Y-%m-%d"), - 'form-0-time':time.strftime('%H:%M'), - 'form-0-duration':'03:00:00', - 'form-0-city':city, - 'form-0-country':country, - 'form-0-timezone':timezone, - 'form-0-remote_instructions':remote_instructions, - 'form-0-agenda':agenda, - 'form-0-agenda_note':agenda_note, - 'form-1-date':date2.strftime("%Y-%m-%d"), - 'form-1-time':time.strftime('%H:%M'), - 'form-1-duration':'03:00:00', - 'form-1-city':city, - 'form-1-country':country, - 'form-1-timezone':timezone, - 'form-1-remote_instructions':remote_instructions, - 'form-1-agenda':agenda, - 'form-1-agenda_note':agenda_note, - 'form-TOTAL_FORMS':2, - 'form-INITIAL_FORMS':0} + 'city':city, + 'country':country, + 'time_zone':time_zone, + 'session_set-0-date':date.strftime("%Y-%m-%d"), + 'session_set-0-time':time.strftime('%H:%M'), + 'session_set-0-requested_duration':'03:00:00', + 'session_set-0-remote_instructions':remote_instructions, + 'session_set-0-agenda':agenda, + 'session_set-0-agenda_note':agenda_note, + 'session_set-1-date':date2.strftime("%Y-%m-%d"), + 'session_set-1-time':time.strftime('%H:%M'), + 'session_set-1-requested_duration':'03:00:00', + 'session_set-1-remote_instructions':remote_instructions, + 'session_set-1-agenda':agenda, + 'session_set-1-agenda_note':agenda_note, + 'session_set-TOTAL_FORMS':2, + 'session_set-INITIAL_FORMS':0} r = self.client.post(urlreverse("ietf.meeting.views.interim_request"),data) @@ -608,7 +648,7 @@ class InterimTests(TestCase): self.assertEqual(meeting.number,'interim-%s-%s-%s' % (date.year,group.acronym,1)) self.assertEqual(meeting.city,city) self.assertEqual(meeting.country,country) - self.assertEqual(meeting.time_zone,timezone) + self.assertEqual(meeting.time_zone,time_zone) self.assertEqual(meeting.session_set.count(),2) # first sesstion session = meeting.session_set.all()[0] @@ -625,6 +665,81 @@ class InterimTests(TestCase): self.assertEqual(timeslot.duration,duration) self.assertEqual(session.agenda_note,agenda_note) + def test_interim_request_series(self): + make_meeting_test_data() + meeting_count_before = Meeting.objects.filter(type='interim').count() + date = datetime.date.today() + datetime.timedelta(days=30) + date2 = date + datetime.timedelta(days=1) + time = datetime.datetime.now().time().replace(microsecond=0,second=0) + dt = datetime.datetime.combine(date, time) + dt2 = datetime.datetime.combine(date2, time) + duration = datetime.timedelta(hours=3) + group = Group.objects.get(acronym='mars') + city = '' + country = '' + time_zone = 'US/Pacific' + remote_instructions = 'Use webex' + agenda = 'Intro. Slides. Discuss.' + agenda_note = 'On second level' + self.client.login(username="secretary", password="secretary+password") + data = {'group':group.pk, + 'meeting_type':'series', + 'city':city, + 'country':country, + 'time_zone':time_zone, + 'session_set-0-date':date.strftime("%Y-%m-%d"), + 'session_set-0-time':time.strftime('%H:%M'), + 'session_set-0-requested_duration':'03:00:00', + 'session_set-0-remote_instructions':remote_instructions, + 'session_set-0-agenda':agenda, + 'session_set-0-agenda_note':agenda_note, + 'session_set-1-date':date2.strftime("%Y-%m-%d"), + 'session_set-1-time':time.strftime('%H:%M'), + 'session_set-1-requested_duration':'03:00:00', + 'session_set-1-remote_instructions':remote_instructions, + 'session_set-1-agenda':agenda, + 'session_set-1-agenda_note':agenda_note, + 'session_set-TOTAL_FORMS':2, + 'session_set-INITIAL_FORMS':0} + + r = self.client.post(urlreverse("ietf.meeting.views.interim_request"),data) + + self.assertRedirects(r,urlreverse('ietf.meeting.views.upcoming')) + meeting_count_after = Meeting.objects.filter(type='interim').count() + self.assertEqual(meeting_count_after,meeting_count_before + 2) + meetings = Meeting.objects.order_by('-id')[:2] + # first meeting + meeting = meetings[1] + self.assertEqual(meeting.type_id,'interim') + self.assertEqual(meeting.date,date) + self.assertEqual(meeting.number,'interim-%s-%s-%s' % (date.year,group.acronym,1)) + self.assertEqual(meeting.city,city) + self.assertEqual(meeting.country,country) + self.assertEqual(meeting.time_zone,time_zone) + self.assertEqual(meeting.session_set.count(),1) + 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) + self.assertEqual(session.agenda_note,agenda_note) + # second meeting + meeting = meetings[0] + self.assertEqual(meeting.type_id,'interim') + self.assertEqual(meeting.date,date2) + self.assertEqual(meeting.number,'interim-%s-%s-%s' % (date.year,group.acronym,2)) + self.assertEqual(meeting.city,city) + self.assertEqual(meeting.country,country) + self.assertEqual(meeting.time_zone,time_zone) + self.assertEqual(meeting.session_set.count(),1) + session = meeting.session_set.first() + self.assertEqual(session.remote_instructions,remote_instructions) + timeslot = session.official_timeslotassignment().timeslot + self.assertEqual(timeslot.time,dt2) + self.assertEqual(timeslot.duration,duration) + self.assertEqual(session.agenda_note,agenda_note) + + def test_interim_pending(self): make_meeting_test_data() url = urlreverse('ietf.meeting.views.interim_pending') @@ -715,6 +830,14 @@ class InterimTests(TestCase): for session in meeting.session_set.all(): self.assertEqual(session.status_id,'canceledpa') + def test_interim_request_edit(self): + make_meeting_test_data() + meeting = Meeting.objects.filter(type='interim',session__status='apprw',session__group__acronym='mars').first() + url = urlreverse('ietf.meeting.views.interim_request_edit',kwargs={'number':meeting.number}) + login_testing_unauthorized(self,"secretary",url) + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + def test_interim_request_details_permissions(self): make_meeting_test_data() meeting = Meeting.objects.filter(type='interim',session__status='apprw',session__group__acronym='mars').first() diff --git a/ietf/meeting/urls.py b/ietf/meeting/urls.py index a384312d5..486e9e297 100644 --- a/ietf/meeting/urls.py +++ b/ietf/meeting/urls.py @@ -72,6 +72,7 @@ urlpatterns = [ url(r'^interim/announce/(?P[A-Za-z0-9._+-]+)/$', views.interim_send_announcement), url(r'^interim/request/$', views.interim_request), url(r'^interim/request/(?P[A-Za-z0-9._+-]+)/$', views.interim_request_details), + url(r'^interim/request/(?P[A-Za-z0-9._+-]+)/edit/$', views.interim_request_edit), url(r'^interim/pending/$', views.interim_pending), url(r'^$', views.current_materials), ] diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index c95fcc3ef..e0280bfdb 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -20,8 +20,9 @@ from django.contrib import messages from django.core.urlresolvers import reverse from django.db.models import Min, Max from django.conf import settings -from django.forms.models import modelform_factory +from django.forms.models import modelform_factory, inlineformset_factory from django.forms import ModelForm, formset_factory +from django.utils.functional import curry from django.views.decorators.csrf import ensure_csrf_cookie from ietf.doc.models import Document, State @@ -36,15 +37,16 @@ from ietf.meeting.helpers import get_modified_from_assignments from ietf.meeting.helpers import get_wg_list, find_ads_for_meeting from ietf.meeting.helpers import get_meeting, get_schedule, agenda_permissions, get_meetings from ietf.meeting.helpers import preprocess_assignments_for_agenda, read_agenda_file -from ietf.meeting.helpers import convert_draft_to_pdf, get_earliest_session -from ietf.meeting.helpers import create_interim_meeting_from_forms +from ietf.meeting.helpers import convert_draft_to_pdf, get_earliest_session_date from ietf.meeting.helpers import can_view_interim_request, can_approve_interim_request from ietf.meeting.helpers import can_request_interim_meeting, get_announcement_initial +from ietf.meeting.helpers import get_interim_initial, get_interim_session_initial +from ietf.meeting.helpers import sessions_post_save, is_meeting_approved from ietf.utils.mail import send_mail_message from ietf.utils.pipe import pipe from ietf.utils.pdf import pdf_pages -from .forms import InterimRequestForm, InterimSessionForm, InterimAnnounceForm +from .forms import InterimMeetingModelForm, InterimSessionForm, InterimAnnounceForm, InterimSessionModelForm def get_menu_entries(request): @@ -901,45 +903,49 @@ def session_details(request, num, acronym ): # Interim Views # ------------------------------------------------- + def ajax_get_utc(request): '''Ajax view that takes arguments time and timezone and returns UTC''' time = request.GET.get('time') timezone = request.GET.get('timezone') - hour,minute = time.split(':') - dt = datetime.datetime(2016,1,1,int(hour),int(minute)) + hour, minute = time.split(':') + dt = datetime.datetime(2016, 1, 1, int(hour), int(minute)) tz = pytz.timezone(timezone) aware_dt = tz.localize(dt, is_dst=None) utc_dt = aware_dt.astimezone(pytz.utc) utc = utc_dt.strftime('%H:%M') - context_data = {'timezone':timezone,'time':time,'utc':utc} - return HttpResponse(json.dumps(context_data),content_type='application/json') + context_data = {'timezone': timezone, 'time': time, 'utc': utc} + return HttpResponse(json.dumps(context_data), + content_type='application/json') @role_required('Secretariat',) def interim_announce(request): '''View which shows interim meeting requests awaiting announcement''' - meetings = Meeting.objects.filter(type='interim',session__status='scheda') + meetings = Meeting.objects.filter(type='interim', session__status='scheda') menu_entries = get_menu_entries(request) selected_menu_entry = 'announce' return render(request, "meeting/interim_announce.html", { 'menu_entries': menu_entries, 'selected_menu_entry': selected_menu_entry, - 'meetings' :meetings}) + 'meetings': meetings}) + @role_required('Secretariat',) -def interim_send_announcement(request,number): +def interim_send_announcement(request, number): '''View for sending the announcement of a new interim meeting''' - meeting = get_object_or_404(Meeting,number=number) + meeting = get_object_or_404(Meeting, number=number) group = meeting.session_set.first().group - + if request.method == 'POST': - form = InterimAnnounceForm(request.POST, initial=get_announcement_initial(meeting)) + form = InterimAnnounceForm(request.POST, + initial=get_announcement_initial(meeting)) if form.is_valid(): message = form.save(user=request.user) message.related_groups.add(group) meeting.session_set.update(status_id='sched') - send_mail_message(request,message) + send_mail_message(request, message) messages.success(request, 'Interim meeting announcement sent') return redirect(interim_announce) @@ -947,91 +953,178 @@ def interim_send_announcement(request,number): return render(request, "meeting/interim_send_announcement.html", { 'meeting': meeting, - 'form': form}) + 'form': form}) -@role_required('Area Director','Secretariat','IRTF Chair','WG Chair','RG Chair') + +@role_required('Area Director', 'Secretariat', 'IRTF Chair', 'WG Chair', + 'RG Chair') def interim_pending(request): '''View which shows interim meeting requests pending approval''' - meetings = Meeting.objects.filter(type='interim',session__status='apprw') + meetings = Meeting.objects.filter(type='interim', session__status='apprw') menu_entries = get_menu_entries(request) selected_menu_entry = 'pending' - - meetings = [ m for m in meetings if can_view_interim_request(m,request.user)] + + meetings = [m for m in meetings if can_view_interim_request( + m, request.user)] for meeting in meetings: - if can_approve_interim_request(meeting,request.user): + if can_approve_interim_request(meeting, request.user): meeting.can_approve = True - + return render(request, "meeting/interim_pending.html", { 'menu_entries': menu_entries, - 'selected_menu_entry':selected_menu_entry, + 'selected_menu_entry': selected_menu_entry, 'meetings': meetings}) - -@role_required('Area Director','Secretariat','IRTF Chair','WG Chair', 'RG Chair') + + +@role_required('Area Director', 'Secretariat', 'IRTF Chair', 'WG Chair', + 'RG Chair') def interim_request(request): '''View for requesting an interim meeting''' - SessionFormset = formset_factory(InterimSessionForm, extra=2) - + SessionFormset = inlineformset_factory( + Meeting, + Session, + form=InterimSessionModelForm, + can_delete=False, extra=2) + if request.method == 'POST': - form = InterimRequestForm(request, data=request.POST) - formset = SessionFormset(data=request.POST) - #person = request.user.person + form = InterimMeetingModelForm(request, data=request.POST) + formset = SessionFormset(instance=Meeting(), data=request.POST) if form.is_valid() and formset.is_valid(): group = form.cleaned_data.get('group') is_approved = form.cleaned_data.get('approved', False) meeting_type = form.cleaned_data.get('meeting_type') - + # pre create meeting - if meeting_type in ('single','multi-day'): - meeting = create_interim_meeting_from_forms( - request_form=form, - session_form=get_earliest_session(formset)) - - for f in formset.forms: - if not f.has_changed(): - continue - if meeting_type == 'series': - meeting = create_interim_meeting_from_forms(form,f) - f.save(request,group,meeting,is_approved) - messages.success(request,'Interim meeting request submitted') + if meeting_type in ('single', 'multi-day'): + meeting = form.save(date=get_earliest_session_date(formset)) + + # need to use curry here to pass custom variable to form init + SessionFormset.form = staticmethod(curry( + InterimSessionModelForm, + user=request.user, + group=group, + is_approved=is_approved)) + formset = SessionFormset(instance=meeting, data=request.POST) + formset.is_valid() + formset.save() + + # post save + sessions_post_save(formset) + + # series require special handling, each session gets it's own + # meeting object we won't see this on edit because series are + # subsequently dealt with individually + elif meeting_type == 'series': + SessionFormset.form = staticmethod(curry( + InterimSessionModelForm, + user=request.user, + group=group, + is_approved=is_approved)) + formset = SessionFormset(instance=Meeting(), data=request.POST) + formset.is_valid() # re-validate + for session_form in formset.forms: + if not session_form.has_changed(): + continue + # create meeting + form = InterimMeetingModelForm(request, data=request.POST) + form.is_valid() + meeting = form.save(date=session_form.cleaned_data['date']) + # create save session + session = session_form.save(commit=False) + session.meeting = meeting + session.save() + + # post save + sessions_post_save([session_form]) + + messages.success(request, 'Interim meeting request submitted') return redirect(upcoming) else: assert False, (form.errors, formset.errors) else: - form = InterimRequestForm(request=request,initial={'meeting_type':'single','timezone':'UTC'}) + form = InterimMeetingModelForm(request=request, + initial={'meeting_type': 'single'}) formset = SessionFormset() - return render(request, "meeting/interim_request.html", {"form":form, "formset":formset}) + return render(request, "meeting/interim_request.html", { + "form": form, + "formset": formset}) -@role_required('Area Director','Secretariat','IRTF Chair','WG Chair', 'RG Chair') + +@role_required('Area Director', 'Secretariat', 'IRTF Chair', 'WG Chair', + 'RG Chair') def interim_request_details(request, number): '''View details of an interim meeting reqeust''' - meeting = get_object_or_404(Meeting,number=number) + meeting = get_object_or_404(Meeting, number=number) sessions = meeting.session_set.all() - can_edit = can_view_interim_request(meeting,request.user) - can_approve = can_approve_interim_request(meeting,request.user) + can_edit = can_view_interim_request(meeting, request.user) + can_approve = can_approve_interim_request(meeting, request.user) if request.method == 'POST': if request.POST.get('approve'): meeting.session_set.update(status_id='scheda') - messages.success(request,'Interim meeting approved') + messages.success(request, 'Interim meeting approved') if has_role(request.user, 'Secretariat'): return redirect(interim_send_announcement, number=number) if request.POST.get('disapprove'): meeting.session_set.update(status_id='disappr') - messages.success(request,'Interim meeting disapproved') + messages.success(request, 'Interim meeting disapproved') if request.POST.get('cancel'): if meeting.session_set.first().status.slug == 'sched': meeting.session_set.update(status_id='canceled') else: meeting.session_set.update(status_id='canceledpa') - messages.success(request,'Interim meeting cancelled') + messages.success(request, 'Interim meeting cancelled') + + return render(request, "meeting/interim_request_details.html", { + "meeting": meeting, + "sessions": sessions, + "can_edit": can_edit, + "can_approve": can_approve}) + + +@role_required('Area Director', 'Secretariat', 'IRTF Chair', 'WG Chair', + 'RG Chair') +def interim_request_edit(request, number): + '''Edit details of an interim meeting reqeust''' + meeting = get_object_or_404(Meeting, number=number) + SessionFormset = inlineformset_factory( + Meeting, + Session, + form=InterimSessionModelForm, + can_delete=False, + extra=1) + + if request.method == 'POST': + form = InterimMeetingModelForm(request=request, instance=meeting, + data=request.POST) + group = Group.objects.get(pk=form.data['group']) + is_approved = is_meeting_approved(meeting) + SessionFormset.form = staticmethod(curry( + InterimSessionModelForm, + user=request.user, + group=group, + is_approved=is_approved)) + formset = SessionFormset(instance=meeting, + data=request.POST) + if form.is_valid() and formset.is_valid(): + meeting = form.save(date=get_earliest_session_date(formset)) + formset.save() + sessions_post_save(formset) + + messages.success(request, 'Interim meeting request saved') + return redirect(interim_request_details, number=number) + else: + assert False, (form.errors, formset.errors) + else: + form = InterimMeetingModelForm(request=request, instance=meeting) + formset = SessionFormset(instance=meeting) + + return render(request, "meeting/interim_request_edit.html", { + "meeting": meeting, + "form": form, + "formset": formset}) - return render(request, "meeting/interim_request_details.html",{ - "meeting":meeting, - "sessions":sessions, - "can_edit":can_edit, - "can_approve":can_approve, - }) def ical_upcoming(request): '''ICAL upcoming meetings''' @@ -1042,14 +1135,18 @@ def ical_upcoming(request): "meetings": meetings, }, content_type="text/calendar") + def upcoming(request): '''List of upcoming meetings''' today = datetime.datetime.today() - meetings = Meeting.objects.filter(date__gte=today,session__status__in=('sched','canceled')).order_by('date') + meetings = Meeting.objects.filter( + date__gte=today, + session__status__in=('sched', 'canceled')).order_by('date') # extract groups hierarchy for display filter seen = set() - groups = [ m.session_set.first().group for m in meetings.filter(type='interim') ] + groups = [m.session_set.first().group for m + in meetings.filter(type='interim')] group_parents = [] for g in groups: if g.parent.acronym not in seen: @@ -1069,16 +1166,16 @@ def upcoming(request): # add menu entries menu_entries = get_menu_entries(request) selected_menu_entry = 'upcoming' - + # add menu actions actions = [] if can_request_interim_meeting(request.user): - actions.append(("Request new interim meeting", reverse("ietf.meeting.views.interim_request"))) - - return render(request, "meeting/upcoming.html", - { 'meetings':meetings, - 'menu_actions': actions, - 'menu_entries': menu_entries, - 'selected_menu_entry':selected_menu_entry, - 'group_parents':group_parents, - }) \ No newline at end of file + actions.append(("Request new interim meeting", + reverse("ietf.meeting.views.interim_request"))) + + return render(request, "meeting/upcoming.html", { + 'meetings': meetings, + 'menu_actions': actions, + 'menu_entries': menu_entries, + 'selected_menu_entry': selected_menu_entry, + 'group_parents': group_parents}) diff --git a/ietf/static/ietf/js/meeting-interim-request.js b/ietf/static/ietf/js/meeting-interim-request.js index e8a56d394..959cbf332 100644 --- a/ietf/static/ietf/js/meeting-interim-request.js +++ b/ietf/static/ietf/js/meeting-interim-request.js @@ -6,32 +6,32 @@ var interimRequest = { interimRequest.form = $(this); interimRequest.addButton = $('#add_session'); interimRequest.inPerson = $('#id_in_person'); + interimRequest.timezone = $('#id_time_zone'); // bind functions $('.select2-field').select2(); interimRequest.addButton.click(interimRequest.addSession); $('.btn-delete').click(interimRequest.deleteSession); interimRequest.inPerson.change(interimRequest.toggleLocation); - //$('input[name="meeting_type"]').change(interimRequest.checkAddButton); $('input[name="meeting_type"]').change(interimRequest.meetingTypeChanged); - $('input[name$="-duration"]').blur(interimRequest.calculateEndTime); + $('input[name$="-requested_duration"]').blur(interimRequest.calculateEndTime); $('input[name$="-time"]').blur(interimRequest.calculateEndTime); - //$('input[name$="-time"]').blur(interimRequest.setUTC); $('input[name$="-time"]').blur(interimRequest.updateInfo); $('input[name$="-end_time"]').change(interimRequest.updateInfo); - $('select[name$="-timezone"]').change(interimRequest.timezoneChange); - //interimRequest.form.submit(interimRequest.onSubmit); + interimRequest.timezone.change(interimRequest.timezoneChange); // init interimRequest.inPerson.each(interimRequest.toggleLocation); interimRequest.checkAddButton(); interimRequest.checkHelpText(); - //interimRequest.showRequired(); + $('input[name$="-time"]').each(interimRequest.calculateEndTime); + $('input[name$="-time"]').each(interimRequest.updateInfo); + //interimRequest.form.submit(interimRequest.onSubmit); + //$('input[name$="-time"]').blur(interimRequest.setUTC); }, addSession : function() { - //var templateData = interimRequest.sessionTemplate.clone(); var template = interimRequest.form.find('.fieldset.template'); var el = template.clone(true); - var totalField = $('#id_form-TOTAL_FORMS'); + var totalField = $('#id_session_set-TOTAL_FORMS'); var total = +totalField.val(); var meeting_type = $('input[name="meeting_type"]:checked').val(); @@ -59,15 +59,8 @@ var interimRequest = { // copy field contents var first_session = $(".fieldset:first"); - el.find("input[name$='city']").val(first_session.find("input[name$='city']").val()); - el.find("select[name$='country']").val(first_session.find("select[name$='country']").val()); - el.find("select[name$='timezone']").val(first_session.find("select[name$='timezone']").val()); el.find("input[name$='remote_instructions']").val(first_session.find("input[name$='remote_instructions']").val()); - if(meeting_type == 'multi-day'){ - el.find(".location").prop('disabled', true); - } - $('.btn-delete').removeClass("hidden"); }, @@ -81,13 +74,14 @@ var interimRequest = { }, updateInfo : function() { - //var url = liaisonForm.form.data("ajaxInfoUrl"); - //alert('called update'); + // makes ajax call to server and sets UTC field + var time = $(this).val(); + if(!time){ + return; + } var url = "/meeting/ajax/get-utc"; var fieldset = $(this).parents(".fieldset"); - var time = $(this).val(); - var timezone_field = fieldset.find('[name$="timezone"]'); - var timezone = timezone_field.val(); + var timezone = interimRequest.timezone.val(); var name = $(this).attr("id") + "_utc"; var utc = fieldset.find("#" + name); $.ajax({ @@ -113,7 +107,7 @@ var interimRequest = { var fieldset = $(this).parents(".fieldset"); var start_time = fieldset.find("input[name$='-time']"); var end_time = fieldset.find("input[name$='-end_time']"); - var duration = fieldset.find("input[name$='-duration']"); + var duration = fieldset.find("input[name$='-requested_duration']"); if(!start_time.val() || !duration.val()){ return; } @@ -125,8 +119,6 @@ var interimRequest = { end_time.val(interimRequest.get_formatted_time(d2)); end_time.trigger('change'); //interimRequest.updateInfo(end_time); - //alert( d2 ); - //alert( end_time.attr("id") ); }, checkAddButton : function() { @@ -209,22 +201,11 @@ var interimRequest = { var utc = fieldset.find("#" + name); var d = new Date(2000,1,1,values[0],values[1]); utc.val(interimRequest.get_formatted_utc_time(d) + " UTC"); - //alert(utc.attr("id")); - }, - - showRequired : function() { - // add a required class on labels on forms that should have - // explicit requirement asterisks - //$("form.show-required").find("input[required],select[required],textarea[required]").closest(".form-group").find("label").first().addClass("required"); }, timezoneChange : function() { - //alert("tz change"); - var fieldset = $(this).parents(".fieldset"); - var start_time = fieldset.find("input[name$='-time']"); - var end_time = fieldset.find("input[name$='-end_time']"); - start_time.trigger('blur'); - end_time.trigger('change'); + $("input[name$='-time']").trigger('blur'); + $("input[name$='-end_time']").trigger('change'); }, toggleLocation : function() { diff --git a/ietf/templates/meeting/interim_request.html b/ietf/templates/meeting/interim_request.html index eda18f055..e75c41eb8 100644 --- a/ietf/templates/meeting/interim_request.html +++ b/ietf/templates/meeting/interim_request.html @@ -61,30 +61,31 @@ - {{ formset.management_form }} - {% for form in formset %} -
- +
{% render_field form.city class="form-control location" placeholder="City" %} {% render_field form.country class="form-control location" placeholder="Country" %} - {% render_field form.timezone class="form-control" %} + {% render_field form.time_zone class="form-control" %}
+ + {{ formset.management_form }} + {% for form in formset %} +
- +
{% render_field form.date class="form-control" %}
- +
{% render_field form.time class="form-control time-field" placeholder="HH:MM" %} {% render_field form.time_utc class="form-control time-field computed" disabled="disabled" placeholder="UTC" %}
- +
{% render_field form.end_time class="form-control time-field computed" placeholder="HH:MM" disabled="disabled" %} {% render_field form.end_time_utc class="form-control time-field computed" disabled="disabled" placeholder="UTC" %} @@ -92,25 +93,25 @@
- -
{% render_field form.duration class="form-control time-field" placeholder="HH:MM" %}
+ +
{% render_field form.requested_duration class="form-control time-field" placeholder="HH:MM" %}
- +
{% render_field form.remote_instructions class="form-control" placeholder="ie. Webex address" %}
- +
{% render_field form.agenda class="form-control" rows="6" placeholder="paste agenda here" %}
- +
{% render_field form.agenda_note class="form-control" placeholder="Note" %}
- +
{% endfor %} diff --git a/ietf/templates/meeting/interim_request_details.html b/ietf/templates/meeting/interim_request_details.html index 9177ac2f1..091a3d5cd 100644 --- a/ietf/templates/meeting/interim_request_details.html +++ b/ietf/templates/meeting/interim_request_details.html @@ -1,7 +1,7 @@ {% extends "base.html" %} {# Copyright The IETF Trust 2015, All Rights Reserved #} {% load origin %} -{% load staticfiles bootstrap3 widget_tweaks %} +{% load staticfiles bootstrap3 widget_tweaks ietf_filters %} {% block title %}Interim Request Details{% endblock %} @@ -32,7 +32,7 @@
Start Time
{{ session.official_timeslotassignment.timeslot.time|date:"H:i" }}
Duration
-
{{ session.requested_duration }} +
{{ session.requested_duration|format_timedelta }}
Remote Instructions
{{ session.remote_instructions }}
Agenda Note
@@ -44,7 +44,7 @@ {% csrf_token %} {% if can_edit %} {% if sessions.0.status.slug == 'apprw' or sessions.0.status.slug == 'scheda' %} - Edit + Edit {% endif %} {% endif %} {% if can_approve and sessions.0.status.slug == 'apprw' %} diff --git a/ietf/templates/meeting/interim_request_edit.html b/ietf/templates/meeting/interim_request_edit.html new file mode 100644 index 000000000..efc72ce8a --- /dev/null +++ b/ietf/templates/meeting/interim_request_edit.html @@ -0,0 +1,129 @@ +{% extends "base.html" %} +{# Copyright The IETF Trust 2015, All Rights Reserved #} +{% load origin %} +{% load staticfiles bootstrap3 widget_tweaks %} + +{% block title %}Edit Interim Request{% endblock %} + +{% block pagehead %} + + + +{% endblock %} + +{% block content %} + {% origin %} +

Edit Interim Meeting Request

+ +
+ {% csrf_token %} + + {% bootstrap_field form.group layout='horizontal' %} + + + +
+
+
+ +
+ +
+ +
+ + + +
+
+ +
+ +
+ {% render_field form.city class="form-control location" placeholder="City" %} + {% render_field form.country class="form-control location" placeholder="Country" %} + {% render_field form.time_zone class="form-control" %} +
+
+ + {{ formset.management_form }} + {% for form in formset %} +
+ + + +
+ +
{% render_field form.date class="form-control" %}
+
+ +
+ +
+ {% render_field form.time class="form-control time-field" placeholder="HH:MM" %} + {% render_field form.time_utc class="form-control time-field computed" disabled="disabled" placeholder="UTC" %} +
+ +
+ {% render_field form.end_time class="form-control time-field computed" placeholder="HH:MM" disabled="disabled" %} + {% render_field form.end_time_utc class="form-control time-field computed" disabled="disabled" placeholder="UTC" %} +
+
+ +
+ +
{% render_field form.requested_duration class="form-control time-field" placeholder="HH:MM" %}
+
+ +
+ +
{% render_field form.remote_instructions class="form-control" placeholder="ie. Webex address" %}
+
+ +
+ +
{% render_field form.agenda class="form-control" rows="6" placeholder="paste agenda here" %}
+
+ +
+ +
{% render_field form.agenda_note class="form-control" placeholder="Note" %}
+
+ +
+ {% endfor %} + +
+
+ +
+
+ +
Submit + Back + {% endbuttons %} +
+ +
+ +{% endblock %} + +{% block js %} + + + +{% endblock %}