Skip to content

Commit 8675711

Browse files
committed
Merged [5902],[5904] from markus.stenberg@iki.fi:
Added support and scripts for generation of wg- and draft-aliases. Fixes issue ietf-tools#713. - Legacy-Id: 5937 Note: SVN reference [5902] has been migrated to Git commit e5b551f Note: SVN reference [5904] has been migrated to Git commit e68e51c
1 parent 9f998e5 commit 8675711

5 files changed

Lines changed: 361 additions & 3 deletions

File tree

ietf/bin/generate-draft-aliases

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
# -*- Python -*-
4+
#
5+
# $Id: generate-draft-aliases $
6+
#
7+
# Author: Markus Stenberg <markus.stenberg@iki.fi>
8+
#
9+
"""
10+
11+
This code dumps Django model InternetDraft's contents as postfix email
12+
aliases
13+
14+
<no suffix> (same as -authors)
15+
.authors (list of authors)
16+
.chairs (WG chairs)
17+
.notify (notify emails(?))
18+
.ad (sponsoring AD)
19+
.all (all of the above)
20+
21+
TODO:
22+
23+
- results somewhat inconsistent with the results from the old tool;
24+
should examine why (ask me for diff tool if interested in fixing it)
25+
26+
"""
27+
28+
DRAFT_EMAIL_SUFFIX='@tools.ietf.org'
29+
30+
# boilerplate (from various other ietf/bin scripts)
31+
import os, sys
32+
basedir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))
33+
sys.path = [ basedir ] + sys.path
34+
35+
from ietf import settings
36+
from django.core import management
37+
management.setup_environ(settings)
38+
39+
from ietf.doc.models import Document
40+
from ietf.group.utils import get_group_chairs_emails, get_group_ads_emails
41+
from ietf.utils.aliases import *
42+
import time
43+
44+
def get_draft_ad_emails(draft):
45+
" Get AD email for the given draft, if any. "
46+
# If working group document, return current WG ADs
47+
wg = draft.group
48+
if wg and wg.acronym != 'none' and wg.parent and wg.parent.acronym != 'none':
49+
return get_group_ads_emails(wg)
50+
# If not, return explicit AD set (whether up to date or not)
51+
ad = draft.ad
52+
#return [ad and ad.user and ad.user.email]
53+
return [ad and ad.email_address()]
54+
55+
def get_draft_authors_emails(draft):
56+
" Get list of authors for the given draft."
57+
58+
# This feels 'correct'; however, it creates fairly large delta
59+
return [email.email_address() for email in draft.authors.all()]
60+
61+
# This gives fairly small delta compared to current state,
62+
# however, it seems to be wrong (doesn't check for emails being
63+
# active etc).
64+
#return [email.address for email in draft.authors.all()]
65+
66+
def get_draft_notify_emails(draft):
67+
" Get list of email addresses to notify for the given draft."
68+
n = draft.notify
69+
if not n:
70+
return
71+
l = []
72+
draft_email = draft.name + DRAFT_EMAIL_SUFFIX
73+
for e in n.split(','):
74+
# If the draft name itself is listed as notify list element, we
75+
# expand it (to make results better verifiable with the old ones)
76+
e = e.strip()
77+
if e == draft_email:
78+
l.extend(get_draft_authors_emails(draft))
79+
else:
80+
l.append(e)
81+
# Alternative: if we don't want to do expansion, just this would be
82+
# perhaps better (MTA can do expansion too):
83+
# l = n.split(',')
84+
return l
85+
86+
if __name__ == '__main__':
87+
import datetime
88+
import time
89+
90+
# Year ago?
91+
show_since = datetime.datetime.now() - datetime.timedelta(365)
92+
# 10 years ago?
93+
#show_since = datetime.datetime.now() - datetime.timedelta(10 * 365)
94+
95+
modname = 'ietf.generate_draft_aliases'
96+
date = time.strftime("%Y-%m-%d_%H:%M:%S")
97+
print '# Generated by python -m %s at %s' % (modname, date)
98+
99+
drafts = Document.objects.all()
100+
101+
# Drafts with active status
102+
active_drafts = drafts.filter(states__slug='active')
103+
104+
# Drafts that expired within year
105+
inactive_recent_drafts = drafts.exclude(states__slug='active').filter(expires__gte=show_since)
106+
107+
interesting_drafts = active_drafts | inactive_recent_drafts
108+
109+
for draft in interesting_drafts.distinct().iterator():
110+
# Omit RFCs, we care only about drafts
111+
if draft.docalias_set.filter(name__startswith='rfc'):
112+
continue
113+
name = draft.name
114+
done = []
115+
all = []
116+
def handle_sublist(name, f, o, is_ad=False):
117+
r = dump_sublist(name, f, o, is_ad)
118+
if r:
119+
done.append(name)
120+
all.extend(r)
121+
return r
122+
#.authors (/and no suffix) = authors
123+
124+
# First, do no suffix case
125+
# If no authors, don't generate list either
126+
r = dump_sublist(name, get_draft_authors_emails, draft)
127+
if not r:
128+
continue
129+
handle_sublist('%s%s' % (name, '.authors'), get_draft_authors_emails, draft)
130+
wg = draft.group
131+
132+
if wg:
133+
# .chairs = WG chairs
134+
handle_sublist('%s%s' % (name, '.chairs'), get_group_chairs_emails, wg)
135+
136+
# .ad = sponsoring AD / WG AD (WG document)
137+
handle_sublist('%s%s' % (name, '.ad'), get_draft_ad_emails, draft, True)
138+
139+
# .notify = notify email list from the Document
140+
handle_sublist('%s%s' % (name, '.notify'), get_draft_notify_emails, draft)
141+
142+
# .all = everything on 'done' (recursive aliases)
143+
#dump_sublist('%s%s' % (name, '.all'), None, done)
144+
# .all = everything on 'all' (expanded aliases)
145+
dump_sublist('%s%s' % (name, '.all'), None, all)
146+
147+

