class ASF::Committee

Representation for a committee (either a PMC, a board committee, or a President's committee). This data is parsed from committee-info.txt, and is augmened by data from LDAP, ASF::Site, and ASF::Mail.

Note that the simple attributes which are sourced from committee-info.txt data is generally not available until ::load_committee_info is called.

Similarly, the simple attributes which are sourced from LDAP is generally not available until ::preload is called.

Constants

GUINEAPIGS

temp list of projects that have moved over to new project LDAP schema

Attributes

chairs[RW]

list of chairs for this committee. Returned as a list of hashes containing the :name and :id. Data is obtained from committee-info.txt.

createTimestamp[RW]

Date this committee was initially created in LDAP.

established[RW]

Date this committee was established in the format MM/YYYY. Data is obtained from committee-info.txt.

info[R]

list of members for this committee. Returned as a list of ids. Data is obtained from committee-info.txt.

modifyTimestamp[RW]

Date this committee was last modified in LDAP.

report[W]

when this committee is next expected to report. May be a string containing values such as “Next month: missing in May”, “Next month: new, montly through July”. Data is obtained from committee-info.txt.

roster[RW]

list of members for this committee. Returned as a list of hash mapping ids to a hash of :name and :date values. Data is obtained from committee-info.txt.

schedule[RW]

list of months when his committee typically reports. Returned as a comma separated string. Data is obtained from committee-info.txt.

Public Class Methods

[](name) click to toggle source

return committee only if it actually exits

Calls superclass method ASF::Base.[]
# File lib/whimsy/asf/ldap.rb, line 1110
def self.[] name
  committee = super
  return committee if GUINEAPIGS.include? name
  committee.members.empty? ? nil : committee
end
add(name, people) click to toggle source

add a new committee to LDAP

# File lib/whimsy/asf/ldap.rb, line 1231
def self.add(name, people)
  entry = [
    mod_add('objectClass', ['groupOfNames', 'top']),
    mod_add('cn', name),
    mod_add('member', Array(people).map(&:dn))
  ]

  ASF::LDAP.add("cn=#{name},#{base}", entry)
end
establish(contents, pmc, date, people) click to toggle source

insert (replacing if necessary) a new committee into committee-info.txt

# File lib/whimsy/asf/committee.rb, line 217
def self.establish(contents, pmc, date, people)
  ########################################################################
  #         insert into assigned quarterly reporting periods             #
  ########################################################################

  # split into blocks
  blocks = contents.split("\n\n")

  # find the reportings schedules
  index =  blocks.find_index {|section| section =~/January/}

  # extract reporting schedules
  slots = [
    blocks[index+0].split("\n"),
    blocks[index+1].split("\n"),
    blocks[index+2].split("\n"),
  ]

  # ensure that spacing is uniform
  slots.each {|slot| slot.unshift '' unless slot[0] == ''}

  # determine tie breakers between months of the same length
  preference = [(date.month)%3, (date.month-1)%3, (date.month-2)%3]

  # pick the month with the shortest list
  slot = (0..2).map {|i| [slots[i].length, preference, i]}.min.last

  # temporarily remove headers
  headers = slots[slot].shift(3)

  # insert pmc into the reporting schedule
  slots[slot] << "    " + pmc

  # sort entries, case insensitive
  slots[slot].sort_by!(&:downcase)

  #restore headers
  slots[slot].unshift *headers

  # re-insert reporting schedules
  blocks[index+0] = slots[0].join("\n")
  blocks[index+1] = slots[1].join("\n")
  blocks[index+2] = slots[2].join("\n")

  # re-attach blocks
  contents = blocks.join("\n\n")

  ########################################################################
  #         insert into COMMITTEE MEMBERSHIP AND CHANGE PROCESS          #
  ########################################################################

  # split into foot, sections (array) and head
  foot = contents[/^=+\s*\Z/]
  contents.sub! /^=+\s*\Z/, ''
  sections = contents.split(/^\* /)
  head = sections.shift

  # remove existing section (if present)
  sections.delete_if {|section| section.downcase.start_with? pmc.downcase}

  # build new section
  people = people.map do |id, person|
    name = "#{person[:name].ljust(26)} <#{id}@apache.org>"
    "    #{(name).ljust(59)} [#{date.strftime('%Y-%m-%d')}]"
  end

  section  = ["#{pmc}  (est. #{date.strftime('%m/%Y')})"] + people.sort

  # add new section
  sections << section.join("\n") + "\n\n\n"

  # sort sections
  sections.sort_by!(&:downcase)

  # re-attach parts
  head + '* ' + sections.join('* ') + foot
end
find(name) click to toggle source

Finds a committee based on the name of the Committee. Is aware of a number of aliases for a given committee. Will set display name if the name being searched on contains an uppercase character.

Calls superclass method ASF::Base.find
# File lib/whimsy/asf/committee.rb, line 395
def self.find(name)
  result = super(@@namemap.call(name))
  result.display_name = name if name =~ /[A-Z]/
  result
end
list(filter='cn=*') click to toggle source

return a list of committees, from LDAP.

# File lib/whimsy/asf/ldap.rb, line 1085
def self.list(filter='cn=*')
  ASF.search_one(base, filter, 'cn').flatten.map {|cn| Committee.find(cn)}
end
load_committee_info(contents = nil, info = nil) click to toggle source

load committee info from committee-info.txt. Will not reparse if the file has already been parsed and the underlying file has not changed.

# File lib/whimsy/asf/committee.rb, line 92
def self.load_committee_info(contents = nil, info = nil)
  if contents
    if info
      @committee_mtime = @@svn_change =
        Time.parse(info[/Last Changed Date: (.*) \(/, 1]).gmtime
    else
      @committee_mtime = @@svn_change = Time.now
    end

    parse_committee_info contents
  else
    board = ASF::SVN['private/committers/board']
    file = "#{board}/committee-info.txt"
    return unless File.exist? file

    if @committee_info and File.mtime(file) <= @committee_mtime
      return @committee_info 
    end

    @committee_mtime = File.mtime(file)
    @@svn_change = Time.parse(
      %x`svn info #{file}`[/Last Changed Date: (.*) \(/, 1]).gmtime

    parse_committee_info File.read(file)
  end
end
new(*args) click to toggle source

create an empty committee instance

Calls superclass method ASF::Base.new
# File lib/whimsy/asf/committee.rb, line 53
def initialize(*args)
  @info = []
  @chairs = []
  @roster = {}
  super
end
nonpmcs() click to toggle source

return a list of non-PMC committees. Data is obtained from committee-info.txt

# File lib/whimsy/asf/committee.rb, line 388
def self.nonpmcs
  @nonpmcs
end
parse_committee_info(contents) click to toggle source

extract chairs, list of nonpmcs, roster, start date, and reporting information from committee-info.txt. Note: this method is intended to be internal, use ::load_committee_info as it will cache this data.

# File lib/whimsy/asf/committee.rb, line 299
def self.parse_committee_info(contents)
  # List uses full (display) names as keys, but the entries use the canonical names
  # - the local version of find() converts the name
  # - and stores the original as the display name if it has some upper case
  list = Hash.new {|hash, name| hash[name] = find(name)}

  # Split the file on lines starting "* ", i.e. the start of each group in section 3
  info = contents.split(/^\* /)
  # Extract the text before first entry in section 3 and split on section headers,
  # keeping sections 1 (COMMITTEES) and 2 (REPORTING).
  head, report = info.shift.split(/^\d\./)[1..2]
  # Drop lines which could match group entries
  head.gsub! /^\s+NAME\s+CHAIR\s*$/,'' # otherwise could match an entry with no e-mail

  # extract the committee chairs (e-mail address is required here)
  # Note: this includes the non-PMC entries
  head.scan(/^[ \t]+(\w.*?)[ \t][ \t]+(.*)[ \t]+<(.*?)@apache\.org>/).
    each do |committee, name, id|
      unless list[committee].chairs.any? {|chair| chair[:id] == id}
        list[committee].chairs << {name: name, id: id}
      end
    end

  # Extract the non-PMC committees (e-mail address may be absent)
  # first drop leading text so we only match non-PMCs
  @nonpmcs = head.sub(/.*?also has /m,'').
    scan(/^[ \t]+(\w.*?)(?:[ \t][ \t]|[ \t]?$)/).flatten.uniq.
    map {|name| list[name]}

  # for each committee in section 3
  info.each do |roster|
    # extract the committee name (and parenthesised comment if any)
    name = roster[/(\w.*?)[ \t]+\(est/,1]
    unless list.include?(name)
      Wunderbar.warn "No chair entry detected for #{name} in section 3"
    end
    committee = list[name]

    # get and normalize the start date
    established = roster[/\(est\. (.*?)\)/, 1]
    established = "0#{established}" if established =~ /^\d\//
    committee.established = established

    # match non-empty entries and check the syntax
    roster.scan(/^[ \t]+.+$/) do |line|
        Wunderbar.warn "Invalid syntax: #{committee.name} '#{line}'" unless line =~ /\s<(.*?)@apache\.org>\s/
    end

    # extract the availids (is this used?)
    committee.info = roster.scan(/<(.*?)@apache\.org>/).flatten

    # drop (chair) markers and extract 0: name, 1: availid, 2: [date], 3: date
    # the date is optional (e.g. infrastructure)
    committee.roster = Hash[roster.gsub(/\(\w+\)/, '').
      scan(/^[ \t]*(.*?)[ \t]*<(.*?)@apache\.org>(?:[ \t]+(\[(.*?)\]))?/).
      map {|list| [list[1], {name: list[0], date: list[3]}]}]
  end

  # process report section
  report.scan(/^([^\n]+)\n---+\n(.*?)\n\n/m).each do |period, committees|
    committees.scan(/^   [ \t]*(.*)/).each do |committee|
      committee, comment = committee.first.split(/[ \t]+#[ \t]+/,2)
      unless list.include? committee
        Wunderbar.warn "Unexpected name '#{committee}' in report section; ignored"
        next
      end
      committee = list[committee]
      if comment
        committee.report = "#{period}: #{comment}"
      elsif period == 'Next month'
        committee.report = 'Every month'
      else
        committee.schedule = period
      end
    end
  end

  @committee_info = list.values.uniq
end
pmcs() click to toggle source

return a list of PMC committees. Data is obtained from committee-info.txt

# File lib/whimsy/asf/committee.rb, line 381
def self.pmcs
  committees = ASF::Committee.load_committee_info
  committees - @nonpmcs
end
preload() click to toggle source

fetch dn, member, modifyTimestamp, and createTimestamp for all committees in LDAP.

# File lib/whimsy/asf/ldap.rb, line 1091
def self.preload
  Hash[ASF.search_one(base, "cn=*", %w(dn member modifyTimestamp createTimestamp)).map do |results|
    cn = results['dn'].first[/^cn=(.*?),/, 1]
    committee = ASF::Committee.find(cn)
    committee.modifyTimestamp = results['modifyTimestamp'].first # it is returned as an array of 1 entry
    committee.createTimestamp = results['createTimestamp'].first # it is returned as an array of 1 entry
    members = results['member'] || []
    committee.members = members
    [committee, members]
  end]
end
remove(name) click to toggle source

remove a committee from LDAP

# File lib/whimsy/asf/ldap.rb, line 1242
def self.remove(name)
  ASF::LDAP.delete("cn=#{name},#{base}")
end
svn_change() click to toggle source

Return the Last Changed Date for committee-info.txt in svn as a Time object. Data is based on the previous call to ::load_committee_info.

# File lib/whimsy/asf/committee.rb, line 404
def self.svn_change
  @@svn_change
end
update_chairs(contents, establish_or_change, terminate) click to toggle source

update chairs

# File lib/whimsy/asf/committee.rb, line 187
def self.update_chairs(contents, establish_or_change, terminate)
  # extract committee section; and then extract the lines containing
  # committee names and chairs
  section = contents[/^1\..*?\n=+/m]
  committees = section[/-\n(.*?)\n\n/m, 1].scan(/^ +(.*?)  +(.*)/).to_h

  # update/add chairs based on establish and change resolutions
  establish_or_change.each do |name, chair|
    person = ASF::Person.find(chair)
    committees[name] = "#{person.public_name} <#{person.id}@apache.org>"
  end

  # remove committees based on terminate resolutions
  terminate.each {|name| committees.delete(name)} if terminate

  # sort and concatenate committees
  committees = committees.sort_by {|name, chair| name.downcase}.
    map {|name, chair| "    #{name.ljust(23)} #{chair}"}.
    join("\n")

  # replace committee info in the section, and then replace the
  # section in the committee-info contents
  section[/-\n(.*?)\n\n/m, 1] = committees
  contents[/^1\..*?\n=+/m] = section

  # return result
  contents
end
update_next_month(contents, date, missing, rejected, establish) click to toggle source

update next month section. Remove entries that have reported or or expired; add (or update) entries that are missing; add entries for new committees.

# File lib/whimsy/asf/committee.rb, line 122
def self.update_next_month(contents, date, missing, rejected, establish)
  # extract next month section; and then extract the lines containing
  # '#' signs from within that section
  next_month = contents[/Next month.*?\n\n/m].chomp
  block = next_month[/(.*#.*\n)+/] || ''

  # remove expired entries
  month = date.strftime("%B")
  block.gsub!(/.* # new, monthly through #{month}\n/, '')

  # update/remove existing 'missing' entries
  existing = []
  block.gsub! /(.*?)# (missing|not accepted) in .*\n/ do |line|
    if rejected.include? $1.strip
      existing << $1.strip
      if line.chomp.end_with? month
        line
      else
        "#{line.chomp}, not accepted #{month}\n"
      end
    elsif missing.include? $1.strip
      existing << $1.strip
      if line.chomp.end_with? month
        line
      elsif line.split(',').last.include? 'not accepted'
        "#{line.chomp}, missing #{month}\n"
      else
        "#{line.chomp}, #{month}\n"
      end
    else
      ''
    end
  end

  # add new 'rejected' entries
  (rejected-existing).each do |pmc|
    block += "    #{pmc.ljust(22)} # not accepted in #{month}\n"
  end

  # add new 'missing' entries
  (missing-rejected-existing).each do |pmc|
    block += "    #{pmc.ljust(22)} # missing in #{month}\n"
  end

  # add new 'established' entries
  month = (date+91).strftime('%B')
  (establish-existing).each do |pmc|
    block += "    #{pmc.ljust(22)} # new, monthly through #{month}\n"
  end

  # replace/append block
  if next_month.include? '#'
    next_month[/(.*#.*\n)+/] = block.split("\n").sort.join("\n")
  else
    next_month += block
  end

  # replace next month section
  contents[/Next month.*?\n\n/m] = next_month + "\n\n"

  # return result
  contents
end

Public Instance Methods

add(people) click to toggle source

DEPRECATED. add people to a committee. Call add_owners instead.

# File lib/whimsy/asf/ldap.rb, line 1221
def add(people)
  @members = nil
  people = (Array(people) - members).map(&:dn)
  return if people.empty?
  ASF::LDAP.modify(self.dn, [ASF::Base.mod_add('member', people)])
ensure
  @members = nil
end
add_committers(people) click to toggle source

add people as committers of a project. This information is stored in LDAP using a members attribute.

# File lib/whimsy/asf/ldap.rb, line 1191
def add_committers(people)
  if GUINEAPIGS.include? name
    ASF::Project.find(name).add_members(people)
  else
    project = ASF::Project[name]
    project.add_members(people) if project
    ASF::Group.find(name).add(people)
  end
end
add_owners(people) click to toggle source

add people as owners of a project in LDAP

# File lib/whimsy/asf/ldap.rb, line 1179
def add_owners(people)
  if GUINEAPIGS.include? name
    ASF::Project.find(name).add_owners(people)
  else
    project = ASF::Project[name]
    project.add_owners(people) if project
    add(people)
  end
end
chair() click to toggle source

returns the (first) chair as an instance of the ASF::Person class.

# File lib/whimsy/asf/committee.rb, line 409
def chair
  Committee.load_committee_info
  if @chairs.length >= 1
    ASF::Person.find(@chairs.first[:id])
  else
    nil
  end
end
committers() click to toggle source

List of committers for this committee. Data is obtained from LDAP. This data is generally stored in an attribute named member.

# File lib/whimsy/asf/ldap.rb, line 1148
def committers
  if GUINEAPIGS.include? name
    ASF::Project.find(name).members
  else
    ASF::Group.find(name).members
  end
end
description() click to toggle source

description for this committee. Data is sourced from ASF::Site.

# File lib/whimsy/asf/site.rb, line 108
def description
  site = ASF::Site.find(name)
  site[:text] if site
end
display_name() click to toggle source

Version of name suitable for display purposes. Typically in uppercase. Data is sourced from committee-info.txt.

# File lib/whimsy/asf/committee.rb, line 420
def display_name
  Committee.load_committee_info
  @display_name || name
end
display_name=(name) click to toggle source

setter for #display_name, should only be used by ::load_committee_info

# File lib/whimsy/asf/committee.rb, line 427
def display_name=(name)
  @display_name ||= name
end
dn() click to toggle source

Designated Name from LDAP

# File lib/whimsy/asf/ldap.rb, line 1202
def dn
  if GUINEAPIGS.include? name
    @dn ||= ASF::Project.find(name).dn
  else
    @dn ||= ASF.search_one(base, "cn=#{name}", 'dn').first.first
  end
end
info=(list) click to toggle source

setter for #display_name, should only be used by ::load_committee_info

# File lib/whimsy/asf/committee.rb, line 441
def info=(list)
  @info = list
end
mail_list() click to toggle source

mailing list for this committee. Generally returns the first name in the dns (e.g. whimsical). If so, it can be prefixed by a number of list names (e.g. dev, private) and .apache.org is to be appended. In some cases, the name contains an @ sign and is the full name for the mail list.

# File lib/whimsy/asf/mail.rb, line 173
def mail_list
  case name.downcase
  when 'comdev'
    'community'
  when 'httpcomponents'
    'hc'
  when 'whimsy'
    'whimsical'

  when 'brandmanagement'
    'trademarks@apache.org'
  when 'executiveassistant'
    'ea@apache.org'
  when 'infrastructure'
    'infra'
  when 'legalaffairs'
    'legal-internal@apache.org'
  when 'marketingandpublicity'
    'press@apache.org'
  when 'tac'
    'travel-assistance@apache.org'
  when 'w3crelations'
    'w3c@apache.org'
  else
    name
  end
end
members() click to toggle source

DEPRECATED. List of members for this committee. Use owners as it is less ambiguous.

# File lib/whimsy/asf/ldap.rb, line 1124
def members
  members = weakref(:members) do
    ASF.search_one(base, "cn=#{name}", 'member').flatten
  end

  members.map {|uid| Person.find uid[/uid=(.*?),/,1]}
end
members=(members) click to toggle source

setter for members attribute, should only be used by ::preload

# File lib/whimsy/asf/ldap.rb, line 1118
def members=(members)
  @members = WeakRef.new(members)
end
names() click to toggle source

hash of availid => public_name for members (owners) of this committee Data is obtained from committee-info.txt.

# File lib/whimsy/asf/committee.rb, line 447
def names
  Committee.load_committee_info
  Hash[@roster.map {|id, info| [id, info[:name]]}]
end
nonpmc?() click to toggle source

if true, this committee is not a PMC.

Data is obtained from committee-info.txt.

# File lib/whimsy/asf/committee.rb, line 454
def nonpmc?
  Committee.nonpmcs.include? self
end
owners() click to toggle source

List of owners for this committee, i.e. people who are members of the committee and have update access. Data is obtained from LDAP.

# File lib/whimsy/asf/ldap.rb, line 1138
def owners
  if GUINEAPIGS.include? name
    ASF::Project.find(name).owners
  else
    members
  end
end
remove(people) click to toggle source

DEPRECATED remove people from a committee. Call remove_owners instead.

# File lib/whimsy/asf/ldap.rb, line 1211
def remove(people)
  @members = nil
  people = (Array(people) & members).map(&:dn)
  return if people.empty?
  ASF::LDAP.modify(self.dn, [ASF::Base.mod_delete('member', people)])
ensure
  @members = nil
end
remove_committers(people) click to toggle source

remove people as members of a project in LDAP

# File lib/whimsy/asf/ldap.rb, line 1168
def remove_committers(people)
  if GUINEAPIGS.include? name
    ASF::Project.find(name).remove_members(people)
  else
    project = ASF::Project[name]
    project.remove_members(people) if project
    ASF::Group.find(name).remove(people)
  end
end
remove_owners(people) click to toggle source

remove people as owners of a project in LDAP

# File lib/whimsy/asf/ldap.rb, line 1157
def remove_owners(people)
  if GUINEAPIGS.include? name
    ASF::Project.find(name).remove_owners(people)
  else
    project = ASF::Project[name]
    project.remove_owners(people) if project
    remove(people)
  end
end
report() click to toggle source

when this committee is next expected to report. May be a string containing values such as “Next month: missing in May”, “Next month: new, montly through July”. Or may be a list of months, separated by commas. Data is obtained from committee-info.txt.

# File lib/whimsy/asf/committee.rb, line 435
def report
  @report || @schedule
end
site() click to toggle source

website for this committee. Data is sourced from ASF::Site.

# File lib/whimsy/asf/site.rb, line 102
def site
  site = ASF::Site.find(name)
  site[:link] if site
end
usesproject?() click to toggle source

does this committee use ou=project?

# File lib/whimsy/asf/auth.rb, line 90
def usesproject?
  @usesproject ||= ASF::Authorization.new('pit').projects.include?(name)
end