blob: a54d8996586987397ac1ff92666517a5dd7d8724 [file] [log] [blame]
require 'weakref'
module ASF
class Member
include Enumerable
@@text = nil
@@mtime = 0
def self.mtime
@@mtime
end
# Return the members.txt value associated with a given id
def self.find_text_by_id(value)
new.each do |id, text|
return text if id==value
end
nil
end
# An iterator that returns a list of ids and associated members.txt entries.
def self.each(&block)
new.each(&block)
end
# extract 1st line and remove any trailing /* comment */
def self.get_name(txt)
txt[/(.*?)\n/, 1].sub(/\s+\/\*.*/,'')
end
# return a list of <tt>members.txt</tt> entries as a Hash. Keys are
# availids. Values are a Hash with the following keys:
# <tt>:text</tt>, <tt>:name</tt>, <tt>"status"</tt>.
# Active members are those with no 'status' value
def self.list
result = Hash[self.new.map {|id, text|
[id, {text: text, name: self.get_name(text)}]
}]
self.status.each do |name, value|
result[name]['status'] = value
end
result
end
# Find the ASF::Person associated with a given email
def self.find_by_email(value)
value = value.downcase
each do |id, text|
emails(text).each do |email|
return Person.find(id) if email.downcase == value
end
end
nil
end
# Return a hash of *non-active* ASF members and their status. Keys are
# availids. Values are strings from the section header under which the
# member is listed: currently either <tt>Emeritus (Non-voting) Member</tt>
# or <tt>Deceased Member</tt>.
# N.B. Does NOT return active members
def self.status
begin
@status = nil if @mtime != @@mtime
@mtime = @@mtime
return Hash[@status.to_a] if @status
rescue
end
status = {}
sections = ASF::Member.text.to_s.split(/(.*\n===+)/)
sections.shift(3)
sections.each_slice(2) do |header, text|
header.sub!(/s\n=+/,'')
text.scan(/Avail ID: (.*)/).flatten.each {|id| status[id] = header}
end
@status = WeakRef.new(status)
status
end
# An iterator that returns a list of ids and associated members.txt entries.
def each
ASF::Member.text.to_s.split(/^ \*\) /).each do |section|
id = section[/Avail ID: (.*)/,1]
yield id, section.sub(/\n.*\n===+\s*?\n(.*\n)+.*/,'').strip if id
end
nil
end
# Determine if the person associated with a given id is an ASF member.
# Includes emeritus and deceased members
# Returns a boolean value.
def self.find(id)
each {|availid| return true if availid == id}
return false
end
# extract member emails from members.txt entry
def self.emails(text)
text.to_s.scan(/Email: (.*(?:\n\s+\S+@.*)*)/).flatten.
join(' ').split(/\s+/).grep(/@/)
end
# Return the Last Changed Date for <tt>members.txt</tt> in svn as
# a <tt>Time</tt> object.
def self.svn_change
foundation = ASF::SVN['foundation']
file = File.join(foundation, 'members.txt')
return Time.parse(`svn info #{file}`[/Last Changed Date: (.*) \(/, 1]).gmtime
end
# sort an entire members.txt file
def self.sort(source)
# split into sections
sections = source.split(/^([A-Z].*\n===+\n\n)/)
# sort sections that contain names
sections.map! do |section|
next section unless section =~ /^\s\*\)\s/
# split into entries, and normalize those entries
entries = section.split(/^\s\*\)\s/)
header = entries.shift
entries.map! {|entry| " *) " + entry.strip + "\n\n"}
# sort the entries
entries.sort_by! do |entry|
ASF::Person.sortable_name(entry[/\)\s(.*?)\s*(\/\*|$)/, 1])
end
header + entries.join
end
sections.join
end
# cache the contents of members.txt. Primary purpose isn't performance,
# but rather to have a local copy that can be updated and used until
# the svn working copy catches up
def self.text
foundation = ASF::SVN.find('foundation')
return nil unless foundation
begin
text = @@text[0..-1] if @@text
rescue WeakRef::RefError
@@mtime = 0
end
member_file = File.join(foundation, 'members.txt')
member_time = File.mtime(member_file)
if member_time.to_i > @@mtime.to_i
@@mtime = member_time
text = File.read(member_file)
@@text = WeakRef.new(text)
end
text
end
# update local copy of members.txt
def self.text=(text)
# normalize text: sort and update active count
text = ASF::Member.sort(text)
pattern = /^Active.*?^=+\n+(.*?)^Emeritus/m
text[/We now number (\d+) active members\./, 1] =
text[pattern].scan(/^\s\*\)\s/).length.to_s
# save
@@mtime = Time.now
@@text = WeakRef.new(text)
end
end
class Person
# text entry from <tt>members.txt</tt>. If <tt>full</tt> is <tt>true</tt>,
# this will also include the text delimiters.
def members_txt(full = false)
prefix, suffix = " *) ", "\n\n" if full
# Is the cached text still valid?
unless @members_time == ASF::Member.mtime
@members_txt = nil
end
# cache the text and its time (may be changed by the find operation)
@members_txt ||= ASF::Member.find_text_by_id(id)
@members_time = ASF::Member.mtime
"#{prefix}#{@members_txt}#{suffix}" if @members_txt
end
# email addresses from members.txt
def member_emails
ASF::Member.emails(members_txt)
end
# Person's name as found in members.txt
def member_name
ASF::Member.get_name(members_txt) if members_txt
end
end
end
if __FILE__ == $0
$LOAD_PATH.unshift '/srv/whimsy/lib'
# N.B. Require 'whimsy/asf' causes error: superclass mismatch for class Person
require 'whimsy/asf/config'
require 'whimsy/asf/svn'
puts ASF::Member.list.size
puts ASF::Member.status.size
puts ASF::Member.text.size
ids=0
ASF::Member.each{|id,txt| ids+= 1}
puts ids
end