tools/collate_minutes.rb (876 lines of code) (raw):

#!/usr/bin/env ruby $LOAD_PATH.unshift '/srv/whimsy/lib' require 'whimsy/asf' 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(File.join('..', '..', 'www', 'board', 'minutes'), __FILE__) # list of SVN resources needed SVN_SITE_RECORDS_MINUTES = ASF::SVN['minutes'] BOARD = ASF::SVN['foundation_board'] 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) NOWARN_LAYOUT = ARGV.delete '--nowarn_layout' # don't add layout change warning to pages (for debug compares) DUMP_AGENDA = ARGV.delete '--dump_agenda' # output agenda details to stdout DUMP_PENDING = ARGV.delete '--dump_pending' # output agenda details to stdout STAMP = (NOSTAMP ? Time.new(1970) : Time.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 = File.join(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 'Processing input 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: "http://incubator.apache.org/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__) if File.exist?(local_copy) && (Time.now - File.stat(local_copy).mtime < 3600) 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 &nbsp;) 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', '&#187;').gsub('&nbsp;', '&#160;')) # 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-bottom: 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 # handle project name changes # see also www/board/minutes/.htaccess # also see parse (Executive) Officer Reports ca. line 670 def name_changes(title) title.sub! 'Ace', 'ACE' # WHIMSY-31 title.sub! 'ADF Faces', 'MyFaces' # via Trinidad title.sub! 'Amber', 'Oltu' title.sub! 'Apache/TCL', 'Tcl' title.sub! 'Argus', 'Ranger' title.sub! 'ASF Rep. for W3C', 'W3C Relations' title.sub! 'Bean Validation', 'BVal' title.sub! 'BeanValidation', 'BVal' title.sub! 'Bluesky', 'BlueSky' title.sub! 'BRPC', 'brpc' title.sub! 'Callback', 'Cordova' title.sub! 'Conferences', 'Conference Planning' title.sub! 'Cxx Standard Library', 'C++ Standard Library' title.sub! 'Deft', 'AWF' title.sub! 'DLab', 'DataLab' title.sub! 'Distributed Release Audit Tool (DRAT)', 'DRAT' title.sub! 'Dolphin Scheduler', 'DolphinScheduler' # board_minutes_2019_11_20.txt title.sub! 'Easyant', 'EasyAnt' title.sub! 'Empire-DB', 'Empire-db' title.sub! 'Fleece', 'Johnzon' title.sub! 'Geroniomo', 'Geronimo' title.sub! 'iBatis', 'iBATIS' title.sub! 'infrastructure', 'Infrastructure' title.sub! 'ISIS', 'Causeway' title.sub! 'Isis', 'Causeway' title.sub! 'IVY', 'Ivy' title.sub! 'JackRabbit', 'Jackrabbit' title.sub! 'James', 'JAMES' title.sub! 'Java Community Process', 'JCP' title.sub! 'JSecurity', 'Shiro' title.sub! 'Juice', 'JuiCE' title.sub! 'log4php', 'Log4php' title.sub! 'Lucene.NET', 'Lucene.Net' title.sub! 'lucene4c', 'Lucene4c' title.sub! 'MesaTEE', 'Teaclave' title.sub! 'Ode', 'ODE' title.sub! 'ODFToolkit', 'ODF Toolkit' title.sub! 'Open for Business', 'OFBiz' title.sub! 'TomEE (OpenEJB)', 'TomEE' title.sub! 'OpenEJB', 'TomEE' title.sub! 'Openmeetings', 'OpenMeetings' title.sub! 'OpenOffice.org', 'OpenOffice' title.sub! 'Optiq', 'Calcite' title.sub! 'Orc', 'ORC' title.sub! 'Oscar', 'Felix' title.sub! 'PonyMail', 'Pony Mail' title.sub! 'PRC', 'Public Relations' title.sub! 'Public Relations Commitee', 'Public Relations' title.sub! 'Quarks', 'Edgent' title.sub! 'SensSoft', 'Flagon' title.sub! 'Servicecomb', 'ServiceComb' title.sub! 'Singa', 'SINGA' title.sub! 'Socialsite', 'SocialSite' title.sub! 'stdcxx', 'C++ Standard Library' title.sub! 'STDCXX', 'C++ Standard Library' title.sub! 'Steve', 'STeVe' title.sub! 'Stratosphere', 'Flink' title.sub! 'SystemML', 'SystemDS' title.sub! 'TCL', 'Tcl' title.sub! 'TubeMQ', 'InLong' title.sub! 'Web services', 'Web Services' title.sub! 'Zest', 'Polygene' title.sub! "Infrastructure (President's)", 'Infrastructure' title.sub! %r{\bKi\b}, 'Shiro' title.sub! %r{^HTTPD?$}, 'HTTP Server' title.sub! %r{^Infrastructure .*}, 'Infrastructure' title.sub! %r{^Labs .*}, 'Labs' title.sub! %r{^Logging$}, 'Logging Services' title.sub! %r{APR$}, 'Portable Runtime (APR)' title.sub! %r{CeltiX[Ff]ire}, 'CXF' title.sub! %r{Fund[- ][rR]aising}, 'Fundraising' title.sub! %r{Perl-Apache( PMC)?}, 'Perl' title.sub! %r{Portable Runtime$}, 'Portable Runtime (APR)' title.sub! %r{Public Relations Committee}, 'Public Relations' title.sub! %r{Security$}, 'Security Team' end agenda = {} posted = Dir[MINUTES_PATH].sort unapproved = Dir[File.join(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 Wunderbar.info "Parsing input for #{date}" seen[date] = txt minutes = open(txt) {|file| file.read} 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 owners = nil if title =~ /^Report from the(?: VP of)? (.+)/i title = $1 if title =~ /^(.+?) +\[([^\]]+)\]/ title = $1 owners = $2 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 /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' name_changes(title) 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.owners ||= owners if owners 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 elsif title =~ /Written Consent of the Directors/ report.attach = '@' + attach end if title == 'Incubator' and text sections = text.split(/\nStatus [rR]eport (.*)\n=+\n/) # Some early 2012 minutes have a 'Detailed Reports' header before the first podling report # i.e. the podling reports follow the line # '-------------------- Detailed Reports --------------------' # instead of the following # '--------------------' # Some reports include trailing spaces after the ---- # podling header may now be prefixed with ## (since June 2019) # Also there may be a blank line before the ## sections = text.split(/\n[-=][-=]+(?: Detailed Reports ---+)?\s*\n(?:\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' name_changes(title) 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 cs = comments.strip report.comments = cs if cs.length > 0 end # fill in comments from missing reports # TODO: temporarily omit Additional Officer processing as it generates some incorrect ownership ['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 # Why? next if comments.include? 'See Attachment' # handled above owners = comments[/\[([^\n]+)\]/,1] comments.sub!(/.*\s+\n/, '') next if comments.empty? # TODO: This does not work properly attach = ('A'..attach).count.to_s if section == 'Additional Officer' report = pending[attach] ||= OpenStruct.new report.meeting = date report.attach = attach report.owners = owners cs = comments.strip report.comments = cs if cs.length > 0 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 title.sub! 'VP, Data Privacy', 'VP Data Privacy' title.sub! /Executive Session \(\d\d.*?\)/, 'Executive Session' # Drop times from titles 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 elsif title =~ /date.+member.+meeting/i || title =~ /member.+meeting.+date/i report.subtitle = title report.title = 'Set Date for Members Meeting' report.attach = '@' + 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! 'Standardise the privacy policy for Foundation web sites', 'Standardise privacy policy for foundation websites' 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 /, '' name_changes(report.title) report.title.sub! 'standing Audit', 'Audit' report.title.sub! 'federated identity', 'Federated Identity' report.title.sub! 'WSIF', 'Web Services' 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' title.sub! 'Vice Chairman', 'Vice Chair' title.sub! 'Acting Chairman', 'Board Chair' # merge report(s) from acting chair title.sub! 'Chairman', 'Board Chair' 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 if DUMP_PENDING puts 'Dump of pending data for ' + date pending.each do |k,v| puts "#{k} #{k == v.attach ? '==' : '!='} #{v.attach}" puts v.title puts "O: #{v.owners}" if v.owners puts "S: #{v.subtitle}" if v.subtitle p "C: #{v.comments}" if v.comments text = v.text puts "#{text.size} #{text.split("\n",2)[0]}" puts '' 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 if DUMP_AGENDA puts 'Dump of agenda data for this run' agenda.each do |title, reports| p [reports.length > 1 ? '>1' : '=1', reports.last.attach[0..1], reports.length, title] end end Wunderbar.info 'Starting to generate output' # 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 collate_minutes.rb 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 unless NOWARN_LAYOUT x.br x.br x.strong 'WARNING: these pages may omit some original contents of the minutes.' x.br x.text 'This is due to changes in the layout of the source minutes over the years.' x.text 'Fixes are being worked on.' end 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_html 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 = File.join(SITE_MINUTES, link[title]) if force or !File.exist?(dest) or (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 # Classification scheme # Pfx = reports.last.attach[0] # Count = reports.length # # Pfx Count Section # '*' >1 Executive Officer Reports # 0-9 >1 Additional Officer Reports # A-Z >1 Committee Reports # '.' any Podling Reports # '@' >1 Repeating Special Orders # '+' >1 Other Agenda Items # !'.' =1 Other Attachments, Special Orders, and Discussions # 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"