-
Notifications
You must be signed in to change notification settings - Fork 69
/
agenda.rb
198 lines (168 loc) · 6.2 KB
/
agenda.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
require_relative '../asf'
require 'time'
require 'active_support'
require 'active_support/time'
require 'digest/md5'
module ASF
#
# module which contains the Agenda class
#
module Board
end
end
#
# Class which contains a number of parsers.
#
class ASF::Board::Agenda
# mapping of agenda section numbers to section names
CONTENTS = {
'2.' => 'Roll Call',
'3A' => 'Minutes',
'4A' => 'Executive Officer',
'1' => 'Additional Officer',
'A' => 'Committee Reports',
'7A' => 'Special Orders',
'8.' => 'Discussion Items',
'8A' => 'Discussion Items',
'9.' => 'Action Items'
}
# Regex for start of officer reports (accounts for style differences in early agendas)
OFFICER_SEPARATOR = /^\s*4. (Executive )?Officer Reports/
@@parsers = []
# convenience method. If passed a file, will create an instance of this
# class and call the parse method on that object. If passed a block, will
# add that block to the list of parsers.
def self.parse(file=nil, quick=false, &block)
@@parsers << block if block
new.parse(file, quick) if file
end
# start with an empty list of sections. Sections are added and returned by
# calling the <tt>parse</tt> method.
def initialize
@sections = {}
end
# helper method to scan a section for a pattern. Regular expression named
# matches will be captured and the section will be added to <tt>@sections</tt>
# if a match is found.
def scan(text, pattern, &block)
# convert tabs to spaces
text.gsub!(/^(\t+)/) {|tabs| ' ' * (8 * tabs.length)}
text.scan(pattern).each do |matches|
hash = Hash[pattern.names.zip(matches)]
yield hash if block
section = hash.delete('section')
section ||= hash.delete('attach')
if section
hash['approved'] &&= hash['approved'].strip.split(/[ ,]+/)
@sections[section] ||= {}
next if hash['text'] and @sections[section]['text']
@sections[section].merge!(hash)
end
end
end
# parse a board agenda file by passing it through each parser. Additionally,
# converts the file to utf-8, adds index markers for major sections, looks
# for flagged reports, and performs various minor cleanup actions.
#
# If <tt>quick</tt> is <tt>false</tt>, cross-checks with committee membership
# will be performed. This supports the board agenda tools's strategy to
# quickly display possibly stale and possible incomplete data and then to
# update the presentation using React.JS once later and/or more complete
# data is available.
#
# Returns a list of sections.
def parse(file, quick=false)
@file = file
@quick = quick
unless @file.valid_encoding?
filter = proc {|c| c.unpack1('U') rescue 0xFFFD}
@file = @file.chars.map(&filter).pack('U*').force_encoding('utf-8')
end
@@parsers.each { |parser| instance_exec(&parser) }
# add index markers for major sections
CONTENTS.each do |section, index|
@sections[section][:index] = index if @sections[section]
end
# quick exit if none found -- non-standard format agenda
return [] if @sections.empty?
# look for flags
flagged_reports = Hash[@file[/ \d\. Committee Reports.*?\n\s+A\./m].
scan(/# (.*?) \[(.*)\]/)] rescue {}
president = @sections.values.find {|item| item['title'] == 'President'}
return [] unless president # quick exit if non-standard format agenda
pattach = president['report'][/\d+ through \d+\.$/]
# pattach is nil before https://whimsy.apache.org/board/minutes/Change_Officers_to_Serve_at_the_Direction_of_the_President.html
preports = Range.new(*pattach.scan(/\d+/)) if pattach
# cleanup text and comment whitespace, add flags
@sections.each do |section, hash|
text = hash['text'] || hash['report']
if text
text.sub!(/\A\s*\n/, '')
text.sub!(/\s+\Z/, '')
unindent = text.sub(/s+\Z/, '').scan(/^ *\S/).map(&:length).min || 1
text.gsub!(/^ {#{unindent - 1}}/, '')
end
text = hash['comments']
if text
text.sub!(/\A\s*\n/, '')
text.sub!(/\s+\Z/, '')
unindent = text.sub(/s+\Z/, '').scan(/^ *\S/).map(&:length).min || 1
text.gsub!(/^ {#{unindent - 1}}/, '')
end
# add flags
flags = flagged_reports[hash['title']]
hash['flagged_by'] = flags.split(', ') if flags
# mark president reports
hash['to'] = 'president' if preports&.include?(section)
end
unless @quick
# add roster and prior report link
whimsy = 'https://whimsy.apache.org'
@sections.each do |section, hash|
next unless section =~ /^(4[A-Z]|\d+|[A-Z][A-Z]?)$/
committee = ASF::Committee.find(hash['title'] ||= 'UNKNOWN')
unless section =~ /^4[A-Z]$/
hash['roster'] =
"#{whimsy}/roster/committee/#{CGI.escape committee.name}"
end
if section =~ /^[A-Z][A-Z]?$/
hash['stats'] = 'https://reporter.apache.org/wizard/statistics?' +
CGI.escape(committee.name)
end
hash['prior_reports'] = minutes(committee.display_name)
end
end
# add attach to section
@sections.each do |section, hash|
hash[:attach] = section
# look for missing titles
hash['title'] ||= 'UNKNOWN'
if hash['title'] == 'UNKNOWN'
hash['warnings'] = ['unable to find attachment']
end
end
# handle case where board meeting crosses a date boundary
if @sections.values.first['timestamp'] > @sections.values.last['timestamp']
@sections.values.last['timestamp'] += 86_400_000 # add one day
end
@sections.values
end
# provide a link to the collated minutes for a given report
def minutes(title)
"https://whimsy.apache.org/board/minutes/#{title.gsub(/\W/, '_')}"
end
# convert a PST/PDT time to UTC as a JavaScript integer
def timestamp(time)
date = @file[/(\w+ \d+, \d+)/]
ASF::Board::TIMEZONE.parse("#{date} #{time}").to_i * 1000
end
end
require_relative 'agenda/front'
require_relative 'agenda/minutes'
require_relative 'agenda/exec-officer'
require_relative 'agenda/attachments'
require_relative 'agenda/committee'
require_relative 'agenda/special'
require_relative 'agenda/discussion'
require_relative 'agenda/back'
require_relative 'agenda/summary'