Move Session.status, .requested, and .requested_by to a new SchedulingEvent

- Legacy-Id: 17122
This commit is contained in:
Ole Laursen 2019-12-05 12:41:09 +00:00
parent 0555879777
commit 3957743b85
48 changed files with 1143 additions and 674 deletions

View file

@ -69,7 +69,7 @@ class PersonalInformationExportView(DetailView, JsonExportMixin):
person = get_object_or_404(self.model, user=request.user)
expand = ['searchrule', 'documentauthor', 'ad_document_set', 'ad_dochistory_set', 'docevent',
'ballotpositiondocevent', 'deletedevent', 'email_set', 'groupevent', 'role', 'rolehistory', 'iprdisclosurebase',
'iprevent', 'liaisonstatementevent', 'whitelisted', 'schedule', 'constraint', 'session', 'message',
'iprevent', 'liaisonstatementevent', 'whitelisted', 'schedule', 'constraint', 'schedulingevent', 'message',
'sendqueue', 'nominee', 'topicfeedbacklastseen', 'alias', 'email', 'apikeys', 'personevent',
'reviewersettings', 'reviewsecretarysettings', 'unavailableperiod', 'reviewwish',
'nextreviewerinteam', 'reviewrequest', 'meetingregistration', 'submissionevent', 'preapproval',

View file

@ -39,7 +39,7 @@ from ietf.doc.utils import create_ballot_if_not_open
from ietf.group.models import Group
from ietf.group.factories import GroupFactory, RoleFactory
from ietf.ipr.factories import HolderIprDisclosureFactory
from ietf.meeting.models import Meeting, Session, SessionPresentation
from ietf.meeting.models import Meeting, Session, SessionPresentation, SchedulingEvent
from ietf.meeting.factories import MeetingFactory, SessionFactory
from ietf.name.models import SessionStatusName, BallotPositionName
from ietf.person.models import Person
@ -712,11 +712,14 @@ class DocTestCase(TestCase):
name = "session-72-mars-1",
meeting = Meeting.objects.get(number='72'),
group = Group.objects.get(acronym='mars'),
status = SessionStatusName.objects.create(slug='scheduled', name='Scheduled'),
modified = datetime.datetime.now(),
requested_by = Person.objects.get(user__username="marschairman"),
type_id = "session",
)
)
SchedulingEvent.objects.create(
session=session,
status=SessionStatusName.objects.create(slug='scheduled'),
by = Person.objects.get(user__username="marschairman"),
)
SessionPresentation.objects.create(session=session, document=doc, rev=doc.rev)
r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=doc.name)))

View file

@ -20,7 +20,7 @@ from ietf.doc.models import Document, State, DocAlias, NewRevisionDocEvent
from ietf.group.factories import RoleFactory
from ietf.group.models import Group
from ietf.meeting.factories import MeetingFactory
from ietf.meeting.models import Meeting, Session, SessionPresentation
from ietf.meeting.models import Meeting, Session, SessionPresentation, SchedulingEvent
from ietf.name.models import SessionStatusName
from ietf.person.models import Person
from ietf.utils.test_utils import TestCase, login_testing_unauthorized
@ -158,11 +158,14 @@ class GroupMaterialTests(TestCase):
name = "session-42-mars-1",
meeting = Meeting.objects.get(number='42'),
group = Group.objects.get(acronym='mars'),
status = SessionStatusName.objects.create(slug='scheduled', name='Scheduled'),
modified = datetime.datetime.now(),
requested_by = Person.objects.get(user__username="marschairman"),
type_id="session",
)
)
SchedulingEvent.objects.create(
session=session,
status=SessionStatusName.objects.create(slug='scheduled'),
by = Person.objects.get(user__username="marschairman"),
)
SessionPresentation.objects.create(session=session, document=doc, rev=doc.rev)
url = urlreverse('ietf.doc.views_material.edit_material', kwargs=dict(name=doc.name, action="revise"))

View file

@ -68,13 +68,13 @@ from ietf.group.models import Role, Group
from ietf.group.utils import can_manage_group_type, can_manage_materials, group_features_role_filter
from ietf.ietfauth.utils import ( has_role, is_authorized_in_doc_stream, user_is_person,
role_required, is_individual_draft_author)
from ietf.name.models import StreamName, BallotPositionName
from ietf.name.models import StreamName, BallotPositionName, SessionStatusName
from ietf.utils.history import find_history_active_at
from ietf.doc.forms import TelechatForm, NotifyForm
from ietf.doc.mails import email_comment
from ietf.mailtrigger.utils import gather_relevant_expansions
from ietf.meeting.models import Session
from ietf.meeting.utils import group_sessions, get_upcoming_manageable_sessions, sort_sessions
from ietf.meeting.utils import group_sessions, get_upcoming_manageable_sessions, sort_sessions, add_event_info_to_session_qs
from ietf.review.models import ReviewAssignment
from ietf.review.utils import can_request_review_of_doc, review_assignments_to_list_for_docs
from ietf.review.utils import no_review_from_teams_on_doc
@ -1345,9 +1345,13 @@ def add_sessionpresentation(request,name):
def all_presentations(request, name):
doc = get_object_or_404(Document, name=name)
sessions = add_event_info_to_session_qs(
doc.session_set.filter(type__in=['session','plenary','other'])
).filter(current_status__in=['sched','schedw','appr','canceled'])
sessions = doc.session_set.filter(status__in=['sched','schedw','appr','canceled'],
type__in=['session','plenary','other'])
status_names = {n.slug: n.name for n in SessionStatusName.objects.all()}
for session in sessions:
session.current_status_name = status_names.get(session.current_status, session.current_status)
future, in_progress, past = group_sessions(sessions)

View file

@ -70,7 +70,7 @@ class GroupInfo(models.Model):
return list(set([ role for role in self.parent.role_set.filter(name__in=['ad', 'chair']) ]))
def is_bof(self):
return (self.state.slug in ["bof", "bof-conc"])
return self.state_id in ["bof", "bof-conc"]
class Meta:
abstract = True

View file

@ -88,7 +88,7 @@ from ietf.group.utils import (get_charter_text, can_manage_group_type,
from ietf.ietfauth.utils import has_role, is_authorized_in_group
from ietf.mailtrigger.utils import gather_relevant_expansions
from ietf.meeting.helpers import get_meeting
from ietf.meeting.utils import group_sessions
from ietf.meeting.utils import group_sessions, add_event_info_to_session_qs
from ietf.name.models import GroupTypeName, StreamName
from ietf.person.models import Email
from ietf.review.models import ReviewRequest, ReviewAssignment, ReviewerSettings, ReviewSecretarySettings
@ -750,9 +750,14 @@ def meetings(request, acronym=None, group_type=None):
four_years_ago = datetime.datetime.now()-datetime.timedelta(days=4*365)
sessions = group.session_set.filter(status__in=['sched','schedw','appr','canceled'],
meeting__date__gt=four_years_ago,
type__in=['session','plenary','other'])
sessions = add_event_info_to_session_qs(
group.session_set.filter(
meeting__date__gt=four_years_ago,
type__in=['session','plenary','other']
)
).filter(
current_status__in=['sched','schedw','appr','canceled'],
)
future, in_progress, past = group_sessions(sessions)

View file

@ -355,7 +355,10 @@ class Recipient(models.Model):
addrs=[]
if 'session' in kwargs:
session = kwargs['session']
addrs.append(session.requested_by.role_email('chair').address)
from ietf.meeting.models import SchedulingEvent
first_event = SchedulingEvent.objects.filter(session=session).select_related('by').order_by('time', 'id').first()
if first_event and first_event.status_id in ['appw', 'schedw']:
addrs.append(first_event.by.role_email('chair').address)
return addrs
def gather_review_team_ads(self, **kwargs):

View file

@ -8,7 +8,7 @@ from django.contrib import admin
from ietf.meeting.models import (Meeting, Room, Session, TimeSlot, Constraint, Schedule,
SchedTimeSessAssignment, ResourceAssociation, FloorPlan, UrlResource,
SessionPresentation, ImportantDate, SlideSubmission, )
SessionPresentation, ImportantDate, SlideSubmission, SchedulingEvent)
class UrlResourceAdmin(admin.ModelAdmin):
@ -80,13 +80,37 @@ class ConstraintAdmin(admin.ModelAdmin):
admin.site.register(Constraint, ConstraintAdmin)
class SessionAdmin(admin.ModelAdmin):
list_display = ["meeting", "name", "group", "attendees", "requested", "status"]
list_filter = ["meeting", ]
raw_id_fields = ["meeting", "group", "requested_by", "materials"]
search_fields = ["meeting__number", "name", "group__name", "group__acronym", ]
ordering = ["-requested"]
class SchedulingEventInline(admin.TabularInline):
model = SchedulingEvent
raw_id_fields = ["by"]
class SessionAdmin(admin.ModelAdmin):
list_display = ["meeting", "name", "group", "attendees", "requested", "current_status"]
list_filter = ["meeting", ]
raw_id_fields = ["meeting", "group", "materials"]
search_fields = ["meeting__number", "name", "group__name", "group__acronym", ]
ordering = ["-id"]
inlines = [SchedulingEventInline]
def get_queryset(self, request):
qs = super(SessionAdmin, self).get_queryset(request)
return qs.prefetch_related('schedulingevent_set')
def current_status(self, instance):
events = sorted(instance.schedulingevent_set.all(), key=lambda e: (e.time, e.id))
if events:
return events[-1].time
else:
return None
def requested(self, instance):
events = sorted(instance.schedulingevent_set.all(), key=lambda e: (e.time, e.id))
if events:
return events[0].time
else:
return None
def name_lower(self, instance):
return instance.name.name.lower()
@ -94,6 +118,13 @@ class SessionAdmin(admin.ModelAdmin):
admin.site.register(Session, SessionAdmin)
class SchedulingEventAdmin(admin.ModelAdmin):
list_display = ["session", "status", "time", "by"]
raw_id_fields = ["session", "by"]
ordering = ["-id"]
admin.site.register(SchedulingEvent, SchedulingEventAdmin)
class ScheduleAdmin(admin.ModelAdmin):
list_display = ["name", "meeting", "owner", "visible", "public", "badness"]
list_filter = ["meeting", ]

View file

@ -10,7 +10,9 @@ from django.views.decorators.http import require_POST
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.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
@ -431,7 +433,11 @@ def session_json(request, num, sessionid):
def sessions_json(request, num):
meeting = get_meeting(num)
sessions = meeting.sessions_that_can_meet.all()
sessions = add_event_info_to_session_qs(
only_sessions_that_can_meet(meeting.session_set),
requested_time=True,
requested_by=True,
)
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

@ -10,7 +10,8 @@ import datetime
from django.core.files.base import ContentFile
from ietf.meeting.models import Meeting, Session, Schedule, TimeSlot, SessionPresentation, FloorPlan, Room, SlideSubmission
from ietf.meeting.models import Meeting, Session, SchedulingEvent, Schedule, TimeSlot, SessionPresentation, FloorPlan, Room, SlideSubmission
from ietf.name.models import SessionStatusName
from ietf.group.factories import GroupFactory
from ietf.person.factories import PersonFactory
@ -83,9 +84,28 @@ class SessionFactory(factory.DjangoModelFactory):
meeting = factory.SubFactory(MeetingFactory)
type_id='session'
group = factory.SubFactory(GroupFactory)
requested_by = factory.SubFactory(PersonFactory)
status_id='sched'
@factory.post_generation
def status_id(obj, create, extracted, **kwargs):
if create:
if not extracted:
extracted = 'sched'
if extracted not in ['apprw', 'schedw']:
# requested event
SchedulingEvent.objects.create(
session=obj,
status=SessionStatusName.objects.get(slug='schedw'),
by=PersonFactory(),
)
# actual state event
SchedulingEvent.objects.create(
session=obj,
status=SessionStatusName.objects.get(slug=extracted),
by=PersonFactory(),
)
@factory.post_generation
def add_to_schedule(obj, create, extracted, **kwargs): # pylint: disable=no-self-argument
'''

View file

@ -21,7 +21,7 @@ from ietf.group.models import Group
from ietf.ietfauth.utils import has_role
from ietf.meeting.models import Session, Meeting, Schedule, countries, timezones
from ietf.meeting.helpers import get_next_interim_number, make_materials_directories
from ietf.meeting.helpers import is_meeting_approved, get_next_agenda_name
from ietf.meeting.helpers import is_interim_meeting_approved, get_next_agenda_name
from ietf.message.models import Message
from ietf.person.models import Person
from ietf.utils.fields import DatepickerDateField, DurationField, MultiEmailField
@ -132,7 +132,7 @@ class InterimMeetingModelForm(forms.ModelForm):
self.fields['group'].widget.attrs['disabled'] = True
if self.instance.city or self.instance.country:
self.fields['in_person'].initial = True
if is_meeting_approved(self.instance):
if is_interim_meeting_approved(self.instance):
self.fields['approved'].initial = True
else:
self.fields['approved'].initial = False
@ -244,15 +244,8 @@ class InterimSessionModelForm(forms.ModelForm):
"""NOTE: as the baseform of an inlineformset self.save(commit=True)
never gets called"""
session = super(InterimSessionModelForm, self).save(commit=kwargs.get('commit', True))
if self.is_approved_or_virtual:
session.status_id = 'scheda'
else:
session.status_id = 'apprw'
session.group = self.group
session.type_id = 'session'
if not self.instance.pk:
session.requested_by = self.user.person
return session
def save_agenda(self):

View file

@ -27,7 +27,8 @@ from ietf.ietfauth.utils import has_role, user_is_person
from ietf.liaisons.utils import get_person_for_user
from ietf.mailtrigger.utils import gather_address_lists
from ietf.person.models import Person
from ietf.meeting.models import Meeting, Schedule, TimeSlot, SchedTimeSessAssignment, ImportantDate
from ietf.meeting.models import Meeting, Schedule, TimeSlot, SchedTimeSessAssignment, ImportantDate, SchedulingEvent
from ietf.meeting.utils import session_requested_by
from ietf.name.models import ImportantDateName
from ietf.utils.history import find_history_active_at, find_history_replacements_active_at
from ietf.utils.mail import send_mail
@ -205,6 +206,11 @@ def preprocess_assignments_for_agenda(assignments_queryset, meeting):
if a.session and a.session.historic_group and a.session.historic_group.parent_id:
a.session.historic_group.historic_parent = parent_replacements.get(a.session.historic_group.parent_id)
# add current session status
sessions = {a.session_id: a.session for a in assignments if a.session}
for e in SchedulingEvent.objects.filter(session__in=sessions.keys()).order_by('time', 'id').iterator():
sessions[e.session_id].current_status = e.status_id
return assignments
def read_session_file(type, num, doc):
@ -435,12 +441,9 @@ def get_earliest_session_date(formset):
return sorted([f.cleaned_data['date'] for f in formset.forms if f.cleaned_data.get('date')])[0]
def is_meeting_approved(meeting):
"""Returns True if the meeting is approved"""
if meeting.session_set.first().status.slug == 'apprw':
return False
else:
return True
def is_interim_meeting_approved(meeting):
from ietf.meeting.utils import add_event_info_to_session_qs
return add_event_info_to_session_qs(meeting.session_set.all()).first().current_status == 'apprw'
def get_next_interim_number(acronym,date):
'''
@ -493,8 +496,9 @@ def send_interim_approval_request(meetings):
"""Sends an email to the secretariat, group chairs, and responsible area
director or the IRTF chair noting that approval has been requested for a
new interim meeting. Takes a list of one or more meetings."""
group = meetings[0].session_set.first().group
requester = meetings[0].session_set.first().requested_by
first_session = meetings[0].session_set.first()
group = first_session.group
requester = session_requested_by(first_session)
(to_email, cc_list) = gather_address_lists('session_requested',group=group,person=requester)
from_email = (settings.SESSION_REQUEST_FROM_EMAIL)
subject = '{group} - New Interim Meeting Request'.format(group=group.acronym)
@ -524,8 +528,9 @@ def send_interim_approval_request(meetings):
def send_interim_announcement_request(meeting):
"""Sends an email to the secretariat that an interim meeting is ready for
announcement, includes the link to send the official announcement"""
group = meeting.session_set.first().group
requester = meeting.session_set.first().requested_by
first_session = meeting.session_set.first()
group = first_session.group
requester = session_requested_by(first_session)
(to_email, cc_list) = gather_address_lists('interim_approved')
from_email = (settings.SESSION_REQUEST_FROM_EMAIL)
subject = '{group} - interim meeting ready for announcement'.format(group=group.acronym)
@ -553,10 +558,8 @@ 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
if meeting.session_set.filter(status='sched').count() > 1:
is_multi_day = True
else:
is_multi_day = False
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'
context = locals()
send_mail(None,
@ -590,12 +593,24 @@ def send_interim_minutes_reminder(meeting):
cc=cc_list)
def sessions_post_save(forms):
def sessions_post_save(request, forms):
"""Helper function to perform various post save operations on each form of a
InterimSessionModelForm formset"""
for form in forms:
if not form.has_changed():
continue
if form.instance.pk is not None and not SchedulingEvent.objects.filter(session=form.instance).exists():
if form.is_approved_or_virtual:
status_id = 'scheda'
else:
status_id = 'apprw'
SchedulingEvent.objects.create(
session=form.instance,
status_id=status_id,
by=request.user.person,
)
if ('date' in form.changed_data) or ('time' in form.changed_data):
update_interim_session_assignment(form)
if 'agenda' in form.changed_data:

View file

@ -0,0 +1,31 @@
# Copyright The IETF Trust 2019, All Rights Reserved
# -*- coding: utf-8 -*-
# Generated by Django 1.11.26 on 2019-11-19 02:41
from __future__ import unicode_literals
import datetime
from django.db import migrations, models
import django.db.models.deletion
import ietf.utils.models
class Migration(migrations.Migration):
dependencies = [
('person', '0009_auto_20190118_0725'),
('name', '0007_fix_m2m_slug_id_length'),
('meeting', '0021_rename_meeting_agenda_to_schedule'),
]
operations = [
migrations.CreateModel(
name='SchedulingEvent',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('time', models.DateTimeField(default=datetime.datetime.now, help_text='When the event happened')),
('by', ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='person.Person')),
('session', ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='meeting.Session')),
('status', ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='name.SessionStatusName')),
],
),
]

View file

@ -0,0 +1,67 @@
# Copyright The IETF Trust 2019, All Rights Reserved
# -*- coding: utf-8 -*-
# Generated by Django 1.11.26 on 2019-11-19 02:42
from __future__ import unicode_literals
from django.db import migrations
import datetime
def create_scheduling_events(apps, schema_editor):
Session = apps.get_model('meeting', 'Session')
SchedulingEvent = apps.get_model('meeting', 'SchedulingEvent')
Person = apps.get_model('person', 'Person')
SessionStatusName = apps.get_model('name', 'SessionStatusName')
system_person = Person.objects.get(name='(System)')
session_status_names = { n.slug: n for n in SessionStatusName.objects.all() }
epoch_time = datetime.datetime(1970, 1, 1, 0, 0, 0)
for s in Session.objects.select_related('requested_by').filter(schedulingevent=None).iterator():
# temporarily fix up weird timestamps for the migration
if s.requested == epoch_time:
s.requested = s.modified
requested_event = SchedulingEvent()
requested_event.session = s
requested_event.time = s.requested
requested_event.by = s.requested_by
requested_event.status = session_status_names[s.status_id if s.status_id == 'apprw' or (s.status_id == 'notmeet' and not s.scheduled) else 'schedw']
requested_event.save()
scheduled_event = None
if s.status_id != requested_event.status_id:
if s.scheduled or s.status_id in ['sched', 'scheda']:
scheduled_event = SchedulingEvent()
scheduled_event.session = s
if s.scheduled:
scheduled_event.time = s.scheduled
else:
# we don't know when this happened
scheduled_event.time = s.modified
scheduled_event.by = system_person # we don't know who did it
scheduled_event.status = session_status_names[s.status_id if s.status_id == 'scheda' else 'sched']
scheduled_event.save()
final_event = None
if s.status_id not in ['apprw', 'schedw', 'notmeet', 'sched', 'scheda']:
final_event = SchedulingEvent()
final_event.session = s
final_event.time = s.modified
final_event.by = system_person # we don't know who did it
final_event.status = session_status_names[s.status_id]
final_event.save()
def noop(apps, schema_editor):
pass
class Migration(migrations.Migration):
dependencies = [
('meeting', '0022_schedulingevent'),
]
operations = [
migrations.RunPython(create_scheduling_events, noop),
]

View file

@ -0,0 +1,28 @@
# Copyright The IETF Trust 2019, All Rights Reserved
# -*- coding: utf-8 -*-
# Generated by Django 1.11.26 on 2019-12-04 17:31
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('meeting', '0023_create_scheduling_events'),
]
operations = [
migrations.RemoveField(
model_name='session',
name='requested',
),
migrations.RemoveField(
model_name='session',
name='requested_by',
),
migrations.RemoveField(
model_name='session',
name='status',
),
]

View file

@ -200,13 +200,6 @@ class Meeting(models.Model):
else:
return None
@property
def sessions_that_can_meet(self):
qs = self.session_set.exclude(status__slug='notmeet').exclude(status__slug='disappr').exclude(status__slug='deleted').exclude(status__slug='apprw')
# Restrict graphical scheduling to meeting requests (Sessions) of type 'session' for now
qs = qs.filter(type__slug='session')
return qs
def json_url(self):
return "/meeting/%s/json" % (self.number, )
@ -705,12 +698,6 @@ class Schedule(models.Model):
def qs_assignments_with_sessions(self):
return self.assignments.filter(session__isnull=False)
@property
def sessions_that_can_meet(self):
if not hasattr(self, "_cached_sessions_that_can_meet"):
self._cached_sessions_that_can_meet = self.meeting.sessions_that_can_meet.all()
return self._cached_sessions_that_can_meet
def delete_schedule(self):
self.assignments.all().delete()
self.delete()
@ -900,11 +887,8 @@ class Session(models.Model):
group = ForeignKey(Group) # The group type historically determined the session type. BOFs also need to be added as a group. Note that not all meeting requests have a natural group to associate with.
attendees = models.IntegerField(null=True, blank=True)
agenda_note = models.CharField(blank=True, max_length=255)
requested = models.DateTimeField(default=datetime.datetime.now)
requested_by = ForeignKey(Person)
requested_duration = models.DurationField(default=datetime.timedelta(0))
comments = models.TextField(blank=True)
status = ForeignKey(SessionStatusName)
scheduled = models.DateTimeField(null=True, blank=True)
modified = models.DateTimeField(auto_now=True)
remote_instructions = models.CharField(blank=True,max_length=1024)
@ -914,9 +898,6 @@ class Session(models.Model):
unique_constraints_dict = None
def not_meeting(self):
return self.status_id == 'notmeet'
# 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):
@ -960,17 +941,17 @@ class Session(models.Model):
return list(self.materials.filter(type='draft'))
def all_meeting_sessions_for_group(self):
from ietf.meeting.utils import add_event_info_to_session_qs
if self.group.type_id in ['wg','rg','ag']:
if not hasattr(self, "_all_meeting_sessions_for_group_cache"):
sessions = [s for s in self.meeting.session_set.filter(group=self.group,type=self.type) if s.official_timeslotassignment()]
sessions = [s for s in add_event_info_to_session_qs(self.meeting.session_set.filter(group=self.group,type=self.type)) if s.official_timeslotassignment()]
self._all_meeting_sessions_for_group_cache = sorted(sessions, key = lambda x: x.official_timeslotassignment().timeslot.time)
return self._all_meeting_sessions_for_group_cache
else:
return [self]
def all_meeting_sessions_cancelled(self):
states = set([s.status_id for s in self.all_meeting_sessions_for_group()])
return 'canceled' in states and len(states) == 1
return set(s.current_status for s in self.all_meeting_sessions_for_group()) == {'canceled'}
def all_meeting_recordings(self):
recordings = [] # These are not sets because we need to preserve relative ordering or redo the ordering work later
@ -1028,13 +1009,21 @@ class Session(models.Model):
if self.meeting.type_id == "interim":
return self.meeting.number
if self.status.slug in ('canceled','disappr','notmeet','deleted'):
ss0name = "(%s)" % self.status.name
status_id = None
if hasattr(self, 'current_status'):
status_id = self.current_status
elif self.pk is not None:
latest_event = SchedulingEvent.objects.filter(session=self.pk).order_by('-time', '-id').first()
if latest_event:
status_id = latest_event.status_id
if status_id in ('canceled','disappr','notmeet','deleted'):
ss0name = "(%s)" % SessionStatusName.objects.get(slug=status_id).name
else:
ss0name = "(unscheduled)"
ss = self.timeslotassignments.filter(schedule=self.meeting.schedule).order_by('timeslot__time')
if ss:
ss0name = ','.join([x.timeslot.time.strftime("%a-%H%M") for x in ss])
ss0name = ','.join(x.timeslot.time.strftime("%a-%H%M") for x in ss)
return "%s: %s %s %s" % (self.meeting, self.group.acronym, self.name, ss0name)
@property
@ -1110,16 +1099,47 @@ class Session(models.Model):
sess1['bof'] = str(self.group.is_bof())
sess1['agenda_note'] = self.agenda_note
sess1['attendees'] = str(self.attendees)
sess1['status'] = self.status.name
# fish out scheduling information - eventually, we should pick
# this out in the caller instead
latest_event = None
first_event = None
if self.pk is not None:
if not hasattr(self, 'current_status') or not hasattr(self, 'requested_time'):
events = list(SchedulingEvent.objects.filter(session=self.pk).order_by('time', 'id'))
if events:
first_event = events[0]
latest_event = events[-1]
status_id = None
if hasattr(self, 'current_status'):
status_id = self.current_status
elif latest_event:
status_id = latest_event.status_id
sess1['status'] = SessionStatusName.objects.get(slug=status_id).name if status_id else None
if self.comments is not None:
sess1['comments'] = self.comments
sess1['requested_time'] = self.requested.strftime("%Y-%m-%d")
# the related person object sometimes does not exist in the dataset.
try:
if self.requested_by is not None:
sess1['requested_by'] = str(self.requested_by)
except Person.DoesNotExist:
pass
requested_time = None
if hasattr(self, 'requested_time'):
requested_time = self.requested_time
elif first_event:
requested_time = first_event.time
sess1['requested_time'] = requested_time.strftime("%Y-%m-%d") if requested_time else None
requested_by = None
if hasattr(self, 'requested_by'):
requested_by = self.requested_by
elif first_event:
requested_by = first_event.by_id
if requested_by is not None:
requested_by_person = Person.objects.filter(pk=requested_by).first()
if requested_by_person:
sess1['requested_by'] = str(requested_by_person)
sess1['requested_duration']= "%.1f" % (float(self.requested_duration.seconds) / 3600)
sess1['special_request'] = str(self.special_request_token)
@ -1137,12 +1157,6 @@ class Session(models.Model):
else:
return "The agenda has not been uploaded yet."
def ical_status(self):
if self.status.slug == 'canceled': # sic
return "CANCELLED"
else:
return "CONFIRMED"
def agenda_file(self):
if not hasattr(self, '_agenda_file'):
self._agenda_file = ""
@ -1164,6 +1178,16 @@ class Session(models.Model):
else:
return self.group.acronym
@python_2_unicode_compatible
class SchedulingEvent(models.Model):
session = ForeignKey(Session)
time = models.DateTimeField(default=datetime.datetime.now, help_text="When the event happened")
status = ForeignKey(SessionStatusName)
by = ForeignKey(Person)
def __str__(self):
return u'%s : %s : %s : %s' % (self.session, self.status, self.time, self.by)
@python_2_unicode_compatible
class ImportantDate(models.Model):
meeting = ForeignKey(Meeting)

View file

@ -13,7 +13,7 @@ from ietf import api
from ietf.meeting.models import ( Meeting, ResourceAssociation, Constraint, Room, Schedule, Session,
TimeSlot, SchedTimeSessAssignment, SessionPresentation, FloorPlan,
UrlResource, ImportantDate, SlideSubmission )
UrlResource, ImportantDate, SlideSubmission, SchedulingEvent )
from ietf.name.resources import MeetingTypeNameResource
class MeetingResource(ModelResource):
@ -163,14 +163,12 @@ api.meeting.register(ScheduleResource())
from ietf.group.resources import GroupResource
from ietf.doc.resources import DocumentResource
from ietf.name.resources import TimeSlotTypeNameResource, SessionStatusNameResource
from ietf.name.resources import TimeSlotTypeNameResource
from ietf.person.resources import PersonResource
class SessionResource(ModelResource):
meeting = ToOneField(MeetingResource, 'meeting')
type = ToOneField(TimeSlotTypeNameResource, 'type')
group = ToOneField(GroupResource, 'group')
requested_by = ToOneField(PersonResource, 'requested_by')
status = ToOneField(SessionStatusNameResource, 'status')
materials = ToManyField(DocumentResource, 'materials', null=True)
resources = ToManyField(ResourceAssociationResource, 'resources', null=True)
assignments = ToManyField('ietf.meeting.resources.SchedTimeSessAssignmentResource', 'timeslotassignments', null=True)
@ -203,6 +201,26 @@ class SessionResource(ModelResource):
}
api.meeting.register(SessionResource())
from ietf.name.resources import SessionStatusNameResource
class SchedulingEventResource(ModelResource):
session = ToOneField(SessionResource, 'session')
status = ToOneField(SessionStatusNameResource, 'status')
by = ToOneField(PersonResource, 'location')
class Meta:
cache = SimpleCache()
queryset = SchedulingEvent.objects.all()
serializer = api.Serializer()
ordering = ['id', 'time', 'modified', 'meeting',]
filtering = {
"id": ALL,
"time": ALL,
"session": ALL_WITH_RELATIONS,
"by": ALL_WITH_RELATIONS,
}
api.meeting.register(SchedulingEventResource())
from ietf.name.resources import TimeSlotTypeNameResource
class TimeSlotResource(ModelResource):
meeting = ToOneField(MeetingResource, 'meeting')

View file

@ -14,7 +14,7 @@ import debug # pyflakes:ignore
from ietf.doc.factories import DocumentFactory
from ietf.group.models import Group
from ietf.meeting.models import (Meeting, Room, TimeSlot, Session, Schedule, SchedTimeSessAssignment,
ResourceAssociation, SessionPresentation, UrlResource)
ResourceAssociation, SessionPresentation, UrlResource, SchedulingEvent)
from ietf.meeting.helpers import create_interim_meeting
from ietf.name.models import RoomResourceName
from ietf.person.models import Person
@ -25,10 +25,11 @@ def make_interim_meeting(group,date,status='sched'):
time = datetime.datetime.combine(date, datetime.time(9))
meeting = create_interim_meeting(group=group,date=date)
session = Session.objects.create(meeting=meeting, group=group,
attendees=10, requested_by=system_person, status_id=status,
attendees=10,
requested_duration=datetime.timedelta(minutes=20),
remote_instructions='http://webex.com',
scheduled=datetime.datetime.now(),type_id="session")
type_id="session")
SchedulingEvent.objects.create(session=session, status_id=status, by=system_person)
slot = TimeSlot.objects.create(
meeting=meeting,
type_id="session",
@ -117,43 +118,44 @@ def make_meeting_test_data(meeting=None):
# mars WG
mars = Group.objects.get(acronym='mars')
mars_session = Session.objects.create(meeting=meeting, group=mars,
attendees=10, requested_by=system_person, status_id="schedw",
requested_duration=datetime.timedelta(minutes=20),
scheduled=datetime.datetime.now(),type_id="session")
attendees=10, requested_duration=datetime.timedelta(minutes=20),
type_id="session")
SchedulingEvent.objects.create(session=mars_session, status_id='schedw', by=system_person)
SchedTimeSessAssignment.objects.create(timeslot=slot1, session=mars_session, schedule=schedule)
SchedTimeSessAssignment.objects.create(timeslot=slot2, session=mars_session, schedule=unofficial_schedule)
# ames WG
ames_session = Session.objects.create(meeting=meeting, group=Group.objects.get(acronym="ames"),
attendees=10, requested_by=system_person, status_id="schedw",
attendees=10,
requested_duration=datetime.timedelta(minutes=20),
scheduled=datetime.datetime.now(),type_id="session")
type_id="session")
SchedulingEvent.objects.create(session=ames_session, status_id='schedw', by=system_person)
SchedTimeSessAssignment.objects.create(timeslot=slot2, session=ames_session, schedule=schedule)
SchedTimeSessAssignment.objects.create(timeslot=slot1, session=ames_session, schedule=unofficial_schedule)
# IESG breakfast
iesg_session = Session.objects.create(meeting=meeting, group=Group.objects.get(acronym="iesg"),
name="IESG Breakfast", attendees=25,
requested_by=system_person, status_id="schedw",
requested_duration=datetime.timedelta(minutes=20),
scheduled=datetime.datetime.now(),type_id="lead")
type_id="lead")
SchedulingEvent.objects.create(session=iesg_session, status_id='schedw', by=system_person)
SchedTimeSessAssignment.objects.create(timeslot=breakfast_slot, session=iesg_session, schedule=schedule)
# No breakfast on unofficial schedule
# Registration
reg_session = Session.objects.create(meeting=meeting, group=Group.objects.get(acronym="secretariat"),
name="Registration", attendees=250,
requested_by=system_person, status_id="schedw",
requested_duration=datetime.timedelta(minutes=480),
scheduled=datetime.datetime.now(),type_id="reg")
type_id="reg")
SchedulingEvent.objects.create(session=reg_session, status_id='schedw', by=system_person)
SchedTimeSessAssignment.objects.create(timeslot=reg_slot, session=reg_session, schedule=schedule)
# Break
break_session = Session.objects.create(meeting=meeting, group=Group.objects.get(acronym="secretariat"),
name="Morning Break", attendees=250,
requested_by=system_person, status_id="schedw",
requested_duration=datetime.timedelta(minutes=30),
scheduled=datetime.datetime.now(),type_id="break")
type_id="break")
SchedulingEvent.objects.create(session=break_session, status_id='schedw', by=system_person)
SchedTimeSessAssignment.objects.create(timeslot=break_slot, session=break_session, schedule=schedule)
meeting.schedule = schedule

