blob: 2f9f98acf64b16ede5193eaef9d97938d14e14d1 [file] [log] [blame]
# Special Orders
class ASF::Board::Agenda
parse do
orders = @file.split(/^ \d. Special Orders/,2).last.
split(/^ \d. Discussion Items/,2).first
pattern = /
\n+(?<indent>\s{3,5})(?<section>[A-Z])\.
\s(?<title>.*?)\n
(?<text>.*?)
(?=\n\s{4}[A-Z]\.\s|\z)
/mx
people = []
scan orders, pattern do |attrs|
attrs['section'] = '7' + attrs['section']
title = attrs['title']
title.strip!
fulltitle = title.dup
title.sub! /^Resolution to /, ''
title.sub! /\sthe\s/, ' '
title.sub! /\sApache\s/, ' '
title.sub! /\sCommittee\s/, ' '
title.sub! /\sProject(\s|$)/i, '\1'
title.sub! /\sPMC(\s|$)/, '\1'
if title =~ /^Establish .* \((.*)\)$/
title.sub! /\s.*?\(/, ' '
title.sub! /\)$/, ''
else
title.sub! /\s\(.*\)$/, ''
end
attrs['fulltitle'] = fulltitle if title != fulltitle
text = attrs['text']
attrs['digest'] = Digest::MD5.hexdigest(text.strip)
attrs['warnings'] = []
if attrs['indent'] != ' '
attrs['warnings'] << 'Heading is not indented 4 spaces'
end
if text.sub(/s+\Z/,'').scan(/^ *\S/).map(&:length).min != 8
attrs['warnings'] << 'Resolution is not indented 7 spaces'
end
title_checks = {
/^Establish/i => /^Establish the Apache .* (Project|Committee)$/,
/^Change.*Chair/i => /^Change the Apache .* Project Chair$/,
/^Terminate/i => /^Terminate the Apache .* Project$/,
}
title_checks.each do |select, match|
if fulltitle =~ select and fulltitle !~ match and
(fulltitle + text) =~ /chair|project|committee/i
then
attrs['warnings'] <<
"Non-standard title wording: #{fulltitle.inspect}; " +
"expected #{match.inspect}"
end
end
attrs.delete 'indent'
attrs.delete 'warnings' if attrs['warnings'].empty?
next if @quick
asfid = '[a-z][-.a-z0-9_]+' # dot added to help detect errors
list_item = '^[[:blank:]]*(?:[-*\u2022]\s*)?(.*?)[[:blank:]]+'
people = text.scan(/#{list_item}\((#{asfid})\)\s*$/)
people += text.scan(/#{list_item}\((#{asfid})(?:@|\s*at\s*)
(?:\.\.\.|apache\.org|apache\sdot\sorg)\)\s*$/xi)
people += text.scan(/#{list_item}<(#{asfid})(?:@|\s*at\s*)
(?:\.\.\.|apache\.org|apache\sdot\sorg)>\s*$/xi)
need_chair = false
whimsy = 'https://whimsy.apache.org'
if title =~ /Change (.*?) Chair/ or title =~ /Terminate (\w+)$/
people.clear
committee = ASF::Committee.find($1)
attrs['roster'] =
"#{whimsy}/roster/committee/#{CGI.escape committee.name}"
attrs['stats'] = 'https://reporter.apache.org/wizard/statistics?' +
CGI.escape(committee.name)
attrs['prior_reports'] = minutes(committee.display_name)
ids = text.scan(/\((\w[-.\w]+)\)/).flatten
unless ids.empty?
ids.each do |id|
person = ASF::Person.find(id)
people << [person.public_name, id] if person.icla
end
end
next unless committee.names
committee.names.each do |id, name|
people << [name, id] if text.include? name or title.include? 'Term'
end
if people.length < 2 and not title.start_with? 'Terminate'
attrs['warnings'] ||= ['Unable to match expected number of names']
attrs['names'] = committee.names
end
if title =~ /Change (.*?) Chair/
need_chair = true
elsif committee.chair # Terminate
attrs['chair'] = committee.chair.id
end
elsif title =~ /Establish (.*)/
name = $1
attrs['prior_reports'] =
"#{whimsy}/board/minutes/#{name.gsub(/\W/,'_')}"
if text.scan(/[<(][-.\w]+@(?:[-\w]+\.)+\w+[>)]/).
any? {|email| not email.include? 'apache.org'}
then
attrs['warnings'] ||= ['non apache.org email address found']
end
need_chair = true if fulltitle =~ /chair|project|committee/i
# extract the committee charter
charters = []
text.scan(%r{\srelated to\s+(.+?)(?:;|\.?\n\n)}m) do |rto|
charters << rto.first.gsub(/\s+/,' ')
end
if charters.size != 2
attrs['warnings'] ||= "Expected 2 'related to' phrases; found #{charters.size}"
elsif charters[0] != charters[1]
attrs['warnings'] ||= "'related to' phrases disagree: '#{charters[0]}' != '#{charters[1]}'"
end
attrs['charter'] = charters.first
end
if need_chair
if text =~ /(BE IT|FURTHER) RESOLVED, that\s+([^,]*?),?\s+be\b/
chairname = $2.gsub(/\s+/, ' ').strip
if chairname =~ /\s\(([-.\w]+)\)$/
# if chair's id is present in parens, use that value
attrs['chair'] = $1 unless $1.empty?
chairname.sub! /\s+\(.*\)$/, ''
else
# match chair's name against people in the committee
chair = people.find {|person| person.first == chairname}
attrs['chair'] = (chair ? chair.last : nil)
end
unless people.include? [chairname, attrs['chair']]
if people.empty?
attrs['warnings'] ||= ['Unable to locate PMC email addresses']
elsif attrs['chair']
attrs['warnings'] ||= ['Chair not member of PMC']
else
attrs['warnings'] ||= ['Chair not found in resolution']
end
end
else
attrs['warnings'] ||= ['Chair not found in resolution']
end
elsif title =~ /^Appoint /
if text =~ /FURTHER\s+RESOLVED, that\s+([^,]*?),?\s+be\b/i
chairname = $1.gsub(/\s+/, ' ').strip
chair = ASF.search_one(ASF::Person.base, "cn=#{chairname}")
attrs['chairname'] = chairname
attrs['chair'] = chair.first['uid'].first if chair.length == 1
end
if attrs['chair']
people = [[chairname, attrs['chair']]]
elsif chairname
attrs['warnings'] ||=
["#{chairname.inspect} doesn't match public name"]
else
attrs['warnings'] ||= ['Officer name not found']
end
end
people.map! do |name, id|
person = ASF::Person.new(id)
icla = person.icla
[id, {name: name, icla: icla ? person.icla.name : false,
member: person.asf_member?}]
end
attrs['people'] = Hash[people] unless people.empty?
end
end
end