From 3957743b8508830f9b294248b13bf7d2eaefac35 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Thu, 5 Dec 2019 12:41:09 +0000 Subject: [PATCH] Move Session.status, .requested, and .requested_by to a new SchedulingEvent - Legacy-Id: 17122 --- ietf/api/views.py | 2 +- ietf/doc/tests.py | 11 +- ietf/doc/tests_material.py | 11 +- ietf/doc/views_doc.py | 12 +- ietf/group/models.py | 2 +- ietf/group/views.py | 13 +- ietf/mailtrigger/models.py | 5 +- ietf/meeting/admin.py | 45 ++- ietf/meeting/ajax.py | 10 +- ietf/meeting/factories.py | 26 +- ietf/meeting/forms.py | 11 +- ietf/meeting/helpers.py | 47 ++- .../migrations/0022_schedulingevent.py | 31 ++ .../0023_create_scheduling_events.py | 67 ++++ .../migrations/0024_auto_20191204_1731.py | 28 ++ ietf/meeting/models.py | 102 +++--- ietf/meeting/resources.py | 26 +- ietf/meeting/test_data.py | 30 +- ietf/meeting/tests_views.py | 78 +++-- ietf/meeting/utils.py | 175 ++++++++-- ietf/meeting/views.py | 302 +++++++++++------- ietf/secr/meetings/tests.py | 13 +- ietf/secr/meetings/views.py | 115 ++++--- ietf/secr/proceedings/forms.py | 7 +- ietf/secr/proceedings/proc_utils.py | 30 +- ietf/secr/proceedings/tests.py | 17 +- ietf/secr/proceedings/views.py | 4 +- ietf/secr/sreq/tests.py | 20 +- ietf/secr/sreq/urls.py | 4 +- ietf/secr/sreq/views.py | 208 +++++++----- ietf/secr/templates/meetings/non_session.html | 2 +- .../secr/templates/meetings/session_edit.html | 8 +- ietf/secr/templates/meetings/sessions.html | 2 +- ietf/secr/templates/sreq/view.html | 2 +- ietf/secr/utils/group.py | 35 +- ietf/settings.py | 2 +- .../doc/material/presentations-row.html | 4 +- ietf/templates/meeting/agenda.html | 4 +- ietf/templates/meeting/agenda.txt | 2 +- ietf/templates/meeting/interim_announce.html | 12 +- ietf/templates/meeting/interim_pending.html | 80 +++-- .../meeting/interim_request_cancel.html | 4 +- .../meeting/interim_request_details.html | 18 +- ietf/templates/meeting/past.html | 58 ++-- ietf/templates/meeting/proceedings.html | 106 +++--- ietf/templates/meeting/requests.html | 6 +- .../meeting/session_details_panel.html | 4 +- ietf/templates/meeting/upcoming.html | 16 +- 48 files changed, 1143 insertions(+), 674 deletions(-) create mode 100644 ietf/meeting/migrations/0022_schedulingevent.py create mode 100644 ietf/meeting/migrations/0023_create_scheduling_events.py create mode 100644 ietf/meeting/migrations/0024_auto_20191204_1731.py diff --git a/ietf/api/views.py b/ietf/api/views.py index 2376d23e4..c25a25f56 100644 --- a/ietf/api/views.py +++ b/ietf/api/views.py @@ -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', diff --git a/ietf/doc/tests.py b/ietf/doc/tests.py index 63d986e59..0f5a29736 100644 --- a/ietf/doc/tests.py +++ b/ietf/doc/tests.py @@ -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))) diff --git a/ietf/doc/tests_material.py b/ietf/doc/tests_material.py index 47c71f90d..c67525cdd 100644 --- a/ietf/doc/tests_material.py +++ b/ietf/doc/tests_material.py @@ -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")) diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py index 603c1650a..f14b963c0 100644 --- a/ietf/doc/views_doc.py +++ b/ietf/doc/views_doc.py @@ -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) diff --git a/ietf/group/models.py b/ietf/group/models.py index aecbf3287..ceb60f23b 100644 --- a/ietf/group/models.py +++ b/ietf/group/models.py @@ -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 diff --git a/ietf/group/views.py b/ietf/group/views.py index e757709f1..a93d71930 100644 --- a/ietf/group/views.py +++ b/ietf/group/views.py @@ -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) diff --git a/ietf/mailtrigger/models.py b/ietf/mailtrigger/models.py index e55324ac0..a7a8e13c8 100644 --- a/ietf/mailtrigger/models.py +++ b/ietf/mailtrigger/models.py @@ -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): diff --git a/ietf/meeting/admin.py b/ietf/meeting/admin.py index 0cac9d327..fcc3c5966 100644 --- a/ietf/meeting/admin.py +++ b/ietf/meeting/admin.py @@ -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", ] diff --git a/ietf/meeting/ajax.py b/ietf/meeting/ajax.py index a7aae7acb..1ad694423 100644 --- a/ietf/meeting/ajax.py +++ b/ietf/meeting/ajax.py @@ -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), diff --git a/ietf/meeting/factories.py b/ietf/meeting/factories.py index 0da2c2320..e72303151 100644 --- a/ietf/meeting/factories.py +++ b/ietf/meeting/factories.py @@ -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 ''' diff --git a/ietf/meeting/forms.py b/ietf/meeting/forms.py index a79eb29b7..c30e72560 100644 --- a/ietf/meeting/forms.py +++ b/ietf/meeting/forms.py @@ -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): diff --git a/ietf/meeting/helpers.py b/ietf/meeting/helpers.py index 0c15b4446..0206ac355 100644 --- a/ietf/meeting/helpers.py +++ b/ietf/meeting/helpers.py @@ -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: diff --git a/ietf/meeting/migrations/0022_schedulingevent.py b/ietf/meeting/migrations/0022_schedulingevent.py new file mode 100644 index 000000000..4ea0091ac --- /dev/null +++ b/ietf/meeting/migrations/0022_schedulingevent.py @@ -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')), + ], + ), + ] diff --git a/ietf/meeting/migrations/0023_create_scheduling_events.py b/ietf/meeting/migrations/0023_create_scheduling_events.py new file mode 100644 index 000000000..d1d07d149 --- /dev/null +++ b/ietf/meeting/migrations/0023_create_scheduling_events.py @@ -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), + ] diff --git a/ietf/meeting/migrations/0024_auto_20191204_1731.py b/ietf/meeting/migrations/0024_auto_20191204_1731.py new file mode 100644 index 000000000..7f7454137 --- /dev/null +++ b/ietf/meeting/migrations/0024_auto_20191204_1731.py @@ -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', + ), + ] diff --git a/ietf/meeting/models.py b/ietf/meeting/models.py index 04a0d4c3a..ef9b58574 100644 --- a/ietf/meeting/models.py +++ b/ietf/meeting/models.py @@ -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) diff --git a/ietf/meeting/resources.py b/ietf/meeting/resources.py index 6bcdf33cb..049196413 100644 --- a/ietf/meeting/resources.py +++ b/ietf/meeting/resources.py @@ -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') diff --git a/ietf/meeting/test_data.py b/ietf/meeting/test_data.py index e311c4be3..d8508567a 100644 --- a/ietf/meeting/test_data.py +++ b/ietf/meeting/test_data.py @@ -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 diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py index 7c23c3301..f27216250 100644 --- a/ietf/meeting/tests_views.py +++ b/ietf/meeting/tests_views.py @@ -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) diff --git a/ietf/meeting/utils.py b/ietf/meeting/utils.py index 022100488..5b1944fd0 100644 --- a/ietf/meeting/utils.py +++ b/ietf/meeting/utils.py @@ -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 diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index 6b1b6f1ce..b1278ecf8 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -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, diff --git a/ietf/secr/meetings/tests.py b/ietf/secr/meetings/tests.py index f9f762e2f..f826be015 100644 --- a/ietf/secr/meetings/tests.py +++ b/ietf/secr/meetings/tests.py @@ -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() diff --git a/ietf/secr/meetings/views.py b/ietf/secr/meetings/views.py index 23d646c10..baea035e3 100644 --- a/ietf/secr/meetings/views.py +++ b/ietf/secr/meetings/views.py @@ -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') diff --git a/ietf/secr/proceedings/forms.py b/ietf/secr/proceedings/forms.py index 7243d4e10..dc3b49846 100644 --- a/ietf/secr/proceedings/forms.py +++ b/ietf/secr/proceedings/forms.py @@ -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: diff --git a/ietf/secr/proceedings/proc_utils.py b/ietf/secr/proceedings/proc_utils.py index 14c3efef5..c8cd6323b 100644 --- a/ietf/secr/proceedings/proc_utils.py +++ b/ietf/secr/proceedings/proc_utils.py @@ -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): diff --git a/ietf/secr/proceedings/tests.py b/ietf/secr/proceedings/tests.py index 432cf46cb..2004b88f6 100644 --- a/ietf/secr/proceedings/tests.py +++ b/ietf/secr/proceedings/tests.py @@ -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) diff --git a/ietf/secr/proceedings/views.py b/ietf/secr/proceedings/views.py index ceb1f28c4..a51a22531 100644 --- a/ietf/secr/proceedings/views.py +++ b/ietf/secr/proceedings/views.py @@ -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 diff --git a/ietf/secr/sreq/tests.py b/ietf/secr/sreq/tests.py index 3d01dc0c5..668faa7a2 100644 --- a/ietf/secr/sreq/tests.py +++ b/ietf/secr/sreq/tests.py @@ -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") diff --git a/ietf/secr/sreq/urls.py b/ietf/secr/sreq/urls.py index c8337e4fc..7e0db8117 100644 --- a/ietf/secr/sreq/urls.py +++ b/ietf/secr/sreq/urls.py @@ -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[A-Za-z0-9_\-\+]+)/%(acronym)s/edit/$' % settings.URL_REGEXPS, views.edit_mtg), + url(r'^(?P[A-Za-z0-9_\-\+]+)/%(acronym)s/edit/$' % settings.URL_REGEXPS, views.edit), ] diff --git a/ietf/secr/sreq/views.py b/ietf/secr/sreq/views.py index 8ddd68ee5..69ffdb8b7 100644 --- a/ietf/secr/sreq/views.py +++ b/ietf/secr/sreq/views.py @@ -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 diff --git a/ietf/secr/templates/meetings/non_session.html b/ietf/secr/templates/meetings/non_session.html index adf0ce8c7..bf5418c8c 100644 --- a/ietf/secr/templates/meetings/non_session.html +++ b/ietf/secr/templates/meetings/non_session.html @@ -24,7 +24,7 @@ {% for assignment in assignments %} - + {{ assignment.timeslot.time|date:"D" }} {{ assignment.timeslot.time|date:"H:i" }}-{{ assignment.timeslot.end_time|date:"H:i" }} {{ assignment.timeslot.name }} diff --git a/ietf/secr/templates/meetings/session_edit.html b/ietf/secr/templates/meetings/session_edit.html index 8837448d8..7d9cd6025 100644 --- a/ietf/secr/templates/meetings/session_edit.html +++ b/ietf/secr/templates/meetings/session_edit.html @@ -23,19 +23,19 @@ Day: - {{ timeslot.time|date:"l" }} + {% if timeslot %}{{ timeslot.time|date:"l" }}{% endif %} Time: - {{ timeslot.time|time:"H:i" }} + {% if timeslot %}{{ timeslot.time|time:"H:i" }}{% endif %} Room: - {{ timeslot.location.name }} + {% if timeslot %}{{ timeslot.location.name }}{% endif %} Status: - {{ session.status }} + {{ current_status_name }} {{ form }} diff --git a/ietf/secr/templates/meetings/sessions.html b/ietf/secr/templates/meetings/sessions.html index ecd57c8c9..a6de5fa08 100644 --- a/ietf/secr/templates/meetings/sessions.html +++ b/ietf/secr/templates/meetings/sessions.html @@ -33,7 +33,7 @@ {% endif %} {{ session.agenda_note }} - {{ session.status }} + {{ session.current_status_name }} Edit
diff --git a/ietf/secr/templates/sreq/view.html b/ietf/secr/templates/sreq/view.html index b5b116dbd..838b1f99e 100644 --- a/ietf/secr/templates/sreq/view.html +++ b/ietf/secr/templates/sreq/view.html @@ -29,7 +29,7 @@
    -
  • +
  • {% if show_approve_button %}
  • {% endif %} diff --git a/ietf/secr/utils/group.py b/ietf/secr/utils/group.py index 6fe69a56b..3895367ec 100644 --- a/ietf/secr/utils/group.py +++ b/ietf/secr/utils/group.py @@ -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 diff --git a/ietf/settings.py b/ietf/settings.py index d9faba0d1..fb74794d4 100644 --- a/ietf/settings.py +++ b/ietf/settings.py @@ -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', ] diff --git a/ietf/templates/doc/material/presentations-row.html b/ietf/templates/doc/material/presentations-row.html index 0516a3558..e16b04815 100644 --- a/ietf/templates/doc/material/presentations-row.html +++ b/ietf/templates/doc/material/presentations-row.html @@ -21,10 +21,10 @@ {% ifchanged s.meeting %}{% if s.meeting.type.slug == 'ietf' %}IETF{% endif %}{{s.meeting.number}}{% endifchanged %} {% if s.name %}{{ s.name }}
    {% 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 %} {% if s.agenda %}Agenda{% endif %} diff --git a/ietf/templates/meeting/agenda.html b/ietf/templates/meeting/agenda.html index 1dbc3cea1..b2f11286d 100644 --- a/ietf/templates/meeting/agenda.html +++ b/ietf/templates/meeting/agenda.html @@ -223,7 +223,7 @@ {{item.timeslot.name}} {% endif %} - {% if item.session.status.slug == 'canceled' %} + {% if item.session.current_status == 'canceled' %} CANCELLED {% endif %} @@ -316,7 +316,7 @@ BOF {% endif %} - {% if item.session.status.slug == 'canceled' %} + {% if item.session.current_status == 'canceled' %} CANCELLED {% endif %} diff --git a/ietf/templates/meeting/agenda.txt b/ietf/templates/meeting/agenda.txt index 96cf3dd20..241b87711 100644 --- a/ietf/templates/meeting/agenda.txt +++ b/ietf/templates/meeting/agenda.txt @@ -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 %} diff --git a/ietf/templates/meeting/interim_announce.html b/ietf/templates/meeting/interim_announce.html index b9b8ee5f4..280e11cfa 100644 --- a/ietf/templates/meeting/interim_announce.html +++ b/ietf/templates/meeting/interim_announce.html @@ -35,17 +35,9 @@ {% for meeting in meetings %} - {% if meeting.type.slug == 'interim' %} - - {% else %} - - {% endif %} + {{ meeting.date }} - {% if meeting.type.slug == 'interim' %} - {{ meeting.session_set.all.0.group.acronym }} - {% else %} - ietf - {% endif %} + {{ meeting.responsible_group.acronym }} {{ meeting.number }} diff --git a/ietf/templates/meeting/interim_pending.html b/ietf/templates/meeting/interim_pending.html index a4e003da7..0bba822cf 100644 --- a/ietf/templates/meeting/interim_pending.html +++ b/ietf/templates/meeting/interim_pending.html @@ -14,54 +14,46 @@ {% origin %}

    Pending Interim Meetings

    - {% if menu_entries %} - - {% endif %} + {% if menu_entries %} + + {% endif %} - {% if meetings %} - - - - - - - - - - - {% for meeting in meetings %} - {% if meeting.type.slug == 'interim' %} - - {% else %} - - {% endif %} - - {% if meeting.type.slug == 'interim' %} - - {% else %} - - {% endif %} - + + + {% endfor %} + +
    DateGroupName
    {{ meeting.date }}{{ meeting.session_set.all.0.group.acronym }}ietf - {% if meeting.type.slug == "interim" %} - {{ meeting.number }}{% if meeting.session_set.all.0.status.slug == "canceled" %} -- CANCELLED --{% endif %} + {% if meetings %} + + + + + + + + + + + {% for meeting in meetings %} + + + + - - - {% endfor %} - -
    DateGroupName
    {{ meeting.date }}{{ meeting.responsible_group.acronym }} + {% if meeting.type_id == "interim" %} + {{ meeting.number }}{% if meeting.interim_meeting_cancelled %}  CANCELLED{% endif %} {% else %} IETF - {{ meeting.number }} {% endif %} - {% if meeting.can_approve %}can be approved{% endif %}
    - {% else %} -

    No pending interim meetings

    - {% endif %} +
    {% if meeting.can_approve %}can be approved{% endif %}
    + {% else %} +

    No pending interim meetings

    + {% endif %} {% endblock %} diff --git a/ietf/templates/meeting/interim_request_cancel.html b/ietf/templates/meeting/interim_request_cancel.html index 908a0cf7e..da93af2d9 100644 --- a/ietf/templates/meeting/interim_request_cancel.html +++ b/ietf/templates/meeting/interim_request_cancel.html @@ -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 %} @@ -13,7 +13,7 @@ {% block content %} {% origin %} -

    Cancel Interim Meeting {% if meeting.session_set.first.status.slug != "sched" %}Request{% endif %}

    +

    {% block title %}Cancel Interim Meeting {% if session_status != "sched" %}Request{% endif %}{% endblock %}

    {% csrf_token %} diff --git a/ietf/templates/meeting/interim_request_details.html b/ietf/templates/meeting/interim_request_details.html index 8e8ed82f9..9d3aaf0ac 100644 --- a/ietf/templates/meeting/interim_request_details.html +++ b/ietf/templates/meeting/interim_request_details.html @@ -15,11 +15,11 @@

    Interim Meeting Request Details

    Group
    -
    {{ sessions.0.group.acronym }} +
    {{ group.acronym }}
    Requested By
    -
    {{ sessions.0.requested_by }} +
    {{ requester }}
    Status
    -
    {{ sessions.0.status }}
    +
    {{ session_status.name }}
    City
    {{ meeting.city }}
    Country
    @@ -46,24 +46,24 @@ {% if can_edit %} Edit {% endif %} - {% if can_approve and sessions.0.status.slug == 'apprw' %} + {% if can_approve and session_status.slug == 'apprw' %} {% endif %} - {% if user|has_role:"Secretariat" and sessions.0.status.slug == 'scheda' %} + {% if user|has_role:"Secretariat" and session_status.slug == 'scheda' %} Announce Skip Announcement {% 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' %} Cancel Meeting {% endif %} {% endif %} - {% if sessions.0.status.slug == "apprw" %} + {% if session_status.slug == "apprw" %} Back - {% elif sessions.0.status.slug == "scheda" %} + {% elif session_status.slug == "scheda" %} Back - {% elif sessions.0.status.slug == "sched" %} + {% elif session_status.slug == "sched" %} Back {% else %} Back diff --git a/ietf/templates/meeting/past.html b/ietf/templates/meeting/past.html index c0e88174b..00d8d3918 100644 --- a/ietf/templates/meeting/past.html +++ b/ietf/templates/meeting/past.html @@ -5,7 +5,7 @@ {% load ietf_filters staticfiles %} {% block pagehead %} - + {% endblock %} {% block bodyAttrs %}data-spy="scroll" data-target="#affix"{% endblock %} @@ -75,47 +75,43 @@ {% else %}
    No past meetings are available.
    {% endif %} - +
- {% if meetings %} -

- - - - - - - - - - {% for meeting in meetings %} - {% if meeting.type.slug == 'interim' %} - - {% else %} - - {% endif %} - - {% if meeting.type.slug == 'interim' %} - + {% if meetings %} +

+
DateGroupName
{{ meeting.date }} - {{ meeting.session_set.all.0.group.acronym }} -
+ + + + + + + + + {% for meeting in meetings %} + + + + {{ meeting.responsible_group.acronym }} {% endif %} - + + - - + {% endifchanged %} @@ -73,7 +73,7 @@ {% for meeting in meetings %} - {% if meeting.type.slug == 'interim' %} - - {% else %} - - {% endif %} + - {% if meeting.type.slug == 'interim' %} - - {% else %} - - {% endif %} +
DateGroupName
{{ meeting.date }} + {% if meeting.responsible_group.type_id != 'ietf' %} + {{ meeting.responsible_group.acronym }} {% else %} - ietf - {% if meeting.type.slug == "interim" %} - {{ meeting.number }}{% if meeting.session_set.all.0.status.slug == "canceled" %}  CANCELLED{% endif %} + + {% if meeting.type_id == "interim" %} + {{ meeting.number }}{% if meeting.interim_meeting_cancelled %}  CANCELLED{% endif %} {% else %} IETF - {{ meeting.number }} {% endif %} - - {% if meeting.type.slug == "interim" %} + + {% if meeting.type_id == "interim" %} {% else %} {% if meeting.get_number > 97 %} Important dates diff --git a/ietf/templates/meeting/proceedings.html b/ietf/templates/meeting/proceedings.html index 10260515a..4700aae74 100644 --- a/ietf/templates/meeting/proceedings.html +++ b/ietf/templates/meeting/proceedings.html @@ -68,60 +68,58 @@ {% endif %} - {% regroup ietf|dictsort:"group.parent.acronym" by group.parent.name as areas %} - {% for sessions in areas %} -

{{sessions.list.0.group.parent.acronym|upper}} {{ sessions.grouper }}

- {% regroup sessions.list by not_meeting as meet_or_not %} - {% for batch in meet_or_not %} - {% if not batch.grouper %} - - - - - - - - - - - - {% for session in batch.list|dictsort:"group.acronym" %} - {% ifchanged session.group.acronym %} - {% include "meeting/group_proceedings.html" %} - {% endifchanged %} - {% endfor %} - -
GroupArtifactsRecordingsSlidesDrafts
- {% else %} -

{{sessions.grouper }} groups not meeting: - {% for session in batch.list|dictsort:"group.acronym" %} - {% ifchanged session.group.acronym %} - {{session.group.acronym}}{% if not forloop.last %},{% endif %} - {% endifchanged %} - {% endfor %} -

- - - - - - - - - - - - {% 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 %} - -
     
- {% endif %} - {% endfor %} + {% for area, meeting_sessions, not_meeting_sessions in ietf_areas %} +

{{ area.acronym|upper }} {{ area.name }}

+ {% if meeting_sessions %} + + + + + + + + + + + + {% for session in meeting_sessions %} + {% ifchanged session.group.acronym %} + {% include "meeting/group_proceedings.html" %} + {% endifchanged %} + {% endfor %} + +
GroupArtifactsRecordingsSlidesDrafts
+ {% endif %} + + {% if not_meeting_sessions %} +

{{ area.name }} groups not meeting: + {% for session in not_meeting_sessions %} + {% ifchanged session.group.acronym %} + {{ session.group.acronym }}{% if not forloop.last %},{% endif %} + {% endifchanged %} + {% endfor %} +

+ + + + + + + + + + + + {% for session in not_meeting_sessions %} + {% ifchanged session.group.acronym %} + {% if session.sessionpresentation_set.exists %} + {% include "meeting/group_proceedings.html" %} + {% endif %} + {% endifchanged %} + {% endfor %} + +
     
+ {% endif %} {% endfor %} diff --git a/ietf/templates/meeting/requests.html b/ietf/templates/meeting/requests.html index a37ce500e..de5e4a28a 100644 --- a/ietf/templates/meeting/requests.html +++ b/ietf/templates/meeting/requests.html @@ -52,14 +52,14 @@ {% ifchanged %}
{{session.status|capfirst}}
{{session.current_status_name|capfirst}}
- + {{session.group.acronym}} {{session.attendees|default:""}} - {{session.requested_by}} + {{session.requested_by_person}} diff --git a/ietf/templates/meeting/session_details_panel.html b/ietf/templates/meeting/session_details_panel.html index fcbbf0a3c..63fa639a8 100644 --- a/ietf/templates/meeting/session_details_panel.html +++ b/ietf/templates/meeting/session_details_panel.html @@ -9,9 +9,9 @@ {% if session.agenda_note %}

{{session.agenda_note}}

{% 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' %}
- {% if meeting.type.slug == 'interim' and user|has_role:"Secretariat" %} + {% if meeting.type_id == 'interim' and user|has_role:"Secretariat" %} Meeting Details {% endif %}
diff --git a/ietf/templates/meeting/upcoming.html b/ietf/templates/meeting/upcoming.html index 3a6618ad5..c280030a1 100644 --- a/ietf/templates/meeting/upcoming.html +++ b/ietf/templates/meeting/upcoming.html @@ -114,20 +114,12 @@
{{ meeting.date }}{{ meeting.session_set.all.0.group.acronym }}ietf{{ meeting.responsible_group.acronym }} - {% if meeting.type.slug == "interim" %} - {{ meeting.number }}{% if meeting.session_set.all.0.status.slug == "canceled" %}  CANCELLED{% endif %} + {% if meeting.type_id == "interim" %} + {{ meeting.number }}{% if meeting.interim_meeting_cancelled %}  CANCELLED{% endif %} {% else %} IETF - {{ meeting.number }} {% endif %}