feat: add 'cancel session' button to meeting schedule editor (#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
This commit is contained in:
Jennifer Richards 2022-11-01 14:41:50 -03:00 committed by GitHub
parent 879bedb2c9
commit 3008c4904e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 144 additions and 5 deletions

View file

@ -773,6 +773,13 @@ class SessionEditForm(SessionDetailsForm):
super().__init__(instance=instance, group=instance.group, *args, **kwargs)
class SessionCancelForm(forms.Form):
confirmed = forms.BooleanField(
label='Cancel session?',
help_text='Confirm that you want to cancel this session.',
)
class SessionDetailsInlineFormSet(forms.BaseInlineFormSet):
def __init__(self, group, meeting, queryset=None, *args, **kwargs):
self._meeting = meeting

View file

@ -3109,6 +3109,19 @@ class EditTests(TestCase):
for s in [s1, s2]:
e = q("#session{}".format(s.pk))
# should be link to edit/cancel session
self.assertTrue(
e.find('a[href="{}"]'.format(
urlreverse('ietf.meeting.views.edit_session', kwargs={'session_id': s.pk}),
))
)
self.assertTrue(
e.find('a[href="{}?sched={}"]'.format(
urlreverse('ietf.meeting.views.cancel_session', kwargs={'session_id': s.pk}),
meeting.schedule.pk,
))
)
# info in the item representing the session that can be moved around
self.assertIn(s.group.acronym, e.find(".session-label").text())
if s.comments:
@ -3697,6 +3710,54 @@ class EditTests(TestCase):
self.assertEqual(session.attendees, 103)
self.assertEqual(session.comments, 'So much to say')
def test_cancel_session(self):
# session for testing with official schedule
session = SessionFactory(meeting__type_id='ietf')
url = urlreverse('ietf.meeting.views.cancel_session', kwargs={'session_id': session.pk})
return_url = urlreverse('ietf.meeting.views.edit_meeting_schedule', kwargs={'num': session.meeting.number})
# session for testing with unofficial schedule
other_session = SessionFactory(meeting=session.meeting)
unofficial_schedule = ScheduleFactory(meeting=other_session.meeting)
url_unofficial = urlreverse(
'ietf.meeting.views.cancel_session',
kwargs={'session_id': other_session.pk},
) + f'?sched={unofficial_schedule.pk}'
return_url_unofficial = urlreverse(
'ietf.meeting.views.edit_meeting_schedule',
kwargs={
'num': other_session.meeting.number,
'name': unofficial_schedule.name,
'owner': unofficial_schedule.owner_email(),
},
)
login_testing_unauthorized(self, 'secretary', url)
r = self.client.get(url)
self.assertContains(r, 'Cancel session', status_code=200)
self.assertIn(return_url, r.content.decode())
r = self.client.get(url_unofficial)
self.assertContains(r, 'Cancel session', status_code=200)
self.assertIn(return_url_unofficial, r.content.decode())
r = self.client.post(url, {})
self.assertFormError(r, 'form', 'confirmed', 'This field is required.')
r = self.client.post(url_unofficial, {})
self.assertFormError(r, 'form', 'confirmed', 'This field is required.')
r = self.client.post(url, {'confirmed': 'on'})
self.assertRedirects(r, return_url)
session = Session.objects.with_current_status().get(pk=session.pk)
self.assertEqual(session.current_status, 'canceled')
r = self.client.get(url)
self.assertRedirects(r, return_url) # should redirect immediately when session is already canceled
r = self.client.post(url_unofficial, {'confirmed': 'on'})
self.assertRedirects(r, return_url_unofficial)
other_session = Session.objects.with_current_status().get(pk=other_session.pk)
self.assertEqual(other_session.current_status, 'canceled')
r = self.client.get(url_unofficial)
self.assertRedirects(r, return_url_unofficial) # should redirect immediately when session is already canceled
def test_edit_timeslots(self):
meeting = make_meeting_test_data()

View file

@ -129,6 +129,7 @@ urlpatterns = [
url(r'^upcoming\.ics/?$', views.upcoming_ical),
url(r'^upcoming\.json/?$', views.upcoming_json),
url(r'^session/(?P<session_id>\d+)/agenda_materials$', views.session_materials),
url(r'^session/(?P<session_id>\d+)/cancel/?', views.cancel_session),
url(r'^session/(?P<session_id>\d+)/edit/?', views.edit_session),
# Then patterns from more specific to less
url(r'^(?P<num>interim-[a-z0-9-]+)/', include(type_interim_patterns)),

View file

@ -56,7 +56,7 @@ from ietf.mailtrigger.utils import gather_address_lists
from ietf.meeting.models import Meeting, Session, Schedule, FloorPlan, SessionPresentation, TimeSlot, SlideSubmission
from ietf.meeting.models import SessionStatusName, SchedulingEvent, SchedTimeSessAssignment, Room, TimeSlotTypeName
from ietf.meeting.forms import ( CustomDurationField, SwapDaysForm, SwapTimeslotsForm, ImportMinutesForm,
TimeSlotCreateForm, TimeSlotEditForm, SessionEditForm )
TimeSlotCreateForm, TimeSlotEditForm, SessionCancelForm, SessionEditForm )
from ietf.meeting.helpers import get_person_by_email, get_schedule_by_name
from ietf.meeting.helpers import get_meeting, get_ietf_meeting, get_current_ietf_meeting_num
from ietf.meeting.helpers import get_schedule, schedule_permissions
@ -4122,6 +4122,46 @@ def edit_session(request, session_id):
{'session': session, 'form': form},
)
def _schedule_edit_url(meeting, schedule):
"""Get the preferred URL to edit a schedule
Returns a link to the official schedule if schedule is None
"""
url_args = {'num': meeting.number}
if schedule and not schedule.is_official:
url_args.update({
'name': schedule.name if schedule and not schedule.is_official else None,
'owner': schedule.owner_email() if schedule and not schedule.is_official else None,
})
return reverse('ietf.meeting.views.edit_meeting_schedule', kwargs=url_args)
@role_required('Secretariat')
def cancel_session(request, session_id):
session = get_object_or_404(Session.objects.with_current_status(), pk=session_id)
schedule = Schedule.objects.filter(pk=request.GET.get('sched', None)).first()
editor_url = _schedule_edit_url(session.meeting, schedule)
if session.current_status in Session.CANCELED_STATUSES:
messages.info(request, 'Session is already canceled.')
return HttpResponseRedirect(editor_url)
if request.method == 'POST':
form = SessionCancelForm(data=request.POST)
if form.is_valid():
SchedulingEvent.objects.create(
session=session,
status_id='canceled',
by=request.user.person,
)
messages.success(request, 'Session canceled.')
return HttpResponseRedirect(editor_url)
else:
form = SessionCancelForm()
return render(
request,
'meeting/cancel_session.html',
{'session': session, 'form': form, 'editor_url': editor_url},
)
@role_required('Secretariat')
def request_minutes(request, num=None):
meeting = get_ietf_meeting(num)

