whimsical-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From Sam Ruby <ru...@apache.org>
Subject [whimsy.git] [1/2] Commit 2cb9320: import board minutes tool
Date Fri, 15 Jan 2016 20:25:16 GMT
Commit 2cb932043ed8c28b38ae240ee16a941f47563291:
    import board minutes tool
    Previously located at
    https://svn.apache.org/repos/private/foundation/board/scripts/collate_minutes.rb


Branch: refs/heads/master
Author: Sam Ruby <rubys@intertwingly.net>
Committer: Sam Ruby <rubys@intertwingly.net>
Pusher: rubys <rubys@apache.org>

------------------------------------------------------------
tools/collate_minutes.rb                                     | +++++++++++++ 
www/.gitignore                                               | ++ 
www/board/.gitignore                                         | + 
------------------------------------------------------------
772 changes: 772 additions, 0 deletions.
------------------------------------------------------------


diff --git a/tools/collate_minutes.rb b/tools/collate_minutes.rb
new file mode 100755
index 0000000..0cf7beb
--- /dev/null
+++ b/tools/collate_minutes.rb
@@ -0,0 +1,769 @@
+#!/usr/bin/ruby
+require 'whimsy/asf'
+require 'date'
+require 'builder'
+require 'ostruct'
+require 'nokogiri'
+require 'net/https'
+require 'fileutils'
+
+# destination directory
+SITE_MINUTES = ASF::Config.get(:board_minutes) ||
+  File.expand_path('../../www/board/minutes', __FILE__)
+
+# list of SVN resources needed
+resources = {
+  TEMPLATES: 'asf/infrastructure/site/trunk/templates',
+  INCUBATOR_SITE_AUTHOR: 'asf/incubator/public/trunk/content',
+  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 ' +
+      "https://svn.apache.org/repos/#{location}"
+    exit 1
+  end
+end
+
+incubator = URI.parse('http://incubator.apache.org/')
+
+# quick exit if everything is up to date
+if File.exist? "#{SITE_MINUTES}/index.html"
+  input = Dir["#{SVN_SITE_RECORDS_MINUTES}/*/board_minutes_20*.txt",
+    "#{BOARD}/board_minutes_20*.txt"].
+    map {|name| File.stat(name).mtime}.push(File.stat(__FILE__).mtime).max
+  exit if File.stat("#{SITE_MINUTES}/index.html").mtime >= input
+end
+
+# mapping of committee names to canonical names (generally from ldap)
+canonical = Hash.new {|hash, name| name}
+canonical.merge! \
+    'conference planning'         => 'concom',
+    'conferences'                 => 'concom',
+    'security team'               => 'security',
+    'c++ standard library'        => 'stdcxx'
+
+# parse podling information
+site = {}
+podlings = Nokogiri::XML(File.read("#{INCUBATOR_SITE_AUTHOR}/podlings.xml"))
+podlings.search('podling').each do |podling|
+  if podling['name'].downcase != podling['resource']
+    canonical[podling['name'].downcase] = podling['resource']
+  end
+
+  if podling['status'] == 'graduated' and podling['enddate']
+    next if podling['enddate'].length < 10
+    next if Date.today - Date.parse(podling['enddate']) > 90
+  end
+
+  site[podling["resource"]] = {
+    :name => podling["name"],
+    :status => podling["status"],
+    :link => incubator + "projects/#{podling["resource"]}.html",
+    :text => podling.at('description').text
+  }
+end
+
+# parse site information
+index = File.read("#{TEMPLATES}/index.html")
+projects = index[/<section id="projects-list".*?<\/section>/m]
+projects.scan(/<a href="(.*?)" title="(.*?)">(.*?)</).each do |link, text, name|
+  site[canonical[name.downcase]] = {:name => name, :link => link, :text => text}
+end
+
+skeleton = File.read("#{TEMPLATES}/index.html")
+projects = skeleton[/<section id="projects-list">(.*?)<\/section>/m,1]
+projects.scan(/<a href="(.*?)" title="(.*?)">(.*?)</).each do |link, text, name|
+  cname = canonical[name.downcase]
+  link = 'https://www.apache.org' + link if link =~ /^\//
+  site[cname] = {:name => name, :link => link, :text => text}
+end
+
+# parse the calendar for layout info (note: hack for &raquo)
+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;'))
+
+# 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["#{SVN_SITE_RECORDS_MINUTES}/*/board_minutes_20*.txt"].sort
+unapproved = Dir["#{BOARD}/board_minutes_20*.txt"].sort
+
+FileUtils.mkdir_p SITE_MINUTES
+
+(posted+unapproved).each do |txt|
+  date = $1 if txt =~ /(\d\d\d\d_\d\d_\d\d)/
+  next unless date
+  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                       # report
+    (?=-{41,}\n(?:End|Attach))     # separator
+  /mx).each do |attach,title,text|
+    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! /^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! '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! '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/)
+      sections = text.split(/\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'
+
+          title.sub! 'ACE', 'Ace'
+          title.sub! 'Bean Validation', 'BeanValidation'
+          title.sub! 'Bluesky', 'BlueSky'
+          title.sub! 'Easyant', 'EasyAnt'
+          title.sub! 'Callback', 'Cordova'
+          title.sub! 'Empire-DB', 'Empire-db'
+          title.sub! 'IVY', 'Ivy'
+          title.sub! 'JackRabbit', 'Jackrabbit'
+          title.sub! 'Juice', 'JuiCE'
+          title.sub! 'log4php', 'Log4php'
+          title.sub! 'lucene4c', 'Lucene4c'
+          title.sub! 'Lucene.NET', 'Lucene.Net'
+          title.sub! 'Ode', 'ODE'
+          title.sub! 'ODFToolkit', 'ODF Toolkit'
+          title.sub! 'OpenOffice.org', 'OpenOffice'
+          title.sub! 'OpenEJB', 'TomEE'
+          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
+  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
+    report = OpenStruct.new
+    report.title ||= title #.downcase
+    report.meeting = date
+    report.attach = '+' + title
+    report.text = text.strip
+    pending[title] = report
+  end
+
+  # parse Special Orders
+  orders = minutes.split(/^ \d\. Special Orders/,2).last.split(/^ \d\./,2).first
+  orders.scan(/
+    \s{4}([A-Z])\.          # agenda item
+    \s+(.*?)\n\s*\n         # title
+    (.*?)                   # text
+    (?=\n\s{4,5}[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!(/^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
+
+    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, 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/,
+       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, '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,
+      '@', '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? Fixnum
+          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'
+    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'
+
+    pending[title] = report
+  end
+
+  # parse (Executive) Officer Reports
+  execs = minutes[/Officer Reports(.*?)\n\s+\d+\./m,1]
+  if execs
+    execs.scan(/
+      \n\s+(\w+)\.\s([^\n]*?)\n         # attach, title
+      (.*?)                             # text
+      (?=\n\s+\w\.\s|\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
+      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
+
+# 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
+  section = $calendar.at('.container p strong').parent.parent
+  paragraphs = section.search('p')
+  paragraphs.first.children.last.content =
+    paragraphs.first.children.last.content.sub 'is a', 'is extracted from a'
+
+  section.children.each {|child| child.remove}
+  section.add_child paragraphs[0]
+  section.add_child paragraphs[1]
+  content.at('body').children.each {|child| section.add_child child}
+
+  $calendar.to_xhtml
+end
+
+# output each individual report by owner
+agenda.sort.each do |title, reports|
+  puts title
+  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|
+      x.h2 do
+        if report.posted
+          href = "http://apache.org/foundation/records/minutes/" +
+            "#{report.meeting[0...4]}/board_minutes_#{report.meeting}.txt"
+        else
+          href = 'https://svn.apache.org/repos/private/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
+      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
+        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?
+          x.p {x.em 'A report was expected, but not received'}
+        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 File.read(dest) == page
+    open(dest, 'w') {|file| file.write page}
+  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
+                x.em { x.a title, :href => link[title] }
+              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.em 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 and Special Orders", :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("#{SITE_MINUTES}/index.html", 'w') {|file| file.write page}
+
+if `hostname`.strip == 'rubix'
+  system "rsync -av #{SITE_MINUTES}/ " +
+    "people.apache.org:public_html/board/minutes/"
+end
diff --git a/www/.gitignore b/www/.gitignore
index b844b14..9e159d1 100644
--- a/www/.gitignore
+++ b/www/.gitignore
@@ -1 +1,3 @@
 Gemfile.lock
+logs
+public
diff --git a/www/board/.gitignore b/www/board/.gitignore
new file mode 100644
index 0000000..5cf0e30
--- /dev/null
+++ b/www/board/.gitignore
@@ -0,0 +1 @@
+minutes

Mime
View raw message