├── VERSION
├── spec
├── rcov.opts
├── spec.opts
├── models
│ ├── answer_spec.rb
│ ├── question_group_spec.rb
│ ├── survey_section_spec.rb
│ ├── validation_spec.rb
│ ├── response_spec.rb
│ ├── survey_spec.rb
│ ├── question_spec.rb
│ ├── dependency_spec.rb
│ └── validation_condition_spec.rb
├── lib
│ ├── parser_spec.rb
│ ├── common_spec.rb
│ ├── redcap_parser_spec.rb
│ └── unparser_spec.rb
├── spec_helper.rb
├── helpers
│ └── surveyor_helper_spec.rb
└── factories.rb
├── .rvmrc
├── rails
└── init.rb
├── app
├── models
│ ├── answer.rb
│ ├── question.rb
│ ├── survey.rb
│ ├── validation.rb
│ ├── dependency.rb
│ ├── response_set.rb
│ ├── question_group.rb
│ ├── survey_section.rb
│ ├── dependency_condition.rb
│ ├── validation_condition.rb
│ ├── response.rb
│ └── survey_section_sweeper.rb
├── views
│ ├── partials
│ │ ├── _section_menu.html.haml
│ │ ├── _dependents.html.haml
│ │ ├── _section.html.haml
│ │ ├── _question.html.haml
│ │ ├── _answer.html.haml
│ │ └── _question_group.html.haml
│ ├── results
│ │ ├── index.html.erb
│ │ └── show.html.erb
│ ├── layouts
│ │ ├── surveyor_default.html.erb
│ │ └── results.html.erb
│ └── surveyor
│ │ ├── new.html.haml
│ │ ├── edit.html.haml
│ │ └── show.html.haml
├── controllers
│ ├── surveyor_controller.rb
│ └── results_controller.rb
└── helpers
│ ├── results_helper.rb
│ └── surveyor_helper.rb
├── generators
├── surveyor
│ ├── templates
│ │ ├── assets
│ │ │ ├── images
│ │ │ │ ├── next.gif
│ │ │ │ └── prev.gif
│ │ │ ├── stylesheets
│ │ │ │ ├── reset.css
│ │ │ │ ├── dateinput.css
│ │ │ │ ├── sass
│ │ │ │ │ └── surveyor.sass
│ │ │ │ └── results.css
│ │ │ └── javascripts
│ │ │ │ └── jquery.surveyor.js
│ │ ├── tasks
│ │ │ └── surveyor.rb
│ │ ├── README
│ │ ├── migrate
│ │ │ ├── add_index_to_surveys.rb
│ │ │ ├── add_default_value_to_answers.rb
│ │ │ ├── add_display_order_to_surveys.rb
│ │ │ ├── add_correct_answer_id_to_questions.rb
│ │ │ ├── add_index_to_response_sets.rb
│ │ │ ├── add_section_id_to_responses.rb
│ │ │ ├── create_validations.rb
│ │ │ ├── create_response_sets.rb
│ │ │ ├── create_dependencies.rb
│ │ │ ├── add_unique_indicies.rb
│ │ │ ├── create_question_groups.rb
│ │ │ ├── create_dependency_conditions.rb
│ │ │ ├── create_survey_sections.rb
│ │ │ ├── create_surveys.rb
│ │ │ ├── create_validation_conditions.rb
│ │ │ ├── create_questions.rb
│ │ │ ├── create_responses.rb
│ │ │ └── create_answers.rb
│ │ ├── surveys
│ │ │ └── quiz.rb
│ │ └── locales
│ │ │ ├── surveyor_he.yml
│ │ │ ├── surveyor_en.yml
│ │ │ └── surveyor_es.yml
│ └── surveyor_generator.rb
└── extend_surveyor
│ ├── templates
│ ├── extensions
│ │ ├── surveyor_custom.html.erb
│ │ └── surveyor_controller.rb
│ └── EXTENDING_SURVEYOR
│ └── extend_surveyor_generator.rb
├── .gitignore
├── testbed
└── Gemfile
├── lib
├── surveyor.rb
├── surveyor
│ ├── acts_as_response.rb
│ ├── models
│ │ ├── question_group_methods.rb
│ │ ├── survey_section_methods.rb
│ │ ├── response_methods.rb
│ │ ├── answer_methods.rb
│ │ ├── validation_methods.rb
│ │ ├── validation_condition_methods.rb
│ │ ├── dependency_methods.rb
│ │ ├── survey_methods.rb
│ │ ├── question_methods.rb
│ │ ├── dependency_condition_methods.rb
│ │ └── response_set_methods.rb
│ ├── common.rb
│ ├── surveyor_controller_methods.rb
│ └── unparser.rb
├── tasks
│ └── surveyor_tasks.rake
└── formtastic
│ └── surveyor_builder.rb
├── features
├── support
│ ├── redcap_siblings.csv
│ ├── paths.rb
│ └── env.rb
├── redcap_parser.feature
├── step_definitions
│ ├── surveyor_steps.rb
│ ├── parser_steps.rb
│ └── web_steps.rb
├── surveyor.feature
└── surveyor_parser.feature
├── ci-env.sh
├── Rakefile
├── MIT-LICENSE
├── config
└── routes.rb
├── hudson.rakefile
├── init_testbed.rakefile
├── README.md
└── surveyor.gemspec
/VERSION:
--------------------------------------------------------------------------------
1 | 0.19.3
2 |
--------------------------------------------------------------------------------
/spec/rcov.opts:
--------------------------------------------------------------------------------
1 | --exclude "spec/*,gems/*"
2 | --rails
--------------------------------------------------------------------------------
/.rvmrc:
--------------------------------------------------------------------------------
1 | rvm_gemset_create_on_use_flag=1; rvm gemset use surveyor-dev
2 |
--------------------------------------------------------------------------------
/rails/init.rb:
--------------------------------------------------------------------------------
1 | # For Rails 2.3 gem engine to work
2 | require 'surveyor'
--------------------------------------------------------------------------------
/spec/spec.opts:
--------------------------------------------------------------------------------
1 | --colour
2 | --format progress
3 | --loadby mtime
4 | --reverse
5 |
--------------------------------------------------------------------------------
/app/models/answer.rb:
--------------------------------------------------------------------------------
1 | class Answer < ActiveRecord::Base
2 | unloadable
3 | include Surveyor::Models::AnswerMethods
4 | end
5 |
--------------------------------------------------------------------------------
/app/models/question.rb:
--------------------------------------------------------------------------------
1 | class Question < ActiveRecord::Base
2 | unloadable
3 | include Surveyor::Models::QuestionMethods
4 | end
--------------------------------------------------------------------------------
/app/models/survey.rb:
--------------------------------------------------------------------------------
1 | class Survey < ActiveRecord::Base
2 | unloadable
3 | include Surveyor::Models::SurveyMethods
4 | end
5 |
--------------------------------------------------------------------------------
/app/models/validation.rb:
--------------------------------------------------------------------------------
1 | class Validation < ActiveRecord::Base
2 | unloadable
3 | include Surveyor::Models::ValidationMethods
4 | end
--------------------------------------------------------------------------------
/app/models/dependency.rb:
--------------------------------------------------------------------------------
1 | class Dependency < ActiveRecord::Base
2 | unloadable
3 | include Surveyor::Models::DependencyMethods
4 | end
5 |
--------------------------------------------------------------------------------
/app/models/response_set.rb:
--------------------------------------------------------------------------------
1 | class ResponseSet < ActiveRecord::Base
2 | unloadable
3 | include Surveyor::Models::ResponseSetMethods
4 | end
--------------------------------------------------------------------------------
/app/models/question_group.rb:
--------------------------------------------------------------------------------
1 | class QuestionGroup < ActiveRecord::Base
2 | unloadable
3 | include Surveyor::Models::QuestionGroupMethods
4 |
5 | end
6 |
--------------------------------------------------------------------------------
/app/models/survey_section.rb:
--------------------------------------------------------------------------------
1 | class SurveySection < ActiveRecord::Base
2 | unloadable
3 | include Surveyor::Models::SurveySectionMethods
4 | end
5 |
6 |
--------------------------------------------------------------------------------
/generators/surveyor/templates/assets/images/next.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brian-lc/surveyor/HEAD/generators/surveyor/templates/assets/images/next.gif
--------------------------------------------------------------------------------
/generators/surveyor/templates/assets/images/prev.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brian-lc/surveyor/HEAD/generators/surveyor/templates/assets/images/prev.gif
--------------------------------------------------------------------------------
/app/models/dependency_condition.rb:
--------------------------------------------------------------------------------
1 | class DependencyCondition < ActiveRecord::Base
2 | unloadable
3 | include Surveyor::Models::DependencyConditionMethods
4 | end
5 |
--------------------------------------------------------------------------------
/app/models/validation_condition.rb:
--------------------------------------------------------------------------------
1 | class ValidationCondition < ActiveRecord::Base
2 | unloadable
3 | include Surveyor::Models::ValidationConditionMethods
4 | end
5 |
--------------------------------------------------------------------------------
/app/models/response.rb:
--------------------------------------------------------------------------------
1 | class Response < ActiveRecord::Base
2 | unloadable
3 | include ActionView::Helpers::SanitizeHelper
4 | include Surveyor::Models::ResponseMethods
5 | end
6 |
--------------------------------------------------------------------------------
/app/views/partials/_section_menu.html.haml:
--------------------------------------------------------------------------------
1 | .surveyor_menu
2 | = t('surveyor.sections')
3 | %ul
4 | - @sections.each do |s|
5 | %li{:class => ("active" if s == @section)}= menu_button_for(s)
--------------------------------------------------------------------------------
/generators/surveyor/templates/tasks/surveyor.rb:
--------------------------------------------------------------------------------
1 | $VERBOSE = nil
2 | if surveyor_gem = Gem.searcher.find('surveyor')
3 | Dir["#{surveyor_gem.full_gem_path}/lib/tasks/*.rake"].each { |ext| load ext }
4 | end
--------------------------------------------------------------------------------
/generators/surveyor/templates/README:
--------------------------------------------------------------------------------
1 |
2 | Surveyor installed. Next, run the migrations:
3 |
4 | rake db:migrate
5 |
6 | Try out the "kitchen sink" survey:
7 |
8 | rake surveyor FILE=surveys/kitchen_sink_survey.rb
9 |
10 | Or see README.md for configuration and customization details
--------------------------------------------------------------------------------
/generators/surveyor/templates/migrate/add_index_to_surveys.rb:
--------------------------------------------------------------------------------
1 | class AddIndexToSurveys < ActiveRecord::Migration
2 | def self.up
3 | add_index(:surveys, :access_code, :name => 'surveys_ac_idx')
4 | end
5 |
6 | def self.down
7 | remove_index(:surveys, :name => 'surveys_ac_idx')
8 | end
9 | end
--------------------------------------------------------------------------------
/generators/surveyor/templates/migrate/add_default_value_to_answers.rb:
--------------------------------------------------------------------------------
1 | class AddDefaultValueToAnswers < ActiveRecord::Migration
2 | def self.up
3 | add_column :answers, :default_value, :string
4 | end
5 |
6 | def self.down
7 | remove_column :answers, :default_value
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/generators/surveyor/templates/migrate/add_display_order_to_surveys.rb:
--------------------------------------------------------------------------------
1 | class AddDisplayOrderToSurveys < ActiveRecord::Migration
2 | def self.up
3 | add_column :surveys, :display_order, :integer
4 | end
5 |
6 | def self.down
7 | remove_column :surveys, :display_order
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/generators/surveyor/templates/migrate/add_correct_answer_id_to_questions.rb:
--------------------------------------------------------------------------------
1 | class AddCorrectAnswerIdToQuestions < ActiveRecord::Migration
2 | def self.up
3 | add_column :questions, :correct_answer_id, :integer
4 | end
5 |
6 | def self.down
7 | remove_column :questions, :correct_answer_id
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/generators/surveyor/templates/migrate/add_index_to_response_sets.rb:
--------------------------------------------------------------------------------
1 | class AddIndexToResponseSets < ActiveRecord::Migration
2 | def self.up
3 | add_index(:response_sets, :access_code, :name => 'response_sets_ac_idx')
4 | end
5 |
6 | def self.down
7 | remove_index(:response_sets, :name => 'response_sets_ac_idx')
8 | end
9 | end
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## MAC OS
2 | .DS_Store
3 |
4 | ## TEXTMATE
5 | *.tmproj
6 | tmtags
7 |
8 | ## EMACS
9 | *~
10 | \#*
11 | .\#*
12 |
13 | ## VIM
14 | *.swp
15 |
16 | ## PROJECT::GENERAL
17 | coverage
18 | rdoc
19 | pkg
20 |
21 | ## PROJECT::SPECIFIC
22 | testbed/log/*.log
23 | testbed/db/*
24 | testbed/.bundle
25 | testbed/*
26 | surveys/fixtures/*.yml
27 |
--------------------------------------------------------------------------------
/app/controllers/surveyor_controller.rb:
--------------------------------------------------------------------------------
1 | # Surveyor Controller allows a user to take a survey. It is semi-RESTful since it does not have a concrete representation model.
2 | # The "resource" is a survey attempt/session populating a response set.
3 | class SurveyorController < ApplicationController
4 | unloadable
5 | include Surveyor::SurveyorControllerMethods
6 | end
--------------------------------------------------------------------------------
/testbed/Gemfile:
--------------------------------------------------------------------------------
1 | source :rubygems
2 |
3 | gem 'rails', '2.3.10'
4 | gem 'rspec', '~> 1.3'
5 | gem 'rspec-rails', '1.3.3'
6 | gem 'sqlite3-ruby', '1.2.5' # Using this version for ruby 1.8.7 compatibility reasons
7 | gem 'webrat'
8 | gem 'cucumber', '~> 0.8.0'
9 | gem 'cucumber-rails'
10 | gem 'database_cleaner'
11 | gem 'factory_girl'
12 |
13 | gem 'surveyor', :path => ".."
--------------------------------------------------------------------------------
/app/views/partials/_dependents.html.haml:
--------------------------------------------------------------------------------
1 | #dependents
2 | .title Follow-up questions from your answers on the previous page
3 | -# @dependents.each_with_index do |question, index|
4 | = dependency_explanation_helper(question, @response_set)
5 | = render question.custom_renderer || "/partials/question", :question => question, :response_set => @response_set, :number => "D#{index+1}"
--------------------------------------------------------------------------------
/app/models/survey_section_sweeper.rb:
--------------------------------------------------------------------------------
1 | class SurveySectionSweeper < ActionController::Caching::Sweeper
2 | observe :survey_section
3 |
4 | def after_save(section)
5 | expire_cache(section)
6 | end
7 |
8 | def after_destroy(section)
9 | expire_cache(section)
10 | end
11 |
12 | def expire_cache(section)
13 | expire_fregment "section_#{section.id}"
14 | end
15 | end
--------------------------------------------------------------------------------
/lib/surveyor.rb:
--------------------------------------------------------------------------------
1 | require 'surveyor/common'
2 | require 'surveyor/acts_as_response'
3 | require 'formtastic/surveyor_builder'
4 | Formtastic::SemanticFormHelper.builder = Formtastic::SurveyorBuilder
5 | Formtastic::SemanticFormBuilder.default_text_area_height = 5
6 | Formtastic::SemanticFormBuilder.default_text_area_width = 50
7 | Formtastic::SemanticFormBuilder.all_fields_required_by_default = false
8 |
--------------------------------------------------------------------------------
/generators/surveyor/templates/migrate/add_section_id_to_responses.rb:
--------------------------------------------------------------------------------
1 | class AddSectionIdToResponses < ActiveRecord::Migration
2 | def self.up
3 | add_column :responses, :survey_section_id, :integer
4 | add_index :responses, :survey_section_id
5 | end
6 |
7 | def self.down
8 | remove_index :responses, :survey_section_id
9 | remove_column :responses, :survey_section_id
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/controllers/results_controller.rb:
--------------------------------------------------------------------------------
1 | class ResultsController < ApplicationController
2 | helper 'surveyor'
3 | layout 'results'
4 | def index
5 | @surveys = Survey.all
6 | end
7 |
8 | def show
9 | @survey = Survey.find_by_access_code(params[:survey_code])
10 | @response_sets = @survey.response_sets
11 | @questions = @survey.sections_with_questions.map(&:questions).flatten
12 | end
13 | end
--------------------------------------------------------------------------------
/generators/surveyor/templates/surveys/quiz.rb:
--------------------------------------------------------------------------------
1 | survey "Favorites" do
2 | section "Foods" do
3 | # In a quiz, both the questions and the answers need to have reference identifiers
4 | # Here, the question has reference_identifier: "1", and the answers: "oint", "tweet", and "moo"
5 | question_1 "What is the best meat?", :pick => :one, :correct => "oink"
6 | a_oink "bacon"
7 | a_tweet "chicken"
8 | a_moo "beef"
9 | end
10 | end
--------------------------------------------------------------------------------
/app/views/results/index.html.erb:
--------------------------------------------------------------------------------
1 |
Listing Surveys
2 |
3 |
4 |
5 | | ID |
6 | Name |
7 | Operation |
8 |
9 |
10 | <% @surveys.each do |survey| -%>
11 |
12 | | <%=h survey.id %> |
13 | <%=h survey.title %> |
14 | <%= link_to "show results list(#{survey.response_sets.count})", result_path(survey.access_code) %> |
15 |
16 | <% end %>
17 |
18 |
--------------------------------------------------------------------------------
/app/views/layouts/surveyor_default.html.erb:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 | Survey: <%= controller.action_name %>
7 | <%= surveyor_includes %>
8 |
9 |
10 | <%= yield %>
11 |
12 |
13 |
--------------------------------------------------------------------------------
/features/support/redcap_siblings.csv:
--------------------------------------------------------------------------------
1 | Variable / Field Name,Form Name,Field Units,Section Header,Field Type,Field Label,Choices OR Calculations,Field Note,Text Validation Type,Text Validation Min,Text Validation Max,Identifier?,Branching Logic (Show field only if...),Required Field?
sibs,a3_subject_family_history,,,text,How many full siblings did the subject have?,,99 = Unknown,integer,0,20,,,
sib1yob,a3_subject_family_history,,,text,Sibling 1 year of birth,,9999 = Unknown,integer,1875,2011,,[sibs] > 0,
--------------------------------------------------------------------------------
/generators/surveyor/templates/migrate/create_validations.rb:
--------------------------------------------------------------------------------
1 | class CreateValidations < ActiveRecord::Migration
2 | def self.up
3 | create_table :validations do |t|
4 | # Context
5 | t.integer :answer_id # the answer to validate
6 |
7 | # Conditional
8 | t.string :rule
9 |
10 | # Message
11 | t.string :message
12 |
13 | t.timestamps
14 | end
15 | end
16 |
17 | def self.down
18 | drop_table :validations
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/app/views/layouts/results.html.erb:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 | Survey: Result Sets
7 | <%= surveyor_includes %>
8 |
9 |
10 | <%= flash[:notice] %>
11 | <%= yield %>
12 |
13 |
--------------------------------------------------------------------------------
/generators/extend_surveyor/templates/extensions/surveyor_custom.html.erb:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 | Survey: <%= controller.action_name %>
7 | <%= surveyor_includes %>
8 |
9 |
10 | <%= yield %>
11 |
12 |
13 |
--------------------------------------------------------------------------------
/generators/surveyor/templates/migrate/create_response_sets.rb:
--------------------------------------------------------------------------------
1 | class CreateResponseSets < ActiveRecord::Migration
2 | def self.up
3 | create_table :response_sets do |t|
4 | # Context
5 | t.integer :user_id
6 | t.integer :survey_id
7 |
8 | # Content
9 | t.string :access_code #unique id for the object used in urls
10 |
11 | # Expiry
12 | t.datetime :started_at
13 | t.datetime :completed_at
14 |
15 | t.timestamps
16 | end
17 | end
18 |
19 | def self.down
20 | drop_table :response_sets
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/app/helpers/results_helper.rb:
--------------------------------------------------------------------------------
1 | module ResultsHelper
2 | def display_response(r_set,question)
3 | sets = r_set.responses.select{|r| r.question.display_order == question.display_order}
4 | if sets.size == 0
5 | return "-"
6 | elsif sets.size == 1
7 | return (sets.first.string_value || sets.first.text_value || show_answer(sets.first))
8 | else
9 | txt = ""
10 | sets.each do |set|
11 | txt << show_answer(set) + "
"
12 | end
13 | return txt
14 | end
15 | end
16 |
17 | def show_answer(set)
18 | set.answer.text
19 | end
20 | end
--------------------------------------------------------------------------------
/lib/surveyor/acts_as_response.rb:
--------------------------------------------------------------------------------
1 | module Surveyor
2 | module ActsAsResponse
3 | # Returns the response as a particular response_class type
4 | def as(type_symbol)
5 | return case type_symbol.to_sym
6 | when :string, :text, :integer, :float, :datetime
7 | self.send("#{type_symbol}_value".to_sym)
8 | when :date
9 | self.datetime_value.nil? ? nil : self.datetime_value.to_date
10 | when :time
11 | self.datetime_value.nil? ? nil : self.datetime_value.to_time
12 | else # :answer_id
13 | self.answer_id
14 | end
15 | end
16 | end
17 | end
--------------------------------------------------------------------------------
/app/views/surveyor/new.html.haml:
--------------------------------------------------------------------------------
1 | #surveyor
2 | - unless (types = flash.keys.select{|k| [:notice, :error, :warning].include?(k)}).blank?
3 | .surveyor_flash
4 | = flash_messages(types)
5 | .close
6 | .survey_title= t('surveyor.take_these_surveys')
7 | %br
8 | #survey_list
9 | %ul
10 | - unless @surveys.empty?
11 | - @surveys.each do |survey|
12 | %li
13 | - form_tag(take_survey_path(:survey_code => survey.access_code)) do
14 | = survey.title
15 |
16 | = submit_tag( t('surveyor.take_it') )
17 | - else
18 | %li
19 | No surveys
20 |
--------------------------------------------------------------------------------
/generators/surveyor/templates/migrate/create_dependencies.rb:
--------------------------------------------------------------------------------
1 | class CreateDependencies < ActiveRecord::Migration
2 | def self.up
3 | create_table :dependencies do |t|
4 | # Context
5 | t.integer :question_id # the dependent question
6 | t.integer :question_group_id
7 |
8 | # Conditional
9 | t.string :rule
10 |
11 | # Result - TODO: figure out the dependency hook presentation options
12 | # t.string :property_to_toggle # visibility, class_name,
13 | # t.string :effect #blind, opacity
14 |
15 | t.timestamps
16 | end
17 | end
18 |
19 | def self.down
20 | drop_table :dependencies
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/app/views/results/show.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | | ID |
4 | Code |
5 | <% @questions.each do |question| %>
6 | <% next if question.display_order == 1 %>
7 | <%= "[" +question.display_order.to_s + "]" + question.text %> |
8 | <% end %>
9 |
10 |
11 | <% @response_sets.each do |r_set| %>
12 |
13 | | <%=h r_set.id %> |
14 | <%=h r_set.access_code %> |
15 | <% @questions.each do |question| %>
16 | <% next if question.display_order == 1 %>
17 | <%= display_response(r_set,question) %> |
18 | <% end %>
19 |
20 | <% end %>
21 |
22 |
23 |
24 |
25 | <%= link_to "Back", results_path %>
26 |
--------------------------------------------------------------------------------
/app/views/partials/_section.html.haml:
--------------------------------------------------------------------------------
1 | - div_for @section, :class => @section.custom_class do
2 | %span.title= @section.title
3 | - qs ||= []
4 | - (questions = @section.questions).each_with_index do |q, i|
5 | - if q.part_of_group?
6 | - qs << q # gather up the group questions
7 | - if (i+1 >= questions.size) or (q.question_group_id != questions[i+1].question_group_id)
8 | - # this is the last question of the section, or the group
9 | = render q.question_group.custom_renderer || "/partials/question_group", :g => q.question_group, :qs => qs, :f => f
10 | - qs = []
11 | - else # gather up the group questions
12 | = render q.custom_renderer || "/partials/question", :q => q, :f => f
--------------------------------------------------------------------------------
/generators/surveyor/templates/locales/surveyor_he.yml:
--------------------------------------------------------------------------------
1 | # Localization file for Hebrew. Add more files in this directory for other locales.
2 | # See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.
3 |
4 | he:
5 | surveyor:
6 | take_these_surveys: "תוכלו לבצע סקרים אלו"
7 | take_it: "בצע"
8 | completed_survey: "סיום הסקר"
9 | unable_to_find_your_responses: "לא נמצאו תשובותיך לסקר"
10 | unable_to_update_survey: ""
11 | unable_to_find_that_survey: "לא ניתן לאתר את הסקר המבוקש"
12 | survey_started_success: "הסקר הוחל בהצלחה"
13 | click_here_to_finish: "לסיום"
14 | previous_section: "חזרה »"
15 | next_section: "« המשך"
16 | Select_one: "בחר/י"
17 | sections: "סעיפים"
18 |
--------------------------------------------------------------------------------
/generators/surveyor/templates/migrate/add_unique_indicies.rb:
--------------------------------------------------------------------------------
1 | class AddUniqueIndicies < ActiveRecord::Migration
2 | def self.up
3 | remove_index(:response_sets, :name => 'response_sets_ac_idx')
4 | add_index(:response_sets, :access_code, :name => 'response_sets_ac_idx', :unique => true)
5 |
6 | remove_index(:surveys, :name => 'surveys_ac_idx')
7 | add_index(:surveys, :access_code, :name => 'surveys_ac_idx', :unique => true)
8 | end
9 |
10 | def self.down
11 | remove_index(:response_sets, :name => 'response_sets_ac_idx')
12 | add_index(:response_sets, :access_code, :name => 'response_sets_ac_idx')
13 |
14 | remove_index(:surveys, :name => 'surveys_ac_idx')
15 | add_index(:surveys, :access_code, :name => 'surveys_ac_idx')
16 | end
17 | end
--------------------------------------------------------------------------------
/ci-env.sh:
--------------------------------------------------------------------------------
1 | ######
2 | # This is not an executable script. It selects and configures rvm for
3 | # bcsec's CI process based on the RVM_RUBY environment variable.
4 | #
5 | # Use it by sourcing it:
6 | #
7 | # . ci-env.sh
8 | #
9 | # Assumes that the create-on-use settings are set in your ~/.rvmrc:
10 | #
11 | # rvm_install_on_use_flag=1
12 | # rvm_gemset_create_on_use_flag=1
13 | #
14 | # Hudson Build Execute Shell Commands:
15 | #
16 | # source ci-env.sh
17 | # rake -f hudson.rakefile --trace
18 | # cd testbed
19 | # export RAILS_ENV="hudson"
20 | # bundle exec rake spec cucumber
21 |
22 | export rvm_gemset_create_on_use_flag=1
23 | export rvm_project_rvmrc=0
24 |
25 | set +xe
26 | echo "Loading RVM ree@surveyor-dev"
27 | source ~/.rvm/scripts/rvm
28 | rvm use ree@surveyor-dev
29 | set -xe
30 |
--------------------------------------------------------------------------------
/generators/surveyor/templates/migrate/create_question_groups.rb:
--------------------------------------------------------------------------------
1 | class CreateQuestionGroups < ActiveRecord::Migration
2 | def self.up
3 | create_table :question_groups do |t|
4 | # Content
5 | t.text :text
6 | t.text :help_text
7 |
8 | # Reference
9 | t.string :reference_identifier # from paper
10 | t.string :data_export_identifier # data export
11 | t.string :common_namespace # maping to a common vocab
12 | t.string :common_identifier # maping to a common vocab
13 |
14 | # Display
15 | t.string :display_type
16 |
17 | t.string :custom_class
18 | t.string :custom_renderer
19 |
20 | t.timestamps
21 | end
22 | end
23 |
24 | def self.down
25 | drop_table :question_groups
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/generators/surveyor/templates/migrate/create_dependency_conditions.rb:
--------------------------------------------------------------------------------
1 | class CreateDependencyConditions < ActiveRecord::Migration
2 | def self.up
3 | create_table :dependency_conditions do |t|
4 | # Context
5 | t.integer :dependency_id
6 | t.string :rule_key
7 |
8 | # Conditional
9 | t.integer :question_id # the conditional question
10 | t.string :operator
11 |
12 | # Value
13 | t.integer :answer_id
14 | t.datetime :datetime_value
15 | t.integer :integer_value
16 | t.float :float_value
17 | t.string :unit
18 | t.text :text_value
19 | t.string :string_value
20 | t.string :response_other
21 |
22 | t.timestamps
23 | end
24 | end
25 |
26 | def self.down
27 | drop_table :dependency_conditions
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/generators/surveyor/templates/migrate/create_survey_sections.rb:
--------------------------------------------------------------------------------
1 | class CreateSurveySections < ActiveRecord::Migration
2 | def self.up
3 | create_table :survey_sections do |t|
4 | # Context
5 | t.integer :survey_id
6 |
7 | # Content
8 | t.string :title
9 | t.text :description
10 |
11 | # Reference
12 | t.string :reference_identifier # from paper
13 | t.string :data_export_identifier # data export
14 | t.string :common_namespace # maping to a common vocab
15 | t.string :common_identifier # maping to a common vocab
16 |
17 | # Display
18 | t.integer :display_order
19 |
20 | t.string :custom_class
21 |
22 | t.timestamps
23 | end
24 | end
25 |
26 | def self.down
27 | drop_table :survey_sections
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/generators/surveyor/templates/migrate/create_surveys.rb:
--------------------------------------------------------------------------------
1 | class CreateSurveys < ActiveRecord::Migration
2 | def self.up
3 | create_table :surveys do |t|
4 | # Content
5 | t.string :title
6 | t.text :description
7 |
8 | # Reference
9 | t.string :access_code
10 | t.string :reference_identifier # from paper
11 | t.string :data_export_identifier # data export
12 | t.string :common_namespace # maping to a common vocab
13 | t.string :common_identifier # maping to a common vocab
14 |
15 | # Expiry
16 | t.datetime :active_at
17 | t.datetime :inactive_at
18 |
19 | # Display
20 | t.string :css_url
21 |
22 | t.string :custom_class
23 |
24 | t.timestamps
25 | end
26 | end
27 |
28 | def self.down
29 | drop_table :surveys
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/generators/extend_surveyor/extend_surveyor_generator.rb:
--------------------------------------------------------------------------------
1 | class ExtendSurveyorGenerator < Rails::Generator::Base
2 | def manifest
3 | record do |m|
4 |
5 | # Copy README to your app
6 | m.file "EXTENDING_SURVEYOR", "surveys/EXTENDING_SURVEYOR"
7 |
8 | # Custom layout
9 | m.directory "app/views/layouts"
10 | m.file "extensions/surveyor_custom.html.erb", "app/views/layouts/surveyor_custom.html.erb"
11 |
12 | # Model, helper, and controller extensions
13 | # http://www.redmine.org/boards/3/topics/4095#message-4136
14 | # http://blog.mattwynne.net/2009/07/11/rails-tip-use-polymorphism-to-extend-your-controllers-at-runtime/
15 | m.file "extensions/surveyor_controller.rb", "app/controllers/surveyor_controller.rb"
16 |
17 | m.readme "EXTENDING_SURVEYOR"
18 |
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/generators/surveyor/templates/locales/surveyor_en.yml:
--------------------------------------------------------------------------------
1 | # Sample localization file for English. Add more files in this directory for other locales.
2 | # See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.
3 |
4 | en:
5 | surveyor:
6 | take_these_surveys: "You may take these surveys"
7 | take_it: "Take it"
8 | completed_survey: "Completed survey"
9 | unable_to_find_your_responses: "Unable to find your responses to the survey"
10 | unable_to_update_survey: "Unable to update survey"
11 | unable_to_find_that_survey: "Unable to find that survey"
12 | survey_started_success: "Survey started successfully"
13 | click_here_to_finish: "Click here to finish"
14 | previous_section: "« Previous section"
15 | next_section: "Next section »"
16 | select_one: "Select one ..."
17 | sections: "Sections"
18 |
--------------------------------------------------------------------------------
/generators/surveyor/templates/migrate/create_validation_conditions.rb:
--------------------------------------------------------------------------------
1 | class CreateValidationConditions < ActiveRecord::Migration
2 | def self.up
3 | create_table :validation_conditions do |t|
4 | # Context
5 | t.integer :validation_id
6 | t.string :rule_key
7 |
8 | # Conditional
9 | t.string :operator
10 |
11 | # Optional external reference
12 | t.integer :question_id
13 | t.integer :answer_id
14 |
15 | # Value
16 | t.datetime :datetime_value
17 | t.integer :integer_value
18 | t.float :float_value
19 | t.string :unit
20 | t.text :text_value
21 | t.string :string_value
22 | t.string :response_other
23 | t.string :regexp
24 |
25 | t.timestamps
26 | end
27 | end
28 |
29 | def self.down
30 | drop_table :validation_conditions
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/app/views/surveyor/edit.html.haml:
--------------------------------------------------------------------------------
1 | #surveyor
2 | - unless (types = flash.keys.select{|k| [:notice, :error, :warning].include?(k)}).blank?
3 | .surveyor_flash
4 | = flash_messages(types)
5 | .close
6 | - semantic_form_for(:r, @response_set, :url => update_my_survey_path, :html => {:method => :put, :id => "survey_form", :class => @survey.custom_class}) do |f|
7 | = render 'partials/section_menu' unless @sections.size < 3
8 | .survey_title= @survey.title
9 | .previous_section= previous_section
10 | = render 'partials/dependents' unless @dependents.empty?
11 | - if @response_set.no_responses_for_section?(@section) # cache if response_set has no responses for current section
12 | = cache("section_#{@section.id}"){ render "/partials/section", :f => f }
13 | - else # no cache
14 | = render "/partials/section", :f => f
15 | .next_section= next_section
16 | %br
17 |
--------------------------------------------------------------------------------
/generators/surveyor/templates/locales/surveyor_es.yml:
--------------------------------------------------------------------------------
1 | # Sample localization file for Spanish. Add more files in this directory for other locales.
2 | # See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.
3 |
4 | es:
5 | surveyor:
6 | take_these_surveys: "Bienvenido, usted puede tomar estas encuestas"
7 | take_it: "Tomar"
8 | completed_survey: "Encuesta completada"
9 | unable_to_find_your_responses: "No se puede encontrar sus respuestas a la encuesta"
10 | unable_to_update_survey: "No se puede actualizar la encuesta"
11 | unable_to_find_that_survey: "No se puede encontrar la encuesta"
12 | survey_started_success: "Encuesta iniciada correctamente"
13 | click_here_to_finish: "Haga clic aquí para terminar"
14 | previous_section: "« Sección anterior"
15 | next_section: "Sección siguiente »"
16 | select_one: "Seleccione una ..."
17 | sections: "Secciones"
18 |
--------------------------------------------------------------------------------
/app/views/surveyor/show.html.haml:
--------------------------------------------------------------------------------
1 | #surveyor
2 | - @survey.sections.each do |section|
3 | - div_for section do
4 | .title= section.title
5 | .questions
6 | - group_questions ||= []
7 | - section.questions.each_with_index do |question, index|
8 | - if question.part_of_group?
9 | - group_questions << question
10 | - if (index + 1 >= section.questions.size) or (question.question_group_id != section.questions[index + 1].question_group_id)
11 | - # skip to the last question of the section, or the last question of the group
12 | = render(:partial => "/partials/question_group", :locals => {:question_group => question.question_group, :response_set => @response_set, :group_questions => group_questions})
13 | - group_questions = []
14 | - else
15 | = render(:partial => "/partials/question", :locals => {:question => question, :response_set => @response_set})
--------------------------------------------------------------------------------
/features/support/paths.rb:
--------------------------------------------------------------------------------
1 | module NavigationHelpers
2 | # Maps a name to a path. Used by the
3 | #
4 | # When /^I go to (.+)$/ do |page_name|
5 | #
6 | # step definition in web_steps.rb
7 | #
8 | def path_to(page_name)
9 | case page_name
10 |
11 | when /the home\s?page/
12 | '/'
13 | when /the surveys page/
14 | '/surveys'
15 |
16 | # Add more mappings here.
17 | # Here is an example that pulls values out of the Regexp:
18 | #
19 | # when /^(.*)'s profile page$/i
20 | # user_profile_path(User.find_by_login($1))
21 |
22 | else
23 | begin
24 | page_name =~ /the (.*) page/
25 | path_components = $1.split(/\s+/)
26 | self.send(path_components.push('path').join('_').to_sym)
27 | rescue Object => e
28 | raise "Can't find mapping from \"#{page_name}\" to a path.\n" +
29 | "Now, go and add a mapping in #{__FILE__}"
30 | end
31 | end
32 | end
33 | end
34 |
35 | World(NavigationHelpers)
36 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'rake'
3 |
4 | begin
5 | require 'jeweler'
6 | Jeweler::Tasks.new do |gem|
7 | gem.name = "surveyor"
8 | gem.summary = %Q{A rails (gem) plugin to enable surveys in your application}
9 | gem.email = "yoon@northwestern.edu"
10 | gem.homepage = "http://github.com/breakpointer/surveyor"
11 | gem.authors = ["Brian Chamberlain", "Mark Yoon"]
12 | gem.add_dependency 'haml'
13 | gem.add_dependency 'fastercsv'
14 | gem.add_dependency 'formtastic'
15 | gem.add_development_dependency "yard", ">= 0"
16 | # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
17 | end
18 | Jeweler::GemcutterTasks.new
19 | rescue LoadError
20 | puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
21 | end
22 |
23 | begin
24 | require 'yard'
25 | YARD::Rake::YardocTask.new
26 | rescue LoadError
27 | task :yardoc do
28 | abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard"
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/generators/extend_surveyor/templates/extensions/surveyor_controller.rb:
--------------------------------------------------------------------------------
1 | module SurveyorControllerCustomMethods
2 | def self.included(base)
3 | # base.send :before_filter, :require_user # AuthLogic
4 | # base.send :before_filter, :login_required # Restful Authentication
5 | # base.send :layout, 'surveyor_custom'
6 | end
7 |
8 | # Actions
9 | def new
10 | super
11 | # @title = "You can take these surveys"
12 | end
13 | def create
14 | super
15 | end
16 | def show
17 | super
18 | end
19 | def edit
20 | super
21 | end
22 | def update
23 | super
24 | end
25 |
26 | # Paths
27 | def surveyor_index
28 | # most of the above actions redirect to this method
29 | super # available_surveys_path
30 | end
31 | def surveyor_finish
32 | # the update action redirects to this method if given params[:finish]
33 | super # available_surveys_path
34 | end
35 | end
36 | class SurveyorController < ApplicationController
37 | include Surveyor::SurveyorControllerMethods
38 | include SurveyorControllerCustomMethods
39 | end
40 |
--------------------------------------------------------------------------------
/features/redcap_parser.feature:
--------------------------------------------------------------------------------
1 | Feature: Survey creation
2 | As a
3 | I want to write out the survey in the DSL
4 | So that I can give it to survey participants
5 |
6 | Scenario: Basic questions
7 | Given I parse redcap file "REDCapDemoDatabase_DataDictionary.csv"
8 | Then there should be 1 survey with:
9 | ||
10 | And there should be 143 questions with:
11 | ||
12 | And there should be 233 answers with:
13 | ||
14 | And there should be 3 resolved dependency_conditions with:
15 | ||
16 | And there should be 2 dependencies with:
17 | | rule |
18 | | A |
19 | | A and B |
20 | Scenario: question level dependencies
21 | Given I parse redcap file "redcap_siblings.csv"
22 | Then there should be 1 survey with:
23 | ||
24 | And there should be 2 questions with:
25 | ||
26 | And there should be 2 answers with:
27 | ||
28 | And there should be 1 resolved dependency_conditions with:
29 | | rule_key |
30 | | A |
31 | And there should be 1 dependencies with:
32 | | rule |
33 | | A |
34 |
--------------------------------------------------------------------------------
/generators/surveyor/templates/migrate/create_questions.rb:
--------------------------------------------------------------------------------
1 | class CreateQuestions < ActiveRecord::Migration
2 | def self.up
3 | create_table :questions do |t|
4 | # Context
5 | t.integer :survey_section_id
6 | t.integer :question_group_id
7 |
8 | # Content
9 | t.text :text
10 | t.text :short_text # For experts (ie non-survey takers). Short version of text
11 | t.text :help_text
12 | t.string :pick
13 |
14 | # Reference
15 | t.string :reference_identifier # from paper
16 | t.string :data_export_identifier # data export
17 | t.string :common_namespace # maping to a common vocab
18 | t.string :common_identifier # maping to a common vocab
19 |
20 | # Display
21 | t.integer :display_order
22 | t.string :display_type
23 | t.boolean :is_mandatory
24 | t.integer :display_width # used only for slider component (if needed)
25 |
26 | t.string :custom_class
27 | t.string :custom_renderer
28 |
29 | t.timestamps
30 | end
31 | end
32 |
33 | def self.down
34 | drop_table :questions
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/lib/surveyor/models/question_group_methods.rb:
--------------------------------------------------------------------------------
1 | module Surveyor
2 | module Models
3 | module QuestionGroupMethods
4 | def self.included(base)
5 | # Associations
6 | base.send :has_many, :questions
7 | base.send :has_one, :dependency
8 | end
9 |
10 | # Instance Methods
11 | def initialize(*args)
12 | super(*args)
13 | default_args
14 | end
15 |
16 | def default_args
17 | self.display_type ||= "inline"
18 | end
19 |
20 | def renderer
21 | display_type.blank? ? :default : display_type.to_sym
22 | end
23 |
24 | def display_type=(val)
25 | write_attribute(:display_type, val.nil? ? nil : val.to_s)
26 | end
27 |
28 | def dependent?
29 | self.dependency != nil
30 | end
31 | def triggered?(response_set)
32 | dependent? ? self.dependency.is_met?(response_set) : true
33 | end
34 | def css_class(response_set)
35 | [(dependent? ? "g_dependent" : nil), (triggered?(response_set) ? nil : "g_hidden"), custom_class].compact.join(" ")
36 | end
37 | end
38 | end
39 | end
--------------------------------------------------------------------------------
/MIT-LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2008-2010 Brian Chamberlain and Mark Yoon
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/generators/surveyor/templates/migrate/create_responses.rb:
--------------------------------------------------------------------------------
1 | class CreateResponses < ActiveRecord::Migration
2 | def self.up
3 | create_table :responses do |t|
4 | # Context
5 | t.integer :response_set_id
6 | t.integer :question_id
7 |
8 | # Content
9 | t.integer :answer_id
10 | t.datetime :datetime_value # handles date, time, and datetime (segregate by answer.response_class)
11 |
12 | #t.datetime :time_value
13 | t.integer :integer_value
14 | t.float :float_value
15 | t.string :unit
16 | t.text :text_value
17 | t.string :string_value
18 | t.string :response_other #used to hold the string entered with "Other" type answers in multiple choice questions
19 |
20 | # arbitrary identifier used to group responses
21 | # the pertinent example here is Q: What's your car's make/model/year
22 | # group 1: Ford/Focus/2007
23 | # group 2: Toyota/Prius/2006
24 | t.string :response_group
25 |
26 | t.timestamps
27 | end
28 | end
29 |
30 | def self.down
31 | drop_table :responses
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | ActionController::Routing::Routes.draw do |map|
2 | map.with_options :controller => 'results' do |r|
3 | r.results "results", :conditions => {:method => :get}, :action => "index"
4 | r.result "results/:survey_code", :conditions => {:method => :get}, :action => "show"
5 | end
6 | map.with_options :controller => 'surveyor' do |s|
7 | s.available_surveys "surveys", :conditions => {:method => :get}, :action => "new" # GET survey list
8 | s.take_survey "surveys/:survey_code", :conditions => {:method => :post}, :action => "create" # Only POST of survey to create
9 | s.view_my_survey "surveys/:survey_code/:response_set_code.:format", :conditions => {:method => :get}, :action => "show", :format => "html" # GET viewable/printable? survey
10 | s.edit_my_survey "surveys/:survey_code/:response_set_code/take", :conditions => {:method => :get}, :action => "edit" # GET editable survey
11 | s.update_my_survey "surveys/:survey_code/:response_set_code", :conditions => {:method => :put}, :action => "update" # PUT edited survey
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/generators/surveyor/templates/assets/stylesheets/reset.css:
--------------------------------------------------------------------------------
1 | html, body, div, span, applet, object, iframe,
2 | h1, h2, h3, h4, h5, h6, p, blockquote, pre,
3 | a, abbr, acronym, address, big, cite, code,
4 | del, dfn, em, font, img, ins, kbd, q, s, samp,
5 | small, strike, strong, sub, sup, tt, var,
6 | dl, dt, dd, ol, ul, li,
7 | fieldset, form, label, legend,
8 | table, caption, tbody, tfoot, thead, tr, th, td {
9 | margin: 0;
10 | padding: 0;
11 | border: 0;
12 | outline: 0;
13 | font-weight: inherit;
14 | font-style: inherit;
15 | font-size: 100%;
16 | font-family: inherit;
17 | vertical-align: baseline;
18 | }
19 | /* remember to define focus styles! */
20 | :focus {
21 | outline: 0;
22 | }
23 | body {
24 | line-height: 1;
25 | color: black;
26 | background: white;
27 | }
28 | ol, ul {
29 | list-style: none;
30 | }
31 | /* tables still need 'cellspacing="0"' in the markup */
32 | table {
33 | border-collapse: separate;
34 | border-spacing: 0;
35 | }
36 | caption, th, td {
37 | text-align: left;
38 | font-weight: normal;
39 | }
40 | blockquote:before, blockquote:after,
41 | q:before, q:after {
42 | content: "";
43 | }
44 | blockquote, q {
45 | quotes: "" "";
46 | }
47 | pre {
48 | background: #333333;
49 | color: #EFEFEF;
50 | }
--------------------------------------------------------------------------------
/lib/surveyor/common.rb:
--------------------------------------------------------------------------------
1 | module Surveyor
2 | class Common
3 | RAND_CHARS = [('a'..'z'), ('A'..'Z'), (0..9)].map{|r| r.to_a}.flatten.to_s
4 | OPERATORS = %w(== != < > <= >= =~)
5 |
6 | class << self
7 | def make_tiny_code(len = 10)
8 | if RUBY_VERSION < "1.8.7"
9 | (1..len).to_a.map{|i| RAND_CHARS[rand(RAND_CHARS.size), 1] }.to_s
10 | else
11 | len.times.map{|i| RAND_CHARS[rand(RAND_CHARS.size), 1] }.to_s
12 | end
13 | end
14 |
15 | def to_normalized_string(text)
16 | words_to_omit = %w(a be but has have in is it of on or the to when)
17 | col_text = text.to_s.gsub(/(<[^>]*>)|\n|\t/s, ' ') # Remove html tags
18 | col_text.downcase! # Remove capitalization
19 | col_text.gsub!(/\"|\'/, '') # Remove potential problem characters
20 | col_text.gsub!(/\(.*?\)/,'') # Remove text inside parens
21 | col_text.gsub!(/\W/, ' ') # Remove all other non-word characters
22 | cols = (col_text.split(' ') - words_to_omit)
23 | (cols.size > 5 ? cols[-5..-1] : cols).join("_")
24 | end
25 |
26 | alias :normalize :to_normalized_string
27 |
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/generators/extend_surveyor/templates/EXTENDING_SURVEYOR:
--------------------------------------------------------------------------------
1 | == SurveyorController
2 |
3 | The SurveyorController class just includes actions from Surveyor::SurveyorControllerMethods module. You may include your own module, and overwrite the methods or add to them using "super". A template for this customization is in your app/controllers/surveyor\_controller.rb. SurveyorController is "unloadable", so changes in development (and any environment that does not cache classes) will be reflected immediately without restarting the app.
4 |
5 | == Models
6 |
7 | Surveyor's models can all be customized:
8 |
9 | - answer
10 | - dependency_condition
11 | - dependency
12 | - question_group
13 | - question
14 | - response_set
15 | - response
16 | - survey_section
17 | - survey
18 | - validation_condition
19 | - validation
20 |
21 | For example, create app/models/survey.rb with the following contents:
22 |
23 | class Survey < ActiveRecord::Base
24 | include Surveyor::Models::SurveyMethods
25 | def title
26 | "Custom #{super}"
27 | end
28 | end
29 |
30 | == SurveyorHelper
31 |
32 | == Views
33 |
34 | Surveyor's views can be overwritten by simply creating views in app/views/surveyor
35 |
36 | == Layout
37 |
38 | Create a custom SurveyorController as above, and specify your custom layout in it.
39 |
--------------------------------------------------------------------------------
/lib/surveyor/models/survey_section_methods.rb:
--------------------------------------------------------------------------------
1 | module Surveyor
2 | module Models
3 | module SurveySectionMethods
4 | def self.included(base)
5 | # Associations
6 | base.send :has_many, :questions, :order => "display_order ASC", :dependent => :destroy
7 | base.send :belongs_to, :survey
8 |
9 | # Scopes
10 | base.send :default_scope, :order => "display_order ASC"
11 | base.send :named_scope, :with_includes, { :include => {:questions => [:answers, :question_group, {:dependency => :dependency_conditions}]}}
12 |
13 | @@validations_already_included ||= nil
14 | unless @@validations_already_included
15 | # Validations
16 | base.send :validates_presence_of, :title, :display_order
17 | # this causes issues with building and saving
18 | #, :survey
19 |
20 | @@validations_already_included = true
21 | end
22 | end
23 |
24 | # Instance Methods
25 | def initialize(*args)
26 | super(*args)
27 | default_args
28 | end
29 |
30 | def default_args
31 | self.display_order ||= survey ? survey.sections.count : 0
32 | self.data_export_identifier ||= Surveyor::Common.normalize(title)
33 | end
34 |
35 | end
36 | end
37 | end
--------------------------------------------------------------------------------
/app/views/partials/_question.html.haml:
--------------------------------------------------------------------------------
1 | -# TODO: js for slider
2 | - rg ||= nil
3 | - renderer = q.renderer(g ||= nil)
4 | - f.inputs q_text(q), :id => rg ? "q_#{q.id}_#{rg.id}" : "q_#{q.id}", :class => "q_#{renderer} #{q.css_class(@response_set)}" do
5 | %span.help= q.help_text
6 | - case renderer
7 | - when :image, :label
8 | - when :dropdown, :inline_dropdown, :slider
9 | - r = response_for(@response_set, q, nil, rg)
10 | - i = response_idx
11 | - f.semantic_fields_for i, r do |ff|
12 | = ff.quiet_input :question_id
13 | = ff.quiet_input :response_group, :value => rg if g && g.display_type == "repeater"
14 | = ff.input :answer_id, :as => :select, :collection => q.answers.map{|a| [a.text, a.id]}, :label => false
15 | - else # :default, :inline, :inline_default
16 | - if q.pick == "one"
17 | - r = response_for(@response_set, q, nil, rg)
18 | - i = response_idx # increment the response index since the answer partial skips for q.pick == one
19 | - f.semantic_fields_for i, r do |ff|
20 | = ff.quiet_input :question_id
21 | = ff.quiet_input :response_group, :value => rg if g && g.display_type == "repeater"
22 | = ff.quiet_input :id unless r.new_record?
23 | - q.answers.each do |a|
24 | = render a.custom_renderer || '/partials/answer', :q => q, :a => a, :f => f, :rg => rg, :g => g
25 |
--------------------------------------------------------------------------------
/hudson.rakefile:
--------------------------------------------------------------------------------
1 | BUNDLER_VERSION="1.0.7"
2 | import 'init_testbed.rakefile'
3 |
4 | namespace :bundle do
5 | task :ensure_bundler_available do
6 | `gem list -i bundler -v '=#{BUNDLER_VERSION}'`
7 | unless $? == 0
8 | puts bordered_message("Installing bundler #{BUNDLER_VERSION}")
9 | system("gem install bundler -v '=#{BUNDLER_VERSION}' --no-ri --no-rdoc")
10 | unless $? == 0
11 | fail bordered_message("Install failed.\nPlease fix the problem and try again or manually install bundler #{BUNDLER_VERSION}.")
12 | end
13 | end
14 | end
15 |
16 | def bordered_message(msg)
17 | len = msg.split("\n").collect { |l| l.size }.max
18 | ['=' * len, msg, '=' * len].join("\n")
19 | end
20 | end
21 | namespace :ci do
22 | task :generate_testbed_for_hudson => [:'testbed:remove', :'testbed:generate'] do
23 | # Hudson
24 | chdir("testbed") do
25 | database_yml = File.read('config/database.yml') + "\n\nhudson:\n <<: *test\n"
26 | File.open('config/database.yml', 'w'){|f| f.write database_yml}
27 | sh "cp config/environments/cucumber.rb config/environments/hudson.rb"
28 | end
29 | end
30 | task :setup_testbed_for_hudson => [:'testbed:setup', :'testbed:migrate']
31 | end
32 |
33 | task :default => [:'bundle:ensure_bundler_available', :'ci:generate_testbed_for_hudson', :'ci:setup_testbed_for_hudson']
34 |
--------------------------------------------------------------------------------
/lib/surveyor/models/response_methods.rb:
--------------------------------------------------------------------------------
1 | module Surveyor
2 | module Models
3 | module ResponseMethods
4 | def self.included(base)
5 | # Associations
6 | base.send :belongs_to, :response_set
7 | base.send :belongs_to, :question
8 | base.send :belongs_to, :answer
9 | @@validations_already_included ||= nil
10 | unless @@validations_already_included
11 | # Validations
12 | base.send :validates_presence_of, :response_set_id, :question_id, :answer_id
13 |
14 | @@validations_already_included = true
15 | end
16 | base.send :include, Surveyor::ActsAsResponse # includes "as" instance method
17 | end
18 |
19 | # Instance Methods
20 | def answer_id=(val)
21 | write_attribute :answer_id, (val.is_a?(Array) ? val.detect{|x| !x.to_s.blank?} : val)
22 | end
23 | def correct?
24 | question.correct_answer_id.nil? or self.answer.response_class != "answer" or (question.correct_answer_id.to_i == answer_id.to_i)
25 | end
26 |
27 | def to_s # used in dependency_explanation_helper
28 | if self.answer.response_class == "answer" and self.answer_id
29 | return self.answer.text
30 | else
31 | return "#{(self.string_value || self.text_value || self.integer_value || self.float_value || nil).to_s}"
32 | end
33 | end
34 | end
35 | end
36 | end
--------------------------------------------------------------------------------
/spec/models/answer_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2 |
3 | describe Answer, "when creating a new answer" do
4 | before(:each) do
5 | @answer = Factory(:answer, :text => "Red")
6 | end
7 |
8 | it "should be valid" do
9 | @answer.should be_valid
10 | end
11 |
12 | # this causes issues with building and saving answers to questions within a grid.
13 | # it "should be invalid without a question_id" do
14 | # @answer.question_id = nil
15 | # @answer.should_not be_valid
16 | # end
17 |
18 | it "should tell me its css class" do
19 | @answer.custom_class = "foo bar"
20 | @answer.css_class.should == "foo bar"
21 | @answer.is_exclusive = true
22 | @answer.css_class.should == "exclusive foo bar"
23 | end
24 |
25 | it "should hide the label when hide_label is set" do
26 | @answer.split_or_hidden_text.should == "Red"
27 | @answer.hide_label = true
28 | @answer.split_or_hidden_text.should == ""
29 | end
30 | it "should split up pre/post labels" do
31 | @answer.text = "before|after|extra"
32 | @answer.split_or_hidden_text(:pre).should == "before"
33 | @answer.split_or_hidden_text(:post).should == "after|extra"
34 | end
35 | it "should delete validation when it is deleted" do
36 | v_id = Factory(:validation, :answer => @answer).id
37 | @answer.destroy
38 | Validation.find_by_id(v_id).should be_nil
39 | end
40 | end
--------------------------------------------------------------------------------
/spec/models/question_group_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2 |
3 | describe QuestionGroup do
4 | before(:each) do
5 | @question_group = Factory(:question_group)
6 | end
7 |
8 | it "should be valid" do
9 | @question_group.should be_valid
10 | end
11 | it "should have defaults" do
12 | @question_group = QuestionGroup.new
13 | @question_group.display_type.should == "inline"
14 | @question_group.renderer.should == :inline
15 | @question_group.display_type = nil
16 | @question_group.renderer.should == :default
17 | end
18 | it "should return its custom css class" do
19 | @question_group.custom_class = "foo bar"
20 | @question_group.css_class(Factory(:response_set)).should == "foo bar"
21 | end
22 | it "should return its dependency class" do
23 | @dependency = Factory(:dependency)
24 | @question_group.dependency = @dependency
25 | @dependency.should_receive(:is_met?).and_return(true)
26 | @question_group.css_class(Factory(:response_set)).should == "g_dependent"
27 |
28 | @dependency.should_receive(:is_met?).and_return(false)
29 | @question_group.css_class(Factory(:response_set)).should == "g_dependent g_hidden"
30 |
31 | @question_group.custom_class = "foo bar"
32 | @dependency.should_receive(:is_met?).and_return(false)
33 | @question_group.css_class(Factory(:response_set)).should == "g_dependent g_hidden foo bar"
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/features/step_definitions/surveyor_steps.rb:
--------------------------------------------------------------------------------
1 | When /^I start the "([^"]*)" survey$/ do |name|
2 | When "I go to the surveys page"
3 | Then "I should see \"#{name}\""
4 | click_button "Take it"
5 | end
6 |
7 | Then /^there should be (\d+) response set with (\d+) responses with:$/ do |rs_num, r_num, table|
8 | ResponseSet.count.should == rs_num.to_i
9 | Response.count.should == r_num.to_i
10 | table.hashes.each do |hash|
11 | if hash.keys == ["answer"]
12 | a = Answer.find_by_text(hash["answer"])
13 | a.should_not be_nil
14 | Response.first(:conditions => {:answer_id => a.id}).should_not be_nil
15 | else
16 | if !(a = hash.delete("answer")).blank? and !(answer = Answer.find_by_text(a)).blank?
17 | Response.first(:conditions => hash.merge({:answer_id => answer.id})).should_not be_nil
18 | elsif
19 | Response.first(:conditions => hash).should_not be_nil
20 | end
21 | end
22 | end
23 | end
24 |
25 | Then /^there should be (\d+) dependencies$/ do |x|
26 | Dependency.count.should == x.to_i
27 | end
28 |
29 | Then /^question "([^"]*)" should have a dependency with rule "([^"]*)"$/ do |qr, rule|
30 | q = Question.find_by_reference_identifier(qr)
31 | q.should_not be_blank
32 | q.dependency.should_not be_nil
33 | q.dependency.rule.should == rule
34 | end
35 |
36 | Then /^the element "([^"]*)" should have the class "([^"]*)"$/ do |selector, css_class|
37 | response.should have_selector(selector, :class => css_class)
38 | end
39 |
--------------------------------------------------------------------------------
/app/views/partials/_answer.html.haml:
--------------------------------------------------------------------------------
1 | -# TODO: disabled
2 | - rg ||= nil
3 | - r = response_for(@response_set, q, a, rg)
4 | - i = response_idx(q.pick != "one") # argument will be false (don't increment i) if we're on radio buttons
5 | - f.semantic_fields_for i, r do |ff|
6 | = ff.quiet_input :question_id unless q.pick == "one" # don't repeat question_id if we're on radio buttons
7 | = ff.quiet_input :id unless q.pick == "one" or r.new_record?
8 | = ff.quiet_input :response_group, :value => rg if q.pick != "one" && g && g.display_type == "repeater"
9 | - case q.pick
10 | - when "one"
11 | = ff.input :answer_id, :as => :surveyor_radio, :collection => [[a.text, a.id]], :label => false, :input_html => {:class => a.css_class}, :response_class => a.response_class
12 | - when "any"
13 | = ff.input :answer_id, :as => :surveyor_check_boxes, :collection => [[a.text, a.id]], :label => false, :input_html => {:class => a.css_class}, :response_class => a.response_class
14 | - when "none"
15 | - if %w(date datetime time float integer string text).include? a.response_class
16 | = ff.quiet_input :answer_id, :input_html => {:class => a.css_class, :value => a.id}
17 | = ff.input rc_to_attr(a.response_class), :as => rc_to_as(a.response_class), :label => a.split_or_hidden_text(:pre).blank? ? false : a.split_or_hidden_text(:pre), :hint => a.split_or_hidden_text(:post), :input_html => generate_pick_none_input_html(r.as(a.response_class), a.default_value, a.css_class)
18 | - else
19 | = a.text
20 |
--------------------------------------------------------------------------------
/spec/lib/parser_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2 |
3 | describe Surveyor::Parser do
4 | before(:each) do
5 | @parser = Surveyor::Parser.new
6 | end
7 | it "should translate shortcuts into full model names" do
8 | @parser.send(:full, "section").should == "survey_section"
9 | @parser.send(:full, "g").should == "question_group"
10 | @parser.send(:full, "repeater").should == "question_group"
11 | @parser.send(:full, "label").should == "question"
12 | @parser.send(:full, "vc").should == "validation_condition"
13 | @parser.send(:full, "vcondition").should == "validation_condition"
14 | end
15 | it "should translate 'condition' based on context" do
16 | @parser.send(:full, "condition").should == "dependency_condition"
17 | @parser.send(:full, "c").should == "dependency_condition"
18 | @parser.context[:validation] = Validation.new
19 | @parser.send(:full, "condition").should == "validation_condition"
20 | @parser.send(:full, "c").should == "validation_condition"
21 | @parser.context[:validation] = nil
22 | @parser.send(:full, "condition").should == "dependency_condition"
23 | @parser.send(:full, "c").should == "dependency_condition"
24 | end
25 | it "should identify models that take blocks" do
26 | @parser.send(:block_models).should == %w(survey survey_section question_group)
27 | end
28 | it "should return a survey object" do
29 | Surveyor::Parser.new.parse("survey 'hi' do\n end").is_a?(Survey).should be_true
30 | end
31 |
32 | end
--------------------------------------------------------------------------------
/generators/surveyor/templates/migrate/create_answers.rb:
--------------------------------------------------------------------------------
1 | class CreateAnswers < ActiveRecord::Migration
2 | def self.up
3 | create_table :answers do |t|
4 | # Context
5 | t.integer :question_id
6 |
7 | # Content
8 | t.text :text
9 | t.text :short_text #Used for presenting responses to experts (ie non-survey takers). Just a shorted version of the string
10 | t.text :help_text
11 | t.integer :weight # Used to assign a weight to an answer object (used for computing surveys that have numerical results) (I added this to support the Urology questionnaire -BLC)
12 | t.string :response_class # What kind of additional data does this answer accept?
13 |
14 | # Reference
15 | t.string :reference_identifier # from paper
16 | t.string :data_export_identifier # data export
17 | t.string :common_namespace # maping to a common vocab
18 | t.string :common_identifier # maping to a common vocab
19 |
20 | # Display
21 | t.integer :display_order
22 | t.boolean :is_exclusive # If set it causes some UI trigger to remove (and disable) all the other answer choices selected for a question (needed for the WHR)
23 | t.boolean :hide_label
24 | t.integer :display_length # if smaller than answer.length the html input length will be this value
25 |
26 | t.string :custom_class
27 | t.string :custom_renderer
28 |
29 | t.timestamps
30 |
31 | end
32 | end
33 |
34 | def self.down
35 | drop_table :answers
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/spec/lib/common_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2 |
3 | describe Surveyor::Common, "" do
4 | it "should convert text to a code that is more appropirate for a database entry" do
5 | # A few answers from the survey
6 | { "This? is a in - t3rrible-@nswer of! (question) on" => "this_t3rrible_nswer",
7 | "Private insurance/ HMO/ PPO" => "private_insurance_hmo_ppo",
8 | "VA" => "va",
9 | "PMS (Premenstrual syndrome)/ PMDD (Premenstrual Dysphoric Disorder)" => "pms_pmdd",
10 | "Have never been employed outside the home" => "never_been_employed_outside_home",
11 | "Professional" => "professional",
12 | "Not working because of temporary disability, but expect to return to a job" => "temporary_disability_expect_return_job",
13 | "How long has it been since you last visited a doctor for a routine checkup (routine being not for a particular reason)?" => "visited_doctor_for_routine_checkup",
14 | "Do you take medications as directed?" => "you_take_medications_as_directed",
15 | "Do you every leak urine (or) water when you didn't want to?" => "urine_water_you_didnt_want", #checking for () and ' removal
16 | "Do your biological family members (not adopted) have a \"history\" of any of the following?" => "family_members_history_any_following",
17 | "Your health:" => "your_health",
18 | "In general, you would say your health is:" => "you_would_say_your_health"
19 | }.each{|k, v| Surveyor::Common.to_normalized_string(k).should == v}
20 | end
21 |
22 | end
--------------------------------------------------------------------------------
/spec/models/survey_section_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2 |
3 | describe SurveySection, "when saving a survey_section" do
4 | before(:each) do
5 | @valid_attributes={:title => "foo", :survey_id => 2, :display_order => 4}
6 | @survey_section = SurveySection.new(@valid_attributes)
7 | end
8 |
9 | it "should be invalid without title" do
10 | @survey_section.title = nil
11 | @survey_section.should have(1).error_on(:title)
12 | end
13 |
14 | it "should have a parent survey" do
15 | # this causes issues with building and saving
16 | # @survey_section.survey_id = nil
17 | # @survey_section.should have(1).error_on(:survey)
18 | end
19 | end
20 |
21 | describe SurveySection, "with questions" do
22 | before(:each) do
23 | @survey_section = Factory(:survey_section, :title => "Rhymes", :display_order => 4)
24 | @q1 = @survey_section.questions.create(:text => "Peep", :display_order => 3)
25 | @q2 = @survey_section.questions.create(:text => "Little", :display_order => 1)
26 | @q3 = @survey_section.questions.create(:text => "Bo", :display_order => 2)
27 | end
28 |
29 | it "should return questions sorted in display order" do
30 | @survey_section.questions.should have(3).questions
31 | @survey_section.questions.should == [@q2,@q3,@q1]
32 | end
33 | it "should delete questions when it is deleted" do
34 | question_ids = @survey_section.questions.map(&:id)
35 | @survey_section.destroy
36 | question_ids.each{|id| Question.find_by_id(id).should be_nil}
37 | end
38 |
39 | end
40 |
--------------------------------------------------------------------------------
/app/views/partials/_question_group.html.haml:
--------------------------------------------------------------------------------
1 | - renderer = g.renderer
2 | - f.inputs q_text(g), :id => "g_#{g.id}", :class => "g_#{renderer} #{g.css_class(@response_set)}" do
3 | %li.help= g.help_text
4 | - case renderer
5 | - when :grid
6 | %li
7 | %table
8 | %col.pre
9 | - qs.first.answers.each do |a|
10 | %col{:class => cycle("odd", "even")}
11 | %col.post
12 | %tbody
13 | - qs.each_slice(10) do |ten_questions| # header row every 10
14 | %tr
15 | %th
16 | - ten_questions.first.answers.each do |a|
17 | %th= a.text
18 | %th
19 | - ten_questions.each_with_index do |q, i|
20 | %tr{:id => "q_#{q.id}", :class => "q_#{renderer} #{q.css_class(@response_set)}"}
21 | %th= q.split_text(:pre)
22 | - response_idx if q.pick == "one" # increment the response index since the answer partial skips for q.pick == one
23 | - q.answers.each do |a|
24 | %td= render a.custom_renderer || '/partials/answer', :g => g, :q => q, :a => a, :f => f
25 | %th= q.split_text(:post)
26 | - when :repeater
27 | - (@response_set.count_group_responses(qs) + 1).times do |rg|
28 | %li
29 | - qs.each do |q|
30 | = render q.custom_renderer || "/partials/question", :g => g, :rg => rg, :q => q, :f => f
31 | = submit_tag("+ add row", :name => "section[#{@section.id}][g_#{g.id}]", :class => "add_row")
32 | - else # :inline
33 | - qs.each do |q|
34 | = render q.custom_renderer || "/partials/question", :g => g, :q => q, :f => f
35 |
--------------------------------------------------------------------------------
/lib/surveyor/models/answer_methods.rb:
--------------------------------------------------------------------------------
1 | module Surveyor
2 | module Models
3 | module AnswerMethods
4 | def self.included(base)
5 | # Associations
6 | base.send :belongs_to, :question
7 | base.send :has_many, :responses
8 | base.send :has_many, :validations, :dependent => :destroy
9 |
10 | # Scopes
11 | base.send :default_scope, :order => "display_order ASC"
12 |
13 | @@validations_already_included ||= nil
14 | unless @@validations_already_included
15 | # Validations
16 | base.send :validates_presence_of, :text
17 | # this causes issues with building and saving
18 | # base.send :validates_numericality_of, :question_id, :allow_nil => false, :only_integer => true
19 | @@validations_already_included = true
20 | end
21 | end
22 |
23 | # Instance Methods
24 | def initialize(*args)
25 | super(*args)
26 | default_args
27 | end
28 |
29 | def default_args
30 | self.display_order ||= self.question ? self.question.answers.count : 0
31 | self.is_exclusive ||= false
32 | self.hide_label ||= false
33 | self.response_class ||= "answer"
34 | self.short_text ||= text
35 | self.data_export_identifier ||= Surveyor::Common.normalize(text)
36 | end
37 |
38 | def css_class
39 | [(is_exclusive ? "exclusive" : nil), custom_class].compact.join(" ")
40 | end
41 |
42 | def split_or_hidden_text(part = nil)
43 | return "" if hide_label.to_s == "true"
44 | part == :pre ? text.split("|",2)[0] : (part == :post ? text.split("|",2)[1] : text)
45 | end
46 |
47 | end
48 | end
49 | end
--------------------------------------------------------------------------------
/lib/surveyor/models/validation_methods.rb:
--------------------------------------------------------------------------------
1 | module Surveyor
2 | module Models
3 | module ValidationMethods
4 | def self.included(base)
5 | # Associations
6 | base.send :belongs_to, :answer
7 | base.send :has_many, :validation_conditions, :dependent => :destroy
8 |
9 | # Scopes
10 |
11 | @@validations_already_included ||= nil
12 | unless @@validations_already_included
13 | # Validations
14 | base.send :validates_presence_of, :rule
15 | base.send :validates_format_of, :rule, :with => /^(?:and|or|\)|\(|[A-Z]|\s)+$/
16 | # this causes issues with building and saving
17 | # base.send :validates_numericality_of, :answer_id
18 |
19 | @@validations_already_included = true
20 | end
21 | end
22 |
23 | # Instance Methods
24 | def is_valid?(response_set)
25 | ch = conditions_hash(response_set)
26 | rgx = Regexp.new(self.validation_conditions.map{|vc| ["a","o"].include?(vc.rule_key) ? "#{vc.rule_key}(?!nd|r)" : vc.rule_key}.join("|")) # exclude and, or
27 | # logger.debug "v: #{self.inspect}"
28 | # logger.debug "rule: #{self.rule.inspect}"
29 | # logger.debug "rexp: #{rgx.inspect}"
30 | # logger.debug "keyp: #{ch.inspect}"
31 | # logger.debug "subd: #{self.rule.gsub(rgx){|m| ch[m.to_sym]}}"
32 | eval(self.rule.gsub(rgx){|m| ch[m.to_sym]})
33 | end
34 |
35 | # A hash of the conditions (keyed by rule_key) and their evaluation (boolean) in the context of response_set
36 | def conditions_hash(response_set)
37 | hash = {}
38 | response = response_set.responses.detect{|r| r.answer_id.to_i == self.answer_id.to_i}
39 | # logger.debug "r: #{response.inspect}"
40 | self.validation_conditions.each{|vc| hash.merge!(vc.to_hash(response))}
41 | return hash
42 | end
43 | end
44 | end
45 | end
--------------------------------------------------------------------------------
/lib/surveyor/models/validation_condition_methods.rb:
--------------------------------------------------------------------------------
1 | module Surveyor
2 | module Models
3 | module ValidationConditionMethods
4 | def self.included(base)
5 | # Associations
6 | base.send :belongs_to, :validation
7 |
8 | # Scopes
9 |
10 | @@validations_already_included ||= nil
11 | unless @@validations_already_included
12 | # Validations
13 | base.send :validates_presence_of, :operator, :rule_key
14 | base.send :validates_inclusion_of, :operator, :in => Surveyor::Common::OPERATORS
15 | base.send :validates_uniqueness_of, :rule_key, :scope => :validation_id
16 | # this causes issues with building and saving
17 | # base.send :validates_numericality_of, :validation_id #, :question_id, :answer_id
18 |
19 | @@validations_already_included = true
20 | end
21 |
22 | base.send :include, Surveyor::ActsAsResponse # includes "as" instance method
23 |
24 | # Class methods
25 | base.instance_eval do
26 | def operators
27 | Surveyor::Common::OPERATORS
28 | end
29 | end
30 | end
31 |
32 | # Instance Methods
33 | def to_hash(response)
34 | {rule_key.to_sym => (response.nil? ? false : self.is_valid?(response))}
35 | end
36 |
37 | def is_valid?(response)
38 | klass = response.answer.response_class
39 | compare_to = Response.find_by_question_id_and_answer_id(self.question_id, self.answer_id) || self
40 | case self.operator
41 | when "==", "<", ">", "<=", ">="
42 | response.as(klass).send(self.operator, compare_to.as(klass))
43 | when "!="
44 | !(response.as(klass) == compare_to.as(klass))
45 | when "=~"
46 | return false if compare_to != self
47 | !(response.as(klass).to_s =~ Regexp.new(self.regexp || "")).nil?
48 | else
49 | false
50 | end
51 | end
52 | end
53 | end
54 | end
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | # This file is copied to ~/spec when you run 'ruby script/generate rspec'
2 | # from the project root directory.
3 | ENV["RAILS_ENV"] ||= 'test'
4 | require File.expand_path(File.join(File.dirname(__FILE__),'..','testbed','config','environment'))
5 | require 'spec/autorun'
6 | require 'spec/rails'
7 | require 'factories'
8 |
9 | # Uncomment the next line to use webrat's matchers
10 | #require 'webrat/integrations/rspec-rails'
11 |
12 | # Requires supporting files with custom matchers and macros, etc,
13 | # in ./support/ and its subdirectories.
14 | Dir[File.expand_path(File.join(File.dirname(__FILE__),'support','**','*.rb'))].each {|f| require f}
15 |
16 | Spec::Runner.configure do |config|
17 | # If you're not using ActiveRecord you should remove these
18 | # lines, delete config/database.yml and disable :active_record
19 | # in your config/boot.rb
20 | config.use_transactional_fixtures = true
21 | config.use_instantiated_fixtures = false
22 | config.fixture_path = RAILS_ROOT + '/spec/fixtures/'
23 |
24 | # == Fixtures
25 | #
26 | # You can declare fixtures for each example_group like this:
27 | # describe "...." do
28 | # fixtures :table_a, :table_b
29 | #
30 | # Alternatively, if you prefer to declare them only once, you can
31 | # do so right here. Just uncomment the next line and replace the fixture
32 | # names with your fixtures.
33 | #
34 | # config.global_fixtures = :table_a, :table_b
35 | #
36 | # If you declare global fixtures, be aware that they will be declared
37 | # for all of your examples, even those that don't use them.
38 | #
39 | # You can also declare which fixtures to use (for example fixtures for test/fixtures):
40 | #
41 | # config.fixture_path = RAILS_ROOT + '/spec/fixtures/'
42 | #
43 | # == Mock Framework
44 | #
45 | # RSpec uses its own mocking framework by default. If you prefer to
46 | # use mocha, flexmock or RR, uncomment the appropriate line:
47 | #
48 | # config.mock_with :mocha
49 | # config.mock_with :flexmock
50 | # config.mock_with :rr
51 | #
52 | # == Notes
53 | #
54 | # For more information take a look at Spec::Runner::Configuration and Spec::Runner
55 | end
56 |
--------------------------------------------------------------------------------
/lib/surveyor/models/dependency_methods.rb:
--------------------------------------------------------------------------------
1 | module Surveyor
2 | module Models
3 | module DependencyMethods
4 | def self.included(base)
5 | # Associations
6 | base.send :belongs_to, :question
7 | base.send :belongs_to, :question_group
8 | base.send :has_many, :dependency_conditions, :dependent => :destroy
9 |
10 | @@validations_already_included ||= nil
11 | unless @@validations_already_included
12 | # Validations
13 | base.send :validates_presence_of, :rule
14 | base.send :validates_format_of, :rule, :with => /^(?:and|or|\)|\(|[A-Z]|\s)+$/ #TODO properly formed parenthesis etc.
15 | base.send :validates_numericality_of, :question_id, :if => Proc.new { |d| d.question_group_id.nil? }
16 | base.send :validates_numericality_of, :question_group_id, :if => Proc.new { |d| d.question_id.nil? }
17 |
18 | @@validations_already_included = true
19 | end
20 |
21 | # Attribute aliases
22 | base.send :alias_attribute, :dependent_question_id, :question_id
23 | end
24 |
25 | # Instance Methods
26 | def question_group_id=(i)
27 | write_attribute(:question_id, nil) unless i.nil?
28 | write_attribute(:question_group_id, i)
29 | end
30 |
31 | def question_id=(i)
32 | write_attribute(:question_group_id, nil) unless i.nil?
33 | write_attribute(:question_id, i)
34 | end
35 |
36 | # Has this dependency has been met in the context of response_set?
37 | # Substitutes the conditions hash into the rule and evaluates it
38 | def is_met?(response_set)
39 | ch = conditions_hash(response_set)
40 | return false if ch.blank?
41 | # logger.debug "rule: #{self.rule.inspect}"
42 | # logger.debug "rexp: #{rgx.inspect}"
43 | # logger.debug "keyp: #{ch.inspect}"
44 | # logger.debug "subd: #{self.rule.gsub(rgx){|m| ch[m.to_sym]}}"
45 | rgx = Regexp.new(self.dependency_conditions.map{|dc| ["a","o"].include?(dc.rule_key) ? "#{dc.rule_key}(?!nd|r)" : dc.rule_key}.join("|")) # exclude and, or
46 | eval(self.rule.gsub(rgx){|m| ch[m.to_sym]})
47 | end
48 |
49 | # A hash of the conditions (keyed by rule_key) and their evaluation (boolean) in the context of response_set
50 | def conditions_hash(response_set)
51 | hash = {}
52 | self.dependency_conditions.each{|dc| hash.merge!(dc.to_hash(response_set))}
53 | return hash
54 | end
55 | end
56 | end
57 | end
--------------------------------------------------------------------------------
/spec/models/validation_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2 |
3 | describe Validation do
4 | before(:each) do
5 | @validation = Factory(:validation)
6 | end
7 |
8 | it "should be valid" do
9 | @validation.should be_valid
10 | end
11 |
12 | it "should be invalid without a rule" do
13 | @validation.rule = nil
14 | @validation.should have(2).errors_on(:rule)
15 | @validation.rule = " "
16 | @validation.should have(1).errors_on(:rule)
17 | end
18 |
19 | # this causes issues with building and saving
20 | # it "should be invalid without a answer_id" do
21 | # @validation.answer_id = nil
22 | # @validation.should have(1).error_on(:answer_id)
23 | # end
24 |
25 | it "should be invalid unless rule composed of only references and operators" do
26 | @validation.rule = "foo"
27 | @validation.should have(1).error_on(:rule)
28 | @validation.rule = "1 to 2"
29 | @validation.should have(1).error_on(:rule)
30 | @validation.rule = "a and b"
31 | @validation.should have(1).error_on(:rule)
32 | end
33 | end
34 | describe Validation, "reporting its status" do
35 | def test_var(vhash, vchashes, ahash, rhash)
36 | a = Factory(:answer, ahash)
37 | v = Factory(:validation, {:answer => a, :rule => "A"}.merge(vhash))
38 | vchashes.each do |vchash|
39 | Factory(:validation_condition, {:validation => v, :rule_key => "A"}.merge(vchash))
40 | end
41 | rs = Factory(:response_set)
42 | r = Factory(:response, {:answer => a, :question => a.question}.merge(rhash))
43 | rs.responses << r
44 | return v.is_valid?(rs)
45 | end
46 |
47 | it "should validate a response by integer comparison" do
48 | test_var({:rule => "A and B"}, [{:operator => ">=", :integer_value => 0}, {:rule_key => "B", :operator => "<=", :integer_value => 120}], {:response_class => "integer"}, {:integer_value => 48}).should be_true
49 | end
50 | it "should validate a response by regexp" do
51 | test_var({}, [{:operator => "=~", :regexp => /^[a-z]{1,6}$/}], {:response_class => "string"}, {:string_value => ""}).should be_false
52 | end
53 | end
54 | describe Validation, "with conditions" do
55 | it "should destroy conditions when destroyed" do
56 | @validation = Factory(:validation)
57 | Factory(:validation_condition, :validation => @validation, :rule_key => "A")
58 | Factory(:validation_condition, :validation => @validation, :rule_key => "B")
59 | Factory(:validation_condition, :validation => @validation, :rule_key => "C")
60 | v_ids = @validation.validation_conditions.map(&:id)
61 | @validation.destroy
62 | v_ids.each{|id| DependencyCondition.find_by_id(id).should == nil}
63 | end
64 | end
--------------------------------------------------------------------------------
/spec/helpers/surveyor_helper_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2 |
3 | describe SurveyorHelper do
4 | before(:each) do
5 |
6 | end
7 | it "should return the question text with number" do
8 | q1 = Factory(:question)
9 | q2 = Factory(:question, :display_type => "label")
10 | q3 = Factory(:question, :dependency => Factory(:dependency))
11 | q4 = Factory(:question, :display_type => "image", :text => "something.jpg")
12 | q5 = Factory(:question, :question_group => Factory(:question_group))
13 | helper.q_text(q1).should == "1) #{q1.text}"
14 | helper.q_text(q2).should == q2.text
15 | helper.q_text(q3).should == q3.text
16 | helper.q_text(q4).should == '
'
17 | helper.q_text(q5).should == q5.text
18 | end
19 | it "should return the group text with number" do
20 | g1 = Factory(:question_group)
21 | helper.q_text(g1).should == "1) #{g1.text}"
22 | end
23 | it "should find or create responses, with index" do
24 | q1 = Factory(:question, :answers => [a = Factory(:answer, :text => "different")])
25 | q2 = Factory(:question, :answers => [b = Factory(:answer, :text => "strokes")])
26 | q3 = Factory(:question, :answers => [c = Factory(:answer, :text => "folks")])
27 | rs = Factory(:response_set, :responses => [r1 = Factory(:response, :question => q1, :answer => a), r3 = Factory(:response, :question => q3, :answer => c, :response_group => 1)])
28 |
29 | helper.response_for(rs, nil).should == nil
30 | helper.response_for(nil, q1).should == nil
31 | helper.response_for(rs, q1).should == r1
32 | helper.response_for(rs, q1, a).should == r1
33 | helper.response_for(rs, q2).attributes.should == Response.new(:question => q2, :response_set => rs).attributes
34 | helper.response_for(rs, q2, b).attributes.should == Response.new(:question => q2, :response_set => rs).attributes
35 | helper.response_for(rs, q3, c, "1").should == r3
36 |
37 | end
38 | it "should keep an index of responses" do
39 | helper.response_idx.should == "1"
40 | helper.response_idx.should == "2"
41 | helper.response_idx(false).should == "2"
42 | helper.response_idx.should == "3"
43 | end
44 | it "should translate response class into attribute" do
45 | helper.rc_to_attr(:string).should == :string_value
46 | helper.rc_to_attr(:text).should == :text_value
47 | helper.rc_to_attr(:integer).should == :integer_value
48 | helper.rc_to_attr(:float).should == :float_value
49 | helper.rc_to_attr(:datetime).should == :datetime_value
50 | helper.rc_to_attr(:date).should == :datetime_value
51 | helper.rc_to_attr(:time).should == :datetime_value
52 | end
53 | end
--------------------------------------------------------------------------------
/spec/models/response_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2 |
3 | describe Response, "when saving a response" do
4 | before(:each) do
5 | # @response = Response.new(:question_id => 314, :response_set_id => 159, :answer_id => 1)
6 | @response = Factory(:response, :question => Factory(:question), :answer => Factory(:answer))
7 | end
8 |
9 | it "should be valid" do
10 | @response.should be_valid
11 | end
12 |
13 | it "should be invalid without a parent response set and question" do
14 | @response.response_set_id = nil
15 | @response.should have(1).error_on(:response_set_id)
16 |
17 | @response.question_id = nil
18 | @response.should have(1).error_on(:question_id)
19 | end
20 |
21 | it "should be correct if the question has no correct_answer_id" do
22 | @response.question.correct_answer_id.should be_nil
23 | @response.correct?.should be_true
24 | end
25 | it "should be correct if the answer's response class != answer" do
26 | @response.answer.response_class.should_not == "answer"
27 | @response.correct?.should be_true
28 | end
29 | it "should be (in)correct if answer_id is (not) equal to question's correct_answer_id" do
30 | @answer = Factory(:answer, :response_class => "answer")
31 | @question = Factory(:question, :correct_answer_id => @answer.id)
32 | @response = Factory(:response, :question => @question, :answer => @answer)
33 | @response.correct?.should be_true
34 | @response.answer_id = 143
35 | @response.correct?.should be_false
36 | end
37 | describe "returns the response as the type requested" do
38 |
39 | it "returns 'string'" do
40 | @response.string_value = "blah"
41 | @response.as("string").should == "blah"
42 | @response.as(:string).should == "blah"
43 | end
44 |
45 | it "returns 'integer'" do
46 | @response.integer_value = 1001
47 | @response.as(:integer).should == 1001
48 | end
49 |
50 | it "returns 'float'" do
51 | @response.float_value = 3.14
52 | @response.as(:float).should == 3.14
53 | end
54 |
55 | it "returns 'answer'" do
56 | @response.answer_id = 14
57 | @response.as(:answer).should == 14
58 | end
59 |
60 | it "default returns answer type if not specified" do
61 | @response.answer_id =18
62 | @response.as(:stuff).should == 18
63 | end
64 |
65 | it "returns empty elements if the response is cast as a type that is not present" do
66 | resp = Response.new(:question_id => 314, :response_set_id => 156)
67 | resp.as(:string).should == nil
68 | resp.as(:integer).should == nil
69 | resp.as(:float).should == nil
70 | resp.as(:answer).should == nil
71 | resp.as(:stuff).should == nil
72 | end
73 |
74 | end
75 |
76 | end
77 |
--------------------------------------------------------------------------------
/lib/surveyor/models/survey_methods.rb:
--------------------------------------------------------------------------------
1 | module Surveyor
2 | module Models
3 | module SurveyMethods
4 | def self.included(base)
5 | # Associations
6 | base.send :has_many, :sections, :class_name => "SurveySection", :order => 'display_order', :dependent => :destroy
7 | base.send :has_many, :sections_with_questions, :include => :questions, :class_name => "SurveySection", :order => 'display_order'
8 | base.send :has_many, :response_sets
9 |
10 | # Scopes
11 | base.send :named_scope, :with_sections, {:include => :sections}
12 |
13 | @@validations_already_included ||= nil
14 | unless @@validations_already_included
15 | # Validations
16 | base.send :validates_presence_of, :title
17 | base.send :validates_uniqueness_of, :access_code
18 |
19 | @@validations_already_included = true
20 | end
21 |
22 | # Class methods
23 | base.instance_eval do
24 | def to_normalized_string(value)
25 | # replace non-alphanumeric with "-". remove repeat "-"s. don't start or end with "-"
26 | value.to_s.downcase.gsub(/[^a-z0-9]/,"-").gsub(/-+/,"-").gsub(/-$|^-/,"")
27 | end
28 | end
29 | end
30 |
31 | # Instance methods
32 | def initialize(*args)
33 | super(*args)
34 | default_args
35 | end
36 |
37 | def default_args
38 | self.inactive_at ||= DateTime.now
39 | end
40 |
41 | def title=(value)
42 | adjusted_value = value
43 | while Survey.find_by_access_code(Survey.to_normalized_string(adjusted_value))
44 | i ||= 0
45 | i += 1
46 | adjusted_value = "#{value} #{i.to_s}"
47 | end
48 | self.access_code = Survey.to_normalized_string(adjusted_value)
49 | super(adjusted_value)
50 | # self.access_code = Survey.to_normalized_string(value)
51 | # super
52 | end
53 |
54 | def active?
55 | self.active_as_of?(DateTime.now)
56 | end
57 | def active_as_of?(datetime)
58 | (self.active_at.nil? or self.active_at < datetime) and (self.inactive_at.nil? or self.inactive_at > datetime)
59 | end
60 | def activate!
61 | self.active_at = DateTime.now
62 | end
63 | def deactivate!
64 | self.inactive_at = DateTime.now
65 | end
66 | def active_at=(datetime)
67 | self.inactive_at = nil if !datetime.nil? and !self.inactive_at.nil? and self.inactive_at < datetime
68 | super(datetime)
69 | end
70 | def inactive_at=(datetime)
71 | self.active_at = nil if !datetime.nil? and !self.active_at.nil? and self.active_at > datetime
72 | super(datetime)
73 | end
74 | end
75 | end
76 | end
--------------------------------------------------------------------------------
/features/support/env.rb:
--------------------------------------------------------------------------------
1 | # IMPORTANT: This file is generated by cucumber-rails - edit at your own peril.
2 | # It is recommended to regenerate this file in the future when you upgrade to a
3 | # newer version of cucumber-rails. Consider adding your own code to a new file
4 | # instead of editing this one. Cucumber will automatically load all features/**/*.rb
5 | # files.
6 |
7 | ENV["RAILS_ENV"] ||= "cucumber"
8 | require File.expand_path(File.dirname(__FILE__) + '/../../testbed/config/environment')
9 |
10 | require 'cucumber/formatter/unicode' # Remove this line if you don't want Cucumber Unicode support
11 | require 'cucumber/rails/world'
12 | require 'cucumber/rails/active_record'
13 | require 'cucumber/web/tableish'
14 |
15 | require 'webrat'
16 | require 'webrat/core/matchers'
17 |
18 | Webrat.configure do |config|
19 | config.mode = :rails
20 | config.open_error_files = false # Set to true if you want error pages to pop up in the browser
21 | end
22 |
23 |
24 | # If you set this to false, any error raised from within your app will bubble
25 | # up to your step definition and out to cucumber unless you catch it somewhere
26 | # on the way. You can make Rails rescue errors and render error pages on a
27 | # per-scenario basis by tagging a scenario or feature with the @allow-rescue tag.
28 | #
29 | # If you set this to true, Rails will rescue all errors and render error
30 | # pages, more or less in the same way your application would behave in the
31 | # default production environment. It's not recommended to do this for all
32 | # of your scenarios, as this makes it hard to discover errors in your application.
33 | ActionController::Base.allow_rescue = false
34 |
35 | # If you set this to true, each scenario will run in a database transaction.
36 | # You can still turn off transactions on a per-scenario basis, simply tagging
37 | # a feature or scenario with the @no-txn tag. If you are using Capybara,
38 | # tagging with @culerity or @javascript will also turn transactions off.
39 | #
40 | # If you set this to false, transactions will be off for all scenarios,
41 | # regardless of whether you use @no-txn or not.
42 | #
43 | # Beware that turning transactions off will leave data in your database
44 | # after each scenario, which can lead to hard-to-debug failures in
45 | # subsequent scenarios. If you do this, we recommend you create a Before
46 | # block that will explicitly put your database in a known state.
47 | Cucumber::Rails::World.use_transactional_fixtures = true
48 | # How to clean your database when transactions are turned off. See
49 | # http://github.com/bmabey/database_cleaner for more info.
50 | if defined?(ActiveRecord::Base)
51 | begin
52 | require 'database_cleaner'
53 | DatabaseCleaner.strategy = :truncation
54 | rescue LoadError => ignore_if_database_cleaner_not_present
55 | end
56 | end
57 |
--------------------------------------------------------------------------------
/lib/surveyor/models/question_methods.rb:
--------------------------------------------------------------------------------
1 | module Surveyor
2 | module Models
3 | module QuestionMethods
4 | def self.included(base)
5 | # Associations
6 | base.send :belongs_to, :survey_section
7 | base.send :belongs_to, :question_group, :dependent => :destroy
8 | base.send :has_many, :answers, :order => "display_order ASC", :dependent => :destroy # it might not always have answers
9 | base.send :has_one, :dependency, :dependent => :destroy
10 | base.send :has_one, :correct_answer, :class_name => "Answer", :dependent => :destroy
11 |
12 | # Scopes
13 | base.send :default_scope, :order => "display_order ASC"
14 |
15 | @@validations_already_included ||= nil
16 | unless @@validations_already_included
17 | # Validations
18 | base.send :validates_presence_of, :text, :display_order
19 | # this causes issues with building and saving
20 | #, :survey_section_id
21 | base.send :validates_inclusion_of, :is_mandatory, :in => [true, false]
22 |
23 | @@validations_already_included = true
24 | end
25 | end
26 |
27 | # Instance Methods
28 | def initialize(*args)
29 | super(*args)
30 | default_args
31 | end
32 |
33 | def default_args
34 | self.is_mandatory ||= true
35 | self.display_type ||= "default"
36 | self.pick ||= "none"
37 | self.display_order ||= self.survey_section ? self.survey_section.questions.count : 0
38 | self.data_export_identifier ||= Surveyor::Common.normalize(text)
39 | self.short_text ||= text
40 | end
41 |
42 | def pick=(val)
43 | write_attribute(:pick, val.nil? ? nil : val.to_s)
44 | end
45 | def display_type=(val)
46 | write_attribute(:display_type, val.nil? ? nil : val.to_s)
47 | end
48 |
49 | def mandatory?
50 | self.is_mandatory == true
51 | end
52 |
53 | def dependent?
54 | self.dependency != nil
55 | end
56 | def triggered?(response_set)
57 | dependent? ? self.dependency.is_met?(response_set) : true
58 | end
59 | def css_class(response_set)
60 | [(dependent? ? "q_dependent" : nil), (triggered?(response_set) ? nil : "q_hidden"), custom_class].compact.join(" ")
61 | end
62 |
63 | def part_of_group?
64 | !self.question_group.nil?
65 | end
66 | def solo?
67 | self.question_group.nil?
68 | end
69 |
70 | def split_text(part = nil)
71 | (part == :pre ? text.split("|",2)[0] : (part == :post ? text.split("|",2)[1] : text)).to_s
72 | end
73 |
74 | def renderer(g = question_group)
75 | r = [g ? g.renderer.to_s : nil, display_type].compact.join("_")
76 | r.blank? ? :default : r.to_sym
77 | end
78 | end
79 | end
80 | end
--------------------------------------------------------------------------------
/features/step_definitions/parser_steps.rb:
--------------------------------------------------------------------------------
1 | Given /^I parse$|^the survey$/ do |string|
2 | Surveyor::Parser.parse(string)
3 | end
4 |
5 | Given /^I parse redcap file "([^"]*)"$/ do |name|
6 | Surveyor::RedcapParser.parse File.read(File.join(RAILS_ROOT, '..', 'features', 'support', name)), name
7 | end
8 |
9 | Then /^there should be (\d+) survey(?:s?) with:$/ do |x, table|
10 | Survey.count.should == x.to_i
11 | table.hashes.each do |hash|
12 | Survey.find(:first, :conditions => hash).should_not be_nil
13 | end
14 | end
15 |
16 | Then /^there should be (\d+) question groups with:$/ do |x, table|
17 | QuestionGroup.count.should == x.to_i
18 | table.hashes.each do |hash|
19 | QuestionGroup.find(:first, :conditions => hash).should_not be_nil
20 | end
21 | end
22 |
23 | Then /^there should be (\d+) question(?:s?) with:$/ do |x, table|
24 | Question.count.should == x.to_i
25 | table.hashes.each do |hash|
26 | hash["reference_identifier"] = nil if hash["reference_identifier"] == "nil"
27 | hash["custom_class"] = nil if hash["custom_class"] == "nil"
28 | Question.find(:first, :conditions => hash).should_not be_nil
29 | end
30 | end
31 |
32 | Then /^there should be (\d+) answer(?:s?) with:$/ do |x, table|
33 | Answer.count.should == x.to_i
34 | table.hashes.each do |hash|
35 | hash["reference_identifier"] = nil if hash["reference_identifier"] == "nil"
36 | Answer.find(:first, :conditions => hash).should_not be_nil
37 | end
38 | end
39 |
40 | Then /^there should be (\d+) dependenc(?:y|ies) with:$/ do |x, table|
41 | Dependency.count.should == x.to_i
42 | table.hashes.each do |hash|
43 | Dependency.find(:first, :conditions => hash).should_not be_nil
44 | end
45 | end
46 |
47 | Then /^there should be (\d+) resolved dependency_condition(?:s?) with:$/ do |x, table|
48 | DependencyCondition.count.should == x.to_i
49 | table.hashes.each do |hash|
50 | d = DependencyCondition.find(:first, :conditions => hash)
51 | d.should_not be_nil
52 | d.question.should_not be_nil
53 | d.answer.should_not be_nil unless d.operator.match(/^count[<>=!]{1,2}\d+/)
54 | end
55 | end
56 |
57 |
58 | Then /^there should be (\d+) validation(?:s?) with:$/ do |x, table|
59 | Validation.count.should == x.to_i
60 | table.hashes.each do |hash|
61 | Validation.find(:first, :conditions => hash).should_not be_nil
62 | end
63 | end
64 |
65 | Then /^there should be (\d+) validation_condition(?:s?) with:$/ do |x, table|
66 | ValidationCondition.count.should == x.to_i
67 | table.hashes.each do |hash|
68 | hash["integer_value"] = nil if hash["integer_value"] == "nil"
69 | ValidationCondition.find(:first, :conditions => hash).should_not be_nil
70 | end
71 | end
72 |
73 | Then /^question "([^"]*)" should have correct answer "([^"]*)"$/ do |qr, ar|
74 | (q = Question.find_by_reference_identifier(qr)).should_not be_nil
75 | q.correct_answer.should == q.answers.find_by_reference_identifier(ar)
76 | end
77 |
78 |
--------------------------------------------------------------------------------
/generators/surveyor/templates/assets/javascripts/jquery.surveyor.js:
--------------------------------------------------------------------------------
1 | // Javascript UI for surveyor
2 | jQuery(document).ready(function(){
3 | // if(jQuery.browser.msie){
4 | // // IE has trouble with the change event for form radio/checkbox elements - bind click instead
5 | // jQuery("form#survey_form input[type=radio], form#survey_form [type=checkbox]").bind("click", function(){
6 | // jQuery(this).parents("form").ajaxSubmit({dataType: 'json', success: successfulSave});
7 | // });
8 | // // IE fires the change event for all other (not radio/checkbox) elements of the form
9 | // jQuery("form#survey_form *").not("input[type=radio], input[type=checkbox]").bind("change", function(){
10 | // jQuery(this).parents("form").ajaxSubmit({dataType: 'json', success: successfulSave});
11 | // });
12 | // }else{
13 | // // Other browsers just use the change event on the form
14 | jQuery("form#survey_form input, form#survey_form select, form#survey_form textarea").change(function(){
15 | question_data = $(this).parents('fieldset[id^="q_"]').find("input, select, textarea").add($("form#survey_form input[name='authenticity_token']")).serialize();
16 | // console.log(unescape(question_data));
17 | $.ajax({ type: "PUT", url: $(this).parents('form#survey_form').attr("action"), data: question_data, dataType: 'json', success: successfulSave })
18 | });
19 | // }
20 |
21 | // If javascript works, we don't need to show dependents from previous sections at the top of the page.
22 | jQuery("#dependents").remove();
23 |
24 | function successfulSave(responseText){ // for(key in responseText) { console.log("key is "+[key]+", value is "+responseText[key]); }
25 | // surveyor_controller returns a json object to show/hide elements and insert/remove ids e.g. {"ids": {"2" => 234}, "remove": {"4" => 21}, "hide":["question_12","question_13"],"show":["question_14"]}
26 | jQuery.each(responseText.show, function(){ jQuery('#' + this).show("fast"); });
27 | jQuery.each(responseText.hide, function(){ jQuery('#' + this).hide("fast"); });
28 | jQuery.each(responseText.ids, function(k,v){ jQuery('#r_'+k+'_question_id').after(' :question_id, :class_name => :question
9 | base.send :belongs_to, :question
10 |
11 | @@validations_already_included ||= nil
12 | unless @@validations_already_included
13 | # Validations
14 | base.send :validates_presence_of, :operator, :rule_key
15 | base.send :validate, :validates_operator
16 | base.send :validates_uniqueness_of, :rule_key, :scope => :dependency_id
17 | # this causes issues with building and saving
18 | # base.send :validates_numericality_of, :question_id, :dependency_id
19 |
20 | @@validations_already_included = true
21 | end
22 |
23 | base.send :include, Surveyor::ActsAsResponse # includes "as" instance method
24 |
25 | # Class methods
26 | base.instance_eval do
27 | def operators
28 | Surveyor::Common::OPERATORS
29 | end
30 | end
31 | end
32 |
33 | # Instance methods
34 | def to_hash(response_set)
35 | # all responses to associated question
36 | responses = response_set.responses.select do |r|
37 | question && question.answers.include?(r.answer)
38 | end
39 | {rule_key.to_sym => (!responses.empty? and self.is_met?(responses))}
40 | end
41 |
42 | # Checks to see if the responses passed in meet the dependency condition
43 | def is_met?(responses)
44 | # response to associated answer if available, or first response
45 | response = if self.answer_id
46 | responses.detect do |r|
47 | r.answer == self.answer
48 | end
49 | end || responses.first
50 | klass = response.answer.response_class
51 | return case self.operator
52 | when "==", "<", ">", "<=", ">="
53 | response.as(klass).send(self.operator, self.as(klass))
54 | when "!="
55 | !(response.as(klass) == self.as(klass))
56 | when /^count[<>=]{1,2}\d+$/
57 | op, i = self.operator.scan(/^count([<>!=]{1,2})(\d+)$/).flatten
58 | responses.count.send(op, i.to_i)
59 | when /^count!=\d+$/
60 | !(responses.count == self.operator.scan(/\d+/).first.to_i)
61 | else
62 | false
63 | end
64 | end
65 |
66 | protected
67 |
68 | def validates_operator
69 | errors.add(:operator, "Invalid operator") unless
70 | Surveyor::Common::OPERATORS.include?(self.operator) ||
71 | self.operator && self.operator.match(/^count(<|>|==|>=|<=|!=)(\d+)/)
72 | end
73 | end
74 | end
75 | end
76 |
--------------------------------------------------------------------------------
/init_testbed.rakefile:
--------------------------------------------------------------------------------
1 | desc "Set up a rails app for testing in the spec dir"
2 | task :default => [:"testbed:generate", :"testbed:setup", :"testbed:migrate"]
3 |
4 | namespace "testbed" do
5 | # "testbed" is also hardcoded in the spec/spec_helper.rb features/support/env.rb and gitignore file. Change it there too...
6 |
7 | "Generate rails, rspec, cucumber"
8 | task :generate do
9 | chdir("testbed") do
10 | sh "bundle install"
11 | sh "bundle exec rails ."
12 | sh "bundle exec script/generate rspec"
13 | sh "bundle exec script/generate cucumber --webrat"
14 | sh "rm -rf spec features"
15 | end
16 | end
17 |
18 | desc "Setup bundler, rspec, cucumber"
19 | task :setup do
20 | chdir("testbed") do
21 | # Bundler
22 | preinitializer_rb = "begin\n require \"rubygems\"\n require \"bundler\"\nrescue LoadError\n raise \"Could not load the bundler gem. Install it with `gem install bundler`.\"\nend\n\nif Gem::Version.new(Bundler::VERSION) <= Gem::Version.new(\"0.9.24\")\n raise RuntimeError, \"Your bundler version is too old for Rails 2.3.\" +\n \"Run `gem install bundler` to upgrade.\"\nend\n\nbegin\n # Set up load paths for all bundled gems\n ENV[\"BUNDLE_GEMFILE\"] = File.expand_path(\"../../Gemfile\", __FILE__)\n Bundler.setup\nrescue Bundler::GemNotFound\n raise RuntimeError, \"Bundler couldn't find some gems.\" +\n \"Did you run `bundle install`?\"\nend\n"
23 | File.open('config/preinitializer.rb', 'w'){|f| f.write preinitializer_rb}
24 |
25 | boot_rb = File.read('config/boot.rb').sub("# All that for this:", "class Rails::Boot\n def run\n load_initializer\n\n Rails::Initializer.class_eval do\n def load_gems\n @bundler_loaded ||= Bundler.require :default, Rails.env\n end\n end\n\n Rails::Initializer.run(:set_load_path)\n end\nend\n\n# All that for this:")
26 | File.open('config/boot.rb', 'w'){|f| f.write boot_rb}
27 | puts "NOTE: These files were created/modified as described here: http://gembundler.com/rails23.html"
28 |
29 | # Rspec
30 | rspec_rake = File.read('lib/tasks/rspec.rake').gsub('#{RAILS_ROOT}/spec/', '#{RAILS_ROOT}/../spec/').gsub("FileList['spec/", "FileList['../spec/").gsub("File.exist?('spec/","File.exist?('../spec/")
31 | File.open('lib/tasks/rspec.rake', 'w'){|f| f.write rspec_rake}
32 |
33 | # Cucumber
34 | cucumber_rake = File.read('lib/tasks/cucumber.rake').sub("begin", "ENV['FEATURE'] ||= '../features'\n\nbegin")
35 | File.open('lib/tasks/cucumber.rake', 'w'){|f| f.write cucumber_rake}
36 | end
37 | end
38 |
39 | desc "Generate, migrate testbed"
40 | task :migrate do
41 | sh "cp -R generators testbed/lib"
42 | chdir("testbed") do
43 | sh "bundle exec script/generate surveyor"
44 | sh "bundle exec rake db:migrate db:test:prepare"
45 | end
46 | end
47 |
48 | desc "Remove testbed app"
49 | task :remove do
50 | puts "Removing the test_app in the spec folder"
51 | chdir("testbed") do
52 | sh 'rm -rf Gemfile.lock README Rakefile app config db doc features lib log public script spec surveys test tmp vendor'
53 | end
54 | end
55 | end
--------------------------------------------------------------------------------
/generators/surveyor/templates/assets/stylesheets/dateinput.css:
--------------------------------------------------------------------------------
1 | /* For the details, see: http://flowplayer.org/tools/dateinput/index.html#skinning */
2 |
3 | /* the input field */
4 | /*
5 | .date {
6 | border:1px solid #ccc;
7 | font-size:18px;
8 | padding:4px;
9 | text-align:center;
10 | width:194px;
11 | -moz-box-shadow:0 0 10px #eee inset;
12 | }
13 | */
14 | /* calendar root element */
15 | #calroot {
16 | /* place on top of other elements. set a higher value if nessessary */
17 | z-index:9999;
18 |
19 | margin-top:-1px;
20 | width:198px;
21 | padding:2px;
22 | background-color:#fff;
23 | font-size:11px;
24 | border:1px solid #ccc;
25 | -moz-border-radius:5px;
26 | -webkit-border-radius:5px;
27 | -moz-box-shadow: 0 0 15px #666;
28 | -webkit-box-shadow: 0 0 15px #666;
29 | position: absolute;
30 | }
31 |
32 | /* head. contains title, prev/next month controls and possible month/year selectors */
33 | #calhead {
34 | padding:2px 0;
35 | height:22px;
36 | }
37 |
38 | #caltitle {
39 | font-size:14px;
40 | color:#0150D1;
41 | float:left;
42 | text-align:center;
43 | width:155px;
44 | line-height:20px;
45 | text-shadow:0 1px 0 #ddd;
46 | }
47 |
48 | #calnext, #calprev {
49 | display:block;
50 | width:20px;
51 | height:20px;
52 | background:transparent url('/images/surveyor/prev.gif') no-repeat scroll center center;
53 | float:left;
54 | cursor:pointer;
55 | }
56 |
57 | #calnext {
58 | background-image:url('/images/surveyor/next.gif');
59 | float:right;
60 | }
61 |
62 | #calprev.caldisabled, #calnext.caldisabled {
63 | visibility:hidden;
64 | }
65 |
66 | /* year/month selector */
67 | #caltitle select {
68 | font-size:10px;
69 | }
70 |
71 | /* names of the days */
72 | #caldays {
73 | height:14px;
74 | border-bottom:1px solid #ddd;
75 | }
76 |
77 | #caldays span {
78 | display:block;
79 | float:left;
80 | width:28px;
81 | text-align:center;
82 | }
83 |
84 | /* container for weeks */
85 | #calweeks {
86 | background-color:#fff;
87 | margin-top:4px;
88 | }
89 |
90 | /* single week */
91 | .calweek {
92 | clear:left;
93 | height:22px;
94 | }
95 |
96 | /* single day */
97 | .calweek a {
98 | display:block;
99 | float:left;
100 | width:27px;
101 | height:20px;
102 | text-decoration:none;
103 | font-size:11px;
104 | margin-left:1px;
105 | text-align:center;
106 | line-height:20px;
107 | color:#666;
108 | -moz-border-radius:3px;
109 | -webkit-border-radius:3px;
110 | }
111 |
112 | /* different states */
113 | .calweek a:hover, .calfocus {
114 | background-color:#ddd;
115 | }
116 |
117 | /* sunday */
118 | a.calsun {
119 | color:red;
120 | }
121 |
122 | /* offmonth day */
123 | a.caloff {
124 | color:#ccc;
125 | }
126 |
127 | a.caloff:hover {
128 | background-color:rgb(245, 245, 250);
129 | }
130 |
131 |
132 | /* unselecteble day */
133 | a.caldisabled {
134 | background-color:#efefef !important;
135 | color:#ccc !important;
136 | cursor:default;
137 | }
138 |
139 | /* current day */
140 | #calcurrent {
141 | background-color:#498CE2;
142 | color:#fff;
143 | }
144 |
145 | /* today */
146 | #caltoday {
147 | background-color:#333;
148 | color:#fff;
149 | }
150 |
--------------------------------------------------------------------------------
/features/surveyor.feature:
--------------------------------------------------------------------------------
1 | Feature: Survey creation
2 | As a survey participant
3 | I want to take a survey
4 | So that I can get paid
5 |
6 | Scenario: Basic questions
7 | Given the survey
8 | """
9 | survey "Favorites" do
10 | section "Colors" do
11 | label "You with the sad eyes don't be discouraged"
12 |
13 | question_1 "What is your favorite color?", :pick => :one
14 | answer "red"
15 | answer "blue"
16 | answer "green"
17 | answer :other
18 |
19 | q_2b "Choose the colors you don't like", :pick => :any
20 | a_1 "orange"
21 | a_2 "purple"
22 | a_3 "brown"
23 | a :omit
24 | end
25 | end
26 | """
27 | When I start the "Favorites" survey
28 | Then I should see "You with the sad eyes don't be discouraged"
29 | And I choose "red"
30 | And I choose "blue"
31 | And I check "orange"
32 | And I check "brown"
33 | And I press "Click here to finish"
34 | Then there should be 1 response set with 3 responses with:
35 | | answer |
36 | | blue |
37 | | orange |
38 | | brown |
39 |
40 | Scenario: Default answers
41 | Given the survey
42 | """
43 | survey "Favorites" do
44 | section "Foods" do
45 | question_1 "What is your favorite food?"
46 | answer "food", :string, :default_value => "beef"
47 | end
48 | section "Section 2" do
49 | end
50 | section "Section 3" do
51 | end
52 | end
53 | """
54 | When I start the "Favorites" survey
55 | And I press "Section 3"
56 | And I press "Click here to finish"
57 | Then there should be 1 response set with 1 responses with:
58 | | string_value |
59 | | beef |
60 |
61 | When I start the "Favorites" survey
62 | And I fill in "food" with "chicken"
63 | And I press "Foods"
64 | And I press "Section 3"
65 | And I press "Click here to finish"
66 | Then there should be 2 response set with 2 responses with:
67 | | string_value |
68 | | chicken |
69 |
70 | Scenario: Quiz time
71 | Given the survey
72 | """
73 | survey "Favorites" do
74 | section "Foods" do
75 | question_1 "What is the best meat?", :pick => :one, :correct => "oink"
76 | a_oink "bacon"
77 | a_tweet "chicken"
78 | a_moo "beef"
79 | end
80 | end
81 | """
82 | Then question "1" should have correct answer "oink"
83 |
84 | Scenario: Custom css class
85 | Given the survey
86 | """
87 | survey "Movies" do
88 | section "First" do
89 | q "What is your favorite movie?"
90 | a :string, :custom_class => "my_custom_class"
91 | q "What is your favorite state?"
92 | a :string
93 | end
94 | end
95 | """
96 | When I start the "Movies" survey
97 | Then the element "input[type='text']:first" should have the class "my_custom_class"
98 | # Then the element "input[type='text']:last" should not contain the class attribute
99 |
100 |
101 |
--------------------------------------------------------------------------------
/spec/models/survey_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2 |
3 | # Validations
4 | describe Survey, "when saving a new one" do
5 | before(:each) do
6 | @survey = Factory(:survey, :title => "Foo")
7 | end
8 |
9 | it "should be invalid without a title" do
10 | @survey.title = nil
11 | @survey.should have(1).error_on(:title)
12 | end
13 |
14 | it "should adjust the title to save unique titles" do
15 | original = Survey.new(:title => "Foo")
16 | original.save.should be_true
17 | imposter = Survey.new(:title => "Foo")
18 | imposter.save.should be_true
19 | imposter.title.should == "Foo 1"
20 | bandwagoneer = Survey.new(:title => "Foo")
21 | bandwagoneer.save.should be_true
22 | bandwagoneer.title.should == "Foo 2"
23 | end
24 | end
25 |
26 | # Associations
27 | describe Survey, "that has sections" do
28 | before(:each) do
29 | @survey = Factory(:survey, :title => "Foo")
30 | @s1 = Factory(:survey_section, :survey => @survey, :title => "wise", :display_order => 2)
31 | @s2 = Factory(:survey_section, :survey => @survey, :title => "er", :display_order => 3)
32 | @s3 = Factory(:survey_section, :survey => @survey, :title => "bud", :display_order => 1)
33 | @q1 = Factory(:question, :survey_section => @s1, :text => "what is wise?", :display_order => 2)
34 | @q2 = Factory(:question, :survey_section => @s2, :text => "what is er?", :display_order => 4)
35 | @q3 = Factory(:question, :survey_section => @s2, :text => "what is mill?", :display_order => 3)
36 | @q4 = Factory(:question, :survey_section => @s3, :text => "what is bud?", :display_order => 1)
37 | end
38 |
39 | it "should return survey_sections in display order" do
40 | @survey.sections.should have(3).sections
41 | @survey.sections.should == [@s3, @s1, @s2]
42 | end
43 |
44 | it "should return survey_sections_with_questions in display order" do
45 | @survey.sections_with_questions.map(&:questions).flatten.should have(4).questions
46 | @survey.sections_with_questions.map(&:questions).flatten.should == [@q4,@q1,@q3,@q2]
47 | end
48 | it "should delete survey sections when it is deleted" do
49 | section_ids = @survey.sections.map(&:id)
50 | @survey.destroy
51 | section_ids.each{|id| SurveySection.find_by_id(id).should be_nil}
52 | end
53 | end
54 |
55 | # Methods
56 | describe Survey do
57 | before(:each) do
58 | @survey = Survey.new
59 | end
60 |
61 | it "should be inactive by default" do
62 | @survey.active?.should == false
63 | end
64 |
65 | it "should be active or active as of a certain date/time" do
66 | @survey.inactive_at = 3.days.ago
67 | @survey.active_at = 2.days.ago
68 | @survey.active?.should be_true
69 | @survey.inactive_at.should be_nil
70 | end
71 |
72 | it "should be able to deactivate as of a certain date/time" do
73 | @survey.active_at = 2.days.ago
74 | @survey.inactive_at = 3.days.ago
75 | @survey.active?.should be_false
76 | @survey.active_at.should be_nil
77 | end
78 |
79 | it "should activate and deactivate" do
80 | @survey.activate!
81 | @survey.active?.should be_true
82 | @survey.deactivate!
83 | @survey.active?.should be_false
84 | end
85 |
86 | end
--------------------------------------------------------------------------------
/generators/surveyor/templates/assets/stylesheets/sass/surveyor.sass:
--------------------------------------------------------------------------------
1 | !background_color = #EEEEEE
2 | !surveyor_flash_background_color = #FFF1A8
3 | !surveyor_color = #FFFFF1
4 | !surveyor_text_color = #333333
5 | !surveyor_dependent_color = #FFFFDF
6 | !surveyor_menus_active_color = #EBEBCC
7 | !surveyor_menus_border_color = #ccc
8 |
9 | body
10 | :background-color= !background_color
11 | #surveyor
12 | :font-family "Century Gothic"
13 | :width 960px
14 | :padding 20px
15 | :margin 0 auto
16 | :text-align left
17 | :font-family Helvetica
18 | :font-size 100%
19 | :background-color= !surveyor_color
20 | :color= !surveyor_text_color
21 | .surveyor_flash
22 | :background-color= !surveyor_flash_background_color
23 | :margin 0 auto
24 | :padding 5px
25 | :width 300px
26 | :font-weight bold
27 | :text-align center
28 | .survey_title
29 | :font-size 2em
30 | :font-weight bold
31 | :padding 5px 0
32 | .surveyor_menu
33 | :float right
34 | ul
35 | :background-color= !background_color
36 | :border= 1px solid !surveyor_menus_border_color
37 | :border-bottom-width 0
38 | :list-style none
39 | li
40 | :border= 0 solid !surveyor_menus_border_color
41 | :border-bottom-width 1px
42 | li.active, li.active input[type="submit"]
43 | :background-color= !surveyor_menus_active_color
44 | .previous_section
45 | .next_section
46 | :float right
47 | .survey_section
48 | span.title
49 | :display block
50 | :padding 5px 0
51 | :font-weight bold
52 | :font-size 1.5em
53 |
54 | // question groups
55 | fieldset.g_inline fieldset
56 | :display inline
57 | fieldset.g_grid
58 | table
59 | :border-collapse collapse
60 | li.surveyor_radio label, li.surveyor_check_boxes label
61 | :visibility hidden
62 | input
63 | :visibility visible
64 | fieldset.g_repeater
65 | ol fieldset
66 | :display inline
67 | input[type="submit"].add_row
68 | :font-size 0.7em
69 |
70 | // question groups and questions
71 | .survey_section>fieldset>ol
72 | :padding-left 1em
73 | fieldset
74 | :padding-top 5px
75 | :margin-bottom 10px
76 | ol
77 | fieldset
78 | :vertical-align top
79 | li
80 | label, input
81 | :vertical-align top
82 | :padding 1px 0
83 | p.inline-hints
84 | :margin 0
85 | :padding 0
86 | :display inline
87 | li.inline
88 | :display inline
89 | li.datetime, li.time
90 | input, fieldset, fieldset ol li
91 | :display inline
92 | &.q_inline ol li
93 | :display inline
94 | input, textarea, select
95 | :margin 0 3px
96 | input[type="text"], textarea
97 | :font-size 0.8em
98 |
99 | fieldset.q_hidden
100 | :display none
101 | fieldset.q_dependent
102 | :background-color= !surveyor_dependent_color
103 | legend
104 | :background-color= !surveyor_dependent_color
105 | :padding 3px 3px 3px 0
106 |
107 |
108 |
109 | // buttons
110 | input[type="submit"]
111 | :font-size 1em
112 | :border 1px
113 | :padding 5px
114 | :cursor pointer
115 | :background-color= !background_color
116 |
--------------------------------------------------------------------------------
/generators/surveyor/surveyor_generator.rb:
--------------------------------------------------------------------------------
1 | class SurveyorGenerator < Rails::Generator::Base
2 | def manifest
3 | record do |m|
4 |
5 | m.directory "surveys"
6 |
7 | # Copy README to your app
8 | # m.file "../../../README.md", "surveys/README.md"
9 |
10 | # Gem plugin rake tasks
11 | m.file "tasks/surveyor.rb", "lib/tasks/surveyor.rb"
12 | if file_has_line(destination_path('Rakefile'), /^require 'tasks\/surveyor'$/ )
13 | logger.skipped 'Rakefile'
14 | else
15 | File.open(destination_path('Rakefile'), 'ab') {|file| file.write("\nrequire 'tasks/surveyor'\n") }
16 | # http://ggr.com/how-to-include-a-gems-rake-tasks-in-your-rails-app.html
17 | logger.appended 'Rakefile'
18 | end
19 |
20 | # Migrate
21 | # not using m.migration_template because all migration timestamps end up the same, causing a collision when running rake db:migrate
22 | # coped functionality from RAILS_GEM_PATH/lib/rails_generator/commands.rb
23 | m.directory "db/migrate"
24 | [ "create_surveys", "create_survey_sections", "create_questions", "create_question_groups", "create_answers",
25 | "create_response_sets", "create_responses",
26 | "create_dependencies", "create_dependency_conditions",
27 | "create_validations", "create_validation_conditions",
28 | "add_display_order_to_surveys", "add_correct_answer_id_to_questions",
29 | "add_index_to_response_sets", "add_index_to_surveys",
30 | "add_unique_indicies", "add_section_id_to_responses",
31 | "add_default_value_to_answers"].each_with_index do |model, i|
32 | unless (prev_migrations = Dir.glob("db/migrate/[0-9]*_*.rb").grep(/[0-9]+_#{model}.rb$/)).empty?
33 | prev_migration_timestamp = prev_migrations[0].match(/([0-9]+)_#{model}.rb$/)[1]
34 | end
35 | # raise "Another migration is already named #{model}" if not Dir.glob("db/migrate/[0-9]*_*.rb").grep(/[0-9]+_#{model}.rb$/).empty?
36 | m.template("migrate/#{model}.rb", "db/migrate/#{(prev_migration_timestamp || Time.now.utc.strftime("%Y%m%d%H%M%S").to_i + i).to_s}_#{model}.rb")
37 | end
38 |
39 | # Assets
40 | ["images", "javascripts", "stylesheets"].each do |asset_type|
41 | m.directory "public/#{asset_type}/surveyor"
42 | Dir.glob(File.join(File.dirname(__FILE__), "templates", "assets", asset_type, "*.*")).map{|path| File.basename(path)}.each do |filename|
43 | m.file "assets/#{asset_type}/#{filename}", "public/#{asset_type}/surveyor/#{filename}"
44 | end
45 | end
46 | m.directory "public/stylesheets/sass"
47 | m.file "assets/stylesheets/sass/surveyor.sass", "public/stylesheets/sass/surveyor.sass"
48 |
49 |
50 | # Locales
51 | m.directory "config/locales"
52 | Dir.glob(File.join(File.dirname(__FILE__), "templates", "locales", "*.yml")).map{|path| File.basename(path)}.each do |filename|
53 | m.file "locales/#{filename}", "config/locales/#{filename}"
54 | end
55 |
56 | # Surveys
57 | m.file "surveys/kitchen_sink_survey.rb", "surveys/kitchen_sink_survey.rb"
58 | m.file "surveys/quiz.rb", "surveys/quiz.rb"
59 |
60 | m.readme "README"
61 |
62 | end
63 | end
64 | def file_has_line(filename, rxp)
65 | File.readlines(filename).each{ |line| return true if line =~ rxp }
66 | false
67 | end
68 | end
69 |
--------------------------------------------------------------------------------
/lib/tasks/surveyor_tasks.rake:
--------------------------------------------------------------------------------
1 | desc "generate and load survey (specify FILE=surveys/your_survey.rb)"
2 | task :surveyor => :"surveyor:parse"
3 |
4 | namespace :surveyor do
5 | task :parse => :environment do
6 | raise "USAGE: file name required e.g. 'FILE=surveys/kitchen_sink_survey.rb'" if ENV["FILE"].blank?
7 | file = File.join(RAILS_ROOT, ENV["FILE"])
8 | raise "File does not exist: #{file}" unless FileTest.exists?(file)
9 | puts "--- Parsing #{file} ---"
10 | Surveyor::Parser.parse File.read(file)
11 | puts "--- Done #{file} ---"
12 | end
13 | desc "generate and load survey from REDCap Data Dictionary (specify FILE=surveys/redcap.csv)"
14 | task :redcap => :environment do
15 | raise "USAGE: file name required e.g. 'FILE=surveys/redcap_demo_survey.csv'" if ENV["FILE"].blank?
16 | file = File.join(RAILS_ROOT, ENV["FILE"])
17 | raise "File does not exist: #{file}" unless FileTest.exists?(file)
18 | puts "--- Parsing #{file} ---"
19 | Surveyor::RedcapParser.parse File.read(file), File.basename(file, ".csv")
20 | puts "--- Done #{file} ---"
21 | end
22 | desc "generate a surveyor DSL file from a survey"
23 | task :unparse => :environment do
24 | surveys = Survey.all
25 | if surveys
26 | puts "The following surveys are available"
27 | surveys.each do |survey|
28 | puts "#{survey.id} #{survey.title}"
29 | end
30 | print "Which survey would you like to unparse? "
31 | id = $stdin.gets.to_i
32 | if survey_to_unparse = surveys.detect{|s| s.id == id}
33 | filename = "surveys/#{survey_to_unparse.access_code}_#{Date.today.to_s(:db)}.rb"
34 | puts "unparsing #{survey_to_unparse.title} to #{filename}"
35 | File.open(filename, 'w') {|f| f.write(Surveyor::Unparser.unparse(survey_to_unparse))}
36 | else
37 | puts "not found"
38 | end
39 | else
40 | puts "There are no surveys available"
41 | end
42 | end
43 | desc "remove surveys (that don't have response sets)"
44 | task :remove => :environment do
45 | surveys = Survey.all.delete_if{|s| !s.response_sets.blank?}
46 | if surveys
47 | puts "The following surveys do not have any response sets"
48 | surveys.each do |survey|
49 | puts "#{survey.id} #{survey.title}"
50 | end
51 | print "Which survey would you like to remove? "
52 | id = $stdin.gets.to_i
53 | if survey_to_delete = surveys.detect{|s| s.id == id}
54 | puts "removing #{survey_to_delete.title}"
55 | survey_to_delete.destroy
56 | else
57 | put "not found"
58 | end
59 | else
60 | puts "There are no surveys without response sets"
61 | end
62 | end
63 | desc "dump all responses to a given survey"
64 | task :dump => :environment do
65 | require 'fileutils.rb'
66 | raise "USAGE: rake surveyor:dump SURVEY_ACCESS_CODE= [OUTPUT_DIR=]" unless ENV["SURVEY_ACCESS_CODE"]
67 | survey = Survey.find_by_access_code(ENV["SURVEY_ACCESS_CODE"])
68 | raise "No Survey found with code " + ENV["SURVEY_ACCESS_CODE"] unless survey
69 | dir = ENV["OUTPUT_DIR"] || Rails.root
70 | mkpath(dir) # Create all non-existent directories
71 | full_path = File.join(dir,"#{survey.access_code}_#{Time.now.to_i}.csv")
72 | File.open(full_path, 'w') do |f|
73 | survey.response_sets.each_with_index{|r,i| f.write(r.to_csv(true, i == 0)) } # print access code every time, print_header first time
74 | end
75 | end
76 | end
77 |
--------------------------------------------------------------------------------
/spec/models/question_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2 |
3 | describe Question, "when creating a new question" do
4 | before(:each) do
5 | @ss = mock_model(SurveySection)
6 | @question = Question.new(:text => "What is your favorite color?", :survey_section => @ss, :is_mandatory => true, :display_order => 1)
7 | end
8 |
9 | it "should be invalid without text" do
10 | @question.text = nil
11 | @question.should have(1).error_on(:text)
12 | end
13 |
14 | it "should have a parent survey section" do
15 | # this causes issues with building and saving
16 | # @question.survey_section = nil
17 | # @question.should have(1).error_on(:survey_section_id)
18 | end
19 |
20 | it "should be mandatory by default" do
21 | @question.mandatory?.should be_true
22 | end
23 |
24 | it "should convert pick attribute to string" do
25 | @question.pick.should == "none"
26 | @question.pick = :one
27 | @question.pick.should == "one"
28 | @question.pick = nil
29 | @question.pick.should == nil
30 | end
31 |
32 | it "should split the text" do
33 | @question.split_text.should == "What is your favorite color?"
34 | @question.split_text(:pre).should == "What is your favorite color?"
35 | @question.split_text(:post).should == ""
36 | @question.text = "before|after|extra"
37 | @question.split_text.should == "before|after|extra"
38 | @question.split_text(:pre).should == "before"
39 | @question.split_text(:post).should == "after|extra"
40 | end
41 | end
42 |
43 | describe Question, "that has answers" do
44 | before(:each) do
45 | @question = Factory(:question, :text => "What is your favorite color?")
46 | Factory(:answer, :question => @question, :display_order => 3, :text => "blue")
47 | Factory(:answer, :question => @question, :display_order => 1, :text => "red")
48 | Factory(:answer, :question => @question, :display_order => 2, :text => "green")
49 | end
50 |
51 | it "should have answers" do
52 | @question.answers.should have(3).answers
53 | end
54 |
55 | it "should retrieve those answers in display_order" do
56 | @question.answers.map(&:display_order).should == [1,2,3]
57 | end
58 | it "should delete answers when it is deleted" do
59 | answer_ids = @question.answers.map(&:id)
60 | @question.destroy
61 | answer_ids.each{|id| Answer.find_by_id(id).should be_nil}
62 | end
63 | end
64 |
65 | describe Question, "when interacting with an instance" do
66 |
67 | before(:each) do
68 | @question = Factory(:question)
69 | end
70 |
71 | it "should return 'default' for nil display type" do
72 | @question.display_type = nil
73 | @question.renderer.should == :default
74 | end
75 |
76 | it "should let you know if it is part of a group" do
77 | @question.question_group = Factory(:question_group)
78 | @question.solo?.should be_false
79 | @question.part_of_group?.should be_true
80 | @question.question_group = nil
81 | @question.solo?.should be_true
82 | @question.part_of_group?.should be_false
83 | end
84 | end
85 |
86 | describe Question, "with dependencies" do
87 | before(:each) do
88 | @rs = mock_model(ResponseSet)
89 | @question = Factory(:question)
90 | end
91 |
92 | it "should check its dependency" do
93 | @dependency = mock_model(Dependency)
94 | @dependency.stub!(:is_met?).with(@rs).and_return(true)
95 | @question.stub!(:dependency).and_return(@dependency)
96 | @question.triggered?(@rs).should == true
97 | end
98 | it "should delete dependency when it is deleted" do
99 | dep_id = Factory(:dependency, :question => @question).id
100 | @question.destroy
101 | Dependency.find_by_id(dep_id).should be_nil
102 | end
103 |
104 | end
105 |
--------------------------------------------------------------------------------
/spec/models/dependency_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2 |
3 | describe Dependency do
4 | before(:each) do
5 | @dependency = Factory(:dependency)
6 | end
7 |
8 | it "should be valid" do
9 | @dependency.should be_valid
10 | end
11 |
12 | it "should be invalid without a rule" do
13 | @dependency.rule = nil
14 | @dependency.should have(2).errors_on(:rule)
15 | @dependency.rule = " "
16 | @dependency.should have(1).errors_on(:rule)
17 | end
18 |
19 | it "should be invalid without a question_id" do
20 | @dependency.question_id = nil
21 | @dependency.should have(1).error_on(:question_id)
22 |
23 | @dependency.question_group_id = 1
24 | @dependency.should be_valid
25 |
26 | @dependency.question_id.should be_nil
27 | @dependency.question_group_id = nil
28 | @dependency.should have(1).error_on(:question_group_id)
29 | end
30 |
31 | it "should alias question_id as dependent_question_id" do
32 | @dependency.question_id = 19
33 | @dependency.dependent_question_id.should == 19
34 | @dependency.dependent_question_id = 14
35 | @dependency.question_id.should == 14
36 | end
37 |
38 | it "should be invalid unless rule composed of only references and operators" do
39 | @dependency.rule = "foo"
40 | @dependency.should have(1).error_on(:rule)
41 | @dependency.rule = "1 to 2"
42 | @dependency.should have(1).error_on(:rule)
43 | @dependency.rule = "a and b"
44 | @dependency.should have(1).error_on(:rule)
45 | end
46 |
47 | end
48 |
49 | describe Dependency, "when evaluating dependency conditions of a question in a response set" do
50 |
51 | before(:each) do
52 | @dep = Dependency.new(:rule => "A", :question_id => 1)
53 | @dep2 = Dependency.new(:rule => "A and B", :question_id => 1)
54 | @dep3 = Dependency.new(:rule => "A or B", :question_id => 1)
55 | @dep4 = Dependency.new(:rule => "!(A and B) and C", :question_id => 1)
56 |
57 | @dep_c = mock_model(DependencyCondition, :id => 1, :rule_key => "A", :to_hash => {:A => true})
58 | @dep_c2 = mock_model(DependencyCondition, :id => 2, :rule_key => "B", :to_hash => {:B => false})
59 | @dep_c3 = mock_model(DependencyCondition, :id => 3, :rule_key => "C", :to_hash => {:C => true})
60 |
61 | @dep.stub!(:dependency_conditions).and_return([@dep_c])
62 | @dep2.stub!(:dependency_conditions).and_return([@dep_c, @dep_c2])
63 | @dep3.stub!(:dependency_conditions).and_return([@dep_c, @dep_c2])
64 | @dep4.stub!(:dependency_conditions).and_return([@dep_c, @dep_c2, @dep_c3])
65 | end
66 |
67 | it "knows if the dependencies are met" do
68 | @dep.is_met?(@response_set).should be_true
69 | @dep2.is_met?(@response_set).should be_false
70 | @dep3.is_met?(@response_set).should be_true
71 | @dep4.is_met?(@response_set).should be_true
72 | end
73 |
74 | it "returns the proper keyed pairs from the dependency conditions" do
75 | @dep.conditions_hash(@response_set).should == {:A => true}
76 | @dep2.conditions_hash(@response_set).should == {:A => true, :B => false}
77 | @dep3.conditions_hash(@response_set).should == {:A => true, :B => false}
78 | @dep4.conditions_hash(@response_set).should == {:A => true, :B => false, :C => true}
79 | end
80 | end
81 | describe Dependency, "with conditions" do
82 | it "should destroy conditions when destroyed" do
83 | @dependency = Dependency.new(:rule => "A and B and C", :question_id => 1)
84 | Factory(:dependency_condition, :dependency => @dependency, :rule_key => "A")
85 | Factory(:dependency_condition, :dependency => @dependency, :rule_key => "B")
86 | Factory(:dependency_condition, :dependency => @dependency, :rule_key => "C")
87 | dc_ids = @dependency.dependency_conditions.map(&:id)
88 | @dependency.destroy
89 | dc_ids.each{|id| DependencyCondition.find_by_id(id).should == nil}
90 | end
91 | end
--------------------------------------------------------------------------------
/app/helpers/surveyor_helper.rb:
--------------------------------------------------------------------------------
1 | module SurveyorHelper
2 | # Layout: stylsheets and javascripts
3 | def surveyor_includes
4 | surveyor_stylsheets + surveyor_javascripts
5 | end
6 | def surveyor_stylsheets
7 | stylesheet_link_tag 'surveyor/reset', 'surveyor/dateinput', 'surveyor'
8 | end
9 | def surveyor_javascripts
10 | javascript_include_tag 'surveyor/jquery.tools.min', 'surveyor/jquery.surveyor'
11 | end
12 | # Helper for displaying warning/notice/error flash messages
13 | def flash_messages(types)
14 | types.map{|type| content_tag(:div, "#{flash[type]}".html_safe, :class => type.to_s)}.join.html_safe
15 | end
16 | # Section: dependencies, menu, previous and next
17 | def dependency_explanation_helper(question,response_set)
18 | # Attempts to explain why this dependent question needs to be answered by referenced the dependent question and users response
19 | trigger_responses = []
20 | dependent_questions = Question.find_all_by_id(question.dependency.dependency_conditions.map(&:question_id)).uniq
21 | response_set.responses.find_all_by_question_id(dependent_questions.map(&:id)).uniq.each do |resp|
22 | trigger_responses << resp.to_s
23 | end
24 | " You answered "#{trigger_responses.join("" and "")}" to the question "#{dependent_questions.map(&:text).join("","")}""
25 | end
26 | def menu_button_for(section)
27 | submit_tag(section.title, :name => "section[#{section.id}]")
28 | end
29 | def previous_section
30 | # use copy in memory instead of making extra db calls
31 | submit_tag(t('surveyor.previous_section'), :name => "section[#{@sections[@sections.index(@section)-1].id}]") unless @sections.first == @section
32 | end
33 | def next_section
34 | # use copy in memory instead of making extra db calls
35 | @sections.last == @section ? submit_tag(t('surveyor.click_here_to_finish'), :name => "finish") : submit_tag(t('surveyor.next_section'), :name => "section[#{@sections[@sections.index(@section)+1].id}]")
36 | end
37 |
38 | # Questions
39 | def q_text(obj)
40 | @n ||= 0
41 | return image_tag(obj.text) if obj.is_a?(Question) and obj.display_type == "image"
42 | return obj.text if obj.is_a?(Question) and (obj.dependent? or obj.display_type == "label" or obj.part_of_group?)
43 | "#{@n += 1}) #{obj.text}"
44 | end
45 | # def split_text(text = "") # Split text into with "|" delimiter - parts to go before/after input element
46 | # {:prefix => text.split("|")[0].blank? ? " " : text.split("|")[0], :postfix => text.split("|")[1] || " "}
47 | # end
48 | # def question_help_helper(question)
49 | # question.help_text.blank? ? "" : %Q(#{question.help_text})
50 | # end
51 |
52 | # Answers
53 | def rc_to_attr(type_sym)
54 | case type_sym.to_s
55 | when /^date|time$/ then :datetime_value
56 | when /(string|text|integer|float|datetime)/ then "#{type_sym.to_s}_value".to_sym
57 | else :answer_id
58 | end
59 | end
60 | def rc_to_as(type_sym)
61 | case type_sym.to_s
62 | when /(integer|float)/ then :string
63 | else type_sym
64 | end
65 | end
66 | def generate_pick_none_input_html(response_class, default_value, css_class)
67 | html = {}
68 | html[:class] = css_class unless css_class.blank?
69 | html[:value] = default_value if response_class.blank?
70 | html
71 | end
72 |
73 | # Responses
74 | def response_for(response_set, question, answer = nil, response_group = nil)
75 | return nil unless response_set && question && question.id
76 | result = response_set.responses.detect{|r| (r.question_id == question.id) && (answer.blank? ? true : r.answer_id == answer.id) && (r.response_group.blank? ? true : r.response_group.to_i == response_group.to_i)}
77 | result.blank? ? response_set.responses.build(:question_id => question.id, :response_group => response_group) : result
78 | end
79 | def response_idx(increment = true)
80 | @rc ||= 0
81 | (increment ? @rc += 1 : @rc).to_s
82 | end
83 | end
84 |
--------------------------------------------------------------------------------
/lib/formtastic/surveyor_builder.rb:
--------------------------------------------------------------------------------
1 | require 'formtastic'
2 | module Formtastic
3 | class SurveyorBuilder < SemanticFormBuilder
4 | def quiet_input(method, options = {})
5 | html_options = options.delete(:input_html) || strip_formtastic_options(options)
6 | html_options[:id] ||= generate_html_id(method, "")
7 | hidden_field(method, html_options)
8 | end
9 | def surveyor_check_boxes_input(method, options)
10 | collection = find_collection_for_column(method, options)
11 | html_options = options.delete(:input_html) || {}
12 |
13 | input_name = generate_association_input_name(method)
14 | hidden_fields = options.delete(:hidden_fields)
15 | value_as_class = options.delete(:value_as_class)
16 | unchecked_value = options.delete(:unchecked_value) || ''
17 | html_options = { :name => "#{@object_name}[#{input_name}][]" }.merge(html_options)
18 | input_ids = []
19 |
20 | selected_values = find_selected_values_for_column(method, options)
21 | disabled_option_is_present = options.key?(:disabled)
22 | disabled_values = [*options[:disabled]] if disabled_option_is_present
23 |
24 | li_options = value_as_class ? { :class => [method.to_s.singularize, 'default'].join('_') } : {}
25 |
26 | list_item_content = collection.map do |c|
27 | label = c.is_a?(Array) ? c.first : c
28 | value = c.is_a?(Array) ? c.last : c
29 | input_id = generate_html_id(input_name, value.to_s.gsub(/\s/, '_').gsub(/\W/, '').downcase)
30 | input_ids << input_id
31 |
32 | html_options[:checked] = selected_values.include?(value)
33 | html_options[:disabled] = disabled_values.include?(value) if disabled_option_is_present
34 | html_options[:id] = input_id
35 |
36 | li_content = create_hidden_field_for_check_boxes(input_name, value_as_class) unless hidden_fields
37 | li_content << template.content_tag(:label,
38 | Formtastic::Util.html_safe("#{create_check_boxes(input_name, html_options, value, unchecked_value, hidden_fields)} #{escape_html_entities(label)}"),
39 | :for => input_id
40 | )
41 | li_content << basic_input_helper(:text_field, :string, :string_value, options) if options[:response_class] == "other_and_string"
42 | li_content << basic_input_helper(:text_field, :string, :string_value, options) if %w(string other_and_string).include?(options[:response_class])
43 |
44 | # li_options = value_as_class ? { :class => [method.to_s.singularize, value.to_s.downcase].join('_') } : {}
45 | Formtastic::Util.html_safe(li_content)
46 | end
47 |
48 | Formtastic::Util.html_safe(list_item_content.join)
49 | end
50 | def surveyor_radio_input(method, options)
51 | collection = find_collection_for_column(method, options)
52 | html_options = strip_formtastic_options(options).merge(options.delete(:input_html) || {})
53 |
54 | input_name = generate_association_input_name(method)
55 | value_as_class = options.delete(:value_as_class)
56 | input_ids = []
57 |
58 | list_item_content = collection.map do |c|
59 | label = c.is_a?(Array) ? c.first : c
60 | value = c.is_a?(Array) ? c.last : c
61 | input_id = generate_html_id(input_name, value.to_s.gsub(/\s/, '_').gsub(/\W/, '').downcase)
62 | input_ids << input_id
63 |
64 | html_options[:id] = input_id
65 |
66 | li_content = template.content_tag(:label,
67 | Formtastic::Util.html_safe("#{radio_button(input_name, value, html_options)} #{escape_html_entities(label)}"),
68 | :for => input_id
69 | )
70 |
71 | li_content << basic_input_helper(:text_field, :string, :integer_value, options) if options[:response_class] == 'integer'
72 | li_content << basic_input_helper(:text_field, :string, :string_value, options) if options[:response_class] == 'string'
73 |
74 | # li_options = value_as_class ? { :class => [method.to_s.singularize, value.to_s.downcase].join('_') } : {}
75 | Formtastic::Util.html_safe(li_content)
76 | end
77 |
78 | Formtastic::Util.html_safe(list_item_content.join)
79 | end
80 | def date_input(method, options)
81 | string_input(method, options)
82 | end
83 | end
84 | end
85 |
--------------------------------------------------------------------------------
/spec/lib/redcap_parser_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2 |
3 | describe Surveyor::RedcapParser do
4 | before(:each) do
5 | # @parser = Surveyor::Parser.new
6 | end
7 | it "should decompose dependency rules" do
8 | # basic
9 | Dependency.decompose_rule('[f1_q12]="1"').should == {:rule => "A", :components => ['[f1_q12]="1"']}
10 | # spacing
11 | Dependency.decompose_rule('[f1_q9] = "1"').should == {:rule => "A", :components => ['[f1_q9] = "1"']}
12 | # and
13 | Dependency.decompose_rule('[pre_q88]="1" and [pre_q90]="1"').should == {:rule => "A and B", :components => ['[pre_q88]="1"', '[pre_q90]="1"']}
14 | # or
15 | Dependency.decompose_rule('[second_q111]="1" or [second_q111]="3"').should == {:rule => "A or B", :components => ['[second_q111]="1"', '[second_q111]="3"']}
16 | # or and
17 | Dependency.decompose_rule('[second_q100]="1" or [second_q100]="3" and [second_q101]="1"').should == {:rule => "A or B and C", :components => ['[second_q100]="1"', '[second_q100]="3"', '[second_q101]="1"']}
18 | # and or
19 | Dependency.decompose_rule('[second_q4]="1" and [second_q11]="1" or [second_q11]="98"').should == {:rule => "A and B or C", :components => ['[second_q4]="1"', '[second_q11]="1"', '[second_q11]="98"']}
20 | # or or or
21 | Dependency.decompose_rule('[pre_q74]="1" or [pre_q74]="2" or [pre_q74]="4" or [pre_q74]="5"').should == {:rule => "A or B or C or D", :components => ['[pre_q74]="1"', '[pre_q74]="2"', '[pre_q74]="4"', '[pre_q74]="5"']}
22 | # and with different operator
23 | Dependency.decompose_rule('[f1_q15] >= 21 and [f1_q28] ="1"').should == {:rule => "A and B", :components => ['[f1_q15] >= 21', '[f1_q28] ="1"']}
24 | end
25 | it "should decompose nested dependency rules" do
26 | # external parenthesis
27 | Dependency.decompose_rule('([pre_q74]="1" or [pre_q74]="2" or [pre_q74]="4" or [pre_q74]="5") and [pre_q76]="2"').should == {:rule => "(A or B or C or D) and E", :components => ['[pre_q74]="1"', '[pre_q74]="2"', '[pre_q74]="4"', '[pre_q74]="5"', '[pre_q76]="2"']}
28 | # internal parenthesis
29 | Dependency.decompose_rule('[f1_q10(4)]="1"').should == {:rule => "A", :components => ['[f1_q10(4)]="1"']}
30 | # internal and external parenthesis
31 | Dependency.decompose_rule('([f1_q7(11)] = "1" or [initial_52] = "1") and [pre_q76]="2"').should == {:rule => "(A or B) and C", :components => ['[f1_q7(11)] = "1"', '[initial_52] = "1"', '[pre_q76]="2"']}
32 | end
33 | it "should decompose shortcut dependency rules" do
34 | # 'or' on the right of the operator
35 | Dependency.decompose_rule('[initial_108] = "1" or "2"').should == {:rule => "A or B", :components => ['[initial_108] = "1"', '[initial_108] = "2"']}
36 | # multiple 'or' on the right
37 | Dependency.decompose_rule('[initial_52] = "1" or "2" or "3"').should == {:rule => "A or B or C", :components => ['[initial_52] = "1"', '[initial_52] = "2"', '[initial_52] = "3"']}
38 | # commas on the right
39 | Dependency.decompose_rule('[initial_189] = "1, 2, 3"').should == {:rule => "(A and B and C)", :components => ['[initial_189] = "1"', '[initial_189] = "2"', '[initial_189] = "3"']}
40 | # multiple internal parenthesis on the left
41 | Dependency.decompose_rule('[initial_119(1)(2)(3)(4)(5)] = "1"').should == {:rule => "(A and B and C and D and E)", :components => ['[initial_119(1)] = "1"', '[initial_119(2)] = "1"', '[initial_119(3)] = "1"', '[initial_119(4)] = "1"', '[initial_119(5)] = "1"']}
42 | end
43 | it "should decompose components" do
44 | Dependency.decompose_component('[initial_52] = "1"').should == {:question_reference => 'initial_52', :operator => '==', :answer_reference => '1'}
45 | Dependency.decompose_component('[initial_119(2)] = "1"').should == {:question_reference => 'initial_119', :operator => '==', :answer_reference => '2'}
46 | Dependency.decompose_component('[f1_q15] >= 21').should == {:question_reference => 'f1_q15', :operator => '>=', :integer_value => '21'}
47 | end
48 | it "should return a survey object" do
49 | x = %("Variable / Field Name","Form Name","Field Units","Section Header","Field Type","Field Label","Choices OR Calculations","Field Note","Text Validation Type","Text Validation Min","Text Validation Max",Identifier?,"Branching Logic (Show field only if...)","Required Field?"\nstudy_id,demographics,,,text,"Study ID",,,,,,,,)
50 | Surveyor::RedcapParser.new.parse(x, "redcaptest").is_a?(Survey).should be_true
51 | end
52 |
53 | end
--------------------------------------------------------------------------------
/spec/factories.rb:
--------------------------------------------------------------------------------
1 | # http://github.com/thoughtbot/factory_girl/tree/master
2 | require 'rubygems'
3 | require 'factory_girl'
4 |
5 | Factory.sequence(:unique_survey_access_code){|n| "simple_survey" << n.to_s }
6 |
7 | Factory.define :survey do |s|
8 | s.title {"Simple survey"}
9 | s.description {"A simple survey for testing"}
10 | s.access_code {Factory.next :unique_survey_access_code}
11 | s.active_at {Time.now}
12 | s.inactive_at {}
13 | s.css_url {}
14 | end
15 |
16 | Factory.sequence(:survey_section_display_order){|n| n }
17 |
18 | Factory.define :survey_section do |s|
19 | s.association :survey # s.survey_id {}
20 | s.title {"Demographics"}
21 | s.description {"Asking you about your personal data"}
22 | s.display_order {Factory.next :survey_section_display_order}
23 | s.reference_identifier {"demographics"}
24 | s.data_export_identifier {"demographics"}
25 | end
26 |
27 | Factory.sequence(:question_display_order){|n| n }
28 |
29 | Factory.define :question do |q|
30 | q.association :survey_section # s.survey_section_id {}
31 | q.question_group_id {}
32 | q.text {"What is your favorite color?"}
33 | q.short_text {"favorite_color"}
34 | q.help_text {"just write it in the box"}
35 | q.pick {:none}
36 | q.reference_identifier {|me| "q_#{me.object_id}"}
37 | q.data_export_identifier {}
38 | q.common_namespace {}
39 | q.common_identifier {}
40 | q.display_order {Factory.next :question_display_order}
41 | q.display_type {} # nil is default
42 | q.is_mandatory {false}
43 | q.display_width {}
44 | q.correct_answer_id {nil}
45 | end
46 |
47 | Factory.define :question_group do |g|
48 | g.text {"Describe your family"}
49 | g.help_text {}
50 | g.reference_identifier {|me| "g_#{me.object_id}"}
51 | g.data_export_identifier {}
52 | g.common_namespace {}
53 | g.common_identifier {}
54 | g.display_type {}
55 | g.custom_class {}
56 | g.custom_renderer {}
57 | end
58 |
59 | Factory.sequence(:answer_display_order){|n| n }
60 |
61 | Factory.define :answer do |a|
62 | a.association :question # a.question_id {}
63 | a.text {"My favorite color is clear"}
64 | a.short_text {"clear"}
65 | a.help_text {"Clear is the absense of color"}
66 | a.weight {}
67 | a.response_class {"String"}
68 | a.reference_identifier {}
69 | a.data_export_identifier {}
70 | a.common_namespace {}
71 | a.common_identifier {}
72 | a.display_order {Factory.next :answer_display_order}
73 | a.is_exclusive {}
74 | a.hide_label {}
75 | a.display_length {}
76 | a.custom_class {}
77 | a.custom_renderer {}
78 | end
79 |
80 | Factory.define :dependency do |d|
81 | # the dependent question
82 | d.association :question # d.question_id {}
83 | d.question_group_id {}
84 | d.rule {"A"}
85 | end
86 |
87 | Factory.define :dependency_condition do |d|
88 | d.association :dependency # d.dependency_id {}
89 | d.rule_key {"A"}
90 | # the conditional question
91 | d.question_id {}
92 | d.operator {"=="}
93 | d.answer_id {}
94 | d.datetime_value {}
95 | d.integer_value {}
96 | d.float_value {}
97 | d.unit {}
98 | d.text_value {}
99 | d.string_value {}
100 | d.response_other {}
101 | end
102 |
103 | Factory.define :response_set do |r|
104 | r.user_id {}
105 | r.association :survey # r.survey_id {}
106 | r.access_code {Surveyor::Common.make_tiny_code}
107 | r.started_at {Time.now}
108 | r.completed_at {}
109 | end
110 |
111 | Factory.define :response do |r|
112 | r.association :response_set # r.response_set_id {}
113 | r.survey_section_id {}
114 | r.question_id {}
115 | r.answer_id {}
116 | r.datetime_value {}
117 | r.integer_value {}
118 | r.float_value {}
119 | r.unit {}
120 | r.text_value {}
121 | r.string_value {}
122 | r.response_other {}
123 | r.response_group {}
124 | end
125 |
126 | Factory.define :validation do |v|
127 | v.association :answer # v.answer_id {}
128 | v.rule {"A"}
129 | v.message {}
130 | end
131 |
132 | Factory.define :validation_condition do |v|
133 | v.association :validation # v.validation_id {}
134 | v.rule_key {"A"}
135 | v.question_id {}
136 | v.operator {"=="}
137 | v.answer_id {}
138 | v.datetime_value {}
139 | v.integer_value {}
140 | v.float_value {}
141 | v.unit {}
142 | v.text_value {}
143 | v.string_value {}
144 | v.response_other {}
145 | v.regexp {}
146 | end
147 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Surveys On Rails
2 |
3 | Surveyor is a ruby gem and developer tool that brings surveys into Rails applications. Surveys are written in the Surveyor DSL (Domain Specific Language).
4 |
5 | Before Rails 2.3, it was implemented as a Rails Engine. It also existed previously as a plugin. Today it is a gem only.
6 |
7 | The rails3 branch is functional for use with Rails 3.
8 |
9 | ## Why you might want to use Surveyor
10 |
11 | If your Rails app needs to asks users questions as part of a survey, quiz, or questionnaire then you should consider using Surveyor. This gem was designed to deliver clinical research surveys to large populations, but it can be used for any type of survey.
12 |
13 | The Surveyor DSL defines questions, answers, question groups, survey sections, dependencies (e.g. if response to question 4 is A, then show question 5), and validations. Answers are the options available for each question - user input is called "responses" and are grouped into "response sets". A DSL makes it significantly easier to import long surveys (no more click/copy/paste). It also enables non-programmers to write out, edit, re-edit... any number of surveys.
14 |
15 | ## DSL example
16 |
17 | The Surveyor DSL supports a wide range of question types (too many to list here) and complex dependency logic. Here are the first few questions of the "kitchen sink" survey which should give you and idea of how it works. The full example with all the types of questions available if you follow the installation instructions below.
18 |
19 | survey "Kitchen Sink survey" do
20 |
21 | section "Basic questions" do
22 | # A label is a question that accepts no answers
23 | label "These questions are examples of the basic supported input types"
24 |
25 | # A basic question with radio buttons
26 | question_1 "What is your favorite color?", :pick => :one
27 | answer "red"
28 | answer "blue"
29 | answer "green"
30 | answer "yellow"
31 | answer :other
32 |
33 | # A basic question with checkboxes
34 | # "question" and "answer" may be abbreviated as "q" and "a"
35 | q_2 "Choose the colors you don't like", :pick => :any
36 | a_1 "red"
37 | a_2 "blue"
38 | a_3 "green"
39 | a_4 "yellow"
40 | a :omit
41 |
42 | # A dependent question, with conditions and rule to logically join them
43 | # the question's reference identifier is "2a", and the answer's reference_identifier is "1"
44 | # question reference identifiers used in conditions need to be unique on a survey for the lookups to work
45 | q_2a "Please explain why you don't like this color?"
46 | a_1 "explanation", :text
47 | dependency :rule => "A or B or C or D"
48 | condition_A :q_2, "==", :a_1
49 | condition_B :q_2, "==", :a_2
50 | condition_C :q_2, "==", :a_3
51 | condition_D :q_2, "==", :a_4
52 |
53 | # ... other question, sections and such. See surveys/kitchen_sink_survey.rb for more.
54 | end
55 |
56 | end
57 |
58 | The first question is "pick one" (radio buttons) with "other". The second question is "pick any" (checkboxes) with the option to "omit". It also features a dependency with a follow up question. Notice the dependency rule is defined as a string. We support complex dependency such as "A and (B or C) and D" or "A or ((B and C) or D)". The conditions are evaluated separately using the operators "==","!=","<>", ">=","<" the substituted by letter into to the dependency rule and evaluated.
59 |
60 | # Installation
61 |
62 | 1. Add it to your bundler Gemfile:
63 |
64 | gem "surveyor"
65 |
66 | `bundle install`
67 |
68 | 2. Generate assets, run migrations:
69 |
70 | `script/generate surveyor`
71 | `rake db:migrate`
72 |
73 | 3. Try out the "kitchen sink" survey. The rake task above generates surveys from our custom survey DSL (a good format for end users and stakeholders).
74 |
75 | `rake surveyor FILE=surveys/kitchen_sink_survey.rb`
76 |
77 | 4. Start up your app and visit:
78 |
79 | http://localhost:3000/surveys
80 |
81 | Try taking the survey and compare it to the contents of the DSL file kitchen\_sink\_survey.rb. See how the DSL maps to what you see.
82 |
83 | There are two other useful rake tasks for removing (only surveys without responses) and un-parsing (from db to DSL file) surveys:
84 |
85 | `rake surveyor:remove`
86 | `rake surveyor:unparse`
87 |
88 | # Customizing surveyor
89 |
90 | Surveyor's controller, models, and views may be customized via classes in your app/models, app/helpers and app/controllers directories. To generate a sample custom controller and layout, run:
91 |
92 | `script/generate extend_surveyor`
93 |
94 | and read surveys/EXTENDING\_SURVEYOR
95 |
96 | # Requirements
97 |
98 | Surveyor depends on Ruby (1.8.7 - 1.9.1), Rails 2.3 and HAML/SASS http://haml.hamptoncatlin.com/. It also depends on fastercsv for csv exports.
99 |
100 | # Contributing, testing
101 |
102 | To work on the code fork this github project. Run:
103 |
104 | `rake -f init_testbed.rakefile`
105 |
106 | which will generate a test app in testbed. Run rake spec and rake cucumber there, and start writing tests!
107 |
108 | Copyright (c) 2008-2010 Brian Chamberlain and Mark Yoon, released under the MIT license
109 |
--------------------------------------------------------------------------------
/lib/surveyor/surveyor_controller_methods.rb:
--------------------------------------------------------------------------------
1 | module Surveyor
2 | module SurveyorControllerMethods
3 | def self.included(base)
4 | base.send :before_filter, :get_current_user, :only => [:new, :create]
5 | base.send :layout, 'surveyor_default'
6 | end
7 |
8 | # Actions
9 | def new
10 | @surveys = Survey.find(:all)
11 | @title = "You can take these surveys"
12 | redirect_to surveyor_index unless surveyor_index == available_surveys_path
13 | end
14 |
15 | def create
16 | @survey = Survey.find_by_access_code(params[:survey_code])
17 | @response_set = ResponseSet.create(:survey => @survey, :user_id => (@current_user.nil? ? @current_user : @current_user.id))
18 | if (@survey && @response_set)
19 | flash[:notice] = t('surveyor.survey_started_success')
20 | redirect_to(edit_my_survey_path(:survey_code => @survey.access_code, :response_set_code => @response_set.access_code))
21 | else
22 | flash[:notice] = t('surveyor.Unable_to_find_that_survey')
23 | redirect_to surveyor_index
24 | end
25 | end
26 |
27 | def show
28 | @response_set = ResponseSet.find_by_access_code(params[:response_set_code], :include => {:responses => [:question, :answer]})
29 | if @response_set
30 | @survey = @response_set.survey
31 | respond_to do |format|
32 | format.html #{render :action => :show}
33 | format.csv {
34 | send_data(@response_set.to_csv, :type => 'text/csv; charset=utf-8; header=present',:filename => "#{@response_set.updated_at.strftime('%Y-%m-%d')}_#{@response_set.access_code}.csv")
35 | }
36 | end
37 | else
38 | flash[:notice] = t('surveyor.unable_to_find_your_responses')
39 | redirect_to surveyor_index
40 | end
41 | end
42 |
43 | def edit
44 | @response_set = ResponseSet.find_by_access_code(params[:response_set_code], :include => {:responses => [:question, :answer]})
45 | if @response_set
46 | @survey = Survey.with_sections.find_by_id(@response_set.survey_id)
47 | @sections = @survey.sections
48 | if params[:section]
49 | @section = @sections.with_includes.find(section_id_from(params[:section])) || @sections.with_includes.first
50 | else
51 | @section = @sections.with_includes.first
52 | end
53 | @dependents = (@response_set.unanswered_dependencies - @section.questions) || []
54 | else
55 | flash[:notice] = t('surveyor.unable_to_find_your_responses')
56 | redirect_to surveyor_index
57 | end
58 | end
59 |
60 | def update
61 | @response_set = ResponseSet.find_by_access_code(params[:response_set_code], :include => {:responses => :answer}, :lock => true)
62 | return redirect_with_message(available_surveys_path, :notice, t('surveyor.unable_to_find_your_responses')) if @response_set.blank?
63 | saved = false
64 | ActiveRecord::Base.transaction do
65 | saved = @response_set.update_attributes(:responses_attributes => ResponseSet.reject_or_destroy_blanks(params[:r]))
66 | saved = @response_set.complete! if saved && params[:finish]
67 | end
68 | return redirect_with_message(surveyor_finish, :notice, t('surveyor.completed_survey')) if saved && params[:finish]
69 |
70 | respond_to do |format|
71 | format.html do
72 | flash[:notice] = t('surveyor.unable_to_update_survey') unless saved
73 | redirect_to :action => "edit", :anchor => anchor_from(params[:section]), :params => {:section => section_id_from(params[:section])}
74 | end
75 | format.js do
76 | ids, remove, question_ids = {}, {}, []
77 | ResponseSet.reject_or_destroy_blanks(params[:r]).each do |k,v|
78 | ids[k] = @response_set.responses.find(:first, :conditions => v).id if !v.has_key?("id")
79 | remove[k] = v["id"] if v.has_key?("id") && v.has_key?("_destroy")
80 | question_ids << v["question_id"]
81 | end
82 | render :json => {"ids" => ids, "remove" => remove}.merge(@response_set.reload.all_dependencies(question_ids))
83 | end
84 | end
85 | end
86 |
87 | private
88 |
89 | # Filters
90 | def get_current_user
91 | @current_user = self.respond_to?(:current_user) ? self.current_user : nil
92 | end
93 |
94 | # Params: the name of some submit buttons store the section we'd like to go to. for repeater questions, an anchor to the repeater group is also stored
95 | # e.g. params[:section] = {"1"=>{"question_group_1"=>"<= add row"}}
96 | def section_id_from(p)
97 | p.respond_to?(:keys) ? p.keys.first : p
98 | end
99 |
100 | def anchor_from(p)
101 | p.respond_to?(:keys) && p[p.keys.first].respond_to?(:keys) ? p[p.keys.first].keys.first : nil
102 | end
103 |
104 | def surveyor_index
105 | available_surveys_path
106 | end
107 | def surveyor_finish
108 | available_surveys_path
109 | end
110 |
111 | def redirect_with_message(path, message_type, message)
112 | respond_to do |format|
113 | format.html do
114 | flash[message_type] = message if !message.blank? and !message_type.blank?
115 | redirect_to path
116 | end
117 | format.js do
118 | render :text => message, :status => 403
119 | end
120 | end
121 | end
122 | end
123 | end
--------------------------------------------------------------------------------
/generators/surveyor/templates/assets/stylesheets/results.css:
--------------------------------------------------------------------------------
1 | body { background-color: #fff; color: #333; }
2 |
3 | body, p, ol, ul, td {
4 | font-family: verdana, arial, helvetica, sans-serif;
5 | font-size: 13px;
6 | line-height: 18px;
7 | }
8 |
9 | pre {
10 | background-color: #eee;
11 | padding: 10px;
12 | font-size: 11px;
13 | }
14 |
15 | a { color: #000; }
16 | a:visited { color: #666; }
17 | a:hover { color: #fff; background-color:#000; }
18 |
19 | .fieldWithErrors {
20 | padding: 2px;
21 | background-color: red;
22 | display: table;
23 | }
24 |
25 | #errorExplanation {
26 | width: 400px;
27 | border: 2px solid red;
28 | padding: 7px;
29 | padding-bottom: 12px;
30 | margin-bottom: 20px;
31 | background-color: #f0f0f0;
32 | }
33 |
34 | #errorExplanation h2 {
35 | text-align: left;
36 | font-weight: bold;
37 | padding: 5px 5px 5px 15px;
38 | font-size: 12px;
39 | margin: -7px;
40 | background-color: #c00;
41 | color: #fff;
42 | }
43 |
44 | #errorExplanation p {
45 | color: #333;
46 | margin-bottom: 0;
47 | padding: 5px;
48 | }
49 |
50 | #errorExplanation ul li {
51 | font-size: 12px;
52 | list-style: square;
53 | }
54 |
55 |
56 |
57 | textarea, .preview {
58 | width: 310px;
59 | height: 360px;
60 | }
61 | .preview {
62 | border: #abc 1px dotted;
63 | padding: 3px;
64 | }
65 |
66 | .bluebox { padding:10px; border:1px solid #577BBF; background-color:#E6EDFF; }
67 | input[type=submit] {
68 | padding: 3px 7px;
69 | font-weight: bold;
70 | }
71 |
72 | .list_table{width:100%;border-collapse: collapse;empty-cells: show;}
73 | .list_table th{font: bold 12px Verdana, sans-serif; color: #fff;padding: 5px 15px 5px 5px;border: solid 1px #A0BDE4;background-color: #577BBF;}
74 | .list_table tr td {border-collapse: collapse;padding: 5px 4px;color: #333;font-family: Verdana, sans-serif;font-size: 12px;border: 1px solid #DCE7F1;}
75 | .list_table tr { background-color: #FFF; }
76 | .list_table tr.even { background-color:#F2F5FB;}
77 | .list_table tr:hover { background-color: #FDFCEA;}
78 | .list_table tr.group td {background-color: #eee;font-weight:bold;font-size:13px;}
79 | .list_table .border2px{border-left:2px solid #DCE7F1;}
80 | .list_table a{padding:0 5px;}
81 | .list_table th.asc{background:#577BBF url(../images/idp_views/arrow_down.gif) no-repeat right;}
82 | .list_table th.desc{background:#577BBF url(../images/idp_views/arrow_up.gif) no-repeat right;}
83 | .list_table th a{color:white;text-decoration:underline;}
84 | .list_table .group{text-align:center;background:#f3f3f3;border-top:1px dotted #586A7E;border-bottom:1px dotted #586A7E;}
85 | .list_table .group_top{border-top:1px dotted #586A7E;}
86 | .list_table .group_bottom{border-bottom:1px dotted #586A7E;}
87 | .list_table_page{padding:5px;text-align:center;}
88 | .list_table_page form{display:inline}
89 |
90 | .list_table1 {width:100%;border-collapse: collapse;empty-cells: show;}
91 | .list_table1 th{ font: bold 12px Verdana, sans-serif; color: #577BBF; padding:3px 5px; background-color:#F2F5FB; border: 1px solid #DCE7F1; }
92 | .list_table1 tr td {border-collapse: collapse;padding: 2px;color: #333;font-family: Verdana, sans-serif;font-size: 10px;border: 1px solid #DCE7F1;}
93 | .list_table1 tr { background-color: #FFF; }
94 | .list_table1 tr:hover { background-color: #FDFCEA;}
95 |
96 | .legend_table { border-top: 1px solid #DCE7F1; border-bottom: 1px solid #DCE7F1; padding:1px 0; }
97 |
98 | .list_table2 { width:100%; empty-cells: show; border-collapse:collapse;}
99 | .list_table2 th{ font: bold 12px Verdana; padding:5px; text-align:left; background-color:#FFF;}
100 | .list_table2 tr td { border-collapse:collapse; padding: 0px 3px; font-family: Verdana, sans-serif;font-size: 10px; border:1px solid #FFF; }
101 | .list_table2 a:link { text-decoration:underline; color:#03F;}
102 | .list_table2 a:visited { text-decoration:underline; color:#03F; }
103 | .list_table2 a:hover { text-decoration:none; color:#03F; }
104 |
105 | .list_table3{width:100%;border-collapse: collapse;empty-cells: show; margin:5px 0;}
106 | .list_table3 th{font: bold 12px Arial; color: #fff;padding: 3px;border: solid 1px #A0BDE4;background-color: #577BBF;}
107 | .list_table3 tr td { border-collapse: collapse;padding: 5px 2px; font-family: Arial;font-size: 11px;border: 1px solid #DCE7F1; line-height:1.2em;}
108 | .list_table3 tr { background-color: #FFF;}
109 | .list_table3 tr.even { background-color:#F2F5FB;}
110 | .list_table3 tr:hover { background-color: #FDFCEA;}
111 |
112 | .list_table4{ border-collapse: collapse;empty-cells: show;}
113 | .list_table4 th{font: bold 11px Arial; padding: 3px; text-align:left; background-color:#F2F5FB; border: 1px solid #DCE7F1; color:#577BBF;}
114 | .list_table4 tr td { border-collapse: collapse;padding: 0px 3px; font-family: Arial;font-size: 10px;border: 1px solid #DCE7F1;}
115 | .list_table4 tr { background-color: #FFF; }
116 | .list_table4 tr.even { background-color:#F2F5FB;}
117 | .list_table4 tr:hover { background-color: #FDFCEA;}
118 |
119 | .list_table5 { width:100%; empty-cells: show; border-collapse:collapse; margin:5px 0;}
120 | .list_table5 th{ font: bold 12px Verdana; padding:5px; text-align:left; background-color:#FFF;}
121 | .list_table5 tr td { border-collapse:collapse; padding: 5px; font-family: Verdana, sans-serif;font-size: 12px; }
122 | .list_table5 a:link { text-decoration:underline; color:#03F;}
123 | .list_table5 a:visited { text-decoration:underline; color:#03F; }
124 | .list_table5 a:hover { text-decoration:none; color:#03F; }
125 |
126 |
--------------------------------------------------------------------------------
/spec/models/validation_condition_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2 |
3 | describe ValidationCondition, "Class methods" do
4 | it "should have a list of operators" do
5 | %w(== != < > <= >= =~).each{|operator| ValidationCondition.operators.include?(operator).should be_true }
6 | end
7 | end
8 |
9 | describe ValidationCondition do
10 | before(:each) do
11 | @validation_condition = Factory(:validation_condition)
12 | end
13 |
14 | it "should be valid" do
15 | @validation_condition.should be_valid
16 | end
17 | # this causes issues with building and saving
18 | # it "should be invalid without a parent validation_id" do
19 | # @validation_condition.validation_id = nil
20 | # @validation_condition.should have(1).errors_on(:validation_id)
21 | # end
22 |
23 | it "should be invalid without an operator" do
24 | @validation_condition.operator = nil
25 | @validation_condition.should have(2).errors_on(:operator)
26 | end
27 |
28 | it "should be invalid without a rule_key" do
29 | @validation_condition.should be_valid
30 | @validation_condition.rule_key = nil
31 | @validation_condition.should_not be_valid
32 | @validation_condition.should have(1).errors_on(:rule_key)
33 | end
34 |
35 | it "should have unique rule_key within the context of a validation" do
36 | @validation_condition.should be_valid
37 | Factory(:validation_condition, :validation_id => 2, :rule_key => "2")
38 | @validation_condition.rule_key = "2" #rule key uniquness is scoped by validation_id
39 | @validation_condition.validation_id = 2
40 | @validation_condition.should_not be_valid
41 | @validation_condition.should have(1).errors_on(:rule_key)
42 | end
43 |
44 | it "should have an operator in ValidationCondition.operators" do
45 | ValidationCondition.operators.each do |o|
46 | @validation_condition.operator = o
47 | @validation_condition.should have(0).errors_on(:operator)
48 | end
49 | @validation_condition.operator = "#"
50 | @validation_condition.should have(1).error_on(:operator)
51 | end
52 |
53 | end
54 |
55 | describe ValidationCondition, "validating responses" do
56 | def test_var(vhash, ahash, rhash)
57 | v = Factory(:validation_condition, vhash)
58 | a = Factory(:answer, ahash)
59 | r = Factory(:response, {:answer => a, :question => a.question}.merge(rhash))
60 | return v.is_valid?(r)
61 | end
62 |
63 | it "should validate a response by regexp" do
64 | test_var({:operator => "=~", :regexp => /^[a-z]{1,6}$/}, {:response_class => "string"}, {:string_value => "clear"}).should be_true
65 | test_var({:operator => "=~", :regexp => /^[a-z]{1,6}$/}, {:response_class => "string"}, {:string_value => "foobarbaz"}).should be_false
66 | end
67 | it "should validate a response by integer comparison" do
68 | test_var({:operator => ">", :integer_value => 3}, {:response_class => "integer"}, {:integer_value => 4}).should be_true
69 | test_var({:operator => "<=", :integer_value => 256}, {:response_class => "integer"}, {:integer_value => 512}).should be_false
70 | end
71 | it "should validate a response by (in)equality" do
72 | test_var({:operator => "!=", :datetime_value => Date.today + 1}, {:response_class => "date"}, {:datetime_value => Date.today}).should be_true
73 | test_var({:operator => "==", :string_value => "foo"}, {:response_class => "string"}, {:string_value => "foo"}).should be_true
74 | end
75 | it "should represent itself as a hash" do
76 | @v = Factory(:validation_condition, :rule_key => "A")
77 | @v.stub!(:is_valid?).and_return(true)
78 | @v.to_hash("foo").should == {:A => true}
79 | @v.stub!(:is_valid?).and_return(false)
80 | @v.to_hash("foo").should == {:A => false}
81 | end
82 | end
83 |
84 | describe ValidationCondition, "validating responses by other responses" do
85 | def test_var(v_hash, a_hash, r_hash, ca_hash, cr_hash)
86 | ca = Factory(:answer, ca_hash)
87 | cr = Factory(:response, cr_hash.merge(:answer => ca, :question => ca.question))
88 | v = Factory(:validation_condition, v_hash.merge({:question_id => ca.question.id, :answer_id => ca.id}))
89 | a = Factory(:answer, a_hash)
90 | r = Factory(:response, r_hash.merge(:answer => a, :question => a.question))
91 | return v.is_valid?(r)
92 | end
93 | it "should validate a response by integer comparison" do
94 | test_var({:operator => ">"}, {:response_class => "integer"}, {:integer_value => 4}, {:response_class => "integer"}, {:integer_value => 3}).should be_true
95 | test_var({:operator => "<="}, {:response_class => "integer"}, {:integer_value => 512}, {:response_class => "integer"}, {:integer_value => 4}).should be_false
96 | end
97 | it "should validate a response by (in)equality" do
98 | test_var({:operator => "!="}, {:response_class => "date"}, {:datetime_value => Date.today}, {:response_class => "date"}, {:datetime_value => Date.today + 1}).should be_true
99 | test_var({:operator => "=="}, {:response_class => "string"}, {:string_value => "donuts"}, {:response_class => "string"}, {:string_value => "donuts"}).should be_true
100 | end
101 | it "should not validate a response by regexp" do
102 | test_var({:operator => "=~"}, {:response_class => "date"}, {:datetime_value => Date.today}, {:response_class => "date"}, {:datetime_value => Date.today + 1}).should be_false
103 | test_var({:operator => "=~"}, {:response_class => "string"}, {:string_value => "donuts"}, {:response_class => "string"}, {:string_value => "donuts"}).should be_false
104 | end
105 | end
--------------------------------------------------------------------------------
/spec/lib/unparser_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2 |
3 | describe Surveyor::Unparser do
4 | before(:each) do
5 | @survey = Survey.new(:title => "Simple survey", :description => "very simple")
6 | @section = @survey.sections.build(:title => "Simple section")
7 | end
8 |
9 | it "should unparse a basic survey, section, and question" do
10 | q1 = @section.questions.build(:text => "What is your favorite color?", :reference_identifier => 1, :pick => :one)
11 | a11 = q1.answers.build(:text => "red", :response_class => "answer", :reference_identifier => 1, :question => q1)
12 | a12 = q1.answers.build(:text => "green", :response_class => "answer", :reference_identifier => 2, :question => q1)
13 | a13 = q1.answers.build(:text => "blue", :response_class => "answer", :reference_identifier => 3, :question => q1)
14 | a14 = q1.answers.build(:text => "Other", :response_class => "string", :reference_identifier => 4, :question => q1)
15 | a15 = q1.answers.build(:text => "Omit", :reference_identifier => 5, :question => q1, :is_exclusive => true)
16 | q2 = @section.questions.build(:text => "What is your name?", :reference_identifier => 2, :pick => :none)
17 | a21 = q2.answers.build(:response_class => "string", :reference_identifier => 1, :question => q2)
18 | Surveyor::Unparser.unparse(@survey).should ==
19 | <<-dsl
20 | survey "Simple survey", :description=>"very simple" do
21 | section "Simple section" do
22 |
23 | q_1 "What is your favorite color?", :pick=>"one"
24 | a_1 "red"
25 | a_2 "green"
26 | a_3 "blue"
27 | a_4 :other, :string
28 | a_5 :omit
29 |
30 | q_2 "What is your name?"
31 | a_1 :string
32 | end
33 | end
34 | dsl
35 | end
36 |
37 | it "should unparse groups" do
38 | q3 = @section.questions.build(:text => "Happy?")
39 | a31 = q3.answers.build(:text => "Yes", :question => q3)
40 | a32 = q3.answers.build(:text => "Maybe", :question => q3)
41 | a33 = q3.answers.build(:text => "No", :question => q3)
42 |
43 | q4 = @section.questions.build(:text => "Energized?")
44 | a41 = q4.answers.build(:text => "Yes", :question => q4)
45 | a42 = q4.answers.build(:text => "Maybe", :question => q4)
46 | a43 = q4.answers.build(:text => "No", :question => q4)
47 |
48 | g1 = q3.build_question_group(:text => "How are you feeling?", :display_type => "grid")
49 | q4.question_group = g1
50 | g1.questions = [q3, q4]
51 |
52 | q5 = @section.questions.build(:text => "Model")
53 | a51 = q5.answers.build(:response_class => "string", :question => q3)
54 |
55 | g2 = q5.build_question_group(:text => "Tell us about the cars you own", :display_type => "repeater")
56 | g2.questions = [q5]
57 |
58 | Surveyor::Unparser.unparse(@survey).should ==
59 | <<-dsl
60 | survey "Simple survey", :description=>"very simple" do
61 | section "Simple section" do
62 |
63 | grid "How are you feeling?" do
64 | a "Yes"
65 | a "Maybe"
66 | a "No"
67 | q "Happy?"
68 | q "Energized?"
69 | end
70 |
71 | repeater "Tell us about the cars you own" do
72 | q "Model"
73 | a :string
74 | end
75 | end
76 | end
77 | dsl
78 | end
79 |
80 | it "should unparse a basic survey, section, and question" do
81 | q6 = @section.questions.build(:text => "What... is your name? (e.g. It is 'Arthur', King of the Britons)", :reference_identifier => "montypython3")
82 | a61 = q6.answers.build(:response_class => "string", :reference_identifier => 1, :question => q6)
83 |
84 | q7 = @section.questions.build(:text => "What... is your quest? (e.g. To seek the Holy Grail)", :display_type => "label")
85 | d1 = q7.build_dependency(:rule => "A", :question => q7)
86 | dc1 = d1.dependency_conditions.build(:dependency => d1, :question => q6, :answer => a61, :operator => "==", :string_value => "It is 'Arthur', King of the Britons", :rule_key => "A")
87 |
88 | q8 = @section.questions.build(:text => "How many pets do you own?")
89 | a81 = q8.answers.build(:response_class => "integer", :question => q8)
90 | v1 = a81.validations.build(:rule => "A", :answer => a81)
91 | vc1 = v1.validation_conditions.build(:operator => ">=", :integer_value => 0, :validation => v1, :rule_key => "A")
92 |
93 | q9 = @section.questions.build(:text => "Pick your favorite date AND time", :custom_renderer => "/partials/custom_question")
94 | a91 = q9.answers.build(:response_class => "datetime", :question => q9)
95 |
96 | q10 = @section.questions.build(:text => "What time do you usually take a lunch break?", :reference_identifier => "time_lunch")
97 | a101 = q10.answers.build(:response_class => "time", :reference_identifier => 1, :question => q10)
98 |
99 | Surveyor::Unparser.unparse(@survey).should ==
100 | <<-dsl
101 | survey "Simple survey", :description=>"very simple" do
102 | section "Simple section" do
103 |
104 | q_montypython3 "What... is your name? (e.g. It is 'Arthur', King of the Britons)"
105 | a_1 :string
106 |
107 | label "What... is your quest? (e.g. To seek the Holy Grail)"
108 | dependency :rule=>"A"
109 | condition_A :q_montypython3, "==", {:string_value=>"It is 'Arthur', King of the Britons", :answer_reference=>"1"}
110 |
111 | q "How many pets do you own?"
112 | a :integer
113 | validation :rule=>"A"
114 | condition_A ">=", :integer_value=>0
115 |
116 | q "Pick your favorite date AND time", :custom_renderer=>"/partials/custom_question"
117 | a :datetime
118 |
119 | q_time_lunch "What time do you usually take a lunch break?"
120 | a_1 :time
121 | end
122 | end
123 | dsl
124 | end
125 | end
126 |
127 |
--------------------------------------------------------------------------------
/lib/surveyor/models/response_set_methods.rb:
--------------------------------------------------------------------------------
1 | module Surveyor
2 | module Models
3 | module ResponseSetMethods
4 | def self.included(base)
5 | # Associations
6 | base.send :belongs_to, :survey
7 | base.send :belongs_to, :user
8 | base.send :has_many, :responses, :dependent => :destroy
9 | base.send :accepts_nested_attributes_for, :responses, :allow_destroy => true
10 |
11 | @@validations_already_included ||= nil
12 | unless @@validations_already_included
13 | # Validations
14 | base.send :validates_presence_of, :survey_id
15 | base.send :validates_associated, :responses
16 | base.send :validates_uniqueness_of, :access_code
17 |
18 | @@validations_already_included = true
19 | end
20 |
21 | # Attributes
22 | base.send :attr_protected, :completed_at
23 |
24 | # Class methods
25 | base.instance_eval do
26 | def reject_or_destroy_blanks(hash_of_hashes)
27 | result = {}
28 | (hash_of_hashes || {}).each_pair do |k, hash|
29 | if has_blank_value?(hash)
30 | result.merge!({k => hash.merge("_destroy" => "true")}) if hash.has_key?("id")
31 | else
32 | result.merge!({k => hash})
33 | end
34 | end
35 | result
36 | end
37 | def has_blank_value?(hash)
38 | hash["answer_id"].blank? or hash.any?{|k,v| v.is_a?(Array) ? v.all?{|x| x.to_s.blank?} : v.to_s.blank?}
39 | end
40 | end
41 | end
42 |
43 | # Instance methods
44 | def initialize(*args)
45 | super(*args)
46 | default_args
47 | end
48 |
49 | def default_args
50 | self.started_at ||= Time.now
51 | self.access_code = Surveyor::Common.make_tiny_code
52 | end
53 |
54 | def access_code=(val)
55 | while ResponseSet.find_by_access_code(val)
56 | val = Surveyor::Common.make_tiny_code
57 | end
58 | super
59 | end
60 |
61 | def to_csv(access_code = false, print_header = true)
62 | qcols = Question.content_columns.map(&:name) - %w(created_at updated_at)
63 | acols = Answer.content_columns.map(&:name) - %w(created_at updated_at)
64 | rcols = Response.content_columns.map(&:name)
65 | require 'fastercsv'
66 | FCSV(result = "") do |csv|
67 | csv << (access_code ? ["response set access code"] : []) + qcols.map{|qcol| "question.#{qcol}"} + acols.map{|acol| "answer.#{acol}"} + rcols.map{|rcol| "response.#{rcol}"} if print_header
68 | responses.each do |response|
69 | csv << (access_code ? [self.access_code] : []) + qcols.map{|qcol| response.question.send(qcol)} + acols.map{|acol| response.answer.send(acol)} + rcols.map{|rcol| response.send(rcol)}
70 | end
71 | end
72 | result
73 | end
74 | def complete!
75 | self.completed_at = Time.now
76 | end
77 |
78 | def correct?
79 | responses.all?(&:correct?)
80 | end
81 | def correctness_hash
82 | { :questions => survey.sections_with_questions.map(&:questions).flatten.compact.size,
83 | :responses => responses.compact.size,
84 | :correct => responses.find_all(&:correct?).compact.size
85 | }
86 | end
87 | def mandatory_questions_complete?
88 | progress_hash[:triggered_mandatory] == progress_hash[:triggered_mandatory_completed]
89 | end
90 | def progress_hash
91 | qs = survey.sections_with_questions.map(&:questions).flatten
92 | ds = dependencies(qs.map(&:id))
93 | triggered = qs - ds.select{|d| !d.is_met?(self)}.map(&:question)
94 | { :questions => qs.compact.size,
95 | :triggered => triggered.compact.size,
96 | :triggered_mandatory => triggered.select{|q| q.mandatory?}.compact.size,
97 | :triggered_mandatory_completed => triggered.select{|q| q.mandatory? and is_answered?(q)}.compact.size
98 | }
99 | end
100 | def is_answered?(question)
101 | %w(label image).include?(question.display_type) or !is_unanswered?(question)
102 | end
103 | def is_unanswered?(question)
104 | self.responses.detect{|r| r.question_id == question.id}.nil?
105 | end
106 |
107 | # Returns the number of response groups (count of group responses enterted) for this question group
108 | def count_group_responses(questions)
109 | questions.map{|q| responses.select{|r| (r.question_id.to_i == q.id.to_i) && !r.response_group.nil?}.group_by(&:response_group).size }.max
110 | end
111 |
112 | def unanswered_dependencies
113 | dependencies.select{|d| d.is_met?(self) and self.is_unanswered?(d.question)}.map(&:question)
114 | end
115 |
116 | def all_dependencies(question_ids = nil)
117 | arr = dependencies(question_ids).partition{|d| d.is_met?(self) }
118 | {:show => arr[0].map{|d| d.question_group_id.nil? ? "q_#{d.question_id}" : "qg_#{d.question_group_id}"}, :hide => arr[1].map{|d| d.question_group_id.nil? ? "q_#{d.question_id}" : "qg_#{d.question_group_id}"}}
119 | end
120 |
121 | # Check existence of responses to questions from a given survey_section
122 | def no_responses_for_section?(section)
123 | !responses.any?{|r| r.survey_section_id == section.id}
124 | end
125 |
126 | protected
127 |
128 | def dependencies(question_ids = nil)
129 | Dependency.all(:include => :dependency_conditions, :conditions => {:dependency_conditions => {:question_id => question_ids || responses.map(&:question_id)}})
130 | end
131 | end
132 | end
133 | end
134 |
--------------------------------------------------------------------------------
/lib/surveyor/unparser.rb:
--------------------------------------------------------------------------------
1 | %w(survey survey_section question_group question dependency dependency_condition answer validation validation_condition).each {|model| require model }
2 | module Surveyor
3 | class Unparser
4 | # Class methods
5 | def self.unparse(survey)
6 | survey.unparse(dsl = "")
7 | dsl
8 | end
9 | end
10 | end
11 |
12 | # Surveyor models with extra parsing methods
13 | class Survey < ActiveRecord::Base
14 | # block
15 |
16 | def unparse(dsl)
17 | attrs = (self.attributes.diff Survey.new(:title => title).attributes).delete_if{|k,v| %w(created_at updated_at inactive_at id title access_code).include? k}.symbolize_keys!
18 | dsl << "survey \"#{title}\""
19 | dsl << (attrs.blank? ? " do\n" : ", #{attrs.inspect.gsub(/\{|\}/, "")} do\n")
20 | sections.each{|section| section.unparse(dsl)}
21 | dsl << "end\n"
22 | end
23 | end
24 | class SurveySection < ActiveRecord::Base
25 | # block
26 |
27 | def unparse(dsl)
28 | attrs = (self.attributes.diff SurveySection.new(:title => title).attributes).delete_if{|k,v| %w(created_at updated_at id survey_id).include? k}.symbolize_keys!
29 | group_questions = []
30 | dsl << " section \"#{title}\""
31 | dsl << (attrs.blank? ? " do\n" : ", #{attrs.inspect.gsub(/\{|\}/, "")} do\n")
32 | questions.each_with_index do |question, index|
33 | if question.solo?
34 | question.unparse(dsl)
35 | else # gather up the group questions
36 | group_questions << question
37 | if (index + 1 >= questions.size) or (question.question_group != questions[index + 1].question_group)
38 | # this is the last question of the section, or the group
39 | question.question_group.unparse(dsl)
40 | end
41 | group_questions = []
42 | end
43 | end
44 | dsl << " end\n"
45 | end
46 | end
47 | class QuestionGroup < ActiveRecord::Base
48 | # block
49 |
50 | def unparse(dsl)
51 | attrs = (self.attributes.diff QuestionGroup.new(:text => text).attributes).delete_if{|k,v| %w(created_at updated_at id).include?(k) or (k == "display_type" && %w(grid repeater default).include?(v))}.symbolize_keys!
52 | method = (%w(grid repeater).include?(display_type) ? display_type : "group")
53 | dsl << "\n"
54 | dsl << " #{method} \"#{text}\""
55 | dsl << (attrs.blank? ? " do\n" : ", #{attrs.inspect.gsub(/\{|\}/, "")} do\n")
56 | questions.first.answers.each{|answer| answer.unparse(dsl)} if display_type == "grid"
57 | questions.each{|question| question.unparse(dsl)}
58 | dsl << " end\n"
59 | end
60 | end
61 | class Question < ActiveRecord::Base
62 | # nonblock
63 |
64 | def unparse(dsl)
65 | attrs = (self.attributes.diff Question.new(:text => text).attributes).delete_if{|k,v| %w(created_at updated_at reference_identifier id survey_section_id question_group_id).include?(k) or (k == "display_type" && v == "label")}.symbolize_keys!
66 | dsl << (solo? ? "\n" : " ")
67 | if display_type == "label"
68 | dsl << " label"
69 | else
70 | dsl << " q"
71 | end
72 | dsl << "_#{reference_identifier}" unless reference_identifier.blank?
73 | dsl << " \"#{text}\""
74 | dsl << (attrs.blank? ? "\n" : ", #{attrs.inspect.gsub(/\{|\}/, "")}\n")
75 | if solo? or question_group.display_type != "grid"
76 | answers.each{|answer| answer.unparse(dsl)}
77 | end
78 | dependency.unparse(dsl) if dependency
79 | end
80 | end
81 | class Dependency < ActiveRecord::Base
82 | # nonblock
83 |
84 | def unparse(dsl)
85 | attrs = (self.attributes.diff Dependency.new.attributes).delete_if{|k,v| %w(created_at updated_at id question_id).include?(k) }.symbolize_keys!
86 | dsl << " " if question.part_of_group?
87 | dsl << " dependency"
88 | dsl << (attrs.blank? ? "\n" : " #{attrs.inspect.gsub(/\{|\}/, "")}\n")
89 | dependency_conditions.each{|dependency_condition| dependency_condition.unparse(dsl)}
90 | end
91 | end
92 | class DependencyCondition < ActiveRecord::Base
93 | # nonblock
94 |
95 | def unparse(dsl)
96 | attrs = (self.attributes.diff Dependency.new.attributes).delete_if{|k,v| %w(created_at updated_at question_id question_group_id rule_key rule operator id dependency_id answer_id).include? k}.symbolize_keys!
97 | dsl << " " if dependency.question.part_of_group?
98 | dsl << " condition"
99 | dsl << "_#{rule_key}" unless rule_key.blank?
100 | dsl << " :q_#{question.reference_identifier}, \"#{operator}\""
101 | dsl << (attrs.blank? ? ", {:answer_reference=>\"#{answer && answer.reference_identifier}\"}\n" : ", {#{attrs.inspect.gsub(/\{|\}/, "")}, :answer_reference=>\"#{answer && answer.reference_identifier}\"}\n")
102 | end
103 | end
104 | class Answer < ActiveRecord::Base
105 | # nonblock
106 |
107 | def unparse(dsl)
108 | attrs = (self.attributes.diff Answer.new(:text => text).attributes).delete_if{|k,v| %w(created_at updated_at reference_identifier response_class id question_id).include? k}.symbolize_keys!
109 | attrs.delete(:is_exclusive) if text == "Omit" && is_exclusive == true
110 | attrs.merge!({:is_exclusive => false}) if text == "Omit" && is_exclusive == false
111 | dsl << " " if question.part_of_group?
112 | dsl << " a"
113 | dsl << "_#{reference_identifier}" unless reference_identifier.blank?
114 | if response_class.to_s.titlecase == text && attrs == {:hide_label => true}
115 | dsl << " :#{response_class}"
116 | else
117 | dsl << [ text.blank? ? nil : text == "Other" ? " :other" : text == "Omit" ? " :omit" : " \"#{text}\"",
118 | (response_class.blank? or response_class == "answer") ? nil : " #{response_class.to_sym.inspect}",
119 | attrs.blank? ? nil : " #{attrs.inspect.gsub(/\{|\}/, "")}\n"].compact.join(",")
120 | end
121 | dsl << "\n"
122 | validations.each{|validation| validation.unparse(dsl)}
123 | end
124 | end
125 | class Validation < ActiveRecord::Base
126 | # nonblock
127 |
128 | def unparse(dsl)
129 | attrs = (self.attributes.diff Validation.new.attributes).delete_if{|k,v| %w(created_at updated_at id answer_id).include?(k) }.symbolize_keys!
130 | dsl << " " if answer.question.part_of_group?
131 | dsl << " validation"
132 | dsl << (attrs.blank? ? "\n" : " #{attrs.inspect.gsub(/\{|\}/, "")}\n")
133 | validation_conditions.each{|validation_condition| validation_condition.unparse(dsl)}
134 | end
135 | end
136 | class ValidationCondition < ActiveRecord::Base
137 | # nonblock
138 |
139 | def unparse(dsl)
140 | attrs = (self.attributes.diff ValidationCondition.new.attributes).delete_if{|k,v| %w(created_at updated_at operator rule_key id validation_id).include? k}.symbolize_keys!
141 | dsl << " " if validation.answer.question.part_of_group?
142 | dsl << " condition"
143 | dsl << "_#{rule_key}" unless rule_key.blank?
144 | dsl << " \"#{operator}\""
145 | dsl << (attrs.blank? ? "\n" : ", #{attrs.inspect.gsub(/\{|\}/, "")}\n")
146 | end
147 | end
148 |
--------------------------------------------------------------------------------
/features/surveyor_parser.feature:
--------------------------------------------------------------------------------
1 | Feature: Survey creation
2 | As a
3 | I want to write out the survey in the DSL
4 | So that I can give it to survey participants
5 |
6 | Scenario: Basic questions
7 | Given I parse
8 | """
9 | survey "Simple survey" do
10 | section "Basic questions" do
11 | label "These questions are examples of the basic supported input types"
12 |
13 | question_1 "What is your favorite color?", :pick => :one
14 | answer "red"
15 | answer "blue"
16 | answer "green"
17 | answer :other
18 |
19 | q_2b "Choose the colors you don't like", :pick => :any
20 | a_1 "orange"
21 | a_2 "purple"
22 | a_3 "brown"
23 | a :omit
24 | end
25 | end
26 | """
27 | Then there should be 1 survey with:
28 | | title |
29 | | Simple survey |
30 | And there should be 3 questions with:
31 | | reference_identifier | text | pick | display_type |
32 | | nil | These questions are examples of the basic supported input types | none | label |
33 | | 1 | What is your favorite color? | one | default |
34 | | 2b | Choose the colors you don't like | any | default |
35 | And there should be 8 answers with:
36 | | reference_identifier | text | response_class |
37 | | nil | red | answer |
38 | | nil | blue | answer |
39 | | nil | green | answer |
40 | | nil | Other | answer |
41 | | 1 | orange | answer |
42 | | 2 | purple | answer |
43 | | 3 | brown | answer |
44 | | nil | Omit | answer |
45 |
46 | Scenario: More complex questions
47 | Given I parse
48 | """
49 | survey "Complex survey" do
50 | section "Complicated questions" do
51 | grid "Tell us how you feel today" do
52 | a "-2"
53 | a "-1"
54 | a "0"
55 | a "1"
56 | a "2"
57 | q "down|up" , :pick => :one
58 | q "sad|happy", :pick => :one
59 | q "limp|perky", :pick => :one
60 | end
61 |
62 | q "Choose your favorite utensils and enter frequency of use (daily, weekly, monthly, etc...)", :pick => :any
63 | a "spoon", :string
64 | a "fork", :string
65 | a "knife", :string
66 | a :other, :string
67 |
68 | repeater "Tell us about the cars you own" do
69 | q "Make", :pick => :one, :display_type => :dropdown
70 | a "Toyota"
71 | a "Ford"
72 | a "GMChevy"
73 | a "Ferrari"
74 | a "Tesla"
75 | a "Honda"
76 | a "Other weak brand"
77 | q "Model"
78 | a :string
79 | q "Year"
80 | a :string
81 | end
82 | end
83 | end
84 | """
85 | Then there should be 1 survey with:
86 | | title |
87 | | Complex survey |
88 | And there should be 2 question groups with:
89 | | text | display_type |
90 | | Tell us how you feel today | grid |
91 | | Tell us about the cars you own | repeater |
92 | And there should be 7 questions with:
93 | | text | pick | display_type |
94 | | Make | one | dropdown |
95 | And there should be 28 answers with:
96 | | text | response_class |
97 | | -2 | answer |
98 | | Other | string |
99 |
100 | Scenario: Dependencies and validations
101 | Given I parse
102 | """
103 | survey "Dependency and validation survey" do
104 | section "Conditionals" do
105 | q_montypython3 "What... is your name? (e.g. It is 'Arthur', King of the Britons)"
106 | a_1 :string
107 |
108 | q_montypython4 "What... is your quest? (e.g. To seek the Holy Grail)"
109 | a_1 :string
110 | dependency :rule => "A"
111 | condition_A :q_montypython3, "==", {:string_value => "It is 'Arthur', King of the Britons", :answer_reference => "1"}
112 |
113 | q "How many pets do you own?"
114 | a :integer
115 | validation :rule => "A"
116 | condition_A ">=", :integer_value => 0
117 |
118 | q "What is your address?", :custom_class => 'address'
119 | a :text, :custom_class => 'mapper'
120 | validation :rule => "AC"
121 | vcondition_AC "=~", :regexp => /[0-9a-zA-z\. #]/
122 |
123 | q_2 "Which colors do you loathe?", :pick => :any
124 | a_1 "red"
125 | a_2 "blue"
126 | a_3 "green"
127 | a_4 "yellow"
128 |
129 | q_2a "Please explain why you hate so many colors?"
130 | a_1 "explanation", :text
131 | dependency :rule => "Z"
132 | condition_Z :q_2, "count>2"
133 | end
134 | end
135 | """
136 | Then there should be 1 survey with:
137 | | title |
138 | | Dependency and validation survey |
139 | And there should be 6 questions with:
140 | | text | pick | display_type | custom_class |
141 | | What... is your name? (e.g. It is 'Arthur', King of the Britons) | none | default | nil |
142 | | What is your address? | none | default | address |
143 | And there should be 2 dependency with:
144 | | rule |
145 | | A |
146 | | Z |
147 | And there should be 2 resolved dependency_condition with:
148 | | rule_key |
149 | | A |
150 | | Z |
151 | And there should be 2 validations with:
152 | | rule |
153 | | A |
154 | | AC |
155 | And there should be 2 validation_conditions with:
156 | | rule_key | integer_value |
157 | | A | 0 |
158 |
159 | Scenario: Dependencies and validations
160 | Given I parse
161 | """
162 | survey "dependency test" do
163 | section "section 1" do
164 |
165 | q_copd_sh_1 "Have you ever smoked cigarettes?",:pick=>:one,:help_text=>"NO means less than 20 packs of cigarettes or 12 oz. of tobacco in a lifetime or less than 1 cigarette a day for 1 year."
166 | a_1 "Yes"
167 | a_2 "No"
168 |
169 | q_copd_sh_1a "How old were you when you first started smoking cigarettes?", :help_text=>"age in years"
170 | a :integer
171 | dependency :rule => "A"
172 | condition_A :q_copd_sh_1, "==", :a_1
173 |
174 | q_copd_sh_1b "Do you currently smoke cigarettes?",:pick=>:one, :help_text=>"as of 1 month ago"
175 | a_1 "Yes"
176 | a_2 "No"
177 | dependency :rule => "B"
178 | condition_B :q_copd_sh_1, "==", :a_1
179 |
180 | q_copd_sh_1c "On the average of the entire time you smoked, how many cigarettes did you smoke per day?"
181 | a :integer
182 | dependency :rule => "C"
183 | condition_C :q_copd_sh_1, "==", :a_1
184 |
185 | q_copd_sh_1bb "How many cigarettes do you smoke per day now?"
186 | a_2 "integer"
187 | dependency :rule => "D"
188 | condition_D :q_copd_sh_1b, "==", :a_1
189 |
190 |
191 | q_copd_sh_1ba "How old were you when you stopped?"
192 | a "Years", :integer
193 | dependency :rule => "E"
194 | condition_E :q_copd_sh_1b, "==", :a_2
195 |
196 | end
197 | end
198 | """
199 | Then there should be 5 dependencies
200 | And question "copd_sh_1a" should have a dependency with rule "A"
201 | And question "copd_sh_1ba" should have a dependency with rule "E"
--------------------------------------------------------------------------------
/surveyor.gemspec:
--------------------------------------------------------------------------------
1 | # Generated by jeweler
2 | # DO NOT EDIT THIS FILE DIRECTLY
3 | # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4 | # -*- encoding: utf-8 -*-
5 |
6 | Gem::Specification.new do |s|
7 | s.name = %q{surveyor}
8 | s.version = "0.19.3"
9 |
10 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11 | s.authors = ["Brian Chamberlain", "Mark Yoon"]
12 | s.date = %q{2011-03-18}
13 | s.email = %q{yoon@northwestern.edu}
14 | s.extra_rdoc_files = [
15 | "README.md"
16 | ]
17 | s.files = [
18 | ".gitignore",
19 | ".rvmrc",
20 | "CHANGELOG",
21 | "MIT-LICENSE",
22 | "README.md",
23 | "Rakefile",
24 | "VERSION",
25 | "app/controllers/results_controller.rb",
26 | "app/controllers/surveyor_controller.rb",
27 | "app/helpers/results_helper.rb",
28 | "app/helpers/surveyor_helper.rb",
29 | "app/models/answer.rb",
30 | "app/models/dependency.rb",
31 | "app/models/dependency_condition.rb",
32 | "app/models/question.rb",
33 | "app/models/question_group.rb",
34 | "app/models/response.rb",
35 | "app/models/response_set.rb",
36 | "app/models/survey.rb",
37 | "app/models/survey_section.rb",
38 | "app/models/survey_section_sweeper.rb",
39 | "app/models/validation.rb",
40 | "app/models/validation_condition.rb",
41 | "app/views/layouts/results.html.erb",
42 | "app/views/layouts/surveyor_default.html.erb",
43 | "app/views/partials/_answer.html.haml",
44 | "app/views/partials/_dependents.html.haml",
45 | "app/views/partials/_question.html.haml",
46 | "app/views/partials/_question_group.html.haml",
47 | "app/views/partials/_section.html.haml",
48 | "app/views/partials/_section_menu.html.haml",
49 | "app/views/results/index.html.erb",
50 | "app/views/results/show.html.erb",
51 | "app/views/surveyor/edit.html.haml",
52 | "app/views/surveyor/new.html.haml",
53 | "app/views/surveyor/show.html.haml",
54 | "ci-env.sh",
55 | "config/routes.rb",
56 | "features/redcap_parser.feature",
57 | "features/step_definitions/parser_steps.rb",
58 | "features/step_definitions/surveyor_steps.rb",
59 | "features/step_definitions/web_steps.rb",
60 | "features/support/REDCapDemoDatabase_DataDictionary.csv",
61 | "features/support/env.rb",
62 | "features/support/paths.rb",
63 | "features/surveyor.feature",
64 | "features/surveyor_parser.feature",
65 | "generators/extend_surveyor/extend_surveyor_generator.rb",
66 | "generators/extend_surveyor/templates/EXTENDING_SURVEYOR",
67 | "generators/extend_surveyor/templates/extensions/surveyor_controller.rb",
68 | "generators/extend_surveyor/templates/extensions/surveyor_custom.html.erb",
69 | "generators/surveyor/surveyor_generator.rb",
70 | "generators/surveyor/templates/README",
71 | "generators/surveyor/templates/assets/images/next.gif",
72 | "generators/surveyor/templates/assets/images/prev.gif",
73 | "generators/surveyor/templates/assets/javascripts/jquery.surveyor.js",
74 | "generators/surveyor/templates/assets/javascripts/jquery.tools.min.js",
75 | "generators/surveyor/templates/assets/stylesheets/dateinput.css",
76 | "generators/surveyor/templates/assets/stylesheets/reset.css",
77 | "generators/surveyor/templates/assets/stylesheets/results.css",
78 | "generators/surveyor/templates/assets/stylesheets/sass/surveyor.sass",
79 | "generators/surveyor/templates/locales/surveyor_en.yml",
80 | "generators/surveyor/templates/locales/surveyor_es.yml",
81 | "generators/surveyor/templates/locales/surveyor_he.yml",
82 | "generators/surveyor/templates/migrate/add_correct_answer_id_to_questions.rb",
83 | "generators/surveyor/templates/migrate/add_default_value_to_answers.rb",
84 | "generators/surveyor/templates/migrate/add_display_order_to_surveys.rb",
85 | "generators/surveyor/templates/migrate/add_index_to_response_sets.rb",
86 | "generators/surveyor/templates/migrate/add_index_to_surveys.rb",
87 | "generators/surveyor/templates/migrate/add_section_id_to_responses.rb",
88 | "generators/surveyor/templates/migrate/add_unique_indicies.rb",
89 | "generators/surveyor/templates/migrate/create_answers.rb",
90 | "generators/surveyor/templates/migrate/create_dependencies.rb",
91 | "generators/surveyor/templates/migrate/create_dependency_conditions.rb",
92 | "generators/surveyor/templates/migrate/create_question_groups.rb",
93 | "generators/surveyor/templates/migrate/create_questions.rb",
94 | "generators/surveyor/templates/migrate/create_response_sets.rb",
95 | "generators/surveyor/templates/migrate/create_responses.rb",
96 | "generators/surveyor/templates/migrate/create_survey_sections.rb",
97 | "generators/surveyor/templates/migrate/create_surveys.rb",
98 | "generators/surveyor/templates/migrate/create_validation_conditions.rb",
99 | "generators/surveyor/templates/migrate/create_validations.rb",
100 | "generators/surveyor/templates/surveys/kitchen_sink_survey.rb",
101 | "generators/surveyor/templates/surveys/quiz.rb",
102 | "generators/surveyor/templates/tasks/surveyor.rb",
103 | "hudson.rakefile",
104 | "init_testbed.rakefile",
105 | "lib/formtastic/surveyor_builder.rb",
106 | "lib/surveyor.rb",
107 | "lib/surveyor/acts_as_response.rb",
108 | "lib/surveyor/common.rb",
109 | "lib/surveyor/models/answer_methods.rb",
110 | "lib/surveyor/models/dependency_condition_methods.rb",
111 | "lib/surveyor/models/dependency_methods.rb",
112 | "lib/surveyor/models/question_group_methods.rb",
113 | "lib/surveyor/models/question_methods.rb",
114 | "lib/surveyor/models/response_methods.rb",
115 | "lib/surveyor/models/response_set_methods.rb",
116 | "lib/surveyor/models/survey_methods.rb",
117 | "lib/surveyor/models/survey_section_methods.rb",
118 | "lib/surveyor/models/validation_condition_methods.rb",
119 | "lib/surveyor/models/validation_methods.rb",
120 | "lib/surveyor/parser.rb",
121 | "lib/surveyor/redcap_parser.rb",
122 | "lib/surveyor/surveyor_controller_methods.rb",
123 | "lib/surveyor/unparser.rb",
124 | "lib/tasks/surveyor_tasks.rake",
125 | "rails/init.rb",
126 | "spec/controllers/surveyor_controller_spec.rb",
127 | "spec/factories.rb",
128 | "spec/helpers/surveyor_helper_spec.rb",
129 | "spec/lib/common_spec.rb",
130 | "spec/lib/parser_spec.rb",
131 | "spec/lib/redcap_parser_spec.rb",
132 | "spec/lib/unparser_spec.rb",
133 | "spec/models/answer_spec.rb",
134 | "spec/models/dependency_condition_spec.rb",
135 | "spec/models/dependency_spec.rb",
136 | "spec/models/question_group_spec.rb",
137 | "spec/models/question_spec.rb",
138 | "spec/models/response_set_spec.rb",
139 | "spec/models/response_spec.rb",
140 | "spec/models/survey_section_spec.rb",
141 | "spec/models/survey_spec.rb",
142 | "spec/models/validation_condition_spec.rb",
143 | "spec/models/validation_spec.rb",
144 | "spec/rcov.opts",
145 | "spec/spec.opts",
146 | "spec/spec_helper.rb",
147 | "surveyor.gemspec",
148 | "testbed/Gemfile"
149 | ]
150 | s.homepage = %q{http://github.com/breakpointer/surveyor}
151 | s.rdoc_options = ["--charset=UTF-8"]
152 | s.require_paths = ["lib"]
153 | s.rubygems_version = %q{1.3.7}
154 | s.summary = %q{A rails (gem) plugin to enable surveys in your application}
155 | s.test_files = [
156 | "spec/controllers/surveyor_controller_spec.rb",
157 | "spec/factories.rb",
158 | "spec/helpers/surveyor_helper_spec.rb",
159 | "spec/lib/common_spec.rb",
160 | "spec/lib/parser_spec.rb",
161 | "spec/lib/redcap_parser_spec.rb",
162 | "spec/lib/unparser_spec.rb",
163 | "spec/models/answer_spec.rb",
164 | "spec/models/dependency_condition_spec.rb",
165 | "spec/models/dependency_spec.rb",
166 | "spec/models/question_group_spec.rb",
167 | "spec/models/question_spec.rb",
168 | "spec/models/response_set_spec.rb",
169 | "spec/models/response_spec.rb",
170 | "spec/models/survey_section_spec.rb",
171 | "spec/models/survey_spec.rb",
172 | "spec/models/validation_condition_spec.rb",
173 | "spec/models/validation_spec.rb",
174 | "spec/spec_helper.rb"
175 | ]
176 |
177 | if s.respond_to? :specification_version then
178 | current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
179 | s.specification_version = 3
180 |
181 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
182 | s.add_runtime_dependency(%q, [">= 0"])
183 | s.add_runtime_dependency(%q, [">= 0"])
184 | s.add_runtime_dependency(%q, [">= 0"])
185 | s.add_development_dependency(%q, [">= 0"])
186 | else
187 | s.add_dependency(%q, [">= 0"])
188 | s.add_dependency(%q, [">= 0"])
189 | s.add_dependency(%q, [">= 0"])
190 | s.add_dependency(%q, [">= 0"])
191 | end
192 | else
193 | s.add_dependency(%q, [">= 0"])
194 | s.add_dependency(%q, [">= 0"])
195 | s.add_dependency(%q, [">= 0"])
196 | s.add_dependency(%q, [">= 0"])
197 | end
198 | end
199 |
200 |
--------------------------------------------------------------------------------
/features/step_definitions/web_steps.rb:
--------------------------------------------------------------------------------
1 | # IMPORTANT: This file is generated by cucumber-rails - edit at your own peril.
2 | # It is recommended to regenerate this file in the future when you upgrade to a
3 | # newer version of cucumber-rails. Consider adding your own code to a new file
4 | # instead of editing this one. Cucumber will automatically load all features/**/*.rb
5 | # files.
6 |
7 |
8 | require 'uri'
9 | require 'cgi'
10 | require File.expand_path(File.join(File.dirname(__FILE__), "..", "support", "paths"))
11 |
12 | # Commonly used webrat steps
13 | # http://github.com/brynary/webrat
14 |
15 | Given /^(?:|I )am on (.+)$/ do |page_name|
16 | visit path_to(page_name)
17 | end
18 |
19 | When /^(?:|I )go to (.+)$/ do |page_name|
20 | visit path_to(page_name)
21 | end
22 |
23 | When /^(?:|I )press "([^"]*)"$/ do |button|
24 | click_button(button)
25 | end
26 |
27 | When /^(?:|I )follow "([^"]*)"$/ do |link|
28 | click_link(link)
29 | end
30 |
31 | When /^(?:|I )follow "([^"]*)" within "([^"]*)"$/ do |link, parent|
32 | click_link_within(parent, link)
33 | end
34 |
35 | When /^(?:|I )fill in "([^"]*)" with "([^"]*)"$/ do |field, value|
36 | fill_in(field, :with => value)
37 | end
38 |
39 | When /^(?:|I )fill in "([^"]*)" for "([^"]*)"$/ do |value, field|
40 | fill_in(field, :with => value)
41 | end
42 |
43 | # Use this to fill in an entire form with data from a table. Example:
44 | #
45 | # When I fill in the following:
46 | # | Account Number | 5002 |
47 | # | Expiry date | 2009-11-01 |
48 | # | Note | Nice guy |
49 | # | Wants Email? | |
50 | #
51 | # TODO: Add support for checkbox, select og option
52 | # based on naming conventions.
53 | #
54 | When /^(?:|I )fill in the following:$/ do |fields|
55 | fields.rows_hash.each do |name, value|
56 | When %{I fill in "#{name}" with "#{value}"}
57 | end
58 | end
59 |
60 | When /^(?:|I )select "([^"]*)" from "([^"]*)"$/ do |value, field|
61 | select(value, :from => field)
62 | end
63 |
64 | # Use this step in conjunction with Rail's datetime_select helper. For example:
65 | # When I select "December 25, 2008 10:00" as the date and time
66 | When /^(?:|I )select "([^"]*)" as the date and time$/ do |time|
67 | select_datetime(time)
68 | end
69 |
70 | # Use this step when using multiple datetime_select helpers on a page or
71 | # you want to specify which datetime to select. Given the following view:
72 | # <%= f.label :preferred %>
73 | # <%= f.datetime_select :preferred %>
74 | # <%= f.label :alternative %>
75 | # <%= f.datetime_select :alternative %>
76 | # The following steps would fill out the form:
77 | # When I select "November 23, 2004 11:20" as the "Preferred" date and time
78 | # And I select "November 25, 2004 10:30" as the "Alternative" date and time
79 | When /^(?:|I )select "([^"]*)" as the "([^"]*)" date and time$/ do |datetime, datetime_label|
80 | select_datetime(datetime, :from => datetime_label)
81 | end
82 |
83 | # Use this step in conjunction with Rail's time_select helper. For example:
84 | # When I select "2:20PM" as the time
85 | # Note: Rail's default time helper provides 24-hour time-- not 12 hour time. Webrat
86 | # will convert the 2:20PM to 14:20 and then select it.
87 | When /^(?:|I )select "([^"]*)" as the time$/ do |time|
88 | select_time(time)
89 | end
90 |
91 | # Use this step when using multiple time_select helpers on a page or you want to
92 | # specify the name of the time on the form. For example:
93 | # When I select "7:30AM" as the "Gym" time
94 | When /^(?:|I )select "([^"]*)" as the "([^"]*)" time$/ do |time, time_label|
95 | select_time(time, :from => time_label)
96 | end
97 |
98 | # Use this step in conjunction with Rail's date_select helper. For example:
99 | # When I select "February 20, 1981" as the date
100 | When /^(?:|I )select "([^"]*)" as the date$/ do |date|
101 | select_date(date)
102 | end
103 |
104 | # Use this step when using multiple date_select helpers on one page or
105 | # you want to specify the name of the date on the form. For example:
106 | # When I select "April 26, 1982" as the "Date of Birth" date
107 | When /^(?:|I )select "([^"]*)" as the "([^"]*)" date$/ do |date, date_label|
108 | select_date(date, :from => date_label)
109 | end
110 |
111 | When /^(?:|I )check "([^"]*)"$/ do |field|
112 | check(field)
113 | end
114 |
115 | When /^(?:|I )uncheck "([^"]*)"$/ do |field|
116 | uncheck(field)
117 | end
118 |
119 | When /^(?:|I )choose "([^"]*)"$/ do |field|
120 | choose(field)
121 | end
122 |
123 | # Adds support for validates_attachment_content_type. Without the mime-type getting
124 | # passed to attach_file() you will get a "Photo file is not one of the allowed file types."
125 | # error message
126 | When /^(?:|I )attach the file "([^"]*)" to "([^"]*)"$/ do |path, field|
127 | type = path.split(".")[1]
128 |
129 | case type
130 | when "jpg"
131 | type = "image/jpg"
132 | when "jpeg"
133 | type = "image/jpeg"
134 | when "png"
135 | type = "image/png"
136 | when "gif"
137 | type = "image/gif"
138 | end
139 |
140 | attach_file(field, path, type)
141 | end
142 |
143 | Then /^(?:|I )should see "([^"]*)"$/ do |text|
144 | if response.respond_to? :should
145 | response.should contain(text)
146 | else
147 | assert_contain text
148 | end
149 | end
150 |
151 | Then /^(?:|I )should see "([^"]*)" within "([^"]*)"$/ do |text, selector|
152 | within(selector) do |content|
153 | if content.respond_to? :should
154 | content.should contain(text)
155 | else
156 | hc = Webrat::Matchers::HasContent.new(text)
157 | assert hc.matches?(content), hc.failure_message
158 | end
159 | end
160 | end
161 |
162 | Then /^(?:|I )should see \/([^\/]*)\/$/ do |regexp|
163 | regexp = Regexp.new(regexp)
164 | if response.respond_to? :should
165 | response.should contain(regexp)
166 | else
167 | assert_match(regexp, response_body)
168 | end
169 | end
170 |
171 | Then /^(?:|I )should see \/([^\/]*)\/ within "([^"]*)"$/ do |regexp, selector|
172 | within(selector) do |content|
173 | regexp = Regexp.new(regexp)
174 | if content.respond_to? :should
175 | content.should contain(regexp)
176 | else
177 | assert_match(regexp, content)
178 | end
179 | end
180 | end
181 |
182 | Then /^(?:|I )should not see "([^"]*)"$/ do |text|
183 | if response.respond_to? :should_not
184 | response.should_not contain(text)
185 | else
186 | assert_not_contain(text)
187 | end
188 | end
189 |
190 | Then /^(?:|I )should not see "([^"]*)" within "([^"]*)"$/ do |text, selector|
191 | within(selector) do |content|
192 | if content.respond_to? :should_not
193 | content.should_not contain(text)
194 | else
195 | hc = Webrat::Matchers::HasContent.new(text)
196 | assert !hc.matches?(content), hc.negative_failure_message
197 | end
198 | end
199 | end
200 |
201 | Then /^(?:|I )should not see \/([^\/]*)\/$/ do |regexp|
202 | regexp = Regexp.new(regexp)
203 | if response.respond_to? :should_not
204 | response.should_not contain(regexp)
205 | else
206 | assert_not_contain(regexp)
207 | end
208 | end
209 |
210 | Then /^(?:|I )should not see \/([^\/]*)\/ within "([^"]*)"$/ do |regexp, selector|
211 | within(selector) do |content|
212 | regexp = Regexp.new(regexp)
213 | if content.respond_to? :should_not
214 | content.should_not contain(regexp)
215 | else
216 | assert_no_match(regexp, content)
217 | end
218 | end
219 | end
220 |
221 | Then /^the "([^"]*)" field should contain "([^"]*)"$/ do |field, value|
222 | field_value = field_labeled(field).value
223 | if field_value.respond_to? :should
224 | field_value.should =~ /#{value}/
225 | else
226 | assert_match(/#{value}/, field_value)
227 | end
228 | end
229 |
230 | Then /^the "([^"]*)" field should not contain "([^"]*)"$/ do |field, value|
231 | field_value = field_labeled(field).value
232 | if field_value.respond_to? :should_not
233 | field_value.should_not =~ /#{value}/
234 | else
235 | assert_no_match(/#{value}/, field_value)
236 | end
237 | end
238 |
239 | Then /^the "([^"]*)" checkbox should be checked$/ do |label|
240 | field = field_labeled(label)
241 | if field.respond_to? :should
242 | field.should be_checked
243 | else
244 | assert field.checked?
245 | end
246 | end
247 |
248 | Then /^the "([^"]*)" checkbox should not be checked$/ do |label|
249 | field = field_labeled(label)
250 | if field.respond_to? :should_not
251 | field.should_not be_checked
252 | else
253 | assert !field.checked?
254 | end
255 | end
256 |
257 | Then /^(?:|I )should be on (.+)$/ do |page_name|
258 | current_path = URI.parse(current_url).path
259 | if current_path.respond_to? :should
260 | current_path.should == path_to(page_name)
261 | else
262 | assert_equal path_to(page_name), current_path
263 | end
264 | end
265 |
266 | Then /^(?:|I )should have the following query string:$/ do |expected_pairs|
267 | query = URI.parse(current_url).query
268 | actual_params = query ? CGI.parse(query) : {}
269 | expected_params = {}
270 | expected_pairs.rows_hash.each_pair{|k,v| expected_params[k] = v.split(',')}
271 |
272 | if actual_params.respond_to? :should
273 | actual_params.should == expected_params
274 | else
275 | assert_equal expected_params, actual_params
276 | end
277 | end
278 |
279 | Then /^show me the page$/ do
280 | save_and_open_page
281 | end
282 |
--------------------------------------------------------------------------------