Skip to content

Commit f8c7be6

Browse files
committed
Merged in [19917] and [19930] from jennifer@painless-security.com:
Create/delete Meetecho conferences when requesting/canceling interim sessions. Fixes ietf-tools#3507. Fixes ietf-tools#3508. - Legacy-Id: 19934 Note: SVN reference [19917] has been migrated to Git commit 81cd64d Note: SVN reference [19930] has been migrated to Git commit c64297e
1 parent 04df65c commit f8c7be6

12 files changed

Lines changed: 1238 additions & 24 deletions

File tree

ietf/meeting/forms.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from django.core.exceptions import ValidationError
1717
from django.db.models import Q
1818
from django.forms import BaseInlineFormSet
19+
from django.utils.functional import cached_property
1920

2021
import debug # pyflakes:ignore
2122

@@ -207,7 +208,8 @@ class InterimSessionModelForm(forms.ModelForm):
207208
time = forms.TimeField(widget=forms.TimeInput(format='%H:%M'), required=True)
208209
requested_duration = CustomDurationField(required=True)
209210
end_time = forms.TimeField(required=False)
210-
remote_instructions = forms.CharField(max_length=1024, required=True)
211+
remote_participation = forms.ChoiceField(choices=(), required=False)
212+
remote_instructions = forms.CharField(max_length=1024, required=False)
211213
agenda = forms.CharField(required=False, widget=forms.Textarea, strip=False)
212214
agenda_note = forms.CharField(max_length=255, required=False)
213215

@@ -233,7 +235,13 @@ def __init__(self, *args, **kwargs):
233235
doc = self.instance.agenda()
234236
content = doc.text_or_error()
235237
self.initial['agenda'] = content
236-
238+
239+
# set up remote participation choices
240+
choices = []
241+
if hasattr(settings, 'MEETECHO_API_CONFIG'):
242+
choices.append(('meetecho', 'Automatically create Meetecho conference'))
243+
choices.append(('manual', 'Manually specify remote instructions...'))
244+
self.fields['remote_participation'].choices = choices
237245