ietf/bin/generate-wg-aliases

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
# -*- Python -*-
4+
#
5+
# $Id: generate-wg-aliases $
6+
#
7+
# Author: Markus Stenberg <markus.stenberg@iki.fi>
8+
#
9+
"""
10+
11+
This code dumps Django model IETFWG's contents as two sets of postfix
12+
mail lists: -ads, and -chairs
13+
14+
"""
15+
16+
# boilerplate (from various other ietf/bin scripts)
17+
import os, sys
18+
basedir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))
19+
sys.path = [ basedir ] + sys.path
20+
21+
from ietf import settings
22+
from django.core import management
23+
management.setup_environ(settings)
24+
25+
from ietf.group.models import Group
26+
from ietf.group.utils import get_group_ads_emails, get_group_chairs_emails, get_area_ads_emails, get_area_chairs_emails
27+
from ietf.person.models import Email
28+
from ietf.utils.aliases import *
29+
30+
# from secr/utils/group.py..
31+
ACTIVE_STATES=['active', 'bof', 'proposed']
32+
33+
if __name__ == '__main__':
34+
import datetime
35+
import time
36+
37+
# Year ago?
38+
#show_since = datetime.datetime.now() - datetime.timedelta(365)
39+
40+
# 2 years ago?
41+
#show_since = datetime.datetime.now() - datetime.timedelta(2 * 365)
42+
43+
# 3 years ago?
44+
#show_since = datetime.datetime.now() - datetime.timedelta(3 * 365)
45+
46+
# 5 years ago?
47+
show_since = datetime.datetime.now() - datetime.timedelta(5 * 365)
48+
49+
modname = 'ietf.generate_wg_aliases'
50+
date = time.strftime("%Y-%m-%d_%H:%M:%S")
51+
print '# Generated by python -m %s at %s' % (modname, date)
52+
wgs = Group.objects.filter(type='wg').all()
53+
54+
print '# WGs'
55+
# - status = Active
56+
active_wgs = wgs.filter(state__in=ACTIVE_STATES)
57+
58+
# - activity within last year? (use concluded_date)
59+
inactive_recent_wgs = wgs.exclude(state__in=ACTIVE_STATES).filter(time__gte=show_since)
60+
interesting_wgs = active_wgs | inactive_recent_wgs
61+
62+
for wg in interesting_wgs.distinct().iterator():
63+
name = wg.acronym
64+
dump_sublist('%s%s' % (name, '-ads'), get_group_ads_emails, wg, True)
65+
dump_sublist('%s%s' % (name, '-chairs'), get_group_chairs_emails, wg)
66+
67+
print '# RGs'
68+
# - status = Active
69+
rgs = Group.objects.filter(type='rg').all()
70+
active_rgs = rgs.filter(state__in=ACTIVE_STATES)
71+
72+
# - activity within last year? (use concluded_date)
73+
inactive_recent_rgs = rgs.exclude(state__in=ACTIVE_STATES).filter(time__gte=show_since)
74+
interesting_rgs = active_rgs | inactive_recent_rgs
75+
76+
for rg in interesting_rgs.distinct().iterator():
77+
name = rg.acronym
78+
#dump_sublist('%s%s' % (name, '-ads'), get_group_ads_emails, rg, True)
79+
dump_sublist('%s%s' % (name, '-chairs'), get_group_chairs_emails, rg)
80+
81+
# Additionally, for areaz, we should list -ads and -chairs
82+
# (for every chair in active groups within the area).
83+
print '# Areas'
84+
areas = Group.objects.filter(type='area').all()
85+
active_areas = areas.filter(state__in=ACTIVE_STATES)
86+
for area in active_areas:
87+
name = area.acronym
88+
dump_sublist('%s%s' % (name, '-ads'), get_area_ads_emails, area, True)
89+
dump_sublist('%s%s' % (name, '-chairs'), get_area_chairs_emails, area)
90+
91+

