whimsical-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From curc...@apache.org
Subject [whimsy] branch master updated: Survey tool alpha design
Date Sat, 25 May 2019 20:06:37 GMT
This is an automated email from the ASF dual-hosted git repository.

curcuru pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/whimsy.git


The following commit(s) were added to refs/heads/master by this push:
     new f868b3d  Survey tool alpha design
f868b3d is described below

commit f868b3d35231f2d1f60d79904ea2c6a968d4a55e
Author: Shane Curcuru <asf@shanecurcuru.org>
AuthorDate: Sat May 25 16:06:20 2019 -0400

    Survey tool alpha design
    
    Strings or symbols, oh my, where's my indifferent hash?
---
 lib/whimsy/asf/forms2.rb | 178 ++++++++++++++++++++++++++++++++++++++++++++
 www/officers/surveys.cgi | 189 +++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 367 insertions(+)

diff --git a/lib/whimsy/asf/forms2.rb b/lib/whimsy/asf/forms2.rb
new file mode 100644
index 0000000..e26d14b
--- /dev/null
+++ b/lib/whimsy/asf/forms2.rb
@@ -0,0 +1,178 @@
+require 'wunderbar'
+require 'wunderbar/markdown'
+
+# Define common page features for whimsy tools using bootstrap styles
+class Wunderbar::HtmlMarkup
+
+  # Emit a form control based on a hash of options with a type:
+  def _whimsy_field_chooser(args)
+    _p "DEBUG #{__method__} #{args.inspect} /// #{args['type']}"
+    case args['type']
+    when 'subhead'
+      _whimsy_forms_subhead args
+    when 'text'
+      _whimsy_forms_input args
+    when 'textarea'
+      args['rows'] ||= '3'
+      _whimsy_forms_input args
+    when 'select'
+      _whimsy_forms_select args
+    when 'radio', 'checkbox'
+      _whimsy_forms_checkradio args
+    else
+      _div "#{__method__}(#{args['type']}) TODO: Error condition?"
+    end
+  end
+
+  # Utility function to add icons after form controls
+  def _whimsy_forms_iconlink(**args)
+    if args['iconlink']
+      _div.input_group_btn do
+        _a.btn.btn_default type: 'button', aria_label: "#{iconlabel}", href: "#{args['iconlink']}",
target: 'whimsy_help' do
+          _span.glyphicon class: "#{args['icon']}", aria_label: "#{args['iconlabel']}"
+        end
+      end
+    elsif args['icon']
+      _span.input_group_addon do
+        _span.glyphicon class: "#{args['icon']}", aria_label: "#{args['iconlabel']}"
+      end
+    end
+  end
+
+  # Utility function for divs around form controls, including help
+  def _whimsy_control_wrapper(**args)
+    _div.form_group do
+      _label.control_label.col_sm_3 args['label'], for: "#{args['name']}"
+      _div.col_sm_9 do
+        _div.input_group do
+          yield
+          _whimsy_forms_iconlink(args)
+        end
+        if args['helptext']
+          _span.help_block id: "#{args['aria_describedby']}" do
+            _markdown "#{args['helptext']}"
+          end
+        end
+      end
+    end
+  end
+
+  # Display a subheader separator between sections of a form
+  # @param text string to display
+  def _whimsy_forms_subhead(text: 'Form Section')
+    _div.form_group do
+      _label.col_sm_offset_3.col_sm_9.strong.text_left text
+    end
+  end
+  
+  # Display a single input control within a form; or if rows, then a textarea
+  # @param name required string ID of control's label/id
+  def _whimsy_forms_input(**args)
+    return unless args['name']
+    args['label'] ||= 'Enter string'
+    args['type'] ||= 'text'
+    args['id'] = args['name']
+    args['aria_describedby'] = "#{args['name']}_help" if args['helptext']
+    _whimsy_control_wrapper(args) do
+      args['class'] = 'form-control'
+      if args['rows']
+        _textarea! type: args['type'], name: args['name'], id: args['id'], value: args['value'],
class: args['class'], aria_describedby: args['aria_describedby'], rows: args['rows'] do
+          _! args['value']
+        end
+      else
+        _input type: args['type'], name: args['name'], id: args['id'], value: args['value'],
class: args['class'], aria_describedby: args['aria_describedby']
+      end
+    end
+  end
+
+  # Display an optionlist control within a form
+  # @param name required string ID of control's label/id
+  # @param options required ['value'] or {"value" => 'Label for value'} of all selectable
values
+  # @param values required 'value' or ['value'] or {"value" => 'Label for value'} of all
selected values
+  # @param placeholder Currently displayed text if passed (not selectable)
+  def _whimsy_forms_select(**args)
+    return unless args['name']
+    return unless args['values']
+    args['label'] ||= 'Select value(s)'
+    args['id'] = args['name']
+    args['aria_describedby'] = "#{args['name']}_help" if args['helptext']
+    _whimsy_control_wrapper(args) do 
+      if args['multiple']
+        args['multiple'] = 'true'
+      end
+      _select.form_control type: args['type'], name: args['name'], id: args['id'], value:
args['value'], aria_describedby: args['aria_describedby'], multiple: args['multiple'] do
+        if ''.eql?(args['placeholder'])
+          _option '', value: '', selected: 'selected'
+        else
+          _option "#{args['placeholder']}", value: '', selected: 'selected', disabled: 'disabled',
hidden: 'hidden'
+        end
+        # Construct selectable list from values (first) then options
+        if args['values'].kind_of?(Array)
+          args['values'].each do |val|
+            _option val, value: val, selected: true
+          end
+        elsif args['values'].kind_of?(Hash)
+          args['values'].each do |val, disp|
+            _option disp, value: val, selected: true
+          end
+        elsif args['values'] # Fallback for simple case of single string value
+          _option "#{args['values']}", value: "#{args['values']}", selected: true
+          args['values'] = [args['values']] # Ensure supports .include? for options loop
below
+        end
+        if args['options'].kind_of?(Array)
+          args['options'].each do |val|
+            _option val, value: val unless args['values'].include?(val)
+          end
+        elsif args['options'].kind_of?(Hash)
+          args['options'].each do |val, disp|
+            _option disp, value: val unless args['values'].include?(val)
+          end
+        end
+      end
+    end
+  end
+
+  # Display a list of radio or checkbox controls
+  # @param name required string ID of control's label/id
+  # @param type required FORM_CHECKBOX|FORM_RADIO
+  # @param options required ['value'...] or {"value" => 'Label for value'} of all values
+  # @param selected optional 'value' or ['value'...] of all selected values
+  def _whimsy_forms_checkradio(**args)
+    return unless args['name']
+    return unless args['type']
+    return unless args['options']
+    args['label'] ||= 'Select value(s)'
+    args['id'] = args['name']
+    args['aria_describedby'] = "#{args['name']}_help" if args['helptext']
+    args['selected'] = [args['selected']] if args['selected'].kind_of?(String)
+    _whimsy_control_wrapper(args) do 
+      # Construct list of all :options; mark any that are in :selected 
+      if args['options'].kind_of?(Array)
+        args['options'].each do |val|
+          checked = true if args['selected'] && args['selected'].include?(val.to_s)
+          _input type: args['type'], name: args['name'], id: args['id'], value: val, class:
args['class'], aria_describedby: args['aria_describedby'], checked: checked do
+            _! val
+          end
+        end
+      elsif args['options'].kind_of?(Hash)
+        args['options'].each do |val, disp|
+          checked = true if args['selected'] && args['selected'].include?(val.to_s)
+          _input type: args['type'], name: args['name'], id: args['id'], value: val, class:
args['class'], aria_describedby: args['aria_describedby'], checked: checked do
+            _! disp
+          end
+        end
+      end
+    end
+  end
+
+  # Gather POST form data into submission Hash
+  # @returns {field: 'string', field2: ['array', 'only for', 'multivalue'] ...}
+  def _whimsy_params2formdata(params)
+    formdata = {}
+    params.each do |k,v|
+      v && (v.length == 1) ? formdata[k] = v[0] : formdata[k] = v
+    end
+    return formdata
+  end
+
+end
diff --git a/www/officers/surveys.cgi b/www/officers/surveys.cgi
new file mode 100755
index 0000000..443e62f
--- /dev/null
+++ b/www/officers/surveys.cgi
@@ -0,0 +1,189 @@
+#!/usr/bin/env ruby
+PAGETITLE = "Whimsy Member and Officer Surveys" # Wvisible:members
+$LOAD_PATH.unshift '/srv/whimsy/lib'
+
+require 'wunderbar'
+require 'wunderbar/bootstrap'
+require 'wunderbar/jquery'
+require 'wunderbar/markdown'
+require 'whimsy/asf'
+require 'whimsy/asf/forms2'
+require 'whimsy/asf/rack'
+require 'json'
+require 'cgi'
+
+FOUNDATION_SVN = ASF::SVN['foundation'] # TODO check pathing
+SURVEY = 'survey'
+SURVEYS_DIR = 'surveys'
+ERRORS = 'errors'
+PARAMS = 'params'
+
+# Convenience method to display alerts
+def display_alert(lead: 'Error', body: '', type: 'alert-danger')
+  _div.alert class: type, role: 'alert' do
+    _p.lead lead
+    _markdown body
+  end
+end
+
+# Emit HTML for a survey form, or display any errors
+# Currently, user can only submit a survey once
+def display_survey(survey_layout)
+  if survey_layout.has_key?(ERRORS)
+    display_alert(lead: "Unable to read survey layout!", body: "#{survey_layout[ERRORS]}\n\nPlease
check your query string params: #{survey_layout[PARAMS]} or check to see if the survey data
is valid: #{survey_file}", type: 'alert-warning')
+  end
+  dataroot = survey_layout['dataroot']
+  datafile = survey_layout['datafile']
+  survey_file = "#{dataroot} /#{SURVEYS_DIR}/ #{datafile}"
+  if dataroot && datafile
+    survey_file = File.join(ASF::SVN[survey_layout['dataroot']], SURVEYS_DIR, datafile)
+    if File.file?(survey_file)
+      # Check if the user has already submitted this survey
+      survey_data = {}
+      begin
+        survey_data = JSON.parse(File.read(survey_file))
+        if survey_data.has_key?($USER)
+          display_alert(lead: 'User already submitted survey', body: "You appear to have
already submitted this survey (#{query}) once; if needed, edit the survey.json in SVN.")
+          # return # TODO should we bail or continue?
+        end
+      rescue StandardError => e
+        display_alert(lead: 'Error reading survey data file', body: "**ERROR:#{__method__}(#{query})
#{e.message}**\n\n    #{e.backtrace[0]}")
+        # return # TODO should we bail or continue?
+      end
+    else
+      display_alert(lead: "Unable to read survey data!", body: "#{survey_layout[ERRORS]}\n\nPlease
check your query string params: #{survey_layout[PARAMS]} or check to see if the survey data
is valid: #{survey_file}")
+      # return # TODO should we bail or continue?
+    end
+  else
+    display_alert(lead: "Unable to read survey layout!", body: "#{survey_layout[ERRORS]}\n\nPlease
check your query string params: #{survey_layout[PARAMS]} or check to see if the survey data
is valid: #{survey_file}")
+    # return # TODO should we bail or continue?
+  end
+
+  # Emit the survey if no errors found
+  _whimsy_panel("#{survey_layout[SURVEY]['form']['title']}", style: 'panel-success') do
+    _form.form_horizontal method: 'post' do
+      survey_layout[SURVEY]['form']['fields'].each do |field|
+        _whimsy_field_chooser(field) # DEBUG: How do properly pass this hash thru?
+      end
+      _div.col_sm_offset_3.col_sm_9 do
+        _span.text_info survey_layout[SURVEY]['form']['buttonhelp']
+        _br
+        _input.btn.btn_default type: 'submit', value: survey_layout[SURVEY]['form']['buttontext']
+      end
+    end
+  end
+  
+  display_alert(lead: "DEBUG: survey_layout data was", body: survey_layout.inspect, type:
'alert-warning')
+end
+
+# Validation as needed within the script
+def validate_survey(formdata: {})
+  return true # TODO: Futureuse
+end
+
+# Handle submission (checkout user's apacheid.json, write form data, checkin file)
+# @return true if we think it succeeded; false in all other cases
+def submit_survey(formdata: {})
+  fn = "#{survey_layout['datafile']}.json".untaint # TODO: check path/file here
+  submission_data = JSON.pretty_generate(formdata) + "\n"
+  _div.well do
+    _p.lead "Submitting your survey data to: #{fn}"
+    _pre submission_data
+    _p "DEBUG: not sending any data for testing! 20190525-sc DEBUG: need to add to existing
file, not overwrite"
+  end
+  return true # DEBUG: not sending any data for testing! 20190525-sc
+  # TODO svn checkout, add data as $USER => {submission_data...}
+end
+
+DEFAULT_SURVEY = {
+  'title' => 'Apache Whimsy Survey Tool',
+  'subtitle' => 'Survey Help Page',
+  'related' => {
+    "committers/tools.cgi" => "All Whimsy Committer-wide Tools",
+    "https://github.com/apache/whimsy/blob/master/www/" => "See Whimsy Source Code",
+    "mailto:dev@whimsical.apache.org?subject=[FEEDBACK] Survey Tool" => "Email Feedback
To dev@whimsical"
+  },
+  'helpblock' => %q(The Whimsy Survey tool allows you to use an SVN-backed `survey.json`
file to capture survey answers from Apache committers (all answers associated with apacheid
and a commit.)
+**If you are reading this**, then the survey you attempted to view has not be configured
yet - sorry!
+For now, see the code for more help, or contact dev@whimsical for questions.
+  ),
+  'dataroot' => '',
+  'datafile' => '',
+  'submitpass' => 'This *markdown-able* message would be displayed after a successful
svn commit of survey data.',
+  'submitfail' => 'This *markdown-able* message would be displayed after a **FAILED**
svn commit of survey data.',
+  'form' => {
+    'title' => 'Survey Form Title',
+    'buttonhelp' => 'This sample survey won\'t work, but you can still press Submit below!',
+    'buttontext' => 'Submit',
+    'fields' => [
+      {
+        'name' => 'field1',
+        'type' => 'text',
+        'label' => 'This field is:',
+        'helptext' => 'This text would explain the field1 (optional)'
+      },
+      {
+        'name' => 'field2',
+        'type' => 'text',
+        'rows' => '3',
+        'label' => 'This is multiline:',
+        'helptext' => 'This text would explain the field2 (optional)'
+      }
+    ]
+  }
+}
+# Return survey layout hash from QUERY_STRING
+# @return {} of form layout data for survey; or a default layout of help
+# Note: does not validate that the survey has a place to store data; only that the layout
exists
+def get_survey_layout(query)
+  params = {}
+  CGI::unescape(query).split('&').each do |keyval|
+    k, v = keyval.split('=', 2)
+    v && (v.length == 1) ? params[k] = v[0] : params[k] = v
+  end
+  data = {}
+  data[PARAMS] = params
+  begin
+    data[SURVEY] = JSON.parse(File.read(File.join(FOUNDATION_SVN, SURVEYS_DIR, "#{params[SURVEY]}.json").untaint))
# TODO: Security, ensure user should have access
+  rescue StandardError => e
+    data[ERRORS] = "**ERROR:#{__method__}(#{query}) #{e.message}**\n\n    #{e.backtrace.join("\n
   ")}"
+  end
+  # Fallback if not successfully read, so we can display something (even if it won't work
to submit)
+  if not data.has_key?(SURVEY)
+    data[SURVEY] = DEFAULT_SURVEY
+  end
+  return data
+end
+
+# produce HTML
+_html do
+  _style :system
+  _style %{
+    .transcript {margin: 0 16px}
+    .transcript pre {border: none; line-height: 0}
+  }
+  _body? do
+    query_string = ENV['QUERY_STRING']
+    survey_layout = get_survey_layout(query_string)
+    _whimsy_body(
+      title: survey_layout[SURVEY]['title'],
+      subtitle: survey_layout[SURVEY]['subtitle'],
+      related: survey_layout[SURVEY]['related'],
+      helpblock: -> {
+        _markdown survey_layout[SURVEY]['helpblock']
+     }
+    ) do
+      # Display data to the user, depending if we're GET (a blank survey) or POST (show SVN
checkin results)
+      if _.post?
+        formdata = _whimsy_params2formdata(_.params)
+        if validate_survey(formdata: formdata) && submit_survey(formdata: formdata)
+            display_alert(lead: 'Survey Submitted', body: survey_layout[SURVEY]['submitpass'],
type: 'alert-success')
+          else
+            display_alert(lead: 'Submission Failed', body: survey_layout[SURVEY]['submitfail'])
+        end
+      else # if _.post?
+        display_survey(survey_layout)
+      end
+    end
+  end
+end


Mime
View raw message