Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 60 additions & 1 deletion ietf/group/tests_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -2147,7 +2147,66 @@ def test_meeting_info(self):
self.assertEqual(response.status_code, 200)
q = PyQuery(response.content)
self.assertFalse(q('#inprogressmeets'))



class PendingInterimMeetingTests(TestCase):
"""Tests for the pending-interim warning on a group's meetings list.

The meetings page shows a ``#pending_warning`` banner when the group has an
interim meeting that is either awaiting approval (session status ``apprw``)
or approved but not yet announced (session status ``scheda``). See
ietf.meeting.helpers.has_pending_interim.
"""

def _meetings_page(self, group):
url = urlreverse('ietf.group.views.meetings', kwargs={'acronym': group.acronym})
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
return PyQuery(response.content)

def test_pending_approval_interim_shows_warning(self):
"""An interim awaiting approval (apprw) triggers the warning."""
group = GroupFactory.create(type_id='wg')
SessionFactory.create(meeting__type_id='interim', group=group, status_id='apprw')
q = self._meetings_page(group)
warning = q('#pending_warning')
self.assertTrue(warning)
# The warning links to both the pending-approval and to-be-announced views
# and names the group.
self.assertIn(urlreverse('ietf.meeting.views.interim_pending'),
[a.attrib['href'] for a in warning.find('a')])
self.assertIn(urlreverse('ietf.meeting.views.interim_announce'),
[a.attrib['href'] for a in warning.find('a')])
self.assertIn(group.acronym, warning.text())

def test_to_be_announced_interim_shows_warning(self):
"""An approved-but-unannounced interim (scheda) triggers the warning."""
group = GroupFactory.create(type_id='wg')
SessionFactory.create(meeting__type_id='interim', group=group, status_id='scheda')
q = self._meetings_page(group)
self.assertTrue(q('#pending_warning'))

def test_scheduled_interim_shows_no_warning(self):
"""A fully scheduled interim (sched) does not trigger the warning."""
group = GroupFactory.create(type_id='wg')
SessionFactory.create(meeting__type_id='interim', group=group, status_id='sched')
q = self._meetings_page(group)
self.assertFalse(q('#pending_warning'))

def test_no_interim_meetings_shows_no_warning(self):
"""A group with no interim meetings does not trigger the warning."""
group = GroupFactory.create(type_id='wg')
q = self._meetings_page(group)
self.assertFalse(q('#pending_warning'))

def test_pending_interim_for_other_group_not_shown(self):
"""A pending interim belonging to another group must not warn on this group."""
group = GroupFactory.create(type_id='wg')
other = GroupFactory.create(type_id='wg')
SessionFactory.create(meeting__type_id='interim', group=other, status_id='apprw')
q = self._meetings_page(group)
self.assertFalse(q('#pending_warning'))


class StatusUpdateTests(TestCase):

Expand Down
5 changes: 4 additions & 1 deletion ietf/group/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
#
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.helpers import get_meeting, has_pending_interim
from ietf.meeting.models import ImportantDate, SchedTimeSessAssignment, SchedulingEvent
from ietf.meeting.utils import group_sessions
from ietf.name.models import GroupTypeName, StreamName
Expand Down Expand Up @@ -953,6 +953,8 @@ def meetings(request, acronym, group_type=None):

future, in_progress, recent, past = group_sessions(sessions)

pending_interims_flag = has_pending_interim(group.acronym)

can_edit = group.has_role(request.user, group.features.groupman_roles)
can_always_edit = has_role(request.user, ["Secretariat", "Area Director"])

Expand Down Expand Up @@ -996,6 +998,7 @@ def meetings(request, acronym, group_type=None):
"can_edit": can_edit,
"can_always_edit": can_always_edit,
"cal_actions": cal_actions,
"pending_interims": pending_interims_flag,
},
),
)
Expand Down
34 changes: 32 additions & 2 deletions ietf/meeting/helpers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright The IETF Trust 2013-2022, All Rights Reserved
# Copyright The IETF Trust 2013-2026, All Rights Reserved
# -*- coding: utf-8 -*-


