datatracker/ietf/meeting/helpers.py
Henrik Levkowetz 52c5c0daa0 Merged in [11792] from rcross@amsl.com:
Requests for virtual interim meeting should go directly Scheduled - Waiting for announcement state and an email notification sent to the secretariat.  Fixes issue #1994.
 - Legacy-Id: 11822
Note: SVN reference [11792] has been migrated to Git commit a13b751e1a
2016-08-18 15:03:55 +00:00

655 lines
24 KiB
Python

# Copyright The IETF Trust 2007, All Rights Reserved
import datetime
import os
import re
from tempfile import mkstemp
from django.http import HttpRequest, Http404
from django.db.models import Max, Q, Prefetch, F
from django.conf import settings
from django.core.cache import cache
from django.core.urlresolvers import reverse
from django.utils.cache import get_cache_key
from django.shortcuts import get_object_or_404
from django.template.loader import render_to_string
import debug # pyflakes:ignore
from ietf.doc.models import Document
from ietf.doc.utils import get_document_content
from ietf.group.models import Group
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
from ietf.utils.history import find_history_active_at, find_history_replacements_active_at
from ietf.utils.mail import send_mail
from ietf.utils.pipe import pipe
def find_ads_for_meeting(meeting):
ads = []
meeting_time = datetime.datetime.combine(meeting.date, datetime.time(0, 0, 0))
num = 0
# get list of ADs which are/were active at the time of the meeting.
# (previous [x for x in y] syntax changed to aid debugging)
for g in Group.objects.filter(type="area").order_by("acronym"):
history = find_history_active_at(g, meeting_time)
num = num +1
if history and history != g:
#print " history[%u]: %s" % (num, history)
if history.state_id == "active":
for x in history.rolehistory_set.filter(name="ad",group__type='area').select_related('group', 'person', 'email'):
#print "xh[%u]: %s" % (num, x)
ads.append(x)
else:
#print " group[%u]: %s" % (num, g)
if g.state_id == "active":
for x in g.role_set.filter(name="ad",group__type='area').select_related('group', 'person', 'email'):
#print "xg[%u]: %s (#%u)" % (num, x, x.pk)
ads.append(x)
return ads
# get list of all areas, + IRTF + IETF (plenaries).
def get_pseudo_areas():
return Group.objects.filter(Q(state="active", name="IRTF")|
Q(state="active", name="IETF")|
Q(state="active", type="area")).order_by('acronym')
# get list of all areas, + IRTF.
def get_areas():
return Group.objects.filter(Q(state="active",
name="IRTF")|
Q(state="active", type="area")).order_by('acronym')
# get list of areas that are referenced.
def get_area_list_from_sessions(assignments, num):
return assignments.filter(timeslot__type = 'Session',
session__group__parent__isnull = False).order_by(
'session__group__parent__acronym').distinct().values_list(
'session__group__parent__acronym',flat=True)
def build_all_agenda_slices(meeting):
time_slices = []
date_slices = {}
for ts in meeting.timeslot_set.filter(type__in=['session',]).order_by('time','name'):
ymd = ts.time.date()
if ymd not in date_slices and ts.location != None:
date_slices[ymd] = []
time_slices.append(ymd)
if ymd in date_slices:
if [ts.time, ts.time+ts.duration] not in date_slices[ymd]: # only keep unique entries
date_slices[ymd].append([ts.time, ts.time+ts.duration])
time_slices.sort()
return time_slices,date_slices
def get_all_assignments_from_schedule(schedule):
ss = schedule.assignments.filter(timeslot__location__isnull = False)
ss = ss.filter(session__type__slug='session')
ss = ss.order_by('timeslot__time','timeslot__name')
return ss
def get_modified_from_assignments(assignments):
return assignments.aggregate(Max('timeslot__modified'))['timeslot__modified__max']
def get_wg_name_list(assignments):
return assignments.filter(timeslot__type = 'Session',
session__group__isnull = False,
session__group__parent__isnull = False).order_by(
'session__group__acronym').distinct().values_list(
'session__group__acronym',flat=True)
def get_wg_list(assignments):
wg_name_list = get_wg_name_list(assignments)
return Group.objects.filter(acronym__in = set(wg_name_list)).order_by('parent__acronym','acronym')
def get_meetings(num=None,type_in=['ietf',]):
meetings = Meeting.objects
if type_in:
meetings = meetings.filter(type__in=type_in)
if num == None:
meetings = meetings.order_by("-date")
else:
meetings = meetings.filter(number=num)
return meetings
def get_meeting(num=None,type_in=['ietf',]):
meetings = get_meetings(num,type_in)
if meetings.exists():
return meetings.first()
else:
raise Http404("No such meeting found: %s" % num)
def get_schedule(meeting, name=None):
if name is None:
schedule = meeting.agenda
else:
schedule = get_object_or_404(meeting.schedule_set, name=name)
return schedule
def get_schedule_by_id(meeting, schedid):
if schedid is None:
schedule = meeting.agenda
else:
schedule = get_object_or_404(meeting.schedule_set, id=int(schedid))
return schedule
# seems this belongs in ietf/person/utils.py?
def get_person_by_email(email):
# email == None may actually match people who haven't set an email!
if email is None:
return None
return Person.objects.filter(email__address=email).distinct().first()
def get_schedule_by_name(meeting, owner, name):
if owner is not None:
return meeting.schedule_set.filter(owner = owner, name = name).first()
else:
return meeting.schedule_set.filter(name = name).first()
def preprocess_assignments_for_agenda(assignments_queryset, meeting):
# prefetch some stuff to prevent a myriad of small, inefficient queries
assignments_queryset = assignments_queryset.select_related(
"timeslot", "timeslot__location", "timeslot__type",
"session",
"session__group", "session__group__charter", "session__group__charter__group",
).prefetch_related(
Prefetch("session__materials",
queryset=Document.objects.exclude(states__type=F("type"),states__slug='deleted').select_related("group").order_by("order"),
to_attr="prefetched_active_materials",
),
"timeslot__meeting",
)
assignments = list(assignments_queryset) # make sure we're set in stone
meeting_time = datetime.datetime.combine(meeting.date, datetime.time())
# replace groups with historic counterparts
for a in assignments:
if a.session:
a.session.historic_group = None
groups = [a.session.group for a in assignments if a.session and a.session.group]
group_replacements = find_history_replacements_active_at(groups, meeting_time)
for a in assignments:
if a.session and a.session.group:
a.session.historic_group = group_replacements.get(a.session.group_id)
# replace group parents with historic counterparts
for a in assignments:
if a.session and a.session.historic_group:
a.session.historic_group.historic_parent = None
parents = Group.objects.filter(pk__in=set(a.session.historic_group.parent_id for a in assignments if a.session and a.session.historic_group and a.session.historic_group.parent_id))
parent_replacements = find_history_replacements_active_at(parents, meeting_time)
for a in assignments:
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)
return assignments
def read_agenda_file(num, doc):
# XXXX FIXME: the path fragment in the code below should be moved to
# settings.py. The *_PATH settings should be generalized to format()
# style python format, something like this:
# DOC_PATH_FORMAT = { "agenda": "/foo/bar/agenda-{meeting.number}/agenda-{meeting-number}-{doc.group}*", }
path = os.path.join(settings.AGENDA_PATH, "%s/agenda/%s" % (num, doc.external_url))
if os.path.exists(path):
with open(path) as f:
return f.read()
else:
return None
def convert_draft_to_pdf(doc_name):
inpath = os.path.join(settings.IDSUBMIT_REPOSITORY_PATH, doc_name + ".txt")
outpath = os.path.join(settings.INTERNET_DRAFT_PDF_PATH, doc_name + ".pdf")
try:
infile = open(inpath, "r")
except IOError:
return
t,tempname = mkstemp()
os.close(t)
tempfile = open(tempname, "w")
pageend = 0;
newpage = 0;
formfeed = 0;
for line in infile:
line = re.sub("\r","",line)
line = re.sub("[ \t]+$","",line)
if re.search("\[?[Pp]age [0-9ivx]+\]?[ \t]*$",line):
pageend=1
tempfile.write(line)
continue
if re.search("^[ \t]*\f",line):
formfeed=1
tempfile.write(line)
continue
if re.search("^ *INTERNET.DRAFT.+[0-9]+ *$",line) or re.search("^ *Internet.Draft.+[0-9]+ *$",line) or re.search("^draft-[-a-z0-9_.]+.*[0-9][0-9][0-9][0-9]$",line) or re.search("^RFC.+[0-9]+$",line):
newpage=1
if re.search("^[ \t]*$",line) and pageend and not newpage:
continue
if pageend and newpage and not formfeed:
tempfile.write("\f")
pageend=0
formfeed=0
newpage=0
tempfile.write(line)
infile.close()
tempfile.close()
t,psname = mkstemp()
os.close(t)
pipe("enscript --margins 76::76: -B -q -p "+psname + " " +tempname)
os.unlink(tempname)
pipe("ps2pdf "+psname+" "+outpath)
os.unlink(psname)
def agenda_permissions(meeting, schedule, user):
# do this in positive logic.
cansee = False
canedit = False
secretariat = False
if has_role(user, 'Secretariat'):
cansee = True
secretariat = True
# NOTE: secretariat is not superuser for edit!
elif schedule.public:
cansee = True
elif schedule.visible and has_role(user, ['Area Director', 'IAB Chair', 'IRTF Chair']):
cansee = True
if user_is_person(user, schedule.owner):
cansee = True
canedit = True
return cansee, canedit, secretariat
def session_constraint_expire(request,session):
from ajax import session_constraints
path = reverse(session_constraints, args=[session.meeting.number, session.pk])
temp_request = HttpRequest()
temp_request.path = path
temp_request.META['HTTP_HOST'] = request.META['HTTP_HOST']
key = get_cache_key(temp_request)
if key is not None and cache.has_key(key):
cache.delete(key)
# -------------------------------------------------
# Interim Meeting Helpers
# -------------------------------------------------
def assign_interim_session(form):
"""Helper function to create a timeslot and assign the interim session"""
time = datetime.datetime.combine(
form.cleaned_data['date'],
form.cleaned_data['time'])
session = form.instance
if session.official_timeslotassignment():
slot = session.official_timeslotassignment().timeslot
slot.time = time
slot.save()
else:
slot = TimeSlot.objects.create(
meeting=session.meeting,
type_id="session",
duration=session.requested_duration,
time=time)
SchedTimeSessAssignment.objects.create(
timeslot=slot,
session=session,
schedule=session.meeting.agenda)
def can_approve_interim_request(meeting, user):
'''Returns True if the user has permissions to approve an interim meeting request'''
if meeting.type.slug != 'interim':
return False
if has_role(user, 'Secretariat'):
return True
person = get_person_for_user(user)
session = meeting.session_set.first()
if not session:
return False
group = session.group
if group.type.slug == 'wg' and group.parent.role_set.filter(name='ad', person=person):
return True
if group.type.slug == 'rg' and group.parent.role_set.filter(name='chair', person=person):
return True
return False
def can_edit_interim_request(meeting, user):
'''Returns True if the user can edit the interim meeting request'''
if meeting.type.slug != 'interim':
return False
if has_role(user, 'Secretariat'):
return True
person = get_person_for_user(user)
session = meeting.session_set.first()
if not session:
return False
group = session.group
if group.role_set.filter(name='chair', person=person):
return True
elif can_approve_interim_request(meeting, user):
return True
else:
return False
def can_request_interim_meeting(user):
if has_role(user, ('Secretariat', 'Area Director', 'WG Chair', 'IRTF Chair', 'RG Chair')):
return True
return False
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':
return False
if has_role(user, 'Secretariat'):
return True
person = get_person_for_user(user)
session = meeting.session_set.first()
if not session:
return False
group = session.group
if has_role(user, 'Area Director') and group.type.slug == 'wg':
return True
if has_role(user, 'IRTF Chair') and group.type.slug == 'rg':
return True
if group.role_set.filter(name='chair', person=person):
return True
return False
def create_interim_meeting(group, date, city='', country='', timezone='UTC',
person=None):
"""Helper function to create interim meeting and associated schedule"""
if not person:
person = Person.objects.get(name='(System)')
number = get_next_interim_number(group.acronym, date)
meeting = Meeting.objects.create(
number=number,
type_id='interim',
date=date,
city=city,
country=country,
time_zone=timezone)
schedule = Schedule.objects.create(
meeting=meeting,
owner=person,
visible=True,
public=True)
meeting.agenda = schedule
meeting.save()
return meeting
def get_announcement_initial(meeting, is_change=False):
'''Returns a dictionary suitable to initialize an InterimAnnouncementForm
(Message ModelForm)'''
group = meeting.session_set.first().group
in_person = bool(meeting.city)
initial = {}
initial['to'] = settings.INTERIM_ANNOUNCE_TO_EMAIL
initial['cc'] = group.list_email
initial['frm'] = settings.INTERIM_ANNOUNCE_FROM_EMAIL
if in_person:
desc = 'Interim'
else:
desc = 'Virtual'
if is_change:
change = ' CHANGED'
else:
change = ''
if group.type.slug == 'rg':
type = 'RG'
elif group.type.slug == 'wg' and group.state.slug == 'bof':
type = 'BOF'
else:
type = 'WG'
initial['subject'] = '{name} ({acronym}) {type} {desc} Meeting: {date}{change}'.format(
name=group.name,
acronym=group.acronym,
type=type,
desc=desc,
date=meeting.date,
change=change)
body = render_to_string('meeting/interim_announcement.txt', locals())
initial['body'] = body
return initial
def get_earliest_session_date(formset):
'''Return earliest date from InterimSession Formset'''
return sorted([f.cleaned_data['date'] for f in formset.forms if f.cleaned_data.get('date')])[0]
def get_interim_initial(meeting):
'''Returns a dictionary suitable to initialize a InterimRequestForm'''
initial = {}
initial['group'] = meeting.session_set.first().group
if meeting.city:
initial['in_person'] = True
else:
initial['in_person'] = False
if meeting.session_set.count() > 1:
initial['meeting_type'] = 'multi-day'
else:
initial['meeting_type'] = 'single'
if meeting.session_set.first().status.slug == 'apprw':
initial['approved'] = False
else:
initial['approved'] = True
return initial
def get_interim_session_initial(meeting):
'''Returns a list of dictionaries suitable to initialize a InterimSessionForm'''
initials = []
for session in meeting.session_set.all():
initial = {}
initial['date'] = session.official_timeslotassignment().timeslot.time
initial['time'] = session.official_timeslotassignment().timeslot.time
initial['duration'] = session.requested_duration
initial['remote_instructions'] = session.remote_instructions
initial['agenda_note'] = session.agenda_note
doc = session.agenda()
if doc:
path = os.path.join(doc.get_file_path(), doc.filename_with_rev())
initial['agenda'] = get_document_content(os.path.basename(path), path, markup=False)
initials.append(initial)
return initials
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 get_next_interim_number(acronym,date):
'''
This function takes a group acronym and date object and returns the next number
to use for an interim meeting. The format is interim-[year]-[acronym]-[01-99]
'''
base = 'interim-%s-%s-' % (date.year, acronym)
# can't use count() to calculate the next number in case one was deleted
meetings = Meeting.objects.filter(type='interim', number__startswith=base)
if meetings:
serial = sorted([ int(x.number.split('-')[-1]) for x in meetings ])[-1]
else:
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
documents = Document.objects.filter(type='agenda', session__meeting=meeting)
if documents:
sequences = [int(d.name.split('-')[-1]) for d in documents]
last_sequence = sorted(sequences)[-1]
else:
last_sequence = 0
return 'agenda-{meeting}-{group}-{sequence}'.format(
meeting=meeting.number,
group=group.acronym,
sequence=str(last_sequence + 1).zfill(2))
def make_materials_directories(meeting):
'''
This function takes a meeting object and creates the appropriate materials directories
'''
path = meeting.get_materials_path()
# Default umask is 0x022, meaning strip write premission for group and others.
# Change this temporarily to 0x0, to keep write permission for group and others.
# (WHY??) (Note: this code is old -- was present already when the secretariat code
# was merged with the regular datatracker code; then in secr/proceedings/views.py
# in make_directories())
saved_umask = os.umask(0)
for leaf in ('slides','agenda','minutes','id','rfc','bluesheets'):
target = os.path.join(path,leaf)
if not os.path.exists(target):
os.makedirs(target)
os.umask(saved_umask)
def send_interim_approval_request(meetings):
"""Sends an email to the secretariat, group chairs, and resposnible 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
(to_email, cc_list) = gather_address_lists('session_requested',group=group,person=requester)
from_email = ('"IETF Meeting Session Request Tool"','session_request_developers@ietf.org')
subject = '{group} - New Interim Meeting Request'.format(group=group.acronym)
template = 'meeting/interim_approval_request.txt'
approval_urls = []
for meeting in meetings:
url = settings.IDTRACKER_BASE_URL + reverse('ietf.meeting.views.interim_request_details', kwargs={'number': meeting.number})
approval_urls.append(url)
if len(meetings) > 1:
is_series = True
else:
is_series = False
context = locals()
send_mail(None,
to_email,
from_email,
subject,
template,
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"""
group = meeting.session_set.first().group
requester = meeting.session_set.first().requested_by
(to_email, cc_list) = gather_address_lists('interim_approved')
from_email = ('"IETF Meeting Session Request Tool"','session_request_developers@ietf.org')
subject = '{group} - interim meeting ready for announcement'.format(group=group.acronym)
template = 'meeting/interim_announcement_request.txt'
announce_url = settings.IDTRACKER_BASE_URL + reverse('ietf.meeting.views.interim_request_details', kwargs={'number': meeting.number})
context = locals()
send_mail(None,
to_email,
from_email,
subject,
template,
context,
cc_list)
def send_interim_cancellation_notice(meeting):
"""Sends an email that a scheduled interim meeting has been cancelled."""
session = meeting.session_set.first()
group = session.group
to_email = settings.INTERIM_ANNOUNCE_TO_EMAIL
(_, cc_list) = gather_address_lists('session_request_cancelled',group=group)
from_email = settings.INTERIM_ANNOUNCE_FROM_EMAIL
subject = '{group} ({acronym}) {type} Interim Meeting Cancelled (was {date})'.format(
group=group.name,
acronym=group.acronym,
type=group.type.slug.upper(),
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
template = 'meeting/interim_cancellation_notice.txt'
context = locals()
send_mail(None,
to_email,
from_email,
subject,
template,
context,
cc=cc_list)
def send_interim_minutes_reminder(meeting):
"""Sends an email reminding chairs to submit minutes of interim *meeting*"""
session = meeting.session_set.first()
group = session.group
(to_email, cc_list) = gather_address_lists('session_minutes_reminder',group=group)
from_email = 'proceedings@ietf.org'
subject = 'Action Required: Minutes from {group} ({acronym}) {type} Interim Meeting on {date}'.format(
group=group.name,
acronym=group.acronym,
type=group.type.slug.upper(),
date=meeting.date.strftime('%Y-%m-%d'))
template = 'meeting/interim_minutes_reminder.txt'
context = locals()
send_mail(None,
to_email,
from_email,
subject,
template,
context,
cc=cc_list)
def check_interim_minutes():
"""Finds interim meetings that occured 10 days ago, if they don't
have minutes send a reminder."""
date = datetime.datetime.today() - datetime.timedelta(days=10)
meetings = Meeting.objects.filter(type='interim', session__status='sched', date=date)
for meeting in meetings:
if not meeting.session_set.first().minutes():
send_interim_minutes_reminder(meeting)
def sessions_post_save(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 ('date' in form.changed_data) or ('time' in form.changed_data):
assign_interim_session(form)
if 'agenda' in form.changed_data:
form.save_agenda()