www/board/agenda/routes.rb (486 lines of code) (raw):

# # Server side Sinatra routes # require 'whimsy/asf/status' # redirect root to latest agenda get '/' do agenda = dir('board_agenda_*.txt').max pass unless agenda redirect "#{request.path}#{agenda[/\d+_\d+_\d+/].gsub('_', '-')}/" end # alias for latest agenda get '/latest/' do agenda = dir('board_agenda_*.txt').max pass unless agenda call env.merge( 'PATH_INFO' => "/#{agenda[/\d+_\d+_\d+/].gsub('_', '-')}/" ) end # alias for latest agenda in JSON format get '/latest.json' do agenda = dir('board_agenda_*.txt').max pass unless agenda call env.merge!( 'PATH_INFO' => "/#{agenda[/\d+_\d+_\d+/].gsub('_', '-')}.json" ) end get '/calendar.json' do _json do { nextMeeting: ASF::Board.nextMeeting.iso8601, calendar: ASF::Board.calendar.map(&:iso8601), agendas: dir('board_agenda_*.txt').sort, drafts: dir('board_minutes_*.txt').sort } end end # icon get '/whimsy.svg' do send_file File.expand_path('../../../whimsy.svg', __FILE__), type: 'image/svg+xml' end # Progress Web App Manifest get '/manifest.json' do @svgmtime = File.mtime(File.expand_path('../../../whimsy.svg', __FILE__)).to_i @pngmtime = File.mtime(File.expand_path('../public/whimsy.png', __FILE__)).to_i # capture all the variable content hash = { source: File.read("#{settings.views}/manifest.json.erb"), svgmtime: @svgmtime } # detect if there were any modifications etag Digest::MD5.hexdigest(JSON.dump(hash)) content_type 'application/json' erb :"manifest.json" end # redirect shepherd to latest agenda get '/shepherd' do user = ASF::Person.find(env.user).public_name.split(' ').first agenda = dir('board_agenda_*.txt').max pass unless agenda redirect File.dirname(request.path) + "/#{agenda[/\d+_\d+_\d+/].gsub('_', '-')}/shepherd/#{user}" end # redirect missing to missing page for the latest agenda get '/missing' do agenda = dir('board_agenda_*.txt').max pass unless agenda # this will result in a 404 # Support for sending out reminders before the agenda is created. # Useful in cases where the agenda creation is delayed due to # a board election. if agenda < Date.today.strftime('board_agenda_%Y_%m_%d.txt') # update in memory cache with a dummy agenda. The only relevant # part of the agenda that matters for this operation is the list # of pmcs (@pmcs). template = File.join(ASF::SVN['foundation_board'], 'templates', 'board_agenda.erb') @meeting = ASF::Board.nextMeeting agenda = @meeting.strftime('board_agenda_%Y_%m_%d.txt') @directors = ['TBD'] @minutes = [] @owner = ASF::Board::ShepherdStream.new @pmcs = ASF::Board.reporting(@meeting) contents = Erubis::Eruby.new(IO.read(template)).result(binding) Agenda.update_cache(agenda, nil, contents, true) end response.headers['Location'] = "#{agenda[/\d+_\d+_\d+/].gsub('_', '-')}/missing" status 302 end get '/session.json' do _json do {session: Session.user(env.user)} end end # for debugging purposes get '/env' do content_type 'text/plain' asset = { path: Wunderbar::Asset.path, root: Wunderbar::Asset.root, virtual: Wunderbar::Asset.virtual, scripts: Wunderbar::Asset.scripts.map do |script| {path: script.path} end } JSON.pretty_generate(env: env, ENV: ENV.to_h, asset: asset) end # enable debugging of the agenda cache get '/cache.json' do _json Agenda.cache end # agenda followup get %r{/(\d\d\d\d-\d\d-\d\d)/followup\.json} do |date| pass unless Dir.exist? '/srv/mail/board' agenda = "board_agenda_#{date.gsub('-', '_')}.txt" pass unless Agenda.parse agenda, :quick # select agenda items that have comments parsed = Agenda[agenda][:parsed] followup = parsed.reject {|item| item['comments'].to_s.empty?}. map {|item| [item['title'], {comments: item['comments'], shepherd: item['shepherd'], mail_list: item['mail_list'], count: 0}] }.to_h # count number of feedback emails found in the board archive start = Time.parse(date) months = Dir['/srv/mail/board/*'].sort[-2..-1] Dir[*months.map {|month| "#{month}/*"}].each do |file| next unless File.mtime(file) > start raw = File.read(file).force_encoding(Encoding::BINARY) next unless raw =~ /Subject: .*Board feedback on #{date} (.*) report/ followup[$1][:count] += 1 if followup[$1] end # return results _json followup end # pending items get %r{/(\d\d\d\d-\d\d-\d\d)/pending\.json} do pending = Pending.get(env.user) _json pending end # agenda digest information get %r{/(\d\d\d\d-\d\d-\d\d)/digest\.json} do |date| agenda = "board_agenda_#{date.gsub('-', '_')}.txt" _json( { agenda: { file: agenda, digest: Agenda[agenda][:digest], etag: Agenda.uptodate(agenda) ? Agenda[agenda][:etag] : nil }, reporter: Reporter.digest } ) end # feedback get %r{/(\d\d\d\d-\d\d-\d\d)/feedback.json} do |date| @agenda = "board_agenda_#{date.gsub('-', '_')}.txt" @dryrun = true _json :'actions/feedback' end post %r{/(\d\d\d\d-\d\d-\d\d)/feedback.json} do |date| unavailable = Status.updates_disallowed_reason # are updates disallowed? return [503, unavailable] if unavailable @agenda = "board_agenda_#{date.gsub('-', '_')}.txt" @dryrun = false _json :'actions/feedback' end def server if env['REMOTE_USER'] userid = env['REMOTE_USER'] elsif ENV['RACK_ENV'] == 'test' userid = env['HTTP_REMOTE_USER'] || 'test' elsif env.respond_to? :user userid = env.user else require 'etc' userid = Etc.getlogin end pending = Pending.get(userid) # determine who is present @present = [] @present_mtime = nil file = File.join(AGENDA_WORK, 'sessions', 'present.yml') if File.exist?(file) and File.mtime(file) != @present_mtime @present_mtime = File.mtime(file) @present = YAML.load_file(file). reject {|name| name =~ /^board_agenda_[_\d]+$/} end if env['SERVER_NAME'] == 'localhost' websocket = 'ws://localhost:34234/' else websocket = (env['rack.url_scheme'].sub('http', 'ws')) + '://' + env['SERVER_NAME'] + env['SCRIPT_NAME'] + '/websocket/' end @server = { userid: userid, agendas: dir('board_agenda_*.txt').sort, drafts: dir('board_minutes_*.txt').sort, pending: pending, username: pending['username'], firstname: pending['firstname'], initials: pending['initials'], online: @present, session: Session.user(userid), role: pending['role'], directors: Hash[ASF::Service['board'].members.map {|person| initials = begin YAML.load_file(File.join(AGENDA_WORK, "#{person.id}.yml"))['initials'] rescue person.public_name.gsub(/[^A-Z]/, '').downcase end [initials, person.public_name.split(' ').first] }], websocket: websocket } end get '/server.json' do _json server end # all agenda pages get %r{/(\d\d\d\d-\d\d-\d\d)/(.*)} do |date, path| agenda = "board_agenda_#{date.gsub('-', '_')}.txt" pass unless Agenda.parse agenda, :quick @base = "#{env['SCRIPT_NAME']}/#{date}/" @server = server @page = { path: path, query: params['q'], agenda: agenda, parsed: Agenda[agenda][:parsed], digest: Agenda[agenda][:digest], etag: Agenda.uptodate(agenda) ? Agenda[agenda][:etag] : nil } minutes = AGENDA_WORK + '/' + agenda.sub('agenda', 'minutes').sub('.txt', '.yml') @page[:minutes] = YAML.safe_load(File.read(minutes), permitted_classes: [Symbol]) if File.exist? minutes @cssmtime = File.mtime('public/stylesheets/app.css').to_i @manmtime = File.mtime("#{settings.views}/manifest.json.erb").to_i @appmtime = Wunderbar::Asset.convert("#{settings.views}/app.js.rb").mtime.to_i @server[:swmtime] = File.mtime("#{settings.views}/sw.js.rb").to_i if path == 'bootstrap.html' unless env.password @server[:userid] = nil @server[:role] = nil end @page[:parsed] = [ {title: 'Roll Call', timestamp: @page[:parsed].first['timestamp']} ] @page[:digest] = nil @page[:etag] = nil @server[:session] = nil # capture all the variable content hash = { source: File.read("#{settings.views}/bootstrap.html.erb"), cssmtime: @cssmtime, appmtime: @appmtime, manmtime: @manmtime, scripts: Wunderbar::Asset.scripts. map {|script| [script.path, script.mtime.to_i]}.sort, stylesheets: Wunderbar::Asset.stylesheets. map {|stylesheet| [stylesheet.path, stylesheet.mtime.to_i]}.sort, server: @server, page: @page } # detect if there were any modifications etag Digest::MD5.hexdigest(JSON.dump(hash)) erb :"bootstrap.html" else _html :main end end # append slash to agenda page if not present get %r{/(\d\d\d\d-\d\d-\d\d)} do |date| redirect to("/#{date}/") end # post item support get '/json/post-data' do _json :"actions/post-data" end # feedback responses get '/json/responses' do _json :"actions/responses" end # posted reports get '/json/posted-reports' do _json :"actions/posted-reports" end post '/json/posted-reports' do unavailable = Status.updates_disallowed_reason # are updates disallowed? return [503, unavailable] if unavailable _json :"actions/posted-reports" end # podling name searches get '/json/podlingnamesearch' do _json ASF::Podling.namesearch end # podling name searches get '/json/reporter' do _json Reporter.drafts(env) end # posted actions post '/json/:file' do unavailable = Status.updates_disallowed_reason # are updates disallowed? return [503, unavailable] if unavailable _json :"actions/#{params[:file]}" end # Raw minutes get %r{/(\d\d\d\d-\d\d-\d\d).ya?ml} do |file| minutes = AGENDA_WORK + '/' + "board_minutes_#{file.gsub('-', '_')}.yml" pass unless File.exist? minutes _text File.read minutes end # updates to agenda data get %r{/(\d\d\d\d-\d\d-\d\d).json} do |date| file = "board_agenda_#{date.gsub('-', '_')}.txt" pass unless Agenda.parse file, :full begin _json do last_modified Agenda[file][:mtime] minutes_file = AGENDA_WORK + '/' + file.sub('_agenda_', '_minutes_'). sub('.txt', '.yml') # merge in minutes, if available if File.exist? minutes_file minutes = YAML.load_file(minutes_file) Agenda[file][:parsed].each do |item| item[:minutes] = minutes[item['title']] if minutes[item['title']] end end agenda = Agenda[file][:parsed] # filter list for non-PMC chairs and non-officers user = env.respond_to?(:user) && ASF::Person.find(env.user) unless !user or user.asf_chair_or_member? status 206 # Partial Content committees = user.committees.map(&:display_name) agenda = agenda.select {|item| committees.include? item['title']} end # Add banner (or nil) to the first entry # must always update the entry as they may be cached agenda.first['banner'] = Status.banner agenda end ensure Agenda[file][:etag] = headers['ETag'] end end # draft committers report get %r{/text/summary/(\d\d\d\d-\d\d-\d\d)} do |date| @date = date.gsub('-', '_') _text :committers_report end # draft minutes get '/text/minutes/:file' do |file| file = "board_minutes_#{file.gsub('-', '_')}.txt" if dir('board_minutes_*.txt').include? file path = File.join(FOUNDATION_BOARD, file) elsif not Dir[File.join(ASF::SVN['minutes'], file[/\d+/], file)].empty? path = File.join(ASF::SVN['minutes'], file[/\d+/], file) else pass end _text do last_modified File.mtime(path) _ File.read(path) end end # jira project info get '/json/jira' do uri = URI.parse('https://issues.apache.org/jira/rest/api/2/project') http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true http.verify_mode = OpenSSL::SSL::VERIFY_NONE request = Net::HTTP::Get.new(uri.request_uri) response = http.request(request) _json { JSON.parse(response.body).map {|project| project['key']} } end # get list of committers (for use in roll-call) get '/json/committers' do _json do members = ASF.search_one(ASF::Group.base, 'cn=member', 'memberUid').first members = Hash[members.map {|name| [name, true]}] ASF.search_one(ASF::Person.base, 'uid=*', ['uid', 'cn']). map {|person| {id: person['uid'].first, member: members[person['uid'].first] || false, name: person['cn'].first.force_encoding('utf-8')}}. sort_by {|person| person[:name].downcase.unicode_normalize(:nfd)} end end # Secretary post-meeting todos get '/json/secretary-todos/:date' do unavailable = Status.updates_disallowed_reason # are updates disallowed? return [503, unavailable] if unavailable _json :'actions/todos' end post '/json/secretary-todos/:date' do unavailable = Status.updates_disallowed_reason # are updates disallowed? return [503, unavailable] if unavailable _json :'actions/todos' end # potential actions get '/json/potential-actions' do _json :'actions/potential-actions' end get %r{/json/(reminder[12]|non-responsive)} do |reminder| @reminder = reminder _json :'actions/reminder-text' end # chat log get %r{/json/chat/(\d\d\d\d_\d\d_\d\d)} do |date| log = "#{AGENDA_WORK}/board_agenda_#{date}-chat.yml" if File.exist? log _json YAML.safe_load(File.read(log), permitted_classes: [Symbol]) else _json [] end end # historical comments, filtered to only include the list of projects which # the user is a member of the PMC for non-ASF-members and non-officers. get '/json/historical-comments' do user = env.respond_to?(:user) && ASF::Person.find(env.user) comments = HistoricalComments.comments unless !user or user.asf_chair_or_member? status 206 # Partial Content committees = user.committees.map(&:display_name) comments = comments.select do |project, _list| committees.include? project end end _json comments.to_h end # draft minutes get '/text/draft/:file' do |file| agenda = "board_agenda_#{file.gsub('-', '_')}.txt" minutes = AGENDA_WORK + '/' + agenda.sub('_agenda_', '_minutes_').sub('.txt', '.yml') _text do Dir.chdir(FOUNDATION_BOARD) do if Dir['board_agenda_*.txt'].include?(agenda) _ Minutes.draft(agenda, minutes) else halt 404 end end end end # draft new agenda get '/new' do # extract time and date for next meeting, month of previous meeting @meeting = ASF::Board.nextMeeting localtime = ASF::Board::TIMEZONE.utc_to_local(@meeting) @tzlink = ASF::Board.tzlink(localtime) zone = ASF::Board::TIMEZONE.name @start_time = localtime.strftime('%H:%M') + ' ' + zone duration = 1.hours @adjournment = (localtime + duration).strftime('%H:%M') + ' ' + zone @prev_month = @meeting.to_date.prev_month.strftime('%B') # retrieve latest committee info # TODO: this is the workspace copy -- should it be using the copy from SVN instead? cinfo = File.join(ASF::SVN['board'], 'committee-info.txt') info = ASF::SVN.getInfo(cinfo, env.user, env.password) contents = ASF::SVN.svn('cat', cinfo, {env: env}) ASF::Committee.load_committee_info(contents, info) # extract committees expected to report 'next month' next_month = contents[/Next month.*?\n\n/m].chomp @next_month = next_month[/(.*#.*\n)+/] || '' # get potential actions begin actions = JSON.parse(Wunderbar::JsonBuilder.new({}).instance_eval( File.read("#{settings.views}/actions/potential-actions.json.rb"), ).target!, symbolize_names: true)[:actions] rescue IOError => e Wunderbar.warn "#{e}, could not access previous actions, continuing" actions = nil end # Get directors, list of pmcs due to report, and shepherds @directors = ASF::Board.directors @pmcs = ASF::Board.reporting(@meeting) @owner = ASF::Board::ShepherdStream.new(actions) # Get list of unpublished and unapproved minutes (used by the agenda template) latest = Dir["#{AGENDA_WORK}/board_minutes*.yml"].max if latest draft = YAML.load_file(latest) else draft = {} # allow for missing yml file end @minutes = dir('board_agenda_*.txt'). map {|file| Date.parse(file[/\d[_\d]+/].gsub('_', '-'))}. reject {|date| date >= @meeting.to_date}. reject {|date| draft[date.strftime('%B %d, %Y')] == 'approved'}. sort template = File.join(ASF::SVN['foundation_board'], 'templates', 'board_agenda.erb') @disabled = dir('board_agenda_*.txt'). include? @meeting.strftime('board_agenda_%Y_%m_%d.txt') begin @agenda = Erubis::Eruby.new(IO.read(template)).result(binding) rescue => error status 500 STDERR.puts error return "error in #{template} in: #{error}" end @cssmtime = File.mtime('public/stylesheets/app.css').to_i _html :new end # post a new agenda post %r{/(\d\d\d\d-\d\d-\d\d)/} do |date| unavailable = Status.updates_disallowed_reason # are updates disallowed? return [503, unavailable] if unavailable boardurl = ASF::SVN.svnurl('foundation_board') agenda = "board_agenda_#{date.gsub('-', '_')}.txt" contents = params[:agenda].gsub("\r\n", "\n") Dir.mktmpdir do |dir| ASF::SVN.svn!('checkout', [boardurl, dir], {depth: 'empty', env: env}) agendapath = File.join(dir, agenda) File.write agendapath, contents ASF::SVN.svn!('add', agendapath) currentpath = File.join(dir, 'current.txt') ASF::SVN.svn!('update', currentpath, {env: env}) if File.symlink? currentpath # Does the symlink exist? File.unlink currentpath File.symlink agenda, currentpath else Wunderbar.warn 'current.txt link does not exist, creating it' File.symlink agenda, currentpath ASF::SVN.svn!('add', currentpath) end ASF::SVN.svn!('commit', [agendapath, currentpath], {msg: "Post #{date} agenda", env: env}) Agenda.update_cache agenda, agendapath, contents, false end auto_remind(date, agenda) redirect to("/#{date}/") end get %r{/testautoremind/(\d\d\d\d-\d\d-\d\d)/} do |date| agenda = "board_agenda_#{date.gsub('-', '_')}.txt" @dryrun = !Status.testnode? # For debug only! _json auto_remind(date, agenda) end # Code to send initial reminders def auto_remind(date, agenda) # draft reminder text @reminder = 'reminder1' # reminder template @tzlink = ASF::Board.tzlink(ASF::Board::TIMEZONE.utc_to_local(ASF::Board.nextMeeting)) reminder = eval(File.read('views/actions/reminder-text.json.rb')) # extract data @subject = reminder[:subject] @message = reminder[:body] # send reminders and summary @summary = 'reminder-summary' # template name @sendsummary = true @agenda = agenda @meeting = date boardchair = ASF::Committee.officers.select{|h| h.name == 'boardchair'}.first.chairs.first[:name] @from = "\"#{boardchair}\" <board-chair@apache.org>" eval(File.read('views/actions/send-reminders.json.rb')) end