www/board/agenda/views/buttons/post.js.rb (608 lines of code) (raw):
#
# Post or edit a report or resolution
#
# For new resolutions, allow entry of title, but not commit message
# For everything else, allow modification of commit message, but not title
class Post < Vue
def initialize
@button = @@button.text
@disabled = false
@alerted = false
@edited = false
@pmcs = []
@roster = []
@parent = nil
end
# default attributes for the button associated with this form
def self.button
{
text: 'post report',
class: 'btn_primary',
disabled: Server.offline,
data_toggle: 'modal',
data_target: '#post-report-form'
}
end
def render
_ModalDialog.wide_form.post_report_form! color: 'commented' do
if @button == 'add item'
_h4 'Select Item Type'
_ul.new_item_type do
_li do
_button.btn.btn_primary 'Change Chair', onClick: selectItem
_ '- change chair for an existing PMC'
end
_li do
_button.btn.btn_primary 'Establish Project', onClick: selectItem
_ '- direct to TLP project and subproject to TLP'
end
_li do
_button.btn.btn_primary 'Terminate Project', onClick: selectItem
_ '- move a project to the attic'
end
_li do
_button.btn.btn_primary 'New Resolution', onClick: selectItem
_ '- free form entry of a new resolution'
end
_li do
_button.btn.btn_info 'Out of Cycle Report', onClick: selectItem
_ '- report from a PMC not currently on the agenda for this month'
end
_li do
_button.btn.btn_success 'Discussion Item', onClick: selectItem
_ '- add a discussion item to the agenda'
end
end
_button.btn_default 'Cancel', data_dismiss: 'modal'
elsif @button == 'Change Chair'
_h4 'Change Chair Resolution'
_div.form_group do
_label 'PMC', for: 'change-chair-pmc'
_select.form_control.change_chair_pmc!(
onChange: ->(event) {chair_pmc_change(event.target.value)}
) do
@pmcs.each {|pmc| _option pmc}
end
end
_div.form_group do
_label 'Outgoing Chair', for: 'outgoing-chair'
_input.form_control.outgoing_chair! value: @outgoing_chair,
disabled: true
end
_div.form_group do
_label 'Incoming Chair', for: 'incoming-chair'
_select.form_control.incoming_chair! do
@pmc_members.each do |person|
_option person.name, value: person.id,
selected: person.id == User.id
end
end
end
_button.btn_default 'Cancel', data_dismiss: 'modal'
_button.btn_primary 'Draft', disabled: @disabled,
onClick: draft_chair_change_resolution
elsif @button == 'Establish Project'
_h4 'Establish Project Resolution'
_div.form_group do
_label 'PMC name', for: 'establish-pmc'
_input.form_control.establish_pmc! value: @pmcname
end
_div.form_group do
# capitalize pmcname
pmcname = @pmcname
if @pmcname and @pmcname !~ /[A-Z]/
pmcname.gsub!(/\b\w/) {|c| c.upcase()}
end
_label 'Complete this sentence:', for: 'establish-description'
_ " Apache #{pmcname} consists of software related to" if pmcname
_textarea.form_control.establish_description! value: @pmcdesc,
disabled: !pmcname
end
_div.form_group do
_label 'Parent PMC name (if applicable)', for: 'parent-pmc'
_select.form_control.parent_pmc!(
onChange: ->(event) {parent_pmc_change(event.target.value)}
) do
_option '-- none --', value: '', selected: true
@pmcs.each {|pmc| _option pmc unless pmc == 'incubator'}
end
end
if @chair
_div.form_group do
_label "Chair: #{@chair.name}"
end
end
_label 'Initial set of PMC members'
_p do
if !@chair
_ 'Search for the chair '
else
_ 'Search for additional PMC members '
end
_ 'using the search box below, and select '
_ 'the desired name using the associated checkbox'
end
@pmc.each do |person|
_div.form_check do
_input.form_check_input type: 'checkbox', checked: true,
value: person.id, id: "person_#{person.id}"
_label.form_check_label person.name, for: "person_#{person.id}"
end
end
_input.form_control value: @search, placeholder: 'search'
if @search.length >= 3 and Server.committers
search = @search.downcase().split(' ')
Server.committers.each do |person|
if
search.all? {|part|
person.id.include? part or
person.name.downcase().include? part
} \
and
not @pmc.include? person
then
_div.form_check key: person.id do
_input.form_check_input type: 'checkbox',
id: "person_#{person.id}",
onClick: -> {establish_pmc(person)}
_label.form_check_label person.name,
for: "person_#{person.id}"
end
end
end
elsif @search.length == 0 and @roster and not @roster.empty?
@roster.each do |person|
unless @pmc.include? person
_div.form_check key: person.id do
_input.form_check_input type: 'checkbox',
id: "person_#{person.id}",
onClick: -> {establish_pmc(person)}
_label.form_check_label person.name,
for: "person_#{person.id}"
end
end
end
end
_button.btn_default 'Cancel', data_dismiss: 'modal'
_button.btn_primary 'Draft', onClick: draft_establish_project,
disabled: (!@pmcname or !@pmcdesc or @pmc.empty?)
elsif @button == 'Terminate Project'
_h4 'Terminate Project Resolution'
_div.form_group do
_label 'PMC', for: 'terminate-pmc'
_select.form_control.terminate_pmc! do
@pmcs.each {|pmc| _option pmc}
end
end
_p 'Reason for termination:'
_div.form_check do
_input.form_check_input.termvote! type: 'radio', name: 'termreason',
onClick: -> {@termreason = 'vote'}
_label.form_check_label 'by vote of the PMC', for: 'termvote'
end
_div.form_check do
_input.form_check_input.termconsensus! type: 'radio',
name: 'termreason', onClick: -> {@termreason = 'consensus'}
_label.form_check_label 'by consensus of the PMC',
for: 'termconsensus'
end
_div.form_check do
_input.form_check_input.termboard! type: 'radio',
name: 'termreason', onClick: -> {@termreason = 'board'}
_label.form_check_label 'by the board for inactivity',
for: 'termboard'
end
_button.btn_default 'Cancel', data_dismiss: 'modal'
_button.btn_primary 'Draft', onClick: draft_terminate_project,
disabled: (@pmcs.empty? or not @termreason)
elsif @button == 'Out of Cycle Report'
_h4 'Out of Cycle PMC Report'
# determine which PMCs are reporting this month
reporting_this_month = []
Agenda.index.each do |item|
if item.roster and item.attach =~ /^[A-Z]+$/
reporting_this_month << item.roster.split('/').pop()
end
end
# provide a selection box with the remainder
_div.form_group do
_label 'PMC', for: 'out-of-cycle-pmc'
_select.form_control.out_of_cycle_pmc! do
@pmcs.each do |pmc|
_option pmc unless reporting_this_month.include? pmc
end
end
end
_button.btn_default 'Cancel', data_dismiss: 'modal'
_button.btn_primary 'Draft', disabled: @pmcs.empty?,
onClick: draft_out_of_cycle_report
else
_h4 @header
#input field: title
if @header == 'Add Resolution' or @header == 'Add Discussion Item'
_input.post_report_title! label: 'title', disabled: @disabled,
placeholder: 'title', value: @title, onFocus: self.default_title
end
#input field: report text
_textarea.post_report_text! label: @label, value: @report,
placeholder: @label, rows: 17, disabled: @disabled,
onInput: self.change_text
# upload of spreadsheet from virtual
if @@item.title == 'Treasurer'
_form do
_div.form_group do
_label 'financial spreadsheet from virtual', for: 'upload'
_input.upload! type: 'file', value: @upload
_button.btn.btn_primary 'Upload', onClick: upload_spreadsheet,
disabled: @disabled || !@upload
end
end
end
#input field: commit_message
if @header != 'Add Resolution' and @header != 'Add Discussion Item'
_input.post_report_message! label: 'commit message',
disabled: @disabled, value: @message
end
# footer buttons
_button.btn_default 'Cancel', data_dismiss: 'modal'
_button 'Reflow', class: self.reflow_color(), onClick: self.reflow
_button.btn_primary 'Submit', onClick: self.submit,
disabled: (not self.ready())
end
end
end
# add item menu support
def selectItem(event)
@button = event.target.textContent
if @button == 'Change Chair'
initialize_chair_change()
elsif @button == 'Establish Project'
initialize_establish_project()
elsif @button == 'Terminate Project'
initialize_terminate_project()
elsif @button == 'Out of Cycle Report'
initialize_out_of_cycle()
end
retitle()
end
# autofocus on report/resolution title/text
def mounted()
jQuery('#post-report-form').on 'show.bs.modal' do
# update contents when modal is about to be shown
@button = @@button.text
self.retitle()
end
jQuery('#post-report-form').on 'shown.bs.modal' do
reposition()
end
end
# reposition after update if header changed
def updated()
reposition() if Post.header != @header
end
# set focus, scroll
def reposition()
# set focus once modal is shown
title = document.getElementById('post-report-title')
text = document.getElementById('post-report-text')
if title || text
(title || text).focus()
# scroll to the top
setTimeout 0 do
text.scrollTop = 0 if text
end
end
Post.header = @header
end
# initialize form title, etc.
def created()
self.retitle()
end
# match form title, input label, and commit message with button text
def retitle()
@report = nil
parent_pmc_change(nil)
case @button
when 'post report'
@header = 'Post Report'
@label = 'report'
@message = "Post #{@@item.title} Report"
# if by chance the report was posted to board@, attempt to fetch
# the text/plain version of the body
posted = Posted.get(@@item.title)
unless posted.empty?
post 'posted-reports', path: posted.last.path do |response|
@report ||= response.text
end
end
# if there is a draft being prepared at reporter.apache.org, use it
draft = Reporter.find(@@item)
@report = draft.text if draft
when 'edit item'
@header = 'Edit Discussion Item'
@label = 'text'
@message = "Edit #{@@item.title} Discussion Item"
when 'edit report'
@header = 'Edit Report'
@label = 'report'
@message = "Edit #{@@item.title} Report"
when 'add resolution', 'New Resolution'
@header = 'Add Resolution'
@label = 'resolution'
@title = ''
when 'edit resolution'
@header = 'Edit Resolution'
@label = 'resolution'
@title = ''
when 'post item', 'Discussion Item'
@header = 'Add Discussion Item'
@label = 'text'
@message = 'Add Discussion Item'
when 'post items'
@header = 'Post Discussion Items'
@label = 'text'
@message = 'Post Discussion Items'
when 'edit items'
@header = 'Edit Discussion Items'
@label = 'text'
@message = 'Edit Discussion Items'
end
if not @edited
text = @report || @@item.text || ''
if @@item.title == 'President'
text.sub! /\s*Additionally, please see Attachments \d through \d\d?\./, ''
end
@report = text
@digest = @@item.digest
@alerted = false
@edited = false
@base = @report
elsif not @alerted and @edited and @digest != @@item.digest
alert 'edit conflict'
@alerted = true
else
@report = @base
end
if @header == 'Add Resolution' or @@item.attach =~ /^[47]/
@indent = ' '
elsif @header == 'Add Discussion Item'
@indent = ' '
elsif @@item.attach == '8.'
@indent = ' '
else
@indent = ''
end
end
# default title based on common resolution patterns
def default_title(event)
return if @title
match = nil
if (match = @report.match(/to\s+be\s+known\s+as\s+the\s+"Apache\s+(.*?)\s+Project",\s+be\s+and\s+hereby\s+is\s+established/))
@title = "Establish the Apache #{match[1]} Project"
elsif (match = @report.match(/appointed\s+to\s+the\s+office\s+of\s+Vice\s+President,\s+Apache\s+(.*?),/))
@title = "Change the Apache #{match[1]} Project Chair"
elsif (match = @report.match(/the\s+Apache\s+(.*?)\s+project\s+is\s+hereby\s+terminated/))
@title = "Terminate the Apache #{match[1]} Project"
end
end
# track changes to text value
def change_text(event)
@report = event.target.value
self.change_message()
end
# update default message to reflect whether only whitespace changes were
# made or if there is something more that was done
def change_message()
@edited = (@base != @report)
if @message =~ /(Edit|Reflow) #{@@item.title} Report/
if @edited and @base.gsub(/[ \t\n]+/, '') == @report.gsub(/[ \t\n]+/, '')
@message = "Reflow #{@@item.title} Report"
else
@message = "Edit #{@@item.title} Report"
end
end
end
# determine if reflow button should be default or danger color
def reflow_color()
width = 80 - @indent.length
if @report.split("\n").all? {|line| line.length <= width}
return 'btn-default'
else
return'btn-danger'
end
end
# perform a reflow of report text
def reflow()
report = @report
textarea = document.getElementById('post-report-text')
indent = start = finish = 0
# extract selection (if any)
if textarea and textarea.selectionEnd > textarea.selectionStart
start = textarea.selectionStart
start -= 1 while start > 0 and report[start-1] != "\n"
finish = textarea.selectionEnd
finish += 1 while report[finish] != "\n" and finish < report.length-1
end
# enable special punctuation rules for the incubator
puncrules = (@@item.title == 'Incubator')
# reflow selection or entire report
if finish > start
report = Flow.text(report[start..finish], @indent+indent, puncrules)
report.gsub(/^/, ' ' * indent) if indent > 0
@report = @report[0...start] + report + @report[finish+1..-1]
else
# remove indentation
unless report =~ /^\S/
regex = RegExp.new('^( +)', 'gm')
indents = []
while (result = regex.exec(report))
indents.push result[1].length
end
unless indents.empty?
indent = Math.min(*indents)
report.gsub!(RegExp.new('^' + ' ' * indent, 'gm'), '')
end
end
@report = Flow.text(report, @indent, puncrules)
end
self.change_message()
end
# determine if the form is ready to be submitted
def ready()
return false if @disabled
if @header == 'Add Resolution' or @header == 'Add Discussion Item'
return @report != '' && @title != ''
else
return @report != @@item.text && @message != ''
end
end
# when save button is pushed, post comment and dismiss modal when complete
def submit(event)
@edited = false
if @header == 'Add Resolution' or @header == 'Add Discussion Item'
data = {
agenda: Agenda.file,
attach: (@header == 'Add Resolution') ? '7?' : '8?',
title: @title,
report: @report
}
else
data = {
agenda: Agenda.file,
attach: @attach || @@item.attach,
digest: @digest,
message: @message,
report: @report
}
end
@disabled = true
post 'post', data do |response|
jQuery('#post-report-form').modal(:hide)
document.body.classList.remove('modal-open')
@attach = nil
@disabled = false
Agenda.load response.agenda, response.digest
end
end
#########################################################################
# Treasurer #
#########################################################################
# upload contents of spreadsheet in base64; append extracted table to report
def upload_spreadsheet(event)
@disabled = true
event.preventDefault()
reader = FileReader.new
def reader.onload(event)
# Convert the spreadsheet a byte at a time because
# Chrome JavaScript did not handle the following properly:
# String.fromCharCode(*Uint8Array.new(event.target.result))
# See commit 46058b1e8baff80c75ea72f5b79f2f23af2e87a5
bytes = Uint8Array.new(event.target.result)
binary = ''
for i in 0...bytes.byteLength
binary += String.fromCharCode(bytes[i])
end
post 'financials', spreadsheet: btoa(binary) do |response|
report = @report
report += "\n" if report and not report.end_with? "\n"
report += "\n" if report
report += response.table
self.change_text target: {value: report}
@upload = nil
@disabled = false
end
end
reader.readAsArrayBuffer(document.getElementById('upload').files[0])
end
#########################################################################
# Establish Project #
#########################################################################
def initialize_establish_project()
@search = ''
@pmcname = nil
@pmcdesc = nil
@chair = nil
@pmc = []
# get a list of committers
unless Server.committers
retrieve 'committers', :json do |committers|
Server.committers = committers || []
end
end
# get a list of PMCs
if @pmcs.empty?
post 'post-data', request: 'committee-list' do |response|
@pmcs = response
end
end
end
def establish_pmc(person)
@chair = person unless @chair
@pmc << person
@search = ''
end
def draft_establish_project()
@disabled = true
people = []
Array(document.querySelectorAll('input:checked')).each do |checkbox|
people << checkbox.value
end
options = {
request: 'establish',
pmcname: @pmcname,
parent: @parent,
description: @pmcdesc,
chair: @chair.id,
people: people.join(',')
}
post 'post-data', options do |response|
@button = @header = 'Add Resolution'
@title = response.title
@report = response.draft
@label = 'resolution'
@disabled = false
end
end
#########################################################################
# Terminate Project #
#########################################################################
def initialize_terminate_project()
# get a list of PMCs
if @pmcs.empty?
post 'post-data', request: 'committee-list' do |response|
@pmcs = response
end
end
@termreason = nil
end
def draft_terminate_project()
@disabled = true
options = {
request: 'terminate',
pmc: document.getElementById('terminate-pmc').value,
reason: @termreason
}
post 'post-data', options do |response|
@button = @header = 'Add Resolution'
@title = response.title
@report = response.draft
@label = 'resolution'
@disabled = false
end
end
#########################################################################
# Out of Cycle report #
#########################################################################
def initialize_out_of_cycle()
@disabled = true
# gather a list of reports already on the agenda
scheduled = {}
Agenda.index.each do |item|
if item.attach =~ /^[A-Z]/
scheduled[item.title.downcase] = true
end
end
# get a list of PMCs and select ones that aren't on the agenda
@pmcs = []
post 'post-data', request: 'committee-list' do |response|
response.each do |pmc|
@pmcs << pmc unless scheduled[pmc]
end
end
end
def draft_out_of_cycle_report()
pmc = document.getElementById('out-of-cycle-pmc').value.
gsub(/\b[a-z]/) {|s| s.upcase()}
@button = 'post report'
@disabled = true
@report = ''
@header = 'Post Report'
@label = 'report'
@message = "Post Out of Cycle #{pmc} Report"
@attach = '+' + pmc
@disabled = false
end
#########################################################################
# Change Project Chair #
#########################################################################
def initialize_chair_change()
@disabled = true
@pmcs = []
chair_pmc_change(nil)
post 'post-data', request: 'committee-list' do |response|
@pmcs = response
chair_pmc_change(@pmcs.first)
end
end
def chair_pmc_change(pmc)
@disabled = true
@outgoing_chair = nil
@pmc_members = []
return unless pmc
post 'post-data', request: 'committee-members', pmc: pmc do |response|
@outgoing_chair = response.chair.name
@pmc_members = response.members
@disabled = false
end
end
def parent_pmc_change(pmc)
@roster = []
@parent = pmc
return unless pmc
post 'post-data', request: 'committer-list', pmc: pmc do |response|
@roster = response.members if response
end
end
def draft_chair_change_resolution()
@disabled = true
options = {
request: 'change-chair',
pmc: document.getElementById('change-chair-pmc').value,
chair: document.getElementById('incoming-chair').value
}
post 'post-data', options do |response|
@button = @header = 'Add Resolution'
@title = response.title
@report = response.draft
@label = 'resolution'
@disabled = false
end
end
end