This is an automated email from the ASF dual-hosted git repository.

sebb pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/whimsy.git

commit c88c9b75f69e3d9db3873a874e51656b6466fdff
Author: Sebb <[email protected]>
AuthorDate: Mon Feb 16 15:50:04 2026 +0000

    Add board second tool
---
 lib/whimsy/asf/member-files.rb |   8 ++
 www/members/second_board.cgi   | 238 +++++++++++++++++++++++++++++++++++++++++
 2 files changed, 246 insertions(+)

diff --git a/lib/whimsy/asf/member-files.rb b/lib/whimsy/asf/member-files.rb
index ed97f8b2..72520af2 100644
--- a/lib/whimsy/asf/member-files.rb
+++ b/lib/whimsy/asf/member-files.rb
@@ -281,6 +281,14 @@ module ASF
       end
     end
 
+    # Add a second for a board nominee
+    def self.commit_board_second(env, wunderbar, entry, msg)
+      nomfile = latest_meeting(NOMINATED_BOARD)
+      ASF::SVN.update(nomfile, msg, env, wunderbar) do |_tmpdir, contents|
+        add_member_second(contents, entry) # same logic works here
+      end
+    end
+
     # create a single director ballot statement (if not present)
     # @param availid of director nominee
     def self.add_board_ballot(env, wunderbar, availid, msg=nil, opt={})
