blob: db797dd99189a7e67a622c02b8f85407ee629d5e [file]
#!/usr/bin/python3
"""
Generates:
html/committer-index.html
html/committers-by-project.html
html/.htaccess (if present)
from json files under html/public
Expected usage in cron job:
python3 /var/www/tools/committers.py
"""
from os.path import dirname, abspath, join, isfile
from inspect import getsourcefile
import datetime
import json
import re
import ezt
MYHOME = dirname(abspath(getsourcefile(lambda:0))) # automatically work out home location
HTML_DIR=join(dirname(MYHOME),'html')
JSON_DIR=join(HTML_DIR,'public')
KEYS_DIR=join(HTML_DIR,'keys')
KEYS_UID=join(KEYS_DIR,'committer')
KEYS_GRP=join(KEYS_DIR,'group')
HTACCESS = join(HTML_DIR,'.htaccess')
versions = {}
def getJson(file, stamp=None):
with open(join(JSON_DIR, file), "r", encoding='utf-8') as f:
j = json.loads(f.read())
if stamp != None:
versions[file] = [stamp, j[stamp]]
else:
versions[file] = ['(No timestamp info)', '']
return j
members = getJson('member-info.json', 'last_updated')['members']
ldap_people = getJson('public_ldap_people.json', 'lastCreateTimestamp')['people']
ldap_groups = getJson('public_ldap_groups.json', 'lastTimestamp')['groups'] # only used for apsite, members, committers
ldap_services = getJson('public_ldap_services.json', 'lastTimestamp')['services']
icla_info = getJson('icla-info.json', 'last_updated')['committers']
projects = getJson('public_ldap_projects.json', 'lastTimestamp')['projects']
idData = {} # hash of ids; entries are group type and name
groupData = {} # hash of group names; entries are lists of committer ids
for prj in projects:
project = projects[prj]
if 'pmc' in project and project['pmc'] == True:
gname=prj
groupData[gname] = []
for id in project['members']:
groupData[gname].append(id)
try:
idData[id].append(['unix', prj])
except KeyError:
idData[id] = [['unix', prj]]
gname=prj+'-pmc'
groupData[gname] = []
for id in project['owners']:
groupData[gname].append(id)
try:
idData[id].append(['pmc', prj])
except KeyError:
idData[id] = [['pmc', prj]]
elif 'podling' in project and project['podling'] == 'current':
groupData[prj] = []
# Assume owners is subset of members
for id in project['members']:
groupData[prj].append(id)
try:
idData[id].append(['podling', prj])
except KeyError:
idData[id] = [['podling', prj]]
# Allow for non-project groups (apsite, member)
for group in ldap_groups:
# don't overwrite existing groups
if group in groupData:
continue
# Only pick up these groups
if not group in ['apsite', 'member']:
continue
groupData[group] = []
for id in ldap_groups[group]['roster']:
groupData[group].append(id)
try:
idData[id].append(['unix', group])
except KeyError:
idData[id] = [['unix', group]]
for group in ldap_services:
groupData[group] = []
for id in ldap_services[group]['roster']:
groupData[group].append(id)
try:
idData[id].append(['service', group])
except KeyError:
idData[id] = [['service', group]]
def podlingName(group):
if group in projects and 'podling' in projects[group] and not 'pmc' in projects[group]:
status = projects[group]['podling']
if status == 'current':
status = "incubating"
return group + " (" + status + ")"
else:
return group
def publicName(id):
if id in ldap_people:
person = ldap_people[id]
if 'urls' in person:
urls = person['urls']
text = "<a href='%s'>%s</a>" % (urls[0],person['name'])
for url in urls[1:]:
text = text + " | <a href='%s'>+</a>" % url
return text
else:
return person['name']
else:
return '(Unknown)'
def isMember(id):
return id in members
def hasICLA(id):
return id in icla_info
lastupdate = '{:%Y-%m-%d %H:%M UTC}'.format(datetime.datetime.now(datetime.UTC))
class _item(object):
def __init__(self, **kw):
vars(self).update(kw)
def boldMember(id, txt):
if isMember(id):
return "<b>%s</b>" % txt
return txt
def notICLA(id, txt):
if not hasICLA(id):
return "<i>%s</i>" % txt
return txt
def linkKey(id):
txt = idStyle(id)
keyFile = join(KEYS_UID,'%s.asc' % id)
if isfile(keyFile):
return """<a href="keys/committer/%s">%s</a>""" % (id, txt)
return txt
def idStyle(id):
return notICLA(id, boldMember(id, id))
def nameStyle(id):
return notICLA(id, boldMember(id, publicName(id)))
# create links to phonebook groups
def linkGroup(groups):
grp_data = [ ]
for type, link in sorted(groups, key=lambda x: x[1]+'-'+x[0]):
name = link
if type == 'pmc':
name += '-pmc'
grp_data.append(_item(type=type, link=link, name=name))
return grp_data
letter='' # Alpha index
roster = [ ]
# iterate over committers (should be sorted already)
for id in ldap_groups['committers']['roster']:
person = _item(id=id, name=nameStyle(id), linkkey=linkKey(id),
is_member=ezt.boolean(isMember(id)),
has_icla=ezt.boolean(hasICLA(id)),
letter=None, groups=None)
ID1 = id[0:1].upper()
if not ID1 == letter: # new first letter
person.letter = ID1
letter = ID1
if id in idData:
person.groups = linkGroup(idData[id])
roster.append(person)
vsn_data = [_item(file=file,
type=versions[file][0],
stamp=versions[file][1]) for file in sorted(versions)]
template = ezt.Template(join(MYHOME, 'committer-index.ezt'), compress_whitespace=0)
template.generate(open(join(HTML_DIR,'committer-index.html'), mode='w'),
{ 'lastupdate': lastupdate,
'versions': vsn_data,
'roster': roster,
})
###############################
from io import StringIO
g = StringIO()
# Create the index
PER_LINE = 5
count = 0
for group in sorted(groupData):
if count % PER_LINE == 0:
g.write("<tr>")
count += 1
g.write("""<td><a href="#%s">%s</a></td>""" % (group, podlingName(group)))
if count % PER_LINE == 0:
g.write("</tr>\n")
needTr = False
while count % PER_LINE != 0:
g.write("<td>&nbsp;</td>")
needTr = True
count += 1
if needTr:
g.write("</tr>\n")
g.write("""</table>
<hr>\n""")
# create the individual listings
# put the unix and pmc groups side by side to reduce page length
col=0
for group in sorted(groupData):
if group+'-pmc' in groupData: # are we processing a unix group with a -pmc group?
col=1
if col == 1: # start the left column
g.write("""<table><tr><td class="left">\n""")
if col == 2:
g.write("""<td class="right">\n""")
g.write("""<h2 id="%s">%s</h2>\n""" % (group, podlingName(group)))
g.write("""<table><tr><th>SVN id</th><th>Name</th></tr>\n""")
for id in groupData[group]:
# SVN id
g.write("""<tr><td id='%s'><a href="committer-index.html#%s">%s</a></td>""" % (f"{group}-{id}", id, idStyle(id)))
# Name
g.write("<td>%s</td></tr>\n" % nameStyle(id))
g.write("""</table>\n""")
if col > 0:
g.write("""</td>\n""")
if col == 2:
g.write("""</tr></table>\n""")
col = 0
if col == 1:
col = 2
content = g.getvalue()
template = ezt.Template(join(MYHOME, 'committers-by-project.ezt'),
compress_whitespace=0)
template.generate(open(join(HTML_DIR,'committers-by-project.html'), mode='w'),
{ 'lastupdate': lastupdate,
'versions': vsn_data,
'content': content,
})
############################################################################
# Generate redirects for personal directories
SELF_RE = r"://(\w+\.)?apache\.org/" # self
if isfile(HTACCESS):
with open(HTACCESS, 'w') as hta:
hta.write("# this file is generated by committers.py; do not edit\n")
hta.write("Redirect /favicon.ico https://apache.org/favicons/favicon.ico\n")
for id in ldap_people:
person = ldap_people[id]
if 'urls' in person:
for url in person['urls']:
# Must be an http[s] URL and must not be an ASF reference
if re.match(r"^https?://", url) and not re.search(SELF_RE, url, re.IGNORECASE):
hta.write(f"RedirectMatch ^/~{id}(/.*)?$ {url}$1\n")
break
##############################################################################