Merge branch 'master' into mod-gui
diff --git a/.rspec b/.rspec
new file mode 100644
index 0000000..ab695a0
--- /dev/null
+++ b/.rspec
@@ -0,0 +1,3 @@
+# ensure that rspec works OK with child spec directory
+
+-I /srv/whimsy/lib/spec
diff --git a/.travis.yml b/.travis.yml
index 91ee289..68c820e 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -43,4 +43,4 @@
# https://issues.apache.org/jira/browse/INFRA-11080
# https://github.com/apache/infrastructure-puppet/pull/319
email:
- - travis@whimsy-vm4.apache.org
+ - travis@whimsy.apache.org
diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md
index 63bfe16..328610e 100644
--- a/DEPLOYMENT.md
+++ b/DEPLOYMENT.md
@@ -90,25 +90,26 @@
store the auth-creds.
* Update the following cron scripts under https://svn.apache.org/repos/infra/infrastructure/apmail/trunk/bin:
- * listmodsubs.sh - if necessary, add an rsync to the old Whimsy host
+ * listmodsubs.sh - add the new host
* whimsy_qmail_ids.sh - add the new host
-
+ * the old hosts should be removed sometime after switchover. This approach requires two edits to the files
+ but ensures that the rsync has been tested for the new host and allows the new host to be better tested
+
* Add the following mail subscriptions:
* Subscribe `svnupdate@whimsy-vm4.apache.org` to `board-commits@apache.org`.
- Alternately, add it to the `board-cvs` alias.
- * Subscribe `svnupdate@whimsy-vm4.apache.org` to
- `committers-cvs@apache.org`.
+ * Subscribe `svnupdate@whimsy-vm4.apache.org` to `committers-cvs@apache.org`.
* Subscribe `board@whimsy-vm4.apache.org` to `board@apache.org`.
* Subscribe `members@whimsy-vm4.apache.org` to `members@apache.org`.
- * Add `secretary@whimsy-vm4.apache.org` to the `secretary@apache.org`
- alias.
-
- * Update the lists of archivers in www/board|members/subscriptions.cgi
+ * Add `secretary@whimsy-vm4.apache.org` to the `secretary@apache.org` alias.
* Using the `www-data` user, copy over the following directories from
- the previous whimsy-vm* server: `/srv/agenda`, `/srv/mail/board`,
- ``/srv/icla`, /srv/mail/members`, `/srv/mail/secretary`.
-
+ the previous whimsy-vm* server:
+ * `/srv/agenda`
+ * `/srv/icla`
+ * `/srv/mail/board`
+ * `/srv/mail/members`
+ * `/srv/mail/secretary`
+
* Verify that email can be sent to non-apache.org email addresses
* Run [testmail.rb](tools/testmail.rb)
diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md
index e03aca8..bc98e7e 100644
--- a/DEVELOPMENT.md
+++ b/DEVELOPMENT.md
@@ -70,7 +70,7 @@
`rbenv` or `rvm`. Rbenv generally requires you to be more aware of what you
are doing (e.g., the need for rbenv shims). Rvm tends to be more of a set
and forget operation, but it tends to be more system intrusive (e.g. aliasing
- 'cd' in bash). Note the Whimsy server currently uses **ruby 2.3+**.
+ 'cd' in bash). Note the Whimsy server currently uses **ruby 2.5+**.
For more information:
@@ -247,6 +247,11 @@
start on dbus SIGNAL=SessionNew
exec /srv/whimsy/tools/toucher
+4. (Optional) Debug your local Whimsy web environment with two scripts:
+
+ localhost:port/test.cgi?debug
+ localhost:port/racktest
+
More details about the production Whimsy instance are in [DEPLOYMENT.md](DEPLOYMENT.md)
Documentation Standards
diff --git a/MACOSX.md b/MACOSX.md
index 5401545..7740d0c 100644
--- a/MACOSX.md
+++ b/MACOSX.md
@@ -22,14 +22,84 @@
Homebrew/homebrew-core (git revision 66e9; last commit 2018-04-11)
```
+Update using:
+
+```
+$ brew update
+```
+
+Homebrew has removed options we need from two of the formulas we need.
+Fix formulas for `openldap` and `apr-util` to make the required options standard.
+Note that we have to remove the bottles otherwise a version of the software is downloaded that does not include the options we require.
+
+```
+$ cd /usr/local/Homebrew/Library/Taps/homebrew/homebrew-core/Formula
+$ # edit apr-util.rb and openldap.rb to make the below diffs
+$ git diff
+diff --git a/Formula/apr-util.rb b/Formula/apr-util.rb
+index 4dee25282..97f460398 100644
+--- a/Formula/apr-util.rb
++++ b/Formula/apr-util.rb
+@@ -5,24 +5,28 @@ class AprUtil < Formula
+ sha256 "d3e12f7b6ad12687572a3a39475545a072608f4ba03a6ce8a3778f607dd0035b"
+ revision 1
+
+- bottle do
+- sha256 "e4927892e16a3c9cf0d037c1777a6e5728fef2f5abfbc0af3d0d444e9d6a1d2b" => :mojave
+- sha256 "1bdf0cda4f0015318994a162971505f9807cb0589a4b0cbc7828531e19b6f739" => :high_sierra
+- sha256 "75c244c3a34abab343f0db7652aeb2c2ba472e7ad91f13af5524d17bba3001f2" => :sierra
+- sha256 "bae285ada445a2b5cc8b43cb8c61a75e177056c6176d0622f6f87b1b17a8502f" => :el_capitan
+- end
+
+ keg_only :provided_by_macos, "Apple's CLT package contains apr"
+
+ depends_on "apr"
+ depends_on "openssl"
++ depends_on "openldap"
+
+ def install
+ # Install in libexec otherwise it pollutes lib with a .exp file.
+ system "./configure", "--prefix=#{libexec}",
+ "--with-apr=#{Formula["apr"].opt_prefix}",
+ "--with-crypto",
+- "--with-openssl=#{Formula["openssl"].opt_prefix}"
++ "--with-openssl=#{Formula["openssl"].opt_prefix}",
++ "--with-ldap",
++ "--with-ldap-lib=#{Formula["openldap"].opt_lib}",
++ "--with-ldap-include=#{Formula["openldap"].opt_include}"
+ system "make"
+ system "make", "install"
+ bin.install_symlink Dir["#{libexec}/bin/*"]
+diff --git a/Formula/openldap.rb b/Formula/openldap.rb
+index bc6bde9fe..710265ec1 100644
+--- a/Formula/openldap.rb
++++ b/Formula/openldap.rb
+@@ -4,11 +4,11 @@ class Openldap < Formula
+ url "https://www.openldap.org/software/download/OpenLDAP/openldap-release/openldap-2.4.47.tgz"
+ sha256 "f54c5877865233d9ada77c60c0f69b3e0bfd8b1b55889504c650047cc305520b"
+
+- bottle do
+- sha256 "07e1f0e3ec1a02340a82259e1ace713cfb362126404575032713174935f4140e" => :mojave
+- sha256 "8901626fc45d76940dec5e516b23d81c9970f4a4a94650bdad60228d604c1b4a" => :high_sierra
+- sha256 "6dc84ff9e088116201a47adc5c3a2aab28ffd10dbab9d677d49ad7eef1ccc349" => :sierra
+- end
+
+ keg_only :provided_by_macos
+
+@@ -35,6 +35,7 @@ class Openldap < Formula
+ --enable-refint
+ --enable-retcode
+ --enable-seqmod
++ --enable-sssvlv=yes
+ --enable-translucent
+ --enable-unique
+ --enable-valsort
+```
+
Upgrade Ruby
------------
-Much of Whimsy is written in Ruby. Install:
-
-```
-$ brew install ruby
-```
+Much of Whimsy is written in Ruby.
Verify:
@@ -39,8 +109,22 @@
```
If you don't see 2.3.1 or later, run `hash -r` and try again. If you previously
-installed ruby via brew, you may need to run `brew upgrade ruby` instead. If you use `rbenv` install via `rbenv install 2.5.0`
+installed ruby via brew, you may need to run `brew upgrade ruby` instead.
+Install:
+
+```
+$ brew install rbenv
+$ rbenv install 2.5.1
+```
+
+Use `ln -s` in `/usr/local/bin` for both `ruby` and `gem` pointing to the locations
+where `rbenv` installed ruby in your home directory
+
+```
+ln -s /usr/local/bin/ruby /Users/${user}/.rbenv/versions/2.5.1/bin/ruby
+ln -s /usr/local/bin/gem /Users/${user}/.rbenv/versions/2.5.1/bin/gem
+```
Upgrade Node.js
---------------
@@ -71,7 +155,14 @@
Install:
```
-$ gem install whimsy-asf bundler mail listen
+sudo gem install mail listen
+sudo gem install bundler -n /usr/local/bin
+sudo gem install nokogumbo
+sudo gem install passenger sinatra kramdown
+sudo gem install setup
+sudo gem install ruby2js
+sudo gem install rack rake
+sudo gem install crass json sanitize
```
Verify:
@@ -165,8 +256,8 @@
```
brew install apache-httpd
-brew install openldap --with-sssvlv
-brew reinstall -s apr-util --with-openldap
+brew install openldap # --with-sssvlv
+brew reinstall -s apr-util # --with-openldap
brew reinstall -s apache-httpd
```
@@ -202,6 +293,7 @@
<pre>
LoadModule proxy_module lib/httpd/modules/mod_proxy.so
LoadModule proxy_wstunnel_module lib/httpd/modules/mod_proxy_wstunnel.so
+ LoadModule negotiation_module lib/httpd/modules/mod_negotiation.so
LoadModule speling_module lib/httpd/modules/mod_speling.so
LoadModule rewrite_module lib/httpd/modules/mod_rewrite.so
LoadModule expires_module lib/httpd/modules/mod_expires.so
@@ -236,6 +328,8 @@
the fork() child process. Crashing
instead.](https://blog.phusion.nl/2017/10/13/why-ruby-app-servers-break-on-macos-high-sierra-and-what-can-be-done-about-it/) message in your `/var/log/apache/error.log` file. If so, do the following:
+On Mojave the failure with forking occurred with Passenger and the following fixes were required.
+
Edit `/usr/local/opt/httpd/homebrew.mxcl.httpd.plist` and add the following:
```
@@ -501,6 +595,6 @@
Debugging
---------
-When things go wrong, check `/var/log/apache2/whimsy_error.log` and
-`/var/log/apache2/error_log`.
+When things go wrong, either check `whimsy_error.log` and `error_log` in
+either `/usr/local/var/log/httpd/` or `/var/log/apache2/`. The location depends on how you have installed httpd.
diff --git a/NOTICE b/NOTICE
index b668de9..94b23c9 100644
--- a/NOTICE
+++ b/NOTICE
@@ -1,5 +1,5 @@
Apache Whimsy
-Copyright 2016-2018 The Apache Software Foundation
+Copyright 2016-2019 The Apache Software Foundation
This product includes software developed at
The Apache Software Foundation (http://www.apache.org/).
diff --git a/Rakefile b/Rakefile
index 68b1a2e..e2b492b 100644
--- a/Rakefile
+++ b/Rakefile
@@ -75,7 +75,7 @@
end
task :config do
- $LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
+ $LOAD_PATH.unshift '/srv/whimsy/lib'
require 'whimsy/asf/config'
end
diff --git a/TODOS.md b/TODOS.md
index e223ee3..848a1e3 100644
--- a/TODOS.md
+++ b/TODOS.md
@@ -50,4 +50,3 @@
Scan curdir; forall *.cgi where second line includes WVisible, display name/link.
Using a positive comment ensures only scripts wishing to be displayed are visible.
Effectively done as much is valuable: [www/committers/tools](https://whimsy.apache.org/committers/tools)
-
diff --git a/examples/board.rb b/examples/board.rb
index 3308c90..925f7d1 100644
--- a/examples/board.rb
+++ b/examples/board.rb
@@ -10,7 +10,7 @@
#
# ruby examples/board.rb --install=/Users/rubys/Sites/
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'whimsy/asf'
_html do
diff --git a/lib/spec/README.md b/lib/spec/README.md
new file mode 100644
index 0000000..879924c
--- /dev/null
+++ b/lib/spec/README.md
@@ -0,0 +1,7 @@
+Test cases (using rspec) for the library
+
+Run the tests:
+
+$ rspec [-I lib/spec] lib
+
+The -I flag and its parameter can be omitted if they are added to the .rspec file
diff --git a/lib/spec/lib/mail/mail_spec.rb b/lib/spec/lib/mail/mail_spec.rb
new file mode 100644
index 0000000..febd1dd
--- /dev/null
+++ b/lib/spec/lib/mail/mail_spec.rb
@@ -0,0 +1,58 @@
+# encoding: utf-8
+# frozen_string_literal: true
+require 'spec_helper'
+require 'whimsy/asf'
+
+describe ASF::Mail do
+
+ describe "ASF::Mail.to_canonical" do
+ it "should return address unaltered for invalid emails" do
+ email = 'textwithnoATsign'
+ expect(ASF::Mail.to_canonical(email)).to eq(email)
+ email = 'textwithtrailing@'
+ expect(ASF::Mail.to_canonical(email)).to eq(email)
+ email = '@textwithleadingAT'
+ expect(ASF::Mail.to_canonical(email)).to eq(email)
+ end
+ it "should return address with downcased domain for valid emails" do
+ expect(ASF::Mail.to_canonical('ABC@DEF')).to eq('ABC@def')
+ end
+ it "should return address with downcased domain and canonicalised name for GMail emails" do
+ expect(ASF::Mail.to_canonical('A.B.C+123@GMail.com')).to eq('abc@gmail.com')
+ end
+ it "should return address with downcased domain and canonicalised name for Googlemail emails" do
+ expect(ASF::Mail.to_canonical('A.B.C+123@Googlemail.com')).to eq('abc@gmail.com')
+ end
+ end
+
+ describe '.cansub(member, pmc_chair, ldap_pmcs)' do
+ lists = ASF::Mail.cansub(false, false, nil)
+ it 'should return public lists only' do
+ whitelist = ['infra-users', 'jobs', 'site-dev', 'committers-cvs', 'site-cvs', 'concom', 'party']
+ board = ['board', 'board-commits', 'board-chat']
+ expect(lists.length).to be >= 1000
+ expect(lists).not_to include('private')
+ expect(lists).not_to include('security')
+ expect(lists).to include(*whitelist)
+ expect(lists).not_to include(*board)
+ end
+ it 'should return the same lists' do
+ mylists = ASF::Mail.cansub(false, false, []) - lists
+ expect(mylists.length).to be(0)
+ end
+ it 'should return private PMC lists' do
+ mylists = ASF::Mail.cansub(false, false, ['ant','whimsical']) - lists
+ expect(mylists.length).to be(2)
+ expect(mylists).to include('ant-private','whimsical-private')
+ end
+ it 'should not return non-existent lists' do
+ mylists = ASF::Mail.cansub(false, false, ['xxxant','xxxwhimsical']) - lists
+ expect(mylists.length).to be(0)
+ end
+ it 'should return private PPMC lists' do
+ podnames = ASF::Podling.current.map(&:name)
+ mylists = ASF::Mail.cansub(false, false, podnames) - lists
+ expect(mylists.length).to be_between(podnames.length-2, podnames.length).inclusive # mailing list may not be set up yet
+ end
+ end
+end
diff --git a/lib/spec/lib/mail/mlist_spec.rb b/lib/spec/lib/mail/mlist_spec.rb
new file mode 100644
index 0000000..859a79c
--- /dev/null
+++ b/lib/spec/lib/mail/mlist_spec.rb
@@ -0,0 +1,85 @@
+# encoding: utf-8
+# frozen_string_literal: true
+require 'spec_helper'
+require 'whimsy/asf'
+require 'whimsy/asf/mlist' # not loaded by default
+
+describe ASF::MLIST do
+
+ describe "ASF::MLIST.members_subscribers" do
+ it "should return an array of members@ subscribers followed by the file update time" do
+ res = ASF::MLIST.members_subscribers()
+ expect(res.class).to eq(Array)
+ expect(res.length).to eq(2)
+ subs,stamp = res
+ expect(subs.class).to eq(Array)
+ expect(stamp.class).to eq(Time)
+ expect(subs.length).to be_between(500, 1000).inclusive
+ end
+ end
+
+ describe "ASF::MLIST.list_archivers" do
+ it "should return array of form [dom, list, [[archiver, type, alias|direct],...]" do
+ ASF::MLIST.list_archivers do |res|
+ expect(res.class).to eq(Array)
+ expect(res.length).to eq(3)
+ dom,list,arches = res # unpack
+ expect(dom.class).to eq(String)
+ expect(list.class).to eq(String)
+ expect(arches.class).to eq(Array)
+ expect(arches[0].length).to eq(3)
+ end
+ end
+ end
+
+ describe "ASF::MLIST.moderates(user_emails, response)" do
+ it "should not find any entries for invalid emails" do
+ user_emails=['user@localhost', 'user@domain.invalid']
+ res = ASF::MLIST.moderates(user_emails)
+ expect(res.length).to eq(2)
+ mods = res[:moderates]
+ expect(mods.length).to eq(0)
+ end
+
+ it "should find some entries for mod-private@gsuite.cloud.apache.org" do
+ user_emails=['mod-private@gsuite.cloud.apache.org']
+ res = ASF::MLIST.moderates(user_emails)
+ expect(res.length).to eq(2)
+ mods = res[:moderates]
+ expect(mods.length).to be_between(8, 20)
+ end
+ end
+
+ describe "ASF::MLIST.subscriptions(user_emails, response)" do
+ it "should not find any entries for invalid emails" do
+ user_emails=['user@localhost', 'user@domain.invalid']
+ res = ASF::MLIST.subscriptions(user_emails)
+ expect(res.length).to eq(2)
+ mods = res[:subscriptions]
+ expect(mods.length).to eq(0)
+ end
+ end
+
+ it "should find lots of entries for archiver@mbox-vm.apache.org" do
+ user_emails=['archiver@mbox-vm.apache.org']
+ res = ASF::MLIST.subscriptions(user_emails)
+ expect(res.length).to eq(2)
+ mods = res[:subscriptions]
+ expect(mods.length).to be_between(1000, 1200)
+ end
+
+ describe "ASF::MLIST.each_list" do
+ it "should return an array of form [[dom, list],...]" do
+ ASF::MLIST.each_list do |res|
+ expect(res.class).to eq(Array)
+ expect(res.length).to eq(2)
+ dom,list = res # unpack
+ expect(dom.class).to eq(String)
+ expect(list.class).to eq(String)
+ expect(dom).to match(/^[a-z.0-9-]+\.[a-z]+$/)
+ expect(list).to match(/^[a-z0-9-]+$/)
+ end
+ end
+ end
+
+end
diff --git a/lib/spec/spec_helper.rb b/lib/spec/spec_helper.rb
new file mode 100644
index 0000000..577cab0
--- /dev/null
+++ b/lib/spec/spec_helper.rb
@@ -0,0 +1,9 @@
+$LOAD_PATH.unshift '/srv/whimsy/lib'
+
+unless defined?(SPEC_ROOT)
+ SPEC_ROOT = File.join(File.dirname(__FILE__))
+end
+
+def fixture_path(*path)
+ File.join SPEC_ROOT, 'fixtures', path
+end
diff --git a/lib/whimsy/asf/agenda/minutes.rb b/lib/whimsy/asf/agenda/minutes.rb
index 3defa68..7dfef54 100644
--- a/lib/whimsy/asf/agenda/minutes.rb
+++ b/lib/whimsy/asf/agenda/minutes.rb
@@ -1,6 +1,9 @@
# Minutes from previous meetings
+
class ASF::Board::Agenda
+ # Must be outside scan loop
+ FOUNDATION_BOARD = ASF::SVN.find('foundation_board') # Use find to placate Travis
parse do
minutes = @file.split(/^ 3. Minutes from previous meetings/,2).last.
split(OFFICER_SEPARATOR,2).first
@@ -19,9 +22,11 @@
attrs['text'] = attrs['text'].strip
attrs['approved'] = attrs['approved'].strip.gsub(/\s+/, ' ')
- file = attrs['text'][/board_minutes[_\d]+\.txt/].untaint
- if file and File.exist?(File.join(FOUNDATION_BOARD, file))
- attrs['mtime'] = File.mtime(File.join(FOUNDATION_BOARD, file)).to_i
+ if FOUNDATION_BOARD
+ file = attrs['text'][/board_minutes[_\d]+\.txt/].untaint
+ if file and File.exist?(File.join(FOUNDATION_BOARD, file))
+ attrs['mtime'] = File.mtime(File.join(FOUNDATION_BOARD, file)).to_i
+ end
end
end
end
diff --git a/lib/whimsy/asf/agenda/summary.rb b/lib/whimsy/asf/agenda/summary.rb
index 5429e75..fb0da5f 100644
--- a/lib/whimsy/asf/agenda/summary.rb
+++ b/lib/whimsy/asf/agenda/summary.rb
@@ -1,4 +1,5 @@
require 'set'
+require 'whimsy/asf/board'
# Creates a summary hash of information from an Agenda
class ASF::Board::Agenda
@@ -18,41 +19,6 @@
REPORT_LEN = 'rl'
APPROVALS_KEY = 'ap'
- INITIALS_IDX = 0
- # Map director ids->names and ids->initials
- # Only filled in since 2007 or so, once the preapp data in meetings is parseable
- DIRECTOR_MAP = {
- 'bayard' => ['hy', 'Henri', 'Henri Yandell'],
- 'bdelacretaz' => ['bd', 'Bertrand', 'Bertrand Delacretaz'],
- 'brett' => ['bp', 'Brett', 'Brett Porter'],
- 'brianm' => ['bmc', 'Brian', 'Brian McCallister'],
- 'cliffs' => ['cs', 'Cliff', 'Cliff Schmidt'],
- 'coar' => ['kc', 'Ken', 'Ken Coar'],
- 'curcuru' => ['sc', 'Shane', 'Shane Curcuru'],
- 'cutting' => ['dc', 'Doug', 'Doug Cutting'],
- 'dirkx' => ['dg', 'Dirk-Willem', 'Dirk-Willem van Gulik'],
- 'dkulp' => ['dk', 'Daniel', 'Daniel Kulp'],
- 'fielding' => ['rf', 'Roy', 'Roy T. Fielding'],
- 'geirm' => ['gmj', 'Geir', 'Geir Magnusson Jr'],
- 'gstein' => ['gs', 'Greg', 'Greg Stein'],
- 'isabel' => ['idf', 'Isabel', 'Isabel Drost-Fromm'],
- 'jerenkrantz' => ['je', 'Justin', 'Justin Erenkrantz'],
- 'jim' => ['jj', 'Jim', 'Jim Jagielski'],
- 'ke4qqq' => ['dn', 'David', 'David Nalley'],
- 'lrosen' => ['lr', 'Larry', 'Lawrence Rosen'],
- 'markt' => ['mt', 'Mark', 'Mark Thomas'],
- 'marvin' => ['mh', 'Marvin', 'Marvin Humphrey'],
- 'mattmann' => ['cm', 'Chris', 'Chris Mattmann'],
- 'noirin' => ['np', 'Noirin', 'Noirin Plunkett'],
- 'psteitz' => ['ps', 'Phil', 'Phil Steitz'],
- 'rbowen' => ['rb', 'Rich', 'Rich Bowen'],
- 'rgardler' => ['rg', 'Ross', 'Ross Gardler'],
- 'rubys' => ['sr', 'Sam', 'Sam Ruby'],
- 'rvs' => ['rs', 'Roman', 'Roman Shaposhnik'],
- 'striker' => ['ss', 'Sander', 'Sander Striker'],
- 'tdunning' => ['td', 'Ted', 'Ted Dunning']
- }
-
SKIP_AGENDAS = {
'board_agenda_2009_11_01' => 'F2F meeting: ApacheCon, St. Helena, CA',
'board_agenda_2010_09_11' => 'F2F meeting: Boston, MA',
@@ -83,7 +49,7 @@
summary[PEOPLE_KEY] = Hash[agenda[1][PEOPLE_KEY]]
summary[PEOPLE_KEY].each do |id, data|
# Note: this adds initials to everyone who was *ever* a director, who was at this meeting
- data['initials'] = DIRECTOR_MAP[id][INITIALS_IDX] if DIRECTOR_MAP[id]
+ data['initials'] = ASF::Board.directorInitials(id) if ASF::Board.directorHasId?(id)
end
rescue StandardError => e
summary[ERRORS_KEY] = "ERROR(#{meeting}) no attendance error: #{e.message} #{e.backtrace[0]}"
diff --git a/lib/whimsy/asf/auth.rb b/lib/whimsy/asf/auth.rb
index 32dd2bf..5d94706 100644
--- a/lib/whimsy/asf/auth.rb
+++ b/lib/whimsy/asf/auth.rb
@@ -78,17 +78,4 @@
end
end
- class Group
- # does this group use ou=project?
- def usesproject?
- @usesproject ||= ASF::Authorization.new('asf').projects.include?(name)
- end
- end
-
- class Committee
- # does this committee use ou=project?
- def usesproject?
- @usesproject ||= ASF::Authorization.new('pit').projects.include?(name)
- end
- end
end
diff --git a/lib/whimsy/asf/board.rb b/lib/whimsy/asf/board.rb
index dc6ec85..8e6d760 100644
--- a/lib/whimsy/asf/board.rb
+++ b/lib/whimsy/asf/board.rb
@@ -73,5 +73,71 @@
end
end
end
+
+ # Does the uid have an entry in the director intials table?
+ def self.directorHasId?(id)
+ DIRECTOR_MAP[id]
+ end
+
+ # Return the initials for the uid
+ # Fails if there is no entry, so check first using directorHasId?
+ def self.directorInitials(id)
+ DIRECTOR_MAP[id][INITIALS]
+ end
+
+ # Return the first name for the uid
+ # Fails if there is no entry, so check first using directorHasId?
+ def self.directorFirstName(id)
+ DIRECTOR_MAP[id][FIRST_NAME]
+ end
+
+ # Return the display name for the uid
+ # Fails if there is no entry, so check first using directorHasId?
+ def self.directorDisplayName(id)
+ DIRECTOR_MAP[id][DISPLAY_NAME]
+ end
+
+ private
+
+ # Map director ids->names and ids->initials
+ # Only filled in since 2007 or so, once the preapp data in meetings is parseable
+ INITIALS = 0
+ FIRST_NAME = 1
+ DISPLAY_NAME = 2
+ DIRECTOR_MAP = {
+ 'bayard' => ['hy', 'Henri', 'Henri Yandell'],
+ 'bdelacretaz' => ['bd', 'Bertrand', 'Bertrand Delacretaz'],
+ 'brett' => ['bp', 'Brett', 'Brett Porter'],
+ 'brianm' => ['bmc', 'Brian', 'Brian McCallister'],
+ 'cliffs' => ['cs', 'Cliff', 'Cliff Schmidt'],
+ 'coar' => ['kc', 'Ken', 'Ken Coar'],
+ 'curcuru' => ['sc', 'Shane', 'Shane Curcuru'],
+ 'cutting' => ['dc', 'Doug', 'Doug Cutting'],
+ 'dirkx' => ['dg', 'Dirk-Willem', 'Dirk-Willem van Gulik'],
+ 'dkulp' => ['dk', 'Daniel', 'Daniel Kulp'],
+ 'druggeri' => ['dr', 'Daniel', 'Daniel Ruggeri'],
+ 'fielding' => ['rf', 'Roy', 'Roy T. Fielding'],
+ 'geirm' => ['gmj', 'Geir', 'Geir Magnusson Jr'],
+ 'gstein' => ['gs', 'Greg', 'Greg Stein'],
+ 'isabel' => ['idf', 'Isabel', 'Isabel Drost-Fromm'],
+ 'jerenkrantz' => ['je', 'Justin', 'Justin Erenkrantz'],
+ 'jim' => ['jj', 'Jim', 'Jim Jagielski'],
+ 'ke4qqq' => ['dn', 'David', 'David Nalley'],
+ 'lrosen' => ['lr', 'Larry', 'Lawrence Rosen'],
+ 'markt' => ['mt', 'Mark', 'Mark Thomas'],
+ 'marvin' => ['mh', 'Marvin', 'Marvin Humphrey'],
+ 'mattmann' => ['cm', 'Chris', 'Chris Mattmann'],
+ 'myrle' => ['mk', 'Myrle', 'Myrle Krantz'],
+ 'noirin' => ['np', 'Noirin', 'Noirin Plunkett'],
+ 'psteitz' => ['ps', 'Phil', 'Phil Steitz'],
+ 'rbowen' => ['rb', 'Rich', 'Rich Bowen'],
+ 'rgardler' => ['rg', 'Ross', 'Ross Gardler'],
+ 'rubys' => ['sr', 'Sam', 'Sam Ruby'],
+ 'rvs' => ['rs', 'Roman', 'Roman Shaposhnik'],
+ 'striker' => ['ss', 'Sander', 'Sander Striker'],
+ 'tdunning' => ['td', 'Ted', 'Ted Dunning'],
+ 'wohali' => ['jt', 'Joan', 'Joan Touzet'],
+ }
+
end
end
diff --git a/lib/whimsy/asf/committee.rb b/lib/whimsy/asf/committee.rb
index f2edf2f..b8562f7 100644
--- a/lib/whimsy/asf/committee.rb
+++ b/lib/whimsy/asf/committee.rb
@@ -18,7 +18,7 @@
# ASF::Committee.load_committee_info is called.
#
# Similarly, the simple attributes which are sourced from LDAP is
- # generally not available until ASF::Committee.preload is called.
+ # generally not available until ASF::Project.preload is called.
class Committee < Base
# list of chairs for this committee. Returned as a list of hashes
@@ -83,7 +83,8 @@
@@namemap = Proc.new do |name|
# Drop parenthesized comments and downcase before lookup; drop all spaces after lookup
# So aliases table does not need to contain entries for Traffic Server and XML Graphics.
- cname = @@aliases[name.sub(/\s+\(.*?\)/, '').downcase].gsub(/\s+/, '')
+ # Also compress white-space before lookup so tabs etc from index.html don't matter
+ cname = @@aliases[name.sub(/\s+\(.*?\)/, '').strip.gsub(/\s+/, ' ').downcase].gsub(/\s+/, '')
cname
end
@@ -527,5 +528,12 @@
Committee.load_committee_info # ensure data is there
Committee.nonpmcs.include? self
end
+
+ # if true, this committee is a PMC.
+ # Data is obtained from <tt>committee-info.txt</tt>.
+ def pmc?
+ Committee.load_committee_info # ensure data is there
+ Committee.pmcs.include? self
+ end
end
end
diff --git a/lib/whimsy/asf/forms.rb b/lib/whimsy/asf/forms.rb
new file mode 100644
index 0000000..2176038
--- /dev/null
+++ b/lib/whimsy/asf/forms.rb
@@ -0,0 +1,107 @@
+require 'wunderbar'
+require 'wunderbar/markdown'
+
+# Define common page features for whimsy tools using bootstrap styles
+class Wunderbar::HtmlMarkup
+
+ # Utility function to add icons after form controls
+ def _whimsy_forms_iconlink(**args)
+ if args[:iconlink]
+ _div.input_group_btn do
+ _a.btn.btn_default type: 'button', aria_label: "#{iconlabel}", href: "#{args[:iconlink]}", target: 'whimsy_help' do
+ _span.glyphicon class: "#{args[:icon]}", aria_label: "#{args[:iconlabel]}"
+ end
+ end
+ elsif args[:icon]
+ _span.input_group_addon do
+ _span.glyphicon class: "#{args[:icon]}", aria_label: "#{args[:iconlabel]}"
+ end
+ end
+ end
+
+ # Utility function for divs around form controls, including help
+ def _whimsy_control_wrapper(**args)
+ _div.form_group do
+ _label.control_label.col_sm_3 args[:label], for: "#{args[:name]}"
+ _div.col_sm_9 do
+ _div.input_group do
+ yield
+ _whimsy_forms_iconlink(args)
+ end
+ if args[:helptext]
+ _span.help_block id: "#{args[:aria_describedby]}" do
+ _markdown "#{args[:helptext]}"
+ end
+ end
+ end
+ end
+ end
+
+ # Display a single input control within a form; or if rows, then a textarea
+ # @param name required string ID of control's label
+ def _whimsy_forms_input(**args)
+ return unless args[:name]
+ args[:label] ||= 'Enter string'
+ args[:type] ||= 'text'
+ args[:id] = args[:name]
+ args[:aria_describedby] = "#{args[:name]}_help" if args[:helptext]
+ _whimsy_control_wrapper(args) do
+ args[:class] = 'form-control'
+ if args[:rows]
+ _textarea! args do
+ _! args[:value]
+ end
+ else
+ _input args
+ end
+ end
+ end
+
+ # Display an optionlist control within a form
+ # @param name required string ID of control's label
+ # @param options required ['value'] or {"value" => 'Label for value'} of all selectable values
+ # @param values required 'value' or ['value'] or {"value" => 'Label for value'} of all selected values
+ # @param placeholder Currently displayed text if passed (not selectable)
+ def _whimsy_forms_select(**args)
+ return unless args[:name]
+ return unless args[:values]
+ args[:label] ||= 'Select value(s)'
+ args[:id] = args[:name]
+ args[:aria_describedby] = "#{args[:name]}_help" if args[:helptext]
+ _whimsy_control_wrapper(args) do
+ if args[:multiple]
+ args[:multiple] = 'true'
+ end
+ _select.form_control args do
+ if ''.eql?(args[:placeholder])
+ _option '', value: '', selected: 'selected'
+ else
+ _option "#{args[:placeholder]}", value: '', selected: 'selected', disabled: 'disabled', hidden: 'hidden'
+ end
+ # Construct selectable list from values (first) then options
+ if args[:values].kind_of?(Array)
+ args[:values].each do |val|
+ _option val, value: val, selected: true
+ end
+ elsif args[:values].kind_of?(Hash)
+ args[:values].each do |val, disp|
+ _option disp, value: val, selected: true
+ end
+ elsif args[:values] # Fallback for simple case of single string value
+ _option "#{args[:values]}", value: "#{args[:values]}", selected: true
+ args[:values] = [args[:values]] # Ensure supports .include? for options loop below
+ end
+ if args[:options].kind_of?(Array)
+ args[:options].each do |val|
+ _option val, value: val unless args[:values].include?(val)
+ end
+ elsif args[:options].kind_of?(Hash)
+ args[:options].each do |val, disp|
+ _option disp, value: val unless args[:values].include?(val)
+ end
+ end
+ end
+ end
+ end
+
+end
diff --git a/lib/whimsy/asf/icla.rb b/lib/whimsy/asf/icla.rb
index 8fd81ab..88ce8c7 100644
--- a/lib/whimsy/asf/icla.rb
+++ b/lib/whimsy/asf/icla.rb
@@ -212,17 +212,20 @@
end
# list of all availids that are are taken or reserved
+ # See also ASF::Mail.taken?
def self.availids_taken()
self.availids_reserved + self.availids
end
# is the availid taken (in use or reserved)?
+ # See also ASF::Mail.taken?
def self.taken?(id)
return self.availids_reserved.include?(id) ||
self.availids.include?(id)
end
# is the id available?
+ # See also ASF::Mail.taken?
def self.available?(id)
return ! self.taken?(id)
end
diff --git a/lib/whimsy/asf/ldap.rb b/lib/whimsy/asf/ldap.rb
index b0c0aa1..664b4ca 100644
--- a/lib/whimsy/asf/ldap.rb
+++ b/lib/whimsy/asf/ldap.rb
@@ -394,10 +394,16 @@
weakref(:pmc_chairs) {Service.find('pmc-chairs').members}
end
- # Obtain a list of committers from LDAP
- # <tt>cn=committers,ou=groups,dc=apache,dc=org</tt>
+ # Obtain a list of committers from LDAP
+ # <tt>cn=committers,ou=role,ou=groups,dc=apache,dc=org</tt>
def self.committers
- weakref(:committers) {Group.find('committers').members}
+ weakref(:committers) {RoleGroup.find('committers').members}
+ end
+
+ # Obtain a list of committers from LDAP (old unix group)
+ # <tt>cn=committers,ou=groups,dc=apache,dc=org</tt>
+ def self.oldcommitters
+ weakref(:oldcommitters) {Group.find('committers').members}
end
# Obtain a list of members from LDAP
@@ -749,6 +755,11 @@
attrs['asf-banned'] == 'yes'
end
+ # is the login marked as inactive?
+ def inactive?
+ nologin? || asf_banned?
+ end
+
# primary mail addresses
def mail
attrs['mail'] || []
@@ -781,9 +792,10 @@
# If the latter, then it needs to be derived from project_owners filtered to keep only PMCs
def committees
# legacy LDAP entries
- committees = weakref(:committees) do
- Committee.list("member=uid=#{name},#{base}")
- end
+ committees = []
+# committees = weakref(:committees) do
+# Committee.list("member=uid=#{name},#{base}")
+# end
# add in projects
# Get list of project names where the person is an owner
@@ -810,6 +822,11 @@
end
end
+ # list of Podlings that this individual is a member (owner) of
+ def podlings
+ ASF::Podling.current.select{|pod| project_owners.map(&:name).include? pod.name}
+ end
+
# list of LDAP groups that this individual is a member of
def groups
weakref(:groups) do
@@ -1236,39 +1253,10 @@
end
end
+ # represenation of Committee, i.e. entry in committee-info.txt
+ # includes PMCs and other committees, but does not include podlings
class Committee < Base
- # TODO what to do about this? Change to ou=project or drop?
- # It's used by the methods: self.list, self.preload, member[id]s
- @base = 'ou=pmc,ou=committees,ou=groups,dc=apache,dc=org'
-
- # return a list of committees, from LDAP.
- # TODO this stopped returning all PMCs when guinea pigs were introduced
- # Should it be dropped, or made to return the list of PMCs ?
- # No longer used
- def self.list(filter='cn=*')
- ASF.search_one(base, filter, 'cn').flatten.map {|cn| Committee.find(cn)}
- end
-
- # fetch <tt>dn</tt>, <tt>member</tt>, <tt>modifyTimestamp</tt>, and
- # <tt>createTimestamp</tt> for all committees in LDAP.
- # TODO - delete? Not sure it's used anymore
- 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
-
- # Date this committee was last modified in LDAP.
- attr_accessor :modifyTimestamp
-
- # Date this committee was initially created in LDAP.
- attr_accessor :createTimestamp
+ @base = nil # not sure it makes sense to define base here
# return committee only if it actually exists
def self.[] name
@@ -1277,29 +1265,16 @@
(ASF::Committee.pmcs+ASF::Committee.nonpmcs).map(&:name).include?(name) ? committee : nil
end
- # setter for members attribute, should only be used by
- # ASF::Committee.preload
- def members=(members)
- @members = WeakRef.new(members)
+ # Date this committee was last modified in LDAP.
+ # defer to Project; must have called project.preload
+ def modifyTimestamp
+ ASF::Project[name].modifyTimestamp
end
- # DEPRECATED. List of members for this committee. Use owners as it
- # is less ambiguous.
- def members
- members = weakref(:members) do
- ASF.search_one(base, "cn=#{name}", 'member').flatten
- end
-
- members.map {|uid| Person.find uid[/uid=(.*?),/,1]}
- end
-
- # List of ids in the member attribute for this committee
- def memberids
- members = weakref(:members) do
- ASF.search_one(base, "cn=#{name}", 'member').flatten
- end
-
- members.map {|uid| uid[/uid=(.*?),/,1]}
+ # Date this committee was initially created in LDAP.
+ # defer to Project; must have called project.preload
+ def createTimestamp
+ ASF::Project[name].createTimestamp
end
# List of owners for this committee, i.e. people who are members of the
@@ -1354,26 +1329,6 @@
@dn ||= ASF::Project.find(name).dn
end
- # DEPRECATED remove people from a committee. Call #remove_owners instead.
- 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
-
- # DEPRECATED. add people to a committee. Call #add_owners instead.
- 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
-
end
#
@@ -1507,6 +1462,10 @@
if __FILE__ == $0
$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'whimsy/asf/config'
+ old=ASF.oldcommitters()
+ puts old.length
+ new=ASF.committers()
+ puts new.length
ASF::RoleGroup.listcns.map {|g| puts ASF::RoleGroup.find(g).dn}
ASF::AppGroup.listcns.map {|g| puts ASF::AppGroup.find(g).dn}
end
diff --git a/lib/whimsy/asf/mail.rb b/lib/whimsy/asf/mail.rb
index e3548c0..d06dc0e 100644
--- a/lib/whimsy/asf/mail.rb
+++ b/lib/whimsy/asf/mail.rb
@@ -64,6 +64,11 @@
public_private ? @lists : @lists.keys
end
+ def self.list_mtime
+ Mail._load_lists
+ @list_mtime
+ end
+
# list of mailing lists that aren't actively seeking new subscribers
def self.deprecated
apmail_bin = ASF::SVN['apmail_bin']
@@ -71,6 +76,9 @@
end
# which lists are available for subscription via Whimsy?
+ # member: true if member
+ # pmc_chair: true if pmc_chair
+ # ldap_pmcs: list of (P)PMC mail_list names
def self.cansub(member, pmc_chair, ldap_pmcs)
Mail._load_lists
if member
@@ -89,9 +97,10 @@
lists += ['board', 'board-commits', 'board-chat']
end
- # PMC members need their private lists
+ # (P)PMC members need their private lists
if ldap_pmcs
- lists += ldap_pmcs.map {|lp| "#{lp}-private"}
+ # ensure that the lists actually exist
+ lists += ldap_pmcs.map {|lp| "#{lp}-private"}.select{|l| @lists.keys.include? l}
end
lists
@@ -125,11 +134,14 @@
end
end
+ # List of .qmail files that could clash with user ids (See: INFRA-14566)
def self.qmail_ids
return [] unless File.exist? '/srv/subscriptions/qmail.ids'
File.read('/srv/subscriptions/qmail.ids').split
end
+ # Is the id used by qmail?
+ # See also ASF::ICLA.taken?
def self.taken?(id)
self.qmail_ids.include? id
end
@@ -140,7 +152,36 @@
return list if dom == 'apache.org'
dom.sub(".apache.org",'-') + list
end
-
+
+ # Canonicalise an email address, removing aliases and ignored punctuation
+ # and downcasing the name if safe to do so
+ #
+ # Currently only handles aliases for @gmail.com and @googlemail.com
+ #
+ # All domains are converted to lower-case
+ #
+ # The case of the name part is preserved since some providers may be case-sensitive
+ # Almost all providers ignore case in names, however that is not guaranteed
+ def self.to_canonical(email)
+ parts = email.split('@')
+ if parts.length == 2
+ name, dom = parts
+ return email if name.length == 0 || dom.length == 0
+ dom.downcase!
+ dom = 'gmail.com' if dom == 'googlemail.com' # same mailbox
+ if dom == 'gmail.com'
+ return name.sub(/\+.*/,'').gsub('.','').downcase + '@' + dom
+ else
+ # Effectively the same:
+ dom = 'apache.org' if dom == 'minotaur.apache.org'
+ # only downcase the domain (done above)
+ return name + '@' + dom
+ end
+ end
+ # Invalid; return input rather than failing
+ return email
+ end
+
end
class Person < Base
@@ -198,6 +239,8 @@
'trademarks@apache.org'
when 'infrastructure'
'infra'
+ when 'dataprivacy'
+ 'legal-internal@apache.org'
when 'legalaffairs'
'legal-internal@apache.org'
when 'fundraising'
diff --git a/lib/whimsy/asf/member.rb b/lib/whimsy/asf/member.rb
index 13c256f..fc7af88 100644
--- a/lib/whimsy/asf/member.rb
+++ b/lib/whimsy/asf/member.rb
@@ -27,6 +27,7 @@
# 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)}]
@@ -50,10 +51,11 @@
nil
end
- # Return a hash of non-active ASF members and their status. Keys are
+ # 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
@@ -143,9 +145,11 @@
@@mtime = 0
end
- if File.mtime(File.join(foundation, 'members.txt')).to_i > @@mtime.to_i
- @@mtime = File.mtime(File.join(foundation, 'members.txt'))
- text = File.read(File.join(foundation, 'members.txt'))
+ 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
diff --git a/lib/whimsy/asf/mlist.rb b/lib/whimsy/asf/mlist.rb
index 9f90e78..14308fc 100644
--- a/lib/whimsy/asf/mlist.rb
+++ b/lib/whimsy/asf/mlist.rb
@@ -1,3 +1,5 @@
+require 'weakref'
+
module ASF
module MLIST
@@ -15,21 +17,32 @@
# Note that the data files don't provide information on whether a list is
# public or private.
+ @@file_times = Hash.new # Key=type, value = modtime
+ @@file_parsed = Hash.new # Key=type, value = cache hash
+
# Return an array of board subscribers followed by the file update time
- def self.board_subscribers
- return list_filter('sub', 'apache.org', 'board'), (File.mtime(LIST_TIME) rescue File.mtime(LIST_SUBS))
+ def self.board_subscribers(archivers=true)
+ return list_filter('sub', 'apache.org', 'board', archivers), (File.mtime(LIST_TIME) rescue File.mtime(LIST_SUBS))
end
# Return an array of members@ subscribers followed by the file update time
- def self.members_subscribers
- return list_filter('sub', 'apache.org', 'members'), (File.mtime(LIST_TIME) rescue File.mtime(LIST_SUBS))
+ def self.members_subscribers(archivers=true)
+ return list_filter('sub', 'apache.org', 'members', archivers), (File.mtime(LIST_TIME) rescue File.mtime(LIST_SUBS))
end
# Return an array of private@pmc subscribers followed by the file update time
# By default does not return the standard archivers
- # TODO - does this need to be updated for non-PMC committees?
+ # pmc can either be a pmc name, in which case it uses private@<pmc>.apache.org
+ # or it can be an ASF list name, e.g. w3c@apache.org
def self.private_subscribers(pmc, archivers=false)
- return list_filter('sub', "#{pmc}.apache.org", 'private', archivers), (File.mtime(LIST_TIME) rescue File.mtime(LIST_SUBS))
+ parts = pmc.split('@', 3) # want to detect trailing '@'
+ if parts.length == 1
+ return list_filter('sub', "#{pmc}.apache.org", 'private', archivers), (File.mtime(LIST_TIME) rescue File.mtime(LIST_SUBS))
+ elsif parts.length == 2 && parts[1] == 'apache.org'
+ return list_filter('sub', parts[1], parts[0], archivers), (File.mtime(LIST_TIME) rescue File.mtime(LIST_SUBS))
+ else
+ raise "Unexpected parameter: #{pmc}"
+ end
end
def self.security_subscribers(pmc, archivers=false)
@@ -48,10 +61,11 @@
response[:subscriptions] = []
response[:subtime] = (File.mtime(LIST_TIME) rescue File.mtime(LIST_SUBS))
+ _emails = emails.map{|email| ASF::Mail.to_canonical(email.downcase)}
list_parse('sub') do |dom, list, subs|
- emails.each do |email|
- if downcase(subs).include? email.downcase
- response[:subscriptions] << ["#{list}@#{dom}", email]
+ subs.each do |sub|
+ if _emails.include? ASF::Mail.to_canonical(sub.downcase)
+ response[:subscriptions] << ["#{list}@#{dom}", sub]
end
end
end
@@ -70,10 +84,11 @@
response[:digests] = []
response[:digtime] = (File.mtime(LIST_TIME) rescue File.mtime(LIST_DIGS))
+ _emails = emails.map{|email| ASF::Mail.to_canonical(email.downcase)}
list_parse('dig') do |dom, list, subs|
- emails.each do |email|
- if downcase(subs).include? email.downcase
- response[:digests] << ["#{list}@#{dom}", email]
+ subs.each do |sub|
+ if _emails.include? ASF::Mail.to_canonical(sub.downcase)
+ response[:digests] << ["#{list}@#{dom}", sub]
end
end
end
@@ -83,7 +98,7 @@
# return the mailing lists which are moderated by any of the list of emails
# the following keys are added to the response hash:
# :modtime - the timestamp when the data was last updated
- # :moderates - a hash. key: list name; entry: array of moderators
+ # :moderates - a hash. key: list name; entry: array of emails that match a moderator for the list
# N.B. not the same format as the subscriptions() method
def self.moderates(user_emails, response = {})
@@ -91,9 +106,9 @@
response[:moderates] = {}
response[:modtime] = (File.mtime(LIST_TIME) rescue File.mtime(LIST_MODS))
- user_emails.map!{|m| m.downcase} # outside loop
+ umails = user_emails.map{|m| ASF::Mail.to_canonical(m.downcase)} # outside loop
list_parse('mod') do |dom, list, emails|
- matching = emails.select{|m| user_emails.include? m.downcase}
+ matching = emails.select{|m| umails.include? ASF::Mail.to_canonical(m.downcase)}
response[:moderates]["#{list}@#{dom}"] = matching unless matching.empty?
end
response
@@ -130,7 +145,16 @@
# for a mail domain, extract related lists and their subscribers (default only the count)
# also returns the time when the data was last checked
+ # For top-level apache.org lists, the mail_domain is either:
+ # - the full list name (e.g. press), or:
+ # - the list prefix (e.g. legal)
# If podling==true, then also check for old-style podling names
+ # If list_subs==true, return subscriber emails else sub count
+ # Matches:
+ # {mail_domain}.apache.org/*
+ # apache.org/{mail_domain}(-.*)? (e.g. press, legal)
+ # incubator.apache.org/{mail_domain}-.* (if podling==true)
+ # Returns: {list}@{dom}
def self.list_subscribers(mail_domain, podling=false, list_subs=false)
return nil, nil unless File.exist? LIST_SUBS
@@ -144,11 +168,14 @@
# normal tlp style:
#/home/apmail/lists/commons.apache.org/dev/mod
+
# possible podling styles (new, old):
#/home/apmail/lists/batchee.apache.org/dev/mod
#/home/apmail/lists/incubator.apache.org/blur-dev/mod
+
#Apache lists (e.g. some non-PMCs)
#/home/apmail/lists/apache.org/list/mod
+
next unless "#{mail_domain}.apache.org" == dom or
(dom == 'apache.org' && list =~ /^#{mail_domain}(-|$)/) or
(podling && dom == 'incubator.apache.org' && list =~ /^#{mail_domain}-/)
@@ -157,15 +184,28 @@
return subscribers.to_h, (File.mtime(LIST_TIME) rescue File.mtime(LIST_SUBS))
end
+ # returns the list time (defaulting to list-subs time if the marker is not present)
+ def self.list_time
+ File.mtime(LIST_TIME) rescue File.mtime(LIST_SUBS)
+ end
+
def self.list_archivers
list_parse('sub') do |dom, list, subs|
yield [dom, list, subs.select {|s| is_archiver? s}.map{|m| [m,archiver_type(m,dom,list)].flatten}]
end
end
+ # return the [domain, list] for all entries in the subscriber listings
+ # the subscribers are not included
+ def self.each_list
+ list_parse('sub') do |dom, list, subs|
+ yield [dom, list]
+ end
+ end
+
private
- # return the archiver type as array: [:MBOX|:PONY|:MINO, 'public'|'private'|'alias'|'direct']
+ # return the archiver type as array: [:MBOX|:PONY|:MINO|:MAIL_ARCH|:MARKMAIL, 'public'|'private'|'alias'|'direct']
# minotaur archiver names do not include any public/private indication as that is in bin/.archives
def self.archiver_type(email, dom,list)
case email
@@ -173,10 +213,13 @@
when ARCH_MBOX_PRV then return [:MBOX, 'private']
when ARCH_PONY_PUB then return [:PONY, 'public']
when ARCH_PONY_PRV then return [:PONY, 'private']
+ when ARCH_EXT_MAIL_ARCHIVE then return [:MAIL_ARCH, 'public']
# normal archiver routed via .qmail-[tlp-]list-archive
when "#{list}-archive@#{dom}" then return [:MINO, 'alias']
# Direct mail to minotaur
when "apmail-#{dom.split('.').first}-#{list}-archive@www.apache.org" then return [:MINO, 'direct']
+ else
+ return [:MARKMAIL, 'public'] if is_markmail_archiver?(email)
end
raise "Unexpected archiver email #{email} for #{list}@#{dom}" # Should not happen?
end
@@ -186,8 +229,18 @@
e =~ /.-archive@([^.]+\.)?(apache\.org|apachecon\.com)$/
end
+ # Is the email a Whimsy archiver?
+ def self.is_whimsy_archiver? (e)
+ e =~ /@whimsy(-vm\d+)?\.apache\.org$/
+ end
+
+ # Is the email a markmail archiver?
+ def self.is_markmail_archiver? (e)
+ e =~ ARCH_EXT_MARKMAIL_RE
+ end
+
def self.is_archiver? (e)
- ARCHIVERS.include? e or is_mino_archiver? e
+ ARCHIVERS.include?(e) or is_mino_archiver?(e) or is_whimsy_archiver?(e) or is_markmail_archiver?(e)
end
def self.downcase(array)
@@ -212,7 +265,7 @@
if archivers
return emails
else
- return (emails - ARCHIVERS) - ["#{list}-archive@#{dom}"]
+ return emails.reject{|e| is_archiver?(e)}
end
end
end
@@ -238,6 +291,24 @@
else
raise ArgumentError.new('type: expecting dig, mod or sub')
end
+ ctime = @@file_times[type] || 0
+ mtime = File.mtime(path).to_i
+ if mtime <= ctime
+ cached = @@file_parsed[type]
+ if cached
+ begin
+ cached.each do |d,l,m|
+ yield d, l, m
+ end
+ return
+ rescue WeakRef::RefError
+ @@file_times[type] = 0
+ end
+ end
+ else
+ @@file_parsed[type] = nil
+ end
+ cache = Array.new # see if this preserves mod cache
# split file into paragraphs
File.read(path).split(/\n\n/).each do |stanza|
# domain may start in column 1 or following a '/'
@@ -249,13 +320,18 @@
dom = match[1].downcase # just in case
list = match[2].downcase # just in case
# Keep original case of email addresses
- yield dom, list, stanza.scan(/^(.*@.*)/).flatten
+ # TODO: a bit slow for subs file, implement cache of parsed file?
+ mails = stanza.split(/\n/).select{|x| x =~ /@/}
+ cache << [dom, list, mails]
+ yield dom, list, mails
else
# don't allow mismatches as that means the RE is wrong
line=stanza[0..(stanza.index("\n")|| -1)]
raise ArgumentError.new("Unexpected section header #{line}")
end
end
+ @@file_parsed[type] = WeakRef.new(cache)
+ @@file_times[type] = mtime
nil # don't return file contents
end
@@ -267,8 +343,12 @@
ARCH_PONY_PUB = "archive-asf-public@cust-asf.ponee.io"
ARCH_PONY_PRV = "archive-asf-private@cust-asf.ponee.io"
+ # Standard external archivers (necessarily public)
+ ARCH_EXT_MAIL_ARCHIVE = "archive@mail-archive.com"
+ ARCH_EXT_MARKMAIL_RE = %r{^\w+\.\w+\.\w+@.\.markmail\.org$} # one.two.three@a.markmail.org
+
ARCHIVERS = [ARCH_PONY_PRV, ARCH_PONY_PUB,
- ARCH_MBOX_PUB, ARCH_MBOX_PRV, ARCH_MBOX_RST]
+ ARCH_MBOX_PUB, ARCH_MBOX_PRV, ARCH_MBOX_RST, ARCH_EXT_MAIL_ARCHIVE]
# TODO alias archivers: either add list or use RE to filter them
LIST_MODS = '/srv/subscriptions/list-mods'
diff --git a/lib/whimsy/asf/podling.rb b/lib/whimsy/asf/podling.rb
index 4e1f035..ae81f1b 100644
--- a/lib/whimsy/asf/podling.rb
+++ b/lib/whimsy/asf/podling.rb
@@ -66,8 +66,10 @@
@reporting = node.at('reporting') if node.at('reporting')
@monthly = @reporting.text.split(/,\s*/) if @reporting and @reporting.text
+ @resolutionLink = node.at('resolution')['link'] if node.at('resolution')
+
# Note: the following optional elements are not currently processed:
- # - resolution
+ # - resolution (except for resolution/@link)
# - retiring/graduating
# The following podling attributes are not processed:
# - longname
@@ -90,6 +92,11 @@
@name || @resource
end
+ # TLP name (name differ from podling name)
+ def tlp_name
+ @resolutionLink || name
+ end
+
# date this podling was accepted for incubation
def startdate
return unless @startdate
@@ -164,7 +171,32 @@
# list of current podlings
def self.current
- list.select { |podling| podling.status == 'current' }
+ self._list('current')
+ end
+
+ # list of current podling ids
+ def self.currentids
+ self._listids('current')
+ end
+
+ # list of graduated podlings
+ def self.graduated
+ self._list('graduated')
+ end
+
+ # list of graduated podling ids
+ def self.graduatedids
+ self._listids('graduated')
+ end
+
+ # list of retired podlings
+ def self.retired
+ self._list('retired')
+ end
+
+ # list of retired podling ids
+ def self.retiredids
+ self._listids('retired')
end
# last modified time of podlings.xml in the local working directory,
@@ -175,7 +207,17 @@
# find a podling by name
def self.find(name)
- list.find { |podling| podling.name == name }
+ name = name.downcase
+
+ result = list.find do |podling|
+ podling.name == name or podling.display_name.downcase == name or
+ podling.resourceAliases.any? {|aname| aname.downcase == name}
+ end
+
+ result ||= list.find do |podling|
+ podling.resource == name or
+ podling.tlp_name.downcase == name
+ end
end
# below is for backwards compatibility
@@ -399,5 +441,15 @@
def namesearch
Podling.namesearch[display_name]
end
+
+ private
+
+ def self._list(status)
+ list.select { |podling| podling.status == status }
+ end
+
+ def self._listids(status)
+ list.select { |podling| podling.status == status }.map(&:id)
+ end
end
end
diff --git a/lib/whimsy/asf/themes.rb b/lib/whimsy/asf/themes.rb
index 9784ac2..c3aef09 100644
--- a/lib/whimsy/asf/themes.rb
+++ b/lib/whimsy/asf/themes.rb
@@ -2,50 +2,7 @@
# Define common page features for whimsy tools using bootstrap styles
class Wunderbar::HtmlMarkup
- # DEPRECATED Emit ASF style header with _h1 title and common links
- def _whimsy_header title, style = :full
- case style
- when :mini
- _div.header do
- _h1 title
- end
- else
- _div.header.container_fluid do
- _div.row do
- _div.col_sm_4.hidden_xs do
- _a href: 'https://www.apache.org/' do
- _img title: 'The Apache Software Foundation', alt: 'ASF Logo', width: 250, height: 101,
- style: "margin-left: 10px; margin-top: 10px;",
- src: 'https://www.apache.org/foundation/press/kit/asf_logo_small.png'
- end
- end
- _div.col_sm_3.col_xs_3 do
- _a href: '/' do
- _img title: 'Whimsy project home', alt: 'Whimsy hat logo', src: '/whimsy.svg', width: 145, height: 101
- end
- end
- _div.col_sm_5.col_xs_9.align_bottom do
- _ul class: 'nav nav-tabs' do
- _li role: 'presentation' do
- _a 'Code', href: 'https://github.com/apache/whimsy/'
- end
- _li role: 'presentation' do
- _a 'Questions', href: 'https://lists.apache.org/list.html?dev@whimsical.apache.org'
- end
- _li role: 'presentation' do
- _a 'About', href: '/technology'
- end
- _li role: 'presentation' do
- _span.badge id: 'script-ok'
- end
- end
- end
- end
- _h1 title
- end
- end
- end
-
+
# DEPRECATED Wrap content with nicer fluid margins
def _whimsy_content colstyle="col-lg-11"
_div.content.container_fluid do
@@ -57,32 +14,6 @@
end
end
- # Emit ASF style footer with (optional) list of related links
- def _whimsy_footer **args
- _div.footer.container_fluid do
- _div.panel.panel_default do
- _div.panel_heading do
- _h3.panel_title 'Related Apache Resources'
- end
- _div.panel_body do
- _ul do
- if args.key?(:related)
- args[:related].each do |url, desc|
- _li do
- _a desc, href: url
- end
- end
- else
- _li do
- _a 'Whimsy Source Code', href: 'https://github.com/apache/whimsy/'
- end
- end
- end
- end
- end
- end
- end
-
# Emit simplistic copyright footer
def _whimsy_foot
_div.footer.container_fluid style: 'background-color: #f5f5f5; padding: 10px;' do
@@ -264,6 +195,34 @@
end
end
_whimsy_foot
- end
+ end
end
+
+ # Emit wrapper panels for a single tablist accordion item
+ # @param listid of the parent _div.panel_group role: "tablist"
+ # @param itemid of this specific item
+ # @param itemtitle to display in the header panel
+ # @param n unique number of this item (for nav links)
+ # @param itemclass optional panel-success or similar styling
+ def _whimsy_accordion_item(listid: 'accordion', itemid: nil, itemtitle: '', n: 0, itemclass: nil)
+ raise ArgumentError.new("itemid must not be nil") if not itemid
+ args = {id: itemid}
+ args[:class] = itemclass if itemclass
+ _div!.panel.panel_default args do
+ _div!.panel_heading role: "tab", id: "#{listid}h#{n}" do
+ _h4!.panel_title do
+ _a!.collapsed role: "button", data_toggle: "collapse", aria_expanded: "false", data_parent: "##{listid}", href: "##{listid}c#{n}", aria_controls: "#{listid}c#{n}" do
+ _ "#{itemtitle} "
+ _span.glyphicon.glyphicon_chevron_down id: "#{itemid}-nav"
+ end
+ end
+ end
+ _div!.panel_collapse.collapse id: "#{listid}c#{n}", role: "tabpanel", aria_labelledby: "#{listid}h#{n}" do
+ _div!.panel_body do
+ yield
+ end
+ end
+ end
+ end
+
end
diff --git a/lib/whimsy/logparser.rb b/lib/whimsy/logparser.rb
index a538dfe..ebfa073 100755
--- a/lib/whimsy/logparser.rb
+++ b/lib/whimsy/logparser.rb
@@ -13,7 +13,17 @@
ERROR_LOG_DIR = '/srv/whimsy/www/members/log'
# Constants and ignored regex for whimsy_access logs
- WHIMSY_APPS = %w(status roster board public secretary)
+ WHIMSY_APPS = {
+ 'roster' => 'Roster tool',
+ 'board/agenda' => 'Board agenda tool',
+ 'board/minutes' => 'Board public minutes',
+ 'public' => 'Public JSON files',
+ 'secretary' => 'Secretary Workbench',
+ 'site.cgi' => 'TLP Site Checker',
+ 'pods.cgi' => 'Podling Site Checker',
+ 'foundation/orgchart' => 'Public OrgChart',
+ 'status' => 'Server Status'
+ }
RUSER = 'remote_user'
REFERER = 'referer'
REMAINDER = 'remainder'
@@ -80,11 +90,11 @@
# Collate/partition whimsy_access entries by app areas
# @param logs full set of items to scan
- # @return apps - WHIMSY_APPS categorized, with REMAINDER entry all others
- def collate_whimsy_access(logs)
+ # @return apps categorized by apphash, with REMAINDER entry all others not captured
+ def collate_whimsy_access(logs, apphash = WHIMSY_APPS)
remainder = logs
apps = {}
- WHIMSY_APPS.each do |a|
+ apphash.keys.each do |a|
apps[a] = Hash.new{|h,k| h[k] = [] }
apps[a][RUSER] = Hash.new{|h,k| h[k] = 0 }
apps[a][REFERER] = Hash.new{|h,k| h[k] = 0 }
diff --git a/repository.yml b/repository.yml
index b84c074..a47de77 100644
--- a/repository.yml
+++ b/repository.yml
@@ -42,6 +42,9 @@
foundation_board:
url: private/foundation/board
+ foundation_mentors:
+ url: private/foundation/mentors
+
grants:
url: private/documents/grants
diff --git a/tools/agenda_summary.rb b/tools/agenda_summary.rb
index f26b78f..19d8389 100755
--- a/tools/agenda_summary.rb
+++ b/tools/agenda_summary.rb
@@ -1,6 +1,6 @@
#!/usr/bin/env ruby
# Parse board meeting minutes and emit statistics
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'whimsy/asf'
require 'whimsy/asf/agenda'
require 'json'
diff --git a/tools/check_auth.rb b/tools/check_auth.rb
index 80d34e5..98bb7db 100755
--- a/tools/check_auth.rb
+++ b/tools/check_auth.rb
@@ -5,9 +5,11 @@
# - name agrees with ldap query
# - incorrect alias reference
+# allowable non-LDAP names
ROLE_NAMES =
%w(buildbot comdev_role projects_role spamassassin_role svn-role acrequser whimsysvn apezmlm puppetsvn apsiteread apsecmail apezmlm smtpd svn rptremind comdev-svn openejb-tck staff
- sk clr uli nick jim upayavira cpluchino mostarda
+ sk clr uli nick jim upayavira cpluchino mostarda druggeri
+ svn-site-role
)
DIR = ARGV.first || '/srv/git/infrastructure-puppet/modules/subversion_server/files/authorization'
@@ -17,6 +19,7 @@
section=''
names=Hash.new(0)
IO.foreach(file) { |x|
+ x.chomp!
next if x =~ /^(#| *$)/
section='groups' and next if x =~ /^\[groups\]$/
section='paths' and next if x =~ /^\[\/\]$/
@@ -44,7 +47,7 @@
next
end
elsif section == 'paths'
- next if x =~ /^\[((asf:|infra:|private:)?\/\S*)\]$/ # [/path]
+ next if x =~ /^\[((asf:|infra:|private:|bigdata:)?\/\S*)\]$/ # [/path]
if x =~ /^(?:@(\S+)|\*|(\S+)) *= *r?w? *$/
if $1
puts "Undefined name: '#{$1}' in #{x}" unless names.has_key?($1)
diff --git a/tools/check_consistency.rb b/tools/check_consistency.rb
deleted file mode 100755
index aae442b..0000000
--- a/tools/check_consistency.rb
+++ /dev/null
@@ -1,111 +0,0 @@
-#!/usr/bin/env ruby
-
-# basic check of LDAP consistency
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../lib', __FILE__))
-require 'whimsy/asf'
-
-fix = ARGV.delete '--fix'
-
-ASF::LDAP.bind if fix
-
-auth_path=ARGV.shift
-
-groups = ASF::Group.preload # for performance
-# TODO drop soon
-committees = ASF::Committee.preload # for performance
-
-projects = ASF::Project.preload
-summary=Hash.new { |h, k| h[k] = { } }
-projects.keys.each do |entry|
- summary[entry.name]['p']=1
-end
-
-puts "project.members ~ group.members"
-groups.keys.sort_by {|a| a.name}.each do |entry|
- summary[entry.name]['g']=1
- project = ASF::Project[entry.name]
- if project
- p = []
- project.members.sort_by {|a| a.name}.each do |e|
- p << e.name
- end
- g = []
- entry.members.sort_by {|a| a.name}.each do |e|
- g << e.name
- end
- if p != g
- puts "#{entry.name}: pm-g=#{p-g} g-pm=#{g-p}"
-
- if fix
- project.add_members(entry.members-project.members) unless (g-p).empty?
- project.remove_members(project.members-entry.members) unless (p-g).empty?
- end
- end
- end
-end
-
-puts ""
-# TODO will no longer be relevant
-puts "project.owners ~ committee.members"
-committees.keys.sort_by {|a| a.name}.each do |entry|
- summary[entry.name]['c']=1
- project = ASF::Project[entry.name]
- if project
- p = []
- project.owners.sort_by {|a| a.name}.each do |e|
- p << e.name
- end
- c = []
- entry.members.sort_by {|a| a.name}.each do |e|
- c << e.name
- end
- if p != c
- puts "#{entry.name}: po-c=#{p-c} c-po=#{c-p}"
-
- if fix
- project.add_owners(entry.members-project.owners) unless (c-p).empty?
- project.remove_owners(project.owners-entry.members) unless (p-c).empty?
- end
- end
- end
-end
-
-puts ""
-puts "current podlings(asf-auth) ~ project(members, owners)"
-pods = Hash[ASF::Podling.list.map {|podling| [podling.name, podling.status]}]
-# flag current podlings to show what records they have
-pods.each do |name,status|
- summary[name]['pod'] = status if status == 'current'
-end
-# Scan the local defines and report differences
-ASF::Authorization.new('asf',auth_path).each do |grp, mem|
- summary[grp]['pod'] = pods[grp] + ' (has local definition)'
- if pods[grp] == 'current'
- mem.sort!.uniq!
- project = ASF::Project[grp]
- if project
- pm = []
- project.members.sort_by {|a| a.name}.each do |e|
- pm << e.name
- end
- po = []
- project.owners.sort_by {|a| a.name}.each do |e|
- po << e.name
- end
- if mem != pm
- puts "#{grp}: pm-auth=#{pm-mem} auth-pm=#{mem-pm}"
- end
- if mem != po
- puts "#{grp}: po-auth=#{po-mem} auth-po=#{mem-po}"
- end
- end
- end
-end
-# Show where names are defined
-puts "\nSummary of name definitions (proj,grp,cttee,status)"
-def show(v,k)
- v[k] == 1 ? k : '-'
-end
-summary.sort.map do |k,v|
- puts "#{k.ljust(30)} #{show(v,'p')} #{show(v,'g')} #{show(v,'c')} #{v['pod'] rescue ''}"
-end
\ No newline at end of file
diff --git a/tools/collate_minutes.rb b/tools/collate_minutes.rb
index 190a124..fce80f4 100755
--- a/tools/collate_minutes.rb
+++ b/tools/collate_minutes.rb
@@ -1,5 +1,5 @@
#!/usr/bin/env ruby
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
puts $LOAD_PATH.first
require 'whimsy/asf'
@@ -35,8 +35,6 @@
# list of SVN resources needed
resources = {
- TEMPLATES: 'asf/infrastructure/site/trunk/templates',
- INCUBATOR_SITE_AUTHOR: 'asf/incubator/public/trunk/content',
SVN_SITE_RECORDS_MINUTES:
'asf/infrastructure/site/trunk/content/foundation/records/minutes',
BOARD: 'private/foundation/board'
@@ -145,13 +143,13 @@
site[id] = {:name => v['display_name'], :link => v['site'], :text => v['description']}
end
-# parse the calendar for layout info (note: hack for »)
+# parse the calendar for layout info (note: hack for » and )
CALENDAR = URI.parse 'https://www.apache.org/foundation/board/calendar.html'
http = Net::HTTP.new(CALENDAR.host, CALENDAR.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
get = Net::HTTP::Get.new CALENDAR.request_uri
-$calendar = Nokogiri::HTML(http.request(get).body.gsub('»','»'))
+$calendar = Nokogiri::HTML(http.request(get).body.gsub('»','»').gsub(' ',' '))
# add some style
style = Nokogiri::XML::Node.new "style", $calendar
@@ -264,6 +262,8 @@
title.sub! 'TCL', 'Tcl'
title.sub! 'Orc', 'ORC'
title.sub! 'Steve', 'STeVe'
+ title.sub! 'Servicecomb', 'ServiceComb'
+ title.sub! 'Zest', 'Polygene'
title.sub! 'Openmeetings', 'OpenMeetings'
title.sub! 'Web services', 'Web Services'
title.sub! 'ASF Rep. for W3C', 'W3C Relations'
@@ -651,6 +651,7 @@
report.title.sub! 'Apache/TCL', 'Tcl'
report.title.sub! 'Orc', 'ORC'
report.title.sub! 'Steve', 'STeVe'
+ report.title.sub! 'Zest', 'Polygene'
report.title.sub! 'Openmeetings', 'OpenMeetings'
report.title.sub! 'Ace', 'ACE' # WHIMSY-31
diff --git a/tools/deliver.rb b/tools/deliver.rb
index d1dfe8f..d7f1543 100644
--- a/tools/deliver.rb
+++ b/tools/deliver.rb
@@ -8,9 +8,7 @@
MAIL_ROOT = '/srv/mail'
# get the message ID
-def self.getmid(message)
- # only search headers for MID
- hdrs = message[/\A(.*?)\r?\n\r?\n/m, 1] || ''
+def self.getmid(hdrs)
mid = hdrs[/^Message-ID:.*/i]
if mid =~ /^Message-ID:\s*$/i # no mid on the first line
# capture the next line and join them together
@@ -24,10 +22,13 @@
STDIN.binmode
mail = STDIN.read
+# only search headers for MID and List-ID etc
+hdrs = mail[/\A(.*?)\r?\n\r?\n/m, 1] || ''
+
# extract info
-dest = mail[/^List-Id: <(.*)>/, 1] || mail[/^Delivered-To.* (\S+)\s*$/, 1] || 'unknown'
+dest = hdrs[/^List-Id: <(.*)>/, 1] || hdrs[/^Delivered-To.* (\S+)\s*$/, 1] || 'unknown'
month = Time.now.strftime('%Y%m')
-hash = Digest::SHA1.hexdigest(getmid(mail) || mail)[0..9]
+hash = Digest::SHA1.hexdigest(getmid(hdrs) || mail)[0..9]
# build file name
file = "#{MAIL_ROOT}/#{dest[/^[-\w]+/]}/#{month}/#{hash}"
diff --git a/tools/iclasort.rb b/tools/iclasort.rb
index 6b2950d..ad7ca3a 100644
--- a/tools/iclasort.rb
+++ b/tools/iclasort.rb
@@ -1,4 +1,4 @@
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'whimsy/asf'
OFFICERS = ASF::SVN['officers']
diff --git a/tools/mboxhdr2csv.rb b/tools/mboxhdr2csv.rb
index c7ca8c9..c4fe5b9 100644
--- a/tools/mboxhdr2csv.rb
+++ b/tools/mboxhdr2csv.rb
@@ -7,7 +7,7 @@
# Count lines of text content in mail body, roughly attempting to
# count just new content (not automated, not > replies)
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'whimsy/asf'
require 'mail'
require 'csv'
diff --git a/tools/membersort.rb b/tools/membersort.rb
index 98df10d..bd7630b 100644
--- a/tools/membersort.rb
+++ b/tools/membersort.rb
@@ -1,6 +1,6 @@
# svn update and sort the members.txt file and show the differences
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'whimsy/asf'
FOUNDATION = ASF::SVN['foundation']
diff --git a/tools/moderationhelper.rb b/tools/moderationhelper.rb
index 742a007..e7b7720 100755
--- a/tools/moderationhelper.rb
+++ b/tools/moderationhelper.rb
@@ -1,5 +1,5 @@
#!/usr/bin/env ruby
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
=begin
APP to generate the correct ezmlm syntax for moderators
diff --git a/tools/modify_pmcchairs.rb b/tools/modify_pmcchairs.rb
index ede42bd..69e3459 100755
--- a/tools/modify_pmcchairs.rb
+++ b/tools/modify_pmcchairs.rb
@@ -1,5 +1,5 @@
#!/usr/bin/env ruby
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
#
# add/remove people from PMC Chairs
diff --git a/tools/monthly_tidy.rb b/tools/monthly_tidy.rb
new file mode 100644
index 0000000..3394d03
--- /dev/null
+++ b/tools/monthly_tidy.rb
@@ -0,0 +1,26 @@
+#!/usr/bin/env ruby
+
+# @(#) monthly tidy-up script
+
+# Script to tidy up directories
+#
+# Deletes files older than 13 months from the following directories:
+# - /srv/mail/board
+# - /srv/mail/members
+
+require 'date'
+require 'fileutils'
+
+keep = (Date.today << 13).strftime('%Y%m')
+
+MAIL = '/srv/mail'
+
+Dir["#{MAIL}/board/20*", "#{MAIL}/members/20*"].each do |dir|
+ if File.basename(dir) < keep
+ begin
+ FileUtils.rm_r dir, :verbose => true
+ rescue => e
+ puts e
+ end
+ end
+end
diff --git a/tools/ponyapi.rb b/tools/ponyapi.rb
index 5c1f696..60519a4 100644
--- a/tools/ponyapi.rb
+++ b/tools/ponyapi.rb
@@ -35,8 +35,8 @@
File.open(File.join("#{dir}", 'lists.json'), "w") do |f|
begin
f.puts JSON.pretty_generate(lists)
- rescue JSON::GeneratorError
- puts "WARN:get_pony_lists() threw JSON::GeneratorError, continuing without pretty"
+ rescue JSON::GeneratorError => e
+ puts "WARN:get_pony_lists() #{e.message} #{e.backtrace[0]}, continuing without pretty"
f.puts lists
end
end
@@ -64,7 +64,7 @@
begin
f.puts JSON.pretty_generate(jzon)
rescue JSON::GeneratorError
- puts "WARN:get_pony_prefs(#{uri.request_uri}) threw JSON::GeneratorError, continuing without pretty"
+ puts "WARN:get_pony_prefs(#{uri.request_uri}) #{e.message} #{e.backtrace[0]}, continuing without pretty"
f.puts jzon
end
end
@@ -87,13 +87,18 @@
uri, request, response = fetch_pony(PONYSTATS % args, cookie)
if response.code == '200' then
File.open(File.join(dir, STATSMBOX % args), "w") do |f|
- jzon = JSON.parse(response.body)
begin
- f.puts JSON.pretty_generate(jzon)
- rescue JSON::GeneratorError
- puts "WARN:get_pony_stats(#{uri.request_uri}) threw JSON::GeneratorError, continuing without pretty"
- f.puts jzon
- end
+ f.puts JSON.pretty_generate(JSON.parse(response.body))
+ rescue JSON::JSONError
+ begin
+ # If JSON threw error, try again forcing to UTF-8 (may lose data)
+ jzon = JSON.parse(response.body.encode('UTF-8', :invalid => :replace, :undef => :replace))
+ f.puts JSON.fast_generate(jzon, {:max_nesting => false, :indent => ' '})
+ rescue JSON::JSONError => e
+ puts "WARN:get_pony_stats(#{uri.request_uri}) #{e.message} #{e.backtrace[0]}, continuing without pretty"
+ f.puts jzon
+ end
+ end
end
else
puts "ERROR:get_pony_stats(#{uri.request_uri}) returned code #{response.code.inspect}"
diff --git a/tools/ponypoop.rb b/tools/ponypoop.rb
index 55958d7..95612af 100755
--- a/tools/ponypoop.rb
+++ b/tools/ponypoop.rb
@@ -173,6 +173,10 @@
options[:mbox] = true
end
+ opts.on('-yYEAR', '--year YEAR', 'Only pull down single year, instead of 2010 thru now') do |y|
+ options[:year] = y
+ end
+
begin
opts.parse!
rescue OptionParser::ParseError => e
@@ -189,9 +193,12 @@
# Main method for command line use
if __FILE__ == $PROGRAM_NAME
months = %w( 1 2 3 4 5 6 7 8 9 10 11 12 )
- years = %w( 2010 2011 2012 2013 2014 2015 2016 2017 )
+ years = %w( 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 )
options = optparse
options[:list] ||= 'board'
+ if options[:year]
+ years = [ options[:year] ]
+ end
if options[:pull]
puts "BEGIN: Pulling down stats JSONs in #{options[:dir]} of list: #{options[:list]}@#{options[:subdomain]}"
PonyAPI::get_pony_stats_many options[:dir], options[:list], options[:subdomain], years, months, options[:cookie]
@@ -200,7 +207,7 @@
PonyAPI::get_pony_mbox_many options[:dir], options[:list], options[:subdomain], years, months, options[:cookie]
else
puts "BEGIN: Analyzing local JSONs in #{options[:dir]} of list: #{options[:list]}"
- run_analyze_stats options[:dir], options[:list], BOARD_REGEX
+ run_analyze_stats options[:dir], options[:list], 'board'.eql?(options[:list]) ? BOARD_REGEX : {}
end
puts "END: Thanks for running ponypoop - see results in #{options[:dir]}"
end
diff --git a/tools/proxyhelper.rb b/tools/proxyhelper.rb
index 460e1e6..2858dd0 100644
--- a/tools/proxyhelper.rb
+++ b/tools/proxyhelper.rb
@@ -3,7 +3,7 @@
# TODO Add function to email proxies with their info
# TODO Add function to cross-check irc log that all proxy/attendee were marked
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'whimsy/asf'
require 'mail'
diff --git a/tools/site-scan.rb b/tools/site-scan.rb
index 88cd3d4..234f070 100755
--- a/tools/site-scan.rb
+++ b/tools/site-scan.rb
@@ -6,7 +6,7 @@
# See Also: lib/whimsy/sitestandards.rb
#
# Makes no value judgements. Simply extracts raw data for offline analysis.
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'net/http'
require 'nokogiri'
require 'json'
diff --git a/tools/svnupdate.rb b/tools/svnupdate.rb
index 6890089..d50a41d 100644
--- a/tools/svnupdate.rb
+++ b/tools/svnupdate.rb
@@ -7,7 +7,8 @@
File.umask(0002)
-mail = Mail.new(STDIN.read.encode(crlf_newline: true))
+STDIN.binmode
+mail = Mail.new(STDIN.read)
LOG = '/srv/whimsy/www/logs/svn-update'
@@ -35,5 +36,17 @@
end
end
+elsif mail.subject =~ %r{^bills: r\d+ -( in)? /financials/Bills}
+
+ # prevent concurrent updates being performed by the cron job
+ File.open(LOG, File::RDWR|File::CREAT, 0644) do |log|
+ log.flock(File::LOCK_EX)
+
+ Dir.chdir '/srv/svn/Bills' do
+ `svn cleanup`
+ `svn update`
+ end
+ end
+
end
diff --git a/tools/testmail.rb b/tools/testmail.rb
old mode 100644
new mode 100755
index 3e38571..33680cd
--- a/tools/testmail.rb
+++ b/tools/testmail.rb
@@ -1,3 +1,4 @@
+#!/usr/bin/env ruby
#
# Test the ability to send email to non-apache.org email addresses
#
@@ -8,7 +9,7 @@
# Note: this will send an email to THAT user.
#
-$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'whimsy/asf'
require 'mail'
require 'etc'
diff --git a/tools/toccomments.sh b/tools/toccomments.sh
index 021f1cb..d62deb6 100755
--- a/tools/toccomments.sh
+++ b/tools/toccomments.sh
@@ -3,7 +3,8 @@
# Fix incorrectly wrapped comments in Incubator ToC section
# Intended for use on archived agendas and published minutes.
-# Look for Comments: preceeded by non-space
-ruby -p -i -e 'gsub(/(\S)\s+(Comments:)/,"\\1\n \\2")' "$@"
+# Look for Comments: preceeded by non-space if line contains e.g. [ ](batchee)
+# N.B. there are some minutes with '[](...)' and some with '[ ] (..)'
+ruby -p -i -e 'gsub(/(\S)\s+(Comments:)/,"\\1\n \\2") if /^ +\[.?\] ?\(\S+\) /' "$@"
# no need to save original files as tool is intended for use with files in SVN/Git
-echo "Done; the updated files can be diffed/checked in as required"
\ No newline at end of file
+echo "Done; the updated files can be diffed/checked in as required"
diff --git a/tools/travis-relay.rb b/tools/travis-relay.rb
index 1d6ff9f..f6399c2 100644
--- a/tools/travis-relay.rb
+++ b/tools/travis-relay.rb
@@ -10,7 +10,7 @@
munge = %w(received delivered-to return-path)
skip = %w(content-type content-transfer-encoding)
-$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'mail'
require 'whimsy/asf'
diff --git a/tools/update_chairs.rb b/tools/update_chairs.rb
new file mode 100755
index 0000000..5a2ba82
--- /dev/null
+++ b/tools/update_chairs.rb
@@ -0,0 +1,88 @@
+#!/usr/bin/env ruby
+
+# @(#) Script to update foundation/index.txt list of chars from committee-info.json
+
+# Must be run locally at present, and the changes checked in manually
+
+$LOAD_PATH.unshift '/srv/whimsy/lib'
+
+require 'json'
+require 'open-uri'
+require 'whimsy/asf'
+
+cttees=JSON.parse(open('https://whimsy.apache.org/public/committee-info.json').read)['committees']
+chairs={}
+cttees.reject{|k,v| v['pmc'] == false}.each do|k,v|
+ cttee=v['display_name']
+ chairs[cttee]=v['chair'].first[1]['name']
+end
+
+idx=File.join(ASF::SVN['site-root'],'foundation','index.mdtext')
+
+puts "Updating #{idx} to latest copy"
+puts `svn update #{idx}`
+
+puts "Checking if any changes are needed"
+lines=[]
+first=nil # first line of chairs
+last=nil
+changes=[]
+seen=Hash.new{|h,k| h[k]=0}
+open(idx).each_line do |l|
+# | V.P., Apache Xalan | A. N. Other |
+ m = l.match %r{^\| V.P., \[?Apache (.+?)(\]\(.+?\))? \| (.+?) \|}
+ if m
+ first ||= lines.length
+ last = lines.length
+ name,_,webchair = m.captures
+ cichair = chairs[name]
+ seen[name] += 1
+ unless cichair
+ puts "Cannot find CI entry for #{name}; dropping entry"
+ changes << name
+ next
+ end
+ if seen[name] > 1
+ puts "Duplicate entry for #{name}; dropping"
+ changes << name
+ next
+ end
+ unless webchair == cichair
+ puts "Changing chair for #{name} from #{webchair} to #{cichair}"
+ lines << l.sub(webchair,cichair)
+ changes << name
+ next
+ end
+ end
+ lines << l
+end
+
+notseen = (chairs.keys - seen.keys).sort
+if notseen.length > 0
+ puts "No entry found for: " + notseen.join(',')
+ notseen.each do |e|
+ puts "Adding #{e}"
+ lines.insert last, "| V.P., Apache #{e} | #{chairs[e]} |\n"
+ changes << e
+ end
+ # N.B. Cannot use sort! on slice
+ lines[first..last+notseen.length] = lines[first..last+notseen.length].sort_by do |l|
+ l.match(/Apache +([^\]|]+)/) do |m|
+ $1.downcase.gsub(' ','')
+ end
+ end
+end
+
+
+if changes.length > 0
+ puts "Updating the file"
+ File.open(idx,"w") do |f|
+ lines.each {|line| f.print line}
+ end
+ puts "#{idx} was updated; check the diffs:"
+ puts `svn diff #{idx}`
+ puts "Copy/paste the next line to commit the change:"
+ puts "svn ci -m'Changed chairs for: #{changes.join(',')}' #{idx}"
+else
+ puts "#{idx} not changed"
+end
\ No newline at end of file
diff --git a/tools/vhosttest.rb b/tools/vhosttest.rb
index d43cd11..251435b 100644
--- a/tools/vhosttest.rb
+++ b/tools/vhosttest.rb
@@ -3,7 +3,7 @@
# preprocess_vhosts.rb puppet macro
#
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'whimsy/asf'
IP = ASF::Git['infrastructure-puppet']
diff --git a/tools/wwwdocs.rb b/tools/wwwdocs.rb
index e85c408..79a3b09 100755
--- a/tools/wwwdocs.rb
+++ b/tools/wwwdocs.rb
@@ -1,6 +1,8 @@
#!/usr/bin/env ruby
-# Scan all /www scripts for WVisible PAGETITLE and categories
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../lib', __FILE__))
+# Utility function to scan various scripts
+# Docs: for WVisible PAGETITLE and categories in .cgi
+# Repos: for ASF::SVN access in .cgi|rb
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'whimsy/asf'
SCANDIR = "../www"
ISERR = '!'
@@ -12,6 +14,9 @@
'ASF Secretarial Team' => 'text-danger'
}
AUTHPUBLIC = 'glyphicon-eye-open'
+IS_PRIVATE = /\A(private|infra\/infrastructure)/
+ASFSVN = 'ASF::SVN'
+SCANDIRSVN = "../"
# Return [PAGETITLE, [cat,egories] ] after WVisible; or same as !Bogosity error
def scan_file(f)
@@ -75,3 +80,55 @@
auth = get_auth()
return annotate_scan(scan, auth)
end
+
+# Read repository.yml for all :svn dirs names to scan for
+# @return [['private1', 'privrepo2', ...], ['public1', 'pubrepo2', ...]
+def read_repository(repofile)
+ svn = YAML.load_file(repofile)[:svn]
+ repos = [[], []]
+ svn.each do |repo, data|
+ data['url'] =~ IS_PRIVATE ? repos[0] << repo : repos[1] << repo
+ end
+ return repos
+end
+
+# Build a regex union from ASFSVN and an array
+# @return Regexp.union(r...)
+def build_regexp(list)
+ r = []
+ list.each do |itm|
+ r << "#{ASFSVN}\['#{itm}']"
+ end
+ return Regexp.union(r)
+end
+
+# Scan file for use of ASF::SVN (private or public)
+# @return [["x = ASF::SVN['Meetings'] # Whole line private repo", ...], [] ]
+def scan_file_svn(f, regexs)
+ repos = [[], []]
+ begin
+ File.open(f).each_line.map(&:chomp).each do |line|
+ if line =~ regexs[0] then
+ repos[0] << line.strip
+ elsif line =~ regexs[1] then
+ repos[1] << line.strip
+ end
+ end
+ return repos
+ rescue Exception => e
+ return [["#{ISERR}Bogosity! #{e.message[0..255]}", "\t#{e.backtrace.join("\n\t")}"],[]]
+ end
+end
+
+# Scan directory for use of ASF::SVN (private or public)
+# @return { file: [['private line'], []] }
+def scan_dir_svn(dir, regexs)
+ links = {}
+ Dir["#{dir}/**/*.{cgi,rb}".untaint].each do |f|
+ l = scan_file_svn(f.untaint, regexs)
+ if (l[0].length + l[1].length) > 0
+ links[f.sub(dir, '')] = l
+ end
+ end
+ return links
+end
diff --git a/www/apmail/mods.cgi b/www/apmail/mods.cgi
index 992be02..c7371c5 100755
--- a/www/apmail/mods.cgi
+++ b/www/apmail/mods.cgi
@@ -1,10 +1,12 @@
#!/usr/bin/env ruby
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'shellwords'
require 'whimsy/asf'
-# TODO: determine apmail@hermes,wheel@hermes gorup membership
+# TODO: determine apmail@hermes,wheel@hermes group membership
+# TODO: .members. checks are obsolete, but this script does not appear to be usable anyway
+# as apmail and wheel don't exist, nor does /home/apmail/subscriptions/mods
unless apmail.include? $USER or wheel.include? $USER
pmc = ENV['PATH_INFO'][/\/([-\w]+)\.apache\.org\//,1]
pmc &&= ASF::Committee.find(pmc)
diff --git a/www/board/agenda/Gemfile b/www/board/agenda/Gemfile
index 5523355..c6e5087 100644
--- a/www/board/agenda/Gemfile
+++ b/www/board/agenda/Gemfile
@@ -13,7 +13,7 @@
gem 'rake'
gem 'wunderbar'
-gem 'ruby2js'
+gem 'ruby2js', '>= 3.0.15'
gem 'sinatra', '~> 2.0'
gem 'nokogumbo'
gem 'execjs', ('<2.5.1' if RUBY_VERSION =~ /^1/)
diff --git a/www/board/agenda/Rakefile b/www/board/agenda/Rakefile
index 975c527..2e45ae3 100644
--- a/www/board/agenda/Rakefile
+++ b/www/board/agenda/Rakefile
@@ -1,4 +1,4 @@
-$LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__)
+$LOAD_PATH.unshift '/srv/whimsy/lib'
# Remove world writable directories that Travis may insert into the PATH,
# as these cause security errors during testing
diff --git a/www/board/agenda/bin/remind-cronjob.rb b/www/board/agenda/bin/remind-cronjob.rb
index ad9fef2..dc1243b 100644
--- a/www/board/agenda/bin/remind-cronjob.rb
+++ b/www/board/agenda/bin/remind-cronjob.rb
@@ -9,7 +9,7 @@
Dir.chdir File.expand_path('../..', __FILE__)
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../../../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'whimsy/asf/agenda'
require 'mail'
require 'listen'
diff --git a/www/board/agenda/models/pending.rb b/www/board/agenda/models/pending.rb
index 3c2c3be..f71db3a 100644
--- a/www/board/agenda/models/pending.rb
+++ b/www/board/agenda/models/pending.rb
@@ -12,7 +12,7 @@
# reset pending when agenda changes
if agenda and agenda > response['agenda'].to_s
- response = {'agenda' => agenda}
+ response = {'agenda' => agenda, 'initials' => response['initials']}
end
# provide empty defaults
diff --git a/www/board/agenda/public/stylesheets/app.css b/www/board/agenda/public/stylesheets/app.css
index aa1e1a7..06793f5 100644
--- a/www/board/agenda/public/stylesheets/app.css
+++ b/www/board/agenda/public/stylesheets/app.css
@@ -548,3 +548,15 @@
font-family: monospace;
overflow: hidden;
}
+
+#email-form input {
+ display: inline;
+ width: 80%;
+ padding-right: 5px;
+ padding-left: 5px;
+}
+
+.reminder .modal-content button {
+ margin-left: 0.5em;
+ margin-bottom: 0em;
+}
diff --git a/www/board/agenda/routes.rb b/www/board/agenda/routes.rb
index cf6f540..2066945 100755
--- a/www/board/agenda/routes.rb
+++ b/www/board/agenda/routes.rb
@@ -54,7 +54,26 @@
# redirect missing to missing page for the latest agenda
get '/missing' do
agenda = dir('board_agenda_*.txt').sort.last
- pass unless agenda # is this correct?
+ pass unless agenda # this will result in a 404
+
+ # Support for sending out reminders before the agenda is created.
+ # Useful in cases where the agenda creation is delayed due to
+ # a board election.
+ if agenda < Date.today.strftime('board_agenda_%Y_%m_%d.txt')
+ # update in memory cache with a dummy agenda. The only relevant
+ # part of the agenda that matters for this operation is the list
+ # of pmcs (@pmcs).
+ template = File.read('templates/agenda.erb')
+ @meeting = ASF::Board.nextMeeting
+ agenda = @meeting.strftime('board_agenda_%Y_%m_%d.txt')
+ @directors = ['TBD']
+ @minutes = []
+ @owner = ASF::Board::ShepherdStream.new
+ @pmcs = ASF::Board.reporting(@meeting)
+ contents = Erubis::Eruby.new(template).result(binding)
+ Agenda.update_cache(agenda, nil, contents, true)
+ end
+
response.headers['Location'] =
"#{agenda[/\d+_\d+_\d+/].gsub('_', '-')}/missing"
status 302
@@ -107,7 +126,7 @@
Dir[*months.map {|month| "#{month}/*"}].each do |file|
next unless File.mtime(file) > start
raw = File.read(file).force_encoding(Encoding::BINARY)
- next unless raw =~ /Subject: .*Board feedback on 2017-05-17 (.*) report/
+ next unless raw =~ /Subject: .*Board feedback on #{date} (.*) report/
followup[$1][:count] += 1 if followup[$1]
end
diff --git a/www/board/agenda/spec/reflow_spec.rb b/www/board/agenda/spec/reflow_spec.rb
index 56199ec..d494d47 100644
--- a/www/board/agenda/spec/reflow_spec.rb
+++ b/www/board/agenda/spec/reflow_spec.rb
@@ -37,5 +37,23 @@
expect(page.body).to eq "a?\nb:\nc"
end
+
+ it "leaves long URLs alone" do
+ @line = "[7] http://example.com" + "/foobar" * 12
+
+ on_vue_server do
+ line = @line
+
+ class TestReflow < Vue
+ def render
+ _ Flow.text(line)
+ end
+ end
+
+ Vue.renderResponse(TestReflow, response)
+ end
+
+ expect(page.body).to eq @line
+ end
end
end
diff --git a/www/board/agenda/spec/secretary_spec.rb b/www/board/agenda/spec/secretary_spec.rb
index be589c5..0e9c0b1 100644
--- a/www/board/agenda/spec/secretary_spec.rb
+++ b/www/board/agenda/spec/secretary_spec.rb
@@ -6,7 +6,7 @@
feature 'report' do
before :each do
- page.driver.header 'REMOTE_USER', 'clr'
+ page.driver.header 'REMOTE_USER', 'mattsicker' # must be a non-director member of the secretarial team
end
it "should allow timestamps to be edited" do
diff --git a/www/board/agenda/templates/agenda.erb b/www/board/agenda/templates/agenda.erb
index e33658a..73093f9 100644
--- a/www/board/agenda/templates/agenda.erb
+++ b/www/board/agenda/templates/agenda.erb
@@ -45,8 +45,6 @@
Ross Gardler
Tom Pappas
Sam Ruby
- Daniel Ruggeri
- Craig L Russell
Matt Sicker
Ulrich Stärk
@@ -58,6 +56,7 @@
Jake Farrell
Daniel Gruno
+ Sally Khudairi
Kevin A. McGrail
Greg Stein
@@ -90,7 +89,7 @@
C. Treasurer [Ulrich]
- D. Secretary [Craig]
+ D. Secretary [Matt]
E. Executive Vice President [Ross]
diff --git a/www/board/agenda/templates/establish.erb b/www/board/agenda/templates/establish.erb
index b8e8681..a4536d0 100644
--- a/www/board/agenda/templates/establish.erb
+++ b/www/board/agenda/templates/establish.erb
@@ -9,8 +9,8 @@
pursuant to Bylaws of the Foundation; and be it further
RESOLVED, that the Apache Apache <%= @pmcname %> be and hereby is responsible
-for the creation and maintenance of software related to large scale code
-license analysis, auditing and reporting; and be it further
+for the creation and maintenance of software related to <%= @description %>;
+and be it further
RESOLVED, that the office of "Vice President, Apache <%= @pmcname %>" be and
hereby is created, the person holding such office to serve at the direction of
diff --git a/www/board/agenda/views/actions/email.json.rb b/www/board/agenda/views/actions/email.json.rb
new file mode 100644
index 0000000..5d208b5
--- /dev/null
+++ b/www/board/agenda/views/actions/email.json.rb
@@ -0,0 +1,28 @@
+#
+# send email
+#
+
+ASF::Mail.configure
+
+# extract values for each field
+to, cc, subject, body = @to, @cc, @subject, @body
+
+# construct from address
+sender = ASF::Person.find(env.user)
+from = "#{sender.public_name.inspect} <#{sender.id}@apache.org>".untaint
+
+# construct email
+mail = Mail.new do
+ from from
+ to to
+ cc cc if cc and not cc.empty?
+ subject subject
+
+ body body
+end
+
+# deliver mail
+mail.deliver!
+
+# return email in the response
+{mail: mail.to_s}
diff --git a/www/board/agenda/views/actions/post-data.json.rb b/www/board/agenda/views/actions/post-data.json.rb
index d8c9a99..1ba30f7 100644
--- a/www/board/agenda/views/actions/post-data.json.rb
+++ b/www/board/agenda/views/actions/post-data.json.rb
@@ -7,7 +7,7 @@
# debugging support: enable script to be run from the command line
if $0 == __FILE__
- $LOAD_PATH.unshift File.realpath(File.expand_path('../'*6 + 'lib', __FILE__))
+ $LOAD_PATH.unshift '/srv/whimsy/lib'
Dir.chdir File.expand_path('../..', __dir__)
require './helpers/string'
require 'whimsy/asf'
diff --git a/www/board/agenda/views/actions/post.json.rb b/www/board/agenda/views/actions/post.json.rb
index 9a2ee84..e59cb78 100644
--- a/www/board/agenda/views/actions/post.json.rb
+++ b/www/board/agenda/views/actions/post.json.rb
@@ -89,7 +89,7 @@
agenda[/^()-+\nEnd of agenda/, 1] =
"-----------------------------------------\n" +
"Attachment #{attach}: Report from the Apache #{pmc.display_name} " +
- "Project [#{pmc.chair.public_name}]\n" +
+ "Project [#{pmc.chair.public_name}]\n\n" +
"#{@report.strip}\n\n"
else
diff --git a/www/board/agenda/views/app.js.rb b/www/board/agenda/views/app.js.rb
index 41d5b82..2046d34 100644
--- a/www/board/agenda/views/app.js.rb
+++ b/www/board/agenda/views/app.js.rb
@@ -22,6 +22,7 @@
require_relative 'pages/search'
require_relative 'pages/comments'
require_relative 'pages/help'
+require_relative 'pages/secrets'
require_relative 'pages/shepherd'
require_relative 'pages/queue'
require_relative 'pages/flagged'
diff --git a/www/board/agenda/views/buttons/email.js.rb b/www/board/agenda/views/buttons/email.js.rb
index 3ae3a7b..d38008f 100644
--- a/www/board/agenda/views/buttons/email.js.rb
+++ b/www/board/agenda/views/buttons/email.js.rb
@@ -3,9 +3,15 @@
#
class Email < Vue
+ def initialize
+ @email = {}
+ end
+
def render
_button.btn 'send email', class: self.mailto_class(),
onClick: self.launch_email_client
+
+ _EmailForm email: @email, id: @@item.mail_list
end
# render 'send email' as a primary button if the viewer is the shepherd for
@@ -31,7 +37,7 @@
end
# launch email client, pre-filling the destination, subject, and body
- def launch_email_client()
+ def launch_email_client(event)
mail_list = @@item.mail_list
mail_list = "private@#{mail_list}.apache.org" unless mail_list.include? '@'
@@ -77,8 +83,66 @@
end
end
- window.location = "mailto:#{to}?cc=#{cc}" +
- "&subject=#{encodeURIComponent(subject)}" +
- "&body=#{encodeURIComponent(body)}"
+ if event.ctrlKey or event.shiftKey or event.metaKey
+ @email = {
+ to: to,
+ cc: cc,
+ subject: subject,
+ body: body
+ }
+
+ jQuery('#email-' + @@item.mail_list).modal(:show)
+ else
+ window.location = "mailto:#{to}?cc=#{cc}" +
+ "&subject=#{encodeURIComponent(subject)}" +
+ "&body=#{encodeURIComponent(body)}"
+ end
+ end
+end
+
+class EmailForm < Vue
+ def render
+ _ModalDialog color: 'commented', id: 'email-' + @@id do
+ _h4 "Send email - #{@@email.subject}"
+
+ # input field: to
+ _div.form_group.row do
+ _label.col_sm_2 'To', for: 'email-to'
+ _input.col_sm_10.email_to! placeholder: "destination email address",
+ disabled: @disabled, value: @@email.to
+ end
+
+ # input field: cc
+ _div.form_group.row do
+ _label.col_sm_2 'CC', for: 'email-cc'
+ _input.col_sm_10.email_cc! placeholder: "cc list", disabled: @disabled,
+ value: @@email.cc
+ end
+
+ # input field: subject
+ _div.form_group.row do
+ _label.col_sm_2 'Subject', for: 'email-subject'
+ _input.col_sm_10.email_subject! placeholder: "email subject",
+ disabled: @disabled, value: @@email.subject
+ end
+
+ # input field: body
+ _textarea.email_body! label: 'Body', placeholder: "email text",
+ disabled: @disabled, value: @@email.body, rows: 10
+
+ _button.btn_default 'Cancel', type: 'button', data_dismiss: 'modal'
+ _button.btn_primary 'Send', type: 'button', onClick: self.send,
+ disabled: @disabled
+ end
+ end
+
+ def send(event)
+ @disabled = true
+ post 'email', @@email do |response|
+ console.log response
+ @disabled = false
+ jQuery('#email-' + @@id).modal(:hide)
+ document.body.classList.remove('modal-open')
+ end
end
end
diff --git a/www/board/agenda/views/buttons/timestamp.js.rb b/www/board/agenda/views/buttons/timestamp.js.rb
index ca3cb5c..e4bdb5e 100644
--- a/www/board/agenda/views/buttons/timestamp.js.rb
+++ b/www/board/agenda/views/buttons/timestamp.js.rb
@@ -22,6 +22,7 @@
post 'minute', data do |minutes|
@disabled = false
Minutes.load minutes
+ Todos.load() if Minutes.complete
end
end
end
diff --git a/www/board/agenda/views/models/agenda.js.rb b/www/board/agenda/views/models/agenda.js.rb
index 4f01956..a28898a 100644
--- a/www/board/agenda/views/models/agenda.js.rb
+++ b/www/board/agenda/views/models/agenda.js.rb
@@ -246,7 +246,7 @@
if @attach =~ /^[A-Z]+$/
Agenda.index.each do |item|
- items << item if item.attach =~ /^7/ and item.roster == @roster
+ items << item if item.attach =~ /^7\w/ and item.roster == @roster
end
end
diff --git a/www/board/agenda/views/pages/adjournment.js.rb b/www/board/agenda/views/pages/adjournment.js.rb
index 99f0e7d..578c44e 100644
--- a/www/board/agenda/views/pages/adjournment.js.rb
+++ b/www/board/agenda/views/pages/adjournment.js.rb
@@ -142,8 +142,9 @@
end
# fetch secretary todos once the minutes are complete
- def mounted()
- if Minutes.complete and Todos.loading and not Todos.fetched
+ def load()
+ if Minutes.complete and not Todos.fetched
+ Todos.loading = true
Todos.fetched = true
retrieve "secretary-todos/#{Agenda.title}", :json do |todos|
Todos.set todos
@@ -151,6 +152,10 @@
end
end
end
+
+ def mounted()
+ self.load() if Todos.loading
+ end
end
class PMCActions < Vue
diff --git a/www/board/agenda/views/pages/help.js.rb b/www/board/agenda/views/pages/help.js.rb
index e26eb75..4d64506 100644
--- a/www/board/agenda/views/pages/help.js.rb
+++ b/www/board/agenda/views/pages/help.js.rb
@@ -61,6 +61,9 @@
end
end
end
+
+ _br
+ _Link text: 'Insider Secrets', href: 'secrets'
end
def setRole(event)
diff --git a/www/board/agenda/views/pages/secrets.js.rb b/www/board/agenda/views/pages/secrets.js.rb
new file mode 100644
index 0000000..3d49b3e
--- /dev/null
+++ b/www/board/agenda/views/pages/secrets.js.rb
@@ -0,0 +1,37 @@
+class InsiderSecrets < Vue
+ def render
+ _p %q(
+ Following are some of the less frequently used features that aren't
+ prominently highlighted by the UI, but you might find useful.
+ )
+
+ _ul do
+ _li { _p %q(
+ Want to reflow only part of a report so as to not mess with the
+ formatting of a table or other pre-formatted text? Select the
+ lines you want to adjust using your mouse before pressing the
+ reflow button.
+ ) }
+
+ _li { _p %q(
+ Want to not use your email client for whatever reason? Press
+ shift before you click a 'send email' button and a form will
+ drop down that you can use instead.
+ ) }
+
+ _li { _p %q(
+ Action items have both a status (which is either shown with a red
+ background if no update has been made or a white background if
+ a status has been provided), and a PMC name. The background of the
+ later is either grey if this PMC is not reporting this month, or
+ a link to the report itself, and the color of the link is the color
+ associated with the report (green if preapproved, red if flagged,
+ etc.). So generally if you see an action item to "pursue a report
+ for..." and the link is green, you can confidently mark that action as
+ complete.
+ ) }
+ end
+ end
+
+ _Link text: 'Back to the agenda', href: '.'
+end
diff --git a/www/board/agenda/views/router.js.rb b/www/board/agenda/views/router.js.rb
index 729f518..15cc195 100644
--- a/www/board/agenda/views/router.js.rb
+++ b/www/board/agenda/views/router.js.rb
@@ -88,6 +88,9 @@
# Progressive Web Application 'Add to Home Screen' support
item.buttons = [{button: Install}] if PageCache.installPrompt
+ elsif path == 'secrets'
+ item = {view: InsiderSecrets}
+
elsif path == 'bootstrap.html'
item = {view: BootStrapPage, title: ' '}
diff --git a/www/board/agenda/views/utils.js.rb b/www/board/agenda/views/utils.js.rb
index 7bead82..cc90386 100644
--- a/www/board/agenda/views/utils.js.rb
+++ b/www/board/agenda/views/utils.js.rb
@@ -57,7 +57,11 @@
elsif xhr.response.exception
message = "Exception\n#{xhr.response.exception}"
else
- message = "Exception\n#{JSON.parse(xhr.responseText).exception}"
+ begin
+ message = "Exception\n#{JSON.parse(xhr.responseText).exception}"
+ rescue
+ message = "Exception\n#{xhr.responseText}"
+ end
end
console.log(message)
@@ -184,12 +188,16 @@
gsub(/(.{1,#{len}})( +|$\n?)/, "$1\n").
sub(/[\n\r]+$/, '')
else
- # preserve indentation.
- n = len - prefix.length;
- indent = prefix.gsub(/\S/, ' ')
- lines[i] = prefix + line[prefix.length..-1].
- gsub(/(.{1,#{n}})( +|$\n?)/, indent + "$1\n").
- sub(indent, '').sub(/[\n\r]+$/, '')
+ # ensure line can be split after column 40
+ lastspace = /^.*\s\S/.exec(line)
+ if lastspace and lastspace[0].length - 1 > 40
+ # preserve indentation.
+ n = len - prefix.length;
+ indent = prefix.gsub(/\S/, ' ')
+ lines[i] = prefix + line[prefix.length..-1].
+ gsub(/(.{1,#{n}})( +|$\n?)/, indent + "$1\n").
+ sub(indent, '').sub(/[\n\r]+$/, '')
+ end
end
end
diff --git a/www/board/missing-reports.cgi b/www/board/missing-reports.cgi
index e13c097..28a6eea 100755
--- a/www/board/missing-reports.cgi
+++ b/www/board/missing-reports.cgi
@@ -1,6 +1,6 @@
#!/usr/bin/env ruby
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'whimsy/asf/agenda'
records = 'http://www.apache.org/foundation/records/minutes/'
diff --git a/www/board/posted-reports.cgi b/www/board/posted-reports.cgi
index f2e3de7..4db5b5d 100755
--- a/www/board/posted-reports.cgi
+++ b/www/board/posted-reports.cgi
@@ -1,6 +1,6 @@
#!/usr/bin/env ruby
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'date'
require 'mail'
require 'wunderbar'
diff --git a/www/board/publish_minutes.cgi b/www/board/publish_minutes.cgi
index 8cbf491..aaf5597 100755
--- a/www/board/publish_minutes.cgi
+++ b/www/board/publish_minutes.cgi
@@ -1,6 +1,6 @@
#!/usr/bin/env ruby
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'wunderbar'
require 'date'
require 'whimsy/asf'
diff --git a/www/board/subscriptions.cgi b/www/board/subscriptions.cgi
index d0d6f0a..690c6af 100755
--- a/www/board/subscriptions.cgi
+++ b/www/board/subscriptions.cgi
@@ -1,24 +1,15 @@
#!/usr/bin/env ruby
PAGETITLE = "Board@ List CrossCheck - PMC Chairs" # Wvisible:board,mail
-$LOAD_PATH.unshift File.expand_path('../../../lib', __FILE__)
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'wunderbar'
require 'wunderbar/bootstrap'
require 'whimsy/asf'
require 'whimsy/asf/mlist'
-ARCHIVERS = %w(
- private@mbox-vm.apache.org
- board-archive@apache.org
- archive-asf-private@cust-asf.ponee.io
- board@mmpoc.apache.org
- board@whimsy-vm4.apache.org
- svnupdate@whimsy-vm4.apache.org
-)
-
info_chairs = ASF::Committee.load_committee_info.group_by(&:chair)
ldap_chairs = ASF.pmc_chairs
-subscribers, modtime = ASF::MLIST.board_subscribers
+subscribers, modtime = ASF::MLIST.board_subscribers(false) # excluding archivers
_html do
_body? do
@@ -95,7 +86,6 @@
end
_tbody do
ids.sort.each do |id, person, email|
- next if ARCHIVERS.include? email
_tr_ do
href = "/roster/committer/#{id}"
if person.asf_member?
diff --git a/www/brand/list.cgi b/www/brand/list.cgi
index dee53a3..5bc18fd 100755
--- a/www/brand/list.cgi
+++ b/www/brand/list.cgi
@@ -4,7 +4,7 @@
# return output in JSON format if the query string includes 'json'
ENV['HTTP_ACCEPT'] = 'application/json' if ENV['QUERY_STRING'].include? 'json'
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'csv'
require 'json'
require 'whimsy/asf'
diff --git a/www/brand/replyedit.cgi b/www/brand/replyedit.cgi
index e2332ed..b4fc49a 100755
--- a/www/brand/replyedit.cgi
+++ b/www/brand/replyedit.cgi
@@ -1,6 +1,6 @@
#!/usr/bin/env ruby
PAGETITLE = "DEMO: proposed UI for editing a response to question from boilerplate"
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'whimsy/asf'
require 'wunderbar'
require 'wunderbar/bootstrap'
diff --git a/www/brand/replylist.cgi b/www/brand/replylist.cgi
index 1caa1df..b854be2 100755
--- a/www/brand/replylist.cgi
+++ b/www/brand/replylist.cgi
@@ -1,6 +1,6 @@
#!/usr/bin/env ruby
PAGETITLE = "DEMO: proposed UI for mailing list view for reply features"
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'whimsy/asf'
require 'wunderbar'
require 'wunderbar/bootstrap'
diff --git a/www/brand/replyui.cgi b/www/brand/replyui.cgi
index 1786ca1..16a679c 100755
--- a/www/brand/replyui.cgi
+++ b/www/brand/replyui.cgi
@@ -1,6 +1,6 @@
#!/usr/bin/env ruby
PAGETITLE = "DEMO: proposed UI for popup/dialog to choose a reply boilerplate"
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'whimsy/asf'
require 'wunderbar'
require 'wunderbar/bootstrap'
diff --git a/www/committers/index.cgi b/www/committers/index.cgi
new file mode 100755
index 0000000..1d8378a
--- /dev/null
+++ b/www/committers/index.cgi
@@ -0,0 +1,61 @@
+#!/usr/bin/env ruby
+PAGETITLE = "Overview of Whimsy Tools for Committers" # Wvisible:tools
+
+$LOAD_PATH.unshift '/srv/whimsy/lib'
+require 'json'
+require 'whimsy/asf'
+require 'wunderbar'
+require 'wunderbar/bootstrap'
+
+MISC = {
+ 'tools.cgi' => "Listing of all available Whimsy tools",
+ 'subscribe.cgi' => "Subscribe or unsubscribe from mailing lists",
+ 'svn-info.cgi' => "Try some Subversion commands from the browser",
+ 'moderationhelper.cgi' => "Get help with mailing list moderation commands"
+}
+_html do
+ _body? do
+ _whimsy_body(
+ title: PAGETITLE,
+ subtitle: 'Committer-restricted tools only',
+ relatedtitle: 'More Useful Links',
+ related: {
+ "/committers/tools" => "Whimsy All Tools Listing",
+ "https://svn.apache.org/repos/private/committers/" => "Checkout the private 'committers' repo for Committers",
+ "https://github.com/apache/whimsy/blob/master/www#{ENV['SCRIPT_NAME']}" => "See This Source Code",
+ "mailto:dev@whimsical.apache.org?subject=[FEEDBACK] members/index idea" => "Email Feedback To dev@whimsical"
+ },
+ helpblock: -> {
+ _p %{
+ This script lists various Whimsy tools restricted to Committers. These all deal with private or
+ sensitive data, so be sure to keep confidential and do not share with non-committers.
+ }
+ _p do
+ _ 'More questions? See the '
+ _a '/dev developer info pages', href: 'https://www.apache.org/dev/'
+ _ ' or ask the '
+ _a 'Community Development PMC', href: 'https://community.apache.org/'
+ _ ' for pointers to everything Apache.'
+ end
+ },
+ breadcrumbs: {
+ members: '/committers/tools#members',
+ meeting: '/committers/tools#meeting'
+ }
+ ) do
+
+ _h2 "Useful Committer-only Tools (require login)"
+ _ul do
+ MISC.each do |url, desc|
+ _li do
+ _a desc, href: url
+ _ ' - '
+ _code! do
+ _a url, href: url
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/www/committers/ldap-map.cgi b/www/committers/ldap-map.cgi
index 8736a80..b3169b4 100755
--- a/www/committers/ldap-map.cgi
+++ b/www/committers/ldap-map.cgi
@@ -1,7 +1,7 @@
#!/usr/bin/env ruby
PAGETITLE = "Mapping Committer IDs In JIRA and Confluence" # Wvisible:tools
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'tmpdir'
require 'json'
require 'time'
diff --git a/www/committers/subscribe.cgi b/www/committers/subscribe.cgi
index d736215..a41cc7e 100755
--- a/www/committers/subscribe.cgi
+++ b/www/committers/subscribe.cgi
@@ -1,6 +1,6 @@
#!/usr/bin/env ruby
PAGETITLE = "ASF Mailing List Subscription Helper" # Wvisible:mail subscribe
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'wunderbar'
require 'wunderbar/bootstrap'
require 'mail'
@@ -39,6 +39,8 @@
pmcs = ASF::Committee.pmcs.map(&:mail_list)
ldap_pmcs = [] # No need to get the info for ASF members
ldap_pmcs = user.committees.map(&:mail_list) unless user.asf_member?
+# Also allow podling private lists to be subscribed by podling owners
+ldap_pmcs += user.podlings.map(&:mail_list) unless user.asf_member?
lists = ASF::Mail.cansub(user.asf_member?, ASF.pmc_chairs.include?(user), ldap_pmcs)
lists -= ASF::Mail.deprecated
lists -= BLACKLIST
@@ -70,9 +72,9 @@
_a 'https://id.apache.org/', href: "https://id.apache.org/details/#{$USER}"
_ 'where you can change your primary Forwarding Address and any other associated Alias email addresses you use.'
end
+ _p 'ASF members can use this form to subscribe to private lists. PMC chairs can subscribe to board lists. (P)PMC members can subscribe to their private@ list.'
+ _p 'The subscription request will be queued and should be processed within about an hour.'
_p do
- _ 'ASF members can use this form to subscribe to private lists. PMC chairs can subscribe to board lists. PMC members can subscribe to their private@ list.'
- _br
_ 'To subscribe to other private lists, send an email to the list-subscribe@ address and wait for the request to be manually approved.'
_ 'This might take a day or two.'
end
diff --git a/www/committers/svn-info.cgi b/www/committers/svn-info.cgi
index 63afa59..5ba4c40 100755
--- a/www/committers/svn-info.cgi
+++ b/www/committers/svn-info.cgi
@@ -1,6 +1,6 @@
#!/usr/bin/env ruby
PAGETITLE = "Subversion Info Helper" # Wvisible:tools svn
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'wunderbar'
require 'wunderbar/bootstrap'
require 'whimsy/asf'
diff --git a/www/committers/testauth.cgi b/www/committers/testauth.cgi
index 04c1c0b..f930cb7 100755
--- a/www/committers/testauth.cgi
+++ b/www/committers/testauth.cgi
@@ -4,7 +4,7 @@
# Small CGI to help debug board agenda authentication issues
#
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'wunderbar'
require 'wunderbar/bootstrap'
require 'whimsy/asf/rack'
@@ -19,18 +19,19 @@
'/status/' => 'Whimsy Server Status'
},
helpblock: -> {
- _ 'This script checks your authorization to use the agenda tool, and checks if you are listed as attending the current board meeting in the official agenda.'
+ _ 'This script checks your authorization to use the agenda tool, and checks if you are listed as attending the current board meeting in the upcoming official agenda.'
}
) do
FOUNDATION_BOARD = ASF::SVN['foundation_board']
- agenda = Dir[File.join(FOUNDATION_BOARD, 'board_agenda_*.txt')].sort.last.untaint
- agenda = ASF::Board::Agenda.parse(File.read(agenda))
+ agendafile = Dir[File.join(FOUNDATION_BOARD, 'board_agenda_*.txt')].sort.last.untaint
+ agenda = ASF::Board::Agenda.parse(File.read(agendafile))
roll = agenda.find {|item| item['title'] == 'Roll Call'}
person = ASF::Auth.decode(env)
+ _p %{ Your data for meeting: #{File.basename(agendafile)} }
_table do
_tr do
- _td 'User id'
+ _td 'Your id'
_td person.id
end
diff --git a/www/committers/tm-report.cgi b/www/committers/tm-report.cgi
index 4e71d55..c66e19f 100755
--- a/www/committers/tm-report.cgi
+++ b/www/committers/tm-report.cgi
@@ -1,6 +1,6 @@
#!/usr/bin/env ruby
PAGETITLE = "Apache Trademark Misuse Reporting Form"
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'wunderbar'
require 'wunderbar/bootstrap'
require 'wunderbar/jquery'
diff --git a/www/committers/tools.cgi b/www/committers/tools.cgi
index a2b9bd2..093f428 100755
--- a/www/committers/tools.cgi
+++ b/www/committers/tools.cgi
@@ -1,7 +1,7 @@
#!/usr/bin/env ruby
PAGETITLE = "Listing Of Whimsy Tools" # Wvisible:tools
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'json'
require 'whimsy/asf'
require 'wunderbar'
diff --git a/www/docs/hardcoded.cgi b/www/docs/hardcoded.cgi
new file mode 100755
index 0000000..767ecbb
--- /dev/null
+++ b/www/docs/hardcoded.cgi
@@ -0,0 +1,53 @@
+#!/usr/bin/env ruby
+PAGETITLE = "Hardcoded Data In Code" # Wvisible:tools data
+
+$LOAD_PATH.unshift '/srv/whimsy/lib'
+require 'json'
+require 'whimsy/asf'
+require 'wunderbar'
+require 'wunderbar/bootstrap'
+GITWHIMSY = 'https://github.com/apache/whimsy/blob/master/'
+HARDCODED = 'hardcoded.json'
+hclist = JSON.parse(File.read(HARDCODED))
+
+_html do
+ _body? do
+ _whimsy_body(
+ title: PAGETITLE,
+ related: {
+ "https://github.com/apache/whimsy/blob/master/DEVELOPMENT.md" => "Whimsy Dev Environment Setup",
+ "/public" => "Whimsy public JSON datafiles",
+ "/docs" => "Whimsy code/API developer documentation"
+ },
+ helpblock: -> {
+ _p %{ Whimsy tools integrate directly with a wide variety of
+ private and public data and processes within the ASF. Many
+ tools also hardcode lists or mappings of data that is
+ canonically stored elsewhere. This is a partial list.
+ }
+ _p %{ Many of these hardcoded lists are good things, and are
+ in the right part of the code. Some lists may turn out to
+ be better stored elsewhere, either in Whimsy or other repos.
+ }
+ }
+ ) do
+ _ul.list_group do
+ hclist.each do |file, info|
+ _li.list_group_item do
+ _a '', name: file.gsub(/[#%\[\]\{\}\\"<>]/, '')
+ _code! do
+ if info['line']
+ _a! file, href: "#{GITWHIMSY}#{file}#L#{info['line']}"
+ else
+ _a! file, href: "#{GITWHIMSY}#{file}"
+ end
+ _span.text_muted " #{info['symbol']}"
+ end
+ _br
+ _ " #{info['description']}"
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/www/docs/hardcoded.json b/www/docs/hardcoded.json
new file mode 100644
index 0000000..9f4ee56
--- /dev/null
+++ b/www/docs/hardcoded.json
@@ -0,0 +1,102 @@
+{
+ "tools/collate_minutes.rb": {
+ "line": "230",
+ "symbol": "",
+ "description": "List of common prefix phrases in resolution titles"
+ },
+ "lib/whimsy/asf/board.rb": {
+ "line": "107",
+ "symbol": "DIRECTOR_MAP",
+ "description": "Map of all director id/initial/names"
+ },
+ "lib/whimsy/asf/committee.rb": {
+ "line": "64",
+ "symbol": "@@aliases.merge!",
+ "description": "Map of PMC display names to shortnames: 'community development' => 'comdev'"
+ },
+ "lib/whimsy/asf/mail.rb": {
+ "line": "86",
+ "symbol": "",
+ "description": "Lists of special cased mailing list names and access"
+ },
+ "lib/whimsy/asf/mlist.rb": {
+ "line": "",
+ "symbol": "",
+ "description": "Various hardcoded logic for details of how lists are archived; some special cases that are related to technical details"
+ },
+ "lib/whimsy/asf/person.rb": {
+ "line": "",
+ "symbol": "",
+ "description": "Various collation bits for people's names; generic to unicode or name prefixes"
+ },
+ "lib/whimsy/asf/podling.rb": {
+ "line": "260",
+ "symbol": "dev_mail_list",
+ "description": "Development mailing list name associated with a given podling"
+ },
+ "lib/whimsy/asf/site.rb": {
+ "line": "",
+ "symbol": "",
+ "description": "Various ASF groups and their website homepage URLs"
+ },
+ "lib/whimsy/asf/agenda/special.rb": {
+ "line": "",
+ "symbol": "",
+ "description": "Various hardcoded strings dealing with board agenda items (historical and current)"
+ },
+ "lib/whimsy/asd/agenda/summary.rb": {
+ "line": "",
+ "symbol": "SKIP_AGENDAS",
+ "description": "defines map of F2F agendas (not traditionally parseable)"
+ },
+ "lib/whimsy/asf/person/override-dates.rb": {
+ "line": "",
+ "symbol": "@@create_date",
+ "description": "Corrected creation dates for accounts created before May 2009"
+ },
+ "tools/check_auth.rb": {
+ "line": "",
+ "symbol": "ROLE_NAMES",
+ "description": "hardcoded list of special non-LDAP names"
+ },
+ "tools/mboxhdr2csv.rb": {
+ "line": "",
+ "symbol": "",
+ "description": "Maps some tool-generated subject lines for different lists"
+ },
+ "tools/ponypoop.rb": {
+ "line": "",
+ "symbol": "",
+ "description": "Mapping some tool-generated subject lines for different lists"
+ },
+ "www/incubator/graduated.cgi": {
+ "line": "9",
+ "symbol": "parents",
+ "description": "List of parent PMCs for some podlings"
+ },
+ "www/members/proxy.cgi": {
+ "line": "",
+ "symbol": "volunteers",
+ "description": "List of volunteers for current member's meeting; update annually per proxies file"
+ },
+ "www/members/subscriptions.cgi": {
+ "line": "",
+ "symbol": "ARCHIVERS",
+ "description": "List of email addreses for private lists"
+ },
+ "www/roster/models/nonpmc.rb": {
+ "line": "10",
+ "symbol": "mail_list",
+ "description": "Map of a few odd mailing list names"
+ },
+ "www/secretary/memapp_check.cgi": {
+ "line": "45",
+ "symbol": "",
+ "description": "List of Member names with special cases for IDs"
+ },
+ "www/test/dataflow.json": {
+ "line": "",
+ "symbol": "",
+ "description": "List of common /public datafiles and explanations"
+ }
+}
diff --git a/www/docs/index.cgi b/www/docs/index.cgi
index f2ea1c1..273d5c7 100755
--- a/www/docs/index.cgi
+++ b/www/docs/index.cgi
@@ -1,7 +1,7 @@
#!/usr/bin/env ruby
PAGETITLE = "Apache Whimsy Code Documentation" # Wvisible:docs
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'json'
require 'whimsy/asf'
require 'wunderbar'
diff --git a/www/events/other.cgi b/www/events/other.cgi
index 59baceb..063542a 100755
--- a/www/events/other.cgi
+++ b/www/events/other.cgi
@@ -1,7 +1,7 @@
#!/usr/bin/env ruby
PAGETITLE = "Other FOSS Conference Listings" # Wvisible:events
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'json'
require 'net/http'
require 'whimsy/asf'
diff --git a/www/events/past.cgi b/www/events/past.cgi
index 5a114a2..e3c1a0a 100755
--- a/www/events/past.cgi
+++ b/www/events/past.cgi
@@ -1,7 +1,7 @@
#!/usr/bin/env ruby
PAGETITLE = "ApacheCon Historical Listing" # Wvisible:events,apachecon
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'csv'
require 'json'
require 'whimsy/asf'
diff --git a/www/events/talks.cgi b/www/events/talks.cgi
index 9899f28..a365bf5 100755
--- a/www/events/talks.cgi
+++ b/www/events/talks.cgi
@@ -1,7 +1,7 @@
#!/usr/bin/env ruby
PAGETITLE = "Apache Related Talks Listing" # Wvisible:events
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'json'
require 'whimsy/asf'
require 'wunderbar'
diff --git a/www/foundation/orgchart.cgi b/www/foundation/orgchart.cgi
index ef1a3be..199ca26 100755
--- a/www/foundation/orgchart.cgi
+++ b/www/foundation/orgchart.cgi
@@ -1,6 +1,6 @@
#!/usr/bin/env ruby
PAGETITLE = "Apache Corporate Organization Chart" # Wvisible:orgchart
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'json'
require 'whimsy/asf'
require 'whimsy/asf/orgchart' # New class not yet in gem; duplicates www/roster/models/orgchart
diff --git a/www/incubator/graduated.cgi b/www/incubator/graduated.cgi
new file mode 100755
index 0000000..7a2a9ba
--- /dev/null
+++ b/www/incubator/graduated.cgi
@@ -0,0 +1,196 @@
+#!/usr/bin/env ruby
+PAGETITLE = "Projects which graduated from the incubator" # Wvisible:incubator
+
+$LOAD_PATH.unshift '/srv/whimsy/lib'
+require 'whimsy/asf'
+require 'wunderbar'
+require 'wunderbar/bootstrap'
+
+parents = {
+ "ant" => "Jakarta",
+ "attic" => "N/A",
+ "community development" => "N/A",
+ "db" => "Jakarta",
+ "incubator" => "N/A",
+ "labs" => "N/A",
+ "logging services" => "Jakarta",
+ "public relations" => "N/A",
+}
+
+source = '/srv/whimsy/www/board/minutes'
+index = File.read("#{source}/index.html")
+
+csection = index[/<h2 id="committee">.*?<h2/m]
+creports = csection.scan(/<a .*?<\/a>/)
+retired = csection.scan(/<del>.*?<\/del>/m)
+
+creports.sort_by! {|committee| committee[/>(.*?)</, 1].downcase}
+zest = creports.find {|committee| committee =~ />Zest</}
+
+
+_html do
+ _body? do
+ _whimsy_body(
+ title: PAGETITLE,
+ related: {
+ "/committers/tools" => "Whimsy Tool Listing",
+ "https://incubator.apache.org/images/incubator_feather_egg_logo_sm.png" => "Incubator Logo, to show that graphics can appear",
+ "https://community.apache.org/" => "Get Community Help",
+ "https://github.com/apache/whimsy/blob/master/www#{ENV['SCRIPT_NAME']}" => "See This Source Code"
+ },
+ helpblock: -> {
+ _p! do
+ _ 'This script cross-checks Committee Reports from '
+ _a 'Board Minutes',
+ href: 'https://whimsy.apache.org/board/minutes/'
+ _ ', '
+ _a 'committee-info.txt',
+ href: 'https://svn.apache.org/repos/private/committers/board/committee-info.txt'
+ _ ', and '
+ _a 'podlings.xml',
+ href: 'https://svn.apache.org/repos/asf/incubator/public/trunk/content/podlings.xml'
+ _ '.'
+ _p do
+ _ul do
+ _li 'Committee: links to Whimsy summary of board minutes'
+ _li 'Established: date is from ASF meeting minutes; links to the published minutes if found'
+ _li 'Parent PMC; links to the Incubator status page if the PMC represents a graduated podling'
+ _li 'Active?: Listed as an active PMC in committee-info.txt'
+ end
+ end
+ end
+ }
+ ) do
+
+ ASF.init_ldap
+
+ unreported = ASF::Committee.pmcs.map(&:display_name).map(&:downcase)
+ incubated = 0
+
+ #
+ ### Podling mentors vs IPMC
+ #
+ _whimsy_panel_table(
+ title: "Establish Resolutions from Projects that have reported",
+ ) do
+ _table.table.table_hover.table_striped do
+ _thead_ do
+ _tr do
+ _th 'Committee'
+ _th 'Established'
+ _th 'Parent PMC'
+ _th 'Active?'
+ end
+ end
+ _tbody do
+ creports.map do |committee|
+ name = committee[/>(.*?)</, 1]
+ href = committee[/href="(.*?)"/, 1].untaint
+ href = 'Polygene.html' if href == 'Zest.html'
+ page = File.read("#{source}/#{href}").
+ sub(/<footer.*<\/footer>/m, '')
+
+ next if name == 'Zest' # renamed to Polygene
+ next if name == 'Metro' # rejected
+
+ active = unreported.delete(name.downcase)
+
+ graduated = false
+
+ parent = nil
+
+ establish = page.split('<h2').map { |report|
+ title = report[/<h3.*?<\/h3>/]
+ next unless title and
+ %w(establish create creation).any? {|word|
+ title.downcase.include? word
+ }
+
+ graduated ||= report.downcase.include? 'incubator'
+
+ discharge = report.split(/\n\s*\n/).grep(/discharged/).last
+ if discharge
+ parent = discharge[/(\w+)\s*(Project|PMC)/, 1]
+ end
+
+ report[/id="(.*?)"/, 1]
+ }.compact.first
+
+ podling = ASF::Podling.find(name.downcase)
+ incubated += 1 if graduated or podling
+
+ _tr_ do
+ _td do
+ _a name, href: "../board/minutes/#{href}"
+ end
+ _td do
+ _a establish, href: "../board/minutes/#{href}##{establish}"
+ end
+ _td do
+ if podling
+ _a 'Incubator', href:
+ "https://incubator.apache.org/projects/#{podling.resource}.html"
+ else
+ _span parent || parents[name.downcase]
+ end
+ end
+ _td !!active
+ end
+ end
+ end
+ end
+ end
+
+ _whimsy_panel_table(
+ title: "Projects that don't have posted reports"
+ ) do
+ _table.table.table_hover.table_striped do
+ _thead_ do
+ _tr do
+ _th 'Committee'
+ end
+ end
+ _tbody do
+ unreported.each do |committee|
+ _tr do
+ _td do
+ _a committee, href: "../roster/committee/" +
+ ASF::Committee.find(committee).name
+ end
+ end
+ end
+ end
+ end
+ end unless unreported.empty?
+
+ _whimsy_panel_table(
+ title: "Projects summary"
+ ) do
+ _table.table.table_hover.table_striped id: 'summary' do
+ _tbody do
+ _tr do
+ _td creports.length
+ _td "Committees that have reported"
+ end
+
+ _tr do
+ _td ASF::Committee.pmcs.length
+ _td "Active Committees"
+ end
+
+ _tr do
+ _td retired.length
+ _td "Committees that have retired"
+ end
+
+ _tr do
+ _td incubated
+ _td "Graduated from the incubator"
+ end
+ end
+ end
+ end
+
+ end
+ end
+end
diff --git a/www/incubator/maillist.cgi b/www/incubator/maillist.cgi
index effe212..922add0 100755
--- a/www/incubator/maillist.cgi
+++ b/www/incubator/maillist.cgi
@@ -1,7 +1,7 @@
#!/usr/bin/env ruby
PAGETITLE = "Incubator Podling Mailing Lists" # Wvisible:incubator mail
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'whimsy/asf'
require 'wunderbar/bootstrap'
diff --git a/www/incubator/podling-crosscheck.cgi b/www/incubator/podling-crosscheck.cgi
index 349bf35..853889e 100755
--- a/www/incubator/podling-crosscheck.cgi
+++ b/www/incubator/podling-crosscheck.cgi
@@ -1,7 +1,7 @@
#!/usr/bin/env ruby
PAGETITLE = "Incubator/Podling crosscheck" # Wvisible:incubator
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'json'
require 'whimsy/asf'
require 'wunderbar'
diff --git a/www/incubator/podlings/by-age.cgi b/www/incubator/podlings/by-age.cgi
index 35ee521..f13d141 100755
--- a/www/incubator/podlings/by-age.cgi
+++ b/www/incubator/podlings/by-age.cgi
@@ -1,6 +1,6 @@
#!/usr/bin/env ruby
PAGETITLE = "Incubator Podlings By Age" # Wvisible:incubator historical
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'nokogiri'
require 'date'
require 'net/http'
@@ -49,7 +49,7 @@
'https://incubator.apache.org/projects/index.html' => 'List Of Incubator Podlings'
},
helpblock: -> {
- _ 'This shows a sorted list of all Incubator podlings by age since joining.'
+ _ 'This shows a sorted list of all active Incubator podlings by age since joining.'
# pie chart
theta = 0
colors = ['0F0', 'FF0', 'F80', 'F50', 'F00', '800']
@@ -80,7 +80,7 @@
_ul do
_li! do
_ "Count: #{duration.length} PPMCs ("
- _a 'history', href: 'https://incubator.apache.org/history/'
+ _a 'history', href: 'https://projects.apache.org/'
_ ') ('
_a 'source data', href: 'https://incubator.apache.org/projects/#current'
_ ')'
diff --git a/www/incubator/signoff.cgi b/www/incubator/signoff.cgi
index b120368..a6ad15e 100755
--- a/www/incubator/signoff.cgi
+++ b/www/incubator/signoff.cgi
@@ -3,7 +3,7 @@
# quick and dirty script to tally up which mentors have been providing
# signoffs and which have not.
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'nokogiri'
require 'wunderbar'
require 'wunderbar/bootstrap'
@@ -11,7 +11,7 @@
# Authenticate - must be first!
user = ASF::Person.find($USER)
-incubator = ASF::Committee.find('incubator').members
+incubator = ASF::Committee.find('incubator').owners
unless user.asf_member? or incubator.include? user
print "Status: 401 Unauthorized\r\n"
print "WWW-Authenticate: Basic realm=\"Incubator PMC and Members\"\r\n\r\n"
diff --git a/www/index.html b/www/index.html
index 2538132..1ab3d69 100644
--- a/www/index.html
+++ b/www/index.html
@@ -129,8 +129,21 @@
</div>
<div class="panel-body">
<ul>
- <li><a href="roster/committer/__self__">Your details</a></li>
- <li><a href="roster/">Rosters (PMCs, committers, members, groups, podlings)</a></li>
+ <li><a href="roster/committer/__self__">Your personal details</a></li>
+ <li><a href="roster/">Rosters</a>
+ including:
+ <a href="roster/committee/">PMCs</a>
+ |
+ <a href="roster/nonpmc/">other committees</a>
+ |
+ <a href="roster/ppmc/">podlings</a>
+ |
+ <a href="roster/group/">groups</a>
+ |
+ <a href="roster/members">members</a>
+ |
+ <a href="roster/committer/">committers</a>
+ </li>
<li><a href="committers/subscribe">Subscribe and unsubscribe from mailing lists</a></li>
<li><a href="committers/svn-info">svn info command helper</a></li>
<li><a href="committers/moderationhelper">Helper for mailing list moderators (Beta)</a></li>
@@ -169,8 +182,9 @@
<li><a href="members/whatif">STV Explorer</a></li>
<li><a href="members/proxy">Proxy form (ASF members meeting)</a></li>
<li><a href="members/security-subs">Security Mailing List Subscriptions</a></li>
- <li><a href="members/archivers">CrossCheck of Archiver Subscriptions (issues only)</a></li>
- <li><a href="members/archivers/all">CrossCheck of Archiver Subscriptions (all entries)</a></li>
+ <li><a href="members/archivers">Archiver Subscription Issues - ignoring missing mail-archive subs</a></li>
+ <li><a href="members/archivers/mail-archive">Archiver Subscription Issues - including missing mail-archive subs</a></li>
+ <li><a href="members/archivers/all">Archiver Subscription Checks (all entries)</a></li>
</div>
</div>
</div>
@@ -184,6 +198,7 @@
<li><a href="secretary/workbench/">Secretary Workbench</a></li>
<li><a href="secretary/icla-lint">Lint test for iclas.txt</a></li>
<li><a href="secretary/ldap-check">LDAP members and owners checks (may take a while to respond)</a></li>
+ <li><a href="secretary/ldap-check-committers">Detailed LDAP missing committer check; shows subs and mods if any (may take a while to respond)</a></li>
<li><a href="secretary/memapp_check">Check members.txt against members_apps</a></li>
<li><a href="secretary/public-names">Public names: LDAP vs icla.txt</a></li>
<li><a href="secretary/ldap-names">LDAP name check: compare cn, sn, givenName</a></li>
@@ -203,7 +218,7 @@
</div>
<div class="panel-body">
<p>
- Copyright © 2015-2018 The Apache Software Foundation, Licensed under
+ Copyright © 2015-2019 The Apache Software Foundation, Licensed under
the <a href="http://www.apache.org/licenses/LICENSE-2.0" rel="license">Apache License, Version 2.0</a>.
|
<a href="https://www.apache.org/foundation/policies/privacy">Privacy Policy</a>
diff --git a/www/members/archivers.cgi b/www/members/archivers.cgi
index c36bfe9..e8c8f8f 100755
--- a/www/members/archivers.cgi
+++ b/www/members/archivers.cgi
@@ -11,7 +11,17 @@
ids={}
binarchives = ASF::Mail.lists(true)
-show_all = (ENV['PATH_INFO'] == '/all')
+binarchtime = ASF::Mail.list_mtime
+
+show_all = (ENV['PATH_INFO'] == '/all') # all entries, regardless of error state
+# default is to show entry if neither mail-archive nor markmail is present (mail-archive is missing from a lot of lists)
+show_mailarchive = (ENV['PATH_INFO'] == '/mail-archive') # show entry if mail-archive is missing
+
+# list of ids deliberately not archived
+# INFRA-18129
+NOT_ARCHIVED = %w{apachecon-aceu19}
+
+sublist_time = ASF::MLIST.list_time
_html do
_body? do
@@ -25,13 +35,44 @@
_p! do
_ 'This script compares bin/.archives with the list of archiver addresses that are subscribed to mailing lists'
_br
- _ 'Every entry in bin/.archives should have up to 3 archive subscribers, except for the mail aliases, which are not lists.'
+ _ 'Every entry in bin/.archives should have up to 3 archive subscribers (5 for public lists), except for the mail aliases, which are not lists.'
_br
_ 'Every mailing list should have an entry in bin/.archives'
_br
_ 'Unexpected/missing entries are flagged'
_br
_ 'Minotaur emails can be either aliases (tlp-list-archive@tlp.apache.org) or direct (apmail-tlp-list-archive@www.apache.org).'
+ _br
+ _ 'Columns:'
+ _ul do
+ _li 'id - short id of list as used on mod_mbox'
+ _li 'list - full list name'
+ _li "Private? - public/private; derived from bin/.archives as at #{binarchtime}"
+ _li 'MINO - minotaur archiver'
+ _li 'MBOX - mbox-vm archiver'
+ _li 'PONY - PonyMail (lists.apache.org) archiver'
+ _li 'MAIL-ARCHIVE - @mail-archive.com archiver (public lists only)'
+ _li 'MARKMAIL - markmail.org archiver (public lists only)'
+ _li "Archivers - list of known archiver subscriptions as at #{sublist_time}"
+ end
+ _ 'Showing: '
+ unless show_all or show_mailarchive
+ _b 'issues (ignoring missing mail-archive subscriptions)'
+ else
+ _a 'issues (ignoring missing mail-archive subscriptions)', href: './'
+ end
+ _ ' | '
+ if show_mailarchive
+ _b 'issues including missing mail-archive subscriptions'
+ else
+ _a 'issues including missing mail-archive subscriptions', href: './mail-archive'
+ end
+ _ ' | '
+ if show_all
+ _b 'details for all lists'
+ else
+ _a 'details for all lists', href: './all'
+ end
end
}
) do
@@ -44,50 +85,84 @@
_th 'MINO'
_th 'MBOX'
_th 'PONY'
+ _th 'MAIL-ARCHIVE'
+ _th 'MARKMAIL'
_th 'Archivers', data_sort: 'string'
end
ASF::MLIST.list_archivers do |dom, list, arcs|
id = ASF::Mail.archivelistid(dom, list)
+ next if NOT_ARCHIVED.include? id # skip error reports. TODO check if it is archived
+
ids[id] = 1 # TODO check for duplicates
options = Hash.new # Any fields have warnings/errors?
- pubprv = binarchives[id]
+ pubprv = binarchives[id] # public/private
- mino = arcs.select{|e| e[1] == :MINO}.map{|e| e[2]}.first
- if mino
+ # in case there are multiple archivers with different classifications, we
+ # join all the unique entries.
+ # This is equivalent to first if there is only one, but will produce
+ # a string such as 'privatepublic' if there are distinct entries
+ # However it generates an empty string if there are no entries.
+
+ mino = arcs.select{|e| e[1] == :MINO}.map{|e| e[2]}.uniq.join('')
+ if ! mino.empty?
options[:mino]={class: 'info'} unless mino == 'alias'
else
mino = 'Missing'
options[:mino]={class: 'warning'}
end
- mbox = arcs.select{|e| e[1] == :MBOX}.map{|e| e[2]}.first
- if mbox
+ mbox = arcs.select{|e| e[1] == :MBOX}.map{|e| e[2]}.uniq.join('')
+ if ! mbox.empty?
options[:mbox] = {class: 'danger'} if pubprv && mbox != pubprv
else
mbox = 'Missing'
options[:mbox] = {class: 'warning'}
end
- pony = arcs.select{|e| e[1] == :PONY}.map{|e| e[2]}.first
- if pony
+ pony = arcs.select{|e| e[1] == :PONY}.map{|e| e[2]}.uniq.join('')
+ if ! pony.empty?
options[:pony] = {class: 'danger'} if pubprv && pony != pubprv
else
pony = 'Missing'
options[:pony] = {class: 'warning'}
end
+ mail_archive = arcs.select{|e| e[1] == :MAIL_ARCHIVE}.map{|e| e[2]}.uniq.join('')
+ if ! mail_archive.empty?
+ options[:mail_archive] = {class: 'danger'} if pubprv && mail_archive != pubprv
+ elsif pubprv == 'private'
+ mail_archive = 'N/A'
+ else
+ mail_archive = 'Missing'
+ options[:mail_archive] = {class: 'warning'}
+ end
- # must be done last
+ markmail = arcs.select{|e| e[1] == :MARKMAIL}.map{|e| e[2]}.uniq.join('')
+ if ! markmail.empty?
+ options[:markmail] = {class: 'danger'} if pubprv && markmail != pubprv
+ elsif pubprv == 'private'
+ markmail = 'N/A'
+ else
+ markmail = 'Missing'
+ options[:markmail] = {class: 'warning'}
+ end
+
+ # must be done last as it changes pubprv
unless pubprv
pubprv = 'Not listed in bin/.archives'
options[:pubprv] = {class: 'warning'}
end
- next unless show_all || options.keys.length > 0 # only show errors unless want all
+ if show_mailarchive
+ needs_attention = options.keys.length > 0
+ else # don't show missing mail-archive
+ needs_attention = options.reject{|k,v| k == :mail_archive && mail_archive == 'Missing'}.length > 0
+ end
+ next unless show_all || needs_attention # only show errors unless want all
_tr do
_td id
@@ -101,6 +176,8 @@
_td mino, options[:mino]
_td mbox, options[:mbox]
_td pony, options[:pony]
+ _td mail_archive, options[:mail_archive]
+ _td markmail, options[:markmail]
_td arcs.map{|e| e.first}.sort
end
end
diff --git a/www/members/attendance-xcheck.cgi b/www/members/attendance-xcheck.cgi
index b24ad84..7d8c647 100755
--- a/www/members/attendance-xcheck.cgi
+++ b/www/members/attendance-xcheck.cgi
@@ -1,6 +1,6 @@
#!/usr/bin/env ruby
PAGETITLE = "Member's Meeting Attendance Cross-Check" # Wvisible:meeting
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'whimsy/asf'
require 'wunderbar/bootstrap'
diff --git a/www/members/board-attend.cgi b/www/members/board-attend.cgi
index 03c0263..db22040 100755
--- a/www/members/board-attend.cgi
+++ b/www/members/board-attend.cgi
@@ -1,9 +1,10 @@
#!/usr/bin/env ruby
PAGETITLE = "Board Meeting Attendance since 2010" # Wvisible:meeting
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'whimsy/asf'
require 'whimsy/asf/agenda'
+require 'whimsy/asf/board'
require 'whimsy/public'
require 'wunderbar/bootstrap'
require 'json'
@@ -13,38 +14,6 @@
IS_DIRECTOR = :director
APPROVED = 'approved'
-# Map director ids->names and ids->initials
-# Only since 2010, once the preapp data in meetings is parseable
-INITIALS = 0
-FIRST_NAME = 1
-DISPLAY_NAME = 2
-DIRECTOR_MAP = {
- 'bdelacretaz' => ['bd', 'Bertrand', 'Bertrand Delacretaz'],
- 'brett' => ['bp', 'Brett', 'Brett Porter'],
- 'brianm' => ['bmc', 'Brian', 'Brian McCallister'],
- 'curcuru' => ['sc', 'Shane', 'Shane Curcuru'],
- 'cutting' => ['dc', 'Doug', 'Doug Cutting'],
- 'dkulp' => ['dk', 'Daniel', 'Daniel Kulp'],
- 'fielding' => ['rf', 'Roy', 'Roy T. Fielding'],
- 'geirm' => ['gmj', 'Geir', 'Geir Magnusson Jr'],
- 'gstein' => ['gs', 'Greg', 'Greg Stein'],
- 'isabel' => ['idf', 'Isabel', 'Isabel Drost-Fromm'],
- 'jerenkrantz' => ['je', 'Justin', 'Justin Erenkrantz'],
- 'jim' => ['jj', 'Jim', 'Jim Jagielski'],
- 'ke4qqq' => ['dn', 'David', 'David Nalley'],
- 'lrosen' => ['lr', 'Larry', 'Lawrence Rosen'],
- 'markt' => ['mt', 'Mark', 'Mark Thomas'],
- 'marvin' => ['mh', 'Marvin', 'Marvin Humphrey'],
- 'mattmann' => ['cm', 'Chris', 'Chris Mattmann'],
- 'noirin' => ['np', 'Noirin', 'Noirin Plunkett'],
- 'psteitz' => ['ps', 'Phil', 'Phil Steitz'],
- 'rbowen' => ['rb', 'Rich', 'Rich Bowen'],
- 'rgardler' => ['rg', 'Ross', 'Ross Gardler'],
- 'rubys' => ['sr', 'Sam', 'Sam Ruby'],
- 'rvs' => ['rs', 'Roman', 'Roman Shaposhnik'],
- 'tdunning' => ['td', 'Ted', 'Ted Dunning']
-}
-
# Summarize director attendance and preapps at one meeting into dstats
# @note that ASF::Board::Agenda has mix of symbols and strings for hash keys
# @return string if error
@@ -73,8 +42,8 @@
actions = agenda.select{ |v| v.has_key?(:index) && v[:index] == "Action Items" }[0]['actions']
dstats.each do |id, dirmtg|
if dirmtg.has_key?(meeting)
- dirmtg[meeting]['preapps'] = (reports.select {|v| v[APPROVED].include?(DIRECTOR_MAP[id][INITIALS])}.length / numreports).round(3)
- dirmtg[meeting]['actions'] = actions.select{ |v| v[:owner] == DIRECTOR_MAP[id][FIRST_NAME] }.length
+ dirmtg[meeting]['preapps'] = (reports.select {|v| v[APPROVED].include?(ASF::Board.directorInitials(id))}.length / numreports).round(3)
+ dirmtg[meeting]['actions'] = actions.select{ |v| v[:owner] == ASF::Board.directorFirstName(id) }.length
end
end
rescue StandardError => e
@@ -153,13 +122,13 @@
totp = 0.0
tota = 0.0
data.each do |k,v|
- totp += v['preapps']
- tota += v['actions']
+ totp += v['preapps'] if v.has_key?('preapps')
+ tota += v['actions'] if v.has_key?('actions')
end
_tr_ do
_td do
- if DIRECTOR_MAP[id] and DIRECTOR_MAP[id][DISPLAY_NAME]
- _ DIRECTOR_MAP[id][DISPLAY_NAME]
+ if ASF::Board.directorHasId?(id) and ASF::Board.directorDisplayName(id)
+ _ ASF::Board.directorDisplayName(id)
else
_em.bg_danger id
end
diff --git a/www/members/inactive.cgi b/www/members/inactive.cgi
index 3ba7980..c262f67 100755
--- a/www/members/inactive.cgi
+++ b/www/members/inactive.cgi
@@ -1,5 +1,5 @@
#!/usr/bin/env ruby
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'whimsy/asf'
require 'wunderbar/bootstrap'
@@ -24,7 +24,8 @@
}
end
_body? do
- _whimsy_header 'Poll of Inactive Members'
+ _whimsy_nav
+ _h1 'Poll of Inactive Members'
# locate and read the attendance file
MEETINGS = ASF::SVN['Meetings']
attendance = JSON.parse(IO.read(File.join(MEETINGS, 'attendance.json')))
diff --git a/www/members/index.cgi b/www/members/index.cgi
index 728d197..e42bb5d 100755
--- a/www/members/index.cgi
+++ b/www/members/index.cgi
@@ -1,7 +1,7 @@
#!/usr/bin/env ruby
PAGETITLE = "Overview of Whimsy Tools for Members" # Wvisible:meeting
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'json'
require 'whimsy/asf'
require 'wunderbar'
diff --git a/www/members/logs.cgi b/www/members/logs.cgi
index 748ec34..258dd95 100755
--- a/www/members/logs.cgi
+++ b/www/members/logs.cgi
@@ -1,6 +1,6 @@
#!/usr/bin/env ruby
PAGETITLE = "Server error log listing" # Wvisible:debug
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'json'
require 'whimsy/asf'
require 'wunderbar'
@@ -27,7 +27,7 @@
_tbody do
logs.each do | key, val |
_tr_ do
- _td do
+ _td :class => 'nowrap' do
_ key
end
_td do
@@ -60,31 +60,36 @@
# Emit table of interesting access logs (optional, with ?access)
def display_access()
apps, misses = LogParser.get_access_reports()
-
_p do
- _ 'This only includes a small subset of possibly interesting access log entries, roughly categorized by major application (board, roster, etc.)'
+ _ 'This only includes a subset of possibly interesting access log entries from the current day, roughly categorized by major application (board, roster, etc.)'
_a 'See the full server logs directory.', href: '/members/log'
end
- _h2 'Access Log Synopsis - by Application'
- apps.each do |name, data|
- _h3 "#{name} - application"
- _table.table.table_hover.table_striped do
- _thead_ do
- _tr do
- _th 'User list'
- _th 'URLs hit (total)'
- end
- _tbody do
- _tr_ do
- _td do
- data['remote_user'].each do |remote_user|
- _ remote_user
- end
+ _h2 'Access Log Synopsis - by Path or Tool'
+ listid = 'applist'
+ _div.panel_group id: listid, role: 'tablist', aria_multiselectable: 'true' do
+ apps.each_with_index do |(name, data), n|
+ itemtitle = LogParser::WHIMSY_APPS[name] ? LogParser::WHIMSY_APPS[name] : 'All Other URLs'
+ itemtitle << " (#{data['remote_user'].sum{|k,v| v}})" if data['remote_user']
+ _whimsy_accordion_item(listid: listid, itemid: name, itemtitle: "#{itemtitle}", n: n, itemclass: 'panel-info') do
+ _table.table.table_hover.table_striped do
+ _thead_ do
+ _tr do
+ _th 'User list'
+ _th 'URLs hit (total)'
end
- _td do
- data['uri'].sort.each do |uri|
- _ uri
- _br
+ _tbody do
+ _tr_ do
+ _td do
+ data['remote_user'].each do |remote_user|
+ _ remote_user
+ end
+ end
+ _td do
+ data['uri'].sort.each do |uri|
+ _ uri
+ _br
+ end
+ end
end
end
end
@@ -92,8 +97,8 @@
end
end
end
- _whimsy_panel(title: 'Access Log Synopsis - Error URLs') do
- _p 'This is a simplistic listing of all URLs with 4xx/5xx error codes (excluding obvious bots).'
+ _whimsy_panel('Access Log Synopsis - Error URLs', style: 'panel-warning') do
+ _p 'This is a simplistic listing of all URLs with 4xx/5xx error codes (excluding obvious bot URLs).'
erruri = {}
errref = {}
misses.each do |h|
@@ -116,6 +121,11 @@
end
_html do
+ _style %{
+ .nowrap {
+ white-space: nowrap;
+ }
+ }
_body? do
_whimsy_body(
title: PAGETITLE,
@@ -124,10 +134,17 @@
related: {
'/members/log' => 'Full server error and access logs',
'/docs' => 'Whimsy code and API documentation',
+ '/status' => 'Whimsy production server status',
"https://github.com/apache/whimsy/blob/master/www#{ENV['SCRIPT_NAME']}" => 'See This Source Code'
},
helpblock: -> {
- _p 'This parses error.log and whimsy_error.log and displays a condensed version, in time order (approximate).'
+ _p 'This parses error.log and whimsy_error.log and displays a condensed version, in time order (approximate) of today\'s entries.'
+ _p do
+ _a 'Append "?week"', href: "#{ENV['SCRIPT_NAME']}?week"
+ _ ' to the URL to get error results for the last week, and '
+ _a 'append "?access"', href: "#{ENV['SCRIPT_NAME']}?access"
+ _ ' to parse the access logs instead.'
+ end
_p do
_span.text_warning 'Reminder: '
_span.glyphicon.glyphicon_lock :aria_hidden
diff --git a/www/members/memberless-pmcs.cgi b/www/members/memberless-pmcs.cgi
index 61fd72f..ca449b9 100755
--- a/www/members/memberless-pmcs.cgi
+++ b/www/members/memberless-pmcs.cgi
@@ -1,6 +1,6 @@
#!/usr/bin/env ruby
PAGETITLE = "Crosscheck PMCs with few/no ASF Members" # Wvisible:members
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'whimsy/asf'
require 'wunderbar'
require 'wunderbar/bootstrap'
diff --git a/www/members/mentor-format.rb b/www/members/mentor-format.rb
new file mode 100644
index 0000000..924a8ec
--- /dev/null
+++ b/www/members/mentor-format.rb
@@ -0,0 +1,72 @@
+# Utility methods and structs related to mentor data
+require 'json'
+require 'tzinfo'
+
+class MentorFormat
+ ROSTER = 'https://whimsy.apache.org/roster/committer/'
+ MENTORS_SVN = 'https://svn.apache.org/repos/private/foundation/mentors/'
+ MENTORS_LIST = 'https://whimsy.apache.org/member/mentors.cgi'
+ PUBLICNAME = 'publicname'
+ NOTAVAILABLE = 'notavailable'
+ ERRORS = 'errors'
+ TIMEZONE = 'timezone'
+ TZ = TZInfo::Timezone.all_country_zone_identifiers
+ PREFERS_TYPES = [
+ 'email',
+ 'phone',
+ 'Slack',
+ 'irc',
+ 'Hangouts',
+ 'Facebook',
+ 'Skype',
+ 'other (text chat)',
+ 'other (video chat)'
+ ]
+ LANGUAGES = [ # Wikipedia top list by total speakers, plus EU
+ 'Arabic',
+ 'Bengali',
+ 'Bulgarian',
+ 'Chinese',
+ 'Croatian',
+ 'Czech',
+ 'Danish',
+ 'Dutch',
+ 'English',
+ 'Estonian',
+ 'Finnish',
+ 'French',
+ 'German',
+ 'Greek',
+ 'Hindi',
+ 'Hungarian',
+ 'Indonesean',
+ 'Irish',
+ 'Italian',
+ 'Japanese',
+ 'Korean',
+ 'Latvian',
+ 'Lithuanian',
+ 'Maltese',
+ 'Marathi',
+ 'Polish',
+ 'Portugese',
+ 'Punjabi',
+ 'Romanian',
+ 'Russian',
+ 'Slovak',
+ 'Slovene',
+ 'Spanish',
+ 'Swahili',
+ 'Swedish',
+ 'Tamil',
+ 'Telugu',
+ 'Thai',
+ 'Turkish',
+ 'Vietnamese'
+ ]
+
+ # Read mapping of labels to fields
+ def self.get_uimap(path)
+ return JSON.parse(File.read(File.join(path, 'ui-map.json')))
+ end
+end
\ No newline at end of file
diff --git a/www/members/mentor-update.cgi b/www/members/mentor-update.cgi
new file mode 100755
index 0000000..25437fe
--- /dev/null
+++ b/www/members/mentor-update.cgi
@@ -0,0 +1,259 @@
+#!/usr/bin/env ruby
+PAGETITLE = "Create/Update Your Mentor Record" # Wvisible:members
+$LOAD_PATH.unshift '/srv/whimsy/lib'
+
+require 'wunderbar'
+require 'wunderbar/bootstrap'
+require 'wunderbar/jquery'
+require 'whimsy/asf'
+require 'wunderbar/markdown'
+require 'whimsy/asf/rack'
+require 'json'
+require 'tzinfo'
+require_relative 'mentor-format'
+require 'whimsy/asf/forms'
+
+# Convenience function
+def emit_mentor_input(field, mdata, uimap, icon, req: false)
+ _whimsy_forms_input(label: uimap[field][0], name: field, required: req,
+ icon: icon, value: (mdata[field] ? mdata[field] : ''),
+ helptext: uimap[field][1]
+ )
+end
+
+# Display the form for user's mentor record (custom function to mentor data structure)
+def emit_form(apacheid, mdata, button_help, uimap)
+ title = mdata.empty?() ? 'Volunteer to Mentor a New ASF Member' : 'Update your Mentor Record'
+ _whimsy_panel("#{title} (#{apacheid})", style: 'panel-success') do
+ _form.form_horizontal method: 'post' do
+ if mdata.has_key?(MentorFormat::ERRORS)
+ _div.alert.alert_danger role: 'alert' do
+ _p 'There was an error parsing the .json; you might need to manually edit it instead:'
+ _p.text_error mdata[MentorFormat::ERRORS]
+ end
+ end
+
+ _div.form_group do
+ _label.col_sm_offset_3.col_sm_9.strong.text_left 'How Mentees Should Work With You'
+ end
+ emit_mentor_input('contact', mdata, uimap, 'glyphicon-bullhorn', req: true)
+ field = 'timezone'
+ _whimsy_forms_select(label: uimap[field][0], name: field,
+ values: (mdata[field] ? mdata[field] : ''),
+ options: MentorFormat::TZ.sort,
+ icon: 'glyphicon-time', iconlabel: 'clock',
+ helptext: uimap[field][1]
+ )
+ emit_mentor_input('availability', mdata, uimap, 'glyphicon-hourglass')
+ field = 'prefers'
+ _whimsy_forms_select(label: uimap[field][0], name: field, multiple: true,
+ values: (mdata[field] ? mdata[field] : ''),
+ options: MentorFormat::PREFERS_TYPES,
+ icon: 'glyphicon-ok-sign', iconlabel: 'ok-sign',
+ helptext: uimap[field][1]
+ )
+ field = 'languages'
+ _whimsy_forms_select(label: uimap[field][0], name: field, multiple: true,
+ values: (mdata[field] ? mdata[field] : ''),
+ options: MentorFormat::LANGUAGES,
+ icon: 'glyphicon-globe', iconlabel: 'globe',
+ helptext: uimap[field][1]
+ )
+
+ _div.form_group do
+ _label.col_sm_offset_3.col_sm_9.strong.text_left 'What You Could Help Mentees With'
+ end
+ emit_mentor_input('experience', mdata, uimap, 'glyphicon-certificate')
+ emit_mentor_input('available', mdata, uimap, 'glyphicon-plus-sign')
+ emit_mentor_input('mentoring', mdata, uimap, 'glyphicon-minus-sign')
+
+ _div.form_group do
+ _label.col_sm_offset_3.col_sm_9.strong.text_left 'More About You Personally'
+ end
+ emit_mentor_input('homepage', mdata, uimap, 'glyphicon-console')
+ emit_mentor_input('pronouns', mdata, uimap, 'glyphicon-user')
+ field = 'aboutme'
+ _whimsy_forms_input(label: uimap[field][0], name: field, rows: 3,
+ icon: 'glyphicon-info-sign', value: (mdata[field] ? mdata[field] : ''),
+ helptext: uimap[field][1]
+ )
+
+ _div.form_group do
+ _label.col_sm_offset_3.col_sm_9.strong.text_left 'Temporarily Opt Out From Any NEW Mentees'
+ _label.control_label.col_sm_3 'Not Accepting New Mentees', for: "#{MentorFormat::NOTAVAILABLE}"
+ _div.col_sm_9 do
+ _div.input_group do
+ _label "#{MentorFormat::NOTAVAILABLE}" do
+ args = { type: 'checkbox', id: "#{MentorFormat::NOTAVAILABLE}", name: "#{MentorFormat::NOTAVAILABLE}", value: "#{MentorFormat::NOTAVAILABLE}" }
+ args[:checked] = true if mdata[MentorFormat::NOTAVAILABLE]
+ _input ' Stop accepting NEW Mentees', args
+ end
+ end
+ _span.help_block do
+ _ "Select checkbox to no longer be listed in active mentor list (you can still work with existing Mentees)."
+ end
+ end
+ end
+
+ _div.col_sm_offset_3.col_sm_9 do
+ _span.text_info button_help
+ _br
+ _input.btn.btn_default type: 'submit', value: 'Update Your Mentor Data'
+ end
+ end
+ end
+end
+
+# Validation as needed within the script
+def validate_form(formdata: {})
+ # Scrub one key if it's blank (only leave it if set to a value)
+ if formdata.has_key?(MentorFormat::NOTAVAILABLE) && formdata[MentorFormat::NOTAVAILABLE] == ''
+ formdata.delete(MentorFormat::NOTAVAILABLE)
+ end
+ return true # TODO: Futureuse
+end
+
+# Handle submission (checkout user's apacheid.json, write form data, checkin file)
+# @return true if we think it succeeded; false in all other cases
+def send_form(formdata: {})
+ rc = 999
+ fn = "#{$USER}.json".untaint
+ mentor_update = JSON.pretty_generate(formdata) + "\n"
+ _div.well do
+ _p.lead "Updating your mentor record #{fn} to be:"
+ _pre mentor_update
+ end
+
+ Dir.mktmpdir do |tmpdir|
+ credentials = ['--username', $USER, '--password', $PASSWORD]
+ svnopts = ['--no-auth-cache', '--non-interactive']
+ # TODO: investigate if we should to --depth empty and attempt to get only that mentor's file
+ _.system ['svn', 'checkout', MentorFormat::MENTORS_SVN, tmpdir.untaint, svnopts, credentials]
+
+ Dir.chdir tmpdir do
+ if File.exist? fn
+ File.write(fn, mentor_update + "\n")
+ _.system ['svn', 'st']
+ message = "Updating my mentoring data (whimsy)"
+ else
+ File.write(fn, mentor_update + "\n")
+ _.system ['svn', 'add', fn]
+ message = "#{$USER} += mentoring volunteer (whimsy)"
+ end
+ rc = _.system ['svn', 'commit', fn, '--message', message, svnopts, credentials]
+ end
+ end
+
+ if rc == 0
+ _div.alert.alert_success role: 'alert' do
+ _p do
+ _span.strong 'Your mentor update was submitted, and will be live within a few minutes. Thanks for volunteering!'
+ end
+ end
+ return true
+ else
+ _div.alert.alert_danger role: 'alert' do
+ _p do
+ _span.strong 'SVN Update Failed, see above for details; contact dev@whimsical.apache.org for help. Alternately, edit your Mentor file directly in SVN: '
+ _a "#{MentorFormat::MENTORS_SVN}#{$USER}.json", href: "#{MentorFormat::MENTORS_SVN}#{$USER}.json"
+ end
+ end
+ return false
+ end
+end
+
+# Read user's *.json from directory of mentor files
+# @return user's current mentor data, or {} if none, or sets:
+# myrecord[ERRORS] = "If any error occoured on read/parse"
+def read_myrecord(id)
+ file = File.join(ASF::SVN['foundation_mentors'], "#{id}.json").untaint
+ if File.exist?(file)
+ begin
+ return JSON.parse(File.read(file))
+ rescue StandardError => e
+ return { MentorFormat::ERRORS => "ERROR:read_myrecord(#{file}) #{e.message} #{e.backtrace[0]}" }
+ end
+ else
+ return {}
+ end
+end
+
+# produce HTML
+_html do
+ _style :system
+ _style %{
+ .transcript {margin: 0 16px}
+ .transcript pre {border: none; line-height: 0}
+ }
+ _body? do
+ myrecord = read_myrecord($USER)
+ intro = "You can use this form to update your existing Mentor record, which will be checked into #{MentorFormat::MENTORS_SVN}"
+ header = 'Update Your Mentor Data (most fields optional)'
+ button_help = "Pressing Update will update your existing Mentoring Record in #{MentorFormat::MENTORS_SVN}#{$USER}.json"
+ if myrecord.empty?
+ intro = "You can use this form to volunteer to Mentor other new ASF Members; when you submit your Mentoring Record will be checked into #{MentorFormat::MENTORS_SVN})"
+ header = 'Enter Your Mentor Data (most fields optional)'
+ button_help = "Pressing Update will checkin your Mentoring Record into #{MentorFormat::MENTORS_SVN}#{$USER}.json and list you as a volunteer mentor here: #{MentorFormat::MENTORS_LIST}##{$USER}"
+ elsif myrecord.has_key?(MentorFormat::ERRORS)
+ intro = "Your existing .json file has an error (see below), please work with the Whimsy PMC to fix it: #{MentorFormat::MENTORS_SVN}#{$USER}.json"
+ header = 'There was an error either finding or JSON parsing your mentor record!'
+ button_help = "ERROR: We couldn't properly parse your existing .json file, this form may not work properly."
+ end
+ uimap = MentorFormat.get_uimap(ASF::SVN['foundation_mentors'])
+ _whimsy_body(
+ title: PAGETITLE,
+ subtitle: header,
+ related: {
+ MentorFormat::MENTORS_SVN => 'See All Mentors Data',
+ '/members/mentors' => 'List Of Active Mentors',
+ '/members/index/' => 'Other Member-Private Tools',
+ 'https://community.apache.org/' => 'Apache Community Development'
+ },
+ helpblock: -> {
+ _p intro
+ _p.text_warning 'Reminder: All Mentoring data is private to the ASF; only ASF Members can sign up here as Mentors or Mentees.'
+ }
+ ) do
+
+ # Display data to the user, depending if we're GET (existing mentor record or just blank data) or POST (show SVN checkin results)
+ if _.post?
+ submission = {
+ "timezone" => "#{@timezone}",
+ "availability" => "#{@availability}",
+ "contact" => "#{@contact}",
+ "available" => "#{@available}",
+ "mentoring" => "#{@mentoring}",
+ "experience" => "#{@experience}",
+ "pronouns" => "#{@pronouns}",
+ "aboutme" => "#{@aboutme}",
+ "homepage" => "#{@homepage}",
+ # Multiple select fields
+ "prefers" => _.params['prefers'],
+ "languages" => _.params['languages']
+ }
+ if @notavailable
+ submission['notavailable'] = "#{@notavailable}"
+ end
+ if validate_form(formdata: submission)
+ if send_form(formdata: submission)
+ _p.lead "Thanks for volunteering to mentor other ASF Members!"
+ _p do
+ _ "Your record will now show up on the list of active mentors (unless you had checked 'notavailable'). "
+ _a 'See the current list of active mentors', href: '/members/mentors'
+ end
+ end
+ else
+ _div.alert.alert_danger role: 'alert' do
+ _p do
+ _span.strong "WARNING: Form data invalid, update was NOT submitted! "
+ _br
+ _ "There was a validation error with your form submission; please contact dev@whimsical.apache.org with a bug report."
+ end
+ end
+ end
+ else # if _.post?
+ emit_form($USER, myrecord, button_help, uimap)
+ end
+ end
+ end
+end
diff --git a/www/members/mentors.cgi b/www/members/mentors.cgi
new file mode 100755
index 0000000..9e1fd86
--- /dev/null
+++ b/www/members/mentors.cgi
@@ -0,0 +1,154 @@
+#!/usr/bin/env ruby
+PAGETITLE = "Available Mentors For New Members" # Wvisible:members
+$LOAD_PATH.unshift '/srv/whimsy/lib'
+
+require 'whimsy/asf'
+require 'wunderbar/bootstrap'
+require 'wunderbar/markdown'
+require 'json'
+
+require_relative 'mentor-format'
+MENTORS_LIST = 'mentors'
+
+# Read apacheid.json and add data to mentors hash (side effect)
+# mentors[id][ERRORS] = "If errors rescued during read/find in ASF::Person"
+def read_mentor(file, mentors)
+ id = File.basename(file).split('.')[0]
+ member = ASF::Person[id] # We want to return nil if id not found
+ if member
+ begin
+ mentors[id] = JSON.parse(File.read(file))
+ mentors[id][MentorFormat::PUBLICNAME] = member.public_name()
+ rescue StandardError => e
+ mentors[id] = { MentorFormat::ERRORS => "ERROR:read_mentor() #{e.message} #{e.backtrace[0]} from #{file}"}
+ end
+ else
+ mentors[id] = { MentorFormat::ERRORS => "ERROR:ASF::Person.find(#{id}) returned nil from #{file}"}
+ end
+end
+
+# Read *.json from directory of mentor files
+# @return hash of mentors by apacheid
+def read_mentors(path)
+ mentors = {}
+ Dir[File.join(path, '*.json')].sort.each do |file|
+ # Skip files with - dashes, they aren't apacheids
+ next if file.include?('-')
+ read_mentor(file.untaint, mentors)
+ end
+ return mentors
+end
+
+# produce HTML
+_html do
+ _body? do
+ uimap = MentorFormat::get_uimap(ASF::SVN['foundation_mentors'])
+ mentors = read_mentors(ASF::SVN['foundation_mentors'])
+ errors, mentors = mentors.partition{ |k,v| v.has_key?(MentorFormat::ERRORS)}.map(&:to_h)
+ notavailable, mentors = mentors.partition{ |k,v| v.has_key?(MentorFormat::NOTAVAILABLE)}.map(&:to_h)
+ _whimsy_body(
+ title: PAGETITLE,
+ subtitle: 'About This Mentoring Program',
+ relatedtitle: 'Other ASF Mentoring Links',
+ related: {
+ MentorFormat::MENTORS_SVN => 'See Raw Mentors Data',
+ '/roster/members' => 'Whimsy All Members Roster',
+ '/members/index/' => 'Other Member-Private Tools',
+ 'https://community.apache.org' => 'Apache Community Development'
+ },
+ helpblock: -> {
+ _p do
+ _ 'This page lists experienced ASF Members who have volunteered to mentor newer ASF Members to help them get more involved in governance and operations within the larger Foundation as a whole.'
+ end
+ _p do
+ _ "If you are a newer Member looking for a mentor, please reach out directly to available volunteers below that fit your interests by #{uimap['contact'][0]} and request mentoring. Not every mentoring pair may be the right fit, so you'll need to decide together if you're a good pair."
+ _ 'Remember, this is an informal program run by volunteers, so please be kind - and patient! Mentors currently listed as available for new mentees:'
+ end
+ _table do
+ _tr do
+ _td do
+ _a.btn.btn_default.btn_sm (mentors.has_key?($USER) ? 'Edit Your Mentor Record' : 'Volunteer To Mentor'), href: "/members/mentor-update.cgi", role: "button"
+ end
+ _td do
+ _{" "*2}
+ end
+ _td do
+ _ul.list_inline do
+ mentors.each do | apacheid, mentor |
+ _li do
+ _a apacheid, href: "##{apacheid}"
+ end
+ end
+ end
+ end
+ end
+ end
+ _p.text_warning 'Reminder: All Mentoring data is private to the ASF; only ASF Members can sign up here as Mentors or Mentees.'
+ }
+ ) do
+ _div.panel_group id: MENTORS_LIST, role: "tablist", aria_multiselectable: "true" do
+ mentors.each_with_index do |(apacheid, mentor), n| # TODO Should we randomize the default listing?
+ timezone = mentor[MentorFormat::TIMEZONE]
+ offset = TZInfo::Timezone.get(timezone).strftime("%:z")
+ _whimsy_accordion_item(listid: MENTORS_LIST, itemid: apacheid, itemtitle: "#{mentor[MentorFormat::PUBLICNAME]} (#{apacheid}) Timezone: #{timezone} (#{offset}) ", n: n, itemclass: 'panel-primary') do
+ _table.table.table_hover do
+ _tbody do
+ mentor.delete(MentorFormat::PUBLICNAME) # So not re-displayed again
+ mentor.each do |k, v|
+ _tr do
+ _td!.text_right do
+ _span.text_primary uimap.has_key?(k) ? "#{uimap[k][0]}" : "#{k}"
+ end
+ _td!.text_left do
+ v = v.join(', ') if v.kind_of?(Array)
+ _markdown v
+ end
+ end
+ end
+ _tr do
+ _td!.text_right do
+ _ 'ASF Projects/Podlings Involved In'
+ end
+ _td!.text_left do
+ # TODO: instead of link to roster, this could read and display here
+ _a "#{MentorFormat::ROSTER}#{apacheid}", href: "#{MentorFormat::ROSTER}#{apacheid}"
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ if not notavailable.empty?
+ _div id: MentorFormat::NOTAVAILABLE do
+ _p! do
+ _! 'Volunteer mentors who are '
+ _strong! 'not'
+ _! ' currently available for new mentees: '
+ notavailable.each do |apacheid, n |
+ _ "#{n[MentorFormat::PUBLICNAME]}, "
+ end
+ end
+ end
+ end
+
+ if not errors.empty?
+ _div id: MentorFormat::ERRORS do
+ _whimsy_panel("Mentor JSON Files With Errors", style: 'panel-danger') do
+ _ul do
+ errors.each do |apacheid, error |
+ _li do
+ _code "#{apacheid}.json"
+ _ "#{error[MentorFormat::ERRORS]}"
+ end
+ end
+ end
+ _p 'Please work with dev@whimsical to fix these JSON files.'
+ end
+ end
+ end
+
+ end
+ end
+end
\ No newline at end of file
diff --git a/www/members/mirror_check.cgi b/www/members/mirror_check.cgi
index 5a96a1f..ff3ee1c 100755
--- a/www/members/mirror_check.cgi
+++ b/www/members/mirror_check.cgi
@@ -1,6 +1,6 @@
#!/usr/bin/env ruby
PAGETITLE = "ASF Distribution Mirror Checker" # Wvisible:infra mirror
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'wunderbar'
require 'wunderbar/bootstrap'
require 'whimsy/asf'
diff --git a/www/members/namediff.cgi b/www/members/namediff.cgi
index fa5598b..6516fe3 100755
--- a/www/members/namediff.cgi
+++ b/www/members/namediff.cgi
@@ -1,6 +1,6 @@
#!/usr/bin/env ruby
PAGETITLE = "Crosscheck Members Names With ICLAs" # Wvisible:members
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'whimsy/asf'
require 'wunderbar/bootstrap'
diff --git a/www/members/nominations.cgi b/www/members/nominations.cgi
index 793eaac..8e28f1e 100755
--- a/www/members/nominations.cgi
+++ b/www/members/nominations.cgi
@@ -1,6 +1,6 @@
#!/usr/bin/env ruby
PAGETITLE = "Member nominations cross-check" # Wvisible:meeting
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'mail'
require 'wunderbar/bootstrap'
diff --git a/www/members/non-participants.cgi b/www/members/non-participants.cgi
index 2979b3e..6930939 100755
--- a/www/members/non-participants.cgi
+++ b/www/members/non-participants.cgi
@@ -1,6 +1,6 @@
#!/usr/bin/env ruby
PAGETITLE = "Active Members not participating in meetings" # Wvisible:meeting
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'whimsy/asf'
require 'wunderbar/bootstrap'
diff --git a/www/members/proxy.cgi b/www/members/proxy.cgi
index 7b76725..f2ae90c 100755
--- a/www/members/proxy.cgi
+++ b/www/members/proxy.cgi
@@ -1,5 +1,5 @@
#!/usr/bin/env ruby
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'wunderbar'
require 'whimsy/asf'
@@ -8,6 +8,16 @@
MEETINGS = ASF::SVN['Meetings']
meeting = File.basename(Dir[File.join(MEETINGS, '2*')].sort.last).untaint
+volunteers = [
+ "Phil Steitz (psteitz)",
+ "Shane Curcuru (curcuru)",
+ "Michael Brohl (mbrohl)",
+ "Jim Jagielski (jim)",
+ "Daniel Ruggeri (druggeri)",
+ "Greg Stein (gstein)",
+ "Craig L Russell (clr)",
+ "Bertrand Delacretaz (bdelacretaz)"
+]
# Calculate how many members required to attend first half for quorum
def calculate_quorum(meeting)
@@ -102,23 +112,28 @@
_ " Calculation: Total voting members: #{num_members}, with one third for quorum: #{quorum_need}, minus previously submitted proxies: #{num_proxies}"
end
end
- _p %{
- IMPORTANT! Be sure to tell the person that you select as proxy
- that you've assigned them to mark your attendance! They simply
- need to mark your proxy attendance when the meeting starts.
- }
help, copypasta = is_user_proxied(meeting, $USER)
if help
_p help
if copypasta
- _ul do
+ _ul.bg_success do
copypasta.each do |copyline|
_pre copyline
end
end
end
+ else
+ _p 'The following members have volunteered to serve as proxies; you can freely select any one of them below:'
+ _ul do
+ volunteers.each do |vol|
+ _pre vol
+ end
+ end
end
- _a 'Read full procedures for Member Meeting', href: 'https://www.apache.org/foundation/governance/members.html#meetings'
+ _p do
+ _ "IMPORTANT! Be sure to tell the person that you select as proxy that you've assigned them to mark your attendance! They simply need to mark your proxy attendance when the meeting starts."
+ _a 'Read full procedures for Member Meeting', href: 'https://www.apache.org/foundation/governance/members.html#meetings'
+ end
end
end
@@ -147,7 +162,8 @@
next if exclude.include? member.id # Not attending
next unless members_txt[member.id] # Non-members
next if members_txt[member.id]['status'] # Emeritus/Deceased
- _option member.public_name
+ # Display the availid to users to match volunteers array above
+ _option "#{member.public_name} (#{member.id})"
end
end
end
@@ -187,8 +203,8 @@
user = ASF::Person.find($USER)
date = Date.today.strftime("%B %-d, %Y")
- # update proxy form
- proxy[/authorize _(#{'_' *@proxy.length})/, 1] = @proxy.gsub(' ', '_')
+ # update proxy form (match as many _ as possible up to the name length)
+ proxy[/authorize _(_{,#{@proxy.length}})/, 1] = @proxy.gsub(' ', '_')
proxy[/signature: _(_#{'_' *user.public_name.length}_)/, 1] =
"/#{user.public_name.gsub(' ', '_')}/"
@@ -196,6 +212,9 @@
proxy[/name: _(#{'_' *user.public_name.length})/, 1] =
user.public_name.gsub(' ', '_')
+ proxy[/availid: _(#{'_' *user.id.length})/, 1] =
+ user.id.gsub(' ', '_')
+
proxy[/Date: _(#{'_' *date.length})/, 1] = date.gsub(' ', '_')
proxyform = proxy.untaint
@@ -226,6 +245,8 @@
id = file[/([-A-Za-z0-9]+)\.\w+$/, 1]
proxy = form[/hereby authorize ([\S].*) to act/, 1].
gsub('_', ' ').strip
+ # Ensure availid is not included in proxy name here
+ proxy = proxy[/([^(]+)/, 1].strip
name = form[/signature: ([\S].*)/, 1].gsub(/[\/_]/, ' ').strip
" #{proxy.ljust(24)} #{name} (#{id})"
diff --git a/www/members/repo-use.cgi b/www/members/repo-use.cgi
new file mode 100755
index 0000000..8dd792d
--- /dev/null
+++ b/www/members/repo-use.cgi
@@ -0,0 +1,64 @@
+#!/usr/bin/env ruby
+PAGETITLE = "Scripts that use ASF::SVN" # Wvisible:tools
+$LOAD_PATH.unshift '/srv/whimsy/lib'
+require 'whimsy/asf'
+require 'wunderbar'
+require 'wunderbar/bootstrap'
+require '../../tools/wwwdocs.rb'
+
+_html do
+ _body? do
+ _whimsy_body(
+ title: PAGETITLE,
+ subtitle: 'Scan all scripts for SVN access',
+ relatedtitle: 'More Useful Links',
+ related: {
+ '/members/log' => 'Full server error and access logs',
+ '/docs' => 'Whimsy code and API documentation',
+ '/status' => 'Whimsy production server status',
+ "https://github.com/apache/whimsy/blob/master/www#{ENV['SCRIPT_NAME']}" => 'See This Source Code'
+ },
+ helpblock: -> {
+ _p 'This scans the whimsy repo for uses of ASF::SVN, either public or private repos.'
+ }
+ ) do
+ priv, pub = read_repository(File.expand_path('../../../repository.yml', __FILE__))
+ priv = build_regexp(priv)
+ pub = build_regexp(pub)
+ scan = scan_dir_svn('../../', [priv, pub])
+ _whimsy_panel_table(title: 'Repo use by script') do
+ _table.table.table_hover do
+ _thead_ do
+ _tr do
+ _th 'Private repos used'
+ _th 'Public repos used'
+ end
+ scan.each do |file, (privlines, publines)|
+ _tbody do
+ _tr_ do
+ _td :colspan => '2' do
+ _code file
+ end
+ end
+ _tr do
+ _td do
+ privlines.each do |l|
+ _ l
+ _br
+ end
+ end
+ _td do
+ publines.each do |l|
+ _ l
+ _br
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/www/members/security-subs.cgi b/www/members/security-subs.cgi
index 9d9a241..37cf3a2 100755
--- a/www/members/security-subs.cgi
+++ b/www/members/security-subs.cgi
@@ -1,5 +1,5 @@
#!/usr/bin/env ruby
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'wunderbar/bootstrap'
require 'whimsy/asf'
require 'whimsy/asf/mlist'
@@ -8,8 +8,17 @@
/^archive-asf-private@cust-asf\.ponee\.io$/,
/^private@mbox-vm\.apache\.org$/,
/^security-archive@.*\.apache\.org$/,
+ /^apmail-\w+-security-archive@www.apache.org/, # Direct subscription
]
+def isArchiver?(email)
+ WHITELIST.any? {|regex| email =~ regex}
+end
+
+NOSUBSCRIBERS = 'No subscribers'
+MINSUB = 3
+TOOFEW = "Not enough subscribers (< #{MINSUB})"
+
# ensure that there is a trailing slash (so relative paths will work)
if not ENV['PATH_INFO']
print "Status: 302 Found\r\nLocation: #{ENV['SCRIPT_URI']}/\r\n\r\n"
@@ -26,14 +35,47 @@
_html do
_whimsy_body(
- title: "Security Mailing List Subscriptions"
+ title: "Security Mailing List Subscriptions",
+ breadcrumbs: {
+ subscriptions: '.'
+ }
+
) do
path = ENV['PATH_INFO'].sub('/', '')
if path == ''
- _ul.list_group do
- lists.each do |dom, subs|
- _li.list_group_item do
- _a dom, href: dom
+ _p do
+ _ 'The counts below exclude the archivers, using the highlights: '
+ _span.bg_danger NOSUBSCRIBERS
+ _span.bg_warning TOOFEW
+ end
+ _table.table.table_responsive do
+ _tr do
+ _th.col_xs_1.text_right 'count'
+ _th.col_xs_3 'project'
+ _th.col_xs_1.text_right 'count'
+ _th.col_xs_3 'project'
+ _th.col_xs_1.text_right 'count'
+ _th.col_xs_3 'project'
+ # cols must add up to twelve
+ end
+ lists.each_slice(3) do |slice|
+ _tr do
+ slice.each do |dom, subs|
+ arch = subs.select{|sub| isArchiver?(sub)}.length
+ subcount = (subs.length - arch)
+ options = {}
+ if subcount == 0
+ options = {class: 'bg-danger', title: NOSUBSCRIBERS}
+ elsif subcount < MINSUB
+ options = {class: 'bg-warning', title: TOOFEW}
+ end
+ _td.text_right options do
+ _ subcount
+ end
+ _td do
+ _a dom, href: dom
+ end
+ end
end
end
end
@@ -42,6 +84,71 @@
podling = ASF::Podling.find(path)
committee = ASF::Committee.find(path)
project = ASF::Project.find(path)
+ colors=Hash.new{|h,k| h[k]=0} # counts of colors
+ order=['bg-danger', 'bg-warning', 'bg-info', 'bg-success', ''] # sort order
+ subh = Hash[
+ lists[path].map do |email|
+ name = '*UNKNOWN*'
+ if WHITELIST.any? {|regex| email =~ regex}
+ person = nil
+ name = '(archiver)'
+ color = ''
+ else
+ person = ASF::Person.find_by_email(email)
+ if person
+ name = person.public_name
+ if person.asf_member? or project.owners.include? person
+ color = 'bg-success'
+ elsif project.members.include? person
+ color = 'bg-info'
+ else
+ color = 'bg-warning'
+ end
+ else
+ color = 'bg-danger'
+ end
+ end
+ colors[color] += 1
+ [email, {person: person , color: color, name: name}]
+ end
+ ].sort_by {|k,v| [order.index(v[:color]),v[:name]]}
+
+ _table do
+ _tr do
+ _th 'Count '
+ _th 'Legend'
+ end
+ _tr do
+ _td colors['bg-danger']
+ _td class: 'bg-danger' do
+ _ 'Person (email) not recognised'
+ end
+ end
+ _tr do
+ _td colors['bg-warning']
+ _td class: 'bg-warning' do
+ _ 'ASF committer not associated with the project'
+ end
+ end
+ _tr do
+ _td colors['bg-info']
+ _td class: 'bg-info' do
+ _ 'Project committer - not on (P)PMC'
+ end
+ end
+ _tr do
+ _td colors['bg-success']
+ _td class: 'bg-success' do
+ _ 'ASF member or project member'
+ end
+ end
+ _tr do
+ _td colors['']
+ _td do
+ _ 'Archiver (there are expected to be up to 3 archivers)'
+ end
+ end
+ end
_h2 do
if podling
_a podling.display_name,
@@ -50,6 +157,9 @@
_a committee.display_name,
href: "../../roster/committee/#{committee.id}"
end
+ _span class: 'small' do
+ _a "(security@#{path}.apache.org)", href: "https://lists.apache.org/list.html?security@#{path}.apache.org"
+ end
end
_table.table do
@@ -61,19 +171,10 @@
end
_tbody do
- lists[path].sort_by {|email| email.downcase}.each do |email|
- person = ASF::Person.find_by_email(email)
- if person
- if person.asf_member? or project.members.include? person
- color = 'bg-success'
- else
- color = 'bg-warning'
- end
- elsif WHITELIST.any? {|regex| email =~ regex}
- color = ''
- else
- color = 'bg-danger'
- end
+ subh.each do |email, hash|
+ color = hash[:color]
+ person = hash[:person]
+ name = hash[:name]
_tr class: color do
_td email
@@ -81,22 +182,24 @@
if person
if person.asf_member?
_b do
- _a person.public_name,
- href: "../../roster/committer/#{person.id}"
+ _a name, href: "../../roster/committer/#{person.id}"
end
else
- _a person.public_name,
- href: "../../roster/committer/#{person.id}"
+ _a name, href: "../../roster/committer/#{person.id}"
end
- elsif WHITELIST.any? {|regex| email =~ regex}
- _ '(archiver)'
+ else
+ _ name
end
end
end
end
end
end
- _p 'Note that there are expected to be upto 3 archivers'
+ else
+ _h3 class: 'bg-warning' do
+ _ "Could not find a security list for the project #{path}"
+ end
+ _br
end
end
end
diff --git a/www/members/subscriptions.cgi b/www/members/subscriptions.cgi
index 0d68e82..e69ae5e 100755
--- a/www/members/subscriptions.cgi
+++ b/www/members/subscriptions.cgi
@@ -1,6 +1,6 @@
#!/usr/bin/env ruby
PAGETITLE = "Apache members@ Subscription Crosscheck" # Wvisible:members
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'wunderbar'
require 'whimsy/asf'
@@ -8,15 +8,7 @@
require 'wunderbar/bootstrap'
require 'wunderbar/jquery/stupidtable'
-ARCHIVERS = %w(
- private@mbox-vm.apache.org
- members-archive@apache.org
- archive-asf-private@cust-asf.ponee.io
- members@mmpoc.apache.org
- members@whimsy-vm4.apache.org
-)
-
-subscribers, modtime = ASF::MLIST.members_subscribers
+subscribers, modtime = ASF::MLIST.members_subscribers(false) # excluding archivers
_html do
_body? do
@@ -91,7 +83,6 @@
_th 'name', data_sort: 'string'
end
subscriptions.sort.each do |id, person, email|
- next if ARCHIVERS.include? email
_tr_ do
if id.include? '*'
_td.text_danger id
diff --git a/www/members/watch.cgi b/www/members/watch.cgi
index 1079cb4..e251727 100755
--- a/www/members/watch.cgi
+++ b/www/members/watch.cgi
@@ -1,6 +1,6 @@
#!/usr/bin/env ruby
PAGETITLE = "Potential ASF Member Watch List" # Wvisible:members
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'wunderbar'
require 'whimsy/asf'
@@ -101,9 +101,6 @@
list = {} # Avoid lint errors of shadowing
if request =~ /multiple/
_h2_ 'Active In Multiple Committees'
-# list = ASF::Committee.list.map {|committee| committee.members}.
-# reduce(&:+).group_by {|person| person}.
-# delete_if {|person,list| list.length<3}.keys
# Use actual PMCs rather than LDAP derived
list = ASF::Committee.pmcs.map {|pmc| pmc.roster.keys}.
reduce(&:+).group_by {|uid| uid}.
@@ -135,9 +132,9 @@
# for efficiency, preload public_names, member status, and
# nominees
- people = ASF::Person.preload('cn', list)
- members = ASF::Member.status
- nominees = ASF::Person.member_nominees
+ ASF::Person.preload('cn', list)
+ ASF::Member.status
+ ASF::Person.member_nominees
_table.table do
diff --git a/www/members/whatif.cgi b/www/members/whatif.cgi
index aada25e..c8e6fc9 100755
--- a/www/members/whatif.cgi
+++ b/www/members/whatif.cgi
@@ -1,5 +1,5 @@
#!/usr/bin/env ruby
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'whimsy/asf/config'
require 'whimsy/asf/svn'
diff --git a/www/officers/acreq.cgi b/www/officers/acreq.cgi
index 8d6771c..eb356bb 100755
--- a/www/officers/acreq.cgi
+++ b/www/officers/acreq.cgi
@@ -1,6 +1,6 @@
#!/usr/bin/env ruby
PAGETITLE = "Apache Account Submission Helper Form" # Wvisible:infra accounts
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'wunderbar'
require 'wunderbar/bootstrap'
require 'wunderbar/jquery'
diff --git a/www/officers/board-stats.cgi b/www/officers/board-stats.cgi
index 5dc9c80..a541fef 100755
--- a/www/officers/board-stats.cgi
+++ b/www/officers/board-stats.cgi
@@ -1,6 +1,6 @@
#!/usr/bin/env ruby
PAGETITLE = "Board Meeting Statistics since 2007" # Wvisible:meeting
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'whimsy/asf'
require 'whimsy/asf/agenda'
diff --git a/www/officers/mlreq.cgi b/www/officers/mlreq.cgi
index b319c7c..07bcb8a 100755
--- a/www/officers/mlreq.cgi
+++ b/www/officers/mlreq.cgi
@@ -1,6 +1,6 @@
#!/usr/bin/env ruby
PAGETITLE = "Apache Mailing list Request Form" # Wvisible:infra mail list
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'wunderbar'
require 'shellwords'
require 'mail'
diff --git a/www/pods.cgi b/www/pods.cgi
index 1b9d0a4..6de5f8c 100755
--- a/www/pods.cgi
+++ b/www/pods.cgi
@@ -9,7 +9,7 @@
exit
end
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'json'
require 'net/http'
require 'time' # for httpdate
diff --git a/www/racktest/config.ru b/www/racktest/config.ru
index 373c732..463fbfb 100644
--- a/www/racktest/config.ru
+++ b/www/racktest/config.ru
@@ -1 +1,10 @@
-run lambda {|env| [200, {'Content-Type' => 'text/plain'}, [env.inspect]]}
+require 'json'
+
+run lambda {|env|
+ env = env.to_a.sort.to_h
+ env.delete('PASSENGER_CONNECT_PASSWORD')
+ env.delete('SECRET_KEY_BASE')
+ env.delete('HTTP_AUTHORIZATION')
+
+ [ 200, {'Content-Type' => 'text/plain'}, [JSON.pretty_generate(env)] ]
+}
diff --git a/www/roster/main.rb b/www/roster/main.rb
index c25bc88..7971489 100755
--- a/www/roster/main.rb
+++ b/www/roster/main.rb
@@ -39,10 +39,11 @@
get '/' do
if env['REQUEST_URI'].end_with? '/'
+ ASF::Person.preload(['asf-banned','loginShell']) # so can get inactive count
@committers = ASF::Person.list
@committees = ASF::Committee.pmcs
@nonpmcs = ASF::Committee.nonpmcs
- @members = ASF::Member.list.keys - ASF::Member.status.keys
+ @members = ASF::Member.list.keys - ASF::Member.status.keys # i.e. active member ids
@groups = Group.list
@podlings = ASF::Podling.to_h.values
_html :index
@@ -157,6 +158,24 @@
end
post '/committer/:userid/:file' do |name, file|
+ # Workround for handling arrays
+ # if the key :array_prefix is defined, the value is assumed to be the prefix for
+ # a list of values with the names: prefix1, prefix2 etc
+ # All non-empty values are collected and stored in an array which is added to the
+ # params with the key prefix
+ prefix = params.delete(:array_prefix)
+ if prefix
+ array = []
+ count = 1
+ loop do
+ key = prefix+count.to_s
+ entry = params.delete(key)
+ break unless entry # no key means end of sequence
+ array << entry if entry.length > 0
+ count += 1
+ end
+ params[prefix] = array
+ end
_json :"actions/#{params[:file]}"
end
@@ -173,7 +192,6 @@
get '/group/' do
@groups = Group.list
- @podlings = ASF::Podling.to_h
_html :groups
end
@@ -216,7 +234,7 @@
@auth = Auth.info(env)
user = ASF::Person.find(env.user)
- @auth[:ipmc] = ASF::Committee.find('incubator').members.include? user
+ @auth[:ipmc] = ASF::Committee.find('incubator').owners.include? user
@ppmc = PPMC.serialize(name, env)
pass unless @ppmc
diff --git a/www/roster/models/committee.rb b/www/roster/models/committee.rb
index 76a64ff..6798e07 100644
--- a/www/roster/models/committee.rb
+++ b/www/roster/models/committee.rb
@@ -3,7 +3,7 @@
response = {}
pmc = ASF::Committee.find(id)
- return if pmc.nonpmc? # Only show PMCs
+ return unless pmc.pmc? # Only show PMCs
members = pmc.owners
committers = pmc.committers
return if members.empty? and committers.empty?
@@ -37,6 +37,7 @@
subscribers, subtime = ASF::MLIST.list_subscribers(pmc.mail_list) # counts only
analysePrivateSubs = currentUser.asf_member?
unless analysePrivateSubs # check for private moderator if not already allowed access
+ # TODO match using canonical emails
user_mail = currentUser.all_mail || []
pMods = moderators["private@#{pmc.mail_list}.apache.org"] || []
analysePrivateSubs = !(pMods & user_mail).empty?
@@ -52,19 +53,37 @@
lists = lists.select {|list, mode| mode == 'public'}
end
- roster = pmc.roster.dup
- roster.each {|key, info| info[:role] = 'PMC member'}
+ roster = pmc.roster.dup # from committee-info
+ # ensure PMC members are all processed even they don't belong to the owner group
+ roster.each do |key, info|
+ info[:role] = 'PMC member'
+ next if pmc.ownerids.include?(key) # skip the rest (expensive) if person is in the owner group
+ person = ASF::Person[key]
+ if analysePrivateSubs
+ # Analyse the subscriptions, matching against canonicalised personal emails
+ allMail = person.all_mail.map{|m| ASF::Mail.to_canonical(m.downcase)}
+ # pSubs is already downcased
+ # TODO should it be canonicalised as well above?
+ roster[key]['notSubbed'] = (allMail & pSubs.map{|m| ASF::Mail.to_canonical(m)}).empty?
+ unMatchedSubs.delete_if {|k| allMail.include? ASF::Mail.to_canonical(k.downcase)}
+ unMatchedSecSubs.delete_if {|k| allMail.include? ASF::Mail.to_canonical(k.downcase)}
+ end
+ roster[key]['githubUsername'] = (person.attrs['githubUsername'] || []).join(', ')
+ end
- members.each do |person|
+ members.each do |person| # process the owners
roster[person.id] ||= {
name: person.public_name,
- role: 'PMC member'
+ role: 'PMC member' # TODO not strictly true, as CI is the canonical source
}
if analysePrivateSubs
- allMail = person.all_mail.map{|m| m.downcase}
- roster[person.id]['notSubbed'] = (allMail & pSubs).empty?
- unMatchedSubs.delete_if {|k| allMail.include? k.downcase}
- unMatchedSecSubs.delete_if {|k| allMail.include? k.downcase}
+ # Analyse the subscriptions, matching against canonicalised personal emails
+ allMail = person.all_mail.map{|m| ASF::Mail.to_canonical(m.downcase)}
+ # pSubs is already downcased
+ # TODO should it be canonicalised as well above?
+ roster[person.id]['notSubbed'] = (allMail & pSubs.map{|m| ASF::Mail.to_canonical(m)}).empty?
+ unMatchedSubs.delete_if {|k| allMail.include? ASF::Mail.to_canonical(k.downcase)}
+ unMatchedSecSubs.delete_if {|k| allMail.include? ASF::Mail.to_canonical(k.downcase)}
end
roster[person.id]['ldap'] = true
roster[person.id]['githubUsername'] = (person.attrs['githubUsername'] || []).join(', ')
@@ -114,7 +133,7 @@
}
nonASFmails.each {|k,v|
@people.each do |person|
- if person[:mail].any? {|mail| mail.downcase == k.downcase}
+ if person[:mail].any? {|mail| ASF::Mail.to_canonical(mail.downcase) == ASF::Mail.to_canonical(k.downcase)}
nonASFmails[k] = person[:id]
end
end
diff --git a/www/roster/models/committer.rb b/www/roster/models/committer.rb
index 34f2733..618bdbb 100644
--- a/www/roster/models/committer.rb
+++ b/www/roster/models/committer.rb
@@ -44,7 +44,9 @@
response[:name] = name
- response[:mail] = person.all_mail
+ response[:email_forward] = person.mail # forwarding
+ response[:email_alt] = person.alt_email # alternates
+ response[:email_other] = person.all_mail - person.mail - person.alt_email # others (ASF mail/ICLA mail if different)
unless person.pgp_key_fingerprints.empty?
response[:pgp] = person.pgp_key_fingerprints
@@ -54,12 +56,14 @@
response[:ssh] = person.ssh_public_keys
end
+ response[:host] = person.attrs['host'] || ['(none)']
+
if person.attrs['asf-sascore']
- response[:sascore] = person.attrs['asf-sascore'].first
+ response[:sascore] = person.attrs['asf-sascore'].first # should only be one, but is returned as array
end
if person.attrs['githubUsername']
- response[:githubUsername] = person.githubUsername
+ response[:githubUsername] = person.attrs['githubUsername'] # always return array
end
response[:urls] = person.urls unless person.urls.empty?
@@ -69,7 +73,8 @@
response[:groups] = person.services
response[:committer] = []
response[:podlings] = []
- pmc_names = ASF::Committee.pmcs.map(&:name) # From CI
+ pmcs = ASF::Committee.pmcs
+ pmc_names = pmcs.map(&:name) # From CI
podlings = ASF::Podling.current.map(&:id)
# Add group names unless they are a PMC group
@@ -182,6 +187,23 @@
end
end
+ response[:pmcs] = []
+ response[:nonpmcs] = []
+
+ pmcs.each do |pmc|
+ response[:pmcs] << pmc.name if pmc.roster.include?(person.id)
+ response[:chairOf] << pmc.name if pmc.chairs.map{|ch| ch[:id]}.include?(person.id)
+ end
+ response[:pmcs].sort!
+
+ response[:nonPMCchairOf] = [] # use separate list to avoid missing pmc-chair warnings
+ nonpmcs = ASF::Committee.nonpmcs
+ nonpmcs.each do |nonpmc|
+ response[:nonpmcs] << nonpmc.name if nonpmc.roster.include?(person.id)
+ response[:nonPMCchairOf] << nonpmc.name if nonpmc.chairs.map{|ch| ch[:id]}.include?(person.id)
+ end
+ response[:nonpmcs].sort!
+
response
end
end
diff --git a/www/roster/models/group.rb b/www/roster/models/group.rb
index 8de7346..212cbf2 100644
--- a/www/roster/models/group.rb
+++ b/www/roster/models/group.rb
@@ -10,7 +10,7 @@
groups.map! {|group| [group, "LDAP group"]}
# add services...
- groups += ASF::Service.listcns.map {|service| [service, "LDAP service"]}
+ groups += ASF::Service.listcns.reject{|s| s=='apldap'}.map {|service| [service, "LDAP service"]}
# add authorization (asf and pit)
groups += ASF::Authorization.new('asf').to_h.
diff --git a/www/roster/models/nonpmc.rb b/www/roster/models/nonpmc.rb
index 8453fcb..6b7776c 100644
--- a/www/roster/models/nonpmc.rb
+++ b/www/roster/models/nonpmc.rb
@@ -8,7 +8,7 @@
committers = cttee.committers
# Hack to fix unusual mail_list values e.g. press@apache.org
mail_list = cttee.mail_list.sub(/@.*/,'')
- mail_list = 'legal' if mail_list =~ /^legal-/
+ mail_list = 'legal' if mail_list =~ /^legal-/ unless cttee.name == 'dataprivacy'
mail_list = 'fundraising' if mail_list =~ /^fundraising-/
ASF::Committee.load_committee_info
diff --git a/www/roster/models/ppmc.rb b/www/roster/models/ppmc.rb
index 400a4e4..a833508 100644
--- a/www/roster/models/ppmc.rb
+++ b/www/roster/models/ppmc.rb
@@ -9,7 +9,8 @@
list =~ /^(incubator-)?#{ppmc.mail_list}\b/
end
- members = ppmc.members
+ committers = ppmc.members
+ owners = ppmc.owners
# separate out the known ASF members and extract any matching committer details
unknownSubs = []
@@ -25,12 +26,13 @@
unMatchedSubs = [] # unknown private@ subscribers
currentUser = ASF::Person.find(env.user)
analysePrivateSubs = false # whether to show missing private@ subscriptions
- if currentUser.asf_member? or members.include? currentUser
+ if currentUser.asf_member? or owners.include? currentUser
require 'whimsy/asf/mlist'
moderators, modtime = ASF::MLIST.list_moderators(ppmc.mail_list, true)
subscribers, subtime = ASF::MLIST.list_subscribers(ppmc.mail_list, true) # counts only
analysePrivateSubs = currentUser.asf_member?
unless analysePrivateSubs # check for private moderator if not already allowed access
+ # TODO match using canonical emails
user_mail = currentUser.all_mail || []
pMods = moderators["private@#{ppmc.mail_list}.apache.org"] || []
analysePrivateSubs = !(pMods & user_mail).empty?
@@ -49,25 +51,38 @@
pmc = ASF::Committee.find('incubator')
ipmc = pmc.owners
incubator_committers = pmc.committers
- owners = ppmc.owners
- roster = members.map {|person|
- notSubbed = false
- if analysePrivateSubs and owners.include? person
- allMail = person.all_mail.map{|m| m.downcase}
- notSubbed = (allMail & pSubs).empty?
- unMatchedSubs.delete_if {|k| allMail.include? k.downcase}
- end
+ # Preload the committers; if a person has another role it will be set up below
+ roster = committers.map {|person|
[person.id, {
- notSubbed: notSubbed,
+ # notSubbed does not apply
name: person.public_name,
member: person.asf_member?,
icommit: incubator_committers.include?(person),
- role: (owners.include?(person) ? 'PPMC Member' : 'Committer'),
+ role: 'Committer',
githubUsername: (person.attrs['githubUsername'] || []).join(', ')
}]
}.to_h
+ # Merge the PPMC members (owners)
+ owners.each do |person|
+ notSubbed = false
+ if analysePrivateSubs
+ allMail = person.all_mail.map{|m| ASF::Mail.to_canonical(m.downcase)}
+ notSubbed = (allMail & pSubs.map{|m| ASF::Mail.to_canonical(m)}).empty?
+ unMatchedSubs.delete_if {|k| allMail.include? ASF::Mail.to_canonical(k.downcase)}
+ end
+ roster[person.id] = {
+ notSubbed: notSubbed,
+ name: person.public_name,
+ member: person.asf_member?,
+ icommit: incubator_committers.include?(person),
+ role: 'PPMC Member',
+ githubUsername: (person.attrs['githubUsername'] || []).join(', ')
+ }
+ end
+
+ # Finally merge the mentors
ppmc.mentors.each do |mentor|
person = ASF::Person.find(mentor)
roster[person.id] = {
@@ -79,9 +94,9 @@
githubUsername: (person.attrs['githubUsername'] || []).join(', ')
}
if analysePrivateSubs
- allMail = person.all_mail.map{|m| m.downcase}
- roster[person.id]['notSubbed'] = (allMail & pSubs).empty?
- unMatchedSubs.delete_if {|k| allMail.include? k.downcase}
+ allMail = person.all_mail.map{|m| ASF::Mail.to_canonical(m.downcase)}
+ roster[person.id]['notSubbed'] = (allMail & pSubs.map{|m| ASF::Mail.to_canonical(m)}).empty?
+ unMatchedSubs.delete_if {|k| allMail.include? ASF::Mail.to_canonical(k.downcase)}
end
end
@@ -108,7 +123,7 @@
}
nonASFmails.each {|k,v|
@people.each do |person|
- if person[:mail].any? {|mail| mail.downcase == k.downcase}
+ if person[:mail].any? {|mail| ASF::Mail.to_canonical(mail.downcase) == ASF::Mail.to_canonical(k.downcase)}
nonASFmails[k] = person[:id]
end
end
@@ -128,7 +143,7 @@
mentors: ppmc.mentors,
hasLDAP: ppmc.hasLDAP?,
owners: owners.map {|person| person.id},
- committers: members.map {|person| person.id},
+ committers: committers.map {|person| person.id},
roster: roster,
mail: Hash[lists.sort],
moderators: moderators,
diff --git a/www/roster/public_committee_info.rb b/www/roster/public_committee_info.rb
index b9dcad3..f34bb6d 100644
--- a/www/roster/public_committee_info.rb
+++ b/www/roster/public_committee_info.rb
@@ -54,7 +54,7 @@
chair: Hash[committee.chairs.map {|chair|
[chair[:id], {:name => chair[:name]} ]}],
roster: committee.roster.sort.to_h, # sort entries by uid
- pmc: !committee.nonpmc?
+ pmc: committee.pmc?
}]
}]
@@ -94,6 +94,7 @@
info[:committees].each { |pmc, entry|
next if pmc == 'infrastructure' # no dates
+ Wunderbar.warn "#{pmc}: no description found" if entry[:pmc] && ! entry[:description]
previouspmc = previous[pmc] # get the original details (if any)
if previouspmc # we have an existing entry
entry[:roster].each { |name, value|
diff --git a/www/roster/public_json_common.rb b/www/roster/public_json_common.rb
index c2ff8a2..ff4de5c 100644
--- a/www/roster/public_json_common.rb
+++ b/www/roster/public_json_common.rb
@@ -7,7 +7,7 @@
# Status updates: https://whimsy-test.apache.org/status/
#
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'whimsy/asf'
require 'json'
diff --git a/www/roster/public_ldap_projects.rb b/www/roster/public_ldap_projects.rb
index 976386c..007fd52 100644
--- a/www/roster/public_ldap_projects.rb
+++ b/www/roster/public_ldap_projects.rb
@@ -39,7 +39,7 @@
# committee status
committees = Hash[ASF::Committee.load_committee_info.map {|committee|
- [ committee.name.gsub(/[^-\w]/,'') , !committee.nonpmc? ]
+ [ committee.name.gsub(/[^-\w]/,'') , committee.pmc? ]
}]
# podling status
diff --git a/www/roster/views/actions/email_alt.json.rb b/www/roster/views/actions/email_alt.json.rb
new file mode 100644
index 0000000..22ec0ab
--- /dev/null
+++ b/www/roster/views/actions/email_alt.json.rb
@@ -0,0 +1,35 @@
+#
+# Update PGP keys attribute for a committer
+#
+
+person = ASF::Person.find(@userid)
+
+# report the previous value in the response
+_previous alt_email: person.alt_email # returns empty array if not defined
+
+if @email_alt # must agree with email_alt.js.rb
+
+ # report the new values
+ _replacement alt_email: @email_alt
+
+ @email_alt.each do |mail|
+ unless mail.match(URI::MailTo::EMAIL_REGEXP)
+ _error "Invalid email address '#{mail}'"
+ return
+ end
+ if mail.downcase.end_with? 'apache.org'
+ _error "Invalid email address '#{mail}' (must not be apache.org)"
+ return
+ end
+ end
+
+ # update LDAP
+ unless @dryrun
+ _ldap.update do
+ person.modify 'asf-altEmail', @email_alt
+ end
+ end
+end
+
+# return updated committer info
+_committer Committer.serialize(@userid, env)
diff --git a/www/roster/views/actions/email_forward.json.rb b/www/roster/views/actions/email_forward.json.rb
new file mode 100644
index 0000000..91826ca
--- /dev/null
+++ b/www/roster/views/actions/email_forward.json.rb
@@ -0,0 +1,39 @@
+#
+# Update PGP keys attribute for a committer
+#
+
+person = ASF::Person.find(@userid)
+
+# report the previous value in the response
+_previous mail: person.attrs['mail']
+
+if @email_forward # must agree with email_forward.js.rb
+
+ # report the new values
+ _replacement mail: @email_forward
+
+ @email_forward.each do |mail|
+ unless mail.match(URI::MailTo::EMAIL_REGEXP)
+ _error "Invalid email address '#{mail}'"
+ return
+ end
+ if mail.downcase.end_with? 'apache.org'
+ _error "Invalid email address '#{mail}' (must not be apache.org)"
+ return
+ end
+ end
+
+ if @email_forward.empty?
+ _error "Forwarding email address must not be empty!"
+ end
+
+ # update LDAP
+ unless @dryrun
+ _ldap.update do
+ person.modify 'mail', @email_forward
+ end
+ end
+end
+
+# return updated committer info
+_committer Committer.serialize(@userid, env)
diff --git a/www/roster/views/actions/github.json.rb b/www/roster/views/actions/github.json.rb
index 559d962..8273345 100644
--- a/www/roster/views/actions/github.json.rb
+++ b/www/roster/views/actions/github.json.rb
@@ -2,16 +2,33 @@
# Update GitHub username attribute for a committer
#
-# update LDAP
-_ldap.update do
- person = ASF::Person.find(@userid)
+person = ASF::Person.find(@userid)
- # report the previous value in the response
- _previous githubUsername: person.attrs['githubUsername']
+# report the previous value in the response
+_previous githubUsername: person.attrs['githubUsername']
- if @githubuser and not @dryrun
- person.modify 'githubUsername', @githubuser
+if @githubuser
+
+ # report the new values
+ _replacement githubUsername: @githubuser
+
+ @githubuser.each do |name|
+ # Should agree with the validation in github.js.rb
+ unless name =~ /^[-0-9a-zA-Z]+$/ # TODO: might need extending?
+ _error "'#{name}' is invalid: must be alphanumeric (or -)"
+ return
+ end
+ # TODO: perhaps check that https://github.com/name exists?
end
+
+ unless @dryrun
+ names = @githubuser.uniq{|n| n.downcase} # duplicates not allowed; case-blind
+ # update LDAP
+ _ldap.update do
+ person.modify 'githubUsername', names
+ end
+ end
+
end
# return updated committer info
diff --git a/www/roster/views/actions/pgpkeys.json.rb b/www/roster/views/actions/pgpkeys.json.rb
new file mode 100644
index 0000000..4a19d87
--- /dev/null
+++ b/www/roster/views/actions/pgpkeys.json.rb
@@ -0,0 +1,38 @@
+#
+# Update PGP keys attribute for a committer
+#
+
+person = ASF::Person.find(@userid)
+
+# report the previous value in the response
+_previous asf_pgpKeyFingerprint: person.attrs['asf-pgpKeyFingerprint']
+
+if @pgpkeys # must agree with pgpkeys.js.rb
+
+ # report the new values
+ _replacement pgpKeyFingerprint: @pgpkeys
+
+ fprints = [] # collect the fingerprints
+ @pgpkeys.each do |fp|
+ fprint = fp.gsub(' ','').upcase
+ if fprint =~ /^[0-9A-F]{40}$/
+ fprints << fprint
+ else
+ _error "'#{fp}' is invalid: expecting 40 hex characters (plus optional spaces)"
+ return
+ end
+ end
+ # convert to canonical format
+ fprints = fprints.uniq.map do |n| # duplicates not allowed
+ "%s %s %s %s %s %s %s %s %s %s" % n.scan(/..../)
+ end
+ # update LDAP
+ unless @dryrun
+ _ldap.update do
+ person.modify 'asf-pgpKeyFingerprint', fprints
+ end
+ end
+end
+
+# return updated committer info
+_committer Committer.serialize(@userid, env)
diff --git a/www/roster/views/actions/sascore.json.rb b/www/roster/views/actions/sascore.json.rb
index afee769..62ba2d1 100644
--- a/www/roster/views/actions/sascore.json.rb
+++ b/www/roster/views/actions/sascore.json.rb
@@ -2,14 +2,14 @@
# Update LDAP SpamAssassin score attribute for a committer
#
-# update LDAP
-_ldap.update do
- person = ASF::Person.find(@userid)
+person = ASF::Person.find(@userid)
- # report the previous value in the response
- _previous sascore: person.attrs['asf-sascore']
+# report the previous value in the response
+_previous sascore: person.attrs['asf-sascore']
- if @sascore and not @dryrun
+if @sascore and not @dryrun
+ # update LDAP
+ _ldap.update do
person.modify 'asf-sascore', @sascore
end
end
diff --git a/www/roster/views/actions/sshkeys.json.rb b/www/roster/views/actions/sshkeys.json.rb
new file mode 100644
index 0000000..6476c70
--- /dev/null
+++ b/www/roster/views/actions/sshkeys.json.rb
@@ -0,0 +1,26 @@
+#
+# Update PGP keys attribute for a committer
+#
+
+person = ASF::Person.find(@userid)
+
+# report the previous value in the response
+_previous sshPublicKey: person.attrs['sshPublicKey']
+
+if @sshkeys # must agree with sshkeys.js.rb
+
+ # report the new values
+ _replacement sshPublicKey: @sshkeys
+
+ # TODO: add validation?
+
+ # update LDAP
+ unless @dryrun
+ _ldap.update do
+ person.modify 'sshPublicKey', @sshkeys
+ end
+ end
+end
+
+# return updated committer info
+_committer Committer.serialize(@userid, env)
diff --git a/www/roster/views/actions/urls.json.rb b/www/roster/views/actions/urls.json.rb
new file mode 100644
index 0000000..be980b0
--- /dev/null
+++ b/www/roster/views/actions/urls.json.rb
@@ -0,0 +1,38 @@
+#
+# Update PGP keys attribute for a committer
+#
+
+person = ASF::Person.find(@userid)
+
+# report the previous value in the response
+_previous asf_personalURL: person.attrs['asf-personalURL']
+
+if @urls # must agree with urls.js.rb
+
+ # report the new values
+ _replacement asf_personalURL: @urls
+
+ @urls.each do |url|
+# next
+ begin
+ uri = URI.parse(url)
+ rescue
+ _error "Cannot parse URL: #{url}"
+ return
+ end
+ unless uri.scheme =~ /^https?$/ && uri.host.length > 5
+ _error "Invalid http(s) URL: #{url}"
+ return
+ end
+ end
+
+ # update LDAP
+ unless @dryrun
+ _ldap.update do
+ person.modify 'asf-personalURL', @urls
+ end
+ end
+end
+
+# return updated committer info
+_committer Committer.serialize(@userid, env)
diff --git a/www/roster/views/app.js.rb b/www/roster/views/app.js.rb
index 6e089a7..796aa9b 100644
--- a/www/roster/views/app.js.rb
+++ b/www/roster/views/app.js.rb
@@ -16,10 +16,12 @@
require_relative 'nonpmc/add'
require_relative 'nonpmc/mod'
-require_relative 'person'
+require_relative 'person/main'
require_relative 'person/fullname'
require_relative 'person/urls'
-require_relative 'person/email'
+require_relative 'person/email_alt'
+require_relative 'person/email_forward'
+require_relative 'person/email_other'
require_relative 'person/pgpkeys'
require_relative 'person/sshkeys'
require_relative 'person/github'
diff --git a/www/roster/views/committees.html.rb b/www/roster/views/committees.html.rb
index d451e6f..89401ce 100644
--- a/www/roster/views/committees.html.rb
+++ b/www/roster/views/committees.html.rb
@@ -7,25 +7,42 @@
_link rel: 'stylesheet', href: "stylesheets/app.css?#{cssmtime}"
_whimsy_body(
title: 'ASF PMC Listing',
+ subtitle: 'List of all Top Level Projects',
+ relatedtitle: 'More Useful Links',
+ related: {
+ "/committers/tools" => "Whimsy All Tools Listing",
+ "https://svn.apache.org/repos/private/committers/" => "Checkout the private 'committers' repo for Committers",
+ "https://github.com/apache/whimsy/blob/master/www#{ENV['SCRIPT_NAME']}" => "See This Source Code",
+ "mailto:dev@whimsical.apache.org?subject=[FEEDBACK] members/index idea" => "Email Feedback To dev@whimsical"
+ },
+ helpblock: -> {
+ _p do
+ _ 'A full list of Apache PMCs; click on the name for a detail page about that PMC. '
+ _ 'You can also view (Member-private) '
+ _a href: '/roster/nonpmc/' do
+ _span.glyphicon.glyphicon_lock :aria_hidden, class: 'text-primary', aria_label: 'ASF Members Private'
+ _ 'Non-PMC Committees (Brand, Legal, etc.)'
+ end
+ _ ' and '
+ _a href: '/roster/group/' do
+ _span.glyphicon.glyphicon_lock :aria_hidden, class: 'text-primary', aria_label: 'ASF Members Private'
+ _ 'Other Groups of various kinds (from LDAP or auth).'
+ end
+ end
+ _p do
+ _ 'Chair names in BOLD below are also ASF Members. Click on column names in table to sort; jump to A-Z project listings here:'
+ _br
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ".each_char do |c|
+ _a c, href: "committee/##{c}"
+ end
+ _ '(note: the links only work properly if the page is sorted by project name ascending)'
+ end
+ },
breadcrumbs: {
roster: '.',
committee: 'committee/'
}
) do
- _p do
- _ 'A full list of Apache PMCs; click on the name for a detail page about that PMC. Non-PMC groups of various kinds '
- _a href: '/roster/group/' do
- _span.glyphicon.glyphicon_lock :aria_hidden, class: 'text-primary', aria_label: 'ASF Members Private'
- _ 'are listed privately.'
- end
- end
- _p do
- _ 'Click on column names to sort.'
- _{" "}
- "ABCDEFGHIJKLMNOPQRSTUVWXYZ".each_char do |c|
- _a c, href: "committee/##{c}"
- end
- end
_table.table.table_hover do
_thead do
@@ -39,14 +56,15 @@
prev_letter=nil
@committees.sort_by {|pmc| pmc.display_name.downcase}.each do |pmc|
letter = pmc.display_name.upcase[0]
- _tr_ do
+ if letter != prev_letter
+ options = {id: letter}
+ else
+ options = {}
+ end
+ prev_letter = letter
+ _tr_ options do
_td do
- if letter != prev_letter
- _a pmc.display_name, href: "committee/#{pmc.name}", id: letter
- else
- _a pmc.display_name, href: "committee/#{pmc.name}"
- end
- prev_letter = letter
+ _a pmc.display_name, href: "committee/#{pmc.name}"
end
_td do
diff --git a/www/roster/views/groups.html.rb b/www/roster/views/groups.html.rb
index 446fb19..4004e31 100644
--- a/www/roster/views/groups.html.rb
+++ b/www/roster/views/groups.html.rb
@@ -59,7 +59,6 @@
_tr do
_th.sorting_asc 'Name', data_sort: 'string-ins'
_th 'Group type', data_sort: 'string'
- _th 'Notes', data_sort: 'notes'
end
end
@@ -69,16 +68,6 @@
_tr_ do
_td {_a name, href: "group/#{name}"}
_td type
-
- if @podlings[name]
- if @podlings[name].status == 'retired'
- _td.issue "retired podling"
- else
- _td "#{@podlings[name].status} podling"
- end
- else
- _td
- end
end
end
end
diff --git a/www/roster/views/index.html.rb b/www/roster/views/index.html.rb
index 5834de2..5ab9cfd 100644
--- a/www/roster/views/index.html.rb
+++ b/www/roster/views/index.html.rb
@@ -26,7 +26,12 @@
_a 'Committers', href: 'committer/'
end
- _td 'Search for committers by name, user id, or email address'
+ _td do
+ _ 'Search for committers by name, user id, or email address'
+ _ ' (includes '
+ _ @committers.select{|c| c.inactive?}.length
+ _ ' inactive accounts)'
+ end
end
### members
diff --git a/www/roster/views/nonpmcs.html.rb b/www/roster/views/nonpmcs.html.rb
index adedeec..9b7b4ff 100644
--- a/www/roster/views/nonpmcs.html.rb
+++ b/www/roster/views/nonpmcs.html.rb
@@ -13,7 +13,10 @@
}
) do
_p do
- _ 'A full list of Apache committees that are not PMCs; click on the name for a detail page about that committee. Other groups of various kinds '
+ _ 'A full list of Apache committees that are not PMCs; click on the name for a detail page about that committee.'
+ _ '(from committee-info.txt)'
+ _br
+ _ 'Other groups of various kinds '
_a href: '/roster/group/' do
_span.glyphicon.glyphicon_lock :aria_hidden, class: 'text-primary', aria_label: 'ASF Members Private'
_ 'are listed privately.'
diff --git a/www/roster/views/person/email.js.rb b/www/roster/views/person/email.js.rb
deleted file mode 100644
index ee3fd55..0000000
--- a/www/roster/views/person/email.js.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-#
-# Render and edit a person's E-mail addresses
-#
-
-class PersonEmail < Vue
- def render
- committer = @@person.state.committer
-
- _div.row do
- _div.name 'Email addresses'
-
- _div.value do
- _ul committer.mail do |url|
- _li do
- _a url, href: 'mailto:' + url
- end
- end
- end
- end
- end
-end
diff --git a/www/roster/views/person/email_alt.js.rb b/www/roster/views/person/email_alt.js.rb
new file mode 100644
index 0000000..f8f5d71
--- /dev/null
+++ b/www/roster/views/person/email_alt.js.rb
@@ -0,0 +1,50 @@
+#
+# Render and edit a person's alt E-mail addresses
+#
+
+class PersonEmailAlt < Vue
+ def render
+ committer = @@person.state.committer
+
+ _div.row data_edit: 'email_alt' do
+ _div.name 'Email addresses (alt)'
+
+ _div.value do
+
+ if @@edit == :email_alt
+
+ _form method: 'post' do
+ current = 1
+ prefix = 'email_alt' # must agree with email_alt.json.rb
+ _input type: 'hidden', name: 'array_prefix', value: prefix
+
+ _div committer.email_alt do |key|
+ _input name: prefix + current, value: key, size: 30
+ _br
+ current += 1
+ end
+ # Spare field to allow new entry to be added
+ _input name: prefix + current, placeholder: '<alternate email>', size: 30
+ _br
+
+ _input type: 'submit', value: 'submit'
+ end
+
+ else
+
+ if committer.email_alt.length == 0
+ _ul do
+ _li '(none defined)'
+ end
+ else
+ _ul committer.email_alt do |mail|
+ _li do
+ _a mail, href: 'mailto:' + mail
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/www/roster/views/person/email_forward.js.rb b/www/roster/views/person/email_forward.js.rb
new file mode 100644
index 0000000..526c906
--- /dev/null
+++ b/www/roster/views/person/email_forward.js.rb
@@ -0,0 +1,44 @@
+#
+# Render and edit a person's forward E-mail addresses
+#
+
+class PersonEmailForwards < Vue
+ def render
+ committer = @@person.state.committer
+
+ _div.row data_edit: 'email_forward' do
+ _div.name 'Email forwarded to'
+
+ _div.value do
+
+ if @@edit == :email_forward
+
+ _form method: 'post' do
+ current = 1
+ prefix = 'email_forward' # must agree with email_forward.json.rb
+ _input type: 'hidden', name: 'array_prefix', value: prefix
+
+ _div committer.email_forward do |key|
+ _input name: prefix + current, value: key, size: 30
+ _br
+ current += 1
+ end
+ # Spare field to allow new entry to be added
+ _input name: prefix + current, placeholder: '<forwarding email>', size: 30
+ _br
+
+ _input type: 'submit', value: 'submit'
+ end
+
+ else
+
+ _ul committer.email_forward do |mail|
+ _li do
+ _a mail, href: 'mailto:' + mail
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/www/roster/views/person/email_other.js.rb b/www/roster/views/person/email_other.js.rb
new file mode 100644
index 0000000..08d12cb
--- /dev/null
+++ b/www/roster/views/person/email_other.js.rb
@@ -0,0 +1,21 @@
+#
+# Render a person's other E-mail address(es)
+#
+
+class PersonEmailOther < Vue
+ def render
+ committer = @@person.state.committer
+
+ _div.row do
+ _div.name 'Email addresses (other)'
+
+ _div.value do
+ _ul committer.email_other do |mail|
+ _li do
+ _a mail, href: 'mailto:' + mail
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/www/roster/views/person/github.js.rb b/www/roster/views/person/github.js.rb
index 27a9195..6f0991a 100644
--- a/www/roster/views/person/github.js.rb
+++ b/www/roster/views/person/github.js.rb
@@ -14,16 +14,39 @@
if @@edit == :github
_form method: 'post' do
- _input name: 'githubuser', value: committer.githubUsername
+ current = 1
+ prefix = 'githubuser'
+ _input type: 'hidden', name: 'array_prefix', value: prefix
+
+ _div committer.githubUsername do |name|
+ _input style: 'font-family:Monospace', size: 20, name: prefix + current, value: name
+ _br
+ current += 1
+ end
+ # Spare field to allow new entry to be added
+ _input style: 'font-family:Monospace', size: 20, name: prefix + current, placeholder: '<new GitHub name>'
+ _br
+
_input type: 'submit', value: 'submit'
end
else
-
- _a committer.githubUsername,
- href: "https://github.com/" + committer.githubUsername
+ if committer.githubUsername.empty?
+ _ul do
+ _li '(none defined)'
+ end
+ else
+ _ul committer.githubUsername do |gh|
+ _li do
+ _a gh, href: "https://github.com/" + gh +"/" # / catches trailing spaces
+ unless gh =~ /^[-0-9a-zA-Z]+$/ # should agree with the validation in github.json.rb
+ _ ' '
+ _span.bg_warning "Invalid: '#{gh}' expecting only alphanumeric and '-'"
+ end
+ end
+ end
+ end
end
-
end
end
end
diff --git a/www/roster/views/person.js.rb b/www/roster/views/person/main.js.rb
similarity index 72%
rename from www/roster/views/person.js.rb
rename to www/roster/views/person/main.js.rb
index dc6f14d..1a53557 100644
--- a/www/roster/views/person.js.rb
+++ b/www/roster/views/person/main.js.rb
@@ -23,18 +23,19 @@
end
# Personal URL
- if @committer.urls
- _PersonUrls person: self
+ if @committer.urls || @auth
+ @committer.urls ||= []
+ _PersonUrls person: self, edit: @edit
end
- # Committees
- committees = @committer.committees
- unless committees.empty?
+ # PMCs
+ noPMCsub = false
+ pmcs = @committer.pmcs
+ unless pmcs.empty?
_div.row do
- _div.name 'Committees'
+ _div.name 'PMCs'
_div.value do
- noPMCsub = false
- _ul committees do |pmc|
+ _ul pmcs do |pmc|
_li {
_a pmc, href: "committee/#{pmc}"
if @committer.privateNosub
@@ -46,19 +47,48 @@
if @committer.chairOf.include? pmc
_ ' (chair)'
end
+ unless @committer.committees.include?(pmc)
+ _b ' (not in LDAP committee group)'
+ end
}
end
+ if noPMCsub
+ _br
+ _p {
+ _ '(*) could not find a subscription to the private@ mailing list for this PMC'
+ _br
+ _ 'Perhaps the subscription address is not listed in the LDAP record'
+ _br
+ _ '(Note that digest subscriptions are not currently included)'
+ }
+ end
+ end
+ end
+ end
- if noPMCsub
- _br
- _p {
- _ '(*) could not find a subscription to the private@ mailing list for this committee'
- _br
- _ 'Perhaps the subscription address is not listed in the LDAP record'
- _br
- _ '(Note that digest subscriptions are not currently included)'
- }
- end
+ # Committees
+ missingPMCs = false
+ committees = @committer.committees
+ unless committees.empty?
+ _div.row do
+ _div.name 'Committees'
+ _div.value do
+ noPMCsub = false
+ _ul committees do |pmc|
+ next if @committer.pmcs.include? pmc
+ missingPMCs = true
+ _li {
+ _a pmc, href: "committee/#{pmc}"
+ if @committer.chairOf.include? pmc
+ _ ' (chair)'
+ end
+ }
+ end
+ if missingPMCs
+ _ 'In LDAP committee group, but not on the corresponding PMC'
+ else
+ _ '(excludes PMCs listed above)'
+ end
end
end
end
@@ -112,9 +142,33 @@
end
end
+ # Non-PMCs
+ nonpmcs = @committer.nonpmcs
+ unless nonpmcs.empty?
+ _div.row do
+ _div.name 'non-PMCs'
+ _div.value do
+ _ul nonpmcs do |nonpmc|
+ _li {
+ _a nonpmc, href: "nonpmc/#{nonpmc}"
+ if @committer.nonPMCchairOf.include? nonpmc
+ _ ' (chair)'
+ end
+ }
+ end
+ end
+ end
+ end
+
# Email addresses
- if @committer.mail
- _PersonEmail person: self
+ # always present
+ _PersonEmailForwards person: self, edit: @edit
+
+ # always present (even if an empty array)
+ _PersonEmailAlt person: self, edit: @edit
+
+ if @committer.email_other
+ _PersonEmailOther person: self # not editable
end
# Moderates
@@ -172,17 +226,29 @@
end
# PGP keys
- if @committer.pgp
- _PersonPgpKeys person: self
+ if @committer.pgp || @auth
+ @committer.pgp ||= []
+ _PersonPgpKeys person: self, edit: @edit
+ end
+
+ # hosts
+ _div.row do
+ _div.name 'Host Access'
+ _div.value do
+ # pre avoids wrapping on hyphens and reduces number of lines on the page
+ _pre @committer.host.join(' ')
+ end
end
# SSH keys
- if @committer.ssh
- _PersonSshKeys person: self
+ if @committer.ssh || @auth
+ @committer.ssh ||= []
+ _PersonSshKeys person: self, edit: @edit
end
# GitHub username
- if @committer.githubUsername
+ if @committer.githubUsername || @auth
+ @committer.githubUsername ||= []
_PersonGitHub person: self, edit: @edit
end
@@ -211,16 +277,16 @@
# SpamAssassin score
_PersonSascore person: self, edit: @edit
- # modal dialog for dry run results
+ # modal dialog for dry run results and errors
_div.modal.fade.wide_form tabindex: -1 do
_div.modal_dialog do
_div.modal_content do
_div.modal_header do
_button.close 'x', data_dismiss: 'modal'
- _h4 'Dry run results'
+ _h4 @response_title
end
_div.modal_body do
- _textarea value: JSON.stringify(@response, nil, 2), readonly: true
+ _textarea value: @response, readonly: true
end
_div.modal_footer do
_button.btn.btn_default 'Close', data_dismiss: 'modal'
@@ -316,9 +382,17 @@
},
complete: ->(response) do
+ json = response.responseJSON
# show results of dryrun
if formData[0] and formData[0].name == 'dryrun'
- @response = response.responseJSON
+ @response_title = 'Dry run results'
+ @response = JSON.stringify(json, nil, 2)
+ jQuery('div.modal').modal('show')
+ end
+
+ if json.error
+ @response_title = 'Error occurred'
+ @response = JSON.stringify(json, nil, 2)
jQuery('div.modal').modal('show')
end
diff --git a/www/roster/views/person/pgpkeys.js.rb b/www/roster/views/person/pgpkeys.js.rb
index 453fbd7..2c6b2d0 100644
--- a/www/roster/views/person/pgpkeys.js.rb
+++ b/www/roster/views/person/pgpkeys.js.rb
@@ -6,19 +6,55 @@
def render
committer = @@person.state.committer
- _div.row do
+ _div.row data_edit: 'pgpkeys' do
_div.name 'PGP keys'
_div.value do
- _ul committer.pgp do |key|
- _li do
- if key =~ /^[0-9a-fA-F ]+$/
- _samp do
- _a key, href: 'https://sks-keyservers.net/pks/lookup?' +
- 'op=index&search=0x' + key.gsub(' ', '')
+ if @@edit == :pgpkeys
+
+ _form method: 'post' do
+ current = 1
+ prefix = 'pgpkeys' # must agree with pgpkeys.json.rb
+ _input type: 'hidden', name: 'array_prefix', value: prefix
+
+ _div committer.pgp do |key|
+ _input style: 'font-family:Monospace', size: 52, name: prefix + current, value: key
+ _br
+ current += 1
+ end
+ # Spare field to allow new entry to be added
+ _input style: 'font-family:Monospace', size: 52, name: prefix + current, placeholder: '<enter a new 40 hex char key>'
+ _br
+
+ _input type: 'submit', value: 'submit'
+ end
+
+ else
+ if committer.pgp.empty?
+ _ul do
+ _li '(none defined)'
+ end
+ else
+ _ul committer.pgp do |key|
+ nbsp = "\u00A0" # non-breaking space as UTF-8
+ keynb = key.gsub(' ', nbsp) # ensure multiple spaces appear as such
+ _li do
+ if key =~ /^[0-9a-fA-F ]+$/
+ keysq = key.gsub(' ', '') # strip spaces for length check and lookup
+ _samp style: 'font-family:Monospace' do
+ _a keynb, href: 'https://sks-keyservers.net/pks/lookup?' +
+ 'op=index&fingerprint=on&search=0x' + keysq
+ unless keysq.length == 40
+ _span.bg_danger ' ?? Expecting exactly 40 hex characters (plus optional spaces)'
+ end
+ end
+ else
+ _samp style: 'font-family:Monospace' do
+ _ keynb
+ _span.bg_danger ' ?? Expecting exactly 40 hex characters (plus optional spaces)'
+ end
+ end
end
- else
- _samp key
end
end
end
diff --git a/www/roster/views/person/sshkeys.js.rb b/www/roster/views/person/sshkeys.js.rb
index 6b4ecdf..41b4142 100644
--- a/www/roster/views/person/sshkeys.js.rb
+++ b/www/roster/views/person/sshkeys.js.rb
@@ -6,13 +6,41 @@
def render
committer = @@person.state.committer
- _div.row do
+ _div.row data_edit: 'sshkeys' do
_div.name 'SSH keys'
_div.value do
- _ul committer.ssh do |key|
- _li.ssh do
- _pre.wide key
+
+ if @@edit == :sshkeys
+
+ _form method: 'post' do
+ current = 1
+ prefix = 'sshkeys' # must agree with sshkeys.json.rb
+ _input type: 'hidden', name: 'array_prefix', value: prefix
+
+ _div committer.ssh do |key|
+ _input style: 'font-family:Monospace', size: 100, name: prefix + current, value: key
+ _br
+ current += 1
+ end
+ # Spare field to allow new entry to be added
+ _input style: 'font-family:Monospace', size: 100, name: prefix + current, placeholder: '<enter a new ssh key>'
+ _br
+
+ _input type: 'submit', value: 'submit'
+ end
+
+ else
+ if committer.ssh.empty?
+ _ul do
+ _li '(none defined)'
+ end
+ else
+ _ul committer.ssh do |key|
+ _li.ssh do
+ _pre.wide key
+ end
+ end
end
end
end
diff --git a/www/roster/views/person/urls.js.rb b/www/roster/views/person/urls.js.rb
index 660e4c6..c8de9a8 100644
--- a/www/roster/views/person/urls.js.rb
+++ b/www/roster/views/person/urls.js.rb
@@ -6,13 +6,40 @@
def render
committer = @@person.state.committer
- _div.row do
+ _div.row data_edit: 'urls' do
_div.name 'Personal URL'
_div.value do
- _ul committer.urls do |url|
- _li {_a url, href: url}
- end
+ if @@edit == :urls
+
+ _form method: 'post' do
+ current = 1
+ prefix = 'urls' # must agree with urls.json.rb
+ _input type: 'hidden', name: 'array_prefix', value: prefix
+
+ _div committer.urls do |url|
+ _input name: prefix + current, value: url
+ _br
+ current += 1
+ end
+ # Spare field to allow new entry to be added
+ _input name: prefix + current, placeholder: '<enter a new URL>'
+ _br
+
+ _input type: 'submit', value: 'submit'
+ end
+
+ else
+ if committer.urls.empty?
+ _ul do
+ _li '(none defined)'
+ end
+ else
+ _ul committer.urls do |url|
+ _li {_a url, href: url}
+ end
+ end
+ end
end
end
end
diff --git a/www/roster/views/podlings.html.rb b/www/roster/views/podlings.html.rb
index 524fe15..5ae640d 100644
--- a/www/roster/views/podlings.html.rb
+++ b/www/roster/views/podlings.html.rb
@@ -2,6 +2,19 @@
# List of all Podings
#
+# Match name and aliases to find the entry
+def findName(podling, list)
+ if list.include?(podling.name)
+ return podling.name
+ end
+ podling.resourceAliases.each do |a|
+ if list.include? a
+ return a
+ end
+ end
+ return nil
+end
+
_html do
_link rel: 'stylesheet', href: "stylesheets/app.css?#{cssmtime}"
_style %{
@@ -57,7 +70,7 @@
end
_ ")"
end
-
+
_table.table.table_hover do
_thead do
_tr do
@@ -69,7 +82,9 @@
_tbody do
@podlings.sort_by {|podling| podling.name.downcase}.each do |podling|
- status = (@attic.include?(podling.name) ? 'attic' : podling.status)
+ attic = findName(podling, @attic)
+ pmc = findName(podling, @committees)
+ status = (attic ? 'attic' : podling.status)
_tr_ class: color[status] do
_td do
@@ -77,14 +92,14 @@
"http://incubator.apache.org/projects/#{podling.name}.html"
end
- if @committees.include? podling.name
+ if pmc
_td data_sort_value: "#{podling.status} - pmc" do
- _a podling.status, href: "committee/#{podling.name}"
+ _a podling.status, href: "committee/#{pmc}"
end
- elsif @attic.include? podling.name
+ elsif attic
_td data_sort_value: "#{podling.status} - attic" do
_a podling.status, href:
- "http://attic.apache.org/projects/#{podling.name}.html"
+ "http://attic.apache.org/projects/#{attic}.html"
end
else
_td podling.status
diff --git a/www/roster/views/ppmc/establish.text.rb b/www/roster/views/ppmc/establish.text.rb
index 83f8d05..d8a6813 100644
--- a/www/roster/views/ppmc/establish.text.rb
+++ b/www/roster/views/ppmc/establish.text.rb
@@ -40,11 +40,6 @@
Bylaws of the Foundation until death, resignation, retirement, removal or
disqualification, or until a successor is appointed; and be it further
-RESOLVED, that the initial Apache #{podling.display_name} PMC be and hereby is
-tasked with the creation of a set of bylaws intended to encourage open
-development and increased participation in the Apache #{podling.display_name}
-Project; and be it further
-
RESOLVED, that the Apache #{podling.display_name} Project be and hereby is
tasked with the migration and rationalization of the Apache Incubator
#{podling.display_name} podling; and be it further
diff --git a/www/roster/views/ppmc/mentors.js.rb b/www/roster/views/ppmc/mentors.js.rb
index a64fe40..8a5a037 100644
--- a/www/roster/views/ppmc/mentors.js.rb
+++ b/www/roster/views/ppmc/mentors.js.rb
@@ -58,11 +58,15 @@
end
if @@person.member
- _td { _b { _a @@person.id, href: "committer/#{@@person.id}" } }
+ _td { _b { _a @@person.id, href: "committer/#{@@person.id}" }
+ _a ' (*)', href: "ppmc/#{@@ppmc.id}#crosscheck" if @@person.notSubbed and @@ppmc.analysePrivateSubs
+ }
_td @@person.githubUsername
_td { _b @@person.name }
elsif @@person.name
- _td { _a @@person.id, href: "committer/#{@@person.id}" }
+ _td { _a @@person.id, href: "committer/#{@@person.id}"
+ _a ' (*)', href: "ppmc/#{@@ppmc.id}#crosscheck" if @@person.notSubbed and @@ppmc.analysePrivateSubs
+ }
_td @@person.githubUsername
_td @@person.name
else
@@ -72,6 +76,7 @@
end
_td data_ids: @@person.id do
+ # TODO: how does this become enabled?
if @@person.selected
if @@auth.ppmc
unless @@ppmc.owners.include? @@person.id
@@ -80,12 +85,22 @@
data_target: '#confirm', data_toggle: 'modal',
data_confirmation: "Add #{@@person.name} as member of the " +
"#{@@ppmc.display_name} PPMC?"
+ else
+ unless @@ppmc.committers.include? @@person.id
+ _button.btn.btn_primary 'Add to the podling committers',
+ data_action: 'add committer',
+ data_target: '#confirm', data_toggle: 'modal',
+ data_confirmation: "Add #{@@person.name} as committer of the " +
+ "#{@@ppmc.display_name} PPMC?"
+ end
end
end
elsif not @@person.name
_span.issue 'invalid user'
elsif not @@ppmc.owners.include? @@person.id
_span.issue 'not on the PPMC'
+ elsif not @@ppmc.committers.include? @@person.id
+ _span.issue 'not listed as a podling committer'
elsif not @@person.ipmc
_span.issue 'not on the IPMC'
elsif not @@person.icommit
diff --git a/www/roster/views/ppmcs.html.rb b/www/roster/views/ppmcs.html.rb
index 1db9a8e..addd561 100644
--- a/www/roster/views/ppmcs.html.rb
+++ b/www/roster/views/ppmcs.html.rb
@@ -13,8 +13,15 @@
ppmc: 'ppmc/'
}
) do
- _p 'A listing of all Podling Project Management Committees (PPMCs) from the Apache Incubator.'
- _p 'Click on column names to sort.'
+ _p 'A listing of current Podling Project Management Committees (PPMCs) from the Apache Incubator.'
+
+ _p do
+ _ 'Click on column names to sort.'
+ _{" "}
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ".each_char do |c|
+ _a c, href: "ppmc/##{c}"
+ end
+ end
_table.table.table_hover do
_thead do
@@ -26,13 +33,21 @@
end
project_names = @projects.map {|project| project.name}
+ prev_letter=nil
@ppmcs.sort_by {|ppmc| ppmc.display_name.downcase}.each do |ppmc|
- _tr_ do
+ letter = ppmc.display_name.upcase[0]
+ if letter != prev_letter
+ options = {id: letter}
+ else
+ options = {}
+ end
+ prev_letter = letter
+ _tr_ options do
_td do
if project_names.include? ppmc.name
_a ppmc.display_name, href: "ppmc/#{ppmc.name}"
else
- _a.label_danger ppmc.display_name, href: "ppmc/#{ppmc.name}"
+ _a.label_danger ppmc.display_name, href: "ppmc/#{ppmc.name}", title: 'LDAP project not yet set up'
end
end
diff --git a/www/secretary/icla-lint.cgi b/www/secretary/icla-lint.cgi
index 5e10de2..316309b 100755
--- a/www/secretary/icla-lint.cgi
+++ b/www/secretary/icla-lint.cgi
@@ -1,6 +1,6 @@
#!/usr/bin/env ruby
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'wunderbar/script'
require 'ruby2js/filter/functions'
diff --git a/www/secretary/ldap-check-committers.cgi b/www/secretary/ldap-check-committers.cgi
new file mode 100755
index 0000000..79a8a8e
--- /dev/null
+++ b/www/secretary/ldap-check-committers.cgi
@@ -0,0 +1,76 @@
+#!/usr/bin/env ruby
+
+=begin
+
+LDAP people should be committers (unless login is disabled)
+
+=end
+
+$LOAD_PATH.unshift '/srv/whimsy/lib'
+
+require 'whimsy/asf'
+require 'whimsy/asf/mlist'
+require 'wunderbar'
+
+_html do
+ _style %{
+ table {border-collapse: collapse}
+ table, th, td {border: 1px solid black}
+ td {padding: 3px 6px}
+ tr:hover td {background-color: #FF8}
+ th {background-color: #a0ddf0}
+ }
+
+ _h1 'LDAP membership checks'
+
+ old = ASF::Group['committers'].memberids
+ people = ASF::Person.preload(%w(uid createTimestamp asf-banned asf-altEmail mail loginShell))
+
+ _h2 'people who are not committers (excluding nologin)'
+
+ non_committers = people.reject { |p| p.nologin? or old.include? p.name or p.name == 'apldaptest'}
+ if non_committers.length > 0
+ _table do
+ _tr do
+ _th 'UID'
+ _th 'asf-banned?'
+ _th 'Date'
+ _th 'ICLA'
+ _th 'Subscriptions'
+ _th 'Moderates'
+ end
+ non_committers.sort_by(&:name).each do |p|
+ icla = ASF::ICLA.find_by_id(p.name)
+ _tr do
+ _td do
+ _a p.name, href: '/roster/committer/' + p.name
+ end
+ _td p.asf_banned?
+ _td p.createDate
+ if icla
+ if icla.claRef
+ _td do
+ _a icla.claRef, href: "https://svn.apache.org/repos/private/documents/iclas/#{icla.claRef}"
+ end
+ else
+ _td icla.form
+ end
+ else
+ _td 'No ICLA entry found'
+ end
+ all_mail = p.all_mail
+ _td do
+ # keep only the list names
+ _ ASF::MLIST.subscriptions(all_mail)[:subscriptions].map{|x| x[0]}
+ end
+ _td do
+ _ ASF::MLIST.moderates(all_mail)[:moderates]
+ end
+ end
+ end
+ end
+ else
+ _p 'All LDAP people entries are committers'
+ end
+
+end
\ No newline at end of file
diff --git a/www/secretary/ldap-check.cgi b/www/secretary/ldap-check.cgi
index 12a2d06..5378de8 100755
--- a/www/secretary/ldap-check.cgi
+++ b/www/secretary/ldap-check.cgi
@@ -2,11 +2,10 @@
=begin
-Compare LDAP lists
+Compare LDAP lists (also CI)
-project.memberids should agree with Group.memberids (if it exixts)
-project.ownerids should agree with Committee.memberids (if it exists)
-
+PMC members should be the same as project owners (for actual PMCs)
+owners should also be members
members and owners should also be committers
The two committers groups should have the same members:
@@ -41,9 +40,9 @@
_h2 'members and owners'
_p do
- _ 'LDAP project members must agree with corresponding (unix) group members'
+ _ 'PMC members should be project owners and vice-versa'
_br
- _ 'LDAP project owners must agree with corresponding committee members'
+ _ 'LDAP project owners should also be project members'
_br
_ 'project/podling committers must be in committers group'
_br
@@ -53,62 +52,55 @@
_table do
_tr do
_th 'Project'
- _th 'project members - group members'
- _th 'group members - project members'
- _th 'project owners - committee members'
- _th 'committee-members - project owners'
- _th 'not in committers group'
+ _th 'PMC member but not project owner'
+ _th 'Project owner but not PMC member'
+ _th 'Project owner but not project member'
+ _th 'in project (owner or member) but not in committers group'
end
projects = ASF::Project.list
+ pmcs = ASF::Committee.pmcs
projects.sort_by(&:name).each do |p|
- po_co=[]
- co_po=[]
- pm_um=[]
- um_pm=[]
+ po=p.ownerids
+ pm=p.memberids
+ po_pm = po - pm
+ cttee = ASF::Committee.find(p.name)
+ # Is this a real PMC?
+ if ASF::Committee.pmcs.include? cttee
+ isPMC = true
+ cm = cttee.roster.keys
+ cm_po = cm - po
+ po_cm = po - cm
+ else
+ isPMC = false
+ cm_po = []
+ po_cm = []
+ end
notc=[]
- # TODO to be removed soon
- # Use hasLDAP? to check if the underlying ou=pmc group exists
- if c=ASF::Committee[p.name] and c.hasLDAP? # we have PMC group
- po=p.ownerids
- co=c.memberids
- po_co=po-co
- co_po=co-po
- notc += po.reject {|n| old.include? n}
- notc += co.reject {|n| old.include? n}
- end
- # TODO likewise, only applies to historic groups
- if u=ASF::Group[p.name] # we have the unix group
- pm=p.memberids
- um=u.memberids
- pm_um=pm-um
- um_pm=um-pm
- notc += pm.reject {|n| old.include? n}
- notc += um.reject {|n| old.include? n}
- end
- if pm_um.size > 0 or um_pm.size > 0 or po_co.size > 0 or co_po.size > 0 or notc.size > 0
+ notc += po.reject {|n| old.include? n}
+ notc += pm.reject {|n| old.include? n}
+ if po_pm.size > 0 or cm_po.size > 0 or po_cm.size > 0 or notc.size > 0
_tr do
_td do
- _a p.name, href: '/roster/committee/' + p.name
+ if isPMC
+ _a p.name, href: '/roster/committee/' + p.name
+ else
+ _a p.name, href: '/roster/ppmc/' + p.name
+ end
end
_td do
- pm_um.each do |id|
+ cm_po.each do |id|
_a id, href: '/roster/committer/' + id
end
end
_td do
- um_pm.each do |id|
+ po_cm.each do |id|
_a id, href: '/roster/committer/' + id
end
end
_td do
- po_co.each do |id|
- _a id, href: '/roster/committer/' + id
- end
- end
- _td do
- co_po.each do |id|
+ po_pm.each do |id|
_a id, href: '/roster/committer/' + id
end
end
diff --git a/www/secretary/workbench/models/message.rb b/www/secretary/workbench/models/message.rb
index ff02dd2..f2b8408 100644
--- a/www/secretary/workbench/models/message.rb
+++ b/www/secretary/workbench/models/message.rb
@@ -348,7 +348,7 @@
begin
from = liberal_email_parser(from_value).display_name
rescue Exception
- from = from_value.sub(/\s+<.*?>$/)
+ from = from_value.sub(/\s+<.*?>$/, '')
end
# determine who should be copied on any responses
diff --git a/www/secretary/workbench/personalize.rb b/www/secretary/workbench/personalize.rb
index 9065ab0..4401210 100644
--- a/www/secretary/workbench/personalize.rb
+++ b/www/secretary/workbench/personalize.rb
@@ -6,18 +6,18 @@
def _personalize_email(user)
if user == 'clr'
- @from = 'Craig L Russell <secretary@apache.org>'
+ @from = 'Craig L Russell <clr@apache.org>'
@sig = %{
-- Craig L Russell
- Secretary, Apache Software Foundation
+ Assistant Secretary, Apache Software Foundation
}
elsif user == 'mattsicker'
- @from = 'Matt Sicker <mattsicker@apache.org>'
+ @from = 'Matt Sicker <secretary@apache.org>'
@sig = %{
-- Matt Sicker
- Assistant Secretary, Apache Software Foundation
+ Secretary, Apache Software Foundation
}
else
diff --git a/www/secretary/workbench/templates/mem.erb b/www/secretary/workbench/templates/mem.erb
index 3e96300..a7acc27 100644
--- a/www/secretary/workbench/templates/mem.erb
+++ b/www/secretary/workbench/templates/mem.erb
@@ -6,7 +6,14 @@
You will shortly be subscribed to the members@apache.org mailing list.
-To verify that you have the proper rights to the foundation repository, please verify that your personal information has been properly entered: https://svn.apache.org/repos/private/foundation/members.txt and add any other information such as your personal web page and the projects that you are working on.
+To verify that you have the proper rights to the foundation repository, please check that your personal information has been properly entered:
+https://svn.apache.org/repos/private/foundation/members.txt
+You can add any other information such as your personal web page and the projects that you are working on.
+
+If you cannot get access, you can use Whimsy to check your settings:
+https://whimsy.apache.org/roster/committer/__self__
+Under 'Groups' you should see 'member'; this gives access to member-only resources.
+Note that it may take a while for all systems to pick up the new setting.
If you have any questions or concerns, please feel free to reach out at members@apache.org
diff --git a/www/secretary/workbench/templates/pubkey.erb b/www/secretary/workbench/templates/pubkey.erb
index e4f3ef1..dc87da3 100644
--- a/www/secretary/workbench/templates/pubkey.erb
+++ b/www/secretary/workbench/templates/pubkey.erb
@@ -3,6 +3,9 @@
We received this document but cannot verify your signature without
your public key. Please upload your public key to pgpkeys.mit.edu.
+If you have trouble uploading your key, you can print, sign, date,
+scan, and email the pdf to secretary@apache.org
+
http://www.apache.org/licenses/#submitting
Warm Regards,
diff --git a/www/secretary/workbench/views/actions/check-signature.json.rb b/www/secretary/workbench/views/actions/check-signature.json.rb
index 35a0ed6..26f4dba 100644
--- a/www/secretary/workbench/views/actions/check-signature.json.rb
+++ b/www/secretary/workbench/views/actions/check-signature.json.rb
@@ -4,6 +4,10 @@
ENV['GNUPGHOME'] = GNUPGHOME if GNUPGHOME
+#KEYSERVER = 'pgpkeys.mit.edu'
+# Perhaps also try keyserver.pgp.com
+KEYSERVERS = %w{hkps.pool.sks-keyservers.net keyserver.ubuntu.com pgpkeys.mit.edu}
+
message = Mailbox.find(@message)
begin
@@ -17,8 +21,11 @@
gpg.untaint
# run gpg verify command
- out, err, rc = Open3.capture3 gpg, '--verify', signature.path,
- attachment.path
+ # TODO: may need to drop the keyid-format parameter when gpg is updated as it might
+ # reduce the keyid length from the full fingerprint
+ out, err, rc = Open3.capture3 gpg,
+ '--keyid-format', 'long', # Show a longer id
+ '--verify', signature.path, attachment.path
# if key is not found, fetch and try again
if
@@ -28,12 +35,25 @@
# extract and fetch key
keyid = err[/[RD]SA key (ID )?(\w+)/,2].untaint
- out2, err2, rc2 = Open3.capture3 gpg, '--keyserver', 'pgpkeys.mit.edu',
- '--recv-keys', keyid
-
+ out2, err2 = '' # needed later
+ KEYSERVERS.each do |server|
+ out2, err2, rc2 = Open3.capture3 gpg, '--keyserver', server,
+ '--debug', 'ipc', # seems to show communication with dirmngr
+ '--recv-keys', keyid
+ # for later analysis
+ Wunderbar.warn "#{gpg} --keyserver #{server} --recv-keys #{keyid} rc2=#{rc2} out2=#{out2} err2=#{err2}"
+ if rc2.exitstatus == 0 # Found the key
+ out2 = err2 = '' # Don't add download error to verify error
+ break
+ end
+ end
+
# run gpg verify command again
- out, err, rc = Open3.capture3 gpg, '--verify', signature.path,
- attachment.path
+ # TODO: may need to drop the keyid-format parameter when gpg is updated as it might
+ # reduce the keyid length from the full fingerprint
+ out, err, rc = Open3.capture3 gpg,
+ '--keyid-format', 'long', # Show a longer id
+ '--verify', signature.path, attachment.path
# if verify failed, concatenate fetch output
if rc.exitstatus != 0
diff --git a/www/secretary/workbench/views/actions/pubkey.json.rb b/www/secretary/workbench/views/actions/pubkey.json.rb
index 9a4d81c..391b87a 100644
--- a/www/secretary/workbench/views/actions/pubkey.json.rb
+++ b/www/secretary/workbench/views/actions/pubkey.json.rb
@@ -34,7 +34,7 @@
complete do
mail.deliver!
- _status 'request to upload public key already has been sent.'
+ _status 'request to upload public key has been sent.'
_disposition :keep
end
end
diff --git a/www/secretary/workbench/views/forms/memapp.js.rb b/www/secretary/workbench/views/forms/memapp.js.rb
index 7f4022c..4dc2409 100644
--- a/www/secretary/workbench/views/forms/memapp.js.rb
+++ b/www/secretary/workbench/views/forms/memapp.js.rb
@@ -108,7 +108,7 @@
# wire up form
jQuery('form')[0].addEventListener('submit', self.file)
jQuery('input[name=message]').val(window.parent.location.pathname)
- jQuery('input[name=selected]').val(@@selected)
+ jQuery('input[name=selected]').val(decodeURIComponent(@@selected))
# default email
@email = @@headers.from
diff --git a/www/site.cgi b/www/site.cgi
index 2a3edea..eff1094 100755
--- a/www/site.cgi
+++ b/www/site.cgi
@@ -9,7 +9,7 @@
exit
end
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'json'
require 'net/http'
require 'time' # for httpdate
diff --git a/www/status/monitors/public_json.rb b/www/status/monitors/public_json.rb
index c508369..16138b5 100644
--- a/www/status/monitors/public_json.rb
+++ b/www/status/monitors/public_json.rb
@@ -94,14 +94,14 @@
$stderr.puts "Would send e-mail for #{name} #{lvl}"
begin
require 'mail'
- $LOAD_PATH.unshift File.realpath(File.expand_path('../../../../lib', __FILE__))
+ $LOAD_PATH.unshift '/srv/whimsy/lib'
require 'whimsy/asf'
ASF::Mail.configure
mail = Mail.new do
from 'Public JSON job monitor <dev@whimsical.apache.org>'
to 'Notification List <notifications@whimsical.apache.org>'
subject "Problem (#{lvl}) detected in #{name} job"
- body "\nLOG: #{contents_save}\nSTATUS: #{status[name]}\n"
+ body "\nLOG:\n#{contents_save}\nSTATUS: #{status[name]}\n"
end
# in spite of what the docs say, this does not seem to work in the body above
mail.charset = 'utf-8'
diff --git a/www/status/svn.cgi b/www/status/svn.cgi
index 6a4a5cf..8cc1bc9 100755
--- a/www/status/svn.cgi
+++ b/www/status/svn.cgi
@@ -1,6 +1,6 @@
#!/usr/bin/env ruby
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
#
# SVN Repository status
diff --git a/www/technology.html b/www/technology.html
index 021e890..4fdfb97 100644
--- a/www/technology.html
+++ b/www/technology.html
@@ -113,7 +113,7 @@
</div>
<div class="panel-body">
<p>
- Copyright © 2015-2018 The Apache Software Foundation, Licensed under
+ Copyright © 2015-2019 The Apache Software Foundation, Licensed under
the <a href="http://www.apache.org/licenses/LICENSE-2.0" rel="license">Apache License, Version 2.0</a>.
|
<a href="https://www.apache.org/foundation/policies/privacy">Privacy Policy</a>
diff --git a/www/test/dataflow.cgi b/www/test/dataflow.cgi
index d9931a6..ed2431a 100755
--- a/www/test/dataflow.cgi
+++ b/www/test/dataflow.cgi
@@ -1,7 +1,7 @@
#!/usr/bin/env ruby
PAGETITLE = "Public Datafiles And Dependencies" # Wvisible:tools data
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'json'
# Command line use: emit replacement for www/public/README.html
@@ -58,7 +58,7 @@
_ ' You can see the '
_a 'code for this script', href: "#{GITWHIMSY}/www#{ENV['SCRIPT_NAME']}"
_ ', the '
- _a 'underlying data file', href: "#{GITWHIMSY}/www/#{DATAFLOWDATA}"
+ _a 'underlying data file', href: "#{GITWHIMSY}/www/test/#{DATAFLOWDATA}"
_ ', the '
_a 'key to this data', href: "#datakey"
_ ', and many of the '
diff --git a/www/test/example.cgi b/www/test/example.cgi
index 64cc311..3080245 100755
--- a/www/test/example.cgi
+++ b/www/test/example.cgi
@@ -1,22 +1,88 @@
#!/usr/bin/env ruby
PAGETITLE = "Example Whimsy Script With Styles" # Wvisible:tools Note: PAGETITLE must be double quoted
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'json'
-require 'whimsy/asf'
+require 'yaml'
require 'wunderbar'
require 'wunderbar/bootstrap'
+require 'wunderbar/jquery'
+require 'wunderbar/markdown'
+require 'whimsy/asf'
+require 'whimsy/asf/forms'
+require 'whimsy/public'
-def get_data(defaults: {})
- return {
- "Sample data processing here" => "row 1",
- "This could come from a file" => "row B"
- }
+# Get data from live whimsy.a.o/public directory
+def get_public_data()
+ return Public.getJSON('public_ldap_authgroups.json')
end
+# Get data from a Subversion directory
+# See /repository.yml for list of auto-updated dirs
+def get_svn_data()
+ dir = ASF::SVN['comdevtalks']
+ filename = 'README.yaml'
+ data = YAML.load(File.read(File.join(dir, filename).untaint))
+ return data['title']
+end
+
+# Gather some data beforehand, if you like, but:
+# Note runtime errors here just write to the log, not to user's browser
+talktitle = get_svn_data()
+
+# Example of handling POST forms cleanly
+def emit_form(title, prev_data)
+ _whimsy_panel("#{title}", style: 'panel-success') do
+ _form.form_horizontal method: 'post' do
+ _div.form_group do
+ _label.col_sm_offset_3.col_sm_9.strong.text_left 'Example Form Section'
+ end
+ field = 'text1'
+ _whimsy_forms_input(label: 'Example Text Field', name: field, id: field,
+ value: prev_data[field], helptext: 'Enter some text, keep it polite!'
+ )
+ field = 'listbox'
+ _whimsy_forms_select(label: 'Select Some Values', name: field,
+ multiple: true, values: prev_data[field],
+ options: ['another value', 'yet another value'],
+ icon: 'glyphicon-time', iconlabel: 'clock',
+ helptext: 'Select as many values as ya like!'
+ )
+ field = 'text2'
+ _whimsy_forms_input(label: 'Another Text Field', name: field, id: field,
+ value: prev_data[field], helptext: 'Pretty boring form example, huh?'
+ )
+ _div.col_sm_offset_3.col_sm_9 do
+ _input.btn.btn_default type: 'submit', value: 'PUSH ME!'
+ end
+ end
+ end
+end
+
+# Validation as needed within the script
+def validate_form(formdata: {})
+ return true # TODO: Futureuse
+end
+
+# Handle submission (checkout user's apacheid.json, write form data, checkin file)
+# @return true if we think it succeeded; false in all other cases
+def send_form(formdata: {})
+ # Example that uses SVN to update an existing file: members/mentor-update.cgi
+ _p class: 'system' do
+ _ 'If this were a real send_form() it would do something with your data:'
+ _br
+ formdata.each do |k,v|
+ _ "#{k} = #{v.inspect}"
+ _br
+ end
+ end
+ return true
+end
+
+# Produce HTML
_html do
- _body? do
- _whimsy_body(
+ _body? do # The ? traps errors inside this block
+ _whimsy_body( # This emits the entire page shell: header, navbar, basic styles, footer
title: PAGETITLE,
subtitle: 'About This Example Script',
relatedtitle: 'More Useful Links',
@@ -33,6 +99,18 @@
Any related whimsy or other (projects.a.o, etc.) links should be in the related: listing on the top right to help users find other useful things.
This provides a consistent user experience.
}
+ _p "You can output data previously processed as well like: #{talktitle}"
+ _ul.list_inline do
+ _li do
+ _a 'example-table', href: '#example-table'
+ end
+ _li do
+ _a 'example-accordion', href: '#example-accordion'
+ end
+ _li do
+ _a 'example-form', href: '#example-form'
+ end
+ end
},
breadcrumbs: {
dataflow: '/test/dataflow.cgi',
@@ -40,6 +118,7 @@
}
) do
# IF YOUR SCRIPT EMITS A LARGE TABLE
+ _div id: 'example-table'
_whimsy_panel_table(
title: "Data Table H2 Title Goes Here",
helpblock: -> {
@@ -49,7 +128,7 @@
# Gather or process your data **here**, so if an error is raised, the _body?
# scope will trap it - and will then display the above help information
# to the user before emitting a polite error traceback.
- datums = get_data()
+ datums = {'one' => 1, 'two' => 2 }
_table.table.table_hover.table_striped do
_thead_ do
_tr do
@@ -71,6 +150,7 @@
end
end
end
+
# IF YOUR SCRIPT ONLY EMITS SIMPLE DATA
_h2 "Simple Data Can Just Use A List"
_ul do
@@ -78,6 +158,53 @@
_li "This is row number #{row}."
end
end
+
+ # NIFTY ACCORDION EXPAND-O LISTING: the _whimsy_accordion_item does most of the work
+ _h2 id: 'example-accordion' do
+ _ 'Lists of Complex Data Can Use An Accordion'
+ end
+ accordionid = 'accordion'
+ officers = get_public_data()
+ _div.panel_group id: accordionid, role: 'tablist', aria_multiselectable: 'true' do
+ officers['auth'].each_with_index do |(listname, rosters), n|
+ _whimsy_accordion_item(listid: accordionid, itemid: listname, itemtitle: "#{listname}", n: n, itemclass: 'panel-primary') do
+ _ul do
+ rosters['roster'].each do |usr|
+ _li usr
+ end
+ end
+ end
+ end
+ end
+
+ # IF YOU WANT TO DISPLAY A FORM and handle the POST
+ _div id: 'example-form'
+ if _.post?
+ # Use magic _. callouts to CGI class to gather POST data into submission hash
+ submission = {}
+ keyz = _.keys
+ keyz.each do |k|
+ submission[k] = _.params[k] # Always as ['val'] or ['one', 'two', ...]
+ end
+ if validate_form(formdata: submission)
+ if send_form(formdata: submission)
+ _p.lead "Thanks for Submitting This Form!"
+ _p do
+ _ "The send_form method would have done any procesing needed with the data, after calling validate_data."
+ end
+ else
+ _div.alert.alert_warning role: 'alert' do
+ _p "SORRY! Your submitted form data failed send_form, please try again."
+ end
+ end
+ else
+ _div.alert.alert_danger role: 'alert' do
+ _p "SORRY! Your submitted form data failed validate_form, please try again."
+ end
+ end
+ else # if _.post?
+ emit_form('Form Title Here', officers)
+ end
end
end
end
diff --git a/www/treasurer/bill-upload.cgi b/www/treasurer/bill-upload.cgi
index a4b0a87..afe5ca6 100755
--- a/www/treasurer/bill-upload.cgi
+++ b/www/treasurer/bill-upload.cgi
@@ -1,6 +1,6 @@
#!/usr/bin/env ruby
PAGETITLE = "Apache Treasurer Bill Upload" # Wvisible:treasurer
-$LOAD_PATH.unshift File.realpath(File.expand_path('../../../lib', __FILE__))
+$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'wunderbar'
require 'wunderbar/bootstrap'
require 'wunderbar/jquery'