diff --git a/www/members/second_board.cgi b/www/members/second_board.cgi
new file mode 100755
index 00000000..db198385
--- /dev/null
+++ b/www/members/second_board.cgi
@@ -0,0 +1,238 @@
+#!/usr/bin/env ruby
+PAGETITLE = "Second someone for ASF Board" # Wvisible:meeting
+$LOAD_PATH.unshift '/srv/whimsy/lib'
+require 'time'
+require 'wunderbar'
+require 'wunderbar/bootstrap'
+require 'whimsy/asf'
+require 'whimsy/asf/forms'
+require 'whimsy/asf/member-files' # See for nomination file parsing
+require 'whimsy/asf/wunderbar_updates'
+require 'whimsy/asf/meeting-util'
+require_relative '../../tools/parsemail'
+require 'whimsy/asf/time-utils'
+require 'mail'
+
+MAILING_LIST = '[email protected]'
+MAIL_ROOT = '/srv/mail' # TODO: this should be config item
+
+# Try to find matching message id for the original nomination email
+def find_mesgid(subject)
+  ParseMail.parse_main(['members']) # ensure we are up to date
+  today = Date.today
+  msgids = [] # potential matches
+  [today.strftime("%Y%m"), (today<<1).strftime("%Y%m")].each do |yyyymm|
+    curfile = File.join(MAIL_ROOT, "members", "#{yyyymm}.yaml")
+    yaml = YamlFile.read(curfile)
+    yaml.each do |key, value|
+      if subject == value[:Subject]
+        msgid = value[:MessageId]
+        if msgid.include? '@whimsy' # currently @whimsy1-ec2-va.mail
+          return msgid # This must be it
+        else
+          msgids << msgid # could be the one
+        end
+      end
+    end
+  end
+  return msgids.first if msgids.size == 1
+  # else did not find unique match
+  nil
+end
+
+def emit_form(title, prev_data)
+  _whimsy_panel(title, style: 'panel-success') do
+    _form.form_horizontal method: 'post' do
+      field = 'nominee'
+      _whimsy_forms_select(label: 'Nominee', name: field,
+        multiple: false,
+        options: ASF::MemberFiles.board_headers,
+        helptext: 'Select the nominee you are seconding for the ASF Board'
+      )
+      _whimsy_forms_input(label: 'Seconded by', name: 'secby', readonly: true, 
value: $USER
+      )
+      field = 'statement'
+      _whimsy_forms_input(label: 'Second Statement', name: field, rows: 10,
+        value: prev_data[field], helptext: 'Explain why you believe this 
person would make a good ASF Board Member,'
+      )
+      _whimsy_forms_submitwrap(
+        noicon: true, label: 'submit', name: 'submit', value: 'submit', 
helptext: 'Checkin this second and send email to members@'
+      )
+    end
+  end
+end
+
+# Validation as needed within the script
+# Returns: 'OK' or a text message describing the problem
+def validate_form(formdata: {})
+  nominee = formdata['nominee']
+  return "You MUST provide a second statement for Candidate #{nominee}; blank 
was provided!" if formdata['statement'].empty?
+  return 'OK'
+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 process_form(formdata: {}, wunderbar: {})
+  _h3 "Transcript of update to nomination file 
#{ASF::MemberFiles::NOMINATED_BOARD}"
+  entry = {
+    nominee: formdata['nominee'], # to find the entry
+    secby: formdata['secby'], # add to seconds
+    statement: formdata['statement'] # the data
+  }
+  environ = Struct.new(:user, :password).new($USER, $PASSWORD)
+  ASF::MemberFiles.commit_board_second(environ, wunderbar, entry, "+= second 
for #{formdata['nominee'].downcase}")
+  return true
+end
+
+# Send email to members@ with this second's data
+# Reports status to user in a _div
+def send_confirmation_mail(formdata: {})
+  public_name = formdata['nominee'].strip # Public Name (no uid)
+  uids = ASF::Person.find_by_name(public_name)
+  if uids.size == 1
+    uid = uids.first
+  else
+    raise ArgumentError.new("Cannot find unique uid for #{public_name}, got: 
#{uids.inspect}")
+  end
+  secby = formdata.fetch('secby', nil)
+  mail_body = <<-MAILBODY
+Added second by #{secby} for #{public_name} (#{uid}) as a New Board Member:
+
+#{formdata['statement']}
+
+--
+- #{ASF::Person[secby].public_name}
+  Email generated by Whimsy (#{File.basename(__FILE__)})
+
+MAILBODY
+  # See check_boardnoms.cgi which parses this in list archives
+  # We don't allow for non-committers on board
+  mailsubject_original = "[BOARD NOMINATION] #{public_name} (#{uid})"
+  mailsubject = "Re: #{mailsubject_original}"
+  msgid = find_mesgid(mailsubject_original)
+  ASF::Mail.configure
+  mail = Mail.new do
+    to MAILING_LIST
+    bcc '[email protected]'
+    from "#{ASF::Person[secby].public_name} <#{secby}@apache.org>"
+    subject mailsubject
+    in_reply_to msgid if msgid
+    text_part do
+      body mail_body
+    end
+  end
+  begin
+    mail.deliver!
+  rescue StandardError => e
+    _div.alert.alert_danger role: 'alert' do
+      _p.strong "ERROR: email was NOT sent due to: #{e.message} 
#{e.backtrace[0]}"
+      _p do
+        _ "To: #{MAILING_LIST}"
+        _br
+        _ "Subject: #{mailsubject}"
+        _br
+        _ "#{mail_body}"
+      end
+    end
+    return
+  end
+  _div.alert.alert_success role: 'alert' do
+    _p "The following email was sent:"
+    _p do
+      _ "To: #{MAILING_LIST}"
+      _br
+      _ "Subject: #{mailsubject}"
+      _br
+      _ "#{mail_body}"
+    end
+  end
+  return
+end
+
+# Produce HTML
+_html do
+  _body? do
+    # Countdown until nominations for current meeting close
+    latest_meeting_dir = ASF::MeetingUtil.latest_meeting_dir
+    timelines = ASF::MeetingUtil.get_timeline(latest_meeting_dir)
+    t_now = Time.now.to_i
+    t_end = Time.parse(timelines['nominations_close_iso']).to_i
+    nomclosed = t_now > t_end
+    _whimsy_body(
+      title: PAGETITLE,
+      subtitle: 'About This Script',
+      related: {
+        'meeting.cgi' => 'Member Meeting FAQ and info',
+        '/roster/committer/' => 'Lookup any committer availID',
+        'check_boardnoms.cgi' => 'Cross-check existing Board nominations',
+        ASF::SVN.svnpath!('Meetings') => 'Official Meeting Agenda Directory'
+      },
+      helpblock: -> {
+        _b "For: #{timelines['meeting_type']} Meeting on: 
#{timelines['meeting_iso']}"
+        _p do
+          _br
+          _ %Q{
+            Use this form to add a second to an existing nomination for teh 
ASF Board.
+            It automatically adds a properly formatted entry to the 
#{ASF::MemberFiles::NOMINATED_BOARD} file,
+            and will then
+          }
+          _strong "send an email to the #{MAILING_LIST} list"
+          _ ' from you with the details of the second.'
+        end
+      }
+    ) do
+
+      if nomclosed
+        _h1 'Nominations are now closed!'
+        _p 'Sorry, no further seconds will be accepted for ballots at this 
meeting.'
+      else
+        _h3 "Nominations close in #{ASFTime.secs2text(t_end - t_now)} at 
#{Time.at(t_end).utc} for Meeting: #{timelines['meeting_iso']}"
+      end
+
+      _div id: 'second-form' do
+        if _.post?
+          unless nomclosed
+            submission = _whimsy_params2formdata(params)
+            valid = validate_form(formdata: submission)
+          end
+          if nomclosed
+            _div.alert.alert_warning role: 'alert' do
+              _p "Nominations have closed"
+            end
+          elsif valid == 'OK'
+            if process_form(formdata: submission, wunderbar: _)
+              _div.alert.alert_success role: 'alert' do
+                _p "Your second was submitted to svn; now sending email to 
#{MAILING_LIST}."
+              end
+              mailval = send_confirmation_mail(formdata: submission)
+            else
+              _div.alert.alert_danger role: 'alert' do
+                _p do
+                  _span.strong "ERROR: Form data invalid in process_form(), 
update was NOT submitted!"
+                  _br
+                  _ "#{submission}"
+                end
+              end
+            end
+          else
+            _div.alert.alert_danger role: 'alert' do
+              _p do
+                _span.strong "ERROR: Form data invalid in validate_form(), 
update was NOT submitted!"
+                _br
+                _p valid
+              end
+            end
+          end
+        else # if _.post?
+          if nomclosed
+            _p do
+              _ 'Sorry, no further seconds will be accepted for ballots at 
this meeting.'
+            end
+          else
+            emit_form('Enter your New Board second', {})
+          end
+        end
+      end
+    end
+  end
+end

Reply via email to