www/secretary/public-names.cgi (311 lines of code) (raw):

#!/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_chair_or_member? print "Status: 401 Unauthorized\r\n" print "WWW-Authenticate: Basic realm=\"ASF Members and Officers\"\r\n\r\n" exit end ASF::ICLAFiles.update_cache({}) # 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 # Seems it needs to be saved in a variable to ensure it is cached _cache = 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? # construct the commit message 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 path = File.join(ASF::SVN.svnurl('officers'),'iclas.txt') env = Struct.new(:user, :password).new($USER, $PASSWORD) ASF::SVN.update(path,message,env,_) do |_tmpdir, iclas| 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 iclas # return the updated file end 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: ASF::SVN.svnpath!('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.sort_by{|icla| icla.id}.each do |icla| next if icla.noId? 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) if file _a icla.claRef, href: ASF::SVN.svnpath!('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 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