238246
def clean_date(self):
239247
'''Date field validator. We can't use required on the input because
@@ -251,6 +259,21 @@ def clean_requested_duration(self):
251259
raise forms.ValidationError('Provide a duration, %s-%smin.' % (min_minutes, max_minutes))
252260
return duration
253261

262+
def clean(self):
263+
if self.cleaned_data.get('remote_participation', None) == 'meetecho':
264+
self.cleaned_data['remote_instructions'] = '' # blank this out if we're creating a Meetecho conference
265+
elif not self.cleaned_data['remote_instructions']:
266+
self.add_error('remote_instructions', 'This field is required')
267+
return self.cleaned_data
268+
269+
# Override to ignore the non-model 'remote_participation' field when computing has_changed()
270+
@cached_property
271+
def changed_data(self):
272+
data = super().changed_data
273+
if 'remote_participation' in data:
274+
data.remove('remote_participation')
275+
return data
276+
254277
def save(self, *args, **kwargs):
255278
"""NOTE: as the baseform of an inlineformset self.save(commit=True)
256279
never gets called"""

ietf/meeting/helpers.py

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from django.http import Http404
1313
from django.db.models import F, Prefetch
1414
from django.conf import settings
15+
from django.contrib import messages
1516
from django.contrib.auth.models import AnonymousUser
1617
from django.urls import reverse
1718
from django.shortcuts import get_object_or_404
@@ -29,7 +30,7 @@
2930
from ietf.meeting.models import Meeting, Schedule, TimeSlot, SchedTimeSessAssignment, ImportantDate, SchedulingEvent, Session
3031
from ietf.meeting.utils import session_requested_by, add_event_info_to_session_qs
3132
from ietf.name.models import ImportantDateName, SessionPurposeName
32-
from ietf.utils import log
33+
from ietf.utils import log, meetecho
3334
from ietf.utils.history import find_history_replacements_active_at
3435
from ietf.utils.mail import send_mail
3536
from ietf.utils.pipe import pipe
@@ -1074,6 +1075,76 @@ def sessions_post_save(request, forms):
10741075
if 'agenda' in form.changed_data:
10751076
form.save_agenda()
10761077

1078+
try:
1079+
create_interim_session_conferences(
1080+
form.instance for form in forms
1081+
if form.cleaned_data.get('remote_participation', None) == 'meetecho'
1082+
)
1083+
except RuntimeError:
1084+
messages.warning(
1085+
request,
1086+
'An error occurred while creating a Meetecho conference. The interim meeting request '
1087+
'has been created without complete remote participation information. '
1088+
'Please edit the request to add this or contact the secretariat if you require assistance.',
1089+
)
1090+
1091+
1092+
def create_interim_session_conferences(sessions):
1093+
error_occurred = False
1094+
if hasattr(settings, 'MEETECHO_API_CONFIG'): # do nothing if not configured
1095+
meetecho_manager = meetecho.ConferenceManager(settings.MEETECHO_API_CONFIG)
1096+
for session in sessions:
1097+
ts = session.official_timeslotassignment().timeslot
1098+
try:
1099+
confs = meetecho_manager.create(
1100+
group=session.group,
1101+
description=str(session),
1102+
start_time=ts.time,
1103+
duration=ts.duration,
1104+
)
1105+
except Exception as err:
1106+
log.log(f'Exception creating Meetecho conference for {session}: {err}')
1107+
confs = []
1108+
1109+
if len(confs) == 1:
1110+
session.remote_instructions = confs[0].url
1111+
session.save()
1112+
else:
1113+
error_occurred = True
1114+
if error_occurred:
1115+
raise RuntimeError('error creating meetecho conferences')
1116+
1117+
1118+
def delete_interim_session_conferences(sessions):
1119+
"""Delete Meetecho conference for the session, if any"""
1120+
if hasattr(settings, 'MEETECHO_API_CONFIG'): # do nothing if Meetecho API not configured
1121+
meetecho_manager = meetecho.ConferenceManager(settings.MEETECHO_API_CONFIG)
1122+
for session in sessions:
1123+
if session.remote_instructions:
1124+
for conference in meetecho_manager.fetch(session.group):
1125+
if conference.url == session.remote_instructions:
1126+
conference.delete()
1127+
break
1128+
1129+
1130+
def sessions_post_cancel(request, sessions):
1131+
"""Clean up after session cancellation
1132+
1133+
When this is called, the session has already been canceled, so exceptions should
1134+
not be raised.
1135+
"""
1136+
try:
1137+
delete_interim_session_conferences(sessions)
1138+
except Exception as err:
1139+
sess_pks = ', '.join(str(s.pk) for s in sessions)
1140+
log.log(f'Exception deleting Meetecho conferences for sessions [{sess_pks}]: {err}')
1141+
messages.warning(
1142+
request,
1143+
'An error occurred while cleaning up Meetecho conferences for the canceled sessions. '
1144+
'The session or sessions have been canceled, but Meetecho conferences may not have been cleaned '
1145+
'up properly.',
1146+
)
1147+
10771148

10781149
def update_interim_session_assignment(form):
10791150
"""Helper function to create / update timeslot assigned to interim session"""
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Copyright The IETF Trust 2022, All Rights Reserved
2+
# -*- coding: utf-8 -*-
3+
import datetime
4+
5+
from textwrap import dedent
6+
7+
from django.conf import settings
8+
from django.core.management.base import BaseCommand, CommandError
9+
10+
from ietf.meeting.models import Session
11+
from ietf.utils.meetecho import ConferenceManager, MeetechoAPIError
12+
13+
14+
class Command(BaseCommand):
15+
help = 'Manage Meetecho conferences'
16+
17+
def add_arguments(self, parser) -> None:
18+
parser.add_argument('group', type=str)
19+
parser.add_argument('-d', '--delete', type=int, action='append',
20+
metavar='SESSION_PK',
21+
help='Delete the conference associated with the specified Session')
22+
23+
def handle(self, group, delete, *args, **options):
24+
conf_mgr = ConferenceManager(settings.MEETECHO_API_CONFIG)
25+
if delete:
26+
self.handle_delete_conferences(conf_mgr, group, delete)
27+
else:
28+
self.handle_list_conferences(conf_mgr, group)
29+
30+
def handle_list_conferences(self, conf_mgr, group):
31+
confs, conf_sessions = self.fetch_conferences(conf_mgr, group)
32+
self.stdout.write(f'Meetecho conferences for {group}:\n\n')
33+
for conf in confs:
34+
sessions_desc = ', '.join(str(s.pk) for s in conf_sessions[conf.id]) or None
35+
self.stdout.write(
36+
dedent(f'''\
37+
* {conf.description}
38+
Start time: {conf.start_time}
39+
Duration: {int(conf.duration.total_seconds() // 60)} minutes
40+
URL: {conf.url}
41+
Associated session PKs: {sessions_desc}
42+
43+
''')
44+
)
45+
46+
def handle_delete_conferences(self, conf_mgr, group, session_pks_to_delete):
47+
sessions_to_delete = Session.objects.filter(pk__in=session_pks_to_delete)
48+
confs, conf_sessions = self.fetch_conferences(conf_mgr, group)
49+
confs_to_delete = []
50+
descriptions = []
51+
for session in sessions_to_delete:
52+
for conf in confs:
53+
associated = conf_sessions[conf.id]
54+
if session in associated:
55+
confs_to_delete.append(conf)
56+
sessions_desc = ', '.join(str(s.pk) for s in associated) or None
57+
descriptions.append(
58+
f'{conf.description} ({conf.start_time}, {int(conf.duration.total_seconds() // 60)} mins) - used by {sessions_desc}'
59+
)
60+
if len(confs_to_delete) > 0:
61+
self.stdout.write('Will delete:')
62+
for desc in descriptions:
63+
self.stdout.write(f'* {desc}')
64+
65+
try:
66+
proceed = input('Proceed [y/N]? ').lower()
67+
except EOFError:
68+
proceed = 'n'
69+
if proceed in ['y', 'yes']:
70+
for conf, desc in zip(confs_to_delete, descriptions):
71+
conf.delete()
72+
self.stdout.write(f'Deleted {desc}')
73+
else:
74+
self.stdout.write('Nothing deleted.')
75+
else:
76+
self.stdout.write('No associated Meetecho conferences found')
77+
78+
def fetch_conferences(self, conf_mgr, group):
79+
try:
80+
confs = conf_mgr.fetch(group)
81+
except MeetechoAPIError as err:
82+
raise CommandError('API error fetching Meetecho conference data') from err
83+
84+
conf_sessions = {}
85+
for conf in confs:
86+
conf_sessions[conf.id] = Session.objects.filter(
87+
group__acronym=group,
88+
meeting__date__gte=datetime.date.today(),
89+
remote_instructions__contains=conf.url,
90+
)
91+
return confs, conf_sessions

ietf/meeting/tests_forms.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
# Copyright The IETF Trust 2021, All Rights Reserved
22
# -*- coding: utf-8 -*-
33
"""Tests of forms in the Meeting application"""
4+
from django.conf import settings
45
from django.core.files.uploadedfile import SimpleUploadedFile
56
from django.test import override_settings
67

7-
from ietf.meeting.forms import FileUploadForm, ApplyToAllFileUploadForm
8+
from ietf.meeting.forms import FileUploadForm, ApplyToAllFileUploadForm, InterimSessionModelForm
89
from ietf.utils.test_utils import TestCase
910

1011

@@ -102,3 +103,19 @@ def test_has_apply_to_all_field_by_default(self):
102103
def test_no_show_apply_to_all_field(self):
103104
form = ApplyToAllFileUploadFormTests.TestClass(show_apply_to_all_checkbox=False)
104105
self.assertNotIn('apply_to_all', form.fields)
106+
107+
108+
class InterimSessionModelFormTests(TestCase):
109+
@override_settings(MEETECHO_API_CONFIG={}) # setting needs to exist, don't care about its value in this test
110+
def test_remote_participation_options(self):
111+
"""Only offer Meetecho conference creation when configured"""
112+
form = InterimSessionModelForm()
113+
choice_vals = [choice[0] for choice in form.fields['remote_participation'].choices]
114+
self.assertIn('meetecho', choice_vals)
115+
self.assertIn('manual', choice_vals)
116+
117+
del settings.MEETECHO_API_CONFIG
118+
form = InterimSessionModelForm()
119+
choice_vals = [choice[0] for choice in form.fields['remote_participation'].choices]
120+
self.assertNotIn('meetecho', choice_vals)
121+
self.assertIn('manual', choice_vals)

0 commit comments

Comments
 (0)