datatracker/ietf/nomcom/utils.py

656 lines
28 KiB
Python

# Copyright The IETF Trust 2012-2020, All Rights Reserved
# -*- coding: utf-8 -*-
import datetime
import hashlib
import os
import re
import tempfile
from email import message_from_string, message_from_bytes
from email.header import decode_header
from email.iterators import typed_subpart_iterator
from email.utils import parseaddr
from django.db.models import Q, Count
from django.conf import settings
from django.contrib.sites.models import Site
from django.core.exceptions import ObjectDoesNotExist
from django.urls import reverse
from django.template.loader import render_to_string
from django.shortcuts import get_object_or_404
from ietf.dbtemplate.models import DBTemplate
from ietf.doc.models import DocEvent, NewRevisionDocEvent
from ietf.group.models import Group, Role
from ietf.person.models import Email, Person
from ietf.mailtrigger.utils import gather_address_lists
from ietf.meeting.models import Meeting
from ietf.utils.pipe import pipe
from ietf.utils.mail import send_mail_text, send_mail, get_payload_text
from ietf.utils.log import log
from ietf.person.name import unidecode_name
import debug # pyflakes:ignore
MAIN_NOMCOM_TEMPLATE_PATH = '/nomcom/defaults/'
QUESTIONNAIRE_TEMPLATE = 'position/questionnaire.txt'
HEADER_QUESTIONNAIRE_TEMPLATE = 'position/header_questionnaire.txt'
REQUIREMENTS_TEMPLATE = 'position/requirements'
HOME_TEMPLATE = 'home.rst'
INEXISTENT_PERSON_TEMPLATE = 'email/inexistent_person.txt'
NOMINEE_EMAIL_TEMPLATE = 'email/new_nominee.txt'
NOMINATION_EMAIL_TEMPLATE = 'email/new_nomination.txt'
NOMINEE_ACCEPT_REMINDER_TEMPLATE = 'email/nomination_accept_reminder.txt'
NOMINEE_QUESTIONNAIRE_REMINDER_TEMPLATE = 'email/questionnaire_reminder.txt'
NOMINATION_RECEIPT_TEMPLATE = 'email/nomination_receipt.txt'
FEEDBACK_RECEIPT_TEMPLATE = 'email/feedback_receipt.txt'
DESCRIPTION_TEMPLATE = 'topic/description'
IESG_GENERIC_REQUIREMENTS_TEMPLATE = 'iesg_requirements'
DEFAULT_NOMCOM_TEMPLATES = [HOME_TEMPLATE,
INEXISTENT_PERSON_TEMPLATE,
NOMINEE_EMAIL_TEMPLATE,
NOMINATION_EMAIL_TEMPLATE,
NOMINEE_ACCEPT_REMINDER_TEMPLATE,
NOMINEE_QUESTIONNAIRE_REMINDER_TEMPLATE,
NOMINATION_RECEIPT_TEMPLATE,
FEEDBACK_RECEIPT_TEMPLATE,
IESG_GENERIC_REQUIREMENTS_TEMPLATE,
]
# See RFC8713 section 4.15
DISQUALIFYING_ROLE_QUERY_EXPRESSION = ( Q(group__acronym__in=['isocbot', 'ietf-trust', 'llc-board', 'iab'], name_id__in=['member', 'chair'])
| Q(group__type_id='area', group__state='active',name_id='ad')
)
def get_nomcom_by_year(year):
from ietf.nomcom.models import NomCom
return get_object_or_404(NomCom,
group__acronym__icontains=year,
)
def get_year_by_nomcom(nomcom):
acronym = nomcom.group.acronym
m = re.search(r'(?P<year>\d\d\d\d)', acronym)
return m.group(0)
def get_user_email(user):
# a user object already has an email field, but we don't want to
# overwrite anything that might be there, and we don't know that
# what's there is the right thing, so we cache the lookup results in a
# separate attribute
if not hasattr(user, "_email_cache"):
user._email_cache = None
if hasattr(user, "person"):
emails = user.person.email_set.filter(active=True).order_by('-time')
if emails:
user._email_cache = emails[0]
for email in emails:
if email.address == user.username:
user._email_cache = email
else:
try:
user._email_cache = Email.objects.get(address=user.username)
except ObjectDoesNotExist:
pass
return user._email_cache
def get_hash_nominee_position(date, nominee_position_id):
return hashlib.md5(('%s%s%s' % (settings.SECRET_KEY, date, nominee_position_id)).encode('utf-8')).hexdigest()
def initialize_templates_for_group(group):
for template_name in DEFAULT_NOMCOM_TEMPLATES:
template_path = MAIN_NOMCOM_TEMPLATE_PATH + template_name
template = DBTemplate.objects.get(path=template_path)
DBTemplate.objects.create(
group=group.group,
title=template.title,
path='/nomcom/' + group.group.acronym + '/' + template_name,
variables=template.variables,
type_id=template.type_id,
content=template.content)
def initialize_questionnaire_for_position(position):
questionnaire_path = MAIN_NOMCOM_TEMPLATE_PATH + QUESTIONNAIRE_TEMPLATE
header_questionnaire_path = MAIN_NOMCOM_TEMPLATE_PATH + HEADER_QUESTIONNAIRE_TEMPLATE
template = DBTemplate.objects.get(path=questionnaire_path)
header_template = DBTemplate.objects.get(path=header_questionnaire_path)
DBTemplate.objects.create(
group=position.nomcom.group,
title=header_template.title + ' [%s]' % position.name,
path='/nomcom/' + position.nomcom.group.acronym + '/' + str(position.id) + '/' + HEADER_QUESTIONNAIRE_TEMPLATE,
variables=header_template.variables,
type_id=header_template.type_id,
content=header_template.content)
questionnaire = DBTemplate.objects.create(
group=position.nomcom.group,
title=template.title + ' [%s]' % position.name,
path='/nomcom/' + position.nomcom.group.acronym + '/' + str(position.id) + '/' + QUESTIONNAIRE_TEMPLATE,
variables=template.variables,
type_id=template.type_id,
content=template.content)
return questionnaire
def initialize_requirements_for_position(position):
requirements_path = MAIN_NOMCOM_TEMPLATE_PATH + REQUIREMENTS_TEMPLATE
template = DBTemplate.objects.get(path=requirements_path)
return DBTemplate.objects.create(
group=position.nomcom.group,
title=template.title + ' [%s]' % position.name,
path='/nomcom/' + position.nomcom.group.acronym + '/' + str(position.id) + '/' + REQUIREMENTS_TEMPLATE,
variables=template.variables,
type_id=template.type_id,
content=template.content)
def initialize_description_for_topic(topic):
description_path = MAIN_NOMCOM_TEMPLATE_PATH + DESCRIPTION_TEMPLATE
template = DBTemplate.objects.get(path=description_path)
return DBTemplate.objects.create(
group=topic.nomcom.group,
title=template.title + ' [%s]' % topic.subject,
path='/nomcom/' + topic.nomcom.group.acronym + '/topic/' + str(topic.id) + '/' + DESCRIPTION_TEMPLATE,
variables=template.variables,
type_id=template.type_id,
content=template.content)
def delete_nomcom_templates(nomcom):
nomcom_template_path = '/nomcom/' + nomcom.group.acronym
DBTemplate.objects.filter(path__contains=nomcom_template_path).delete()
def retrieve_nomcom_private_key(request, year):
private_key = request.session.get('NOMCOM_PRIVATE_KEY_%s' % year, None)
if not private_key:
return private_key
command = "%s bf -d -in /dev/stdin -k \"%s\" -a"
code, out, error = pipe(command % (settings.OPENSSL_COMMAND,
settings.SECRET_KEY), private_key)
if code != 0:
log("openssl error: %s:\n Error %s: %s" %(command, code, error))
return out
def store_nomcom_private_key(request, year, private_key):
if not private_key:
request.session['NOMCOM_PRIVATE_KEY_%s' % year] = ''
else:
command = "%s bf -e -in /dev/stdin -k \"%s\" -a"
code, out, error = pipe(command % (settings.OPENSSL_COMMAND,
settings.SECRET_KEY), private_key)
if code != 0:
log("openssl error: %s:\n Error %s: %s" %(command, code, error))
if error:
out = ''
request.session['NOMCOM_PRIVATE_KEY_%s' % year] = out
def validate_private_key(key):
key_file = tempfile.NamedTemporaryFile(delete=False)
key_file.write(key.encode('utf-8'))
key_file.close()
command = "%s rsa -in %s -check -noout"
code, out, error = pipe(command % (settings.OPENSSL_COMMAND,
key_file.name))
if code != 0:
log("openssl error: %s:\n Error %s: %s" %(command, code, error))
os.unlink(key_file.name)
return (not error, error)
def validate_public_key(public_key):
key_file = tempfile.NamedTemporaryFile(delete=False)
for chunk in public_key.chunks():
key_file.write(chunk)
key_file.close()
command = "%s x509 -in %s -noout"
code, out, error = pipe(command % (settings.OPENSSL_COMMAND,
key_file.name))
if code != 0:
log("openssl error: %s:\n Error %s: %s" %(command, code, error))
os.unlink(key_file.name)
return (not error, error)
def send_accept_reminder_to_nominee(nominee_position):
today = datetime.date.today().strftime('%Y%m%d')
subject = 'Reminder: please accept (or decline) your nomination.'
domain = Site.objects.get_current().domain
position = nominee_position.position
nomcom = position.nomcom
from_email = settings.NOMCOM_FROM_EMAIL.format(year=nomcom.year())
nomcom_template_path = '/nomcom/%s/' % nomcom.group.acronym
mail_path = nomcom_template_path + NOMINEE_ACCEPT_REMINDER_TEMPLATE
nominee = nominee_position.nominee
(to_email, cc) = gather_address_lists('nomination_accept_reminder',nominee=nominee.email.address)
hash = get_hash_nominee_position(today, nominee_position.id)
accept_url = reverse('ietf.nomcom.views.process_nomination_status',
None,
args=(get_year_by_nomcom(nomcom),
nominee_position.id,
'accepted',
today,
hash))
decline_url = reverse('ietf.nomcom.views.process_nomination_status',
None,
args=(get_year_by_nomcom(nomcom),
nominee_position.id,
'declined',
today,
hash))
context = {'nominee': nominee.person.name,
'position': position,
'domain': domain,
'accept_url': accept_url,
'decline_url': decline_url,
'year': nomcom.year(),
}
body = render_to_string(mail_path, context)
path = '%s%d/%s' % (nomcom_template_path, position.id, QUESTIONNAIRE_TEMPLATE)
body += '\n\n%s' % render_to_string(path, context)
send_mail_text(None, to_email, from_email, subject, body, cc=cc)
def send_questionnaire_reminder_to_nominee(nominee_position):
subject = 'Reminder: please complete the Nomcom questionnaires for your nomination.'
domain = Site.objects.get_current().domain
position = nominee_position.position
nomcom = position.nomcom
from_email = settings.NOMCOM_FROM_EMAIL.format(year=nomcom.year())
nomcom_template_path = '/nomcom/%s/' % nomcom.group.acronym
mail_path = nomcom_template_path + NOMINEE_QUESTIONNAIRE_REMINDER_TEMPLATE
nominee = nominee_position.nominee
(to_email,cc) = gather_address_lists('nomcom_questionnaire_reminder',nominee=nominee.email.address)
context = {'nominee': nominee.person.name,
'position': position,
'domain': domain,
'year': nomcom.year(),
}
body = render_to_string(mail_path, context)
path = '%s%d/%s' % (nomcom_template_path, position.id, QUESTIONNAIRE_TEMPLATE)
body += '\n\n%s' % render_to_string(path, context)
send_mail_text(None, to_email, from_email, subject, body, cc=cc)
def send_reminder_to_nominees(nominees,type):
addrs = []
if type=='accept':
for nominee in nominees:
for nominee_position in nominee.nomineeposition_set.pending():
send_accept_reminder_to_nominee(nominee_position)
addrs.append(nominee_position.nominee.email.address)
elif type=='questionnaire':
for nominee in nominees:
for nominee_position in nominee.nomineeposition_set.accepted().without_questionnaire_response():
send_questionnaire_reminder_to_nominee(nominee_position)
addrs.append(nominee_position.nominee.email.address)
return addrs
def make_nomineeposition(nomcom, candidate, position, author):
from ietf.nomcom.models import Nominee, NomineePosition
nomcom_template_path = '/nomcom/%s/' % nomcom.group.acronym
# Add the nomination for a particular position
nominee, created = Nominee.objects.get_or_create(person=candidate,email=candidate.email(), nomcom=nomcom)
while nominee.duplicated:
nominee = nominee.duplicated
nominee_position, nominee_position_created = NomineePosition.objects.get_or_create(position=position, nominee=nominee)
if nominee_position_created:
# send email to nominee
subject = 'IETF Nomination Information'
from_email = settings.NOMCOM_FROM_EMAIL.format(year=nomcom.year())
(to_email, cc) = gather_address_lists('nomination_new_nominee',nominee=nominee.email.address)
domain = Site.objects.get_current().domain
today = datetime.date.today().strftime('%Y%m%d')
hash = get_hash_nominee_position(today, nominee_position.id)
accept_url = reverse('ietf.nomcom.views.process_nomination_status',
None,
args=(nomcom.year(),
nominee_position.id,
'accepted',
today,
hash))
decline_url = reverse('ietf.nomcom.views.process_nomination_status',
None,
args=(nomcom.year(),
nominee_position.id,
'declined',
today,
hash))
context = {'nominee': nominee.person.name,
'position': position.name,
'year': nomcom.year(),
'domain': domain,
'accept_url': accept_url,
'decline_url': decline_url,
}
path = nomcom_template_path + NOMINEE_EMAIL_TEMPLATE
send_mail(None, to_email, from_email, subject, path, context, cc=cc)
# send email to nominee with questionnaire
if nomcom.send_questionnaire:
subject = '%s Questionnaire' % position
from_email = settings.NOMCOM_FROM_EMAIL.format(year=nomcom.year())
(to_email, cc) = gather_address_lists('nomcom_questionnaire',nominee=nominee.email.address)
context = {'nominee': nominee.person.name,
'position': position.name,
'year' : nomcom.year(),
}
path = '%s%d/%s' % (nomcom_template_path,
position.id, HEADER_QUESTIONNAIRE_TEMPLATE)
body = render_to_string(path, context)
path = '%s%d/%s' % (nomcom_template_path,
position.id, QUESTIONNAIRE_TEMPLATE)
body += '\n\n%s' % render_to_string(path, context)
send_mail_text(None, to_email, from_email, subject, body, cc=cc)
# send emails to nomcom chair
subject = 'Nomination Information'
from_email = settings.NOMCOM_FROM_EMAIL.format(year=nomcom.year())
(to_email, cc) = gather_address_lists('nomination_received',nomcom=nomcom)
context = {'nominee': nominee.person.name,
'nominee_email': nominee.email.address,
'position': position.name,
'year': nomcom.year(),
}
if author:
context.update({'nominator': author.person.name,
'nominator_email': author.address})
else:
context.update({'nominator': 'Anonymous',
'nominator_email': ''})
path = nomcom_template_path + NOMINATION_EMAIL_TEMPLATE
send_mail(None, to_email, from_email, subject, path, context, cc=cc)
return nominee
def make_nomineeposition_for_newperson(nomcom, candidate_name, candidate_email, position, author):
# This is expected to fail if called with an existing email address
email = Email.objects.create(address=candidate_email, origin="nominee: %s" % nomcom.group.acronym)
person = Person.objects.create(name=candidate_name,
ascii=unidecode_name(candidate_name),
)
email.person = person
email.save()
# send email to secretariat and nomcomchair to warn about the new person
subject = 'New person is created'
from_email = settings.NOMCOM_FROM_EMAIL.format(year=nomcom.year())
(to_email, cc) = gather_address_lists('nomination_created_person',nomcom=nomcom)
context = {'email': email.address,
'fullname': email.person.name,
'person_id': email.person.id,
'year': nomcom.year(),
}
nomcom_template_path = '/nomcom/%s/' % nomcom.group.acronym
path = nomcom_template_path + INEXISTENT_PERSON_TEMPLATE
send_mail(None, to_email, from_email, subject, path, context, cc=cc)
return make_nomineeposition(nomcom, email.person, position, author)
def getheader(header_text, default="ascii"):
"""Decode the specified header"""
tuples = decode_header(header_text)
header_sections = [ text.decode(charset or default) if isinstance(text, bytes) else text for text, charset in tuples]
return "".join(header_sections)
def get_charset(message, default="ascii"):
"""Get the message charset"""
if message.get_content_charset():
return message.get_content_charset()
if message.get_charset():
return message.get_charset()
return default
def get_body(message):
"""Get the body of the email message"""
if message.is_multipart():
# get the plain text version only
text_parts = [part for part in typed_subpart_iterator(message,
'text',
'plain')]
body = []
for part in text_parts:
charset = get_charset(part)
body.append(get_payload_text(part, default_charset=charset))
return "\n".join(body).strip()
else: # if it is not multipart, the payload will be a string
# representing the message body
body = get_payload_text(message)
return body.strip()
def parse_email(text):
if isinstance(text, bytes):
msg = message_from_bytes(text)
elif isinstance(text, str):
msg = message_from_string(text)
else:
raise ValueError("Expected email message text to be str or bytes")
body = get_body(msg)
subject = getheader(msg['Subject'])
__, addr = parseaddr(msg['From'])
return addr.lower(), subject, body
def create_feedback_email(nomcom, msg):
from ietf.nomcom.models import Feedback
by, subject, body = parse_email(msg)
#name, addr = parseaddr(by)
feedback = Feedback(nomcom=nomcom,
author=by,
subject=subject or '',
comments=nomcom.encrypt(body))
feedback.save()
return feedback
class EncryptedException(Exception):
pass
def remove_disqualified(queryset):
disqualified_roles = Role.objects.filter(DISQUALIFYING_ROLE_QUERY_EXPRESSION)
return queryset.exclude(role__in=disqualified_roles)
def is_eligible(person, nomcom=None, date=None):
return list_eligible(nomcom=nomcom, date=date, base_qs=Person.objects.filter(pk=person.pk)).exists()
def list_eligible(nomcom=None, date=None, base_qs=None):
if not base_qs:
base_qs = Person.objects.all()
eligibility_date = get_eligibility_date(nomcom, date)
if eligibility_date.year in range(2008,2020):
return list_eligible_8713(date=eligibility_date, base_qs=base_qs)
elif eligibility_date.year == 2020:
return list_eligible_8788(date=eligibility_date, base_qs=base_qs)
elif eligibility_date.year in (2021,2022):
return list_eligible_8989(date=eligibility_date, base_qs=base_qs)
else:
return Person.objects.none()
def decorate_volunteers_with_qualifications(volunteers, nomcom=None, date=None, base_qs=None):
if not base_qs:
base_qs = Person.objects.all()
eligibility_date = get_eligibility_date(nomcom, date)
if eligibility_date.year in (2021,2022):
three_of_five_qs, officer_qs, author_qs = get_8989_eligibility_querysets(eligibility_date, base_qs)
for v in volunteers:
qualifications = []
if v.person in three_of_five_qs:
qualifications.append('path_1')
if v.person in officer_qs:
qualifications.append('path_2')
if v.person in author_qs:
qualifications.append('path_3')
v.qualifications = ", ".join(qualifications)
else:
for v in volunteers:
v.qualifications = ''
def list_eligible_8713(date, base_qs=None):
if not base_qs:
base_qs = Person.objects.all()
previous_five = previous_five_meetings(date)
return remove_disqualified(three_of_five_eligible(previous_five=previous_five, queryset=base_qs))
def list_eligible_8788(date, base_qs=None):
if not base_qs:
base_qs = Person.objects.all()
previous_five = Meeting.objects.filter(number__in=['102','103','104','105','106'])
return remove_disqualified(three_of_five_eligible(previous_five=previous_five, queryset=base_qs))
def get_8989_eligibility_querysets(date, base_qs):
if not base_qs:
base_qs = Person.objects.all()
previous_five = previous_five_meetings(date)
three_of_five_qs = three_of_five_eligible(previous_five=previous_five, queryset=base_qs)
three_years_ago = datetime.date(date.year-3,date.month,date.day)
officer_qs = base_qs.filter(
# is currently an officer
Q(role__name_id__in=('chair','secr'),
role__group__state_id='active',
role__group__type_id='wg',
role__group__time__lte=date,
)
# was an officer since the given date (I think this is wrong - it looks at when roles _start_, not when roles end)
| Q(rolehistory__group__time__gte=three_years_ago,
rolehistory__group__time__lte=date,
rolehistory__name_id__in=('chair','secr'),
rolehistory__group__state_id='active',
rolehistory__group__type_id='wg',
)
).distinct()
five_years_ago = datetime.date(date.year-5,date.month,date.day)
rfc_pks = set(DocEvent.objects.filter(type='published_rfc',time__gte=five_years_ago,time__lte=date).values_list('doc__pk',flat=True))
iesgappr_pks = set(DocEvent.objects.filter(type='iesg_approved',time__gte=five_years_ago,time__lte=date).values_list('doc__pk',flat=True))
qualifying_pks = rfc_pks.union(iesgappr_pks.difference(rfc_pks))
author_qs = base_qs.filter(
documentauthor__document__pk__in=qualifying_pks
).annotate(
document_author_count = Count('documentauthor')
).filter(document_author_count__gte=2)
return three_of_five_qs, officer_qs, author_qs
def list_eligible_8989(date, base_qs=None):
if not base_qs:
base_qs = Person.objects.all()
three_of_five_qs, officer_qs, author_qs = get_8989_eligibility_querysets(date, base_qs)
# Would be nice to use queryset union here, but the annotations in the three existing querysets make that difficult
three_of_five_pks = three_of_five_qs.values_list('pk',flat=True)
officer_pks = officer_qs.values_list('pk',flat=True)
author_pks = author_qs.values_list('pk',flat=True)
return remove_disqualified(Person.objects.filter(pk__in=set(three_of_five_pks).union(set(officer_pks)).union(set(author_pks))))
def get_eligibility_date(nomcom=None, date=None):
if date:
return date
elif nomcom:
if nomcom.first_call_for_volunteers:
return nomcom.first_call_for_volunteers
else:
return datetime.date(int(nomcom.group.acronym[6:]),5,1)
else:
last_seated=Role.objects.filter(group__type_id='nomcom',name_id='member').order_by('-group__acronym').first()
if last_seated:
last_nomcom_year = int(last_seated.group.acronym[6:])
if last_nomcom_year == datetime.date.today().year:
next_nomcom_year = last_nomcom_year
else:
next_nomcom_year = int(last_seated.group.acronym[6:])+1
next_nomcom_group = Group.objects.filter(acronym=f'nomcom{next_nomcom_year}').first()
if next_nomcom_group and next_nomcom_group.nomcom_set.first().first_call_for_volunteers:
return next_nomcom_group.nomcom_set.first().first_call_for_volunteers
else:
return datetime.date(next_nomcom_year,5,1)
else:
return datetime.date(datetime.date.today().year,5,1)
def previous_five_meetings(date = None):
if date is None:
date = datetime.date.today()
return Meeting.objects.filter(type='ietf',date__lte=date).order_by('-date')[:5]
def three_of_five_eligible(previous_five, queryset=None):
""" Return a list of Person records who attended at least
3 of the 5 type_id='ietf' meetings before the given
date. Does not disqualify anyone based on held roles.
"""
if queryset is None:
queryset = Person.objects.all()
return queryset.filter(meetingregistration__meeting__in=list(previous_five),meetingregistration__attended=True).annotate(mtg_count=Count('meetingregistration')).filter(mtg_count__gte=3)
def new_three_of_five_eligible(previous_five, queryset=None):
""" Return a list of Person records who attended at least
3 of the 5 type_id='ietf' meetings before the given
date. Does not disqualify anyone based on held roles.
This 'new' variant bases the calculation on the Meeting.Session model rather than Stats.MeetingRegistration
Leadership will have to create a new RFC specifying eligibility (RFC8989 is timing out) before it can be used.
"""
if queryset is None:
queryset = Person.objects.all()
return queryset.filter(
Q(attended__session__meeting__in=list(previous_five)),
Q(attended__session__type='plenary')|Q(attended__session__group__type__in=['wg','rg'])
).annotate(mtg_count=Count('attended__session__meeting',distinct=True)).filter(mtg_count__gte=3)
def suggest_affiliation(person):
recent_meeting = person.meetingregistration_set.order_by('-meeting__date').first()
affiliation = recent_meeting.affiliation if recent_meeting else ''
if not affiliation:
recent_volunteer = person.volunteer_set.order_by('-nomcom__group__acronym').first()
if recent_volunteer:
affiliation = recent_volunteer.affiliation
if not affiliation:
recent_draft_revision = NewRevisionDocEvent.objects.filter(doc__type_id='draft',doc__documentauthor__person=person).order_by('-time').first()
if recent_draft_revision:
affiliation = recent_draft_revision.doc.documentauthor_set.filter(person=person).first().affiliation
return affiliation
def extract_volunteers(year):
nomcom = get_nomcom_by_year(year)
# pull list of volunteers
# get queryset of all eligible (from utils)
# decorate members of the list with eligibility
volunteers = nomcom.volunteer_set.all()
eligible = list_eligible(nomcom)
for v in volunteers:
v.eligible = v.person in eligible
decorate_volunteers_with_qualifications(volunteers,nomcom=nomcom)
volunteers = sorted(volunteers,key=lambda v:(not v.eligible,v.person.last_name()))
return nomcom, volunteers