Expand Down Expand Up @@ -28,7 +28,7 @@
from ietf.mailtrigger.utils import gather_address_lists
from ietf.person.models import Person
from ietf.meeting.models import Meeting, Schedule, TimeSlot, SchedTimeSessAssignment, ImportantDate, SchedulingEvent, Session
from ietf.meeting.utils import session_requested_by, add_event_info_to_session_qs
from ietf.meeting.utils import session_requested_by, add_event_info_to_session_qs, data_for_meetings_overview
from ietf.name.models import ImportantDateName, SessionPurposeName
from ietf.utils import log, meetecho
from ietf.utils.mail import send_mail
Expand Down Expand Up @@ -773,6 +773,7 @@ def can_edit_interim_request(meeting, user):
def can_request_interim_meeting(user):
return can_manage_some_groups(user)


def can_view_interim_request(meeting, user):
'''Returns True if the user can see the pending interim request in the pending interim view'''
if meeting.type.slug != 'interim':
Expand Down Expand Up @@ -855,6 +856,29 @@ def get_earliest_session_date(formset):
def is_interim_meeting_approved(meeting):
return add_event_info_to_session_qs(meeting.session_set.all()).first().current_status == 'apprw'


def has_pending_interim(acronym):
'''
This function takes a group acronym and returns True if that group has at least
one pending interim meeting request or a to-be-announced interim request.
'''
rv = False
pending = data_for_meetings_overview(Meeting.objects.filter(type='interim'), interim_status='apprw')
for m in pending:
if m.responsible_group.acronym == acronym:
rv = True
break

if not rv:
to_be_announced = data_for_meetings_overview(Meeting.objects.filter(type='interim'), interim_status='scheda')
for m in to_be_announced:
if m.responsible_group.acronym == acronym:
rv = True
break

return rv


