Skip to content

Commit ed37139

Browse files
committed
add tests. fix form validations
- Legacy-Id: 11219
1 parent 6ea92cc commit ed37139

12 files changed

Lines changed: 154 additions & 135 deletions

File tree

ietf/meeting/forms.py

Lines changed: 21 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ def label_from_instance(self, obj):
137137

138138

139139
class InterimMeetingModelForm(forms.ModelForm):
140-
group = GroupModelChoiceField(queryset=Group.objects.filter(type__in=('wg', 'rg'), state='active').order_by('acronym'))
140+
group = GroupModelChoiceField(queryset=Group.objects.filter(type__in=('wg', 'rg'), state__in=('active', 'proposed')).order_by('acronym'), required=False)
141141
in_person = forms.BooleanField(required=False)
142142
meeting_type = forms.ChoiceField(choices=(
143143
("single", "Single"),
@@ -172,9 +172,16 @@ def __init__(self, request, *args, **kwargs):
172172
self.fields['approved'].initial = False
173173
self.fields['approved'].widget.attrs['disabled'] = True
174174

175+
def clean(self):
176+
super(InterimMeetingModelForm, self).clean()
177+
cleaned_data = self.cleaned_data
178+
if not cleaned_data.get('group'):
179+
raise forms.ValidationError("You must select a group")
180+
181+
return self.cleaned_data
182+
175183
def set_group_options(self):
176184
'''Set group options based on user accessing the form'''
177-
178185
if has_role(self.user, "Secretariat"):
179186
return # don't reduce group options
180187
if has_role(self.user, "Area Director"):
@@ -215,9 +222,9 @@ def save(self, *args, **kwargs):
215222

216223
class InterimSessionModelForm(forms.ModelForm):
217224
date = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={"autoclose": "1"}, label='Date', required=False)
218-
time = forms.TimeField(widget=forms.TimeInput(format='%H:%M'), required=False)
225+
time = forms.TimeField(widget=forms.TimeInput(format='%H:%M'), required=True)
219226
time_utc = forms.TimeField(required=False)
220-
requested_duration = DurationField(required=False)
227+
requested_duration = DurationField(required=True)
221228
end_time = forms.TimeField(required=False)
222229
end_time_utc = forms.TimeField(required=False)
223230
remote_instructions = forms.CharField(max_length=1024, required=True)
@@ -247,6 +254,14 @@ def __init__(self, *args, **kwargs):
247254
path = os.path.join(doc.get_file_path(), doc.filename_with_rev())
248255
self.initial['agenda'] = get_document_content(os.path.basename(path), path, markup=False)
249256

257+
def clean_date(self):
258+
'''Date field validator. We can't use required on the input because
259+
it is a datepicker widget'''
260+
date = self.cleaned_data.get('date')
261+
if not date:
262+
raise forms.ValidationError('Required field')
263+
return date
264+
250265
def save(self, *args, **kwargs):
251266
"""NOTE: as the baseform of an inlineformset self.save(commit=True)
252267
never gets called"""
@@ -291,68 +306,8 @@ def save_agenda(self):
291306
with open(path, "w") as file:
292307
file.write(self.cleaned_data['agenda'])
293308

294-
'''
295-
class InterimSessionForm(forms.Form):
296-
date = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={"autoclose": "1"}, label='Date', required=False)
297-
time = forms.TimeField(required=False)
298-
time_utc = forms.TimeField(required=False)
299-
duration = DurationField(required=False)
300-
end_time = forms.TimeField(required=False)
301-
end_time_utc = forms.TimeField(required=False)
302-
remote_instructions = forms.CharField(max_length=1024, required=False)
303-
agenda = forms.CharField(required=False, widget=forms.Textarea)
304-
agenda_note = forms.CharField(max_length=255, required=False)
305-
306-
def save(self, request, group, meeting, is_approved):
307-
person = request.user.person
308-
agenda = self.cleaned_data.get('agenda')
309-
agenda_note = self.cleaned_data.get('agenda_note')
310-
date = self.cleaned_data.get('date')
311-
time = self.cleaned_data.get('time')
312-
duration = self.cleaned_data.get('duration')
313-
remote_instructions = self.cleaned_data.get('remote_instructions')
314-
time = datetime.datetime.combine(date, time)
315-
if is_approved:
316-
status_id = 'scheda'
317-
else:
318-
status_id = 'apprw'
319-
session = Session.objects.create(
320-
meeting=meeting,
321-
group=group,
322-
requested_by=person,
323-
requested_duration=duration,
324-
status_id=status_id,
325-
type_id='session',
326-
remote_instructions=remote_instructions,
327-
agenda_note=agenda_note,)
328-
assign_interim_session(session, time)
329-
330-
if agenda:
331-
# create objects
332-
filename = 'agenda-interim-%s-%s' % (group.acronym, time.strftime("%Y-%m-%d-%H%M"))
333-
doc = Document.objects.create(type_id='agenda', group=group, name=filename, rev='00')
334-
doc.set_state(State.objects.get(type=doc.type, slug='active'))
335-
DocAlias.objects.create(name=doc.name, document=doc)
336-
session.sessionpresentation_set.create(document=doc, rev=doc.rev)
337-
NewRevisionDocEvent.objects.create(
338-
type='new_revision',
339-
by=request.user.person,
340-
doc=doc,
341-
rev=doc.rev,
342-
desc='New revision available')
343-
# write file
344-
path = os.path.join(get_upload_root(meeting), 'agenda', doc.filename_with_rev())
345-
directory = os.path.dirname(path)
346-
if not os.path.exists(directory):
347-
os.makedirs(directory)
348-
with open(path, "w") as file:
349-
file.write(agenda)
350-
351-
return session
352-
'''
353309

354310
class InterimAnnounceForm(forms.ModelForm):
355-
356311
class Meta:
357312
model = Message
358313
fields = ('to', 'frm', 'cc', 'bcc', 'reply_to', 'subject', 'body')
@@ -367,11 +322,11 @@ def save(self, *args, **kwargs):
367322

368323

369324
class InterimCancelForm(forms.Form):
370-
group = forms.CharField(max_length=255,required=False)
325+
group = forms.CharField(max_length=255, required=False)
371326
date = forms.DateField(required=False)
372327
comments = forms.CharField(required=False, widget=forms.Textarea(attrs={'placeholder': 'enter optional comments here'}))
373328

374329
def __init__(self, *args, **kwargs):
375330
super(InterimCancelForm, self).__init__(*args, **kwargs)
376331
self.fields['group'].widget.attrs['disabled'] = True
377-
self.fields['date'].widget.attrs['disabled'] = True
332+
self.fields['date'].widget.attrs['disabled'] = True

ietf/meeting/helpers.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -337,11 +337,21 @@ def can_approve_interim_request(meeting, user):
337337

338338
def can_edit_interim_request(meeting, user):
339339
'''Returns True if the user can edit the interim meeting request'''
340-
341-
if can_approve_interim_request(meeting, user):
340+
if meeting.type.slug != 'interim':
341+
return False
342+
if has_role(user, 'Secretariat'):
342343
return True
343-
344-
return False
344+
person = get_person_for_user(user)
345+
session = meeting.session_set.first()
346+
if not session:
347+
return False
348+
group = session.group
349+
if group.role_set.filter(name='chair', person=person):
350+
return True
351+
elif can_approve_interim_request(meeting, user):
352+
return True
353+
else:
354+
return False
345355

346356

347357
def can_request_interim_meeting(user):

ietf/meeting/tests_views.py

Lines changed: 52 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import os
23
import shutil
34
import datetime
@@ -349,67 +350,67 @@ def check_interim_tabs(self, url):
349350
# no logged in - no tabs
350351
r = self.client.get(url)
351352
q = PyQuery(r.content)
352-
self.assertEqual(len(q("ul.nav-tabs")),0)
353+
self.assertEqual(len(q("ul.nav-tabs")), 0)
353354
# plain user - no tabs
354355
username = "plain"
355-
self.client.login(username=username, password= username + "+password")
356+
self.client.login(username=username, password=username + "+password")
356357
r = self.client.get(url)
357358
q = PyQuery(r.content)
358-
self.assertEqual(len(q("ul.nav-tabs")),0)
359+
self.assertEqual(len(q("ul.nav-tabs")), 0)
359360
self.client.logout()
360361
# privileged user
361362
username = "ad"
362-
self.client.login(username=username, password= username + "+password")
363+
self.client.login(username=username, password=username + "+password")
363364
r = self.client.get(url)
364365
q = PyQuery(r.content)
365-
self.assertEqual(len(q("a:contains('Pending')")),1)
366-
self.assertEqual(len(q("a:contains('Announce')")),0)
366+
self.assertEqual(len(q("a:contains('Pending')")), 1)
367+
self.assertEqual(len(q("a:contains('Announce')")), 0)
367368
self.client.logout()
368369
# secretariat
369370
username = "secretary"
370-
self.client.login(username=username, password= username + "+password")
371+
self.client.login(username=username, password=username + "+password")
371372
r = self.client.get(url)
372373
q = PyQuery(r.content)
373-
self.assertEqual(len(q("a:contains('Pending')")),1)
374-
self.assertEqual(len(q("a:contains('Announce')")),1)
374+
self.assertEqual(len(q("a:contains('Pending')")), 1)
375+
self.assertEqual(len(q("a:contains('Announce')")), 1)
375376
self.client.logout()
376377

377378
def test_interim_announce(self):
378379
make_meeting_test_data()
379380
url = urlreverse("ietf.meeting.views.interim_announce")
380-
meeting = Meeting.objects.filter(type='interim',session__group__acronym='mars').first()
381+
meeting = Meeting.objects.filter(type='interim', session__group__acronym='mars').first()
381382
session = meeting.session_set.first()
382383
session.status = SessionStatusName.objects.get(slug='scheda')
383384
session.save()
384-
login_testing_unauthorized(self,"secretary",url)
385+
login_testing_unauthorized(self, "secretary", url)
385386
r = self.client.get(url)
386387
self.assertEqual(r.status_code, 200)
387388
self.assertTrue(meeting.number in r.content)
388389

389390
def test_interim_send_announcement(self):
390391
make_meeting_test_data()
391-
meeting = Meeting.objects.filter(type='interim',session__status='apprw',session__group__acronym='mars').first()
392-
url = urlreverse("ietf.meeting.views.interim_send_announcement", kwargs={'number':meeting.number})
393-
login_testing_unauthorized(self,"secretary",url)
392+
meeting = Meeting.objects.filter(type='interim', session__status='apprw', session__group__acronym='mars').first()
393+
url = urlreverse("ietf.meeting.views.interim_send_announcement", kwargs={'number': meeting.number})
394+
login_testing_unauthorized(self, "secretary", url)
394395
r = self.client.get(url)
395396
self.assertEqual(r.status_code, 200)
396397
initial = r.context['form'].initial
397398
# send announcement
398399
len_before = len(outbox)
399-
r = self.client.post(url,initial)
400-
self.assertRedirects(r,urlreverse('ietf.meeting.views.interim_announce'))
401-
self.assertEqual(len(outbox),len_before+1)
400+
r = self.client.post(url, initial)
401+
self.assertRedirects(r, urlreverse('ietf.meeting.views.interim_announce'))
402+
self.assertEqual(len(outbox), len_before + 1)
402403
self.assertTrue('WG Virtual Meeting' in outbox[-1]['Subject'])
403404

404405
def test_interim_approve(self):
405406
make_meeting_test_data()
406-
meeting = Meeting.objects.filter(type='interim',session__status='apprw',session__group__acronym='mars').first()
407-
url = urlreverse('ietf.meeting.views.interim_request_details',kwargs={'number':meeting.number})
408-
login_testing_unauthorized(self,"secretary",url)
409-
r = self.client.post(url,{'approve':'approve'})
410-
self.assertRedirects(r,urlreverse('ietf.meeting.views.interim_send_announcement',kwargs={'number':meeting.number}))
407+
meeting = Meeting.objects.filter(type='interim', session__status='apprw', session__group__acronym='mars').first()
408+
url = urlreverse('ietf.meeting.views.interim_request_details', kwargs={'number': meeting.number})
409+
login_testing_unauthorized(self, "secretary", url)
410+
r = self.client.post(url, {'approve': 'approve'})
411+
self.assertRedirects(r, urlreverse('ietf.meeting.views.interim_send_announcement', kwargs={'number': meeting.number}))
411412
for session in meeting.session_set.all():
412-
self.assertEqual(session.status.slug,'scheda')
413+
self.assertEqual(session.status.slug, 'scheda')
413414

414415
def test_upcoming(self):
415416
make_meeting_test_data()
@@ -478,8 +479,8 @@ def test_interim_request_options(self):
478479
r = self.client.get("/meeting/interim/request/")
479480
self.assertEqual(r.status_code, 200)
480481
q = PyQuery(r.content)
481-
self.assertEqual(Group.objects.filter(type__in=('wg','rg'),state='active').count(),
482-
len(q("#id_group option")) -1 ) # -1 for options placeholder
482+
self.assertEqual(Group.objects.filter(type__in=('wg', 'rg'), state__in=('active', 'proposed')).count(),
483+
len(q("#id_group option")) - 1) # -1 for options placeholder
483484

484485

485486
def test_interim_request_single(self):
@@ -882,4 +883,29 @@ def test_send_interim_minutes_reminder(self):
882883
length_before = len(outbox)
883884
send_interim_minutes_reminder(meeting=meeting)
884885
self.assertEqual(len(outbox),length_before+1)
885-
self.assertTrue('Action Required: Minutes' in outbox[-1]['Subject'])
886+
self.assertTrue('Action Required: Minutes' in outbox[-1]['Subject'])
887+
888+
889+
class AjaxTests(TestCase):
890+
def test_ajax_get_utc(self):
891+
# test bad queries
892+
url = urlreverse('ietf.meeting.views.ajax_get_utc') + "?date=2016-1-1&time=badtime&timezone=UTC"
893+
r = self.client.get(url)
894+
self.assertEqual(r.status_code, 200)
895+
data = json.loads(r.content)
896+
self.assertEqual(data["error"], True)
897+
url = urlreverse('ietf.meeting.views.ajax_get_utc') + "?date=2016-1-1&time=25:99&timezone=UTC"
898+
r = self.client.get(url)
899+
self.assertEqual(r.status_code, 200)
900+
data = json.loads(r.content)
901+
self.assertEqual(data["error"], True)
902+
# test good query
903+
url = urlreverse('ietf.meeting.views.ajax_get_utc') + "?date=2016-1-1&time=12:00&timezone=US/Pacific"
904+
r = self.client.get(url)
905+
self.assertEqual(r.status_code, 200)
906+
data = json.loads(r.content)
907+
self.assertTrue('timezone' in data)
908+
self.assertTrue('time' in data)
909+
self.assertTrue('utc' in data)
910+
self.assertTrue('error' not in data)
911+
self.assertEqual(data['utc'], '20:00')

ietf/meeting/views.py

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from django.conf import settings
2323
from django.forms.models import modelform_factory, inlineformset_factory
2424
from django.forms import ModelForm
25+
from django.template.loader import render_to_string
2526
from django.utils.functional import curry
2627
from django.views.decorators.csrf import ensure_csrf_cookie
2728

@@ -39,6 +40,7 @@
3940
from ietf.meeting.helpers import preprocess_assignments_for_agenda, read_agenda_file
4041
from ietf.meeting.helpers import convert_draft_to_pdf, get_earliest_session_date
4142
from ietf.meeting.helpers import can_view_interim_request, can_approve_interim_request
43+
from ietf.meeting.helpers import can_edit_interim_request
4244
from ietf.meeting.helpers import can_request_interim_meeting, get_announcement_initial
4345
from ietf.meeting.helpers import sessions_post_save, is_meeting_approved
4446
from ietf.meeting.helpers import send_interim_cancellation_notice
@@ -910,8 +912,17 @@ def ajax_get_utc(request):
910912
'''Ajax view that takes arguments time and timezone and returns UTC'''
911913
time = request.GET.get('time')
912914
timezone = request.GET.get('timezone')
915+
date = request.GET.get('date')
916+
time_re = re.compile(r'^\d{2}:\d{2}')
917+
if not time_re.match(time):
918+
return HttpResponse(json.dumps({'error': True}),
919+
content_type='application/json')
913920
hour, minute = time.split(':')
914-
dt = datetime.datetime(2016, 1, 1, int(hour), int(minute))
921+
if not (int(hour) <= 23 and int(minute) <= 59):
922+
return HttpResponse(json.dumps({'error': True}),
923+
content_type='application/json')
924+
year, month, day = date.split('-')
925+
dt = datetime.datetime(int(year), int(month), int(day), int(hour), int(minute))
915926
tz = pytz.timezone(timezone)
916927
aware_dt = tz.localize(dt, is_dst=None)
917928
utc_dt = aware_dt.astimezone(pytz.utc)
@@ -1045,8 +1056,7 @@ def interim_request(request):
10451056

10461057
messages.success(request, 'Interim meeting request submitted')
10471058
return redirect(upcoming)
1048-
else:
1049-
assert False, (form.errors, formset.errors)
1059+
10501060
else:
10511061
form = InterimMeetingModelForm(request=request,
10521062
initial={'meeting_type': 'single'})
@@ -1091,7 +1101,7 @@ def interim_request_details(request, number):
10911101
'''View details of an interim meeting reqeust'''
10921102
meeting = get_object_or_404(Meeting, number=number)
10931103
sessions = meeting.session_set.all()
1094-
can_edit = can_view_interim_request(meeting, request.user)
1104+
can_edit = can_edit_interim_request(meeting, request.user)
10951105
can_approve = can_approve_interim_request(meeting, request.user)
10961106

10971107
if request.method == 'POST':
@@ -1145,8 +1155,7 @@ def interim_request_edit(request, number):
11451155

11461156
messages.success(request, 'Interim meeting request saved')
11471157
return redirect(interim_request_details, number=number)
1148-
else:
1149-
assert False, (form.errors, formset.errors)
1158+
11501159
else:
11511160
form = InterimMeetingModelForm(request=request, instance=meeting)
11521161
formset = SessionFormset(instance=meeting)
@@ -1161,7 +1170,7 @@ def upcoming(request):
11611170
'''List of upcoming meetings'''
11621171
today = datetime.datetime.today()
11631172
meetings = Meeting.objects.filter(date__gte=today).exclude(
1164-
session__status__in=('apprw', 'schedpa', 'canceledpa')).order_by('date')
1173+
session__status__in=('apprw', 'scheda', 'canceledpa')).order_by('date')
11651174

11661175
# extract groups hierarchy for display filter
11671176
seen = set()
@@ -1222,7 +1231,17 @@ def upcoming_ical(request):
12221231
a.session.group.acronym in filters or
12231232
a.session.group.parent.acronym in filters]
12241233

1234+
# gather vtimezones
1235+
vtimezones = set()
1236+
for meeting in meetings:
1237+
if meeting.vtimezone():
1238+
vtimezones.add(meeting.vtimezone())
1239+
vtimezones = ''.join(vtimezones)
1240+
1241+
# icalendar response file should have '\r\n' line endings per RFC5545
1242+
response = render_to_string('meeting/upcoming.ics', {
1243+
'vtimezones': vtimezones,
1244+
'assignments': assignments})
1245+
response = re.sub("\r(?!\n)|(?<!\r)\n", "\r\n", response)
12251246

1226-
return render(request, 'meeting/upcoming.ics', {
1227-
'assignments': assignments,
1228-
}, content_type='text/calendar')
1247+
return HttpResponse(response, content_type='text/calendar')

0 commit comments

Comments
 (0)