blob: e0996d1db10f38c02a5e147fafd7767b4052f2e7 [file] [log] [blame]
#!/usr/bin/env ruby
# Utility methods and structs related to Member's Meetings
# NOTE: Assumes 21st century '2*'
$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'whimsy/asf'
require 'json'
class MeetingUtil
RECORDS = ASF::SVN.svnurl!('Meetings')
MEETING_FILES = { # Filename in meeting dir, pathname to another deployed tool, or URL
'README.txt' => 'README For Meeting Process And Roll Call',
'nomination_of_board.txt' => 'How To Nominate Someone For Board',
'nomination_of_members.txt' => 'How To Nominate A New Member',
'/members/proxy.cgi' => 'How To Submit A Proxy/Check Your Proxies',
'https://www.apache.org/foundation/governance/meetings' => 'How Voting Via Email Works',
'agenda.txt' => 'Official Meeting Agenda',
'board_ballot.txt' => 'Official Board Candidate Ballots',
'nominated-members.txt' => 'Official New Member Nominees/Seconds',
'proxies' => 'Official List Of Meeting Proxies',
'record' => 'Official List Of Voting Members',
'attend' => 'Official List Of Meeting Attendees (afterwards)',
'voter-tally' => 'Official List Of Who Voted (afterwards)',
'raw_board_votes.txt' => 'Official List Of Votes For Board (afterwards)',
'raw-irc-log' => 'ASFBot logs all postings on #asfmembers during meeting'
}
# Calculate how many members required to attend first half for quorum
def self.calculate_quorum(mtg_dir)
begin
num_members = File.read(File.join(mtg_dir, 'record')).each_line.count
quorum_need = (num_members + 2) / 3
num_proxies = Dir[File.join(mtg_dir, 'proxies-received', '*')].count
attend_irc = quorum_need - num_proxies
rescue StandardError => e
# Ensure we can't break rest of script
puts "ERROR: #{e}"
return 0, 0, 0, 0
end
return num_members, quorum_need, num_proxies, attend_irc
end
# get list of proxy volunteers
def self.getVolunteers(mtg_dir)
lines = IO.read(File.join(mtg_dir, 'proxies'))
# split by ---- underlines, then by blank lines; pick second para and drop leading spaces
volunteers = lines.split(/^-----------/)[1].split(/\n\n/)[1].scan(/^\ +(\S.*$)/).flatten
end
# Get info about current users's proxying
# @return "help text", ["id | name (proxy)", ...] if they are a proxy for other(s)
# @return "You have already submitted a proxy form" to someone else
# @return nil otherwise
def self.is_user_proxied(mtg_dir, id)
user = ASF::Person.find(id)
lines = IO.read(File.join(mtg_dir, 'proxies'))
proxylist = lines.scan(/\s\s(.{25})(.*?)\((.*?)\)/) # [["Shane Curcuru ", "David Fisher ", "wave"], ...]
help = nil
copypasta = [] # theiravailid | Their Name in Rolls (proxy)
begin
proxylist.each do |arr|
if user.cn == arr[0].strip
copypasta << "#{arr[2].ljust(12)} | #{arr[1].strip} (proxy)"
elsif user.id == arr[2]
help = "NOTE: You have already submitted a proxy form for #{arr[0].strip} to mark your attendance (be sure they know to mark you at Roll Call)! "
end
end
rescue StandardError => e
(help ||= "") << "ERROR, could not read LDAP, proxy data may not be correct: #{e.message}"
end
if copypasta.empty?
return help
else
(help ||= "") << "During the meeting, to mark your proxies' attendance, AFTER the 2. Roll Call is called, you may copy/paste the below lines to mark your and your proxies attendance."
copypasta.unshift("#{user.id.ljust(12)} | #{user.cn}")
return help, copypasta
end
end
# Get the latest available Meetings dir
def self.get_latest(mtg_root)
return Dir[File.join(mtg_root, '2*')].sort.last
end
# Get the second latest available Meetings dir
def self.get_previous(mtg_root)
return Dir[File.join(mtg_root, '2*')].sort[-2]
end
# Read attendance.json file
def self.get_attendance(mtg_root)
return JSON.parse(IO.read(File.join(mtg_root, 'attendance.json')))
end
# Parse all memapp-received.txt files to get better set of names
# @see whimsy/www/members/attendance-xcheck.cgi
def self.read_memapps(dir)
memapps = Hash.new('unknown')
Dir[File.join(dir, '*', 'memapp-received.txt')].each do |received|
meeting = File.basename(File.dirname(received))
next if meeting.include? 'template'
text = File.read(received)
list = text.scan(/(.+)\s<(.*)@.*>.*Yes/i)
if list.empty?
list = text.scan(/^(?:no\s*)*(?:yes\s+)+(\w\S*)\s+(.*)\s*/)
else
# reverse order of id name type files
list.each {|a| a[0], a[1] = a[1], a[0] }
end
list.each { |itm| memapps[itm[1].strip] = [itm[0], meeting] }
end
return memapps
end
# Annotate the attendance.json file with cohorts by id
# This allows easy use by other tools
def self.annotate_attendance(dir)
attendance = JSON.parse(IO.read(File.join(dir, 'attendance.json')))
memapps = read_memapps(dir)
iclas = ASF::ICLA.preload
memapp_map = JSON.parse(IO.read(File.join(dir, 'memapp-map.json')))
attendance['cohorts'] = {}
attendance['unmatched'] = []
attendance['members'].each do |date, ary|
ary.each do |nam|
found = iclas.select{|i| i.icla.legal_name == nam}
found = iclas.select{|i| i.icla.name == nam} if found.empty?
if found.empty?
if memapps.has_key?(nam)
attendance['cohorts'][memapps[nam][0]] = date
elsif memapp_map.has_key?(nam)
attendance['cohorts'][memapp_map[nam]] = date
else
attendance['unmatched'] << nam
end
else
attendance['cohorts'][found[0].icla.id] = date
end
end
end
File.open(File.join(dir, 'attendance-cohorts.json'), 'w') do |f| # Do not overwrite blindly; manual copy if desired
f.puts JSON.pretty_generate(attendance)
end
end
end
# ## ### #### ##### ######
# Main method for command line use
if __FILE__ == $PROGRAM_NAME
dir = ARGV[0]
dir ||= '.'
MeetingUtil.annotate_attendance(dir)
puts "DONE, check attendance-cohorts.json"
end