Skip to content

Commit 3008c49

Browse files
feat: add 'cancel session' button to meeting schedule editor (ietf-tools#4682)
* feat: add 'cancel session' button to meeting schedule editor * fix: only show edit/cancel session buttons for secretariat Other users cannot access these views. * feat: refuse to cancel a canceled session; give feedback to user * test: test cancel_session view * test: test that sessions have edit/cancel buttons
1 parent 879bedb commit 3008c49

6 files changed

Lines changed: 144 additions & 5 deletions

File tree

ietf/meeting/forms.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -773,6 +773,13 @@ def __init__(self, instance, *args, **kwargs):
773773
super().__init__(instance=instance, group=instance.group, *args, **kwargs)
774774

775775

776+
class SessionCancelForm(forms.Form):
777+
confirmed = forms.BooleanField(
778+
label='Cancel session?',
779+
help_text='Confirm that you want to cancel this session.',
780+
)
781+
782+
776783
class SessionDetailsInlineFormSet(forms.BaseInlineFormSet):
777784
def __init__(self, group, meeting, queryset=None, *args, **kwargs):
778785
self._meeting = meeting

ietf/meeting/tests_views.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3109,6 +3109,19 @@ def test_edit_meeting_schedule(self):
31093109
for s in [s1, s2]:
31103110
e = q("#session{}".format(s.pk))
31113111

3112+
# should be link to edit/cancel session
3113+
self.assertTrue(
3114+
e.find('a[href="{}"]'.format(
3115+
urlreverse('ietf.meeting.views.edit_session', kwargs={'session_id': s.pk}),
3116+
))
3117+
)
3118+
self.assertTrue(
3119+
e.find('a[href="{}?sched={}"]'.format(
3120+
urlreverse('ietf.meeting.views.cancel_session', kwargs={'session_id': s.pk}),
3121+
meeting.schedule.pk,
3122+
))
3123+
)
3124+
31123125
# info in the item representing the session that can be moved around
31133126
self.assertIn(s.group.acronym, e.find(".session-label").text())
31143127
if s.comments:
@@ -3697,6 +3710,54 @@ def test_edit_session(self):
36973710
self.assertEqual(session.attendees, 103)
36983711
self.assertEqual(session.comments, 'So much to say')
36993712

3713+
def test_cancel_session(self):
3714+
# session for testing with official schedule
3715+
session = SessionFactory(meeting__type_id='ietf')
3716+
url = urlreverse('ietf.meeting.views.cancel_session', kwargs={'session_id': session.pk})
3717+
return_url = urlreverse('ietf.meeting.views.edit_meeting_schedule', kwargs={'num': session.meeting.number})
3718+
# session for testing with unofficial schedule
3719+
other_session = SessionFactory(meeting=session.meeting)
3720+
unofficial_schedule = ScheduleFactory(meeting=other_session.meeting)
3721+
url_unofficial = urlreverse(
3722+
'ietf.meeting.views.cancel_session',
3723+
kwargs={'session_id': other_session.pk},
3724+
) + f'?sched={unofficial_schedule.pk}'
3725+
return_url_unofficial = urlreverse(
3726+
'ietf.meeting.views.edit_meeting_schedule',
3727+
kwargs={
3728+
'num': other_session.meeting.number,
3729+
'name': unofficial_schedule.name,
3730+
'owner': unofficial_schedule.owner_email(),
3731+
},
3732+
)
3733+
3734+
login_testing_unauthorized(self, 'secretary', url)
3735+
r = self.client.get(url)
3736+
self.assertContains(r, 'Cancel session', status_code=200)
3737+
self.assertIn(return_url, r.content.decode())
3738+
r = self.client.get(url_unofficial)
3739+
self.assertContains(r, 'Cancel session', status_code=200)
3740+
self.assertIn(return_url_unofficial, r.content.decode())
3741+
3742+
r = self.client.post(url, {})
3743+
self.assertFormError(r, 'form', 'confirmed', 'This field is required.')
3744+
r = self.client.post(url_unofficial, {})
3745+
self.assertFormError(r, 'form', 'confirmed', 'This field is required.')
3746+
3747+
r = self.client.post(url, {'confirmed': 'on'})
3748+
self.assertRedirects(r, return_url)
3749+
session = Session.objects.with_current_status().get(pk=session.pk)
3750+
self.assertEqual(session.current_status, 'canceled')
3751+
r = self.client.get(url)
3752+
self.assertRedirects(r, return_url) # should redirect immediately when session is already canceled
3753+
3754+
r = self.client.post(url_unofficial, {'confirmed': 'on'})
3755+
self.assertRedirects(r, return_url_unofficial)
3756+
other_session = Session.objects.with_current_status().get(pk=other_session.pk)
3757+
self.assertEqual(other_session.current_status, 'canceled')
3758+
r = self.client.get(url_unofficial)
3759+
self.assertRedirects(r, return_url_unofficial) # should redirect immediately when session is already canceled
3760+
37003761
def test_edit_timeslots(self):
37013762
meeting = make_meeting_test_data()
37023763

ietf/meeting/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ def get_redirect_url(self, *args, **kwargs):
129129
url(r'^upcoming\.ics/?$', views.upcoming_ical),
130130
url(r'^upcoming\.json/?$', views.upcoming_json),
131131
url(r'^session/(?P<session_id>\d+)/agenda_materials$', views.session_materials),
132+
url(r'^session/(?P<session_id>\d+)/cancel/?', views.cancel_session),
132133
url(r'^session/(?P<session_id>\d+)/edit/?', views.edit_session),
133134
# Then patterns from more specific to less
134135
url(r'^(?P<num>interim-[a-z0-9-]+)/', include(type_interim_patterns)),

ietf/meeting/views.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
from ietf.meeting.models import Meeting, Session, Schedule, FloorPlan, SessionPresentation, TimeSlot, SlideSubmission
5757
from ietf.meeting.models import SessionStatusName, SchedulingEvent, SchedTimeSessAssignment, Room, TimeSlotTypeName
5858
from ietf.meeting.forms import ( CustomDurationField, SwapDaysForm, SwapTimeslotsForm, ImportMinutesForm,
59-
TimeSlotCreateForm, TimeSlotEditForm, SessionEditForm )
59+
TimeSlotCreateForm, TimeSlotEditForm, SessionCancelForm, SessionEditForm )
6060
from ietf.meeting.helpers import get_person_by_email, get_schedule_by_name
6161
from ietf.meeting.helpers import get_meeting, get_ietf_meeting, get_current_ietf_meeting_num
6262
from ietf.meeting.helpers import get_schedule, schedule_permissions
@@ -4122,6 +4122,46 @@ def edit_session(request, session_id):
41224122
{'session': session, 'form': form},
41234123
)
41244124

