class ASF::Board::Agenda
Class which contains a number of parsers.
Attachments
Back sections:
-
Review Outstanding Action Items
-
Unfinished Business
-
New Business
-
Announcements
-
Adjournment
Additional Officer Reports and Committee Reports
Discussion Items
Executive Officer Reports
Front sections:
-
Call to Order
-
Roll Call
Minutes from previous meetings
Special Orders
Creates a summary hash of information from an Agenda
Constants
- ACTIONS_KEY
- APPROVALS_KEY
- APPROVED_KEY
- ATTACH_KEY
-
Strings or symbols returned from
ASF::Board::Agenda.parse
- COMMENT_LEN
- CONTENTS
-
mapping of agenda section numbers to section names
- ERRORS_KEY
-
Hash keys returned by summarize()
- FOUNDATION_BOARD
-
Must be outside scan loop. Use find to placate Travis.
- INDEX_KEY
- MINUTES
- OFFICERS_KEY
- OFFICER_SEPARATOR
-
Regex for start of officer reports (accounts for style differences in early agendas)
- PEOPLE_KEY
- PMCS_KEY
- REPORT_LEN
- SKIP_AGENDAS
- STATS_KEY
- TITLE_KEY
Public Class Methods
Source
# File lib/whimsy/asf/agenda.rb, line 47 def initialize @sections = {} end
start with an empty list of sections. Sections are added and returned by calling the parse
method.
Source
# File lib/whimsy/asf/agenda.rb, line 40 def self.parse(file=nil, quick=false, &block) @@parsers << block if block new.parse(file, quick) if file end
convenience method. If passed a file, will create an instance of this class and call the parse method on that object. If passed a block, will add that block to the list of parsers.
Source
# File lib/whimsy/asf/agenda/summary.rb, line 35 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)) 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
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’,…]
Public Instance Methods
Source
# File lib/whimsy/asf/agenda.rb, line 179 def minutes(title) "https://whimsy.apache.org/board/minutes/#{title.gsub(/\W/, '_')}" end
provide a link to the collated minutes for a given report
Source
# File lib/whimsy/asf/agenda.rb, line 86 def parse(file, quick=false) @file = file @quick = quick unless @file.valid_encoding? filter = proc {|c| c.unpack1('U') rescue 0xFFFD} @file = @file.chars.map(&filter).pack('U*').force_encoding('utf-8') end @@parsers.each { |parser| instance_exec(&parser) } # add index markers for major sections CONTENTS.each do |section, index| @sections[section][:index] = index if @sections[section] end # quick exit if none found -- non-standard format agenda return [] if @sections.empty? # look for flags flagged_reports = Hash[@file[/ \d\. Committee Reports.*?\n\s+A\./m]. scan(/# (.*?) \[(.*)\]/)] rescue {} president = @sections.values.find {|item| item['title'] == 'President'} return [] unless president # quick exit if non-standard format agenda pattach = president['report'][/\d+ through \d+\.$/] # pattach is nil before https://whimsy.apache.org/board/minutes/Change_Officers_to_Serve_at_the_Direction_of_the_President.html preports = Range.new(*pattach.scan(/\d+/)) if pattach # cleanup text and comment whitespace, add flags @sections.each do |section, hash| text = hash['text'] || hash['report'] if text text.sub!(/\A\s*\n/, '') text.sub!(/\s+\Z/, '') unindent = text.sub(/s+\Z/, '').scan(/^ *\S/).map(&:length).min || 1 text.gsub!(/^ {#{unindent - 1}}/, '') end text = hash['comments'] if text text.sub!(/\A\s*\n/, '') text.sub!(/\s+\Z/, '') unindent = text.sub(/s+\Z/, '').scan(/^ *\S/).map(&:length).min || 1 text.gsub!(/^ {#{unindent - 1}}/, '') end # add flags flags = flagged_reports[hash['title']] hash['flagged_by'] = flags.split(', ') if flags # mark president reports hash['to'] = 'president' if preports&.include?(section) end unless @quick # add roster and prior report link whimsy = 'https://whimsy.apache.org' @sections.each do |section, hash| next unless section =~ /^(4[A-Z]|\d+|[A-Z][A-Z]?)$/ committee = ASF::Committee.find(hash['title'] ||= 'UNKNOWN') unless section =~ /^4[A-Z]$/ hash['roster'] = "#{whimsy}/roster/committee/#{CGI.escape committee.name}" end if section =~ /^[A-Z][A-Z]?$/ hash['stats'] = 'https://reporter.apache.org/wizard/statistics?' + CGI.escape(committee.name) end hash['prior_reports'] = minutes(committee.display_name) end end # add attach to section @sections.each do |section, hash| hash[:attach] = section # look for missing titles hash['title'] ||= 'UNKNOWN' if hash['title'] == 'UNKNOWN' hash['warnings'] = ['unable to find attachment'] end end # handle case where board meeting crosses a date boundary if @sections.values.first['timestamp'] > @sections.values.last['timestamp'] @sections.values.last['timestamp'] += 86_400_000 # add one day end @sections.values end
parse a board agenda file by passing it through each parser. Additionally, converts the file to utf-8, adds index markers for major sections, looks for flagged reports, and performs various minor cleanup actions.
If quick
is false
, cross-checks with committee membership will be performed. This supports the board agenda tools’s strategy to quickly display possibly stale and possible incomplete data and then to update the presentation using React.JS once later and/or more complete data is available.
Returns a list of sections.
Source
# File lib/whimsy/asf/agenda.rb, line 54 def scan(text, pattern, &block) # convert tabs to spaces text.gsub!(/^(\t+)/) {|tabs| ' ' * (8 * tabs.length)} text.scan(pattern).each do |matches| hash = Hash[pattern.names.zip(matches)] yield hash if block section = hash.delete('section') section ||= hash.delete('attach') if section hash['approved'] &&= hash['approved'].strip.split(/[ ,]+/) @sections[section] ||= {} next if hash['text'] and @sections[section]['text'] @sections[section].merge!(hash) end end end
helper method to scan a section for a pattern. Regular expression named matches will be captured and the section will be added to @sections
if a match is found.
Source
# File lib/whimsy/asf/agenda.rb, line 184 def timestamp(time) date = @file[/(\w+ \d+, \d+)/] ASF::Board::TIMEZONE.parse("#{date} #{time}").to_i * 1000 end
convert a PST/PDT time to UTC as a JavaScript integer