├── 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 | 6 | 7 | 8 | 9 | 10 | <% @surveys.each do |survey| -%> 11 | 12 | 13 | 14 | 15 | 16 | <% end %> 17 |
IDNameOperation
<%=h survey.id %><%=h survey.title %><%= link_to "show results list(#{survey.response_sets.count})", result_path(survey.access_code) %>
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 | 4 | 5 | <% @questions.each do |question| %> 6 | <% next if question.display_order == 1 %> 7 | 8 | <% end %> 9 | 10 | 11 | <% @response_sets.each do |r_set| %> 12 | 13 | 14 | 15 | <% @questions.each do |question| %> 16 | <% next if question.display_order == 1 %> 17 | 18 | <% end %> 19 | 20 | <% end %> 21 |
IDCode<%= "[" +question.display_order.to_s + "]" + question.text %>
<%=h r_set.id %><%=h r_set.access_code %><%= display_response(r_set,question) %>
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 == 'Something' 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 | --------------------------------------------------------------------------------