4125+
def _schedule_edit_url(meeting, schedule):
4126+
"""Get the preferred URL to edit a schedule
4127+
4128+
Returns a link to the official schedule if schedule is None
4129+
"""
4130+
url_args = {'num': meeting.number}
4131+
if schedule and not schedule.is_official:
4132+
url_args.update({
4133+
'name': schedule.name if schedule and not schedule.is_official else None,
4134+
'owner': schedule.owner_email() if schedule and not schedule.is_official else None,
4135+
})
4136+
return reverse('ietf.meeting.views.edit_meeting_schedule', kwargs=url_args)
4137+
4138+
@role_required('Secretariat')
4139+
def cancel_session(request, session_id):
4140+
session = get_object_or_404(Session.objects.with_current_status(), pk=session_id)
4141+
schedule = Schedule.objects.filter(pk=request.GET.get('sched', None)).first()
4142+
editor_url = _schedule_edit_url(session.meeting, schedule)
4143+
if session.current_status in Session.CANCELED_STATUSES:
4144+
messages.info(request, 'Session is already canceled.')
4145+
return HttpResponseRedirect(editor_url)
4146+
if request.method == 'POST':
4147+
form = SessionCancelForm(data=request.POST)
4148+
if form.is_valid():
4149+
SchedulingEvent.objects.create(
4150+
session=session,
4151+
status_id='canceled',
4152+
by=request.user.person,
4153+
)
4154+
messages.success(request, 'Session canceled.')
4155+
return HttpResponseRedirect(editor_url)
4156+
else:
4157+
form = SessionCancelForm()
4158+
return render(
4159+
request,
4160+
'meeting/cancel_session.html',
4161+
{'session': session, 'form': form, 'editor_url': editor_url},
4162+
)
4163+
4164+
41254165
@role_required('Secretariat')
41264166
def request_minutes(request, num=None):
41274167
meeting = get_ietf_meeting(num)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{% extends "base.html" %}
2+
{# Copyright The IETF Trust 2021-2022, All Rights Reserved #}
3+
{% load origin %}
4+
{% load django_bootstrap5 %}
5+
{% block pagehead %}{{ form.media.css }}{% endblock %}
6+
{% block title %} Cancel session "{{ session }}"{% endblock %}
7+
{% block content %}
8+
{% origin %}
9+
<h1>
10+
Cancel session
11+
<br>
12+
<small class="text-muted">{{ session }}</small>
13+
</h1>
14+
<form class="session-details-form my-3" method="post">
15+
{% csrf_token %}
16+
{% bootstrap_form form %}
17+
<button type="submit" class="btn btn-primary">Save</button>
18+
<a class="btn btn-secondary float-end"
19+
href="{{ editor_url }}">
20+
Back
21+
</a>
22+
</form>
23+
{% endblock %}
24+
{% block js %}{{ form.media.js }}{% endblock %}

ietf/templates/meeting/edit_meeting_schedule_session.html

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,15 @@
102102
</div>
103103
</div>
104104
{% endfor %}
105-
<a class="btn btn-primary btn-sm mt-2"
106-
href="{% url 'ietf.meeting.views.edit_session' session_id=session.pk %}">
107-
Edit session
108-
</a>
105+
{% if secretariat %}
106+
<a class="btn btn-primary btn-sm mt-2"
107+
href="{% url 'ietf.meeting.views.edit_session' session_id=session.pk %}">
108+
Edit session
109+
</a>
110+
<a class="btn btn-danger btn-sm mt-2"
111+
href="{% url 'ietf.meeting.views.cancel_session' session_id=session.pk %}?sched={{ schedule.pk }}">
112+
Cancel session
113+
</a>
114+
{% endif %}
109115
</div>
110116
</div>

0 commit comments

Comments
 (0)