whimsy-asf 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/asf.gemspec +32 -0
- data/asf.version +1 -0
- data/lib/whimsy/asf.rb +9 -0
- data/lib/whimsy/asf/agenda.rb +117 -0
- data/lib/whimsy/asf/agenda/attachments.rb +42 -0
- data/lib/whimsy/asf/agenda/back.rb +29 -0
- data/lib/whimsy/asf/agenda/committee.rb +20 -0
- data/lib/whimsy/asf/agenda/exec-officer.rb +34 -0
- data/lib/whimsy/asf/agenda/front.rb +72 -0
- data/lib/whimsy/asf/agenda/minutes.rb +23 -0
- data/lib/whimsy/asf/agenda/special.rb +107 -0
- data/lib/whimsy/asf/auth.rb +28 -0
- data/lib/whimsy/asf/committee.rb +124 -0
- data/lib/whimsy/asf/icla.rb +67 -0
- data/lib/whimsy/asf/ldap.rb +232 -0
- data/lib/whimsy/asf/mail.rb +81 -0
- data/lib/whimsy/asf/member.rb +82 -0
- data/lib/whimsy/asf/nominees.rb +32 -0
- data/lib/whimsy/asf/podlings.rb +23 -0
- data/lib/whimsy/asf/rack.rb +81 -0
- data/lib/whimsy/asf/site.rb +24 -0
- data/lib/whimsy/asf/svn.rb +27 -0
- data/lib/whimsy/asf/watch.rb +41 -0
- metadata +149 -0
data/asf.gemspec
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
version = File.read(File.expand_path('../asf.version', __FILE__)).strip
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
|
5
|
+
# Change these as appropriate
|
6
|
+
s.name = "whimsy-asf"
|
7
|
+
s.license = 'Apache License, Version 2.0'
|
8
|
+
s.version = version
|
9
|
+
s.summary = "Whimsy 'model' of the ASF"
|
10
|
+
s.author = "Sam Ruby"
|
11
|
+
s.email = "rubys@intertwingly.net"
|
12
|
+
s.homepage = "https://whimsy.apache.org/"
|
13
|
+
s.description = <<-EOD
|
14
|
+
This package contains a set of classes which encapsulate access
|
15
|
+
to a number of data sources such as LDAP, ICLAs, auth lists, etc.
|
16
|
+
EOD
|
17
|
+
|
18
|
+
# Add any extra files to include in the gem
|
19
|
+
s.files = Dir.glob(["asf.*", "lib/**/*"])
|
20
|
+
s.require_paths = ["lib"]
|
21
|
+
|
22
|
+
# If you want to depend on other gems, add them here, along with any
|
23
|
+
# relevant versions
|
24
|
+
s.add_dependency("nokogiri")
|
25
|
+
s.add_dependency("rack")
|
26
|
+
s.add_dependency("ruby-ldap")
|
27
|
+
s.add_dependency("tzinfo")
|
28
|
+
s.add_dependency("wunderbar")
|
29
|
+
|
30
|
+
# If your tests use any gems, include them here
|
31
|
+
# s.add_development_dependency("mocha") # for example
|
32
|
+
end
|
data/asf.version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.1
|
data/lib/whimsy/asf.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
require File.expand_path('../asf/committee', __FILE__)
|
2
|
+
require File.expand_path('../asf/ldap', __FILE__)
|
3
|
+
require File.expand_path('../asf/mail', __FILE__)
|
4
|
+
require File.expand_path('../asf/svn', __FILE__)
|
5
|
+
require File.expand_path('../asf/watch', __FILE__)
|
6
|
+
require File.expand_path('../asf/nominees', __FILE__)
|
7
|
+
require File.expand_path('../asf/icla', __FILE__)
|
8
|
+
require File.expand_path('../asf/auth', __FILE__)
|
9
|
+
require File.expand_path('../asf/member', __FILE__)
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require_relative '../asf'
|
2
|
+
|
3
|
+
require 'time'
|
4
|
+
require 'tzinfo'
|
5
|
+
require 'digest/md5'
|
6
|
+
|
7
|
+
module ASF
|
8
|
+
module Board
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class ASF::Board::Agenda
|
13
|
+
CONTENTS = {
|
14
|
+
'2.' => 'Roll Call',
|
15
|
+
'3A' => 'Minutes',
|
16
|
+
'4A' => 'Executive Officer',
|
17
|
+
'1' => 'Additional Officer',
|
18
|
+
'A' => 'Committee Reports',
|
19
|
+
'7A' => 'Special Orders',
|
20
|
+
'8.' => 'Discussion Items',
|
21
|
+
'9.' => 'Action Items'
|
22
|
+
}
|
23
|
+
|
24
|
+
@@parsers = []
|
25
|
+
def self.parse(file=nil, &block)
|
26
|
+
@@parsers << block if block
|
27
|
+
new.parse(file) if file
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize
|
31
|
+
@sections = {}
|
32
|
+
end
|
33
|
+
|
34
|
+
def scan(text, pattern, &block)
|
35
|
+
text.scan(pattern).each do |matches|
|
36
|
+
hash = Hash[pattern.names.zip(matches)]
|
37
|
+
yield hash if block
|
38
|
+
|
39
|
+
section = hash.delete('section')
|
40
|
+
section ||= hash.delete('attach')
|
41
|
+
|
42
|
+
if section
|
43
|
+
hash['approved'] &&= hash['approved'].strip.split(/[ ,]+/)
|
44
|
+
|
45
|
+
@sections[section] ||= {}
|
46
|
+
next if hash['text'] and @sections[section]['text']
|
47
|
+
@sections[section].merge!(hash)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def parse(file)
|
53
|
+
@file = file
|
54
|
+
@@parsers.each { |parser| instance_exec(&parser) }
|
55
|
+
|
56
|
+
# add index markers for major sections
|
57
|
+
CONTENTS.each do |section, index|
|
58
|
+
@sections[section][:index] = index if @sections[section]
|
59
|
+
end
|
60
|
+
|
61
|
+
# cleanup text and comment whitespace
|
62
|
+
@sections.each do |section, hash|
|
63
|
+
text = hash['text'] || hash['report']
|
64
|
+
if text
|
65
|
+
text.sub!(/\A\s*\n/, '')
|
66
|
+
text.sub!(/\s+\Z/, '')
|
67
|
+
unindent = text.sub(/s+\Z/,'').scan(/^ *\S/).map(&:length).min || 1
|
68
|
+
text.gsub! /^ {#{unindent-1}}/, ''
|
69
|
+
end
|
70
|
+
|
71
|
+
text = hash['comments']
|
72
|
+
if text
|
73
|
+
text.sub!(/\A\s*\n/, '')
|
74
|
+
text.sub!(/\s+\Z/, '')
|
75
|
+
unindent = text.sub(/s+\Z/,'').scan(/^ *\S/).map(&:length).min || 1
|
76
|
+
text.gsub! /^ {#{unindent-1}}/, ''
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# add roster and prior report link
|
81
|
+
whimsy = 'https://whimsy.apache.org'
|
82
|
+
@sections.each do |section, hash|
|
83
|
+
next unless section =~ /^(4[A-Z]|\d+|[A-Z][A-Z]?)$/
|
84
|
+
committee = ASF::Committee.find(hash['title'] ||= 'UNKNOWN')
|
85
|
+
unless section =~ /^4[A-Z]$/
|
86
|
+
hash['roster'] =
|
87
|
+
"#{whimsy}/roster/committee/#{CGI.escape committee.name}"
|
88
|
+
end
|
89
|
+
hash['prior_reports'] = minutes(committee.display_name)
|
90
|
+
end
|
91
|
+
|
92
|
+
# add attach to section
|
93
|
+
@sections.each do |section, hash|
|
94
|
+
hash[:attach] = section
|
95
|
+
end
|
96
|
+
|
97
|
+
@sections.values
|
98
|
+
end
|
99
|
+
|
100
|
+
def minutes(title)
|
101
|
+
"https://whimsy.apache.org/board/minutes/#{title.gsub(/\W/,'_')}"
|
102
|
+
end
|
103
|
+
|
104
|
+
def timestamp(time)
|
105
|
+
date = @file[/(\w+ \d+, \d+)/]
|
106
|
+
tz = TZInfo::Timezone.get('America/Los_Angeles')
|
107
|
+
tz.local_to_utc(Time.parse("#{date} #{time}")).to_i * 1000
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
require_relative 'agenda/front'
|
112
|
+
require_relative 'agenda/minutes'
|
113
|
+
require_relative 'agenda/exec-officer'
|
114
|
+
require_relative 'agenda/attachments'
|
115
|
+
require_relative 'agenda/committee'
|
116
|
+
require_relative 'agenda/special'
|
117
|
+
require_relative 'agenda/back'
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# Attachments
|
2
|
+
|
3
|
+
class ASF::Board::Agenda
|
4
|
+
parse do
|
5
|
+
pattern = /
|
6
|
+
-{41}\n
|
7
|
+
Attachment\s\s?(?<attach>\w+):\s(?<title>.*?)\n+
|
8
|
+
(?<report>.*?)
|
9
|
+
(?=-{41,}\n(?:End|Attach))
|
10
|
+
/mx
|
11
|
+
|
12
|
+
scan @file, pattern do |attrs|
|
13
|
+
|
14
|
+
attrs['title'].sub! /^Report from the VP of /, ''
|
15
|
+
attrs['title'].sub! /^Report from the /, ''
|
16
|
+
attrs['title'].sub! /^Status report for the /, ''
|
17
|
+
attrs['title'].sub! /^Apache /, ''
|
18
|
+
|
19
|
+
if attrs['title'] =~ /\s*\[.*\]$/
|
20
|
+
attrs['owner'] = attrs['title'][/\[(.*?)\]/, 1]
|
21
|
+
attrs['title'].sub! /\s*\[.*\]$/, ''
|
22
|
+
end
|
23
|
+
|
24
|
+
attrs['title'].sub! /\sTeam$/, ''
|
25
|
+
attrs['title'].sub! /\sCommittee$/, ''
|
26
|
+
attrs['title'].sub! /\sProject$/, ''
|
27
|
+
|
28
|
+
attrs['digest'] = Digest::MD5.hexdigest(attrs['report'])
|
29
|
+
|
30
|
+
attrs['report'].sub! /\n+\Z/, "\n"
|
31
|
+
attrs.delete('report') if attrs['report'] == "\n"
|
32
|
+
|
33
|
+
attrs['missing'] = true if attrs['report'].strip.empty?
|
34
|
+
|
35
|
+
begin
|
36
|
+
attrs['chair_email'] =
|
37
|
+
ASF::Committee.find(attrs['title']).chair.mail.first
|
38
|
+
rescue
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# Back sections:
|
2
|
+
# * Discussion Items
|
3
|
+
# * Review Outstanding Action Items
|
4
|
+
# * Unfinished Business
|
5
|
+
# * New Business
|
6
|
+
# * Announcements
|
7
|
+
# * Adjournment
|
8
|
+
|
9
|
+
class ASF::Board::Agenda
|
10
|
+
parse do
|
11
|
+
pattern = /
|
12
|
+
^(?<attach>(?:\s[89]|\s9|1\d)\.)
|
13
|
+
\s(?<title>.*?)\n
|
14
|
+
(?<text>.*?)
|
15
|
+
(?=\n[\s1]\d\.|\n===)
|
16
|
+
/mx
|
17
|
+
|
18
|
+
scan @file, pattern do |attrs|
|
19
|
+
attrs['attach'].strip!
|
20
|
+
attrs['title'].sub! /^Review Outstanding /, ''
|
21
|
+
|
22
|
+
if attrs['title'] =~ /Discussion|Action|Business|Announcements/
|
23
|
+
attrs['prior_reports'] = minutes(attrs['title'])
|
24
|
+
elsif attrs['title'] == 'Adjournment'
|
25
|
+
attrs['timestamp'] = timestamp(attrs['text'][/\d+:\d+([ap]m)?/])
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# Additional Officer Reports and Committee Reports
|
2
|
+
|
3
|
+
class ASF::Board::Agenda
|
4
|
+
parse do
|
5
|
+
pattern = /
|
6
|
+
\[(?<owner>[^\n]+)\]\n\n
|
7
|
+
\s{7}See\sAttachment\s\s?(?<attach>\w+)[^\n]*?\s+
|
8
|
+
\[\s[^\n]*\s*approved:\s*?(?<approved>.*?)
|
9
|
+
\s*comments:(?<comments>.*?)\n\s{9}\]
|
10
|
+
/mx
|
11
|
+
|
12
|
+
scan @file, pattern do |attrs|
|
13
|
+
attrs['shepherd'] = attrs['owner'].split('/').last.strip
|
14
|
+
attrs['owner'] = attrs['owner'].split('/').first.strip
|
15
|
+
|
16
|
+
attrs['comments'].gsub! /^ {1,10}(\w+:)/, '\1'
|
17
|
+
attrs['comments'].gsub! /^ {11}/, ''
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# Executive Officer Reports
|
2
|
+
|
3
|
+
class ASF::Board::Agenda
|
4
|
+
parse do
|
5
|
+
reports = @file.split(/^ 4. Executive Officer Reports/,2).last.
|
6
|
+
split(/^ 5. Additional Officer Reports/,2).first
|
7
|
+
|
8
|
+
pattern = /
|
9
|
+
\s{4}(?<section>[A-Z])\.
|
10
|
+
\s(?<title>[^\[]+?)
|
11
|
+
\s\[(?<owner>[^\]]+?)\]
|
12
|
+
(?<report>.*?)
|
13
|
+
(?=\n\s{4}[A-Z]\.\s|\z)
|
14
|
+
/mx
|
15
|
+
|
16
|
+
scan reports, pattern do |attrs|
|
17
|
+
attrs['section'] = '4' + attrs['section']
|
18
|
+
attrs['shepherd'] = attrs['owner'].split('/').last
|
19
|
+
attrs['owner'] = attrs['owner'].split('/').first
|
20
|
+
|
21
|
+
attrs['report'].sub! /\A\s*\n/, ''
|
22
|
+
|
23
|
+
attrs['report'].gsub! /\n\s*\n\s+\[ comments:(.*)\]\s*$/m do
|
24
|
+
attrs['comments'] = $1.sub(/\A\s*\n/, '').sub(/\s+\Z/, '')
|
25
|
+
"\n"
|
26
|
+
end
|
27
|
+
|
28
|
+
report = attrs['report'].strip
|
29
|
+
if report.empty? or report[0..12] == 'Additionally,'
|
30
|
+
attrs['missing'] = true
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# Front sections:
|
2
|
+
# * Call to Order
|
3
|
+
# * Roll Call
|
4
|
+
|
5
|
+
class ASF::Board::Agenda
|
6
|
+
@@people_cache = {}
|
7
|
+
|
8
|
+
parse do
|
9
|
+
pattern = /
|
10
|
+
^\n\x20(?<section>[12]\.)
|
11
|
+
\s(?<title>.*?)\n\n+
|
12
|
+
(?<text>.*?)
|
13
|
+
(?=\n\s[23]\.)
|
14
|
+
/mx
|
15
|
+
|
16
|
+
scan @file, pattern do |attr|
|
17
|
+
if attr['title'] == 'Roll Call'
|
18
|
+
attr['people'] = {}
|
19
|
+
list = nil
|
20
|
+
|
21
|
+
# attempt to identify the people mentioned in the Roll Call
|
22
|
+
people = attr['text'].scan(/ {8}(\w.*)/).flatten.each do |sname|
|
23
|
+
name = sname
|
24
|
+
|
25
|
+
# first try the cache
|
26
|
+
person = @@people_cache[name]
|
27
|
+
|
28
|
+
# next try a simple name look up
|
29
|
+
if not person
|
30
|
+
search = ASF::Person.list("cn=#{name}")
|
31
|
+
person = search.first if search.length == 1
|
32
|
+
end
|
33
|
+
|
34
|
+
# finally try harder to match the name
|
35
|
+
if not person
|
36
|
+
sname = sname.strip.downcase.split(/\s+/)
|
37
|
+
|
38
|
+
if not list
|
39
|
+
ASF::Person.preload('cn')
|
40
|
+
list = ASF::Person.list
|
41
|
+
end
|
42
|
+
|
43
|
+
search = []
|
44
|
+
list.select do |person|
|
45
|
+
next if person == 'none'
|
46
|
+
pname = person.public_name.downcase.split(/\s+/)
|
47
|
+
if sname.all? {|t1| pname.any? {|t2| t2.start_with? t1}}
|
48
|
+
search << person
|
49
|
+
elsif pname.all? {|t1| sname.any? {|t2| t2.start_with? t1}}
|
50
|
+
search << person
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
person = search.first if search.length == 1
|
55
|
+
end
|
56
|
+
|
57
|
+
# save results in both the cache and the attributes
|
58
|
+
if person
|
59
|
+
@@people_cache[name] = person
|
60
|
+
|
61
|
+
attr['people'][person.id] = {
|
62
|
+
name: name,
|
63
|
+
member: person.asf_member?
|
64
|
+
}
|
65
|
+
end
|
66
|
+
end
|
67
|
+
elsif attr['title'] == 'Call to order'
|
68
|
+
attr['timestamp'] = timestamp(attr['text'][/\d+:\d+([ap]m)?/])
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# Minutes from previous meetings
|
2
|
+
|
3
|
+
class ASF::Board::Agenda
|
4
|
+
parse do
|
5
|
+
minutes = @file.split(/^ 3. Minutes from previous meetings/,2).last.
|
6
|
+
split(/^ 4. Executive Officer Reports/,2).first
|
7
|
+
|
8
|
+
pattern = /
|
9
|
+
\s{4}(?<section>[A-Z])\.
|
10
|
+
\sThe.meeting.of\s+(?<title>.*?)\n
|
11
|
+
(?<text>.*?)
|
12
|
+
\[\s(?:.*?):\s*?(?<approved>.*?)
|
13
|
+
\s*comments:(?<comments>.*?)\n
|
14
|
+
\s{8,9}\]\n
|
15
|
+
/mx
|
16
|
+
|
17
|
+
scan minutes, pattern do |attrs|
|
18
|
+
attrs['section'] = '3' + attrs['section']
|
19
|
+
attrs['text'] = attrs['text'].strip
|
20
|
+
attrs['approved'] = attrs['approved'].strip.gsub(/\s+/, ' ')
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# Special Orders
|
2
|
+
|
3
|
+
class ASF::Board::Agenda
|
4
|
+
parse do
|
5
|
+
orders = @file.split(/^ 7. Special Orders/,2).last.
|
6
|
+
split(/^ 8. Discussion Items/,2).first
|
7
|
+
|
8
|
+
pattern = /
|
9
|
+
\n+(?<indent>\s{3,5})(?<section>[A-Z])\.
|
10
|
+
\s(?<title>.*?)\n
|
11
|
+
(?<text>.*?)
|
12
|
+
(?=\n\s{4}[A-Z]\.\s|\z)
|
13
|
+
/mx
|
14
|
+
|
15
|
+
people = []
|
16
|
+
scan orders, pattern do |attrs|
|
17
|
+
attrs['section'] = '7' + attrs['section']
|
18
|
+
|
19
|
+
title = attrs['title']
|
20
|
+
fulltitle = title.dup
|
21
|
+
title.sub! /^Resolution to /, ''
|
22
|
+
title.sub! /\sthe\s/, ' '
|
23
|
+
title.sub! /\sApache\s/, ' '
|
24
|
+
title.sub! /\sCommittee\s/, ' '
|
25
|
+
title.sub! /\sProject(\s|$)/, '\1'
|
26
|
+
title.sub! /\sPMC(\s|$)/, '\1'
|
27
|
+
title.sub! /\s\(.*\)$/, ''
|
28
|
+
|
29
|
+
attrs['fulltitle'] = fulltitle if title != fulltitle
|
30
|
+
|
31
|
+
text = attrs['text']
|
32
|
+
attrs['digest'] = Digest::MD5.hexdigest(attrs['text'])
|
33
|
+
|
34
|
+
attrs['warnings'] = []
|
35
|
+
if attrs['indent'] != ' '
|
36
|
+
attrs['warnings'] << 'Heading is not indented 4 spaces'
|
37
|
+
attrs['warnings'] << attrs['indent'].inspect
|
38
|
+
attrs['warnings'] << attrs['indent'].length
|
39
|
+
end
|
40
|
+
if text.sub(/s+\Z/,'').scan(/^ *\S/).map(&:length).min != 8
|
41
|
+
attrs['warnings'] << 'Resolution is not indented 7 spaces'
|
42
|
+
end
|
43
|
+
attrs.delete 'indent'
|
44
|
+
attrs.delete 'warnings' if attrs['warnings'].empty?
|
45
|
+
|
46
|
+
asfid = '[a-z][-.a-z0-9_]+' # dot added to help detect errors
|
47
|
+
list_item = '^\s*(?:[-*\u2022]\s*)?(.*?)\s+'
|
48
|
+
|
49
|
+
people = text.scan(/#{list_item}\((#{asfid})\)\s*$/)
|
50
|
+
people += text.scan(/#{list_item}\((#{asfid})(?:@|\s*at\s*)
|
51
|
+
(?:\.\.\.|apache\.org)\)\s*$/x)
|
52
|
+
people += text.scan(/#{list_item}<(#{asfid})(?:@|\s*at\s*)
|
53
|
+
(?:\.\.\.|apache\.org)>\s*$/x)
|
54
|
+
|
55
|
+
whimsy = 'https://whimsy.apache.org'
|
56
|
+
if people.empty?
|
57
|
+
if title =~ /Change (.*?) Chair/ or title =~ /Terminate (\w+)$/
|
58
|
+
committee = ASF::Committee.find($1)
|
59
|
+
attrs['roster'] =
|
60
|
+
"#{whimsy}/roster/committee/#{CGI.escape committee.name}"
|
61
|
+
attrs['prior_reports'] = minutes(committee.display_name)
|
62
|
+
name1 = text[/heretofore\sappointed\s(\w.*)\sto/,1]
|
63
|
+
sname1 = name1.to_s.downcase.gsub('.', ' ').split(/\s+/)
|
64
|
+
name2 = text[/recommend\s(\w.*)\sas/,1]
|
65
|
+
sname2 = name2.to_s.downcase.gsub('.', ' ').split(/\s+/)
|
66
|
+
next unless committee.names
|
67
|
+
committee.names.each do |id, name|
|
68
|
+
name.sub!(/ .* /,' ') unless text.include? name
|
69
|
+
pname = name.downcase.split(/\s+/)
|
70
|
+
if text.include? name
|
71
|
+
people << [name, id]
|
72
|
+
elsif name1 && sname1.all? {|t1| pname.any? {|t2| t2.start_with? t1}}
|
73
|
+
people << [name1, id]
|
74
|
+
elsif name1 && pname.all? {|t1| sname1.any? {|t2| t2.start_with? t1}}
|
75
|
+
people << [name1, id]
|
76
|
+
elsif name2 && sname2.all? {|t1| pname.any? {|t2| t2.start_with? t1}}
|
77
|
+
people << [name2, id]
|
78
|
+
elsif name2 && pname.all? {|t1| sname2.any? {|t2| t2.start_with? t1}}
|
79
|
+
people << [name2, id]
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
else
|
84
|
+
if title =~ /Establish (.*)/
|
85
|
+
name = $1
|
86
|
+
attrs['prior_reports'] =
|
87
|
+
"#{whimsy}/board/minutes/#{name.gsub(/\W/,'_')}"
|
88
|
+
if text =~ /FURTHER RESOLVED, that ([^,]*?),?\s+be\b/
|
89
|
+
chairname = $1.gsub(/\s+/, ' ').strip
|
90
|
+
chair = people.find {|person| person.first == chairname}
|
91
|
+
attrs['chair'] = (chair ? chair.last : nil)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
people.map! do |name, id|
|
98
|
+
person = ASF::Person.new(id)
|
99
|
+
icla = person.icla
|
100
|
+
[id, {name: name, icla: icla ? person.icla.name : false,
|
101
|
+
member: person.asf_member?}]
|
102
|
+
end
|
103
|
+
|
104
|
+
attrs['people'] = Hash[people] unless people.empty?
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|