whimsical-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From Sam Ruby <ru...@apache.org>
Subject [whimsy.git] [6/50] Commit c218d25: Commit 'model'
Date Fri, 04 Dec 2015 19:19:43 GMT
Commit c218d25ae15ab1d42765960b2601270b369fd188:
    Commit 'model'
    git-svn-id: https://svn.apache.org/repos/infra/infrastructure/trunk/projects/whimsy@819384
90ea9780-b833-de11-8433-001ec94261de


Branch: refs/heads/master
Author: Sam Ruby <rubys@apache.org>
Committer: Sam Ruby <rubys@apache.org>
Pusher: rubys <rubys@apache.org>

------------------------------------------------------------
README                                                       | +++++++++ 
asf.rb                                                       | +++++++++ 
asf/auth.rb                                                  | ++++++++++++ 
asf/committee.rb                                             | +++++++++ 
asf/icla.rb                                                  | ++++++++ 
asf/ldap.rb                                                  | ++++++++ 
asf/mail.rb                                                  | ++++++++ 
asf/member.rb                                                | ++++++++ 
asf/nominees.rb                                              | ++++++++ 
asf/svn.rb                                                   | +++++++++++ 
asf/watch.rb                                                 | +++++++++++ 
------------------------------------------------------------
643 changes: 643 additions, 0 deletions.
------------------------------------------------------------


