/surveyor

gem to create surveys

Primary LanguageRubyMIT LicenseMIT

Surveyor

Gem to manage surveys.

A survey is essentially a meta-form, or a form template.

It is described through a DSL where every survey element is identified by a (hierarchical) name.

An example of a survey definition:

survey 'hotel' do
  section 'header' do
    string 'owner', :readonly => true
    calendar 'opened_on'
  end
  multiplier 'rooms' do
    string 'number', :regexp => '[a-z]\d+', :required => true
    list_selector 'floor', :values => (1..4).to_a
    radio_selector 'beds', :values => (1..4).to_a
  end
  multiplier 'clients' do
    string 'name'
    sequence 'address' do
      string 'street'
      list_selector 'city', :values => :cities, :hide => rule('clients.address.street') != ''
      list_selector 'country', :values => lambda { Country.all }
    end
  end

  sheet 'setup' do
    header.owner.readonly false
    clients.hidden true
  end
  sheet 'registration' do
    header.hidden true
    rooms.readonly true
  end
end

This survey can be managed in controller as:

def edit
  # get the hob from database (if hob is a ActiveRecord)
  @hob = Hob.find(...)

  # get the survey
  @survey = ...
  # gather survey information in hash (for example, from a document-oriented database)
  hash = ...
  # if necessary, use a sheet
  @sheet = @survey.sheets['my sheet']
  # create the hob to be edited
  @hob = Surveyor::Hob.new(@survey, hash)
end

def update
  # get back the hob
  @hob = Surveyor::Hob.new(@survey, @nest.document)
  @hob.update(params[@survey.name])
  if @hob.valid?
    @hob.save!  # or what else to store data
    redirect_to hobs_path
  else
    # correct the mistakes
    render :action => :edit
  end
end

And in the edit view:

<h1>Editing hob</h1>
<%= @survey.form_for(@hob, @sheet, :url => hob_path(@hob)) %>
<%= link_to 'Back', hobs_path %>

When associated to an object (which contains data), a survey can be rendered, and that (as current only option) generates a HTML form.

The submission of a survey form is a hierarchical hash (i.e. a hash without complex objects, only simple values, hashes and arrays. Ex: params).

Three object types could be associated to a survey:

  • Surveyor::Hob: typical survey object, it is built on a survey and maps to and from a hierarchical Hash;

  • ActiveRecord: as long as survey’s elements are mapped on fields and relationships;

  • Document: from a document-based or key-value database, like CouchDb, MongoDb or Redis.

A survey can be rendered totally or partially (based on a survey sheet, see later). The object associated is updated accordingly.

Any surveyor element has a name, a label, a type and a set of options.

A survey is internationalized (I18n) by design, since element names must be lowercase identifiers. That allow the element’s label to be automatically calculated, based on current language. An element can override the default label calculation through the :label option.

A survey element can have a tip, that is an help text that can be variously rendered associated to the field label.

A survey element is generally rendered as a <div> element with opportune id and class, depending on the element itself.

Anyway, it can be given an additional CSS class through the :class option.

In a <div> structure rendered from a survey element, fields have a unique id that depends on the survey name, the location in the survey structure and the index (when in an array) of the rendered object. Input fields within the structure have also unique names, that reflects their position in the resulting survey hash.

The simplest survey element is the string element, that allows for editing textual data on a single line. Its content can also be validated on a regular expression (:regexp option).

Any element of a survey (as well as the survey itself) can be rendered as read-only. This (as well as other rendering options, like mandatory-ness and visibility) can be specified in the options of an element or in a survey sheet.

A survey sheet is a set of options for survey’s elements that can be merged with a survey on rendering. This is particularly useful when a survey is used in a workflow, as it often happens, since in any workflow node a different profile can access the survey, requiring custom visibility and editing possibilities.

A survey (or a survey sheet) can be associated a set of rules for:

  • visibility: is an element currently visible?

  • readonlyness, or editability: can an element currently be edited?

  • validation: does an element contain valid data?

  • existance: is an element part of this form?

All rules are evaluated server-side.

All rule kinds, except existance, can have a special implementation that allows these rules to be evaluated client-side (with limitations).

Es: a choice element shows only a container among a set of containers depending on the chosen value.

Some easy validation rules, like presence, could also be rendered in javascript.

The visibility rules, when actively implemented client-side, can be effectively used to build wizards.

A survey can contain different types of containers:

  • a sequence contains a list of elements that are displayed sequentially.

    The value of a sequence is a hash

  • a section is a simple field aggregator that contains a list of elements.

    It differs from a sequence in that it does not change its fields’ names.

    A section has no value, since the value of its fields are stored in parent object. Es:

    section 'area' do
      text 'width'    # => form['width']
      text 'height'   # => form['height']
    end
    

    A section could be rendered as an accordion or as a set of tabs.

    It can also be used empty to freely mix text between fields:

    section 'first_note' do
    end
    
  • a multiplier is a sequence that can be instantiated multiple times.

    Every multiplier items, called “factor”, is like a sequence, and can be dynamically added and removed.

    Possible options:

    • :limit => number: maximum number of elements allowed (other possibility: :max_size and :min_size)

    • :no_add => boolean: cannot add elements if boolean is true

    • :no_delete => boolean: cannot remove elements if boolean is true

