Allow cancellation of individual sessions of multi-session interim meeting. Fixes #2959. Commit ready for merge.
- Legacy-Id: 18724
This commit is contained in:
parent
1b19353382
commit
79971e14c7
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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'''
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 }}
|
||||
|
||||
|
Loading…
Reference in a new issue