class ASF::Board::Agenda

Class which contains a number of parsers.


Back sections:

Additional Officer Reports and Committee Reports

Executive Officer Reports

Front sections:

Minutes from previous meetings

Special Orders



mapping of agenda section numbers to section names

Public Class Methods

new() click to toggle source

start with an empty list of sections. Sections are added and returned by calling the parse method.

# File lib/whimsy/asf/agenda.rb, line 43
def initialize
  @sections = {}
parse(file=nil, quick=false, &block) click to toggle source

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.

# File lib/whimsy/asf/agenda.rb, line 36
def self.parse(file=nil, quick=false, &block)
  @@parsers << block if block
  new.parse(file, quick)  if file

Public Instance Methods

minutes(title) click to toggle source

provide a link to the collated minutes for a given report

# File lib/whimsy/asf/agenda.rb, line 170
def minutes(title)
parse(file, quick=false) click to toggle source

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.

# File lib/whimsy/asf/agenda.rb, line 82
def parse(file, quick=false)
  @file = file
  @quick = quick
  if not @file.valid_encoding?
    filter = {|c| c.unpack('U').first rescue 0xFFFD}
    @file ='U*').force_encoding('utf-8')

  @@parsers.each { |parser| instance_exec(&parser) }

  # add index markers for major sections
  CONTENTS.each do |section, index|
    @sections[section][:index] = index if @sections[section]

  # 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
  preports =*president['report'][/\d+ through \d+\.$/].scan(/\d+/)) 
  # 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}}/, ''

    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}}/, ''

    # add flags
    flags = flagged_reports[hash['title']]
    hash['flagged_by'] = flags.split(', ') if flags

    # mark president reports
    hash['to'] = 'president' if preports.include? section

  unless @quick
    # add roster and prior report link
    whimsy = ''
    @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'] = 
      if section =~ /^[A-Z][A-Z]?$/
        hash['stats'] = 
      hash['prior_reports'] = minutes(committee.display_name)

  # add attach to section
  @sections.each do |section, hash|
    hash[:attach] = section

  # look for missing titles
  @sections.each do |section, hash|
    hash['title'] ||= "UNKNOWN"

    if hash['title'] == "UNKNOWN"
      hash['warnings'] = ['unable to find attachment']

scan(text, pattern) { |hash| ... } click to toggle source

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.

# File lib/whimsy/asf/agenda.rb, line 50
def scan(text, pattern, &block)
  # convert tabs to spaces
  text.gsub!(/^(\t+)/) {|tabs| ' ' * (8*tabs.length)}

  text.scan(pattern).each do |matches|
    hash = Hash[]
    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']
timestamp(time) click to toggle source

convert a PST/PDT time to UTC as a JavaScript integer

# File lib/whimsy/asf/agenda.rb, line 175
def timestamp(time)
  date = @file[/(\w+ \d+, \d+)/]
  tz = TZInfo::Timezone.get('America/Los_Angeles')
  tz.local_to_utc(Time.parse("#{date} #{time}")).to_i * 1000