def get_next_interim_number(acronym,date):
'''
This function takes a group acronym and date object and returns the next number
Expand All @@ -869,6 +893,7 @@ def get_next_interim_number(acronym,date):
serial = 0
return "%s%02d" % (base, serial+1)


def get_next_agenda_name(meeting):
"""Returns the next name to use for an agenda document for *meeting*"""
group = meeting.session_set.first().group
Expand Down Expand Up @@ -941,6 +966,7 @@ def send_interim_approval_request(meetings):
context,
cc=cc_list)


def send_interim_approval(user, meeting):
"""Send an email to chairs and whoever initiated the action that resulted in approval that an interim is approved"""
first_session = meeting.session_set.first()
Expand All @@ -961,6 +987,7 @@ def send_interim_approval(user, meeting):
context,
cc=cc_list)


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"""
Expand All @@ -981,6 +1008,7 @@ def send_interim_announcement_request(meeting):
context,
cc_list)


def send_interim_meeting_cancellation_notice(meeting):
"""Sends an email that a scheduled interim meeting has been cancelled."""
session = meeting.session_set.first()
Expand Down Expand Up @@ -1182,12 +1210,14 @@ def update_interim_session_assignment(form):
session=session,
schedule=meeting.schedule)


def populate_important_dates(meeting):
assert ImportantDate.objects.filter(meeting=meeting).exists() is False
assert meeting.type_id=='ietf'
for datename in ImportantDateName.objects.filter(used=True):
ImportantDate.objects.create(meeting=meeting,name=datename,date=meeting.date+datetime.timedelta(days=datename.default_offset_days))


def update_important_dates(meeting):
assert meeting.type_id=='ietf'
for datename in ImportantDateName.objects.filter(used=True):
Expand Down
71 changes: 41 additions & 30 deletions ietf/meeting/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from django.core.cache import caches
from django.core.files.base import ContentFile
from django.db import IntegrityError
from django.db.models import OuterRef, Subquery, TextField, Q, Value, Max
from django.db.models import Exists, OuterRef, Subquery, TextField, Q, Value, Max
from django.db.models.functions import Coalesce
from django.template.loader import render_to_string
from django.utils import timezone
Expand Down Expand Up @@ -351,49 +351,60 @@ def data_for_meetings_overview(meetings, interim_status=None):
"""Return filtered meetings with sessions and group hierarchy (for the
interim menu)."""

# filter
if interim_status == "apprw":
session_status_condition = Q(current_status="apprw")
elif interim_status == "scheda":
session_status_condition = Q(current_status="scheda")
else:
session_status_condition = ~Q(
current_status__in=["apprw", "scheda", "canceledpa"]
)

meetings = meetings.filter(
~Q(type_id="interim")
| Exists(
Session.objects.filter(meeting=OuterRef("pk"))
.with_current_status()
.filter(session_status_condition)
)
)

# extract sessions
for m in meetings:
m.sessions = []

sessions = Session.objects.filter(
meeting__in=meetings
).order_by(
'meeting', 'pk'
).with_current_status(
).select_related(
'group', 'group__parent'
sessions = (
Session.objects.filter(
meeting__in=meetings,
meeting__type_id="interim",
)
.order_by("meeting", "pk")
.with_current_status()
.select_related("group", "group__parent")
)

meeting_dict = {m.pk: m for m in meetings}
for s in sessions.iterator():
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)
]

ietf_group = Group.objects.get(acronym='ietf')
ietf_group = (
Group.objects.get(acronym="ietf")
if any(m.type_id != "interim" for m in meetings)
else None
)

# 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)
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

Expand Down
57 changes: 42 additions & 15 deletions ietf/meeting/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4110,15 +4110,29 @@ def delete_schedule(request, num, owner, name):
# Interim Views
# -------------------------------------------------
def interim_announce(request):
'''View which shows interim meeting requests awaiting announcement'''
meetings = data_for_meetings_overview(Meeting.objects.filter(type='interim').order_by('date'), interim_status='scheda')
"""View which shows interim meeting requests awaiting announcement"""
# Ignore meetings older than this - gives people time to notice that a meeting was
# left as "scheda" and complain before the meeting falls off the visible list.
MAX_LOOKBACK = datetime.timedelta(days=28)

meetings = data_for_meetings_overview(
Meeting.objects.filter(
type="interim", date__gte=date_today() - MAX_LOOKBACK
).order_by("date"),
interim_status="scheda",
)
menu_entries = get_interim_menu_entries(request)
selected_menu_entry = 'announce'
selected_menu_entry = "announce"

return render(request, "meeting/interim_announce.html", {
'menu_entries': menu_entries,
'selected_menu_entry': selected_menu_entry,
'meetings': meetings})
return render(
request,
"meeting/interim_announce.html",
{
"menu_entries": menu_entries,
"selected_menu_entry": selected_menu_entry,
"meetings": meetings,
},
)


@role_required('Secretariat',)
Expand Down Expand Up @@ -4173,21 +4187,34 @@ def interim_skip_announcement(request, number):


def interim_pending(request):

'''View which shows interim meeting requests pending approval'''
meetings = data_for_meetings_overview(Meeting.objects.filter(type='interim').order_by('date'), interim_status='apprw')
"""View which shows interim meeting requests pending approval"""
# Ignore meetings older than this - gives people time to notice that a meeting was
# left as "apprw" and complain before the meeting falls off the visible list.
MAX_LOOKBACK = datetime.timedelta(days=28)

meetings = data_for_meetings_overview(
Meeting.objects.filter(
type="interim", date__gte=date_today() - MAX_LOOKBACK
).order_by("date"),
interim_status="apprw",
)

menu_entries = get_interim_menu_entries(request)
selected_menu_entry = 'pending'
selected_menu_entry = "pending"

for meeting in meetings:
if can_approve_interim_request(meeting, request.user):
meeting.can_approve = True

return render(request, "meeting/interim_pending.html", {
'menu_entries': menu_entries,
'selected_menu_entry': selected_menu_entry,
'meetings': meetings})
return render(
request,
"meeting/interim_pending.html",
{
"menu_entries": menu_entries,
"selected_menu_entry": selected_menu_entry,
"meetings": meetings,
},
)


@login_required
Expand Down
18 changes: 15 additions & 3 deletions ietf/person/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,9 +278,21 @@ def rfcs(self):
# When RfcAuthors are populated, this may over-return if an author is dropped
# from the author list between the final draft and the published RFC. Should
# ignore DocumentAuthors when an RfcAuthor exists for a draft.
rfcs = list(Document.objects.filter(type="rfc").filter(models.Q(documentauthor__person=self)|models.Q(rfcauthor__person=self)).distinct())
rfcs.sort(key=lambda d: d.name )
return rfcs
#
# The two authorship tables are queried separately and combined here. As a
# single ORed queryset, neither person_id index is usable and the join has to
# be materialized in full before being deduplicated.
ids = set(
Document.objects.filter(
type="rfc", documentauthor__person=self
).values_list("pk", flat=True)
)
ids.update(
Document.objects.filter(
type="rfc", rfcauthor__person=self
).values_list("pk", flat=True)
)
return sorted(Document.objects.filter(pk__in=ids), key=lambda d: d.name)

def active_drafts(self):
from ietf.doc.models import Document
Expand Down
Loading
Loading