blob: db220401a9a8990ae320df4771d1f9fc283defd0 [file] [log] [blame]
#!/usr/bin/env ruby
PAGETITLE = "Board Meeting Attendance since 2010" # Wvisible:meeting
$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'whimsy/asf'
require 'whimsy/asf/agenda'
require 'whimsy/asf/board'
require 'whimsy/public'
require 'wunderbar/bootstrap'
require 'json'
require 'set'
BOARD = ASF::SVN['foundation_board']
IS_DIRECTOR = :director
APPROVED = 'approved'
# Summarize director attendance and preapps at one meeting into dstats
# @note that ASF::Board::Agenda has mix of symbols and strings for hash keys
# @return string if error
# Side effect: fills in dstats
# Each director gets one entry for each meeting they were a director
def summarize(fname, dstats)
meeting = File.basename(fname, '.*')
begin
agenda = ASF::Board::Agenda.parse(File.read(fname.untaint))
rescue StandardError => e
return "summarize(#{fname}) Agenda parse error: #{e.message} #{e.backtrace[0]}"
end
begin
people = agenda[1]['people']
rescue StandardError => e
return "summarize(#{fname}) no attendance error: #{e.message} #{e.backtrace[0]}"
end
begin
people.each do |id, att|
if IS_DIRECTOR.eql?(att[:role])
dstats[id][meeting] = {'present' => att[:attending]}
end
end
reports = agenda.select{ |v| v.has_key?(APPROVED) && ! v.has_key?('missing') }
numreports = reports.length.to_f
actions = agenda.select{ |v| v.has_key?(:index) && v[:index] == "Action Items" }[0]['actions']
dstats.each do |id, dirmtg|
if dirmtg.has_key?(meeting)
dirmtg[meeting]['preapps'] = (reports.select {|v| v[APPROVED].include?(ASF::Board.directorInitials(id))}.length / numreports).round(3)
dirmtg[meeting]['actions'] = actions.select{ |v| v[:owner] == ASF::Board.directorFirstName(id) }.length
end
end
rescue StandardError => e
return "summarize(#{fname}) process error: #{e.message} #{e.backtrace[0]}"
end
return nil
end
# Create summary of director attendance from board minutes (includes private data)
# Note that prior to Dec 2012 and for F2F meetings, this won't parse
# @param dir pointing to /foundation/board/archived_agendas
# @return stats hash of each director's attendance & etc., error array
def summarize_all(dir = BOARD)
summaries = Hash.new{|h,k| h[k] = {} }
errors = []
Dir[File.join(dir, 'archived_agendas', "board_agenda_201*.txt")].each do |f|
e = summarize(f, summaries)
errors << e if e
end
return summaries, errors
end
# If JSON requested, simply return the data hash
_json do
summaries, errors = summarize_all
summaries
end
# produce HTML
_html do
_whimsy_body(
title: PAGETITLE,
related: {
'/board/minutes' => 'Monthly Board Meeting Minutes (categorized)',
'https://www.apache.org/foundation/board/calendar.html' => 'Monthly Board Meeting Minutes (by date)',
'/roster/group/board' => 'Current List of Directors'
},
helpblock: -> {
_ 'This lists statistics about director attendance and report preapprovals at monthly board meetings since 2013. Before 2013, the preapproval data isn\'t easily parseable.'
}
) do
datums = JSON.parse(File.read(File.join(BOARD, 'scripts', 'board-attend.json')))
months = Set.new()
datums.each do |id, data|
data.keys.each do |m|
months << m
end
end
_whimsy_panel_table(
title: "Director attendance at monthly board meetings",
helpblock: -> {
_p do
_ "Includes data from #{months.min} to #{months.max} regularly scheduled monthly board meetings. Key:"
_ul do
_li 'Mtgs Attended - # of meetings attended when they were a director'
_li 'Mtgs Missed - # of meetings missed when they were a director'
_li.text_muted '% Reports Preapp - average %age of reports they pre-approved in Whimsy (member-private)'
_li 'Avg Action Items - average number of Action Items assigned per month'
end
end
}
) do
_table.table.table_hover.table_striped do
_thead_ do
_tr do
_th 'Director'
_th 'Mtgs Attended'
_th 'Mtgs Missed'
_th do
_span.text_muted '% Reports Preapp'
end
_th 'Avg Action Items'
end
_tbody do
datums.each do | id, data |
totp = 0.0
tota = 0.0
data.each do |k,v|
totp += v['preapps'] if v.has_key?('preapps')
tota += v['actions'] if v.has_key?('actions')
end
_tr_ do
_td do
if ASF::Board.directorHasId?(id) and ASF::Board.directorDisplayName(id)
_ ASF::Board.directorDisplayName(id)
else
_em.bg_danger id
end
end
_td do
_ data.select{|k,v| v['present']}.length
end
_td do
_ data.select{|k,v| !v['present']}.length
end
_td do
_span.text_muted "#{((totp / data.length)*100).round(0)}%"
end
_td do
_ (tota / data.length).round(2)
end
end
end
end
end
end
end
end
end