blob: aea8c17c0cfc8efabf7258ae27f77ae8fdd55e56 [file] [log] [blame]
#
# 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 &amp;) 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! /(\(|&lt;)(#{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! /(&lt;|\()[-.\w]+@(([-\w]+\.)+\w+)(&gt;|\))/ 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]*&lt;private&gt;(?:\n|.)*?&lt;/private&gt;)(\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*)(&lt;\/?private&gt;)(\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