ietf/group/utils.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,50 @@ def get_charter_text(group):
4545
filename = os.path.join(settings.IETFWG_DESCRIPTIONS_PATH, group.acronym) + ".desc.txt"
4646
desc_file = open(filename)
4747
desc = desc_file.read()
48-
except BaseException:
48+
except BaseException:
4949
desc = 'Error Loading Work Group Description'
5050
return desc
5151

52+
def get_area_ads_emails(area):
53+
if area.acronym == 'none':
54+
return []
55+
emails = [r.email.email_address()
56+
for r in area.role_set.filter(name__in=('ad', 'chair'))]
57+
return filter(None, emails)
58+
59+
def get_group_ads_emails(wg):
60+
" Get list of area directors' emails for a given WG "
61+
if wg.acronym == 'none':
62+
return []
63+
64+
if wg.parent and wg.parent.acronym != 'none':
65+
# By default, we should use _current_ list of ads!
66+
return get_area_ads_emails(wg.parent)
67+
68+
# As fallback, just return the single ad within the wg
69+
return [wg.ad and wg.ad.email_address()]
70+
71+
def get_group_chairs_emails(wg):
72+
" Get list of area chairs' emails for a given WG "
73+
if wg.acronym == 'none':
74+
return []
75+
emails = Email.objects.filter(role__group=wg,
76+
role__name='chair')
77+
if not emails:
78+
return
79+
emails = [e.email_address() for e in emails]
80+
emails = filter(None, emails)
81+
return emails
82+
83+
def get_area_chairs_emails(area):
84+
emails = {}
85+
# XXX - should we filter these by validity? Or not?
86+
wgs = Group.objects.filter(parent=area, type="wg", state="active")
87+
for wg in wgs:
88+
for e in get_group_chairs_emails(wg):
89+
emails[e] = True
90+
return emails.keys()
91+
5292
def save_milestone_in_history(milestone):
5393
h = get_history_object_for(milestone)
5494
h.milestone = milestone

ietf/person/models.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,14 @@ class Meta:
7373

7474
class Person(PersonInfo):
7575
user = models.OneToOneField(User, blank=True, null=True)
76-
76+
7777
def person(self): # little temporary wrapper to help porting to new schema
7878
return self
7979

8080
class PersonHistory(PersonInfo):
8181
person = models.ForeignKey(Person, related_name="history_set")
8282
user = models.ForeignKey(User, blank=True, null=True)
83-
83+
8484
class Alias(models.Model):
8585
"""This is used for alternative forms of a name. This is the
8686
primary lookup point for names, and should always contain the
@@ -115,3 +115,14 @@ def formatted_email(self):
115115
def invalid_address(self):
116116
# we have some legacy authors with unknown email addresses
117117
return self.address.startswith("unknown-email") and "@" not in self.address
118+
119+
def email_address(self):
120+
"""Get valid, current email address; in practise, for active,
121+
non-invalid addresses it is just the address field. In other
122+
cases, we default to person's email address."""
123+
if self.invalid_address() or not self.active:
124+
if self.person:
125+
return self.person.email_address()
126+
return
127+
return self.address
128+

ietf/utils/aliases.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
# -*- Python -*-
4+
#
5+
# $Id: aliasutil.py $
6+
#
7+
# Author: Markus Stenberg <mstenber@cisco.com>
8+
#
9+
"""
10+
11+
Mailing list alias dumping utilities
12+
13+
"""
14+
15+
def rewrite_email_address(email, is_ad):
16+
""" Prettify the email address (and if it's empty, skip it by
17+
returning None). """
18+
if not email:
19+
return
20+
email = email.strip()
21+
if not email:
22+
return
23+
if email[0]=='<' and email[-1] == '>':
24+
email = email[1:-1]
25+
# If it doesn't look like email, skip
26+
if '@' not in email and '?' not in email:
27+
return
28+
return email
29+
30+
def rewrite_address_list(l):
31+
""" This utility function makes sure there is exactly one instance
32+
of an address within the result list, and preserves order
33+
(although it may not be relevant to start with) """
34+
h = {}
35+
for address in l:
36+
#address = address.strip()
37+
if h.has_key(address): continue
38+
h[address] = True
39+
yield address
40+
41+
def dump_sublist(alias, f, wg, is_adlist=False):
42+
if f:
43+
l = f(wg)
44+
else:
45+
l = wg
46+
if not l:
47+
return
48+
# Nones in the list should be skipped
49+
l = filter(None, l)
50+
51+
# Make sure emails are sane and eliminate the Nones again for
52+
# non-sane ones
53+
l = [rewrite_email_address(e, is_adlist) for e in l]
54+
l = filter(None, l)
55+
56+
# And we'll eliminate the duplicates too but preserve order
57+
l = list(rewrite_address_list(l))
58+
if not l:
59+
return
60+
try:
61+
print '%s: %s' % (alias, ', '.join(l))
62+
except UnicodeEncodeError:
63+
# If there's unicode in email address, something is badly
64+
# wrong and we just silently punt
65+
# XXX - is there better approach?
66+
print '# Error encoding', alias, repr(l)
67+
return
68+
return l
69+

0 commit comments

Comments
 (0)