diff --git a/ietf/meeting/forms.py b/ietf/meeting/forms.py index 9348883d3..2beb01e33 100644 --- a/ietf/meeting/forms.py +++ b/ietf/meeting/forms.py @@ -137,7 +137,7 @@ class GroupModelChoiceField(forms.ModelChoiceField): class InterimMeetingModelForm(forms.ModelForm): - group = GroupModelChoiceField(queryset=Group.objects.filter(type__in=('wg', 'rg'), state='active').order_by('acronym')) + group = GroupModelChoiceField(queryset=Group.objects.filter(type__in=('wg', 'rg'), state__in=('active', 'proposed')).order_by('acronym'), required=False) in_person = forms.BooleanField(required=False) meeting_type = forms.ChoiceField(choices=( ("single", "Single"), @@ -172,9 +172,16 @@ class InterimMeetingModelForm(forms.ModelForm): 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 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 if has_role(self.user, "Area Director"): @@ -215,9 +222,9 @@ class InterimMeetingModelForm(forms.ModelForm): 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 = forms.TimeField(widget=forms.TimeInput(format='%H:%M'), required=True) time_utc = forms.TimeField(required=False) - requested_duration = DurationField(required=False) + requested_duration = DurationField(required=True) end_time = forms.TimeField(required=False) end_time_utc = forms.TimeField(required=False) remote_instructions = forms.CharField(max_length=1024, required=True) @@ -247,6 +254,14 @@ class InterimSessionModelForm(forms.ModelForm): 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""" @@ -291,68 +306,8 @@ class InterimSessionModelForm(forms.ModelForm): with open(path, "w") as file: file.write(self.cleaned_data['agenda']) -''' -class InterimSessionForm(forms.Form): - 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) - - def save(self, request, group, meeting, is_approved): - person = request.user.person - 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') - remote_instructions = self.cleaned_data.get('remote_instructions') - time = datetime.datetime.combine(date, time) - if is_approved: - status_id = 'scheda' - else: - status_id = 'apprw' - session = Session.objects.create( - meeting=meeting, - group=group, - requested_by=person, - requested_duration=duration, - status_id=status_id, - type_id='session', - remote_instructions=remote_instructions, - agenda_note=agenda_note,) - 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')) - DocAlias.objects.create(name=doc.name, document=doc) - 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()) - directory = os.path.dirname(path) - if not os.path.exists(directory): - os.makedirs(directory) - with open(path, "w") as file: - file.write(agenda) - - return session -''' class InterimAnnounceForm(forms.ModelForm): - class Meta: model = Message fields = ('to', 'frm', 'cc', 'bcc', 'reply_to', 'subject', 'body') @@ -367,11 +322,11 @@ class InterimAnnounceForm(forms.ModelForm): class InterimCancelForm(forms.Form): - group = forms.CharField(max_length=255,required=False) + 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'})) 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 \ No newline at end of file + self.fields['date'].widget.attrs['disabled'] = True diff --git a/ietf/meeting/helpers.py b/ietf/meeting/helpers.py index 8b9cf95e3..94e09661e 100644 --- a/ietf/meeting/helpers.py +++ b/ietf/meeting/helpers.py @@ -337,11 +337,21 @@ def can_approve_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 meeting.type.slug != 'interim': + return False + if has_role(user, 'Secretariat'): return True - - return False + person = get_person_for_user(user) + session = meeting.session_set.first() + if not session: + return False + group = session.group + if group.role_set.filter(name='chair', person=person): + return True + elif can_approve_interim_request(meeting, user): + return True + else: + return False def can_request_interim_meeting(user): diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py index 2378c5106..8d21db47a 100644 --- a/ietf/meeting/tests_views.py +++ b/ietf/meeting/tests_views.py @@ -1,3 +1,4 @@ +import json import os import shutil import datetime @@ -349,67 +350,67 @@ class InterimTests(TestCase): # no logged in - no tabs r = self.client.get(url) q = PyQuery(r.content) - self.assertEqual(len(q("ul.nav-tabs")),0) + self.assertEqual(len(q("ul.nav-tabs")), 0) # plain user - no tabs username = "plain" - self.client.login(username=username, password= username + "+password") + self.client.login(username=username, password=username + "+password") r = self.client.get(url) q = PyQuery(r.content) - self.assertEqual(len(q("ul.nav-tabs")),0) + self.assertEqual(len(q("ul.nav-tabs")), 0) self.client.logout() # privileged user username = "ad" - self.client.login(username=username, password= username + "+password") + self.client.login(username=username, password=username + "+password") r = self.client.get(url) q = PyQuery(r.content) - self.assertEqual(len(q("a:contains('Pending')")),1) - self.assertEqual(len(q("a:contains('Announce')")),0) + self.assertEqual(len(q("a:contains('Pending')")), 1) + self.assertEqual(len(q("a:contains('Announce')")), 0) self.client.logout() # secretariat username = "secretary" - self.client.login(username=username, password= username + "+password") + self.client.login(username=username, password=username + "+password") r = self.client.get(url) q = PyQuery(r.content) - self.assertEqual(len(q("a:contains('Pending')")),1) - self.assertEqual(len(q("a:contains('Announce')")),1) + self.assertEqual(len(q("a:contains('Pending')")), 1) + self.assertEqual(len(q("a:contains('Announce')")), 1) self.client.logout() def test_interim_announce(self): make_meeting_test_data() url = urlreverse("ietf.meeting.views.interim_announce") - meeting = Meeting.objects.filter(type='interim',session__group__acronym='mars').first() + meeting = Meeting.objects.filter(type='interim', session__group__acronym='mars').first() session = meeting.session_set.first() session.status = SessionStatusName.objects.get(slug='scheda') session.save() - login_testing_unauthorized(self,"secretary",url) + login_testing_unauthorized(self, "secretary", url) r = self.client.get(url) self.assertEqual(r.status_code, 200) self.assertTrue(meeting.number in r.content) def test_interim_send_announcement(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_send_announcement", kwargs={'number':meeting.number}) - login_testing_unauthorized(self,"secretary",url) + meeting = Meeting.objects.filter(type='interim', session__status='apprw', session__group__acronym='mars').first() + url = urlreverse("ietf.meeting.views.interim_send_announcement", kwargs={'number': meeting.number}) + login_testing_unauthorized(self, "secretary", url) r = self.client.get(url) self.assertEqual(r.status_code, 200) initial = r.context['form'].initial # send announcement len_before = len(outbox) - r = self.client.post(url,initial) - self.assertRedirects(r,urlreverse('ietf.meeting.views.interim_announce')) - self.assertEqual(len(outbox),len_before+1) + r = self.client.post(url, initial) + self.assertRedirects(r, urlreverse('ietf.meeting.views.interim_announce')) + self.assertEqual(len(outbox), len_before + 1) self.assertTrue('WG Virtual Meeting' in outbox[-1]['Subject']) def test_interim_approve(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_details',kwargs={'number':meeting.number}) - login_testing_unauthorized(self,"secretary",url) - r = self.client.post(url,{'approve':'approve'}) - self.assertRedirects(r,urlreverse('ietf.meeting.views.interim_send_announcement',kwargs={'number':meeting.number})) + meeting = Meeting.objects.filter(type='interim', session__status='apprw', session__group__acronym='mars').first() + url = urlreverse('ietf.meeting.views.interim_request_details', kwargs={'number': meeting.number}) + login_testing_unauthorized(self, "secretary", url) + r = self.client.post(url, {'approve': 'approve'}) + self.assertRedirects(r, urlreverse('ietf.meeting.views.interim_send_announcement', kwargs={'number': meeting.number})) for session in meeting.session_set.all(): - self.assertEqual(session.status.slug,'scheda') + self.assertEqual(session.status.slug, 'scheda') def test_upcoming(self): make_meeting_test_data() @@ -478,8 +479,8 @@ class InterimTests(TestCase): r = self.client.get("/meeting/interim/request/") self.assertEqual(r.status_code, 200) q = PyQuery(r.content) - self.assertEqual(Group.objects.filter(type__in=('wg','rg'),state='active').count(), - len(q("#id_group option")) -1 ) # -1 for options placeholder + self.assertEqual(Group.objects.filter(type__in=('wg', 'rg'), state__in=('active', 'proposed')).count(), + len(q("#id_group option")) - 1) # -1 for options placeholder def test_interim_request_single(self): @@ -882,4 +883,29 @@ class InterimTests(TestCase): length_before = len(outbox) send_interim_minutes_reminder(meeting=meeting) self.assertEqual(len(outbox),length_before+1) - self.assertTrue('Action Required: Minutes' in outbox[-1]['Subject']) \ No newline at end of file + self.assertTrue('Action Required: Minutes' in outbox[-1]['Subject']) + + +class AjaxTests(TestCase): + def test_ajax_get_utc(self): + # test bad queries + url = urlreverse('ietf.meeting.views.ajax_get_utc') + "?date=2016-1-1&time=badtime&timezone=UTC" + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + data = json.loads(r.content) + self.assertEqual(data["error"], True) + url = urlreverse('ietf.meeting.views.ajax_get_utc') + "?date=2016-1-1&time=25:99&timezone=UTC" + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + data = json.loads(r.content) + self.assertEqual(data["error"], True) + # test good query + url = urlreverse('ietf.meeting.views.ajax_get_utc') + "?date=2016-1-1&time=12:00&timezone=US/Pacific" + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + data = json.loads(r.content) + self.assertTrue('timezone' in data) + self.assertTrue('time' in data) + self.assertTrue('utc' in data) + self.assertTrue('error' not in data) + self.assertEqual(data['utc'], '20:00') diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index b1876bab5..df8339881 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -22,6 +22,7 @@ from django.db.models import Min, Max from django.conf import settings from django.forms.models import modelform_factory, inlineformset_factory from django.forms import ModelForm +from django.template.loader import render_to_string from django.utils.functional import curry from django.views.decorators.csrf import ensure_csrf_cookie @@ -39,6 +40,7 @@ from ietf.meeting.helpers import get_meeting, get_schedule, agenda_permissions, from ietf.meeting.helpers import preprocess_assignments_for_agenda, read_agenda_file 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_edit_interim_request from ietf.meeting.helpers import can_request_interim_meeting, get_announcement_initial from ietf.meeting.helpers import sessions_post_save, is_meeting_approved from ietf.meeting.helpers import send_interim_cancellation_notice @@ -910,8 +912,17 @@ 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') + date = request.GET.get('date') + time_re = re.compile(r'^\d{2}:\d{2}') + if not time_re.match(time): + return HttpResponse(json.dumps({'error': True}), + content_type='application/json') hour, minute = time.split(':') - dt = datetime.datetime(2016, 1, 1, int(hour), int(minute)) + if not (int(hour) <= 23 and int(minute) <= 59): + return HttpResponse(json.dumps({'error': True}), + content_type='application/json') + year, month, day = date.split('-') + dt = datetime.datetime(int(year), int(month), int(day), int(hour), int(minute)) tz = pytz.timezone(timezone) aware_dt = tz.localize(dt, is_dst=None) utc_dt = aware_dt.astimezone(pytz.utc) @@ -1045,8 +1056,7 @@ def interim_request(request): messages.success(request, 'Interim meeting request submitted') return redirect(upcoming) - else: - assert False, (form.errors, formset.errors) + else: form = InterimMeetingModelForm(request=request, initial={'meeting_type': 'single'}) @@ -1091,7 +1101,7 @@ def interim_request_details(request, number): '''View details of an interim meeting reqeust''' meeting = get_object_or_404(Meeting, number=number) sessions = meeting.session_set.all() - can_edit = can_view_interim_request(meeting, request.user) + can_edit = can_edit_interim_request(meeting, request.user) can_approve = can_approve_interim_request(meeting, request.user) if request.method == 'POST': @@ -1145,8 +1155,7 @@ def interim_request_edit(request, number): 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) @@ -1161,7 +1170,7 @@ def upcoming(request): '''List of upcoming meetings''' today = datetime.datetime.today() meetings = Meeting.objects.filter(date__gte=today).exclude( - session__status__in=('apprw', 'schedpa', 'canceledpa')).order_by('date') + session__status__in=('apprw', 'scheda', 'canceledpa')).order_by('date') # extract groups hierarchy for display filter seen = set() @@ -1222,7 +1231,17 @@ def upcoming_ical(request): a.session.group.acronym in filters or a.session.group.parent.acronym in filters] + # gather vtimezones + vtimezones = set() + for meeting in meetings: + if meeting.vtimezone(): + vtimezones.add(meeting.vtimezone()) + vtimezones = ''.join(vtimezones) - return render(request, 'meeting/upcoming.ics', { - 'assignments': assignments, - }, content_type='text/calendar') + # icalendar response file should have '\r\n' line endings per RFC5545 + response = render_to_string('meeting/upcoming.ics', { + 'vtimezones': vtimezones, + 'assignments': assignments}) + response = re.sub("\r(?!\n)|(?IETF - {{ meeting.number }} {% endif %} -