| # |
| # A two section representation of an agenda item (typically a PMC report), |
| # where the two sections will show up as two columns on wide enough windows. |
| # |
| # The first section contains the item text, with a missing indicator if |
| # the report isn't present. It also contains an inline copy of draft |
| # minutes for agenda items in section 3. |
| # |
| # The second section contains posted comments, pending comments, and |
| # action items associated with this agenda item. |
| # |
| # Filters may be used to highlight or hypertext link portions of the text. |
| # |
| |
| class Report < Vue |
| def render |
| _section.flexbox do |
| _section do |
| if @@item.warnings |
| _ul.missing @@item.warnings do |warning| |
| _li warning |
| end |
| end |
| |
| _pre.report do |
| if @@item.text |
| _Text raw: @@item.text, filters: filters |
| elsif @@item.missing |
| draft = Reporter.find(@@item) |
| if draft |
| _p do |
| _em 'Unposted draft being prepared at ' |
| _a 'reporter.apache.org', |
| href: "https://reporter.apache.org/wizard?#{draft.project}" |
| _span ':' |
| end |
| _Text raw: draft.text, filters: [self.draft] |
| else |
| _p {_em 'Missing'} |
| end |
| else |
| _p {_em 'Empty'} |
| end |
| end |
| |
| if (@@item.missing or @@item.comments) and @@item.mail_list |
| _section.reminder do |
| if @@item.missing |
| if not Posted.get(@@item.title).empty? or Reporter.find(@@item) |
| _button.btn.btn_primary 'post report', data_toggle: 'modal', |
| data_target: '#post-report-form' |
| elsif |
| @@item.attach =~ /^[A-Z]/ and |
| User.firstname and @@item.shepherd and |
| User.firstname.start_with? @@item.shepherd.downcase() |
| then |
| _p.comment do |
| _ 'No report was found on ' |
| _a 'board@apache.org', |
| href: 'https://lists.apache.org/list.html?board@apache.org' |
| _ ' archives since the last board report. If/when a report' |
| _ ' is posted there with a ' |
| _tt '[Report]' |
| _ ' tag in the subject line a POST button will appear here ' |
| _ ' to assist with the posting the report.' |
| end |
| end |
| end |
| |
| _Email item: @@item |
| end |
| end |
| |
| if minutes |
| _pre.comment do |
| if minutes === :missing |
| _p { _em 'missing' } |
| else |
| _Text raw: minutes, filters: [hotlink] |
| end |
| end |
| end |
| end |
| |
| _section do |
| _AdditionalInfo item: @@item |
| |
| _div.report_info do |
| _h4 'Report Info' |
| _Info item: @@item |
| end |
| end |
| end |
| end |
| |
| # determine what text filters to run |
| def filters |
| list = [self.linebreak, self.todo, hotlink, self.privates, self.jira, |
| self.cve] |
| list = [self.localtime, hotlink] if @@item.title == 'Call to order' |
| list << self.names if @@item.people |
| list << self.president_attachments if @@item.title == 'President' |
| list << self.linkMinutes if @@item.attach =~ /^[37][A-Z]$/ |
| |
| list |
| end |
| |
| # special processing for Minutes from previous meetings |
| def minutes |
| if @@item.attach =~ /^3[A-Z]$/ |
| # if draft is available, fetch minutes for display |
| date = @@item.text[/board_minutes_(\d+_\d+_\d+)\.txt/, 1] |
| |
| if date and not defined? @@item.minutes and defined? XMLHttpRequest |
| if @@item.mtime |
| Vue.set @@item, 'minutes', '' |
| retrieve "minutes/#{date}?#{@@item.mtime}", :text do |minutes| |
| @@item.minutes = minutes |
| end |
| else |
| @@item.minutes = :missing |
| end |
| end |
| end |
| |
| @@item.minutes |
| end |
| |
| # |
| ### filters |
| # |
| |
| # Highlight todos |
| def todo(text) |
| return text.gsub 'TODO', '<span class="missing">TODO</span>' |
| end |
| |
| # Break long lines, treating HTML Entities (like &) as one character |
| def linebreak(text) |
| # find long, breakable lines |
| regex = Regexp.new(/(\&\w+;|.){80}.+/, 'g') |
| result = nil |
| indicies = []; |
| while result = regex.exec(text) |
| line = result[0] |
| break if line.gsub(/\&\w+;/, '.').length < 80 |
| |
| lastspace = /^.*\s\S/.exec(line) |
| if lastspace and lastspace[0].gsub(/\&\w+;/, '.').length - 1 > 40 |
| indicies.unshift([line, result.index]) |
| end |
| end |
| |
| # reflow each line found |
| indicies.each do |info| |
| line = info[0] |
| index = info[1] |
| replacement = '<span class="hilite" title="reflowed">' + |
| Flow.text(line) + "</span>" |
| |
| text = text.slice(0, index) + replacement + |
| text.slice(index + line.length) |
| end |
| |
| return text |
| end |
| |
| # Convert start time to local time on Call to order page |
| def localtime(text) |
| return text.sub /\n(\s+)(Other Time Zones:.*)/ do |match, spaces, text| |
| localtime = Date.new(@@item.timestamp).toLocaleString() |
| "\n#{spaces}<span class='hilite'>" + |
| "Local Time: #{localtime}</span>#{spaces}#{text}" |
| end |
| end |
| |
| # replace ids with committer links |
| def names(text) |
| roster = '/roster/committer/' |
| |
| @@item.people.each_pair do |id, person| |
| # email addresses in 'Establish' resolutions and (ids) everywhere |
| text.gsub! /(\(|<)(#{id})( at |@|\))/ do |m, pre, id, post| |
| if person.icla |
| if post == ')' and person.member |
| "#{pre}<b><a href='#{roster}#{id}'>#{id}</a></b>#{post}" |
| else |
| "#{pre}<a href='#{roster}#{id}'>#{id}</a>#{post}" |
| end |
| else |
| "#{pre}<a class='missing' href='#{roster}?q=#{person.name}'>" + |
| "#{id}</a>#{post}" |
| end |
| end |
| |
| # names |
| if person.icla or @@item.title == 'Roll Call' |
| pattern = escapeRegExp(person.name).gsub(/ +/, '\s+') |
| if defined? person.member |
| text.gsub! /#{pattern}/ do |match| |
| "<a href='#{roster}#{id}'>#{match}</a>" |
| end |
| else |
| text.gsub! /#{pattern}/ do |match| |
| "<a href='#{roster}?q=#{person.name}'>#{match}</a>" |
| end |
| end |
| end |
| |
| # highlight potentially misspelled names |
| if person.icla and not person.icla == person.name |
| names = person.name.split(/\s+/) |
| iclas = person.icla.split(/\s+/) |
| ok = false |
| ok ||= names.all? {|part| iclas.any? {|icla| icla.include? part}} |
| ok ||= iclas.all? {|part| names.any? {|name| name.include? part}} |
| if @@item.title =~ /^Establish/ and not ok |
| text.gsub! /#{escapeRegExp("#{id}'>#{person.name}")}/, |
| "?q=#{encodeURIComponent(person.name)}'>" + |
| "<span class='commented'>#{person.name}</span>" |
| else |
| text.gsub! /#{escapeRegExp(person.name)}/, |
| "<a href='#{roster}#{id}'>#{person.name}</a>" |
| end |
| end |
| |
| # put members names in bold |
| if person.member |
| pattern = escapeRegExp(person.name).gsub(/ +/, '\s+') |
| text.gsub!(/#{pattern}/) {|match| "<b>#{match}</b>"} |
| end |
| end |
| |
| # treat any unmatched names in Roll Call as misspelled |
| if @@item.title == 'Roll Call' |
| text.gsub! /(\n\s{4})([A-Z].*)/ do |match, space, name| |
| "#{space}<a class='commented' href='#{roster}?q=#{name}'>#{name}</a>" |
| end |
| end |
| |
| # highlight any non-apache.org email addresses in establish resolutions |
| if @@item.title =~ /^Establish/ |
| text.gsub! /(<|\()[-.\w]+@(([-\w]+\.)+\w+)(>|\))/ do |match| |
| if match =~ /@apache\.org/ |
| match |
| else |
| '<span class="commented" title="non @apache.org email address">' + |
| match + '</span>' |
| end |
| end |
| end |
| |
| |
| # highlight mis-spelling of previous and proposed chair names |
| if @@item.title.start_with? 'Change' and text =~ /\(\w[-_.\w]+\)/ |
| text.sub!(/heretofore\s+appointed\s+(\w(\s|.)*?)\s+\(/) do |text, name| |
| text.sub(name, "<span class='hilite'>#{name}</span>") |
| end |
| |
| text.sub!(/chosen\sto\s+recommend\s+(\w(\s|.)*?)\s+\(/) do |text, name| |
| text.sub(name, "<span class='hilite'>#{name}</span>") |
| end |
| end |
| |
| return text |
| end |
| |
| # link to board minutes and other attachments |
| def linkMinutes(text) |
| text.gsub! /board_minutes_(\d+)_\d+_\d+\.txt/ do |match, year| |
| if Server.drafts.include? match |
| link = "https://svn.apache.org/repos/private/foundation/board/#{match}" |
| else |
| link = "http://apache.org/foundation/records/minutes/#{year}/#{match}" |
| end |
| "<a href='#{link}'>#{match}</a>" |
| end |
| |
| footer = '' |
| |
| text.gsub! /Attachment (\w+)/ do |match, attach| |
| item = Agenda.index.find {|item| item.attach == attach} |
| if item |
| footer += "<hr/><h4>#{match}</h4><pre>#{item.text}</pre>" |
| "<a href='#{item.title.gsub(' ', '-')}'>#{match}</a>" |
| else |
| match |
| end |
| end |
| |
| return text + footer |
| end |
| |
| # highlight private sections - these sections appear in the agenda but |
| # will be removed when the minutes are produced (see models/minutes.rb) |
| def privates(text) |
| # block of lines (and preceding whitespace) where the first line starts |
| # with <private> and the last line ends </private>. |
| private_lines = |
| Regexp.new('^([ \t]*<private>(?:\n|.)*?</private>)(\s*)$', |
| 'mig') |
| |
| # mark private sections with class private |
| text.gsub!(private_lines) do |match, text| |
| "<div class='private'>#{text}</div>" |
| end |
| |
| # flag remaining private markers |
| private_tag =/(\s*.\s*)(<\/?private>)(\s*.\s*)/i |
| text.gsub! private_tag do |match, before, text, after| |
| if before.include? '>' or after.include? '<' |
| match |
| elsif before.include? "\n" or after.include? "\n" |
| match |
| else |
| "#{before}<span class='error' " + |
| "title='private sections must consist only of full lines of text'" + |
| ">#{text}</span>#{after}" |
| end |
| end |
| |
| return text |
| end |
| |
| # expand president's attachments |
| def president_attachments(text) |
| match = text.match(/Additionally, please see Attachments (\d) through (\d)/) |
| if match |
| agenda = Agenda.index |
| for i in 0...agenda.length |
| next unless agenda[i].attach =~ /^\d$/ |
| if agenda[i].attach >= match[1] and agenda[i].attach <= match[2] |
| text += "\n #{agenda[i].attach}. " + |
| "<a #{ agenda[i].text.empty? ? 'class="pres-missing" ' : ''}" + |
| "href='#{agenda[i].href}'>#{agenda[i].title}</a>" |
| end |
| end |
| end |
| |
| return text |
| end |
| |
| # hotlink to JIRA issues |
| def jira(text) |
| jira_issue = |
| Regexp.new(/(^|\s|\(|\[)([A-Z][A-Z0-9]+)-([1-9][0-9]*) |
| (\.(\D|$)|[,;:\s)\]]|$)/x, 'g') |
| |
| text.gsub! jira_issue do |m, pre, name, issue, post| |
| if JIRA.find(name) |
| return "#{pre}<a target='_self' " + |
| "href='https://issues.apache.org/jira/browse/#{name}-#{issue}'>" + |
| "#{name}-#{issue}</a>#{post}" |
| else |
| return "#{pre}#{name}-#{issue}#{post}" |
| end |
| end |
| |
| return text |
| end |
| |
| # hotlink to CVE |
| def cve(text) |
| return text.gsub(/\bCVE-\d{4}-\d{4,}\b/) do |id| |
| "<a href='https://cve.mitre.org/cgi-bin/cvename.cgi?name=#{id}'>#{id}</a>" |
| end |
| end |
| |
| def draft(text) |
| return "<div class='private'>#{text}</div>" |
| end |
| end |