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
