class ASF::MeetingUtil
Constants
- APPLICATION_EXPIRY_POST_VOTE_DAYS
-
www.apache.org/foundation/bylaws.html#article-iv application must be received by the Secretary no later than 30 days following the vote. Current thinking is that the vote is considered to have occurred when the results are announced. TBC
- APPLICATION_EXPIRY_POST_VOTE_SECS
- MEETINGS_DIR
- MEETING_FILES
-
The URL is generated using emit_link() in meeting.cgi if the name includes ‘/’ then use as is unless it starts with ‘runbook/’
- PROXIES_FILENAME
- RECORDS
- VCAL_EVENTS_FILENAME
Public Class Methods
Source
# File lib/whimsy/asf/meeting-util.rb, line 279 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| next unless date.start_with? '20' # exclude 'active' 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
Annotate the attendance.json file with cohorts by id This allows easy use by other tools
Source
# File lib/whimsy/asf/meeting-util.rb, line 459 def self.application_time_remaining meetingend = self.meeting_end # this is in seconds now = DateTime.now.to_time.to_i remain = (meetingend + APPLICATION_EXPIRY_POST_VOTE_SECS - now) / 3600 {hoursremain: remain, days: remain.abs/24, hours: remain.abs%24} end
How long remains before applications close? (Time is measured from scheduled end of the meeting in which the votes were declared) Returned as hash, e.g. {:hoursremain=>605, :days=>25, :hours=>5} If applications have expired, :hoursremain is negative and :days/:hours are elapsed time since expiry
Source
# File lib/whimsy/asf/meeting-util.rb, line 476 def self.application_valid?(message_datetime) expirytime = self.meeting_end + APPLICATION_EXPIRY_POST_VOTE_SECS msgtime = DateTime.iso8601(message_datetime).to_time.to_i msgtime <= expirytime end
Is this particular membership application still valid? Used to check if an application was received before the close date return: true/false
Source
# File lib/whimsy/asf/meeting-util.rb, line 469 def self.applications_valid self.application_time_remaining[:hoursremain] > 0 end
Are membership applications still valid? Applications close date has yet to be reached return: true/false
Source
# File lib/whimsy/asf/meeting-util.rb, line 55 def self.calculate_quorum(mtg_dir) begin begin num_members = File.read(File.join(mtg_dir, 'record')).each_line.count rescue num_members = ASF::Member.list.length - ASF::Member.status.length end quorum_need = (num_members + 2) / 3 num_proxies = Dir[File.join(mtg_dir, 'proxies-received', '*')].count attend_irc = quorum_need - num_proxies attend_irc = 0 if attend_irc < 0 # allow for more proxies than quorum 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
Calculate how many members required to attend first half for quorum Returns: num_members, quorum_need, num_proxies, attend_irc where: num_members = number of active members (taken from ‘record’ if possible, else members.txt) quorum_need = (num_members + 2) / 3 num_proxies = number of files under ‘proxies-received’ attend_irc = quorum_need - num_proxies
Source
# File lib/whimsy/asf/meeting-util.rb, line 342 def self.current_status(cur_mtg_dir) proxies = Dir["#{cur_mtg_dir}/proxies-received/*"]. map {|file| File.basename(file, '.*')} _tag,emeritus = ASF::SVN.getlisting('emeritus-requests-received') emeritus.map! {|file| File.basename(file, '.*')} lambda do |id| if emeritus.include? id 'Emeritus request received' elsif proxies.include? id 'Proxy received' else 'No response' end end end
return a function to determine the current status of a member by id
Source
# File lib/whimsy/asf/meeting-util.rb, line 92 def self.getProxyAssignments(mtg_dir=nil) _, assignments = self.parseProxies(mtg_dir) hdr = assignments.shift # work out the column layout re = %r{^((\s+)<name>\s+)<name>} if hdr.match re total, offset = [$1.length, $2.length] else raise ArgumentError, "proxies: bad header '#{hdr}'" end assignments.map do |line| proxy = line[offset..total-1].strip if line[total..-1].strip.match %r{(.+) +\((.+)\)} proxied = $1 uid = $2 else raise ArgumentError, "proxies: bad assignment '#{line}'" end [proxy, proxied, uid] end end
get list of proxy assignments returns array of: [proxy, subject, subject id]
Source
# File lib/whimsy/asf/meeting-util.rb, line 115 def self.getProxyNominees(mtg_dir=nil) assignments = self.getProxyAssignments(mtg_dir) assignments.map do |line| line[0] end.uniq end
get list of proxy nominees
Source
# File lib/whimsy/asf/meeting-util.rb, line 85 def self.getVolunteers(mtg_dir=nil) volunteers, _ = self.parseProxies(mtg_dir) volunteers.each.filter_map {|line| l = line.strip; l if l.length > 0} end
get list of proxy volunteers
Source
# File lib/whimsy/asf/meeting-util.rb, line 310 def self.get_attend_matrices(dir) attendance = MeetingUtil.get_attendance(dir) # extract and format dates dates = attendance['dates'].sort. map {|date| Date.parse(date).strftime('%Y-%b')} # compute mappings of names to ids members = ASF::Member.list active = Hash[members.select {|_id, data| not data['status']}] nameMap = Hash[members.map {|id, data| [id, data[:name]]}] idMap = Hash[nameMap.to_a.map(&:reverse)] # analyze attendance matrix = attendance['matrix'].map do |name, meetings| id = idMap[name] next unless id and active[id] # exclude 'active entry' data = meetings.select {|key, value| key.start_with? '20'}. sort.reverse.map(&:last) first = data.length missed = (data.index {|datum| datum != '-'} || data.length) [id, name, first, missed] end return attendance, matrix.compact, dates, nameMap end
Precompute matrix and dates from attendance
Source
# File lib/whimsy/asf/meeting-util.rb, line 173 def self.get_attendance(mtg_root) return JSON.parse(IO.read(File.join(mtg_root, 'attendance.json'))) end
Read attendance.json file
Source
# File lib/whimsy/asf/meeting-util.rb, line 388 def self.get_invite_times() times = MeetingUtil.get_timeline(latest_meeting_dir) # Needs more work to reconcile recent changes to time calculations return { # TEMP HACK: return times in seconds, as per the original get_invite_times method. nominations_close: DateTime.iso8601(times['nominations_close_iso']).to_time.to_i, polls_close: DateTime.iso8601(times['polls_close_iso']).to_time.to_i, meeting_start: DateTime.iso8601(times['meeting_start_iso']).to_time.to_i, meeting_end: DateTime.iso8601(times['meeting_end_iso']).to_time.to_i, } end
get the times from the timeline file returns: hash with keys: nominations_close
:, polls_close
:, meeting_start
, meeting_close:
Source
# File lib/whimsy/asf/meeting-util.rb, line 401 def self.get_invite_times_ical times = {} File.readlines(File.join(latest_meeting_dir, VCAL_EVENTS_FILENAME)).slice_before(/^BEGIN:VEVENT/).drop(1).each do |ev| uid = nil dtstart = dtend = nil ev.each do |line| case line when /^UID:(.+)/ uid = $1.chomp.sub(/-?\d{4}/, '') when /^DTSTART;TZID=(.+):(.+)/ tz = $1 if tz == 'UTC' dtstart = DateTime.iso8601($2.chomp).to_time.to_i else raise ArgumentError.new("Cannot parse #{line.chomp} in #{VCAL_EVENTS_FILENAME}") end when /^DTEND;TZID=(.+):(.+)/ tz = $1 if tz == 'UTC' dtend = DateTime.iso8601($2.chomp).to_time.to_i else raise ArgumentError.new("Cannot parse #{line.chomp} in #{VCAL_EVENTS_FILENAME}") end end end times[uid] = dtstart times['asf-members-end'] = dtend if uid == 'asf-members' end return { nominations_close: times['asf-members-nominations-close'], polls_close: times['asf-members-polls-close'], meeting_start: times['asf-members'], meeting_end: times['asf-members-end'] } end
get the times from the VCAL events file returns: hash with keys: nominations_close
:, polls_close
:, meeting_start
, meeting_close:
Source
# File lib/whimsy/asf/meeting-util.rb, line 163 def self.get_latest(mtg_root) return Dir[File.join(mtg_root, '2*')].max end
Get the latest available Meetings dir
Source
# File lib/whimsy/asf/meeting-util.rb, line 154 def self.get_latest_completed(mtg_root, sentinel='raw-irc-log') return Dir[File.join(mtg_root, '2*')].select {|d| File.exist? File.join(d, sentinel) }.max end
Get the latest completed Meetings dir (i.e. has raw-irc-log; this can be overridden) TODO: is that the most appropriate file to check?
Source
# File lib/whimsy/asf/meeting-util.rb, line 158 def self.get_latest_file(file='.', mtg_root=nil) return Dir[File.join(mtg_root || MEETINGS_DIR, '2???????', file)].max end
Source
# File lib/whimsy/asf/meeting-util.rb, line 168 def self.get_previous(mtg_root) return Dir[File.join(mtg_root, '2*')].sort[-2] end
Get the second latest available Meetings dir
Source
# File lib/whimsy/asf/meeting-util.rb, line 179 def self.get_timeline(mtg_root) begin return JSON.parse(IO.read(File.join(mtg_root, 'runbook', 'timeline.json'))) rescue StandardError => e return "ERROR: get_timeline(#{mtg_root}) threw: #{e.message}" end end
Read runbook/timeline.json file, not present before 2025 @return hash, or string error if not found
Source
# File lib/whimsy/asf/meeting-util.rb, line 126 def self.is_user_proxied(mtg_dir, id) proxylist = self.getProxyAssignments(mtg_dir) user = ASF::Person.find(id) help = nil copypasta = [] # theiravailid | Their Name in Rolls (proxy) max_uid_len = 16 # for alignment begin proxylist.each do |proxy, subject, uid| if user.cn == proxy copypasta << "#{uid.ljust(max_uid_len)} | #{subject} (proxy)" elsif user.id == uid help = "NOTE: You have already submitted a proxy form for #{proxy} 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(max_uid_len)} | #{user.cn}") return help, copypasta end end
Get proxy info for current user @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
Source
# File lib/whimsy/asf/meeting-util.rb, line 361 def self.latest_meeting_dir MeetingUtil.get_latest(MEETINGS_DIR) end
return the dir containing the latest meeting files
Source
# File lib/whimsy/asf/meeting-util.rb, line 450 def self.meeting_end self.get_invite_times[:meeting_end] end
Source
# File lib/whimsy/asf/meeting-util.rb, line 446 def self.meeting_start self.get_invite_times[:meeting_start] end
Source
# File lib/whimsy/asf/meeting-util.rb, line 438 def self.nominations_close self.get_invite_times[:nominations_close] end
Shorthand methods for callers
Source
# File lib/whimsy/asf/meeting-util.rb, line 75 def self.parseProxies(mtg_dir=nil) mtg_dir ||= latest_meeting_dir lines = IO.readlines(File.join(mtg_dir, PROXIES_FILENAME)) parts = lines.slice_before(%r{^(Volunteers|Assignments):}).drop(1) volunteers = parts.shift.drop(3) # heading assignments = parts.shift.drop(4) return volunteers, assignments end
parse the proxies file
Source
# File lib/whimsy/asf/meeting-util.rb, line 213 def self.parse_memapp(path=nil,header=false) path ||= get_latest_file('memapp-received.txt') text = File.read(path) # latest layout; look for at least one yes column; trim the user name list = text.scan(/^(no|yes)\s+(no|yes)(?:\s+(no|yes)\s+(no|yes))?\s+(\S+)\s+(.+)/).each {|a| a.last.strip!} if header hdr = text.split(/\R/)[0..1] # Assume 2 line header # Assume 6 columns for now hyphens=hdr[1].scan(/^(--+ +)(---+ +)(---+ +)(---+ +)(---+ +)(----+ *)$/).first hyphens.pop # drop last; don't want to pad that fmt = [hyphens.map{|h| '%%-%ds' % (h.size - 1)},'%s'].join(' ') return [list, hdr, fmt] else return list end end
parse a memapp file, optionally returning the format Params:
- path to file; if omitted, pick the latest found - parse header to extract format, default false
Does not support files before 2010 Return: array of arrays or [array of arrays, format, hdr lines] The original contents can be regenerated as follows: Parse the file:
list,hdr,fmt = ASF::MeetingUtil.parse_memapp(nil, true)
Regenerate an indidividual entry: fmt % entry Regenerate all the contents:
[hdr, list.map{|item| fmt % item}].join("\n")
N.B. you may need to add a trailing EOL or two when writing the file
Source
# File lib/whimsy/asf/meeting-util.rb, line 245 def self.parse_memapp_to_h(path=nil,header=false) keys = %i(invite apply mail karma id name) res = self.parse_memapp(path, header) if header list, hdr, fmt = res # split the response return [list.map{|entry| keys.zip(entry).to_h}, hdr, fmt, keys] else return res.map{|entry| keys.zip(entry).to_h} end end
parse a memapp file; if omitted, pick the latest found optionally return the line format and key list Does not support files before 2010 Return: array of hash entries with the symbolic keys: :invite :apply :mail :karma :id :name optionally followed by format, keylist, hdr The original contents can be regenerated as follows: Parse the file:
list,hdr,fmt,keys = ASF::MeetingUtil.parse_memapp_to_h(nil,true)
Regenerate an indidividual entry: fmt % keys.map{|key| entry} Regenerate all the contents:
[hdr, list.map{|item| fmt % keys.map{|key| item[key]} }].join("\n")
N.B. you may need to add a trailing EOL or two when writing the file
Source
# File lib/whimsy/asf/meeting-util.rb, line 442 def self.polls_close self.get_invite_times[:polls_close] end
Source
# File lib/whimsy/asf/meeting-util.rb, line 258 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) # early layout if list.empty? # latest layout; look for at least one yes column 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
Parse all memapp-received.txt files to get better set of names @see whimsy/www/members/attendance-xcheck.cgi
Source
# File lib/whimsy/asf/meeting-util.rb, line 366 def self.tracker(meetingsMissed) cur_mtg_dir = MeetingUtil.get_latest(MEETINGS_DIR) current_status = self.current_status(cur_mtg_dir) _attendance, matrix, dates, _nameMap = MeetingUtil.get_attend_matrices(MEETINGS_DIR) inactive = matrix.select do |id, _name, _first, missed| id and missed >= meetingsMissed end Hash[inactive.map {|id, name, first, missed| [id, { 'name' => name, 'missed' => missed, 'status' => current_status[id], 'since' => dates[-first-1] || dates.first, 'last' => dates[-missed-1] }] }] end
return the current status of all inactive members