Allow cancellation of individual sessions of multi-session interim meeting. Fixes #2959. Commit ready for merge.

- Legacy-Id: 18724
This commit is contained in:
Jennifer Richards 2020-12-01 17:37:09 +00:00
parent 1b19353382
commit 79971e14c7
14 changed files with 789 additions and 117 deletions

View file

@ -11,8 +11,6 @@ from ietf.ietfauth.utils import role_required, has_role
from ietf.meeting.helpers import get_meeting, get_schedule, schedule_permissions, get_person_by_email, get_schedule_by_name
from ietf.meeting.models import TimeSlot, Session, Schedule, Room, Constraint, SchedTimeSessAssignment, ResourceAssociation
from ietf.meeting.views import edit_timeslots, edit_schedule
from ietf.meeting.utils import only_sessions_that_can_meet
from ietf.meeting.utils import add_event_info_to_session_qs
import debug # pyflakes:ignore
@ -433,11 +431,7 @@ def session_json(request, num, sessionid):
def sessions_json(request, num):
meeting = get_meeting(num)
sessions = add_event_info_to_session_qs(
only_sessions_that_can_meet(meeting.session_set),
requested_time=True,
requested_by=True,
)
sessions = meeting.session_set.that_can_meet().with_requested_time().with_requested_by()
sess1_dict = [ x.json_dict(request.build_absolute_uri('/')) for x in sessions ]
return HttpResponse(json.dumps(sess1_dict, sort_keys=True, indent=2),

View file

@ -467,7 +467,8 @@ def get_announcement_initial(meeting, is_change=False):
type = 'BOF'
assignments = SchedTimeSessAssignment.objects.filter(
schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None]
schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None],
session__in=meeting.session_set.not_canceled()
).order_by('timeslot__time')
initial['subject'] = '{name} ({acronym}) {type} {desc} Meeting: {date}{change}'.format(
@ -614,7 +615,7 @@ def send_interim_announcement_request(meeting):
context,
cc_list)
def send_interim_cancellation_notice(meeting):
def send_interim_meeting_cancellation_notice(meeting):
"""Sends an email that a scheduled interim meeting has been cancelled."""
session = meeting.session_set.first()
group = session.group
@ -627,9 +628,39 @@ def send_interim_cancellation_notice(meeting):
date=meeting.date.strftime('%Y-%m-%d'))
start_time = session.official_timeslotassignment().timeslot.time
end_time = start_time + session.requested_duration
from ietf.meeting.utils import add_event_info_to_session_qs
is_multi_day = add_event_info_to_session_qs(meeting.session_set.all()).filter(current_status='sched').count() > 1
template = 'meeting/interim_cancellation_notice.txt'
is_multi_day = session.meeting.session_set.with_current_status().filter(current_status='sched').count() > 1
template = 'meeting/interim_meeting_cancellation_notice.txt'
context = locals()
send_mail(None,
to_email,
from_email,
subject,
template,
context,
cc=cc_list)
def send_interim_session_cancellation_notice(session):
"""Sends an email that one session of a scheduled interim meeting has been cancelled."""
group = session.group
start_time = session.official_timeslotassignment().timeslot.time
end_time = start_time + session.requested_duration
(to_email, cc_list) = gather_address_lists('interim_cancelled',group=group)
from_email = settings.INTERIM_ANNOUNCE_FROM_EMAIL_PROGRAM if group.type_id=='program' else settings.INTERIM_ANNOUNCE_FROM_EMAIL_DEFAULT
if session.name:
description = '"%s" session' % session.name
else:
description = 'interim meeting session'
subject = '{group} ({acronym}) {type} {description} cancelled (was {date})'.format(
group=group.name,
acronym=group.acronym,
type=group.type.slug.upper(),
description=description,
date=start_time.date().strftime('%Y-%m-%d'))
is_multi_day = session.meeting.session_set.with_current_status().filter(current_status='sched').count() > 1
template = 'meeting/interim_session_cancellation_notice.txt'
context = locals()
send_mail(None,
to_email,

View file

@ -17,7 +17,8 @@ import debug # pyflakes:ignore
from django.core.validators import MinValueValidator, RegexValidator
from django.db import models
from django.db.models import Max
from django.db.models import Max, Subquery, OuterRef, TextField, Value
from django.db.models.functions import Coalesce
from django.conf import settings
# mostly used by json_dict()
#from django.template.defaultfilters import slugify, date as date_format, time as time_format
@ -924,12 +925,78 @@ class SessionPresentation(models.Model):
constraint_cache_uses = 0
constraint_cache_initials = 0
class SessionQuerySet(models.QuerySet):
def with_current_status(self):
"""Annotate session with its current status
Adds current_status, containing the text representation of the status.
"""
return self.annotate(
# coalesce with '' to avoid nulls which give funny
# results, e.g. .exclude(current_status='canceled') also
# skips rows with null in them
current_status=Coalesce(
Subquery(
SchedulingEvent.objects.filter(
session=OuterRef('pk')
).order_by(
'-time', '-id'
).values('status')[:1]),
Value(''),
output_field=TextField()),
)
def with_requested_by(self):
"""Annotate session with requested_by field
Adds requested_by field - pk of the Person who made the request
"""
return self.annotate(
requested_by=Subquery(
SchedulingEvent.objects.filter(
session=OuterRef('pk')
).order_by(
'time', 'id'
).values('by')[:1]),
)
def with_requested_time(self):
"""Annotate session with requested_time field"""
return self.annotate(
requested_time=Subquery(
SchedulingEvent.objects.filter(
session=OuterRef('pk')
).order_by(
'time', 'id'
).values('time')[:1]),
)
def not_canceled(self):
"""Queryset containing all sessions not canceled
Results annotated with current_status
"""
return self.with_current_status().exclude(current_status__in=Session.CANCELED_STATUSES)
def that_can_meet(self):
"""Queryset containing sessions that can meet
Results annotated with current_status
"""
return self.with_current_status().exclude(
current_status__in=['notmeet', 'disappr', 'deleted', 'apprw']
).filter(
type__slug='regular'
)
class Session(models.Model):
"""Session records that a group should have a session on the
meeting (time and location is stored in a TimeSlot) - if multiple
timeslots are needed, multiple sessions will have to be created.
Training sessions and similar are modeled by filling in a
responsible group (e.g. Edu team) and filling in the name."""
objects = SessionQuerySet.as_manager() # sets default query manager
meeting = ForeignKey(Meeting)
name = models.CharField(blank=True, max_length=255, help_text="Name of session, in case the session has a purpose rather than just being a group meeting.")
short = models.CharField(blank=True, max_length=32, help_text="Short version of 'name' above, for use in filenames.")
@ -951,6 +1018,8 @@ class Session(models.Model):
unique_constraints_dict = None
CANCELED_STATUSES = ['canceled', 'canceledpa']
# Should work on how materials are captured so that deleted things are no longer associated with the session
# (We can keep the information about something being added to and removed from a session in the document's history)
def get_material(self, material_type, only_one):

View file

@ -33,7 +33,7 @@ from ietf.group.utils import can_manage_group
from ietf.person.models import Person
from ietf.meeting.helpers import can_approve_interim_request, can_view_interim_request
from ietf.meeting.helpers import send_interim_approval_request
from ietf.meeting.helpers import send_interim_cancellation_notice
from ietf.meeting.helpers import send_interim_meeting_cancellation_notice, send_interim_session_cancellation_notice
from ietf.meeting.helpers import send_interim_minutes_reminder, populate_important_dates, update_important_dates
from ietf.meeting.models import Session, TimeSlot, Meeting, SchedTimeSessAssignment, Schedule, SessionPresentation, SlideSubmission, SchedulingEvent, Room, Constraint, ConstraintName
from ietf.meeting.test_data import make_meeting_test_data, make_interim_meeting, make_interim_test_data
@ -1934,67 +1934,288 @@ class InterimTests(TestCase):
# test_interim_announce subsumed by test_appears_on_announce
def test_interim_skip_announcement(self):
def do_interim_skip_announcement_test(self, base_session=False, extra_session=False, canceled_session=False):
make_meeting_test_data()
group = Group.objects.get(acronym='irg')
date = datetime.date.today() + datetime.timedelta(days=30)
meeting = make_interim_meeting(group=group, date=date, status='scheda')
session = meeting.session_set.first()
if base_session:
base_session = SessionFactory(meeting=meeting, status_id='apprw', add_to_schedule=False)
meeting.schedule.base = Schedule.objects.create(
meeting=meeting, owner=PersonFactory(), name="base", visible=True, public=True
)
SchedTimeSessAssignment.objects.create(
timeslot=TimeSlotFactory.create(meeting=meeting),
session=base_session,
schedule=meeting.schedule.base,
)
meeting.schedule.save()
if extra_session:
extra_session = SessionFactory(meeting=meeting, status_id='scheda')
if canceled_session:
canceled_session = SessionFactory(meeting=meeting, status_id='canceledpa')
url = urlreverse("ietf.meeting.views.interim_skip_announcement", kwargs={'number': meeting.number})
login_testing_unauthorized(self, "secretary", url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
# check post
len_before = len(outbox)
r = self.client.post(url)
self.assertRedirects(r, urlreverse('ietf.meeting.views.interim_announce'))
self.assertEqual(add_event_info_to_session_qs(meeting.session_set).first().current_status, 'sched')
meeting_sessions = meeting.session_set.with_current_status()
self.assertEqual(meeting_sessions.get(pk=session.pk).current_status, 'sched')
if base_session:
self.assertEqual(meeting_sessions.get(pk=base_session.pk).current_status, 'sched')
if extra_session:
self.assertEqual(meeting_sessions.get(pk=extra_session.pk).current_status, 'sched')
if canceled_session:
self.assertEqual(meeting_sessions.get(pk=canceled_session.pk).current_status, 'canceledpa')
self.assertEqual(len(outbox), len_before)
def test_interim_send_announcement(self):
def test_interim_skip_announcement(self):
"""skip_announcement should move single session to sched state"""
self.do_interim_skip_announcement_test()
def test_interim_skip_announcement_with_base_sched(self):
"""skip_announcement should move single session to sched state"""
self.do_interim_skip_announcement_test(base_session=True)
def test_interim_skip_announcement_with_extra_session(self):
"""skip_announcement should move multiple sessions to sched state"""
self.do_interim_skip_announcement_test(extra_session=True)
def test_interim_skip_announcement_with_extra_session_and_base_sched(self):
"""skip_announcement should move multiple sessions to sched state"""
self.do_interim_skip_announcement_test(extra_session=True, base_session=True)
def test_interim_skip_announcement_with_canceled_session(self):
"""skip_announcement should not schedule a canceled session"""
self.do_interim_skip_announcement_test(canceled_session=True)
def test_interim_skip_announcement_with_canceled_session_and_base_sched(self):
"""skip_announcement should not schedule a canceled session"""
self.do_interim_skip_announcement_test(canceled_session=True, base_session=True)
def test_interim_skip_announcement_with_extra_and_canceled_sessions(self):
"""skip_announcement should schedule multiple sessions and leave canceled session alone"""
self.do_interim_skip_announcement_test(extra_session=True, canceled_session=True)
def test_interim_skip_announcement_with_extra_and_canceled_sessions_and_base_sched(self):
"""skip_announcement should schedule multiple sessions and leave canceled session alone"""
self.do_interim_skip_announcement_test(extra_session=True, canceled_session=True, base_session=True)
def do_interim_send_announcement_test(self, base_session=False, extra_session=False, canceled_session=False):
make_interim_test_data()
meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='apprw').first().meeting
session = Session.objects.with_current_status().filter(
meeting__type='interim', group__acronym='mars', current_status='apprw').first()
meeting = session.meeting
meeting.time_zone = 'America/Los_Angeles'
meeting.save()
if base_session:
base_session = SessionFactory(meeting=meeting, status_id='apprw', add_to_schedule=False)
meeting.schedule.base = Schedule.objects.create(
meeting=meeting, owner=PersonFactory(), name="base", visible=True, public=True
)
SchedTimeSessAssignment.objects.create(
timeslot=TimeSlotFactory.create(meeting=meeting),
session=base_session,
schedule=meeting.schedule.base,
)
meeting.schedule.save()
if extra_session:
extra_session = SessionFactory(meeting=meeting, status_id='apprw')
if canceled_session:
canceled_session = SessionFactory(meeting=meeting, status_id='canceledpa')
url = urlreverse("ietf.meeting.views.interim_send_announcement", kwargs={'number': meeting.number})
login_testing_unauthorized(self, "secretary", url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
initial = r.context['form'].initial
# send announcement
len_before = len(outbox)
r = self.client.post(url, initial)
self.assertRedirects(r, urlreverse('ietf.meeting.views.interim_announce'))
self.assertEqual(len(outbox), len_before + 1)
self.assertIn('WG Virtual Meeting', outbox[-1]['Subject'])
self.assertIn('09:00 to 09:20 America/Los_Angeles', get_payload_text(outbox[-1]))
announcement_msg = outbox[-1]
announcement_text = get_payload_text(announcement_msg)
self.assertIn('WG Virtual Meeting', announcement_msg['Subject'])
self.assertIn('09:00 to 09:20 America/Los_Angeles', announcement_text)
for sess in [session, base_session, extra_session]:
if sess:
timeslot = sess.official_timeslotassignment().timeslot
self.assertIn(timeslot.time.strftime('%Y-%m-%d'), announcement_text)
self.assertIn(
'(%s to %s UTC)' % (
timeslot.utc_start_time().strftime('%H:%M'),timeslot.utc_end_time().strftime('%H:%M')
), announcement_text)
# Count number of sessions listed
if base_session and extra_session:
expected_session_matches = 3
elif base_session or extra_session:
expected_session_matches = 2
else:
expected_session_matches = 0 # no session list when only one session
session_matches = re.findall(r'Session \d+:', announcement_text)
self.assertEqual(len(session_matches), expected_session_matches)
timeslot = meeting.session_set.first().official_timeslotassignment().timeslot
self.assertIn('(%s to %s UTC)'%(timeslot.utc_start_time().strftime('%H:%M'),timeslot.utc_end_time().strftime('%H:%M')), get_payload_text(outbox[-1]))
meeting_sessions = meeting.session_set.with_current_status()
self.assertEqual(meeting_sessions.get(pk=session.pk).current_status, 'sched')
if base_session:
self.assertEqual(meeting_sessions.get(pk=base_session.pk).current_status, 'sched')
if extra_session:
self.assertEqual(meeting_sessions.get(pk=extra_session.pk).current_status, 'sched')
if canceled_session:
self.assertEqual(meeting_sessions.get(pk=canceled_session.pk).current_status, 'canceledpa')
def test_interim_approve_by_ad(self):
def test_interim_send_announcement(self):
self.do_interim_send_announcement_test()
def test_interim_send_announcement_with_base_sched(self):
self.do_interim_send_announcement_test(base_session=True)
def test_interim_send_announcement_with_extra_session(self):
self.do_interim_send_announcement_test(extra_session=True)
def test_interim_send_announcement_with_extra_session_and_base_sched(self):
self.do_interim_send_announcement_test(extra_session=True, base_session=True)
def test_interim_send_announcement_with_canceled_session(self):
self.do_interim_send_announcement_test(canceled_session=True)
def test_interim_send_announcement_with_canceled_session_and_base_sched(self):
self.do_interim_send_announcement_test(canceled_session=True, base_session=True)
def test_interim_send_announcement_with_extra_and_canceled_sessions(self):
self.do_interim_send_announcement_test(extra_session=True, canceled_session=True)
def test_interim_send_announcement_with_extra_and_canceled_sessions_and_base_sched(self):
self.do_interim_send_announcement_test(extra_session=True, canceled_session=True, base_session=True)
def do_interim_approve_by_ad_test(self, base_session=False, extra_session=False, canceled_session=False):
make_interim_test_data()
meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='apprw').first().meeting
session = Session.objects.with_current_status().filter(
meeting__type='interim', group__acronym='mars', current_status='apprw').first()
meeting = session.meeting
if base_session:
base_session = SessionFactory(meeting=meeting, status_id='apprw', add_to_schedule=False)
meeting.schedule.base = Schedule.objects.create(
meeting=meeting, owner=PersonFactory(), name="base", visible=True, public=True
)
SchedTimeSessAssignment.objects.create(
timeslot=TimeSlotFactory.create(meeting=meeting),
session=base_session,
schedule=meeting.schedule.base,
)
meeting.schedule.save()
if extra_session:
extra_session = SessionFactory(meeting=meeting, status_id='apprw')
if canceled_session:
canceled_session = SessionFactory(meeting=meeting, status_id='canceledpa')
url = urlreverse('ietf.meeting.views.interim_request_details', kwargs={'number': meeting.number})
length_before = len(outbox)
login_testing_unauthorized(self, "ad", url)
r = self.client.post(url, {'approve': 'approve'})
self.assertRedirects(r, urlreverse('ietf.meeting.views.interim_pending'))
for session in add_event_info_to_session_qs(meeting.session_set.all()):
self.assertEqual(session.current_status, 'scheda')
for sess in [session, base_session, extra_session]:
if sess:
self.assertEqual(Session.objects.with_current_status().get(pk=sess.pk).current_status,
'scheda')
if canceled_session:
self.assertEqual(Session.objects.with_current_status().get(pk=canceled_session.pk).current_status,
'canceledpa')
self.assertEqual(len(outbox), length_before + 1)
self.assertIn('ready for announcement', outbox[-1]['Subject'])
def test_interim_approve_by_secretariat(self):
def test_interim_approve_by_ad(self):
self.do_interim_approve_by_ad_test()
def test_interim_approve_by_ad_with_base_sched(self):
self.do_interim_approve_by_ad_test(base_session=True)
def test_interim_approve_by_ad_with_extra_session(self):
self.do_interim_approve_by_ad_test(extra_session=True)
def test_interim_approve_by_ad_with_extra_session_and_base_sched(self):
self.do_interim_approve_by_ad_test(extra_session=True, base_session=True)
def test_interim_approve_by_ad_with_canceled_session(self):
self.do_interim_approve_by_ad_test(canceled_session=True)
def test_interim_approve_by_ad_with_canceled_session_and_base_sched(self):
self.do_interim_approve_by_ad_test(canceled_session=True, base_session=True)
def test_interim_approve_by_ad_with_extra_and_canceled_sessions(self):
self.do_interim_approve_by_ad_test(extra_session=True, canceled_session=True)
def test_interim_approve_by_ad_with_extra_and_canceled_sessions_and_base_sched(self):
self.do_interim_approve_by_ad_test(extra_session=True, canceled_session=True, base_session=True)
def do_interim_approve_by_secretariat_test(self, base_session=False, extra_session=False, canceled_session=False):
make_interim_test_data()
meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='apprw').first().meeting
session = Session.objects.with_current_status().filter(
meeting__type='interim', group__acronym='mars', current_status='apprw').first()
meeting = session.meeting
if base_session:
base_session = SessionFactory(meeting=meeting, status_id='apprw', add_to_schedule=False)
meeting.schedule.base = Schedule.objects.create(
meeting=meeting, owner=PersonFactory(), name="base", visible=True, public=True
)
SchedTimeSessAssignment.objects.create(
timeslot=TimeSlotFactory.create(meeting=meeting),
session=base_session,
schedule=meeting.schedule.base,
)
meeting.schedule.save()
if extra_session:
extra_session = SessionFactory(meeting=meeting, status_id='apprw')
if canceled_session:
canceled_session = SessionFactory(meeting=meeting, status_id='canceledpa')
url = urlreverse('ietf.meeting.views.interim_request_details', kwargs={'number': meeting.number})
length_before = len(outbox)
login_testing_unauthorized(self, "secretary", url)
r = self.client.post(url, {'approve': 'approve'})
self.assertRedirects(r, urlreverse('ietf.meeting.views.interim_send_announcement', kwargs={'number': meeting.number}))
for session in add_event_info_to_session_qs(meeting.session_set.all()):
self.assertEqual(session.current_status, 'scheda')
for sess in [session, base_session, extra_session]:
if sess:
self.assertEqual(Session.objects.with_current_status().get(pk=sess.pk).current_status,
'scheda')
if canceled_session:
self.assertEqual(Session.objects.with_current_status().get(pk=canceled_session.pk).current_status,
'canceledpa')
self.assertEqual(len(outbox), length_before)
def test_interim_approve_by_secretariat(self):
self.do_interim_approve_by_secretariat_test()
def test_interim_approve_by_secretariat_with_base_sched(self):
self.do_interim_approve_by_secretariat_test(base_session=True)
def test_interim_approve_by_secretariat_with_extra_session(self):
self.do_interim_approve_by_secretariat_test(extra_session=True)
def test_interim_approve_by_secretariat_with_extra_session_and_base_sched(self):
self.do_interim_approve_by_secretariat_test(extra_session=True, base_session=True)
def test_interim_approve_by_secretariat_with_canceled_session(self):
self.do_interim_approve_by_secretariat_test(canceled_session=True)
def test_interim_approve_by_secretariat_with_canceled_session_and_base_sched(self):
self.do_interim_approve_by_secretariat_test(canceled_session=True, base_session=True)
def test_interim_approve_by_secretariat_with_extra_and_canceled_sessions(self):
self.do_interim_approve_by_secretariat_test(extra_session=True, canceled_session=True)
def test_interim_approve_by_secretariat_with_extra_and_canceled_sessions_and_base_sched(self):
self.do_interim_approve_by_secretariat_test(extra_session=True, canceled_session=True, base_session=True)
def test_past(self):
today = datetime.date.today()
@ -2387,6 +2608,10 @@ class InterimTests(TestCase):
meeting_count_before = Meeting.objects.filter(type='interim').count()
date = datetime.date.today() + datetime.timedelta(days=30)
date2 = date + datetime.timedelta(days=1)
# ensure dates are in the same year
if date.year != date2.year:
date += datetime.timedelta(days=1)
date2 += datetime.timedelta(days=1)
time = datetime.datetime.now().time().replace(microsecond=0,second=0)
dt = datetime.datetime.combine(date, time)
dt2 = datetime.datetime.combine(date2, time)
@ -2536,7 +2761,8 @@ class InterimTests(TestCase):
def test_interim_request_details(self):
make_interim_test_data()
meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='apprw').first().meeting
meeting = Session.objects.with_current_status().filter(
meeting__type='interim', group__acronym='mars', current_status='apprw').first().meeting
url = urlreverse('ietf.meeting.views.interim_request_details',kwargs={'number':meeting.number})
login_testing_unauthorized(self,"secretary",url)
r = self.client.get(url)
@ -2568,51 +2794,317 @@ class InterimTests(TestCase):
q = PyQuery(r.content)
self.assertEqual(len(q("a.btn:contains('Announce')")),2)
def test_interim_request_disapprove(self):
def test_interim_request_details_cancel(self):
"""Test access to cancel meeting / session features"""
make_interim_test_data()
meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='apprw').first().meeting
mars_sessions = Session.objects.with_current_status(
).filter(
meeting__type='interim',
group__acronym='mars',
)
meeting_apprw = mars_sessions.filter(current_status='apprw').first().meeting
meeting_sched = mars_sessions.filter(current_status='sched').first().meeting
# All these roles should have access to cancel the request
usernames_and_passwords = (
('marschairman', 'marschairman+password'),
('secretary', 'secretary+password')
)
# Start with one session - there should not be any cancel session buttons
for meeting in (meeting_apprw, meeting_sched):
url = urlreverse('ietf.meeting.views.interim_request_details',
kwargs={'number': meeting.number})
for username, password in usernames_and_passwords:
self.client.login(username=username, password=password)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
cancel_meeting_btns = q("a.btn:contains('Cancel Meeting')")
self.assertEqual(len(cancel_meeting_btns), 1,
'Should be exactly one cancel meeting button for user %s' % username)
self.assertEqual(cancel_meeting_btns.eq(0).attr('href'),
urlreverse('ietf.meeting.views.interim_request_cancel',
kwargs={'number': meeting.number}),
'Cancel meeting points to wrong URL')
self.assertEqual(len(q("a.btn:contains('Cancel Session')")), 0,
'Should be no cancel session buttons for user %s' % username)
# Add a second session
SessionFactory(meeting=meeting_apprw, status_id='apprw')
SessionFactory(meeting=meeting_sched, status_id='sched')
for meeting in (meeting_apprw, meeting_sched):
url = urlreverse('ietf.meeting.views.interim_request_details',
kwargs={'number': meeting.number})
for username, password in usernames_and_passwords:
self.client.login(username=username, password=password)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
cancel_meeting_btns = q("a.btn:contains('Cancel Meeting')")
self.assertEqual(len(cancel_meeting_btns), 1,
'Should be exactly one cancel meeting button for user %s' % username)
self.assertEqual(cancel_meeting_btns.eq(0).attr('href'),
urlreverse('ietf.meeting.views.interim_request_cancel',
kwargs={'number': meeting.number}),
'Cancel meeting button points to wrong URL')
cancel_session_btns = q("a.btn:contains('Cancel Session')")
self.assertEqual(len(cancel_session_btns), 2,
'Should be two cancel session buttons for user %s' % username)
hrefs = [btn.attr('href') for btn in cancel_session_btns.items()]
for index, session in enumerate(meeting.session_set.all()):
self.assertIn(urlreverse('ietf.meeting.views.interim_request_session_cancel',
kwargs={'sessionid': session.pk}),
hrefs,
'Session missing a link to its cancel URL')
def test_interim_request_details_status(self):
"""Test statuses on the interim request details page"""
make_interim_test_data()
some_person = PersonFactory()
self.client.login(username='marschairman', password='marschairman+password')
# These are the first sessions for each meeting - hang on to them
sessions = list(
Session.objects.with_current_status().filter(meeting__type='interim', group__acronym='mars')
)
# Hack: change the name for the 'canceled' session status so we can tell it apart
# from the 'canceledpa' session status more easily
canceled_status = SessionStatusName.objects.get(slug='canceled')
canceled_status.name = 'This is cancelled'
canceled_status.save()
canceledpa_status = SessionStatusName.objects.get(slug='canceledpa')
notmeet_status = SessionStatusName.objects.get(slug='notmeet')
# Simplest case - single session for each meeting
for session in [Session.objects.with_current_status().get(pk=s.pk) for s in sessions]:
url = urlreverse('ietf.meeting.views.interim_request_details',
kwargs={'number': session.meeting.number})
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
status = SessionStatusName.objects.get(slug=session.current_status)
self.assertEqual(
len(q("dd:contains('%s')" % status.name)),
1 # once - for the meeting status, no session status shown when only one session
)
# Now add a second session with a different status - it should not change meeting status
for session in [Session.objects.with_current_status().get(pk=s.pk) for s in sessions]:
SessionFactory(meeting=session.meeting, status_id=notmeet_status.pk)
url = urlreverse('ietf.meeting.views.interim_request_details',
kwargs={'number': session.meeting.number})
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
status = SessionStatusName.objects.get(slug=session.current_status)
self.assertEqual(
len(q("dd:contains('%s')" % status.name)),
2 # twice - once as the meeting status, once as the session status
)
self.assertEqual(
len(q("dd:contains('%s')" % notmeet_status.name)),
1 # only for the session status
)
# Now cancel the first session - second meeting status should be shown for meeting
for session in [Session.objects.with_current_status().get(pk=s.pk) for s in sessions]:
# Use 'canceledpa' here and 'canceled' later
SchedulingEvent.objects.create(session=session,
status=canceledpa_status,
by=some_person)
url = urlreverse('ietf.meeting.views.interim_request_details',
kwargs={'number': session.meeting.number})
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(
len(q("dd:contains('%s')" % canceledpa_status.name)),
1 # only for the session status
)
self.assertEqual(
len(q("dd:contains('%s')" % notmeet_status.name)),
2 # twice - once as the meeting status, once as the session status
)
# Now cancel the second session - first meeting status should be shown for meeting again
for session in [Session.objects.with_current_status().get(pk=s.pk) for s in sessions]:
second_session = session.meeting.session_set.exclude(pk=session.pk).first()
# use canceled so we can differentiate between the first and second session statuses
SchedulingEvent.objects.create(session=second_session,
status=canceled_status,
by=some_person)
url = urlreverse('ietf.meeting.views.interim_request_details',
kwargs={'number': session.meeting.number})
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(
len(q("dd:contains('%s')" % canceledpa_status.name)),
2 # twice - once as the meeting status, once as the session status
)
self.assertEqual(
len(q("dd:contains('%s')" % canceled_status.name)),
1 # only as the session status
)
def do_interim_request_disapprove_test(self, extra_session=False, canceled_session=False):
make_interim_test_data()
session = Session.objects.with_current_status().filter(
meeting__type='interim', group__acronym='mars', current_status='apprw').first()
meeting = session.meeting
if extra_session:
extra_session = SessionFactory(meeting=meeting, status_id='apprw')
if canceled_session:
canceled_session = SessionFactory(meeting=meeting, status_id='canceledpa')
url = urlreverse('ietf.meeting.views.interim_request_details',kwargs={'number':meeting.number})
login_testing_unauthorized(self,"secretary",url)
r = self.client.post(url,{'disapprove':'Disapprove'})
self.assertRedirects(r, urlreverse('ietf.meeting.views.interim_pending'))
for session in add_event_info_to_session_qs(meeting.session_set.all()):
self.assertEqual(session.current_status,'disappr')
for sess in [session, extra_session]:
if sess:
self.assertEqual(Session.objects.with_current_status().get(pk=sess.pk).current_status,
'disappr')
if canceled_session:
self.assertEqual(Session.objects.with_current_status().get(pk=canceled_session.pk).current_status,
'canceledpa')
def test_interim_request_disapprove(self):
self.do_interim_request_disapprove_test()
def test_interim_request_disapprove_with_extra_session(self):
self.do_interim_request_disapprove_test(extra_session=True)
def test_interim_request_disapprove_with_canceled_session(self):
self.do_interim_request_disapprove_test(canceled_session=True)
def test_interim_request_disapprove_with_extra_and_canceled_sessions(self):
self.do_interim_request_disapprove_test(extra_session=True, canceled_session=True)
def test_interim_request_cancel(self):
"""Test that interim request cancel function works
Does not test that UI buttons are present, that is handled elsewhere.
"""
make_interim_test_data()
meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='apprw').first().meeting
url = urlreverse('ietf.meeting.views.interim_request_details', kwargs={'number': meeting.number})
self.client.login(username="marschairman", password="marschairman+password")
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q("a.btn:contains('Cancel')")), 1)
meeting = Session.objects.with_current_status(
).filter(
meeting__type='interim',
group__acronym='mars',
current_status='apprw',
).first().meeting
# ensure fail unauthorized
url = urlreverse('ietf.meeting.views.interim_request_cancel', kwargs={'number': meeting.number})
comments = 'Bob cannot make it'
self.client.login(username="ameschairman", password="ameschairman+password")
r = self.client.post(url, {'comments': comments})
self.assertEqual(r.status_code, 403)
# test cancelling before announcement
self.client.login(username="marschairman", password="marschairman+password")
length_before = len(outbox)
r = self.client.post(url, {'comments': comments})
self.assertRedirects(r, urlreverse('ietf.meeting.views.upcoming'))
for session in add_event_info_to_session_qs(meeting.session_set.all()):
for session in meeting.session_set.with_current_status():
self.assertEqual(session.current_status,'canceledpa')
self.assertEqual(session.agenda_note, comments)
self.assertEqual(len(outbox), length_before) # no email notice
# test cancelling after announcement
meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='sched').first().meeting
url = urlreverse('ietf.meeting.views.interim_request_cancel', kwargs={'number': meeting.number})
r = self.client.post(url, {'comments': comments})
self.assertRedirects(r, urlreverse('ietf.meeting.views.upcoming'))
for session in add_event_info_to_session_qs(meeting.session_set.all()):
for session in meeting.session_set.with_current_status():
self.assertEqual(session.current_status,'canceled')
self.assertEqual(session.agenda_note, comments)
self.assertEqual(len(outbox), length_before + 1)
self.assertIn('Interim Meeting Cancelled', outbox[-1]['Subject'])
def test_interim_request_session_cancel(self):
"""Test that interim meeting session cancellation functions
Does not test that UI buttons are present, that is handled elsewhere.
"""
make_interim_test_data()
session = Session.objects.with_current_status().filter(
meeting__type='interim', group__acronym='mars', current_status='apprw',).first()
meeting = session.meeting
comments = 'Bob cannot make it'
# Should not be able to cancel when there is only one session
self.client.login(username="marschairman", password="marschairman+password")
url = urlreverse('ietf.meeting.views.interim_request_session_cancel', kwargs={'sessionid': session.pk})
r = self.client.post(url, {'comments': comments})
self.assertEqual(r.status_code, 409)
# Add a second session
SessionFactory(meeting=meeting, status_id='apprw')
# ensure fail unauthorized
url = urlreverse('ietf.meeting.views.interim_request_session_cancel', kwargs={'sessionid': session.pk})
self.client.login(username="ameschairman", password="ameschairman+password")
r = self.client.post(url, {'comments': comments})
self.assertEqual(r.status_code, 403)
# test cancelling before announcement
self.client.login(username="marschairman", password="marschairman+password")
length_before = len(outbox)
canceled_count_before = meeting.session_set.with_current_status().filter(
current_status__in=['canceled', 'canceledpa']).count()
r = self.client.post(url, {'comments': comments})
self.assertRedirects(r, urlreverse('ietf.meeting.views.interim_request_details',
kwargs={'number': meeting.number}))
# This session should be canceled...
sessions = meeting.session_set.with_current_status()
session = sessions.filter(id=session.pk).first() # reload our session info
self.assertEqual(session.current_status, 'canceledpa')
self.assertEqual(session.agenda_note, comments)
# But others should not - count should have changed by only 1
self.assertEqual(
sessions.filter(current_status__in=['canceled', 'canceledpa']).count(),
canceled_count_before + 1
)
self.assertEqual(len(outbox), length_before) # no email notice
# test cancelling after announcement
session = Session.objects.with_current_status().filter(
meeting__type='interim', group__acronym='mars', current_status='sched').first()
meeting = session.meeting
# Try to cancel when there's only one session in the meeting
url = urlreverse('ietf.meeting.views.interim_request_session_cancel', kwargs={'sessionid': session.pk})
r = self.client.post(url, {'comments': comments})
self.assertEqual(r.status_code, 409)
# Add another session
SessionFactory(meeting=meeting, status_id='sched') # two sessions so canceling a session makes sense
canceled_count_before = meeting.session_set.with_current_status().filter(
current_status__in=['canceled', 'canceledpa']).count()
r = self.client.post(url, {'comments': comments})
self.assertRedirects(r, urlreverse('ietf.meeting.views.interim_request_details',
kwargs={'number': meeting.number}))
# This session should be canceled...
sessions = meeting.session_set.with_current_status()
session = sessions.filter(id=session.pk).first() # reload our session info
self.assertEqual(session.current_status, 'canceled')
self.assertEqual(session.agenda_note, comments)
# But others should not - count should have changed by only 1
self.assertEqual(
sessions.filter(current_status__in=['canceled', 'canceledpa']).count(),
canceled_count_before + 1
)
self.assertEqual(len(outbox), length_before + 1) # email notice sent
self.assertIn('session cancelled', outbox[-1]['Subject'])
def test_interim_request_edit_no_notice(self):
'''Edit a request. No notice should go out if it hasn't been announced yet'''
make_interim_test_data()
@ -2716,14 +3208,32 @@ class InterimTests(TestCase):
self.assertEqual(len(outbox),length_before+1)
self.assertIn('New Interim Meeting Request', outbox[-1]['Subject'])
def test_send_interim_cancellation_notice(self):
def test_send_interim_meeting_cancellation_notice(self):
make_interim_test_data()
meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='sched').first().meeting
meeting = Session.objects.with_current_status(
).filter(
meeting__type='interim',
group__acronym='mars',
current_status='sched',
).first().meeting
length_before = len(outbox)
send_interim_cancellation_notice(meeting=meeting)
self.assertEqual(len(outbox),length_before+1)
send_interim_meeting_cancellation_notice(meeting)
self.assertEqual(len(outbox),length_before + 1)
self.assertIn('Interim Meeting Cancelled', outbox[-1]['Subject'])
def test_send_interim_session_cancellation_notice(self):
make_interim_test_data()
session = Session.objects.with_current_status(
).filter(
meeting__type='interim',
group__acronym='mars',
current_status='sched',
).first()
length_before = len(outbox)
send_interim_session_cancellation_notice(session)
self.assertEqual(len(outbox), length_before + 1)
self.assertIn('session cancelled', outbox[-1]['Subject'])
def test_send_interim_minutes_reminder(self):
make_meeting_test_data()
group = Group.objects.get(acronym='mars')

