blob: c665bf7be71995424a4e4b02a3871429ee003012 [file] [log] [blame]
#!/usr/bin/env ruby
$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'whimsy/asf'
require 'date'
require 'builder'
require 'ostruct'
require 'nokogiri'
require 'net/https'
require 'fileutils'
require 'wunderbar'
Wunderbar.log_level = 'info' unless Wunderbar.logger.info? # try not to override CLI flags
# Add datestamp to log messages (progname is not needed as each prog has its own logfile)
Wunderbar.logger.formatter = proc { |severity, datetime, progname, msg|
"_#{severity} #{datetime} #{msg}\n"
}
# for monitoring purposes
at_exit do
if $! and not $!.instance_of? SystemExit
msg = "#{$!.backtrace.first} #{$!.message}" rescue $!
puts "\n*** Exception #{$!.class} : #{msg} ***"
end
Wunderbar.info "Finished #{__FILE__}"
end
Wunderbar.info "Starting #{__FILE__}"
# destination directory
SITE_MINUTES = ASF::Config.get(:board_minutes) ||
File.expand_path('../../www/board/minutes', __FILE__)
# list of SVN resources needed
resources = {
SVN_SITE_RECORDS_MINUTES:
'asf/infrastructure/site/trunk/content/foundation/records/minutes',
BOARD: 'private/foundation/board'
}
# verify that the SVN resources can be found
resources.each do |const, location|
Kernel.const_set const, ASF::SVN[location]
unless Kernel.const_get const
STDERR.puts 'Unable to locate local checkout for ' + location
exit 1
end
end
incubator = URI.parse('http://incubator.apache.org/')
KEEP = ARGV.delete '--keep' # keep obsolete files?
force = ARGV.delete '--force' # rerun regardless
NOSTAMP = ARGV.delete '--nostamp' # don't add dynamic timestamp to pages (for debug compares)
STAMP = (NOSTAMP ? DateTime.new(1970) : DateTime.now).strftime '%Y-%m-%d %H:%M'
YYYYMMDD = ARGV.shift || '20*' # Allow override of minutes to process
TIME_DIFF = (ARGV.shift || '300').to_i # Allow override of seconds of time diff (WHIMSY-204) for testing
MINUTES_NAME = "board_minutes_#{YYYYMMDD}.txt"
MINUTES_PATH = "#{SVN_SITE_RECORDS_MINUTES}/*/#{MINUTES_NAME}"
Wunderbar.info "Processing minutes matching #{MINUTES_NAME}"
INDEX_FILE = "#{SITE_MINUTES}/index.html"
# quick exit if everything is up to date
if File.exist? INDEX_FILE
input = Dir[MINUTES_PATH,
"#{BOARD}/board_minutes_20*.txt"].
map {|name| File.stat(name).mtime}.
push(File.stat(__FILE__).mtime, ASF.library_mtime).
max
indexmtime = File.stat(INDEX_FILE).mtime
diff = indexmtime - input
Wunderbar.info "Most recent update: #{input}"
Wunderbar.info "Index file update: #{indexmtime} Diff: #{diff}"
# WHIMSY-204: allow for update window
# TODO: consider storing actual update check time
if diff >= TIME_DIFF
Wunderbar.info "All up to date! (#{TIME_DIFF})"
unless force
# Add stamp to index page
page = File.read(INDEX_FILE)
open(INDEX_FILE, 'w') { |file|
# must agree with section.add_child
file.write page.sub(/(Last run: )\d{4}-\d\d-\d\d \d\d:\d\d(\. The data is extracted from a list of)/,"\\1#{STAMP}\\2")
}
exit
end
end
end
Wunderbar.info "Updating files"
# mapping of committee names to canonical names (generally from ldap)
canonical = Hash.new {|hash, name| name}
# extract podling information
site = {}
ASF::Podling.list.each do |podling|
if podling.display_name.downcase != podling.name
canonical[podling.display_name.downcase] = podling.name
end
if podling.status == 'graduated' and podling.enddate
next if Date.today - podling.enddate > 90
end
site[podling.name] = {
name: podling.display_name,
status: podling.status,
link: incubator + "projects/#{podling.name}.html",
text: podling.description
}
end
# get site information
DATAURI = 'https://whimsy.apache.org/public/committee-info.json'
local_copy = File.expand_path('../../www/public/committee-info.json', __FILE__).untaint
if File.exist? local_copy
Wunderbar.info "Using #{local_copy}"
cinfo = JSON.parse(File.read(local_copy))
else
Wunderbar.info "Fetching remote copy of committee-info.json"
response = Net::HTTP.get_response(URI(DATAURI))
response.value() # Raises error if not OK
cinfo = JSON.parse(response.body)
end
cinfo['committees'].each do |id,v|
if v['display_name'].downcase != id
canonical[v['display_name'].downcase] = id
end
site[id] = {:name => v['display_name'], :link => v['site'], :text => v['description']}
end
# parse the calendar for layout info (note: hack for &raquo and  )
CALENDAR = URI.parse 'https://www.apache.org/foundation/board/calendar.html'
http = Net::HTTP.new(CALENDAR.host, CALENDAR.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
get = Net::HTTP::Get.new CALENDAR.request_uri
$calendar = Nokogiri::HTML(http.request(get).body.gsub('&raquo','»').gsub(' ',' '))
# Link to headerlink css
link = Nokogiri::XML::Node.new "link", $calendar
link.set_attribute('rel', 'stylesheet')
link.set_attribute('href', 'https://www.apache.org/css/headerlink.css')
$calendar.at('head').add_child(link)
# add some style
style = Nokogiri::XML::Node.new "style", $calendar
style.content = %{
table {
border: 1px solid #ccc;
margin-botton: 10px;
width: 100%;
border-collapse: collapse;
border-spacing: 0;
}
tbody th, tbody td {
border-bottom: 1px solid #ccc;
border-top: 1px solid #ccc;
padding: 0.2em 1em;
}
pre.report {
color: black;
font-family: Consolas,monospace
}
}
$calendar.at('head').add_child(style)
# Make links absolute
%w(a img link script).each do |name|
$calendar.search(name).each do |element|
element['href'] = (CALENDAR + element['href'].strip).to_s if element['href']
element['src'] = (CALENDAR + element['src'].strip).to_s if element['src']
end
end
# Dir.chdir(SVN_SITE_RECORDS_MINUTES) { system 'svn update' }
agenda = {}
posted = Dir[MINUTES_PATH].sort
unapproved = Dir["#{BOARD}/#{MINUTES_NAME}"].sort
FileUtils.mkdir_p SITE_MINUTES
seen={}
(posted+unapproved).each do |txt|
date = $1 if txt =~ /(\d\d\d\d_\d\d_\d\d)/
next unless date
if seen.has_key? date
Wunderbar.warn "Already processed #{seen[date]}; skipping #{txt}"
next
end
seen[date] = txt
minutes = open(txt) {|file| file.read}
print "\r#{date}"
$stdout.flush
pending = {}
# parse Attachments (includes both Officer Reports and Committee Reports)
minutes.scan(/
-{41}\n # separator
Attachment\s\s?(\w+):[ ](.+?)\n # Attachment, Title
(.)(.*?)\n # separator, report
(?=-{41,}\n(?:End|Attach)) # separator
/mx).each do |attach,title,cont,text|
# We need to keep the start of the second line.
# Otherwise leading spaces in the report body look like a continuation line
if cont == ' ' # continuation line was not empty; check if it's a continuation
# join multiline titles
while text.start_with? ' '
append, text = text.split("\n", 2)
title += ' ' + append.strip
end
end
title.sub! /Special /, ''
title.sub! /Requested /, ''
title.sub! /(^| )Report To The Board( On)?( |$)/i, ''
title.sub! /^Board Report for /, ''
title.sub! /^Status [Rr]eport for (the )?/, ''
title.sub! /^Report from the VP of /, ''
title.sub! /^Report from the /i, ''
title.sub! /^Status report for the /i, ''
title.sub! /^Apache /, ''
title.sub! /^\/ /, ''
title.sub! /\s+\[.*\]\s*$/, ''
title.sub! /\sTeam$/, ''
title.sub! /\s[Cc]ommittee?\s*$/, ''
title.sub! /\s[Pp]roject\s*$/, ''
title.sub! /\sPMC$/, ''
title.sub! 'Apache Software Foundation', 'ASF'
title.sub! /^Logging$/, 'Logging Services'
title.sub! 'stdcxx', 'C++ Standard Library'
title.sub! 'Cxx Standard Library', 'C++ Standard Library'
title.sub! 'Conferences', 'Conference Planning'
title.sub! /Fund[- ][rR]aising/, 'Fundraising'
title.sub! 'Geroniomo', 'Geronimo'
title.sub! "Infrastructure (President's)", 'Infrastructure'
title.sub! 'Java Community Process', 'JCP'
title.sub! 'James', 'JAMES'
title.sub! /APR$/, 'Portable Runtime (APR)'
title.sub! /Portable Runtime$/, 'Portable Runtime (APR)'
title.sub! 'TomEE (OpenEJB)', 'TomEE'
title.sub! 'OpenEJB', 'TomEE'
title.sub! 'Public Relations Commitee', 'Public Relations'
title.sub! /Security$/, 'Security Team'
title.sub! /^Infrastructure .*/, 'Infrastructure'
title.sub! /^Labs .*/, 'Labs'
title.sub! 'TCL', 'Tcl'
title.sub! 'Orc', 'ORC'
title.sub! 'Steve', 'STeVe'
title.sub! 'Servicecomb', 'ServiceComb'
title.sub! 'Zest', 'Polygene'
title.sub! 'Openmeetings', 'OpenMeetings'
title.sub! 'Web services', 'Web Services'
title.sub! 'ASF Rep. for W3C', 'W3C Relations'
next if title.strip.empty?
next if text.strip.empty? and title =~ /Intentionally (left )?Blank/i
next if text.strip.empty? and title =~ /There is No/i
report = pending[attach] || OpenStruct.new
report.meeting = date
report.attach = attach
report.title = title.strip #.downcase
report.text = text
if title =~ /budget|spending/i
report.subtitle = title
report.title = 'Budget'
report.attach = '@' + attach
elsif title =~ /Contributor License Agreement/
report.subtitle = title
report.title = 'Legal Affairs'
report.attach = '1' + attach
elsif title =~ /P(rofit-and-|&)L(oss)? Report/
report.subtitle = title
report.title = 'Treasurer'
report.attach = '1' + attach
elsif title =~ /alleged JBoss IP infringement/
report.subtitle = title
report.title = 'Alleged JBoss IP Infringement'
report.attach = '@' + attach
end
pending[attach] = report
if title == 'Incubator' and text
sections = text.split(/\nStatus [rR]eport (.*)\n=+\n/)
# Some minutes have a 'Detailed Reports' header before the first podling report
# podling header may now be prefixed with ## (since June 2019)
sections = text.split(/\n[-=][-=]+(?: Detailed Reports ---+)?\n(?:##)?\s*([a-zA-Z].*)\n\n/) if sections.length < 9
sections = [''] if sections.include? 'FAILED TO REPORT'
sections = text.split(/\n(\w+)\n-+\n\n/) if sections.length < 9
sections = text.split(/\n=+\s+([\w.]+)\s+=+\n+/) if sections.length < 9
prev = nil
if sections.length > 1
report.text = sections.shift
sections.each_slice(2) do |title, text|
title.sub! /^regarding /, ''
title.sub! /^for /, ''
title.sub! /^from /, ''
title.sub! /^the /, ''
title.sub! /\sPPMC$/, ''
if title =~ /Apache (.*) is a/
text = title + "\n" + text
title = $1
end
if title =~ /(.*) has been incubating/
text = title + "\n" + text
title = $1
end
if title =~ /(.*) -- (DID NOT REPORT)/
text = $2 + "\n" + text
title = $1
end
if title =~ /(.*?) - (.*)/
text = $2 + "\n" + text
title = $1
end
if title =~ /(.*? sponsored) incubation \((.*)\)/
text = $2 + "\n" + text
title = $1
end
next if title == 'April 2011 podling reports'
title.sub! 'Ace', 'ACE' # WHIMSY-31
title.sub! 'ADF Faces', 'MyFaces' # via Trinidad
title.sub! 'Bean Validation', 'BVal'
title.sub! 'BeanValidation', 'BVal'
title.sub! 'Amber', 'Oltu'
title.sub! 'Argus', 'Ranger'
title.sub! 'BRPC', 'brpc'
title.sub! 'Bluesky', 'BlueSky'
title.sub! 'Easyant', 'EasyAnt'
title.sub! 'Callback', 'Cordova'
title.sub! 'Deft', 'AWF'
title.sub! /CeltiX[Ff]ire/, 'CXF'
title.sub! 'Empire-DB', 'Empire-db'
title.sub! 'Fleece', 'Johnzon'
title.sub! 'IVY', 'Ivy'
title.sub! 'JackRabbit', 'Jackrabbit'
title.sub! 'Juice', 'JuiCE'
title.sub! /\bKi\b/, 'Shiro'
title.sub! 'JSecurity', 'Shiro'
title.sub! 'log4php', 'Log4php'
title.sub! 'lucene4c', 'Lucene4c'
title.sub! 'Lucene.NET', 'Lucene.Net'
title.sub! 'Ode', 'ODE'
title.sub! 'Optiq', 'Calcite'
title.sub! 'Oscar', 'Felix'
title.sub! 'Quarks', 'Edgent'
title.sub! 'Singa', 'SINGA'
title.sub! 'Openmeetings', 'OpenMeetings'
title.sub! 'ODFToolkit', 'ODF Toolkit'
title.sub! 'OpenOffice.org', 'OpenOffice'
title.sub! 'OpenEJB', 'TomEE'
title.sub! 'Stratosphere', 'Flink'
title.sub! 'Socialsite', 'SocialSite'
title.sub! 'stdcxx', 'C++ Standard Library'
title.sub! 'STDCXX', 'C++ Standard Library'
title.sub! /\s+\(.*\)$/, ''
title.sub! /^Apache(: Project)?/, ''
if %w(Mentors Committers).include? title
prev.text += "\n== #{title}==\n\n#{text}" if prev
next
end
report = OpenStruct.new
report.meeting = date
report.attach = '.' + title
report.title = title.strip
report.text = text
pending[report.attach] = report
prev = report
end
end
end
end
# parse Officer and Committee Reports for owners and comments
minutes.scan(/
\[([^\n]+)\]\n\n # owners
\s{7}See\sAttachment\s\s?(\w+) # attach
(.*?)\n # comments
\s\s\s\s?\w # separator
/mx).each do |owners,attach,comments|
report = pending[attach] || OpenStruct.new
report.meeting = date
report.attach = attach
report.owners = owners
report.comments = comments.strip
pending[attach] = report
end
# fill in comments from missing reports
['Committee', 'Additional Officer'].each do |section|
reports = minutes[/^ \d\. #{section} Reports(\s*(\n| .*\n)+)/,1]
next unless reports
reports.split(/^ (\w+)\./)[1..-1].each_slice(2) do |attach, comments|
next if attach.length > 2
owners = comments[/\[([^\n]+)\]/,1]
next if comments.include? 'See Attachment'
comments.sub! /.*\s+\n/, ''
next if comments.empty?
attach = ('A'..attach).count.to_s if section == 'Additional Officer'
report = pending[attach] || OpenStruct.new
report.meeting = date
report.attach = attach
report.owners = owners
report.comments = comments.strip
pending[attach] = report
end
end
# parse Action Items
minutes.scan(/
\n\s+(\w+)\.\s # attach
Review\sOutstanding\s(Action\sItems)\n\n?
(.*?) # text
\n\s?\d # separator
/mx).each do |attach, title, text|
report = OpenStruct.new
report.title ||= title #.downcase
report.meeting = date
report.attach = '+' + title
text.gsub! /^\s?\d+\.\s.*\s*\Z/, ''
report.text = text.gsub Regexp.new('^'+text.match(/^ */)[0]), '' if text
pending[title] = report
end
# parse other agenda items
establish='' # pick up misplaced PMC creates
minutes.scan(/
\n\s*(\w+)\.\s # attach
(Discussion\sItems|Unfinished\sBusiness|New\sBusiness|Announcements)\n
(.*?) # text
(?=\n\s?\d) # separator
/mx).each do |attach, title, text|
next if text.strip.empty?
next if text =~ /\A\s*none\.?\s*\z/i
next if text =~ /\A\s*no unfinished business\.?\s*\z/i
if text =~ /Establish the Apache \S+ Project/ # 2012_08_28
establish += text
next
end
if title !~ /Discussion/ or text !~ /\A\n*\s{3,5}[0-9A-Z]\.\s.*\n\n/
report = OpenStruct.new
report.title ||= title #.downcase
report.meeting = date
report.attach = '+' + title
report.text = text.strip
pending[title] = report
else
text.scan(/
\s{3}[\s\d]([0-9A-Z])\. # agenda item
\s+(.*?)\n # title
(.*?) # text
(?=\n\s{3,5}\d?[0-9A-Z]\.\s|\z) # next section
/mx).each do |attach,title,text|
if title.include? "\n" and title.length > 120
title = title.split("\n")
text = title[1..-1].join("\n") + "\n" + text
title = title[0]
end
report = OpenStruct.new
report.title = title.gsub(/\s+/, ' ')
report.meeting = date
report.attach = '+' + title
report.text = text.strip
if title =~ /budget|spending/i
report.subtitle = title
report.title = 'Budget'
report.attach = '@' + attach
elsif title =~ /Legal Affairs/
report.subtitle = title
report.title = 'Legal Affairs'
report.attach = '1' + attach
else
pmcs = %w{Geronimo iBATIS Santuario}
pmcs.each do |pmc|
if title =~ /#{pmc}/i
report.subtitle = title
report.title = pmc
report.attach = '.' + pmc
end
end
end
pending[title] = report
end
end
end
# parse Special Orders
orders = establish + minutes.split(/^ \d\. Special Orders/,2).last.split(/^ \d\./,2).first
# Some section ids have a leading digit, hence [\s\d]
orders.scan(/
\s{3}[\s\d]([A-Z])\. # agenda item
\s+(.*?)\n\s*\n # title
(.*?) # text
(?=\n\s{3,4}[\s\d][A-Z]\.\s|\z) # next section
/mx).each do |attach,title,text|
next if title.count("\n")>1
report = OpenStruct.new
title.sub! /(^|\n)\s*Resolution R\d:/, ''
title.sub!(/^(?:Proposed )?Resolution (\[R\d\]|to|for) ./) {|c| c[-1..-1].upcase}
title.sub! /\.$/, ''
report.title ||= title.strip
report.meeting = date
report.attach = '@' + title
report.text = text.strip
# Columns:
# Pfx Title Match
# If Title is a number, then extract that part of the match
rules = [
:X, 2, /Terminat(e|ion of) the (.+?) (Project|PMC|Committee)/,
:X, 1, /Separate (.+?) from the Apache Software Foundation/,
:E, 1, /Establishing a PMC for a (.*) project/,
:E, 1, /Establish (.+?) as a top level project/,
:E, 1, /Establish (AsterixDB)/, # 2016_04_20
:E, 4, /Estab?lish(ing|ment)? (of )?(the |an )?(.+?) (board )?(PMC|[pP]roject|[cC]ommittee)$/,
:E, 2, /Creat(e|ion of) the (.+?) (Project|PMC)/,
:E, 2, /To (re-establish|create) the (.+?) PMC/,
:E, 2, /Reestablish(ing the)? (.+?)( Project| Committee | Team)/,
:E, 1, /^Apache (.+?) Project$/,
:C, 3, /(Change|Appoint).* Vice President of (the )?(.+)/,
:C, 2, /(Appoint|Establish) a new (.+?) PMC Chair/,
:C, 1, /New Vice President for the (.+?) PMC/,
:C, 1, /Appoint.* as the (.*?) of the ASF/,
:C, 1, /Appointment of (.*?) Committee Chair/,
:C, 3, /Appoint(ing a)? new [cC]hair (for|of the) (.*?)( Project|$)/,
:C, 1, /Alter the Chair of the (.+?) Project/,
:C, 2, /[cC]hange (the )?[cC]hair of the (.+?) (Project|PMC)/,
:C, 3, /[Cc]hang(e|ing) (to )?the (.+?) (Project |PMC )?Chair/,
:C, 2, /Change (of|the) (.+?) (PMC |Project |Committee )Chair/,
:C, 1, /Resolution to change the (.+?) Chair/,
:C, 1, /PMC chair change for (.+)/,
:C, 1, /Change PMC [Cc]hair for (.+?) Project/,
:C, 3, /Appoint a (new )?(chair for |Vice President of )(.+)/,
:C, 1, /Appoint .*? as (.+?) chairman/,
:C, 1, /Change Chair for Apache (.+)/,
:M, 1, /Reboot the (.+?) (PMC|Committee)/,
:M, 1, /(.+?) election of new PMC/,
:M, 2, /Update (membership of the )?(.+?) Committee/,
:M, 1, /Change to the (.*)? Committee Membership/,
:M, 1, /Change the Apache (.*) Project Name/,
:M, 1, /Change the Apache (.*) Project Management Committee/,
1, 1, /Update ?(audit.+?) Membership/i,
:M, 1, /Update ?(.+?) Membership/,
:R, 1, /Rename.* to the ?(.+?) Project/,
'@', 1, /(.*) Renewal/,
:C, 'Conference Planning', /Conferences? Committee/,
'@', 'Budget', /Spending Resolution/i,
'@', 'Budget', /Budget/i,
'@', 'Bylaws', /Bylaw/i,
'@', 'Chief Media Officer', /Chief Media Officer/i,
1, 'JCP', /Java Community Process/,
1, 'JCP', /JCP/,
1, 'Public Relations', /Public Relations/i,
1, 'Marketing and Publicity', /Press/i,
1, 'Legal Affairs', /License/i,
1, 'Legal Affairs', /Copyright/i,
1, 'Legal Affairs', /contributor agreement/i,
1, 'Legal Affairs', /CLA/,
1, 'Legal Affairs', /[MG]PL/,
1, 'Brand Management', /use.*feather/,
1, 'Brand Management', /Trademark/,
1, 'Brand Management', /use.*Apache name/,
1, 'Brand Management', /Brand Management/i,
1, 'Travel Assistance', /TAC/,
1, 'Travel Assistance', /Travel Assistance/,
1, 'Conference Planning', /Conference Planning/,
1, 'Fundraising', /Fundraising/,
1, 'Audit', /Audit/i,
:C, 'Public Relations', /Appoint Brian Fitzpatrick as a Vice President/,
'@', 'Appoint Executive Officers', /Appoint(ment of)? (new |ASF )?[oO]fficers/,
'@', 'Appoint Executive Officers', /Election of Officers/,
'@', 'Appoint Executive Officers', /Officer Appointments/i,
'@', 'Set Date for Members Meeting', /date.* member'?s meeting/i,
'@', 'PMC Membership Change Process', /Empower PMC chairs to change the membership/i,
'@', 'PMC Membership Change Process', /Amend the Procedure for PMC Membership Changes/i,
'@', 'Secretarial Assistant', /Approve contract with Jon Jagielski/,
'@', 'Alleged JBoss IP Infringement', /alleged JBoss IP infringe?ment/,
'@', 'Discussion Items', /^Discuss/
]
rules.each_slice(3) do |prefix, select, pattern|
match = pattern.match(report.title)
if match
report.subtitle = report.title
if select.is_a? Integer
report.title = match[select]
else
report.title = select
end
report.attach = "#{prefix}#{report.attach}"
break
end
end
report.title.sub! /^Apache /, ''
report.title.sub! /APR$/, 'Portable Runtime (APR)'
report.title.sub! /Portable Runtime$/, 'Portable Runtime (APR)'
report.title.sub! 'standing Audit', 'Audit'
report.title.sub! /^HTTPD?$/, 'HTTP Server'
report.title.sub! 'ISIS', 'Isis'
report.title.sub! 'iBatis', 'iBATIS'
report.title.sub! 'James', 'JAMES'
report.title.sub! 'infrastructure', 'Infrastructure'
report.title.sub! 'federated identity', 'Federated Identity'
report.title.sub! 'Open for Business', 'OFBiz'
report.title.sub! /^OpenEJB/, 'TomEE'
report.title.sub! /Perl-Apache( PMC)?/, 'Perl'
report.title.sub! /Public Relations Committee/, 'Public Relations'
report.title.sub! 'PRC', 'Public Relations'
report.title.sub! /Security$/, 'Security Team'
report.title.sub! 'Apache/TCL', 'Tcl'
report.title.sub! 'Orc', 'ORC'
report.title.sub! 'Steve', 'STeVe'
report.title.sub! 'Zest', 'Polygene'
report.title.sub! 'Openmeetings', 'OpenMeetings'
report.title.sub! 'Ace', 'ACE' # WHIMSY-31
pending[title] = report
end
# parse (Executive) Officer Reports
execs = minutes[/Officer Reports(.*?)\n[[:blank:]]{1,3}\d+\./m,1]
if execs
execs.sub! /\s*Executive officer reports approved.*?\n*\Z/, ''
# attachments start like this:
att_prefix = '\n[[:blank:]]{1,5}([A-Z])\.[[:blank:]]'
execs.scan(/
#{att_prefix}([^\n]*?)\n # attach, title
(.*?) # text
(?=#{att_prefix}|\Z) # separator
/mx).each do |attach, title, text|
next unless text
next unless title
next if title.start_with? 'This interim budget shows a surplus'
next if title.start_with? "President's discretionary fund returned to"
title.sub! 'Executive VP', 'Executive Vice President'
title.sub! 'Exec. V.P. and Secretary', 'Secretary'
report = OpenStruct.new
if title.include? ' ['
report.owners = title.split(' [').last.sub(']','').strip
title = title.split(' [').first
end
report.title ||= title.strip #.downcase
report.title.gsub! /^V\.?P\.? of /, ''
report.title.gsub! /\/Apache$/, ''
report.title = 'Infrastructure' if report.title =~ /Infrastructure/
report.title = 'Treasurer' if report.title =~ /Treasurer/
report.meeting = date
report.attach = '*' + title
report.text = text.dup
pending[title] = report
end
end
# Add to the running tally
pending.each_value do |report|
next if not report.title or report.title.empty?
# flag unposted reports; exclude unposted special orders
report.posted = posted.include? txt
next if not report.posted and
(report.attach =~ /^[A-Z]?@/ or report.attach !~ /^[A-Z.]/)
agenda[report.title] ||= []
agenda[report.title] << report
end
end
puts
# determine link for each report
link = {}
agenda.each do |title, reports|
link[title] = title.sub('C++','Cxx').gsub(/\W/,'_') + '.html'
end
# Simplify creating content
def getHTMLbody()
builder = Builder::XmlMarkup.new :indent => 2
yield builder
return Nokogiri::HTML(builder.target!).at('body').children
end
# Combine content produced here with the template fetched previously
def layout(title = nil)
builder = Builder::XmlMarkup.new :indent => 2
yield builder
content = Nokogiri::HTML(builder.target!)
if title
$calendar.at('title').content = "Board Meeting Minutes - #{title}"
# $calendar.at('h2').content = "Board Meeting Minutes - #{title}"
else
$calendar.at('title').content = "Board Meeting Minutes"
# $calendar.at('h2').content = "Board Meeting Minutes"
end
# Adjust the page header
# find the intro para; assume it is the first para with a strong tag
# then back up to the main container class for the page content
section = $calendar.at('.container p strong').parent.parent
# Extract all the paragraphs
paragraphs = section.search('p')
# remove all the existing content
section.children.each {|child| child.remove}
# Add the replacement first para
section.add_child getHTMLbody {|x|
x.p do
if title
x.text! "This was extracted (@ #{STAMP}) from a list of"
else # main index, which is always replaced if any input files have changed
# text below must agree with code that updates the index when no changes have occurred
x.text! "Last run: #{STAMP}. The data is extracted from a list of"
end
x.a 'minutes', :href => 'http://www.apache.org/foundation/records/minutes/'
x.text! "which have been approved by the Board."
x.br
x.strong 'Please Note'
# squiggly heredoc causes problems for Eclipse plugin, but leading spaces don't matter here
x.text! <<-EOT
The Board typically approves the minutes of the previous meeting at the
beginning of every Board meeting; therefore, the list below does not
normally contain details from the minutes of the most recent Board meeting.
EOT
end
}
# and the second para which is assumed to be the list of years
section.add_child paragraphs[1]
section.add_child "\n" # separator to make it easier to read source
# now add the content provided by the builder block
content.at('body').children.each {|child| section.add_child child}
$calendar.to_xhtml
end
Dir.entries(SITE_MINUTES).each do |p|
next unless p.end_with? '.html'
next if p == 'index.html'
unless link.has_value? p
unless KEEP
Wunderbar.info "Dropping #{p}"
File.delete(File.join(SITE_MINUTES,p))
else
Wunderbar.info "Outdated? #{p}"
end
end
end
# remove variable date from page
def remove_date(page)
# '%Y-%m-%d %H:%M'
page.sub /This was extracted \(@ \d\d\d\d-\d\d-\d\d \d\d:\d\d\) from a list of/,''
end
# output each individual report by owner
agenda.sort.each do |title, reports|
page = layout(title) do |x|
info = site[canonical[title.downcase]]
if info
# site information found, link to it
x.h1 do
x.a info[:name], :href => info[:link], :title => info[:text]
end
else
x.h1 title
end
reports.reverse.each do |report|
_id = report.meeting.gsub('_', '-')
x.h2 id: _id do
if report.posted
href = "http://apache.org/foundation/records/minutes/" +
"#{report.meeting[0...4]}/board_minutes_#{report.meeting}.txt"
else
href = ASF::SVN.svnpath!('foundation_board', "board_minutes_#{report.meeting}.txt")
end
x.a Date.parse(report.meeting.gsub('_','/')).strftime("%d %b %Y"),
href: href, id: "minutes_#{report.meeting}"
if report.owners
x.span "[#{report.owners}]", :style => 'font-size: 14px'
end
# Add headerlink marker
x.a '¶', href: "##{_id}", title: 'Permanent link', :class => 'headerlink'
end
x.h3 report.subtitle if report.subtitle
if report.posted
text = report.text.gsub(/^\t+/) {|tabs| " " * (8*tabs.length)}
text.gsub!(/ *$/, "")
indent = text.scan(/^([ ]+)/).flatten.min.to_s.length - 1
text.gsub! /^#{' '*indent}/, '' if indent > 0
text = $1 + text if text =~ /\A\w.*\n(\s+)/
text = text.to_s.rstrip
# N.B. The syntax "class: report" causes problems for the Eclipse Ruby plugin
x.pre text, 'class' => 'report' unless text.strip.empty?
if report.comments and report.comments.strip != ''
report.comments.split(/\n\s*\n/).each do |p|
x.p p, :style => "width: 40em"
end
elsif text.strip.empty?
if report.subtitle and not report.subtitle.empty?
x.p {x.em 'Discussion Item with no text or minutes'}
else
x.p {x.em 'A report was expected, but not received'}
end
end
elsif report.text.strip.empty?
x.p {x.em 'A report was expected, but not received'}
else
x.p do
x.em 'Report was filed, but display is awaiting the approval ' +
'of the Board minutes.'
end
end
end
end
dest = "#{SITE_MINUTES}/#{link[title]}"
unless File.exist?(dest) and remove_date(File.read(dest)) == remove_date(page)
Wunderbar.info "Writing #{link[title]}"
open(dest, 'w') {|file| file.write page}
# else
# Wunderbar.info "Not updating #{link[title]}"
end
end
# output index
agenda = agenda.sort_by {|title, reports| title.downcase}
page = layout do |x|
x.h2 "Executive Officer Reports", :id => 'executive'
x.ul do
agenda.each do |title, reports|
next unless reports.last.attach =~ /^\*/
next if reports.length == 1
x.li do
x.a title, :href => link[title]
end
end
end
x.h2 "Additional Officer Reports", :id => 'officer'
x.ul do
agenda.each do |title, reports|
next unless reports.last.attach =~ /^\d/
next if reports.length == 1
x.li do
x.a title, :href => link[title]
end
end
end
x.h2 "Committee Reports", :id => 'committee'
list = []
agenda.each do |title, reports|
next unless reports.last.attach =~ /^[A-Z]/
next if reports.length == 1
list << title
end
cols = 6
slice = (list.length+cols-1)/cols
x.table do
(0...slice).each do |i|
x.tr do
(0...cols).each do |j|
x.td do
title = list[i+j*slice]
if title
info = site[canonical[title.downcase]]
if info
x.a title, :href => link[title], :title => info[:text]
else
if cinfo['committees'][title]
x.em { x.a title, :href => link[title] }
else
x.del { x.a title, :href => link[title] }
end
end
end
end
end
end
end
end
x.h2 "Podling Reports", :id => 'podling'
list = []
agenda.each do |title, reports|
next unless reports.last.attach =~ /^[.]/
list << title
end
cols = 6
slice = (list.length+cols-1)/cols
x.table do
(0...slice).each do |i|
x.tr do
(0...cols).each do |j|
x.td do
title = list[i+j*slice]
if title
info = site[canonical[title.downcase]]
if info
if %w{dormant retired}.include? info[:status]
x.del do
x.a title, :href => link[title], :title => info[:text]
end
else
x.a title, :href => link[title], :title => info[:text]
end
else
x.em { x.a title, :href => link[title] }
end
end
end
end
end
end
end
x.h2 "Repeating Special Orders", :id => 'orders'
x.ul do
agenda.each do |title, reports|
next unless reports.last.attach =~ /^@/
next if reports.length == 1
x.li do
x.a title, :href => link[title]
end
end
end
x.h2 "Other Attachments, Special Orders, and Discussions", :id => 'other'
x.ul do
other = {}
agenda.each do |title, reports|
next unless reports.length == 1
next if reports.last.attach =~ /^[.]/
other[reports.first.subtitle || title] = title
end
other.sort.each do |subtitle, title|
x.li do
x.a subtitle, :href => link[title]
end
end
end
x.h2 "Other Agenda Items", :id => 'agenda'
x.ul do
agenda.each do |title, reports|
next unless reports.last.attach =~ /^\+/
next if reports.length == 1
x.li do
x.a title, :href => link[title]
end
end
end
end
open(INDEX_FILE, 'w') {|file| file.write page}
Wunderbar.info "Wrote #{SITE_MINUTES}/index.html"