whimsy-asf 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|