View file

@ -118,6 +118,7 @@ urlpatterns = [
url(r'^interim/request/(?P<number>[A-Za-z0-9._+-]+)/?$', views.interim_request_details),
url(r'^interim/request/(?P<number>[A-Za-z0-9._+-]+)/edit/?$', views.interim_request_edit),
url(r'^interim/request/(?P<number>[A-Za-z0-9._+-]+)/cancel/?$', views.interim_request_cancel),
url(r'^interim/session/(?P<sessionid>[A-Za-z0-9._+-]+)/cancel/?$', views.interim_request_session_cancel),
url(r'^interim/pending/?$', views.interim_pending),
url(r'^requests.html$', RedirectView.as_view(url='/meeting/requests', permanent=True)),
url(r'^past/?$', views.past),

View file

@ -12,7 +12,6 @@ from urllib.error import HTTPError
from django.conf import settings
from django.template.loader import render_to_string
from django.db.models.expressions import Subquery, OuterRef
from django.utils.html import format_html
from django.utils.safestring import mark_safe
@ -48,7 +47,7 @@ def session_requested_by(session):
return None
def current_session_status(session):
last_event = SchedulingEvent.objects.filter(session=session).order_by('-time', '-id').first()
last_event = SchedulingEvent.objects.filter(session=session).select_related('status').order_by('-time', '-id').first()
if last_event:
return last_event.status
@ -220,34 +219,18 @@ def condition_slide_order(session):
def add_event_info_to_session_qs(qs, current_status=True, requested_by=False, requested_time=False):
"""Take a session queryset and add attributes computed from the
scheduling events. A queryset is returned and the added attributes
can be further filtered on."""
from django.db.models import TextField, Value
from django.db.models.functions import Coalesce
can be further filtered on.
Treat this method as deprecated. Use the SessionQuerySet methods directly, chaining if needed.
"""
if current_status:
qs = qs.annotate(
# coalesce with '' to avoid nulls which give funny
# results, e.g. .exclude(current_status='canceled') also
# skips rows with null in them
current_status=Coalesce(Subquery(SchedulingEvent.objects.filter(session=OuterRef('pk')).order_by('-time', '-id').values('status')[:1]), Value(''), output_field=TextField()),
)
qs = qs.with_current_status()
if requested_by:
qs = qs.annotate(
requested_by=Subquery(SchedulingEvent.objects.filter(session=OuterRef('pk')).order_by('time', 'id').values('by')[:1]),
)
qs = qs.with_requested_by()
if requested_time:
qs = qs.annotate(
requested_time=Subquery(SchedulingEvent.objects.filter(session=OuterRef('pk')).order_by('time', 'id').values('time')[:1]),
)
return qs
def only_sessions_that_can_meet(session_qs):
qs = add_event_info_to_session_qs(session_qs).exclude(current_status__in=['notmeet', 'disappr', 'deleted', 'apprw'])
# Restrict graphical scheduling to meeting requests (Sessions) of type 'regular' for now
qs = qs.filter(type__slug='regular')
qs = qs.with_requested_time()
return qs
@ -279,9 +262,14 @@ def data_for_meetings_overview(meetings, interim_status=None):
for m in meetings:
m.sessions = []
sessions = add_event_info_to_session_qs(
Session.objects.filter(meeting__in=meetings).order_by('meeting', 'pk')
).select_related('group', 'group__parent')
sessions = Session.objects.filter(
meeting__in=meetings
).order_by(
'meeting', 'pk'
).with_current_status(
).select_related(
'group', 'group__parent'
)
meeting_dict = {m.pk: m for m in meetings}
for s in sessions.iterator():

View file

@ -72,7 +72,7 @@ from ietf.meeting.helpers import can_view_interim_request, can_approve_interim_r
from ietf.meeting.helpers import can_edit_interim_request
from ietf.meeting.helpers import can_request_interim_meeting, get_announcement_initial
from ietf.meeting.helpers import sessions_post_save, is_interim_meeting_approved
from ietf.meeting.helpers import send_interim_cancellation_notice
from ietf.meeting.helpers import send_interim_meeting_cancellation_notice, send_interim_session_cancellation_notice
from ietf.meeting.helpers import send_interim_approval
from ietf.meeting.helpers import send_interim_approval_request
from ietf.meeting.helpers import send_interim_announcement_request
@ -2108,7 +2108,7 @@ def get_sessions(num, acronym):
if not sessions:
sessions = Session.objects.filter(meeting=meeting,short=acronym,type__in=['regular','plenary','other'])
sessions = add_event_info_to_session_qs(sessions)
sessions = sessions.with_current_status()
return sorted(sessions, key=lambda s: session_time_for_sorting(s, use_meeting_date=False))
@ -2142,15 +2142,15 @@ def session_details(request, num, acronym):
session.times = [ x.timeslot.utc_start_time() for x in ss ]
else:
session.times = [ x.timeslot.local_start_time() for x in ss ]
session.cancelled = session.current_status == 'canceled'
session.cancelled = session.current_status in Session.CANCELED_STATUSES
session.status = ''
elif meeting.type_id=='interim':
session.times = [ meeting.date ]
session.cancelled = session.current_status == 'canceled'
session.cancelled = session.current_status in Session.CANCELED_STATUSES
session.status = ''
else:
session.times = []
session.cancelled = session.current_status == 'canceled'
session.cancelled = session.current_status in Session.CANCELED_STATUSES
session.status = status_names.get(session.current_status, session.current_status)
session.filtered_artifacts = list(session.sessionpresentation_set.filter(document__type__slug__in=['agenda','minutes','bluesheets']))
@ -2987,7 +2987,8 @@ def interim_send_announcement(request, number):
if form.is_valid():
message = form.save(user=request.user)
message.related_groups.add(group)
for session in meeting.session_set.all():
for session in meeting.session_set.not_canceled():
SchedulingEvent.objects.create(
session=session,
status=SessionStatusName.objects.get(slug='sched'),
@ -3012,7 +3013,7 @@ def interim_skip_announcement(request, number):
meeting = get_object_or_404(Meeting, number=number)
if request.method == 'POST':
for session in meeting.session_set.all():
for session in meeting.session_set.not_canceled():
SchedulingEvent.objects.create(
session=session,
status=SessionStatusName.objects.get(slug='sched'),
@ -3156,7 +3157,7 @@ def interim_request_cancel(request, number):
was_scheduled = session_status.slug == 'sched'
result_status = SessionStatusName.objects.get(slug='canceled' if was_scheduled else 'canceledpa')
for session in meeting.session_set.all():
for session in meeting.session_set.not_canceled():
SchedulingEvent.objects.create(
session=session,
status=result_status,
@ -3164,7 +3165,7 @@ def interim_request_cancel(request, number):
)
if was_scheduled:
send_interim_cancellation_notice(meeting)
send_interim_meeting_cancellation_notice(meeting)
messages.success(request, 'Interim meeting cancelled')
return redirect(upcoming)
@ -3178,20 +3179,70 @@ def interim_request_cancel(request, number):
})
@login_required
def interim_request_session_cancel(request, sessionid):
'''View for cancelling an interim meeting request'''
session = get_object_or_404(Session, pk=sessionid)
group = session.group
if not can_manage_group(request.user, group):
permission_denied(request, "You do not have permissions to cancel this session")
session_status = current_session_status(session)
if request.method == 'POST':
form = InterimCancelForm(request.POST)
if form.is_valid():
remaining_sessions = session.meeting.session_set.with_current_status().exclude(
current_status__in=['canceled', 'canceledpa']
)
if remaining_sessions.count() <= 1:
return HttpResponse('Cannot cancel only remaining session. Cancel the request instead.',
status=409)
if 'comments' in form.changed_data:
session.agenda_note=form.cleaned_data.get('comments')
session.save()
was_scheduled = session_status.slug == 'sched'
result_status = SessionStatusName.objects.get(slug='canceled' if was_scheduled else 'canceledpa')
SchedulingEvent.objects.create(
session=session,
status=result_status,
by=request.user.person,
)
if was_scheduled:
send_interim_session_cancellation_notice(session)
messages.success(request, 'Interim meeting session cancelled')
return redirect(interim_request_details, number=session.meeting.number)
else:
session_time = session.official_timeslotassignment().timeslot.time
form = InterimCancelForm(initial={'group': group.acronym, 'date': session_time.date()})
return render(request, "meeting/interim_request_cancel.html", {
"form": form,
"session": session,
"session_status": session_status,
})
@login_required
def interim_request_details(request, number):
'''View details of an interim meeting request'''
meeting = get_object_or_404(Meeting, number=number)
group = meeting.session_set.first().group
sessions_not_canceled = meeting.session_set.not_canceled()
first_session = meeting.session_set.first() # first, whether or not canceled
group = first_session.group
if not can_manage_group(request.user, group):
permission_denied(request, "You do not have permissions to manage this meeting request")
sessions = meeting.session_set.all()
can_edit = can_edit_interim_request(meeting, request.user)
can_approve = can_approve_interim_request(meeting, request.user)
if request.method == 'POST':
if request.POST.get('approve') and can_approve_interim_request(meeting, request.user):
for session in meeting.session_set.all():
for session in sessions_not_canceled:
SchedulingEvent.objects.create(
session=session,
status=SessionStatusName.objects.get(slug='scheda'),
@ -3204,7 +3255,7 @@ def interim_request_details(request, number):
send_interim_announcement_request(meeting)
return redirect(interim_pending)
if request.POST.get('disapprove') and can_approve_interim_request(meeting, request.user):
for session in meeting.session_set.all():
for session in sessions_not_canceled:
SchedulingEvent.objects.create(
session=session,
status=SessionStatusName.objects.get(slug='disappr'),
@ -3213,20 +3264,32 @@ def interim_request_details(request, number):
messages.success(request, 'Interim meeting disapproved')
return redirect(interim_pending)
first_session = sessions.first()
assignments = SchedTimeSessAssignment.objects.filter(schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None])
# Determine meeting status from non-canceled sessions, if any.
# N.b., meeting_status may be None after either of these code paths,
# though I am not sure what circumstances would cause this.
if sessions_not_canceled.count() > 0:
meeting_status = current_session_status(sessions_not_canceled.first())
else:
meeting_status = current_session_status(first_session)
meeting_assignments = SchedTimeSessAssignment.objects.filter(
schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None]
).select_related(
'session', 'timeslot'
)
for ma in meeting_assignments:
ma.status = current_session_status(ma.session)
ma.can_be_canceled = ma.status.slug in ('sched', 'scheda', 'apprw')
return render(request, "meeting/interim_request_details.html", {
"meeting": meeting,
"sessions": sessions,
"assignments": assignments,
"group": first_session.group,
"meeting_assignments": meeting_assignments,
"group": group,
"requester": session_requested_by(first_session),
"session_status": current_session_status(first_session),
"meeting_status": meeting_status or SessionStatusName.objects.get(slug='canceled'),
"can_edit": can_edit,
"can_approve": can_approve})
@login_required
def interim_request_edit(request, number):
'''Edit details of an interim meeting reqeust'''

View file

@ -20,7 +20,6 @@ from ietf.meeting.forms import duration_string
from ietf.meeting.helpers import get_meeting, make_materials_directories, populate_important_dates
from ietf.meeting.models import Meeting, Session, Room, TimeSlot, SchedTimeSessAssignment, Schedule, SchedulingEvent
from ietf.meeting.utils import add_event_info_to_session_qs
from ietf.meeting.utils import only_sessions_that_can_meet
from ietf.name.models import SessionStatusName
from ietf.group.models import Group, GroupEvent
from ietf.secr.meetings.blue_sheets import create_blue_sheets
@ -662,9 +661,7 @@ def regular_sessions(request, meeting_id, schedule_name):
meeting = get_object_or_404(Meeting, number=meeting_id)
schedule = get_object_or_404(Schedule, meeting=meeting, name=schedule_name)
sessions = add_event_info_to_session_qs(
only_sessions_that_can_meet(meeting.session_set)
).order_by('group__acronym')
sessions = meeting.session_set.that_can_meet().order_by('group__acronym')
if request.method == 'POST':
if 'cancel' in request.POST:

View file

@ -1,11 +1,11 @@
{% load ietf_filters %}{% if is_change %}MEETING DETAILS HAVE CHANGED. SEE LATEST DETAILS BELOW.
{% endif %}The {{ group.name }} ({{ group.acronym }}) {% if group.type.slug == 'wg' and group.state.slug == 'bof' %}BOF{% else %}{{group.type.name}}{% endif %} will hold
{% if meeting.session_set.count == 1 %}a{% if meeting.city %}n {% else %} virtual {% endif %}interim meeting on {{ meeting.date }} from {{ assignments.first.timeslot.time | date:"H:i" }} to {{ assignments.first.timeslot.end_time | date:"H:i" }} {{ meeting.time_zone}}{% if meeting.time_zone != 'UTC' %} ({{ assignments.first.timeslot.utc_start_time | date:"H:i" }} to {{ assignments.first.timeslot.utc_end_time | date:"H:i" }} UTC){% endif %}.
{% if assignments.count == 1 %}a{% if meeting.city %}n {% else %} virtual {% endif %}interim meeting on {{ meeting.date }} from {{ assignments.first.timeslot.time | date:"H:i" }} to {{ assignments.first.timeslot.end_time | date:"H:i" }} {{ meeting.time_zone}}{% if meeting.time_zone != 'UTC' %} ({{ assignments.first.timeslot.utc_start_time | date:"H:i" }} to {{ assignments.first.timeslot.utc_end_time | date:"H:i" }} UTC){% endif %}.
{% else %}a multi-day {% if not meeting.city %}virtual {% endif %}interim meeting.
{% for assignment in assignments %}Session {{ forloop.counter }}:
{{ assignment.timeslot.time | date:"Y-m-d" }} {{ assignment.timeslot.time | date:"H:i" }} to {{ assignment.timeslot.end_time | date:"H:i" }} {{ meeting.time_zone }}{% if meeting.time_zone != 'UTC' %}({{ assignment.timeslot.utc_start_time | date:"H:i" }} to {{ assignment.timeslot.utc_end_time | date:"H:i" }} UTC){% endif %}
{{ assignment.timeslot.time | date:"Y-m-d" }} {{ assignment.timeslot.time | date:"H:i" }} to {{ assignment.timeslot.end_time | date:"H:i" }} {{ meeting.time_zone }}{% if meeting.time_zone != 'UTC' %}({{ assignment.timeslot.utc_start_time | date:"H:i" }} to {{ assignment.timeslot.utc_end_time | date:"H:i" }} UTC){% endif %}
{% endfor %}{% endif %}
{% if meeting.city %}Meeting Location:
{{ meeting.city }}, {{ meeting.country }}

View file

@ -8,7 +8,7 @@
Country: {{ meeting.country }}
{% else %}Meeting Type: Virtual Meeting{% endif %}
{% for session in meeting.session_set.all %}Session {{ forloop.counter }}:
{% for session in meeting.session_set.not_canceled %}Session {{ forloop.counter }}:
Date: {{ session.official_timeslotassignment.timeslot.time|date:"Y-m-d" }}
Start Time: {{ session.official_timeslotassignment.timeslot.time|date:"H:i" }} {{ meeting.time_zone }}

View file

@ -13,17 +13,18 @@
{% block content %}
{% origin %}
<h1>{% block title %}Cancel Interim Meeting {% if session_status != "sched" %}Request{% endif %}{% endblock %}</h1>
<h1>{% block title %}Cancel Interim {% if meeting %}Meeting{% else %}Session{% endif %} {% if session_status != "sched" %}Request{% endif %}{% endblock %}</h1>
<form id="interim-request-cancel-form" role="form" method="post" class="form-horizontal">
{% csrf_token %}
{% bootstrap_form form layout='horizontal' %}
<div class="form-group"
<div class="form-group">
{% buttons %}
<button type="submit" class="btn btn-primary">Submit</button>
<a class="btn btn-default pull-right" href="{% url 'ietf.meeting.views.interim_request_details' number=meeting.number %}">Back</a>
{% if meeting %}<a class="btn btn-default pull-right" href="{% url 'ietf.meeting.views.interim_request_details' number=meeting.number %}">Back</a>
{% else %}<a class="btn btn-default pull-right" href="{% url 'ietf.meeting.views.interim_request_details' number=session.meeting.number %}">Back</a>{% endif %}
{% endbuttons %}
</div>

View file

@ -19,18 +19,21 @@
<dt>Requested By</dt>
<dd>{{ requester }}
<dt>Status</dt>
<dd>{{ session_status.name }}</dd>
<dd>{{ meeting_status.name }}</dd>
<dt>City</dt>
<dd>{{ meeting.city }}</dd>
<dt>Country</dt>
<dd>{{ meeting.country }}</dd>
<dt>Timezone</dt>
<dd>{{ meeting.time_zone }}</dd>
{% for assignment in assignments %}
{% for assignment in meeting_assignments %}
<br>
{% if meeting_assignments|length > 1 %}
<dt>Session</dt><dd>{{ assignment.status.name }}</dd>
{% endif %}
<dt>Date</dt>
<dd>{{ assignment.timeslot.time|date:"Y-m-d" }}
<dt>Start Time</dt>
<dt>Start Time</dt>
<dd>{{ assignment.timeslot.time|date:"H:i" }} {% if meeting.time_zone != 'UTC' %}( {{ assignment.timeslot.utc_start_time|date:"H:i"}} UTC ){% endif %}
<dt>Duration</dt>
<dd>{{ assignment.session.requested_duration|format_timedelta }}
@ -38,36 +41,44 @@
<dd>{{ assignment.session.remote_instructions }}
<dt>Additional Info</dt>
<dd>{{ assignment.session.agenda_note }}</dd>
{% if meeting_assignments|length > 1 %}
{% if can_edit and assignment.can_be_canceled %}
<dt>Actions</dt>
<dd><a class="btn btn-default btn-sm" href="{% url 'ietf.meeting.views.interim_request_session_cancel' sessionid=assignment.session.pk %}">Cancel Session</a></dd>
{% endif %}
{% endif %}
{% endfor %}
</dl>
<form method="post">
{% csrf_token %}
{% with meeting_status.slug as status_slug %}
{% if can_edit %}
<a class="btn btn-default" href="{% url 'ietf.meeting.views.interim_request_edit' number=meeting.number %}">Edit</a>
{% endif %}
{% if can_approve and session_status.slug == 'apprw' %}
{% if can_approve and status_slug == 'apprw' %}
<input class="btn btn-default" type="submit" value="Approve" name='approve' />
<input class="btn btn-default" type="submit" value="Disapprove" name='disapprove' />
{% endif %}
{% if user|has_role:"Secretariat" and session_status.slug == 'scheda' %}
{% if user|has_role:"Secretariat" and status_slug == 'scheda' %}
<a class="btn btn-default" href="{% url 'ietf.meeting.views.interim_send_announcement' number=meeting.number %}">Announce</a>
<a class="btn btn-default" href="{% url 'ietf.meeting.views.interim_skip_announcement' number=meeting.number %}">Skip Announcement</a>
{% endif %}
{% if can_edit %}
{% if session_status.slug == 'apprw' or session_status.slug == 'scheda' or session_status.slug == 'sched' %}
{% if status_slug == 'apprw' or status_slug == 'scheda' or status_slug == 'sched' %}
<a class="btn btn-default" href="{% url 'ietf.meeting.views.interim_request_cancel' number=meeting.number %}">Cancel Meeting</a>
{% endif %}
{% endif %}
{% if session_status.slug == "apprw" %}
{% if status_slug == "apprw" %}
<a class="btn btn-default" href="{% url 'ietf.meeting.views.interim_pending' %}">Back</a>
{% elif session_status.slug == "scheda" %}
{% elif status_slug == "scheda" %}
<a class="btn btn-default" href="{% url 'ietf.meeting.views.interim_announce' %}">Back</a>
{% elif session_status.slug == "sched" %}
{% elif status_slug == "sched" %}
<a class="btn btn-default" href="{% url 'ietf.meeting.views.session_details' num=meeting.number acronym=meeting.session_set.first.group.acronym %}">Back</a>
{% else %}
<a class="btn btn-default" href="{% url 'ietf.meeting.views.upcoming' %}">Back</a>
{% endif %}
{% endwith %}
</form>
{% endblock %}

View file

@ -0,0 +1,7 @@
{% load ams_filters %}
{% if session.name %}The "{{ session.name }}"{% else %}A{% endif %} session of the {{ group.name }} ({{ group.acronym }}) {% if not session.meeting.city %}virtual {% endif %}{% if is_multi_day %}multi-day {% endif %}
interim meeting has been cancelled. This session had been scheduled for {{ meeting.date|date:"Y-m-d" }} from {{ start_time|time:"H:i" }} to {{ end_time|time:"H:i" }} {{ meeting.time_zone }}.
{{ session.agenda_note }}