blob: d8cb58d2560e1fcb845a481963b0c27c721ec584 [file] [log] [blame]
#!/usr/bin/env ruby
$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'whimsy/asf'
require 'wunderbar/script'
require 'ruby2js/filter/functions'
# only available to ASF members and PMC chairs
user = ASF::Person.new($USER)
unless user.asf_member? or ASF.pmc_chairs.include? user
print "Status: 401 Unauthorized\r\n"
print "WWW-Authenticate: Basic realm=\"ASF Members and Officers\"\r\n\r\n"
exit
end
# default HOME directory
require 'etc'
ENV['HOME'] ||= Etc.getpwuid.dir
_html do
_style :system if @updates
_style_ %{
table {border-collapse: collapse}
table, th, td {border: 1px solid black}
td {padding: 3px 6px}
th {background-color: #a0ddf0}
tr:hover .diff {background-color: #AAF}
td[draggable=true] {cursor: move}
td.modified {background-color: #FF0}
td.over {background-color: #FFA}
input[type=text] {width: 100%; box-sizing: border-box}
input[type=submit] {margin-top: 1em}
}
_h1 "public names: LDAP vs iclas.txt"
# prefetch LDAP data
people = ASF::Person.preload(%w(cn dn))
if @updates
##################################################################
# Apply Updates #
##################################################################
_h2_ 'Applying updates'
updates = JSON.parse(@updates)
# scope out the work to be done
svn_updates = []
ldap_updates = []
updates.each do |id, names|
svn_updates << id if names['legal_name'] or names['public_name']
ldap_updates << id if names['ldap']
end
# update SVN
unless svn_updates.empty?
officers = Dir.mktmpdir.untaint
_.system ['svn', 'checkout', '--depth', 'empty',
(['--username', $USER, '--password', $PASSWORD] if $PASSWORD),
'https://svn.apache.org/repos/private/foundation/officers',
officers]
_.system ['svn', 'update',
(['--username', $USER, '--password', $PASSWORD] if $PASSWORD),
officers + '/iclas.txt']
next unless File.exist? officers + '/iclas.txt'
iclas = File.read(officers + '/iclas.txt')
updates.each do |id, names|
pattern = Regexp.new("^#{Regexp.escape(id)}:(.*?):(.*?):")
if names['legal_name']
iclas[pattern,1] = names['legal_name'].gsub("\u00A0", ' ')
end
if names['public_name']
iclas[pattern,2] = names['public_name'].gsub("\u00A0", ' ')
end
end
File.write(officers + '/iclas.txt', ASF::ICLA.sort(iclas))
_.system ['svn', 'diff', officers + '/iclas.txt']
if svn_updates.length > 8
message = "Update #{svn_updates.length} names"
else
message = "Update names for #{svn_updates.sort.join(', ')}"
if svn_updates.length == 1
update = updates[svn_updates.first]
if not update['legal_name']
message = "Update public name for #{svn_updates.first}"
elsif not update['public_name']
message = "Update legal name for #{svn_updates.first}"
end
else
if svn_updates.all? {|update| not update['legal_name']}
message = "Update public names for #{svn_updates.sort.join(', ')}"
elsif svn_updates.all? {|update| not update['public_name']}
message = "Update legal names for #{svn_updates.sort.join(', ')}"
end
end
end
_.system ['svn', 'commit', '-m', message,
['--no-auth-cache', '--non-interactive'],
(['--username', $USER, '--password', $PASSWORD] if $PASSWORD),
officers + '/iclas.txt']
end
# update LDAP
unless ldap_updates.empty?
ASF::LDAP.bind($USER, $PASSWORD) do
_pre 'ldapmodify', class: '_stdin'
updates.each do |id, names|
next unless names['ldap']
person = ASF::Person.new(id)
_pre person.dn, class: '_stdout'
person.cn = names['ldap'].gsub("\u00A0", ' ')
end
end
end
else
##################################################################
# Instructions #
##################################################################
_h2_ 'Instructions:'
_ul do
_li 'Double click to edit.'
_li 'Drag/drop to copy.'
_li 'When done, click "Commit Changes" (at the bottom of the page).'
end
end
####################################################################
# Show LDAP differences where entry is present in icla.txt #
####################################################################
# prefetch ICLA data
ASF::ICLA.preload
_h2_!.present! do
_ 'Present in '
_a 'iclas.txt',
href: 'https://svn.apache.org/repos/private/foundation/officers/iclas.txt'
_ ':'
end
_table do
# column number and order MUST agree with columnNames variable below
_tr do
_th "availid"
_th "ICLA file"
_th "iclas.txt real name"
_th "iclas.txt public name"
_th "LDAP cn"
end
ASF::ICLA.each do |icla|
next if icla.id == 'notinavail'
person = ASF::Person.find(icla.id)
next unless person.dn and person.attrs['cn']
if person.cn != icla.name
# locate point at which names differ
first, last = 0, -1
length = [icla.name.length, person.cn.length].min
while icla.name[first] == person.cn[first]
first += 1
end
while icla.name[last] == person.cn[last] and length >= first-last
last -= 1
end
if icla.name[last] == ' ' and icla.name[last] == person.cn[last]
last -= 1 if (icla.name.length - person.cn.length).abs > 1
end
_tr_ do
_td! do
_a icla.id, href: "/roster/committer/#{icla.id}"
end
_td do
file = ASF::ICLAFiles.match_claRef(icla.claRef.untaint)
if file
_a icla.claRef, href: "https://svn.apache.org/repos/private/documents/iclas/#{file}"
else
_ icla.claRef || 'unknown'
end
end
_td icla.legal_name.gsub(' ', "\u00A0"), draggable: 'true'
if
icla.name[first..last].length > length/2 and
person.cn[first..last].length > length/2
then
_td icla.name, draggable: 'true'
_td person.cn, draggable: 'true'
else
_td! draggable: 'true' do
_ icla.name[0...first] unless first == 0
_span.diff icla.name[first..last].gsub(' ', "\u00A0")
_ icla.name[last+1..-1] unless last == -1
end
_td! draggable: 'true' do
_ person.cn[0...first] unless first == 0
_span.diff person.cn[first..last].gsub(' ', "\u00A0")
_ person.cn[last+1..-1] unless last == -1
end
end
end
end
end
end
####################################################################
# Show LDAP differences where entry is NOT present in iclas.txt #
####################################################################
icla = ASF::ICLA.availids
ldap = ASF::Person.list.sort_by(&:name)
ldap.delete ASF::Person.new('apldaptest')
unless ldap.all? {|person| icla.include? person.id}
_h2_.missing! 'Only in LDAP'
_table do
_tr do
_th 'id'
_th 'cn'
_th 'mail'
_th 'Committer?' # non-committers won't have iclas (usually)
end
ldap.each do |person|
next if icla.include? person.id
_tr_ do
_td! do
_a person.id, href: "/roster/committer/#{person.id}"
end
_td person.cn
_td person.mail.first
_td person.asf_committer?
end
end
end
end
####################################################################
# Form used to submit changes #
####################################################################
_form_ method: 'post' do
_input type: 'hidden', name: 'updates'
_input type: 'submit', value: 'Commit Changes', disabled: true
end
####################################################################
# Client side logic #
####################################################################
_script do
# track current drag operation
row = nil
dragText = nil
# enable submit button only when there is modifications
def enable_submit()
button = document.querySelector('input[type=submit]')
modified = document.querySelectorAll('td.modified')
button.disabled = (modified.length == 0)
end
# add drag/drop, mouse click event handlers to cells marked as draggable
Array(document.getElementsByTagName('td')).each do |td|
next unless td.getAttribute('draggable') == 'true'
# dragstart: capture row and textContent
td.addEventListener(:dragstart) do |event|
row = event.target.parentNode
dragText = this.textContent
event.dataTransfer.setData('text/plain', dragText)
end
# dragover: add CSS class 'over' if same row and text is different
td.addEventListener(:dragover) do |event|
return unless row == event.target.parentNode
if event.target.textContent != dragText
event.target.classList.add 'over'
event.preventDefault()
end
end
# dragleave: remove CSS class 'over'
td.addEventListener(:dragleave) do |event|
event.currentTarget.classList.remove 'over'
end
# drop: update text after capturing original text
td.addEventListener(:drop) do |event|
data = event.dataTransfer.getData('text/plain')
event.target.classList.remove 'over'
if not event.target.getAttribute('data-original')
event.target.setAttribute('data-original', event.target.textContent)
event.target.classList.add 'modified'
elsif data == event.target.getAttribute('data-original')
event.target.removeAttribute('data-original')
event.target.classList.remove 'modified'
else
event.target.classList.add 'modified'
end
event.target.textContent = data
event.preventDefault()
enable_submit()
row = nil
end
# mouseup: replace cell with an input field
td.addEventListener(:dblclick) do |event|
input = document.createElement('input')
input.setAttribute('type', 'text')
input.value = event.target.textContent
if not event.target.getAttribute('data-original')
event.target.setAttribute('data-original', input.value)
end
event.target.firstChild.remove() while event.target.firstChild
event.target.appendChild(input)
event.target.setAttribute('draggable', 'false')
input.focus()
# when focus leaves input, replace cell with modified text
input.addEventListener(:blur) do |event|
parent = input.parentNode
value = input.value
input.remove()
parent.textContent = value
parent.setAttribute('draggable', 'true')
if value == parent.getAttribute('data-original')
parent.removeAttribute('data-original')
parent.classList.remove 'modified'
else
parent.classList.add 'modified'
end
enable_submit()
end
end
end
# capture modifications when button is pressed
document.querySelector('input[type=submit]').addEventListener(:click) do
updates = {}
# Must agree with number of columns in the main table above
columnNames = %w(id icla_file legal_name public_name ldap)
Array(document.querySelectorAll('td.modified')).each do |td|
id = td.parentNode.firstElementChild.textContent.strip()
updates[id] ||= {}
updates[id][columnNames[td.cellIndex]] = td.textContent
end
document.querySelector('form input').value = JSON.stringify(updates)
end
# force submit state on initial load (i.e., disable submit button)
enable_submit()
end
end