diff --git a/README b/README
new file mode 100644
index 0000000..20b971f
--- /dev/null
+++ b/README
@@ -0,0 +1,9 @@
+This directory has two subdirectories...
+
+1) "asf" contains the "model", i.e., a set of classes which encapsulate access
+   to a number of data sources such as LDAP, ICLAs, auth lists, etc.
+
+2) "www" contains the "view", largely a set of cgi scripts that produce HTML.
+   Generally a cgi script is self contained, including all of the CSS,
+   scripts, AJAX logic (client and server), SVG images, etc.  A single script
+   may also produce a set (subtree) of web pages. 
diff --git a/asf.rb b/asf.rb
new file mode 100644
index 0000000..f9ac8b2
--- /dev/null
+++ b/asf.rb
@@ -0,0 +1,9 @@
+require File.expand_path('../asf/committee', __FILE__)
+require File.expand_path('../asf/ldap', __FILE__)
+require File.expand_path('../asf/mail', __FILE__)
+require File.expand_path('../asf/svn', __FILE__)
+require File.expand_path('../asf/watch', __FILE__)
+require File.expand_path('../asf/nominees', __FILE__)
+require File.expand_path('../asf/icla', __FILE__)
+require File.expand_path('../asf/auth', __FILE__)
+require File.expand_path('../asf/member', __FILE__)
diff --git a/asf/auth.rb b/asf/auth.rb
new file mode 100644
index 0000000..f98b521
--- /dev/null
+++ b/asf/auth.rb
@@ -0,0 +1,24 @@
+module ASF
+
+  class Authorization
+    include Enumerable
+
+    def self.find_by_id(value)
+      new.select {|auth, ids| ids.include? value}.map(&:first)
+    end
+
+    def each
+      auth = ASF::SVN['infra/infrastructure/trunk/subversion/authorization']
+      File.read("#{auth}/asf-authorization-template").
+        scan(/^([-\w]+)=(\w.*)$/).each do |pmc, ids|
+        yield pmc, ids.split(',')
+      end
+    end
+  end
+
+  class Person
+    def auth
+      @auths ||= ASF::Authorization.find_by_id(name)
+    end
+  end
+end
diff --git a/asf/committee.rb b/asf/committee.rb
new file mode 100644
index 0000000..1eab5a8
--- /dev/null
+++ b/asf/committee.rb
@@ -0,0 +1,69 @@
+module ASF
+
+  class Base
+  end
+
+  class Committee < Base
+    @@aliases = Hash.new {|hash, name| name}
+    @@aliases.merge! \
+      'community development'       => 'comdev',
+      'conference planning'         => 'concom',
+      'conferences'                 => 'concom',
+      'http server'                 => 'httpd',
+      'httpserver'                  => 'httpd',
+      'java community process'      => 'jcp',
+      'quetzalcoatl'                => 'quetz',
+      'security team'               => 'security',
+      'c++ standard library'        => 'stdcxx',
+      'travel assistance'           => 'tac',
+      'traffic server'              => 'trafficserver',
+      'web services'                => 'ws',
+      'xml graphics'                => 'xmlgraphics'
+
+    def self.load_committee_info
+      return @committee_info if @committee_info
+      board = ASF::SVN['private/committers/board']
+      committee = File.read("#{board}/committee-info.txt").split(/^\* /)
+      head = committee.shift.split(/^\d\./)[1]
+      head.scan(/^\s+(\w.*?)\s\s+.*<(\w+)@apache\.org>/).each do |name, id|
+        find(name).chair = ASF::Person.find(id) 
+      end
+      @nonpmcs = head.sub(/.*?also has/m,'').
+        scan(/^\s+(\w.*?)\s\s+.*<\w+@apache\.org>/).flatten.uniq.
+        map {|name| find(name)}
+      @committee_info = ASF::Committee.collection.values
+    end
+
+    def self.nonpmcs
+      @nonpmcs
+    end
+
+    def self.find(name)
+      result = super(@@aliases[name.downcase])
+      result.display_name = name if name =~ /[A-Z]/
+      result
+    end
+
+    def chair
+      Committee.load_committee_info
+      @chair
+    end
+
+    def display_name
+      Committee.load_committee_info
+      @display_name || name
+    end
+
+    def display_name=(name)
+      @display_name ||= name
+    end
+
+    def chair=(person)
+      @chair = person
+    end
+
+    def nonpmc?
+      Committee.nonpmcs.include? self
+    end
+  end
+end
diff --git a/asf/icla.rb b/asf/icla.rb
new file mode 100644
index 0000000..808d503
--- /dev/null
+++ b/asf/icla.rb
@@ -0,0 +1,64 @@
+module ASF
+
+  class ICLA
+    include Enumerable
+
+    def self.find_by_id(value)
+      return if value == 'notinavail'
+      new.each do |id, name, email|
+        if id == value
+	  return Struct.new(:id, :name, :email).new(id, name, email)
+        end 
+      end
+      nil
+    end
+
+    def self.find_by_email(value)
+      value = value.downcase
+      ICLA.new.each do |id, name, email|
+        if email.downcase == value
+	  return Struct.new(:id, :name, :email).new(id, name, email)
+        end 
+      end
+      nil
+    end
+
+    def self.availids
+      return @availids if @availids
+      availids = []
+      ICLA.new.each {|id, name, email| availids << id unless id == 'notinavail'}
+      @availids = availids
+    end
+
+    def each(&block)
+      officers = ASF::SVN['private/foundation/officers']
+      iclas = File.read("#{officers}/iclas.txt")
+      iclas.scan(/^(\w+):.*?:(.*?):(.*?):/).each(&block)
+    end
+  end
+
+  class Person
+    def icla
+      @icla ||= ASF::ICLA.find_by_id(name)
+    end
+
+    def icla?
+      ICLA.availids.include? name
+    end
+  end
+
+  def self.search_archive_by_id(value)
+    require 'net/http'
+    require 'nokogiri'
+    committers = 'http://people.apache.org/~rubys/committers.html'
+    doc = Nokogiri::HTML(Net::HTTP.get(URI.parse(committers)))
+    doc.search('tr').each do |tr|
+      tds = tr.search('td')
+      next unless tds.length == 3
+      return tds[1].text if tds[0].text == value
+    end
+    nil
+  rescue
+    nil
+  end
+end
diff --git a/asf/ldap.rb b/asf/ldap.rb
new file mode 100644
index 0000000..df74f3c
--- /dev/null
+++ b/asf/ldap.rb
@@ -0,0 +1,251 @@
+require 'wunderbar'
+
+module ASF
+
+  # determine whether or not the LDAP API can be used
+  def self.init_ldap
+    @ldap = nil
+    begin
+      conf = '/etc/ldap/ldap.conf'
+      host = File.read(conf).scan(/^uri\s+ldaps:\/\/(\S+?):(\d+)/i).first
+      Wunderbar.info "Connecting to LDAP server: [ldaps://#{host[0]}:#{host[1]}]"
+    rescue Errno::ENOENT
+      host = nil
+    end
+
+    if host
+      begin
+        require 'rubygems'
+        require 'ldap'
+        begin
+          @ldap = LDAP::SSLConn.new(host.first, host.last.to_i)
+        rescue LDAP::ResultError=>re
+          Wunderbar.error "Error binding to LDAP server: message: ["+ re.message + "]"
+        end
+      rescue LoadError=>e
+          Wunderbar.info "ruby-ldap wasn't found; ldapsearch will be used instead: [" + e.message
+ "]"
+      end
+    end
+  end
+
+  # emulate the LDAP API by shelling out to ldapsearch and parsing LDIF
+  def self.ldapsearch(base, scope, filter, attrs)
+    attrs = attrs.join(' ')  if attrs.respond_to? :join
+    search = `ldapsearch -x -LLL -b #{base} -s #{scope} #{filter} #{attrs}`
+    search.sub!(/\Aversion: \d+\n/, '')
+    search.gsub!(/\n /, '')
+    search.gsub!(/^(\w+):: ([A-Za-z0-9+\/]+=?=?)/) do
+      "#{$1}: #{$2.unpack('m*').join}"
+    end
+    search.force_encoding('utf-8') if search.respond_to? :force_encoding
+    search.split("\n\n").map do |lines| 
+      Hash[lines.scan(/^(\w+): (.*)/).group_by(&:first).
+        map {|name, value| [name, value.map(&:last)]}]
+    end
+  end
+
+  # search with a scope of one
+  def self.search_one(base, filter, attrs=nil)
+    init_ldap unless defined? @ldap
+
+    Wunderbar.info "ldapsearch -x -LLL -b #{base} -s one #{filter} " +
+      "#{[attrs].flatten.join(' ')}"
+    
+    if @ldap
+      result = @ldap.search2(base, LDAP::LDAP_SCOPE_ONELEVEL, filter, attrs)
+    else
+      result = ldapsearch(base, 'one', filter, attrs)
+    end
+
+    result.map! {|hash| hash[attrs]} if String === attrs
+
+    result
+  end
+
+  def self.pmc_chairs
+    @pmc_chairs ||= Service.find('pmc-chairs').members
+  end
+
+  def self.committers
+    @committers ||= Group.find('committers').members
+  end
+
+  def self.members
+    @members ||= Group.find('member').members
+  end
+
+  class Base
+    attr_reader :name
+
+    def self.base
+      @base
+    end
+
+    def base
+      self.class.base
+    end
+
+    def self.collection
+      @collection ||= Hash.new
+    end
+
+    def self.[] name
+      collection[name] || new(name)
+    end
+
+    def self.find name
+      collection[name] || new(name)
+    end
+
+    def self.new name
+      collection[name] || super
+    end
+
+    def initialize name
+      self.class.collection[name] = self
+      @name = name
+    end
+
+    unless Object.respond_to? :id
+      def id
+        @name
+      end
+    end
+  end
+
+  class LazyHash < Hash
+    def initialize(&initializer)
+      @initializer = initializer
+    end
+
+    def load
+     return unless @initializer
+     merge! @initializer.call || {}
+     @initializer = super
+    end
+
+    def [](key)
+      result = super
+      if not result and not keys.include? key and @initializer
+        merge! @initializer.call || {}
+        @initializer = nil
+        result = super
+      end
+      result
+    end
+  end
+
+  class Person < Base
+    @base = 'ou=people,dc=apache,dc=org'
+
+    def self.list(filter='uid=*')
+      ASF.search_one(base, filter, 'uid').flatten.map {|uid| find(uid)}
+    end
+
+    # pre-fetch a given attribute, for a given list of people
+    def self.preload(attributes, people={})
+      attributes = [attributes].flatten
+
+      if people.empty?
+        filter = "(|#{attributes.map {|attribute| "(#{attribute}=*)"}.join})"
+      else
+        filter = "(|#{people.map {|person| "(uid=#{person.name})"}.join})"
+      end
+      
+      zero = Hash[attributes.map {|attribute| [attribute,nil]}]
+
+      data = ASF.search_one(base, filter, attributes + ['uid'])
+      data = Hash[data.map! {|hash| [find(hash['uid'].first), hash]}]
+      data.each {|person, hash| person.attrs.merge!(zero.merge(hash))}
+
+      if people.empty?
+        (collection.values - data.keys).each do |person| 
+          person.attrs.merge! zero
+        end
+      end
+    end
+
+    def attrs
+      @attrs ||= LazyHash.new {ASF.search_one(base, "uid=#{name}").first}
+    end
+
+    def public_name
+      cn = [attrs['cn']].flatten.first
+      cn.force_encoding('utf-8') if cn.respond_to? :force_encoding
+      return cn if cn
+      return icla.name if icla
+      ASF.search_archive_by_id(name)
+    end
+
+    def asf_member?
+      ASF::Member.status[name] or ASF.members.include? self
+    end
+
+    def banned?
+      not attrs['loginShell'] or attrs['loginShell'].include? "/usr/bin/false"
+    end
+
+    def mail
+      attrs['mail'] || []
+    end
+
+    def alt_email
+      attrs['asf-altEmail'] || []
+    end
+
+    def pgp_key_fingerprints
+      attrs['asf-pgpKeyFingerprint']
+    end
+
+    def urls
+      attrs['asf-personalURL'] || []
+    end
+
+    def committees
+      Committee.list("member=uid=#{name},#{base}")
+    end
+
+    def groups
+      Group.list("memberUid=#{name}")
+    end
+  end
+
+  class Group < Base
+    @base = 'ou=groups,dc=apache,dc=org'
+
+    def self.list(filter='cn=*')
+      ASF.search_one(base, filter, 'cn').flatten.map {|cn| find(cn)}
+    end
+
+    def members
+      ASF.search_one(base, "cn=#{name}", 'memberUid').flatten.
+        map {|uid| Person.find(uid)}
+    end
+  end
+
+  class Committee < Base
+    @base = 'ou=pmc,ou=committees,ou=groups,dc=apache,dc=org'
+
+    def self.list(filter='cn=*')
+      ASF.search_one(base, filter, 'cn').flatten.map {|cn| Committee.find(cn)}
+    end
+
+    def members
+      ASF.search_one(base, "cn=#{name}", 'member').flatten.
+        map {|uid| Person.find uid[/uid=(.*?),/,1]}
+    end
+  end
+
+  class Service < Base
+    @base = 'ou=groups,ou=services,dc=apache,dc=org'
+
+    def self.list(filter='cn=*')
+      ASF.search_one(base, filter, 'cn').flatten
+    end
+
+    def members
+      ASF.search_one(base, "cn=#{name}", 'member').flatten.
+        map {|uid| Person.find uid[/uid=(.*?),/,1]}
+    end
+  end
+end
diff --git a/asf/mail.rb b/asf/mail.rb
new file mode 100644
index 0000000..085fb0a
--- /dev/null
+++ b/asf/mail.rb
@@ -0,0 +1,61 @@
+
+module ASF
+
+  class Mail
+    def self.list
+      return @list if @list
+
+      list = Hash.new
+
+      # load info from LDAP
+      ASF::Person.preload(['mail', 'asf-altEmail'])
+      ASF::Person.collection.each do |name, person|
+        (person.mail+person.alt_email).each do |mail|
+          list[mail.downcase] = person
+        end
+      end
+
+      # load all member emails in one pass
+      ASF::Member.each do |id, text|
+        Member.emails(text).each {|mail| list[mail.downcase] ||= Person[id]}
+      end
+
+      # load all ICLA emails in one pass
+      ASF::ICLA.new.each do |id, name, email|
+        list[email.downcase] ||= Person.find(id)
+        next if id == 'notinavail'
+        list["#{id.downcase}@apache.org"] ||= Person.find(id)
+      end
+
+      @list = list
+    end
+  end
+
+  class Person < Base
+    def self.find_by_email(value)
+      value.downcase!
+
+      person = Mail.list[value]
+      return person if person
+    end
+
+    def obsolete_emails
+      return @obsolete_emails if @obsolete_emails
+      result = []
+      if icla
+        unless active_emails.any? {|mail| mail.downcase == icla.email.downcase}
+          result << icla.email
+        end
+      end
+      @obsolete_emails = result
+    end
+
+    def active_emails
+      (mail + alt_email + member_emails).uniq
+    end
+
+    def all_mail
+      active_emails + obsolete_emails
+    end
+  end
+end
diff --git a/asf/member.rb b/asf/member.rb
new file mode 100644
index 0000000..3ac6adb
--- /dev/null
+++ b/asf/member.rb
@@ -0,0 +1,63 @@
+module ASF
+  class Member
+    include Enumerable
+
+    def self.find_text_by_id(value)
+      new.each do |id, text|
+        return text if id==value
+      end
+      nil
+    end
+
+    def self.each(&block)
+      new.each(&block)
+    end
+
+    def self.find_by_email(value)
+      value = value.downcase
+      each do |id, text|
+        emails(text).each do |email|
+          return Person[id] if email.downcase == value
+        end
+      end
+      nil
+    end
+
+    def self.status
+      return @status if @status
+      status = {}
+      foundation = ASF::SVN['private/foundation']
+      sections = File.read("#{foundation}/members.txt").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 = status
+    end
+
+    def each
+      return unless ASF::Person.new($USER).asf_member?
+      foundation = ASF::SVN['private/foundation']
+      File.read("#{foundation}/members.txt").split(/^ \*\) /).each do |section|
+        id = section[/Avail ID: (.*)/,1]
+        yield id, section.sub(/\n.*\n===+\s*?\n(.*\n)+.*/,'').strip if id
+      end
+    end
+
+    def self.emails(text)
+      emails = text.to_s.scan(/Email: (.*(?:\n\s+\S+@.*)*)/).flatten.
+        join(' ').split(/\s+/).grep(/@/)
+    end
+  end
+
+  class Person
+    def members_txt
+      @members_txt ||= ASF::Member.find_text_by_id(name)
+    end
+
+    def member_emails
+      ASF::Member.emails(members_txt)
+    end
+  end
+end
diff --git a/asf/nominees.rb b/asf/nominees.rb
new file mode 100644
index 0000000..535786d
--- /dev/null
+++ b/asf/nominees.rb
@@ -0,0 +1,31 @@
+module ASF
+
+  class Person < Base
+  
+    def self.member_nominees
+      return @member_nominees if @member_nominees
+
+      foundation = ASF::SVN['private/foundation/Meetings']
+      text = File.read "#{foundation}/20120522/nominated-members.txt"
+
+      nominations = text.split(/^\s*---+\s*/)
+      nominations.shift(2)
+
+      nominees = {}
+      nominations.each do |nomination|
+        id = nomination[/^\s?\w+.*<(\S+)@apache.org>/,1]
+        id ||= nomination[/^\s?\w+.*\(([a-z]+)\)/,1]
+
+        next unless id
+
+        nominees[find(id)] = nomination
+      end
+
+      @member_nominees = nominees
+    end
+
+    def member_nomination
+      Person.member_nominees[self]
+    end
+  end
+end
diff --git a/asf/svn.rb b/asf/svn.rb
new file mode 100644
index 0000000..bc6c2ae
--- /dev/null
+++ b/asf/svn.rb
@@ -0,0 +1,21 @@
+require 'uri'
+
+module ASF
+
+  class SVN
+    @base = URI.parse('https://svn.apache.org/repos/')
+
+    def self.repos
+      @repos ||= Hash[Dir['/home/whimsysvn/svn/*'].map { |name| 
+        Dir.chdir name.untaint do
+          [`svn info`[/URL: (.*)/,1].sub(/^http:/,'https:'), Dir.pwd.untaint]
+        end
+      }]
+    end
+
+    def self.[](name)
+      repos[(@base+name).to_s]
+    end
+  end
+
+end
diff --git a/asf/watch.rb b/asf/watch.rb
new file mode 100644
index 0000000..8e691aa
--- /dev/null
+++ b/asf/watch.rb
@@ -0,0 +1,41 @@
+module ASF
+
+  class Person < Base
+  
+    def self.member_watch_list
+      return @member_watch_list if @member_watch_list
+
+      foundation = ASF::SVN['private/foundation']
+      text = File.read "#{foundation}/potential-member-watch-list.txt"
+
+      nominations = text.scan(/^\s+\*\)\s+\w.*?\n\s*(?:---|\Z)/m)
+
+      i = 0
+      member_watch_list = {}
+      nominations.each do |nomination|
+        id = nil
+        name = nomination[/\*\)\s+(.+?)\s+(\(|\<|$)/,1]
+        id ||= nomination[/\*\)\s.+?\s\((.*?)\)/,1]
+        id ||= nomination[/\*\)\s.+?\s<(.*?)@apache.org>/,1]
+
+        unless id
+          id = "notinavail_#{i+=1}"
+          find(id).attrs['cn'] = name
+        end
+
+        member_watch_list[find(id)] = nomination
+      end
+
+      @member_watch_list = member_watch_list
+    end
+
+    def member_watch
+      text = Person.member_watch_list[self]
+      if text
+        text.sub!(/\A\s*\n/,'')
+        text.sub!(/\n---\Z/,'')
+      end
+      text
+    end
+  end
+end

Mime
View raw message