| require 'set' |
| require 'whimsy/asf/board' |
| |
| # Creates a summary hash of information from an Agenda |
| class ASF::Board::Agenda |
| # Strings or symbols returned from ASF::Board::Agenda.parse |
| ATTACH_KEY = :attach |
| INDEX_KEY = :index |
| APPROVED_KEY = 'approved' |
| TITLE_KEY = 'title' |
| # Hash keys returned by summarize() |
| ERRORS_KEY = 'errors' |
| PEOPLE_KEY = 'people' |
| OFFICERS_KEY = 'officers' |
| PMCS_KEY = 'pmcs' |
| ACTIONS_KEY = 'actions' |
| STATS_KEY = 'stats' |
| COMMENT_LEN = 'cl' |
| REPORT_LEN = 'rl' |
| APPROVALS_KEY = 'ap' |
| |
| SKIP_AGENDAS = { |
| 'board_agenda_2009_11_01' => 'F2F meeting: ApacheCon, St. Helena, CA', |
| 'board_agenda_2010_09_11' => 'F2F meeting: Boston, MA', |
| 'board_agenda_2010_11_02' => 'F2F meeting: ApacheCon, Atlanta, GA', |
| 'board_agenda_2011_11_07' => 'F2F meeting: ApacheCon, Vancouver, BC', |
| 'board_agenda_2012_08_28' => 'F2F meeting: Adobe, McLean, VA', |
| 'board_agenda_2017_06_15' => 'F2F meeting: Capital One, Mclean, VA' |
| } |
| |
| # Summarize data from these meeting minutes |
| # @param fname of agenda file to summarize |
| # @return hash of summary statistics from this meeting |
| # @note if error, includes details in [ERRORS_KEY] = ['SKIP(meeting): foo', 'ERROR(meeting): bar',...] |
| def self.summarize(fname) |
| summary = {} |
| meeting = File.basename(fname, '.*') |
| if SKIP_AGENDAS.has_key?(meeting) |
| summary[ERRORS_KEY] = "SKIP(#{meeting}) was: #{SKIP_AGENDAS[meeting]}" |
| return summary |
| end |
| begin |
| agenda = ASF::Board::Agenda.parse(File.read(fname.untaint)) |
| rescue StandardError => e |
| summary[ERRORS_KEY] = "ERROR(#{meeting}) Agenda parse error: #{e.message} #{e.backtrace[0]}" |
| return summary |
| end |
| begin |
| summary[PEOPLE_KEY] = Hash[agenda[1][PEOPLE_KEY]] |
| summary[PEOPLE_KEY].each do |id, data| |
| # Note: this adds initials to everyone who was *ever* a director, who was at this meeting |
| data['initials'] = ASF::Board.directorInitials(id) if ASF::Board.directorHasId?(id) |
| end |
| rescue StandardError => e |
| summary[ERRORS_KEY] = "ERROR(#{meeting}) no attendance error: #{e.message} #{e.backtrace[0]}" |
| return summary |
| end |
| begin |
| # Gather statistics about reports with preapprovals |
| approvals = agenda.select{ |v| v.has_key?(APPROVED_KEY) } |
| # PMC report :attach starts with letter; rest are officer or misc reports |
| preports, oreports = approvals.partition{ |v| /\A[[:alpha:]]/ =~ v[ATTACH_KEY] } |
| summary[OFFICERS_KEY] = Hash.new{|h,k| h[k] = {} } |
| oreports.each do |r| |
| summary[OFFICERS_KEY][r[TITLE_KEY]]['owner'] = r['owner'] if r.has_key?('owner') |
| summary[OFFICERS_KEY][r[TITLE_KEY]][APPROVALS_KEY] = Array.new(r['approved']) |
| summary[OFFICERS_KEY][r[TITLE_KEY]][COMMENT_LEN] = r['comments'].length |
| summary[OFFICERS_KEY][r[TITLE_KEY]][REPORT_LEN] = r['report'].length if r['report'] |
| end |
| summary[PMCS_KEY] = Hash.new{|h,k| h[k] = {} } |
| preports.each do |r| |
| summary[PMCS_KEY][r[TITLE_KEY]]['owner'] = r['owner'] |
| if r.has_key?('missing') |
| summary[PMCS_KEY][r[TITLE_KEY]]['missing'] = true |
| else |
| summary[PMCS_KEY][r[TITLE_KEY]][APPROVALS_KEY] = Array.new(r['approved']) |
| summary[PMCS_KEY][r[TITLE_KEY]][COMMENT_LEN] = r['comments'].length |
| summary[PMCS_KEY][r[TITLE_KEY]][REPORT_LEN] = r['report'].length if r['report'] |
| end |
| end |
| actions = agenda.select{ |v| v.has_key?(INDEX_KEY) && v[INDEX_KEY] == "Action Items" }[0][ACTIONS_KEY] |
| if actions |
| summary[ACTIONS_KEY] = Hash.new{|h,k| h[k] = [] } |
| actions.each do |r| |
| summary[ACTIONS_KEY][r[:owner]] << r[:pmc] |
| end |
| end |
| # Summarize across this report |
| summary[STATS_KEY] = {} |
| summary[STATS_KEY]['specialorders'] = agenda.select{ |v| /\A7/ =~ v[ATTACH_KEY] }.length |
| summary[STATS_KEY]['discusstextlen'] = agenda.select{ |v| |
| v[INDEX_KEY] == "Discussion Items" || /\A8[A-Z]/ =~ v[ATTACH_KEY] |
| }.map {|v| v['text'].length}.sum |
| totapprovals = 0 |
| totcommentlen = 0 |
| totreportlen = 0 |
| totreports = (summary[OFFICERS_KEY].length + summary[PMCS_KEY].length).to_f |
| # TODO figure out the ruby way to average these |
| summary[OFFICERS_KEY].each do |x, data| |
| totapprovals += data[APPROVALS_KEY].length if data[APPROVALS_KEY] |
| totcommentlen += data[COMMENT_LEN] if data[COMMENT_LEN] |
| totreportlen += data[REPORT_LEN] if data[REPORT_LEN] |
| end |
| summary[PMCS_KEY].each do |x, data| |
| totapprovals += data[APPROVALS_KEY].length if data[APPROVALS_KEY] |
| totcommentlen += data[COMMENT_LEN] if data[COMMENT_LEN] |
| totreportlen += data[REPORT_LEN] if data[REPORT_LEN] |
| end |
| if totreports != 0 # Avoid NaN in minutes that aren't parsed fully |
| summary[STATS_KEY]['avgapprovals'] = (totapprovals / totreports).round(2) |
| summary[STATS_KEY]['avgcommentlen'] = (totcommentlen / totreports).round(0) |
| summary[STATS_KEY]['avgreportlen'] = (totreportlen / totreports).round(0) |
| end |
| rescue StandardError => e |
| summary[ERRORS_KEY] ||= "ERROR(#{meeting}) process error: #{e.message} #{e.backtrace[0]}" |
| end |
| return summary |
| end |
| end |