View file

@ -0,0 +1,24 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2021-2022, All Rights Reserved #}
{% load origin %}
{% load django_bootstrap5 %}
{% block pagehead %}{{ form.media.css }}{% endblock %}
{% block title %} Cancel session "{{ session }}"{% endblock %}
{% block content %}
{% origin %}
<h1>
Cancel session
<br>
<small class="text-muted">{{ session }}</small>
</h1>
<form class="session-details-form my-3" method="post">
{% csrf_token %}
{% bootstrap_form form %}
<button type="submit" class="btn btn-primary">Save</button>
<a class="btn btn-secondary float-end"
href="{{ editor_url }}">
Back
</a>
</form>
{% endblock %}
{% block js %}{{ form.media.js }}{% endblock %}

View file

@ -102,9 +102,15 @@
</div>
</div>
{% endfor %}
<a class="btn btn-primary btn-sm mt-2"
href="{% url 'ietf.meeting.views.edit_session' session_id=session.pk %}">
Edit session
</a>
{% if secretariat %}
<a class="btn btn-primary btn-sm mt-2"
href="{% url 'ietf.meeting.views.edit_session' session_id=session.pk %}">
Edit session
</a>
<a class="btn btn-danger btn-sm mt-2"
href="{% url 'ietf.meeting.views.cancel_session' session_id=session.pk %}?sched={{ schedule.pk }}">
Cancel session
</a>
{% endif %}
</div>
</div>