View file

@ -28,13 +28,16 @@ import debug # pyflakes:ignore
from ietf.doc.models import Document
from ietf.group.models import Group, Role
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_minutes_reminder, populate_important_dates, update_important_dates
from ietf.meeting.models import Session, TimeSlot, Meeting, SchedTimeSessAssignment, Schedule, SessionPresentation, SlideSubmission
from ietf.meeting.models import Session, TimeSlot, Meeting, SchedTimeSessAssignment, Schedule, SessionPresentation, SlideSubmission, SchedulingEvent
from ietf.meeting.test_data import make_meeting_test_data, make_interim_meeting
from ietf.meeting.utils import finalize
from ietf.meeting.utils import add_event_info_to_session_qs
from ietf.meeting.utils import current_session_status
from ietf.name.models import SessionStatusName, ImportantDateName
from ietf.utils.decorators import skip_coverage
from ietf.utils.mail import outbox, empty_outbox
@ -195,8 +198,11 @@ class MeetingTests(TestCase):
self.assertContains(r, slot.location.name)
# week view with a cancelled session
session.status_id='canceled'
session.save()
SchedulingEvent.objects.create(
session=session,
status=SessionStatusName.objects.get(slug='canceled'),
by=Person.objects.get(name='(System)')
)
r = self.client.get(urlreverse("ietf.meeting.views.week_view", kwargs=dict(num=meeting.number)))
self.assertContains(r, 'CANCELLED')
self.assertContains(r, session.group.acronym)
@ -874,8 +880,11 @@ class InterimTests(TestCase):
url = urlreverse("ietf.meeting.views.interim_announce")
meeting = Meeting.objects.filter(type='interim', session__group__acronym='mars').first()
session = meeting.session_set.first()
session.status = SessionStatusName.objects.get(slug='scheda')
session.save()
SchedulingEvent.objects.create(
session=session,
status=SessionStatusName.objects.get(slug='scheda'),
by=Person.objects.get(name='(System)')
)
login_testing_unauthorized(self, "secretary", url)
r = self.client.get(url)
self.assertContains(r, meeting.number)
@ -894,12 +903,12 @@ class InterimTests(TestCase):
len_before = len(outbox)
r = self.client.post(url)
self.assertRedirects(r, urlreverse('ietf.meeting.views.interim_announce'))
self.assertEqual(meeting.session_set.first().status.slug,'sched')
self.assertEqual(add_event_info_to_session_qs(meeting.session_set).first().current_status, 'sched')
self.assertEqual(len(outbox), len_before)
def test_interim_send_announcement(self):
make_meeting_test_data()
meeting = Meeting.objects.filter(type='interim', session__status='apprw', session__group__acronym='mars').first()
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_send_announcement", kwargs={'number': meeting.number})
login_testing_unauthorized(self, "secretary", url)
r = self.client.get(url)
@ -914,26 +923,26 @@ class InterimTests(TestCase):
def test_interim_approve_by_ad(self):
make_meeting_test_data()
meeting = Meeting.objects.filter(type='interim', session__status='apprw', session__group__acronym='mars').first()
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})
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 meeting.session_set.all():
self.assertEqual(session.status.slug, 'scheda')
for session in add_event_info_to_session_qs(meeting.session_set.all()):
self.assertEqual(session.current_status, 'scheda')
self.assertEqual(len(outbox), length_before + 1)
self.assertIn('ready for announcement', outbox[-1]['Subject'])
def test_interim_approve_by_secretariat(self):
make_meeting_test_data()
meeting = Meeting.objects.filter(type='interim', session__status='apprw', session__group__acronym='mars').first()
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})
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 meeting.session_set.all():
self.assertEqual(session.status.slug, 'scheda')
for session in add_event_info_to_session_qs(meeting.session_set.all()):
self.assertEqual(session.current_status, 'scheda')
def test_past(self):
today = datetime.date.today()
@ -951,8 +960,9 @@ class InterimTests(TestCase):
make_meeting_test_data()
url = urlreverse("ietf.meeting.views.upcoming")
today = datetime.date.today()
mars_interim = Meeting.objects.filter(date__gt=today, type='interim', session__group__acronym='mars', session__status='sched').first()
ames_interim = Meeting.objects.filter(date__gt=today, type='interim', session__group__acronym='ames', session__status='canceled').first()
add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='apprw').first()
mars_interim = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', meeting__date__gt=today, group__acronym='mars')).filter(current_status='sched').first().meeting
ames_interim = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', meeting__date__gt=today, group__acronym='ames')).filter(current_status='canceled').first().meeting
r = self.client.get(url)
self.assertContains(r, mars_interim.number)
self.assertContains(r, ames_interim.number)
@ -1078,7 +1088,7 @@ class InterimTests(TestCase):
session = meeting.session_set.first()
self.assertEqual(session.remote_instructions,remote_instructions)
self.assertEqual(session.agenda_note,agenda_note)
self.assertEqual(session.status.slug,'scheda')
self.assertEqual(current_session_status(session).slug,'scheda')
timeslot = session.official_timeslotassignment().timeslot
self.assertEqual(timeslot.time,dt)
self.assertEqual(timeslot.duration,duration)
@ -1322,7 +1332,7 @@ class InterimTests(TestCase):
def test_interim_pending(self):
make_meeting_test_data()
url = urlreverse('ietf.meeting.views.interim_pending')
count = Meeting.objects.filter(type='interim',session__status='apprw').distinct().count()
count = len(set(s.meeting_id for s in add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim')).filter(current_status='apprw')))
# unpriviledged user
login_testing_unauthorized(self,"plain",url)
@ -1343,7 +1353,7 @@ class InterimTests(TestCase):
# unprivileged user
user = User.objects.get(username='plain')
group = Group.objects.get(acronym='mars')
meeting = Meeting.objects.filter(type='interim',session__status='apprw',session__group=group).first()
meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group=group)).filter(current_status='apprw').first().meeting
self.assertFalse(can_approve_interim_request(meeting=meeting,user=user))
# Secretariat
user = User.objects.get(username='secretary')
@ -1363,7 +1373,7 @@ class InterimTests(TestCase):
# unprivileged user
user = User.objects.get(username='plain')
group = Group.objects.get(acronym='mars')
meeting = Meeting.objects.filter(type='interim',session__status='apprw',session__group=group).first()
meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group=group)).filter(current_status='apprw').first().meeting
self.assertFalse(can_view_interim_request(meeting=meeting,user=user))
# Secretariat
user = User.objects.get(username='secretary')
@ -1383,7 +1393,7 @@ class InterimTests(TestCase):
def test_interim_request_details(self):
make_meeting_test_data()
meeting = Meeting.objects.filter(type='interim',session__status='apprw',session__group__acronym='mars').first()
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})
login_testing_unauthorized(self,"secretary",url)
r = self.client.get(url)
@ -1413,17 +1423,17 @@ class InterimTests(TestCase):
def test_interim_request_disapprove(self):
make_meeting_test_data()
meeting = Meeting.objects.filter(type='interim',session__status='apprw',session__group__acronym='mars').first()
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})
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 meeting.session_set.all():
self.assertEqual(session.status_id,'disappr')
for session in add_event_info_to_session_qs(meeting.session_set.all()):
self.assertEqual(session.current_status,'disappr')
def test_interim_request_cancel(self):
make_meeting_test_data()
meeting = Meeting.objects.filter(type='interim', session__status='apprw', session__group__acronym='mars').first()
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})
# ensure no cancel button for unauthorized user
self.client.login(username="ameschairman", password="ameschairman+password")
@ -1448,17 +1458,17 @@ class InterimTests(TestCase):
length_before = len(outbox)
r = self.client.post(url, {'comments': comments})
self.assertRedirects(r, urlreverse('ietf.meeting.views.upcoming'))
for session in meeting.session_set.all():
self.assertEqual(session.status_id, 'canceledpa')
for session in add_event_info_to_session_qs(meeting.session_set.all()):
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 = Meeting.objects.filter(type='interim', session__status='sched', session__group__acronym='mars').first()
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 meeting.session_set.all():
self.assertEqual(session.status_id, 'canceled')
for session in add_event_info_to_session_qs(meeting.session_set.all()):
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'])
@ -1466,7 +1476,7 @@ class InterimTests(TestCase):
def test_interim_request_edit_no_notice(self):
'''Edit a request. No notice should go out if it hasn't been announced yet'''
make_meeting_test_data()
meeting = Meeting.objects.filter(type='interim', session__status='apprw', session__group__acronym='mars').first()
meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='apprw').first().meeting
group = meeting.session_set.first().group
url = urlreverse('ietf.meeting.views.interim_request_edit', kwargs={'number': meeting.number})
# test unauthorized access
@ -1504,7 +1514,7 @@ class InterimTests(TestCase):
def test_interim_request_edit(self):
'''Edit request. Send notice of change'''
make_meeting_test_data()
meeting = Meeting.objects.filter(type='interim', session__status='sched', session__group__acronym='mars').first()
meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='sched').first().meeting
group = meeting.session_set.first().group
url = urlreverse('ietf.meeting.views.interim_request_edit', kwargs={'number': meeting.number})
# test unauthorized access
@ -1550,7 +1560,7 @@ class InterimTests(TestCase):
def test_interim_request_details_permissions(self):
make_meeting_test_data()
meeting = Meeting.objects.filter(type='interim',session__status='apprw',session__group__acronym='mars').first()
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})
# unprivileged user
@ -1560,7 +1570,7 @@ class InterimTests(TestCase):
def test_send_interim_approval_request(self):
make_meeting_test_data()
meeting = Meeting.objects.filter(type='interim',session__status='apprw',session__group__acronym='mars').first()
meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='apprw').first().meeting
length_before = len(outbox)
send_interim_approval_request(meetings=[meeting])
self.assertEqual(len(outbox),length_before+1)
@ -1568,7 +1578,7 @@ class InterimTests(TestCase):
def test_send_interim_cancellation_notice(self):
make_meeting_test_data()
meeting = Meeting.objects.filter(type='interim',session__status='sched',session__group__acronym='mars').first()
meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='sched').first().meeting
length_before = len(outbox)
send_interim_cancellation_notice(meeting=meeting)
self.assertEqual(len(outbox),length_before+1)

View file

@ -11,28 +11,49 @@ import six.moves.urllib.request
from six.moves.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
import debug # pyflakes:ignore
from ietf.dbtemplate.models import DBTemplate
from ietf.meeting.models import Session, Meeting
from ietf.meeting.models import Session, Meeting, SchedulingEvent, TimeSlot
from ietf.group.models import Group
from ietf.group.utils import can_manage_materials
from ietf.person.models import Email
from ietf.secr.proceedings.proc_utils import import_audio_files
def session_time_for_sorting(session, use_meeting_date):
official_timeslot = TimeSlot.objects.filter(sessionassignments__session=session, sessionassignments__schedule=session.meeting.schedule).first()
if official_timeslot:
return official_timeslot.time
elif use_meeting_date and session.meeting.date:
return datetime.datetime.combine(session.meeting.date, datetime.time.min)
else:
first_event = SchedulingEvent.objects.filter(session=session).order_by('time', 'id').first()
if first_event:
return first_event.time
else:
return datetime.datetime.min
def session_requested_by(session):
first_event = SchedulingEvent.objects.filter(session=session).order_by('time', 'id').first()
if first_event:
return first_event.by
return None
def current_session_status(session):
last_event = SchedulingEvent.objects.filter(session=session).order_by('-time', '-id').first()
if last_event:
return last_event.status
return None
def group_sessions(sessions):
def sort_key(session):
official_sessions = session.timeslotassignments.filter(schedule=session.meeting.schedule)
if official_sessions:
return official_sessions.first().timeslot.time
elif session.meeting.date:
return datetime.datetime.combine(session.meeting.date,datetime.datetime.min.time())
else:
return session.requested
for s in sessions:
s.time=sort_key(s)
s.time = session_time_for_sorting(s, use_meeting_date=True)
sessions = sorted(sessions,key=lambda s:s.time)
@ -65,29 +86,24 @@ def get_upcoming_manageable_sessions(user):
# This notion of searching by end-of-meeting is also present in Document.future_presentations.
# It would be nice to make it easier to use querysets to talk about meeting endings wthout a heuristic like this one
candidate_sessions = Session.objects.exclude(status__in=['canceled','disappr','notmeet','deleted']).filter(meeting__date__gte=datetime.date.today()-datetime.timedelta(days=15))
refined_candidates = [ sess for sess in candidate_sessions if sess.meeting.end_date()>=datetime.date.today()]
# We can in fact do that with something like
# .filter(date__gte=today - F('days')), but unfortunately, it
# doesn't work correctly with Django 1.11 and MySQL/SQLite
return [ sess for sess in refined_candidates if can_manage_materials(user, sess.group) ]
today = datetime.date.today()
candidate_sessions = add_event_info_to_session_qs(
Session.objects.filter(meeting__date__gte=today - datetime.timedelta(days=15))
).exclude(
current_status__in=['canceled', 'disappr', 'notmeet', 'deleted']
).prefetch_related('meeting')
return [
sess for sess in candidate_sessions if sess.meeting.end_date() >= today and can_manage_materials(user, sess.group)
]
def sort_sessions(sessions):
# Python sorts are stable since version 2,2, so this series results in a list sorted first
# by the meeting 'number', then by session's group acronym, then by scheduled time
# (or the time of the session request if the session isn't scheduled).
def time_sort_key(session):
official_sessions = session.timeslotassignments.filter(schedule=session.meeting.schedule)
if official_sessions:
return official_sessions.first().timeslot.time
else:
return session.requested
time_sorted = sorted(sessions,key=time_sort_key)
acronym_sorted = sorted(time_sorted,key=lambda x: x.group.acronym)
meeting_sorted = sorted(acronym_sorted,key=lambda x: x.meeting.number)
return meeting_sorted
return sorted(sessions, key=lambda s: (s.meeting.number, s.group.acronym, session_time_for_sorting(s, use_meeting_date=False)))
def create_proceedings_templates(meeting):
'''Create DBTemplates for meeting proceedings'''
@ -178,3 +194,100 @@ def sort_accept_tuple(accept):
tup.append((keys[0], q))
return sorted(tup, key = lambda x: float(x[1]), reverse = True)
return tup
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
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()),
)
if requested_by:
qs = qs.annotate(
requested_by=Subquery(SchedulingEvent.objects.filter(session=OuterRef('pk')).order_by('time', 'id').values('by')[:1]),
)
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 'session' for now
qs = qs.filter(type__slug='session')
return qs
def data_for_meetings_overview(meetings, interim_status=None):
"""Return filtered meetings with sessions and group hierarchy (for the
interim menu)."""
# extract sessions
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')
meeting_dict = {m.pk: m for m in meetings}
for s in sessions.iterator():
meeting_dict[s.meeting_id].sessions.append(s)
# filter
if interim_status == 'apprw':
meetings = [
m for m in meetings
if not m.type_id == 'interim' or any(s.current_status == 'apprw' for s in m.sessions)
]
elif interim_status == 'scheda':
meetings = [
m for m in meetings
if not m.type_id == 'interim' or any(s.current_status == 'scheda' for s in m.sessions)
]
else:
meetings = [
m for m in meetings
if not m.type_id == 'interim' or not all(s.current_status in ['apprw', 'scheda', 'canceledpa'] for s in m.sessions)
]
# group hierarchy
ietf_group = Group.objects.get(acronym='ietf')
group_hierarchy = [ietf_group]
parents = {}
for m in meetings:
if m.type_id == 'interim' and m.sessions:
for s in m.sessions:
parent = parents.get(s.group.parent_id)
if not parent:
parent = s.group.parent
parent.group_list = []
group_hierarchy.append(parent)
parent.group_list.append(s.group)
for p in parents.values():
p.group_list.sort(key=lambda g: g.acronym)
# set some useful attributes
for m in meetings:
m.end = m.date + datetime.timedelta(days=m.days)
m.responsible_group = (m.sessions[0].group if m.sessions else None) if m.type_id == 'interim' else ietf_group
m.interim_meeting_cancelled = m.type_id == 'interim' and all(s.current_status == 'canceled' for s in m.sessions)
return meetings, group_hierarchy

