| # |
| # 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 |