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:
parent
879bedb2c9
commit
3008c4904e
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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)),
|
||||
|
|
|
@ -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)
|
||||
|
|
24
ietf/templates/meeting/cancel_session.html
Normal file
24
ietf/templates/meeting/cancel_session.html
Normal 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 %}
|
|
@ -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>
|
Loading…
Reference in a new issue