Possible simple element types:

  • text field

  • text area

  • calendar

  • fill-in-the-blanks

  • checkbox

  • rating

  • list-selector

  • radio-selector

  • upload-file

  • percent

  • slider

  • spinner

Hob

A Hob:

* is an prototypical object that holds a survey status;
* it accepts a survey in constructor, dynamically extending its protocol
  to provide access to all survey fields;
* it can be partially or totally updated by a HHash;
* it can output its representation as a HHash
* it behaves like an ActiveModel
NOTE: A HHash is a hierarchical hash that can only contain strings, arrays
or other HHashes. It is what generally comes from a form (i.e. params)

  class Hob
    def initialize(survey, hhash = {}) ... end
    def update(hhash) ... end               # update a hob from a hhash (like controller's :params)
    def to_hash(except_keys = []) ... end   # generate a hhash from a hob
  end
Thoughts about rules

A rule can be a block, a symbol or an array of constants.

A block can be directly executed, but only on server side.

A symbol references an existent rule object, both predefined or defined in the survey. Depending on the rule, the rule object can be server-side only, of provide also a client-side implementation.

The client side implementation is particularly useful for visibility or editability rules.

An array of constants contains a symbol and a list of constants used as arguments for the rule referenced by the symbol.

Examples:

:presence, # also with :required => true
:is_number, # also with :regexp => /\d*/
[:match, 'age', :greater_than],   #different from another field
feature

a survey can have a further mode, :design, that lets the user change the survey structure.

feature

if a survey can be changed dynamically (see :design mode), it needs to be stored somewhere. Although it would be easier in a document-based or a key-value database, a classic choice is a relational database (ActiveRecord)

feature

apply a survey on an object. Path changes depending on the object being an ActiveRecord or else.

Information must be applied hierarchically (so in ActiveRecord it handles relationships)

Es:

<%= @survey.form_for(Blog.new) %>

with access to blog.posts and blog.posts[].user

feature

a survey can be nested within a form.

For example, an object, among other regular attributes, could have an attribute marked as :serializable which may contain a Hob and is best rendered through a survey.

In this case, the editing form should resemble:

<%= form_for(@obj) do |f| %>
  <%= f.text_field 'header' %>
  <%= @obj.survey.form_for(@obj.document, :without => :form) %>
  <%= f.text_field 'footer' %>
<% end %>

With this feature, error messages and error field localizations will be hard to manage. Better to delay it

feature

a survey can be applied a survey sheet.

Since a complex form like a survey is often used in a workflow, where in any node a different profile can access the same form, a survey profile can externally hold some survey :hiding and :editing rules and apply them over the survey when requested.

I would like the survey sheet declared within a survey like:

survey 'exam' do
  ...

  sheet 'examinee' do
    members.rating.hidden true
    rooms.location do
      required true
      size 60
      hidden rule(assignee == 'Mark')
    end
  end
end

Alternative syntax:

sheet 'examinee' do
  members.rating.hidden = true
  rooms.location do
    required = true
    size = 60
    hidden = rule(assignee == 'Mark')
  end
end

This alternative syntax could be more readable, but there is a problem: in parsing blocks, statements like

required = true

are interpreted as variable assignment.

feature

a survey is often used as an exam or quiz, expecting some results.

Furthermore, a survey can be used for an evaluation, where every answer provides a rating, not a boolean (as a quiz does).

Enhance the survey features to manage quizzes and evaluations as well.

feature

a survey element can optionally provide an external HTML renderer (option)


TO DO LIST

  • option :killed as boolean or rule (server side only)

  • check element names validity (syntax, duplication, …)

  • calendar element

  • text area element

  • checkbox element

  • rating element

  • list-selector element (with :other option)

  • radio-selector element (with :other option)

  • file uploader element

  • string element: global option to manage :required and :regexp options on the client side.

  • :renderer option

  • rules for [:validation, :hiding, :editing]

  • :hide option, rule with client side implementation

  • :readonly option, rule with client side implementation

  • survey nesting within an external form

  • :limit option for multipliers

  • :no_add option for multipliers

  • :no_delete (or :no_resize) option for multipliers

  • hob as activerecord (or anyway an ActiveRecord container for hobs)

  • fill-in-the-blanks element

  • survey collating on single objects rather than hobs

  • :design mode for surveys

  • quiz evaluation

  • percent element

  • slider element

  • spinner element

  • multiple surveys in the same page

*** That’s all, folks! ***