Commit 99a387a9ca5b0cc8fee7ae3be8a9cc1e681f791a:
split committer => person/*
Branch: refs/heads/master
Author: Sam Ruby <[email protected]>
Committer: Sam Ruby <[email protected]>
Pusher: rubys <[email protected]>
------------------------------------------------------------
www/roster/views/actions/fullname.json.rb |
www/roster/views/app.js.rb | ++++++++++++ -
www/roster/views/committer.html.rb | + -
www/roster/views/committer.js.rb | -------------
www/roster/views/person.js.rb | ++++++++
www/roster/views/person/email.js.rb | +++++++++++
www/roster/views/person/forms.js.rb | +++++++++
www/roster/views/person/fullname.js.rb | +++++++++++++++
www/roster/views/person/github.js.rb | +++++++++
www/roster/views/person/memstat.js.rb | +++++++++
www/roster/views/person/memtext.rb | +++++++++++++++
www/roster/views/person/pgpkeys.js.rb | +++++++++++++++
www/roster/views/person/sascore.js.rb | +++++++++++++++
www/roster/views/person/sshkeys.js.rb | ++++++++++++
www/roster/views/person/urls.js.rb | ++++++++++
------------------------------------------------------------
954 changes: 551 additions, 403 deletions.
------------------------------------------------------------
diff --git a/www/roster/views/actions/fullname.json.rb
b/www/roster/views/actions/fullname.json.rb
new file mode 100644
index 0000000..7ef586e
--- /dev/null
+++ b/www/roster/views/actions/fullname.json.rb
@@ -0,0 +1,48 @@
+#
+# Update LDAP SpamAssassin score attribute for a committer
+#
+person = ASF::Person.find(@userid)
+
+# update LDAP
+if person.public_name != @publicname
+ _ldap.update do
+ _previous person.public_name
+
+ if @publicname and not @dryrun
+ person.modify 'cn', @publicname
+ end
+ end
+end
+
+# determine commit message
+if person.icla.legal_name != @legalname
+ if person.icla.name != @publicname
+ message = "Update legal and public name for #{@userid}"
+ else
+ message = "Update legal name for #{@userid}"
+ end
+elsif person.icla.name != @publicname
+ message = "Update public name for #{@userid}"
+else
+ message = nil
+end
+
+# update iclas.txt
+if message
+ icla_txt = ASF::SVN['private/foundation/officers/iclas.txt']
+ _svn.update icla_txt, message: message do |dir, text|
+ # replace legal and public names in icla record
+ userid = Regexp.escape(@userid)
+ text[/^#{userid}:(.*?):/, 1] = @legalname
+ text[/^#{userid}:.*?:(.*?):/, 1] = @publicname
+
+ text
+ end
+end
+
+# update cache
+person.icla.legal_name = @legalname
+person.icla.name = @publicname
+
+# return updated committer info
+_committer Committer.serialize(@userid, env)
diff --git a/www/roster/views/actions/pubname.json.rb
b/www/roster/views/actions/pubname.json.rb
deleted file mode 100644
index 7ef586e..0000000
--- a/www/roster/views/actions/pubname.json.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-#
-# Update LDAP SpamAssassin score attribute for a committer
-#
-person = ASF::Person.find(@userid)
-
-# update LDAP
-if person.public_name != @publicname
- _ldap.update do
- _previous person.public_name
-
- if @publicname and not @dryrun
- person.modify 'cn', @publicname
- end
- end
-end
-
-# determine commit message
-if person.icla.legal_name != @legalname
- if person.icla.name != @publicname
- message = "Update legal and public name for #{@userid}"
- else
- message = "Update legal name for #{@userid}"
- end
-elsif person.icla.name != @publicname
- message = "Update public name for #{@userid}"
-else
- message = nil
-end
-
-# update iclas.txt
-if message
- icla_txt = ASF::SVN['private/foundation/officers/iclas.txt']
- _svn.update icla_txt, message: message do |dir, text|
- # replace legal and public names in icla record
- userid = Regexp.escape(@userid)
- text[/^#{userid}:(.*?):/, 1] = @legalname
- text[/^#{userid}:.*?:(.*?):/, 1] = @publicname
-
- text
- end
-end
-
-# update cache
-person.icla.legal_name = @legalname
-person.icla.name = @publicname
-
-# return updated committer info
-_committer Committer.serialize(@userid, env)
diff --git a/www/roster/views/app.js.rb b/www/roster/views/app.js.rb
index 67d716e..59ae46e 100644
--- a/www/roster/views/app.js.rb
+++ b/www/roster/views/app.js.rb
@@ -5,7 +5,18 @@
require_relative 'pmc/committers'
require_relative 'pmc/confirm'
-require_relative 'committer'
+require_relative 'person'
+require_relative 'person/fullname'
+require_relative 'person/urls'
+require_relative 'person/email'
+require_relative 'person/pgpkeys'
+require_relative 'person/sshkeys'
+require_relative 'person/github'
+require_relative 'person/memstat'
+require_relative 'person/memtext'
+require_relative 'person/forms'
+require_relative 'person/sascore'
+
require_relative 'committerSearch'
require_relative 'group'
diff --git a/www/roster/views/committer.html.rb
b/www/roster/views/committer.html.rb
index 97dae72..3a53fd7 100644
--- a/www/roster/views/committer.html.rb
+++ b/www/roster/views/committer.html.rb
@@ -17,6 +17,6 @@
_script src: 'app.js'
_.render '#main' do
- _Committer committer: @committer, auth: @auth
+ _Person committer: @committer, auth: @auth
end
end
diff --git a/www/roster/views/committer.js.rb b/www/roster/views/committer.js.rb
deleted file mode 100644
index 8ffd3ee..0000000
--- a/www/roster/views/committer.js.rb
+++ /dev/null
@@ -1,401 +0,0 @@
-class Committer < React
- def initialize
- @committer = {}
- @response = nil
- end
-
- def render
- # usage information for authenticated users (owner, secretary, etc.)
- if @auth
- _div.alert.alert_success 'Double click on a field in this color to edit.'
- end
-
- _h2 "#{@committer.id}@apache.org"
-
- _table.wide do
-
- # Name
- _tr data_edit: ('pubname' if @@auth.secretary) do
- _td 'Name'
- _td do
- name = @committer.name
-
- if @edit_pubname
- _form.inline method: 'post' do
- _div do
- _label 'public name', for: 'publicname'
- _input.publicname! name: 'publicname', required: true,
- defaultValue: name.public_name
- end
- _div do
- _label 'legal name', for: 'legalname'
- _input.legalname! name: 'legalname', required: true,
- defaultValue: name.legal_name
- end
- _button.btn.btn_primary 'submit'
- end
- else
- if
- name.public_name==name.legal_name and
- name.public_name==name.ldap
- then
- _span @committer.name.public_name
- else
- _ul do
- _li "#{@committer.name.public_name} (public name)"
-
- if name.legal_name and name.legal_name != name.public_name
- _li "#{@committer.name.legal_name} (legal name)"
- end
-
- if name.ldap and name.ldap != name.public_name
- _li "#{@committer.name.ldap} (ldap)"
- end
- end
- end
- end
- end
- end
-
- # Personal URL
- if @committer.urls
- _tr do
- _td 'Personal URL'
- _td do
- _ul @committer.urls do |url|
- _li {_a url, href: url}
- end
- end
- end
- end
-
- # Committees
- committees = @committer.committees
- unless committees.empty?
- _tr do
- _td 'Committees'
- _td do
- _ul committees do |pmc|
- _li {_a pmc, href: "committee/#{pmc}"}
- end
- end
- end
- end
-
- # Committer
- commit_list = @committer.committer
- unless commit_list.all? {|pmc| committees.include? pmc}
- _tr do
- _td 'Committer'
- _td do
- _ul commit_list do |pmc|
- next if committees.include? pmc
- _li {_a pmc, href: "committee/#{pmc}"}
- end
- end
- end
- end
-
- # Groups
- unless @committer.groups.empty?
- _tr do
- _td 'Groups'
- _td do
- _ul @committer.groups do |group|
- next if group == 'apldap'
-
- if group == 'committers'
- _li {_a group, href: "committer/"}
- elsif group == 'member'
- _li {_a group, href: "members"}
- else
- _li {_a group, href: "group/#{group}"}
- end
- end
- end
- end
- end
-
- # Email addresses
- if @committer.mail
- _tr do
- _td 'Email addresses'
- _td do
- _ul @committer.mail do |url|
- _li do
- _a url, href: 'mailto:' + url
- end
- end
- end
- end
- end
-
- # PGP keys
- if @committer.pgp
- _tr do
- _td 'PGP keys'
- _td do
- _ul @committer.pgp do |key|
- _li do
- if key =~ /^[0-9a-fA-F ]+$/
- _samp do
- _a key, href: 'https://sks-keyservers.net/pks/lookup?' +
- 'op=index&search=0x' + key.gsub(' ', '')
- end
- else
- _samp key
- end
- end
- end
- end
- end
- end
-
- # SSH keys
- if @committer.ssh
- _tr do
- _td 'SSH keys'
- _td do
- _ul @committer.ssh do |key|
- _li.ssh do
- _pre.wide key
- end
- end
- end
- end
- end
-
- # GitHub username
- if @committer.githubUsername
- _tr do
- _td 'GitHub username'
- _td do
- _a @committer.githubUsername, href:
- "https://github.com/" + @committer.githubUsername
- end
- end
- end
-
- if @committer.member
- # Member status
- if @committer.member.status
- _tr data_edit: ('memstat' if @@auth.secretary) do
- _td 'Member status'
- if @committer.member.info
- _td do
- _span @committer.member.status
- if @edit_memstat
- _form.inline method: 'post' do
- if @committer.member.status.include? 'Active'
- _button.btn.btn_primary 'move to emeritus',
- name: 'action', value: 'emeritus'
- elsif @committer.member.status.include? 'Emeritus'
- _button.btn.btn_primary 'move to active',
- name: 'action', value: 'active'
- end
- end
- end
- end
- else
- _td.not_found 'Not in members.txt'
- end
- end
- end
-
- # Members.txt
- if @committer.member.info
- _tr data_edit: 'memtext' do
- _td 'Members.txt'
- _td do
- if @edit_memtext
- _form.inline method: 'post' do
- _div do
- _textarea defaultValue: @committer.member.info
- end
- _button.btn.btn_primary 'submit'
- end
- else
- _pre @committer.member.info,
- class: ('small' if @committer.member.info =~ /.{81}/)
- end
- end
- end
- end
-
- if @committer.member.nomination
- _tr do
- _td 'Nomination'
- _td {_pre @committer.member.nomination}
- end
- end
-
- # Forms on file
- if @committer.forms
- documents = "https://svn.apache.org/repos/private/documents"
- _tr do
- _td 'Forms on file'
- _td do
- _ul do
- for form in @committer.forms
- link = @committer.forms[form]
-
- if form == 'icla'
- _li do
- _a 'ICLA', href: "#{documents}/iclas/#{link}"
- end
- elsif form == 'member'
- _li do
- _a 'Membership App',
- href: "#{documents}/member_apps/#{link}"
- end
- else
- _li "#{form}: #{link}"
- end
- end
- end
- end
- end
- end
- end
-
- # SpamAssassin score
- _tr data_edit: 'sascore' do
- _td 'SpamAssassin score'
- _td do
- if @edit_sascore
- _form method: 'post' do
- _input type: 'number', min: 0, max: 10,
- name: 'sascore', defaultValue: @committer.sascore
- _input type: 'submit', value: 'submit'
- end
- else
- _span @committer.sascore
- end
- end
- end
- end
-
- # modal dialog for dry run results
- _div.modal.fade tabindex: -1 do
- _div.modal_dialog do
- _div.modal_content do
- _div.modal_header do
- _button.close 'x', data_dismiss: 'modal'
- _h4 'Dry run results'
- end
- _div.modal_body do
- _textarea value: JSON.stringify(@response, nil, 2), readonly: true
- end
- _div.modal_footer do
- _button.btn.btn_default 'Close', data_dismiss: 'modal'
- end
- end
- end
- end
- end
-
- # capture committer on initial load
- def componentWillMount()
- self.componentWillReceiveProps()
- end
-
- # replace committer on change, determine if user is authorized to make
- # changes
- def componentWillReceiveProps()
- if @committer.id != @@committer.id
- @committer = @@committer
- @auth = (@@auth.id == @@committer.id or @@auth.secretary or @@auth.root)
- end
- end
-
- # on initial display, look for add editable rows, highlight them,
- # and watch for double clicks on them
- def componentDidMount()
- return unless @auth
- Array(document.querySelectorAll('tr[data-edit]')).each do |tr|
- tr.addEventListener('dblclick', self.dblclick)
- tr.querySelector('td').classList.add 'bg-success'
- end
- end
-
- # when a double click occurs, toggle the associated state
- def dblclick(event)
- tr = event.currentTarget
- field = "edit_#{tr.dataset.edit}"
- changes = {}
- changes[field] = !self.state[field]
- self.setState changes
- window.getSelection().removeAllRanges()
- end
-
- # after update, register event listeners on forms
- def componentDidUpdate()
- Array(document.querySelectorAll('tr[data-edit]')).each do |tr|
- form = tr.querySelector('form')
- if form
- form.setAttribute 'data-action', tr.getAttribute('data-edit')
- jQuery('input[type=submit],button', form).click(self.submit)
- end
- end
- end
-
- # submit form using AJAX
- def submit(event)
- event.preventDefault()
- form = jQuery(event.currentTarget).closest('form')
- target = event.target
-
- # serialize form
- formData = form.serializeArray();
-
- # add button if it has a value
- if target and target.getAttribute('name') and target.getAttribute('value')
- formData.push name: target.getAttribute('name'),
- value: target.getAttribute('value')
- end
-
- # indicate dryrun is requested if option or control key is down
- if event.altKey or event.ctrlKey
- formData.unshift name: 'dryrun', value: true
- end
-
- # issue request
- jQuery.ajax(
- method: (form[0].method || 'GET').upcase(),
- url: document.location.href + '/' + form[0].getAttribute('data-action'),
- data: formData,
- dataType: 'json',
-
- success: ->(response) {
- @committer = response.committer if response.committer
-
- # turn off edit mode on this field
- tr = form.closest('tr')[0]
- if tr
- field = "edit_#{tr.dataset.edit}"
- changes = {}
- changes[field] = false
- self.setState changes
- end
- },
-
- error: ->(response) {
- alert response.statusText
- },
-
- complete: ->(response) do
- # show results of dryrun
- if formData[0] and formData[0].name == 'dryrun'
- @response = response.responseJSON
- jQuery('div.modal').modal('show')
- end
-
- # reenable form for later reuse
- jQuery('input, button', form).prop('disabled', false)
- end
- )
-
- # disable input
- jQuery('input, button', form).prop('disabled', true)
- end
-end
diff --git a/www/roster/views/person.js.rb b/www/roster/views/person.js.rb
new file mode 100644
index 0000000..e051fd7
--- /dev/null
+++ b/www/roster/views/person.js.rb
@@ -0,0 +1,242 @@
+class Person < React
+ def initialize
+ @committer = {}
+ @response = nil
+ end
+
+ def render
+ # usage information for authenticated users (owner, secretary, etc.)
+ if @auth
+ _div.alert.alert_success 'Double click on a field in this color to edit.'
+ end
+
+ _h2 "#{@committer.id}@apache.org"
+
+ _table.wide do
+ _tbody do
+
+ # Name
+ _PersonName person: self
+
+ # Personal URL
+ if @committer.urls
+ _PersonUrls person: self
+ end
+
+ # Committees
+ committees = @committer.committees
+ unless committees.empty?
+ _tr do
+ _td 'Committees'
+ _td do
+ _ul committees do |pmc|
+ _li {_a pmc, href: "committee/#{pmc}"}
+ end
+ end
+ end
+ end
+
+ # Committer
+ commit_list = @committer.committer
+ unless commit_list.all? {|pmc| committees.include? pmc}
+ _tr do
+ _td 'Committer'
+ _td do
+ _ul commit_list do |pmc|
+ next if committees.include? pmc
+ _li {_a pmc, href: "committee/#{pmc}"}
+ end
+ end
+ end
+ end
+
+ # Groups
+ unless @committer.groups.empty?
+ _tr do
+ _td 'Groups'
+ _td do
+ _ul @committer.groups do |group|
+ next if group == 'apldap'
+
+ if group == 'committers'
+ _li {_a group, href: "committer/"}
+ elsif group == 'member'
+ _li {_a group, href: "members"}
+ else
+ _li {_a group, href: "group/#{group}"}
+ end
+ end
+ end
+ end
+ end
+
+ # Email addresses
+ if @committer.mail
+ _PersonEmail person: self
+ end
+
+ # PGP keys
+ if @committer.pgp
+ _PersonPgpKeys person: self
+ end
+
+ # SSH keys
+ if @committer.ssh
+ _PersonSshKeys person: self
+ end
+
+ # GitHub username
+ if @committer.githubUsername
+ _PersonGitHub person: self
+ end
+
+ if @committer.member
+ _PersonMemberStatus person: self
+
+ # Members.txt
+ if @committer.member.info
+ _PersonMemberText person: self
+ end
+
+ if @committer.member.nomination
+ _tr do
+ _td 'Nomination'
+ _td {_pre @committer.member.nomination}
+ end
+ end
+
+ # Forms on file
+ if @committer.forms
+ _PersonForms person: self
+ end
+ end
+
+ # SpamAssassin score
+ _PersonSascore person: self
+ end
+ end
+
+ # modal dialog for dry run results
+ _div.modal.fade tabindex: -1 do
+ _div.modal_dialog do
+ _div.modal_content do
+ _div.modal_header do
+ _button.close 'x', data_dismiss: 'modal'
+ _h4 'Dry run results'
+ end
+ _div.modal_body do
+ _textarea value: JSON.stringify(@response, nil, 2), readonly: true
+ end
+ _div.modal_footer do
+ _button.btn.btn_default 'Close', data_dismiss: 'modal'
+ end
+ end
+ end
+ end
+ end
+
+ # capture committer on initial load
+ def componentWillMount()
+ self.componentWillReceiveProps()
+ end
+
+ # replace committer on change, determine if user is authorized to make
+ # changes
+ def componentWillReceiveProps()
+ if @committer.id != @@committer.id
+ @committer = @@committer
+ @auth = (@@auth.id == @@committer.id or @@auth.secretary or @@auth.root)
+ end
+ end
+
+ # on initial display, look for add editable rows, highlight them,
+ # and watch for double clicks on them
+ def componentDidMount()
+ return unless @auth
+ Array(document.querySelectorAll('tr[data-edit]')).each do |tr|
+ tr.addEventListener('dblclick', self.dblclick)
+ tr.querySelector('td').classList.add 'bg-success'
+ end
+ end
+
+ # when a double click occurs, toggle the associated state
+ def dblclick(event)
+ tr = event.currentTarget
+ field = "edit_#{tr.dataset.edit}"
+ changes = {}
+ changes[field] = !self.state[field]
+ self.setState changes
+ window.getSelection().removeAllRanges()
+ end
+
+ # after update, register event listeners on forms
+ def componentDidUpdate()
+ Array(document.querySelectorAll('tr[data-edit]')).each do |tr|
+ form = tr.querySelector('form')
+ if form
+ form.setAttribute 'data-action', tr.getAttribute('data-edit')
+ jQuery('input[type=submit],button', form).click(self.submit)
+ end
+ end
+ end
+
+ # submit form using AJAX
+ def submit(event)
+ event.preventDefault()
+ form = jQuery(event.currentTarget).closest('form')
+ target = event.target
+
+ # serialize form
+ formData = form.serializeArray();
+
+ # add button if it has a value
+ if target and target.getAttribute('name') and target.getAttribute('value')
+ formData.push name: target.getAttribute('name'),
+ value: target.getAttribute('value')
+ end
+
+ # indicate dryrun is requested if option or control key is down
+ if event.altKey or event.ctrlKey
+ formData.unshift name: 'dryrun', value: true
+ end
+
+ # issue request
+ jQuery.ajax(
+ method: (form[0].method || 'GET').upcase(),
+ url: document.location.href + '/' + form[0].getAttribute('data-action'),
+ data: formData,
+ dataType: 'json',
+
+ success: ->(response) {
+ @committer = response.committer if response.committer
+
+ # turn off edit mode on this field
+ tr = form.closest('tr')[0]
+ if tr
+ field = "edit_#{tr.dataset.edit}"
+ changes = {}
+ changes[field] = false
+ self.setState changes
+ end
+ },
+
+ error: ->(response) {
+ alert response.statusText
+ },
+
+ complete: ->(response) do
+ # show results of dryrun
+ if formData[0] and formData[0].name == 'dryrun'
+ @response = response.responseJSON
+ jQuery('div.modal').modal('show')
+ end
+
+ # reenable form for later reuse
+ jQuery('input, button', form).prop('disabled', false)
+ end
+ )
+
+ # disable input
+ jQuery('input, button', form).prop('disabled', true)
+ end
+end
diff --git a/www/roster/views/person/email.js.rb
b/www/roster/views/person/email.js.rb
new file mode 100644
index 0000000..3537221
--- /dev/null
+++ b/www/roster/views/person/email.js.rb
@@ -0,0 +1,21 @@
+#
+# Render and edit a person's E-mail addresses
+#
+
+class PersonEmail < React
+ def render
+ committer = @@person.state.committer
+
+ _tr do
+ _td 'Email addresses'
+
+ _td do
+ _ul committer.mail do |url|
+ _li do
+ _a url, href: 'mailto:' + url
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/www/roster/views/person/forms.js.rb
b/www/roster/views/person/forms.js.rb
new file mode 100644
index 0000000..d936f16
--- /dev/null
+++ b/www/roster/views/person/forms.js.rb
@@ -0,0 +1,35 @@
+#
+# Show a list of a person's forms on file
+#
+
+class PersonForms < React
+ def render
+ committer = @@person.state.committer
+ documents = "https://svn.apache.org/repos/private/documents"
+
+ _tr do
+ _td 'Forms on file'
+
+ _td do
+ _ul do
+ for form in committer.forms
+ link = committer.forms[form]
+
+ if form == 'icla'
+ _li do
+ _a 'ICLA', href: "#{documents}/iclas/#{link}"
+ end
+ elsif form == 'member'
+ _li do
+ _a 'Membership App',
+ href: "#{documents}/member_apps/#{link}"
+ end
+ else
+ _li "#{form}: #{link}"
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/www/roster/views/person/fullname.js.rb
b/www/roster/views/person/fullname.js.rb
new file mode 100644
index 0000000..a12a9a5
--- /dev/null
+++ b/www/roster/views/person/fullname.js.rb
@@ -0,0 +1,58 @@
+#
+# Render and edit a person's name
+#
+
+class PersonName < React
+ def render
+ committer = @@person.state.committer
+
+ _tr data_edit: ('fullname' if @@person.props.auth.secretary) do
+ _td 'Name'
+
+ _td do
+ name = committer.name
+
+ if @@person.state.edit_fullname
+
+ _form.inline method: 'post' do
+ _div do
+ _label 'public name', for: 'publicname'
+ _input.publicname! name: 'publicname', required: true,
+ defaultValue: name.public_name
+ end
+
+ _div do
+ _label 'legal name', for: 'legalname'
+ _input.legalname! name: 'legalname', required: true,
+ defaultValue: name.legal_name
+ end
+
+ _button.btn.btn_primary 'submit'
+ end
+
+ else
+
+ if
+ name.public_name==name.legal_name and
+ name.public_name==name.ldap
+ then
+ _span committer.name.public_name
+ else
+ _ul do
+ _li "#{committer.name.public_name} (public name)"
+
+ if name.legal_name and name.legal_name != name.public_name
+ _li "#{committer.name.legal_name} (legal name)"
+ end
+
+ if name.ldap and name.ldap != name.public_name
+ _li "#{committer.name.ldap} (ldap)"
+ end
+ end
+ end
+
+ end
+ end
+ end
+ end
+end
diff --git a/www/roster/views/person/github.js.rb
b/www/roster/views/person/github.js.rb
new file mode 100644
index 0000000..038bc6f
--- /dev/null
+++ b/www/roster/views/person/github.js.rb
@@ -0,0 +1,18 @@
+#
+# Render and edit a person's GitHub user name
+#
+
+class PersonGitHub < React
+ def render
+ committer = @@person.state.committer
+
+ _tr do
+ _td 'GitHub username'
+ _td do
+ _a committer.githubUsername,
+ href: "https://github.com/" + committer.githubUsername
+ end
+ end
+ end
+end
+
diff --git a/www/roster/views/person/memstat.js.rb
b/www/roster/views/person/memstat.js.rb
new file mode 100644
index 0000000..31cdd81
--- /dev/null
+++ b/www/roster/views/person/memstat.js.rb
@@ -0,0 +1,33 @@
+#
+# Render and edit a person's member status
+#
+
+class PersonMemberStatus < React
+ def render
+ committer = @@person.state.committer
+
+ _tr data_edit: ('memstat' if @@person.props.auth.secretary) do
+ _td 'Member status'
+
+ if committer.member.info
+ _td do
+ _span committer.member.status
+
+ if @@person.state.edit_memstat
+ _form.inline method: 'post' do
+ if committer.member.status.include? 'Active'
+ _button.btn.btn_primary 'move to emeritus',
+ name: 'action', value: 'emeritus'
+ elsif committer.member.status.include? 'Emeritus'
+ _button.btn.btn_primary 'move to active',
+ name: 'action', value: 'active'
+ end
+ end
+ end
+ end
+ else
+ _td.not_found 'Not in members.txt'
+ end
+ end
+ end
+end
diff --git a/www/roster/views/person/memtext.rb
b/www/roster/views/person/memtext.rb
new file mode 100644
index 0000000..2af57ae
--- /dev/null
+++ b/www/roster/views/person/memtext.rb
@@ -0,0 +1,30 @@
+#
+# Render and edit a person's members.txt entry
+#
+
+class PersonMemberText < React
+ def render
+ committer = @@person.state.committer
+
+ _tr data_edit: 'memtext' do
+ _td 'Members.txt'
+
+ _td do
+ if @@person.state.edit_memtext
+
+ _form.inline method: 'post' do
+ _div do
+ _textarea defaultValue: committer.member.info
+ end
+ _button.btn.btn_primary 'submit'
+ end
+
+ else
+
+ _pre committer.member.info,
+ class: ('small' if committer.member.info =~ /.{81}/)
+ end
+ end
+ end
+ end
+end
diff --git a/www/roster/views/person/pgpkeys.js.rb
b/www/roster/views/person/pgpkeys.js.rb
new file mode 100644
index 0000000..815c59c
--- /dev/null
+++ b/www/roster/views/person/pgpkeys.js.rb
@@ -0,0 +1,29 @@
+#
+# Render and edit a person's PGP keys
+#
+
+class PersonPgpKeys < React
+ def render
+ committer = @@person.state.committer
+
+ _tr do
+ _td 'PGP keys'
+
+ _td do
+ _ul committer.pgp do |key|
+ _li do
+ if key =~ /^[0-9a-fA-F ]+$/
+ _samp do
+ _a key, href: 'https://sks-keyservers.net/pks/lookup?' +
+ 'op=index&search=0x' + key.gsub(' ', '')
+ end
+ else
+ _samp key
+ end
+ end
+ end
+ end
+ end
+ end
+end
+
diff --git a/www/roster/views/person/sascore.js.rb
b/www/roster/views/person/sascore.js.rb
new file mode 100644
index 0000000..c5c1a92
--- /dev/null
+++ b/www/roster/views/person/sascore.js.rb
@@ -0,0 +1,30 @@
+#
+# Render and edit a person's SpamAssassin score
+#
+
+class PersonSascore < React
+ def render
+ committer = @@person.state.committer
+
+ _tr data_edit: 'sascore' do
+ _td 'SpamAssassin score'
+
+ _td do
+
+ if @edit_sascore
+
+ _form method: 'post' do
+ _input type: 'number', min: 0, max: 10,
+ name: 'sascore', defaultValue: committer.sascore
+ _input type: 'submit', value: 'submit'
+ end
+
+ else
+
+ _span committer.sascore
+
+ end
+ end
+ end
+ end
+end
diff --git a/www/roster/views/person/sshkeys.js.rb
b/www/roster/views/person/sshkeys.js.rb
new file mode 100644
index 0000000..4124df3
--- /dev/null
+++ b/www/roster/views/person/sshkeys.js.rb
@@ -0,0 +1,23 @@
+#
+# Render and edit a person's SSH keys
+#
+
+class PersonSshKeys < React
+ def render
+ committer = @@person.state.committer
+
+ _tr do
+ _td 'SSH keys'
+
+
+ _td do
+ _ul committer.ssh do |key|
+ _li.ssh do
+ _pre.wide key
+ end
+ end
+ end
+ end
+ end
+end
+
diff --git a/www/roster/views/person/urls.js.rb
b/www/roster/views/person/urls.js.rb
new file mode 100644
index 0000000..a73bc1b
--- /dev/null
+++ b/www/roster/views/person/urls.js.rb
@@ -0,0 +1,19 @@
+#
+# Render and edit a person's URLs
+#
+
+class PersonUrls < React
+ def render
+ committer = @@person.state.committer
+
+ _tr do
+ _td 'Personal URL'
+
+ _td do
+ _ul committer.urls do |url|
+ _li {_a url, href: url}
+ end
+ end
+ end
+ end
+end