View file

@ -8,6 +8,7 @@ import csv
import datetime
import glob
import io
import itertools
import json
import os
import pytz
@ -34,7 +35,7 @@ from django.contrib.auth.decorators import login_required
from django.core.exceptions import ValidationError
from django.core.validators import URLValidator
from django.urls import reverse,reverse_lazy
from django.db.models import Min, Max, Q
from django.db.models import Min, Max, Q, F
from django.forms.models import modelform_factory, inlineformset_factory
from django.template import TemplateDoesNotExist
from django.template.loader import render_to_string
@ -49,9 +50,10 @@ from ietf.doc.fields import SearchableDocumentsField
from ietf.doc.models import Document, State, DocEvent, NewRevisionDocEvent, DocAlias
from ietf.group.models import Group
from ietf.group.utils import can_manage_materials
from ietf.person.models import Person
from ietf.ietfauth.utils import role_required, has_role
from ietf.mailtrigger.utils import gather_address_lists
from ietf.meeting.models import Meeting, Session, Schedule, FloorPlan, SessionPresentation, TimeSlot, SlideSubmission
from ietf.meeting.models import Meeting, Session, Schedule, FloorPlan, SessionPresentation, TimeSlot, SlideSubmission, SessionStatusName, SchedulingEvent, SchedTimeSessAssignment
from ietf.meeting.helpers import get_areas, get_person_by_email, get_schedule_by_name
from ietf.meeting.helpers import build_all_agenda_slices, get_wg_name_list
from ietf.meeting.helpers import get_all_assignments_from_schedule
@ -63,12 +65,17 @@ from ietf.meeting.helpers import convert_draft_to_pdf, get_earliest_session_date
from ietf.meeting.helpers import can_view_interim_request, can_approve_interim_request
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_meeting_approved
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_approval_request
from ietf.meeting.helpers import send_interim_announcement_request
from ietf.meeting.utils import finalize
from ietf.meeting.utils import sort_accept_tuple
from ietf.meeting.utils import add_event_info_to_session_qs
from ietf.meeting.utils import session_time_for_sorting
from ietf.meeting.utils import session_requested_by
from ietf.meeting.utils import current_session_status
from ietf.meeting.utils import data_for_meetings_overview
from ietf.message.utils import infer_message
from ietf.secr.proceedings.utils import handle_upload_file
from ietf.secr.proceedings.proc_utils import (get_progress_stats, post_process, import_audio_files,
@ -85,7 +92,7 @@ from .forms import (InterimMeetingModelForm, InterimAnnounceForm, InterimSession
InterimCancelForm, InterimSessionInlineFormSet, FileUploadForm, RequestMinutesForm,)
def get_menu_entries(request):
def get_interim_menu_entries(request):
'''Setup menu entries for interim meeting view tabs'''
entries = []
if has_role(request.user, ('Area Director','Secretariat','IRTF Chair','WG Chair', 'RG Chair')):
@ -129,22 +136,21 @@ def materials(request, num=None):
past_cutoff_date = datetime.date.today() > meeting.get_submission_correction_date()
#sessions = Session.objects.filter(meeting__number=meeting.number, timeslot__isnull=False)
schedule = get_schedule(meeting, None)
sessions = ( Session.objects
.filter(meeting__number=meeting.number, timeslotassignments__schedule=schedule)
.select_related('meeting__schedule','status','group__state','group__parent', )
)
for session in sessions:
session.past_cutoff_date = past_cutoff_date
sessions = Session.objects.filter(
meeting__number=meeting.number,
timeslotassignments__schedule=schedule
).distinct().select_related('meeting__schedule', 'group__state', 'group__parent')
plenaries = sessions.filter(name__icontains='plenary')
ietf = sessions.filter(group__parent__type__slug = 'area').exclude(group__acronym='edu')
irtf = sessions.filter(group__parent__acronym = 'irtf')
training = sessions.filter(group__acronym__in=['edu','iaoc'], type_id__in=['session', 'other', ])
iab = sessions.filter(group__parent__acronym = 'iab')
other = sessions.filter(type_id__in=['session'], group__type__features__has_meetings = True)
for ss in [plenaries, ietf, irtf, training, iab]:
other = other.exclude(pk__in=[s.pk for s in ss])
session_pks = [s.pk for ss in [plenaries, ietf, irtf, training, iab] for s in ss]
other = sessions.filter(type_id__in=['session'], group__type__features__has_meetings=True).exclude(pk__in=session_pks)
for topic in [plenaries, ietf, training, irtf, iab]:
for event in topic:
@ -152,6 +158,10 @@ def materials(request, num=None):
for slide_event in event.all_meeting_slides(): date_list.append(slide_event.time)
for agenda_event in event.all_meeting_agendas(): date_list.append(agenda_event.time)
if date_list: setattr(event, 'last_update', sorted(date_list, reverse=True)[0])
for session_list in [plenaries, ietf, training, irtf, iab, other]:
for session in session_list:
session.past_cutoff_date = past_cutoff_date
return render(request, "meeting/materials.html", {
'meeting': meeting,
@ -819,7 +829,7 @@ def week_view(request, num=None, name=None, owner=None):
if a.session and a.session.agenda():
item["agenda"] = a.session.agenda().href()
if a.session.status_id=='canceled':
if a.session.current_status == 'canceled':
item["name"] = "CANCELLED - " + item["name"]
items.append(item)
@ -887,6 +897,12 @@ def room_view(request, num=None, name=None, owner=None):
template = "meeting/room-view.html"
return render(request, template,{"meeting":meeting,"schedule":schedule,"unavailable":unavailable,"assignments":assignments,"rooms":rooms,"days":days})
def ical_session_status(session_with_current_status):
if session_with_current_status == 'canceled':
return "CANCELLED"
else:
return "CONFIRMED"
def ical_agenda(request, num=None, name=None, acronym=None, session_id=None):
meeting = get_meeting(num, type_in=None)
schedule = get_schedule(meeting, name)
@ -933,6 +949,10 @@ def ical_agenda(request, num=None, name=None, acronym=None, session_id=None):
elif session_id:
assignments = [ a for a in assignments if a.session_id == int(session_id) ]
for a in assignments:
if a.session:
a.session.ical_status = ical_session_status(a.session)
return render(request, "meeting/agenda.ics", {
"schedule": schedule,
"assignments": assignments,
@ -1007,7 +1027,7 @@ def json_agenda(request, num=None ):
rev_docevent = doc.latest_event(NewRevisionDocEvent,'new_revision')
modified = max(modified, (rev_docevent and rev_docevent.time) or modified)
sessdict['modified'] = modified
sessdict['status'] = asgn.session.status_id
sessdict['status'] = asgn.session.current_status
sessions.append(sessdict)
rooms = []
@ -1060,7 +1080,27 @@ def json_agenda(request, num=None ):
def meeting_requests(request, num=None):
meeting = get_meeting(num)
sessions = Session.objects.filter(meeting__number=meeting.number, type__slug='session', group__parent__isnull = False).exclude(requested_by=0).order_by("group__parent__acronym","status__slug","group__acronym").prefetch_related("group","group__ad_role__person","requested_by")
sessions = add_event_info_to_session_qs(
Session.objects.filter(
meeting__number=meeting.number,
type__slug='session',
group__parent__isnull=False
),
requested_by=True,
).exclude(
requested_by=0
).order_by(
"group__parent__acronym", "current_status", "group__acronym"
).prefetch_related(
"group","group__ad_role__person"
)
status_names = {n.slug: n.name for n in SessionStatusName.objects.all()}
session_requesters = {p.pk: p for p in Person.objects.filter(pk__in=[s.requested_by for s in sessions if s.requested_by is not None])}
for s in sessions:
s.current_status_name = status_names.get(s.current_status, s.current_status)
s.requested_by_person = session_requesters.get(s.requested_by)
groups_not_meeting = Group.objects.filter(state='Active',type__in=['wg','rg','ag','bof']).exclude(acronym__in = [session.group.acronym for session in sessions]).order_by("parent__acronym","acronym").prefetch_related("parent")
@ -1075,36 +1115,32 @@ def get_sessions(num, acronym):
if not sessions:
sessions = Session.objects.filter(meeting=meeting,short=acronym,type__in=['session','plenary','other'])
def sort_key(session):
official_sessions = session.timeslotassignments.filter(schedule=session.meeting.schedule)
if official_sessions:
return official_sessions.first().timeslot.time
else:
return session.requested
sessions = add_event_info_to_session_qs(sessions)
return sorted(sessions,key=sort_key)
return sorted(sessions, key=lambda s: session_time_for_sorting(s, use_meeting_date=False))
def session_details(request, num, acronym ):
def session_details(request, num, acronym):
meeting = get_meeting(num=num,type_in=None)
sessions = get_sessions(num, acronym)
if not sessions:
raise Http404
status_names = {n.slug: n.name for n in SessionStatusName.objects.all()}
for session in sessions:
session.type_counter = Counter()
ss = session.timeslotassignments.filter(schedule=meeting.schedule).order_by('timeslot__time')
if ss:
session.time = ', '.join(x.timeslot.time.strftime("%A %b-%d-%Y %H%M") for x in ss)
if session.status.slug == 'canceled':
if session.current_status == 'canceled':
session.time += " CANCELLED"
elif session.meeting.type_id=='interim':
session.time = session.meeting.date.strftime("%A %b-%d-%Y")
if session.status.slug == 'canceled':
if session.current_status == 'canceled':
session.time += " CANCELLED"
else:
session.time = session.status.name
session.time = status_names.get(session.current_status, session.current_status)
session.filtered_artifacts = list(session.sessionpresentation_set.filter(document__type__slug__in=['agenda','minutes','bluesheets']))
session.filtered_artifacts.sort(key=lambda d:['agenda','minutes','bluesheets'].index(d.document.type.slug))
@ -1115,12 +1151,12 @@ def session_details(request, num, acronym ):
qs = [p for p in qs if p.document.get_state_slug(p.document.type_id)!='deleted']
session.type_counter.update([p.document.type.slug for p in qs])
# we somewhat arbitrarily use the group of the last session wet get from
# we somewhat arbitrarily use the group of the last session we get from
# get_sessions() above when checking can_manage_materials()
can_manage = can_manage_materials(request.user, session.group)
scheduled_sessions=[s for s in sessions if s.status_id=='sched']
unscheduled_sessions = [s for s in sessions if s.status_id!='sched']
scheduled_sessions = [s for s in sessions if s.current_status == 'sched']
unscheduled_sessions = [s for s in sessions if s.current_status != 'sched']
pending_suggestions = None
if request.user.is_authenticated:
@ -1790,8 +1826,8 @@ def ajax_get_utc(request):
@role_required('Secretariat',)
def interim_announce(request):
'''View which shows interim meeting requests awaiting announcement'''
meetings = Meeting.objects.filter(type='interim', session__status='scheda').distinct()
menu_entries = get_menu_entries(request)
meetings, _ = data_for_meetings_overview(Meeting.objects.filter(type='interim').order_by('date'), interim_status='scheda')
menu_entries = get_interim_menu_entries(request)
selected_menu_entry = 'announce'
return render(request, "meeting/interim_announce.html", {
@ -1812,7 +1848,12 @@ def interim_send_announcement(request, number):
if form.is_valid():
message = form.save(user=request.user)
message.related_groups.add(group)
meeting.session_set.update(status_id='sched')
for session in meeting.session_set.all():
SchedulingEvent.objects.create(
session=session,
status=SessionStatusName.objects.get(slug='sched'),
by=request.user.person,
)
send_mail_message(request, message)
messages.success(request, 'Interim meeting announcement sent')
return redirect(interim_announce)
@ -1832,7 +1873,12 @@ def interim_skip_announcement(request, number):
meeting = get_object_or_404(Meeting, number=number)
if request.method == 'POST':
meeting.session_set.update(status_id='sched')
for session in meeting.session_set.all():
SchedulingEvent.objects.create(
session=session,
status=SessionStatusName.objects.get(slug='sched'),
by=request.user.person,
)
messages.success(request, 'Interim meeting scheduled. No announcement sent.')
return redirect(interim_announce)
@ -1843,12 +1889,12 @@ def interim_skip_announcement(request, number):
@role_required('Area Director', 'Secretariat', 'IRTF Chair', 'WG Chair', 'RG Chair')
def interim_pending(request):
'''View which shows interim meeting requests pending approval'''
meetings = Meeting.objects.filter(type='interim', session__status='apprw').distinct().order_by('date')
menu_entries = get_menu_entries(request)
meetings, group_parents = data_for_meetings_overview(Meeting.objects.filter(type='interim').order_by('date'), interim_status='apprw')
menu_entries = get_interim_menu_entries(request)
selected_menu_entry = 'pending'
meetings = [m for m in meetings if can_view_interim_request(
m, request.user)]
meetings = [m for m in meetings if can_view_interim_request(m, request.user)]
for meeting in meetings:
if can_approve_interim_request(meeting, request.user):
meeting.can_approve = True
@ -1891,7 +1937,7 @@ def interim_request(request):
formset = SessionFormset(instance=meeting, data=request.POST)
formset.is_valid()
formset.save()
sessions_post_save(formset)
sessions_post_save(request, formset)
if not (is_approved or is_virtual):
send_interim_approval_request(meetings=[meeting])
@ -1922,7 +1968,7 @@ def interim_request(request):
session.meeting = meeting
session.save()
series.append(meeting)
sessions_post_save([session_form])
sessions_post_save(request, [session_form])
if not (is_approved or is_virtual):
send_interim_approval_request(meetings=series)
@ -1947,7 +1993,9 @@ def interim_request(request):
def interim_request_cancel(request, number):
'''View for cancelling an interim meeting request'''
meeting = get_object_or_404(Meeting, number=number)
group = meeting.session_set.first().group
first_session = meeting.session_set.first()
session_status = current_session_status(first_session)
group = first_session.group
if not can_view_interim_request(meeting, request.user):
return HttpResponseForbidden("You do not have permissions to cancel this meeting request")
@ -1956,11 +2004,20 @@ def interim_request_cancel(request, number):
if form.is_valid():
if 'comments' in form.changed_data:
meeting.session_set.update(agenda_note=form.cleaned_data.get('comments'))
if meeting.session_set.first().status.slug == 'sched':
meeting.session_set.update(status_id='canceled')
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():
SchedulingEvent.objects.create(
session=first_session,
status=result_status,
by=request.user.person,
)
if was_scheduled:
send_interim_cancellation_notice(meeting)
else:
meeting.session_set.update(status_id='canceledpa')
messages.success(request, 'Interim meeting cancelled')
return redirect(upcoming)
else:
@ -1968,7 +2025,9 @@ def interim_request_cancel(request, number):
return render(request, "meeting/interim_request_cancel.html", {
"form": form,
"meeting": meeting})
"meeting": meeting,
"session_status": session_status,
})
@role_required('Area Director', 'Secretariat', 'IRTF Chair', 'WG Chair', 'RG Chair')
@ -1981,7 +2040,12 @@ def interim_request_details(request, number):
if request.method == 'POST':
if request.POST.get('approve') and can_approve_interim_request(meeting, request.user):
meeting.session_set.update(status_id='scheda')
for session in meeting.session_set.all():
SchedulingEvent.objects.create(
session=session,
status=SessionStatusName.objects.get(slug='scheda'),
by=request.user.person,
)
messages.success(request, 'Interim meeting approved')
if has_role(request.user, 'Secretariat'):
return redirect(interim_send_announcement, number=number)
@ -1989,13 +2053,23 @@ 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):
meeting.session_set.update(status_id='disappr')
for session in meeting.session_set.all():
SchedulingEvent.objects.create(
session=session,
status=SessionStatusName.objects.get(slug='disappr'),
by=request.user.person,
)
messages.success(request, 'Interim meeting disapproved')
return redirect(interim_pending)
first_session = sessions.first()
return render(request, "meeting/interim_request_details.html", {
"meeting": meeting,
"sessions": sessions,
"group": first_session.group,
"requester": session_requested_by(first_session),
"session_status": current_session_status(first_session),
"can_edit": can_edit,
"can_approve": can_approve})
@ -2018,7 +2092,7 @@ def interim_request_edit(request, number):
form = InterimMeetingModelForm(request=request, instance=meeting,
data=request.POST)
group = Group.objects.get(pk=form.data['group'])
is_approved = is_meeting_approved(meeting)
is_approved = is_interim_meeting_approved(meeting)
SessionFormset.form.__init__ = curry(
InterimSessionModelForm.__init__,
@ -2031,10 +2105,11 @@ def interim_request_edit(request, number):
if form.is_valid() and formset.is_valid():
meeting = form.save(date=get_earliest_session_date(formset))
formset.save()
sessions_post_save(formset)
sessions_post_save(request, formset)
message = 'Interim meeting request saved'
if (form.has_changed() or formset.has_changed()) and meeting.session_set.filter(status='sched'):
meeting_is_scheduled = add_event_info_to_session_qs(meeting.session_set).filter(current_status='sched').exists()
if (form.has_changed() or formset.has_changed()) and meeting_is_scheduled:
send_interim_change_notice(request, meeting)
message = message + ' and change announcement sent'
messages.success(request, message)
@ -2053,32 +2128,8 @@ def interim_request_edit(request, number):
def past(request):
'''List of past meetings'''
today = datetime.datetime.today()
meetings = ( Meeting.objects.filter(date__lte=today)
.exclude(session__status__in=('apprw', 'scheda', 'canceledpa'))
.order_by('-date')
.select_related('type')
.prefetch_related('session_set__status','session_set__group',)
)
# extract groups hierarchy for display filter
seen = set()
groups = [m.session_set.first().group for m
in meetings.filter(type='interim')]
group_parents = [ Group.objects.get(acronym='ietf') ]
for g in groups:
if g.parent and g.parent.acronym not in seen:
group_parents.append(g.parent)
seen.add(g.parent.acronym)
seen = set()
for p in group_parents:
p.group_list = []
for g in groups:
if g.acronym not in seen and g.parent == p:
p.group_list.append(g)
seen.add(g.acronym)
p.group_list.sort(key=lambda g: g.acronym)
meetings, group_parents = data_for_meetings_overview(Meeting.objects.filter(date__lte=today).order_by('-date'))
return render(request, 'meeting/past.html', {
'meetings': meetings,
@ -2087,35 +2138,11 @@ def past(request):
def upcoming(request):
'''List of upcoming meetings'''
today = datetime.datetime.today()
meetings = Meeting.objects.filter(date__gte=today).exclude(
session__status__in=('apprw', 'scheda', 'canceledpa')).order_by('date')
# extract groups hierarchy for display filter
seen = set()
groups = [m.session_set.first().group for m
in meetings.filter(type='interim')]
group_parents = []
for g in groups:
if g.parent.acronym not in seen:
group_parents.append(g.parent)
seen.add(g.parent.acronym)
seen = set()
for p in group_parents:
p.group_list = []
for g in groups:
if g.acronym not in seen and g.parent == p:
p.group_list.append(g)
seen.add(g.acronym)
p.group_list.sort(key=lambda g: g.acronym)
meetings = list(meetings)
for m in meetings:
m.end = m.date+datetime.timedelta(days=m.days)
meetings, group_parents = data_for_meetings_overview(Meeting.objects.filter(date__gte=today).order_by('date'))
# add menu entries
menu_entries = get_menu_entries(request)
menu_entries = get_interim_menu_entries(request)
selected_menu_entry = 'upcoming'
# add menu actions
@ -2138,14 +2165,17 @@ def upcoming_ical(request):
'''Return Upcoming meetings in iCalendar file'''
filters = request.GET.getlist('filters')
today = datetime.datetime.today()
meetings = Meeting.objects.filter(date__gte=today).exclude(
session__status__in=('apprw', 'schedpa')).order_by('date')
assignments = []
for meeting in meetings:
items = meeting.schedule.assignments.order_by(
'session__type__slug', 'timeslot__time')
assignments.extend(items)
meetings, _ = data_for_meetings_overview(Meeting.objects.filter(date__gte=today).order_by('date'))
assignments = list(SchedTimeSessAssignment.objects.filter(
schedule__meeting__schedule=F('schedule'),
session__in=[s.pk for m in meetings for s in m.sessions]
).order_by(
'schedule__meeting__date', 'session__type', 'timeslot__time'
).select_related(
'session__group', 'session__group__parent', 'timeslot', 'schedule', 'schedule__meeting'
).distinct())
# apply filters
if filters:
@ -2155,6 +2185,14 @@ def upcoming_ical(request):
a.session.group.parent and a.session.group.parent.acronym in filters
)
) ]
# we already collected sessions with current_status, so reuse those
sessions = {s.pk: s for m in meetings for s in m.sessions}
for a in assignments:
if a.session_id is not None:
a.session = sessions.get(a.session_id) or a.session
a.session.ical_status = ical_session_status(a.session.current_status)
# gather vtimezones
vtimezones = set()
for meeting in meetings:
@ -2198,17 +2236,36 @@ def proceedings(request, num=None):
now = datetime.date.today()
schedule = get_schedule(meeting, None)
sessions = Session.objects.filter(meeting__number=meeting.number).filter(Q(timeslotassignments__schedule=schedule)|Q(status='notmeet')).select_related().order_by('-status_id')
plenaries = sessions.filter(name__icontains='plenary').exclude(status='notmeet')
sessions = add_event_info_to_session_qs(
Session.objects.filter(meeting__number=meeting.number)
).filter(
Q(timeslotassignments__schedule=schedule) | Q(current_status='notmeet')
).select_related().order_by('-current_status')
plenaries = sessions.filter(name__icontains='plenary').exclude(current_status='notmeet')
ietf = sessions.filter(group__parent__type__slug = 'area').exclude(group__acronym='edu')
irtf = sessions.filter(group__parent__acronym = 'irtf')
training = sessions.filter(group__acronym__in=['edu','iaoc'], type_id__in=['session', 'other', ]).exclude(status='notmeet')
iab = sessions.filter(group__parent__acronym = 'iab').exclude(status='notmeet')
training = sessions.filter(group__acronym__in=['edu','iaoc'], type_id__in=['session', 'other', ]).exclude(current_status='notmeet')
iab = sessions.filter(group__parent__acronym = 'iab').exclude(current_status='notmeet')
cache_version = Document.objects.filter(session__meeting__number=meeting.number).aggregate(Max('time'))["time__max"]
ietf_areas = []
for area, sessions in itertools.groupby(sorted(ietf, key=lambda s: (s.group.parent.acronym, s.group.acronym)), key=lambda s: s.group.parent):
sessions = list(sessions)
meeting_groups = set(s.group_id for s in sessions if s.current_status != 'notmeet')
meeting_sessions = []
not_meeting_sessions = []
for s in sessions:
if s.current_status == 'notmeet' and s.group_id not in meeting_groups:
not_meeting_sessions.append(s)
else:
meeting_sessions.append(s)
ietf_areas.append((area, meeting_sessions, not_meeting_sessions))
return render(request, "meeting/proceedings.html", {
'meeting': meeting,
'plenaries': plenaries, 'ietf': ietf, 'training': training, 'irtf': irtf, 'iab': iab,
'ietf_areas': ietf_areas,
'cut_off_date': cut_off_date,
'cor_cut_off_date': cor_cut_off_date,
'submission_started': now > begin_date,
@ -2424,12 +2481,19 @@ def request_minutes(request, num=None):
)
return HttpResponseRedirect(reverse('ietf.meeting.views.materials',kwargs={'num':num}))
else:
needs_minutes = set()
for a in meeting.schedule.assignments.filter(session__group__type_id__in=('wg','rg','ag')).exclude(session__status='canceled'):
if not a.session.all_meeting_minutes():
group = a.session.group
needs_minutes = set()
session_qs = add_event_info_to_session_qs(
Session.objects.filter(
timeslotassignments__schedule__meeting=meeting,
timeslotassignments__schedule__meeting__schedule=F('timeslotassignments__schedule'),
group__type__in=['wg','rg','ag'],
)
).filter(~Q(current_status='canceled')).select_related('group', 'group__parent')
for session in session_qs:
if not session.all_meeting_minutes():
group = session.group
if group.parent and group.parent.type_id in ('area','irtf'):
needs_minutes.add(a.session.group)
needs_minutes.add(group)
needs_minutes = list(needs_minutes)
needs_minutes.sort(key=lambda g: ('zzz' if g.parent.acronym == 'irtf' else g.parent.acronym)+":"+g.acronym)
body_context = {'meeting':meeting,

View file

@ -16,7 +16,7 @@ from django.conf import settings
from django.urls import reverse
from ietf.group.models import Group, GroupEvent
from ietf.meeting.models import Meeting, Room, TimeSlot, SchedTimeSessAssignment, Session
from ietf.meeting.models import Meeting, Room, TimeSlot, SchedTimeSessAssignment, Session, SchedulingEvent
from ietf.meeting.test_data import make_meeting_test_data
from ietf.name.models import SessionStatusName
from ietf.person.models import Person
@ -152,10 +152,9 @@ class SecrMeetingTestCase(TestCase):
mars_group = Group.objects.get(acronym='mars')
ames_group = Group.objects.get(acronym='ames')
ames_stsa = meeting.schedule.assignments.get(session__group=ames_group)
assert ames_stsa.session.status_id == 'schedw'
assert SchedulingEvent.objects.filter(session=ames_stsa.session).order_by('-id')[0].status_id == 'schedw'
mars_stsa = meeting.schedule.assignments.get(session__group=mars_group)
mars_stsa.session.status = SessionStatusName.objects.get(slug='appr')
mars_stsa.session.save()
SchedulingEvent.objects.create(session=mars_stsa.session, status=SessionStatusName.objects.get(slug='appr'), by=Person.objects.get(name="(System)"))
url = reverse('ietf.secr.meetings.views.notifications',kwargs={'meeting_id':72})
self.client.login(username="secretary", password="secretary+password")
response = self.client.get(url)
@ -185,9 +184,9 @@ class SecrMeetingTestCase(TestCase):
self.assertEqual(response.status_code, 302)
self.assertEqual(len(outbox), mailbox_before + 1)
ames_stsa = meeting.schedule.assignments.get(session__group=ames_group)
assert ames_stsa.session.status_id == 'sched'
self.assertEqual(SchedulingEvent.objects.filter(session=ames_stsa.session).order_by('-id')[0].status_id, 'sched')
mars_stsa = meeting.schedule.assignments.get(session__group=mars_group)
assert mars_stsa.session.status_id == 'sched'
self.assertEqual(SchedulingEvent.objects.filter(session=mars_stsa.session).order_by('-id')[0].status_id, 'sched')
def test_meetings_rooms(self):
meeting = make_meeting_test_data()
@ -363,7 +362,7 @@ class SecrMeetingTestCase(TestCase):
response = self.client.post(url, {'post':'yes'})
self.assertRedirects(response, redirect_url)
session = slot.sessionassignments.filter(schedule=meeting.schedule).first().session
self.assertEqual(session.status_id, 'canceled')
self.assertEqual(SchedulingEvent.objects.filter(session=session).order_by('-id')[0].status_id, 'canceled')
def test_meetings_session_edit(self):
meeting = make_meeting_test_data()

View file

@ -17,7 +17,9 @@ from ietf.ietfauth.utils import role_required
from ietf.utils.mail import send_mail
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
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.person.models import Person
@ -35,17 +37,6 @@ from ietf.mailtrigger.utils import gather_address_lists
# --------------------------------------------------
# Helper Functions
# --------------------------------------------------
def assign(session,timeslot,meeting,schedule=None):
'''
Robust function to assign a session to a timeslot. Much simplyfied 2014-03-26.
'''
if schedule == None:
schedule = meeting.schedule
SchedTimeSessAssignment.objects.create(schedule=schedule,
session=session,
timeslot=timeslot)
session.status_id = 'sched'
session.save()
def build_timeslots(meeting,room=None):
'''
@ -155,16 +146,21 @@ def send_notifications(meeting, groups, person):
items[i]['period'] = '%s-%s' % (t.time.strftime('%H%M'),(t.time + t.duration).strftime('%H%M'))
# send email
first_event = SchedulingEvent.objects.filter(session=sessions[0]).select_related('by').order_by('time', 'id').first()
requested_by = None
if first_event and first_event.status_id in ['appw', 'schedw']:
requested_by = first_event.by
context = {
'items': items,
'meeting': meeting,
'baseurl': settings.IDTRACKER_BASE_URL,
}
context['to_name'] = sessions[0].requested_by
context['to_name'] = str(requested_by) or "Requester"
context['agenda_note'] = sessions[0].agenda_note
context['session'] = get_initial_session(sessions)
context['group'] = group
context['login'] = sessions[0].requested_by
context['login'] = requested_by
send_mail(None,
addrs.to,
@ -411,16 +407,18 @@ def non_session(request, meeting_id, schedule_name):
group = Group.objects.get(acronym='secretariat')
# create associated Session object
session = Session(meeting=meeting,
name=name,
short=short,
group=group,
requested_by=Person.objects.get(name='(System)'),
status_id='sched',
type=type,
)
session.save()
session = Session.objects.create(meeting=meeting,
name=name,
short=short,
group=group,
type=type)
SchedulingEvent.objects.create(
session=session,
status=SessionStatusName.objects.get(slug='sched'),
by=request.user.person,
)
# create association
SchedTimeSessAssignment.objects.create(timeslot=timeslot,
session=session,
@ -434,6 +432,14 @@ def non_session(request, meeting_id, schedule_name):
if TimeSlot.objects.filter(meeting=meeting,type='other',location__isnull=True):
messages.warning(request, 'There are non-session items which do not have a room assigned')
session_statuses = {
e.session_id: e.status_id
for e in SchedulingEvent.objects.filter(session__in=[a.session_id for a in assignments]).order_by('time', 'id')
}
for a in assignments:
a.current_session_status = session_statuses.get(a.session_id)
return render(request, 'meetings/non_session.html', {
'assignments': assignments,
'form': form,
@ -453,8 +459,12 @@ def non_session_cancel(request, meeting_id, schedule_name, slot_id):
schedule = get_object_or_404(Schedule, meeting=meeting, name=schedule_name)
if request.method == 'POST' and request.POST['post'] == 'yes':
assignments = slot.sessionassignments.filter(schedule=schedule)
Session.objects.filter(pk__in=[x.session.pk for x in assignments]).update(status_id='canceled')
for session in Session.objects.filter(timeslotassignments__schedule=schedule, timeslotassignments__timeslot=slot):
SchedulingEvent.objects.create(
session=session,
status=SessionStatusName.objects.get(slug='canceled'),
by=request.user.person,
)
messages.success(request, 'The session was cancelled successfully')
return redirect('ietf.secr.meetings.views.non_session', meeting_id=meeting_id, schedule_name=schedule_name)
@ -569,12 +579,13 @@ def notifications(request, meeting_id):
if request.method == "POST":
# ensure session state is scheduled
for ss in meeting.schedule.assignments.all():
session = ss.session
if session.status.slug in ["schedw", "appr"]:
session.status_id = "sched"
session.scheduled = datetime.datetime.now()
session.save()
sessions = add_event_info_to_session_qs(Session.objects.filter(timeslotassignments__schedule=meeting.schedule_id)).filter(current_status__in=["schedw", "appr"])
for session in sessions:
SchedulingEvent.objects.create(
session=session,
status=SessionStatusName.objects.get(slug='sched'),
by=request.user.person,
)
send_notifications(meeting,groups,request.user.person)
messages.success(request, "Notifications Sent")
@ -638,16 +649,27 @@ def 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 = schedule.sessions_that_can_meet.order_by('group__acronym')
sessions = add_event_info_to_session_qs(
only_sessions_that_can_meet(schedule.meeting.session_set)
).order_by('group__acronym')
if request.method == 'POST':
if 'cancel' in request.POST:
pk = request.POST.get('pk')
session = Session.objects.get(pk=pk)
session.status = SessionStatusName.objects.get(slug='canceled')
session.save()
session = get_object_or_404(sessions, pk=pk)
SchedulingEvent.objects.create(
session=session,
status=SessionStatusName.objects.get(slug='canceled'),
by=request.user.person,
)
messages.success(request, 'Session cancelled')
status_names = {n.slug: n.name for n in SessionStatusName.objects.all()}
for s in sessions:
s.current_status_name = status_names.get(s.current_status, s.current_status)
return render(request, 'meetings/sessions.html', {
'meeting': meeting,
'schedule': schedule,
@ -664,7 +686,7 @@ def session_edit(request, meeting_id, schedule_name, session_id):
meeting = get_object_or_404(Meeting, number=meeting_id)
schedule = get_object_or_404(Schedule, meeting=meeting, name=schedule_name)
session = get_object_or_404(Session, id=session_id)
assignment = SchedTimeSessAssignment.objects.get(schedule=schedule,session=session)
assignment = SchedTimeSessAssignment.objects.filter(schedule=schedule, session=session).first()
if request.method == 'POST':
form = SessionEditForm(request.POST, instance=session)
@ -676,11 +698,17 @@ def session_edit(request, meeting_id, schedule_name, session_id):
else:
form = SessionEditForm(instance=session)
current_status_name = None
latest_event = SchedulingEvent.objects.filter(session=session).order_by('-time', '-id').first()
if latest_event:
current_status_name = latest_event.status.name
return render(request, 'meetings/session_edit.html', {
'meeting': meeting,
'schedule': schedule,
'session': session,
'timeslot': assignment.timeslot,
'timeslot': assignment.timeslot if assignment else None,
'current_status_name': current_status_name,
'form': form},
)
@ -821,8 +849,13 @@ def times_delete(request, meeting_id, schedule_name, time):
for assignment in slot.sessionassignments.all():
if assignment.session:
session = assignment.session
session.status = status
session.save()
latest_event = SchedulingEvent.objects.filter(session=session).order_by('-time', '-id').first()
if not latest_event or latest_event.status_id != 'schedw':
SchedulingEvent.objects.create(
session=session,
status=status,
by=request.user.person,
)
assignment.delete()
slot.delete()
messages.success(request, 'The entry was deleted successfully')

View file

@ -1,8 +1,10 @@
# Copyright The IETF Trust 2007-2019, All Rights Reserved
from django import forms
from ietf.doc.models import Document
from ietf.meeting.models import Session
from ietf.meeting.utils import add_event_info_to_session_qs
# ---------------------------------------------
@ -25,8 +27,9 @@ class RecordingForm(forms.Form):
def __init__(self, *args, **kwargs):
self.meeting = kwargs.pop('meeting')
super(RecordingForm, self).__init__(*args,**kwargs)
self.fields['session'].queryset = Session.objects.filter(meeting=self.meeting,
type__in=('session','plenary','other'),status='sched').order_by('group__acronym')
self.fields['session'].queryset = add_event_info_to_session_qs(
Session.objects.filter(meeting=self.meeting, type__in=('session','plenary','other'))
).filter(current_status='sched').order_by('group__acronym')
class RecordingEditForm(forms.ModelForm):
class Meta:

View file

@ -22,7 +22,7 @@ from django.core.exceptions import ObjectDoesNotExist
from ietf.doc.models import Document, DocAlias, DocEvent, NewRevisionDocEvent, State
from ietf.group.models import Group
from ietf.meeting.models import Meeting, SessionPresentation, TimeSlot, SchedTimeSessAssignment
from ietf.meeting.models import Meeting, SessionPresentation, TimeSlot, SchedTimeSessAssignment, Session
from ietf.person.models import Person
from ietf.utils.log import log
from ietf.utils.mail import send_mail
@ -65,6 +65,8 @@ def import_audio_files(meeting):
Example: ietf90-salonb-20140721-1710.mp3
'''
from ietf.meeting.utils import add_event_info_to_session_qs
unmatched_files = []
path = os.path.join(settings.MEETING_RECORDINGS_DIR, meeting.type.slug + meeting.number)
if not os.path.exists(path):
@ -72,15 +74,18 @@ def import_audio_files(meeting):
for filename in os.listdir(path):
timeslot = get_timeslot_for_filename(filename)
if timeslot:
sessionassignments = timeslot.sessionassignments.filter(
schedule=timeslot.meeting.schedule,
session__status='sched',
).exclude(session__agenda_note__icontains='canceled').order_by('timeslot__time')
if not sessionassignments:
sessions = add_event_info_to_session_qs(Session.objects.filter(
timeslotassignments__schedule=timeslot.meeting.schedule_id,
).exclude(
agenda_note__icontains='canceled'
)).filter(
current_status='sched',
).order_by('timeslotassignments__timeslot__time')
if not sessions:
continue
url = settings.IETF_AUDIO_URL + 'ietf{}/{}'.format(meeting.number, filename)
doc = get_or_create_recording_document(url,sessionassignments[0].session)
attach_recording(doc, [ x.session for x in sessionassignments ])
doc = get_or_create_recording_document(url, sessions[0])
attach_recording(doc, sessions)
else:
# use for reconciliation email
unmatched_files.append(filename)
@ -92,6 +97,8 @@ def get_timeslot_for_filename(filename):
'''Returns a timeslot matching the filename given.
NOTE: currently only works with ietfNN prefix (regular meetings)
'''
from ietf.meeting.utils import add_event_info_to_session_qs
basename, _ = os.path.splitext(filename)
match = AUDIO_FILE_RE.match(basename)
if match:
@ -104,9 +111,10 @@ def get_timeslot_for_filename(filename):
location__name=room_mapping[match.groupdict()['room']],
time=time,
sessionassignments__schedule=meeting.schedule,
).exclude(sessions__status_id='canceled').distinct()
return slots.get()
except (ObjectDoesNotExist, KeyError):
).distinct()
uncancelled_slots = [t for t in slots if not add_event_info_to_session_qs(t.sessions.all()).filter(current_status='canceled').exists()]
return uncancelled_slots[0]
except (ObjectDoesNotExist, KeyError, IndexError):
return None
def attach_recording(doc, sessions):

View file

@ -15,8 +15,9 @@ from django.urls import reverse
from ietf.doc.models import Document
from ietf.group.factories import RoleFactory
from ietf.meeting.models import SchedTimeSessAssignment
from ietf.meeting.models import SchedTimeSessAssignment, SchedulingEvent
from ietf.meeting.factories import MeetingFactory, SessionFactory
from ietf.person.models import Person
from ietf.name.models import SessionStatusName
from ietf.utils.test_utils import TestCase
from ietf.utils.mail import outbox
@ -129,10 +130,16 @@ class RecordingTestCase(TestCase):
mars_session = SessionFactory(meeting=meeting,status_id='sched',group__acronym='mars')
ames_session = SessionFactory(meeting=meeting,status_id='sched',group__acronym='ames')
scheduled = SessionStatusName.objects.get(slug='sched')
mars_session.status = scheduled
mars_session.save()
ames_session.status = scheduled
ames_session.save()
SchedulingEvent.objects.create(
session=mars_session,
status=scheduled,
by=Person.objects.get(name='(System)')
)
SchedulingEvent.objects.create(
session=ames_session,
status=scheduled,
by=Person.objects.get(name='(System)')
)
timeslot = mars_session.official_timeslotassignment().timeslot
SchedTimeSessAssignment.objects.create(timeslot=timeslot,session=ames_session,schedule=meeting.schedule)
self.create_audio_file_for_timeslot(timeslot)

View file

@ -25,6 +25,7 @@ from ietf.doc.models import Document, DocEvent
from ietf.person.models import Person
from ietf.ietfauth.utils import has_role, role_required
from ietf.meeting.models import Meeting, Session
from ietf.meeting.utils import add_event_info_to_session_qs
from ietf.secr.proceedings.forms import RecordingForm, RecordingEditForm
from ietf.secr.proceedings.proc_utils import (create_recording)
@ -170,7 +171,8 @@ def main(request):
meetings = [m for m in Meeting.objects.filter(type='ietf').order_by('-number') if m.get_submission_correction_date()>=today]
groups = get_my_groups(request.user)
interim_meetings = Meeting.objects.filter(type='interim',session__group__in=groups,session__status='sched').order_by('-date')
interim_sessions = add_event_info_to_session_qs(Session.objects.filter(group__in=groups, meeting__type='interim')).filter(current_status='sched').select_related('meeting')
interim_meetings = sorted({s.meeting for s in interim_sessions}, key=lambda m: m.date, reverse=True)
# tac on group for use in templates
for m in interim_meetings:
m.group = m.session_set.first().group

View file

@ -13,7 +13,7 @@ import debug # pyflakes:ignore
from ietf.utils.test_utils import TestCase
from ietf.group.factories import GroupFactory, RoleFactory
from ietf.meeting.models import Session, ResourceAssociation
from ietf.meeting.models import Session, ResourceAssociation, SchedulingEvent
from ietf.meeting.factories import MeetingFactory, SessionFactory
from ietf.person.models import Person
from ietf.utils.mail import outbox, empty_outbox
@ -42,16 +42,16 @@ class SessionRequestTestCase(TestCase):
def test_main(self):
meeting = MeetingFactory(type_id='ietf', date=datetime.date.today())
SessionFactory.create_batch(2, meeting=meeting, status_id='sched')
SessionFactory.create_batch(2, meeting=meeting, status_id='unsched')
SessionFactory.create_batch(2, meeting=meeting, status_id='disappr')
# An additional unscheduled group comes from make_immutable_base_data
url = reverse('ietf.secr.sreq.views.main')
self.client.login(username="secretary", password="secretary+password")
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
sched = r.context['scheduled_groups']
self.assertEqual(len(sched), 2)
unsched = r.context['unscheduled_groups']
self.assertEqual(len(unsched),8)
self.assertEqual(len(sched),2)
self.assertEqual(len(unsched), 8)
def test_approve(self):
meeting = MeetingFactory(type_id='ietf', date=datetime.date.today())
@ -64,25 +64,23 @@ class SessionRequestTestCase(TestCase):
self.client.login(username="ad", password="ad+password")
r = self.client.get(url)
self.assertRedirects(r,reverse('ietf.secr.sreq.views.view', kwargs={'acronym':'mars'}))
session = Session.objects.get(pk=session.pk)
self.assertEqual(session.status_id,'appr')
self.assertEqual(SchedulingEvent.objects.filter(session=session).order_by('-id')[0].status_id, 'appr')
def test_cancel(self):
meeting = MeetingFactory(type_id='ietf', date=datetime.date.today())
ad = Person.objects.get(user__username='ad')
area = RoleFactory(name_id='ad', person=ad, group__type_id='area').group
mars = SessionFactory(meeting=meeting, group__parent=area, group__acronym='mars', status_id='sched').group
session = SessionFactory(meeting=meeting, group__parent=area, group__acronym='mars', status_id='sched')
url = reverse('ietf.secr.sreq.views.cancel', kwargs={'acronym':'mars'})
self.client.login(username="ad", password="ad+password")
r = self.client.get(url)
self.assertRedirects(r,reverse('ietf.secr.sreq.views.main'))
sessions = Session.objects.filter(meeting=meeting, group=mars)
self.assertEqual(sessions[0].status_id,'deleted')
self.assertEqual(SchedulingEvent.objects.filter(session=session).order_by('-id')[0].status_id, 'deleted')
def test_edit(self):
meeting = MeetingFactory(type_id='ietf', date=datetime.date.today())
mars = RoleFactory(name_id='chair', person__user__username='marschairman', group__acronym='mars').group
SessionFactory(meeting=meeting,group=mars,status_id='sched',scheduled=datetime.datetime.now())
SessionFactory(meeting=meeting,group=mars,status_id='sched')
url = reverse('ietf.secr.sreq.views.edit', kwargs={'acronym':'mars'})
self.client.login(username="marschairman", password="marschairman+password")

View file

@ -1,3 +1,5 @@
# Copyright The IETF Trust 2007-2019, All Rights Reserved
from django.conf import settings
from ietf.secr.sreq import views
@ -14,5 +16,5 @@ urlpatterns = [
url(r'^%(acronym)s/edit/$' % settings.URL_REGEXPS, views.edit),
url(r'^%(acronym)s/new/$' % settings.URL_REGEXPS, views.new),
url(r'^%(acronym)s/no_session/$' % settings.URL_REGEXPS, views.no_session),
url(r'^(?P<num>[A-Za-z0-9_\-\+]+)/%(acronym)s/edit/$' % settings.URL_REGEXPS, views.edit_mtg),
url(r'^(?P<num>[A-Za-z0-9_\-\+]+)/%(acronym)s/edit/$' % settings.URL_REGEXPS, views.edit),
]

View file

@ -5,22 +5,25 @@
from __future__ import absolute_import, print_function, unicode_literals
import datetime
from collections import defaultdict
from django.conf import settings
from django.contrib import messages
from django.db.models import Q
from django.shortcuts import render, get_object_or_404, redirect
from django.http import Http404
import debug # pyflakes:ignore
from ietf.group.models import Group
from ietf.group.models import Group, GroupFeatures
from ietf.ietfauth.utils import has_role, role_required
from ietf.meeting.models import Meeting, Session, Constraint, ResourceAssociation
from ietf.meeting.models import Meeting, Session, Constraint, ResourceAssociation, SchedulingEvent
from ietf.meeting.helpers import get_meeting
from ietf.meeting.utils import add_event_info_to_session_qs
from ietf.name.models import SessionStatusName, ConstraintName
from ietf.secr.sreq.forms import SessionForm, ToolStatusForm
from ietf.secr.utils.decorators import check_permissions
from ietf.secr.utils.group import groups_by_session
from ietf.secr.utils.group import get_my_groups
from ietf.utils.mail import send_mail
from ietf.person.models import Person
from ietf.mailtrigger.utils import gather_address_lists
@ -171,11 +174,18 @@ def approve(request, acronym):
'''
meeting = get_meeting()
group = get_object_or_404(Group, acronym=acronym)
session = Session.objects.get(meeting=meeting,group=group,status='apprw')
session = add_event_info_to_session_qs(Session.objects.filter(meeting=meeting, group=group)).filter(current_status='apprw').first()
if session is None:
raise Http404
if has_role(request.user,'Secretariat') or group.parent.role_set.filter(name='ad',person=request.user.person):
session.status = SessionStatusName.objects.get(slug='appr')
session_save(session)
SchedulingEvent.objects.create(
session=session,
status=SessionStatusName.objects.get(slug='appr'),
by=request.user.person,
)
session_changed(session)
messages.success(request, 'Third session approved')
return redirect('ietf.secr.sreq.views.view', acronym=acronym)
@ -205,8 +215,12 @@ def cancel(request, acronym):
# mark sessions as deleted
for session in sessions:
session.status_id = 'deleted'
session_save(session)
SchedulingEvent.objects.create(
session=session,
status=SessionStatusName.objects.get(slug='deleted'),
by=request.user.person,
)
session_changed(session)
# clear schedule assignments if already scheduled
session.timeslotassignments.all().delete()
@ -236,7 +250,7 @@ def confirm(request, acronym):
login = request.user.person
# check if request already exists for this group
if Session.objects.filter(group=group,meeting=meeting).exclude(status__in=('deleted','notmeet')):
if add_event_info_to_session_qs(Session.objects.filter(group=group, meeting=meeting)).filter(Q(current_status__isnull=True) | ~Q(current_status__in=['deleted', 'notmeet'])):
messages.warning(request, 'Sessions for working group %s have already been requested once.' % group.acronym)
return redirect('ietf.secr.sreq.views.main')
@ -258,7 +272,7 @@ def confirm(request, acronym):
if request.method == 'POST' and button_text == 'Submit':
# delete any existing session records with status = canceled or notmeet
Session.objects.filter(group=group,meeting=meeting,status__in=('canceled','notmeet')).delete()
add_event_info_to_session_qs(Session.objects.filter(group=group, meeting=meeting)).filter(current_status__in=['canceled', 'notmeet']).delete()
# create new session records
count = 0
@ -268,19 +282,22 @@ def confirm(request, acronym):
count += 1
if duration:
slug = 'apprw' if count == 3 else 'schedw'
new_session = Session(meeting=meeting,
group=group,
attendees=form.data['attendees'],
requested=datetime.datetime.now(),
requested_by=login,
requested_duration=datetime.timedelta(0,int(duration)),
comments=form.data['comments'],
status=SessionStatusName.objects.get(slug=slug),
type_id='session',
)
session_save(new_session)
new_session = Session.objects.create(
meeting=meeting,
group=group,
attendees=form.data['attendees'],
requested_duration=datetime.timedelta(0,int(duration)),
comments=form.data['comments'],
type_id='session',
)
SchedulingEvent.objects.create(
session=new_session,
status=SessionStatusName.objects.get(slug=slug),
by=login,
)
if 'resources' in form.data:
new_session.resources.set(session_data['resources'])
session_changed(new_session)
# write constraint records
save_conflicts(group,meeting,form.data.get('conflict1',''),'conflict')
@ -293,7 +310,7 @@ def confirm(request, acronym):
Constraint.objects.create(name=bethere_cn, source=group, person=p, meeting=new_session.meeting)
# clear not meeting
Session.objects.filter(group=group,meeting=meeting,status='notmeet').delete()
add_event_info_to_session_qs(Session.objects.filter(group=group, meeting=meeting)).filter(current_status='notmeet').delete()
# send notification
send_notification(group,meeting,login,session_data,'new')
@ -322,23 +339,21 @@ def add_essential_people(group,initial):
initial['bethere'] = list(people)
def edit(request, *args, **kwargs):
return edit_mtg(request, None, *args, **kwargs)
def session_changed(session):
latest_event = SchedulingEvent.objects.filter(session=session).order_by('-time', '-id').first()
def session_save(session):
session.save()
if session.status_id == "schedw" and session.meeting.schedule != None:
if latest_event and latest_event.status_id == "schedw" and session.meeting.schedule != None:
# send an email to iesg-secretariat to alert to change
pass
@check_permissions
def edit_mtg(request, num, acronym):
def edit(request, acronym, num=None):
'''
This view allows the user to edit details of the session request
'''
meeting = get_meeting(num)
group = get_object_or_404(Group, acronym=acronym)
sessions = Session.objects.filter(meeting=meeting,group=group).exclude(status__in=('deleted','notmeet')).order_by('id')
sessions = add_event_info_to_session_qs(Session.objects.filter(group=group, meeting=meeting)).filter(Q(current_status__isnull=True) | ~Q(current_status__in=['canceled', 'notmeet'])).order_by('id')
sessions_count = sessions.count()
initial = get_initial_session(sessions)
if 'resources' in initial:
@ -370,7 +385,8 @@ def edit_mtg(request, num, acronym):
if 'length_session1' in form.changed_data:
session = sessions[0]
session.requested_duration = datetime.timedelta(0,int(form.cleaned_data['length_session1']))
session_save(session)
session.save()
session_changed(session)
# session 2
if 'length_session2' in form.changed_data:
@ -379,22 +395,24 @@ def edit_mtg(request, num, acronym):
sessions[1].delete()
elif sessions_count < 2:
duration = datetime.timedelta(0,int(form.cleaned_data['length_session2']))
new_session = Session(meeting=meeting,
group=group,
attendees=form.cleaned_data['attendees'],
requested=datetime.datetime.now(),
requested_by=login,
requested_duration=duration,
comments=form.cleaned_data['comments'],
status=SessionStatusName.objects.get(slug='schedw'),
type_id='session',
)
new_session.save()
new_session = Session.objects.create(
meeting=meeting,
group=group,
attendees=form.cleaned_data['attendees'],
requested_duration=duration,
comments=form.cleaned_data['comments'],
type_id='session',
)
SchedulingEvent.objects.create(
session=new_session,
status=SessionStatusName.objects.get(slug='schedw'),
by=request.user.person,
)
else:
duration = datetime.timedelta(0,int(form.cleaned_data['length_session2']))
session = sessions[1]
session.requested_duration = duration
session_save(session)
session.save()
# session 3
if 'length_session3' in form.changed_data:
@ -403,22 +421,25 @@ def edit_mtg(request, num, acronym):
sessions[2].delete()
elif sessions_count < 3:
duration = datetime.timedelta(0,int(form.cleaned_data['length_session3']))
new_session = Session(meeting=meeting,
group=group,
attendees=form.cleaned_data['attendees'],
requested=datetime.datetime.now(),
requested_by=login,
requested_duration=duration,
comments=form.cleaned_data['comments'],
status=SessionStatusName.objects.get(slug='apprw'),
type_id='session',
)
new_session.save()
new_session = Session.objects.create(
meeting=meeting,
group=group,
attendees=form.cleaned_data['attendees'],
requested_duration=duration,
comments=form.cleaned_data['comments'],
type_id='session',
)
SchedulingEvent.objects.create(
session=new_session,
status=SessionStatusName.objects.get(slug='apprw'),
by=request.user.person,
)
else:
duration = datetime.timedelta(0,int(form.cleaned_data['length_session3']))
session = sessions[2]
session.requested_duration = duration
session_save(session)
session.save()
session_changed(session)
if 'attendees' in form.changed_data:
@ -495,7 +516,28 @@ def main(request):
meeting = get_meeting()
scheduled_groups, unscheduled_groups = groups_by_session(request.user, meeting)
scheduled_groups = []
unscheduled_groups = []
group_types = GroupFeatures.objects.filter(has_meetings=True).values_list('type', flat=True)
my_groups = [g for g in get_my_groups(request.user, conclude=True) if g.type_id in group_types]
sessions_by_group = defaultdict(list)
for s in add_event_info_to_session_qs(Session.objects.filter(meeting=meeting, group__in=my_groups)).filter(current_status__in=['schedw', 'apprw', 'appr', 'sched']):
sessions_by_group[s.group_id].append(s)
for group in my_groups:
group.meeting_sessions = sessions_by_group.get(group.pk, [])
if group.pk in sessions_by_group:
# include even if concluded as we need to to see that the
# sessions are there
scheduled_groups.append(group)
else:
if group.state_id not in ['conclude', 'bof-conc']:
# too late for unscheduled if concluded
unscheduled_groups.append(group)
# warn if there are no associated groups
if not scheduled_groups and not unscheduled_groups:
@ -503,15 +545,14 @@ def main(request):
# add session status messages for use in template
for group in scheduled_groups:
sessions = group.session_set.filter(meeting=meeting)
if sessions.count() < 3:
group.status_message = sessions[0].status
if len(group.meeting_sessions) < 3:
group.status_message = group.meeting_sessions[0].current_status
else:
group.status_message = 'First two sessions: %s, Third session: %s' % (sessions[0].status,sessions[2].status)
group.status_message = 'First two sessions: %s, Third session: %s' % (group.meeting_sessions[0].current_status, group.meeting_sessions[2].current_status)
# add not meeting indicators for use in template
for group in unscheduled_groups:
if group.session_set.filter(meeting=meeting,status='notmeet'):
if any(s.current_status == 'notmeet' for s in group.meeting_sessions):
group.not_meeting = True
return render(request, 'sreq/main.html', {
@ -550,7 +591,7 @@ def new(request, acronym):
# pre-populated with data from last meeeting's session request
elif request.method == 'GET' and 'previous' in request.GET:
previous_meeting = Meeting.objects.get(number=str(int(meeting.number) - 1))
previous_sessions = Session.objects.filter(meeting=previous_meeting,group=group).exclude(status__in=('notmeet','deleted')).order_by('id')
previous_sessions = add_event_info_to_session_qs(Session.objects.filter(meeting=previous_meeting, group=group)).filter(current_status__in=['notmeet', 'deleted']).order_by('id')
if not previous_sessions:
messages.warning(request, 'This group did not meet at %s' % previous_meeting)
return redirect('ietf.secr.sreq.views.new', acronym=acronym)
@ -586,22 +627,25 @@ def no_session(request, acronym):
login = request.user.person
# delete canceled record if there is one
Session.objects.filter(group=group,meeting=meeting,status='canceled').delete()
add_event_info_to_session_qs(Session.objects.filter(group=group, meeting=meeting)).filter(current_status='canceled').delete()
# skip if state is already notmeet
if Session.objects.filter(group=group,meeting=meeting,status='notmeet'):
if add_event_info_to_session_qs(Session.objects.filter(group=group, meeting=meeting)).filter(current_status='notmeet'):
messages.info(request, 'The group %s is already marked as not meeting' % group.acronym)
return redirect('ietf.secr.sreq.views.main')
session = Session(group=group,
meeting=meeting,
requested=datetime.datetime.now(),
requested_by=login,
requested_duration=datetime.timedelta(0),
status=SessionStatusName.objects.get(slug='notmeet'),
type_id='session',
)
session_save(session)
session = Session.objects.create(
group=group,
meeting=meeting,
requested_duration=datetime.timedelta(0),
type_id='session',
)
SchedulingEvent.objects.create(
session=session,
status=SessionStatusName.objects.get(slug='notmeet'),
by=login,
)
session_changed(session)
# send notification
(to_email, cc_list) = gather_address_lists('session_request_not_meeting',group=group,person=login)
@ -669,7 +713,7 @@ def view(request, acronym, num = None):
'''
meeting = get_meeting(num)
group = get_object_or_404(Group, acronym=acronym)
sessions = Session.objects.filter(~Q(status__in=('canceled','notmeet','deleted')),meeting=meeting,group=group).order_by('id')
sessions = add_event_info_to_session_qs(Session.objects.filter(meeting=meeting, group=group)).filter(Q(current_status__isnull=True) | ~Q(current_status__in=('canceled','notmeet','deleted'))).order_by('id')
# check if app is locked
is_locked = check_app_locked()
@ -683,16 +727,12 @@ def view(request, acronym, num = None):
else:
return redirect('ietf.secr.sreq.views.new', acronym=acronym)
# TODO simulate activity records
activities = [{'act_date':sessions[0].requested.strftime('%b %d, %Y'),
'act_time':sessions[0].requested.strftime('%H:%M:%S'),
'activity':'New session was requested',
'act_by':sessions[0].requested_by}]
if sessions[0].scheduled:
activities.append({'act_date':sessions[0].scheduled.strftime('%b %d, %Y'),
'act_time':sessions[0].scheduled.strftime('%H:%M:%S'),
'activity':'Session was scheduled',
'act_by':'Secretariat'})
activities = [{
'act_date': e.time.strftime('%b %d, %Y'),
'act_time': e.time.strftime('%H:%M:%S'),
'activity': e.status.name,
'act_by': e.by,
} for e in sessions[0].schedulingevent_set.select_related('status', 'by')]
# other groups that list this group in their conflicts
session_conflicts = session_conflicts_as_string(group, meeting)
@ -700,7 +740,7 @@ def view(request, acronym, num = None):
# if sessions include a 3rd session waiting approval and the user is a secretariat or AD of the group
# display approve button
if sessions.filter(status='apprw'):
if any(s.current_status == 'apprw' for s in sessions):
if has_role(request.user,'Secretariat') or group.parent.role_set.filter(name='ad',person=request.user.person):
show_approve_button = True

View file

@ -24,7 +24,7 @@
</thead>
<tbody>
{% for assignment in assignments %}
<tr class="{% cycle 'row1' 'row2' %}{% ifchanged assignment.session.type %} break{% endifchanged %}{% if assignment.session.status.slug == "canceled" %} cancelled{% endif %}">
<tr class="{% cycle 'row1' 'row2' %}{% ifchanged assignment.session.type %} break{% endifchanged %}{% if assignment.current_session_status == "canceled" %} cancelled{% endif %}">
<td>{{ assignment.timeslot.time|date:"D" }}</td>
<td>{{ assignment.timeslot.time|date:"H:i" }}-{{ assignment.timeslot.end_time|date:"H:i" }}</td>
<td>{{ assignment.timeslot.name }}</td>

View file

@ -23,19 +23,19 @@
<col width="200">
<tr>
<th>Day:</th>
<td>{{ timeslot.time|date:"l" }}</td>
<td>{% if timeslot %}{{ timeslot.time|date:"l" }}{% endif %}</td>
</tr>
<tr>
<th>Time:</th>
<td>{{ timeslot.time|time:"H:i" }}</td>
<td>{% if timeslot %}{{ timeslot.time|time:"H:i" }}{% endif %}</td>
</tr>
<tr>
<th>Room:</th>
<td>{{ timeslot.location.name }}</td>
<td>{% if timeslot %}{{ timeslot.location.name }}{% endif %}</td>
</tr>
<tr>
<th>Status:</th>
<td>{{ session.status }}</td>
<td>{{ current_status_name }}</td>
</tr>
{{ form }}
</table>

View file

@ -33,7 +33,7 @@
{% endif %}
</td>
<td>{{ session.agenda_note }}</td>
<td>{{ session.status }}</td>
<td>{{ session.current_status_name }}</td>
<td><a href="{% url 'ietf.secr.meetings.views.session_edit' meeting_id=meeting.number schedule_name=schedule.name session_id=session.id %}">Edit</a></td>
<td>
<form method="post">

View file

@ -29,7 +29,7 @@
<div class="button-group">
<ul>
<li><button name="edit" onclick="window.location='{% url "ietf.secr.sreq.views.edit_mtg" acronym=group.acronym num=meeting.number %}'"{% if is_locked %} disabled{% endif %}>Edit</button></li>
<li><button name="edit" onclick="window.location='{% url "ietf.secr.sreq.views.edit" acronym=group.acronym num=meeting.number %}'"{% if is_locked %} disabled{% endif %}>Edit</button></li>
{% if show_approve_button %}
<li><button onclick="window.location='approve/'">Approve Third Session</button></li>
{% endif %}

View file

@ -13,8 +13,7 @@ from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
# Datatracker imports
from ietf.group.models import Group, GroupFeatures
from ietf.meeting.models import Session
from ietf.group.models import Group
from ietf.ietfauth.utils import has_role
@ -75,35 +74,3 @@ def get_my_groups(user,conclude=False):
continue
return list(my_groups)
def groups_by_session(user, meeting, types=None):
'''
Takes a Django User object, Meeting object and optionally string of meeting types to
include. Returns a tuple scheduled_groups, unscheduled groups. sorted lists of those
groups that the user has access to, secretariat defaults to all groups
If user=None than all groups are returned.
For groups with a session, we must include "concluded" groups because we still want to know
who had a session at a particular meeting even if they are concluded after. This is not true
for groups without a session because this function is often used to build select lists (ie.
Session Request Tool) and you don't want concluded groups appearing as options.
'''
groups_session = []
groups_no_session = []
my_groups = get_my_groups(user,conclude=True)
sessions = Session.objects.filter(meeting=meeting,status__in=('schedw','apprw','appr','sched'))
groups_with_sessions = [ s.group for s in sessions ]
for group in my_groups:
if group in groups_with_sessions:
groups_session.append(group)
else:
if group.state_id not in ('conclude','bof-conc'):
groups_no_session.append(group)
if not types:
types = GroupFeatures.objects.filter(has_meetings=True).values_list('type', flat=True)
groups_session = [x for x in groups_session if x.type_id in types]
groups_no_session = [x for x in groups_no_session if x.type_id in types]
return groups_session, groups_no_session

View file

@ -377,7 +377,7 @@ MIDDLEWARE = [
'django_referrer_policy.middleware.ReferrerPolicyMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.security.SecurityMiddleware',
'csp.middleware.CSPMiddleware',
# 'csp.middleware.CSPMiddleware',
'ietf.middleware.unicode_nfkc_normalization_middleware',
]

View file

@ -21,10 +21,10 @@
<td>{% ifchanged s.meeting %}{% if s.meeting.type.slug == 'ietf' %}IETF{% endif %}{{s.meeting.number}}{% endifchanged %}</td>
<td>
{% if s.name %}{{ s.name }}<br>{% else %}{{ s.group.acronym }} - {% endif %}
{% if s.status.slug == "sched" %}
{% if s.current_status == "sched" %}
{% if s.meeting.type.slug == 'ietf' %}{{s.time|date:"D M d, Y Hi"}}{% else %}{{s.time|date:"D M d, Y"}}{% endif %}
{% else %}
{{s.status}}
{{s.current_status_name}}
{% endif %}
</td>
<td>{% if s.agenda %}<a href="{{ s.agenda.get_absolute_url }}">Agenda</a>{% endif %}</td>

View file

@ -223,7 +223,7 @@
{{item.timeslot.name}}
{% endif %}
{% if item.session.status.slug == 'canceled' %}
{% if item.session.current_status == 'canceled' %}
<span class="label label-danger pull-right">CANCELLED</span>
{% endif %}
@ -316,7 +316,7 @@
<span class="label label-success pull-right">BOF</span>
{% endif %}
{% if item.session.status.slug == 'canceled' %}
{% if item.session.current_status == 'canceled' %}
<span class="label label-danger pull-right">CANCELLED</span>
{% endif %}

View file

@ -22,7 +22,7 @@
{% endif %}{% if item.timeslot.type.slug == "session" %}{% if item.session.historic_group %}{% ifchanged %}
{{ item.timeslot.time_desc }} {{ item.timeslot.name }}
{% endifchanged %}{{ item.timeslot.location.name|ljust:14 }} {{ item.session.historic_group.historic_parent.acronym|upper|ljust:4 }} {{ item.session.historic_group.acronym|ljust:10 }} {{ item.session.historic_group.name }} {% if item.session.historic_group.state_id == "bof" %}BOF{% elif item.session.historic_group.type_id == "wg" %}WG{% endif %}{% if item.session.agenda_note %} - {{ item.session.agenda_note }}{% endif %}{% if item.session.status.slug == 'canceled' %} *** CANCELLED ***{% endif %}
{% endifchanged %}{{ item.timeslot.location.name|ljust:14 }} {{ item.session.historic_group.historic_parent.acronym|upper|ljust:4 }} {{ item.session.historic_group.acronym|ljust:10 }} {{ item.session.historic_group.name }} {% if item.session.historic_group.state_id == "bof" %}BOF{% elif item.session.historic_group.type_id == "wg" %}WG{% endif %}{% if item.session.agenda_note %} - {{ item.session.agenda_note }}{% endif %}{% if item.session.current_status == 'canceled' %} *** CANCELLED ***{% endif %}
{% endif %}{% endif %}{% if item.timeslot.type.slug == "break" %}
{{ item.timeslot.time_desc }} {{ item.timeslot.name }}{% if schedule.meeting.break_area and item.timeslot.show_location %} - {{ schedule.meeting.break_area }}{% endif %}{% endif %}{% if item.timeslot.type.slug == "other" %}
{{ item.timeslot.time_desc }} {{ item.timeslot.name }} - {{ item.timeslot.location.name }}{% endif %}{% endfor %}

View file

@ -35,17 +35,9 @@
</thead>
<tbody>
{% for meeting in meetings %}
{% if meeting.type.slug == 'interim' %}
<tr id="row-{{ forloop.counter }}-{{ meeting.session_set.all.0.group.acronym }}">
{% else %}
<tr id="row-{{ forloop.counter }}-ietf">
{% endif %}
<tr id="row-{{ forloop.counter }}{% if meeting.responsible_group.parent %}-{{ meeting.responsible_group.parent.acronym }}{% endif %}-{{ meeting.responsible_group.acronym }}">
<td>{{ meeting.date }}</td>
{% if meeting.type.slug == 'interim' %}
<td>{{ meeting.session_set.all.0.group.acronym }}</td>
{% else %}
<td>ietf</td>
{% endif %}
<td>{{ meeting.responsible_group.acronym }}</td>
<td>
<a href="{% url 'ietf.meeting.views.interim_request_details' number=meeting.number %}">{{ meeting.number }}</a>
</td>

View file

@ -14,54 +14,46 @@
{% origin %}
<h1>Pending Interim Meetings</h1>
{% if menu_entries %}
<ul class="nav nav-tabs" role="tablist">
{% for name, url in menu_entries %}
<li {% if selected_menu_entry == name.lower %}class="active"{% endif %}>
<a href="{{ url }}">{{ name }}</a>
</li>
{% endfor %}
</ul>
{% endif %}
{% if menu_entries %}
<ul class="nav nav-tabs" role="tablist">
{% for name, url in menu_entries %}
<li {% if selected_menu_entry == name.lower %}class="active"{% endif %}>
<a href="{{ url }}">{{ name }}</a>
</li>
{% endfor %}
</ul>
{% endif %}
{% if meetings %}
<table id="pending-interim-meetings-table" class="table table-condensed table-striped">
<thead>
<tr>
<th>Date</th>
<th>Group</th>
<th>Name</th>
<th></th>
</tr>
</thead>
<tbody>
{% for meeting in meetings %}
{% if meeting.type.slug == 'interim' %}
<tr id="row-{{ forloop.counter }}-{{ meeting.session_set.all.0.group.acronym }}">
{% else %}
<tr id="row-{{ forloop.counter }}-ietf">
{% endif %}
<td>{{ meeting.date }}</td>
{% if meeting.type.slug == 'interim' %}
<td>{{ meeting.session_set.all.0.group.acronym }}</td>
{% else %}
<td>ietf</td>
{% endif %}
<td>
{% if meeting.type.slug == "interim" %}
<a href="{% url 'ietf.meeting.views.interim_request_details' number=meeting.number %}">{{ meeting.number }}{% if meeting.session_set.all.0.status.slug == "canceled" %} -- CANCELLED --{% endif %}</a>
{% if meetings %}
<table id="pending-interim-meetings-table" class="table table-condensed table-striped">
<thead>
<tr>
<th>Date</th>
<th>Group</th>
<th>Name</th>
<th></th>
</tr>
</thead>
<tbody>
{% for meeting in meetings %}
<tr id="row-{{ forloop.counter }}{% if meeting.responsible_group.parent %}-{{ meeting.responsible_group.parent.acronym }}{% endif %}-{{ meeting.responsible_group.acronym }}">
<td>{{ meeting.date }}</td>
<td>{{ meeting.responsible_group.acronym }}</td>
<td>
{% if meeting.type_id == "interim" %}
<a href="{% url 'ietf.meeting.views.interim_request_details' number=meeting.number %}">{{ meeting.number }}{% if meeting.interim_meeting_cancelled %}&nbsp;&nbsp;<span class="label label-warning">CANCELLED</span>{% endif %}</a>
{% else %}
<a href="{% url 'ietf.meeting.views.interim_request_details' number=meeting.number %}">IETF - {{ meeting.number }}</a>
{% endif %}
</td>
<td>{% if meeting.can_approve %}<span class="label label-success">can be approved</span>{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<h3>No pending interim meetings</h3>
{% endif %}
</td>
<td>{% if meeting.can_approve %}<span class="label label-success">can be approved</span>{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<h3>No pending interim meetings</h3>
{% endif %}
{% endblock %}

View file

@ -3,7 +3,7 @@
{% load origin %}
{% load staticfiles bootstrap3 widget_tweaks %}
{% block title %}Cancel Interim Meeting {% if meeting.session_set.first.status.slug != "sched" %}Request{% endif %}{% endblock %}
{% block pagehead %}
<link rel="stylesheet" href="{% static 'select2/select2.css' %}">
@ -13,7 +13,7 @@
{% block content %}
{% origin %}
<h1>Cancel Interim Meeting {% if meeting.session_set.first.status.slug != "sched" %}Request{% endif %}</h1>
<h1>{% block title %}Cancel Interim Meeting {% if session_status != "sched" %}Request{% endif %}{% endblock %}</h1>
<form id="interim-request-cancel-form" role="form" method="post" class="form-horizontal">
{% csrf_token %}

View file

@ -15,11 +15,11 @@
<h1>Interim Meeting Request Details</h1>
<dl class="dl-horizontal">
<dt>Group</dt>
<dd>{{ sessions.0.group.acronym }}
<dd>{{ group.acronym }}
<dt>Requested By</dt>
<dd>{{ sessions.0.requested_by }}
<dd>{{ requester }}
<dt>Status</dt>
<dd>{{ sessions.0.status }}</dd>
<dd>{{ session_status.name }}</dd>
<dt>City</dt>
<dd>{{ meeting.city }}</dd>
<dt>Country</dt>
@ -46,24 +46,24 @@
{% 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 sessions.0.status.slug == 'apprw' %}
{% if can_approve and session_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 sessions.0.status.slug == 'scheda' %}
{% if user|has_role:"Secretariat" and session_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 sessions.0.status.slug == 'apprw' or sessions.0.status.slug == 'scheda' or sessions.0.status.slug == 'sched' %}
{% if session_status.slug == 'apprw' or session_status.slug == 'scheda' or session_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 sessions.0.status.slug == "apprw" %}
{% if session_status.slug == "apprw" %}
<a class="btn btn-default" href="{% url 'ietf.meeting.views.interim_pending' %}">Back</a>
{% elif sessions.0.status.slug == "scheda" %}
{% elif session_status.slug == "scheda" %}
<a class="btn btn-default" href="{% url 'ietf.meeting.views.interim_announce' %}">Back</a>
{% elif sessions.0.status.slug == "sched" %}
{% elif session_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>

View file

@ -5,7 +5,7 @@
{% load ietf_filters staticfiles %}
{% block pagehead %}
<link rel="stylesheet" href="{% static "jquery.tablesorter/css/theme.bootstrap.min.css" %}">
<link rel="stylesheet" href="{% static "jquery.tablesorter/css/theme.bootstrap.min.css" %}">
{% endblock %}
{% block bodyAttrs %}data-spy="scroll" data-target="#affix"{% endblock %}
@ -75,47 +75,43 @@
{% else %}
<blockquote><i>No past meetings are available.</i></blockquote>
{% endif %}
</div> <!-- panel-body -->
</div> <!-- panel-collapse -->
</div> <!-- panel -->
</div> <!-- panel-group -->
{% if meetings %}
<h3></h3>
<table class="table table-condensed table-striped tablesorter">
<thead>
<tr>
<th>Date</th>
<th>Group</th>
<th>Name</th>
</tr>
</thead>
<tbody>
{% for meeting in meetings %}
{% if meeting.type.slug == 'interim' %}
<tr id="row-{{ forloop.counter }}-{{ meeting.session_set.first.group.parent.acronym }}-{{ meeting.session_set.first.group.acronym }}">
{% else %}
<tr id="row-{{ forloop.counter }}-ietf">
{% endif %}
<td>{{ meeting.date }}</td>
{% if meeting.type.slug == 'interim' %}
<td>
<a href="{% url 'ietf.group.views.group_home' meeting.session_set.all.0.group.acronym %}">{{ meeting.session_set.all.0.group.acronym }}</a>
</td>
{% if meetings %}
<h3></h3>
<table class="table table-condensed table-striped tablesorter">
<thead>
<tr>
<th>Date</th>
<th>Group</th>
<th>Name</th>
</tr>
</thead>
<tbody>
{% for meeting in meetings %}
<tr id="row-{{ forloop.counter }}{% if meeting.responsible_group.parent %}-{{ meeting.responsible_group.parent.acronym }}{% endif %}-{{ meeting.responsible_group.acronym }}">
<td>{{ meeting.date }}</td>
<td>
{% if meeting.responsible_group.type_id != 'ietf' %}
<a href="{% url 'ietf.group.views.group_home' meeting.responsible_group.acronym %}">{{ meeting.responsible_group.acronym }}</a>
{% else %}
<td>ietf</td>
{{ meeting.responsible_group.acronym }}
{% endif %}
<td>
{% if meeting.type.slug == "interim" %}
<a href="{% url 'ietf.meeting.views.session_details' num=meeting.number acronym=meeting.session_set.all.0.group.acronym %}">{{ meeting.number }}{% if meeting.session_set.all.0.status.slug == "canceled" %}&nbsp;&nbsp;<span class="label label-warning">CANCELLED</span>{% endif %}</a>
</td>
<td>
{% if meeting.type_id == "interim" %}
<a href="{% url 'ietf.meeting.views.session_details' num=meeting.number acronym=meeting.responsible_group.acronym %}">{{ meeting.number }}{% if meeting.interim_meeting_cancelled %}&nbsp;&nbsp;<span class="label label-warning">CANCELLED</span>{% endif %}</a>
{% else %}
<a href="{% url 'ietf.meeting.views.agenda' num=meeting.number %}">IETF - {{ meeting.number }}</a>
{% endif %}
</td>
</td>
<td>
{% if meeting.type.slug == "interim" %}
<td>
{% if meeting.type_id == "interim" %}
{% else %}
{% if meeting.get_number > 97 %}
<a href="{% url 'ietf.meeting.views.important_dates' num=meeting.number %}">Important dates</a>

View file

@ -68,60 +68,58 @@
{% endif %}
<!-- Working groups -->
{% regroup ietf|dictsort:"group.parent.acronym" by group.parent.name as areas %}
{% for sessions in areas %}
<h2 class="anchor-target" id="{{sessions.list.0.group.parent.acronym}}">{{sessions.list.0.group.parent.acronym|upper}} <small>{{ sessions.grouper }}</small></h2>
{% regroup sessions.list by not_meeting as meet_or_not %}
{% for batch in meet_or_not %}
{% if not batch.grouper %}
<table class="table table-condensed table-striped tablesorter">
<thead>
<tr>
<th class="col-md-1">Group</th>
<th class="col-md-1">Artifacts</th>
<th class="col-md-2">Recordings</th>
<th class="col-md-4">Slides</th>
<th class="col-md-3">Drafts</th>
</tr>
</thead>
<tbody>
{% for session in batch.list|dictsort:"group.acronym" %}
{% ifchanged session.group.acronym %}
{% include "meeting/group_proceedings.html" %}
{% endifchanged %}
{% endfor %}
</tbody>
</table>
{% else %}
<p><small>{{sessions.grouper }} groups not meeting: </small>
{% for session in batch.list|dictsort:"group.acronym" %}
{% ifchanged session.group.acronym %}
<a href="{% url 'ietf.group.views.group_home' acronym=session.group.acronym %}">{{session.group.acronym}}</a>{% if not forloop.last %},{% endif %}
{% endifchanged %}
{% endfor %}
</p>
<table class="table table-condensed table-striped">
<thead>
<tr>
<th class="col-md-1">&nbsp;</th>
<th class="col-md-1">&nbsp;</th>
<th class="col-md-2">&nbsp;</th>
<th class="col-md-4">&nbsp;</th>
<th class="col-md-3">&nbsp;</th>
</tr>
</thead>
<tbody>
{% for session in batch.list|dictsort:"group.acronym" %}
{% ifchanged session.group.acronym %}
{% if session.sessionpresentation_set.exists %}
{% include "meeting/group_proceedings.html" %}
{% endif %}
{% endifchanged %}
{% endfor %}
</tbody>
</table>
{% endif %}
{% endfor %}
{% for area, meeting_sessions, not_meeting_sessions in ietf_areas %}
<h2 class="anchor-target" id="{{ area.acronym }}">{{ area.acronym|upper }} <small>{{ area.name }}</small></h2>
{% if meeting_sessions %}
<table class="table table-condensed table-striped tablesorter">
<thead>
<tr>
<th class="col-md-1">Group</th>
<th class="col-md-1">Artifacts</th>
<th class="col-md-2">Recordings</th>
<th class="col-md-4">Slides</th>
<th class="col-md-3">Drafts</th>
</tr>
</thead>
<tbody>
{% for session in meeting_sessions %}
{% ifchanged session.group.acronym %}
{% include "meeting/group_proceedings.html" %}
{% endifchanged %}
{% endfor %}
</tbody>
</table>
{% endif %}
{% if not_meeting_sessions %}
<p><small>{{ area.name }} groups not meeting: </small>
{% for session in not_meeting_sessions %}
{% ifchanged session.group.acronym %}
<a href="{% url 'ietf.group.views.group_home' acronym=session.group.acronym %}">{{ session.group.acronym }}</a>{% if not forloop.last %},{% endif %}
{% endifchanged %}
{% endfor %}
</p>
<table class="table table-condensed table-striped">
<thead>
<tr>
<th class="col-md-1">&nbsp;</th>
<th class="col-md-1">&nbsp;</th>
<th class="col-md-2">&nbsp;</th>
<th class="col-md-4">&nbsp;</th>
<th class="col-md-3">&nbsp;</th>
</tr>
</thead>
<tbody>
{% for session in not_meeting_sessions %}
{% ifchanged session.group.acronym %}
{% if session.sessionpresentation_set.exists %}
{% include "meeting/group_proceedings.html" %}
{% endif %}
{% endifchanged %}
{% endfor %}
</tbody>
</table>
{% endif %}
{% endfor %}
<!-- Training Sessions -->

View file

@ -52,14 +52,14 @@
{% ifchanged %}
</tbody>
<tbody>
<tr><th class="warning" colspan="7">{{session.status|capfirst}}</th></tr>
<tr><th class="warning" colspan="7">{{session.current_status_name|capfirst}}</th></tr>
</tbody>
<tbody>
{% endifchanged %}
<tr>
<th>
<a href="{% url "ietf.secr.sreq.views.edit_mtg" num=meeting.number acronym=session.group.acronym %}">
<a href="{% url "ietf.secr.sreq.views.edit" num=meeting.number acronym=session.group.acronym %}">
{{session.group.acronym}}
</a>
</th>
@ -73,7 +73,7 @@
<td class="text-right">{{session.attendees|default:""}}</td>
<td>
<a href="mailto:{{session.requested_by.email_address}}">{{session.requested_by}}</a>
<a href="mailto:{{session.requested_by_person.email_address}}">{{session.requested_by_person}}</a>
</td>
<td>

View file

@ -9,9 +9,9 @@
{% if session.agenda_note %}<h3>{{session.agenda_note}}</h3>{% endif %}
{% if can_manage_materials %}
{% if session.status.slug == 'sched' or session.status.slug == 'schedw' %}
{% if session.current_status == 'sched' or session.current_status == 'schedw' %}
<div class="buttonlist">
{% if meeting.type.slug == 'interim' and user|has_role:"Secretariat" %}
{% if meeting.type_id == 'interim' and user|has_role:"Secretariat" %}
<a class="btn btn-default" href="{% url 'ietf.meeting.views.interim_request_details' number=meeting.number %}">Meeting Details</a>
{% endif %}
</div>

View file

@ -114,20 +114,12 @@
</thead>
<tbody>
{% for meeting in meetings %}
{% if meeting.type.slug == 'interim' %}
<tr id="row-{{ forloop.counter }}-{{ meeting.session_set.first.group.parent.acronym }}-{{ meeting.session_set.first.group.acronym }}">
{% else %}
<tr id="row-{{ forloop.counter }}-ietf">
{% endif %}
<tr id="row-{{ forloop.counter }}{% if meeting.responsible_group.parent %}-{{ meeting.responsible_group.parent.acronym }}{% endif %}-{{ meeting.responsible_group.acronym }}">
<td>{{ meeting.date }}</td>
{% if meeting.type.slug == 'interim' %}
<td>{{ meeting.session_set.all.0.group.acronym }}</td>
{% else %}
<td>ietf</td>
{% endif %}
<td>{{ meeting.responsible_group.acronym }}</td>
<td>
{% if meeting.type.slug == "interim" %}
<a href="{% url 'ietf.meeting.views.session_details' num=meeting.number acronym=meeting.session_set.all.0.group.acronym %}">{{ meeting.number }}{% if meeting.session_set.all.0.status.slug == "canceled" %}&nbsp;&nbsp;<span class="label label-warning">CANCELLED</span>{% endif %}</a>
{% if meeting.type_id == "interim" %}
<a href="{% url 'ietf.meeting.views.session_details' num=meeting.number acronym=meeting.session_set.all.0.group.acronym %}">{{ meeting.number }}{% if meeting.interim_meeting_cancelled %}&nbsp;&nbsp;<span class="label label-warning">CANCELLED</span>{% endif %}</a>
{% else %}
<a href="{% url 'ietf.meeting.views.agenda' num=meeting.number %}">IETF - {{ meeting.number }}</a>
{% endif %}